ESP32-H2 (Thread) with Deep Sleep for Cheap ESP32 Battery Sensors

Hi! I am playing with thread and wanted to see if I could use battery powered sensors where it’s inconvenient to have cables. This project demonstrates how to do it using deep sleep. I literally just tried if I am able to read some values, send them to HA via thread, let it go to deep sleep, repeat, and do that on battery. I am launching the test today, who knows when the battery get drained, will try to report here. My hopes are that it could last for months with sensible readings of temperature every e.g. 5 minutes.

I have the following setup:

  1. LiOn Battery (3.7V) Original 18650 Battery NCR18650B 3.7v 3400mAh Lithium Rechargeable with a 18650 Battery Holder Box 1 Slot 3.7V
  2. ESP32-H2 Supermini (Thread only device)
  3. ESPHome 2025.7.5
  4. BME280 for temp measurement (not shown in the diagram below)

Overall about $15. “Cheapest” zigbee Aqara temp sensor is about $13, for comparison (they lasted about 3 years and then simply died).

There are already some threads about esp32-h2 like here, my ESP32 border router is described here.

Wiring

The best is soldering the battery holder connectors to the B+/B- from the bottom of the pad. It has integrated voltage regulator etc., so will be the most efficient and you save yourself an extra component.

If you don’t want to solder, you can use what I used originally: Voltage regulator from 3.7V (battery) → 3.3V (ESP32 3v pin), AMS1117 like this:

ESPHome Config

It’s clearly not minimal, and some things IMHO do not work at all. But it’s a start, and my hope is that people can try and build on top of it etc. I haven’t found many concrete examples, hence writing this.

This config is not production ready, you almost surely want to tweak the deep sleep time interval, add your own sensors (right now this shows cpu temp, heap, uptime, to demonstrate some readings, what I want to add here is just BME280), and so on.

esphome:
  name: h2beta
  friendly_name: Thread H2 Beta
  # Boot actions to increment wake counter
  on_boot:
    priority: 600  # Run early
    then:
      - globals.set:
          id: wake_count
          value: !lambda 'return id(wake_count) + 1;'
      - component.update: sleep_cycle_count
      - component.update: wakeup_cause
      # Blink blue LED on wake to show activity
      - light.turn_on: blue_led_light
      - delay: 500ms
      - light.turn_off: blue_led_light
      - delay: 200ms
      - light.turn_on: blue_led_light
      - delay: 500ms
      - light.turn_off: blue_led_light

esp32:
  board: esp32-h2-devkitm-1 
  variant: ESP32H2
  framework:
    type: esp-idf

# Enable logging - note that I couldn't get logs on h2 serial via USB, I only could see some via mqtt
logger:
  level: DEBUG

# not necessary, was somewhat useful for testing
mqtt:
  broker: 192.168.0.202
  username: z2m
  password: <pwd>
  topic_prefix: null
  log_topic: h2test

api:

ota:
  - platform: esphome

network:
  enable_ipv6: true

# Deep Sleep Configuration
deep_sleep:
  run_duration: 30s
  sleep_duration: 60s
  id: deep_sleep_control

# Global variables for persistent wake counter
globals:
  - id: wake_count
    type: int
    restore_value: yes  # This persists across deep sleep
    initial_value: '0'

# Blue LED output (GPIO13)
output:
  - platform: gpio
    pin: 13
    id: blue_led

# Blue LED light control
light:
  - platform: binary
    name: "Blue LED"
    id: blue_led_light
    output: blue_led
    restore_mode: RESTORE_DEFAULT_OFF

# interval:
#   - interval: 2s
#     then:
#       - logger.log: "Heartbeat - ESP32-h2-beta is alive!"

openthread:
  tlv: <...fill in...>
  force_dataset: true

text_sensor:
  - platform: openthread_info
    ip_address:
      name: "Off-mesh routable IP Address"
    channel:
      name: "Channel"
    role:
      name: "Device Role"
    rloc16:
      name: "RLOC16"
    ext_addr:
      name: "Extended Address"
    eui64:
      name: "EUI64 Interface ID"
    network_name:
      name: "Network Name"
    network_key:
      name: "Network Key"
    pan_id:
      name: "PAN ID"
    ext_pan_id:
      name: "Extended PAN ID"

  # Deep sleep wakeup cause sensor
  - platform: template
    name: "Wakeup Cause"
    id: wakeup_cause
    update_interval: never  # Only update once on boot
    lambda: |-
      auto cause = esp_sleep_get_wakeup_cause();
      switch (cause) {
        case ESP_SLEEP_WAKEUP_UNDEFINED: return std::string("Reset/Power On");
        case ESP_SLEEP_WAKEUP_TIMER: return std::string("Timer");
        case ESP_SLEEP_WAKEUP_EXT0: return std::string("External 0");
        case ESP_SLEEP_WAKEUP_EXT1: return std::string("External 1");
        case ESP_SLEEP_WAKEUP_TOUCHPAD: return std::string("Touchpad");
        case ESP_SLEEP_WAKEUP_ULP: return std::string("ULP");
        case ESP_SLEEP_WAKEUP_GPIO: return std::string("GPIO");
        case ESP_SLEEP_WAKEUP_UART: return std::string("UART");
        default: return std::string("Unknown (" + to_string(cause) + ")");
      }

sensor:
  # Internal temperature sensor (simulates external temperature sensor)
  - platform: internal_temperature
    name: "CPU Temperature"
    id: cpu_temp
    update_interval: 5s

  # Free heap memory sensor
  - platform: template
    name: "Free Heap"
    id: free_heap
    lambda: |-
      return heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
    unit_of_measurement: "bytes"
    accuracy_decimals: 0
    update_interval: 5s

  # Uptime sensor (will reset each wake cycle)
  - platform: uptime
    name: "Uptime Since Wake"
    id: uptime_sensor
    update_interval: 5s

  # Deep sleep cycle counter (stored in global variable that persists)
  - platform: template
    name: "Sleep Cycle Count"
    id: sleep_cycle_count
    accuracy_decimals: 0
    update_interval: never  # Only update once on boot
    lambda: |-
      return (float)id(wake_count);

switch:
  # Switch to prevent deep sleep (useful for debugging/OTA updates)
  - platform: template
    name: "Prevent Deep Sleep"
    id: prevent_sleep_switch
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF
    turn_on_action:
      - deep_sleep.prevent: deep_sleep_control
      - logger.log: "Deep sleep prevented"
    turn_off_action:
      - deep_sleep.allow: deep_sleep_control
      - logger.log: "Deep sleep allowed"

# Optional: Manual deep sleep trigger
button:
  - platform: template
    name: "Enter Deep Sleep Now"
    on_press:
      - logger.log: "Manually entering deep sleep..."
      - delay: 1s
      - deep_sleep.enter: deep_sleep_control

# Log sensor readings before going to sleep
# I don't think this work
interval:
  - interval: 20s  # Trigger X seconds before sleep
    then:
      - logger.log: 
          format: "Sensor readings - CPU: %.1f°C, Heap: %.0f bytes, Uptime: %.1fs"
          args: ['id(cpu_temp).state', 'id(free_heap).state', 'id(uptime_sensor).state']
      - logger.log: "Preparing for deep sleep in 10 seconds..."
      # Dim the blue LED as a visual indicator before sleep
      - light.turn_on: 
          id: blue_led_light
          brightness: 50%
      - delay: 3s
      - light.turn_off: blue_led_light

Flashing this config to the device results in reporting the metrics inside home assistant (to which I added this device before, described here)

Various findings

  1. I am flashing manually via CLI from my Mac as e.g. uv run esphome run h2-sleep.yaml
  2. OTA also seemed to work fine, but is of course a bit finicky unless you manage to switch of the deep sleep entry - you can’t flash when it’s sleeping.
  3. find the ipv6 address in homeassistant under Off-mesh routable IP Address , and do ping6 <address>. You can see when the device is up and connected. It takes only 2-5 seconds for me for the ping to start receiving reply (pretty cool that this is possible with thread, and that the device is so fast). You should see e.g.:
$ ping6 fd84:c977:29b2:1:7cbe:7175:21b8:be55
PING6(56=40+8+8 bytes) fd49:e5fc:83af:0:c41:5092:6308:5a0f --> fd84:c977:29b2:1:7cbe:7175:21b8:be55
16 bytes from fd84:c977:29b2:1:7cbe:7175:21b8:be55, icmp_seq=143 hlim=254 time=616.515 ms
16 bytes from fd84:c977:29b2:1:7cbe:7175:21b8:be55, icmp_seq=144 hlim=254 time=40.652 ms
  1. with mqtt, I see this:
$ mosquitto_sub -h 192.168.0.202 -u z2m -P <mypwd> -t "h2test" --pretty -F "%t>%p"
h2test>[D][esp32.preferences:142]: Writing 1 items: 0 cached, 1 written, 0 failed
h2test>[W][component:307]: mqtt cleared Warning flag
h2test>[I][mqtt:310]: Connected
h2test>[W][component:407]: mqtt took a long time for an operation (113 ms)
h2test>[W][component:408]: Components should block for at most 30 ms
h2test>[D][sensor:104]: 'Uptime Since Wake': Sending state 21.55600 s with 0 decimals of accuracy
h2test>[D][sensor:104]: 'CPU Temperature': Sending state 22.00000 °C with 1 decimals of accuracy
h2test>[D][sensor:104]: 'Free Heap': Sending state 136424.00000 bytes with 0 decimals of accuracy
h2test>[D][esp32.preferences:142]: Writing 3 items: 2 cached, 1 written, 0 failed
h2test>[D][sensor:104]: 'Uptime Since Wake': Sending state 26.55800 s with 0 decimals of accuracy
h2test>[D][sensor:104]: 'CPU Temperature': Sending state 22.00000 °C with 1 decimals of accuracy
h2test>[I][deep_sleep:062]: Beginning sleep



h2test>[W][component:307]: mqtt cleared Warning flag
h2test>[I][mqtt:310]: Connected
h2test>[W][component:407]: mqtt took a long time for an operation (121 ms)
h2test>[W][component:408]: Components should block for at most 30 ms
h2test>[D][sensor:104]: 'CPU Temperature': Sending state 21.00000 °C with 1 decimals of accuracy
h2test>[D][sensor:104]: 'Uptime Since Wake': Sending state 20.97000 s with 0 decimals of accuracy
h2test>[D][light:052]: 'Blue LED' Setting:
h2test>[D][light:065]:   State: OFF
h2test>[D][sensor:104]: 'Free Heap': Sending state 136428.00000 bytes with 0 decimals of accuracy
h2test>[D][sensor:104]: 'CPU Temperature': Sending state 21.00000 °C with 1 decimals of accuracy
h2test>[D][sensor:104]: 'Uptime Since Wake': Sending state 25.97400 s with 0 decimals of accuracy
h2test>[I][deep_sleep:062]: Beginning sleep
h2test>[I][deep_sleep:062]: Beginning sleep

as you can see, it takes quite some time before it actually connects and starts logging to mqtt. I don’t know why but I see it 26 seconds after the wakeup, which is quite late.

  1. It does take up maybe a dozen of seconds before the data show up in homeassistant.

Overall, this is pretty fun and already somewhat easier to work with than with zigbee devices that I use. With those, I have virtually no “visibility” than “coordinator knows about these”. But on thread, I can just ping the devices, or let them directly talk to mqtt or HA, which is neat. And I am a big fan of ESPHome, which is just a crazy beast. LLMs can write the spec flawlessly (as long as you just copy paste the relevant - usually well written - docs)

Good luck!

3 Likes

Hi, if your battery has protection circit you can connect it to the battery connectors on the under side of ESP32-H2 and save components and power :slight_smile: //Björn

1 Like

oh, shoot, that seems even better, thanks for the tip

Yes, works like a charm, the regulator is not needed. It was a bit tricky for me to solder it (the pads dissipated heat too fast?).

This is my full config now with BME280:

esphome:
  name: h2beta
  friendly_name: Thread H2 Beta
  on_boot:
    priority: 800
    then:
      # Increment wake counter first
      - globals.set:
          id: wake_count
          value: !lambda 'return id(wake_count) + 1;'
      # Take readings
      - component.update: sleep_cycle_count
      - delay: 20s
      - deep_sleep.enter: deep_sleep_control

esp32:
  board: esp32-h2-devkitm-1 
  variant: ESP32H2
  framework:
    type: esp-idf

# Minimal logging to save power
logger:
  level: DEBUG  
  # baud_rate: 0  # Disable UART completely to save power

# Minimal API for connectivity
api:

ota:
  - platform: esphome

network:
  enable_ipv6: true

# Deep Sleep - wake every 5 minutes
deep_sleep:
  run_duration: 60s  # Just enough to connect and send
  sleep_duration: 60s 
  id: deep_sleep_control

# Thread configuration
openthread:
  tlv: ...
  force_dataset: true

# SPI for BME280
spi:
  clk_pin: GPIO10
  mosi_pin: GPIO11
  miso_pin: GPIO12

globals:
  - id: wake_count
    type: int
    restore_value: yes  # This persists across deep sleep
    initial_value: '0'

sensor:
  - platform: bme280_spi
    temperature:
      name: "Temperature"
      id: bme_temp
      oversampling: 1x  # Minimal oversampling for power saving
    pressure:
      name: "Pressure"
      id: bme_pressure
      oversampling: 1x
    humidity:
      name: "Humidity"
      id: bme_humidity
      oversampling: 1x
    cs_pin: GPIO14
    iir_filter: "OFF"  # No filtering to save processing
    update_interval: 5s
  
  - platform: template
    name: "Sleep Cycle Count"
    id: sleep_cycle_count
    accuracy_decimals: 0
    update_interval: never  # Only update once on boot
    lambda: |-
      return (float)id(wake_count);

# note we have only brief to turn off
switch:
  - platform: template
    name: "Prevent Deep Sleep"
    id: ota_mode
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF
    turn_on_action:
      - deep_sleep.prevent: deep_sleep_control
    turn_off_action:
      - deep_sleep.allow: deep_sleep_control

text_sensor:
  - platform: openthread_info
    ip_address:
      name: "Off-mesh routable IP Address"
  1. you must be quick to toggle the switch to prevent deep sleep if you want to do OTA updates
  2. With deep sleep longer than a minute, it seems to take longer to reestabilish the connection. E.g. It seems that the node is pingable only in the last 20 seconds of the whole 60 seconds period, so, it takes to reconnect maybe up to 30-40 seconds?
  3. usually I need to reconnect the board before pushing new updates otherwise it’s not found probably due to deepsleep?
  4. you really won’t get any logs via serial, I can only see them when connecting OTA.
  5. It also seems to be… unreliable? It seems that it sometimes skips the loop entirely, maybe because thread kicks it off the network, see the gaps - it should be a minute, but sometimes it doesn’t get through for full cycle minutes:
