Initil after Upgrade
This commit is contained in:
1 parent
2fe9bf0dd6
commit
887feaa50a
143 files changed
+2288
-881
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -141,7 +141,13 @@ class ProfileLibrary:
|
||||
raise LibraryError(f"Model {model_info.manufacturer} {model_info.model} not found")
|
||||
model_info = next(iter(models))
|
||||
|
||||
profile = await self.create_power_profile(model_info, source_entity, custom_directory, variables, process_variables)
|
||||
profile = await self.create_power_profile(
|
||||
model_info,
|
||||
source_entity,
|
||||
custom_directory,
|
||||
variables,
|
||||
process_variables,
|
||||
)
|
||||
|
||||
if sub_profile:
|
||||
await profile.select_sub_profile(sub_profile)
|
||||
@@ -163,7 +169,11 @@ class ProfileLibrary:
|
||||
|
||||
if linked_profile := json_data.get("linked_profile", json_data.get("linked_lut")):
|
||||
linked_manufacturer, linked_model = linked_profile.split("/")
|
||||
linked_json_data, directory = await self._load_model_data(linked_manufacturer, linked_model, custom_directory)
|
||||
linked_json_data, directory = await self._load_model_data(
|
||||
linked_manufacturer,
|
||||
linked_model,
|
||||
custom_directory,
|
||||
)
|
||||
json_data.update(linked_json_data)
|
||||
|
||||
raw_sub_profiles = await self._hass.async_add_executor_job(load_sub_profile_data, directory)
|
||||
@@ -202,7 +212,12 @@ class ProfileLibrary:
|
||||
replacements = self.compute_replacement_variables(placeholders, variables.copy(), source_entity)
|
||||
return cast(dict[str, Any], replace_placeholders(json_data, replacements))
|
||||
|
||||
def compute_replacement_variables(self, placeholders: set[str], variables: dict[str, str], source_entity: SourceEntity | None) -> dict[str, str]:
|
||||
def compute_replacement_variables(
|
||||
self,
|
||||
placeholders: set[str],
|
||||
variables: dict[str, str],
|
||||
source_entity: SourceEntity | None,
|
||||
) -> dict[str, str]:
|
||||
variables = variables or {}
|
||||
|
||||
if source_entity:
|
||||
@@ -216,7 +231,9 @@ class ProfileLibrary:
|
||||
source_entity=source_entity,
|
||||
)
|
||||
if not related_entity:
|
||||
raise LibraryError(build_related_entity_placeholder_not_found_message(placeholder, source_entity.entity_id))
|
||||
raise LibraryError(
|
||||
build_related_entity_placeholder_not_found_message(placeholder, source_entity.entity_id),
|
||||
)
|
||||
variables[placeholder] = related_entity
|
||||
|
||||
return variables
|
||||
@@ -286,7 +303,9 @@ class ProfileLibrary:
|
||||
|
||||
async def _load_model_data(self, manufacturer: str, model: str, custom_directory: str | None) -> tuple[dict, str]:
|
||||
"""Load the model data from the appropriate directory."""
|
||||
loader = LocalLoader(self._hass, custom_directory, is_custom_directory=True) if custom_directory else self._loader
|
||||
loader = (
|
||||
LocalLoader(self._hass, custom_directory, is_custom_directory=True) if custom_directory else self._loader
|
||||
)
|
||||
result = await loader.load_model(manufacturer, model)
|
||||
if not result:
|
||||
raise LibraryError(f"Model {manufacturer} {model} not found")
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"""
|
||||
|
||||
@@ -95,7 +95,9 @@ DEVICE_TYPE_DOMAIN: dict[DeviceType, str | set[str]] = {
|
||||
DeviceType.UPS: SENSOR_DOMAIN,
|
||||
}
|
||||
|
||||
SUPPORTED_DOMAINS: set[str] = {domain for domains in DEVICE_TYPE_DOMAIN.values() for domain in (domains if isinstance(domains, set) else {domains})}
|
||||
SUPPORTED_DOMAINS: set[str] = {
|
||||
domain for domains in DEVICE_TYPE_DOMAIN.values() for domain in (domains if isinstance(domains, set) else {domains})
|
||||
}
|
||||
|
||||
|
||||
def _build_domain_device_type_mapping() -> Mapping[str, set[DeviceType]]:
|
||||
@@ -393,7 +395,8 @@ class PowerProfile:
|
||||
|
||||
if found_profile is None:
|
||||
raise ModelNotSupportedError(
|
||||
f"Sub profile not found (manufacturer: {self._manufacturer}, model: {self._model}, sub_profile: {sub_profile})",
|
||||
f"Sub profile not found (manufacturer: {self._manufacturer}, "
|
||||
f"model: {self._model}, sub_profile: {sub_profile})",
|
||||
)
|
||||
|
||||
self._sub_profile_dir = os.path.join(self._directory, sub_profile)
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
import re
|
||||
from typing import Any, NamedTuple, Protocol
|
||||
from typing import NamedTuple, Protocol
|
||||
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
|
||||
@@ -79,7 +79,13 @@ class SubProfileSelectConfig(NamedTuple):
|
||||
|
||||
class SubProfileMatcher(Protocol):
|
||||
@classmethod
|
||||
def from_config(cls, config: dict, **kwargs: Any) -> SubProfileMatcher: # noqa: ANN401
|
||||
def from_config(
|
||||
cls,
|
||||
config: dict,
|
||||
*,
|
||||
hass: HomeAssistant | None = None,
|
||||
source_entity: SourceEntity | None = None,
|
||||
) -> SubProfileMatcher:
|
||||
"""Create a matcher from a config dict."""
|
||||
|
||||
def match(self, entity_state: State, source_entity: SourceEntity) -> str | None:
|
||||
@@ -114,8 +120,15 @@ class EntityStateMatcher(SubProfileMatcher):
|
||||
return self._mapping.get(state.state)
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: dict, **kwargs: Any) -> EntityStateMatcher: # noqa: ANN401
|
||||
return cls(kwargs["hass"], kwargs["source_entity"], config["entity_id"], config["map"])
|
||||
def from_config(
|
||||
cls,
|
||||
config: dict,
|
||||
*,
|
||||
hass: HomeAssistant | None = None,
|
||||
source_entity: SourceEntity | None = None,
|
||||
) -> EntityStateMatcher:
|
||||
assert hass is not None
|
||||
return cls(hass, source_entity, config["entity_id"], config["map"])
|
||||
|
||||
def get_tracking_entities(self) -> list[str]:
|
||||
return [self._entity_id]
|
||||
@@ -134,7 +147,13 @@ class AttributeMatcher(SubProfileMatcher):
|
||||
return self._mapping.get(val)
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: dict, **kwargs: Any) -> AttributeMatcher: # noqa: ANN401
|
||||
def from_config(
|
||||
cls,
|
||||
config: dict,
|
||||
*,
|
||||
hass: HomeAssistant | None = None,
|
||||
source_entity: SourceEntity | None = None,
|
||||
) -> AttributeMatcher:
|
||||
return cls(config["attribute"], config["map"])
|
||||
|
||||
def get_tracking_entities(self) -> list[str]:
|
||||
@@ -153,7 +172,13 @@ class EntityIdMatcher(SubProfileMatcher):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: dict, **kwargs: Any) -> EntityIdMatcher: # noqa: ANN401
|
||||
def from_config(
|
||||
cls,
|
||||
config: dict,
|
||||
*,
|
||||
hass: HomeAssistant | None = None,
|
||||
source_entity: SourceEntity | None = None,
|
||||
) -> EntityIdMatcher:
|
||||
return cls(config["pattern"], config["profile"])
|
||||
|
||||
def get_tracking_entities(self) -> list[str]:
|
||||
@@ -176,7 +201,13 @@ class IntegrationMatcher(SubProfileMatcher):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: dict, **kwargs: Any) -> IntegrationMatcher: # noqa: ANN401
|
||||
def from_config(
|
||||
cls,
|
||||
config: dict,
|
||||
*,
|
||||
hass: HomeAssistant | None = None,
|
||||
source_entity: SourceEntity | None = None,
|
||||
) -> IntegrationMatcher:
|
||||
return cls(config["integration"], config["profile"])
|
||||
|
||||
def get_tracking_entities(self) -> list[str]:
|
||||
@@ -204,7 +235,13 @@ class EntityRegistryMatcher(SubProfileMatcher):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: dict, **kwargs: Any) -> EntityRegistryMatcher: # noqa: ANN401
|
||||
def from_config(
|
||||
cls,
|
||||
config: dict,
|
||||
*,
|
||||
hass: HomeAssistant | None = None,
|
||||
source_entity: SourceEntity | None = None,
|
||||
) -> EntityRegistryMatcher:
|
||||
return cls(config["property"], config["value"], config["profile"])
|
||||
|
||||
def get_tracking_entities(self) -> list[str]:
|
||||
@@ -236,7 +273,13 @@ class ModelIdMatcher(SubProfileMatcher):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: dict, **kwargs: Any) -> ModelIdMatcher: # noqa: ANN401
|
||||
def from_config(
|
||||
cls,
|
||||
config: dict,
|
||||
*,
|
||||
hass: HomeAssistant | None = None,
|
||||
source_entity: SourceEntity | None = None,
|
||||
) -> ModelIdMatcher:
|
||||
return cls(config["model_id"], config["profile"])
|
||||
|
||||
def get_tracking_entities(self) -> list[str]:
|
||||
|
||||
Reference in new issue
Block a user