Initil after Upgrade
This commit is contained in:
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