Iter 26. Lands ADR-122 §2.1 HA-DISCO config-message generator.
Counterpart to iter 21's state-topic router: this produces the
homeassistant/<type>/<unique_id>/config messages HA reads on
startup to auto-create the six BFLD entities as a single device.
Discovery payloads are intended to be published once per node
session with retain = true (so HA finds them on subsequent starts).
The RumqttPublisher from iter 23 already exposes with_retain(true)
for this purpose; the state-topic loop must keep retain = false to
avoid stale-state flapping.
Added (gated on `feature = "std"`):
- src/ha_discovery.rs:
* render_discovery_payloads(node_id, class) -> Vec<TopicMessage>
class < Anonymous: empty vec (HA doesn't see raw/derived)
class == Anonymous: 6 entities incl. identity_risk
class == Restricted: 5 entities, no identity_risk
* Per-entity HA metadata:
presence binary_sensor, device_class: occupancy
motion sensor, entity_category: diagnostic
person_count sensor, unit_of_measurement: people
zone_activity sensor, entity_category: diagnostic
confidence sensor, entity_category: diagnostic
identity_risk sensor, entity_category: diagnostic
* Each payload carries:
name, unique_id, state_topic (pointing at the iter-21 path),
device block with identifiers / model: "BFLD" / manufacturer: "RuView"
* Manual JSON builder with minimal escape coverage — node_id is
ASCII alphanumeric + dash by convention; full escape via
serde_json is a follow-up if operator-controlled names ever land.
- pub use render_discovery_payloads from lib.rs
tests/ha_discovery.rs (10 named tests, all green):
raw_and_derived_classes_produce_no_discovery_payloads
anonymous_class_produces_six_discovery_payloads
restricted_class_omits_identity_risk_discovery
discovery_topic_format_matches_ha_convention
(validates all six homeassistant/.../config topics exist)
presence_payload_carries_occupancy_device_class
motion_payload_marked_as_diagnostic
person_count_payload_carries_unit_of_measurement
every_payload_contains_unique_id_and_state_topic_pointing_at_correct_state_topic
(the state_topic in the discovery payload must match the topic the
state-topic router from iter 21 actually publishes on — closes
the discovery↔state loop)
unique_id_matches_topic_segment
(the unique_id baked into the payload equals the topic segment so
HA dedupe works correctly across reboot/restart)
class_2_discovery_includes_identity_risk_explicitly
ACs progressed:
- ADR-122 §2.1 — HA auto-discovery surface now complete: an operator
can start mosquitto, publish-retained discovery once, and HA spins
up the entire BFLD device on next start with zero YAML config.
- ADR-122 AC1 (six entities per node) — discovery + state-topic
publishers are now symmetric: render_discovery_payloads emits the
same six entity definitions render_events emits state messages for.
- ADR-118 §1.5 — privacy_mode = Restricted strips identity_risk at
BOTH the discovery layer (entity not advertised to HA) AND the
state layer (no state messages). Two-layer defense.
Test config:
- cargo test --no-default-features → 72 passed (ha_discovery cfg-out)
- cargo test → 187 passed (177 + 10)
Out of scope (next iter target):
- HA discovery + state publish coordinator: a small function or
BfldPipelineHandle::publish_discovery(&mut self, retained: bool)
that calls render_discovery_payloads + publish_event(retained=true)
once at startup, then enters the per-frame loop.
- GitHub Actions workflow with mosquitto Docker service so the
iter-24 integration test runs in CI with BFLD_MQTT_BROKER set.
Co-Authored-By: claude-flow <ruv@ruv.net>