MQTT Topic Reference¶
Complete topic schema reference for cosalette MQTT topics. For conceptual background — rationale, routing internals, wildcard monitoring — see MQTT Topics (concept).
Topic Schema¶
Every cosalette application uses a flat, Home Assistant-aligned topic
hierarchy. The {prefix} is the application name, and {device} is
the device name registered with @app.device(), @app.command(), or
@app.telemetry(). When the device name is omitted, the device publishes
to root-level topics without a {device} segment.
| Topic Pattern | Direction | QoS | Retain | Description |
|---|---|---|---|---|
{prefix}/{device}/state |
Outbound | 1 | Yes | Device state (JSON) |
{prefix}/{device}/set |
Inbound | — | — | Command input; also used by telemetry with an MQTT trigger source (triggerable=True / "mqtt" / "both"). A triggerable="local" telemetry entity or device subscribes nothing extra. |
{prefix}/{device}/{sub}/set |
Inbound | — | — | Sub-topic command input |
{prefix}/{device}/{sub}/state |
Outbound | 1 | Yes | Sub-entity state (JSON) |
{prefix}/{device}/{sub}/availability |
Outbound | 1 | Yes | Sub-entity online/offline (string) |
{prefix}/{device}/availability |
Outbound | 1 | Yes | Per-device online/offline (string) |
{prefix}/{device}/error |
Outbound | 1 | No | Per-device error event (JSON) |
{prefix}/error |
Outbound | 1 | No | Global error event (JSON) |
{prefix}/status |
Outbound | 1 | Yes | App heartbeat (JSON) and LWT ("offline") |
Root Device Topics¶
When name is omitted from a decorator, the device publishes at the
root level:
| Topic Pattern | Direction | QoS | Retain | Description |
|---|---|---|---|---|
{prefix}/state |
Outbound | 1 | Yes | Root device state (JSON) |
{prefix}/set |
Inbound | — | — | Root device command input |
{prefix}/availability |
Outbound | 1 | Yes | Root device online/offline |
Root device errors appear only on the global {prefix}/error topic —
there is no per-device error topic since it would be identical.
At most one root device per app. See Device Archetypes for naming rules.
QoS and retain
All framework-managed publishes use QoS 1 (at-least-once) — this is
hard-coded and not configurable. For custom channels that need QoS 0,
use ctx.publish(channel, payload, qos=0). Retain defaults match the
table above; publish_state() accepts an optional retain keyword
argument to override per-call.
Framework Metadata Topics¶
The reserved _meta/ namespace carries framework introspection metadata. Both
topics are retained, QoS 1, always published (no setting), republished on every
broker connect, and never validated against the AsyncAPI schema or turned into a
Home Assistant discovery entity.
| Topic Pattern | Direction | QoS | Retain | Description |
|---|---|---|---|---|
{prefix}/_meta/registry |
Outbound | 1 | Yes | Canonical AsyncAPI document, with inbound command channels stripped |
{prefix}/_meta/state_model_drift |
Outbound | 1 | Yes | state_model= declaration drift snapshot (ADR-069) |
_meta/state_model_drift is the machine-readable rendering of the ADR-068
registration warning: it lists every @app.telemetry / @app.command handler
that declares state_model=M but is annotated -> N. A clean app publishes
drift_count: 0, so "no drift" is distinguishable from "never ran a version
that publishes drift".
{
"schema_version": 1,
"drift_count": 1,
"entries": [
{
"handler": "brightness",
"archetype": "telemetry",
"kind": "annotation_conflict",
"declared_model": "Reading",
"effective_annotation": "dict"
}
]
}
The snapshot covers declaration drift only — a runtime ReturnValidationError
is published to {prefix}/{name}/error as usual.
Shared brokers
Both topics expose channel addresses, payload schemas and handler/model
names. Protect _meta/# with broker ACLs on shared brokers.
Topic Prefix¶
The {prefix} placeholder resolves at runtime from two sources,
checked in order:
Settings.mqtt.topic_prefix— when set (e.g. viaMQTT__TOPIC_PREFIX=staging-velux), this value is used as{prefix}.App(name="velux2mqtt")— whentopic_prefixis empty (the default), thenameargument is used.
This lets you deploy the same binary with different topic namespaces (e.g. staging vs production) by setting an environment variable.
For example, App(name="velux2mqtt") with a device "blind" produces:
velux2mqtt/blind/state
velux2mqtt/blind/set
velux2mqtt/blind/availability
velux2mqtt/error
velux2mqtt/status
See Settings Reference for all MQTT configuration options.
State Topics¶
Published by ctx.publish_state() in device functions.
- Topic:
{prefix}/{device}/state - QoS: 1 — at-least-once delivery
- Retain: Yes — subscribers receive the last-known state immediately
- Payload: JSON dict (user-defined structure)
Command Topics¶
Inbound topics the framework subscribes to for command & control devices.
- Topic:
{prefix}/{device}/set - Direction: Inbound — the broker delivers messages to the framework
- Payload: Plain string, decoded by the user's command handler
The TopicRouter subscribes to {prefix}/{device}/set and
{prefix}/{device}/+/set for each device that has a registered command
handler (via @app.command(), @ctx.on_command, or ctx.commands() inside
an @app.device() function). The +/set wildcard captures sub-topic
commands. Telemetry-only devices are not subscribed.
Routing behaviour¶
The router extracts the device name and optional sub-topic using a three-step resolution. MQTT wildcards are used only for subscription, not for parsing:
- Exact registered-name match —
{prefix}/{device}/setresolves to(device, None). Handles both simple names and slash-composed Router names such as"sensors/temperature". - One-level sub-topic prefix match — when a registered name is a
path-prefix of the topic's middle segment,
{prefix}/{device}/{sub}/setresolves to(device, sub). - Syntactic fallback — for topics with no matching registered name, a one- or two-segment parse is attempted so that a useful "No handler registered" warning can be emitted. This never dispatches to a handler.
Topics that do not match {prefix}/…/set (or {prefix}/set for root
devices) are silently ignored. Messages for a device with no handler
produce a WARNING log entry.
Availability Topics¶
Published automatically by the HealthReporter at device startup and
during graceful shutdown.
- Topic:
{prefix}/{device}/availability - QoS: 1
- Retain: Yes
- Payload:
"online"or"offline"(plain string, not JSON)
Compatible with Home Assistant's MQTT availability schema.
Re-announced on every (re)connect
The framework re-asserts "online" availability for all currently-available
devices on every successful MQTT (re)connect. This means retained availability
recovers automatically after a broker restart or network interruption — late
subscribers always see the correct state. Devices that transitioned offline
after initial startup keep their last retained "offline" and are not
re-announced. See ADR-012 amendment.
Sub-Entity Availability¶
Sub-entities follow the same pattern one level deeper:
velux2mqtt/cover/calibrate/availability → "online"
velux2mqtt/cover/calibrate/availability → "offline"
Sub-entity availability is managed automatically by ctx.sub_entity() — "online" on
enter, "offline" on exit, with retained state cleared on teardown. See
ADR-031.
Error Topics¶
Published by the ErrorPublisher service. Every error is published to
the global topic; when a device name is known, a copy goes to the
per-device topic as well.
- Global topic:
{prefix}/error - Per-device topic:
{prefix}/{device}/error - QoS: 1
- Retain: No — errors are events, not state
- Payload: JSON (
ErrorPayloadschema)
velux2mqtt/error → {"error_type": "error", "message": "...", ...}
velux2mqtt/blind/error → {"error_type": "invalid_command", "message": "...", ...}
See Payload Schemas for the full ErrorPayload structure.
App Status and LWT¶
The {prefix}/status topic serves two roles on the same topic:
Last Will and Testament (LWT)¶
When the MQTT client connects, the framework registers a Last Will and
Testament via build_will_config():
| Property | Value |
|---|---|
| Topic | {prefix}/status |
| Payload | "offline" |
| QoS | 1 |
| Retain | Yes |
If the client disconnects unexpectedly (crash, network loss), the broker
publishes "offline" to {prefix}/status on the application's behalf.
During graceful shutdown, the HealthReporter publishes "offline"
explicitly.
Heartbeat¶
The HealthReporter publishes a structured JSON heartbeat to the
same {prefix}/status topic. An initial heartbeat is published
on the first MQTT connection (overwriting the LWT "offline"), then
periodically at the configured heartbeat_interval (default 60 s,
set to None to disable). A fresh heartbeat is also published on
every subsequent reconnect, ensuring the retained payload is current
after a broker restart:
{
"status": "online",
"uptime_s": 3600.0,
"version": "0.3.0",
"devices": {
"blind": {"status": "ok"},
"temp": {"status": "ok"}
}
}
- QoS: 1
- Retain: Yes
Consumers can distinguish LWT from heartbeat by attempting JSON parse:
the LWT payload is a plain string "offline", while heartbeats are
valid JSON objects.
See Payload Schemas for the full HeartbeatPayload and
DeviceStatus structures.
Wildcards¶
The framework uses explicit per-device subscriptions, not MQTT wildcards. However, external consumers (monitoring tools, Home Assistant) can use wildcards for fleet-level monitoring:
| 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/+/availability |
Per-device availability in one app |
velux2mqtt/blind/+/set |
All sub-topic commands for one device |
+/_meta/state_model_drift |
Scrape state_model= drift across a fleet |
# 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¶
- MQTT Topics (concept) — retained vs not-retained rationale and wildcard monitoring strategy
- Payload Schemas — JSON payload structures
- Error Handling (concept) — error semantics
- Health & Availability (concept) — heartbeat and LWT details
- Settings Reference —
MQTT__TOPIC_PREFIXand other MQTT settings - ADR-002 — MQTT Topic Conventions
- ADR-025 — Command Channel and Sub-Topic Routing