How to control Eve Energy plugs via Matter/Thread on a Raspberry Pi with Home Assistant

I bought the Wi-Fi version of the Apple TV 4K thinking it would act as a Thread Border Router for my Eve Energy plugs. It does not. The Wi-Fi model has no Thread radio โ€” only the Ethernet model does. By the time I figured this out I had already bought the plugs, so I decided to build my own border router instead.

This guide covers the full stack: flashing an ESP32-C6 as a Thread radio, building OpenThread Border Router on a Raspberry Pi 5, and wiring it all into Home Assistant via Matter. There's very little documentation that covers this end-to-end, and I hit several non-obvious pitfalls along the way โ€” so I'm writing it up in the hope that it saves someone else the debugging time.

The end result: Eve Energy plugs fully controllable from Home Assistant, including automations, with no dependency on Apple Home, Google Home, or any commercial Thread Border Router hub. Everything runs locally on the Pi.

The entire process โ€” working through the setup step by step, debugging failures, and figuring out the non-obvious issues like the Ethernet routing conflict and the two-step commissioning flow โ€” was facilitated by Claude Code. This guide is the write-up of that session.


What you need

Hardware

  • Raspberry Pi 5 (4 GB recommended โ€” the build steps are RAM-heavy)
  • ESP32-C6 development board โ€” this provides the Thread radio as an RCP (Radio Co-Processor)
  • USB-A to USB-C cable to connect the ESP32-C6 to the Pi
  • One or more Eve Energy smart plugs
  • Wi-Fi network (see the critical note on Ethernet below)

Software (on the Pi)

  • Raspberry Pi OS 64-bit (Bookworm)
  • Docker
  • Python 3.11+
  • Git, cmake, ninja-build

Architecture overview

The key insight is that matter-server (running in Docker) cannot do Bluetooth commissioning because Docker has no Bluetooth passthrough on Linux. Eve Energy plugs require BLE for initial commissioning. The solution is a two-step process:

  1. chip-tool (compiled natively on the Pi) commissions the plug over BLE+Thread and establishes a Matter fabric
  2. matter-server joins that same fabric via a multi-admin commissioning window over the Thread network, which registers the device with Home Assistant

This means you need both chip-tool and matter-server, and they cannot run simultaneously (both implement the CHIP stack and interfere with each other during commissioning).


Part 1 โ€” Flash the ESP32-C6 as an RCP

The ESP32-C6 needs to run OpenThread RCP firmware, which makes it a Thread radio slave to the Pi.

Install esptool

pip install esptool

Get the RCP firmware

Espressif provides pre-built RCP firmware in the ESP Thread Border Router SDK releases:

Download the ot-rcp image for ESP-IDF v5.x.

Flash

Connect the ESP32-C6 via its CP210x USB-C port (the non-JTAG port) and flash:

esptool.py --chip esp32c6 --port /dev/ttyUSB0 --baud 460800 \
  write_flash 0x0 ot-rcp.bin

After reconnecting, verify:

ls /dev/ttyUSB0   # CP210x โ€” this is the RCP port
ls /dev/ttyACM0   # JTAG debug โ€” ignore

Part 2 โ€” Build and install OTBR

OTBR (OpenThread Border Router) must be built from source. This takes 20โ€“40 minutes on a Pi 5.

Clone and bootstrap

git clone https://github.com/openthread/ot-br-posix.git ~/ot-br-posix
cd ~/ot-br-posix
./script/bootstrap

Build

./script/setup -DOT_POSIX_CONFIG_RCP_BUS=UART \
               -DOTBR_BORDER_ROUTING=ON \
               -DOTBR_BACKBONE_ROUTER=ON \
               -DOTBR_REST=ON \
               -DOTBR_SRP_ADVERTISING_PROXY=ON \
               -DOTBR_TREL=ON

The important flags:

  • OTBR_REST=ON โ€” enables the REST API on port 8081 that Home Assistant uses
  • DOTBR_SRP_ADVERTISING_PROXY=ON โ€” required for Matter CASE session establishment (device address resolution)
  • DOTBR_TREL=ON โ€” Thread Radio Encapsulation Link, needed for Thread over Wi-Fi backbone

Install the binary and service

sudo cp ~/ot-br-posix/build/otbr-agent /usr/sbin/otbr-agent
sudo chmod +x /usr/sbin/otbr-agent