INFO Processing unexpected disconnect from ESPHome API for h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55
WARNING Disconnected from API
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Trying to connect to h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in the background
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully connected to h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.058s
INFO Successful handshake with h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.115s
[16:22:51][D][esp32.preferences:142]: Writing 1 items: 1 cached, 0 written, 0 failed
[16:22:54][D][sensor:104]: 'Temperature': Sending state 24.03078 °C with 1 decimals of accuracy
[16:22:54][D][sensor:104]: 'Pressure': Sending state 986.51733 hPa with 1 decimals of accuracy
[16:22:54][D][sensor:104]: 'Humidity': Sending state 51.17188 % with 1 decimals of accuracy
[16:22:59][D][sensor:104]: 'Temperature': Sending state 24.05129 °C with 1 decimals of accuracy
[16:22:59][D][sensor:104]: 'Pressure': Sending state 986.46783 hPa with 1 decimals of accuracy
[16:22:59][D][sensor:104]: 'Humidity': Sending state 51.12988 % with 1 decimals of accuracy
[16:23:04][D][sensor:104]: 'Temperature': Sending state 24.07160 °C with 1 decimals of accuracy
[16:23:04][D][sensor:104]: 'Pressure': Sending state 986.44580 hPa with 1 decimals of accuracy
[16:23:04][D][sensor:104]: 'Humidity': Sending state 51.02051 % with 1 decimals of accuracy
[16:23:08][I][deep_sleep:062]: Beginning sleep
[16:23:08][I][deep_sleep:064]: Sleeping for 60000000us
[16:23:08][D][esp32.preferences:142]: Writing 1 items: 0 cached, 1 written, 0 failed
INFO Processing expected disconnect from ESPHome API for h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55
WARNING Disconnected from API
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Trying to connect to h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in the background
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully connected to h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.050s
INFO Successful handshake with h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.068s
[16:24:12][D][esp-idf:000][ot_main]: W(4360) OPENTHREAD:
[16:24:12][D][esp-idf:000][ot_main]: [W] DuaManager----: Failed to perform next registration: NotFound
[16:24:12][D][esp-idf:000][ot_main]: 
[16:24:14][D][sensor:104]: 'Temperature': Sending state 24.05637 °C with 1 decimals of accuracy
[16:24:14][D][sensor:104]: 'Pressure': Sending state 986.47626 hPa with 1 decimals of accuracy
[16:24:14][D][sensor:104]: 'Humidity': Sending state 51.07520 % with 1 decimals of accuracy
[16:24:19][D][sensor:104]: 'Temperature': Sending state 24.07160 °C with 1 decimals of accuracy
[16:24:19][D][sensor:104]: 'Pressure': Sending state 986.41797 hPa with 1 decimals of accuracy
[16:24:19][D][sensor:104]: 'Humidity': Sending state 51.02051 % with 1 decimals of accuracy
[16:24:24][D][sensor:104]: 'Temperature': Sending state 24.07668 °C with 1 decimals of accuracy
[16:24:24][D][sensor:104]: 'Pressure': Sending state 986.45404 hPa with 1 decimals of accuracy
[16:24:24][D][sensor:104]: 'Humidity': Sending state 50.99902 % with 1 decimals of accuracy
[16:24:29][I][deep_sleep:062]: Beginning sleep
[16:24:29][I][deep_sleep:064]: Sleeping for 60000000us
[16:24:29][D][esp32.preferences:142]: Writing 2 items: 1 cached, 1 written, 0 failed
INFO Processing expected disconnect from ESPHome API for h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55
WARNING Disconnected from API
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Trying to connect to h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in the background
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully connected to h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 1.089s
INFO Successful handshake with h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.111s
[16:25:35][D][sensor:104]: 'Temperature': Sending state 24.06652 °C with 1 decimals of accuracy
[16:25:35][D][sensor:104]: 'Pressure': Sending state 986.46515 hPa with 1 decimals of accuracy
[16:25:35][D][sensor:104]: 'Humidity': Sending state 51.06445 % with 1 decimals of accuracy
[16:25:40][D][sensor:104]: 'Temperature': Sending state 24.08176 °C with 1 decimals of accuracy
[16:25:40][D][sensor:104]: 'Pressure': Sending state 986.46252 hPa with 1 decimals of accuracy
[16:25:40][D][sensor:104]: 'Humidity': Sending state 51.00000 % with 1 decimals of accuracy
[16:25:42][D][esp32.preferences:142]: Writing 1 items: 1 cached, 0 written, 0 failed
[16:25:45][D][sensor:104]: 'Temperature': Sending state 24.09191 °C with 1 decimals of accuracy
[16:25:45][D][sensor:104]: 'Pressure': Sending state 986.45142 hPa with 1 decimals of accuracy
[16:25:45][D][sensor:104]: 'Humidity': Sending state 51.00098 % with 1 decimals of accuracy
[16:25:49][I][deep_sleep:062]: Beginning sleep
[16:25:49][I][deep_sleep:064]: Sleeping for 60000000us
[16:25:49][D][esp32.preferences:142]: Writing 1 items: 0 cached, 1 written, 0 failed
INFO Processing expected disconnect from ESPHome API for h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55
WARNING Disconnected from API
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Trying to connect to h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in the background
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully connected to h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.039s
INFO Successful handshake with h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.094s
[16:28:17][D][sensor:104]: 'Temperature': Sending state 24.09719 °C with 1 decimals of accuracy
[16:28:17][D][sensor:104]: 'Pressure': Sending state 986.43237 hPa with 1 decimals of accuracy
[16:28:17][D][sensor:104]: 'Humidity': Sending state 51.02344 % with 1 decimals of accuracy
[16:28:22][D][sensor:104]: 'Temperature': Sending state 24.12766 °C with 1 decimals of accuracy
[16:28:22][D][sensor:104]: 'Pressure': Sending state 986.48248 hPa with 1 decimals of accuracy
[16:28:22][D][sensor:104]: 'Humidity': Sending state 50.89355 % with 1 decimals of accuracy
[16:28:24][D][switch:012]: 'Prevent Deep Sleep' Turning ON.
[16:28:24][D][switch:055]: 'Prevent Deep Sleep': Sending state ON
[16:28:27][D][sensor:104]: 'Temperature': Sending state 24.14309 °C with 1 decimals of accuracy
[16:28:27][D][sensor:104]: 'Pressure': Sending state 986.42462 hPa with 1 decimals of accuracy
[16:28:27][D][sensor:104]: 'Humidity': Sending state 50.87207 % with 1 decimals of accuracy
[16:28:29][D][esp32.preferences:142]: Writing 1 items: 0 cached, 1 written, 0 failed
[16:28:30][I][deep_sleep:062]: Beginning sleep
[16:28:30][I][deep_sleep:064]: Sleeping for 60000000us
[16:28:30][D][esp32.preferences:142]: Writing 1 items: 0 cached, 1 written, 0 failed
INFO Processing expected disconnect from ESPHome API for h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55
WARNING Disconnected from API
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Trying to connect to h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in the background
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully connected to h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.053s
INFO Successful handshake with h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.102s
[16:30:55][D][esp-idf:000][ot_main]: W(5545) OPENTHREAD:
[16:30:56][D][esp-idf:000][ot_main]: [W] DuaManager----: Failed to perform next registration: NotFound
[16:30:56][D][esp-idf:000][ot_main]: 
[16:30:57][D][sensor:104]: 'Temperature': Sending state 24.12766 °C with 1 decimals of accuracy
[16:30:57][D][sensor:104]: 'Pressure': Sending state 986.45477 hPa with 1 decimals of accuracy
[16:30:57][D][sensor:104]: 'Humidity': Sending state 50.87109 % with 1 decimals of accuracy
[16:31:02][D][sensor:104]: 'Temperature': Sending state 24.13273 °C with 1 decimals of accuracy
[16:31:02][D][sensor:104]: 'Pressure': Sending state 986.40747 hPa with 1 decimals of accuracy
[16:31:02][D][sensor:104]: 'Humidity': Sending state 50.89355 % with 1 decimals of accuracy
[16:31:07][D][sensor:104]: 'Temperature': Sending state 24.15832 °C with 1 decimals of accuracy
[16:31:07][D][sensor:104]: 'Pressure': Sending state 986.50525 hPa with 1 decimals of accuracy
[16:31:07][D][sensor:104]: 'Humidity': Sending state 50.80566 % with 1 decimals of accuracy
[16:31:11][I][deep_sleep:062]: Beginning sleep
[16:31:11][I][deep_sleep:064]: Sleeping for 60000000us
[16:31:11][D][esp32.preferences:142]: Writing 2 items: 1 cached, 1 written, 0 failed
INFO Processing expected disconnect from ESPHome API for h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55
WARNING Disconnected from API
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Trying to connect to h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in the background
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.001s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully connected to h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.038s
INFO Successful handshake with h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.067s
[16:33:33][D][esp-idf:000][ot_main]: W(2502) OPENTHREAD:
[16:33:33][D][esp-idf:000][ot_main]: [W] DuaManager----: Failed to perform next registration: NotFound
[16:33:33][D][esp-idf:000][ot_main]: 
[16:33:38][D][sensor:104]: 'Temperature': Sending state 24.15832 °C with 1 decimals of accuracy
[16:33:38][D][sensor:104]: 'Pressure': Sending state 986.44971 hPa with 1 decimals of accuracy
[16:33:38][D][sensor:104]: 'Humidity': Sending state 50.82812 % with 1 decimals of accuracy
[16:33:43][D][sensor:104]: 'Temperature': Sending state 24.15832 °C with 1 decimals of accuracy
[16:33:43][D][sensor:104]: 'Pressure': Sending state 986.44971 hPa with 1 decimals of accuracy
[16:33:43][D][sensor:104]: 'Humidity': Sending state 50.73926 % with 1 decimals of accuracy
[16:33:48][D][sensor:104]: 'Temperature': Sending state 24.18371 °C with 1 decimals of accuracy
[16:33:48][D][sensor:104]: 'Pressure': Sending state 986.43591 hPa with 1 decimals of accuracy
[16:33:48][D][sensor:104]: 'Humidity': Sending state 50.74219 % with 1 decimals of accuracy
[16:33:51][I][deep_sleep:062]: Beginning sleep
[16:33:51][I][deep_sleep:064]: Sleeping for 60000000us
[16:33:51][D][esp32.preferences:142]: Writing 2 items: 1 cached, 1 written, 0 failed
INFO Processing expected disconnect from ESPHome API for h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55
WARNING Disconnected from API
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Trying to connect to h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in the background
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.001s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully connected to h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 1.098s
INFO Successful handshake with h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.066s
[16:34:56][D][api:146]: Accept FD49:E5FC:83AF::3E5
[16:34:56][D][api.connection:1466]: Home Assistant 2025.9.1 (FD49:E5FC:83AF::3E5) connected
[16:34:58][D][esp32.preferences:142]: Writing 1 items: 1 cached, 0 written, 0 failed
[16:34:59][D][sensor:104]: 'Temperature': Sending state 24.17356 °C with 1 decimals of accuracy
[16:34:59][D][sensor:104]: 'Pressure': Sending state 986.47479 hPa with 1 decimals of accuracy
[16:34:59][D][sensor:104]: 'Humidity': Sending state 50.80957 % with 1 decimals of accuracy
[16:35:04][D][sensor:104]: 'Temperature': Sending state 24.19914 °C with 1 decimals of accuracy
[16:35:04][D][sensor:104]: 'Pressure': Sending state 986.40576 hPa with 1 decimals of accuracy
[16:35:04][D][sensor:104]: 'Humidity': Sending state 50.91211 % with 1 decimals of accuracy
[16:35:09][D][sensor:104]: 'Temperature': Sending state 24.19914 °C with 1 decimals of accuracy
[16:35:09][D][sensor:104]: 'Pressure': Sending state 986.43359 hPa with 1 decimals of accuracy
[16:35:09][D][sensor:104]: 'Humidity': Sending state 50.73340 % with 1 decimals of accuracy
[16:35:12][I][deep_sleep:062]: Beginning sleep
[16:35:12][I][deep_sleep:064]: Sleeping for 60000000us
[16:35:12][D][esp32.preferences:142]: Writing 1 items: 0 cached, 1 written, 0 failed
INFO Processing expected disconnect from ESPHome API for h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55
WARNING Disconnected from API
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Trying to connect to h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in the background
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s
INFO Successfully resolved h2beta @ fd84:c977:29b2:1:7cbe:7175:21b8:be55 in 0.000s

