diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..a27954a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,44 @@ +name: Build + +on: + push: + pull_request: + schedule: + # Weekly, to notice when a new ESPHome release breaks the config + - cron: "17 4 * * 1" + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install ESPHome + run: pip install esphome + + # Dummy secrets: a made-up test dataset and a random key. + # The resulting firmware is only built, never published. + - name: Create dummy secrets + run: | + cat > secrets.yaml < -[![ESPHome](https://img.shields.io/badge/ESPHome-Compatible-blue?logo=esphome)](https://esphome.io/) +[![ESPHome](https://img.shields.io/badge/ESPHome-2026.8%2B-blue?logo=esphome)](https://esphome.io/) [![Thread](https://img.shields.io/badge/Thread-1.3-green)](https://www.threadgroup.org/) [![Home Assistant](https://img.shields.io/badge/Home%20Assistant-Integration-41BDF5?logo=homeassistant)](https://www.home-assistant.io/) @@ -16,16 +16,27 @@ This ESPHome configuration turns an ESP32-C6 board into a Thread FTD (Full Threa +## ✨ Features + +- πŸ“Ά **Thread router (FTD)**: extends your Thread mesh, no WiFi needed +- πŸ” **Encrypted API and OTA**: Home Assistant connection and firmware updates use one encryption key +- πŸ“‘ **Antenna switch** (Seeed XIAO ESP32C6): choose the built-in or external antenna from Home Assistant +- πŸ“Š **Diagnostics in Home Assistant**: Thread role, RLOC16, channel, IP address, parent RSSI, TX retries, CCA errors, partition changes, uptime +- πŸ’‘ **Status LED**: blinks when something is wrong +- πŸ” **Restart button** in Home Assistant + ## πŸ“‹ Prerequisites | Requirement | Description | |-------------|-------------| | **πŸ”§ Hardware** | ESP32-C6 board (tested with [Seeed Studio XIAO ESP32C6](https://www.seeedstudio.com/Seeed-Studio-XIAO-ESP32C6-p-5884.html)) | | **🌐 Network** | Thread Border Router in network (e.g., Home Assistant with Thread integration) | -| **πŸ’» Software** | ESPHome installed | +| **πŸ’» Software** | ESPHome **2026.8 or newer** (required for the Thread diagnostic sensors) | | **πŸ”Œ Cable** | USB cable for initial flashing | | **πŸ“¦ Optional** | 3D-printed case - [XIAO ESP32-C6 Case](https://www.printables.com/model/1543275-xiao-esp32-c6-zigbee-router-case-split-lid-sma-ext) | +> **πŸ’‘ Other boards:** The config also works on other ESP32-C6 boards. Remove the block marked `Seeed XIAO ESP32C6 specific` in `esp32c6-thread-router.yaml`, since it drives GPIO3, GPIO14 and GPIO15. ESPHome also supports Thread on ESP32-C5, ESP32-H2 and (since 2026.7) nRF52 boards, but this config is only tested on the ESP32-C6. + ## πŸ”‘ Required Information ### 🌐 1. Thread Network Dataset (TLV) @@ -38,7 +49,7 @@ This ESPHome configuration turns an ESP32-C6 board into a Thread FTD (Full Threa **Step 1:** SSH to your Server -**Step 2:** Retrieve TLV data: +**Step 2:** Retrieve TLV data (use `podman` instead of `docker` if you run Podman): ```bash docker exec otbr ot-ctl dataset active -x ``` @@ -54,29 +65,57 @@ thread_tlv: "YOUR_TLV_HEX_HERE" - +
βœ… Channelβœ… Network Nameβœ… PAN ID & Extended PAN ID
βœ… Network Key (encrypted)βœ… PSKc (encrypted)βœ… Mesh-Local Prefix
βœ… Network Keyβœ… PSKcβœ… Mesh-Local Prefix
-### πŸ” 2. ESPHome Secrets +> ⚠️ **Keep the TLV secret!** The network key is stored in the TLV **in plain text**. Anyone with your TLV can join your Thread network. Never commit `secrets.yaml` or post the TLV anywhere. The firmware image also contains the TLV, so don't share compiled `.bin` files. + +### πŸ” 2. API Encryption Key + +The Home Assistant API and OTA updates are encrypted with a shared key. Generate one: + +```bash +openssl rand -base64 32 +``` + +Or copy a freshly generated key from the [ESPHome API docs](https://esphome.io/components/api/). + +### πŸ“„ 3. ESPHome Secrets In `secrets.yaml` you need: | Key | Description | Required | |-----|-------------|----------| -| **thread_tlv** | Your Thread network commissioning data | βœ… Required | +| **thread_tlv** | Your Thread network dataset (hex) | βœ… Required | +| **api_encryption_key** | 32-byte base64 key for API and OTA encryption | βœ… Required | ## πŸš€ Installation ### πŸ“ Step 1: Configure Secrets -Edit `secrets.yaml` with your Thread TLV: +Create `secrets.yaml` next to `esp32c6-thread-router.yaml`: ```yaml thread_tlv: "YOUR_TLV_HEX_HERE" +api_encryption_key: "YOUR_BASE64_KEY_HERE" ``` That's all you need - no WiFi credentials required! -### ⚑ Step 2: Compile and Flash Firmware +### πŸ“‘ Step 2: Choose the Antenna (Seeed XIAO ESP32C6 only) + +The XIAO ESP32C6 has a built-in ceramic antenna and a U.FL connector for an external antenna (e.g. the SMA antenna of the recommended case). The antenna is selected by an RF switch (GPIO3 = switch power, GPIO14 = antenna select). + +If you use an **external antenna**, change this substitution before flashing: +```yaml +substitutions: + antenna_default: RESTORE_DEFAULT_ON # external antenna +``` + +You can also toggle the **External Antenna** switch in Home Assistant later. The setting is kept across reboots. + +> ⚠️ Without an external antenna connected, keep the built-in antenna selected. Otherwise the range will be very poor. + +### ⚑ Step 3: Compile and Flash Firmware Connect your ESP32-C6 via USB and flash the firmware using local ESPHome: @@ -94,7 +133,7 @@ esphome run esp32c6-thread-router.yaml --device=/dev/ttyACM0 | OS | Typical Paths | |----|--------------| | 🐧 Linux | `/dev/ttyUSB0`, `/dev/ttyACM0`, `/dev/ttyUSB1` | -| 🍎 macOS | `/dev/cu.usbserial-*`, `/dev/cu.wchusbserial*` | +| 🍎 macOS | `/dev/cu.usbserial-*`, `/dev/cu.usbmodem*` | | πŸͺŸ Windows | `COM3`, `COM4`, etc. | **Check available ports:** @@ -122,7 +161,7 @@ ls -la /dev/ttyACM0 sudo chmod 666 /dev/ttyACM0 # If using Docker/Podman, restart the container -docker-compose restart +docker compose restart ``` > ⚠️ **Note:** You need to run `sudo chmod 666` each time you reconnect the USB device. @@ -134,18 +173,20 @@ docker-compose restart
🐳 Alternative: Using Docker/Podman -> ⚠️ **Important:** When using Docker/Podman (rootless), you need to fix USB permissions before flashing. +The included `docker-compose.yml` mounts this repository as `/config` and passes the USB device into the container. + +> ⚠️ **Important:** The USB device must be plugged in **before** the container starts. When using Docker/Podman (rootless), fix USB permissions first (see above). ```bash -# 1. Start container -docker-compose up -d +# 1. Start container (default device: /dev/ttyACM0) +docker compose up -d +# or with another device: +ESPHOME_DEVICE=/dev/ttyUSB0 docker compose up -d # 2. Flash the firmware -docker-compose exec esphome esphome run /config/Thread/esp32c6-thread-router.yaml --device=/dev/ttyACM0 +docker compose exec esphome esphome run /config/esp32c6-thread-router.yaml --device=/dev/ttyACM0 ``` -> **πŸ“Œ Note:** The docker-compose.yml mounts the parent `config/` directory to `/config` in the container. That's why you need `/config/Thread/` in the path above. This is necessary so ESPHome can find the build files in `.esphome/`. -
--- @@ -153,17 +194,13 @@ docker-compose exec esphome esphome run /config/Thread/esp32c6-thread-router.yam
🌐 Alternative: Web Dashboard (GUI) -Choose between local or hosted dashboard: - **Docker Dashboard:** ```bash -# Start container if not already running -docker-compose up -d - -# Start the dashboard -docker-compose exec esphome esphome dashboard /config +docker compose up -d ``` -Then open **http://localhost:6052** in your browser and navigate to the Thread folder. +The ESPHome container starts the dashboard automatically. Open **http://localhost:6052** in your browser. + +> ⚠️ The dashboard has no password and listens on all interfaces (host networking). Only run it on a trusted network, or stop the container after flashing. **Local Dashboard** (requires local ESPHome): ```bash @@ -171,15 +208,14 @@ esphome dashboard . ``` Then open **http://localhost:6052** in your browser and use the web interface. -**Hosted Dashboard** (no installation needed): -> **πŸŽ‰ No installation needed!** Flash directly from your browser. +**Browser Flashing via [web.esphome.io](https://web.esphome.io/):** -1. Visit **https://web.esphome.io/** -2. Click "Connect" and select your ESP32-C6 device -3. Upload your `esp32c6-thread-router.yaml` configuration file -4. Click "Install" to compile and flash +web.esphome.io can only **flash** a finished firmware file. It cannot compile your YAML. Because the TLV is compiled into the firmware, you have to build it yourself first: -**Perfect for:** Users who prefer GUI over command line, or quick flashing without local ESPHome installation. +1. Compile locally: `esphome compile esp32c6-thread-router.yaml` +2. The firmware is written to `.esphome/build/esp32c6-thread-router/.pioenvs/esp32c6-thread-router/firmware.factory.bin` +3. Open **https://web.esphome.io/** in Chrome or Edge, click "Connect", choose your ESP32-C6 and select "Install" +4. Upload the `firmware.factory.bin` file
@@ -212,51 +248,98 @@ Choose "Over The Air" option when prompted. You should see: #### 🌐 Method 2: Check Thread Network ```bash -# List all Thread devices in network -podman exec otbr ot-ctl router table +# List all Thread routers in the network +docker exec otbr ot-ctl router table # Your ESP32-C6 should appear with Extended MAC and good Link Quality # Show network state -podman exec otbr ot-ctl state +docker exec otbr ot-ctl state ``` +> **πŸ’‘ Note:** A new FTD first joins as a child (*REED*, Router Eligible End Device). Thread only promotes it to *router* when the mesh needs another router, which can take a few minutes, or not happen at all in a small network. It only shows up in the router table after that. You can see the current role in the **Thread Role** sensor in Home Assistant. + --- #### 🏠 Method 3: Home Assistant Integration - Go to **Settings β†’ Devices & Services** - The ESP32-C6 should appear as a discovered device - Add it to Home Assistant (use `esp32c6-thread-router.local` or IPv6 address) +- Enter the `api_encryption_key` from your `secrets.yaml` when asked - Check device status - should show "Online" -## Multiple Thread Routers +## πŸ“Š Diagnostics in Home Assistant -To flash multiple ESP32-C6 devices and use them as separate routers in the same Thread network: +The device exposes these diagnostic entities: -### 1. Create Device-Specific Configuration Files +| Entity | Meaning | +|--------|---------| +| **Thread Role** | `router`, `child`, `leader`, `detached` or `disabled` | +| **Thread RLOC16** | Short address in the mesh. Routers end in `00` (e.g. `0x3400`) | +| **Thread Channel** / **Thread IP Address** | Current channel and IPv6 address | +| **Thread Parent RSSI** | Signal strength to the parent. Only meaningful while the device is a child, not as router | +| **Thread TX Retries** / **Thread TX CCA Errors** | Rising fast = poor link or busy channel (e.g. WiFi on the same frequencies) | +| **Thread Partition Changes** | Rising = the mesh keeps splitting, check placement/range | +| **Uptime** | Detects unexpected reboots | +| **External Antenna** (XIAO only) | Switch between built-in and external antenna | +| **Restart** | Restart the device | -For each additional device, create a new YAML file (e.g., `esp32c6-thread-router-2.yaml`): +More sensors (link quality, RX/TX totals, attach attempts, ...) are available in the [openthread_info](https://esphome.io/components/text_sensor/openthread_info/) component and can be added to the `sensor:` section. -```yaml -esphome: - name: esp32c6-thread-router-2 # Must be unique! - friendly_name: ESP32-C6 Thread Router 2 # Optional but helpful +## βš™οΈ Advanced Options + +### πŸ“Ά Transmit Power +Since ESPHome 2026.3 the transmit power can be set. The ESP32-C6 supports -15 to 20 dBm: + +```yaml openthread: - device_type: FTD - tlv: !secret thread_tlv # Same TLV = same Thread network + output_power: 10dBm ``` -### 2. Flash Additional Devices -Each device will join the same Thread network and act as an independent router, extending your mesh coverage. +> ⚠️ Respect the regulatory limits for 2.4 GHz in your country. More power only helps if the other devices can also reach the router, since Thread links are bidirectional. + +### πŸ”„ Thread Network Was Recreated / TLV Changed + +The router stores the Thread dataset in flash after the first start and then **ignores** the `tlv` from the config. If you recreate your Thread network or change the TLV, the router stays in the old network. + +To apply a new TLV: + +1. Update `thread_tlv` in `secrets.yaml` +2. Uncomment `force_dataset: true` in the `openthread:` section and flash +3. Once the router has joined, comment it out again and flash once more + +> **πŸ’‘ Why not keep `force_dataset: true` permanently?** The Thread network can change its dataset at runtime, e.g. when Home Assistant moves it to another channel. With `force_dataset: true` the router would fall back to the old dataset from the config after every reboot. + +## πŸ”€ Multiple Thread Routers + +To flash multiple ESP32-C6 devices and use them as separate routers in the same Thread network, create a small file per additional device (e.g. `esp32c6-thread-router-2.yaml`) that reuses the main config: + +```yaml +substitutions: + name: esp32c6-thread-router-2 # Must be unique! + friendly_name: ESP32-C6 Thread Router 2 + # antenna_default: RESTORE_DEFAULT_ON # Optional: external antenna + +packages: + base: !include esp32c6-thread-router.yaml +``` + +Then flash it: +```bash +esphome run esp32c6-thread-router-2.yaml --device=/dev/ttyACM0 +``` + +All devices share the same `secrets.yaml` (same TLV = same Thread network). Changes to the main config apply to all devices on the next flash. Each device will join the same Thread network and act as an independent router, extending your mesh coverage. ## πŸ“‚ File Structure ``` . β”œβ”€β”€ esp32c6-thread-router.yaml # Main ESPHome configuration -β”œβ”€β”€ esp32c6-thread-router-2.yaml # Optional: Second router -β”œβ”€β”€ esp32c6-thread-router-3.yaml # Optional: Third router -β”œβ”€β”€ secrets.yaml # Shared credentials (not in git) +β”œβ”€β”€ esp32c6-thread-router-2.yaml # Optional: second router (includes the main config) +β”œβ”€β”€ secrets.yaml # Shared secrets (not in git) +β”œβ”€β”€ docker-compose.yml # Optional: ESPHome in Docker/Podman +β”œβ”€β”€ .github/workflows/ # CI: validates and compiles the config β”œβ”€β”€ .gitignore # Excludes secrets and build artifacts └── README.md # This file ``` @@ -266,6 +349,8 @@ Each device will join the same Thread network and act as an independent router, | Resource | Description | |----------|-------------| | πŸ“– [ESPHome OpenThread Documentation](https://esphome.io/components/openthread/) | Official ESPHome Thread component docs | +| πŸ“Š [ESPHome OpenThread Info](https://esphome.io/components/text_sensor/openthread_info/) | Thread diagnostic sensors | +| πŸ“‘ [Seeed XIAO ESP32C6 Wiki](https://wiki.seeedstudio.com/xiao_esp32c6_getting_started/) | Pinout and RF switch of the XIAO ESP32C6 | | πŸ”— [OpenThread Primer](https://openthread.io/guides/thread-primer/) | Learn the basics of Thread networking | | 🏠 [Home Assistant Thread Integration](https://www.home-assistant.io/integrations/thread/) | How Thread works in Home Assistant | @@ -278,4 +363,3 @@ Each device will join the same Thread network and act as an independent router, ⭐ Star us on [GitHub](https://github.com/firsttris/esp32c6-thread-router) β€’ πŸ› [Report a Bug](https://github.com/firsttris/esp32c6-thread-router/issues) β€’ πŸ’‘ [Request a Feature](https://github.com/firsttris/esp32c6-thread-router/issues) - diff --git a/docker-compose.yml b/docker-compose.yml index 7310aa9..c3a25b7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,15 @@ -version: '3' services: esphome: container_name: esphome - image: ghcr.io/esphome/esphome:latest + image: ghcr.io/esphome/esphome:stable volumes: - - /home/tristan/Projects/esphome/config:/config + # This repository is mounted as /config + - ./:/config - /etc/localtime:/etc/localtime:ro - restart: always - privileged: true + # USB device used for the first flash. Override with ESPHOME_DEVICE=/dev/ttyUSB0 docker compose up -d + # The device must be plugged in when the container starts. + devices: + - ${ESPHOME_DEVICE:-/dev/ttyACM0}:${ESPHOME_DEVICE:-/dev/ttyACM0} + # Host networking is needed for mDNS discovery and the dashboard on port 6052 network_mode: host - environment: - - USERNAME=test - - PASSWORD=test + restart: unless-stopped diff --git a/esp32c6-thread-router.yaml b/esp32c6-thread-router.yaml index 2f64170..89635d6 100644 --- a/esp32c6-thread-router.yaml +++ b/esp32c6-thread-router.yaml @@ -1,30 +1,103 @@ -# ESPHome Configuration fΓΌr ESP32-C6 Thread Router -esphome: +# ESPHome configuration for an ESP32-C6 Thread router (FTD) +# +# Required secrets (secrets.yaml): thread_tlv, api_encryption_key +# See README.md for details. + +substitutions: name: esp32c6-thread-router friendly_name: ESP32-C6 Thread Router + # Seeed XIAO ESP32C6 antenna at boot, can be changed in Home Assistant later. + # RESTORE_DEFAULT_OFF = built-in ceramic antenna, RESTORE_DEFAULT_ON = external U.FL/SMA antenna + antenna_default: RESTORE_DEFAULT_OFF + +esphome: + name: ${name} + friendly_name: ${friendly_name} -# ESP32-C6 Board Konfiguration mit ESP-IDF Framework (erforderlich fΓΌr Thread) +# ESP-IDF is required for OpenThread esp32: board: esp32-c6-devkitm-1 framework: type: esp-idf -# Logger fΓΌr Debugging logger: -# API fΓΌr Home Assistant Verbindung api: + encryption: + key: !secret api_encryption_key -# OTA Updates +# OTA uploads are encrypted and authenticated with the API encryption key ota: - platform: esphome + encryption: {} -# IPv6 aktivieren (erforderlich fΓΌr Thread) +# Thread is IPv6-only network: enable_ipv6: true -# OpenThread Konfiguration openthread: - device_type: FTD # Full Thread Device = Router + device_type: FTD # Full Thread Device: router-eligible tlv: !secret thread_tlv + # Stored dataset in flash wins over `tlv` by default. Uncomment once after + # your Thread network was recreated so the new TLV is applied (see README). + # force_dataset: true + # Transmit power, ESP32-C6 allows -15..20 dBm. Respect your local regulatory limits. + # output_power: 10dBm + +# --- Seeed XIAO ESP32C6 specific (remove this block for other boards) --- +# GPIO3 LOW powers the RF switch, GPIO14 selects the antenna (LOW = built-in, HIGH = external). +switch: + - platform: gpio + id: rf_switch_power + pin: + number: GPIO3 + inverted: true + restore_mode: ALWAYS_ON + internal: true + - platform: gpio + name: External Antenna + pin: GPIO14 + restore_mode: ${antenna_default} + entity_category: config + icon: mdi:antenna + +# User LED (active low): blinks on warnings/errors, off when healthy +status_led: + pin: + number: GPIO15 + inverted: true + ignore_strapping_warning: true +# --- end of XIAO specific block --- + +button: + - platform: restart + name: Restart + +text_sensor: + - platform: openthread_info + role: + name: Thread Role + rloc16: + name: Thread RLOC16 + channel: + name: Thread Channel + ip_address: + name: Thread IP Address +sensor: + - platform: uptime + name: Uptime + - platform: openthread_info + # Parent values are only meaningful while the device is a child/REED, not once it is a router + parent_average_rssi: + name: Thread Parent RSSI + update_interval: 60s + tx_retries: + name: Thread TX Retries + update_interval: 60s + tx_err_cca: + name: Thread TX CCA Errors + update_interval: 60s + partition_id_changes: + name: Thread Partition Changes + update_interval: 60s