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

Voke ESM Sandbox

An in-container stub ESM for exercising the full Voke trading flow without external infrastructure.

Voke ESM is a built-in testing sandbox for dev/staging environments without a real partner. Production partners obtain credentials via /orgs/<orgId>/settings/connections — see the Quick start. This page is for super-admins exercising the trading flow against fake plants.

Voke ESM is a loopback stub baked into the Voke API container that implements the real ESM HTTP contract. It lets you exercise Voke's outbound HTTP reverse-channel to a partner's ESM without any external infrastructure — no CPI Energo test environment, no VPN, no shared secrets with a third party. The module lives at apps/api/src/modules/voke-esm/ and exposes endpoints under /api/v1/voke-esm.

Enable flow (superadmin)

  1. Sign in as a SuperAdmin account.
  2. Navigate to /platform/voke-esm in the admin SPA.
  3. Pick an organization from the dropdown and click Enable.
  4. Voke generates ESM OAuth2 client credentials (clientId, clientSecret, esmApiUrl) and writes them encrypted into the org's TradingPartnerConfig.
  5. One default fake plant is auto-created for that org.
  6. The credentials are shown once in the UI dialog. Copy them immediately.

What these credentials do and do not do

  • clientId + clientSecret authorize Voke to call the Voke ESM's HTTP API (the POST /voke-esm/trading/tokenGET /voke-esm/trading/plant/listPOST /voke-esm/trading/plant/config/update flow).
  • They are not AMQP credentials. To receive VCP messages over AMQP, you still need a separately-created API key with the vcp:connect scope. See API keys and auth.
  • If the credentials are lost, disable and re-enable Voke ESM for the org to rotate them.

Fake plant CRUD

Each fake plant is a row in the voke_esm_plants table. The admin endpoints all require SuperAdmin authentication and live at /api/v1/voke-esm/admin/plants.

ActionMethod + Path
List all plantsGET /api/v1/voke-esm/admin/plants
Get a single plantGET /api/v1/voke-esm/admin/plants/:id
Create a plantPOST /api/v1/voke-esm/admin/plants
Update a plantPATCH /api/v1/voke-esm/admin/plants/:id
Delete a plantDELETE /api/v1/voke-esm/admin/plants/:id

Create request body

{
  "clientId": "acme-client-id",
  "externalPlantId": "PLANT-001",
  "name": "Acme Site A",
  "description": "Battery storage + PV",
  "fvePeakPowerKwp": 120.0,
  "capacityKwh": 200.0,
  "configEnabled": true,
  "timeConfigs": [
    { "from": "00:00", "to": "07:00", "pBatMax": 10, "pBatMin": -10, "socMax": 95, "socMin": 20 },
    { "from": "07:00", "to": "23:59", "pBatMax": 50, "pBatMin": -50, "socMax": 90, "socMin": 30 }
  ]
}

clientId must match the clientId returned when you enabled Voke ESM for the org — this is how the Voke ESM scopes plants to a trading partner.

Plant entity shape

{
  id: string;               // UUID
  clientId: string;         // links plant to the org's ESM credentials
  externalPlantId: string;  // unique per clientId; must match plants.external_plant_id on Voke side
  name: string;
  description: string | null;
  fvePeakPowerKwp: number;
  capacityKwh: number;
  configEnabled: boolean;
  plantWithLoad: boolean;   // whether the site has a load measurement
  esiState: string;         // default "PLC_READY"; reflects ESI contract state
  timeConfigs: TimeConfigBlock[];
  createdAt: string;        // ISO 8601
  updatedAt: string;        // ISO 8601
}

Time config block shape

{
  from: string;    // "HH:MM", start of time window (inclusive)
  to: string;      // "HH:MM", end of time window (inclusive)
  pBatMax: number; // maximum battery charge power (kW, positive)
  pBatMin: number; // minimum battery discharge power (kW, negative)
  socMax: number;  // maximum state of charge (%)
  socMin: number;  // minimum state of charge (%)
}

Time config blocks are stored as a JSONB array (time_configs column) and are returned verbatim by GET /voke-esm/trading/plant/config.