Create /etc/systemd/system/otbr-agent.service:

[Unit]
Description=Border Router Agent
After=network.target

[Service]
Type=simple
ExecStart=/usr/sbin/otbr-agent -I wpan0 -B wlan0 spinel+hdlc+uart:///dev/ttyUSB0 trel://wlan0
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable --now otbr-agent

Verify:

sudo systemctl status otbr-agent
curl http://localhost:8081/node/state

Part 3 โ€” Form the Thread network

sudo ot-ctl dataset init new
sudo ot-ctl dataset commit active
sudo ot-ctl ifconfig up
sudo ot-ctl thread start

After ~30 seconds, the node should become a Leader:

sudo ot-ctl state         # should show "leader"

Save your dataset hex โ€” you will need it for commissioning and for disaster recovery:

sudo ot-ctl dataset active -x

Verify the Thread prefix is routed via wpan0, not eth0 (see Critical Note below):

ip -6 route show | grep wpan0

Part 4 โ€” Build chip-tool

chip-tool is the Matter commissioning CLI from the connectedhomeip SDK. It handles the BLE commissioning step.

Install build dependencies

sudo apt-get install -y git gcc g++ pkg-config libssl-dev libdbus-1-dev \
  libglib2.0-dev libavahi-client-dev ninja-build python3-venv python3-dev \
  python3-pip unzip libgirepository1.0-dev libcairo2-dev libreadline-dev

Set up a 4 GB SSD swapfile

Do not use zRAM โ€” it is RAM-backed and useless here. The compiler peaks at ~3 GB RAM and will OOM without a real swapfile on SSD:

sudo fallocate -l 4G /mnt/swapfile   # /mnt should be on SSD, not the SD card
sudo chmod 600 /mnt/swapfile
sudo mkswap /mnt/swapfile
sudo swapon /mnt/swapfile

Clone connectedhomeip

git clone --depth=1 --branch v1.4.2.0 \
  https://github.com/project-chip/connectedhomeip.git ~/connectedhomeip
cd ~/connectedhomeip
git submodule update --init --recursive --depth=1

Build

cd ~/connectedhomeip
source scripts/activate.sh
gn gen out/chip-tool --args='chip_enable_wifi=false is_debug=false treat_warnings_as_errors=false'
ninja -j1 -C out/chip-tool chip-tool

Use -j1 (single thread) to avoid OOM. This takes 1โ€“2 hours.

Install a wrapper script

chip-tool requires --storage-directory to come after the subcommand, which is easy to forget. A wrapper handles this:

mkdir -p ~/.local/bin ~/.local/share/chip-tool
cat > ~/.local/bin/chip-tool <<'EOF'
#!/bin/bash
exec ~/connectedhomeip/out/chip-tool/chip-tool "$@" --storage-directory ~/.local/share/chip-tool
EOF
chmod +x ~/.local/bin/chip-tool

Verify:

chip-tool --version

Part 5 โ€” Start Docker containers

matter-server

mkdir -p ~/matter-server
docker run -d --name matter-server --restart unless-stopped \
  --network host --privileged \
  -v ~/matter-server:/data \
  -v /run/dbus:/run/dbus:ro \
  ghcr.io/home-assistant-libs/python-matter-server:stable \
  matter-server --storage-path /data --port 5580

Home Assistant

mkdir -p ~/homeassistant
docker run -d --name home-assistant --privileged --restart unless-stopped \
  --network host \
  -v ~/homeassistant:/config \
  ghcr.io/home-assistant/home-assistant:stable

Pi 5 note: Use ghcr.io/home-assistant/home-assistant:stable. Pi 4-specific images crash on Pi 5 due to jemalloc incompatibility with the Pi 5's 16 KB memory page size.


Part 6 โ€” Configure Home Assistant

Open http://<pi-ip>:8123 and complete the onboarding wizard.

Add OTBR integration

Settings โ†’ Devices & Services โ†’ Add Integration โ†’ OpenThread Border Router

  • URL: http://localhost:8081

HA will detect the active Thread dataset automatically.

Add Matter integration

Settings โ†’ Devices & Services โ†’ Add Integration โ†’ Matter (BETA)

  • Select "Use the Python Matter Server running on this host"
  • WebSocket URL: ws://localhost:5580/ws

