Help with MQTT on_message for deepsleep mode

So I am trying to toggle OTA mode for deepsleep with a MQTT message. I have looked at the guides, but all use a different topic for ON vs OFF. It seems like you could do this with the same topic and make it cleaner but for the life of me I can’t get it to work.

Here is my ESPHome config for this:


mqtt:
  broker: !secret mqtt_broker
  username: !secret mqtt_username
  password: !secret mqtt_password
  topic_prefix: ESPHome/$devicename
  birth_message:
    topic: "$upper_devicename/topic"
    payload: 
  will_message:
    topic: "$upper_devicename/topic"
    payload: 
  on_message:
    - topic: ESPHome/ota_mode_on/$devicename
      payload: 'ON'
      then:
        - logger.log: OTA Mode set to ON from MQTT
        - deep_sleep.prevent: deep_sleep_1
    - topic: ESPHome/ota_mode_on/$devicename
      payload: 'OFF'
      then:
        - logger.log: OTA Mode set to OFF from MQTT
        - deep_sleep.enter: deep_sleep_1

So if I set it to ON things work, if I set it to OFF it immediately deepsleeps the device as it should, however when it boots back up from deep sleep it tells me the OTA Mode is set to ON, even though the MQTT topic payload still states it is OFF. No messages are set as persistent and QOS is 0 on the message. My guess is this is why people use 2 topics, however this seems like a bug, or maybe I am missing something?

Any help would be appreciated.

I spent a lot (A LOT!) of time trying to figure out exactly what you are trying to do. It didn’t help that I wasn’t quite sure what I wanted as I iterated. I have it working in a satisfactory way now.

The tricky part, as you probably already know, is that you have to use a retained MQTT message so that your device will still receive it when it wakes up. But, if the message is retained, it will still be there forever, even after the reboot after your OTA update.

My solution was to give up trying to do it with mqtt: on_message: (which has the convenience of checking the payload for you.) Instead, I use an mqtt_subscribe text sensor. That preserves the payload in the sensor’s state variable. When it sees the “stay awake” message, it sends a new retained message to that topic to clear out the “stay awake” message. It’s a bit inconvenient because of the cumbersome YAML nesting of if/then/else, but here’s what it looks like:

text_sensor:
  - platform: mqtt_subscribe
    name: "deep sleep control"
    id: m_deepsleep
    topic: ${node_name}/deepsleep
    internal: true
    qos: 2
    on_value:
      then:
        - if:
            condition:
              text_sensor.state:
                id: m_deepsleep
                state: "prevent ${mqtt_secretword}"
            then:
              - logger.log: "deepsleep prevent"
              - globals.set:
                  id: can_sleep
                  value: 'false'
              - mqtt.publish:
                  topic: ${node_name}/deepsleep
                  payload: 'ignore me'
                  retain: true
              - mqtt.publish:
                  topic: ${node_name}/status
                  payload: '${node_name} ===================='
              - mqtt.publish:
                  topic: ${node_name}/status
                  payload: '${node_name} AWAKE! AWAKE! AWAKE!'
              - mqtt.publish:
                  topic: ${node_name}/status
                  payload: '${node_name} ===================='
            else:
              - if:
                  condition:
                    text_sensor.state:
                      id: m_deepsleep
                      state: "enter ${mqtt_secretword}"
                  then:
                    - logger.log: "deepsleep enter"
                    - delay: 1s
                    - globals.set:
                        id: can_sleep
                        value: 'true'
                  else:
                    - if:
                        condition:
                          text_sensor.state:
                            id: m_deepsleep
                            state: 'ignore me'
                        then:
                        else:
                          - logger.log:
                              level: WARN
                              format: 'BOGUS ${node_name}/deepsleep message: %s'
                              args: [ id(m_deepsleep).state.c_str() ]

Since my deep sleep is 20 minutes long for this device, I have a distinctive message emitted in the MQTT status stream for the device to try to get me to notice when it finally wakes up. Otherwise, it will just stay awake and use up the battery that I’m trying to preserve with the deep sleep in the first place.

I’ve been using mosquitto_pub to send the messages and mostquitto_sub to monitor things. I send the “stay awake” message as retained, but the “go to sleep” message is non-retained. That’s something of a holdover from when the config was organized a bit differently, and receiving the “go to sleep” message would trigger an immediate deep sleep. A retained message would make that happen every time the device woke up, before it had a chance to do real work.

I’m not making the prevent and enter calls directly in that text sensor. Instead, I’m using a global boolean variable. The reason for that is because I use an esphome: on_boot trigger to read my environmental sensor (a BME280 with an update_interval: never) as soon as possible and then go back to sleep. The code won’t go back to sleep if the global boolean tells it not to. That looks like this:

  on_boot:
    then:
      - deep_sleep.prevent
      - while:
          condition:
            not:
              mqtt.connected:
          then:
            - delay: 0.2s
      - logger.log:
          level: INFO
          format: "MQTT is connected"

      - component.update: i_bme280
      
      - while:
          condition:
            lambda: 'return !id(can_sleep);'
          then:
            - delay: 1s

      - mqtt.publish:
          topic: ${node_name}/status
          payload: '${node_name} about to go to sleep for ${deepsleep_sleep}'
      - delay: 1s
      - deep_sleep.enter

There is probably a less convoluted way to go about this, but I think I spent enough time fiddling with it already. Maybe someday I’ll go back and squeeze out some of those delays.

1 Like

The core of your problem was you were using deep_sleep.enter in response to your ESPHome/ota_mode_on/$devicename OFF message. This makes it immediately enter deep_sleep, so if you send a retained OFF message the device will go to sleep as soon as it boots and mqtt loads the retained messages.

What you actually want is deep_sleep.allow which will re-enable your deep_sleep_1 sleep/wake cycle. This way you can send a retained ON message to turn on OTA mode and stop sleeping, and send a retained OFF message to turn off OTA mode and re-enable sleeping. Note typically when you send the OFF message the device immediately goes to sleep because you are way-past it’s bed-time when you send it.