Sub-devices
Sub-devices model the physical components — cabinets, meters, inverters, batteries — that live under a plant. They are never MQTT participants; all their telemetry arrives through the plant.
What is a sub-device?
A sub-device is a logical record under a plant that represents a physical field component: a distribution cabinet, an energy meter, a solar inverter, a battery pack, or any other device whose telemetry arrives via the plant's MQTT connection. Sub-devices never hold MQTT credentials and never connect to the broker directly.
Telemetry for all sub-devices in a site arrives in a single snapshot envelope published by the PLC on the plant's telemetry topic. Voke unpacks the envelope, resolves each entry to a sub-device row, applies the appropriate device template to decode signals, and fans out one telemetry row per signal.
externalId
externalId is a short, operator-assigned identifier that the PLC firmware writes into each snapshot entry. It is:
- Unique within a plant — enforced by
sub_devices_plant_external_id_unique (plantId, externalId). Two plants may have sub-devices with the sameexternalId. - Not globally unique across organizations or plants.
- Case-sensitive and uppercase only —
R1andr1are distinct values. The Zod schema enforces^[A-Z0-9_-]+$(1–32 characters).
Conventional naming used in the field:
| Component | Convention | Examples |
|---|---|---|
| Cabinet | R + number | R1, R2, R3 |
| Meter | E + number | E1, E2 |
| Inverter | project-specific | INV1, PV1, M1 |
| Battery | project-specific | BAT1 |
The PLC firmware dictates these values. Voke treats them as opaque identifiers — there is no validation of the naming convention beyond the character-set and length constraints.
type
type is a typed enum (SubDeviceType from @cpi/shared/types/enums) that identifies the physical kind of the component:
export enum SubDeviceType {
CABINET = 'CABINET',
METER = 'METER',
INVERTER = 'INVERTER',
BATTERY = 'BATTERY',
}The type drives the snapshot entry shape. Cabinet entries carry a raw bitmask integer decoded by binary signals. Meter, inverter, and battery entries carry a values map decoded by numeric signals. See Signals for the decode rules.
The type is also used when assigning a device template to a sub-device — the template's type must match the sub-device's type.
Template linkage
Each sub-device may be linked to a DeviceTemplate at SUB_DEVICE level. Like the plant-to-template binding, this is validated on write:
- Existence — the template must exist in the global catalogue.
- Type match — the template's
SubDeviceTypemust match the sub-device'stype. A mismatch returnsVALIDATION_ERROR.
Current implementation note: the sub-device service validates existence and type match when assigning templates. Admin UI filters to SUB_DEVICE templates, and direct API callers should do the same.
A sub-device without a template produces no decoded telemetry. The telemetry pipeline simply skips entries with templateId: null.
Auto-discovery
When SubDeviceTelemetryService receives a snapshot that references an externalId not yet present in the database, it creates a placeholder sub-device row automatically (SubDevicesService.upsertForIngest). The created row has:
externalIdandtypetaken from the snapshot entrynamedefaulted to theexternalIdvaluestatus: ONLINEmetadata.autoProvisioned: trueandmetadata.firstSeenAtset to the ingest timetemplateId: null— no template assigned yet
The sub_device.discovered event is emitted to the plant Socket.IO room as sub-device:discovered, prompting operators to review and attach a template. No firmware change is needed — PLCs can freely evolve the device list they publish, and Voke will track new entries automatically.
Admin surfaces
Plant detail is built around the sub-device model:
- Overview renders template-fed KPI cards and today's energy summary. The v1 KPI helpers use Solinteg-style external IDs (
PV1,GRID,BAT1,BACKUP,INV1) until a formalSubDevice.rolefield exists. - Devices renders a compact, searchable list. Each row shows
externalId, name, type badge, status, and up to five featured signals from the assigned template. Row actions handle rename, template change, command dispatch, and delete. - Device detail opens as a responsive dialog on desktop and drawer on mobile. It keeps live signal inspection above the fold, with collapsible command and template sections.
- Live is template-driven. Numeric signals with the same
chartGrouprender together in one chart panel;chartAxischooses left or right axis. No React code change is required to add a chart group.
The REST API reference pages are:
- Sub Devices — plant-scoped CRUD, latest signals, historical telemetry, and sub-device commands
- Device Templates — global template catalogue
Status
Sub-device status follows SubDeviceStatus:
| Status | Meaning |
|---|---|
ONLINE | Seen in at least one recent snapshot. |
OFFLINE | Not seen for longer than the configured staleness threshold. |
DEGRADED | Seen but reporting partial or erroneous data. |
UNKNOWN | Default at creation (before first telemetry). |
The telemetry pipeline stamps status: ONLINE and updates lastSeenAt on every ingest for all sub-devices referenced in the envelope, in a single batch UPDATE.
What sub-devices are NOT
- Not MQTT clients. A sub-device never holds an MQTT credential or connects to the broker.
- Not directly addressable on the wire. A PLC does not publish to a per-sub-device topic. All traffic flows on the plant's topic.
- Commands are routed through the plant. When sending a command to a specific sub-device, the command payload includes a
targetfield set to the sub-device'sexternalId. The plant receives the full command envelope and routes it to the target component internally. See Commands & alerts.
See also
- Plants — the MQTT bridge that owns the connection
- Device templates — the signal catalogue that decodes telemetry
- Signals — binary vs. numeric decode rules and storage model
- Commands & alerts — how commands are routed to a specific sub-device