Skip to content

HA MQTT Discovery + CodeRabbit fixes + docs overhaul (ADRs, lessons split)#39

Merged
ewowi merged 3 commits into
mainfrom
ha-mqtt-discovery
Jul 7, 2026
Merged

HA MQTT Discovery + CodeRabbit fixes + docs overhaul (ADRs, lessons split)#39
ewowi merged 3 commits into
mainfrom
ha-mqtt-discovery

Conversation

@ewowi

@ewowi ewowi commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What & why

A Shelly-board user tried to control the device from Home Assistant's "Easy MQTT" addon and got nothing: HA's addon publishes to HA's own schema (homeassistant/light/<name>/set + {"state":"ON"} JSON), while the device subscribed only to the mqttthing schema (projectMM/<mac6>/on/set + "true"). He unblocked himself by hand-pointing HA at the real topics — proving this is an ergonomics gap, not a capability gap: the device was controllable from HA, just not auto-discoverable.

This branch closes that gap. On connect the device publishes a retained homeassistant/light/<id>/config so HA (and any Discovery-aware hub — the Tasmota/ESPHome/Zigbee2MQTT convention) auto-creates a wired light entity, no hand-matching topics. It then speaks HA's own JSON schema on ha/set / ha/state alongside the existing mqttthing topics (the user's working setup is untouched), and backs availability with an MQTT Last-Will so HA greys the entity out on an ungraceful drop.

Decisions (PO-locked): JSON schema (not default schema — chosen for extensibility: future presets/effects add a key via effect_list, not a new topic); availability via LWT (industry-standard, cheap); uniq_id = the MAC-stable projectMM_<mac6>, never the editable name. Discovery is opt-out (haDiscovery, default on).

Highlights

  • Heap-on-use — the 768 B discovery scratch allocates on first announce and frees in teardown() / on disable / on discovery-off (dynamicBytes reports it). A device that never enables discovery pays zero. This is recorded as the "Pay for what you use" rule in architecture.md § Memory strategy — it governs every module, not just this one.
  • Live, no reboot — toggling haDiscovery announces/retracts over the existing socket, no reconnect.
  • Own client — the Last-Will support is added to the dependency-free MqttPacket.h and pinned by a byte-exact golden vector, same as the rest of the framing.

Bug found + fixed during live hardware testing

Testing against a real Mosquitto broker on three boards over two transports (MM-S31 eth, MM-SE16 WiFi, MM-P4 eth) surfaced a heap-strand the unit tests missed: turning haDiscovery off while the socket was mid-reconnect returned before freeing the buffers, leaking 768 B until teardown. Fixed (retract now frees unconditionally; only the entity-clearing PUBLISH needs a live link) and pinned with a regression test; re-verified on the P4 that dynamicBytes drops to 0 on a disconnected discovery-off.

Verified live end-to-end (against a real broker)

MM-S31 (eth) MM-SE16 (WiFi) MM-P4 (eth)
Boots clean, on LAN
dynamicBytes = 0 idle / 768 connected
Retained discovery config (JSON matches golden vector)
…/status = online (LWT availability)
ha/set drives on/brightness (0-255, no rescale)
ha/state reflects changes back
Toggle: dyn 768↔0 + config retract/re-announce

Tests

  • unit_MqttPacket — byte-exact golden vector for CONNECT with a retained Last-Will.
  • unit_MqttModuleha/set command tests (state, brightness no-rescale, key-order-independent, state-only-preserves-brightness), a CONNACK-publishes-retained-discovery-config test via the outbound-capture seam, and the disconnected-retract regression test.
  • scenario_MqttModule_haDiscovery_toggle — live-only toggle scenario (FPS + heap stay in range; SKIPs on desktop, runs on a board with a broker).

