Updating deep sleep devices

Let me preface this by acknowledging this isn’t the most elegant solution, but it works for me and I hope it may help others.

I have a number of devices that spend the vast majority of their time in deep sleep, only occasionally waking up to take measurements. And some of these are in places that I don’t want to have to disturb (eg humidity sensors in sealed containers). So how can I reliably update these sensors when their boot time can be unpredictable (deep sleep’s clock isn’t great), and I don’t want to be jumping out of bed at 3:00am to update a device?

I took inspiration from Tatham Oddie’s great guide to keeping devices awake when you want to do maintenance and added to it.

My approach is as follows: Have a toggle in HA that tells a device to stay awake, listen for when the device next boots, then call the compile & OTA flash function to update the device.

You’ll need Node-Red, MQTT and ESPHome running in a separate Docker instance (ie not the one integrated in HA). Note that throughout this example I have used generic terms – you would customise the names in production to individually control multiple devices.

First the client config.

esphome:
  name: esphome-web-d91c58
  on_boot:
    - script.execute: consider_deep_sleep

esp32:
  board: pico32
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

time:
  - platform: sntp
    id: sntp_time
    servers: <removed>
    on_time:
      - seconds: /5
        then:
          - logger.log: "Test number 1"  #Just something to print so that you can track if it did properly update.  Increment as you test.

binary_sensor:
  - platform: homeassistant
    id: prevent_deep_sleep
    entity_id: input_boolean.prevent_deep_sleep

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: <removed>
    password: <removed>

captive_portal:

deep_sleep:
  id: deep_sleep_control
  sleep_duration: 5s


mqtt:
  broker: <removed>
  discovery: false   #Important so that the device is only seen via HA API
  birth_message:
    topic: IOT/sleep-test
    payload: awake
    retain: false

script:
  - id: consider_deep_sleep
    mode: queued
    then:
      - delay: 30s
      - if:
          condition:
            binary_sensor.is_on: prevent_deep_sleep
          then:
            - logger.log: 'Skipping sleep, per prevent_deep_sleep'
          else:
            - deep_sleep.enter: deep_sleep_control
      - script.execute: consider_deep_sleep

After adding the device, go into HA → Configuration → Automation & Scenes → Helpers, and create an appropriately named toggle helper. Then edit it, and give it the same entity id that you defined in the binary_sensor above. Then add this to a LoveLace card for easy toggling.

At this point, the device will be sleeping & waking regularly, and publishing when it wakes via MQTT. If you use the toggle in HA, it will stay awake. Great start.

Now onto NodeRed.

Summarised, we are listening on the Birth Message topic, then checking if the input_boolean.prevent_deep_sleep is set, running a shell command to call the CLI of ESPHome, listening for the ReturnCode, and either sending an error if it failed or turning off the input_boolean.prevent_deep_sleep if successful.

The Exec command is:

ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i /config/.ssh/id_rsa [email protected] "docker exec -t <ESPHome-ContainerName> esphome run esphome-web-d91c58.yaml --no-logs"

You’ll naturally need to tailor the filename for your purposes.

The full NodeRed export is:

[{"id":"8e6ea03e8cb283d9","type":"exec","z":"4aa4900726a8a246","command":"ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i /config/.ssh/id_rsa [email protected] \"docker exec -t esphome esphome run esphome-web-d91c58.yaml --no-logs\"","addpay":"","append":"","useSpawn":"false","timer":"","winHide":false,"oldrc":false,"name":"Shell","x":630,"y":820,"wires":[[],[],["ec5a6ce5832a5991"]]},{"id":"cac9271ca3a383a5","type":"api-call-service","z":"4aa4900726a8a246","name":"Turn off toggle","server":"586184e.ace327c","version":1,"debugenabled":false,"service_domain":"input_boolean","service":"turn_off","entityId":"input_boolean.prevent_deep_sleep","data":"","dataType":"json","mergecontext":"","output_location":"payload","output_location_type":"none","mustacheAltTags":false,"x":1100,"y":840,"wires":[[]]},{"id":"ec5a6ce5832a5991","type":"switch","z":"4aa4900726a8a246","name":"Success?","property":"payload.code","propertyType":"msg","rules":[{"t":"eq","v":"0","vt":"num"},{"t":"neq","v":"0","vt":"num"}],"checkall":"true","repair":false,"outputs":2,"x":820,"y":840,"wires":[["cac9271ca3a383a5"],["e7c8a72f1e2ca53b"]]},{"id":"f6aeb9d1aed03ac2","type":"mqtt out","z":"4aa4900726a8a246","name":"","topic":"IOT/alert","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"e7a92ea.adecdd","x":1360,"y":900,"wires":[]},{"id":"e7c8a72f1e2ca53b","type":"change","z":"4aa4900726a8a246","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"Update of node1 failed","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1100,"y":900,"wires":[["f6aeb9d1aed03ac2"]]},{"id":"408458d6a6a93286","type":"mqtt in","z":"4aa4900726a8a246","name":"","topic":"IOT/sleep-test","qos":"2","datatype":"auto","broker":"e7a92ea.adecdd","nl":false,"rap":true,"rh":0,"inputs":0,"x":130,"y":820,"wires":[["abeda55d6d723eae"]]},{"id":"abeda55d6d723eae","type":"api-current-state","z":"4aa4900726a8a246","name":"Update waiting?","server":"586184e.ace327c","version":1,"outputs":2,"halt_if":"on","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"input_boolean.prevent_deep_sleep","state_type":"str","state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":420,"y":820,"wires":[["8e6ea03e8cb283d9"],[]]},{"id":"586184e.ace327c","type":"server","name":"Home Assistant","addon":true},{"id":"e7a92ea.adecdd","type":"mqtt-broker","name":"","broker":"192.168.1.x","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]

Now you just need to make the config changes (or upgrades) as necessary, save them, and then turn on the toggle. When the device next boots it will upgrade all by itself.

I hope this is helpful to someone who faces the same challenge!