# OTGW-firmware MQTT Topic Documentation This document describes all MQTT topics published and subscribed to by the OTGW-firmware. > **Breaking change in 2.0.0 (MQTT consumers please read):** three OT-bus presence values (`boiler_connected`, `thermostat_connected`, `otgw_connected`) have moved out of the hardware-specific `otgw-pic/` and `otgw-otdirect/` subtrees into the generic `OTGW/value//` namespace. The `otgw-otdirect/ot_online` topic has been retired; the same concept now lives under `otgw_connected`. The firmware self-heals retained payloads on the deprecated topics at first MQTT reconnect. Home Assistant users do not need to do anything; custom consumers should update their topic patterns. See [OT-bus state (generic, since 2.0.0)](#ot-bus-state-generic-since-200) and [Migration from 1.4.x / pre-release 2.0.0 (OT-bus state topics)](#migration-from-14x--pre-release-200-ot-bus-state-topics). Background: ADR-084. > > **Breaking change in 2.0.0 (binary_sensor topic labels):** 37 OT-spec-derived binary_sensor topic labels have been replaced with HA-core-style self-describing aliases (`dhw_present` → `supports_hot_water`, etc.). The two name sets are mutually exclusive; setting `mqttuselegacyottopics = true` switches back to the legacy labels. The firmware drains the retained payloads of the *other* name set on toggle (cleanup state persisted to `/mqtt_topic_cleanup.bin` to survive power loss mid-drain). Background: ADR-105 (superseded) → ADR-106. > > **Discovery in 2.0.0 is pure JIT (ADR-100).** The boot-time and `homeassistant/status → online` bulk publishes are gone; only climate, the outside-temp number, Dallas sensors, and heap-stats are pre-published. Per-MsgID configs are published the first time the firmware sees the matching OT frame. A broker-restart heuristic still triggers a full republish when the gateway has been disconnected from MQTT for more than 5 minutes (`MQTT_REPUBLISH_OFFLINE_THRESHOLD_MS`). > > **Flat per-value topics are policy (ADR-101).** The firmware never publishes aggregated JSON value payloads. Each value lives on its own topic with a scalar payload. HA discovery configs are the only JSON publishes, as required by the discovery protocol. Do not propose OT-Thing-style nested-JSON shims. ## Overview The OTGW-firmware uses MQTT as its primary integration method with Home Assistant and other home automation systems. It publishes OpenTherm data, device status, and sensor readings, and subscribes to command topics for controlling the OpenTherm Gateway. ## Topic Structure ### Namespace Convention All MQTT topics follow a namespace convention based on user-configurable settings: - **Publish namespace**: `{TopTopic}/value/{UniqueId}/` - **Subscribe namespace**: `{TopTopic}/set/{UniqueId}/` - **Last Will and Testament**: published to the publish namespace root Where: - `{TopTopic}` = `settings.mqtt.sTopTopic` (default: `OTGW`) - `{UniqueId}` = `settings.mqtt.sUniqueid` (default: `otgw-{MAC_ADDRESS}`) **Example** with defaults: - Publish: `OTGW/value/otgw-AABBCCDDEEFF/boilertemperature` - Subscribe: `OTGW/set/otgw-AABBCCDDEEFF/setpoint` ### Connection Lifecycle On MQTT connect: 1. **Birth message**: Publishes `"online"` to the publish namespace root (retained) 2. **Last Will**: Configured to publish `"offline"` to the publish namespace root (retained) when the connection drops 3. **Discovery reset**: Clears all HA discovery state so JIT discovery re-publishes 4. **Subscribes** to `{TopTopic}/set/{UniqueId}/#` for incoming commands 5. **Subscribes** to `homeassistant/status` for Home Assistant lifecycle detection 6. **Publishes** version info, state information, and cached PIC settings --- ## Published Topics All published topics are under the publish namespace `{TopTopic}/value/{UniqueId}/` unless otherwise noted. Topics are published with `retain = true` by default. ### Firmware & Device Information Published at startup, on MQTT (re)connect, and every 5 minutes. | Topic | Value | Description | | ----- | ----- | ----------- | | `otgw-firmware/version` | `"1.3.0"` | Firmware version string | | `otgw-firmware/reboot_count` | `"42"` | Number of reboots since first boot | | `otgw-firmware/reboot_reason` | `"Software/System restart"` | Last reboot reason | | `otgw-firmware/uptime` | `"12345"` | Uptime in seconds (not retained) | | `otgw-firmware/hardware_type` | `"otgw-classic"` / `"otgw32"` | Static hardware type (board class), compile-time. Use this for codepath/UI selection instead of `otgw-pic/picavailable` (ADR-113). Future: `"ot-thing"`. Retained. | | `otgw-firmware/board` | `"esp8266"` / `"esp32s3"` | Hardware board identifier | | `otgw-firmware/hardware_mode` | `"pic"` / `"otdirect"` | Active hardware mode | | `otgw-firmware/network_mode` | `"wifi"` / `"ethernet"` / `"ap"` | Active network mode | | `otgw-firmware/error` | `"LittleFS mount failed..."` | Error messages (not retained, only when applicable) | ### OT-bus state (generic, since 2.0.0) Published at startup, on MQTT (re)connect, and whenever the values change. These three topics describe what the OTGW-firmware observes on the OpenTherm bus, independent of whether the hardware uses the PIC coprocessor or the native OTDirect driver. Before 2.0.0 these values lived under `otgw-pic/` or `otgw-otdirect/`; see the [migration section](#migration-from-14x--pre-release-200-ot-bus-state-topics) for details. | Topic | Value | Description | | ----- | ----- | ----------- | | `boiler_connected` | `"ON"` / `"OFF"` | A boiler is visible on the OpenTherm bus (frames received from the slave side). | | `thermostat_connected` | `"ON"` / `"OFF"` | A thermostat is visible on the OpenTherm bus (frames received from the master side). | | `otgw_connected` | `"ON"` / `"OFF"` | The OpenTherm bus is active (the firmware is exchanging frames). This topic replaces the former `otgw-otdirect/ot_online` and mirrors the former `otgw-pic/otgw_connected`. | Payloads are **not** retained: these three values reflect live OpenTherm-bus state, and broker-side liveness is carried separately by the retained birth/LWT availability at the publish-namespace root (ADR-102). Published as `OTGW/value//` using the configured `TopTopic` and `UniqueId` (see [Namespace Convention](#namespace-convention)). ### PIC Gateway Information Published at startup, on MQTT (re)connect, and every 5 minutes. Note: the OT-bus presence values (`boiler_connected`, `thermostat_connected`, `otgw_connected`) were previously listed here. Since 2.0.0 they are published under the generic namespace; see [OT-bus state (generic, since 2.0.0)](#ot-bus-state-generic-since-200) and the [migration section](#migration-from-14x--pre-release-200-ot-bus-state-topics). | Topic | Value | Description | | ----- | ----- | ----------- | | `otgw-pic/version` | `"5.4"` | PIC firmware version | | `otgw-pic/deviceid` | `"gateway"` | PIC device ID | | `otgw-pic/firmwaretype` | `"gateway"` | PIC firmware type (gateway/interface/diagnose) | | `otgw-pic/gateway_mode` | `"ON"` / `"OFF"` | Gateway mode (ON) vs monitor mode (OFF) | ### PIC Settings Published at startup, on MQTT (re)connect, every 5 minutes, and when settings are queried. Only fields that have been queried (non-empty) are published. | Topic | Value | Description | | ----- | ----- | ----------- | | `otgw-pic/settings/setpoint_override` | `"20.00"` | Current setpoint override | | `otgw-pic/settings/setback` | `"15.00"` | Setback temperature | | `otgw-pic/settings/dhw_override` | `""` | DHW override state | | `otgw-pic/settings/gpio` | `"0/1"` | GPIO pin configuration | | `otgw-pic/settings/gpio_states` | `"0/0"` | GPIO pin states | | `otgw-pic/settings/led` | `"F/X/O/M/P/C"` | LED function assignments | | `otgw-pic/settings/tweaks` | `"1/1/1/1"` | Tweak flags | | `otgw-pic/settings/temp_sensor` | `"O=19.50"` | Temperature sensor reading | | `otgw-pic/settings/smart_power` | `"Low"` | Smart power mode | | `otgw-pic/settings/thermostat_detect` | `"I"` | Thermostat detection mode | | `otgw-pic/settings/builddate` | `"2023-01-01"` | PIC firmware build date | | `otgw-pic/settings/clock_mhz` | `"4"` | PIC clock speed | | `otgw-pic/settings/reset_cause` | `"Power-on"` | Last PIC reset cause | | `otgw-pic/settings/standalone_interval` | `"0"` | Standalone mode interval | | `otgw-pic/settings/voltage_ref` | `"3.3"` | Voltage reference | ### OT Direct (OTGW32) Published at startup, on MQTT (re)connect, and every 5 minutes. Only present in OTGW32 builds (`HAS_DIRECT_OT=1`). On standard ESP8266+PIC hardware, only `otgw-otdirect/available` is published with value `"OFF"`. Note: the OT-bus presence values (`boiler_connected`, `thermostat_connected`) and the former `ot_online` topic were previously listed here. Since 2.0.0 these values are published under the generic namespace; `ot_online` has been retired in favour of `otgw_connected`. See [OT-bus state (generic, since 2.0.0)](#ot-bus-state-generic-since-200) and the [migration section](#migration-from-14x--pre-release-200-ot-bus-state-topics). | Topic | Value | Description | | ----- | ----- | ----------- | | `otgw-otdirect/available` | `"ON"` / `"OFF"` | Whether OT-direct hardware is present and enabled | | `otgw-otdirect/mode` | `"gateway"` / `"monitor"` / `"bypass"` / `"master"` / `"loopback"` | Current operating mode of the OT-direct engine | | `otgw-otdirect/bypass` | `"ON"` / `"OFF"` | Whether the bypass relay is active | | `otgw-otdirect/monitor_mode` | `"ON"` / `"OFF"` | Whether transparent pass-through mode is active | | `otgw-otdirect/master_mode` | `"ON"` / `"OFF"` | Whether OTGW32 is acting as sole OT master | | `otgw-otdirect/stepup` | `"ON"` / `"OFF"` | Whether the 24V step-up converter is active | | `otgw-otdirect/setback_active` | `"ON"` / `"OFF"` | Whether setback override is active (thermostat disconnected) | | `otgw-otdirect/schedule_active` | `"11"` | Number of OT polling schedule entries currently active | | `otgw-otdirect/schedule_disabled` | `"1"` | Number of schedule entries disabled because boiler returned UNKNOWN_DATA_ID | | `otgw-otdirect/overrides_active` | `"2"` | Number of active write-override slots | These topics are only published when `isOTDirectEnabled()` returns true. Topics prefixed `otgw-otdirect/` are skipped during MQTT discovery replay when OT-direct is not enabled. ### OpenTherm Status Flags (Message ID 0) Published when status flag values change. Individual bits are published as `"ON"` / `"OFF"`. #### Master Status (Thermostat to Boiler) | Topic | Description | | ----- | ----------- | | `status_master` | Full master status as text string | | `chenable` | Central heating enable | | `dhwenable` | DHW enable | | `coolingenable` | Cooling enable | | `otc_active` | Outside temperature compensation active | | `ch2enable` | Central heating 2 enable | | `summerwintermode` | Summer/winter mode | | `dhwblockingenable` | DHW blocking enable | #### Slave Status (Boiler to Thermostat) | Topic | Description | | ----- | ----------- | | `status_slave` | Full slave status as text string | | `faultindicator` | Fault indicator | | `chmodus` | Central heating active | | `dhwmode` | DHW active | | `flamestatus` | Flame status | | `coolingactive` | Cooling active | | `ch2modus` | CH2 active | | `diagnosticindicator` | Diagnostic indicator | ### OpenTherm ASF Flags (Message ID 5) | Topic | Value | Description | | ----- | ----- | ----------- | | `ASF_flags` | `"00000000"` | Application-specific fault flags (binary) | | `OEMFaultCode` | `"0"` | OEM fault code | | `service_request` | `"ON"` / `"OFF"` | Service request flag | | `lockout_reset` | `"ON"` / `"OFF"` | Lockout reset flag | | `low_water_pressure` | `"ON"` / `"OFF"` | Low water pressure flag | | `gas_flame_fault` | `"ON"` / `"OFF"` | Gas/flame fault flag | | `air_pressure_fault` | `"ON"` / `"OFF"` | Air pressure fault flag | | `water_over_temperature` | `"ON"` / `"OFF"` | Water over-temperature flag | ### OpenTherm Configuration (Message IDs 3, 70, 74) #### Slave Configuration (Boiler, ID 3) | Topic | Value | Description | | ----- | ----- | ----------- | | `slave_configuration` | `"00000001"` | Slave configuration flags (binary) | | `slave_memberid_code` | `"0"` | Slave member ID code | | `dhw_present` | `"ON"` / `"OFF"` | DHW present | | `control_type_modulation` | `"ON"` / `"OFF"` | Modulating control type | | `cooling_config` | `"ON"` / `"OFF"` | Cooling configuration | | `dhw_config` | `"ON"` / `"OFF"` | DHW configuration | | `master_low_off_pump_control_function` | `"ON"` / `"OFF"` | Pump control | | `ch2_present` | `"ON"` / `"OFF"` | CH2 present | | `remote_water_filling_function` | `"ON"` / `"OFF"` | Remote water filling | | `heat_cool_mode_control` | `"ON"` / `"OFF"` | Heat/cool mode control | #### Master Configuration (Thermostat, ID 70) | Topic | Value | Description | | ----- | ----- | ----------- | | `master_configuration` | `"00000000"` | Master configuration flags (binary) | | `master_configuration_smart_power` | `"ON"` / `"OFF"` | Smart power | | `master_memberid_code` | `"0"` | Master member ID code | #### VH Configuration (Ventilation/Heat-recovery, ID 74) | Topic | Value | Description | | ----- | ----- | ----------- | | `vh_configuration` | `"00000000"` | VH configuration flags (binary) | | `vh_configuration_system_type` | `"ON"` / `"OFF"` | System type | | `vh_configuration_bypass` | `"ON"` / `"OFF"` | Bypass | | `vh_configuration_speed_control` | `"ON"` / `"OFF"` | Speed control | | `vh_memberid_code` | `"0"` | VH member ID code | ### OpenTherm Numeric Values Published when OpenTherm messages are received. The topic name matches the OpenTherm message label. Common topics include: | Topic | Unit | Description | | ----- | ---- | ----------- | | `controlsetpoint` | C | Control setpoint (CH water temp target) | | `roomsetpoint` | C | Room setpoint | | `roomtemperature` | C | Current room temperature | | `relmodlvl` | % | Relative modulation level | | `maxrelmodlvl` | % | Max relative modulation level setting | | `boilertemperature` | C | Boiler water temperature | | `returnwatertemperature` | C | Return water temperature | | `dhwtemperature` | C | DHW temperature | | `dhwsetpoint` | C | DHW setpoint | | `maxchwatersetpoint` | C | Max CH water setpoint | | `outsidetemperature` | C | Outside temperature | | `chwaterpressure` | bar | CH water pressure | | `oemdiagnosticcode` | - | OEM diagnostic code | | `controlsetpoint2` | C | Control setpoint for CH2 | | `roomsetpoint2` | C | Room setpoint for CH2 | | `dhw_flowrate` | l/min | DHW flow rate | | `exhaust_temperature` | C | Exhaust temperature | | `boiler_fan_speed` | rpm | Boiler fan speed | | `electrical_current_burner_flame` | uA | Burner flame current | | `dhwboundaries` | C | DHW boundaries | | `maxchboundaries` | C | Max CH boundaries | | `otc_hc_ratio_ub_lb` | - | OTC HC ratio upper/lower bound | The exact set of published topics depends on which OpenTherm message IDs the thermostat and boiler exchange. Any valid OT message ID (0-127) with a defined label will generate a corresponding MQTT topic. ### Remote Boiler Parameters (Message ID 6) | Topic | Value | Description | | ----- | ----- | ----------- | | `RBP_flags_transfer_enable` | `"00000000"` | Transfer enable flags (binary) | | `RBP_flags_read_write` | `"00000000"` | Read/write flags (binary) | | `rbp_dhw_setpoint` | `"ON"` / `"OFF"` | DHW setpoint transfer enabled | | `rbp_max_ch_setpoint` | `"ON"` / `"OFF"` | Max CH setpoint transfer enabled | | `rbp_rw_dhw_setpoint` | `"ON"` / `"OFF"` | DHW setpoint read/write | | `rbp_rw_max_ch_setpoint` | `"ON"` / `"OFF"` | Max CH setpoint read/write | ### Remote Override (Message ID 100) | Topic | Value | Description | | ----- | ----- | ----------- | | `remote_override_function` | `"00000000"` | Remote override flags (binary) | | `remote_override_manual_change_priority` | `"ON"` / `"OFF"` | Manual change priority | | `remote_override_program_change_priority` | `"ON"` / `"OFF"` | Program change priority | ### VH Status Flags (Message IDs 70, 71) | Topic | Value | Description | | ----- | ----- | ----------- | | `status_vh_master` | text | VH master status text | | `status_vh_slave` | text | VH slave status text | ### VH Transfer/RW Flags (Message ID 73) | Topic | Value | Description | | ----- | ----- | ----------- | | `vh_transfer_enable_nominal_ventilation_value` | `"ON"` / `"OFF"` | Nominal ventilation transfer enabled | | `vh_rw_nominal_ventilation_value` | `"ON"` / `"OFF"` | Nominal ventilation read/write | ### Solar Storage (Message IDs 101, 102, 103) | Topic | Value | Description | | ----- | ----- | ----------- | | `solar_storage_slave_configuration` | `"00000000"` | Solar configuration (binary) | | `solar_storage_slave_memberid_code` | `"0"` | Solar member ID | | `solar_storage_system_type` | `"ON"` / `"OFF"` | System type | | `solar_storage_master_mode` | `"0"` | Master solar mode | | `solar_storage_slave_fault_indicator` | `"ON"` / `"OFF"` | Solar fault indicator | | `solar_storage_mode_status` | `"0"` | Solar mode status | | `solar_storage_slave_status` | `"0"` | Solar slave status | ### SAT (Smart Autotune Thermostat) Topics are under the standard publish namespace when SAT is enabled. The complete topic inventory is in [backlog/docs/doc-1 - sat-mqtt-topics.md](../../backlog/docs/doc-1%20-%20sat-mqtt-topics.md). #### Publish semantics (ADR-111, since 2.0.0-alpha.72) SAT topics no longer publish on every control cycle. Each topic under `sat/*` uses **on-change + jittered heartbeat** semantics: 1. **First-seen**: published immediately at first evaluation (boot or first SAT enable). 2. **On-change**: published when the value differs from the last-published value by more than a per-field tolerance (float fields) or strictly (bool/int/string fields). See tolerance table below. 3. **Jittered heartbeat**: each topic independently schedules a heartbeat at a random time in the `[7 min, 11 min]` window after its last publish. This keeps non-retained topics alive in Home Assistant without a synchronised burst. 4. **Boot-scatter**: the first heartbeat after boot is scheduled at a random point in `[0, 11 min]` per topic, so the post-boot wave is spread across the full 11-minute window. **Maximum observation delay:** 11 minutes in the worst case (silent value, first heartbeat not yet due). Retained topics recover immediately from broker memory on reconnect; non-retained topics re-appear within 11 minutes. **MQTT reconnect:** SAT shadows are **not** reset on reconnect. Retained topics are already present on the broker. Non-retained topics re-publish within 11 minutes via the scheduled heartbeat. This is an intentional divergence from the OT publish contract (ADR-052), which resets on reconnect. **Float tolerance constants** (from `SATmqttPublish.h`): | Constant | Value | Used for | |---|---|---| | `SAT_EPS_TEMP` | 0.05 °C | `setpoint`, `target`, `room_temp`, `outside_temp` | | `SAT_EPS_TEMP_COARSE` | 0.1 °C | `heating_curve`, `thermal_drop_rate` display | | `SAT_EPS_PID_OUTPUT` | 0.5 °C | `pid_output` (derivative jitter damping) | | `SAT_EPS_PID_TERM` | 0.1 °C | `pid_p`, `pid_i`, `pid_d` | | `SAT_EPS_ERROR` | 0.05 °C | `error` | | `SAT_EPS_KP` | 0.01 | `kp` | | `SAT_EPS_KI` | 0.00001 | `ki` | | `SAT_EPS_KD` | 0.1 | `kd` | | `SAT_EPS_DERIVATIVE` | 0.0005 | `raw_derivative` | | `SAT_EPS_FRACTION` | 0.005 | `duty_ratio`, `overshoot_fraction` | | `SAT_EPS_PRESSURE` | 0.02 bar | `pressure`, `pressure_drop_rate` | | `SAT_EPS_POWER` | 0.05 kW | `power` | | `SAT_EPS_ENERGY` | 0.005 kWh | `energy_total` | | `SAT_EPS_PV_W` | 25 W | PV topics | | `SAT_EPS_DURATION` | 1.0 s | duration fields | These thresholds are tunable; they are guideline-level under ADR-111 and may be adjusted based on field feedback without requiring a new ADR. #### Core Control State | Topic | Value | Retained | Description | | ----- | ----- | -------- | ----------- | | `sat/mode` | `"off"` / `"continuous"` / `"pwm"` | yes | Current control mode | | `sat/active` | `"true"` / `"false"` | yes | Whether SAT controller is actively controlling | | `sat/setpoint` | `"43.1"` | yes | Final flow temperature setpoint sent to boiler (°C) | | `sat/target` | `"21.0"` | yes | Target room temperature (°C) | | `sat/heating_curve` | `"42.3"` | yes | Heating curve calculated value (°C) | | `sat/pid_output` | `"43.1"` | yes | PID output = curve + P + I + D (°C) | | `sat/overshoot_margin` | `"2.0"` | yes | Configured overshoot margin (°C) | #### PID Components | Topic | Value | Retained | Description | | ----- | ----- | -------- | ----------- | | `sat/error` | `"0.50"` | no | PID error = target - room temp (°C) | | `sat/pid_p` | `"0.82"` | no | Proportional term (°C) | | `sat/pid_i` | `"0.03"` | no | Integral term (°C) | | `sat/pid_d` | `"-0.04"` | no | Derivative term (°C) | | `sat/pid_attributes` | JSON | no | Extended PID attributes (`kp`, `ki`, `kd`, `p_term`, `i_term`, `d_term`, `raw_derivative`) | | `sat/raw_derivative` | `"-0.04"` | no | Raw (unfiltered) derivative term | | `sat/kp` | `"5.0"` | no | Current Kp gain | | `sat/ki` | `"0.5"` | no | Current Ki gain | | `sat/kd` | `"0.05"` | no | Current Kd gain | | `sat/error_mean` | `"0.12"` | no | Mean PID error over ring buffer (when error monitoring enabled) | | `sat/error_stddev` | `"0.34"` | no | Std deviation of PID error over ring buffer | #### Temperature Sensors | Topic | Value | Retained | Description | | ----- | ----- | -------- | ----------- | | `sat/room_temp` | `"20.8"` | no | Room temperature used by PID (°C) | | `sat/outside_temp` | `"8.0"` | no | Outside temperature used by heating curve (°C) | | `sat/estimated_room` | `"20.5"` | no | Estimated room temp during sensor fallback (°C) | | `sat/humidity` | `"58.0"` | no | Indoor humidity % (when humidity sensor active) | | `sat/humidity_valid` | `"true"` / `"false"` | no | Whether humidity reading is valid | | `sat/comfort_offset` | `"0.5"` | no | Comfort adjustment applied to target (°C) | #### Boiler and Cycle Status | Topic | Value | Retained | Description | | ----- | ----- | -------- | ----------- | | `sat/boiler_status` | `"3"` | no | Boiler status code (see table below) | | `sat/flame_status` | `"healthy"` | no | Flame health status string (see table below) | | `sat/flame_health` | `"ON"` / `"OFF"` | yes | Binary: ON = flame healthy | | `sat/cycle_class` | `"good"` | no | Last cycle classification string (see table below) | | `sat/cycle_attributes` | JSON | no | Extended cycle attributes (`class`, `duration_sec`, `max_flow`, `overshoot_sec`, `kind`, `duty_ratio`) | | `sat/cycle_phase` | `"startup"` / `"steady"` / `"cooldown"` / `"idle"` | no | Current cycle phase | | `sat/pwm_duty` | `"0.65"` | no | PWM duty cycle (0.00-1.00) | | `sat/duty_ratio` | `"0.70"` | no | EMA duty ratio (flame-on fraction) | | `sat/overshoot_fraction` | `"0.12"` | no | EMA fraction of cycles with overshoot | #### System Health and Safety | Topic | Value | Retained | Description | | ----- | ----- | -------- | ----------- | | `sat/safety_tripped` | `"false"` | no | Safety shutdown active | | `sat/valves_open` | `"true"` / `"false"` | no | Whether TRV valves are reported open | | `sat/window_open` | `"false"` | no | Window-open detection state | | `sat/ch_sync` | `"ON"` / `"OFF"` | yes | CH sync health (ON = OK) | | `sat/modulation_reliable` | `"true"` | no | Whether boiler modulation feedback is reliable | | `sat/setpoint_mismatch` | `"false"` | no | Setpoint mismatch between SAT and OT bus detected | #### Pressure Monitoring | Topic | Value | Retained | Description | | ----- | ----- | -------- | ----------- | | `sat/pressure` | `"1.5"` | no | Smoothed system pressure (bar) | | `sat/pressure_drop_rate` | `"-0.02"` | no | Pressure drop rate (bar/hour, negative = dropping) | | `sat/pressure_alarm` | `"false"` | no | Pressure alarm active | | `sat/pressure_health` | `"ON"` / `"OFF"` | yes | Binary: ON = pressure healthy | | `sat/pressure_health_attr` | JSON | no | Extended pressure health attributes | #### Power and Energy | Topic | Value | Retained | Description | | ----- | ----- | -------- | ----------- | | `sat/power` | `"12.5"` | no | Current boiler power (kW, modulation × capacity) | | `sat/energy_total` | `"234.5"` | yes | Cumulative energy (kWh, retained for HA energy dashboard) | #### Heating Curve Recommendation | Topic | Value | Retained | Description | | ----- | ----- | -------- | ----------- | | `sat/curve_recommendation` | `"increase"` / `"decrease"` / `"hold"` / `"insufficient"` | no | Automatic heating curve recommendation | | `sat/curve_recommendation_attributes` | JSON | no | Extended recommendation attributes (error stats, sample count) | #### Presets | Topic | Value | Retained | Description | | ----- | ----- | -------- | ----------- | | `sat/preset_comfort` | `"21.0"` | yes | Comfort preset target temperature (°C) | | `sat/preset_eco` | `"18.0"` | yes | Eco preset target temperature (°C) | | `sat/preset_away` | `"15.0"` | yes | Away preset target temperature (°C) | | `sat/preset_sleep` | `"16.0"` | yes | Sleep preset target temperature (°C) | | `sat/preset_activity` | `"10.0"` | yes | Activity preset target temperature (°C) | | `sat/preset_home` | `"18.0"` | yes | Home preset target temperature (°C) | #### Settings Echo Topics The following topics are published (retained) when SAT settings change. They echo the current configured values. | Topic | Example | Description | | ----- | ------- | ----------- | | `sat/heating_curve_coeff` | `"1.5"` | Heating curve coefficient | | `sat/deadband` | `"0.25"` | PID deadband (°C) | | `sat/control_interval` | `"30"` | Control loop interval (seconds) | | `sat/max_modulation` | `"100"` | Max relative modulation % | | `sat/flame_off_offset` | `"0.0"` | Setpoint offset when flame is off (°C) | | `sat/flow_offset` | `"2.0"` | Continuous mode max setpoint drop (°C) | | `sat/boiler_capacity` | `"24.0"` | Boiler capacity (kW) | | `sat/summer_threshold` | `"18.0"` | Summer simmer outdoor temperature threshold (°C) | | `sat/ovp_value` | `"52.5"` | OPV calibrated maximum flow temperature (°C) | | `sat/ovp_enabled` | `"false"` | Whether OPV is enabled | | `sat/heating_system` | `"1"` | Heating system type (0=auto, 1=radiators, 2=heat pump, 3=underfloor) | | `sat/simulation` | `"ON"` / `"OFF"` | Simulation mode active | | `sat/auto_tune` | `"ON"` / `"OFF"` | Auto-tune analysis active | | `sat/manufacturer` | `"Remeha"` | Detected/configured boiler manufacturer name | Settings echo topics are retained. More settings echo topics exist; the full list is in the [SAT MQTT topics reference](../../backlog/docs/doc-1%20-%20sat-mqtt-topics.md). #### Thermal Model | Topic | Value | Retained | Description | | ----- | ----- | -------- | ----------- | | `sat/thermal_coeff` | `"0.05"` | yes | Learned thermal drop coefficient (°C/hr per °C delta) | | `sat/thermal_drop_rate` | `"0.03"` | no | Current thermal drop rate sample | | `sat/thermal_model_valid` | `"true"` | yes | Whether thermal model has sufficient data | #### Solar Gain | Topic | Value | Retained | Description | | ----- | ----- | -------- | ----------- | | `sat/solar_gain` | `"true"` / `"false"` | no | Solar gain compensation currently active | | `sat/indoor_rise_rate` | `"0.8"` | no | Measured indoor temperature rise rate (°C/hr) | | `sat/solar_gain_sun_elevation` | `"32.5"` | no | Current sun elevation (degrees) | #### Summer Simmer | Topic | Value | Retained | Description | | ----- | ----- | -------- | ----------- | | `sat/summer_active` | `"false"` | no | Summer simmer mode currently suppressing heating | | `sat/summer_hours_above` | `"4.5"` | no | Hours outdoor temp has been above threshold | #### SAT Switch State Topics (TASK-284) State topics for the 13 SAT boolean switches. Payloads are `"true"` / `"false"`. All retained. | Topic | Description | | ----- | ----------- | | `sat/solar_gain_enable` | Solar gain compensation enable state | | `sat/summer_simmer_enable` | Summer simmer enable state | | `sat/comfort_adjust_enable` | Humidity-based comfort adjust enable state | | `sat/multi_area_enable` | Multi-area averaging enable state | | `sat/auto_tune_enable` | Auto-tune analysis enable state | | `sat/simulation_enable` | Simulation mode enable state | | `sat/window_detection_enable` | Window detection enable state | | `sat/force_pwm_enable` | Force-PWM override enable state | | `sat/push_setpoint_enable` | Push-setpoint enable state | | `sat/ovp_enabled` | OPV (overshoot-protection value) enabled state | | `sat/preset_sync_enable` | Preset-sync enable state | | `sat/dhw_enabled` | DHW heating enable state | | `sat/pwm_auto_switch_enable` | Auto-switch between PWM and continuous mode | #### Climate Entity Attributes | Topic | Value | Retained | Description | | ----- | ----- | -------- | ----------- | | `sat/climate_attributes` | JSON | no | HA climate entity JSON attributes (mode, setpoint, heating_curve, pid_output, error, boiler_status, cycle_class, safety_tripped) | #### BLE Temperature Sensor (ESP32 only) These topics are only available when BLE is enabled on ESP32 hardware. | Topic | Value | Retained | Description | | ----- | ----- | -------- | ----------- | | `sat/ble_temp` | `"20.5"` | no | BLE sensor temperature (°C). Best/configured slot only — for multi-sensor visibility see the per-MAC topics below. | | `sat/ble_humidity` | `"55.0"` | no | BLE sensor humidity (%) | | `sat/ble_sensor_rssi` | `"-72"` | no | BLE sensor RSSI (dBm) | | `sat/ble_battery` | `"85"` | no | BLE sensor battery level (%) | | `sat/ble_sensor_count` | `"1"` | no | Number of active BLE sensors seen | | `sat/ble_temp_valid` | `"true"` | no | BLE temperature reading is valid and non-stale | ##### Per-MAC topics (since 2.0.0, TASK-488) Every BLE sensor that is currently in a valid slot publishes its full set of state values under a per-MAC topic prefix. The MAC is rendered as 12 lowercase hex characters with no separators. These topics let multi-sensor deployments see every sensor independently, where the legacy flat topics above only cover the best/configured slot used as SAT control input. | Topic | Value | Retained | Description | | ----- | ----- | -------- | ----------- | | `sat/ble//temp` | `"20.50"` | no | Per-sensor temperature (°C, two decimals) | | `sat/ble//rh` | `"55.20"` | no | Per-sensor humidity (%, two decimals) | | `sat/ble//bat` | `"85"` | no | Per-sensor battery level (%) | | `sat/ble//rssi` | `"-72"` | no | Per-sensor RSSI (dBm) | Example: a Xiaomi LYWSD03MMC at MAC `A4:C1:38:12:34:56` publishes under `OTGW/sat/ble/a4c138123456/temp` etc. The per-MAC topics update at the `iBleInterval` cadence (see `Advanced Settings`) on top of every BLE advertisement that the radio sees (continuous scan since 2.0.0). ##### Home Assistant auto-discovery (since 2.0.0, TASK-488) For each first-seen MAC, the firmware publishes four retained auto-discovery configs to the standard HA prefix so the sensor appears in Home Assistant without manual yaml. Discovery is one-shot per MAC per session; subsequent scans of the same MAC do not re-publish. | Topic | Retained | Description | | ----- | -------- | ----------- | | `/sensor/_ble__temp/config` | yes | Temperature entity, `device_class: temperature`, °C | | `/sensor/_ble__rh/config` | yes | Humidity entity, `device_class: humidity`, % | | `/sensor/_ble__bat/config` | yes | Battery entity, `device_class: battery`, % | | `/sensor/_ble__rssi/config` | yes | Signal-strength entity, `device_class: signal_strength`, dBm | Each config also carries a `device` block grouping the four entities under a single HA device with `model: "BLE Sensor"`, `manufacturer: "BLE"`, and `via_device: `. The state_topic field references the per-MAC topics in the table above. If `bDiscoveryPublished` was set but the publish failed (low heap, MQTT not yet connected, broker hiccup), the helper returns `false` and the caller retries on the next iBleInterval cycle — discovery is never silently dropped (TASK-489 + TASK-493). #### Value Tables **Boiler status codes** (`sat/boiler_status`): 0=Off, 1=Idle, 2=Preheating, 3=At Setpoint, 4=Modulating Up, 5=Modulating Down, 6=Ignition Surge, 7=Stalled Ignition, 8=Anti-Cycling, 9=Pump Starting, 10=Waiting Flame, 11=Overshoot Cooling, 12=Post-Cycle, 13=Heating, 14=Cooling **Cycle class** (`sat/cycle_class`): `none`, `good`, `overshoot`, `underheat`, `short`, `uncertain` **Flame status** (`sat/flame_status`): `insufficient_data`, `healthy`, `idle_ok`, `stuck_on`, `stuck_off`, `pwm_short`, `short_cycling` ### Raw OpenTherm Message (Optional) When `settings.mqtt.bOTmessage` is enabled: | Topic | Value | Description | | ----- | ----- | ----------- | | `otmessage` | `"T80000200"` | Raw OpenTherm message string | ### Event Reports | Topic | Value | Description | | ----- | ----- | ----------- | | `event_report` | text | OTGW event notification messages | ### Error Reports | Topic | Value | Description | | ----- | ----- | ----------- | | `Error 01` | text | Error: line too short | | `Error 02` | text | Error: line too long | | `Error 03` | text | Error: parity check failed | | `Error 04` | text | Error: invalid response type | | `Error_BufferOverflow` | count | Serial buffer overflow counter | ### S0 Pulse Counter Published when S0 counter is enabled (`settings.s0.bEnabled`): | Topic | Value | Description | | ----- | ----- | ----------- | | `s0pulsecount` | `"123"` | Pulse count in current interval | | `s0pulsecounttot` | `"456789"` | Total pulse count since boot | | `s0pulsetime` | `"500"` | Last pulse duration (ms) | | `s0powerkw` | `"1.234"` | Calculated power in kW | ### Dallas Temperature Sensors Published when GPIO sensors are enabled (`settings.sensors.bEnabled`): | Topic | Value | Description | | ----- | ----- | ----------- | | `{sensor_address}` | `"21.5"` | Temperature in Celsius | Where `{sensor_address}` is the Dallas 1-Wire address (e.g., `28FF64D1841703F1`). The address format depends on the `gpiosensorslegacyformat` setting. ### Source-Separated Topics (Optional) When `settings.mqtt.bSeparateSources` is enabled, OpenTherm data is published to two source-specific sibling topics alongside the canonical topic: ``` {TopTopic}/value/{UniqueId}/{label} ← canonical (always published) {TopTopic}/value/{UniqueId}/{label}_thermostat ← when bSeparateSources=true {TopTopic}/value/{UniqueId}/{label}_boiler ← when bSeparateSources=true ``` All three are sibling leaves. Each is a normal MQTT topic — the canonical no longer has children, which makes topic-browser UX (mosquitto_sub, MQTT Explorer) straightforward and removes the structural ambiguity that the earlier nested shape created. #### Topic semantics (2.0.0+, ADR-096 worldview model + ADR-097 sibling-suffix shape) Each per-source topic shows what *that device* sees on the OpenTherm bus, regardless of which side put the frame on the wire. - **`{label}_thermostat`** = the value the thermostat sent (write requests) or received (read responses, including any gateway-faked answer). - **`{label}_boiler`** = the value the boiler received (write requests, including any gateway override) or sent (read responses). - **`{label}` (canonical)** = boiler-side worldview, identical to `{label}_boiler` when both are published. Override behavior is implicit: when the two source topics diverge for the same metric, the gateway is intervening. **Example: `TSet` with `CS=27.37` setpoint override active, thermostat asking 23 °C:** | Topic | Value | Meaning | |---|---|---| | `…/value//TSet_thermostat` | `23.00` | What the thermostat asked for | | `…/value//TSet_boiler` | `27.37` | What the boiler actually received | | `…/value//TSet` (canonical) | `27.37` | What was actually transmitted (= boiler-side worldview) | Without override, all three publish the same value. `_thermostat` and `_boiler` always update independently regardless of override state — `_thermostat` keeps showing the thermostat's intent, `_boiler` keeps showing what reaches the boiler. #### Frame-to-topic routing reference | OT frame | Direction | Routes to `_thermostat` | Routes to `_boiler` | Routes to canonical | |---|---|---|---|---| | `T` (thermostat-write) | M→S | yes | yes (when no R follows) | yes (when no R follows) | | `R` (gateway-substituted write) | M→S | no | yes | yes | | `B` (boiler-response) | S→M | yes (when no A follows) | yes | yes | | `A` (gateway-faked answer) | S→M | yes | no | no | There is no `_gateway` topic — override is visible by comparing `_thermostat` and `_boiler`. #### Canonical-topic publish gating (ADR-097, preserved) The canonical topic `{TopTopic}/value/{UniqueId}/{label}` does not receive Write-Ack frames. The `_boiler` topic is additionally gated by a per-MsgID `bSlaveEchoesValue` flag in the OTlookup table. For MsgIDs where the OpenTherm v4.2 specification defines the slave's Write-Ack data field as undefined (typically `Tr` 24, `TrSet` 16, `MaxRelModLevelSetting` 14, `TrSetCH2` 23, `TRoomCH2` 37, `RFstrengthbatterylevel` 98), the `_boiler` topic is NOT updated for Write-Ack messages. The slave's acknowledgement carries no measurement; suppressing it avoids polluting the per-source observability surface with fake-zero readings. For MsgIDs where the slave does store and echo the value (most R/W parameters, Class 5 remote boiler parameters such as `MaxTSet` 57 and `TdhwSet` 56, Class 6 transparent slave parameters, R/W counters), the `_boiler` topic continues to publish the slave's stored value, including clamped or modified variants distinct from the master's request. See `docs/api/MQTT-message-id-echo-audit.md` for the full per-MsgID classification with spec-citation rationale. #### Migration note (2.0.0 topic-shape transition) Two changes shipped in successive 2.0.0 alpha builds. Migration guidance: 1. **Worldview routing (ADR-096, alpha.3).** Earlier builds routed `A` (gateway-faked answer) frames to `/boiler` and dropped `T` frames during gateway override. ADR-096 corrected both: `A` now routes to the thermostat topic (where the value actually arrives), and `T` is preserved on the thermostat topic and canonical even when the gateway substitutes a different value to the boiler. 2. **Sibling-suffix shape (ADR-097, alpha.4+).** Earlier builds used nested children topics (`{label}/thermostat`, `{label}/boiler`). ADR-097 replaces these with siblings (`{label}_thermostat`, `{label}_boiler`) so the canonical topic is a clean leaf without children. Discovery configs are auto-updated on boot — Home Assistant unsubscribes from the old topic and subscribes to the new one in place (verified against `homeassistant/components/mqtt/subscription.py`). The mutual-exclusion rule from ADR-095 is dropped; the canonical entity now stays advertised alongside the two source variants. **Cleanup of stale retained values:** old retained values at the previous nested topics (`{label}/thermostat`, `{label}/boiler`) linger on the broker as orphans because the firmware no longer publishes there. Home Assistant ignores them after discovery refresh (it has unsubscribed). Users with broker access can clear them with: ```bash mosquitto_pub -h -t '/value//