Notes

  • Commit gates all pass (spec / build / ctest 638 / scenarios 16 / platform boundary / ESP32 builds: esp32, S3-n8r8, S3-n16r8, P4-eth, P4-eth-wifi, S31 / KPI).
  • The committed ESP32 KPI line (1369us/730) is the stale monitor.log baseline; live captures on the HA-test boards were S31 2641us/378 FPS and P4 7033us/141 FPS — both healthy through the full round-trip.
  • HA end-to-end (entity appears in HA's own UI) will be verified by the PO on hardware — everything the device emits is confirmed correct on the broker; the HA-side auto-create is the remaining human check.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Home Assistant MQTT discovery for lights, including automatic entity setup, retained availability, and state updates.
    • Added support for Home Assistant-style set commands with brightness control.
  • Bug Fixes

    • Improved device availability reporting so disconnected devices can clear their published presence cleanly.
    • Refined MQTT connection handling to support retained Last Will messages.
  • Documentation

    • Updated home automation, MQTT settings, architecture, and backlog docs to reflect the new discovery flow and memory behavior.

Follow-up commit: CodeRabbit findings + docs overhaul

A second commit lands on this branch after the initial feature.

CodeRabbit findings processed (7, all hardware-verified)

  • 🐇 deviceName not escaped in discovery JSON (Major) — now escaped via the shared jsonEscape; verified a quoted name keeps the retained config valid.
  • 🐇 discovery-off retract didn't reach the broker when disconnected (Major) — retract now frames into a local buffer and frees unconditionally, and CONNACK retracts when discovery is off; verified live the retained config clears and HA removes the entity.
  • 🐇 failed HA sends didn't reset the connection (Major) — subscribe / discovery-publish / availability-publish now resetConnection on a failed send, matching the ping/state paths.
  • 🐇 unknown HA state mapped to OFF (Minor) — ha/set acts only on exact "ON"/"OFF"; a malformed value is ignored.
  • 🐇 two forward-looking comments (Minor) — rephrased present-tense.
  • 🐇 retain bit not actually verified in the test (Minor) — the CONNACK test now parses the packet stream and asserts the RETAIN bit on the config + availability publishes.
  • 🐇 magic 768 in the test (Trivial) — exposed as kDiscoveryDynamicBytes with a static_assert tied to the real buffer sizes.

Docs-infrastructure overhaul

Reorganised the accumulated decision/lesson knowledge into the industry-standard shape:

  • docs/adr/ — 11 Nygard-format ADRs (immutable, superseded-not-edited) capturing the architectural decisions (persistence, adaptive memory, buffer persistence, composable modifiers, set-control primitive, device discovery, MoonLive, board injection, generated docs, integration identity, pull-not-pub/sub) + an index.
  • decisions.mdlessons.md — split by genre: debugging gotchas stay (pruned as absorbed, 861 → ~240 lines), durable rules extracted to coding-standards.md (new Defaults + Debugging-and-verification sections) and architecture.md, decisions promoted to ADRs.
  • architecture.md — decision-rationale moved to ADRs and cross-linked; verbose what-prose tightened (12406 → 11430 words); "What we leave undesigned" moved to the backlog; all backlog/history references removed so the doc is present-tense only.
  • Doc bug fix — 34 broken catalog card-image paths (../../../assets../../assets) that 404'd on the live site while passing --strict; closed with a test/python guard that resolves every catalog <img src> on disk.
  • CLAUDE.md / history/README.md — doc-map updated for docs/adr/ and lessons.md; the carry-forward gate routes decisions → ADRs, gotchas → lessons.md, rules → CLAUDE.md/coding-standards.md. A plans/README.md added for consistency.

Verification

All commit gates pass on both commits (spec, desktop build zero-warnings, ctest 638, scenarios, platform boundary, ESP32 esp32 build, host Python 52). The docs restructure is validated by a --strict MkDocs build; the MQTT fixes are hardware-verified end-to-end against a live Mosquitto broker on three boards (S31 eth, SE16 WiFi, P4 eth).

The device now announces itself to Home Assistant over MQTT: on connect it publishes a retained homeassistant/light/<id>/config so HA (or any Discovery-aware hub) auto-creates a wired light entity, no hand-matching topics. It speaks HA's own JSON schema on ha/set + ha/state alongside the existing mqttthing topics, and backs availability with an MQTT Last-Will so HA greys the entity out when the device drops. Discovery is opt-out (haDiscovery, default on) and its buffers are heap-on-use, so a device that never enables it pays zero bytes.

KPI: 16384lights | PC:666KB | tick:120/91/119/15/2/274/58/17/21/160/115/17/1/34us(FPS:8333/10989/8403/66666/500000/3649/17241/58823/47619/6250/8695/58823/1000000/29411) | tick:1369us(FPS:730) | heap:155KB | src:176(34978) | test:122(18006) | lizard:126w

Note: the ESP32 KPI line (1369us/730) is the stale monitor.log baseline. Live captures on the actual HA-test boards this session: MM-S31 (eth) 2641us/378 FPS, MM-P4 (eth) 7033us/141 FPS — both healthy across the full discovery toggle + command round-trip.

Core:
- MqttPacket.h: extended buildMqttConnect with optional Last-Will (topic/payload/retain) via the connect-flag bits, same mechanism as username/password; a partial will (topic without message) is dropped per §3.1.3. Pinned by a byte-exact golden vector.
- MqttModule: added HA MQTT Discovery — retained JSON-schema light config on homeassistant/light/projectMM_<mac6>/config (uniq_id is the MAC-stable id, never the editable name), the ha/set inbound command topic (reuses mm::json flat helpers, HA brightness 0-255 with no rescale), the retained ha/state feedback publish (change-gated), and a retained online/offline availability topic backed by the CONNECT Last-Will. Toggling haDiscovery announces/retracts live over the existing socket, no reconnect. Discovery scratch buffers (768 B) allocate on first announce and free in teardown() / on disable / on discovery-off, reported via dynamicBytes — the pay-for-what-you-use rule.
- MqttModule: fixed a heap-strand found live on P4/S31 hardware — turning haDiscovery OFF while the socket was mid-reconnect returned before freeing the buffers, leaking 768 B until teardown. The retract path now frees unconditionally (freeing local memory needs no socket); only the entity-clearing empty PUBLISH needs a live link.

Docs / CI:
- architecture.md: recorded the "Pay for what you use" memory rule under § Memory strategy — a module holds heap only for capabilities it is actually exercising (absent → 0; present-but-off → class instance only; active → +dynamicBytes, freed in teardown). Governs every module; the LED driver and MQTT discovery are the worked examples.
- services.md: documented the haDiscovery control on the MQTT card.
- home-automation.md: rewrote the HA path — MQTT Discovery (auto-wired, recommended) and the WLED integration (no broker); note not to hand-configure HA's generic MQTT against the mqttthing topics.
- backlog-core.md: marked HA MQTT Discovery shipped; kept the generic-topics escape hatch as unbuilt.
- Plan-20260706 - Home Assistant MQTT Discovery.md: saved the approved plan.

Tests:
- unit_MqttPacket: added a byte-exact golden vector for CONNECT with a retained Last-Will (will-flag bits + will topic/payload tail).
- unit_MqttModule: added ha/set command tests ({state}, {brightness} no-rescale, key-order-independent, state-only-preserves-brightness), a CONNACK-publishes-retained-discovery-config test via the outbound-capture seam, and a regression test pinning the disconnected-retract fix (connect → allocate → force reset → discovery-off while disconnected → dynamicBytes drops to 0).
- scenario_MqttModule_haDiscovery_toggle: added a live-only scenario toggling haDiscovery on/off/on, asserting FPS + heap stay in range (SKIPs on desktop, runs on a board with a broker).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f0eed3ba-e943-4bde-b475-436a3311d5fd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements Home Assistant MQTT Discovery in MqttModule, including Last-Will CONNECT support, retained discovery/availability publishing, ha/set command routing, ha/state publishing, buffer lifecycle management, and associated tests/scenario. Separately, it introduces an ADR directory, consolidates debugging history into lessons.md, rewrites architecture.md sections, updates coding standards, and fixes documentation image paths.

Changes

HA MQTT Discovery implementation

Layer / File(s) Summary
MQTT CONNECT Last-Will support
src/core/MqttPacket.h, test/unit/core/unit_MqttPacket.cpp
Adds Will flag constants and extends buildMqttConnect with optional will topic/payload/retain, validated by new byte-exact unit tests.
HA discovery buffer lifecycle and topic helpers
src/core/MqttModule.h, src/core/MqttModule.cpp
Declares/implements lazy heap allocation and freeing of discovery buffers, HA topic builders, teardown() override, and the haDiscovery_ flag.
HA discovery publish/retract and connection wiring
src/core/MqttModule.cpp
Implements announce/retract discovery flow, haDiscovery control toggling, CONNACK-triggered availability/discovery, and a send-capture test seam.
ha/set command routing and ha/state publishing
src/core/MqttModule.cpp, test/unit/core/unit_MqttModule.cpp
Parses ha/set JSON into control updates, publishes retained ha/state, and adds unit tests for command parsing and discovery retraction.
Toggle scenario and feature documentation
test/scenarios/core/scenario_MqttModule_haDiscovery_toggle.json, docs/history/plans/Plan-20260706 - Home Assistant MQTT Discovery.md, docs/backlog/backlog-core.md, docs/moonmodules/core/services.md, docs/usecases/home-automation.md
Adds a live discovery-toggle scenario and documents the shipped feature across the implementation plan, backlog, services, and home-automation guide.

Estimated code review effort: 4 (Complex) | ~60 minutes

Documentation platform reorganization

Layer / File(s) Summary
ADR directory introduction
docs/adr/0001-...md ... docs/adr/0011-...md, docs/adr/README.md
Adds eleven new Architecture Decision Records and an index README.
lessons.md creation and decisions.md migration
docs/history/lessons.md, CLAUDE.md, docs/history/README.md, docs/backlog/backlog-light.md, docs/backlog/livescripts-analysis-top-down.md, docs/usecases/led-signal-integrity.md, moondeck/diag/preview_health.py, src/light/drivers/*, src/platform/esp32/*, src/core/DevicePlugin.h, src/core/DevicesModule.h, docs/history/plans/README.md
Creates the consolidated debugging lessons log and updates references throughout the codebase from decisions.md to lessons.md or specific ADR paths.
architecture.md content rewrites
docs/architecture.md
Rewrites persistence, data exchange, provenance, identity, backpressure, layouts, buffer persistence, dimensionality, MoonLive, drivers, and adaptive allocation sections, tying several to ADRs.
Backlog documentation updates
docs/backlog/backlog-core.md, docs/backlog/backlog-light.md
Adds PSRAM mitigation option, WiFi runtime disable open question, Improv-as-child blocker note, and light-type mixing open question.
Coding standards updates
docs/coding-standards.md
Adds integer-width, JSON symmetry, hot-path syscall audit, and hardware-default/debugging discipline rules.
Documentation image path corrections and verification test
docs/moonmodules/core/services.md, docs/moonmodules/light/*.md, test/python/test_mkdocs_slug.py
Corrects relative image paths across module/driver/effect/layout/modifier/supporting docs and adds a test verifying catalog image paths resolve on disk.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Broker
  participant MqttModule
  Broker->>MqttModule: CONNACK
  MqttModule->>Broker: publish retained "online" (status, LWT topic)
  MqttModule->>MqttModule: check haDiscovery_
  MqttModule->>Broker: publish retained HA discovery config JSON
  MqttModule->>Broker: subscribe ha/set
  MqttModule->>Broker: publish initial ha/state
  Broker->>MqttModule: publish ha/set {"state":"ON","brightness":128}
  MqttModule->>MqttModule: parse JSON, update Drivers controls
  MqttModule->>Broker: publish retained ha/state
Loading

Related PRs: None specified.

Suggested labels: documentation, enhancement, mqtt

Suggested reviewers: None specified.

Poem:
A rabbit hops through docs anew,
Lessons stacked where decisions once grew,
MQTT now whispers "online" to the sky,
Home Assistant lights blink and comply,
Through ADRs and code, the burrow shines through. 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change set by highlighting Home Assistant MQTT Discovery and the accompanying documentation updates.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ha-mqtt-discovery

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/MqttModule.cpp (1)

193-198: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Publish retained offline before clean DISCONNECT.

Line 196 sends MQTT DISCONNECT, so the broker suppresses the Last-Will. Because Line 400 publishes retained online, disabling MQTT can leave HA showing the device as available.

Suggested fix
     if (!enabled && conn_.valid()) {
-        uint8_t buf[4];
-        const size_t n = buildMqttDisconnect(buf, sizeof(buf));
-        if (n) sendPacket(buf, n);   // best-effort courtesy DISCONNECT
+        bool sendDisconnect = true;
+        if (state_ == Conn::Connected) {
+            char statusTopic[96];
+            uint8_t statusBuf[kSendBufLen];
+            buildStatusTopic(statusTopic, sizeof(statusTopic));
+            const size_t sn = buildMqttPublish(statusTopic, reinterpret_cast<const uint8_t*>("offline"), 7,
+                                               statusBuf, sizeof(statusBuf), /*retain=*/true);
+            sendDisconnect = sn != 0 && sendPacket(statusBuf, sn);
+        }
+        if (sendDisconnect) {
+            uint8_t buf[4];
+            const size_t n = buildMqttDisconnect(buf, sizeof(buf));
+            if (n) sendPacket(buf, n);   // best-effort courtesy DISCONNECT
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/MqttModule.cpp` around lines 193 - 198, When MQTT is being disabled
in MqttModule::disconnect or the cleanup path that sends the courtesy
DISCONNECT, publish a retained offline availability state before sending the
DISCONNECT packet so the broker does not suppress the Last-Will and Home
Assistant no longer stays on retained online. Update the MQTT shutdown flow
around the sendPacket/buildMqttDisconnect logic to explicitly clear the retained
availability for the same device/topic used by the retained online publish, then
proceed with DISCONNECT and freeDiscoveryBuffers as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/MqttModule.cpp`:
- Line 445: Remove the forward-looking effect roadmap note from the runtime
source in MqttModule and keep that guidance in a backlog/decision document
instead; update the surrounding comment near the current HA command handling to
describe only the implemented command surface, using the relevant
setControlValue and effect/palette context as the anchor for where the note
currently lives.
- Around line 145-146: The MQTT subscribe/discovery/availability send path in
MqttModule::sendPacket usage currently ignores failure, which can leave a
truncated frame on the stream and keep the device running in a wedged state.
Update the affected API-call sequences around buildMqttSubscribe and the other
sendPacket call sites (including the HA subscribe/discovery/availability paths)
to check the sendPacket return value, and on failure immediately trigger the
module’s reset/reconnect handling instead of continuing to send further packets.
- Around line 123-127: Escape the device name before building the Home Assistant
discovery payload in MqttModule::publishDiscovery (the snprintf block that
writes discoveryPayload_). The raw dn value is inserted into JSON string fields,
so use the project’s JSON string escaping/writer for both "name" entries before
formatting the payload, while keeping the other identifiers (id, cmd, stat,
avty) unchanged.
- Around line 88-97: The discovery-disable path in MqttModule is only retracting
retained Home Assistant config when Conn::Connected and discoveryBuf_ are both
present, so turning haDiscovery off while disconnected or during reconnect can
leave stale retained topics behind. Update the retraction logic around the
discovery-off handling and the reconnect path that checks haDiscovery_ so it
still publishes the empty/tombstone discovery message even when not currently
connected or when discoveryBuf_ has already been freed, using
buildDiscoveryTopic, buildMqttPublish, and sendPacket consistently to ensure the
broker receives the cleanup.
- Around line 431-435: The MQTT Home Assistant state handling in
MqttModule::setControlValue mapping currently treats every parsed value other
than exact "ON" as false, which incorrectly converts unknown or malformed states
to OFF. Update the state parsing branch to explicitly accept only valid HA
values (for example, ON/OFF or equivalent supported states) and ignore anything
else by returning early without calling setControlValue. Keep the fix localized
to the json::hasKey/body "state" handling in MqttModule.cpp so truncated or
invalid strings no longer flip the control to false.

In `@src/core/MqttModule.h`:
- Around line 46-47: Rephrase the documentation in MqttModule.h to describe only
the current schema behavior instead of future controls or “future add” wording.
Update the nearby comment around the present schema note so it stays in
present-state terms, while still clarifying the distinction between the default
schema and HA’s native effect/effect_list mapping. Keep the change limited to
the header comment and preserve the intent of the existing schema guidance.

In `@test/unit/core/unit_MqttModule.cpp`:
- Around line 196-203: The test in unit_MqttModule.cpp is hard-coding the
dynamicBytes expectation with a magic number, which makes the assertion brittle
and unclear. Update the checks around the MqttModule discovery/broker flow to
reference the underlying buffer-size source used by dynamicBytes(), such as the
discovery buffer constants or a test-accessible accessor on MqttModule, so the
intent is self-documenting. Keep the assertion behavior the same, but replace
the raw 768 value with a named value tied to the MqttModule buffer sizing.
- Around line 163-182: The current MqttModule discovery test only searches the
captured payload as text, so it never actually verifies the MQTT retain flag.
Update the TEST_CASE for CONNACK discovery publish to use a packet-level helper
(or reuse the existing Last-Will packet parser) to locate the PUBLISH packet for
the Home Assistant discovery topic and assert that its fixed-header retain bit
is set, while keeping the existing string checks for topic and config fields.

---

Outside diff comments:
In `@src/core/MqttModule.cpp`:
- Around line 193-198: When MQTT is being disabled in MqttModule::disconnect or
the cleanup path that sends the courtesy DISCONNECT, publish a retained offline
availability state before sending the DISCONNECT packet so the broker does not
suppress the Last-Will and Home Assistant no longer stays on retained online.
Update the MQTT shutdown flow around the sendPacket/buildMqttDisconnect logic to
explicitly clear the retained availability for the same device/topic used by the
retained online publish, then proceed with DISCONNECT and freeDiscoveryBuffers
as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 23133d2f-9f4f-48cc-a809-3a8ea615e673

📥 Commits

Reviewing files that changed from the base of the PR and between bcb085b and 2e57161.

📒 Files selected for processing (11)
  • docs/architecture.md
  • docs/backlog/backlog-core.md
  • docs/history/plans/Plan-20260706 - Home Assistant MQTT Discovery.md
  • docs/moonmodules/core/services.md
  • docs/usecases/home-automation.md
  • src/core/MqttModule.cpp
  • src/core/MqttModule.h
  • src/core/MqttPacket.h
  • test/scenarios/core/scenario_MqttModule_haDiscovery_toggle.json
  • test/unit/core/unit_MqttModule.cpp
  • test/unit/core/unit_MqttPacket.cpp

Comment thread src/core/MqttModule.cpp
Comment thread src/core/MqttModule.cpp Outdated
Comment thread src/core/MqttModule.cpp Outdated
Comment thread src/core/MqttModule.cpp Outdated
Comment thread src/core/MqttModule.cpp Outdated
Comment thread src/core/MqttModule.h Outdated
Comment thread test/unit/core/unit_MqttModule.cpp
Comment thread test/unit/core/unit_MqttModule.cpp Outdated
…, architecture trim)

Two areas land together. First, the eight CodeRabbit findings on the HA MQTT Discovery PR are processed and hardware-verified: the discovery config now escapes the editable device name, a discovery-off retract reaches the broker even mid-reconnect, failed HA sends reset the connection like the sibling paths, and unknown HA state values are ignored rather than mapped to OFF. Second, the accumulated decision/lesson knowledge is reorganised into the industry-standard shape: architectural decisions become immutable ADRs, the old decisions.md is split into a pruned lessons log, and architecture.md is trimmed of decision-rationale (now in ADRs) and forward/backward-looking references (it is present-tense only).

KPI: 16384lights | PC:666KB | tick:117/90/118/15/2/272/57/17/20/159/116/16/1/35us(FPS:8547/11111/8474/66666/500000/3676/17543/58823/50000/6289/8620/62500/1000000/28571) | tick:1392us(FPS:718) | heap:155KB | src:176(35000) | test:122(18043) | lizard:126w

Note: the ESP32 KPI line (1392us/718) is the stale monitor.log baseline. Live captures on the HA-test boards this session: MM-S31 (eth) ~2641us/378 FPS, MM-P4 (eth) ~7033us/141 FPS, both healthy through the full discovery + command round-trip.

Core:
- MqttModule: escape the user-editable deviceName with the shared jsonEscape before it enters the discovery JSON, so a quote or backslash in the name can't produce an invalid retained config.
- MqttModule: the discovery-off retract now sends the empty retained payload from a local buffer and frees unconditionally, and CONNACK retracts when discovery is off, so turning discovery off (even while disconnected or reconnecting) clears the broker's retained config and HA removes the entity.
- MqttModule: failed HA subscribe / discovery-publish / availability-publish sends now resetConnection, matching the ping/state paths, so a partial write can't leave a truncated frame on the stream.
- MqttModule: ha/set state acts only on exact "ON"/"OFF"; a malformed or truncated value is ignored, not silently treated as OFF.
- Two forward-looking comments rephrased to present tense; the discovery buffer size is exposed as kDiscoveryDynamicBytes (static_assert-tied to the real sizes) so the test asserts a named constant.
- DevicePlugin / DevicesModule / the LED drivers / platform files: retarget decisions.md references to the new lessons.md or the relevant ADR.

