github.com/anycable/anycable-go@v1.5.1/docs/ocpp.md (about) 1 # OCPP support (_alpha_) 2 3 <p class="pro-badge-header"></p> 4 5 [OCPP][] (Open Charge Point Protocol) is a communication protocol for electric vehicle charging stations. It defines a WebSocket-based RPC communication protocol to manage station and receive status updates. 6 7 AnyCable-Go Pro supports OCPP and allows you to _connect_ your charging stations to Ruby or Rails applications and control everything using Action Cable at the backend. 8 9 **NOTE:** Currently, AnyCable-Go Pro supports OCPP v1.6 only. Please, contact us if you need support for other versions. 10 11 ## How it works 12 13 - EV charging station connects to AnyCable-Go via WebSocket 14 - The station sends a `BootNotification` request to initialize the connection 15 - AnyCable transforms this request into several AnyCable RPC calls to match the Action Cable interface: 16 1) `Authenticate -> Connection#connect` to authenticate the station. 17 2) `Command{subscribe} -> OCCPChannel#subscribed` to initialize a channel entity to association with this station. 18 3) `Command{perform} -> OCCPChannel#boot_notification` to handle the `BootNotification` request. 19 - Subsequent requests from the station are converted into `OCCPChannel` action calls (e.g., `Authorize -> OCCPChannel#authorize`, `StartTransaction -> OCCPChannel#start_transaction`). 20 21 AnyCable also takes care of heartbeats and acknowledgment messages (unless you send them manually, see below). 22 23 ## Usage 24 25 To enable OCPP support, you need to specify the `--ocpp_path` flag (or `ANYCABLE_OCPP_PATH` environment variable) specify the prefix for OCPP connections: 26 27 ```sh 28 $ anycable-go --ocpp_path=/ocpp 29 30 ... 31 INFO 2023-03-28T19:06:58.725Z context=main Handle OCPP v1.6 WebSocket connections at http://localhost:8080/ocpp/{station_id} 32 ... 33 ``` 34 35 AnyCable automatically adds the `/:station_id` part to the path. You can use it to identify the station in your application. 36 37 ## Example Action Cable channel class 38 39 Now, to manage EV connections at the Ruby side, you need to create a channel class. Here is an example: 40 41 ```ruby 42 class OCPPChannel < ApplicationCable::Channel 43 def subscribed 44 # You can subscribe the station to its personal stream to 45 # send remote comamnds to it 46 # params["sn"] contains the station's serial number 47 # (meterSerialNumber from the BootNotification request) 48 stream_for "ev/#{params["sn"]}" 49 end 50 51 def boot_notification(data) 52 # Data contains the following fields: 53 # - id - a unique message ID 54 # - command - an original command name 55 # - payload - a hash with the original request data 56 id, payload = data.values_at("id", "payload") 57 58 logger.info "BootNotification: #{payload}" 59 60 # By default, if not ack sent, AnyCable sends the following: 61 # [3, <id>, {"status": "Accepted"}] 62 # 63 # For boot notification response, the "interval" is also added. 64 end 65 66 def status_notification(data) 67 id, payload = data.values_at("id", "payload") 68 69 logger.info "Status Notification: #{payload}" 70 end 71 72 def authorize(data) 73 id, payload = data.values_at("id", "payload") 74 75 logger.info "Authorize: idTag — #{payload["idTag"]}" 76 77 # For some actions, you may want to send a custom response. 78 transmit_ack(id:, idTagInfo: {status: "Accepted"}) 79 end 80 81 def start_transaction(data) 82 id, payload = data.values_at("id", "payload") 83 84 id_tag, connector_id = payload.values_at("idTag", "connectorId") 85 86 logger.info "StartTransaction: idTag — #{id_tag}, connectorId — #{connector_id}" 87 88 transmit_ack(id:, transactionId: rand(1000), idTagInfo: {status: "Accepted"}) 89 end 90 91 def stop_transaction(data) 92 id, payload = data.values_at("id", "payload") 93 94 id_tag, connector_id, transaction_id = payload.values_at("idTag", "connectorId", "transactionId") 95 96 logger.info "StopTransaction: transcationId - #{transaction_id}, idTag — #{id_tag}" 97 98 transmit_ack(id:, idTagInfo: {status: "Accepted"}) 99 end 100 101 # These are special methods to handle OCPP errors and acks 102 def error(data) 103 id, code, message, details = data.values_at("id", "code", "message", "payload") 104 logger.error "Error from EV: #{code} — #{message} (#{details})" 105 end 106 107 def ack(data) 108 logger.info "ACK from EV: #{data["id"]} — #{data.dig("payload", "status")}" 109 end 110 111 private 112 113 def transmit_ack(id:, **payload) 114 # IMPORTANT: You must use "Ack" as the command for acks, 115 # so AnyCable can correctly translate them into OCPP acks. 116 transmit({command: :Ack, id:, payload:}) 117 end 118 end 119 ``` 120 121 ### Single-action variant 122 123 It's possible to handle all OCCP commands with a single `#receive` method at the channel class. For that, you must configure `anycable-go` to not use granular actions for OCPP: 124 125 ```sh 126 anycable-go --ocpp_granular_actions=false 127 128 # or 129 130 ANYCABLE_OCPP_GRANULAR_ACTIONS=false anycable-go 131 ``` 132 133 In your channel class: 134 135 ```ruby 136 137 class OCPPChannel < ApplicationCable::Channel 138 def subscribed 139 stream_for "ev/#{params["sn"]}" 140 end 141 142 def receive(data) 143 id, command, payload = data.values_at("id", "command", "payload") 144 145 logger.info "[#{id}] #{command}: #{payload}" 146 end 147 end 148 ``` 149 150 ### Remote commands 151 152 You can send remote commands to stations via Action Cable broadcasts: 153 154 ```ruby 155 OCCPChannel.broadcast_to( 156 "ev/#{serial_number}", 157 { 158 command: "TriggerMessage", 159 id: "<uniq_id>", 160 payload: { 161 requestedMessage: "BootNotification" 162 } 163 } 164 ) 165 ``` 166 167 [OCPP]: https://en.wikipedia.org/wiki/Open_Charge_Point_Protocol