ocpp-rails 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42e87a1079adc1b8d7641883fa8e57b21d07ff38a5b150dd231a64d637d37a47
4
- data.tar.gz: f4fd46249622b68f30f035c99da715913219940eabb2dabdefc8e15255840a84
3
+ metadata.gz: ec3b88d04d4b69372c6ca9dadf03a43053e54788a79eef1374a2dbec1992ef45
4
+ data.tar.gz: e4f9453a7e00727790f1af2b2998a8f177e93d75036c9870e96f7b6dee0ee90e
5
5
  SHA512:
6
- metadata.gz: b103ecc7ed7e89acdd7aad80e93314cffe57f8c3fd84664a3cbd8da00e10a60f0a7e2b8c03870b27f58ee2fbf93e89b09f020e88db274a776a79d32b6a5f6f4f
7
- data.tar.gz: 3880fb4073c68ece89623df6740b529684c7a71f284dab18b4080528418ba31d339cf00565de3039d867ec0e6d757322e26b4970ba73d27425dfc433e93cdb8d
6
+ metadata.gz: fb468229c8aad08762333d4b55693bb6d1fdd3934fb9b36777380dd4d923499a76d28d8dd4a02ae089891e54ccaef0d94e5354be1e2cd4a37ed8d91a830a1405
7
+ data.tar.gz: '0079e806237f2e081ef026ca594ba9d4a9f49793aa065be8cb4d1a5b288357b48853b60f084bd24096d6ee400e8060d57d7187916a49859c5d015f7cd3ebd3af'
data/README.md CHANGED
@@ -138,19 +138,41 @@ Ocpp::Rails.setup do |config|
138
138
  end
139
139
  ```
140
140
 
141
- **Charge points connect to:**
142
- ```
143
- ws://your-server:3000/ocpp/cable
141
+ #### Transports
142
+
143
+ `config.transport` selects how charge points reach the engine:
144
+
145
+ - **`:raw`** — native **bare OCPP-J** over a plain WebSocket, which is what
146
+ real/commercial charge points speak. Stations connect to:
147
+ ```
148
+ ws://your-server:3000/ocpp/<charge-point-identifier>
149
+ ```
150
+ The identity is the last path segment; frames are the bare OCPP arrays
151
+ (`[2,"id","Action",{…}]`) with subprotocol `ocpp1.6`.
152
+ - **`:action_cable`** (default) — OCPP-J wrapped in the ActionCable JSON protocol.
153
+ Stations connect to `ws://your-server:3000/ocpp/cable` and must speak the
154
+ ActionCable subscribe/`message` handshake (a generic OCPP station cannot; this
155
+ is aimed at ActionCable-aware clients/simulators). Kept as the default for
156
+ backwards compatibility.
157
+ - **`:both`** — mount both endpoints side by side (useful while migrating).
158
+
159
+ ```ruby
160
+ Ocpp::Rails.setup do |config|
161
+ config.transport = :raw # accept real OCPP-J wallboxes directly
162
+ end
144
163
  ```
145
164
 
146
- Stations authenticate with HTTP Basic Auth on the WebSocket upgrade
147
- (OCPP-J Security Profile 1, enabled by default). Provision a per-station
148
- credential first:
165
+ Both transports authenticate identically: HTTP Basic Auth on the WebSocket
166
+ upgrade (OCPP-J Security Profile 1, enabled by default; the Basic username must
167
+ equal the station identity). Provision a per-station credential first:
149
168
 
150
169
  ```ruby
151
170
  charge_point.update!(auth_password: SecureRandom.base58(32))
152
171
  ```
153
172
 
173
+ For internet-facing deployments use TLS (`wss://`, Security Profile 2),
174
+ terminated by a reverse proxy in front of the app.
175
+
154
176
  For detailed setup instructions, see the [Getting Started Guide](docs/getting-started.md)
155
177
  and the [Security Guide](docs/security.md).
156
178
 
