MQTT Topics¶
Cosalette uses a flat, Home Assistant-aligned topic hierarchy where the
application name serves as the prefix and device names form the second segment.
Every topic follows the pattern {app}/{device}/{channel} or {app}/{channel}.
Complete Topic Map¶
| Topic | Direction | Retained | QoS | Purpose |
|---|---|---|---|---|
{app}/{device}/state |
Outbound | Yes | 1 | Device state (JSON) |
{app}/{device}/set |
Inbound | — | — | Command input (subscribed, routed) |
{app}/{device}/{sub}/set |
Inbound | — | — | Sub-topic command input |
{app}/{device}/availability |
Outbound | Yes | 1 | Per-device online/offline |
{app}/{device}/error |
Outbound | No | 1 | Per-device error events |
{app}/error |
Outbound | No | 1 | Global error events |
{app}/status |
Outbound | Yes | 1 | App heartbeat / LWT |
Topic prefix = app name
The {app} prefix is the name argument to App(name="velux2mqtt").
For example, a device "blind" in a "velux2mqtt" app publishes state to
velux2mqtt/blind/state.
QoS is hard-coded at 1
All framework-managed publishes use QoS 1 (at-least-once). This is not configurable — it matches Home Assistant expectations and is the right trade-off for idempotent state payloads. If you need QoS 0 for a high-frequency custom channel, use the escape hatch:
State Topics¶
- JSON payload — always a serialised dict
- Retained — Home Assistant and other consumers receive the last-known state immediately upon subscribing
- QoS 1 — at-least-once delivery for reliability
- Published by
ctx.publish_state()in device code
Command Topics¶
- Inbound — the framework subscribes to
{app}/{device}/setand{app}/{device}/+/setfor every command & control device - The
TopicRouterparses the topic, extracts the device name and optional sub-topic segment, and dispatches to the appropriate handler or command queue - Telemetry devices do not subscribe to
/set— they have no command handler
Sub-Topic Commands¶
Devices that handle multiple command types use sub-topic routing. Each sub-topic gets its own MQTT topic:
velux2mqtt/cover/set ← "50" (root command)
velux2mqtt/cover/calibrate/set ← "HIGH" (sub-topic: calibrate)
The sub-topic appears as a segment between the device name and /set.
Register sub-topic handlers via @ctx.on_command("calibrate") inside an
@app.device function. See ADR-025
for the design rationale.
Topic Routing Internals¶
The TopicRouter extracts the device name and optional sub-topic from the
MQTT topic string:
# Root command
"velux2mqtt/blind/set" → device="blind", sub_topic=None
# Sub-topic command
"velux2mqtt/blind/calibrate/set" → device="blind", sub_topic="calibrate"
The router silently ignores topics that do not match the expected pattern. If a message arrives for a device with no registered handler, a warning is logged but no error is raised.
Availability Topics¶
- String payload —
"online"or"offline"(not JSON) - Retained — subscribers always know the last-known status
- Home Assistant compatible — matches the MQTT availability schema directly
- Published automatically by the
HealthReporterat device startup and during graceful shutdown
Error Topics¶
velux2mqtt/error → {"error_type": "error", "message": "...", ...}
velux2mqtt/blind/error → {"error_type": "invalid_command", "message": "...", ...}
- Not retained — errors are events, not state. A retained error would mislead operators into thinking the error is ongoing after a restart.
- QoS 1 — reliable delivery so monitoring tools receive the event
- Dual publication — every error goes to
{app}/error(global), and if a device name is known, also to{app}/{device}/error - See Error Handling for payload structure
Why not retained?
Consider a scenario: a device publishes an error, then recovers. If the error message were retained, a new subscriber would see it and incorrectly believe the error is still active. Non-retained errors are ephemeral — they are delivered to current subscribers only.
App Status Topic¶
The {app}/status topic serves double duty:
When the MQTT client connects, it registers a Last Will and Testament:
if the client disconnects unexpectedly (crash, network loss), the broker
publishes "offline" to {app}/status on the client's behalf.
The two formats coexist on the same topic — the LWT payload is a plain string
"offline", while the heartbeat is JSON. Consumers can distinguish them by
attempting JSON parse. See Health & Availability for
details.
Retained vs Not-Retained Rationale¶
| Topic type | Retained? | Rationale |
|---|---|---|
| State | Yes | Consumers need last-known value on subscribe |
| Availability | Yes | Consumers need last-known online/offline status |
| Status | Yes | Crash detection requires retained LWT |
| Error | No | Errors are events — stale errors mislead operators |
| Set (command) | — | Inbound — retention is the publisher's choice |
Wildcard Monitoring¶
MQTT wildcards enable fleet-level monitoring without knowing device names in advance:
| Pattern | Use case |
|---|---|
+/status |
Monitor all apps in a fleet |
velux2mqtt/+/state |
All device states in one app |
+/error |
Global errors across all apps |
velux2mqtt/+/error |
Per-device errors in one app |
velux2mqtt/+/availability |
Per-device availability in one app |
# Subscribe to all errors across all bridges
mosquitto_sub -t '+/error' -v
# Subscribe to all state updates from a single bridge
mosquitto_sub -t 'velux2mqtt/+/state' -v
See Also¶
- Device Archetypes — which devices use which topics
- Error Handling — error payload structure and semantics
- Health & Availability — heartbeat and LWT details
- Configuration —
topic_prefixsetting - ADR-002 — MQTT Topic Conventions
- ADR-025 — Command Channel and Sub-Topic Routing