Skip to content

Commit 043322b

Browse files
authored
Merge pull request #4287 from bramstroker/fix/cache-invalidatio
Fix no stale profiles are cache after library reload
2 parents 78d8e3f + 443e0cd commit 043322b

3 files changed

Lines changed: 70 additions & 2 deletions

File tree

custom_components/powercalc/helpers.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44
import os.path
55
import re
6-
from typing import Any, NamedTuple, TypeVar
6+
from typing import Any, NamedTuple, TypeVar, cast
77
import uuid
88

99
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
@@ -112,9 +112,18 @@ async def wrapper(*args: Any, **kwargs: Any) -> R: # noqa: ANN401
112112
cache[cache_key] = result
113113
return result
114114

115+
cast(Any, wrapper).cache_clear = cache.clear
115116
return wrapper
116117

117118

119+
def clear_async_cache(func: Callable[..., Coroutine[Any, Any, Any]]) -> None:
120+
"""Clear a function wrapped with async_cache."""
121+
target = getattr(func, "__func__", func)
122+
cache_clear = getattr(target, "cache_clear", None)
123+
if callable(cache_clear):
124+
cache_clear()
125+
126+
118127
def collect_placeholders(data: list | str | dict[str, Any]) -> set[str]:
119128
found: set[str] = set()
120129
if isinstance(data, dict):

custom_components/powercalc/power_profile/loader/remote.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from homeassistant.loader import async_get_integration
1818

1919
from custom_components.powercalc.const import API_URL, BUILT_IN_LIBRARY_DIR, DOMAIN
20-
from custom_components.powercalc.helpers import async_cache
20+
from custom_components.powercalc.helpers import async_cache, clear_async_cache
2121
from custom_components.powercalc.power_profile.error import LibraryLoadingError, ProfileDownloadError
2222
from custom_components.powercalc.power_profile.loader.protocol import Loader
2323
from custom_components.powercalc.power_profile.power_profile import DeviceType, DiscoveryBy
@@ -66,6 +66,7 @@ async def initialize(self) -> None:
6666
integration = await async_get_integration(self.hass, DOMAIN)
6767
powercalc_version = AwesomeVersion(str(integration.version))
6868

69+
self._clear_caches()
6970
self.library_contents = await self.load_library_json()
7071
self.profile_hashes = await self.hass.async_add_executor_job(self._load_profile_hashes)
7172

@@ -123,6 +124,15 @@ async def initialize(self) -> None:
123124
self.manufacturer_models[manufacturer_name] = kept_models
124125
self.model_lookup[manufacturer_name] = lookup
125126

127+
def _clear_caches(self) -> None:
128+
"""Clear cached lookups backed by mutable library state."""
129+
clear_async_cache(self.get_manufacturer_listing)
130+
clear_async_cache(self.find_manufacturers)
131+
clear_async_cache(self.get_model_listing)
132+
clear_async_cache(self.find_model)
133+
clear_async_cache(self.find_model_migration)
134+
clear_async_cache(self.load_model)
135+
126136
async def load_library_json(self) -> dict[str, Any]:
127137
"""Load library.json file"""
128138

tests/power_profile/loader/test_remote.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,55 @@ def load_library_json() -> dict:
637637
assert sorted(actual_models) == expected_models
638638

639639

640+
async def test_initialize_clears_cached_library_lookups(hass: HomeAssistant) -> None:
641+
first_library = {
642+
"manufacturers": [
643+
{
644+
"name": "Test",
645+
"dir_name": "test",
646+
"models": [
647+
{
648+
"id": "old_model",
649+
"name": "Old Model",
650+
"device_type": "light",
651+
},
652+
],
653+
},
654+
],
655+
}
656+
second_library = {
657+
"manufacturers": [
658+
{
659+
"name": "Test",
660+
"dir_name": "test",
661+
"models": [
662+
{
663+
"id": "new_model",
664+
"name": "New Model",
665+
"device_type": "light",
666+
},
667+
],
668+
},
669+
],
670+
}
671+
672+
with patch(
673+
"custom_components.powercalc.power_profile.loader.remote.RemoteLoader.load_library_json",
674+
new_callable=AsyncMock,
675+
side_effect=[first_library, second_library],
676+
):
677+
loader = RemoteLoader(hass)
678+
679+
await loader.initialize()
680+
assert await loader.find_model("test", {"old_model"}) == ["old_model"]
681+
assert await loader.get_model_listing("test", {DeviceType.LIGHT}) == {("old_model", "Old Model")}
682+
683+
await loader.initialize()
684+
assert await loader.find_model("test", {"old_model"}) == []
685+
assert await loader.find_model("test", {"new_model"}) == ["new_model"]
686+
assert await loader.get_model_listing("test", {DeviceType.LIGHT}) == {("new_model", "New Model")}
687+
688+
640689
def clear_storage_dir(storage_path: str) -> None:
641690
if not os.path.exists(storage_path):
642691
return

0 commit comments

Comments
 (0)