Alarms
Consume PLC-originated alarm events forwarded by Voke on the event.alarm queue.
Overview
Alarms are PLC-originated device events — they are raised by the plant's controller and forwarded upstream by Voke. This is distinct from Voke-generated alerts (rule-based notifications triggered by Voke's alerting engine). See Concepts / Commands and alerts for the alert/alarm distinction.
When a plant publishes an alarm on the MQTT topic cpi/{plantId}/alarm, Voke's PlcAlarmListener validates the message (HMAC if applicable), normalises it, and VcpAmqpService.publishAlarm forwards it to the partner on vcp.{slug}.event.alarm (routing key {slug}.event.alarm).
Alarm payload
interface AlarmPayload {
alarmId: string; // Unique alarm instance ID (partner-facing)
severity: VcpAlarmSeverity; // Severity classification
code: VcpAlarmCode; // Structured alarm type
message: string; // Human-readable description
deviceId?: string; // Sub-device externalId (when alarm is device-scoped)
raisedAt: string; // ISO 8601 UTC — when the alarm was raised
clearedAt?: string; // ISO 8601 UTC — set when the alarm clears
actualValue?: string | number;
expectedValue?: string | number;
breachedField?: string;
tolerancePercent?: number;
metadata?: Record<string, unknown>; // Arbitrary key–value context
}| Field | Required | Description |
|---|---|---|
alarmId | Yes | Unique identifier for this alarm instance. Use it as the deduplication key alongside messageId. |
severity | Yes | Priority classification — see severity levels below. |
code | Yes | Structured alarm type — see alarm codes below. |
message | Yes | Human-readable description of the condition. |
deviceId | No | Sub-device externalId (e.g. B1, S2) when the alarm is scoped to a specific asset. Omitted for site-level alarms. |
raisedAt | Yes | ISO 8601 UTC timestamp when the alarm condition was first detected by the PLC. |
clearedAt | No | ISO 8601 UTC timestamp when the alarm cleared. If present, the alarm condition has resolved. |
actualValue | No | Actual measured value for machine-readable alarms. |
expectedValue | No | Expected target or threshold value. |
breachedField | No | Constraint or field that was breached. |
tolerancePercent | No | Tolerance used when evaluating a sustained deviation. |
metadata | No | Arbitrary structured context from the PLC (measurement values, threshold references, etc.). |
Severity levels
enum VcpAlarmSeverity {
P1_CRITICAL = 'P1_CRITICAL', // Immediate action required — significant operational impact
P2_MAJOR = 'P2_MAJOR', // Action required soon — degraded operation
P3_MINOR = 'P3_MINOR', // Informational — no immediate impact
P4_INFO = 'P4_INFO', // Diagnostic / informational only
}| Value | Description |
|---|---|
P1_CRITICAL | Immediate operator response required. The plant is unable to fulfil dispatch. |
P2_MAJOR | Degraded operation. The plant is partially functional; response required within the operating window. |
P3_MINOR | No immediate operational impact. Schedule investigation. |
P4_INFO | Informational event. No action required. |
Alarm codes
enum VcpAlarmCode {
COMM_LOSS = 'COMM_LOSS',
COMM_DEGRADED = 'COMM_DEGRADED',
BESS_SOC_LOW = 'BESS_SOC_LOW',
BESS_SOC_HIGH = 'BESS_SOC_HIGH',
BESS_TEMP_HIGH = 'BESS_TEMP_HIGH',
BESS_FAULT = 'BESS_FAULT',
FVE_CURTAILED = 'FVE_CURTAILED',
FVE_INVERTER_FAULT = 'FVE_INVERTER_FAULT',
GRID_EXPORT_LIMIT_REACHED = 'GRID_EXPORT_LIMIT_REACHED',
GRID_IMPORT_LIMIT_REACHED = 'GRID_IMPORT_LIMIT_REACHED',
SETPOINT_DEVIATION = 'SETPOINT_DEVIATION',
SETPOINT_UNACHIEVABLE = 'SETPOINT_UNACHIEVABLE',
FALLBACK_ACTIVATED = 'FALLBACK_ACTIVATED',
OPERATING_MODE_NOT_HONORED = 'OPERATING_MODE_NOT_HONORED',
SCHEDULE_SLOT_MISSED = 'SCHEDULE_SLOT_MISSED',
CONSTRAINT_VIOLATION = 'CONSTRAINT_VIOLATION',
}| Code | Description |
|---|---|
COMM_LOSS | Complete loss of communication with the device or controller. |
COMM_DEGRADED | Intermittent or degraded communication — some data may be delayed or missing. |
BESS_SOC_LOW | Battery state of charge below the configured minimum threshold. |
BESS_SOC_HIGH | Battery state of charge above the configured maximum threshold. |
BESS_TEMP_HIGH | Battery temperature exceeds safe operating limits. |
BESS_FAULT | Battery management system (BMS) reported a hardware or protection fault. |
FVE_CURTAILED | FVE / solar output has been curtailed by the plant controller. |
FVE_INVERTER_FAULT | FVE inverter reported a fault condition. |
GRID_EXPORT_LIMIT_REACHED | Site is at its maximum grid export limit. |
GRID_IMPORT_LIMIT_REACHED | Site is at its maximum grid import limit. |
SETPOINT_DEVIATION | The plant is operating more than the allowed tolerance from its dispatch setpoint. |
SETPOINT_UNACHIEVABLE | The plant cannot reach the commanded setpoint given current hardware state. |
FALLBACK_ACTIVATED | The fallback operating mode has been activated (ESM connection timeout or plan expiry). |
OPERATING_MODE_NOT_HONORED | Reported mode differs from expected mode for a sustained duration. |
SCHEDULE_SLOT_MISSED | Grid power drifts outside the active schedule slot tolerance for a sustained duration. |
CONSTRAINT_VIOLATION | Voke rejected a setpoint because it breached site constraints. |
Plant-to-partner flow
PLC Voke Partner
| | |
|--MQTT alarm-----> | |
| cpi/{id}/alarm | |
| |--validate HMAC + schema----> |
| |--emit plc-alarm.verified |
| | |
| |--VcpAmqpService.publishAlarm |
| | routing key: {slug}.event.alarm
| |---> vcp.{slug}.event.alarm ---> (your consumer)In more detail:
- The plant controller publishes an alarm message to
cpi/{plantId}/alarmover MQTT. PlcAlarmListenerfetches the plant record and checkslifecycleStatus. Alarms fromSUSPENDEDorDECOMMISSIONEDplants are silently dropped.- If the plant uses HMAC authentication, the listener verifies the signature and checks the timestamp and nonce for replay attacks. Invalid or replayed messages are dropped and logged in the audit trail.
- The parsed alarm is emitted as
plc-alarm.verifiedon the internal event bus. - A downstream handler wraps the alarm in a
VcpMessageenvelope (Voke-assignedmessageId,source: 'voke',siteId = plant.externalPlantId) and publishes to{slug}.event.alarmon thevcpexchange. - RabbitMQ routes the message to
vcp.{slug}.event.alarm.
Acknowledgement model
There is no VCP alarm acknowledgement message today. Alarms delivered to partners are informational. If your system acknowledges an alarm internally, do not publish anything back to Voke — there is no AlarmAckPayload or inbound alarm-ack routing key in the current VCP specification.
Alarm acknowledgement may be added in a future VCP version. Until then, consumers should treat vcp.{slug}.event.alarm as a one-way outbound stream and never publish back to it.
Deduplication
- Use the outer VCP envelope's
messageIdas the primary deduplication key. Voke assigns a new UUIDv4 for every published alarm. - On consumer reconnect, RabbitMQ may redeliver messages that were unacknowledged before the channel dropped. Always ack after processing and keep a short deduplication window (e.g. an in-memory set with TTL, or a bloom filter keyed on
messageId). - Alarms with the same
codefor the samedeviceIdwithin a short window are typically deduplicated at the PLC level before reaching Voke — alarm storm suppression is the controller's responsibility, not Voke's. Your consumer should be tolerant of receiving repeat codes for the same asset.
Example consumer
import { connectVoke, type VokeAmqpCreds } from './examples/esm/amqp-connect';
interface AlarmPayload {
alarmId: string;
severity: string;
code: string;
message: string;
deviceId?: string;
raisedAt: string;
clearedAt?: string;
metadata?: Record<string, unknown>;
}
async function startAlarmConsumer(creds: VokeAmqpCreds) {
const { ch, outbound } = await connectVoke(creds);
const seen = new Set<string>();
await ch.consume(outbound.alarm, (msg) => {
if (!msg) return;
try {
const envelope = JSON.parse(msg.content.toString()) as {
messageId: string;
siteId: string;
payload: AlarmPayload;
};
if (seen.has(envelope.messageId)) {
ch.ack(msg);
return;
}
seen.add(envelope.messageId);
const { payload } = envelope;
const cleared = payload.clearedAt ? ` (cleared at ${payload.clearedAt})` : '';
console.log(
`[${envelope.siteId}] ALARM ${payload.severity} ${payload.code}` +
` — ${payload.message}${cleared}`,
);
ch.ack(msg);
} catch {
ch.nack(msg, false, false);
}
});
console.log(`Consuming alarms from ${outbound.alarm}`);
}Related pages
- Concepts / Commands and alerts — alert vs alarm distinction; Voke rule-based alerts are separate from PLC-originated alarms
- VCP data model —
AlarmPayloadfield reference and enum listings - Per-org AMQP queues — queue layout and routing key conventions