Repository navigation
Developers
How amble-home works, how to build and test it, and how to contribute. The rules of the repository, including who decides what, are in AGENTS.md; the reasons behind the design are in docs/decisions.md. Read those before changing anything that touches the mesh.
amble-home is one ESPHome external component (components/amble/) plus two small web surfaces. ESPHome supplies everything a connected device needs: Wi-Fi, the captive portal, Improv, over-the-air updates, the native Home Assistant API, logging and the web server. The component adds only what ESPHome does not have: a Bluetooth mesh node that speaks amaran's dialect.
Inside the component:
-
The mesh node (
mesh_node). ESP-IDF's BLE mesh stack, with the imported network and application keys injected through the IDF's internal calls rather than provisioning. The box is its own node at a unicast address it picks at first boot from the top half of the address space, kept for life in flash. It never provisions and never relays: the amaran app stays the owner of the mesh, and the box is a guest on it. In a public build it also never sends to a group or broadcast address; that path is compiled out because a group command reaches every light on the mesh at once, lights in use and a neighbor's on a shared mesh included, and nothing on a stranger's box can say which of those are its own. -
The Telink codec (
telink). The vendor-model messages the lights understand: on/off, intensity 0 to 1000, CCT, HSI, green/magenta, status request, status decode. Ported from the earlier bridge with its comments. -
The status snoop. Lights reply to the app's address, not to the box, so the IDF never delivers those replies to our model. The component wraps the stack's network receive entry point at link time (
-Wl,--wrap=bt_mesh_generic_net_recv), lets the real function run, then decrypts a copy of the frame with the keys it holds. No patch to ESP-IDF; the project builds against whatever IDF ESPHome pins. -
The transmit guard (
guard). Three modes. Dev: a fail-closed allowlist of one spare fixture. Release: exactly the fixtures in the box's own table. Production: the author's studio build, which may also use the group address. The guard is what keeps a box, bench or production, from reaching lights that are not its own. -
The fixture table and store (
fixture,store,identity). The imported payload in flash (NVS), and a record of every mesh identity the box has used, so an address it returns to resumes its sequence number past where it left off. -
The lights (
light). One Home Assistant light, green/magenta number and identify button per fixture, created at boot from the table. ESPHome has no public way to register a component created at runtime, so the component drives each light's lifecycle itself. That is a reach around the framework, not a feature of it; the upstream ask under Open ideas is for ESPHome to offer that entry point. -
The web routes (
web,pin,cloud). The Home and Edit pages, gzip-embedded at build time; the status JSON; the import route; the PIN; and the account door's box side, which makes amaran's sign-in call and hands the page a one-time token.
Outside the component:
-
web/box/: the Home and Edit pages and their JavaScript.build.mjsassemblesdist/, which the component embeds. -
web/importer/: the database reader (sql.js, in the browser) and the account door's page side, shared by the Edit page and the tools. -
web/flasher/: the flasher page and its manifest template, published to GitHub Pages on each release. -
tools/: bench tools, including the fixture matrix generator. -
spikes/: the four measured questions that settled the design, kept as the record of how they were measured.
You need Python 3.13 with ESPHome in a virtual environment, Node 20 or later, and clang for the host tests.
python3 -m venv .venv && . .venv/bin/activate && pip install esphome
cd web/importer && npm install && cd ../..
node web/box/build.mjs
esphome compile amble-home.yaml # ESP32-S3 release image
esphome compile amble-home-c6.yaml # ESP32-C6 release image
The first compile downloads ESP-IDF and its toolchains and takes a while; later ones are a minute or two.
Tests:
bash tests/host/run.sh # the component's pure logic, under clang, no ESPHome
node --test web/box/box.test.mjs # the box pages
cd web/importer && node --test # the importer and the account door's page side
node tools/door-check.mjs # the Edit page's three doors in headless Chrome, no box, no amaran
node scripts/check-ai-tooling.mjs # the agent instructions and skills are consistent
Development happens on a dedicated board against a spare fixture, never against lights you use. amble-home.dev.yaml is the bench configuration: it takes Wi-Fi and the spare's mesh address from a gitignored secrets.yaml (secrets.yaml.example shows the shape), logs over UART instead of the native USB port, and sets the guard to the dev allowlist so the board can address that one fixture and nothing else. docs/bench.md has the procedure, including the USB trap on macOS that makes a freshly reset board look dead.
Two things are never done on a bench: sending a group or broadcast mesh command (it reaches every light on the mesh, including ones in use), and flashing a production box with anything but a release image that the bench board has already run.
The repository is owned and directed by Chris Williams. The software is written and maintained by Claude (Anthropic) working as the project's engineer, with Codex (OpenAI) reviewing the slices that touch keys, the mesh, the import page or the release pipeline. Chris reads the design documents, the decisions and the issues, decides scope and owns the merge button; he does not read the code. Every artifact that asks him for a decision is written to be understood without opening a source file.
The working method, in short:
- Every change rides on a GitHub issue, which records what changed, how it was verified and what was reviewed.
- Hardware evidence or it did not happen. A change to the mesh layer is verified on a bench board with a fixture responding, and the evidence (a log excerpt, a Home Assistant state read-back) goes on the issue. A passing build is not evidence.
- Code-bearing changes get a read-only review of a committed range before they are pushed; the sensitive ones get a cross-model review.
- No review verdict authorizes a push, a release or a production flash. Those are Chris's.
Issues and pull requests are welcome.
- A bug report follows the Troubleshooting page's list, and never includes keys, a database, fixture hardware addresses or a password.
- A tested fixture or board is the easiest contribution: an issue naming the model (or board), what you drove, and what read back. It joins the tables.
-
A pull request gets the same review as any other change. One that touches the mesh layer needs hardware evidence on the PR, saying which board and which fixture, before it merges. Keep to the standing rules in
AGENTS.md: keys never enter the repository, the amaran app stays the source of truth for the mesh, and ESPHome owns everything ESPHome already does. If a change looks like it needs a Wi-Fi reconnect loop, a watchdog or an MQTT client, the answer is an ESPHome component, not new code. - Labels: one area (
mesh,esphome,importer,flasher,ha,release,hardware,agent-ops) and the kinds that apply (bug,idea,documentation,upstream,decision,spike,blocked,operator-action).
Each is an issue; none is promised.
- The take-over door: pairing lights into the box's own mesh for people without an account, a database, or a desire to have either.
- Identify a fixture before Save, with keys that are not yet stored.
- Failure injection on the bench, so the recovery paths can be watched reacting.
- A "Visit" link from the box's Home Assistant device page. Home Assistant shows that link only for a device running ESPHome's
web_server, which this box deliberately does not (its controls would let the box drive lights outside the guard); the ask to ESPHome is for a board serving its own pages to report its port. - An upstream ask to ESPHome for a public API to register a component at runtime.
MIT, like the projects it descends from (Lineage).
Using amble-home
Working on it