Plants
A plant is the MQTT bridge between Voke and field equipment. Everything else — sub-devices, telemetry, commands — flows through it.
What is a plant?
A plant is the primary field-side entity in Voke. It represents the MQTT client credential — exactly one MQTT connection, one set of authentication secrets, and one MQTT topic namespace. Physical components hanging off a plant (cabinets, meters, inverters) are modelled as sub-devices and never connect to MQTT directly. All their telemetry arrives through the plant.
One plant = one MQTT client = one credential.
Identifiers
| Field | Type | Notes |
|---|---|---|
id | UUID | Voke-assigned primary key. Also used as the MQTT client ID (mqttUsername) for mTLS and TOKEN plants. |
serialNumber | string | Operator-supplied. Unique within an organization — a UQ_plants_org_serial constraint enforces (organizationId, serialNumber) uniqueness. Two plants in different organizations may share a serial number. |
externalPlantId | string | null | Optional free-form identifier used by ESM (energy-trading) partners to link a Voke plant to an ESM-tracked plant. Populated by the trading integration; irrelevant to PLC firmware. |
templateId | string | null | FK to a DeviceTemplate at PLANT level. Optional — a plant without a template still forwards sub-device telemetry; the template is only needed if the plant itself produces signals decoded by Voke (rather than via sub-devices). |
Template linkage
A plant may reference at most one device template. Template assignment is validated at write-time against two conditions:
- Existence — the template ID must exist in the global catalogue.
- Level — the template must be
DeviceTemplateLevel.PLANT. Assigning aSUB_DEVICE-level template to a plant returnsDEVICE_TEMPLATE_WRONG_LEVEL.
Pass templateId: null to remove the assignment without deleting the template.
// Validation path in PlantsService.update (apps/api/src/modules/plants/plants.service.ts)
const template = await this.templateRepo.findOne({
where: { id: templateId },
select: { id: true, level: true },
});
if (!template) throw DEVICE_TEMPLATE_NOT_FOUND;
if (template.level !== DeviceTemplateLevel.PLANT) throw DEVICE_TEMPLATE_WRONG_LEVEL;Authentication methods
Plants authenticate to Mosquitto using one of three methods, set at creation:
authMethod | Description |
|---|---|
MTLS | Mutual TLS — Voke-issued client certificate + HMAC shared secret. |
TOKEN | JWT-based MQTT authentication + HMAC shared secret. No certificate needed. |
PASSWORD | Username + password on the password-authenticated MQTT listener. |
The method is fixed at creation. Credentials are issued once via POST /api/v1/plants/:id/provision and never stored in plaintext after issuance.
Lifecycle
Plants progress through PlantLifecycleStatus states:
PENDING → ACTIVE → SUSPENDED → ACTIVE (reactivation)
→ DECOMMISSIONED (revocation, terminal)| Status | Meaning |
|---|---|
PENDING | Created but not yet provisioned. No credentials issued. |
ACTIVE | Provisioned and operational. Telemetry and commands flow normally. |
SUSPENDED | Temporarily disabled. MQTT credentials remain valid at the broker level; Voke rejects telemetry. |
DECOMMISSIONED | Revoked. Credentials cleared. Terminal — cannot be reactivated. |
Suspending a plant does not disconnect its MQTT client or tear down the credential at the broker level. The PLC may still publish; Voke will silently discard the messages. Re-activating (POST /api/v1/plants/:id/reactivate) resumes normal forwarding with the existing credentials. Decommissioning (POST /api/v1/plants/:id/revoke) clears all credentials — the plant cannot reconnect until re-provisioned as a new entity.
HMAC signing
All telemetry and command-acknowledgement payloads are HMAC-SHA256-signed. The shared secret (hmacSecret) is generated during provisioning and stored encrypted. Voke validates every inbound message before passing it to the telemetry pipeline.
See Device security for the secret-rotation flow and the 1-hour grace period.
Sub-devices under a plant
A plant may have zero or many sub-devices. Sub-devices share the plant's MQTT connection and are addressed by their short externalId (R1, GRID, INV1, BAT1, …) inside the snapshot envelope. The plant itself is transparent to sub-device telemetry — SubDeviceTelemetryService uses the plant ID as the scoping key, resolves every entry to a sub-device row, and writes decoded signal rows into the shared telemetry hypertable.
GET /api/v1/plants/:id/energy/today derives the plant's today summary from sub-device telemetry. The first implementation is Solinteg-convention based: it sums PV sub-devices whose externalId starts with PV and grid readings from GRID, using UTC day boundaries.
See also
- Sub-devices — physical components under a plant
- Device templates — signal and action catalogues
- Signals — how telemetry values are decoded and stored