Skip to content

Reload HomeKit security system on stale valid values#172752

Open
mattias-arrelid wants to merge 4 commits into
home-assistant:devfrom
mattias-arrelid:homekit-securitysystem-stale-valid-values
Open

Reload HomeKit security system on stale valid values#172752
mattias-arrelid wants to merge 4 commits into
home-assistant:devfrom
mattias-arrelid:homekit-securitysystem-stale-valid-values

Conversation

@mattias-arrelid
Copy link
Copy Markdown
Contributor

@mattias-arrelid mattias-arrelid commented Jun 1, 2026

Proposed change

The HomeKit SecuritySystem accessory freezes its valid SecuritySystemCurrentState / SecuritySystemTargetState values from the alarm entity's supported_features at build time. async_update_state then maps the live state and calls set_value.

ATTR_SUPPORTED_FEATURES is in RELOAD_ON_CHANGE_ATTRS, so the accessory is normally rebuilt when features change. But the reload check in async_update_event_state_callback only compares attributes when both the old and new states are available. A supported_features change arriving across an unavailable/unknown boundary (e.g. reconfiguring the panel so an area exposes different arm modes, which takes the entity through unavailable while the integration reloads) is skipped, leaving the frozen valid-values set stale. The next push then maps to a value that set no longer contains and raises:

ValueError: SecuritySystemCurrentState: value=0 is an invalid value.

Before pushing a value, this now checks it against the entity's current supported_features:

  • value already valid for the characteristic: push it.
  • value not valid yet, but the current features support it: the features grew since build, so reload to rebuild the accessory.
  • value not valid and not supported now either: the entity is reporting a state it never advertised, so reloading would rebuild the same set and loop — log and skip instead.

Timeline that triggers it:

  1. Accessory is built. The area exposes e.g. away only, so the valid current states are {disarmed, triggered, away}; armed_home (0) is not among them.
  2. The panel is reconfigured to also allow arm-home; the entity goes unavailable while the integration reloads.
  3. The entity returns advertising ARM_HOME. The reload guard in async_update_event_state_callback only compares attributes when both old and new states are available, so the unavailable -> available transition is skipped and the accessory keeps its stale valid values.
  4. The entity reports armed_home -> mapped to 0 -> set_value(0) raised the error above.

This addresses the runtime set_value ValueError that propagates out of async_update_state and breaks the state-change dispatch. It does not touch the separate, harmless build-time log from pyhap's override_properties (ikalchev/HAP-python#473, #491), nor does it make an integration that permanently under-declares supported_features work in HomeKit — for that case the accessory now skips the unsupported state instead of raising or reload-looping.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

Reconfiguring the alarm panel so an area exposes different arm modes
changes the entity's supported_features. The HomeKit accessory freezes its
valid security states from supported_features at build time and is only
rebuilt while the entity stays available. A panel reconfiguration typically
takes the entity through unavailable, so the reload guard misses the
change and the valid-values set goes stale; a later state then maps to a
value it no longer allows and raises ValueError.

Before pushing a value, check it against the entity's current
supported_features:

- value already valid for the characteristic: push it.
- value not valid yet, but the current features support it: the features
  grew since build, so reload to rebuild the accessory.
- value not valid and not supported now either: the entity is reporting a
  state it never advertised, so reloading would rebuild the same set and
  loop. Log and skip instead.

Timeline that triggers it:

  1. Accessory is built. The area exposes e.g. away only, so the valid
     current states are {disarmed, triggered, away}; armed_home (0) is not
     among them.
  2. The panel is reconfigured to also allow arm-home; the entity goes
     unavailable while the integration reloads.
  3. The entity returns advertising ARM_HOME. The reload guard in
     async_update_event_state_callback only compares attributes when both
     old and new states are available, so the unavailable -> available
     transition is skipped and the accessory keeps its stale valid values.
  4. The entity reports armed_home -> mapped to 0 -> set_value(0) raised
     "SecuritySystemCurrentState: value=0 is an invalid value."
Copilot AI review requested due to automatic review settings June 1, 2026 14:59
@home-assistant home-assistant Bot added bugfix cla-signed has-tests integration: homekit Top 100 Integration is ranked within the top 100 by usage Top 200 Integration is ranked within the top 200 by usage Top 50 Integration is ranked within the top 50 by usage labels Jun 1, 2026
@home-assistant
Copy link
Copy Markdown
Contributor

home-assistant Bot commented Jun 1, 2026

Hey there @bdraco, mind taking a look at this pull request as it has been labeled with an integration (homekit) you are listed as a code owner for? Thanks!

