Skip to content

Ignore unavailable members when computing light group state - #888

Open
RReverser wants to merge 1 commit into
zigpy:devfrom
RReverser:light-group-ignore-unavailable-members
Open

RReverser wants to merge 1 commit into
zigpy:devfrom
RReverser:light-group-ignore-unavailable-members

Conversation

@RReverser

Copy link
Copy Markdown

LightGroup.update() derives on/off from every member's last reported state, with no availability check. A member that drops off the network while on keeps voting "on" indefinitely. The group then stays marked as "on" even once every reachable member is off, and only clears once the stale member itself reports again or the whole group entity is force refreshed some other way.

_make_members_assume_group_state() already skips unavailable members when writing optimistic state after a command. update() should apply the same rule when reading it back.

`LightGroup.update()` derives on/off from every member's last reported
state, with no availability check. A member that drops off the network
while on keeps voting "on" indefinitely. The group then stays on even
once every reachable member is off, and only clears once the stale
member itself reports off again or the whole group entity is force
refreshed some other way.

`_make_members_assume_group_state()` already skips unavailable members
when writing optimistic state after a command. `update()` should apply
the same rule when reading it back.
Copilot AI lite review requested due to automatic review settings September 4, 2026 16:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The change is narrowly scoped, matches the PR description, and includes a targeted regression test covering the reported failure mode.

Pull request overview

This PR fixes LightGroup.update() so a light group’s computed on/off state ignores members that are currently unavailable, preventing a stale “on” report from an offline device from keeping the whole group marked on.

Changes:

  • Filtered on_states in LightGroup.update() to include only members that are both on and available.
  • Added a regression test ensuring an unavailable “on” member does not keep the group on after all reachable members are off, and that it counts again once it becomes reachable.
File summaries
File Description
zha/application/platforms/light/__init__.py Updates group state derivation to exclude unavailable members from contributing to the “on” vote.
tests/test_light.py Adds a test covering the offline-member-stale-state scenario for light groups.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.19%. Comparing base (6660343) to head (990b483).

Additional details and impacted files
@@           Coverage Diff           @@
##              dev     #888   +/-   ##
=======================================
  Coverage   97.19%   97.19%           
=======================================
  Files          57       57           
  Lines       10560    10560           
=======================================
  Hits        10264    10264           
  Misses        296      296           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@RReverser

Copy link
Copy Markdown
Author

Ping? This one should be a straightforward fix.

@zigpy-review-bot zigpy-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve — the fix is correct, minimal, and restores parity with Home Assistant's own group-light semantics.

Worth recording why this is the right shape, since the one-line diff hides it: LightGroup.update() is a port of HA core's group light (it arrived in 98610b2, the initial import of the HA code), and there the availability gate was implicit — HA computes on_states = [state for state in states if state.state == STATE_ON] (homeassistant/components/group/light.py:213), and an unavailable entity's state string is unavailable, never on, so it was never in on_states to begin with. The port to a typed LightState dataclass carried state.on over faithfully but dropped that implicit gate, which is exactly the bug described. Adding and state.available restores the original semantics rather than inventing new ones.

The two edge cases I'd have wanted answered both hold up:

  • All members unavailable doesn't produce a misleading "off": GroupEntity.available is any(member.available) (zha/application/platforms/init.py:765-770), so the group entity itself goes unavailable in that case and the _state value is never surfaced.
  • Keeping capabilities merged over every member while only on_states is filtered is right, and matches HA core's split — min_mireds / max_mireds / supported_color_modes / supported_features / effect_list are derived from states there too, only brightness / color / effect / color_mode follow on_states. The comment in the diff says this explicitly, which is good.

One behavior note for the changelog rather than a change request: a group holding a reachable-and-off member plus an unreachable-and-on member now flips to off at the moment the unreachable member is marked unavailable, so state-change automations on such a group can fire where they previously wouldn't. That is the intended fix and it's what an HA group would do, but it is a user-visible transition that didn't happen before.

Optional, explicitly out of scope for this PR: FanGroup.update() (zha/application/platforms/fan/init.py:370-372) has the same shape — percentage_states / preset_mode_states filter member states with no availability check and then take [0] — so an offline member's stale percentage can win the vote there. Same class of bug, worth a separate PR if you feel like it.

Verified (8 checks)
  • The new test is a real regression guard. Reverted the production line back to [state for state in states if state.on] in a worktree at the PR head and re-ran just test_group_state_ignores_unavailable_members — it fails; with the fix in place it passes. It is not a test that would pass either way.
  • Test conventions match the file. The gateway/coordinator wiring and the await asyncio.sleep(0.1) + async_block_till_done() debounce dance mirror test_group_member_assume_state (tests/test_light.py:1858-1861) and test_zha_group_light_entity, and bool(entity.state.on) is the assertion form already used for group on/off there.
  • Availability actually propagates to the group. Device.on_network setter and the check-in path both route through update_available(), which emits maybe_emit_state_changed_event() on every platform entity (zha/zigbee/device.py:869-901); Group subscribes each member entity's STATE_CHANGED to group_entity.debounced_update (zha/zigbee/group.py:283-285). The becoming-available direction goes through _async_became_available()async_initialize(False) → re-emit, so the member's stale on is refreshed rather than merely re-counted.
  • Full test suite at the PR head: 1383 passed (pytest tests/), and tests/test_light.py is 21/21.
  • Merged with current dev: the branch is 3 commits behind dev (dependency bumps 890 / 891 / 892 only). Merged origin/dev in locally, re-synced, re-ran the light suite — still 21/21, so the staleness is cosmetic.
  • Lint/type: ruff check clean, ruff format --check clean, and mypy zha/ inside the worktree venv (where zigpy/zhaquirks actually resolve, unlike the pre-commit hook's isolated env) reports no issues in 59 source files.
  • CI: all shared-ci jobs green on 990b483 across Python 3.12 / 3.13 / 3.14, patch coverage 100%.
  • Independent second opinion (a different model, read-only over the same diff, with the ha-core group-light source available for comparison): no findings.

@RReverser

Copy link
Copy Markdown
Author

Is this good to go then?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants