1.Audience and what a profile is for
This cookbook is for charger OEM partners, system integrators, and PixellEnergy field-engineers responding to a joint-test (JT-N) event. We assume you have already read the OCPP onboarding cookbook and have a station connected to staging or production.
A vendor-quirk profile is a per-vendor configuration row that the CSMS gateway resolves at WSS-upgrade time and pins for the WSS session. The profile carries narrow overrides for known OCPP-spec deviations — heartbeat-interval clamps, MeterValues cadence hints, SignedMeterValue tax-grade declarations, diagnostics tarball upload styles, smart-charging recurrency allowlists, and so on. The handlers consume ctx.profile directly — they never re-resolve and never branch on raw vendor strings.
The profile registry exists because:
- Firmware ships before spec compliance is complete — a fleet of chargers in the field cannot be re-flashed in a day.
- The OCPP spec is permissive about several fields (optional timestamps, severity-bands, transactionId monotonicity) and vendors interpret them differently.
- India-market vendors carry domestic firmware quirks (Eichrecht pre-certification, RTC drift, aggressive MeterValues cadence) that international CSMS implementations do not document.
The contract: the registry is hot-loadable — a new profile is live within the elevated-benchmark target of 2 hours, with no gateway redeploy.
2.The VendorQuirkProfile interface
The full TypeScript interface lives at packages/api/src/services/vendor-profile.ts in the CitrineOS fork. Every field is optional — the absence of an override means the handler uses its spec-default path. The fields fall into eight thematic groups:
- Boot-notification quirks — model + heartbeat interval overrides.
- Idempotency / dedup quirks — transactionId-based dedup for buggy retry semantics.
- Authorize quirks — cache TTL clamps for chargers that aggressively cache
Acceptedresults. - Transaction quirks — boot-during-session tolerance + SignedMeterValue demotion.
- Outbound CALL quirks — per-action and global timeout overrides for cellular sites.
- Subprotocol & cadence — preference order hints + MeterValues fan-in expectations.
- Diagnostics + smart charging — R2 upload style, recurrency allowlist, Ampere-floor rounding.
- Firmware + OCPI gates — protocol floor and OCPI Authorize peer timeout.
The full field reference table — every override, its permitted values, default, and one-line semantic — is in the next section.
3.Field reference
The 23 override fields below are projected verbatim from the live VendorQuirkProfile interface in packages/api/src/services/vendor-profile.ts. The handlers branch only when a field is set; absence means spec-default behaviour. All fields are optional and the API schema is additive — appending a new override field is a backwards- compatible change.
| Field | Type | Required | Notes |
|---|---|---|---|
| bootNotificationModelOverride | string | optional | Override the chargePointModel reported by the device. Used when a vendor ships a marketing name vs the OCPP model string. Default: undefined. |
| heartbeatIntervalSecondsOverride | number | optional | Heartbeat interval (seconds) returned in BootNotificationConf when the spec-default 300 s does not match the firmware. Default: 300. |
| dedupOnTransactionIdInsteadOfMessageId | boolean | optional | When TRUE, the gateway dedups on (action, transaction_id) instead of messageId — for vendors that retry frames with different messageIds. Default: false. |
| authCacheTtlSecondsOverride | number | optional | Shorten the cacheExpiryDateTime returned to chargers that aggressively cache Accepted Authorize results locally. Default: 300. |
| tolerateStopWithoutStart | boolean | optional | When TRUE, StopTransaction without a confirmed StartTransaction is upserted (boot-during-session race tolerance). Default: false. |
| demoteSignedMeterValueToAdvisory | boolean | optional | When TRUE, SignedMeterValue from this vendor is treated as advisory regardless of OCPP version (firmware with nonsense bytes). Default: false. |
| outboundCallTimeoutSecondsOverride | number | optional | Override the default 30 s timeout for outbound OCPP CALL frames. Cellular sites typically widen to 45-60 s. Default: 30. |
| subprotocolPreference | array | optional | Preferred OCPP subprotocols in priority order. Consumed by the gateway at WSS upgrade to break ties when the charger offers multiple. Default: ['ocpp2.0.1', 'ocpp2.1', 'ocpp1.6']. Permitted: 'ocpp1.6', 'ocpp2.0.1', 'ocpp2.1'. |
| statusNotificationServerTimeFallback | boolean | optional | When TRUE, the StatusNotification handler uses server-clock as a fallback when the inbound timestamp is absent (1.6J §6.21). Default: true. |
| meterValuesCadenceSecondsExpected | number | optional | Expected interval between periodic MeterValues frames (seconds). Drives webhook-throttle policy pre-allocation. Default: 60. |
| signedMeterValueSupport | enum | optional | Declares the vendor SignedMeterValue capability. 1.6J is always advisory; v1 supports only the 2.0.1 tax-grade path. Default: undefined. Permitted: 'none', 'advisory-only', 'tax-grade-2.0.1-only', 'tax-grade-both'. |
| transactionIdMonotonicStrictness | enum | optional | Records observed transactionId behaviour for forensics. The server-side counter (migration 0010) is always strict-monotonic regardless. Default: 'strict'. Permitted: 'strict', 'tolerant'. |
| diagnosticsCallTimeoutSecondsOverride | number | optional | Override the GetLog / GetDiagnostics outbound CALL timeout (seconds) for vendors uploading large compressed tarballs over slow cellular. Default: 30. |
| diagnosticsTimestampMillisecondTruncate | boolean | optional | When TRUE, diagnostics handlers truncate fractional-ms to 3 digits before persistence (`.999999Z` → `.999Z`). Default: false. |
| notifyEventSeverityMappingOverride | enum | optional | Override the OCPP §F.4 severity-band mapping for vendors that emit a constant int regardless of actual severity. Default: 'standard'. Permitted: 'standard', 'always-informational', 'always-warning'. |
| r2UploadStyle | enum | optional | Cloudflare R2 upload preference for diagnostics tarballs. Multipart for firmware that chunks large logs (> 5 MiB). Default: 'single-put'. Permitted: 'single-put', 'multipart'. |
| smartChargingCallTimeoutSecondsOverride | number | optional | Override the SetChargingProfile / ClearChargingProfile / GetCompositeSchedule outbound CALL timeout (seconds). Default: 30. |
| smartChargingAmpereLimitIntegerFloor | boolean | optional | When TRUE, the SetChargingProfile handler floors fractional Ampere limits to integer before wire dispatch. Default: false. |
| smartChargingCompositeFanOutWholeStation | boolean | optional | When TRUE, GetCompositeSchedule(evseId=0) is rewritten as a fan-out across each provisioned EVSE (2.0.1 misinterpretation). Default: false. |
| smartChargingSupportedRecurrencyKinds | array | optional | Pre-validate the recurrencyKind against this allowlist; throws UnsupportedRecurrencyKindError BEFORE wire dispatch. Default: ['Daily', 'Weekly']. Permitted: 'Daily', 'Weekly'. |
| bootBeforeInstallStatus | boolean | optional | When TRUE, BootNotification.req arrives BEFORE FirmwareStatusNotification.status=Installed (Delta + Servotech AC22 v1.x quirk). Default: false. |
| protocolFloor | enum | optional | Lowest OCPP subprotocol this vendor firmware can run. Enforced at firmware-job dispatch for protocol transitions. Default: undefined. Permitted: 'OCPP1.6J', 'OCPP2.0.1', 'OCPP2.1'. |
| ocpiPeerCallTimeoutSecondsOverride | number | optional | Override the real-time OCPI Authorize fallback peer-call timeout (seconds) for eMSP peers with high tail-latency. Default: 5. |
4.Profile registry & hot-reload
The vendor-quirk profile registry is hot-loadable. The admin endpoint accepts a new profile body and, on a successful 201, the API pod that owns the request:
- Inserts the row into
vendor_quirk_profiles(migration 0037). The Ed25519 signature + signing identity are captured for tamper-evidence (v1.5; v1 stores the signing metadata as advisory). - Publishes a NATS event on
csms.vendor_quirk.profile_updated.<vendor_id>so all peer API pods invalidate their resolver caches. - Invalidates the local pod cache synchronously ahead of the NATS broadcast (defence-in-depth for the propagation gap).
- Emits an
audit_logrow taggedvendor_quirk.profile_updated(hash-anchored daily).
curl -X POST 'https://api.pixellenergy.com/v1/admin/vendor-quirks/<vendor_id>/profile' \
-H 'Authorization: Bearer <your-super-admin-jwt>' \
-H 'Idempotency-Key: vendor-quirk-register-<your-ulid>' \
-H 'Content-Type: application/json' \
-d '{
"profile_jsonb": {
"subprotocolPreference": ["ocpp2.0.1", "ocpp1.6"],
"outboundCallTimeoutSecondsOverride": 45,
"meterValuesCadenceSecondsExpected": 60,
"statusNotificationServerTimeFallback": true,
"signedMeterValueSupport": "tax-grade-2.0.1-only",
"transactionIdMonotonicStrictness": "strict",
"diagnosticsCallTimeoutSecondsOverride": 90,
"r2UploadStyle": "multipart"
},
"model_pattern": "Terra(AC|DC).*",
"firmware_range": ">=1.6.5"
}'{
"data": {
"id": "vqp_01HZX...",
"vendor_id": "ABB",
"model_pattern": "Terra(AC|DC).*",
"firmware_range": ">=1.6.5",
"registered_at": "2026-05-18T08:00:00Z",
"signed_by": "ops@pixellenergy.com",
"signature_hex": "<ed25519-sig-hex · v1.5>"
},
"error": null
}The same endpoint accepts a per-tenant override when the body carries a tenant_id. The hot-reload resolver selects the tenant-scoped row before the global fallback. Tenant overrides require the caller to be super_admin OR a tenant_admin whose JWT tenant_id matches the body.
curl -X POST 'https://api.pixellenergy.com/v1/admin/vendor-quirks/<vendor_id>/profile' \
-H 'Authorization: Bearer <your-tenant-admin-jwt>' \
-H 'Idempotency-Key: vendor-quirk-tenant-<your-ulid>' \
-H 'Content-Type: application/json' \
-d '{
"profile_jsonb": {
"outboundCallTimeoutSecondsOverride": 60
},
"tenant_id": "<your-tenant-uuid>"
}'# When a new profile lands, the API pod that accepted it
# publishes on a fanout subject. Every other api pod
# (including the WSS gateway) subscribes + invalidates its
# resolver cache. Subsequent WSS upgrades resolve the new
# profile; in-flight sessions keep their pinned profile.
#
# Subject pattern:
# csms.vendor_quirk.profile_updated.<vendor_id>
#
# Example:
csms.vendor_quirk.profile_updated.ABB5.Seed catalog walkthrough
The PixellEnergy CSMS ships with a seed catalog of nine vendor stubs today (eight in the seed catalog file at packages/api/src/ocpp/vendor-quirks/profiles/seed-catalog.ts + Zenergize registered directly via the registry). Each stub is a conservative best-guess derived from public OCPP vendor documentation + community-known firmware quirks + prior JT-0 dry-run observations + the upstream CitrineOS patterns. Every field narrows at the corresponding JT-N event with the vendor-quirk-curator subagent on the bridge.
India-market vendors (5):
Exicom— Exicom Tele-Systems (AC + DC Harmony Gen 2)TataPower-EZCharge— Tata Power EZ Charge (public-charging franchise · rebadged)Mass-Tech— Mass-Tech / Massive Charging (Pune · AC + DC)OKAYA— OKAYA EV (residential + commercial · Faridabad)Zenergize— Zenergize (PixellEnergy Customer 0 · JT-N pilot vendor)
International vendors (4):
ABB— ABB Terra AC / Terra DCDelta— Delta Electronics (AC Mini / DC Wallbox)Schneider-Electric— Schneider Electric EVlinkWallbox— Wallbox Pulsar / Commander / Quasar
The three highest-leverage India-market quirks to know about:
6.JT-N validation pattern
No real charger is used for ambient testing. Profile narrowing happens at the four-step joint-test cadence with a PixellEnergy bridge engineer on the call. The cycle is:
| Field | Type | Required | Notes |
|---|---|---|---|
| JT-1 | staging dry-run | required | Resolve the stub profile · verify WSS upgrade picks it up · observe MeterValues cadence + StatusNotification timestamp presence · narrow the cadence + server-clock-fallback fields. |
| JT-2 | session lifecycle | required | Run RemoteStart / Authorize / TransactionEvent / StopTransaction · observe SignedMeterValue payload + transactionId monotonicity · narrow the signedMeterValueSupport + transactionIdMonotonicStrictness fields. |
| JT-3 | resilience | required | Loss-of-network mid-session · firmware-update reboot · CSMS restart · narrow the tolerateStopWithoutStart + bootBeforeInstallStatus fields. |
| JT-4 | production cutover | required | Production tenant slug · Profile 3 client cert · 24 h soak · narrow the protocolFloor + ocpiPeerCallTimeoutSecondsOverride fields based on production observations. |
The elevated-benchmark target for the profile hot-patch window from the moment a quirk is observed in production to the moment the patch is live is under 2 hours. See §8 for the workflow.
7.OEM Compatibility Program + badge
The OEM Compatibility Program is the public on-ramp for charger vendors who want a badge on the PixellEnergy network. The badge is keyed off chargingStation.vendorName from BootNotification (see the OCPP onboarding cookbook §5) and surfaces in the operator portal + the tenant-facing public site. Badge eligibility is:
- Profile pre-registration. A vendor-quirk profile for your vendor is registered via the admin route in §4 — either authored by your firmware team and submitted to PixellEnergy support, or authored by vendor-quirk-curator after the first JT-0 dry-run observation.
- Fixture authoring. A MicroOcppSimulator scenario fixture for your vendor lives in the conformance harness at
conformance/microocpp-fixtures/vendor-profile-stubs/— one JSON file per (vendor, model) covering the BootNotification, Authorize, MeterValues, StatusNotification, and StopTransaction happy paths. The existing fixture set today:
conformance/microocpp-fixtures/vendor-profile-stubs/
├── abb-terra-ac.json
├── delta-ac-mini.json
├── exicom-harmony-gen2.json
├── mass-tech-default.json
├── okaya-ac.json
├── schneider-evlink-pro-ac.json
├── tata-power-ezcharge.json
├── wallbox-pulsar-plus.json
└── zenergize-dc60-v2.json- Pytest scaffold. A pytest scaffold case for your vendor lives at
conformance/mobilityhouse-tests/test_vendor_quirk_profiles.py— parametrised over the (vendor, model, firmware_version) triple. Scaffolds arepytest.skipat v1; live execution arrives once the OCPP conformance runner is fully wired.
# conformance/mobilityhouse-tests/test_vendor_quirk_profiles.py
import pytest
from ocpp.routing import on
@pytest.mark.parametrize(
"vendor_id, model, firmware_version",
[
("ABB", "Terra-AC-W11", "1.6.5"),
("Delta", "AC-Mini-7kW", "4.2.0"),
("Exicom", "Harmony-Gen2-DC", "2.1.0"),
# ... one row per vendor stub
],
)
def test_vendor_quirk_profile_resolves_at_wss_upgrade(
vendor_id, model, firmware_version, csms_gateway_url, station_credentials,
):
"""The CSMS gateway resolves the registered vendor profile during the
BootNotification handshake and applies the declared overrides for the
rest of the WSS session."""
# Test harness is scaffold-only at v1; live execution arrives once the
# OCPP conformance runner is wired in Wk 4 D5+.
pytest.skip("Scaffold-only at v1 — see conftest.py roadmap")- At-least-one production-day clean. A 24 h soak on a real production tenant with zero P1 incidents traceable to the vendor profile.
Once all four conditions are satisfied, vendor-quirk-curator stamps the badge on the vendor record and the operator portal renders the OEM Compatibility Program badge next to the charger card.
8.Hot-patch workflow
When a JT-N event (or a production incident) surfaces a quirk that needs a profile change, the workflow is:
- vendor-quirk-curator agent narrows the profile. The agent reads the JT-N notes + the OCPP frame capture + observed handler logs, then drafts the override delta against the existing profile.
- Admin route hot-loads the new profile. The agent calls
POST /v1/admin/vendor-quirks/<vendor_id>/profilewith the narrowed body. The new row lands invendor_quirk_profilesimmediately. - NATS fan-out invalidates resolver caches. Every API pod subscribes to
csms.vendor_quirk.profile_updated.<vendor_id>and flushes its cache on receive. Propagation across the cluster is sub-second on the production NATS HA (3-node Railway). - Subsequent WSS upgrades pick up the new profile. In-flight WSS sessions keep their pinned profile — new sessions resolve the updated profile from the next BootNotification onward. A pod restart is not required.
- Audit + verification. The patch event is audit-logged. vendor-quirk-curator then opens a follow-up ticket for the v1.5 fixture + pytest update so the regression is covered going forward.
The full SLA target — quirk observation to patch live — is under 2 hours. The seed-catalog pre-staging shrinks this window further: at JT-N the curator narrows by (model, firmware) rather than authoring from scratch.
9.Audit + compliance
Every profile change emits:
- An
audit_log_appendrow taggedvendor_quirk.profile_updatedwith the fullprofile_jsonbbody, the prior profile id, the signing identity, and the signature hex (v1.5). - A NATS event on
csms.vendor_quirk.profile_updated.<vendor_id>— durable JetStream subject (CSMS_OCPP stream); consumed by the audit-chain worker for daily hash anchoring. - A webhook event
vendor_quirk.profile.updatedon the tenant webhook surface — HMAC-SHA256 signed; 7d uniqueness onevent.id; consumer-side dedup expected.
The audit chain is hash-anchored daily per CLAUDE.md DPDP Phase I Day-1 non-negotiables. Profile changes form part of the per-tenant audit trail retained for 8 years (Income Tax Act).
10.Support, escalation, and rollback
Profile changes are reversible. To roll back to a known-good profile, re-register that body via the same admin route — the most-recent insert wins per the resolution order at StaticProfileResolver.keyOf (exact > vendor+model > vendor-only > base).
curl -X POST 'https://api.pixellenergy.com/v1/admin/vendor-quirks/<vendor_id>/profile' \
-H 'Authorization: Bearer <your-super-admin-jwt>' \
-H 'Idempotency-Key: vendor-quirk-rollback-<your-ulid>' \
-H 'Content-Type: application/json' \
-d '{
"profile_jsonb": <prior-known-good-profile-body>,
"model_pattern": null,
"firmware_range": null
}'Coordinate JT-N sessions + escalations via oem-compatibility@pixellenergy.com. The OEM Compatibility team responds within one business day (Mumbai, Mon–Fri, 09:00–18:00 IST). For production incidents, the on-call pager rotation reaches a vendor-quirk-curator-trained engineer within 30 minutes; rollback execution is single-call.
For the full OCPP onboarding context see the OCPP onboarding cookbook. For peer-roaming integration see the OCPI handshake cookbook. For the full REST surface see the API reference.