Retaining specific states from MQTT sensors across Home Assistant restarts using retained messages

Retaining specific states from MQTT sensors across Home Assistant restarts using retained messages
Photo by zhang kaiyv / Unsplash

Introduction

One of the great things about Home Assistant is its ability to integrate data from virtually anywhere. This flexibility allows you to choose the best sensor for each use case, regardless of the protocol it uses. From Zigbee and Z-Wave to MQTT and beyond, Home Assistant makes it possible to unify your smart home ecosystem seamlessly.

For example, in my smart home setup, I use Govee H5054 Leak Detectors for water leak detection. These sensors are excellent because they emit a loud audible alarm, are relatively inexpensive (~$10 on sale), have great battery life (1+ years) and of course, communicate with Home Assistant. Although not advertised, these sensors operate over the 433 MHz frequency, which I capture using a Nooelec's RTL-SDR software-defined radio (SDR).

Capturing 433Mhz sensor readings and landing them into Home Assistant

To process the 433 MHz signals, I run the outstanding rtl_433 project on a Raspberry Pi. This tool collects data from the SDR and forwards it to my MQTT broker (I use EMQX, though Mosquitto is another popular option). From there, Home Assistant subscribes to the MQTT topics using an YAML-defined MQTT sensor and updates the states of my entities accordingly.

mqtt:
  binary_sensor:
    - state_topic: rtl_433/Govee-Water/28604/detect_wet
      json_attributes_topic: rtl_433/Govee-Water/28604
      device_class: moisture
      unique_id: dishwasher_leak_28604_moisture
      name: Dishwasher leak moisture
      payload_on: 1
      payload_off: 0
      off_delay: 30
      device:
        identifiers: h5054-28604
        manufacturer: Govee
        name: Dishwasher water leak
        model: H5054
        suggested_area: Kitchen
    - state_topic: rtl_433/Govee-Water/28604/battery_ok
      json_attributes_topic: rtl_433/Govee-Water/28604
      device_class: battery
      value_template: "{{ (value_json | float * 100) | int }}"
      unique_id: dishwasher_leak_28604
      name: Dishwasher leak battery level
      unit_of_measurement: "%"
      state_class: "measurement"
      device:
        identifiers: h5054-28604
        manufacturer: Govee
        name: Dishwasher water leak
        model: H5054
        suggested_area: Kitchen

Here I define the wet/dry entity, and the battery level entity for the Govee H5054

The challenge

However, integrating these sensors into Home Assistant presents a challenge: the default behavior of MQTT. Since MQTT is a publish/subscribe protocol, Home Assistant only updates a sensor’s state when a new message is published while it is subscribed to the topic. After a restart, the sensor’s state defaults to ‘Unknown’ until the next reading is emitted by the Govee. For sensors like the H5054, which only send battery reports infrequently (triggered by a significant change, like a 5% drop), this means the state could remain ‘Unknown’ for months. This leaves a gap in reporting until the next reading comes in, only to create another gap starting the next Home Assistant restart or MQTT sensor reload.

Home Assistant has no way to get the current state of an MQTT sensor until it sends a reading.
For H5054, it can take a few months to get the next battery level, so the entity state will remain unknown until then.

Having all rtl_433 messages come in as retained creates other issues

One option is to configure rtl_433 to publish all MQTT messages with the retain flag set to true. This means that the most recent message for each topic is always available to new MQTT clients, including Home Assistant after a restart. However, this approach has a significant downside. If you have 433 MHz remotes or buttons, their ‘pressed’ states will also be retained, potentially triggering automations upon every Home Assistant restart. This is clearly not ideal. It also means you retaining all sorts of things that have no value (such as timestamps, randomly generated message IDs, etc)

My approach: selectively retaining MQTT messages by republishing them with HA

To solve this, I have an automation to selectively retain MQTT messages for specific sensor readings, such as battery levels and 'no leak' readings, while avoiding retained messages for other events like leak alerts (or button presses on other sensors that also come in via rtl_433)

Here’s how it works:

Battery level entity state retention

For battery levels, I created a Home Assistant automation that listens to the topic where the sensor publishes its battery reports. When a matching message is received, the automation extracts the battery level and republishes it to the same topic with the retain flag set to true. This ensures that Home Assistant always has the last known battery level available, even after a restart.

alias: Retain Govee water battery level message
description: Republish Govee Water battery level message back to MQTT with retain flag
triggers:
  - topic: rtl_433/Govee-Water/#
    trigger: mqtt
    payload: Battery Report
    value_template: "{{ value_json.event }}"
conditions: []
actions:
  - data:
      topic: "{{ trigger.topic }}/battery_ok"
      payload: |
        {{ trigger.payload_json.battery_ok }}
      retain: true
    action: mqtt.publish
mode: single

'No leak' entity state retention

For water leak detection, the H5054 sensors emit ‘wet’ or 'not wet' readings. To ensure the default state remains ‘dry’ without retaining every message, I publish a retained wet_detect=0 message whenever a ‘dry’ reading is received. This way, Home Assistant assumes the default state is ‘not wet’ when it connects and only triggers leak alerts when the sensor publishes a wet_detect=1 message (indicating a leak is present).

alias: Retain Govee Water dry message
description: Republish Govee Water dry message back to MQTT with retain flag
triggers:
  - topic: rtl_433/Govee-Water/#
    trigger: mqtt
    payload: 0
    value_template: "{{ value_json.detect_wet }}"
  - topic: rtl_433/Govee-Water/#
    trigger: mqtt
    payload: Button Press
    value_template: "{{ value_json.event }}"
conditions: []
actions:
  - data:
      topic: "{{ trigger.topic }}/detect_wet"
      payload: |
        {{ trigger.payload_json.detect_wet }}
      retain: true
    action: mqtt.publish
mode: single

When a client (in this case, Home Assistant) connects to MQTT and subscribes to the topics, they receive the retained messages and can load initial states.

Conclusion

By selectively applying the retain flag through Home Assistant automations, I’ve been able to create a setup that ensures reliable state retention for key sensors while avoiding the downsides of globally retained MQTT messages. This approach provides the best of both worlds: accurate state tracking across restarts and minimal risk of unintended automation triggers.

This method has significantly improved the reliability and usability of my smart home, and I hope it inspires others to tackle similar challenges in their setups. If you have other creative solutions or enhancements, feel free to share them!