Failing to connect ESP-H2 via Thread

Hi. I am trying to connect a simple esp-h2 (esp-h2-supermini - is that relevant?) to my new thread network. The thread network seems to be working fine, and the flashing of esp-h2 as well. But the esp-h2 doesn’t join the network nor I see anything like valid logs at it. I checked all of these:

  1. ESPHome 2025.6 adds OpenThread support | Matter Alpha
  2. esphome 2025.6.2 and openthread component · Issue #7200 · esphome/issues · GitHub
  3. Thread working on ESP32-H2 - Example Project - #11
  4. OpenThread Component — ESPHome

and seemingly I am doing exactly the same thing, but I don’t see the board cooperating.

My config is simply:

esphome:
  name: h2alpha
  friendly_name: h2alpha

esp32:
  board: esp32-h2-devkitm-1 # maybe this is wrong, as I have supermini ?
  variant: ESP32H2
  framework:
    type: esp-idf

# Enable logging
logger:


# Enable Home Assistant API
api:

ota:
  - platform: esphome


network:
  enable_ipv6: true


openthread:
  # tlv: 0e08000000000001000000030000194a0300001935060004001fffe002085d6cb48f82003d94070...c0402a0f7f8  # used this one originally as it seems dead simple, but same result
  # device_type: MTD  # tried switching from FTD to MTD, no difference
  channel: 25
  network_name: openthread-iota
  network_key: 0x35...aa605c9
  pan_id: 0x4..8
  ext_pan_id: 0x5d6c...3d94
  pskc: "0x2fca54...e7a3c"
  force_dataset: true

# found this on the github, it actually doesn't work though - the board doesn't output anything
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"

When I install the above and flash it to the board, I only get:

[22:34:24]ESP-ROM:esp32h2-20221101
[22:34:24]Build:Nov  1 2022
[22:34:24]rst:0xc (SW_CPU),boot:0x8 (SPI_FAST_FLASH_BOOT)
[22:34:24]Saved PC:0x400031b6
[22:34:24]SPIWP:0xee
[22:34:24]mode:DIO, clock div:1
[22:34:24]load:0x408460e0,len:0x1894
[22:34:24]load:0x4083cad0,len:0xf20
[22:34:24]load:0x4083efd0,len:0x2dc0
[22:34:24]entry 0x4083cad0
[22:34:24]I (22) boot: ESP-IDF 5.3.2 2nd stage bootloader
[22:34:24]I (22) boot: compile time Aug  7 2025 21:55:44
[22:34:24]I (24) boot: chip revision: v0.1
[22:34:24]I (25) boot: efuse block revision: v0.3
[22:34:24]I (29) boot.esp32h2: SPI Speed      : 64MHz
[22:34:24]I (34) boot.esp32h2: SPI Mode       : DIO
[22:34:24]I (39) boot.esp32h2: SPI Flash Size : 4MB
[22:34:24]I (43) boot: Enabling RNG early entropy source...
[22:34:24]I (49) boot: Partition Table:
[22:34:24]I (52) boot: ## Label            Usage          Type ST Offset   Length
[22:34:24]I (60) boot:  0 otadata          OTA data         01 00 00009000 00002000
[22:34:24]I (67) boot:  1 phy_init         RF data          01 01 0000b000 00001000
[22:34:24]I (75) boot:  2 app0             OTA app          00 10 00010000 001c0000
[22:34:24]I (82) boot:  3 app1             OTA app          00 11 001d0000 001c0000
[22:34:24]I (89) boot:  4 nvs              WiFi data        01 02 00390000 0006d000
[22:34:24]I (97) boot: End of partition table
[22:34:24]I (101) esp_image: segment 0: paddr=00010020 vaddr=420a0020 size=1dc9ch (122012) map
[22:34:24]I (160) esp_image: segment 1: paddr=0002dcc4 vaddr=40800000 size=02354h (  9044) load
[22:34:24]I (166) esp_image: segment 2: paddr=00030020 vaddr=42000020 size=90e5ch (593500) map
[22:34:24]I (409) esp_image: segment 3: paddr=000c0e84 vaddr=40802354 size=0a498h ( 42136) load
[22:34:24]I (431) esp_image: segment 4: paddr=000cb324 vaddr=4080c7f0 size=01b44h (  6980) load
[22:34:24]I (443) boot: Loaded app from partition at offset 0x10000
[22:34:24]I (444) boot: Disabling RNG early entropy source...
[22:34:24]I (456) cpu_start: Unicore app
[22:34:24]I (465) cpu_start: Pro cpu start user code
[22:34:24]I (465) cpu_start: cpu freq: 96000000 Hz
[22:34:24]I (466) app_init: Application information:
[22:34:24]I (468) app_init: Project name:     h2alpha
[22:34:24]I (473) app_init: App version:      2025.7.5
[22:34:24]I (477) app_init: Compile time:     Aug  7 2025 22:12:38
[22:34:24]I (483) app_init: ELF file SHA256:  3e28fb491...
[22:34:24]I (489) app_init: ESP-IDF:          5.3.2
[22:34:24]I (493) efuse_init: Min chip rev:     v0.0
[22:34:24]I (498) efuse_init: Max chip rev:     v0.99 
[22:34:24]I (503) efuse_init: Chip rev:         v0.1
[22:34:24]I (508) heap_init: Initializing. RAM available for dynamic allocation:
[22:34:24]I (515) heap_init: At 40815780 len 00037C00 (223 KiB): RAM
[22:34:24]I (521) heap_init: At 4084D380 len 00002B60 (10 KiB): RAM
[22:34:24]I (529) spi_flash: detected chip: generic
[22:34:24]I (532) spi_flash: flash io: dio
[22:34:24]I (537) sleep: Configure to isolate all GPIO pins in sleep state
[22:34:24]I (543) sleep: Enable automatic switching of GPIO sleep configuration
[22:34:24]I (550) main_task: Started on CPU0
[22:34:24]I (554) main_task: Calling app_main()
[22:34:24]I (714) main_task: Returned from app_main()

