Split-endpoint energy measurement attribution (IKEA GRILLPLATS)#80
Conversation
IKEA GRILLPLATS-style devices expose Electrical Power (0x0090) / Energy (0x0091) / Power Topology (0x009C) on a dedicated Electrical Sensor endpoint (0x0510) separate from the relay. Previously that endpoint became a stateless matterUnknown placeholder and all readings were lost. - Per-node meter-link map (forward + reverse) resolved between the new two-pass create_devices: SetTopology endpoint lists when present, sole-actuator heuristic for NodeTopology / missing 0x9C - 0x009C added to _NON_DEVICE_CLUSTERS (merge-only) - _lookup_for_cluster rewrites 0x90/0x91 events to the linked target, preferring meter-capable device types - _prime_states and _capability_props (now instance method) fold in linked source endpoints — reconcile self-heals pre-fix relays in place - matterEnergyMeter fallback device when attribution is ambiguous (state ids match the electrical handlers' keys exactly) - Placeholder log now annotates merge-only electrical clusters - Meter-source candidacy requires all clusters within _NON_DEVICE_CLUSTERS so unsupported-cluster endpoints still placeholder - Tests: 792 passing (15 new: link matrix, live routing, priming, self-heal, Tapo regression, GRILLPLATS zoo entry) - Design plan: docs/plans/ISSUE-79-split-endpoint-energy.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Arj5R66d73tc3PMPvHS65Z
📝 WalkthroughWalkthroughThis PR implements split-endpoint energy attribution (issue ChangesSplit-endpoint energy meter linking
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Node as Matter Node
participant DeviceSync as DeviceSync
participant LinkResolver as Meter-Link Resolver
participant Relay as Relay Device (ep1)
participant EnergyMeter as matterEnergyMeter (fallback)
Node->>DeviceSync: create_devices() / attribute_updated (ep2: ElectricalPower/Energy)
DeviceSync->>LinkResolver: resolve_links(node)
LinkResolver->>LinkResolver: check SetTopology/AvailableEndpoints
alt topology resolves single actuator
LinkResolver-->>DeviceSync: forward/reverse link ep2->ep1
DeviceSync->>Relay: inject SupportsPowerMeter/SupportsEnergyMeter
DeviceSync->>Relay: update curEnergyLevel/accumEnergyTotal states
else ambiguous or no actuator
LinkResolver-->>DeviceSync: no link resolved
DeviceSync->>EnergyMeter: create standalone fallback device
DeviceSync->>EnergyMeter: prime energy states
end
Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/test_device_sync.py (1)
1796-1820: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winStrengthen self-heal test to assert primed values, not just key presence.
The assertions only check
"curEnergyLevel" in devices[dev_id].states/"accumEnergyTotal" in devices[dev_id].states. This would pass even if the self-heal path added the Supports* props but failed to actually re-prime the correct values (e.g. left them at the FakeDev default0). Asserting the expected values (1.2 / 3.6, matchingGRILLPLATS_NODE) would close that gap.♻️ Proposed strengthening
assert devices[dev_id].pluginProps.get("SupportsPowerMeter") is True assert devices[dev_id].pluginProps.get("SupportsEnergyMeter") is True - assert "curEnergyLevel" in devices[dev_id].states - assert "accumEnergyTotal" in devices[dev_id].states + assert devices[dev_id].states.get("curEnergyLevel") == 1.2 + assert devices[dev_id].states.get("accumEnergyTotal") == 3.6🤖 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 `@tests/test_device_sync.py` around lines 1796 - 1820, The self-heal test in test_reconcile_self_heals_meter_props_via_link_without_recreation only verifies that curEnergyLevel and accumEnergyTotal keys exist, so it can miss cases where reconcile_all() adds the meter props but fails to prime the actual values. Update the assertions in this test to check the expected primed values from GRILLPLATS_NODE (1.2 and 3.6) on devices[dev_id].states after ds.reconcile_all(), while keeping the existing lookup checks to ensure the device is healed in place.
🤖 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.
Nitpick comments:
In `@tests/test_device_sync.py`:
- Around line 1796-1820: The self-heal test in
test_reconcile_self_heals_meter_props_via_link_without_recreation only verifies
that curEnergyLevel and accumEnergyTotal keys exist, so it can miss cases where
reconcile_all() adds the meter props but fails to prime the actual values.
Update the assertions in this test to check the expected primed values from
GRILLPLATS_NODE (1.2 and 3.6) on devices[dev_id].states after
ds.reconcile_all(), while keeping the existing lookup checks to ensure the
device is healed in place.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f579b45d-1048-400f-be3d-0f2fb3c17015
📒 Files selected for processing (7)
docs/plans/ISSUE-79-split-endpoint-energy.mdindigo-matter.indigoPlugin/Contents/Info.plistindigo-matter.indigoPlugin/Contents/Server Plugin/Devices.xmlindigo-matter.indigoPlugin/Contents/Server Plugin/device_sync.pyindigo-matter.indigoPlugin/Contents/Server Plugin/matter_handlers/electrical.pytests/test_device_sync.pytests/test_device_zoo.py
…ed in Consolidated fixes from the four-agent review pass on PR #80 plus the 2026-07-05 cluster-coverage audit: - Exclude endpoints with their own co-located 0x90/0x91 from meter-link targets — an orphan electrical endpoint could otherwise overwrite a Tapo-style relay's genuine readings (reproduced data corruption) - Placeholder-obsolescence log restructured (had_placeholder up front) so it fires for meter-linked and fallback endpoints too — previously structurally unreachable there, silently orphaning e.g. the GRILLPLATS ep2 placeholder on existing installs - Guard int() over SetTopology endpoint lists — malformed matter-server data degraded to aborting the whole node's device creation - Debug logging on both no-link resolution paths - #81: leftover unmapped clusters on a device-producing endpoint now get the report-invitation INFO log (pump-class silent capability loss) - #82: PowerSource attribution is endpoint-scoped when a node has more than one PowerSource-bearing endpoint (bridge battery cross-contamination); single-PS nodes keep node-wide behaviour - Tests: DynamicPowerFlow decoy, event-before-create no-op, malformed list, placeholder-preseeded reconciles, bridge isolation, mixed_unmapped zoo entry — 806 passing (was 792) - Docs: plan amended to shipped SetTopology semantics + post-review addendum; spec citation made checkable; docstring exec-order fix Closes #81, closes #82 (via PR #80) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Arj5R66d73tc3PMPvHS65Z
Maintainer commit on top of Highsteads' PR simons-plugins#73: merge current main (post-simons-plugins#80 split-endpoint energy) and bump PluginVersion per the every-PR version rule. Patch bump — startup-noise cleanup, no functional change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Arj5R66d73tc3PMPvHS65Z
Summary
Comprehensive energy/cluster-attribution fix. Core: the IKEA GRILLPLATS (and any Matter 1.3 "Electrical Sensor" 0x0510 layout) getting a stateless placeholder instead of energy readings — Electrical Power (0x0090) / Energy (0x0091) / Power Topology (0x009C) on a dedicated endpoint are now attributed to the endpoint they measure, matching the Tapo co-located behaviour. Two adjacent bugs from the same audit are rolled in.
Closes #79
Closes #74
Closes #81
Closes #82
Design
Adversarially-verified plan in
docs/plans/ISSUE-79-split-endpoint-energy.md(including post-review addendum). Core mechanism: a per-node meter-link map (forward for live routing, reverse for priming/self-heal), rebuilt deterministically on every create/reconcile pass.matterEnergyMeterdevice (custom type, state ids exactly matching the electrical handlers' keys)._capability_propsfolds linked source clusters into the target's props — pre-fix relays (e.g. GRILLPLATS device 1794293937 on jarvis) gain meter props on first reconcile viareplacePluginPropsOnServer, no recreation. Stale ep2 placeholders now get the "can be deleted" obsolescence log on every path.Review
Four-agent review pass (code / tests / silent-failures / comments) + cluster-coverage audit; all critical and important findings fixed in the second commit, including a reproduced crosstalk data-corruption bug and a reproduced unreachable-obsolescence-log bug. Follow-up filed separately: #84 (pre-existing).
Tests
806 passing (29 new across both commits): link-resolution matrix (NodeTopology / SetTopology / DynamicPowerFlow decoy / malformed list / no-0x9C / ambiguous / electrical-only / co-located crosstalk), live cross-endpoint routing, event-before-create no-op, priming, reconcile self-heal, placeholder-preseeded reconciles, bridge battery isolation, placeholder log annotation, Tapo co-located regression, GRILLPLATS + mixed_unmapped zoo entries.
Version
2026.3.2 → 2026.4.0 (user-visible feature).
Post-merge validation (jarvis, live GRILLPLATS on house fabric)
🤖 Generated with Claude Code
https://claude.ai/code/session_01Arj5R66d73tc3PMPvHS65Z