# MQTT topics and message schema

The device-plane MQTT contract between Edge site controllers and the platform. Topics per site, settings and config keys, the event types a site controller publishes, units, and the identifiers used.

Canonical: https://edgetech.eu/docs/mqtt-topics  
Updated: 2026-10-09

Site controllers connect to the EdgeTech broker over MQTT 3.1.1 with TLS and a per-device pre-shared key. Customers running their own platform get the same contract on their own broker.

## Identifiers

| Identifier | Format | Where |
|---|---|---|
| Site ID | 24 hex characters (the site controller's device ID) | Topic prefix, `coreid` in events |
| Socket ID | 20 hex characters (the charge controller's serial) | `@<socketId>` suffix on socket events |
| External ID | 10-digit number in the QR code on the device | App, invoices, support |

## Server to device

All topics are prefixed with `<siteId>/`.

| Topic | Payload | Meaning |
|---|---|---|
| `settings` | string | `logondebug`, `logoff`, `p1charge` (all sockets 1-phase), `p3charge`, `reset`, `silence`, `unsilence`, `streaming` |
| `config` | JSON | Per-site and per-socket configuration, keys below |
| `discover` | string or JSON | `clearsockets`, `quietclearsockets`, `clear`, `recover`, `{"restart":"<socketId>"}` |
| `routing` | string, retained | Where the forwarder posts events for this site, for example `ev.edgetech.nl:8443` |

### `config` keys

| Key | Value | Meaning |
|---|---|---|
| `allowcharging@<socket>` | 0/1 | Socket enabled |
| `startcharging@<socket>` | 0/1 | Start a session now (app start) |
| `settings@<socket>` | bits | Bit 0: 3-phase |
| `cptype@<socket>` | 0/1 | CP type, see register 0x020A |
| `maxcurrent` | `"3x25"` | Site limit per phase, amps |
| `maxsitecurrent` | 0–255 | Site limit, 255 = uncapped |
| `minstartc` | A | Minimum current to start a session, default 8 |
| `minc` | A | Minimum charge current, default 6 |
| `solar` | 0/1 | Solar mode on |
| `charge_solar` | 0/1 | Solar-only (1) or solar-assist (0) |
| `plugandcharge` | 0/1 | Start without app authorisation |

## Device to server

Events are published as JSON: `{"coreid": "<siteId>", "event": "pwh_<type>[@<socketId>]", "data": {...}, "published_at": "<ISO 8601>"}`.

| Event | When | Content |
|---|---|---|
| `pwh_spark/status` | connect / last will | `online` or `offline`; offline detection about 90 s |
| `pwh_site_status` | every 5 min and on change | `site_current_1..3` (A), `pwh_day_budget`, `board_errors`, `crc_errors`, `mb_quality` (%), WAN and LAN IP |
| `pwh_site_config` | on boot and on change, retained | `firmware`, `hardware` (hardware revision), socket list |
| `pwh_socket_status@<id>` | on change | `state` (1–4), `error` (bitmask), `current_1..3` (raw 1/256 A), `maxcurrent`, `temperature` |
| `pwh_socket_config@<id>` | once per boot | `socketfwversion` (packed: `10769` = 0x2A11 = 2.10.17), settings, phases |
| `pwh_data@<id>` | end of session | `session_kwh` (watt-hours, despite the name), start and end time |
| `pwh_health` | every 5 min | Heap, uptime, WiFi RSSI, reset reason |
| `pwh_log` | batched, QoS 0 | Remote log lines with `[rlog seq=N dropped=M]` headers |
| `pwh_spark/flash/status` | during OTA | `started`, `success`, `failed` |

## Units, exactly

| Field | Unit |
|---|---|
| `current_1..3` on socket status | 1/256 A (5632 = 22.0 A) |
| `site_current_1..3` | A, decimal |
| `session_kwh`, `kwh_h`, `kwh_l` | Wh |
| `temperature` | °C |
| `mb_quality` | % of Modbus transactions succeeded |
| `pwh_day_budget` | messages left today, of 100 |

## Broker-side extras

- `<siteId>/wip`, retained: the WAN IP the broker saw the device connect from, set by a broker plugin on connect. Useful for finding a site behind a shared router.
- Last will on `pwh_spark/status` gives offline detection without polling.
- Message budget: 100 messages per device per day, to keep a misbehaving site from flooding the platform.

## Provisioning

Each device authenticates with TLS-PSK: identity = device ID, key derived per device from a platform master key. New devices are added to the broker before first connect; there is no self-registration. Customers on their own platform receive the derivation tool.