Tests:
- unit_MqttModule: the CONNACK discovery test now parses the packet stream and asserts the RETAIN bit on the config and availability publishes (a dropped retain fails it); a regression test pins the disconnected-retract fix (dynamicBytes drops to 0); ha/set-unknown-state coverage added.
- test_mkdocs_slug: a guard that every catalog-page <img src> resolves on disk, closing the --strict blind spot that let broken card-image paths ship.

Docs / CI:
- docs/adr/: eleven Nygard-format ADRs (immutable, superseded-not-edited) capturing the architectural decisions (persistence, adaptive memory, buffer persistence, composable modifiers, set-control primitive, device discovery, MoonLive, board injection, generated docs, integration identity, pull-not-pub/sub) + an index README.
- decisions.md split: renamed to lessons.md (debugging gotchas, pruned as absorbed); the durable rules extracted to coding-standards.md (numeric-width, defaults, a new Debugging-and-verification section) and architecture.md; the decisions promoted to ADRs. 861 -> 236 lines.
- architecture.md: decision-rationale moved to ADRs and cross-linked; verbose what-prose tightened (graceful degradation, config provenance, live reconfiguration); "What we leave undesigned" moved to the backlog; all backlog/history references removed so the doc is present-tense only. 12406 -> 11430 words.
- Catalog pages (services/effects/modifiers/layouts/drivers/supporting): fixed 34 broken card-image paths (../../../assets -> ../../assets); the wrong depth 404'd on the live site while passing --strict.
- CLAUDE.md + history/README.md: doc-map updated for docs/adr/ and lessons.md; the carry-forward gate routes decisions to ADRs, gotchas to lessons.md, rules to CLAUDE.md/coding-standards.md. A plans/README.md added for consistency.

