Skip to content

Latest commit

 

History

121 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

homecadia

A battery-powered Matter-over-Thread room sensor for Home Assistant, built from scratch on the ESP32-C6 with esp-matter and ESP-IDF. 125 µA average, e-ink display, rotary dial, no cloud, no Wi-Fi, and every trap written down.

build-sensor-01 License: MIT ESP-IDF v5.5.5 esp-matter v1.6 Matter over Thread Average current 125 µA

sensor-01

A room temperature and humidity sensor that reports to Home Assistant over Thread as a Matter sleepy end device, shows its own readings on a 2.9" e-ink panel, and takes input from a rotary dial. It averages 125 µA measured on the bench, about eighteen months on one 2000 mAh cell at 85 % usable. No vendor cloud, no hub beyond a Thread border router, and no Wi-Fi: the radio is compiled out.

Everything is here: firmware, drivers, pin map with schematic-verified board facts, measured power budget, 3D-printable enclosure, bill of materials with real prices, and the reasoning behind each decision, including the ones that turned out wrong.

At a glance

MCU Seeed XIAO ESP32-C6 (RISC-V, 802.15.4 + BLE; Wi-Fi compiled out)
Stack esp-matter v1.6 on ESP-IDF v5.5.5, C++
Network Matter over Thread, OpenThread minimal device (MTD), commissioned over BLE
Power mode Intermittently Connected Device (ICD), short idle time: 15 s parent polls, 600 s idle interval, automatic light sleep
Controller Home Assistant with matterjs-server, Home Assistant Connect ZBT-2 border router
Clusters Temperature Measurement, Relative Humidity Measurement, Power Source (battery % and voltage), Thread Network Diagnostics with counters
Measured current 125 µA paired and settled (sleep floor 57 µA, parent poll ~0.48 mC); 19.7 µA unpaired in deep sleep
Display 2.9" 296×128 SSD1680 e-paper, partial refresh ~6–7.5 mC, full ~19 mC
Status One unit bench-verified on a Nordic PPK2; enclosure being redesigned

If you are building Matter or Thread devices on an ESP32

Most of this repo is reusable even if you never build this sensor. The findings that took longest, with where to read them:

Topic Where
Getting an ESP32-C6 Matter device from ~376 µA to 125 µA: ADC power domain, bus power-down flags, ICD mode, a pin regression power-budget.md, field-notes §20–§22, §28
Long Idle Time ICD buys nothing with Home Assistant (no check-in client) and costs ~67 µA; what to use instead retro M8, sdkconfig.defaults
A non-wake GPIO with a level interrupt under CONFIG_PM_SLP_DISABLE_GPIO: +16 µA floor, polls 60 % dearer field-notes §28
Waking light sleep from a rotary encoder: LP GPIOs only, level not edge field-notes §21, ec11_encoder
An unpaired Matter device draws 28 mA; deep sleep that works (restart, then sleep before the stack starts) field-notes §26–§27
attribute::update() silently does nothing for code-driven clusters in esp-matter v1.6 field-notes §9
Commissioning to Home Assistant without HAOS: test certificates, BLE proxy, border router commissioning.md
Thread coverage, border-router outages, a bare XIAO ESP32-C6 as a Thread router field-notes §6–§7, §24, infrastructure.md, bringup.md Radio / Matter
Measuring µA on a sleepy radio device with a PPK2, and analysing captures offline tools/ppk2-events.py, retro T7–T9
Hardware bring-up traps: FPC ribbons, flux, underside pads, breadboard battery paths assembly.md, battery-runbook.md, field-notes §15–§19, §25
Eight weeks of bring-up in one place: what went wrong, why, and the checklist for the next unit retro.md

Status: bench-verified, not yet assembled. One unit runs on the bench (XIAO, driver board, panel, SHT40, encoder on a breadboard) with a Nordic PPK2 standing in for the cell. Verified there: commissioning to Home Assistant over Thread, display output, battery-voltage reading (within 3 mV of a meter, 3.4–4.0 V), 125 µA settled paired current (2026-10-05), 19.7 µA unpaired deep sleep, and the dial waking the chip from light sleep on battery power. It has run 21 h on a real LiPo with no restart. Not yet done: a long soak on a real cell, and the enclosure, which is being redesigned. Open rows are in docs/bringup.md; code written ahead of hardware is marked HW-VERIFY and tracked there. The build procedure, with a test gate after every soldering stage, is docs/assembly.md.

Devices

Device Status Description
sensor-01 in development Battery-powered room temp/humidity sensor. 2.9" e-ink display, rotary dial, Matter-over-Thread sleepy end device (SIT ICD). 3 units.

What you can lift from here

