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

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 UTCwhen the alarm was raised
  clearedAt?: string;          // ISO 8601 UTCset when the alarm clears
  actualValue?: string | number;
  expectedValue?: string | number;
  breachedField?: string;
  tolerancePercent?: number;
  metadata?: Record<string, unknown>; // Arbitrary keyvalue context
}
FieldRequiredDescription
alarmIdYesUnique identifier for this alarm instance. Use it as the deduplication key alongside messageId.
severityYesPriority classification — see severity levels below.
codeYesStructured alarm type — see alarm codes below.
messageYesHuman-readable description of the condition.
deviceIdNoSub-device externalId (e.g. B1, S2) when the alarm is scoped to a specific asset. Omitted for site-level alarms.
raisedAtYesISO 8601 UTC timestamp when the alarm condition was first detected by the PLC.
clearedAtNoISO 8601 UTC timestamp when the alarm cleared. If present, the alarm condition has resolved.
actualValueNoActual measured value for machine-readable alarms.
expectedValueNoExpected target or threshold value.
breachedFieldNoConstraint or field that was breached.
tolerancePercentNoTolerance used when evaluating a sustained deviation.
metadataNoArbitrary 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
}
ValueDescription
P1_CRITICALImmediate operator response required. The plant is unable to fulfil dispatch.
P2_MAJORDegraded operation. The plant is partially functional; response required within the operating window.
P3_MINORNo immediate operational impact. Schedule investigation.
P4_INFOInformational 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',
}
CodeDescription
COMM_LOSSComplete loss of communication with the device or controller.
COMM_DEGRADEDIntermittent or degraded communication — some data may be delayed or missing.
BESS_SOC_LOWBattery state of charge below the configured minimum threshold.
BESS_SOC_HIGHBattery state of charge above the configured maximum threshold.
BESS_TEMP_HIGHBattery temperature exceeds safe operating limits.
BESS_FAULTBattery management system (BMS) reported a hardware or protection fault.
FVE_CURTAILEDFVE / solar output has been curtailed by the plant controller.
FVE_INVERTER_FAULTFVE inverter reported a fault condition.
GRID_EXPORT_LIMIT_REACHEDSite is at its maximum grid export limit.
GRID_IMPORT_LIMIT_REACHEDSite is at its maximum grid import limit.
SETPOINT_DEVIATIONThe plant is operating more than the allowed tolerance from its dispatch setpoint.
SETPOINT_UNACHIEVABLEThe plant cannot reach the commanded setpoint given current hardware state.
FALLBACK_ACTIVATEDThe fallback operating mode has been activated (ESM connection timeout or plan expiry).
OPERATING_MODE_NOT_HONOREDReported mode differs from expected mode for a sustained duration.
SCHEDULE_SLOT_MISSEDGrid power drifts outside the active schedule slot tolerance for a sustained duration.
CONSTRAINT_VIOLATIONVoke 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:

  1. The plant controller publishes an alarm message to cpi/{plantId}/alarm over MQTT.
  2. PlcAlarmListener fetches the plant record and checks lifecycleStatus. Alarms from SUSPENDED or DECOMMISSIONED plants are silently dropped.
  3. 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.
  4. The parsed alarm is emitted as plc-alarm.verified on the internal event bus.
  5. A downstream handler wraps the alarm in a VcpMessage envelope (Voke-assigned messageId, source: 'voke', siteId = plant.externalPlantId) and publishes to {slug}.event.alarm on the vcp exchange.
  6. 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 messageId as 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 code for the same deviceId within 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}`);
}

On this page