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

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

ValueMeaning
1WARNING — degraded operation, not yet critical. Inform operator.
2ERROR — significant fault, operation impacted. Requires attention.
3CRITICAL — 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:

RangeCategory
100–199Inverter faults
200–299Battery faults
300–399Switchboard / circuit breaker faults
400–499Grid faults
500–599Communication 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 | sev
  • tsstr(ts) (13-digit Unix ms as string)
  • codestr(code) (integer as string, e.g. "106")
  • sevstr(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 alarmId per fault onset (e.g. {fault-code}-{timestamp-of-onset} or a UUID generated at first detection).
  • Do not publish the same alarmId multiple times unless it is the RESOLVE counterpart.
  • If your firmware restarts mid-alarm, re-publish the RAISE with a new alarmId — do not attempt to resume the previous one.

See also

On this page