Integration guides

Vendor-quirk profile cookbook

A 10-section hand-rail for OEM partners, integrators, and PixellEnergy field engineers handling a joint-test (JT-N) event. Covers the VendorQuirkProfile field reference, the hot-reloadable profile registry, the seed catalog of 9 vendors, the JT-N validation pattern, the OEM Compatibility Program + badge onboarding path, and the under-2-hour hot-patch workflow.

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 Accepted results.
  • 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.

VendorQuirkProfile override fields
FieldTypeRequiredNotes
bootNotificationModelOverridestringoptionalOverride the chargePointModel reported by the device. Used when a vendor ships a marketing name vs the OCPP model string. Default: undefined.
heartbeatIntervalSecondsOverridenumberoptionalHeartbeat interval (seconds) returned in BootNotificationConf when the spec-default 300 s does not match the firmware. Default: 300.
dedupOnTransactionIdInsteadOfMessageIdbooleanoptionalWhen TRUE, the gateway dedups on (action, transaction_id) instead of messageId — for vendors that retry frames with different messageIds. Default: false.
authCacheTtlSecondsOverridenumberoptionalShorten the cacheExpiryDateTime returned to chargers that aggressively cache Accepted Authorize results locally. Default: 300.
tolerateStopWithoutStartbooleanoptionalWhen TRUE, StopTransaction without a confirmed StartTransaction is upserted (boot-during-session race tolerance). Default: false.
demoteSignedMeterValueToAdvisorybooleanoptionalWhen TRUE, SignedMeterValue from this vendor is treated as advisory regardless of OCPP version (firmware with nonsense bytes). Default: false.
outboundCallTimeoutSecondsOverridenumberoptionalOverride the default 30 s timeout for outbound OCPP CALL frames. Cellular sites typically widen to 45-60 s. Default: 30.
subprotocolPreferencearrayoptionalPreferred 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'.
statusNotificationServerTimeFallbackbooleanoptionalWhen TRUE, the StatusNotification handler uses server-clock as a fallback when the inbound timestamp is absent (1.6J §6.21). Default: true.
meterValuesCadenceSecondsExpectednumberoptionalExpected interval between periodic MeterValues frames (seconds). Drives webhook-throttle policy pre-allocation. Default: 60.
signedMeterValueSupportenumoptionalDeclares 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'.
transactionIdMonotonicStrictnessenumoptionalRecords observed transactionId behaviour for forensics. The server-side counter (migration 0010) is always strict-monotonic regardless. Default: 'strict'. Permitted: 'strict', 'tolerant'.
diagnosticsCallTimeoutSecondsOverridenumberoptionalOverride the GetLog / GetDiagnostics outbound CALL timeout (seconds) for vendors uploading large compressed tarballs over slow cellular. Default: 30.
diagnosticsTimestampMillisecondTruncatebooleanoptionalWhen TRUE, diagnostics handlers truncate fractional-ms to 3 digits before persistence (`.999999Z` → `.999Z`). Default: false.
notifyEventSeverityMappingOverrideenumoptionalOverride 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'.
r2UploadStyleenumoptionalCloudflare R2 upload preference for diagnostics tarballs. Multipart for firmware that chunks large logs (> 5 MiB). Default: 'single-put'. Permitted: 'single-put', 'multipart'.
smartChargingCallTimeoutSecondsOverridenumberoptionalOverride the SetChargingProfile / ClearChargingProfile / GetCompositeSchedule outbound CALL timeout (seconds). Default: 30.
smartChargingAmpereLimitIntegerFloorbooleanoptionalWhen TRUE, the SetChargingProfile handler floors fractional Ampere limits to integer before wire dispatch. Default: false.
smartChargingCompositeFanOutWholeStationbooleanoptionalWhen TRUE, GetCompositeSchedule(evseId=0) is rewritten as a fan-out across each provisioned EVSE (2.0.1 misinterpretation). Default: false.
smartChargingSupportedRecurrencyKindsarrayoptionalPre-validate the recurrencyKind against this allowlist; throws UnsupportedRecurrencyKindError BEFORE wire dispatch. Default: ['Daily', 'Weekly']. Permitted: 'Daily', 'Weekly'.
bootBeforeInstallStatusbooleanoptionalWhen TRUE, BootNotification.req arrives BEFORE FirmwareStatusNotification.status=Installed (Delta + Servotech AC22 v1.x quirk). Default: false.
protocolFloorenumoptionalLowest 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'.
ocpiPeerCallTimeoutSecondsOverridenumberoptionalOverride 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:

  1. 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).
  2. Publishes a NATS event on csms.vendor_quirk.profile_updated.<vendor_id> so all peer API pods invalidate their resolver caches.
  3. Invalidates the local pod cache synchronously ahead of the NATS broadcast (defence-in-depth for the propagation gap).
  4. Emits an audit_log row tagged vendor_quirk.profile_updated (hash-anchored daily).
POST /v1/admin/vendor-quirks/{vendor_id}/profile — register a global profile
Shell
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"
  }'
201 Created response
JSON
{
  "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.

POST tenant-scoped override (tenant_admin OR super_admin)
Shell
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>"
  }'
NATS fan-out subject for cache invalidation
# 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.ABB

5.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):

  • ExicomExicom Tele-Systems (AC + DC Harmony Gen 2)
  • TataPower-EZChargeTata Power EZ Charge (public-charging franchise · rebadged)
  • Mass-TechMass-Tech / Massive Charging (Pune · AC + DC)
  • OKAYAOKAYA EV (residential + commercial · Faridabad)
  • ZenergizeZenergize (PixellEnergy Customer 0 · JT-N pilot vendor)

International vendors (4):

  • ABBABB Terra AC / Terra DC
  • DeltaDelta Electronics (AC Mini / DC Wallbox)
  • Schneider-ElectricSchneider Electric EVlink
  • WallboxWallbox 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:

JT-N exit criteria mapped to profile-narrowing scope
FieldTypeRequiredNotes
JT-1staging dry-runrequiredResolve the stub profile · verify WSS upgrade picks it up · observe MeterValues cadence + StatusNotification timestamp presence · narrow the cadence + server-clock-fallback fields.
JT-2session lifecyclerequiredRun RemoteStart / Authorize / TransactionEvent / StopTransaction · observe SignedMeterValue payload + transactionId monotonicity · narrow the signedMeterValueSupport + transactionIdMonotonicStrictness fields.
JT-3resiliencerequiredLoss-of-network mid-session · firmware-update reboot · CSMS restart · narrow the tolerateStopWithoutStart + bootBeforeInstallStatus fields.
JT-4production cutoverrequiredProduction 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:

  1. 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.
  2. 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/
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
  1. 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 are pytest.skip at v1; live execution arrives once the OCPP conformance runner is fully wired.
conformance/mobilityhouse-tests/test_vendor_quirk_profiles.py
Shell
# 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")
  1. 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:

  1. 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.
  2. Admin route hot-loads the new profile. The agent calls POST /v1/admin/vendor-quirks/<vendor_id>/profile with the narrowed body. The new row lands in vendor_quirk_profiles immediately.
  3. 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).
  4. 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.
  5. 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_append row tagged vendor_quirk.profile_updated with the full profile_jsonb body, 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.updated on the tenant webhook surface — HMAC-SHA256 signed; 7d uniqueness on event.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).

Rollback to a prior known-good profile
Shell
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.