Skip to content

Refactor: Pipeline Keymaster refreshes through dirty lock scopes #682

Description

@tykeal

Part of #670

Summary

Convert refresh behavior to manager-owned dirty-lock refreshes. The manager owns one
60-second loop, refreshes locks sequentially with one provider poll in flight at a time, and
notifications target dirty per-lock coordinators. After task-7 proves no entities remain bound
to the manager, this task removes the bridge's global-notify leg.

Scope / changes

  • Refactor _async_update_data() at coordinator.py:1950-1979 into explicit manager methods
    such as async_refresh_lock(entry_id) -> set[str] and async_refresh_all_locks() -> set[str].
  • Preserve data updates performed by _update_lock_data() and parent sync currently invoked at
    coordinator.py:1962-1967, save at coordinator.py:1974, and quick-refresh scheduling at
    coordinator.py:1977.
  • Make the manager's single 60-second loop refresh locks sequentially: iterate entry IDs and
    await each provider poll before starting the next. Do not use gather() or per-lock
    update_interval polling; per-lock coordinators were created with update_interval=None in
    task-6.
  • Convert the bridge into per-lock-only deferred notification once no entities remain on the
    manager: pending entry-id set, call_soon flush, skip IDs missing from self.kmlocks, and
    call only lock_coordinator.async_set_updated_data(...).
  • Keep self.data = dict(self.kmlocks) current after every refresh/mutation for
    config_flow.py:240-245 and __init__.py:313-319.
  • Replace Phase A global-notification fallback with per-lock notification scheduling where dirty
    IDs are known, using conservative “all entry IDs” fallback only for paths not yet precisely
    scoped and only through per-lock targets.
  • Update refresh request paths from coordinator.py:1382, coordinator.py:1439,
    coordinator.py:1466, coordinator.py:1730, coordinator.py:1778, and
    providers/zha.py:481-487 to use the manager pipeline.

Plan reference

§5.3 Phase B, §5.4 refresh paths, §5.5, §7.2,
§8 PR 4; decision 3; 2026-07-07 refresh-loop, dual-target, and guard-regression decisions.

Dependencies

Depends on: #681

Acceptance criteria

  • Manager refresh completion no longer synchronously notifies entity listeners.
  • The manager owns exactly one 60-second loop and refreshes locks sequentially with one provider
    poll in flight at a time; per-lock coordinators do not independently poll.
  • Dirty lock IDs are returned and scheduled for deferred per-lock notification.
  • The dual-target bridge's global manager notification leg is removed only after tests prove no
    entities remain manager-bound, so no entity loses notifications during the transition.
  • Re-run the deterministic guard regression from plan §7.2 after per-lock binding and prove the
    fan-out is both deferred and scoped to the dirty entry set, not global.
  • Targeted validation: pytest tests/test_coordinator_lifecycle.py tests/test_coordinator.py tests/test_entity.py plus per-lock fan-out and refreshed guard tests added for this task.
  • Broader validation: ruff check custom_components/ tests/, mypy custom_components/keymaster/.

Risk / notes

The main risk is breaking setup/add/update/delete/reset refresh semantics.
Keep any fallback dirty set conservative until task-9 and task-10 narrow specific flows, but
never reintroduce manager/global listener fan-out after this task removes that bridge leg.

Size

L

Persistence note (2026-07-07, per @raman325)

Do not build custom disk-write tracking. _async_save_data() (coordinator.py:260-289) already uses HA's Store (coordinator.py:133) and already dedupes via config == self._prev_kmlocks_dict (coordinator.py:284-286). It currently calls self._store.async_save() immediately on each changed refresh (coordinator.py:288); optionally switch to Store.async_delay_save (HA's built-in debounced save) to coalesce disk writes under bursts — a small optional refinement, not a requirement. The "dirty lock" sets here and in #684 are notification scoping (which per-lock coordinator to notify), not a reimplementation of HA state dedup (#677) or of the existing save dedup.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions