197 files

This commit is contained in:
Home Assistant Version Control committed 2026-08-21 18:53:37 +00:00
1 parent 89d9f5e49b
commit 383e9ed04e
197 files changed
+4386 -1837

No files matched your search

@@ -1,3 +1,4 @@
from copy import deepcopy
import json
import os
import re
@@ -19,7 +20,7 @@ from custom_components.powercalc.helpers import (
from .error import LibraryError
from .loader.composite import CompositeLoader
from .loader.local import LocalLoader
from .loader.protocol import Loader
from .loader.protocol import Loader, ModelMetadata
from .loader.remote import RemoteLoader
from .power_profile import DeviceType, DiscoveryBy, PowerProfile
@@ -48,14 +49,23 @@ class ProfileLibrary:
self._loader = loader
self._profiles: dict[str, list[PowerProfile]] = {}
self._manufacturer_models: dict[str, set[tuple[str, str]]] = {}
self._sub_profile_data: dict[str, list[tuple[str, dict[str, Any]]]] = {}
self._found_models: dict[ModelInfo, list[ModelInfo]] = {}
async def initialize(self) -> None:
await self._loader.initialize()
async def initialize(self, prefer_cached: bool = False) -> None:
"""Initialize the underlying loaders, see `Loader.initialize` for `prefer_cached`."""
self._sub_profile_data.clear()
self._found_models.clear()
await self._loader.initialize(prefer_cached)
@property
def discovery_ignored_domains(self) -> set[str]:
"""Get integration domains globally excluded from discovery."""
return self._loader.get_discovery_ignored_domains()
def discovery_low_priority_domains(self) -> set[str]:
"""Get integration domains that are the least preferred source for discovery.
Devices behind these integrations are only discovered when no other integration
represents them, and their entities are never discovered by entity discovery.
"""
return self._loader.get_discovery_low_priority_domains()
@staticmethod
@singleton("powercalc_library")
@@ -65,7 +75,8 @@ class ProfileLibrary:
Make sure we have a single instance throughout the application.
"""
library = ProfileLibrary(hass, ProfileLibrary.create_loader(hass))
await library.initialize()
# Startup must not block on the download API. The periodic library update refreshes it.
await library.initialize(prefer_cached=True)
return library
@staticmethod
@@ -129,15 +140,20 @@ class ProfileLibrary:
custom_directory: str | None = None,
variables: dict[str, str] | None = None,
process_variables: bool = True,
model_resolved: bool = False,
) -> PowerProfile:
"""Get a power profile for a given manufacturer and model."""
"""Get a power profile for a given manufacturer and model.
Pass `model_resolved` when `model_info` already comes out of `find_models`, to skip
looking the model up in the library a second time.
"""
# Support multiple LUT in subdirectories
sub_profile = None
if "/" in model_info.model:
(model, sub_profile) = model_info.model.split("/", 1)
model_info = ModelInfo(model_info.manufacturer, model, model_info.model_id)
if not custom_directory:
if not custom_directory and not model_resolved:
models = await self.find_models(model_info)
if not models:
raise LibraryError(f"Model {model_info.manufacturer} {model_info.model} not found")
@@ -178,7 +194,11 @@ class ProfileLibrary:
)
json_data.update(linked_json_data)
raw_sub_profiles = await self._hass.async_add_executor_job(load_sub_profile_data, directory)
raw_sub_profiles = self._sub_profile_data.get(directory)
if raw_sub_profiles is None:
raw_sub_profiles = await self._hass.async_add_executor_job(load_sub_profile_data, directory)
self._sub_profile_data[directory] = raw_sub_profiles
sub_profiles = [
(
sub_dir,
@@ -202,10 +222,14 @@ class ProfileLibrary:
source_entity: SourceEntity | None,
process_variables: bool,
) -> dict[str, Any]:
# json_data is potentially retrieved from cache, so we need to copy it to avoid modifying the cache
json_data = json_data.copy()
if not process_variables:
return json_data
# json_data is retrieved from cache, so we need to copy it to avoid modifying the cache
return json_data.copy()
# replace_placeholders rewrites nested dicts and lists in place, so a shallow copy is not
# enough here. Without a deep copy the substituted values leak into the cached profile data
# and the next profile built from the same model would reuse them.
json_data = deepcopy(json_data)
if json_data.get("fields"): # When custom fields in profile are defined, make sure all variables are passed
self.validate_variables(json_data, variables)
@@ -258,8 +282,25 @@ class ProfileLibrary:
"""Resolve the manufacturer, either from the model info or by loading it."""
return await self._loader.find_manufacturers(manufacturer)
async def get_model_metadata(self, model_info: ModelInfo) -> ModelMetadata | None:
"""Return discovery metadata for an already resolved model, without building the profile."""
return await self._loader.get_model_metadata(model_info.manufacturer, model_info.model)
async def find_models(self, model_info: ModelInfo) -> list[ModelInfo]:
"""Resolve the model identifier, searching for it if no custom directory is provided."""
"""Resolve the model identifier, searching for it if no custom directory is provided.
Discovery resolves the same handful of models for every entity of a device, so the
result is memoized until the library is reloaded.
"""
if model_info in self._found_models:
return self._found_models[model_info]
found = await self._find_models(model_info)
self._found_models[model_info] = found
return found
async def _find_models(self, model_info: ModelInfo) -> list[ModelInfo]:
"""Search the loaders for all models matching the given model info."""
search: set[str] = set()
for model_identifier in (model_info.model_id, model_info.model):
if model_identifier: