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!

4 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
1 Like

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.

Can I ask if anyone tried to wake up esp32 h2 with ext1. ESP home seems like added this support since Fix support for ESP32-H2 in deep_sleep by baal86 · Pull Request #8290 · esphome/esphome · GitHub. But didn’t see any example config for this. Thank you very much.

Have you considered the nRF52840 recently added to ESPHome supported chips?

No. I would love to copy cat someone but given nRF52840 is significantly less popular I am almost having a hard time to figure out how to even put it together, honestly.

BBC Micro:bit has one… Every British schoolchild has played with one of those. They are available as a SOC board too for around $10, ready to be programmed by ESPHome, as easily as a Raspberry Pi Pico and ESP boards. Power requirements ranges from 3 µA to a few milliamps…

Your battery short life experience is not new, being well documented since the first release of the ESP processors.

The drawback for the ESP is the radio for WiFi drawing so much power. You need to keep one processor alive in the background, especially designed for low power consumption, doing the counting, and then wake the other one that has network connectivity to quickly connect, raise a session, transfer the buffered data and go back to sleep. Later versions of the ESP series have been tuned to do that (see Ultra Low Power (ULP) coprocessor - ESP32 - — ESP-IDF Programming Guide v5.5.2 documentation), and other manufacturers have been doing it well for decades, but not specifically aimed at hobbyists. Water meter readers routinely have a 10 year battery life, using the nRF52840 type processors paired with Lithium Thionyl Chloride batteries.

As an aside, there have been ultra-ultra-low power FRAM binary counter chips that retain their count when power interrupted, connected to SOCs such as the ESP series, however the cheap ones seemed to have disappeared. This would have solved your requirements perfectly. Is 10nanoAmps low enough, compared to the self drain of modern coin cell batteries? Introducing Our Counter IC – ABLIC Inc.

Even the nRD52840 can be wrangled to low power consumption. See More successful adventures in reducing nRF52840 power consumption – @tomasmcguinness

ESP32-H2 goes down to 30μA, and check out the ESP32-E22 and ESP32-H21 specifications just announced at the CES2026.

OK, thanks for the tip :person_shrugging: , I might look into it more. When I checked ESPHome NRF52 Platform - ESPHome - Smart Home Made Simple it looked slightly more complicated compared to vanilla ESP32.

It looks like the original deep sleep component doesn’t work will with esp32h2. I have created an external component wacsy/esphh2: Fork from esphome to enable the esp32h2 deep sleep and tried it on a contact sensor. GPIO 8 provide the power to a hall sensor and GPIO 10 to check the input. I also managed to use GPIO 10 as ext1 wake up source and change the wake-up level every time it changed.

I wonder if anyone is interested in the power consumption for the deep sleep mode with such simple peripheral circuit.

Thanks. Can you say more about why the default one doesn’t work? Like, not at all (it clearly go “off” based on the schedule, but was it fake or what)?

Hey OP, you may want to check one of the comments on one of the @tomasmcguinness’s posts:

I’ve got my hands on a ESP32H2 Supermini, here a my results (measured with PPK2):

  • 3.3V Input to the 3.3V PIN
  • deep sleep TimerWakeup Example from the Arduino ESP32 core
  • Board unmodified: 488 µA
  • WS2812 removed: 223 µA
  • LDO regulator removed: 148 µA
  • Battery charger IC removed: 115 µA
  • Charger LED removed: 85 µA at the start, 17 µA after 20s (probably due to charging capacitors)

In theory:

  • An unmodified board at ~500 µA should give you about 7 months on an 18650 2500mAh Li-ion.
  • But, if you remove all these components you can get 2+ years on a 500 mAh AA-sized LiFePO4, which is smaller and safer.

Then why did you get ~2 months of runtime? Assuming all else is good enough (your code, your specific battery), it might be because you feed the board from a battery using the BAT pins. The battery input feeds into the LDO, which is likely not efficient enough.

I did this and removed all these on my ESP32-C6 SuperMini (which is near identical to the H2 SuperMini), as well as the battery LED, and it works. I can connect and OTA update over my $5 Thread Border Router. I don’t have a way to measure the power consumption other than running it on the battery and see how long it goes.

Show photos

I accidentally removed a few more capacitors and resistors but they seem to be non-critical elements.

Thanks to your efforts, I can try with deep sleep now, didn’t know this was even possible. I’ll try and report back the results after a few months.

If you want to follow this path and remove the components:

  • Do not use a hot air gun, use a soldering iron.
  • You might lose the ability to connect and update over USB. No, it works. But since the board can only be powered by the battery, keep the battery connected during USB connection.
  • You can’t use a Li-ion.
  • You won’t have over-discharge protection. You can probably get that by using an ADC pin.
3 Likes

Wow that’s cool! I think I won’t do that now, want to keep the optionality of the board, but maybe in the future. I would appreciate if you come back reporting how long it lasts

Have you built your own Matter device?

I did not. How would that be done?

My understanding is that matter is kinda “too strict” (for good reasons though), that makes it hard for tinkerers like us. The way to go is IMHO via https://matterbridge.io/ instead, for example.

I previously created a “test” Matter-over-Thread device for esp32-h2 using Espressif ZeroCode, which lets you design and compile your own Matter (over Thread or WiFi) devices for esp32. You are limited to the device types and peripherals they allow, but I did manage to get it commissioned to both my Apple and HA Matter fabrics (the latter requiring me to enable test certificates). Also as noted in that quote, you can run Tasmota32 which also has Matter (experiemental cert) support, but that does not support Thread networking.