Code owner commands

Code owners of homekit can trigger bot actions by commenting:

  • @home-assistant close Closes the pull request.
  • @home-assistant mark-draft Mark the pull request as draft.
  • @home-assistant ready-for-review Remove the draft status from the pull request.
  • @home-assistant rename Awesome new title Renames the pull request.
  • @home-assistant reopen Reopen the pull request.
  • @home-assistant unassign homekit Removes the current integration label and assignees on the pull request, add the integration domain after the command.
  • @home-assistant update-branch Update the pull request branch with the base branch.
  • @home-assistant add-label needs-more-information Add a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) on the pull request.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR improves HomeKit SecuritySystem accessory robustness when an alarm control panel’s supported_features changes at runtime by reloading (or skipping) updates when a HomeKit characteristic’s frozen ValidValues set becomes stale.

Changes:

  • Factor out supported-state derivation into _supported_states() and introduce DEFAULT_SUPPORTED_FEATURES.
  • Add _set_or_reload() to avoid ValueError when pushing states not present in the characteristic’s frozen ValidValues, reloading when appropriate.
  • Add tests covering reload vs skip behavior when supported_features grows or a state is unadvertised.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
homeassistant/components/homekit/type_security_systems.py Adds runtime staleness detection for ValidValues and reload/skip logic based on current supported_features.
tests/components/homekit/test_type_security_systems.py Adds test cases to verify reload vs skip behavior for supported feature changes and invalid states.

Comment on lines +192 to +204
if value in char.properties.get(PROP_VALID_VALUES, {}).values():
char.set_value(value)
return True
if value in currently_supported:
self.async_reload()
else:
_LOGGER.debug(
"%s: Skipping unsupported security state %d; not in %s",
self.entity_id,
value,
currently_supported,
)
return False
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

pyhap always stores ValidValues as a dict (the existing code here already calls .values() on it), so this should be safe as written.

acc.run()
await hass.async_block_till_done()

assert 0 not in acc.char_current_state.properties["ValidValues"].values()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

pyhap always stores ValidValues as a dict (the existing code here already calls .values() on it), so this should be safe as written.

Comment on lines +215 to +219
current_supported, target_supported = _supported_states(
new_state.attributes.get(
ATTR_SUPPORTED_FEATURES, DEFAULT_SUPPORTED_FEATURES
)
)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6272c83async_update_state now resolves the current/target HomeKit states first and returns early when neither maps, so _supported_states() is only computed when a value is actually being pushed.

Compute current/target HomeKit states first and bail before deriving the
supported sets when neither maps, so async_update_state allocates nothing on
the (common) path where the state has no HomeKit equivalent.
Copilot AI review requested due to automatic review settings June 1, 2026 15:03
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

Comment on lines 132 to 133
default_current_states = current_char.properties.get("ValidValues")
default_target_services = target_char.properties.get("ValidValues")
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — done in 8f7a734, the build-time lookups now use PROP_VALID_VALUES too so the module no longer mixes the constant with the literal.

Comment on lines +86 to +87
def _supported_states(supported_features: int) -> tuple[list[int], list[int]]:
"""Return the supported (current_states, target_services) for the features.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

target_supported_services (and HASS_TO_HOMEKIT_SERVICES / default_target_services) is the existing naming in this module; the extracted helper just keeps it. Renaming only the helper would make it inconsistent with the surrounding original code, and renaming everything is out of scope for this fix — leaving as-is for consistency.

stay in sync.
"""
current_supported_states = [HK_ALARM_DISARMED, HK_ALARM_TRIGGERED]
target_supported_services = [HK_ALARM_DISARMED]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as above — keeping the existing module naming (*_services) for consistency; out of scope to rename here.

current_supported_states.append(HK_ALARM_NIGHT_ARMED)
target_supported_services.append(HK_ALARM_NIGHT_ARMED)

return current_supported_states, target_supported_services
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as above — keeping the existing module naming (*_services) for consistency; out of scope to rename here.

acc.async_update_state(grew)
await hass.async_block_till_done()

assert mock_reload.called
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, tightened to call_count == 1 in 8f7a734 — a loop is exactly what the skip path guards against, so asserting a single reload is a better regression test.

Use the PROP_VALID_VALUES constant for the build-time lookups too, so the
module no longer mixes the constant with the "ValidValues" literal. Tighten
the reload test to assert exactly one reload, catching an accidental loop.
Copilot AI review requested due to automatic review settings June 1, 2026 15:08
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

