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

Commands

Publish VCP commands to Voke plants and track ACK + execution lifecycle via AMQP.

Overview

ESM partners send commands by publishing a VCP envelope to the vcp exchange with a routing key of the form {slug}.command.{subtype}. Voke's per-org consumer picks messages up from vcp.{slug}.command, validates the envelope and payload via VcpCommandListener, and dispatches the resulting action to the target plant over MQTT. Voke then publishes the lifecycle back to the partner on two queues: an immediate acknowledgement on vcp.{slug}.event.status (routing key {slug}.event.command.ack) and an execution status update on vcp.{slug}.event.execution (routing key {slug}.event.execution) once the plant reports back.


Command types

Site setpoint — {slug}.command.site-setpoint

Adjusts the plant-level power or energy setpoint for a given time window.

interface SiteSetpointPayload {
  type: 'POWER' | 'ENERGY';
  targetValueKw?: number;      // Required when type = POWER
  targetValueKwh?: number;     // Required when type = ENERGY
  intervalMinutes?: number;    // Required when type = ENERGY
  direction: 'IMPORT' | 'EXPORT';
  includeConsumption: boolean;
  priority: 'NORMAL' | 'HIGH' | 'EMERGENCY';  // SchedulePriority
  validFrom: string;          // ISO 8601 UTC — earliest activation time
  validUntil?: string;        // ISO 8601 UTCexpiry; omit for indefinite
}
FieldRequiredDescription
typeYesPOWER dispatches a power-tracking setpoint; ENERGY dispatches an energy-quantum setpoint.
targetValueKwConditionalTarget in kW. Required when type = POWER.
targetValueKwhConditionalTarget in kWh. Required when type = ENERGY.
intervalMinutesConditionalRequired when type = ENERGY; optional for POWER.
directionYesWhether the target applies to IMPORT or EXPORT.
includeConsumptionYesWhether local consumption is included in site-level target calculation.
priorityYesNORMAL follows the schedule stack; HIGH pre-empts lower-priority plans; EMERGENCY bypasses scheduling logic entirely.
validFromYesEarliest time Voke may activate the setpoint.
validUntilNoSetpoint expires at this time even if not explicitly cancelled.

Device command — {slug}.command.device

Targets one or more sub-devices (BESS, FVE, etc.) by operator-assigned externalId. VCP v1.1 uses a batch payload; wrap a single command in a one-item commands array. Voke routes each entry to the matching asset declared in the site topology (see config.topology).

interface BatchDeviceCommandPayload {
  commands: DeviceCommand[]; // 1-32 commands
}

interface DeviceCommand {
  deviceId: string;           // Sub-device externalId (e.g. 'B1', 'S2')
  assetType: AssetType;       // Asset class of the target device
  command: DeviceCommandType; // The action to execute
  params?: DeviceCommandParams;
}

interface DeviceCommandParams {
  powerKw?: number;           // Power level for charge/discharge commands (kW)
  percent?: number;           // Percent reduction for FVE curtailment commands
  respectLimits?: boolean;    // Whether to honour configured SOC and power limits
}

AssetType and DeviceCommandType values:

enum AssetType {
  BESS       = 'BESS',
  FVE        = 'FVE',
  METER      = 'METER',
  HEAT_PUMP  = 'HEAT_PUMP',
  EV_CHARGER = 'EV_CHARGER',
  THERMOSTAT = 'THERMOSTAT',
  INVERTER   = 'INVERTER',
  GENERIC    = 'GENERIC',
}

enum DeviceCommandType {
  FVE_PRODUCE_MAX        = 'FVE_PRODUCE_MAX',
  FVE_REDUCE_PERCENT     = 'FVE_REDUCE_PERCENT',
  FVE_REDUCE_POWER       = 'FVE_REDUCE_POWER',
  FVE_STOP               = 'FVE_STOP',
  BESS_CHARGE            = 'BESS_CHARGE',
  BESS_DISCHARGE         = 'BESS_DISCHARGE',
  BESS_STOP              = 'BESS_STOP',
  BESS_CHARGE_ONLY       = 'BESS_CHARGE_ONLY',
  BESS_DISCHARGE_ONLY    = 'BESS_DISCHARGE_ONLY',
  BESS_CONTINUOUS_CHARGE = 'BESS_CONTINUOUS_CHARGE',
}

deviceId in this payload maps to the sub-device's externalId on the Voke plant — a short, operator-assigned string such as B1 or M2. It does not use the Voke UUID. All other command types address the whole site through the envelope's siteId.


Emergency command — {slug}.command.emergency

Instructs all controllable assets to stop or hold immediately. Priority is highest — bypasses the dispatch schedule.

interface EmergencyCommandPayload {
  type: 'STOP' | 'HOLD';  // STOP shuts assets down; HOLD freezes current output
  reason?: string;          // Human-readable reason (max 500 chars), logged in CommandLog
}
FieldRequiredDescription
typeYesSTOP commands all assets to deactivate. HOLD freezes them at their current output level.
reasonNoLogged to VcpCommandLog for operator visibility.

Operating mode — {slug}.command.mode

Switches the site's control strategy.

interface OperatingModePayload {
  mode: OperatingMode;  // Target operating mode
  reason?: string;       // Human-readable reason (max 500 chars)
  validUntil?: string;    // Optional override expiry
}

enum OperatingMode {
  STANDARD           = 'STANDARD',
  ZERO_EXPORT        = 'ZERO_EXPORT',
  MAX_EXPORT         = 'MAX_EXPORT',
  PEAK_SHAVING       = 'PEAK_SHAVING',
  LOCAL_OPTIMIZATION = 'LOCAL_OPTIMIZATION',
  GRID_TARGET        = 'GRID_TARGET',
  LDS_SUPPORT        = 'LDS_SUPPORT',
}
FieldRequiredDescription
modeYesTarget control strategy for the site.
reasonNoLogged for auditing.
validUntilNoOptional expiry for the override.

Routing and envelope

All four command types share the same VCP envelope structure. The routing key suffix determines which payload schema Voke validates.

{
  "version": "1.1",
  "messageId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "correlationId": "batch-2026-04-19-01",
  "timestamp": "2026-04-19T14:00:00.000Z",
  "source": "my-esm",
  "siteId": "PLANT-42",
  "payload": {
    "type": "POWER",
    "targetValueKw": 50,
    "direction": "EXPORT",
    "includeConsumption": true,
    "priority": "HIGH",
    "validFrom": "2026-04-19T14:00:00.000Z",
    "validUntil": "2026-04-19T14:15:00.000Z"
  }
}

Using the publish-command.ts example helper:

import { publishCommand } from './examples/esm/publish-command';

await publishCommand(creds, {
  commandType: 'site-setpoint',         // routing key suffix
  siteId: 'd290f1ee-6c54-4b01-90e6-d701748f0851', // Voke plant UUID (legacy externalPlantId still accepted)
  source: 'my-esm',                     // your partner identifier
  correlationId: 'batch-2026-04-19-01', // echoed back in event.status
  payload: {
    type: 'POWER',
    targetValueKw: 50,
    direction: 'EXPORT',
    includeConsumption: true,
    priority: 'HIGH',
    validFrom: '2026-04-19T14:00:00.000Z',
    validUntil: '2026-04-19T14:15:00.000Z',
  },
});

The full routing key published to the vcp exchange becomes {orgSlug}.command.site-setpoint. Voke's consumer is bound to {slug}.command.# and will receive all subtypes.

Routing key suffixes by command type:

Command typeRouting key suffix
Site setpointcommand.site-setpoint
Device commandcommand.device
Emergencycommand.emergency
Operating modecommand.mode

command.device, command.mode, and schedule.* envelopes must be HMAC-signed with the partner key's signingKey. Site setpoints are scope-gated by vcp:write:setpoint but do not currently require HMAC. See VCP message integrity.


