197 files

This commit is contained in:
Home Assistant Version Control
2026-08-21 18:53:37 +00:00
parent 89d9f5e49b
commit 383e9ed04e
197 changed files with 4386 additions and 1837 deletions
+7 -2
View File
@@ -90,6 +90,7 @@ from .const import (
DATA_DOMAIN_ENTITIES,
DATA_ENTITIES,
DATA_GROUP_ENTITIES,
DATA_MEASURE_APP_COORDINATOR,
DATA_STANDBY_POWER_SENSORS,
DATA_USED_UNIQUE_IDS,
DISCOVERY_TYPE,
@@ -109,6 +110,7 @@ from .const import (
)
from .device_binding import is_composite_device_id
from .discovery import DiscoveryManager, DiscoveryStatus, get_discovery_manager
from .measure import MeasureAppCoordinator
from .migrate import async_fix_legacy_profile_config_entry, async_migrate_config_entry
from .power_profile.power_profile import DeviceType
from .sensors.group.config_entry_utils import (
@@ -213,8 +215,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
global_config = get_global_configuration(hass, config)
discovery_manager = create_discovery_manager_instance(hass, config, global_config)
measure_app_coordinator = MeasureAppCoordinator(hass, config)
hass.data[DOMAIN] = {
DATA_DISCOVERY_MANAGER: discovery_manager,
DATA_MEASURE_APP_COORDINATOR: measure_app_coordinator,
DOMAIN_CONFIG: global_config,
DATA_CONFIGURED_ENTITIES: {},
DATA_DOMAIN_ENTITIES: {},
@@ -225,7 +229,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
DATA_ANALYTICS: {},
}
await discovery_manager.setup()
discovery_manager.setup()
measure_app_coordinator.async_setup()
register_services(hass)
@@ -463,7 +468,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if discovery_enabled and discovery_manager.status == DiscoveryStatus.DISABLED:
_LOGGER.debug("Enabling discovery manager based on global configuration")
discovery_manager.enable()
await discovery_manager.setup()
discovery_manager.setup()
if not discovery_enabled and discovery_manager.status != DiscoveryStatus.DISABLED:
_LOGGER.debug("Disabling discovery manager based on global configuration")
await discovery_manager.disable()
@@ -26,6 +26,7 @@ from custom_components.powercalc.const import (
DATA_GROUP_SIZES,
DATA_GROUP_TYPES,
DATA_HAS_GROUP_INCLUDE,
DATA_MEASURE_APP_COORDINATOR,
DATA_POWER_PROFILE_SOURCES,
DATA_POWER_PROFILES,
DATA_SENSOR_TYPES,
@@ -160,6 +161,7 @@ class Analytics:
DOMAIN,
ENTRY_GLOBAL_CONFIG_UNIQUE_ID,
)
measure_app_coordinator = self.hass.data[DOMAIN].get(DATA_MEASURE_APP_COORDINATOR)
return {
"install_id": self.install_id,
"install_date": await self._get_install_date(),
@@ -170,6 +172,7 @@ class Analytics:
"custom_profile_count": await self._get_custom_profile_count(),
"has_global_gui_config": global_config_entry is not None,
"has_group_include": runtime_data.get(DATA_HAS_GROUP_INCLUDE, False),
"has_measure_app": measure_app_coordinator is not None and measure_app_coordinator.data is not None,
"group_sizes": Counter(group_sizes),
"counts": {
"by_config_type": runtime_data.setdefault(DATA_CONFIG_TYPES, Counter()),
+31 -15
View File
@@ -1,8 +1,7 @@
from dataclasses import dataclass
import math
import re
from typing import NamedTuple
from homeassistant.components.light import ATTR_SUPPORTED_COLOR_MODES, ColorMode
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant, split_entity_id
import homeassistant.helpers.device_registry as dr
@@ -29,17 +28,45 @@ from .const import (
from .errors import SensorConfigurationError
class SourceEntity(NamedTuple):
@dataclass(frozen=True)
class SourceEntity:
"""The appliance a powercalc sensor measures, resolved from the entity and device registry."""
object_id: str
entity_id: str
domain: str
unique_id: str | None = None
name: str | None = None
supported_color_modes: list[ColorMode] | None = None
entity_entry: er.RegistryEntry | None = None
device_entry: dr.DeviceEntry | None = None
config_entry_id: str | None = None
@property
def is_dummy(self) -> bool:
"""Whether this source has no real entity behind it, such as a daily fixed energy or group sensor."""
return self.entity_id == DUMMY_ENTITY_ID
@property
def device_id(self) -> str | None:
"""The ID of the device this source belongs to, when it is bound to one."""
return self.device_entry.id if self.device_entry else None
@property
def log_identifier(self) -> str:
"""Build a label identifying this source, used as prefix for log messages about it."""
if self.config_entry_id:
return _label("config_entry", self.config_entry_id, self.name)
if self.entity_id and not self.is_dummy:
return self.entity_id
if self.device_entry:
return _label("device", self.device_entry.id, self.device_entry.name_by_user or self.device_entry.name)
return self.object_id # pragma: no cover
def _label(prefix: str, identifier: str, name: str | None) -> str:
"""Build `prefix id (name)`, omitting the name when the registry does not have one."""
return f"{prefix} {identifier} ({name})" if name else f"{prefix} {identifier}"
EXCLUDE_FROM_PARENT_CONFIG = (
CONF_NAME,
@@ -85,19 +112,9 @@ def create_source_entity(entity_id: str, hass: HomeAssistant) -> SourceEntity:
)
unique_id = None
supported_color_modes: list[ColorMode] = []
if entity_entry:
source_entity_domain = entity_entry.domain
unique_id = entity_entry.unique_id
if entity_entry.capabilities:
supported_color_modes = entity_entry.capabilities.get(
ATTR_SUPPORTED_COLOR_MODES,
[],
)
entity_state = hass.states.get(entity_id)
if entity_state:
supported_color_modes = entity_state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, [])
return SourceEntity(
source_object_id,
@@ -111,7 +128,6 @@ def create_source_entity(entity_id: str, hass: HomeAssistant) -> SourceEntity:
entity_entry,
device_entry,
),
supported_color_modes or [],
entity_entry,
device_entry,
)
+2 -2
View File
@@ -47,7 +47,7 @@ from .const import (
CalculationStrategy,
SensorType,
)
from .device_binding import attach_configured_device_entry
from .device_binding import resolve_source_device
from .errors import ModelNotSupportedError, StrategyConfigurationError
from .flow_helper.common import FlowType, PowercalcFormStep, Step, fill_schema_defaults, flatten_sections
from .flow_helper.flows.cost import CostConfigFlow, CostOptionsFlow
@@ -510,7 +510,7 @@ class PowercalcOptionsFlow(PowercalcCommonFlow, OptionsFlow):
self.sensor_config = dict(self.config_entry.data)
if self.source_entity_id:
self.source_entity = attach_configured_device_entry(
self.source_entity = resolve_source_device(
self.hass,
self.sensor_config,
create_source_entity(
+4 -1
View File
@@ -30,6 +30,7 @@ DATA_DISCOVERY_MANAGER = "discovery_manager"
DATA_DOMAIN_ENTITIES = "domain_entities"
DATA_ENTITIES = "entities"
DATA_GROUP_ENTITIES = "group_entities"
DATA_MEASURE_APP_COORDINATOR = "measure_app_coordinator"
DATA_USED_UNIQUE_IDS = "used_unique_ids"
DATA_STANDBY_POWER_SENSORS = "standby_power_sensors"
DATA_ANALYTICS = "analytics"
@@ -78,6 +79,7 @@ CONF_CREATE_GROUP = "create_group"
CONF_CREATE_STANDBY_GROUP = "create_standby_group"
CONF_CREATE_STANDBY_ENERGY_SENSOR = "create_standby_energy_sensor"
CONF_CREATE_UTILITY_METERS = "create_utility_meters"
CONF_CURRENT_ENTITY = "current_entity"
CONF_CUSTOM_MODEL_DIRECTORY = "custom_model_directory"
CONF_DAILY_FIXED_ENERGY = "daily_fixed_energy"
CONF_DELAY = "delay"
@@ -242,7 +244,7 @@ DISCOVERY_SOURCE_ENTITY = "source_entity"
DISCOVERY_POWER_PROFILES = "power_profiles"
DISCOVERY_INTEGRATION_NAME = "integration_name"
DISCOVERY_TYPE = "discovery_type"
LIBRARY_DISCOVERY_IGNORED_DOMAINS = "discovery_ignored_domains"
LIBRARY_DISCOVERY_LOW_PRIORITY_DOMAINS = "discovery_low_priority_domains"
LIBRARY_URL = "https://library.powercalc.nl"
API_URL = "https://api.powercalc.nl"
@@ -316,6 +318,7 @@ class PowercalcDiscoveryType(StrEnum):
DOMAIN_GROUP = "domain_group"
STANDBY_GROUP = "standby_group"
LIBRARY = "library"
MEASURE_APP = "measure_app"
USER_YAML = "user_yaml"
+26 -15
View File
@@ -1,3 +1,4 @@
from dataclasses import replace
import logging
from homeassistant.config_entries import ConfigEntry
@@ -12,7 +13,7 @@ from homeassistant.helpers.entity_registry import RegistryEntry
from homeassistant.helpers.typing import ConfigType
from custom_components.powercalc.common import SourceEntity
from custom_components.powercalc.const import CONF_AREA, DUMMY_ENTITY_ID
from custom_components.powercalc.const import CONF_AREA
_LOGGER = logging.getLogger(__name__)
@@ -31,6 +32,19 @@ def is_composite_device_id(hass: HomeAssistant, device_id: str) -> bool:
return bool(is_composite(device_id))
def get_non_composite_devices(hass: HomeAssistant) -> list[DeviceEntry]:
"""Return all registered devices which are not legacy composite devices.
Resolves the registry and the composite device support once, instead of per device like
a per-entry `is_composite_device_id` call would.
"""
device_reg = device_registry.async_get(hass)
is_composite = getattr(device_reg, "async_is_composite_device_id", None)
if not callable(is_composite):
return list(device_reg.devices.values())
return [device for device in device_reg.devices.values() if not is_composite(device.id)]
def get_related_device_ids(hass: HomeAssistant, device_id: str) -> set[str]:
"""
Return the IDs of all devices representing the same physical device, including `device_id` itself.
@@ -97,11 +111,7 @@ def get_first_device_for_config_entry(hass: HomeAssistant, config_entry_id: str)
def get_devices_for_config_entry(hass: HomeAssistant, config_entry_id: str) -> list[DeviceEntry]:
"""Return all non-composite devices belonging to a config entry."""
return [
device
for device in device_registry.async_get(hass).devices.values()
if config_entry_id in get_config_entry_ids(device) and not is_composite_device_id(hass, device.id)
]
return [device for device in get_non_composite_devices(hass) if config_entry_id in get_config_entry_ids(device)]
def get_related_devices(hass: HomeAssistant, device_id: str) -> list[DeviceEntry]:
@@ -116,25 +126,25 @@ def get_related_devices(hass: HomeAssistant, device_id: str) -> list[DeviceEntry
return list(devices.values())
def attach_configured_device_entry(
def resolve_source_device(
hass: HomeAssistant,
sensor_config: ConfigType,
source_entity: SourceEntity,
) -> SourceEntity:
"""Attach the configured device entry to a device-based source entity."""
if source_entity.entity_id != DUMMY_ENTITY_ID:
if not source_entity.is_dummy:
return source_entity
device_entry = get_device_entry(hass, sensor_config=sensor_config)
if device_entry:
return source_entity._replace(device_entry=device_entry)
return replace(source_entity, device_entry=device_entry)
return source_entity
def attach_entities_to_resolved_device(
def assign_device_to_entities(
hass: HomeAssistant,
config_entry: ConfigEntry | None,
entities_to_add: list[Entity],
hass: HomeAssistant,
source_entity: SourceEntity | None,
sensor_config: ConfigType | None = None,
) -> None:
@@ -145,11 +155,12 @@ def attach_entities_to_resolved_device(
return
for entity in entities_to_add:
try:
# Home Assistant only accepts `device_entry` on entities belonging to a config entry.
# Setting it for YAML entities makes HA report a deprecation warning, so those rely
# solely on the registry update `bind_entity_to_device` does after they are added.
if config_entry:
entity.device_entry = device_entry
setattr(entity, "_powercalc_device_entry", device_entry) # noqa: B010
except AttributeError: # pragma: no cover
_LOGGER.error("%s: Cannot set device id on entity", entity.entity_id)
setattr(entity, "_powercalc_device_entry", device_entry) # noqa: B010
def get_device_entry(
File diff suppressed because it is too large Load Diff
@@ -17,7 +17,6 @@ from custom_components.powercalc.const import (
CONF_SUB_PROFILE,
CONF_VARIABLES,
DOMAIN,
DUMMY_ENTITY_ID,
LIBRARY_URL,
CalculationStrategy,
)
@@ -466,7 +465,7 @@ class LibraryFlow:
if source_entity.config_entry_id:
return DiscoveryBy.CONFIG_ENTRY
if source_entity.entity_id != DUMMY_ENTITY_ID:
if not source_entity.is_dummy:
return None
profile = self.flow.selected_profile
@@ -2,8 +2,18 @@ from collections.abc import Callable
from pathlib import Path
from typing import TYPE_CHECKING, Any
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.config_entries import ConfigFlowResult
from homeassistant.const import CONF_ATTRIBUTE, CONF_ENTITIES, CONF_ENTITY_ID, CONF_ID, CONF_NAME, CONF_PATH, Platform
from homeassistant.const import (
CONF_ATTRIBUTE,
CONF_ENTITIES,
CONF_ENTITY_ID,
CONF_ID,
CONF_NAME,
CONF_PATH,
Platform,
UnitOfElectricCurrent,
)
from homeassistant.helpers import selector
from homeassistant.helpers.schema_config_entry_flow import SchemaFlowError
import voluptuous as vol
@@ -16,6 +26,7 @@ from custom_components.powercalc.const import (
CONF_CREATE_ENERGY_SENSOR,
CONF_CREATE_STANDBY_ENERGY_SENSOR,
CONF_CREATE_UTILITY_METERS,
CONF_CURRENT_ENTITY,
CONF_FIXED,
CONF_FIXED_VALUE,
CONF_GAMMA_CURVE,
@@ -67,7 +78,7 @@ from custom_components.powercalc.flow_helper.strategy_form import (
wrap_strategy_form_data,
)
from custom_components.powercalc.power_profile.power_profile import DeviceType
from custom_components.powercalc.strategy.wled import CONFIG_SCHEMA as SCHEMA_POWER_WLED
from custom_components.powercalc.strategy.wled import CONFIG_SCHEMA as CONFIG_SCHEMA_WLED
if TYPE_CHECKING:
from custom_components.powercalc.config_flow import PowercalcCommonFlow, PowercalcConfigFlow, PowercalcOptionsFlow
@@ -152,6 +163,21 @@ SCHEMA_POWER_LINEAR = vol.Schema(
},
)
# The WLED strategy config schema, with the current entity rendered as an entity picker in the GUI.
SCHEMA_POWER_WLED = CONFIG_SCHEMA_WLED.extend(
{
vol.Optional(CONF_CURRENT_ENTITY): selector.EntitySelector(
selector.EntitySelectorConfig(
filter={
"domain": "sensor",
"device_class": SensorDeviceClass.CURRENT,
"unit_of_measurement": UnitOfElectricCurrent.MILLIAMPERE,
},
),
),
},
)
SCHEMA_POWER_MULTI_SWITCH_MANUAL = vol.Schema(
{
vol.Required(CONF_POWER): vol.Coerce(float),
@@ -170,13 +170,7 @@ class LightGroupFilter(EntityFilter):
@staticmethod
def _find_light_group(hass: HomeAssistant, group_entity_id: str) -> Entity | None:
light_component = cast(EntityComponent, hass.data.get(LIGHT_DOMAIN))
return next(
filter(
lambda entity: entity.entity_id == group_entity_id,
light_component.entities,
),
None,
)
return light_component.get_entity(group_entity_id)
def find_all_entity_ids_recursively(
self,
@@ -62,8 +62,8 @@ async def find_entities(
if _LOGGER.isEnabledFor(logging.DEBUG): # pragma: no cover
_LOGGER.debug("Source entities: %s", [entity.entity_id for entity in source_entities])
for source_entity in source_entities:
entity_id = source_entity.entity_id
for entity_entry in source_entities:
entity_id = entity_entry.entity_id
mapped = source_entity_powercalc_entity_map.get(entity_id)
if mapped:
@@ -75,12 +75,12 @@ async def find_entities(
resolved_entities.append(existing)
continue
real_sensor = _create_real_sensor(source_entity)
real_sensor = _create_real_sensor(entity_entry)
if real_sensor:
resolved_entities.append(real_sensor)
continue
if await _is_discoverable_source_entity(hass, source_entity):
if await _is_discoverable_source_entity(hass, entity_entry):
discoverable_entities.append(entity_id)
if exclude_utility_meters:
@@ -95,18 +95,18 @@ async def find_entities(
def _is_source_entity_eligible(
hass: HomeAssistant,
source_entity: RegistryEntry,
entity_entry: RegistryEntry,
include_non_powercalc: bool,
) -> bool:
"""Return whether a registry entity is eligible for Powercalc discovery."""
if source_entity.platform != DOMAIN:
return include_non_powercalc or source_entity.domain != sensor.DOMAIN
if entity_entry.platform != DOMAIN:
return include_non_powercalc or entity_entry.domain != sensor.DOMAIN
# YAML-created Powercalc entities have no config entry and are classified by their runtime type later.
if source_entity.config_entry_id is None:
if entity_entry.config_entry_id is None:
return True
config_entry = hass.config_entries.async_get_entry(source_entity.config_entry_id)
config_entry = hass.config_entries.async_get_entry(entity_entry.config_entry_id)
if config_entry is None:
return False
@@ -117,30 +117,30 @@ def _is_source_entity_eligible(
config_entry.data.get(ENTRY_DATA_POWER_ENTITY),
config_entry.data.get(ENTRY_DATA_ENERGY_ENTITY),
}
return source_entity.entity_id in main_entity_ids
return entity_entry.entity_id in main_entity_ids
def _create_real_sensor(source_entity: RegistryEntry) -> Entity | None:
if source_entity.domain != sensor.DOMAIN:
def _create_real_sensor(entity_entry: RegistryEntry) -> Entity | None:
if entity_entry.domain != sensor.DOMAIN:
return None
device_class = source_entity.device_class or source_entity.original_device_class
device_class = entity_entry.device_class or entity_entry.original_device_class
if device_class == SensorDeviceClass.POWER:
return RealPowerSensor(source_entity.entity_id, source_entity.unit_of_measurement)
return RealPowerSensor(entity_entry.entity_id, entity_entry.unit_of_measurement)
if device_class == SensorDeviceClass.ENERGY:
return RealEnergySensor(source_entity.entity_id)
return RealEnergySensor(entity_entry.entity_id)
return None # pragma: no cover
async def _is_discoverable_source_entity(hass: HomeAssistant, source_entity: RegistryEntry) -> bool:
async def _is_discoverable_source_entity(hass: HomeAssistant, entity_entry: RegistryEntry) -> bool:
power_profile = await get_power_profile_by_source_entity(
hass,
create_source_entity(source_entity.entity_id, hass),
create_source_entity(entity_entry.entity_id, hass),
)
return bool(
power_profile
and not await power_profile.needs_user_configuration
and power_profile.is_entity_domain_supported(source_entity),
and power_profile.is_entity_domain_supported(entity_entry),
)
+7 -7
View File
@@ -16,7 +16,6 @@ from homeassistant.helpers.typing import ConfigType
from custom_components.powercalc.common import SourceEntity
from custom_components.powercalc.const import (
DUMMY_ENTITY_ID,
PLACEHOLDER_ENTITY_BY_DEVICE_CLASS,
PLACEHOLDER_ENTITY_BY_TRANSLATION_KEY,
CalculationStrategy,
@@ -52,13 +51,13 @@ def get_or_create_unique_id(
# For multi-switch and wled strategy we need to use the device id as unique id
# As we don't want to start a discovery for each switch entity
if (
source_entity.device_entry
source_entity.device_id
and power_profile
and power_profile.calculation_strategy in [CalculationStrategy.WLED, CalculationStrategy.MULTI_SWITCH]
):
return f"pc_{source_entity.device_entry.id}"
return f"pc_{source_entity.device_id}"
if source_entity and source_entity.entity_id != DUMMY_ENTITY_ID:
if source_entity and not source_entity.is_dummy:
source_unique_id = source_entity.unique_id or source_entity.entity_id
# Prefix with pc_ to avoid conflicts with other integrations
return f"pc_{source_unique_id}"
@@ -276,19 +275,20 @@ def _get_related_entity_for_device(
) -> str | None:
"""Get the first related entity on the same device matching the given predicate."""
entity_reg = entity_registry.async_get(hass)
if not source_entity.device_entry:
device_id = source_entity.device_id
if not device_id:
_LOGGER.debug("No device_id available, cannot find related entity")
return None
related_entities = [
entity_entry.entity_id
for entity_entry in entity_registry.async_entries_for_device(entity_reg, source_entity.device_entry.id)
for entity_entry in entity_registry.async_entries_for_device(entity_reg, device_id)
if matcher(entity_entry)
]
if not related_entities:
_LOGGER.debug(
"No related entities found for device %s with %s %s",
source_entity.device_entry.id,
device_id,
match_label,
match_value,
)
+18
View File
@@ -1,4 +1,22 @@
{
"entity": {
"sensor": {
"measure_session_status": {
"default": "mdi:progress-wrench",
"state": {
"validating": "mdi:progress-question",
"ready": "mdi:progress-check",
"awaiting_confirmation": "mdi:progress-clock",
"running": "mdi:progress-wrench",
"cancelling": "mdi:progress-close",
"cancelled": "mdi:cancel",
"completed": "mdi:check-circle",
"failed": "mdi:alert-circle",
"resumable": "mdi:restore"
}
}
}
},
"services": {
"activate_playbook": {
"service": "mdi:play"
+1 -1
View File
@@ -22,5 +22,5 @@
"requirements": [
"numpy>=1.21.1"
],
"version": "v1.24.1"
"version": "v1.25.1"
}
@@ -28,7 +28,13 @@ async def get_power_profile(
model_info: ModelInfo | None = None,
log_errors: bool = True,
process_variables: bool = True,
model_resolved: bool = False,
) -> PowerProfile | None:
"""Build the power profile for a given configuration or model.
Pass `model_resolved` when `model_info` already comes out of `ProfileLibrary.find_models`,
to skip resolving it against the library again.
"""
manufacturer = config.get(CONF_MANUFACTURER)
model = config.get(CONF_MODEL)
model_id = None
@@ -60,6 +66,7 @@ async def get_power_profile(
custom_model_directory,
variables,
process_variables,
model_resolved,
)
except LibraryError as err:
if log_errors:
@@ -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:
@@ -1,7 +1,7 @@
import logging
from typing import Any
from custom_components.powercalc.power_profile.loader.protocol import Loader
from custom_components.powercalc.power_profile.loader.protocol import Loader, ModelMetadata
from custom_components.powercalc.power_profile.power_profile import DeviceType, DiscoveryBy
_LOGGER = logging.getLogger(__name__)
@@ -11,13 +11,13 @@ class CompositeLoader(Loader):
def __init__(self, loaders: list[Loader]) -> None:
self.loaders = loaders
async def initialize(self) -> None:
async def initialize(self, prefer_cached: bool = False) -> None:
for loader in self.loaders:
await loader.initialize()
await loader.initialize(prefer_cached)
def get_discovery_ignored_domains(self) -> set[str]:
"""Get all integration domains excluded by the combined libraries."""
return {domain for loader in self.loaders for domain in loader.get_discovery_ignored_domains()}
def get_discovery_low_priority_domains(self) -> set[str]:
"""Get all low priority integration domains of the combined libraries."""
return {domain for loader in self.loaders for domain in loader.get_discovery_low_priority_domains()}
async def get_manufacturer_listing(
self,
@@ -75,6 +75,15 @@ class CompositeLoader(Loader):
return models
async def get_model_metadata(self, manufacturer: str, model: str) -> ModelMetadata | None:
"""Return the metadata of the first loader knowing the model, matching load_model precedence."""
for loader in self.loaders:
metadata = await loader.get_model_metadata(manufacturer, model)
if metadata:
return metadata
return None
async def find_model_migration(self, manufacturer: str, model: str) -> str | None:
"""Find the canonical model id for a legacy profile id."""
matches: set[str] = set()
@@ -7,7 +7,7 @@ from typing import Any, cast
from homeassistant.core import HomeAssistant
from custom_components.powercalc.power_profile.error import LibraryLoadingError
from custom_components.powercalc.power_profile.loader.protocol import Loader
from custom_components.powercalc.power_profile.loader.protocol import Loader, ModelMetadata
from custom_components.powercalc.power_profile.power_profile import DeviceType, DiscoveryBy, PowerProfile
_LOGGER = logging.getLogger(__name__)
@@ -20,12 +20,12 @@ class LocalLoader(Loader):
self._hass = hass
self._manufacturer_model_listing: dict[str, dict[str, PowerProfile]] = {}
async def initialize(self) -> None:
"""Initialize the loader."""
async def initialize(self, prefer_cached: bool = False) -> None:
"""Initialize the loader. Local profiles are read from disk, so nothing is ever cached remotely."""
if not self._is_custom_directory:
await self._hass.async_add_executor_job(self._load_custom_library)
def get_discovery_ignored_domains(self) -> set[str]:
def get_discovery_low_priority_domains(self) -> set[str]:
"""Local profile directories do not provide global library metadata."""
return set()
@@ -139,6 +139,15 @@ class LocalLoader(Loader):
"""Local custom libraries do not support metadata-driven legacy profile migrations."""
return None
async def get_model_metadata(self, manufacturer: str, model: str) -> ModelMetadata | None:
"""Return discovery metadata from the already parsed local profile."""
models = self._manufacturer_model_listing.get(manufacturer.lower())
profile = models.get(model.lower()) if models else None
if profile is None or profile.device_type is None:
return None
return ModelMetadata(device_type=profile.device_type, discovery_by=profile.discovery_by)
def _load_custom_library(self) -> None:
"""Loading custom models and aliases from file system.
Manufacturer directories without model directories and model.json files within
@@ -1,14 +1,25 @@
from typing import Any, Protocol
from typing import Any, NamedTuple, Protocol
from custom_components.powercalc.power_profile.power_profile import DeviceType, DiscoveryBy
class Loader(Protocol):
async def initialize(self) -> None:
"""Initialize the loader."""
class ModelMetadata(NamedTuple):
"""Discovery relevant model properties, available from the library index without loading the profile."""
def get_discovery_ignored_domains(self) -> set[str]:
"""Get integration domains excluded from discovery."""
device_type: DeviceType
discovery_by: DiscoveryBy
class Loader(Protocol):
async def initialize(self, prefer_cached: bool = False) -> None:
"""Initialize the loader.
Pass `prefer_cached` to load from local storage when available, keeping remote
downloads off the caller's critical path.
"""
def get_discovery_low_priority_domains(self) -> set[str]:
"""Get integration domains that are the least preferred source for discovery."""
async def get_manufacturer_listing(
self,
@@ -36,3 +47,6 @@ class Loader(Protocol):
async def find_model_migration(self, manufacturer: str, model: str) -> str | None:
"""Return the canonical model id for a legacy profile id using library metadata."""
async def get_model_metadata(self, manufacturer: str, model: str) -> ModelMetadata | None:
"""Return discovery metadata for a model, or None when this loader does not know the model."""
@@ -20,11 +20,11 @@ from custom_components.powercalc.const import (
API_URL,
BUILT_IN_LIBRARY_DIR,
DOMAIN,
LIBRARY_DISCOVERY_IGNORED_DOMAINS,
LIBRARY_DISCOVERY_LOW_PRIORITY_DOMAINS,
)
from custom_components.powercalc.helpers import async_cache, clear_async_cache
from custom_components.powercalc.power_profile.error import LibraryLoadingError, ProfileDownloadError
from custom_components.powercalc.power_profile.loader.protocol import Loader
from custom_components.powercalc.power_profile.loader.protocol import Loader, ModelMetadata
from custom_components.powercalc.power_profile.power_profile import DeviceType, DiscoveryBy
_LOGGER = logging.getLogger(__name__)
@@ -65,14 +65,18 @@ class RemoteLoader(Loader):
self.manufacturer_lookup: dict[str, set[str]] = {}
self.profile_hashes: dict[str, str] = {}
async def initialize(self) -> None:
"""Initialize the loader."""
async def initialize(self, prefer_cached: bool = False) -> None:
"""Initialize the loader.
Pass `prefer_cached` to keep the network off the critical path, using the library.json
already in local storage when there is one. Only the very first run has to download.
"""
integration = await async_get_integration(self.hass, DOMAIN)
powercalc_version = AwesomeVersion(str(integration.version))
self._clear_caches()
self.library_contents = await self.load_library_json()
self.library_contents = await self.load_library_json(prefer_cached)
self.profile_hashes = await self.hass.async_add_executor_job(self._load_profile_hashes)
self.model_infos.clear()
@@ -85,9 +89,9 @@ class RemoteLoader(Loader):
for manufacturer in manufacturers:
self._index_manufacturer(manufacturer, powercalc_version)
def get_discovery_ignored_domains(self) -> set[str]:
"""Get integration domains excluded from discovery by library metadata."""
return set(self.library_contents.get(LIBRARY_DISCOVERY_IGNORED_DOMAINS, []))
def get_discovery_low_priority_domains(self) -> set[str]:
"""Get the low priority discovery integration domains declared by library metadata."""
return set(self.library_contents.get(LIBRARY_DISCOVERY_LOW_PRIORITY_DOMAINS, []))
def _index_manufacturer(self, manufacturer: LibraryManufacturer, powercalc_version: AwesomeVersion) -> None:
"""Register a manufacturer, its aliases and all of its supported models in the lookup tables."""
@@ -160,53 +164,74 @@ class RemoteLoader(Loader):
clear_async_cache(self.find_model_migration)
clear_async_cache(self.load_model)
async def load_library_json(self) -> dict[str, Any]:
"""Load library.json file"""
async def load_library_json(self, prefer_cached: bool = False) -> dict[str, Any]:
"""Load library.json, from local storage or from the download API.
local_path = self.hass.config.path(STORAGE_DIR, BUILT_IN_LIBRARY_DIR, "library.json")
def _load_local_library_json() -> dict[str, Any]:
"""Load library.json file from local storage"""
if not os.path.exists(local_path):
raise ProfileDownloadError("Local library.json file not found")
with open(local_path) as f:
return cast(dict[str, Any], json.load(f))
async def _download_remote_library_json() -> dict[str, Any] | None:
"""
Download library.json from Github.
On success, save it to local storage as a fallback for internet connection issues.
"""
_LOGGER.debug("Loading library.json from github")
session = async_get_clientsession(self.hass)
try:
async with asyncio.timeout(TIMEOUT_SECONDS), session.get(ENDPOINT_LIBRARY) as resp:
if resp.status != 200:
raise ProfileDownloadError(
f"Failed to download library.json, unexpected status code: {resp.status}",
)
data = await resp.read()
except (TimeoutError, ClientError) as err:
raise ProfileDownloadError(f"Failed to download library.json: {err}") from err
def _save_to_local_storage(data: bytes) -> None:
os.makedirs(os.path.dirname(local_path), exist_ok=True)
with open(local_path, "wb") as f:
f.write(data)
await self.hass.async_add_executor_job(_save_to_local_storage, data)
return cast(dict[str, Any], json.loads(data))
With `prefer_cached` the locally stored copy wins when it exists, so the caller never
waits on the network. The periodic library update refreshes it later.
"""
if prefer_cached:
cached_library = await self.hass.async_add_executor_job(self._read_local_library_json)
if cached_library is not None:
_LOGGER.debug("Loaded library.json from local storage")
return cached_library
_LOGGER.debug("No library.json in local storage yet, downloading it")
try:
return cast(dict[str, Any], await self.download_with_retry(_download_remote_library_json))
return cast(dict[str, Any], await self.download_with_retry(self._download_remote_library_json))
except ProfileDownloadError:
_LOGGER.debug("Failed to download library.json, falling back to local copy")
return await self.hass.async_add_executor_job(_load_local_library_json)
return await self.hass.async_add_executor_job(self._load_local_library_json)
def _get_library_json_path(self) -> str:
"""Retrieve the local storage path for the library.json file."""
return str(self.hass.config.path(STORAGE_DIR, BUILT_IN_LIBRARY_DIR, "library.json"))
def _read_local_library_json(self) -> dict[str, Any] | None:
"""Read library.json from local storage, None when it has not been downloaded yet."""
local_path = self._get_library_json_path()
if not os.path.exists(local_path):
return None
with open(local_path) as f:
return cast(dict[str, Any], json.load(f))
def _load_local_library_json(self) -> dict[str, Any]:
"""Load library.json from local storage, raising when it is not there."""
library_json = self._read_local_library_json()
if library_json is None:
raise ProfileDownloadError("Local library.json file not found")
return library_json
async def _download_remote_library_json(self) -> dict[str, Any] | None:
"""
Download library.json from Github.
On success, save it to local storage as a fallback for internet connection issues.
"""
_LOGGER.debug("Loading library.json from github")
local_path = self._get_library_json_path()
session = async_get_clientsession(self.hass)
try:
async with asyncio.timeout(TIMEOUT_SECONDS), session.get(ENDPOINT_LIBRARY) as resp:
if resp.status != 200:
raise ProfileDownloadError(
f"Failed to download library.json, unexpected status code: {resp.status}",
)
data = await resp.read()
except (TimeoutError, ClientError) as err:
raise ProfileDownloadError(f"Failed to download library.json: {err}") from err
def _save_to_local_storage(data: bytes) -> None:
os.makedirs(os.path.dirname(local_path), exist_ok=True)
with open(local_path, "wb") as f:
f.write(data)
await self.hass.async_add_executor_job(_save_to_local_storage, data)
return cast(dict[str, Any], json.loads(data))
@async_cache
async def get_manufacturer_listing(
@@ -221,7 +246,8 @@ class RemoteLoader(Loader):
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", [])
# Use the indexed models, so models requiring a newer Powercalc version are left out here as well.
for model in self.manufacturer_models.get(str(manufacturer.get("dir_name")), [])
)
}
@@ -254,11 +280,20 @@ class RemoteLoader(Loader):
device_types: set[DeviceType] | None,
discovery_by: DiscoveryBy | None,
) -> bool:
model_device_type = DeviceType(model.get("device_type", DeviceType.LIGHT))
"""Check whether an indexed model passes the requested filters.
Device types and discovery modes this Powercalc version does not know about are treated
as a non match, so profiles using a newly introduced value never break the listings.
"""
try:
model_device_type = DeviceType(model.get("device_type", DeviceType.LIGHT))
model_discovery_by = DiscoveryBy(model.get("discovery_by", DiscoveryBy.ENTITY))
except ValueError:
return False
if device_types and model_device_type not in device_types:
return False
model_discovery_by = DiscoveryBy(model.get("discovery_by", DiscoveryBy.ENTITY))
return not discovery_by or model_discovery_by == discovery_by
@async_cache
@@ -289,6 +324,20 @@ class RemoteLoader(Loader):
return next(iter(matches))
async def get_model_metadata(self, manufacturer: str, model: str) -> ModelMetadata | None:
"""Return discovery metadata straight from the library index, without downloading the profile."""
model_info = self.model_infos.get(f"{manufacturer}/{model}")
if not model_info:
return None
try:
device_type = DeviceType(model_info.get("device_type", DeviceType.LIGHT))
discovery_by = DiscoveryBy(model_info.get("discovery_by", DiscoveryBy.ENTITY))
except ValueError:
return None
return ModelMetadata(device_type=device_type, discovery_by=discovery_by)
@async_cache
async def load_model(
self,
@@ -400,8 +449,8 @@ class RemoteLoader(Loader):
async def download_with_retry(
self,
callback: Callable[[], Coroutine[Any, Any, None | dict[str, Any]]],
) -> None | dict[str, Any]:
callback: Callable[[], Coroutine[Any, Any, dict[str, Any] | None]],
) -> dict[str, Any] | None:
"""Download a file from a remote endpoint with retries"""
max_retries = 3
retry_count = 0
@@ -117,6 +117,22 @@ def _build_domain_device_type_mapping() -> Mapping[str, set[DeviceType]]:
DOMAIN_DEVICE_TYPE_MAPPING: Mapping[str, set[DeviceType]] = _build_domain_device_type_mapping()
def is_device_type_supported_for_entity(device_type: DeviceType | None, entity_entry: RegistryEntry) -> bool:
"""Check whether a device type can be applied to a given entity.
Kept module level so discovery can apply it to the device type from the library index,
without having to build the full power profile first.
"""
if device_type is None:
return False
# see https://github.com/bramstroker/homeassistant-powercalc/issues/2529
if device_type == DeviceType.PRINTER and entity_entry.unit_of_measurement:
return False
return device_type in DOMAIN_DEVICE_TYPE_MAPPING[entity_entry.domain]
class PowerProfile:
def __init__(
self,
@@ -433,16 +449,7 @@ class PowerProfile:
def is_entity_domain_supported(self, entity_entry: RegistryEntry) -> bool:
"""Check whether this power profile supports a given entity domain."""
if self.device_type is None:
return False
domain = entity_entry.domain
# see https://github.com/bramstroker/homeassistant-powercalc/issues/2529
if self.device_type == DeviceType.PRINTER and entity_entry.unit_of_measurement:
return False
return self.device_type in DOMAIN_DEVICE_TYPE_MAPPING[domain]
return is_device_type_supported_for_entity(self.device_type, entity_entry)
@property
def is_custom_profile(self) -> bool:
+14 -7
View File
@@ -59,13 +59,13 @@ from .const import (
DATA_ENTITY_TYPES,
DATA_GROUP_ENTITIES,
DATA_HAS_GROUP_INCLUDE,
DATA_MEASURE_APP_COORDINATOR,
DATA_SENSOR_TYPES,
DATA_SOURCE_DOMAINS,
DATA_USED_UNIQUE_IDS,
DISCOVERY_TYPE,
DOMAIN,
DOMAIN_CONFIG,
DUMMY_ENTITY_ID,
ENTRY_DATA_ENERGY_ENTITY,
ENTRY_DATA_POWER_ENTITY,
ENTRY_GLOBAL_CONFIG_UNIQUE_ID,
@@ -87,8 +87,8 @@ from .const import (
SensorType,
)
from .device_binding import (
attach_configured_device_entry,
attach_entities_to_resolved_device,
assign_device_to_entities,
resolve_source_device,
)
from .errors import (
PowercalcSetupError,
@@ -97,6 +97,7 @@ from .errors import (
)
from .group_include.filter import FilterOperator, create_composite_filter
from .group_include.include import find_entities
from .measure import MeasureAppCoordinator
from .sensors.cost import CostSensor, create_cost_sensor_for_energy_entity
from .sensors.daily_energy import (
create_daily_fixed_energy_power_sensor,
@@ -108,6 +109,7 @@ from .sensors.group.config_entry_utils import add_to_associated_groups
from .sensors.group.custom import GroupedSensor
from .sensors.group.factory import create_group_sensors
from .sensors.group.standby import StandbyPowerSensor
from .sensors.measure import MeasureSessionStatusSensor
from .sensors.power import PowerSensor, VirtualPowerSensor, create_power_sensor
_LOGGER = logging.getLogger(__name__)
@@ -127,6 +129,11 @@ async def async_setup_platform(
) -> None:
"""Setup sensors from YAML config sensor entries."""
if discovery_info and discovery_info.get(DISCOVERY_TYPE) == PowercalcDiscoveryType.MEASURE_APP:
coordinator: MeasureAppCoordinator = hass.data[DOMAIN][DATA_MEASURE_APP_COORDINATOR]
async_add_entities([MeasureSessionStatusSensor(coordinator)])
return
# Legacy sensor platform config is used. Raise an issue.
if not discovery_info and config:
async_create_issue(
@@ -218,7 +225,7 @@ async def _async_setup_entities(
_LOGGER.error(err)
return
attach_entities_to_resolved_device(config_entry, entities.new, hass, None, config)
assign_device_to_entities(hass, config_entry, entities.new, None, config)
entities_to_add = [entity for entity in entities.new if isinstance(entity, SensorEntity)]
for entity in entities_to_add:
@@ -662,7 +669,7 @@ async def create_individual_sensors(
source_entity = create_source_entity(sensor_config[CONF_ENTITY_ID], hass)
# For device-based profiles, attach the device entry to the source entity
source_entity = attach_configured_device_entry(hass, sensor_config, source_entity)
source_entity = resolve_source_device(hass, sensor_config, source_entity)
used_unique_ids = hass.data[DOMAIN].get(DATA_USED_UNIQUE_IDS, [])
@@ -699,7 +706,7 @@ async def create_individual_sensors(
create_energy_related_sensors(hass, sensor_config, energy_sensor, source_entity, config_entry),
)
attach_entities_to_resolved_device(config_entry, entities_to_add, hass, source_entity, sensor_config)
assign_device_to_entities(hass, config_entry, entities_to_add, source_entity, sensor_config)
hass.data[DOMAIN][DATA_CONFIGURED_ENTITIES].update(
{source_entity.entity_id: [(entity, context.is_yaml) for entity in entities_to_add]},
)
@@ -738,7 +745,7 @@ def check_entity_not_already_configured(
used_unique_ids: list[str],
context: CreationContext,
) -> None:
if source_entity.entity_id == DUMMY_ENTITY_ID:
if source_entity.is_dummy:
return
entity_id = source_entity.entity_id
@@ -389,9 +389,9 @@ def create_grouped_energy_sensor(
) -> EnergySensor:
name = generate_energy_sensor_name(sensor_config, group_name)
unique_id = sensor_config.get(CONF_UNIQUE_ID)
energy_unique_id = None
if unique_id:
energy_unique_id = f"{unique_id}_energy"
if not unique_id:
unique_id = generate_unique_id(sensor_config)
energy_unique_id = f"{unique_id}_energy"
entity_id = generate_energy_sensor_entity_id(
hass,
sensor_config,
@@ -43,6 +43,11 @@ class SensorType(StrEnum):
TRACKED = "tracked"
UNTRACKED = "untracked"
@property
def label(self) -> str:
"""Capitalized variant, used in the friendly names of the created sensors."""
return self.value.capitalize()
async def find_auto_tracked_power_entities(hass: HomeAssistant, exclude_entities: set[str] | None = None) -> set[str]:
"""Find tracked power entities."""
@@ -124,7 +129,7 @@ class TrackedPowerSensorFactory:
self.config,
energy_sensor,
utility_meter_config={CONF_UTILITY_METER_NET_CONSUMPTION: True, **self.config},
cost_name=str(sensor_type),
cost_name=sensor_type.label,
),
)
@@ -186,7 +191,7 @@ class TrackedPowerSensorFactory:
_LOGGER.debug("Creating tracked grouped power sensor, entities: %s", tracked_entities)
unique_id = f"{unique_id}_{sensor_type}_power"
entity_id = generate_power_sensor_entity_id(self.hass, self.config, name=sensor_type, unique_id=unique_id)
name = generate_power_sensor_name(self.config, name=sensor_type)
name = generate_power_sensor_name(self.config, name=sensor_type.label)
return GroupedPowerSensor(
self.hass,
sensor_config=self.config,
@@ -207,7 +212,7 @@ class TrackedPowerSensorFactory:
_LOGGER.debug("Creating untracked grouped power sensor")
unique_id = f"{unique_id}_{sensor_type}_power"
entity_id = generate_power_sensor_entity_id(self.hass, self.config, name=sensor_type, unique_id=unique_id)
name = generate_power_sensor_name(self.config, name=sensor_type)
name = generate_power_sensor_name(self.config, name=sensor_type.label)
return SubtractGroupSensor(
self.hass,
entity_id=entity_id,
@@ -226,7 +231,7 @@ class TrackedPowerSensorFactory:
"""Create an energy sensor for a power sensor."""
_LOGGER.debug("Creating %s grouped energy sensor", sensor_type)
unique_id = f"{power_sensor.unique_id}_{sensor_type}_energy"
name = generate_energy_sensor_name(self.config, sensor_type)
name = generate_energy_sensor_name(self.config, sensor_type.label)
entity_id = generate_energy_sensor_entity_id(self.hass, self.config, name=sensor_type, unique_id=unique_id)
return VirtualEnergySensor(
hass=self.hass,
+4 -7
View File
@@ -242,7 +242,7 @@ async def _get_power_profile(
power_profile = None
try:
model_info = await discovery_manager.extract_model_info_from_device_info(source_entity.entity_entry)
model_info = discovery_manager.extract_model_info(source_entity)
power_profile = await get_power_profile(
hass,
sensor_config,
@@ -482,7 +482,7 @@ class VirtualPowerSensor(PowerSensor, SensorEntity):
await self._strategy_instance.on_start(hass)
entities = self._track_entities
if (not entities and self._source_entity.entity_id == DUMMY_ENTITY_ID) or not entities:
if (not entities and self._source_entity.is_dummy) or not entities:
entities.add(DUMMY_ENTITY_ID)
for entity_id in entities:
new_state = (
@@ -558,7 +558,7 @@ class VirtualPowerSensor(PowerSensor, SensorEntity):
)
entities_to_track.extend(self._sub_profile_selector.get_tracking_entities())
if self._source_entity.entity_id != DUMMY_ENTITY_ID:
if not self._source_entity.is_dummy:
entities_to_track.append(self._source_entity.entity_id)
if self._availability_entity and self._availability_entity not in entities_to_track:
@@ -681,10 +681,7 @@ class VirtualPowerSensor(PowerSensor, SensorEntity):
return self._apply_power_adjustments(power, standby_power)
def _resolve_calculation_state(self, state: State) -> State | None:
if (
self._source_entity.entity_id == DUMMY_ENTITY_ID
and self._calculation_strategy != CalculationStrategy.MULTI_SWITCH
):
if self._source_entity.is_dummy and self._calculation_strategy != CalculationStrategy.MULTI_SWITCH:
if self._availability_entity and state.entity_id == self._availability_entity:
return State(DUMMY_ENTITY_ID, STATE_ON)
return state
+11 -2
View File
@@ -3,13 +3,14 @@ import logging
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers import entity_registry
from homeassistant.helpers import config_validation as cv, entity_registry
from homeassistant.helpers.event import TrackTemplate
from homeassistant.helpers.typing import ConfigType
import voluptuous as vol
from custom_components.powercalc.common import SourceEntity
from custom_components.powercalc.const import (
CONF_CURRENT_ENTITY,
CONF_POWER_FACTOR,
CONF_VOLTAGE,
OFF_STATES,
@@ -25,6 +26,7 @@ CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_VOLTAGE): vol.Coerce(float),
vol.Optional(CONF_POWER_FACTOR, default=0.9): vol.Coerce(float),
vol.Optional(CONF_CURRENT_ENTITY): cv.entity_id,
},
)
@@ -44,6 +46,7 @@ class WledStrategy(PowerCalculationStrategyInterface):
self._power_factor = config.get(CONF_POWER_FACTOR) or 0.9
self._light_entity = light_entity
self._standby_power: Decimal = Decimal(standby_power or 0)
self._configured_current_entity: str | None = config.get(CONF_CURRENT_ENTITY)
self._estimated_current_entity: str | None = None
async def calculate(self, entity_state: State) -> Decimal | None:
@@ -85,6 +88,9 @@ class WledStrategy(PowerCalculationStrategyInterface):
return evaluate_to_decimal(power)
async def find_estimated_current_entity(self) -> str:
if self._configured_current_entity:
return self._configured_current_entity
entity_reg = entity_registry.async_get(self._hass)
entity_id = f"sensor.{self._light_entity.object_id}_estimated_current"
entry = entity_reg.async_get(entity_id)
@@ -97,7 +103,10 @@ class WledStrategy(PowerCalculationStrategyInterface):
return entity
raise StrategyConfigurationError(
"No estimated current entity found. Probably brightness limiter not enabled. See documentation",
"No estimated current entity found. Probably brightness limiter not enabled, "
"or configured per output rather than globally. "
"You can also point Powercalc to a current entity yourself using the current_entity option. "
"See documentation",
)
def get_entities_to_track(self) -> list[str | TrackTemplate]:
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Entita proudu",
"power_factor": "Účiník",
"voltage": "Napětí"
},
"description": "Ujistěte se, že je v softwaru WLED povolen omezovač jasu. Viz také {docs_uri}",
"title": "Konfigurace WLED"
"title": "Konfigurace WLED",
"data_description": {
"current_entity": "Volitelně vyberte senzor poskytující odhadovaný proud v mA. Je potřeba pouze tehdy, když Powercalc nemůže automaticky najít senzor odhadovaného proudu WLED, například když je omezovač jasu nastaven pro jednotlivé výstupy."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "\"{profile}\" není známý podprofil. Dostupné podprofily: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "Možnosti WLED",
"data": {
"current_entity": "Entita proudu",
"power_factor": "Účiník",
"voltage": "Napětí"
},
"data_description": {
"current_entity": "Volitelně vyberte senzor poskytující odhadovaný proud v mA. Je potřeba pouze tehdy, když Powercalc nemůže automaticky najít senzor odhadovaného proudu WLED, například když je omezovač jasu nastaven pro jednotlivé výstupy."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Strømenhed",
"power_factor": "Effektfaktor",
"voltage": "Spænding"
},
"description": "Sørg for at aktivere lysstyrkebegrænser i WLED-software. Se også {docs_uri}",
"title": "WLED-konfiguration"
"title": "WLED-konfiguration",
"data_description": {
"current_entity": "Vælg eventuelt en sensor, der leverer den estimerede strøm i mA. Kun nødvendigt når Powercalc ikke automatisk kan finde WLED-sensoren for estimeret strøm, f.eks. når lysstyrkebegrænseren er konfigureret pr. output."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "\"{profile}\" er ikke en kendt underprofil. Tilgængelige underprofiler: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "WLED-indstillinger",
"data": {
"current_entity": "Strømenhed",
"power_factor": "Effektfaktor",
"voltage": "Spænding"
},
"data_description": {
"current_entity": "Vælg eventuelt en sensor, der leverer den estimerede strøm i mA. Kun nødvendigt når Powercalc ikke automatisk kan finde WLED-sensoren for estimeret strøm, f.eks. når lysstyrkebegrænseren er konfigureret pr. output."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Strom-Entität",
"power_factor": "Leistungs-Faktor",
"voltage": "Volt"
},
"description": "Stelle sicher, dass der Helligkeitsbegrenzer in der WLED-Software aktiviert ist. Siehe auch {docs_uri}",
"title": "WLED Konfiguration"
"title": "WLED Konfiguration",
"data_description": {
"current_entity": "Optional einen Sensor auswählen, der den geschätzten Strom in mA bereitstellt. Nur erforderlich, wenn Powercalc den WLED-Sensor für den geschätzten Strom nicht automatisch finden kann, z. B. wenn der Helligkeitsbegrenzer pro Ausgang konfiguriert ist."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "\"{profile}\" ist kein bekanntes Unterprofil. Verfügbare Unterprofile: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "WLED-Einstellungen",
"data": {
"current_entity": "Strom-Entität",
"power_factor": "Leistungsfaktor",
"voltage": "Spannung"
},
"data_description": {
"current_entity": "Optional einen Sensor auswählen, der den geschätzten Strom in mA bereitstellt. Nur erforderlich, wenn Powercalc den WLED-Sensor für den geschätzten Strom nicht automatisch finden kann, z. B. wenn der Helligkeitsbegrenzer pro Ausgang konfiguriert ist."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Current entity",
"power_factor": "Power factor",
"voltage": "Voltage"
},
"description": "Make sure to enable brightness limiter in WLED software. Also see {docs_uri}",
"title": "WLED config"
"title": "WLED config",
"data_description": {
"current_entity": "Optionally select a sensor providing the estimated current in mA. Only needed when Powercalc cannot find the WLED estimated current sensor automatically, for example when the brightness limiter is configured per output."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "\"{profile}\" is not a known sub profile. Available sub profiles: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "WLED options",
"data": {
"current_entity": "Current entity",
"power_factor": "Power factor",
"voltage": "Voltage"
},
"data_description": {
"current_entity": "Optionally select a sensor providing the estimated current in mA. Only needed when Powercalc cannot find the WLED estimated current sensor automatically, for example when the brightness limiter is configured per output."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Entidad de corriente",
"power_factor": "Factor de potencia",
"voltage": "Voltaje"
},
"description": "Asegúrate de habilitar el limitador de brillo en el software WLED. También consulta {docs_uri}",
"title": "Configuración WLED"
"title": "Configuración WLED",
"data_description": {
"current_entity": "Selecciona opcionalmente un sensor que proporcione la corriente estimada en mA. Solo es necesario cuando Powercalc no puede encontrar automáticamente el sensor de corriente estimada de WLED, por ejemplo, cuando el limitador de brillo está configurado por salida."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "\"{profile}\" no es un subperfil conocido. Subperfiles disponibles: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "Opciones de WLED",
"data": {
"current_entity": "Entidad de corriente",
"power_factor": "Factor de potencia",
"voltage": "Voltaje"
},
"data_description": {
"current_entity": "Selecciona opcionalmente un sensor que proporcione la corriente estimada en mA. Solo es necesario cuando Powercalc no puede encontrar automáticamente el sensor de corriente estimada de WLED, por ejemplo, cuando el limitador de brillo está configurado por salida."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Virtayksikkö",
"power_factor": "Tehokerroin",
"voltage": "Jännite"
},
"description": "Varmista, että kirkkauden rajoitin on käytössä WLED-ohjelmistossa. Katso myös {docs_uri}",
"title": "WLED määritys"
"title": "WLED määritys",
"data_description": {
"current_entity": "Valitse halutessasi anturi, joka ilmoittaa arvioidun virran mA:ina. Tarvitaan vain, jos Powercalc ei löydä WLED:n arvioidun virran anturia automaattisesti, esimerkiksi kun kirkkauden rajoitin on määritetty lähdöittäin."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "\"{profile}\" ei ole tunnettu aliprofiili. Saatavilla olevat aliprofiilit: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "WLED vaihtoehdot",
"data": {
"current_entity": "Virtayksikkö",
"power_factor": "Tehokerroin",
"voltage": "Jännite"
},
"data_description": {
"current_entity": "Valitse halutessasi anturi, joka ilmoittaa arvioidun virran mA:ina. Tarvitaan vain, jos Powercalc ei löydä WLED:n arvioidun virran anturia automaattisesti, esimerkiksi kun kirkkauden rajoitin on määritetty lähdöittäin."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Entité de courant",
"power_factor": "Facteur de puissance",
"voltage": "Tension"
},
"description": "Assurez-vous dactiver le limiteur de luminosité dans le logiciel WLED. Voir aussi {docs_uri}",
"title": "Configuration WLED"
"title": "Configuration WLED",
"data_description": {
"current_entity": "Sélectionnez éventuellement un capteur fournissant le courant estimé en mA. Nécessaire uniquement lorsque Powercalc ne peut pas trouver automatiquement le capteur de courant estimé WLED, par exemple lorsque le limiteur de luminosité est configuré par sortie."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "« {profile} » n'est pas un sous-profil connu. Sous-profils disponibles : {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "Options WLED",
"data": {
"current_entity": "Entité de courant",
"power_factor": "Facteur de puissance",
"voltage": "Tension"
},
"data_description": {
"current_entity": "Sélectionnez éventuellement un capteur fournissant le courant estimé en mA. Nécessaire uniquement lorsque Powercalc ne peut pas trouver automatiquement le capteur de courant estimé WLED, par exemple lorsque le limiteur de luminosité est configuré par sortie."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Áram-entitás",
"power_factor": "Teljesítménytényező",
"voltage": "Feszültség"
},
"description": "Győződjön meg arról, hogy engedélyezte a fényerő-korlátozót a WLED szoftverben. Lásd még: {docs_uri}",
"title": "WLED beállításai"
"title": "WLED beállításai",
"data_description": {
"current_entity": "Opcionálisan válasszon egy, az áram becsült értékét mA-ben biztosító érzékelőt. Csak akkor szükséges, ha a Powercalc nem találja meg automatikusan a WLED becsültáram-érzékelőjét, például ha a fényerőkorlátozó kimenetenként van beállítva."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "A \"{profile}\" nem ismert alprofil. Elérhető alprofilok: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "WLED beállításai",
"data": {
"current_entity": "Áram-entitás",
"power_factor": "Teljesítménytényező",
"voltage": "Feszültség"
},
"data_description": {
"current_entity": "Opcionálisan válasszon egy, az áram becsült értékét mA-ben biztosító érzékelőt. Csak akkor szükséges, ha a Powercalc nem találja meg automatikusan a WLED becsültáram-érzékelőjét, például ha a fényerőkorlátozó kimenetenként van beállítva."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Entità della corrente",
"power_factor": "Fattore di potenza",
"voltage": "Tensione"
},
"description": "Assicurati di abilitare il limitatore di luminosità nel software WLED. Vedi anche {docs_uri}",
"title": "Configurazione WLED"
"title": "Configurazione WLED",
"data_description": {
"current_entity": "Seleziona facoltativamente un sensore che fornisca la corrente stimata in mA. Necessario solo quando Powercalc non riesce a trovare automaticamente il sensore della corrente stimata WLED, ad esempio quando il limitatore di luminosità è configurato per uscita."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "\"{profile}\" non è un sottoprofilo conosciuto. Sottoprofili disponibili: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "Opzioni WLED",
"data": {
"current_entity": "Entità della corrente",
"power_factor": "Fattore di potenza",
"voltage": "Tensione"
},
"data_description": {
"current_entity": "Seleziona facoltativamente un sensore che fornisca la corrente stimata in mA. Necessario solo quando Powercalc non riesce a trovare automaticamente il sensore della corrente stimata WLED, ad esempio quando il limitatore di luminosità è configurato per uscita."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Strømenhet",
"power_factor": "Effektfaktor",
"voltage": "Spenning"
},
"description": "Sørg for å aktivere lysstyrkebegrenser i WLED-programvaren. Se også {docs_uri}",
"title": "WLED-konfigurasjon"
"title": "WLED-konfigurasjon",
"data_description": {
"current_entity": "Velg eventuelt en sensor som oppgir beregnet strøm i mA. Bare nødvendig når Powercalc ikke finner WLED-sensoren for beregnet strøm automatisk, for eksempel når lysstyrkebegrenseren er konfigurert per utgang."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "\"{profile}\" er ikke en kjent underprofil. Tilgjengelige underprofiler: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "WLED-innstillinger",
"data": {
"current_entity": "Strømenhet",
"power_factor": "Effektfaktor",
"voltage": "Spenning"
},
"data_description": {
"current_entity": "Velg eventuelt en sensor som oppgir beregnet strøm i mA. Bare nødvendig når Powercalc ikke finner WLED-sensoren for beregnet strøm automatisk, for eksempel når lysstyrkebegrenseren er konfigurert per utgang."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Stroomentiteit",
"power_factor": "Vermogensfactor",
"voltage": "Spanning"
},
"description": "Zorg ervoor dat u helderheidsbegrenzer inschakelt in WLED-software. Zie ook {docs_uri}",
"title": "WLED configuratie"
"title": "WLED configuratie",
"data_description": {
"current_entity": "Selecteer optioneel een sensor die de geschatte stroom in mA levert. Alleen nodig wanneer Powercalc de WLED-sensor voor geschatte stroom niet automatisch kan vinden, bijvoorbeeld wanneer de helderheidsbegrenzer per uitgang is geconfigureerd."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "\"{profile}\" is geen bekend subprofiel. Beschikbare subprofielen: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "WLED opties",
"data": {
"current_entity": "Stroomentiteit",
"power_factor": "Vermogensfactor",
"voltage": "Spanning"
},
"data_description": {
"current_entity": "Selecteer optioneel een sensor die de geschatte stroom in mA levert. Alleen nodig wanneer Powercalc de WLED-sensor voor geschatte stroom niet automatisch kan vinden, bijvoorbeeld wanneer de helderheidsbegrenzer per uitgang is geconfigureerd."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Encja prądu",
"power_factor": "Współczynnik mocy (cos φ)",
"voltage": "Napięcie"
},
"description": "Upewnij się, że włączono ogranicznik jasności w oprogramowaniu WLED. Zobacz również {docs_uri}",
"title": "Konfiguracja WLED"
"title": "Konfiguracja WLED",
"data_description": {
"current_entity": "Opcjonalnie wybierz sensor udostępniający szacowany prąd w mA. Jest to potrzebne tylko wtedy, gdy Powercalc nie może automatycznie znaleźć sensora szacowanego prądu WLED, na przykład gdy ogranicznik jasności jest skonfigurowany dla każdego wyjścia."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "\"{profile}\" nie jest znanym podprofilem. Dostępne podprofile: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "Opcje WLED",
"data": {
"current_entity": "Encja prądu",
"power_factor": "Współczynnik mocy (cos φ)",
"voltage": "Napięcie"
},
"data_description": {
"current_entity": "Opcjonalnie wybierz sensor udostępniający szacowany prąd w mA. Jest to potrzebne tylko wtedy, gdy Powercalc nie może automatycznie znaleźć sensora szacowanego prądu WLED, na przykład gdy ogranicznik jasności jest skonfigurowany dla każdego wyjścia."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Entidade de corrente",
"power_factor": "Fator de potência",
"voltage": "Voltagem"
},
"description": "Certifique-se de habilitar o limitador de brilho no software WLED. Veja também {docs_uri}",
"title": "Configuração WLED"
"title": "Configuração WLED",
"data_description": {
"current_entity": "Opcionalmente, selecione um sensor que forneça a corrente estimada em mA. Necessário apenas quando o Powercalc não consegue encontrar automaticamente o sensor de corrente estimada do WLED, por exemplo, quando o limitador de brilho está configurado por saída."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "\"{profile}\" não é um subperfil conhecido. Subperfis disponíveis: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "Opções de WLED",
"data": {
"current_entity": "Entidade de corrente",
"power_factor": "Fator de potência",
"voltage": "Voltagem"
},
"data_description": {
"current_entity": "Opcionalmente, selecione um sensor que forneça a corrente estimada em mA. Necessário apenas quando o Powercalc não consegue encontrar automaticamente o sensor de corrente estimada do WLED, por exemplo, quando o limitador de brilho está configurado por saída."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Entidade de corrente",
"power_factor": "Factor de potência",
"voltage": "Voltagem"
},
"description": "Certifique-se de que ativa o limitador de brilho no software WLED. Consulte também {docs_uri}",
"title": "Configuração WLED"
"title": "Configuração WLED",
"data_description": {
"current_entity": "Opcionalmente, selecione um sensor que forneça a corrente estimada em mA. Necessário apenas quando o Powercalc não consegue encontrar automaticamente o sensor de corrente estimada do WLED, por exemplo, quando o limitador de brilho está configurado por saída."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "\"{profile}\" não é um subperfil conhecido. Subperfis disponíveis: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "Opções WLED",
"data": {
"current_entity": "Entidade de corrente",
"power_factor": "Fator de potência",
"voltage": "Tensão"
},
"data_description": {
"current_entity": "Opcionalmente, selecione um sensor que forneça a corrente estimada em mA. Necessário apenas quando o Powercalc não consegue encontrar automaticamente o sensor de corrente estimada do WLED, por exemplo, quando o limitador de brilho está configurado por saída."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Entitate de curent",
"power_factor": "Factor de putere",
"voltage": "Voltaj"
},
"description": "Asigurați-vă că activați limitatorul de luminozitate în software-ul WLED. Vedeți și {docs_uri}",
"title": "Configurare WLED"
"title": "Configurare WLED",
"data_description": {
"current_entity": "Selectați opțional un senzor care furnizează curentul estimat în mA. Necesar doar atunci când Powercalc nu poate găsi automat senzorul WLED pentru curentul estimat, de exemplu atunci când limitatorul de luminozitate este configurat pentru fiecare ieșire."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "\"{profile}\" nu este un subprofil cunoscut. Subprofiluri disponibile: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "Opțiuni WLED",
"data": {
"current_entity": "Entitate de curent",
"power_factor": "Factorul de putere",
"voltage": "Voltaj"
},
"data_description": {
"current_entity": "Selectați opțional un senzor care furnizează curentul estimat în mA. Necesar doar atunci când Powercalc nu poate găsi automat senzorul WLED pentru curentul estimat, de exemplu atunci când limitatorul de luminozitate este configurat pentru fiecare ieșire."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Сущность тока",
"power_factor": "Коэффициент мощности",
"voltage": "Напряжение"
},
"description": "Убедитесь, что ограничитель яркости включён в программе WLED. Также см. [документацию]({docs_uri})",
"title": "Конфигурация WLED"
"title": "Конфигурация WLED",
"data_description": {
"current_entity": "При необходимости выберите сенсор, предоставляющий расчётный ток в мА. Это нужно только если Powercalc не может автоматически найти сенсор расчётного тока WLED, например когда ограничитель яркости настроен отдельно для каждого выхода."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "«{profile}» не является известным подпрофилем. Доступные подпрофили: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "Параметры WLED",
"data": {
"current_entity": "Сущность тока",
"power_factor": "Коэффициент мощности",
"voltage": "Напряжение"
},
"data_description": {
"current_entity": "При необходимости выберите сенсор, предоставляющий расчётный ток в мА. Это нужно только если Powercalc не может автоматически найти сенсор расчётного тока WLED, например когда ограничитель яркости настроен отдельно для каждого выхода."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Entita prúdu",
"power_factor": "Účinník",
"voltage": "Napätie"
},
"description": "Uistite sa, že ste povolili obmedzovač jasu v softvéri WLED. Pozri tiež {docs_uri}",
"title": "WLED konfigurácia"
"title": "WLED konfigurácia",
"data_description": {
"current_entity": "Voliteľne vyberte snímač poskytujúci odhadovaný prúd v mA. Potrebné len vtedy, keď Powercalc nedokáže automaticky nájsť snímač odhadovaného prúdu WLED, napríklad keď je obmedzovač jasu nastavený pre jednotlivé výstupy."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "\"{profile}\" nie je známy podprofil. Dostupné podprofily: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "WLED možnosti",
"data": {
"current_entity": "Entita prúdu",
"power_factor": "Účiník",
"voltage": "Napätie"
},
"data_description": {
"current_entity": "Voliteľne vyberte snímač poskytujúci odhadovaný prúd v mA. Potrebné len vtedy, keď Powercalc nedokáže automaticky nájsť snímač odhadovaného prúdu WLED, napríklad keď je obmedzovač jasu nastavený pre jednotlivé výstupy."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "Strömenhet",
"power_factor": "Effektfaktor",
"voltage": "Spänning"
},
"description": "Se till att aktivera ljusstyrkebegränsning i WLED-programvaran. Se även {docs_uri}",
"title": "WLED konfig"
"title": "WLED konfig",
"data_description": {
"current_entity": "Välj valfritt en sensor som anger den uppskattade strömmen i mA. Behövs endast när Powercalc inte kan hitta WLED-sensorn för uppskattad ström automatiskt, till exempel när ljusstyrkebegränsaren är konfigurerad per utgång."
}
}
}
},
@@ -603,6 +607,25 @@
"message": "\"{profile}\" är inte en känd underprofil. Tillgängliga underprofiler: {known_profiles}."
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "WLED alternativ",
"data": {
"current_entity": "Strömenhet",
"power_factor": "Effektfaktor",
"voltage": "Spänning"
},
"data_description": {
"current_entity": "Välj valfritt en sensor som anger den uppskattade strömmen i mA. Behövs endast när Powercalc inte kan hitta WLED-sensorn för uppskattad ström automatiskt, till exempel när ljusstyrkebegränsaren är konfigurerad per utgång."
}
},
"cost_options": {
@@ -581,11 +581,15 @@
},
"wled": {
"data": {
"current_entity": "电流实体",
"power_factor": "功率因数",
"voltage": "电压"
},
"description": "请确保在 WLED 软件中启用亮度限制器。另请参阅 {docs_uri}",
"title": "WLED 配置"
"title": "WLED 配置",
"data_description": {
"current_entity": "可选取一个提供估算电流(mA)的传感器。仅当 Powercalc 无法自动找到 WLED 估算电流传感器时才需要,例如亮度限制器按输出进行配置时。"
}
}
}
},
@@ -603,6 +607,25 @@
"message": "“{profile}”不是已知的子配置文件。可用的子配置文件:{known_profiles}。"
}
},
"entity": {
"sensor": {
"measure_session_status": {
"name": "Measure session status",
"state": {
"idle": "Idle",
"validating": "Validating",
"ready": "Ready",
"awaiting_confirmation": "Awaiting confirmation",
"running": "Running",
"cancelling": "Cancelling",
"cancelled": "Cancelled",
"completed": "Completed",
"failed": "Failed",
"resumable": "Resumable"
}
}
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
@@ -1083,8 +1106,12 @@
"wled": {
"title": "WLED 选项",
"data": {
"current_entity": "电流实体",
"power_factor": "功率因数",
"voltage": "电压"
},
"data_description": {
"current_entity": "可选取一个提供估算电流(mA)的传感器。仅当 Powercalc 无法自动找到 WLED 估算电流传感器时才需要,例如亮度限制器按输出进行配置时。"
}
},
"cost_options": {