ESM Partners — Overview
Cloud-to-cloud architecture, adapter pattern, per-org AMQP queues, and the HTTP reverse channel.
An ESM (energy system management) partner is an external platform that dispatches optimisation commands to energy assets — solar inverters, battery systems, or EV chargers — via Voke. Current known partners are Obzor and CPI Energo; the integration surface is deliberately open so operators can wire any custom ESM without forking Voke core. The ESM never touches Mosquitto or MQTT directly: it communicates with Voke exclusively over AMQP and a lightweight HTTP reverse channel, and Voke does the last-mile delivery to field devices over MQTT.
Where to start as a partner. If your org admin has provisioned an API key for you, the bundle
they handed over (AMQPS URI + raw key + signing key) is everything you need — jump to the
Quick start. If they haven't, ask them to walk through
/orgs/<orgId>/settings/connections?tab=api-keys → Connect partner.
All partner-specific translation is isolated in an adapter class. Voke core remains agnostic to any partner's wire format; adding a new partner means adding one adapter class and nothing else. The rest of this document explains the structure in detail.
Adapter pattern
Every ESM partner requires exactly one adapter class, located under apps/api/src/modules/trading/adapters/. An adapter implements a shared interface with two responsibilities:
normalizeTelemetry(raw)— maps raw device telemetry into Voke's canonicalCanonicalTelemetryshape.formatTelemetry(canonical)— formats the canonical form into the partner's expected outbound payload before publishing to AMQP.
Inbound commands (partner → Voke) are validated by VcpCommandListener using Zod schemas from @cpi/shared — the adapter does not need to validate them. The AdapterRegistry service maintains one adapter instance per organisation at runtime, keyed by orgId.
The adapter is the only partner-specific code inside Voke. Routing, queue lifecycle, signing, and retries are handled generically by VcpAmqpService, VcpTelemetryForwarder, and VcpCommandListener.
Per-org AMQP queues
Each organisation that has trading enabled gets its own set of queues on the shared vcp topic exchange in RabbitMQ. All queue names are prefixed with vcp.{orgSlug}:
| Queue | Direction | Purpose |
|---|---|---|
vcp.{orgSlug}.command | Inbound (partner → Voke) | Dispatched commands: setpoints, mode changes, schedules |
vcp.{orgSlug}.config | Inbound (partner → Voke) | Config update requests |
vcp.{orgSlug}.schedule | Inbound (partner → Voke) | Scheduled dispatch plans |
vcp.{orgSlug}.event.telemetry | Outbound (Voke → partner) | Real-time telemetry and 1-minute meter readings |
vcp.{orgSlug}.event.status | Outbound (Voke → partner) | Command ACK/NACK and mode change events |
vcp.{orgSlug}.event.alarm | Outbound (Voke → partner) | PLC-originated alarms forwarded upstream |
vcp.{orgSlug}.event.execution | Outbound (Voke → partner) | Execution status updates for dispatched commands |
Queue lifecycle is dynamic: queues are asserted (created if absent) when trading or Voke ESM is enabled for an org, and torn down when trading is disabled. On API startup, VcpAmqpService re-asserts queues for all orgs that have status = ENABLED in trading_partner_configs, so queues survive API restarts without any operator action.
The partner authenticates to RabbitMQ using a Voke API key with the vcp:connect scope. The AMQP username is the organization slug and the password is the API key secret. If the key response includes a non-null vhost, the partner connects to that per-key vhost; otherwise it uses / for legacy/default mode. See API keys & auth for key creation and scope assignment.
HTTP reverse channel
In addition to the AMQP queues, Voke periodically calls the partner's ESM HTTP API to fetch configuration. This is a reverse channel: Voke is the HTTP client, and the ESM platform is the server.
The primary use case today is time-config block retrieval — Voke fetches optimisation schedule blocks (e.g. allowable battery charge/discharge windows) from the partner before executing dispatch commands. Voke uses a client-credentials OAuth2 flow to obtain a bearer token from the partner's token endpoint, then calls the partner's plant-config and schedule endpoints.
Per-org ESM HTTP credentials (esmApiUrl, esmClientId, esmClientSecret) are stored in the trading_partner_configs table. The secret is encrypted at rest using AES-256-GCM via CredentialEncryptionService. Operators configure these via the Settings → Trading page in the admin UI (TradingSettingsCard), or directly via the PUT /api/v1/trading/config endpoint.
EsmClientService handles token acquisition and renewal; TradingSyncService schedules periodic syncs. Neither of these services is partner-specific — they use the configured URL and credentials for whichever real or Voke ESM endpoint is registered for the org.
Full data flow
The adapter sits at the boundary between the partner's native format and the VCP wire format. Voke core never speaks the partner's native language.
Next steps
- Quick start (Voke ESM) — end-to-end walkthrough in 15 minutes using the built-in sandbox.
- Per-org AMQP queues — queue semantics, routing keys, and durability guarantees.
- Adapter pattern — how to implement a custom adapter for a new partner.