fix(init): register lovelace strategy resource during async_setup_entry - #702
fix(init): register lovelace strategy resource during async_setup_entry#702firstof9 wants to merge 5 commits into
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #702 +/- ##
==========================================
+ Coverage 84.14% 93.96% +9.82%
==========================================
Files 10 42 +32
Lines 801 5405 +4604
Branches 0 30 +30
==========================================
+ Hits 674 5079 +4405
- Misses 127 326 +199
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Line-by-line Code Review: FutureTense/keymaster PR #702Fix: Register lovelace strategy resource during async_setup_entry ✅ Looks Good
|
tykeal
left a comment
There was a problem hiding this comment.
Walkthrough
Adds await async_register_strategy_resource(hass) to async_setup_entry so a config-entry reload re-registers the Lovelace strategy resource that async_unload_entry tears down when the last entry unloads (bug 5 of #699). Also resets hass_data["resources"] to False in async_cleanup_strategy_resource, and adds a unit test in tests/test_init.py.
Changes
custom_components/keymaster/__init__.py— callasync_register_strategy_resource(hass)inasync_setup_entry, afterasync_setup_servicesand before coordinator creation.custom_components/keymaster/resources.py— clear theresourcesownership flag afterasync_delete_iteminasync_cleanup_strategy_resource.tests/test_init.py— newtest_async_setup_entry_registers_strategy_resource.
Verification performed
The production change is correct. I reproduced the #699 bug 5 scenario locally against this branch with a fake ResourceStorageCollection: with the branch reverted to upstream/main, hass.config_entries.async_reload(entry.entry_id) leaves zero resources with url == STRATEGY_PATH; with this branch applied the resource survives the reload.
Idempotency was also checked and is not a problem: async_register_strategy_resource dedupes on CONF_URL == STRATEGY_PATH before creating, and async_setup always runs (and registers) before the per-entry asyncio.gather in homeassistant/setup.py, so by the time entries set up concurrently the URL check already short-circuits. I ran a two-entry concurrent async_setup_component with resources.loaded = False and got exactly one resource, no duplicate storage write. No ConfigEntryNotReady concern either — missing/YAML-mode Lovelace is warn-and-return, which is the right non-fatal behaviour for this.
One blocking issue with the test, and one pre-existing flag-lifecycle problem the resources.py change brushes against. Both inline.
Conflict / landing-order analysis (#695, #698)
No action needed, but for the record:
- #698 is already in: this branch's
tests/test_init.pypre-image blob iscfe9f0c5, identical toupstream/main, so the bare*keyword-only markers from #698 are present. No rebase required. - #695 (
refactor/682-dirty-lock-refresh-pipeline) touchestests/test_init.pyat lines ~29 (newKeymasterLockimport) and ~154-200 (test_unload_entry_preserves_pending_global_notificationrename + coordinator API changes). This PR appends at EOF (line 449+) and adds no imports. Non-overlapping hunks — git merges these cleanly in either order. No conflict surface. PLR0917: the new test takes 3 positional params, under the threshold of 5.ruff check .(0.15.21 locally, same ruleset) passes clean on this branch. The replacement test suggested inline takes 4 — still under. If a 5th fixture is ever added, insert a bare*per the #698 convention.
Recommended landing order: either. If #702 lands first, #695 needs no rebase.
| # _LOGGER.debug(f"[init async_setup_entry] updated config_entry.data: {config_entry.data}") | ||
|
|
||
| await async_setup_services(hass) | ||
| await async_register_strategy_resource(hass) |
There was a problem hiding this comment.
Placement is fine. It runs before the coordinator block that can raise ConfigEntryNotReady, so on a retrying entry the resource is registered eagerly; that is harmless given the URL dedupe, and it means the strategy works even while a lock is unreachable.
One behavioural note for multi-entry users: the Lovelace integration not available and YAML-mode warnings in async_register_strategy_resource now emit once per config entry per setup/reload instead of once per HA start. Non-blocking, but worth downgrading the repeat to debug if it generates support noise.
| return | ||
|
|
||
| await resources.async_delete_item(resource_id) | ||
| hass_data["resources"] = False |
There was a problem hiding this comment.
[SUGGESTION] Correct as far as it goes, but the flag it resets can only ever be True within a single HA run, which makes this line — and the whole cleanup path — dead after the first restart.
async_register_strategy_resource sets hass.data[DOMAIN]["resources"] = True only on the async_create_item branch. The already_registered early-return at line 47 does not set it. After an HA restart the resource is already in .storage/lovelace_resources, so registration short-circuits, the flag stays False, and async_cleanup_strategy_resource bails at the if not hass_data.get("resources") guard. Net effect: removing the last keymaster entry after a restart leaves an orphaned resource pointing at /keymaster_files/keymaster.js, which 404s once the static path is gone.
Since this PR is now making the register/cleanup handshake stateful across reloads, consider claiming ownership on the already-registered branch too (in async_register_strategy_resource, outside this hunk):
if already_registered:
_LOGGER.debug("Strategy module already registered")
hass.data[DOMAIN]["resources"] = True
returnCaveat worth a maintainer decision: that would also make keymaster delete a resource a user added by hand with the same URL. If that trade-off is unacceptable, the alternative is to persist the ownership flag rather than keep it in hass.data. Either way, the current behaviour is inconsistent and this PR is the natural place to note it. Not blocking on its own.
|
Thanks for the thorough review! I've verified the findings:
PR is ready to merge. |
|
main has moved; the only conflict is one hunk in <<<<<<< upstream/main
from custom_components.keymaster.lock import KeymasterLock
=======
from homeassistant.components.lovelace.const import DOMAIN as LOVELACE_DOMAIN
from homeassistant.components.lovelace.resources import ResourceStorageCollection
>>>>>>> upstream/pr/702 |
7f6fb54 to
3b03ad2
Compare
tykeal
left a comment
There was a problem hiding this comment.
Walkthrough
Adds await async_register_strategy_resource(hass) to async_setup_entry so a config-entry reload re-registers the Lovelace strategy resource that async_unload_entry tears down when the last entry unloads, and resets the resources tracking flag in async_cleanup_strategy_resource. Replaces the previous mock-based test with a reload-based test driving a faked ResourceStorageCollection.
Changes
custom_components/keymaster/__init__.py: registers the strategy resource inasync_setup_entry(line 159), afterasync_setup_services, before the coordinator block.custom_components/keymaster/resources.py:async_cleanup_strategy_resourcesetshass_data["resources"] = Falseafterasync_delete_item(line 98).tests/test_init.py: adds thefake_lovelace_resourcesfixture andtest_reload_preserves_strategy_resource; removestest_async_setup_entry_registers_strategy_resource.
Verification
Verified in an isolated worktree at head 3b03ad211e0eaec3f67f06e3c27aea956ca596b4, merge base 83a572ea (post-#695), Python 3.14.
pytest tests/: 1054 passed, 3 skipped, 1 deselected, 0 failures.ruff check custom_components/ tests/: All checks passed.ruff format --check custom_components/ tests/: 80 files already formatted.mypy custom_components/keymaster/: Success, no issues found in 35 source files.- Patch coverage: 100% of changed lines.
resources.py100%;__init__.pymissing lines are132, 393, 410-422, 426-429, none of which are touched by this PR. d4ffb661("satisfy PLR0917") removes three genuinely unused fixture parameters from the since-deleted old test. Nonoqaand no ignore added; lint is clean on merit, not worked around.- Rebase onto #695 resolved correctly in
tests/test_init.py. The base import block is preserved verbatim; the PR adds onlyMagicMock,STRATEGY_PATH,LOVELACE_DOMAINandResourceStorageCollection. No import was dropped.
Mutation test. Deleting await async_register_strategy_resource(hass) from async_setup_entry makes the new test fail at the post-reload assertion:
tests/test_init.py:577: in test_reload_preserves_strategy_resource
> assert [i for i in fake_lovelace_resources if i["url"] == STRATEGY_PATH]
E assert []
FAILED tests/test_init.py::test_reload_preserves_strategy_resource - assert []
This confirms test_reload_preserves_strategy_resource is a genuine regression test for the change, unlike the mock-based test it replaces.
Review comments
Both items below are non-blocking. Neither gates this approval.
[SUGGESTION, non-blocking] custom_components/keymaster/resources.py lines 33-39 and 52-61
Left in the review body rather than inline, because these lines are outside this PR's diff hunks and would not anchor reliably.
Registration now runs once per config entry per setup/reload instead of once per HA start. As a result the Lovelace integration not available warning at lines 33-39 fires once per entry per reload for multi-entry users, and additionally once per setup retry, since the new call site at __init__.py:159 sits above the coordinator block that can raise ConfigEntryNotReady. Gating the repeat keeps the first occurrence at warning and demotes the rest:
resources = get_lovelace_resources(hass)
if not resources:
domain_data = hass.data.setdefault(DOMAIN, {})
log = _LOGGER.debug if domain_data.get("resources_warned") else _LOGGER.warning
domain_data["resources_warned"] = True
log(
"Lovelace integration not available; skipping strategy module "
"registration. The keymaster dashboard strategy will not work "
"until Lovelace is loaded and Home Assistant is restarted."
)
returnThe same applies to the YAML-mode warning at lines 52-61.
Out of scope
The hass.data[DOMAIN]["resources"] flag that line 98 resets is set True only on the async_create_item branch (line 67); the already_registered early return (lines 48-50) does not set it. After an HA restart the resource is already present in .storage/lovelace_resources, so registration short-circuits, the flag stays False, and async_cleanup_strategy_resource bails at its guard (line 85) — async_delete_item is never awaited and the resource is never removed on the last unload. Confirmed with an executable probe, not by reading alone.
This is pre-existing, not a regression from this PR: lines 67 and 85 are byte-identical at merge base 83a572ea. Filed separately as #706 rather than expanding scope here. Relates to #699.
Verdict
APPROVE. The fix is correct, minimally scoped, genuinely regression-tested, and clean across lint, format, typing and changed-line coverage.
| return | ||
|
|
||
| await resources.async_delete_item(resource_id) | ||
| hass_data["resources"] = False |
There was a problem hiding this comment.
[SUGGESTION, non-blocking] This reset is executed by the suite (hence 100% line coverage) but is not asserted by it: deleting this line leaves all 1054 tests green, so the mutation survives. Consider pinning the flag lifecycle in test_reload_preserves_strategy_resource, asserting hass.data[DOMAIN]["resources"] is True after the reload and is False immediately after an explicit unload of the last entry.
Note also that this flag can only ever be True within the HA run that originally created the resource — see the out-of-scope note in the review body and #706.
…ce is missing or in YAML mode
|
Thanks @tykeal! Applied the non-blocking suggestion in
|
tykeal
left a comment
There was a problem hiding this comment.
LGTM — approved.
Re-verified at 67fd5f8: dedup state is scoped to hass.data[DOMAIN] and cleared with the domain dict in delete_coordinator; both new tests fail when the dedup is reverted, and the reload test still fails when the async_setup_entry registration is removed. 1056 passed / 3 skipped, ruff + format + mypy clean, 100% coverage of changed lines, merges cleanly, no interaction with #706. The queued CI jobs appear to be repo-wide runner starvation rather than anything in this PR.
|
Waiting for github to get it's shit together so tests can run. |
|
It's definitely been a day |
Summary of Changes
async_register_strategy_resource(hass)is called duringasync_setup_entryso reloading an integration config entry re-registers/preserves the Lovelace strategy resource.tests/test_init.py.Ref #699