Trading endpoints (what Voke calls)

These endpoints implement the ESM HTTP contract that EsmClientService calls. They are @Public() (bypass Voke JWT) because they use their own OAuth2 client-credentials flow, but they are also protected by LoopbackOnlyGuard: requests from non-loopback peer IPs are rejected. They are rate-limited to 10 requests per minute.

The base URL Voke uses is controlled by the VOKE_ESM_INTERNAL_URL env var (default: http://localhost:4410/api/v1/voke-esm).

In production, VOKE_ESM_ENABLED=true must be set when the module is registered, and VOKE_ESM_INTERNAL_URL must not start with http://localhost; use a loopback IP such as http://127.0.0.1:4410/api/v1/voke-esm instead.

POST /api/v1/voke-esm/trading/token

OAuth2 client-credentials token exchange.

POST /api/v1/voke-esm/trading/token
Content-Type: application/json

{ "clientId": "...", "clientSecret": "..." }

Returns:

{
  "accessToken": "<JWT signed with iss: 'voke-esm'>",
  "tokenType": "Bearer",
  "expiresIn": 3600
}

The token is signed with the same JWT_SECRET used by the Voke API.

GET /api/v1/voke-esm/trading/plant/list

Returns all fake plants belonging to the authenticated client.

GET /api/v1/voke-esm/trading/plant/list
Authorization: Bearer <accessToken>

Response includes plantId, name, descr, fvePeakPowerKwp, capacityKwh, esiState, configEnabled, plantWithLoad, and an empty meters array.

GET /api/v1/voke-esm/trading/plant/config

Returns the current time-config schedule for a plant.

GET /api/v1/voke-esm/trading/plant/config?plantId=PLANT-001
Authorization: Bearer <accessToken>

Response:

{
  "plantId": "PLANT-001",
  "version": 1,
  "configEnabled": true,
  "esiState": "PLC_READY",
  "timeConfigs": [...]
}

POST /api/v1/voke-esm/trading/plant/config/update

Push a new time-config schedule to the Voke ESM. Supports validateOnly: true for a dry-run.

{
  "plantId": "PLANT-001",
  "configUpdateReqCode": "req-abc123",
  "validateOnly": false,
  "timeConfigs": [
    { "from": "00:00", "to": "23:59", "pBatMax": 50, "pBatMin": -50, "socMax": 90, "socMin": 20 }
  ]
}

Responds with resultCode: "applied_ok" (or "cfg_accepted" for validate-only).

Linking a Voke plant to a fake plant

Set plants.external_plant_id on a Voke plant to match the fake plant's externalPlantId. This single field is the wire between the real plant and the Voke ESM's trading flow. Once linked:

  • Voke's trading sync calls GET /voke-esm/trading/plant/config?plantId=PLANT-001 and stores the time configs locally.
  • Config updates from Voke are delivered to POST /voke-esm/trading/plant/config/update.

Enable and disable endpoints

ActionMethod + Path
Enable for orgPOST /api/v1/voke-esm/admin/enable?orgId=<uuid>
Disable for orgPOST /api/v1/voke-esm/admin/disable?orgId=<uuid>
Check statusGET /api/v1/voke-esm/admin/status?orgId=<uuid>

Full end-to-end smoke test

  1. Enable Voke ESM for org acme via the /platform/voke-esm admin page.
  2. Note the auto-created fake plant's externalPlantId (visible in the plant list).
  3. Create or update a Voke plant with externalPlantId set to the same value.
  4. Start an AMQP consumer for vcp.acme.event.telemetry following the ESM quick-start.
  5. Trigger a telemetry snapshot from a PLC (local dev) or push a test command via the admin UI.
  6. Observe the event.telemetry message arriving in your AMQP consumer.
  7. Verify Voke's config-update path: trigger a schedule change and confirm POST /voke-esm/trading/plant/config/update is called (check API logs: Voke ESM: applied config for PLANT-001).
  • ESM quick-start — AMQP consumer setup
  • Adapter pattern — what the adapter calls on the partner HTTP side
  • PLC quick-start — generating live telemetry from a controller (see PLC section, coming in a future task)

On this page