A standalone Zigbee repeater for the Seeed XIAO ESP32-C6, with battery monitoring and remote antenna / TX-power control from Zigbee2MQTT.
![]() |
![]() |
| XIAO side: U.FL lead going through the wall | Other side: the external antenna |
I wanted a customisable, standalone Zigbee repeater to reach my Linky power meter at the end of a corridor. So I put a XIAO ESP32-C6 on one side of the wall and ran a long PCB antenna I had through to the other side.
The XIAO supports battery operation, so I added battery level monitoring. It needs a little resistor mod (see below): the voltage is read through Seeed Studio's documented 1:2 divider, and the percentage is estimated from a standard single-cell LiPo voltage curve. I also added a setting to switch remotely between the internal and external antenna, so you can compare the range of both. Unsurprisingly, the external antenna worked better in my case.
Linky before and after. Before the router, I had to move my Zigbee coordinator out of my office to sit next to the front door, and even then the Linky only reached a link quality (LQI) of about 20β40. With the XIAO in the corridor and the coordinator back in the office, the Linky jumped to ~160 straight away, and it has stayed there since (180 in the screenshot above).
Antenna and TX power comparison. Same spot, flipped from Zigbee2MQTT without touching the board. LQI is the router's link quality as seen by Zigbee2MQTT:
| External antenna, +20 dBm | Internal antenna, +20 dBm | External antenna, 0 dBm |
|---|---|---|
![]() |
![]() |
![]() |
| LQI 180 | LQI 136 | LQI 92 |
These are single readings, and LQI moves around a bit. Watch it for a few minutes after each change before drawing conclusions.
This project is a fork of firsttris/esp32c6-zigbee-router. If you just want a simple router on any ESP32-C6, start there. This fork is for XIAO ESP32-C6 owners who want to tune and monitor the router from their coordinator without reflashing.
| This fork | Original | |
|---|---|---|
| Zigbee router / range extender, no WiFi or secrets | β | β |
| Internal / external antenna selection | β at runtime, from Zigbee2MQTT | at build time |
| Zigbee TX power (β15β¦+20 dBm), kept across reboots | β at runtime, from Zigbee2MQTT | β |
| LiPo battery voltage and % (standard Power Configuration cluster) | β | β |
| Zigbee2MQTT external converter | β | β |
| Prebuilt firmware, no toolchain needed | β | β |
| Hold BOOT 5 s to leave the network and re-pair | β | β |
| Status LED on warnings / errors | β | β |
| Pinned ESPHome / ESP-IDF / component versions | β | latest |
| Zigbee stack | luar123/zigbee_esphome |
ESPHome native zigbee |
| Boards | XIAO ESP32-C6 only | any ESP32-C6 |
Why not ESPHome's native Zigbee component? As of ESPHome 2026.7, the native component on ESP32 only reports read-only sensors. It can't expose writable or custom attributes, so the remote antenna/TX-power controls and the standard battery attributes aren't possible with it yet.
- Seeed Studio XIAO ESP32-C6 and a USB data cable
- A Zigbee coordinator. Zigbee2MQTT is recommended; ZHA works for routing only.
- Optional: an external 2.4 GHz antenna with a U.FL connector
- Optional: a 1-cell LiPo and two resistors for battery monitoring
The router must stay powered. A backup LiPo does not turn it into a sleepy end device; the Basic cluster still reports mains power.
Pick one option. The prebuilt image is the easiest. It starts on the internal antenna at +20 dBm, and you can change both later from Zigbee2MQTT, so most people never need to build anything.
- Download
xiao-esp32c6-zigbee-router.factory.binfrom the latest release.SHA256SUMSin the same release lets you check the download. - Plug the XIAO into your computer with a data cable.
- Open web.esphome.io in Chrome or Edge (Web Serial is not available in Firefox or Safari).
- Click Connect and pick the XIAO's serial port.
- Click Install, select the
.factory.binfile, and confirm. - When it finishes, press RESET on the XIAO (or unplug and replug it).
Prefer the command line? Use esptool:
esptool --chip esp32c6 --port /dev/ttyACM0 write-flash 0x0 xiao-esp32c6-zigbee-router.factory.bin(esptool v4 spells it write_flash.) Always flash the .factory.bin at
0x0. The .ota.bin is an app-only image and won't boot from there.
Use this if you want to change the YAML. It requires Python 3.11+ and Git, and the first build downloads ESP-IDF (~600 MB).
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
esphome run esp32-c6-zigbee-router.yaml --device /dev/ttyACM0Or with Docker (plug the XIAO in before starting the container):
ESPHOME_DEVICE=/dev/ttyACM0 docker compose up -d
docker compose exec esphome esphome run /config/esp32-c6-zigbee-router.yaml --device=/dev/ttyACM0The container also serves the ESPHome dashboard on port 6052 without a password. Only run it on a trusted network.
Serial port names and flashing troubleshooting
| OS | Port |
|---|---|
| Linux | /dev/ttyACM0 (add yourself to dialout if access is denied) |
| macOS | /dev/cu.usbmodem* |
| Windows | COM3, COM4, β¦ |
python -m serial.tools.list_ports lists them. If the board isn't detected or
the upload fails: hold BOOT, tap RESET, release BOOT, then retry. Tap
RESET once more after flashing. Close any other serial monitor first.
- Enable Permit join in Zigbee2MQTT (or Add device in ZHA).
- Power the router. A factory-new router starts joining within about a minute.
- Wait for the interview to finish and the device type to show Router.
- Move it to its final spot and check the network map for neighbours. Joining proves membership, not better coverage.
To move it to another network, remove it from the coordinator, enable pairing, then hold BOOT for 5 seconds. It leaves the network, clears its Zigbee data and rejoins. Antenna and TX power settings are kept. (Holding BOOT while plugging in enters the bootloader instead.)
Without the converter the router still routes, but Zigbee2MQTT shows it as unsupported and you get no battery or RF controls. With it, you get this:
Battery and voltage read N/A here because this board doesn't have the resistor mod fitted, which is the expected behaviour.
- Find your Zigbee2MQTT data folder, the one containing
configuration.yaml:- Home Assistant add-on:
/config/zigbee2mqtt/(or/homeassistant/zigbee2mqtt/when browsing with the File editor / Samba / SSH add-ons) - Docker: the folder you mounted to
/app/data
- Home Assistant add-on:
- Create an
external_convertersfolder next toconfiguration.yamland copyzigbee2mqtt/xiao-esp32c6-zigbee-router.mjsinto it:zigbee2mqtt/ βββ configuration.yaml βββ external_converters/ βββ xiao-esp32c6-zigbee-router.mjs - Allow external converters. Since Zigbee2MQTT 2.11, new installations
have them disabled by default. Add this to
configuration.yamlif youradvanced:section doesn't already have it (add the line to the existingadvanced:block rather than creating a second one):advanced: enable_external_js: true
- Restart Zigbee2MQTT itself. This setting is only read when Zigbee2MQTT
starts. On Home Assistant, go to the Zigbee2MQTT add-on page and click
Stop, then Start.
β οΈ Settings β Restart Home Assistant is not enough: it only restarts Home Assistant Core, and the add-on keeps running with the old settings. - Check the Zigbee2MQTT log for
Loaded external converter 'xiao-esp32c6-zigbee-router.mjs'. - Re-interview the router if it was paired before the converter was installed: use the Interview button on its device page, or remove it and pair again.
The device then shows up as XIAO-ESP32C6-Zigbee-Router with battery,
voltage, antenna and tx_power. Through MQTT discovery, these also become
Home Assistant entities. You can also send commands to
zigbee2mqtt/<friendly-name>/set:
{"antenna": "external"}
{"tx_power": 10}Converter save times out? If you paste the converter into the Zigbee2MQTT web UI and get
bridge/request/converter/save β Request timed out, Zigbee2MQTT is still running with external JavaScript disabled. Check step 3, then Stop/Start the add-on as in step 4. A401 Unauthorizedfor the web manifest in the browser console is unrelated noise.
No converter? Use the Dev console
The settings are plain Zigbee attributes, so you can read/write them from Zigbee2MQTT's Dev console tab without installing anything:
| Setting | Endpoint | Cluster | Attribute | Type | Values |
|---|---|---|---|---|---|
| Antenna | 1 | 64512 (0xFC00) |
0 |
boolean (0x10) |
0 internal, 1 external |
| TX power | 1 | 64512 (0xFC00) |
1 |
int8 (0x28) |
-15 β¦ 20 dBm |
- The XIAO has an onboard ceramic antenna and a U.FL connector. GPIO3 enables the RF switch; GPIO14 selects internal (low, the default) or external (high).
- Connect the external antenna before selecting it, or range collapses.
- Use it to compare: switch antennas, wait a few minutes, and watch the link quality to the router and to the devices behind it.
- TX power defaults to +20 dBm. Antenna gain adds to radiated power, so choose a value that keeps you within your local EIRP limit.
The XIAO ESP32-C6 can run from and charge a single-cell LiPo, but the battery isn't connected to any ADC pin out of the box. To read it, add a 1:2 voltage divider from BAT to A0 (GPIO0) using 200 kΞ© resistors, as described in Seeed's battery-voltage monitoring guide (section "Reading Battery Voltage"):
BAT (+) ββ[ 200 kΞ© ]βββ¬βββΊ A0 / GPIO0
β
[ 200 kΞ© ]
β
GND βββββββββββββββββββ
- A full LiPo (4.2 V) shows up as 2.1 V on A0, well within the ADC range. The firmware doubles the reading to get the battery voltage.
- Never wire BAT straight to A0.
- Battery % comes from a standard single-cell LiPo voltage curve (3.3 V = 0 %, 4.2 V = 100 %). It reads high while charging and low under load. Check it against a multimeter once.
- Without the mod, the readings are meaningless. Usually they show N/A, but a floating pin can produce plausible-looking values. This is not a battery-presence detector.
Readings update every 60 seconds and are reported on the standard Power Configuration cluster, so any coordinator can read them.
| Cluster / attribute | Type | Meaning |
|---|---|---|
0x0001 / 0x0020 |
UINT8, report | Battery voltage, 100 mV units (0xFF unknown) |
0x0001 / 0x0021 |
UINT8, report | Battery %, half-percent units 0β200 (0xFF unknown) |
0xFC00 / 0x0000 |
BOOL, read/write/report | false internal antenna, true external |
0xFC00 / 0x0001 |
INT8, read/write/report | TX power in dBm, β15β¦+20 |
Device type RANGE_EXTENDER, model ID xiao-esp32c6-zigbee-router. The
converter also matches the older model ID esp32-c6-zigbee-router.
The original now uses ESPHome's native Zigbee stack and a different partition layout. To switch to this fork (or back), the Zigbee data must be wiped:
- Remove the router from your coordinator.
- Erase the flash:
esptool --port /dev/ttyACM0 erase-flash - Flash this firmware and pair again.
Normal updates of this firmware keep the pairing and RF settings. Don't erase the flash routinely.
- XIAO ESP32-C6 only. The pins in the YAML (GPIO0/3/9/14/15) are XIAO-specific.
- WiFi is off. WiFi and Zigbee share one radio on the C6; enabling WiFi makes the router miss Zigbee traffic.
- A new TX power is applied after waiting up to 100 ms for the Zigbee stack; if that times out, the failure isn't reported. The attribute readback alone doesn't prove the radio took it.
- No custom ZHA quirk is provided. ZHA routes fine but doesn't expose the RF controls.
- Tristan Teufel (firsttris): the original project this is forked from, including the Docker setup and the BOOT-reset / status-LED pattern.
- luar123/zigbee_esphome: the Zigbee component for ESPHome.
- Seeed Studio: XIAO RF-switch and battery-divider documentation.
MIT Β© framethespace. The original project by firsttris has no license file; the Docker setup adapted from it remains credited to its author.







