ADR 0049 — Matter exposes one endpoint per physical device by default¶
Historical paths. Since 0.74.0 the Matter stack is a dependency, not a subtree: it lives in the go-fabric module and
internal/north/matter/no longer exists in this repository. The paths below are left as they were when the decision was made — a record rewritten to match today's tree stops being a record. For where each piece lives now, seeSPECIFICATION.md§6.
- Status: Accepted
- Date: 2026-07-10
- Related: ADR 0012 — Matter pure-Go implementation (supersedes its
valve.Irrigation"stays MQTT-only" rows), ADR 0015 — DataPointUsageIgnoredsplit (the usage taxonomy this extends), By-design catalogue:notes/parity/by_design.md(BD-Visibility-CDPStateGroupStatus)
Context¶
A HomeMatic custom entity spans several CCU channels off one physical device. A valve.Irrigation (ELV-SH-WSM), for example, groups a primary actor channel (WATER_SWITCH_VIRTUAL_RECEIVER, its bool STATE), two sibling actor channels (offsets +1/+2, classified ce_secondary), a group-STATE transmitter channel (offset -1, whose bool STATE merely restates the primary's on/off), and read-only metering channels (WATER_FLOW, WATER_VOLUME — genuine extra sensors, ce_visible). The same shape recurs across light, lock, switch, cover, siren, ….
Two facts collided:
-
The primary channel already projects to Matter.
valve.Irrigationembeds*generic.Switch, which implementsMatterEndpointSourceto mapSTATEonto OnOff (0x0006) / OnOffPlugInUnit (0x010A). Go method promotion carries that projection onto the valve unchanged. ADR 0012 had parked the valve as "stays MQTT-only", but the code bridged it — a documented gap the chiptool negative suite had tot.Skip. -
Every constituent channel with a bool
STATEprojects the same way. Left unfiltered, one physical valve materialises three-to-four separate on/off endpoints in Apple/Google Home — the primary, its two secondary actors, and the redundant group-STATE transmitter — which reads to a user as four lamps for one device. Meanwhile a genuine extra sensor on the same custom entity (a wall thermostat's HUMIDITY, a contact's STATE) is alsoce_visibleand SHOULD surface.ce_visiblealone cannot tell the redundant transmitter apart from a real second sensor.
Decision¶
Matter exposes one endpoint per physical device by default: the primary channel's projection. Three parts:
-
Bridge
valve.Irrigationas OnOff on its primary channel. This supersedes ADR 0012's twovalve.Irrigation"stays MQTT-only" rows (the Out-of-scope table and the Custom-DP mapping table). The valve is a first-class OnOff / OnOffPlugInUnit endpoint like any other switch. -
New
DataPointUsagevaluece_state(DataPointUsageCDPState). The materializer marks theFieldGroupStateconstituent (the -1 offset group-STATE transmitter)ce_stateinstead ofce_visible. HA / MQTT / REST treatce_stateidentically toce_visible— it stays a visible data point there, no behaviour change — but the Matter projection drops it by default, keeping genuine extrace_visiblesensors (HUMIDITY, a contact STATE). This is the classification that lets the filter distinguish "a redundant restatement of the primary" from "a real second measurement". -
Expert flag
north.matter.expose_secondary_channels(defaultfalse,cfg:"expert"). When enabled, the Matter projection also surfaces the secondary actor channels (ce_secondary) and the group-STATE (ce_state) as their own endpoints, for power users who want per-channel control. The default keeps the device presenting as one endpoint. -
Align the Matter candidate set with the entity-creation gate. MQTT (raw + HA-Discovery), REST and the SPA already skip entity creation for
ignoredandno_createdata points (service / status / overflow params the visibility gate suppresses —INSTALL_TEST,*_STATUS,*_OVERFLOW,PROCESS,CONFIG_PENDING, … — and raw constituents an aggregating parent consumes). The Matter eligibility collector previously did not apply that gate, so every such data point leaked intoGET /api/v1/matter/exposableas an "unmappable" candidate, cluttering the exposure UI with dozens of rows an operator can never meaningfully act on. The collector (and the endpoint assembler) now drop them: a Matter candidate is a data point that would be a standalone entity elsewhere —data_point,event,ce_primary,ce_visible— neverignored, never theno_createraw constituent of a parent that already projects. A DP an operator later un-ignores flips todata_pointand reappears automatically.
This is a Matter-only concern. MQTT (raw + HA-Discovery), REST, and the Config UI are unaffected — they continue to surface every channel and data point exactly as before. The filter lives in the Matter eligibility collector (internal/north/matter/eligibility) and endpoint assembler (internal/north/matter/endpoint); both drop ce_secondary + ce_state constituents (and whole secondary channels) unless the flag is set.
Consequences¶
- One clean Matter endpoint per device by default. A valve is one OnOff endpoint, not four. Power users opt into the full channel fan-out with a single expert flag; the flag is plumbed through
bridge.Config→endpoint.Configandinternal/north/matteradapter.CollectCandidates. (The Matter stack moved to the go-fabric module, and the candidate walk stayed here: there is noeligibility.CollectCandidatesin either repository, which is what this line used to name.) ce_statehas no Python-reference equivalent. aiohomematic's model has no such usage, so the cross-stack model-parity snapshot would flag every group-STATE DP as drift.script/model_snapshot_diff.pycanonicalisesce_state→ce_visibleon both sides (canon_state_usage), matching the behavioural equivalence HA/MQTT/REST already honour. Documented asBD-Visibility-CDPStateGroupStatusinnotes/parity/by_design.md.- Standing guards.
TestHideFromMatter(internal/north/matter/eligibility) pins the full usage × flag × channel-owns-custom matrix the filter applies; the chiptoolTestNegative_UnmappableDeviceClassesvalve cases assert the ELV-SH-WSM surfaces a mappableSTATEcandidate on exactly its primary channel (the group-STATE transmitter + secondary actors stay hidden while the flag is off) and that itsignored/no_createservice params never appear as candidates. The oldt.Skipdocumenting the ADR-0012 gap is gone. - The negative suite reframes the valve. It is no longer an "unmappable device class"; it is a "device whose custom-entity plumbing must not multiply endpoints" — the boundary the suite now guards.