so as claude put it:

Looking at your config, you have:

  • 20s delay before sleep
  • 60s max run_duration (safety timeout)
  • 60s sleep duration

With this config, the device should always sleep after 20 seconds, not wait 87-88 seconds for Thread to connect.

Revised Analysis (subtracting 60s sleep):

Cycle Sleep Start Wake Time First Reading Time from Wake to Reading What Likely Happened
1 16:23:08 16:24:08 16:24:14 6s Normal connection
2 16:24:29 16:25:29 16:25:35 6s Normal connection
3 16:25:49 16:26:49 16:28:17 88s Cycle skipped!
4 16:28:30 16:29:30 16:30:57 87s Cycle skipped!
5 16:31:11 16:32:11 16:33:38 87s Cycle skipped!
6 16:33:51 16:34:51 16:34:59 8s Normal connection
7 16:35:12 16:36:12 16:37:39 87s Cycle skipped!
8 16:37:52 16:38:52 16:39:00 8s Normal connection
9 16:39:13 16:40:13 16:40:19 6s Normal connection
10 16:40:33 16:41:33 16:43:01 88s Cycle skipped!

What’s Actually Happening:

Your device is missing entire wake cycles!

With your 20s delay, the timeline for each wake should be:

  1. Wake up (0s)
  2. Increment counter, update sensors
  3. Wait 20s
  4. Go back to sleep (regardless of Thread connection status)