Reviews:
- 🐇 deviceName not escaped in discovery JSON (Major): fixed via jsonEscape; hardware-verified a quoted name keeps the config valid.
- 🐇 discovery-off retract didn't reach the broker when disconnected (Major): fixed (local buffer + CONNACK retract-when-off); hardware-verified the retained config clears.
- 🐇 failed HA sends didn't reset the connection (Major): fixed to match the ping/state paths.
- 🐇 unknown HA state mapped to OFF (Minor): fixed to act only on exact ON/OFF.
- 🐇 two forward-looking comments (Minor): rephrased present-tense.
- 🐇 retain bit not actually verified in the test (Minor): the test now parses the packet and asserts the bit.
- 🐇 magic 768 in the test (Trivial): exposed as kDiscoveryDynamicBytes with a static_assert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ewowi ewowi changed the title Add Home Assistant MQTT Discovery (JSON schema) with LWT availability HA MQTT Discovery + CodeRabbit fixes + docs overhaul (ADRs, lessons split) Jul 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/adr/0001-persistence-pod-memcpy-not-json.md`:
- Around line 1-18: Rewrite the ADR text in present tense throughout, keeping
the same decision and technical meaning but removing changelog-style phrasing
such as “attempted,” “was fully abandoned,” and “took this path and shipped.”
Update the prose in the Context, Decision, and Consequences sections of
0001-persistence-pod-memcpy-not-json.md so it reads as current guidance, and
keep the wording aligned with the document’s policy language for markdown docs
outside docs/backlog/ and docs/history/.

In `@docs/adr/0003-layer-buffer-persists-frame-to-frame.md`:
- Around line 1-17: The ADR in this document uses retrospective wording
throughout the Context and Decision sections, so rewrite the affected prose in
present tense while keeping the same technical meaning. Update the narrative
around Layer::loop(), fadeToBlackBy, and the persistence behavior so it reads as
current guidance rather than history, and avoid phrases like “cleared,” “broke,”
or “surfaced” in past-tense form.

In `@docs/adr/0004-composable-modifiers-physical-to-logical.md`:
- Around line 1-17: The ADR text uses retrospective phrasing in the Context
section, so rewrite it in present tense as current architecture guidance while
keeping the Decision and Consequences intact. Update the wording in the ADR
itself to avoid “needed,” “did not compose,” and “black-screened,” and make sure
the description of the modifier chain, the physical→logical fold, and the CSR
build are stated as the repo’s present behavior. Focus the edit on the document
content only, not on changing the architectural meaning.

In `@docs/usecases/led-signal-integrity.md`:
- Line 5: The sentence in the markdown content uses past tense (“were”) outside
the allowed history/backlog sections, so rewrite the `docs/usecases` text in
present tense. Update the wording around the `lessons.md` reference to keep the
meaning but make it read as current guidance, and ensure no past-vs-present
contrast remains in this `usecases` document.

In `@src/light/drivers/ParlioLedDriver.h`:
- Line 28: The bench note in ParlioLedDriver.h is written in past tense and
should be changed to present tense to match the repo comment style. Update the
comment near the P4-NANO pin mapping so it reads like a current configuration
note, using the existing ParlioLedDriver context and pin list, and avoid
retrospective phrasing such as “used”.

In `@src/platform/esp32/platform_esp32_lcd.cpp`:
- Line 19: The LCD_CAM comment uses retrospective phrasing (“hung its boot”)
instead of present tense. Update the note in the esp32 LCD context to describe
the behavior in present tense, keeping the same meaning while avoiding
past-tense or changelog-style wording; make the change directly in the nearby
comment that references platform_config.h and lessons.md.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e942919e-c280-41b6-a8be-97d65c835176

📥 Commits

Reviewing files that changed from the base of the PR and between 2e57161 and b360706.

📒 Files selected for processing (43)
  • CLAUDE.md
  • docs/adr/0001-persistence-pod-memcpy-not-json.md
  • docs/adr/0002-adaptive-memory-degradation-cascade.md
  • docs/adr/0003-layer-buffer-persists-frame-to-frame.md
  • docs/adr/0004-composable-modifiers-physical-to-logical.md
  • docs/adr/0005-set-control-primitive-on-scheduler.md
  • docs/adr/0006-device-discovery-udp-mdns-advertise-only.md
  • docs/adr/0007-moonlive-expressions-host-bound-functions.md
  • docs/adr/0008-board-injection-name-only-http-fanout.md
  • docs/adr/0009-docs-generated-technical-plus-summary.md
  • docs/adr/0010-integration-identity-stable-hardware-id.md
  • docs/adr/0011-data-exchange-pull-and-prepare-pass-not-pubsub.md
  • docs/adr/README.md
  • docs/architecture.md
  • docs/backlog/backlog-core.md
  • docs/backlog/backlog-light.md
  • docs/backlog/livescripts-analysis-top-down.md
  • docs/coding-standards.md
  • docs/history/README.md
  • docs/history/decisions.md
  • docs/history/lessons.md
  • docs/history/plans/Plan-20260706 - Home Assistant MQTT Discovery.md
  • docs/history/plans/README.md
  • docs/moonmodules/core/services.md
  • docs/moonmodules/light/drivers.md
  • docs/moonmodules/light/effects.md
  • docs/moonmodules/light/layouts.md
  • docs/moonmodules/light/modifiers.md
  • docs/moonmodules/light/supporting.md
  • docs/usecases/led-signal-integrity.md
  • moondeck/diag/preview_health.py
  • src/core/DevicePlugin.h
  • src/core/DevicesModule.h
  • src/core/MqttModule.cpp
  • src/core/MqttModule.h
  • src/light/drivers/HueDriver.h
  • src/light/drivers/LcdLedDriver.h
  • src/light/drivers/ParlioLedDriver.h
  • src/light/drivers/RmtLedDriver.h
  • src/platform/esp32/platform_esp32.cpp
  • src/platform/esp32/platform_esp32_lcd.cpp
  • test/python/test_mkdocs_slug.py
  • test/unit/core/unit_MqttModule.cpp

Comment on lines +1 to +18
# 1. Persist POD module state with memcpy, not JSON

Status: Accepted

## Context

Plan-09 attempted ~1700 LOC of JSON-based persistence for module state. It was fully abandoned. The premise "persistence is JSON" was assumed without justification: neither human-readability nor manual editability were real requirements. The JSON design spawned ~15 helpers (`rebuildControls`, `LoadAllFn`, `applyNode`, `serializeNode`, `cleanupTmpLeafCb_`, and more), which was the system signalling the design was too elaborate for the job. It forced a Scheduler reorder (3→5 phases) that bred secondary bugs (a duplicate-children bug, a MAC→deviceName guard, multiple "device shows nothing" failures), and needed five defensive null guards that masked an allocate-new-before-free fragmentation invariant.

## Decision

Persist POD module state with a single `memcpy(file, this + sizeof(MoonModule), classSize - sizeof(MoonModule))`, loading it back before any module's `setup()` / `onBuildControls()` by memcpy into member memory directly. Plan-10 took this path and shipped.

## Consequences

- Save and restore are one line each; no serializer/deserializer helper sprawl.
- Loading before `setup()` means no Scheduler phase reorder and none of its secondary bugs.
- POD-only: state is a flat memory image, so there is no schema-versioned migration across a struct-layout change (a future need would be its own decision, not a reason to pay for JSON now).
- The lesson that generalised: question a format premise before building to it; suspicious helper proliferation is a design smell; fix an invariant, do not paper it with per-call-site guards.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rewrite this ADR in present tense.

The context/decision prose reads like a changelog ("attempted", "was fully abandoned", "took this path and shipped"), which violates the repo rule for markdown docs outside docs/backlog/ and docs/history/. Keep the same decision content, but phrase it as current guidance.

As per coding guidelines, **/*.h,hpp,cpp,cc,mm,m,md: write code, comments, and documentation in the present tense only; avoid changelog-style or absence-narration phrasing, except in docs/backlog/ and docs/history/.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr/0001-persistence-pod-memcpy-not-json.md` around lines 1 - 18,
Rewrite the ADR text in present tense throughout, keeping the same decision and
technical meaning but removing changelog-style phrasing such as “attempted,”
“was fully abandoned,” and “took this path and shipped.” Update the prose in the
Context, Decision, and Consequences sections of
0001-persistence-pod-memcpy-not-json.md so it reads as current guidance, and
keep the wording aligned with the document’s policy language for markdown docs
outside docs/backlog/ and docs/history/.