@@ -0,0 +1,181 @@
1
+ module Ocpp
2
+ module Rails
3
+ module RawSocket
4
+ # One live raw OCPP-J station connection. It wires a websocket driver to the
5
+ # same transport-agnostic core the ActionCable channel uses, so the wire
6
+ # format changes but nothing downstream does:
7
+ #
8
+ # inbound station frame ──▶ MessageHandler (parse, route, hooks, audit)
9
+ # outbound MessageHandler / remote-control jobs ──▶ ChargePointChannel
10
+ # .broadcast_to(cp, {message:}) ──▶ ActionCable pub/sub ──▶ this
11
+ # Connection's subscription ──▶ driver.text down the socket
12
+ #
13
+ # Reusing the existing broadcast as the outbound bus means the 10 producer
14
+ # sites (MessageHandler#send_callresult/#send_callerror and every remote
15
+ # command job) are unchanged, and cross-process routing (a job running on a
16
+ # different Puma worker than the socket) comes for free from the pub/sub
17
+ # adapter (async in dev, solid_cable/redis in production).
18
+ #
19
+ # `driver` is any websocket driver (WebSocket::Driver.rack in production, a
20
+ # stub in tests) responding to #on, #text, #ping, #close and, once started,
21
+ # relaying frames via its :message event. This object is transport-only; it
22
+ # never parses OCPP (Protocol/MessageHandler do).
23
+ class Connection
24
+ attr_reader :charge_point
25
+
26
+ def initialize(charge_point:, driver:, io: nil, logger: ::Rails.logger, pubsub: nil)
27
+ @charge_point = charge_point
28
+ @driver = driver
29
+ @io = io
30
+ @logger = logger
31
+ @pubsub = pubsub
32
+ @write_mutex = Mutex.new
33
+ @closed = false
34
+ end
35
+
36
+ # Wire driver callbacks, displace any prior socket for this station, mark
37
+ # it connected, and start relaying server->station frames. The caller
38
+ # (endpoint) still owns writing the 101 (driver.start) and the read loop.
39
+ def open
40
+ @driver.on(:message) { |event| handle_inbound(event.data) }
41
+ @driver.on(:close) { close }
42
+ @driver.on(:error) { |event| log_error("driver error", event) }
43
+
44
+ displace_previous
45
+ ::Rails.application.executor.wrap { mark_connected }
46
+ subscribe_outbound
47
+ self
48
+ end
49
+
50
+ # Feed one inbound OCPP-J frame (a JSON string) to the shared handler,
51
+ # dropping it if the station is over its per-minute message budget — the
52
+ # same guard, in the same place, as ChargePointChannel#receive.
53
+ def handle_inbound(text)
54
+ return if @closed
55
+
56
+ unless Ocpp::Rails.message_rate_limiter.allow?(@charge_point.identifier)
57
+ @logger.warn("[OCPP][security] #{ident}: message rate limit exceeded, dropping message")
58
+ return
59
+ end
60
+
61
+ ::Rails.application.executor.wrap do
62
+ MessageHandler.new(@charge_point, text).process
63
+ end
64
+ rescue => e
65
+ log_error("error handling inbound message", e)
66
+ end
67
+
68
+ # Relay a frame published on this station's ActionCable broadcasting down
69
+ # the socket. `encoded` is what the pub/sub adapter carries: the JSON
70
+ # dump of {message: "<ocpp-json>"} that broadcast_to produced.
71
+ def relay(encoded)
72
+ return if @closed
73
+
74
+ frame = JSON.parse(encoded)["message"]
75
+ write(frame) if frame
76
+ rescue => e
77
+ log_error("error relaying outbound frame", e)
78
+ end
79
+
80
+ def write(frame)
81
+ @write_mutex.synchronize { @driver.text(frame) }
82
+ end
83
+
84
+ def ping
85
+ @write_mutex.synchronize { @driver.ping }
86
+ rescue => e
87
+ log_error("ping failed", e)
88
+ end
89
+
90
+ # Idempotent teardown. Safe to call from the driver's :close event, the
91
+ # endpoint reader thread's ensure, or a displacing reconnect.
92
+ def close
93
+ return if @closed
94
+
95
+ @closed = true
96
+ unsubscribe_outbound
97
+ Registry.remove(@charge_point.identifier, self)
98
+ ::Rails.application.executor.wrap { mark_disconnected }
99
+ close_io
100
+ @driver.close
101
+ rescue => e
102
+ log_error("error during teardown", e)
103
+ end
104
+
105
+ def closed?
106
+ @closed
107
+ end
108
+
109
+ private
110
+
111
+ def displace_previous
112
+ previous = Registry.register(@charge_point.identifier, self)
113
+ return if previous.nil? || previous.equal?(self)
114
+
115
+ @logger.info("[OCPP][raw] #{ident}: superseding previous connection")
116
+ previous.close
117
+ end
118
+
119
+ def subscribe_outbound
120
+ @broadcasting = ChargePointChannel.broadcasting_for(@charge_point)
121
+ @relay = ->(encoded) { relay(encoded) }
122
+ pubsub.subscribe(@broadcasting, @relay)
123
+ end
124
+
125
+ def unsubscribe_outbound
126
+ pubsub.unsubscribe(@broadcasting, @relay) if @broadcasting && @relay
127
+ rescue => e
128
+ log_error("unsubscribe failed", e)
129
+ end
130
+
131
+ def pubsub
132
+ @pubsub ||= ActionCable.server.pubsub
133
+ end
134
+
135
+ def mark_connected
136
+ was = @charge_point.connected
137
+ @charge_point.update(connected: true, last_heartbeat_at: Time.current)
138
+ log_connection_change(was, true)
139
+ @logger.info("[OCPP][raw] #{ident}: connected")
140
+ end
141
+
142
+ def mark_disconnected
143
+ was = @charge_point.connected
144
+ @charge_point.disconnect!
145
+ log_connection_change(was, false)
146
+ @logger.info("[OCPP][raw] #{ident}: disconnected")
147
+ end
148
+
149
+ def log_connection_change(old_connected, new_connected)
150
+ return if old_connected == new_connected
151
+
152
+ StateChange.create!(
153
+ charge_point: @charge_point,
154
+ change_type: "connection",
155
+ connector_id: nil,
156
+ old_value: old_connected.to_s,
157
+ new_value: new_connected.to_s,
158
+ metadata: { source: "raw_socket" }
159
+ )
160
+ rescue => e
161
+ log_error("failed to log state change", e)
162
+ end
163
+
164
+ def close_io
165
+ @io.close if @io && !@io.closed?
166
+ rescue => e
167
+ log_error("io close failed", e)
168
+ end
169
+
170
+ def ident
171
+ "ChargePoint #{@charge_point.identifier}"
172
+ end
173
+
174
+ def log_error(context, error)
175
+ message = error.respond_to?(:message) ? error.message : error.to_s
176
+ @logger.error("[OCPP][raw] #{ident}: #{context}: #{message}")
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,135 @@
1
+ require "websocket/driver"
2
+ require "socket"
3
+
4
+ module Ocpp
5
+ module Rails
6
+ module RawSocket
7
+ # Rack app that terminates native OCPP-J WebSocket connections — what a real
8
+ # charge point speaks (bare `[2,"id","Action",{}]` frames over a plain
9
+ # WebSocket, subprotocol "ocpp1.6", identity in the URL path), with no
10
+ # ActionCable subscription handshake.
11
+ #
12
+ # It authenticates from the Rack env *before* taking the socket, then
13
+ # full-hijacks (Rack `rack.hijack`, supported by Puma), completes the
14
+ # WebSocket upgrade with websocket-driver, and hands the socket to a
15
+ # Connection that bridges to the shared OCPP core. One reader thread per
16
+ # socket feeds inbound bytes to the driver; outbound frames arrive via the
17
+ # Connection's pub/sub subscription.
18
+ #
19
+ # Mount it from the engine when `config.transport` includes :raw.
20
+ class Endpoint
21
+ READ_CHUNK = 16_384
22
+
23
+ def call(env)
24
+ return not_websocket unless WebSocket::Driver.websocket?(env)
25
+ return no_hijack unless env["rack.hijack"]
26
+
27
+ decision = Handshake.new(env).call
28
+ unless decision.accepted?
29
+ log_reject(decision)
30
+ return unauthorized
31
+ end
32
+
33
+ accept(env, decision)
34
+ # Rack full-hijack sentinel: the server must not write a response.
35
+ [ -1, {}, [] ]
36
+ rescue => e
37
+ ::Rails.logger.error("[OCPP][raw] endpoint error: #{e.class}: #{e.message}")
38
+ internal_error
39
+ end
40
+
41
+ private
42
+
43
+ def accept(env, decision)
44
+ env["rack.hijack"].call
45
+ io = env["rack.hijack_io"]
46
+ enable_tcp_keepalive(io)
47
+
48
+ driver = WebSocket::Driver.rack(
49
+ RackSocket.new(env, io),
50
+ protocols: Ocpp::Rails.configuration.websocket_subprotocols,
51
+ max_length: Ocpp::Rails.configuration.raw_socket_max_frame_bytes
52
+ )
53
+
54
+ connection = Connection.new(charge_point: decision.charge_point, driver: driver, io: io)
55
+ connection.open
56
+ driver.start # writes the 101 handshake, echoing the negotiated Sec-WebSocket-Protocol
57
+ spawn_reader(io, driver, connection)
58
+ end
59
+
60
+ # One blocking reader per socket (Puma's threaded model). websocket-driver
61
+ # parses frames and fires the Connection's :message callback synchronously
62
+ # on this thread; teardown is funnelled through Connection#close so it runs
63
+ # exactly once whether the peer closed, the socket errored, or the driver
64
+ # emitted :close.
65
+ def spawn_reader(io, driver, connection)
66
+ Thread.new do
67
+ Thread.current.name = "ocpp-raw-#{connection.charge_point.identifier}"
68
+ begin
69
+ until connection.closed?
70
+ driver.parse(io.readpartial(READ_CHUNK))
71
+ end
72
+ rescue EOFError, IOError, Errno::ECONNRESET, Errno::EPIPE
73
+ # peer went away / socket closed — normal disconnect
74
+ rescue => e
75
+ ::Rails.logger.error(
76
+ "[OCPP][raw] reader error for #{connection.charge_point.identifier}: #{e.message}"
77
+ )
78
+ ensure
79
+ connection.close
80
+ end
81
+ end
82
+ end
83
+
84
+ # Detect a silently-dropped peer (no FIN) so a half-open socket doesn't
85
+ # pin the station "connected" forever; OCPP Heartbeat covers the rest.
86
+ def enable_tcp_keepalive(io)
87
+ io.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) if io.respond_to?(:setsockopt)
88
+ rescue StandardError
89
+ # non-fatal; some IO objects (e.g. in tests) don't support socket opts
90
+ end
91
+
92
+ def not_websocket
93
+ [ 426, { "content-type" => "text/plain", "connection" => "upgrade", "upgrade" => "websocket" },
94
+ [ "Upgrade required" ] ]
95
+ end
96
+
97
+ def no_hijack
98
+ [ 501, { "content-type" => "text/plain" }, [ "WebSocket hijacking not supported" ] ]
99
+ end
100
+
101
+ # Security-Profile-1 challenge, so a station may (re)send Basic credentials.
102
+ def unauthorized
103
+ [ 401, { "content-type" => "text/plain", "www-authenticate" => %(Basic realm="OCPP") },
104
+ [ "Unauthorized" ] ]
105
+ end
106
+
107
+ def internal_error
108
+ [ 500, { "content-type" => "text/plain" }, [ "Internal Server Error" ] ]
109
+ end
110
+
111
+ def log_reject(decision)
112
+ ::Rails.logger.warn(
113
+ "[OCPP][security] raw OCPP-J upgrade rejected for #{decision.identifier.inspect}: #{decision.failure}"
114
+ )
115
+ end
116
+
117
+ # Adapter presented to WebSocket::Driver.rack: the driver reads the
118
+ # Sec-WebSocket-* handshake headers from #env and writes the 101 response
119
+ # and subsequent frames through #write to the hijacked socket.
120
+ class RackSocket
121
+ attr_reader :env
122
+
123
+ def initialize(env, io)
124
+ @env = env
125
+ @io = io
126
+ end
127
+
128
+ def write(data)
129
+ @io.write(data)
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,78 @@
1
+ require "cgi"
2
+
3
+ module Ocpp
4
+ module Rails
5
+ module RawSocket
6
+ # Decides whether a raw OCPP-J WebSocket upgrade should be accepted, purely
7
+ # from the Rack env — no socket I/O — so it is unit-testable in isolation
8
+ # and identical whether the transport hijacks under Puma or is driven by a
9
+ # test harness.
10
+ #
11
+ # OCPP-J carries the charge-point identity in the URL *path* (unlike the
12
+ # ActionCable channel, which reads it from the subscribe command) and the
13
+ # Security-Profile-1 credential in the HTTP Basic `Authorization` header of
14
+ # the upgrade. This mirrors `ChargePointChannel#subscribed` — connection
15
+ # rate limit, then `StationAuthenticator` — so both transports authenticate
16
+ # a station identically.
17
+ class Handshake
18
+ Result = Struct.new(:charge_point, :identifier, :subprotocol, :failure, keyword_init: true) do
19
+ def accepted?
20
+ failure.nil?
21
+ end
22
+ end
23
+
24
+ def initialize(env, subprotocols: Ocpp::Rails.configuration.websocket_subprotocols)
25
+ @env = env
26
+ @subprotocols = Array(subprotocols)
27
+ end
28
+
29
+ def call
30
+ identifier = self.class.identifier_from_path(@env["PATH_INFO"])
31
+ return reject(identifier, :missing_identifier) if identifier.nil? || identifier.empty?
32
+
33
+ unless Ocpp::Rails.connection_rate_limiter.allow?(identifier)
34
+ return reject(identifier, :rate_limited)
35
+ end
36
+
37
+ result = StationAuthenticator.authenticate(
38
+ identifier: identifier,
39
+ authorization_header: @env["HTTP_AUTHORIZATION"]
40
+ )
41
+ return reject(identifier, result.failure) unless result.success?
42
+
43
+ Result.new(
44
+ charge_point: result.charge_point,
45
+ identifier: identifier,
46
+ subprotocol: negotiated_subprotocol,
47
+ failure: nil
48
+ )
49
+ end
50
+
51
+ # The OCPP identity is the last non-empty path segment, URL-decoded:
52
+ # "/ocpp/CP-1" and vendor variants like "/ocpp/steve/CP-1" both yield
53
+ # "CP-1". (Under the mounted engine the endpoint sees the engine-relative
54
+ # path, e.g. "/CP-1".)
55
+ def self.identifier_from_path(path)
56
+ return nil if path.nil?
57
+
58
+ segment = path.split("/").reject(&:empty?).last
59
+ segment && CGI.unescape(segment)
60
+ end
61
+
62
+ private
63
+
64
+ # The actual Sec-WebSocket-Protocol echo is performed by the websocket
65
+ # driver from the same list; this is the value we expect it to pick, kept
66
+ # on the Result for logging/inspection. nil when the station offered none.
67
+ def negotiated_subprotocol
68
+ offered = (@env["HTTP_SEC_WEBSOCKET_PROTOCOL"] || "").split(/ *, */)
69
+ (@subprotocols & offered).first
70
+ end
71
+
72
+ def reject(identifier, failure)
73
+ Result.new(identifier: identifier, failure: failure)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,52 @@
1
+ module Ocpp
2
+ module Rails
3
+ module RawSocket
4
+ # Process-local map of the live raw connections this process owns, keyed by
5
+ # charge-point identifier. Used to displace a stale socket when a station
6
+ # reconnects (OCPP allows only one live connection per station) and to see
7
+ # how many stations a process holds. NOT used for message delivery — that
8
+ # rides the ActionCable pub/sub bus (see Connection), which already routes
9
+ # across processes.
10
+ #
11
+ # State lives in class ivars, so in development each code reload starts
12
+ # empty (dev drops sockets on reload anyway); in production the process is
13
+ # eager-loaded and never reloaded, so entries persist for the socket's life.
14
+ class Registry
15
+ @mutex = Mutex.new
16
+ @connections = {}
17
+
18
+ class << self
19
+ # Record `connection` as the current owner of `identifier`, returning
20
+ # the connection it replaced (if any) so the caller can close it.
21
+ def register(identifier, connection)
22
+ @mutex.synchronize do
23
+ previous = @connections[identifier]
24
+ @connections[identifier] = connection
25
+ previous
26
+ end
27
+ end
28
+
29
+ # Drop `connection` only if it is still the current owner (a newer
30
+ # reconnect must not be evicted by an older socket's teardown).
31
+ def remove(identifier, connection)
32
+ @mutex.synchronize do
33
+ @connections.delete(identifier) if @connections[identifier].equal?(connection)
34
+ end
35
+ end
36
+
37
+ def [](identifier)
38
+ @mutex.synchronize { @connections[identifier] }
39
+ end
40
+
41
+ def size
42
+ @mutex.synchronize { @connections.size }
43
+ end
44
+
45
+ def clear!
46
+ @mutex.synchronize { @connections = {} }
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
data/config/routes.rb CHANGED
@@ -1,5 +1,19 @@
1
1
  Ocpp::Rails::Engine.routes.draw do
2
- # OCPP WebSocket endpoint via ActionCable
3
- # Charge points connect to: ws://your-host/ocpp/cable
4
- mount ActionCable.server => "/cable"
2
+ # OCPP transport(s), selected by `config.transport` (see Ocpp::Rails::Configuration):
3
+ #
4
+ # :action_cable (default) — OCPP-J wrapped in the ActionCable JSON protocol.
5
+ # Charge points connect to: ws://your-host/ocpp/cable
6
+ # :raw — native bare OCPP-J over a plain WebSocket, what real stations speak.
7
+ # Charge points connect to: ws://your-host/ocpp/<charge-point-identifier>
8
+ # :both — mount both (migration).
9
+ #
10
+ # /cable is declared first so it keeps winning over the greedy raw mount at "/".
11
+ if Ocpp::Rails.transport_enabled?(:action_cable)
12
+ mount ActionCable.server => "/cable"
13
+ end
14
+
15
+ if Ocpp::Rails.transport_enabled?(:raw)
16
+ mount Ocpp::Rails::RawSocket::Endpoint.new => Ocpp::Rails.configuration.raw_socket_path,
17
+ as: :ocpp_raw_socket
18
+ end
5
19
  end
@@ -1,5 +1,5 @@
1
1
  module Ocpp
2
2
  module Rails
