Skip to content

Refactor: Bind Keymaster entities to per-lock coordinators#693

Open
tykeal wants to merge 1 commit into
FutureTense:mainfrom
tykeal:refactor/681-bind-entities-per-lock
Open

Refactor: Bind Keymaster entities to per-lock coordinators#693
tykeal wants to merge 1 commit into
FutureTense:mainfrom
tykeal:refactor/681-bind-entities-per-lock

Conversation

@tykeal

@tykeal tykeal commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Phase B of issue #670: move entity listeners off the global manager onto the per-entry KeymasterLockCoordinator introduced in #680. Entity registry stability (unique IDs, names, device identifiers) and the persisted storage format are unchanged.

Changes

  • Entities (entity.py + all 9 platforms): KeymasterEntity is now CoordinatorEntity[KeymasterLockCoordinator]; each platform constructs entities with manager.async_get_lock_coordinator(entry_id) while setup-time lock lookups use the manager. _kmlock, unique IDs, names, and device info are unchanged.
  • Conservative per-lock fan-out (coordinator.py): the manager's deferred global-notification flush now also pushes fresh lock data to all per-lock coordinators (via shared _flush_global_leg + _push_lock_coordinator_update helpers), so mutation and periodic-refresh notifications reach the rebound entities. Per-dirty-lock scoping is deferred to Refactor: Pipeline Keymaster refreshes through dirty lock scopes #682/Fix: Scope quick and debounced refresh to Keymaster lock entries #684.
  • Manager refresh keepalive: HA only reschedules a coordinator's interval poll while it has listeners. Since entities no longer listen on the manager, a no-op keepalive listener keeps the manager's 60s refresh loop alive while lock coordinators exist (removed on shutdown).
  • Notification flush handles: the scoped dual-target bridge and the all-lock global flush now symmetrically absorb each other's pending handle, eliminating a same-turn swallow/double-fire. switch.py optimistic updates route through the new KeymasterLockCoordinator.async_schedule_notification().

Testing

  • New/updated tests: entities listen on the per-lock coordinator (not the manager) after setup; unique IDs remain {entry_id}_{slugify(property)}; conservative fan-out and refresh-completion reach per-lock listeners; both flush orderings notify every coordinator exactly once; keepalive install/remove.
  • ruff check, ruff format, mypy, targeted platform suites, and the full suite pass; 100% patch coverage on new lines.

Closes #681
Part of #670

@codecov-commenter

codecov-commenter commented Jul 13, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.28%. Comparing base (cdb4922) to head (7ef4063).
⚠️ Report is 201 commits behind head on main.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #693      +/-   ##
==========================================
+ Coverage   84.14%   93.28%   +9.14%     
==========================================
  Files          10       42      +32     
  Lines         801     5258    +4457     
  Branches        0       30      +30     
==========================================
+ Hits          674     4905    +4231     
- Misses        127      353     +226     
Flag Coverage Δ
python 93.17% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This comment was marked as outdated.

@tykeal
tykeal force-pushed the refactor/681-bind-entities-per-lock branch from cf46de8 to bdc6135 Compare July 13, 2026 13:05
@tykeal
tykeal requested a review from Copilot July 13, 2026 13:08

This comment was marked as outdated.

Bind every Keymaster entity platform to its per-entry KeymasterLockCoordinator instead of the global manager, so entity listeners live on the per-lock coordinator. The manager remains the storage owner and sole periodic refresher; a keepalive listener keeps its 60s refresh loop scheduled now that entities no longer listen on it. Notifications reach rebound entities through a conservative fan-out from the manager's deferred flush to all per-lock coordinators, while the scoped dual-target bridge is retained for optimistic updates and future dirty-lock scoping. Entity unique IDs, names, device identifiers, and the persisted storage format are unchanged.

Closes FutureTense#681

Part of FutureTense#670

Assisted-by: GitHub Copilot CLI 1.0.68 (Claude Opus 4.8, model claude-opus-4.8)
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
@tykeal
tykeal force-pushed the refactor/681-bind-entities-per-lock branch from bdc6135 to 7ef4063 Compare July 13, 2026 13:16
@tykeal
tykeal requested a review from Copilot July 13, 2026 13:34

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.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

@secondof9

This comment was marked as outdated.

@tykeal

tykeal commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator Author

Both flagged points reference code that isn't in the current PR head (7ef4063) — they look like a pre-refactor revision. Clarifying against the live source:

KeymasterLockCoordinator.__init__ (the "critical" item). It already subclasses DataUpdateCoordinator and calls the correct super-init — unchanged since #680:

class KeymasterLockCoordinator(DataUpdateCoordinator[KeymasterLock | None]):
    def __init__(self, manager: KeymasterCoordinator, config_entry_id: str) -> None:
        self.manager = manager
        self.config_entry_id = config_entry_id
        super().__init__(
            manager.hass,
            _LOGGER,
            name=f"{DOMAIN}_{config_entry_id}",
            update_interval=None,
            config_entry=manager.hass.config_entries.async_get_entry(config_entry_id),
            always_update=False,
        )
        self.data = manager.sync_get_lock_by_config_entry_id(config_entry_id)

There is no __init__(self, hass) / super().__init__(self, hass), and it does not inherit from KeymasterCoordinator. It must remain a real DataUpdateCoordinator: entities are CoordinatorEntity[KeymasterLockCoordinator], so async_add_listener, async_set_updated_data, listener bookkeeping, etc. have to come from the coordinator framework. Converting it to a plain value proxy would break CoordinatorEntity binding.

Double async_set_updated_data (the "warning"). The if manager_healthy: lock_coordinator.async_set_updated_data(lock) block was removed in the flush refactor. The current _flush_pending_keymaster_notifications pushes once per target:

        data_update, preserve_failed_refresh = self._flush_global_leg()
        targets = list(self._lock_coordinators) if all_fanout else list(entry_ids)
        for entry_id in targets:
            self._push_lock_coordinator_update(
                entry_id, data_update=data_update, preserve_failed_refresh=preserve_failed_refresh
            )

_push_lock_coordinator_update is the only per-coordinator notify path, so there is no duplicate async_set_updated_data. If you're seeing something different against 7ef4063, point me at the lines and I'll take another look.

@firstof9 firstof9 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.

Looks good.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: Bind Keymaster entities to per-lock coordinators

5 participants