The drivers are self-contained ESP-IDF components with no dependencies beyond the IDF itself. Copy the directory into your own components/ and it builds. MIT licensed.

Component Why it might be useful
sht40 Sensirion SHT40 on the ESP-IDF 5.x i2c_master API (not the deprecated legacy driver), high-precision single-shot with CRC-8 validation
ssd1680 2.9" e-paper with differential partial refresh, periodic full refresh to clear ghosting, and panel deep sleep between updates — the parts most sample code leaves out
monogfx 1-bit framebuffer renderer with a scaled 5×7 font and a seven-segment digit routine. No LVGL, no external graphics library
ec11_encoder EC11 rotary encoder decoded from a Gray-code transition table in an ISR — bounce-immune without debounce delays, and draws no idle current

Tools, usable on any project:

Tool What it does
tools/ppk2-events.py Reads a Nordic PPK2 .ppk2 capture offline and splits it into sleep floor, radio polls, bursts and short wakes with their charge, so one capture gives a per-item current budget
tools/matter-node.py Reads a node through the Home Assistant Matter server WebSocket: the cached attributes (free for the device) or a forced interview
tools/check-profiles.sh Asserts a shipping and a bench sdkconfig really differ on sleep, and that test-only options are off in shipping. Runs in CI

Also reusable regardless of hardware: the power budget and the firmware policies it forced, the retro, and the documented traps below.

Features

