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 UTC — expiry; omit for indefinite
}| Field | Required | Description |
|---|---|---|
type | Yes | POWER dispatches a power-tracking setpoint; ENERGY dispatches an energy-quantum setpoint. |
targetValueKw | Conditional | Target in kW. Required when type = POWER. |
targetValueKwh | Conditional | Target in kWh. Required when type = ENERGY. |
intervalMinutes | Conditional | Required when type = ENERGY; optional for POWER. |
direction | Yes | Whether the target applies to IMPORT or EXPORT. |
includeConsumption | Yes | Whether local consumption is included in site-level target calculation. |
priority | Yes | NORMAL follows the schedule stack; HIGH pre-empts lower-priority plans; EMERGENCY bypasses scheduling logic entirely. |
validFrom | Yes | Earliest time Voke may activate the setpoint. |
validUntil | No | Setpoint 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 validates each command against the device template's actions[] list before forwarding to the PLC.
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
}| Field | Required | Description |
|---|---|---|
type | Yes | STOP commands all assets to deactivate. HOLD freezes them at their current output level. |
reason | No | Logged 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',
}| Field | Required | Description |
|---|---|---|
mode | Yes | Target control strategy for the site. |
reason | No | Logged for auditing. |
validUntil | No | Optional 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": "cpi-energo",
"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: 'PLANT-42', // plant's externalPlantId
source: 'cpi-energo', // 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 type | Routing key suffix |
|---|---|
| Site setpoint | command.site-setpoint |
| Device command | command.device |
| Emergency | command.emergency |
| Operating mode | command.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:
-
Envelope validation. Voke parses the raw message and validates the
VcpMessageEnvelopeSchema. If the envelope is malformed (e.g. missingversion, unparseable JSON), the message is nacked withrequeue = falseand dead-lettered. No ACK is published; the partner should deduplicate onmessageIdand not resend automatically. -
Payload validation. Voke validates the
payloadagainst the command-type schema (e.g.SiteSetpointPayloadSchema). On failure, Voke publishes aREJECTEDACK withrejectionCode: 'INVALID_PAYLOAD'tovcp.{slug}.event.statusand the command is logged. -
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.
- 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. Before forwarding to MQTT, Voke:
- Looks up the sub-device by
externalIdon the target plant. - Verifies the
commandvalue is listed in the matched device template'sactions[]. - If either check fails, publishes a
REJECTEDACK.
Validation failures use these rejection codes:
enum RejectionCode {
CONSTRAINT_VIOLATION = 'CONSTRAINT_VIOLATION', // setpoint exceeds site constraints
INVALID_COMMAND = 'INVALID_COMMAND', // command not in template actions
INVALID_PAYLOAD = 'INVALID_PAYLOAD', // envelope / schema parse failure
DEVICE_OFFLINE = 'DEVICE_OFFLINE', // sub-device not reachable
DEVICE_FAULT = 'DEVICE_FAULT', // sub-device in fault state
UNSUPPORTED_FOR_TOPOLOGY = 'UNSUPPORTED_FOR_TOPOLOGY',// command unsupported at this site
}Failure modes
| Scenario | What Voke does | What the partner sees |
|---|---|---|
| Malformed envelope | nack, no requeue, no ACK published | Silence — dedupe on messageId, do not auto-retry |
| Payload schema invalid | Publishes REJECTED with INVALID_PAYLOAD to event.status | ACK on event.status with status: 'REJECTED' |
deviceId not found on plant | Publishes REJECTED with INVALID_COMMAND | ACK on event.status |
Command not in device template actions[] | Publishes REJECTED with INVALID_COMMAND | ACK on event.status |
| Plant offline at dispatch time | Command logged as pending in CommandLog | Execution status fires later on event.execution when plant reconnects and confirms |
| Plant returns fault on MQTT | Publishes FAILED to event.execution | Status on event.execution with status: 'FAILED' |
Related pages
- VCP data model — full schema reference for all command and event payloads
- VCP message integrity — envelope signing, ordering, and replay protection
- Telemetry ingress — observing the post-command effect on the plant
- Concepts / Commands and alerts — internal command lifecycle and alert distinction