Source: Coding guidelines

Comment on lines +1 to +17
# 3. The layer buffer persists frame-to-frame

Status: Accepted

## Context

An early design cleared the render buffer before every effect frame, on the reasoning "the buffer is the effect's to fill, every time." This silently broke every persistence effect: a scroll reads the prior column and shifts it (reading a wiped buffer, only the fresh pixel survived); a trail calls `fadeToBlackBy` to decay the previous frame (fading zeroes never forms a trail); Game-of-Life reads its prior cell state (gone). The symptom that surfaced it: FreqMatrix lit only one row, and ~13 effects' `fade` controls did nothing.

## Decision

The render buffer is **not** cleared each frame. `Layer::loop()` leaves the previous frame's pixels in place, zeroed once on allocation/resize, then persistent, matching the FastLED / WLED / MoonLight convention (their `leds[]` / segment / VirtualLayer buffers all persist). Each effect owns its background inside its own `loop()`: a full-grid effect overwrites every pixel, a trail calls `fadeToBlackBy`, a sparse effect that wants a clean frame calls `draw::fill` itself. There is no per-effect "persist" flag (a flag would be bespoke). `fadeToBlackBy` is a Layer operation collected once per frame: effects register an amount, the Layer keeps the MIN across them and applies one pass at the next frame's start.