The ~147-148s gaps mean the device:

  1. Woke up
  2. Couldn’t connect to Thread within 20s
  3. Went back to sleep without transmitting
  4. Woke up again 60s later
  5. Connected successfully this time

Somewhat improved config with a check to HA connection:

esphome:
  name: h2beta
  friendly_name: Thread H2 Beta
  on_boot:
    priority: 800
    then:
      # Increment wake counter first
      - globals.set:
          id: wake_count
          value: !lambda 'return id(wake_count) + 1;'
      - if:
          condition:
            lambda: 'return !id(prevent_sleep_mode);'
          then:
            - wait_until:
                # empirically, 45s works about 80% of the time
                timeout: 60s
                condition:
                  api.connected:
            - component.update: sleep_cycle_count
            - delay: 15s
            - if:
                condition:
                  lambda: 'return !id(prevent_sleep_mode);'
                then:
                  - deep_sleep.enter:
                      id: deep_sleep_control
                      sleep_duration: 1min
          else:
            - logger.log: "Prevent sleep mode active, staying awake"
            - deep_sleep.prevent: deep_sleep_control


esp32:
  board: esp32-h2-devkitm-1 
  variant: ESP32H2
  framework:
    type: esp-idf

deep_sleep:
  id: deep_sleep_control

# Minimal logging to save power
logger:
  level: DEBUG  # Only log errors
  # baud_rate: 0  # Disable UART completely to save power

# Minimal API for connectivity
api:

ota:
  - platform: esphome

network:
  enable_ipv6: true

# Thread configuration
openthread:
  tlv: ,,,
  force_dataset: true
  device_type: MTD

# SPI for BME280
spi:
  clk_pin: GPIO10
  mosi_pin: GPIO11
  miso_pin: GPIO12

globals:
  - id: wake_count
    type: int
    restore_value: yes
    initial_value: '0'
  - id: prevent_sleep_mode
    type: bool
    restore_value: yes
    initial_value: 'false'

sensor:
  - platform: bme280_spi
    temperature:
      name: "Temperature"
      id: bme_temp
      oversampling: 1x  # Minimal oversampling for power saving
    pressure:
      name: "Pressure"
      id: bme_pressure
      oversampling: 1x
    humidity:
      name: "Humidity"
      id: bme_humidity
      oversampling: 1x
    cs_pin: GPIO14
    iir_filter: "OFF"  # No filtering to save processing
    update_interval: 4s  # must be less than delay after waiting forconnection
  
  - platform: template
    name: "Sleep Cycle Count"
    id: sleep_cycle_count
    accuracy_decimals: 0
    update_interval: never  # Only update once on boot
    lambda: |-
      return (float)id(wake_count);

# note we have only brief to turn off
switch:
  - platform: template
    name: "Prevent Deep Sleep"
    id: ota_mode
    # The lambda now reports the state of our persistent global
    lambda: |-
      return id(prevent_sleep_mode);
    turn_on_action:
      - globals.set:
          id: prevent_sleep_mode
          value: 'true'
    turn_off_action:
      - globals.set:
          id: prevent_sleep_mode
          value: 'false'

text_sensor:
  - platform: openthread_info
    ip_address:
      name: "IP Address"
      id: ip_address_sensor