Comment on lines +192 to +204
if value in char.properties.get(PROP_VALID_VALUES, {}).values():
char.set_value(value)
return True
if value in currently_supported:
self.async_reload()
else:
_LOGGER.debug(
"%s: Skipping unsupported security state %d; not in %s",
self.entity_id,
value,
currently_supported,
)
return False
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As noted on the earlier thread: pyhap always stores ValidValues as a dict (its own valid_value_or_raise calls .values(), and the build-time code here does too), so the list shape doesn't occur.

Comment on lines +86 to +94
def _supported_states(supported_features: int) -> tuple[list[int], list[int]]:
"""Return the supported (current_states, target_services) for the features.

Mirrors how HomeKit's valid values are derived from the alarm entity's
supported_features, so the accessory build and the runtime staleness check
stay in sync.
"""
current_supported_states = [HK_ALARM_DISARMED, HK_ALARM_TRIGGERED]
target_supported_services = [HK_ALARM_DISARMED]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As on the other naming threads — *_services is the existing module naming; keeping it consistent and out of scope to rename here.

Comment on lines +186 to +191
- value not valid and not supported now either: the entity is reporting
a state it doesn't advertise -> reloading wouldn't help (and would
loop), so log once and skip.

Returns True if the value was pushed, False otherwise.
"""
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed in 4e680ed by dropping "once" from the docstring. The skip is debug-level and rare, so a per-value log-once guard isn't worth the extra state; the wording was just wrong.

Comment on lines +197 to +203
else:
_LOGGER.debug(
"%s: Skipping unsupported security state %d; not in %s",
self.entity_id,
value,
currently_supported,
)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as above — docstring corrected in 4e680ed.

The skip path logs on every unsupported state, not once; drop the
misleading word from the docstring.
@mattias-arrelid mattias-arrelid marked this pull request as ready for review June 1, 2026 15:12
@mattias-arrelid mattias-arrelid requested a review from bdraco as a code owner June 1, 2026 15:12
Copilot AI review requested due to automatic review settings June 1, 2026 15:12
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.

)


def _supported_states(supported_features: int) -> tuple[list[int], list[int]]:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As on the earlier naming threads — *_services is the existing module naming; keeping it consistent, renaming is out of scope here.

stay in sync.
"""
current_supported_states = [HK_ALARM_DISARMED, HK_ALARM_TRIGGERED]
target_supported_services = [HK_ALARM_DISARMED]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As on the earlier naming threads — *_services is the existing module naming; keeping it consistent, renaming is out of scope here.


if supported_features & AlarmControlPanelEntityFeature.ARM_HOME:
current_supported_states.append(HK_ALARM_STAY_ARMED)
target_supported_services.append(HK_ALARM_STAY_ARMED)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As on the earlier naming threads — *_services is the existing module naming; keeping it consistent, renaming is out of scope here.

| AlarmControlPanelEntityFeature.ARM_VACATION
):
current_supported_states.append(HK_ALARM_AWAY_ARMED)
target_supported_services.append(HK_ALARM_AWAY_ARMED)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As on the earlier naming threads — *_services is the existing module naming; keeping it consistent, renaming is out of scope here.

Comment on lines +109 to +111
target_supported_services.append(HK_ALARM_NIGHT_ARMED)

return current_supported_states, target_supported_services
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As on the earlier naming threads — *_services is the existing module naming; keeping it consistent, renaming is out of scope here.

entity_id = "alarm_control_panel.test"

attrs = {"supported_features": AlarmControlPanelEntityFeature.ARM_HOME}
hass.states.async_set(entity_id, AlarmControlPanelState.DISARMED, attributes=attrs)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ruff check (incl. E501) passes on this file — the line is 87 chars, under the 88 limit, so no wrapping needed.

acc.run()
await hass.async_block_till_done()

assert 0 not in acc.char_current_state.properties["ValidValues"].values()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The test file already references "ValidValues" as a literal in several existing assertions; keeping these consistent with that. The constant matters in the production lookup (which can drift with pyhap); asserting on the well-known HAP property name by literal in tests is fine.


assert not mock_reload.called
# State left unchanged; no ValueError raised.
assert 0 not in acc.char_current_state.properties["ValidValues"].values()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as above — consistent with the existing "ValidValues" literals already used in this test file.

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

Labels

bugfix cla-signed has-tests integration: homekit Quality Scale: No score Top 50 Integration is ranked within the top 50 by usage Top 100 Integration is ranked within the top 100 by usage Top 200 Integration is ranked within the top 200 by usage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Error messages about SIA alarm states with Ajax alarm connected

3 participants