and then nothing.

I computed the pskc when trying to do it manually via this LLM written function based on the docs:

#!/usr/bin/env python3
"""
OpenThread PSKc Generator
Generates PSKc from Commissioner Credential/Passphrase
Compatible with OpenThread specification
"""

import hashlib
import binascii
from typing import Union

def generate_pskc(passphrase: str, ext_pan_id: Union[str, bytes], network_name: str) -> str:
    """
    Generate PSKc using PBKDF2 as per Thread specification
    
    Args:
        passphrase: Commissioner Credential (e.g., "j01Nme")
        ext_pan_id: Extended PAN ID in hex string or bytes
        network_name: Network name string
    
    Returns:
        PSKc as hex string
    """
    # Convert ext_pan_id to bytes if it's a string
    if isinstance(ext_pan_id, str):
        ext_pan_id = ext_pan_id.replace('0x', '').replace(':', '')
        ext_pan_id_bytes = bytes.fromhex(ext_pan_id)
    else:
        ext_pan_id_bytes = ext_pan_id
    
    # Create salt: Extended PAN ID + Network Name
    salt = ext_pan_id_bytes + network_name.encode('utf-8')
    
    # Generate PSKc using PBKDF2
    # Thread uses 16384 iterations and generates 16 bytes
    pskc_bytes = hashlib.pbkdf2_hmac(
        'sha256',           # Hash algorithm
        passphrase.encode('utf-8'),  # Password
        salt,               # Salt
        16384,              # Iterations (Thread specification)
        16                  # Key length in bytes
    )
    
    return binascii.hexlify(pskc_bytes).decode('ascii')

def main():
    # Your network parameters
    passphrase = "j01Nme"
    ext_pan_id = "5d6cb48f82003d94"
    network_name = "openthread-iota"
    
    print("OpenThread PSKc Generator")
    print("=" * 50)
    print(f"Passphrase/Commissioner Credential: {passphrase}")
    print(f"Extended PAN ID: {ext_pan_id}")
    print(f"Network Name: {network_name}")
    print("=" * 50)
    
    # Generate PSKc
    pskc = generate_pskc(passphrase, ext_pan_id, network_name)
    
    print(f"\nGenerated PSKc: {pskc}")
    print(f"PSKc (with 0x prefix): 0x{pskc}")
    
    # Compare with the value from TLV
    expected_pskc = "3b04ec0f77e42cfe2e7fc2b4ad094e49"
    print(f"\nExpected PSKc from TLV: {expected_pskc}")
    
    if pskc == expected_pskc:
        print("✓ PSKc matches! The generation is correct.")
    else:
        print("✗ PSKc doesn't match. There might be a configuration issue.")
    
    print("\n" + "=" * 50)
    print("ESPHome Configuration:")
    print("=" * 50)
    print(f"""
openthread:
  channel: 25
  network_name: "{network_name}"
  network_key: "0x35b078634e77cdea5ba51ee14aa605c9"
  pan_id: 0x42a8
  ext_pan_id: "0x{ext_pan_id}"
  pskc: "0x{pskc}"
  device_type: MTD  # or FTD
""")