button:
  - platform: template
    name: "Reset Wake Count"
    on_press:
      then:
        # 1. Set the global variable back to zero
        - globals.set:
            id: wake_count
            value: '0'
        # 2. Immediately update the sensor that displays this value
        - component.update: sleep_cycle_count

  - platform: template
    name: "Exit Prevent Sleep"
    on_press:
      then:
        # 1. Clear the prevent_sleep_mode flag
        - globals.set:
            id: prevent_sleep_mode
            value: 'false'
        # 2. Force deep sleep right away
        - deep_sleep.enter:
            id: deep_sleep_control
            sleep_duration: 1min

shows similar behavior (sometimes it seems it doesn’t actually connect to thread and skips a cycle).

My final config that seems to be the most reliable:

esphome:
  name: h2beta
  friendly_name: Thread H2 Beta
  on_boot:
    priority: 800
    then:
      # Increment wake counter first
      - delay: 1s  # memory needs a bit to retrieve correct value
      - globals.set:
          id: wake_cycle_count
          value: !lambda 'return id(wake_cycle_count) + 1;'
      - if:
          condition:
            lambda: 'return !id(prevent_sleep_mode);'
          then:
            - wait_until:
                # empirically, 45s works about 80% of the time
                timeout: 60s
                condition:
                  api.connected:
            - delay: 1s  # cycle_count somehow didn't update if we don't wait a bit, maybe ESP needs a little bit of extra time before pushing updates
            - component.update: sleep_cycle_count
            - component.update: bme_sensor
            - delay: 10s  # in case we need to prevent deep sleep
            - if:
                condition:
                  lambda: 'return !id(prevent_sleep_mode);'
                then:
                  - deep_sleep.enter:
                      id: deep_sleep_control
                      sleep_duration: 3min
          else:
            - logger.log: "Prevent sleep mode active, staying awake"
            - deep_sleep.prevent: deep_sleep_control


esp32:
  board: esp32-h2-devkitm-1 
  variant: ESP32H2
  framework:
    type: esp-idf

deep_sleep:
  id: deep_sleep_control

# Minimal logging to save power
logger:
  level: DEBUG  # Only log errors
  # baud_rate: 0  # Disable UART completely to save power

# Minimal API for connectivity
api:

ota:
  - platform: esphome

network:
  enable_ipv6: true

# Thread configuration
openthread:
  tlv: 0e08000000000001000000030000194a0300001935060004001fffe002085d6cb48f82003d940708fd763a94f772a165051035b078634e77cdea5ba51ee14aa605c9030f6f70656e7468726561642d696f7461010242a804103b04ec0f77e42cfe2e7fc2b4ad094e490c0402a0f7f8
  force_dataset: true
  # do not promote to router
  device_type: MTD

# SPI for BME280
spi:
  clk_pin: GPIO10
  mosi_pin: GPIO11
  miso_pin: GPIO12

globals:
  - id: wake_cycle_count
    type: int
    restore_value: yes
    initial_value: '0'
  - id: prevent_sleep_mode
    type: bool
    restore_value: yes
    initial_value: 'false'

sensor:
  - platform: bme280_spi
    id: bme_sensor
    temperature:
      name: "Temperature"
      id: bme_temp
      oversampling: 1x  # Minimal oversampling for power saving
    pressure:
      name: "Pressure"
      id: bme_pressure
      oversampling: 1x
    humidity:
      name: "Humidity"
      id: bme_humidity
      oversampling: 1x
    cs_pin: GPIO14
    iir_filter: "OFF"  # No filtering to save processing
    update_interval: 1h  # we will update this manually in on_boot
  
  - platform: template
    name: "Sleep Cycle Count"
    id: sleep_cycle_count
    accuracy_decimals: 0
    update_interval: never #1s  # not sure why it didn't work with never
    lambda: |-
      return (float)id(wake_cycle_count);

# note we have only brief to turn off
switch:
  - platform: template
    name: "Prevent Deep Sleep"
    id: ota_mode
    # The lambda now reports the state of our persistent global
    lambda: |-
      return id(prevent_sleep_mode);
    turn_on_action:
      - globals.set:
          id: prevent_sleep_mode
          value: 'true'
      - logger.log: "Prevent Deep Sleep: ACTIVATED"

    turn_off_action:
      - globals.set:
          id: prevent_sleep_mode
          value: 'false'
      - logger.log: "Prevent Deep Sleep: DEACTIVATED"


text_sensor:
  - platform: openthread_info
    ip_address:
      name: "IP Address"
      id: ip_address_sensor

button:
  - platform: template
    name: "Reset Wake Count"
    on_press:
      then:
        # 1. Set the global variable back to zero
        - globals.set:
            id: wake_cycle_count
            value: '0'
        # 2. Immediately update the sensor that displays this value
        - component.update: sleep_cycle_count

  - platform: template
    name: "Exit Prevent Sleep"
    on_press:
      then:
        # 1. Clear the prevent_sleep_mode flag
        - globals.set:
            id: prevent_sleep_mode
            value: 'false'
        - logger.log: "Exit Prevent Sleep button pressed, going back to normal sleep"
        # 2. Force deep sleep right away
        - deep_sleep.enter:
            id: deep_sleep_control
            sleep_duration: 10s

