Initil after Upgrade

This commit is contained in:
2026-06-15 10:53:52 -04:00
parent 2fe9bf0dd6
commit 887feaa50a
143 changed files with 2288 additions and 881 deletions
@@ -11,7 +11,8 @@ class CompositeLoader(Loader):
self.loaders = loaders
async def initialize(self) -> None:
[await loader.initialize() for loader in self.loaders] # type: ignore[func-returns-value]
for loader in self.loaders:
await loader.initialize()
async def get_manufacturer_listing(
self,
@@ -20,7 +21,11 @@ class CompositeLoader(Loader):
) -> set[tuple[str, str]]:
"""Get listing of available manufacturers."""
return {manufacturer for loader in self.loaders for manufacturer in await loader.get_manufacturer_listing(device_types, discovery_by)}
return {
manufacturer
for loader in self.loaders
for manufacturer in await loader.get_manufacturer_listing(device_types, discovery_by)
}
async def find_manufacturers(self, search: str) -> set[str]:
"""Check if a manufacturer is available. Also must check aliases."""
@@ -42,7 +47,11 @@ class CompositeLoader(Loader):
) -> set[tuple[str, str]]:
"""Get listing of available models and display names for a given manufacturer."""
return {model for loader in self.loaders for model in await loader.get_model_listing(manufacturer, device_types, discovery_by)}
return {
model
for loader in self.loaders
for model in await loader.get_model_listing(manufacturer, device_types, discovery_by)
}
async def load_model(self, manufacturer: str, model: str) -> tuple[dict, str] | None:
for loader in self.loaders:
@@ -1,3 +1,4 @@
from functools import partial
import json
import logging
import os
@@ -101,12 +102,9 @@ class LocalLoader(Loader):
_model = model.lower()
if self._is_custom_directory:
model_path = os.path.join(self._data_directory)
model_json_path = os.path.join(model_path, "model.json")
if not os.path.exists(model_json_path):
raise LibraryLoadingError(f"model.json not found for manufacturer {_manufacturer} " + f"and model {_model} in path {model_json_path}")
model_json = await self._hass.async_add_executor_job(self._load_json, model_json_path)
model_path, model_json = await self._hass.async_add_executor_job(
partial(self._load_custom_model, _manufacturer, _model),
)
return model_json, model_path
lib_models = self._manufacturer_model_listing.get(_manufacturer)
@@ -205,6 +203,17 @@ class LocalLoader(Loader):
self._manufacturer_model_listing[manufacturer].update({search_key: profile})
def _load_custom_model(self, manufacturer: str, model: str) -> tuple[str, dict[str, Any]]:
"""Load model.json from a directly configured custom model directory."""
model_path = os.path.join(self._data_directory)
model_json_path = os.path.join(model_path, "model.json")
if not os.path.exists(model_json_path):
raise LibraryLoadingError(
f"model.json not found for manufacturer {manufacturer} and model {model} in path {model_json_path}",
)
return model_path, self._load_json(model_json_path)
def _load_json(self, model_json_path: str) -> dict[str, Any]:
"""Load model.json file for a given model."""
with open(model_json_path) as file:
@@ -64,7 +64,7 @@ class RemoteLoader(Loader):
"""Initialize the loader."""
integration = await async_get_integration(self.hass, DOMAIN)
powercalc_version = AwesomeVersion(integration.version)
powercalc_version = AwesomeVersion(str(integration.version))
self.library_contents = await self.load_library_json()
self.profile_hashes = await self.hass.async_add_executor_job(self._load_profile_hashes)
@@ -138,7 +138,7 @@ class RemoteLoader(Loader):
async def _download_remote_library_json() -> dict[str, Any] | None:
"""
Download library.json from Github.
If download is successful, save it to local storage to use as fallback in case of internet connection issues.
On success, save it to local storage as a fallback for internet connection issues.
"""
_LOGGER.debug("Loading library.json from github")
@@ -182,7 +182,10 @@ class RemoteLoader(Loader):
return {
(manufacturer["dir_name"], manufacturer["full_name"])
for manufacturer in self.library_contents.get("manufacturers", [])
if any(self._model_matches_filters(model, device_types, discovery_by) for model in manufacturer.get("models", []))
if any(
self._model_matches_filters(model, device_types, discovery_by)
for model in manufacturer.get("models", [])
)
}
@async_cache
@@ -225,7 +228,12 @@ class RemoteLoader(Loader):
async def find_model(self, manufacturer: str, search: set[str]) -> list[str]:
"""Find matching model IDs in the library."""
models = self.model_lookup.get(manufacturer, {})
return [model["id"] for phrase in search if (phrase_lower := phrase.lower()) in models for model in models[phrase_lower]]
return [
model["id"]
for phrase in search
if (phrase_lower := phrase.lower()) in models
for model in models[phrase_lower]
]
@async_cache
async def find_model_migration(self, manufacturer: str, model: str) -> str | None:
@@ -274,12 +282,19 @@ class RemoteLoader(Loader):
raise LibraryLoadingError("Model not found in library: %s/%s", manufacturer, model)
return model_info
async def _needs_update(self, model_info: LibraryModel, manufacturer: str, model: str, model_path: str, force_update: bool) -> bool:
async def _needs_update(
self,
model_info: LibraryModel,
manufacturer: str,
model: str,
model_path: str,
force_update: bool,
) -> bool:
"""Check if the model needs to be updated."""
if force_update:
return True
path_exists = os.path.exists(model_path)
path_exists = await self.hass.async_add_executor_job(os.path.exists, model_path)
if not path_exists:
return True
@@ -287,7 +302,13 @@ class RemoteLoader(Loader):
new_hash = model_info.get("hash")
return existing_hash != new_hash
async def _download_profile_with_retry(self, manufacturer: str, model: str, storage_path: str, model_path: str) -> None:
async def _download_profile_with_retry(
self,
manufacturer: str,
model: str,
storage_path: str,
model_path: str,
) -> None:
"""Attempt to download the profile, with retry logic and error handling."""
try:
model_info = self._get_library_model(manufacturer, model)
@@ -297,12 +318,22 @@ class RemoteLoader(Loader):
self.profile_hashes[f"{manufacturer}/{model}"] = model_hash
await self.hass.async_add_executor_job(self._write_profile_hashes, self.profile_hashes)
except ProfileDownloadError as e:
if not os.path.exists(model_path):
if os.path.exists(storage_path):
path_exists, storage_path_exists = await self.hass.async_add_executor_job(
self._profile_paths_exist,
model_path,
storage_path,
)
if not path_exists:
if storage_path_exists:
await self.hass.async_add_executor_job(shutil.rmtree, storage_path) # pragma: no cover
raise e
_LOGGER.debug("Failed to download profile, falling back to local profile")
@staticmethod
def _profile_paths_exist(model_path: str, storage_path: str) -> tuple[bool, bool]:
"""Check profile paths from the executor."""
return os.path.exists(model_path), os.path.exists(storage_path)
async def _load_model_json(self, model_path: str) -> dict:
"""Load the JSON data from the model file."""
@@ -330,7 +361,10 @@ class RemoteLoader(Loader):
"""Retrieve the storage path for a given manufacturer and model."""
return str(self.hass.config.path(STORAGE_DIR, BUILT_IN_LIBRARY_DIR, manufacturer, model))
async def download_with_retry(self, callback: Callable[[], Coroutine[Any, Any, None | dict[str, Any]]]) -> None | dict[str, Any]:
async def download_with_retry(
self,
callback: Callable[[], Coroutine[Any, Any, None | dict[str, Any]]],
) -> None | dict[str, Any]:
"""Download a file from a remote endpoint with retries"""
max_retries = 3
retry_count = 0
@@ -342,7 +376,9 @@ class RemoteLoader(Loader):
_LOGGER.debug(e)
retry_count += 1
if retry_count == max_retries:
raise ProfileDownloadError(f"Failed to download even after {max_retries} retries, falling back to local copy") from e
raise ProfileDownloadError(
f"Failed to download even after {max_retries} retries, falling back to local copy",
) from e
await asyncio.sleep(self.retry_timeout)
_LOGGER.warning("Failed to download, retrying... (Attempt %d of %d)", retry_count + 1, max_retries)
@@ -396,7 +432,7 @@ class RemoteLoader(Loader):
return {}
with open(path) as f:
return json.load(f) # type: ignore
return json.load(f) # type: ignore[no-any-return]
def _write_profile_hashes(self, hashes: dict[str, str]) -> None:
"""Write profile hashes to local storage"""