Commands & alerts
Commands flow from Voke (or an ESM partner) down to a plant over MQTT and return an ACK. Alerts are Voke-generated events produced when telemetry breaks a threshold rule. Alarms are a separate, PLC-originated mechanism.
Commands
Commands travel from Voke (or an ESM dispatch partner) through the Voke API → Mosquitto → plant and return an ACK in the reverse direction. Every command dispatch writes a row to the command_logs table, which records the command name, payload, optional sub-device target, and the eventual ACK status.
Command lifecycle
- Receive — the API receives a
POST /api/v1/organizations/:orgId/plants/:plantId/commandsrequest containing aSendCommandDto. - Validate —
CommandsService.sendlooks up the plant, optionally resolves the target sub-device, and checks that the action is declared in the sub-device's templateactions[](see target validation below). - Build payload — a
cmdId(UUID) and millisecond timestamptsare generated. If the plant has an HMAC secret, asigfield is appended (see Device security). - Publish — the message is published to
cpi/{plantId}/commandsover MQTT. - Log — a
command_logsrow is saved withsentAttimestamped to now;responseStatusandackedAtare null until the ACK arrives. - ACK — the plant executes the command and publishes an ACK on
cpi/{plantId}/commands/ack. - Update — Voke receives the ACK on
mqtt.command-ack, verifies the HMAC signature and nonce (if applicable), finds the matchingcommand_logsrow bycmdId, and setsresponseStatusandackedAt.
ESM partner ──▶ POST /commands ──▶ CommandsService.send
│
MQTT publish (cpi/{plantId}/commands)
│
Plant
│
MQTT publish (cpi/{plantId}/commands/ack)
│
CommandsService.handleCommandAck
│
commandLog.responseStatus = <status>MQTT command payload
The message published to cpi/{plantId}/commands has the following shape:
{
"cmdId": "550e8400-e29b-41d4-a716-446655440000",
"ts": 1713619200000,
"action": "reboot",
"payload": { "delay": 5 },
"target": "M1",
"sig": "<hmac-sha256-hex>"
}| Field | Type | Required | Description |
|---|---|---|---|
cmdId | UUID string | Always | Unique command identifier; echoed back in the ACK to correlate the response |
ts | Unix ms | Always | Issue timestamp; the plant must validate it is within the replay-protection window |
action | string | Always | Command name (e.g. reboot, open_contactor) |
payload | object | Optional | Arbitrary command parameters; max nesting depth 10 |
target | string | Optional | Sub-device externalId (R1, M1, …); omitted for plant-level commands |
sig | string | If secret set | HMAC-SHA256 signature; absent when the plant has no HMAC secret configured |
Target validation
target identifies a sub-device to receive the command using its short externalId (R1, M2, E1, …). When present:
- Voke resolves the sub-device by
(orgId, plantId, externalId). - If the sub-device has a linked
DeviceTemplate, Voke checks thatdto.commandmatches one of the template'sactions[].keyvalues. - A mismatch raises
400 COMMAND_ACTION_NOT_IN_TEMPLATE.
When target is absent the command is plant-level, no action-vocabulary check is performed, and subDeviceId is stored as null in the log row.
CommandLog schema
CommandLog records every dispatched command. It uses TypeORM's BaseEntity but does not extend the project's custom BaseEntity — it therefore has createdAt only and no updatedAt column.
| Column | Type | Notes |
|---|---|---|
id | UUID | Primary key |
plantId | UUID | FK to plants (CASCADE delete) |
subDeviceId | UUID | null | FK to sub_devices; null for plant-level commands |
command | string | Action name |
payload | jsonb | null | Command parameters |
cmdId | string | null | UUID echoed in the ACK |
responseStatus | string | null | Status string from the ACK (e.g. ok, error) |
ackedAt | timestamptz | null | When the ACK was received and verified |
sentAt | timestamptz | When the command was published (default NOW()) |
createdAt | timestamptz | Row creation timestamp |
Failure modes
| Scenario | Behaviour |
|---|---|
| Unknown action in template | 400 COMMAND_ACTION_NOT_IN_TEMPLATE — command rejected before publish |
| Invalid payload params | 400 COMMAND_PARAMS_INVALID — rejected by DTO validation |
| Plant not in org | 404 Plant not found |
| ACK not received | responseStatus and ackedAt remain null indefinitely; visible in command-log queries |
| Plant offline at publish time | MQTT publish still succeeds; the plant receives the message on reconnect (MQTT topics are not retained by default — no guarantee of delivery if the plant stays offline past the broker session) |
| Invalid HMAC on ACK | ACK is dropped, command_logs row stays unacknowledged, and an AUTH_FAILURE audit event is written |
Alerts
Voke's alert engine evaluates incoming telemetry against org-defined rules. When a condition matches, an alert is created and flows through an acknowledge/resolve workflow.
Alert rules
An alert rule monitors a named telemetry metric on one or all plants within an organization:
| Field | Type | Description |
|---|---|---|
name | string | Human-readable rule name |
metric | string | Telemetry metric key to watch (e.g. soc, grid_voltage) |
condition | AlertRuleCondition | LT (less than), GT (greater than), or EQ (equal to) |
threshold | number | Numeric threshold value |
severity | AlertSeverity | Severity to assign when the rule fires |
plantId | UUID | null | Restrict to a specific plant; null means all plants in the org |
cooldownMinutes | integer (1–1440) | How long to suppress re-firing after the rule triggers; default 15 |
enabled | boolean | Rules can be disabled without deletion |
Severity levels
Alert rules and alert records use AlertSeverity from @cpi/shared/types/enums:
export enum AlertSeverity {
INFO = 'INFO',
WARNING = 'WARNING',
CRITICAL = 'CRITICAL',
EMERGENCY = 'EMERGENCY',
}Alert lifecycle
Telemetry ingest
│
Rule evaluation
│ condition matches + cooldown not active
▼
Alert created (status: ACTIVE)
│
├── Operator acknowledges ──▶ status: ACKNOWLEDGED
│ (acknowledgedAt, acknowledgedBy set)
│
└── Operator or rule engine resolves ──▶ status: RESOLVED
(resolvedAt set)AlertStatus values from @cpi/shared/types/enums:
export enum AlertStatus {
ACTIVE = 'ACTIVE',
ACKNOWLEDGED = 'ACKNOWLEDGED',
RESOLVED = 'RESOLVED',
}Active — the alert has fired and no one has acted on it yet. Acknowledged — an operator has seen it (and optionally started remediation); the alert is still open. Resolved — the condition has cleared or an operator marked it resolved manually.
Cooldown
Once an alert rule fires for a given plant, lastTriggeredAt is updated on the rule row. Subsequent evaluations skip rule firing until NOW() > lastTriggeredAt + cooldownMinutes. This prevents alert storms when telemetry is noisy around the threshold boundary. The default cooldown is 15 minutes; the maximum is 1440 minutes (24 hours).
When alerts fire
Alert evaluation happens on telemetry ingestion (not on a polling schedule). Alerts are written to the alerts table and forwarded to the ESM partner's AMQP queue so the dispatch system receives them in near real-time. See ESM / Alarms for the partner-facing wire format.
Alerts vs alarms
These two terms refer to different sources and should not be confused:
| Term | Origin | Transport | Who creates it |
|---|---|---|---|
| Alert | Voke API rule engine | REST + AMQP | Voke — on threshold breach during telemetry ingest |
| Alarm | PLC / field controller | MQTT cpi/{plantId}/alarm topic | The plant — autonomously, without rule evaluation |
Alarms are published directly by the plant's firmware when it detects an internal fault, communication loss, or other device-originated event. They arrive on the cpi/{plantId}/alarm MQTT topic, are parsed and HMAC-verified by PlcAlarmListener, and then forwarded to the partner AMQP queue as VCP alarm messages. They do not go through rule evaluation and are not stored in the alerts table.
Use alert when referring to Voke's rule-driven threshold events. Use alarm when referring to PLC-originated fault events. Full PLC alarm details are in PLC / Alarms upstream.
See also
- ESM / Commands — full protocol details for ESM partners dispatching commands
- ESM / Alarms — how alerts and alarms are forwarded to the partner AMQP queue
- PLC / Commands receipt — how the plant firmware receives and ACKs commands
- PLC / Alarms upstream — the alarm wire format the PLC publishes
- Signals —
alertOnbinary signal flag that triggers alerts on bit-state changes
Signals
Signals are the named telemetry values decoded from device payloads. Two kinds exist — binary (bitmask bits) and numeric (named fields) — with different storage strategies.
Data retention
Raw telemetry is retained for five years in TimescaleDB and backed by three continuous-aggregate tiers for longer-horizon charts.