OK, so my final solution that seems to be working well is connecting the battery directly to the pads from bottom of H2, connecting BME280 via SPI (as i2c wanted 2x3v3 and I didn’t want to fork it/breadboard), and my final config is this:

esphome:
  name: h2beta
  friendly_name: Thread H2 Beta
  on_boot:
    priority: 800
    then:
      # Increment wake counter first
      - delay: 1s  # memory needs a bit to retrieve correct value
      - globals.set:
          id: wake_cycle_count
          value: !lambda 'return id(wake_cycle_count) + 1;'
      - if:
          condition:
            lambda: 'return !id(prevent_sleep_mode);'
          then:
            - wait_until:
                # empirically, 45s works about 80% of the time
                timeout: 60s
                condition:
                  api.connected:
            - delay: 1s  # cycle_count somehow didn't update if we don't wait a bit, maybe ESP needs a little bit of extra time before pushing updates
            - component.update: sleep_cycle_count
            - component.update: bme_sensor
            - delay: 10s  # in case we need to prevent deep sleep
            - if:
                condition:
                  lambda: 'return !id(prevent_sleep_mode);'
                then:
                  - deep_sleep.enter:
                      id: deep_sleep_control
                      sleep_duration: 3min
          else:
            - logger.log: "Prevent sleep mode active, staying awake"
            - deep_sleep.prevent: deep_sleep_control


esp32:
  board: esp32-h2-devkitm-1 
  variant: ESP32H2
  framework:
    type: esp-idf

deep_sleep:
  id: deep_sleep_control

# Minimal logging to save power
logger:
  level: DEBUG  # Only log errors
  # baud_rate: 0  # Disable UART completely to save power

# Minimal API for connectivity
api:

ota:
  - platform: esphome

network:
  enable_ipv6: true

# Thread configuration
openthread:
  tlv: ...
  force_dataset: true
  # do not promote to router
  device_type: MTD

# SPI for BME280
spi:
  clk_pin: GPIO10
  mosi_pin: GPIO11
  miso_pin: GPIO12

globals:
  - id: wake_cycle_count
    type: int
    restore_value: yes
    initial_value: '0'
  - id: prevent_sleep_mode
    type: bool
    restore_value: yes
    initial_value: 'false'

sensor:
  - platform: bme280_spi
    id: bme_sensor
    temperature:
      name: "Temperature"
      id: bme_temp
      oversampling: 1x  # Minimal oversampling for power saving
    pressure:
      name: "Pressure"
      id: bme_pressure
      oversampling: 1x
    humidity:
      name: "Humidity"
      id: bme_humidity
      oversampling: 1x
    cs_pin: GPIO14
    iir_filter: "OFF"  # No filtering to save processing
    update_interval: 1h  # we will update this manually in on_boot
  
  - platform: template
    name: "Sleep Cycle Count"
    id: sleep_cycle_count
    accuracy_decimals: 0
    update_interval: never #1s  # not sure why it didn't work with never
    lambda: |-
      return (float)id(wake_cycle_count);

# note we have only brief to turn off
switch:
  - platform: template
    name: "Prevent Deep Sleep"
    id: ota_mode
    # The lambda now reports the state of our persistent global
    lambda: |-
      return id(prevent_sleep_mode);
    turn_on_action:
      - globals.set:
          id: prevent_sleep_mode
          value: 'true'
      - logger.log: "Prevent Deep Sleep: ACTIVATED"

    turn_off_action:
      - globals.set:
          id: prevent_sleep_mode
          value: 'false'
      - logger.log: "Prevent Deep Sleep: DEACTIVATED"


text_sensor:
  - platform: openthread_info
    ip_address:
      name: "IP Address"
      id: ip_address_sensor

button:
  - platform: template
    name: "Reset Wake Count"
    on_press:
      then:
        # 1. Set the global variable back to zero
        - globals.set:
            id: wake_cycle_count
            value: '0'
        # 2. Immediately update the sensor that displays this value
        - component.update: sleep_cycle_count

  - platform: template
    name: "Exit Prevent Sleep"
    on_press:
      then:
        # 1. Clear the prevent_sleep_mode flag
        - globals.set:
            id: prevent_sleep_mode
            value: 'false'
        - logger.log: "Exit Prevent Sleep button pressed, going back to normal sleep"
        # 2. Force deep sleep right away
        - deep_sleep.enter:
            id: deep_sleep_control
            sleep_duration: 10s

Came here to report that:

  1. the battery seems to got drained after just about 2 months (so 10x less than I hoped :smile: )
  2. I measured the battery capacity via my charger and it showed 3300 mAh (so actually very close to the official spec)

YMMW

Update: also found this post that seems somewhat consistent? Matter – low power on an ESP32-H2 SuperMini – @tomasmcguinness

The ESP32 is a power hungry chip at the best of times.

Have you tried measuring the power consumption?

Instead of putting the device into deep sleep, it might be better to set it up as a “sleepy end device”. This is part of the Minimal Thread Device role, which you are using.

This would put the device into a light sleep, which will use more power, but it should be softer on the radio when it wakes up.

In my experience, it’s a lot of trial and error!

I did not measure consumption, no, don’t think I have good gear for it (however, maybe I could put together something with another esp32/nodemcu). Is there an easy way I could do with esphome?

Someone said the power regulator on the Supermini is not that good so maybe I could also replace that.

I might try that light sleep as you did but I am skeptical :upside_down_face: … Man, I would love to have a cheap, thread powered battery device that is ESPHome configurable. Too many wishes.