Repository navigation
feat: add OCPP 1.2 support over JSON/WebSocket - #107
Conversation
c7dd112 to
ef766f2
Compare
pbourseau
left a comment
There was a problem hiding this comment.
Reviewed the full diff, checked the 36 schemas against the ocpp-1-2-core model, and ran ./gradlew :ocpp-1-2-json:test :toolkit:test locally — green.
The approach is sound and the scoping is right. Things I verified rather than took on trust:
- The 36 schemas cover exactly the 18 actions in the 1.2
Actionsregistry — no gap, no extra. - For every message,
propertiesandrequiredmatch the Kotlin model, optionality included (BootNotificationResp.currentTime/heartbeatInterval,MeterValuesReq.values,StopTransactionResp.idTagInfo,GetDiagnosticsResp.fileName). - The schema enums match the Kotlin enums:
ChargePointStatus(4),ChargePointErrorCode(8),AuthorizationStatus(5). - The classpath collision you describe is real, and larger than the description suggests: 1.5 and 1.6 alone share 20+
*Response.jsonnames at the resources root, andtoolkitpulls in both. Namespacing 1.2 underocpp12/was necessary, and leaving the rest to its own change is the right call. - Both
OcppVersionenums line up on constants and subprotocols, so thevalueOf(name)bridge inWebsocketServerholds.OcppVersionBridgeTestcovers a coupling nothing checked before — good addition. - Removing the server-side guard does not leave
getWampVersiondead: the client path (ApiFactory.kt:92) still calls it, so the exhaustivewhenkeeps doing its job. - Dropping the 1.5
EnumMixinfromOcpp12JsonObjectMapperis correct — no 1.2 enum has avaluethat differs from its constant name.
The schema-tightness tests are the part I liked most: they assert the schemas are narrower than 1.5 rather than merely valid, and the "BootNotification.conf with only status" case really does guard the resource prefix against a classpath regression.
One thing to fix before merge — see the inline comment on StopTransaction.json.
Two non-blocking notes:
Ocpp12FactoryTestreserves a port withServerSocket(0).use { it.localPort }and rebinds it afterwards — the usual TOCTOU window and a possible flake source on a loaded CI. No simpler alternative here, just flagging it.- Worth opening a follow-up issue for the 1.5/1.6/2.0 resource collision, so the decision to defer it is tracked somewhere other than this PR description.
Happy to approve once the maxLength is in.
| "idTag": { | ||
| "type": "string" | ||
| }, |
There was a problem hiding this comment.
idTag is the only idTag/parentIdTag in the 1.2 set without a maxLength: Authorize, RemoteStartTransaction, StartTransaction and both IdTagInfo blocks all carry maxLength: 15. The 1.5 counterpart has maxLength: 20 on this very field, so the constraint was dropped here rather than narrowed.
Concretely: a StopTransaction carrying a 40-character idTag validates, while the same idTag is rejected by Authorize and StartTransaction — the two ends of one transaction disagree. It also runs against the "1.2 is systematically narrower than 1.5" invariant the rest of the PR is built on.
| "idTag": { | |
| "type": "string" | |
| }, | |
| "idTag": { | |
| "type": "string", | |
| "maxLength": 15 | |
| }, |
There was a problem hiding this comment.
Fixed in 050d000 — you are right, and the inconsistency is in the specification itself rather than in the reading of it.
I had left maxLength off deliberately: §6.32 of the 1.2 specification types this field idTag string 0..1, with no [15], while §6.1 (Authorize.req), §6.22 (RemoteStartTransaction.req), §6.30 (StartTransaction.req) and §7.10 (IdTagInfo.parentIdTag) all say string[15]. So the schema followed the document literally, one field at a time.
What decides it is that the 1.2 WSDL cannot arbitrate — it declares no maxLength anywhere, every field is a bare s:string, which is why the lengths come from the PDF in the first place. And 1.5, which does model it, uses a single named IdToken type (maxLength 20) for this very element:
<s:element name="idTag" type="tns:IdToken" minOccurs="0" maxOccurs="1"/>One identifier, one constraint, StopTransaction.req included. There is no reading under which 1.2 widens the identifier only when stopping the transaction it just started under 15 characters — the omission in §6.32 is a documentation slip, and the rest of the PR's invariant is the better guide.
Applied your suggestion. The seven idTag/parentIdTag occurrences across the 1.2 set now carry maxLength: 15 uniformly.
Covered red/green by a new test in Ocpp12JsonParserErrorTest, which pins the narrowing rather than merely the presence of a limit:
@Test
fun `should reject a stopTransaction idTag longer than the 1-2 limit`() {
// 18 characters: within the 1.5 limit of 20, over the 1.2 limit of 15.
val overLimit = stopTransactionWith(idTag = "012345678901234567")
val atLimit = stopTransactionWith(idTag = "012345678901234")
expectRejectedWith(overLimit, ValidatorTypeCode.MAX_LENGTH)
expectThat(parser.parseAnyFromString(atLimit)).get { action }.isEqualTo("StopTransaction")
}An 18-character tag is chosen so the test fails if the constraint is ever relaxed back to the 1.5 value of 20, not just if it disappears. Verified red before the schema change, green after.
|
Follow-up on my note about the deferred 1.5/1.6/2.0 collision: I opened #111 for it, so no need to file one on your side. Digging into it while reviewing this PR, it turned out to be worse than "a latent risk". On Which is to say your |
ef766f2 to
e678180
Compare
|
Thanks for opening #111, and for going further than I did — I had this filed under "latent risk", and the 2.0.1 The detail that makes it nasty is exactly the one you name: the per-module suites cannot see it. Agreed that I have referenced #111 from the PR description in place of the "worth a follow-up" note. On your two review notes: the |
pbourseau
left a comment
There was a problem hiding this comment.
Re-reviewed at e678180f. The fix is exactly what was asked, and the added test is better than what I suggested.
StopTransaction.json now carries maxLength: 15 on idTag, and all 7 idTag/parentIdTag occurrences across the 1.2 schema set are consistent at 15. should reject a stopTransaction idTag longer than the 1-2 limit pins both edges — 18 characters rejected (valid under the 1.5 limit of 20, so it would catch a silent widening), 15 accepted — which is a stronger guard than a one-sided assertion.
Verified on my side:
./gradlew :ocpp-1-2-json:test :toolkit:testgreen on the new head;Ocpp12JsonParserErrorTestat 17 tests, 0 failures. CI green too.- The force-push is a clean amend: the only delta from the version I reviewed is the
maxLengthline plus the new test and its helper. Same merge base (a254105d), nothing else moved.
One follow-up from my earlier note, so it does not get filed twice: I opened #111 for the 1.5/1.6/2.0 collision. Investigating it turned up that it is already breaking 2.0.1 payloads on dev — AuthorizeRequest.json resolves to the 1.6 jar ahead of the 2.0 one, so a valid 2.0.1 Authorize comes back as a ProtocolError. That makes the schemaFolder parameter here the mechanism the fix depends on. Still right to have kept it out of this PR.
LGTM.
OCPP-J defines the JSON/WebSocket transport independently of message content and registers `ocpp1.2` as a WebSocket subprotocol, so OCPP 1.2 is not SOAP-only. The toolkit shipped `ocpp-1-5-json` but had no 1.2 counterpart. Mirror `ocpp-1-5-json`: an `Ocpp12JsonParser`, a Jackson mapper and the 36 JSON schemas for the 18 OCPP 1.2 messages, derived from the OCPP 1.2 WSDL with the string lengths declared by the specification. The schemas are deliberately narrower than the 1.5 ones: 4 charge point statuses instead of 5, 8 error codes instead of 13, a plain integer meter value instead of a SampledValue list, three StatusNotification properties instead of seven, and only `status` required in BootNotification.conf. `OcppJsonValidator` resolves schemas as classpath resources by action name, and every version module ships its schemas under the same names, so 1.5, 1.6 and 2.0 already shadow each other. Give the validator an optional `schemaFolder` and have the 1.2 module namespace its own resources under `ocpp12`. The default keeps the existing modules byte-for-byte unchanged; migrating them is left for a follow-up.
`ApiFactory` rejected OCPP 1.2 over websocket outright, because no JSON parser existed for that version. Now that `ocpp-1-2-json` provides one, register the `ocpp1.2` subprotocol and route it to `Ocpp12JsonParser`. `getJsonMapper` becomes exhaustive: its `else` branch was unreachable and reported the wrong version, and a future OCPP version should now fail to compile here rather than throw at runtime. `createServerTransportWebsocket` guarded against a version with no WebSocket binding by calling `getWampVersion` for its side effect. Every version maps now, so the guard can no longer fire; drop it and cover what it really protected — that the transport and WAMP version enums stay in step, since `WebsocketServer` bridges them by name — with a test that fails on a divergence. The two `Ocpp12FactoryTest` cases asserting that the combination is rejected become tests of the supported behaviour, alongside an end-to-end websocket round trip covering both directions.
The README stated that OCPP-J covered "1.5 and later" and that OCPP 1.2 had no WebSocket binding, which stopped being true with `ocpp-1-2-json`.
e678180 to
8bfd7e6
Compare
|



