197 files
This commit is contained in:
@@ -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()),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 d’activer 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": {
|
||||
|
||||
Reference in New Issue
Block a user