if __name__ == "__main__":
    main()

My Passphrase/Commissioner Credential when forming the network via this guide is lowercased string of two words - does it have to be something else.

By the way essentially the same config with esp32-c6 supermini works just fine.

What am I doing wrong? Thanks!

I use this syntax on my 2 ESP32-H routers :

openthread:
   tlv: 0e08000000000001000000030000194a0300001935060004001fffe002085d6cb48f82003d94070...c0402a0f7f8  # used this one originally as it seems dead simple, but same result

(One full sized, one mini)

Same as what I get using serial log… Only using wireless log I get more.

Ha, you saved me, thank you!

Yeah, I couldn’t get any output, so thought it’s wrong, but adding:

mqtt:
  broker: 192.168.0.202
  username: z2m
  password: myzpwd
  topic_prefix: null
  log_topic: h2test

made it possible to see some logs:

mosquitto_sub -h 192.168.0.202 -u z2m -P myzpwd -t "h2test" --pretty -F "%t>%p"

like this:

h2test>[D][main:224]: Heartbeat - ESP32-h2-delta is alive!
h2test>[D][main:224]: Heartbeat - ESP32-h2-delta is alive!
h2test>[W][component:307]: mqtt cleared Warning flag
h2test>[I][mqtt:310]: Connected
h2test>[W][component:307]: mqtt cleared Warning flag
h2test>[I][mqtt:310]: Connected
h2test>[D][main:224]: Heartbeat - ESP32-h2-delta is alive!
h2test>[D][main:224]: Heartbeat - ESP32-h2-delta is alive!
h2test>[D][api:146]: Accept FD49:E5FC:83AF::3E5
h2test>[W][component:307]: api cleared Warning flag
h2test>[D][api.connection:1466]: Home Assistant 2025.8.0 (FD49:E5FC:83AF::3E5) connected
h2test>[D][main:224]: Heartbeat - ESP32-h2-delta is alive!

Anyway, with this config:

esphome:
  name: h2delta
  friendly_name: h2delta

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

# Enable logging
logger:
  level: DEBUG

mqtt:
  broker: 192.168.0.202
  username: z2m
  password: myzpwd
  topic_prefix: null
  log_topic: h2test

api:

ota:
  - platform: esphome


network:
  enable_ipv6: true

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

openthread:
  tlv: 0e08000000000001000000030...7f8
  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"

and applying via esphome run h2gamma.yaml, I was able to see the device being autodiscovered by the ESPHome in homeassistant and I could one-click add it:

Gotchas

A note for my futureself. thread/esphome doesn’t like changing names. I had three boards that I was frantically switching and they got the same hostname - the problem is that the hostname is probably partly cached on the border router level, so when I flashed boardA with h2alpha name and it connected (but I did not add it), and then I flashed boardB with the same config and therefore same h2alpha hostname, esphome gets confused and will show “Unable to add/connect” because the name is associated with the old ipv6 address (I think). Similarly, I couldn’t change hostname if I wanted to change it. The solution I found out (with a minor heartattack as I thought I factory-reset my border router) I could go to Reset Router:

It disappears for a second, then appears under “Other networks”, but you can click again on “Add to preferred network” - the network settings are the same, fortunately.

After this procedure, I was able to “reregister” new hostnames fine.

However, one particularly stuborn device was still showing with wrong name, and I couldn’t add it. I flashed the device again with a new name, used http://esp-ot-br.local/topology to figure out it’s IP address, click on “Add device”, and manually changed the IPV6 address as the one there was wrong with my new from /topology and tada it worked (it even fetched the correct name after adding it)

And to get the network status on http://esp-ot-br.local/index.html#Status, you need to click on “OverView” → it then fetches the data and you see the details instead of just “Unknown”

Thanks for the

text_sensor:
  - platform: openthread_info

I must say, I don’t have the ‘reset border router’, but I think it would be a problem for the other border routers

1 Like

Hi, a bit late, but I made this write up which works for me. Tested it both on the C6 and H2, including OTA updates using Thread:

Thanks, that one is nice!

I did manage to get it working, here