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

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

  1. Receive — the API receives a POST /api/v1/organizations/:orgId/plants/:plantId/commands request containing a SendCommandDto.
  2. ValidateCommandsService.send looks up the plant, optionally resolves the target sub-device, and checks that the action is declared in the sub-device's template actions[] (see target validation below).
  3. Build payload — a cmdId (UUID) and millisecond timestamp ts are generated. If the plant has an HMAC secret, a sig field is appended (see Device security).
  4. Publish — the message is published to cpi/{plantId}/commands over MQTT.
  5. Log — a command_logs row is saved with sentAt timestamped to now; responseStatus and ackedAt are null until the ACK arrives.
  6. ACK — the plant executes the command and publishes an ACK on cpi/{plantId}/commands/ack.
  7. Update — Voke receives the ACK on mqtt.command-ack, verifies the HMAC signature and nonce (if applicable), finds the matching command_logs row by cmdId, and sets responseStatus and ackedAt.
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>"
}
FieldTypeRequiredDescription
cmdIdUUID stringAlwaysUnique command identifier; echoed back in the ACK to correlate the response
tsUnix msAlwaysIssue timestamp; the plant must validate it is within the replay-protection window
actionstringAlwaysCommand name (e.g. reboot, open_contactor)
payloadobjectOptionalArbitrary command parameters; max nesting depth 10
targetstringOptionalSub-device externalId (R1, M1, …); omitted for plant-level commands
sigstringIf secret setHMAC-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:

  1. Voke resolves the sub-device by (orgId, plantId, externalId).
  2. If the sub-device has a linked DeviceTemplate, Voke checks that dto.command matches one of the template's actions[].key values.
  3. 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.

ColumnTypeNotes
idUUIDPrimary key
plantIdUUIDFK to plants (CASCADE delete)
subDeviceIdUUID | nullFK to sub_devices; null for plant-level commands
commandstringAction name
payloadjsonb | nullCommand parameters
cmdIdstring | nullUUID echoed in the ACK
responseStatusstring | nullStatus string from the ACK (e.g. ok, error)
ackedAttimestamptz | nullWhen the ACK was received and verified
sentAttimestamptzWhen the command was published (default NOW())
createdAttimestamptzRow creation timestamp

Failure modes

ScenarioBehaviour
Unknown action in template400 COMMAND_ACTION_NOT_IN_TEMPLATE — command rejected before publish
Invalid payload params400 COMMAND_PARAMS_INVALID — rejected by DTO validation
Plant not in org404 Plant not found
ACK not receivedresponseStatus and ackedAt remain null indefinitely; visible in command-log queries
Plant offline at publish timeMQTT 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 ACKACK 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:

FieldTypeDescription
namestringHuman-readable rule name
metricstringTelemetry metric key to watch (e.g. soc, grid_voltage)
conditionAlertRuleConditionLT (less than), GT (greater than), or EQ (equal to)
thresholdnumberNumeric threshold value
severityAlertSeveritySeverity to assign when the rule fires
plantIdUUID | nullRestrict to a specific plant; null means all plants in the org
cooldownMinutesinteger (1–1440)How long to suppress re-firing after the rule triggers; default 15
enabledbooleanRules 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:

TermOriginTransportWho creates it
AlertVoke API rule engineREST + AMQPVoke — on threshold breach during telemetry ingest
AlarmPLC / field controllerMQTT cpi/{plantId}/alarm topicThe 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
  • SignalsalertOn binary signal flag that triggers alerts on bit-state changes

On this page