Alarms upstream
How PLC devices raise and resolve alarms on the cpi/{plantId}/alarm topic — envelope shape, RAISE/RESOLVE lifecycle, severity levels, and HMAC signing.
PLC devices report hardware faults and operational events by publishing PlcAlarm messages to cpi/{plantId}/alarm. These are device-originated events — distinct from Voke-generated alerts, which fire from telemetry threshold rules and are managed entirely within Voke.
Voke-generated alerts (email, SMS, push notifications) are configured in the admin UI under Alerts and are triggered by telemetry values crossing thresholds. PLC alarms are raw events from the hardware itself — Voke ingests them and can trigger downstream alert rules based on alarm codes, but the alarms themselves come from the device.
Alarm envelope
// packages/shared/src/types/plc-protocol.ts
type PlcAlarmEvent = 'RAISE' | 'RESOLVE';
type PlcAlarmSeverity = 1 | 2 | 3;
interface PlcAlarm {
ts: number; // Unix milliseconds — alarm event time
n: string; // nonce — 8+ hex chars, unique per message
ev: PlcAlarmEvent; // 'RAISE' to open, 'RESOLVE' to close
alarmId: string; // unique ID for this alarm instance
code: FaultCode; // integer fault code — see FaultCode enum
sev: PlcAlarmSeverity; // severity: 1 = WARNING, 2 = ERROR, 3 = CRITICAL
msg?: string; // optional human-readable description
detail?: Record<string, unknown>; // optional structured context
sig?: string; // HMAC-SHA256 hex (recommended for PASSWORD/JWT devices)
}All power values and field names follow the same compact convention as the telemetry envelope.
Lifecycle: RAISE and RESOLVE
An alarm has a two-event lifecycle:
1. Raise the alarm — publish with ev: 'RAISE' when the fault condition is detected:
{
"ts": 1700000000000,
"n": "a1b2c3d4",
"ev": "RAISE",
"alarmId": "inv-fault-20240115-001",
"code": 106,
"sev": 3,
"msg": "Inverter general fault — DC protection triggered",
"sig": "<hmac-sha256-hex>"
}2. Resolve the alarm — publish with the same alarmId and ev: 'RESOLVE' when the condition clears:
{
"ts": 1700000300000,
"n": "e5f6a7b8",
"ev": "RESOLVE",
"alarmId": "inv-fault-20240115-001",
"code": 106,
"sev": 3,
"sig": "<hmac-sha256-hex>"
}Re-raise — if the same fault condition occurs again after resolving, generate a new alarmId. Do not reuse the previous one. This lets Voke and downstream systems distinguish separate alarm events from a prolonged single event.
Publish alarms on state change, not on a timer. Raising the same alarmId repeatedly (e.g. once per telemetry tick) will create duplicate records and may be rate-limited. One RAISE per fault onset, one RESOLVE per clearance.
Severity levels
| Value | Meaning |
|---|---|
1 | WARNING — degraded operation, not yet critical. Inform operator. |
2 | ERROR — significant fault, operation impacted. Requires attention. |
3 | CRITICAL — severe fault, potential for data loss or equipment damage. Immediate action needed. |
Fault codes (code field)
code is an integer from the FaultCode enum defined in packages/shared/src/types/plc-protocol.ts. Ranges by category:
| Range | Category |
|---|---|
| 100–199 | Inverter faults |
| 200–299 | Battery faults |
| 300–399 | Switchboard / circuit breaker faults |
| 400–499 | Grid faults |
| 500–599 | Communication faults |
Examples: 106 = INV_GENERAL_FAULT, 203 = BAT_COMM_LOST, 310 = SW_EMERGENCY_STOP, 403 = GRID_FREQUENCY.
Signing the alarm
Part order (from computeAlarmSignature in packages/shared/src/utils/hmac.ts):
deviceId | ts | nonce | ev | alarmId | code | sevts→str(ts)(13-digit Unix ms as string)code→str(code)(integer as string, e.g."106")sev→str(sev)(e.g."3")- All parts joined with
|(pipe), no surrounding whitespace
from hmac_sign import sign_alarm
sig = sign_alarm(
device_id=PLANT_ID,
ts=ts,
nonce=nonce,
ev="RAISE",
alarm_id="inv-fault-20240115-001",
code=106,
sev=3,
secret=HMAC_SECRET,
)Full implementation in apps/docs/content/examples/plc/hmac_sign.py — see HMAC signing for the complete reference.
Publishing
import json, secrets, time
import paho.mqtt.client as mqtt
from hmac_sign import sign_alarm
PLANT_ID = "REPLACE-WITH-YOUR-PLANT-ID"
ALARM_TOPIC = f"cpi/{PLANT_ID}/alarm"
HMAC_SECRET = "REPLACE-WITH-YOUR-DEVICE-SECRET"
def publish_alarm(
client: mqtt.Client,
alarm_id: str,
ev: str, # 'RAISE' or 'RESOLVE'
code: int,
sev: int,
msg: str | None = None,
) -> None:
ts = int(time.time() * 1000)
nonce = secrets.token_hex(8)
sig = sign_alarm(PLANT_ID, ts, nonce, ev, alarm_id, code, sev, HMAC_SECRET)
payload: dict = {
"ts": ts,
"n": nonce,
"ev": ev,
"alarmId": alarm_id,
"code": code,
"sev": sev,
"sig": sig,
}
if msg:
payload["msg"] = msg
client.publish(ALARM_TOPIC, json.dumps(payload, separators=(",", ":")), qos=1)Deduplication guidance
Voke stores each alarm message as received. To avoid duplicates in the record:
- Use a stable, unique
alarmIdper fault onset (e.g.{fault-code}-{timestamp-of-onset}or a UUID generated at first detection). - Do not publish the same
alarmIdmultiple times unless it is theRESOLVEcounterpart. - If your firmware restarts mid-alarm, re-publish the
RAISEwith a newalarmId— do not attempt to resume the previous one.
See also
- HMAC signing — canonical part orders, Python helper, and known-value test vectors
- Topics — full MQTT topic reference including
cpi/{plantId}/alarm - Concepts — commands and alerts — distinction between device alarms and Voke-generated alerts
Commands (receipt)
How PLC devices receive signed commands from Voke, verify them, execute, and acknowledge — including failure modes and target routing.
HMAC signing
Complete reference for Voke's HMAC-SHA256 signing scheme — canonical JSON, part orders for all four message types, Python helper, and known-value test vectors.