Part 7 โ€” Commission the Eve Energy plugs

This is the most involved part. As explained above, it's a two-step process: BLE via chip-tool, then Thread via matter-server.

Step 1: Factory reset the plug

Hold the button for ~15 seconds until the LED flashes red repeatedly (around 30 rapid flashes). The plug is now in pairing mode with BLE advertising active for approximately 3 minutes.

Step 2: Commission with chip-tool over BLE

Stop matter-server โ€” chip-tool and matter-server cannot both run the CHIP stack at the same time:

docker stop matter-server

Commission the plug. The passcode and discriminator are on the label (or encoded in the QR code):

chip-tool pairing ble-thread 1 hex:<your-dataset-hex> <passcode> <discriminator> \
  --bypass-attestation-verifier true

--bypass-attestation-verifier true is required for Eve plugs because they use a development PAA (Product Attestation Authority) certificate that is not in the standard trust store.

On success: CHIP:TOO: Commissioning success

Step 3: Open a multi-admin commissioning window

chip-tool administratorcommissioning open-basic-commissioning-window 600 1 0 \
  --timedInteractionTimeoutMs 10000

This gives matter-server 600 seconds to join the same fabric.

Step 4: Commission via matter-server

Start matter-server and wait a few seconds:

docker start matter-server
sleep 5

Connect to the matter-server WebSocket and send the commission command (replace the code value with the QR code string from your plug's label):

pip install websockets   # if not already installed

python3 - <<'EOF'
import asyncio, json, websockets

async def commission():
    async with websockets.connect("ws://localhost:5580/ws") as ws:
        await ws.recv()  # server info message
        await ws.send(json.dumps({
            "message_id": "1",
            "command": "commission_with_code",
            "args": {
                "code": "MT:REPLACE-WITH-YOUR-QR-CODE",
                "network_only": True
            }
        }))
        print(json.loads(await ws.recv()))

asyncio.run(commission())
EOF

Within ~30 seconds, Home Assistant will discover the plug as a new Matter device and create a switch entity for it.

Repeat for each additional plug

Use a different node ID (the first argument after ble-thread) for each chip-tool commissioning.


Critical: if you have Ethernet connected, unplug it

This cost me a lot of debugging time. If eth0 and wlan0 are both connected to the same LAN, all Matter devices will time out.

Here is why: OTBR sends Router Advertisements on wlan0 announcing the Thread mesh prefix. If eth0 is on the same LAN, it also receives these RAs and installs the Thread prefix as a route on eth0 with metric 100. The wpan0 route has metric 256. So all Matter CASE (Certificate-Authenticated Session Establishment) packets exit via Ethernet and never reach the Thread network โ€” every device times out indefinitely.

Fix: keep eth0 unplugged if you are using Wi-Fi for the backbone.

Verify routing is correct:

ip -6 route show | grep <your-thread-prefix>
# Should show ONLY: <prefix>/64 dev wpan0
# If eth0 appears, unplug Ethernet.

Troubleshooting

All devices unavailable after Pi reboot or OTBR restart

  1. Check routing (see above). Unplug eth0.
  2. Clear stale SRP entries โ€” devices re-register after a reboot but the SRP server may still hold the old entry and reject re-registration, which silently breaks CASE:
    sudo ot-ctl srp server disable
    sudo ot-ctl srp server enable
    
    Wait 30 seconds, then retry.
  3. If still failing: factory reset and recommission.

BLE scan times out during chip-tool commissioning

The plug is not in pairing mode. Factory reset it and retry within 3 minutes.

SRP name conflict on retry

If a commissioning attempt was interrupted and you retry, the SRP name from the first attempt is still cached. Clear it:

sudo ot-ctl srp server disable && sudo ot-ctl srp server enable

chip-tool keeps recreating the same old fabric

Deleting chip_tool_kvs alone is not enough. The fabric root CA key lives in chip_tool_config.ini. Delete everything in the storage directory for a fresh start:

rm -f ~/.local/share/chip-tool/*

HA shows a Matter device as unavailable after recommissioning

The stale node entry needs to be removed via the matter-server WebSocket:

{"message_id": "1", "command": "remove_node", "args": {"node_id": <old_node_id>}}

Then restart HA.


I hope this can help anyone in the same situation as me.

1 Like