3
- VERSION = "0.3.0"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  end
data/lib/ocpp/rails.rb CHANGED
@@ -19,6 +19,15 @@ module Ocpp
19
19
  configuration.supported_versions
20
20
  end
21
21
 
22
+ # Whether a given charge-point transport is active. `transport` is one of
23
+ # :action_cable (default), :raw, or :both — so a deployment can run the
24
+ # legacy ActionCable-wrapped endpoint, the raw OCPP-J endpoint, or both side
25
+ # by side during migration. Consulted when the engine draws its routes.
26
+ def self.transport_enabled?(kind)
27
+ transport = configuration.transport
28
+ transport == kind || transport == :both
29
+ end
30
+
22
31
  def self.message_rate_limiter
23
32
  @message_rate_limiter ||= RateLimiter.new { configuration.max_messages_per_minute }
24
33
  end
@@ -37,7 +46,8 @@ module Ocpp
37
46
  :state_change_hooks, :state_change_retention_days, :state_change_cleanup_enabled,
38
47
  :authorization_hooks, :authorization_retention_days, :authorization_cleanup_enabled,
39
48
  :session_hooks, :implausible_energy_jump_wh, :authentication_mode,
40
- :max_messages_per_minute, :max_connection_attempts_per_minute
49
+ :max_messages_per_minute, :max_connection_attempts_per_minute,
50
+ :transport, :raw_socket_path, :websocket_subprotocols, :raw_socket_max_frame_bytes
41
51
 
42
52
  def initialize
43
53
  @ocpp_version = "1.6"
@@ -63,6 +73,22 @@ module Ocpp
63
73
  # nil disables the respective check.
64
74
  @max_messages_per_minute = 300
65
75
  @max_connection_attempts_per_minute = 12
76
+ # Charge-point transport. :action_cable (default; ActionCable-wrapped
77
+ # OCPP-J, backwards compatible) | :raw (native bare OCPP-J over a plain
78
+ # WebSocket, what real stations speak) | :both (run both endpoints).
79
+ @transport = :action_cable
80
+ # Where the raw OCPP-J endpoint is mounted *within the engine*. With the
81
+ # host app mounting the engine at "/ocpp", the default "/" makes a station
82
+ # connect to ws://host/ocpp/<identifier> (the trailing path segment is the
83
+ # OCPP identity). "/cable" stays reserved for the ActionCable endpoint.
84
+ @raw_socket_path = "/"
85
+ # Subprotocols offered back to a raw station, in server-preference order.
86
+ # OCPP 1.6-J stations send "ocpp1.6"; the negotiated value is echoed in
87
+ # the handshake's Sec-WebSocket-Protocol response header.
88
+ @websocket_subprotocols = [ "ocpp1.6", "ocpp1.6j" ]
89
+ # Max inbound WebSocket message size on the raw endpoint (bytes). OCPP-J
90
+ # messages are small; oversized frames are a DoS vector and are refused.
91
+ @raw_socket_max_frame_bytes = 64 * 1024
66
92
  end
67
93
 
68
94
  def register_state_change_hook(hook)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ocpp-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakob Sommerhuber
@@ -143,6 +143,10 @@ files:
143
143
  - app/services/ocpp/rails/meter_anomaly_detector.rb
144
144
  - app/services/ocpp/rails/protocol.rb
145
145
  - app/services/ocpp/rails/rate_limiter.rb
146
+ - app/services/ocpp/rails/raw_socket/connection.rb
147
+ - app/services/ocpp/rails/raw_socket/endpoint.rb
148
+ - app/services/ocpp/rails/raw_socket/handshake.rb
149
+ - app/services/ocpp/rails/raw_socket/registry.rb
146
150
  - app/services/ocpp/rails/session_hook_manager.rb
147
151
  - app/services/ocpp/rails/state_change_hook_manager.rb
148
152
  - app/services/ocpp/rails/station_authenticator.rb