## Consequences

- Persistence effects (scroll, trail, Game-of-Life) work; multiple effects on one layer deliberately interact through the shared persistent buffer.
- N fading effects cost one buffer pass, not N, and never fade each other's fresh pixels.
- A state-advancing effect (Game-of-Life) separates when the simulation steps (gated on `bpm`) from when it paints (frame rate); its state lives in the persistent buffer. `unit_Layer_persistence` and `unit_GameOfLifeEffect` pin it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rewrite this ADR in present tense.

The context text uses retrospective phrasing ("cleared", "broke", "surfaced it"), which conflicts with the repo rule for markdown docs outside docs/backlog/ and docs/history/. Keep the same behavior description, but phrase it as current guidance.

As per coding guidelines, **/*.h,hpp,cpp,cc,mm,m,md: write code, comments, and documentation in the present tense only; avoid changelog-style or absence-narration phrasing, except in docs/backlog/ and docs/history/.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr/0003-layer-buffer-persists-frame-to-frame.md` around lines 1 - 17,
The ADR in this document uses retrospective wording throughout the Context and
Decision sections, so rewrite the affected prose in present tense while keeping
the same technical meaning. Update the narrative around Layer::loop(),
fadeToBlackBy, and the persistence behavior so it reads as current guidance
rather than history, and avoid phrases like “cleared,” “broke,” or “surfaced” in
past-tense form.

Source: Coding guidelines

Comment on lines +1 to +17
# 4. Build the map physical→logical so modifiers compose

Status: Accepted

## Context

Modifiers needed to chain (Region, then Multiply, then Rotate). The old interface `mapToPhysical(logicalCoord) → [physical indices]` (a logical→physical fan-out) did not compose: stages emitted flat indices, not coordinates, and chaining would need a product-of-`maxMultiplier` fan-out ceiling, the exact `uint16` overflow that black-screened the high-fan-out MultiplyModifier.

## Decision

Invert the map build to **physical→logical**, adopting MoonLight's proven model written in projectMM's own code. Each modifier is an in-place coordinate fold with three hooks: `modifyLogicalSize`, `modifyLogical` (returns false to reject a coordinate), and `modifyLive` (for per-frame transforms like Rotate). The Layer walks the *physical* lights and folds each through the chain. Because a scatter onto arbitrary logical keys does not fit `setMapping`'s in-order contract, the build is a textbook counting-sort CSR construction (count, prefix-sum, scatter, replay) entirely on the cold path.

## Consequences

- Fan-out becomes free: N physical lights folding onto one logical cell *is* the fan-out, with no fan-out list, no product ceiling, no overflow. `destinationCount ≤ driverCount` is now a hard invariant; the `maxMultiplier`/scratch-buffer machinery was deleted.
- The per-frame read is byte-identical; the hot path is untouched.
- Static folds (mask/tile/crop) happen at build time; only rotation gathers per frame, so a static-only chain pays nothing per frame.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rewrite this ADR in present tense.

The context text reads retrospectively ("needed", "did not compose", "black-screened"), which conflicts with the repo rule for markdown docs outside docs/backlog/ and docs/history/. Keep the decision, but state it as current architecture guidance.

As per coding guidelines, **/*.h,hpp,cpp,cc,mm,m,doc: write code, comments, and documentation in the present tense only; avoid changelog-style or absence-narration phrasing, except in docs/backlog/ and docs/history/.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/adr/0004-composable-modifiers-physical-to-logical.md` around lines 1 -
17, The ADR text uses retrospective phrasing in the Context section, so rewrite
it in present tense as current architecture guidance while keeping the Decision
and Consequences intact. Update the wording in the ADR itself to avoid “needed,”
“did not compose,” and “black-screened,” and make sure the description of the
modifier chain, the physical→logical fold, and the CSR build are stated as the
repo’s present behavior. Focus the edit on the document content only, not on
changing the architectural meaning.

Source: Coding guidelines

Comment thread docs/usecases/led-signal-integrity.md Outdated
Random wrong colours on LEDs the effect leaves black — most often a few stray pixels flickering — is, on a 3.3 V ESP32 driving WS2812 **directly**, almost always a **data-line signal-integrity** problem, not a firmware bug. WS2812 wants a logic-high near 0.7 × VDD (≈ 3.5 V on a 5 V strip), but the ESP32 drives only 3.3 V, so individual bits sit at the margin and noise tips them.

Confirm the firmware is innocent **before** reaching for the soldering iron. These checks were the actual diagnosis path on the bench (recorded in [decisions.md](../history/decisions.md)):
Confirm the firmware is innocent **before** reaching for the soldering iron. These checks were the actual diagnosis path on the bench (recorded in [lessons.md](../history/lessons.md)):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep this in present tense.

docs/usecases/** is not exempt from the present-tense rule, but were makes this read like a retrospective. Rephrase it in present tense.

As per coding guidelines, only docs/history/ and docs/backlog/ may use past-vs-present contrast.

♻️ Proposed fix
-Confirm the firmware is innocent **before** reaching for the soldering iron. These checks were the actual diagnosis path on the bench (recorded in [lessons.md](../history/lessons.md)):
+Confirm the firmware is innocent **before** reaching for the soldering iron. These checks are the bench diagnosis path recorded in [lessons.md](../history/lessons.md):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Confirm the firmware is innocent **before** reaching for the soldering iron. These checks were the actual diagnosis path on the bench (recorded in [lessons.md](../history/lessons.md)):
Confirm the firmware is innocent **before** reaching for the soldering iron. These checks are the bench diagnosis path recorded in [lessons.md](../history/lessons.md):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/usecases/led-signal-integrity.md` at line 5, The sentence in the
markdown content uses past tense (“were”) outside the allowed history/backlog
sections, so rewrite the `docs/usecases` text in present tense. Update the
wording around the `lessons.md` reference to keep the meaning but make it read
as current guidance, and ensure no past-vs-present contrast remains in this
`usecases` document.

