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