feat(group): target-aware scene status from /api/groups link data#175
Conversation
Group on/off was a coarse "any member ST non-zero" aggregate — wrong for radio-style KPL scenes (a keypad always has one button lit, so group_any_on was always True and group_all_on structurally impossible). Fetch the optional /api/groups endpoint during load and resolve each member's scene intent from the canonical (id == group) ctl block: native OL>0 -> on / OL=0 -> off; default -> on (state-tracked); cmd DON/DFON -> on, DOF/DFOF -> off, anything else (BL/BEEP/SETST/…) -> discard; ignore -> not a member. Unknown link type or a native link with no OL leaves the group unresolved -> legacy aggregate (never regress). group_any_on/group_all_on now aggregate over the on-target subset; an empty on-set (fire-only / config-only / auto-DR scene) reads OFF, matching the admin console. New Group.has_state_target exposes whether the scene maintains any on/off state (False for fire-only) so consumers can model those as a button not a switch (hacs-udi-iox#86). - paths: GROUPS_PATH - client: _get_json_or_empty, GroupRecord.member_intents + targets_resolved, apply_group_link_targets(), wired into load() - runtime/group: _on_set(), has_state_target, target-aware group_any_on/group_all_on with legacy fallback; to_dict adds has_state_target - tests: 18 parse + aggregation cases; FakeSession defaults /api/groups to an empty payload (optional endpoint) so full-load tests need no per-test scripting; full-load fan-out count 8 -> 9 860 passed, mypy clean, pylint 10/10, ruff clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Claude finished @shbatm's task in 3m 34s —— View job Code Review: PR #175 — feat(group): target-aware scene status from /api/groups link dataTasks:
SummaryThis is a well-designed additive feature. The intent-resolution logic is cleanly separated, the legacy fallback is solid (
|
has_dimmable_members: True iff any member node is_dimmable (nodedef-derived; independent of /api/groups resolution so it works on older firmware). Pairs with has_state_target so a consumer picks the scene HA platform: no state target -> button; dimmable members -> on/off light (preserves light semantics + the group/more-info framework, no switch_as_x); else -> switch. Scenes have no settable brightness (fade/brt/dim are separate manual commands), so "light" here is on/off only. Added to to_dict. Review follow-ups (all non-blocking, from the PR #175 review): - _link_intent: comment the "" sentinel (skip link, not abort group) - apply_group_link_targets: rank >= -> > (first-seen wins among duplicate same-type links; cross-type precedence unchanged) - _get_json_or_empty docstring: swallows any HTTPError, not just 404 - tests: no-"ctl"-key resolves empty; native-before-default reverse precedence; resolved on-set all-stateless -> group_all_on False; has_dimmable_members (dimmer true / relay false / no-nodes false / legacy-path independent of targets_resolved) 867 passed, mypy clean, pylint 10/10, ruff clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Claude finished @shbatm's task in 2m 44s —— View job Code Review: PR #175 — feat(group): target-aware scene status from /api/groups link dataTasks:
SummaryThe follow-up commit ( Previous Review Items — All Addressed ✓
New Feature:
|
|
Thanks for the thorough review — addressed in
rank no-
reverse-order precedence — added resolved on-set all stateless — added Folded in 867 passed · mypy clean · pylint 10/10 · ruff clean. PR description updated. |
Re-review of #175 (non-blocking): has_dimmable_members built a temp Node per member on every call (incl. every to_dict). Memoise it on the instance — safe because it's derived from member nodedefs (static for the record), unlike the live group_*_on aggregates which must re-read ST and are deliberately not cached. Added _dimmable_cache slot. test_connect_runs_full_load: the explicit /api/groups set_route is intentional (the full-load test pins every fan-out route so its call-count assertion is self-contained) even though FakeSession now defaults it — added a comment so it isn't "cleaned up" as dead. 867 passed, mypy clean, pylint 10/10, ruff clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Claude finished @shbatm's task in 2m 8s —— View job Code Review: PR #175 — feat(group): target-aware scene status from /api/groups link dataTasks:
SummaryThe latest commit ( Latest Commit:
|
| Item | Commit |
|---|---|
>= → > for same-type duplicate links (first-seen wins) |
4582da2 |
| Reverse insertion order (native before default) test | 4582da2 |
Missing test for absent ctl key in group entry |
4582da2 |
_get_json_or_empty docstring ("any HTTPError", not just 404) |
4582da2 |
Stateless-only on-set causing silent False — documented in test |
4582da2 |
_link_intent "" sentinel comment (skip-link vs. abort-group) |
4582da2 |
has_dimmable_members memoisation (avoids per-call Node construction) |
a3a048e |
Full-load test /api/groups route comment (self-contained intent) |
a3a048e |
Overall
Design is sound throughout: best-effort /api/groups enrichment with conservative fallback, correct _on_set() / has_state_target / has_dimmable_members separation, memoization correctly scoped to static data, and all review items from both passes resolved. Ready to merge.
|
Both non-blocking observations addressed in the latest push.
Redundant explicit
867 passed · mypy clean · pylint 10/10 · ruff clean. |
Implements the first slice of #174. Composes with #172 (merged) — that
delivers the change notification (member→group re-emit); this makes
the value correct.
Problem
group_any_on/group_all_onwere a coarse "any/all stateful memberST non-zero" aggregate. For a radio-style KeypadLinc scene a keypad
always has exactly one button lit, so
group_any_onwas permanentlyTrue and
group_all_onstructurally impossible — the scene switchnever reflected reality.
Fix
Fetch the optional
/api/groupsendpoint during load and resolve eachmember's scene intent from the canonical ctl block (the one whose
id== the group address — the scene's own responder definition;per-controller cross-link blocks are ignored):
typenativeOL > 0→ on,OL == 0→ offdefaultcmdDON/DFON→ on,DOF/DFOF→ off, anything else (BL/BEEP/SETST/BRT/DIM/FD*) → discardignorenativew/oOLgroup_any_on/group_all_onnow aggregate over the on-targetsubset. An empty on-set (fire-only / config-only / the auto-DR groups)
reads OFF, matching the IoX admin console.
Group.has_state_targetexposes whether the scene maintains any on/off state —
Falsefor afire-only scene, so a consumer can model it as a momentary button
rather than a stuck switch (hacs-udi-iox#86).
Strictly additive and consumer-transparent:
/api/groupsisbest-effort (older firmware 404 →
{}→ groups keep legacy behaviour);exact on-level match (
scene_matches_target) is deliberately deferred.Validated against real captures: radio scene
8499(oneOL=100member, rest
OL=0), curtain19359, the auto-DRADR0001(emptycanonical block → OFF), and the
default+cmd/BL-only test scenes.Changes
paths:GROUPS_PATHclient:_get_json_or_empty;GroupRecord.member_intents+targets_resolved;apply_group_link_targets(); wired intoload()(so
refresh()re-enriches too)runtime/group:_on_set(),has_state_target, target-awaregroup_any_on/group_all_onwith legacy fallback;to_dictaddshas_state_targetFakeSessiondefaults/api/groupsto an empty payload so the many full-load tests need noper-test scripting; full-load fan-out asserted 8 → 9
Scene HA-platform inputs (
has_state_target+has_dimmable_members)pyisyox exposes two orthogonal capability booleans; the consumer
composes the platform (capability stays here, the HA light/switch/button
decision stays policy):
Group.has_state_target—Falseonly for a resolved fire-only /config scene (→ button);
Trueotherwise (incl. unresolved:conservative, stays a switch).
Group.has_dimmable_members—Trueiff any member nodeis_dimmable(nodedef-derived; independent of/api/groupsso itworks on older firmware / unresolved groups too).
Consumer mapping:
not has_state_target→ button; elsehas_dimmable_members→ on/offlight(lets hacs create the scenenatively at the right platform, preserving the
Entity.group/more-info framework instead of losing it through
switch_as_x); else →switch. Scenes have no settable brightness — ISY groups don't take a
scene-level on-level; fade/brt/dim are separate manual commands
(reachable via
send_node_command; sceneacceptsaren't exposed asbuttons yet) — so the light is on/off only.
Review nits from the first pass are addressed (rank
>first-seen,""-sentinel comment, docstring, +no-ctl / reverse-precedence /all-stateless / has_dimmable tests).
867 passed · mypy clean · pylint 10/10 · ruff clean.
🤖 Generated with Claude Code