Firmware capabilities, all implemented in this repo unless noted:

  • Matter over Thread, Wi-Fi compiled out. OpenThread MTD, commissioned over Bluetooth Low Energy, joins via any OpenThread border router (developed against Home Assistant Connect ZBT-2).
  • Short Idle Time ICD (Intermittently Connected Device): automatic light sleep between 15 s parent polls, and a 600 s idle interval that the device offers as its subscription max interval, so a report with nothing new goes out every 10 min. Long Idle Time was dropped on 2026-10-04: Home Assistant registers no check-in client, so it only added cost (retro M8).
  • Unpaired deep sleep: a unit nobody pairs deep-sleeps at 19.7 µA once its 15-minute commissioning window closes, and a turn of the dial starts a new window (field-notes §27).
  • Thread Network Diagnostics with counters (attach attempts, parent changes, MAC retries), readable from the controller at no reporting cost.
  • Matter clusters: TemperatureMeasurement, RelativeHumidityMeasurement, and PowerSource with battery percentage and voltage.
  • Delta-gated reporting — a report is sent on ≥0.2 °C / ≥1 %RH change, with a forced heartbeat every N polls, instead of on every sample.
  • SHT40 driver (firmware/components/sht40) on the ESP-IDF 5.x i2c_master API, high-precision single-shot with Sensirion CRC-8 checking.
  • SSD1680 e-paper driver (firmware/components/ssd1680) with differential partial refresh, periodic full refresh for ghosting, and panel deep sleep between updates.
  • 1-bit framebuffer renderer (firmware/components/monogfx) with a scaled 5×7 font and a seven-segment digit routine — no external graphics library.
  • EC11 rotary encoder (firmware/components/ec11_encoder) decoded from a Gray-code transition table in an interrupt handler: bounce-immune and drawing no idle current.
  • Local-only UI: readings, diagnostics, and settings views; settings persist in NVS. The display never renders Home Assistant state — it shows what this device measured.
  • Battery monitoring via a 2×1 MΩ + 100 nF divider on GPIO4 (an underside pad): 64 one-shot ADC samples after a 20 ms settle, eFuse curve-fitting calibration, and a scale factor fitted against a metered source to correct the ADC input loading the 500 kΩ divider. Open-circuit-voltage lookup table for percent.
  • Dial wakes the chip from light sleep on a detent (encoder A on an LP GPIO underside pad, level-triggered).
  • Factory reset on a 10-second encoder press (turn first: the switch is on a pin that cannot wake the chip); commissioning and low-battery states shown on a single LED and on the display.
  • CI builds the flashable images on every push touching firmware/** and uploads them as an artifact.

sensor-01 hardware

Seeed XIAO ESP32-C6 + Seeed ePaper Driver Board for XIAO V2 + 2.9" mono e-ink (296×128, SSD1680) + Grove SHT40 + EC11 rotary encoder + 2000mAh LiPo, in a wall-mount enclosure remixed from veltoc (hardware/case — rev 4, FDM PLA, laser-engraved wordmark).

assembled render

Assembled view rendered from the enclosure STLs. Colours and finish are indicative; no unit has been built yet.

sensor-01 pin map

Full bill of materials: docs/bom.md. Pin map with the schematic-verified board facts behind this diagram: docs/pinmap.md. The official bare-board pinout is on the Seeed XIAO ESP32-C6 wiki (not reproduced here — Seeed's wiki content is GPL-3.0, this repo is MIT).

⚠️ This build involves a bare lithium-polymer cell soldered directly to the board. Read the polarity warning at the top of docs/assembly.md before connecting anything — reversed polarity destroys the charge circuit, and damaged LiPo cells are a fire hazard.

Firmware

Espressif esp-matter v1.6 on ESP-IDF v5.5.5, C++. Thread only (Wi-Fi disabled), commissions over Bluetooth Low Energy to Home Assistant via an OpenThread border router (Home Assistant Connect ZBT-2). Build instructions: docs/build.md.

The device advertises Espressif's test vendor ID 0xFFF1 and shared test commissioning credentials. It is not a Matter-certified product; controllers will show an uncertified-device warning. Per-device factory partitions (esp-matter-mfg-tool) are an open work item — see docs/commissioning.md.

Documentation

Doc Contents
docs/bom.md Bill of materials, part SKUs, divider-value rationale
docs/pinmap.md Pin assignments and schematic-verified XIAO ESP32-C6 board facts
docs/assembly.md Wiring and build order, battery-polarity warning
docs/build.md Pinned toolchain SHAs, Docker and native builds, flashing from WSL2
docs/commissioning.md Pairing to Home Assistant: the three preconditions, the BLE-proxy workaround, factory reset. Apple Home is not available here and the doc says why
docs/retro.md Retrospective of the whole bring-up: lessons, timeline, every problem with its cause and rule, the checklist for the next unit, open questions
docs/field-notes.md Traps that cost real time during bring-up, one numbered section per incident (cited as §n). Read before bringing up another unit
docs/power-budget.md Modelled and measured current draw per item, battery-life calculator, the policies it forced
docs/battery-runbook.md Why a pack reads 0 V or a unit resets on battery: protection-board causes, look-alikes, and the test order
docs/bringup.md Hardware verification checklist — every HW-VERIFY marker has a row
docs/diagrams/ Bench wiring, battery/divider schematic, enclosure internals and the no-solder bring-up ladder (self-contained HTML)
docs/infrastructure.md Network-side hardware: ZBT-2 border router, Thread coverage, Voice PE, order status
docs/source-reliability.md Documented traps in vendor docs and third-party sources
hardware/case/README.md Enclosure: revision history, measured wall thicknesses, print vendor and material, engraving artwork
circuit-board-maker/ Custom-PCB evaluation and two paper board designs. Concluded: not worth it for three units. Never fabricated — see the conflicts list before reviving it

Traps that cost time

Findings that aren't in any datasheet, written down so the next person doesn't pay for them twice. Full list in docs/source-reliability.md and docs/pinmap.md.

  • A0 and D0 are the same pin on the XIAO ESP32-C6. If your display uses D0 for reset, the obvious ADC pin is gone. Battery sense moved to an underside test pad (pinmap).
  • The C6's RF switch is off at reset. GPIO3 has a 10k pull-up and must be driven low in firmware before the antenna works; GPIO14 selects onboard ceramic versus U.FL. Neither pin is on the header. Schematic-verified.
  • espboards.dev pin tables are wrong for the C6 — they're shared boilerplate across ESP32 variants.
  • The Seeed wiki's battery-sense snippet doesn't apply. It assumes an onboard divider the C6 schematic shows isn't populated.
  • A 100k battery divider costs ~9% of a 300µA budget. 2×1MΩ + 100nF instead, at the price of a high-impedance ADC source (power budget).
  • A live ADC unit keeps the C6's radio power domain on through every light sleep (~95 µA). Create and delete the unit per reading, and hold a no-light-sleep lock across it or the chip can fail to wake (field notes §21).
  • The chip only powers down its peripherals in light sleep if every I2C and SPI bus asks for it (flags.allow_pd, SPICOMMON_BUSFLAG_SLP_ALLOW_PD). Without them the sleep floor was 279 µA; with them, 56 µA (§21).
  • A multimeter on a high-impedance node is a load, not a reference. A 10 MΩ meter reads a 1 MΩ:1 MΩ divider midpoint ~5 % low. Calibrate the ADC against the supply, end to end (§23).
  • Light sleep kills the USB serial port in ~2 seconds, which makes a board effectively unflashable without CONFIG_USJ_NO_AUTO_LS_ON_CONNECTION (build has the recovery procedure, including the WSL2/usbipd quirks).
  • Opening /dev/ttyACM0 with a plain shell read can hard-reset the chip — it pulses the USB-Serial-JTAG control lines. Use idf.py monitor.
  • Seeed's sleep-current figures are for a bare board. Measured on this build: a 45–57 µA light-sleep floor once the fixes here were in; 376 µA before them (power budget).
  • Long Idle Time ICD does nothing for you under Home Assistant. No check-in client registers, so the device runs short-idle mode anyway, while LIT forces a 5 s minimum active period after every exchange and an extra wake a minute. Turning it off and raising the idle interval to 600 s took the average from 307 to 185 µA (retro M8).
  • A non-wake GPIO with a level interrupt reads as triggered in light sleep. With CONFIG_PM_SLP_DISABLE_GPIO, pins that are not wake sources are isolated and read low. A push switch on an HP pin with a low-level interrupt raised the floor 16 µA and made every Thread poll 60 % dearer; gpio_sleep_sel_dis() fixed it, 185 → 125 µA (§28).
  • Only LP GPIOs (0–7) wake the C6 from light sleep with peripheral power-down on, and only on a level. On the XIAO the free LP pins are underside pads (§21, pinmap).
  • An unpaired Matter device on the C6 never light-sleeps and draws ~28 mA. esp_deep_sleep_start() from the running stack hung or stuck at 20 mA; what worked is restarting and entering deep sleep at the top of the next boot, before Bluetooth, Thread or power management start (§26–§27).
  • A USB host keeps the C6 out of light sleep, so a sleep bug can be invisible for weeks on the bench. Test sleep on battery or a PPK2 with USB out (§20).
  • A breadboard row in the battery path brown-outs the radio. Thousands of restarts read as a firmware fault; the Matter bootReason attribute said brown-out all along (battery-runbook B3).

Milestones

# Deliverable Status
1 Repo scaffold, docs, CI compiling an esp-matter skeleton for esp32c6 done
2 Matter temp/humidity over Thread, commissions to HA (TinyENV parity) done 2026-08-23 — commissioned to HA over ZBT-2 OTBR, readings live in HA; see docs/field-notes.md for the preconditions
3 Display driver, view 1 rendering readings, measured refresh cost done — display verified 2026-08-22 (full 1.79 s / partial 0.54 s BUSY); refresh charge measured 2026-10-05: partial ~6–7.5 mC, full ~19 mC
4 Encoder, views, settings, wake behavior done 2026-09-28 — a detent wakes the chip from light sleep on battery (encoder A on the MTCK pad); the push switch registers within 2 s of a turn
5 ICD tuning, battery reporting, power budget with measured numbers done — 238 µA over 12.6 h (2026-09-22); 125 µA settled after the ICD and pin fixes (2026-10-05); unpaired deep sleep 19.7 µA; battery voltage within 3 mV. See docs/power-budget.md
6 Factory reset, low-battery behavior, assembly guide final, v1.0.0 factory reset + LED + low-bat display done; rest awaits hardware

Repo layout

docs/               BOM, pin map, assembly, commissioning, power budget, build,
                    field notes, retro
hardware/case/      wall-mount enclosure (veltoc remix, MIT) + engraving artwork
circuit-board-maker/ custom-PCB evaluation + two unfabricated KiCad designs
firmware/
  components/       drivers shared across future homecadia devices
  sensor-01/        esp-matter application
tools/              PPK2 capture analysis, Matter server node reader, profile check
.github/workflows/  CI: firmware build on push, .bin artifacts

Building it yourself

  1. Read docs/bom.md — real parts, real prices in CAD, real vendors, and which ones have known counterfeits or reversed polarity.
  2. Print the enclosure from hardware/case, or adapt it. The revision history there explains why each dimension is what it is, which matters if you swap the battery or the display.
  3. Build the firmware with docs/build.md — Docker path matches CI exactly and is the low-friction option.
  4. Wire it using docs/pinmap.md and docs/assembly.md. Read the LiPo polarity warning first.
  5. Commission it with docs/commissioning.md, then work through docs/bringup.md.

Forking for different hardware? firmware/sensor-01/main/app_config.h is the single source of pin truth and the place to start.

Contributing

This is a personal build against one specific parts list, so the parts list, the pinned toolchain and the locked design decisions are unlikely to change. That said: corrections are very welcome, especially if you've measured something this repo only models, or if one of the traps turns out to be wrong on your hardware. Issues and pull requests both fine.

If you are working on Matter or Thread devices on the ESP32-C6 (or H2, or any ESP32 with an 802.15.4 radio) and hit something documented here, or something that should be, open an issue: comparing measured numbers across boards is the fastest way to tell a board quirk from a firmware one.

License

MIT, see LICENSE. Third-party attribution — including the veltoc enclosure remix and the TinyENV architecture references — is in NOTICE.md.

About

Battery-powered Matter-over-Thread room sensor on the ESP32-C6 (esp-matter, ESP-IDF) for Home Assistant: 125 µA sleepy end device, e-ink display, rotary dial, every bring-up trap documented

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages