Direct control of Amaran lights over Bluetooth. No desktop app required.
by wesbosTypeScript
Last 12 weeks · 2 commits
2 of 6 standards met
runs on every , not just the first one, so each Wi-Fi reconnect starts MQTT and HTTP a second time. Why it happens resets the address to 0.0.0.0 when Wi-Fi disconnects ( → ), so the DHCP callback posts again after every reconnect, even if the lease hands back the same IP. then creates a second esp-mqtt client with the same default client ID ( + the last three MAC bytes, from ). The first client keeps running without a handle, a second publish timer is created, and the light state is reset to its defaults. Mosquitto gives a session to the newest client with that ID ("session taken over"). The older client reconnects after its reconnect timeout, so the two keep taking the session from each other. A second only fails, because port 80 is still bound. Fix Start each service once. esp-mqtt reconnects by itself, and the HTTP server's listening socket survives the reconnect. If a start fails, it is retried at the next . Testing Builds with ESP-IDF v5.3.2 on top of the current , with no new warnings. The same change has been running on my bridge since 2026-09-13, as part of a fork with more changes (abrockmann/amaran-BLE-control-streamdeck). After broker restarts, MQTT reconnected by itself, exactly once each time. I found this by reading the code and the ESP-IDF sources. My bridge hasn't had a real Wi-Fi reconnect since the fix went in, so I haven't reproduced the double client on hardware. Thanks for the project. The bridge has been a great base. 🤖 Generated with Claude Code
Repro: power-cycle a light while the daemon is running. Before this change, POST /lights/all/on returns {"ok":true} and the light stays dark, with no way for the caller to know. doConnect() never listened for noble's disconnect event, so when a light dropped the link the controller kept writing into a dead handle. Because writes use writeWithoutResponse there is no acknowledgement, so the HTTP layer returned {"ok":true} whether or not anything was delivered — a silent failure with no way for a caller to detect it. Adds a once("disconnect") handler that clears the link state, a public getter, on GET /, and a 503 on POST /lights when the link is down. Set AMARAN_EXIT_ON_DISCONNECT=1 to have the daemon exit when the link drops, so a supervisor (launchd KeepAlive, systemd Restart=) restarts it with a fresh connection. Without it the daemon reports connected: false and returns 503, but has no path back on its own.
Problem Mesh sends are fire-and-forget ( — no ack, no retry). When an on/off PDU is lost in the air, the fixture keeps its old state while the bridge has already optimistically told HA the new one. The post-command auto-refresh (~600 ms) then reads the fixture's true state — and adopts it as an "external change", flipping HA back. Field observation that motivated this: a scripted left the fixture physically on, twice in two days, with HA's state bouncing → ~0.95 s after the command (visible in HA's logbook as two state changes sharing one service-call context). The bridge detected the loss each time — the refresh is what corrected HA — it just reported the failure instead of fixing it. Fix Close the loop that already exists: When dispatching an on/off, record the commanded state and arm a 5 s verification window (). If a status reply contradicts a fresh command, re-send it (bounded, 2 retries) instead of adopting the stale state. Each re-send goes through the normal dispatch, whose fetches a fresh status ~600 ms later — so every retry is itself verified. Retries are dispatched from a one-shot (the same pattern as ), never from the BLE rx path. On confirmation, or once retries/window are exhausted, the pending flag clears and behavior is exactly as before: genuine external changes (phone app, physical knob) still flow to HA. Scope is deliberately on/off only — it's the binary, user-visible failure. A lost brightness/CCT PDU shows as a wrong-looking light, which the existing refresh at least reports honestly. Testing Builds clean (ESP-IDF 5.3, classic ESP32 target). Running in production on a 7-fixture rig: off→on→off cycles confirm the happy path (command → status confirms → clears, no spurious retries), and phone-app changes still propagate to HA. The retry path logs at warning level when it fires. 🤖 Generated with Claude Code https://claude.ai/code/session_014ENfJHGWP7gnG3V7KmbSP9
Hi! Thanks for publishing this project! I noticed that declares the license as , but the repository doesn’t include a standalone file. Would you be open to adding one? That would make the intended licensing clearer for GitHub users and downstream projects, and it would also allow GitHub to detect and display the license automatically. Assuming MIT is still the intended license, you could use this template: . Thanks again for making this available.
Repository: wesbos/amaran-BLE-control. Description: Direct control of Amaran lights over Bluetooth. No desktop app required. Stars: 39, Forks: 15. Primary language: TypeScript. Languages: TypeScript (57.4%), C (40.3%), JavaScript (1.6%), CMake (0.7%). License: MIT. Open PRs: 5, open issues: 0. Last activity: 2mo ago. Community health: 42%. Top contributors: wesbos, kevinschaich.