Refs #84, which #98 closed by adding OCPP 1.2 over SOAP. This completes 1.2 with the JSON/WebSocket transport.
Summary
OCPP 1.2 was assumed to be SOAP-only, but that is an implementation gap rather than a protocol one. The OCPP-J specification defines the JSON/WebSocket transport independently of message content and lists
ocpp1.2among the registered WebSocket subprotocols, alongsideocpp1.5,ocpp1.6andocpp2.0. The toolkit already shippedocpp-1-5-json; it simply had no 1.2 counterpart, soApiFactoryrejected the combination outright.This adds
ocpp-1-2-jsonand wires it through the WebSocket transport, so a 1.2 charge point connects end to end, and corrects the README, which claimed 1.2 had no WebSocket binding.What's included
ocpp-1-2-json(com.izivia.ocpp.json12):Ocpp12JsonParserandOcpp12JsonObjectMapper, mirroringocpp-1-5-json, plus the 36 JSON schemas for the 18 OCPP 1.2 messages.OcppVersiongainsOCPP_1_2("ocpp1.2"),getJsonMapperroutes it toOcpp12JsonParser, andApiFactory.getWampVersionmaps it instead of throwing.The schemas
OCPP 1.2 has no official OCPP-J schema set, so the 36 schemas are derived from the OCPP 1.2 WSDL, with the string lengths taken from the specification (the WSDL declares none). They are not copies of the 1.5 schemas — 1.2 is systematically narrower:
ChargePointStatusReserved)ChargePointErrorCodeStatusNotification.reqMeterValue.valueSampledValuelistBootNotification.confstatusrequiredstatus,currentTime,heartbeatIntervalrequiredidTagmaxLength15maxLength20Where the WSDL and the specification PDF disagree — the PDF adds an optional
connectorIdtoRemoteStartTransaction.req— the WSDL wins, which is also what the existingocpp-1-2-coremodel does.Schemas live in an
ocpp12resource folderOcppJsonValidatorresolves schemas by bare file name off the classpath.ocpp-1-5-jsonships<Action>.json/<Action>Response.jsonat the resources root whileocpp-1-6-jsonandocpp-2-0-jsonship<Action>Request.json/<Action>Response.json, so the*Response.jsonnames already shadow each other today — andtoolkitdepends on all of them.Without a namespace, a 1.2 payload could therefore be validated against the 1.5 schema, which is not hypothetical: 1.5's
BootNotificationResponse.jsonrequirescurrentTimeandheartbeatInterval, both optional in 1.2.So
OcppJsonValidatorgains an optionalschemaFolder, andocpp-1-2-jsonships its resources underocpp12/. The parameter defaults to the resources root, so 1.5, 1.6 and 2.0 are byte-for-byte unchanged, and@JvmOverloadskeeps the single-argument constructor in the published artifact.Ocpp12JsonParser.validateJsonis then identical to its 1.5 counterpart.The pre-existing collision between 1.5, 1.6 and 2.0 is left as is — tracked by #111, which confirms it already misvalidates 2.0.1 payloads against the 1.6 schemas on
dev. The mechanism to fix it now exists; moving their resources belongs in that change.Drive-by fix
getJsonMapper'selsebranch threw"Websocket transport is not supported by the ocpp version 1.5"while 1.5 was supported on the line above. Thewhenis now exhaustive overOcppVersion, so a future version fails to compile here instead of reporting the wrong thing at runtime.ApiFactory.createServerTransportWebsocketguarded against a version with no WebSocket binding by callinggetWampVersionfor its side effect. Every version maps now, so that guard can no longer fire; it is replaced by a test asserting that the transport and WAMPOcppVersionenums stay in step — the matchWebsocketServerrelies on when it bridges them by name, and which nothing else checked.Tests
JsonSchemaTest— a parse/serialise round trip per message, 36 in total.Ocpp12JsonParserErrorTest— the parser error paths (validation on/off, ignored validation codes, forced field types) plus schema-tightness guards:Reserved,GroundFailure, the 1.5-onlyStatusNotificationfields and the 1.5SampledValueshape are all rejected, while aBootNotification.confcarrying onlystatusis accepted. That last one also guards the resource prefix: it would fail if the 1.5 schema were picked up from the classpath.Ocpp12FactoryTest— the two cases asserting that 1.2 over WebSocket is rejected become tests of the supported behaviour, and a new end-to-end test runs a real WebSocket round trip (authorizecharge point → central system,remoteStartTransactioncentral system → charge point).Verification