ACK / status lifecycle

After Voke consumes a command, the following sequence occurs:

  1. Envelope validation. Voke parses the raw message and validates the VcpMessageEnvelopeSchema. If the envelope is malformed (e.g. missing version, unparseable JSON), the message is nacked with requeue = false and dead-lettered. No ACK is published; the partner should deduplicate on messageId and not resend automatically.

  2. Payload validation. Voke validates the payload against the command-type schema (e.g. SiteSetpointPayloadSchema). On failure, Voke publishes a REJECTED ACK with rejectionCode: 'INVALID_PAYLOAD' to vcp.{slug}.event.status and the command is logged.

  3. Immediate ACK on vcp.{slug}.event.status (routing key {slug}.event.command.ack):

interface CommandAckPayload {
  status: 'ACCEPTED' | 'REJECTED' | 'PARTIAL' | 'QUEUED'; // Outcome of command receipt
  commandType: string;                          // Echoes the command type
  message?: string;                             // Human-readable explanation
  rejectionCode?: RejectionCode;                // Structured reason on rejection
  results?: CommandResult[];                    // Required when status = PARTIAL
}

The correlationId from the original envelope is echoed in the outbound VCP envelope wrapping this payload — use it to tie the ACK to your outbound command.

  1. Execution status on vcp.{slug}.event.execution (routing key {slug}.event.execution) once the plant responds over MQTT:
interface CommandStatusPayload {
  commandType: string;
  status: 'EXECUTING' | 'COMPLETED' | 'DEVIATED' | 'FAILED';
  actualValueKw?: number;  // Observed output
  targetValueKw?: number;  // Requested value
  deviationKw?: number;    // Difference between actual and target
  reason?: string;         // Reason for deviation or failure
}

Again, the wrapping envelope carries the original correlationId.


Target sub-device addressing

Device command items require setting deviceId to the sub-device's externalId. Voke resolves each entry against the site's declared topology (config.topology.assets[]) — set the topology once at onboarding and Voke uses it to route every subsequent device-scoped command. If a deviceId doesn't match any declared asset, Voke publishes a REJECTED ACK with rejectionCode: 'DEVICE_OFFLINE'.

Rejection codes:

enum RejectionCode {
  CONSTRAINT_VIOLATION     = 'CONSTRAINT_VIOLATION',   // setpoint exceeds site constraints
  INVALID_COMMAND          = 'INVALID_COMMAND',         // command type unsupported for this asset
  INVALID_PAYLOAD          = 'INVALID_PAYLOAD',         // envelope / schema parse failure
  DEVICE_OFFLINE           = 'DEVICE_OFFLINE',          // deviceId not in declared topology, or asset unreachable
  DEVICE_FAULT             = 'DEVICE_FAULT',            // sub-device in fault state
  UNSUPPORTED_FOR_TOPOLOGY = 'UNSUPPORTED_FOR_TOPOLOGY',// command not valid for this site's topology
}

Failure modes

ScenarioWhat Voke doesWhat the partner sees
Malformed envelopenack with requeue=false, no ACK publishedSilence — dedupe on messageId, do not auto-retry
Payload schema invalidPublishes REJECTED with INVALID_PAYLOAD to event.statusACK on event.status with status: 'REJECTED'
deviceId not in declared config.topology.assets[]Publishes REJECTED with DEVICE_OFFLINEACK on event.status
Setpoint exceeds a configured site constraintPublishes REJECTED with CONSTRAINT_VIOLATION and emits a matching CONSTRAINT_VIOLATION alarmACK on event.status plus an event.alarm envelope
Command type not supported by the resolved assetPublishes REJECTED with INVALID_COMMANDACK on event.status
Site offline / unable to dispatchCommand logged; ACK may delayExecution status fires later on event.execution once dispatch completes
Dispatch fails at the assetPublishes FAILED to event.executionStatus on event.execution with status: 'FAILED'

On this page