Skip to content

Split-endpoint energy measurement attribution (IKEA GRILLPLATS)#80

Merged
simons-plugins merged 2 commits into
mainfrom
fix/issue-79-split-endpoint-energy
Jul 6, 2026
Merged

Split-endpoint energy measurement attribution (IKEA GRILLPLATS)#80
simons-plugins merged 2 commits into
mainfrom
fix/issue-79-split-endpoint-energy

Conversation

@simons-plugins

@simons-plugins simons-plugins commented Jul 5, 2026

Copy link
Copy Markdown
Owner

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.

  • Link resolution: SetTopology (0x9C FeatureMap bit 2) → AvailableEndpoints/ActiveEndpoints list (malformed lists degrade to the heuristic); NodeTopology / no 0x9C → sole meter-capable-actuator heuristic. Endpoints with their own co-located energy clusters are excluded as targets (prevents reading crosstalk). Zero or ambiguous candidates → no guess, debug-logged.
  • Fallback: unattributable energy endpoints get a proper matterEnergyMeter device (custom type, state ids exactly matching the electrical handlers' keys).
  • Self-heal: _capability_props folds linked source clusters into the target's props — pre-fix relays (e.g. GRILLPLATS device 1794293937 on jarvis) gain meter props on first reconcile via replacePluginPropsOnServer, no recreation. Stale ep2 placeholders now get the "can be deleted" obsolescence log on every path.
  • Unmapped clusters on an endpoint with a recognised device are dropped silently (no log, no placeholder) #81: an endpoint that produces a real device but carries unmapped clusters now logs the report-invitation INFO instead of silently dropping capability (pump class).
  • Bridge nodes: node_scoped PowerSource fan-out cross-contaminates battery levels between bridged children #82: PowerSource attribution is endpoint-scoped when a node has >1 PowerSource-bearing endpoint (bridge battery cross-contamination); single-PS nodes keep node-wide behaviour.

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)

  • Reconcile heals relay 1794293937 (meter props + energy states appear)
  • Live wattage/energy from GRILLPLATS
  • Obsolescence log fires for old ep2 placeholder 1456954491 → delete it manually
  • Tapo P110M co-located path unchanged
  • Check whether matter-server's node dump includes 0x9C FeatureMap (informs SetTopology field-readiness; NodeTopology path needs no attribute reads)

🤖 Generated with Claude Code

https://claude.ai/code/session_01Arj5R66d73tc3PMPvHS65Z

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
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements split-endpoint energy attribution (issue #79) for IKEA GRILLPLATS-style devices where On/Off and electrical energy clusters live on separate endpoints. It adds meter-link resolution, cross-endpoint routing, a matterEnergyMeter fallback device, related tests, and a design document.

Changes

Split-endpoint energy meter linking

Layer / File(s) Summary
Implementation plan document
docs/plans/ISSUE-79-split-endpoint-energy.md
Documents the problem, meter-link concept, resolution algorithm, planned changes, test plan, rollout steps, and risks.
Power Topology constants and cluster tracking
matter_handlers/electrical.py, device_sync.py
Adds Power Topology cluster/attribute/feature constants, extends non-device and merge-only electrical cluster sets, and adds a role label for matterEnergyMeter.
Meter-link resolution helpers
device_sync.py
Introduces forward/reverse link maps and helpers to detect meter-source endpoints and resolve targets via SetTopology/NodeTopology heuristics.
Creation, routing, and capability wiring
device_sync.py
Reworks device creation to resolve links up front, routes electrical readings to linked targets, injects meter-support props, converts _capability_props to an instance method, and extends priming and placeholder diagnostics.
matterEnergyMeter fallback device and version bump
Devices.xml, Info.plist
Adds the matterEnergyMeter custom device with states/UI fields and bumps plugin version.
Test fixtures and coverage
tests/test_device_sync.py, tests/test_device_zoo.py
Updates fake device state seeding, adjusts placeholder expectations, and adds GRILLPLATS/ambiguous/unsupported/Tapo regression scenarios plus a zoo fixture.

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
Loading

Possibly related issues

Possibly related PRs

🚥 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 clearly matches the main change: split-endpoint energy attribution for IKEA GRILLPLATS.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-79-split-endpoint-energy

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.

🧹 Nitpick comments (1)
tests/test_device_sync.py (1)

1796-1820: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen 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 default 0). Asserting the expected values (1.2 / 3.6, matching GRILLPLATS_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

📥 Commits

Reviewing files that changed from the base of the PR and between 450b9ee and e2a5f88.

📒 Files selected for processing (7)
  • docs/plans/ISSUE-79-split-endpoint-energy.md
  • indigo-matter.indigoPlugin/Contents/Info.plist
  • indigo-matter.indigoPlugin/Contents/Server Plugin/Devices.xml
  • indigo-matter.indigoPlugin/Contents/Server Plugin/device_sync.py
  • indigo-matter.indigoPlugin/Contents/Server Plugin/matter_handlers/electrical.py
  • tests/test_device_sync.py
  • tests/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
@simons-plugins simons-plugins merged commit e60f4d4 into main Jul 6, 2026
3 checks passed
@simons-plugins simons-plugins deleted the fix/issue-79-split-endpoint-energy branch July 6, 2026 07:13
simons-plugins added a commit to Highsteads/indigo-matter that referenced this pull request Jul 6, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment