This is a development version of the documentation. Content may change without notice.
Voke Documentation
ESM Partners

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-keysConnect 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 canonical CanonicalTelemetry shape.
  • 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}:

QueueDirectionPurpose
vcp.{orgSlug}.commandInbound (partner → Voke)Dispatched commands: setpoints, mode changes, schedules
vcp.{orgSlug}.configInbound (partner → Voke)Config update requests
vcp.{orgSlug}.scheduleInbound (partner → Voke)Scheduled dispatch plans
vcp.{orgSlug}.event.telemetryOutbound (Voke → partner)Real-time telemetry and 1-minute meter readings
vcp.{orgSlug}.event.statusOutbound (Voke → partner)Command ACK/NACK and mode change events
vcp.{orgSlug}.event.alarmOutbound (Voke → partner)PLC-originated alarms forwarded upstream
vcp.{orgSlug}.event.executionOutbound (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

Loading diagram...

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

On this page