Source: Coding guidelines

Comment thread src/light/drivers/ParlioLedDriver.h Outdated
// strand is user-soldered: a hard-coded pin would guess the user's wiring and
// could drive a pin committed elsewhere ("default only when it cannot do harm",
// see decisions.md). The P4-NANO bench used pins "20,21,22,23,24,25,26,27",
// see lessons.md). The P4-NANO bench used pins "20,21,22,23,24,25,26,27",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use present tense for the bench note.

"The P4-NANO bench used pins ..." reads as a retrospective note and violates the repo's present-tense rule for header comments. Rephrase it as a current configuration note instead.

As per coding guidelines, **/*.h,hpp,cpp,cc,mm,m,md: write code, comments, and documentation in the present tense only; avoid changelog-style or absence-narration phrasing, except in docs/backlog/ and docs/history/.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/drivers/ParlioLedDriver.h` at line 28, The bench note in
ParlioLedDriver.h is written in past tense and should be changed to present
tense to match the repo comment style. Update the comment near the P4-NANO pin
mapping so it reads like a current configuration note, using the existing
ParlioLedDriver context and pin list, and avoid retrospective phrasing such as
“used”.

Source: Coding guidelines

// SOC_LCDCAM_I80_LCD_SUPPORTED, NOT SOC_LCD_I80_SUPPORTED: the classic ESP32 sets
// the latter for its unrelated I2S-LCD peripheral, which wired this driver onto a
// chip with no LCD_CAM and hung its boot (see platform_config.h + decisions.md).
// chip with no LCD_CAM and hung its boot (see platform_config.h + lessons.md).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use present tense in the LCD_CAM note.

"…hung its boot" is retrospective phrasing and breaks the repo's present-tense rule for comments. Rewrite it as a current behavior note.

As per coding guidelines, **/*.h,hpp,cpp,cc,mm,m,md: write code, comments, and documentation in the present tense only; avoid changelog-style or absence-narration phrasing, except in docs/backlog/ and docs/history/.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/platform/esp32/platform_esp32_lcd.cpp` at line 19, The LCD_CAM comment
uses retrospective phrasing (“hung its boot”) instead of present tense. Update
the note in the esp32 LCD context to describe the behavior in present tense,
keeping the same meaning while avoiding past-tense or changelog-style wording;
make the change directly in the nearby comment that references platform_config.h
and lessons.md.

Source: Coding guidelines

…nDeck DX

Processes the second CodeRabbit round (present-tense in non-exempt files) and the pre-merge gates for PR #39: two hard-won lessons carried forward, and two MoonDeck developer-experience fixes surfaced while testing the branch.

Core:
- ParlioLedDriver / platform_esp32_lcd: reword two bench comments to present tense (the P4-NANO bench pin map, and the LCD_CAM SOC-macro guard) so they read as current configuration notes, not history.
- platform_esp32: retarget the mDNS-pool comment from lessons.md to ADR-0006 (the device-discovery decision that lesson became), matching its DevicePlugin/DevicesModule siblings.

Scripts / MoonDeck:
- moondeck.py: free port 8420 from any prior instance on startup, so re-running MoonDeck replaces the old server instead of failing to bind; also picks up deviceModels.json edits (the catalog loads at import). MOONDECK_NO_KILL=1 opts out. Targets the port owner, not a name match, so it can never self-kill.
- moondeck.py + app.js: the discover response now reports which subnet was scanned + how many devices answered, so a wrong/unset-network scan (finds nothing on an empty subnet) reads differently from "no devices online" — the discover log names the subnet and, on zero hits, hints to check the machine's subnet / VPN. Transient fields, not persisted.

Docs / CI:
- CLAUDE.md: add docs/adr/ to the present-tense exemption (and the principles-audit sweep) alongside history/backlog/lessons.md — an ADR's Context is the situation that forced the decision, a before/after contrast by nature, the same reason lessons.md is exempt. This closes the policy gap the ADR directory introduced.
- led-signal-integrity.md: reword one sentence to present tense ("these checks are the bench diagnosis path").
- lessons.md: carry forward two branch lessons — a retract/cleanup path gated on "connected" strands the resource when disconnected (the HA discovery bug, found live on P4/S31), and raw-HTML <img src> is not validated by MkDocs --strict so a wrong ../ depth ships a silent 404.

Reviews:
- 🐇 present-tense in led-signal-integrity.md / ParlioLedDriver.h / platform_esp32_lcd.cpp (3 findings): fixed, reworded present-tense.
- 🐇 present-tense in ADRs 0001/0003/0004 (3 findings): accepted, not changed — an ADR's Context section is a point-in-time record whose before-state contrast is the decision's justification (Nygard format); root cause fixed instead by exempting docs/adr/ in CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ewowi ewowi merged commit 8e2df73 into main Jul 7, 2026
3 checks passed
@ewowi ewowi deleted the ha-mqtt-discovery branch July 7, 2026 09:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant