429 files

This commit is contained in:
Home Assistant Version Control committed 2026-08-30 02:43:31 +00:00
1 parent 472c19bceb
commit 963cce81a7
566 files changed
+50311 -47616

No files matched your search

+20 -3
View File
@@ -1,6 +1,7 @@
from dataclasses import dataclass
import math
import re
from typing import TYPE_CHECKING, cast
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant, split_entity_id
@@ -27,6 +28,15 @@ from .const import (
)
from .errors import SensorConfigurationError
_HAS_CHILD_DEVICES = hasattr(dr, "ChildDeviceEntry")
if TYPE_CHECKING:
from homeassistant.helpers.device_registry import AnyDeviceEntry as AnyDeviceEntry
else:
# AnyDeviceEntry was introduced together with child devices in HA 2026.9.
# Keep imports working on older supported versions, where every entry is a DeviceEntry.
AnyDeviceEntry = dr.DeviceEntry
@dataclass(frozen=True)
class SourceEntity:
@@ -38,7 +48,7 @@ class SourceEntity:
unique_id: str | None = None
name: str | None = None
entity_entry: er.RegistryEntry | None = None
device_entry: dr.DeviceEntry | None = None
device_entry: AnyDeviceEntry | None = None
config_entry_id: str | None = None
@property
@@ -92,6 +102,13 @@ def is_number(value: str) -> bool:
return math.isfinite(fvalue)
def get_main_device_entry(device_registry: dr.DeviceRegistry, device_id: str) -> dr.DeviceEntry | None:
"""Return a main device entry, excluding child devices on versions which support them."""
if _HAS_CHILD_DEVICES:
return device_registry.async_get(device_id, include_child_devices=False)
return cast(dr.DeviceEntry | None, device_registry.async_get(device_id)) # pragma: no cover
def create_source_entity(entity_id: str, hass: HomeAssistant) -> SourceEntity:
"""Create object containing all information about the source entity."""
@@ -138,7 +155,7 @@ def get_wrapped_entity_name(
entity_id: str,
object_id: str,
entity_entry: er.RegistryEntry | None,
device_entry: dr.DeviceEntry | None,
device_entry: AnyDeviceEntry | None,
) -> str:
"""Construct entity name based on the wrapped entity"""
if entity_entry is None:
@@ -156,7 +173,7 @@ def get_wrapped_entity_name(
def _get_device_entity_name(
entity_entry: er.RegistryEntry,
device_entry: dr.DeviceEntry | None,
device_entry: AnyDeviceEntry | None,
) -> str | None:
if not entity_entry.has_entity_name or device_entry is None:
return None
+29 -19
View File
@@ -1,5 +1,6 @@
from dataclasses import replace
import logging
from typing import cast
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE
@@ -12,37 +13,45 @@ import homeassistant.helpers.entity_registry as er
from homeassistant.helpers.entity_registry import RegistryEntry
from homeassistant.helpers.typing import ConfigType
from custom_components.powercalc.common import SourceEntity
from custom_components.powercalc.common import AnyDeviceEntry, SourceEntity
from custom_components.powercalc.const import CONF_AREA
_LOGGER = logging.getLogger(__name__)
_HAS_SINGLE_CONFIG_ENTRY = hasattr(DeviceEntry, "config_entry_id")
_HAS_CHILD_DEVICES = hasattr(device_registry, "ChildDeviceEntry")
def is_composite_device_id(hass: HomeAssistant, device_id: str) -> bool:
"""
Return whether a device ID identifies a legacy composite device.
Check for availability of async_is_composite_device_id, because this function is only available in HA >=2026.8
"""
"""Return whether a device ID identifies a legacy composite device."""
device_reg = device_registry.async_get(hass)
if _HAS_CHILD_DEVICES:
return (
device_reg.async_get(device_id) is not None
and device_reg.async_get(
device_id,
include_composite_devices=False,
)
is None
)
is_composite = getattr(device_reg, "async_is_composite_device_id", None)
if not callable(is_composite):
return False
return bool(is_composite(device_id))
return False # pragma: no cover
return bool(is_composite(device_id)) # pragma: no cover
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.
Composite devices are synthesized on lookup and are not included in registry enumeration.
"""
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)]
return (
list(device_reg.devices)
if _HAS_CHILD_DEVICES
else list(cast(dict[str, DeviceEntry], device_reg.devices).values()) # pragma: no cover
)
def get_related_device_ids(hass: HomeAssistant, device_id: str) -> set[str]:
@@ -58,7 +67,7 @@ def get_related_device_ids(hass: HomeAssistant, device_id: str) -> set[str]:
related.update(device.id for device in _get_composite_split_devices(device_reg, device_id))
device = device_reg.async_get(device_id)
if device is None:
if not isinstance(device, DeviceEntry):
return related
sibling_devices = _get_composite_split_devices(device_reg, getattr(device, "composite_device_id", None))
@@ -93,7 +102,7 @@ def _get_composite_split_devices(
return list(get_split_devices(composite_device_id))
def get_config_entry_ids(device: DeviceEntry) -> set[str]:
def get_config_entry_ids(device: AnyDeviceEntry) -> set[str]:
"""
Return the config entry IDs a device belongs to.
HA >=2026.8 splits composite devices, so a device belongs to exactly one config entry and
@@ -111,7 +120,8 @@ 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 get_non_composite_devices(hass) if config_entry_id in get_config_entry_ids(device)]
device_reg = device_registry.async_get(hass)
return list(device_registry.async_entries_for_config_entry(device_reg, config_entry_id))
def get_related_devices(hass: HomeAssistant, device_id: str) -> list[DeviceEntry]:
@@ -168,7 +178,7 @@ def get_device_entry(
sensor_config: ConfigType | None = None,
source_entity: SourceEntity | None = None,
config_entry: ConfigEntry | None = None,
) -> DeviceEntry | None:
) -> AnyDeviceEntry | None:
"""
Get device entry for a given powercalc entity configuration.
Prefer user configured device, when it is not set fallback to the same device as the source entity
@@ -193,7 +203,7 @@ def get_device_entry(
def bind_entity_to_registry_metadata(
hass: HomeAssistant,
entity_id: str | None,
device_entry: DeviceEntry | None,
device_entry: AnyDeviceEntry | None,
sensor_config: ConfigType | None,
) -> None:
"""Bind a Powercalc entity to configured registry metadata."""
@@ -213,7 +223,7 @@ def bind_entity_to_registry_metadata(
def bind_entity_to_device(
entity_reg: er.EntityRegistry,
entity_entry: RegistryEntry,
device_entry: DeviceEntry | None,
device_entry: AnyDeviceEntry | None,
) -> None:
"""Bind a Powercalc entity to the resolved device."""
# Home Assistant only consumes entity.device_entry while creating registry
+1 -1
View File
@@ -753,7 +753,7 @@ class DiscoveryManager:
def extract_model_info(self, source_entity: SourceEntity) -> ModelInfo | None:
"""Try to fetch manufacturer and model from the known device information."""
device_entry = source_entity.device_entry
if not device_entry:
if not isinstance(device_entry, dr.DeviceEntry):
return None
log_identifier = source_entity.log_identifier
+1 -1
View File
@@ -22,5 +22,5 @@
"requirements": [
"numpy>=1.21.1"
],
"version": "v1.25.1"
"version": "v1.25.2"
}
@@ -5,8 +5,11 @@ import json
from json import JSONDecodeError
import logging
import os
from pathlib import Path
import shutil
import tempfile
from typing import Any, NotRequired, TypedDict, cast
from urllib.parse import urlsplit
import aiohttp
from aiohttp import ClientError
@@ -33,6 +36,106 @@ ENDPOINT_LIBRARY = f"{API_URL}/library"
ENDPOINT_DOWNLOAD = f"{API_URL}/download"
TIMEOUT_SECONDS = 30
MODEL_JSON_RETRY_LIMIT = 2
ALLOWED_RESOURCE_HOSTS = frozenset({"github.com", "raw.githubusercontent.com"})
def _validate_resource_url(url: object) -> str:
"""Validate that a resource URL points to an allowed HTTPS host."""
if not isinstance(url, str):
raise ProfileDownloadError("Remote profile resource has an invalid URL")
try:
parsed_url = urlsplit(url)
is_allowed = (
parsed_url.scheme == "https"
and parsed_url.hostname in ALLOWED_RESOURCE_HOSTS
and parsed_url.username is None
and parsed_url.password is None
and parsed_url.port in (None, 443)
)
except ValueError as err:
raise ProfileDownloadError(f"Remote profile resource has an invalid URL: {url}") from err
if not is_allowed:
raise ProfileDownloadError(f"Remote profile resource URL is not allowed: {url}")
return url
def _resolve_resource_path(storage_path: str, resource_path: object) -> Path:
"""Resolve and validate a resource path within the profile storage directory."""
if not isinstance(resource_path, str) or not resource_path or "\0" in resource_path:
raise ProfileDownloadError("Remote profile resource has an invalid path")
relative_path = Path(resource_path)
if relative_path.is_absolute():
raise ProfileDownloadError(f"Remote profile resource path is not allowed: {resource_path}")
try:
storage_directory = Path(storage_path).resolve()
destination = (storage_directory / relative_path).resolve()
except (OSError, ValueError) as err:
raise ProfileDownloadError(f"Remote profile resource has an invalid path: {resource_path}") from err
if destination == storage_directory or not destination.is_relative_to(storage_directory):
raise ProfileDownloadError(f"Remote profile resource path is not allowed: {resource_path}")
return destination
def _validate_resources(resources: object, storage_path: str) -> list[tuple[str, Path]]:
"""Validate all resources in a remote profile response."""
if not isinstance(resources, list) or not all(isinstance(resource, dict) for resource in resources):
raise ProfileDownloadError("Remote profile response contains invalid resources")
return [
(_validate_resource_url(resource.get("url")), _resolve_resource_path(storage_path, resource.get("path")))
for resource in resources
]
def _sync_directory(directory: Path) -> None:
"""Persist a directory entry, so a completed rename survives an unclean shutdown."""
try:
directory_descriptor = os.open(directory, os.O_RDONLY)
except OSError: # pragma: no cover - directories cannot be opened on all platforms
return
try:
os.fsync(directory_descriptor)
except OSError: # pragma: no cover - directory fsync is not supported on all platforms
pass
finally:
os.close(directory_descriptor)
def _save_resource(data: bytes, path: Path) -> None:
"""Atomically save a downloaded resource to the local profile storage directory.
The contents are flushed to disk before the rename, and the directory entry is flushed
after it. Without both, a power loss shortly after an update can leave the new file name
pointing at unwritten data, which is how a cached profile ends up as invalid JSON.
"""
os.makedirs(path.parent, exist_ok=True)
file_descriptor, temporary_name = tempfile.mkstemp(
dir=path.parent,
prefix=f".{path.name}.",
suffix=".tmp",
)
temporary_path = Path(temporary_name)
try:
with os.fdopen(file_descriptor, "wb") as file_handle:
file_handle.write(data)
file_handle.flush()
os.fsync(file_handle.fileno())
os.replace(temporary_path, path)
_sync_directory(path.parent)
finally:
temporary_path.unlink(missing_ok=True)
def _save_resources(resources: list[tuple[bytes, Path]]) -> None:
"""Save all downloaded resources after every response has completed successfully."""
for data, path in resources:
_save_resource(data, path)
class LibraryModel(TypedDict):
@@ -64,6 +167,7 @@ class RemoteLoader(Loader):
self.model_lookup: dict[str, dict[str, list[LibraryModel]]] = {}
self.manufacturer_lookup: dict[str, set[str]] = {}
self.profile_hashes: dict[str, str] = {}
self._model_load_locks: dict[tuple[str, str], asyncio.Lock] = {}
async def initialize(self, prefer_cached: bool = False) -> None:
"""Initialize the loader.
@@ -188,18 +292,26 @@ class RemoteLoader(Loader):
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."""
"""Read library.json from local storage, None when it is missing or unusable.
A truncated or unreadable copy is reported as absent rather than raised, so the caller
falls through to a fresh download instead of failing setup on every restart.
"""
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))
try:
with open(local_path) as f:
return cast(dict[str, Any], json.load(f))
except (JSONDecodeError, OSError) as err:
_LOGGER.warning("Local library.json is unusable (%s), discarding it and downloading a fresh copy", err)
return None
def _load_local_library_json(self) -> dict[str, Any]:
"""Load library.json from local storage, raising when it is not there."""
"""Load library.json from local storage, raising when it is not usable."""
library_json = self._read_local_library_json()
if library_json is None:
raise ProfileDownloadError("Local library.json file not found")
raise ProfileDownloadError("Local library.json file not found or unusable")
return library_json
async def _download_remote_library_json(self) -> dict[str, Any] | None:
@@ -224,12 +336,7 @@ class RemoteLoader(Loader):
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)
await self.hass.async_add_executor_job(_save_resource, data, Path(local_path))
return cast(dict[str, Any], json.loads(data))
@@ -347,19 +454,51 @@ class RemoteLoader(Loader):
retry_count: int = 0,
) -> tuple[dict[str, Any], str] | None:
"""Load a model, downloading it if necessary, with retry logic."""
lock = self._model_load_locks.setdefault((manufacturer, model), asyncio.Lock())
async with lock:
return await self._load_model_locked(manufacturer, model, force_update, retry_count)
async def _load_model_locked(
self,
manufacturer: str,
model: str,
force_update: bool,
retry_count: int,
) -> tuple[dict[str, Any], str] | None:
"""Load a model while holding its per-profile lock."""
model_info = self._get_library_model(manufacturer, model)
storage_path = self.get_storage_path(manufacturer, model)
model_path = os.path.join(storage_path, "model.json")
if await self._needs_update(model_info, manufacturer, model, model_path, force_update):
await self._download_profile_with_retry(manufacturer, model, storage_path, model_path)
while True:
if await self._needs_update(model_info, manufacturer, model, model_path, force_update):
await self._download_profile_with_retry(manufacturer, model, storage_path, model_path)
try:
json_data = await self._load_model_json(model_path)
except JSONDecodeError as e:
return await self._handle_json_decode_error(e, manufacturer, model, retry_count)
try:
json_data = await self._load_model_json(model_path)
except JSONDecodeError as error:
if retry_count >= MODEL_JSON_RETRY_LIMIT:
_LOGGER.error(
"model.json remains invalid after %d redownload attempts for manufacturer: %s, model: %s",
MODEL_JSON_RETRY_LIMIT,
manufacturer,
model,
)
raise LibraryLoadingError("Failed to load model.json file") from error
return json_data, storage_path
retry_count += 1
force_update = True
_LOGGER.warning(
"model.json is not valid JSON for manufacturer: %s, model: %s; redownloading profile "
"(attempt %d of %d)",
manufacturer,
model,
retry_count,
MODEL_JSON_RETRY_LIMIT,
)
continue
return json_data, storage_path
def _get_library_model(self, manufacturer: str, model: str) -> LibraryModel:
"""Retrieve model info, or raise an error if not found."""
@@ -402,7 +541,7 @@ class RemoteLoader(Loader):
callback = partial(self.download_profile, manufacturer, model, storage_path, model_hash)
await self.download_with_retry(callback)
self.profile_hashes[f"{manufacturer}/{model}"] = model_hash
await self.hass.async_add_executor_job(self._write_profile_hashes, self.profile_hashes)
await self.hass.async_add_executor_job(self._write_profile_hashes, dict(self.profile_hashes))
except ProfileDownloadError as e:
path_exists, storage_path_exists = await self.hass.async_add_executor_job(
self._profile_paths_exist,
@@ -429,20 +568,6 @@ class RemoteLoader(Loader):
return await self.hass.async_add_executor_job(_load_json)
async def _handle_json_decode_error(
self,
error: JSONDecodeError,
manufacturer: str,
model: str,
retry_count: int,
) -> tuple[dict[str, Any], str] | None:
"""Handle JSON decode errors with retry logic."""
_LOGGER.error("model.json file is not valid JSON for manufacturer: %s, model: %s", manufacturer, model)
if retry_count < 2:
_LOGGER.debug("Retrying to load model.json file")
return await self.load_model(manufacturer, model, True, retry_count + 1)
raise LibraryLoadingError("Failed to load model.json file") from error
def get_storage_path(self, manufacturer: str, model: str) -> str:
"""Retrieve the storage path for a given manufacturer and model."""
return str(self.hass.config.path(STORAGE_DIR, BUILT_IN_LIBRARY_DIR, manufacturer, model))
@@ -480,13 +605,6 @@ class RemoteLoader(Loader):
endpoint = f"{ENDPOINT_DOWNLOAD}/{manufacturer}/{model}"
def _save_file(data: bytes, directory: str) -> None:
"""Save file from Github to local storage directory"""
path = os.path.join(storage_path, directory)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as f:
f.write(data)
session = async_get_clientsession(self.hass)
try:
@@ -496,17 +614,25 @@ class RemoteLoader(Loader):
raise ProfileDownloadError(f"Failed to download profile: {manufacturer}/{model}")
resources = await resp.json()
validated_resources = await self.hass.async_add_executor_job(
_validate_resources,
resources,
storage_path,
)
await self.hass.async_add_executor_job(lambda: os.makedirs(storage_path, exist_ok=True))
# Download the files
for resource in resources:
url = resource.get("url")
async with session.get(url) as resp:
downloaded_resources: list[tuple[bytes, Path]] = []
for url, destination in validated_resources:
async with session.get(url, allow_redirects=False) as resp:
if resp.status != 200:
raise ProfileDownloadError(f"Failed to download github URL: {url}")
contents = await resp.read()
await self.hass.async_add_executor_job(_save_file, contents, resource.get("path"))
downloaded_resources.append((contents, destination))
await self.hass.async_add_executor_job(_save_resources, downloaded_resources)
except (TimeoutError, aiohttp.ClientError) as e:
raise ProfileDownloadError(f"Failed to download profile: {manufacturer}/{model}") from e
@@ -515,18 +641,25 @@ class RemoteLoader(Loader):
return str(self.hass.config.path(STORAGE_DIR, BUILT_IN_LIBRARY_DIR, ".profile_hashes"))
def _load_profile_hashes(self) -> dict[str, str]:
"""Load profile hashes from local storage"""
"""Load profile hashes from local storage.
An unusable file is treated as empty rather than raised: the hashes are only a cache
validity marker, so the worst case is that every profile is downloaded once more.
"""
path = self._get_profile_hashes_path()
if not os.path.exists(path):
return {}
with open(path) as f:
return json.load(f) # type: ignore[no-any-return]
try:
with open(path) as f:
return cast(dict[str, str], json.load(f))
except (JSONDecodeError, OSError) as err:
_LOGGER.warning("Profile hashes file is unusable (%s), profiles will be downloaded again", err)
return {}
def _write_profile_hashes(self, hashes: dict[str, str]) -> None:
"""Write profile hashes to local storage"""
"""Write profile hashes to local storage, atomically."""
path = self._get_profile_hashes_path()
with open(path, "w") as json_file:
json.dump(hashes, json_file, indent=4)
_save_resource(json.dumps(hashes, indent=4).encode(), Path(path))
@@ -69,6 +69,7 @@ class DiscoveryBy(StrEnum):
CONFIG_ENTRY = "config_entry"
DEVICE = "device"
ENTITY = "entity"
MANUAL = "manual"
@dataclass(frozen=True)
@@ -3,6 +3,7 @@ import re
from typing import Any, NamedTuple, Protocol
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.device_registry import DeviceEntry
from custom_components.powercalc.common import SourceEntity
from custom_components.powercalc.errors import PowercalcSetupError
@@ -197,7 +198,7 @@ class ModelIdMatcher(SubProfileMatcher):
def match(self, entity_state: State, source_entity: SourceEntity) -> str | None:
device_entry = source_entity.device_entry
if not device_entry:
if not isinstance(device_entry, DeviceEntry):
return None
if device_entry.model_id == self._model_id:
+7 -7
View File
@@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, issue_registry as ir, selector
import voluptuous as vol
from custom_components.powercalc.common import create_source_entity
from custom_components.powercalc.common import create_source_entity, get_main_device_entry
from custom_components.powercalc.const import CONF_MODEL, CONF_SUB_PROFILE, DOMAIN, ISSUE_COMPOSITE_DEVICE_ID
from custom_components.powercalc.device_binding import is_composite_device_id
from custom_components.powercalc.flow_helper.schema import build_sub_profile_schema
@@ -32,7 +32,7 @@ class SubProfileRepairFlow(RepairsFlow):
new_data = self._config_entry.data.copy()
new_data[CONF_MODEL] = f"{new_data[CONF_MODEL]}/{user_input[CONF_SUB_PROFILE]}"
self._hass.config_entries.async_update_entry(self._config_entry, data=new_data)
return self.async_create_entry(title="", data={}) # type: ignore[no-any-return]
return self.async_create_entry(title="", data={})
source_entity = create_source_entity(self._config_entry.data[CONF_ENTITY_ID], self._hass)
profile = await get_power_profile(self.hass, dict(self._config_entry.data), source_entity)
@@ -43,7 +43,7 @@ class SubProfileRepairFlow(RepairsFlow):
if remarks:
remarks = "\n\n" + remarks
return self.async_show_form( # type: ignore[no-any-return]
return self.async_show_form(
step_id="sub_profile",
data_schema=sub_profile_schema,
description_placeholders={
@@ -73,14 +73,14 @@ class CompositeDeviceIdRepairFlow(RepairsFlow):
if user_input is not None:
entry = self.hass.config_entries.async_get_entry(self._entry_id)
if entry is None:
return self.async_abort(reason="entry_removed") # type: ignore[no-any-return]
return self.async_abort(reason="entry_removed")
selected_device_id = user_input.get(CONF_DEVICE)
if selected_device_id is not None:
device_reg = dr.async_get(self.hass)
if (
not isinstance(selected_device_id, str)
or device_reg.async_get(selected_device_id) is None
or get_main_device_entry(device_reg, selected_device_id) is None
or is_composite_device_id(self.hass, selected_device_id)
):
errors["base"] = "invalid_device"
@@ -99,9 +99,9 @@ class CompositeDeviceIdRepairFlow(RepairsFlow):
f"{ISSUE_COMPOSITE_DEVICE_ID}_{self._entry_id}",
)
await self.hass.config_entries.async_reload(self._entry_id)
return self.async_create_entry(title="", data={}) # type: ignore[no-any-return]
return self.async_create_entry(title="", data={})
return self.async_show_form( # type: ignore[no-any-return]
return self.async_show_form(
step_id="select_device",
data_schema=vol.Schema({vol.Optional(CONF_DEVICE): selector.DeviceSelector()}),
errors=errors,
@@ -239,7 +239,10 @@ class DailyEnergySensor(EnergySensor, RestoreEntity, SensorEntity):
self._state,
)
self.async_schedule_update_ha_state()
self._last_updated = dt_util.now().timestamp()
# Reuse the timestamp captured by calculate_delta. Reading the clock again here can
# cross a whole-second boundary and make the next interval one second too short.
assert self._last_delta_calculate is not None
self._last_updated = self._last_delta_calculate
self._update_timer_removal = async_track_time_interval(
self.hass,
@@ -1,6 +1,5 @@
from decimal import Decimal
from homeassistant.components import lawn_mower, vacuum
from homeassistant.core import State
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import TrackTemplate
@@ -23,11 +22,6 @@ CONFIG_SCHEMA = vol.Schema(
},
)
STATE_BASED_ENTITY_DOMAINS = [
vacuum.DOMAIN,
lawn_mower.DOMAIN,
]
class FixedStrategy(PowerCalculationStrategyInterface):
def __init__(
@@ -68,12 +62,6 @@ class FixedStrategy(PowerCalculationStrategyInterface):
"fixed_mandatory",
)
if self._source_entity.domain in STATE_BASED_ENTITY_DOMAINS and self._per_state_power is None:
raise StrategyConfigurationError(
"This entity can only work with 'states_power' not 'power'",
"fixed_states_power_only",
)
def get_entities_to_track(self) -> list[str | TrackTemplate]:
"""Return entities that should be tracked."""
track_templates: list[str | TrackTemplate] = []
@@ -69,6 +69,7 @@ class LinearStrategy(PowerCalculationStrategyInterface):
self._attribute: str | None = None
self._standby_power = standby_power
self._initialized: bool = False
self._missing_attribute_warned: bool = False
self._calibration: list[tuple[int, float]] | None = None
async def initialize(self) -> None:
@@ -199,12 +200,15 @@ class LinearStrategy(PowerCalculationStrategyInterface):
value = entity_state.attributes.get(self._attribute)
if value is None:
_LOGGER.warning(
"No %s attribute for entity: %s",
self._attribute,
entity_state.entity_id,
)
if not self._missing_attribute_warned:
_LOGGER.warning(
"No %s attribute for entity: %s",
self._attribute,
entity_state.entity_id,
)
self._missing_attribute_warned = True
return None
self._missing_attribute_warned = False
# Convert volume level to 0-100 range
if self._attribute == ATTR_MEDIA_VOLUME_LEVEL:
if entity_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is True:
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Musíte zadat alespoň jednu z možností „Hodnota“ nebo „Šablona hodnoty“",
"entity_mandatory": "Pro všechny strategie kromě playbook je nutné vybrat entitu",
"fixed_mandatory": "Musíte zadat alespoň jednu z následujících možností: pevnou hodnotu, šablonu nebo mapování stavů",
"fixed_states_power_only": "Tato entita může pracovat pouze s 'states_power', ne s 'power'",
"group_mandatory": "Musíte definovat alespoň podskupiny nebo entity výkonu a energie",
"linear_mandatory": "Musíte zadat alespoň jednu z možností max_power nebo calibrate",
"linear_min_higher_as_max": "Maximální spotřeba nebůže být nižší, nežli minimální spotřeba",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "Musíte zadat buď cenu energie, nebo senzor ceny energie",
"fixed_mandatory": "Musíte zadat alespoň jednu z možností Výkon, šablona výkonu nebo výkon podle stavů",
"fixed_states_power_only": "Tato entita může pracovat pouze s 'states_power', ne s 'power'",
"group_mandatory": "Musíte definovat alespoň podskupiny nebo entity výkonu a energie",
"linear_mandatory": "Musíte zadat alespoň jednu z možností max_power nebo calibrate",
"linear_min_higher_as_max": "Maximální spotřeba nebůže být nižší, nežli minimální spotřeba",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Du skal angive mindst én af Værdi eller Værdiskabelon",
"entity_mandatory": "Der skal vælges en entitet for alle andre strategier end playbook",
"fixed_mandatory": "Du skal angive mindst én af Effekt, Effektskabelon eller Effekt efter tilstand",
"fixed_states_power_only": "Denne entitet kan kun bruges med 'states_power', ikke 'power'",
"group_mandatory": "Du skal som minimum definere undergrupper eller effekt- og energientiteter",
"linear_mandatory": "Du skal angive mindst én af max_power eller calibrate",
"linear_min_higher_as_max": "Maksimal effekt kan ikke være lavere end minimal effekt",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "Du skal angive enten en energipris eller en energiprissensor",
"fixed_mandatory": "Du skal angive mindst én af Effekt, Effektskabelon eller Effekt efter tilstand",
"fixed_states_power_only": "Denne entitet kan kun bruges med 'states_power', ikke 'power'",
"group_mandatory": "Du skal som minimum definere undergrupper eller effekt- og energientiteter",
"linear_mandatory": "Du skal angive mindst én af max_power eller calibrate",
"linear_min_higher_as_max": "Maksimal effekt kan ikke være lavere end minimal effekt",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Du musst mindestens einen Wert oder ein Werte-Template angeben",
"entity_mandatory": "Die Auswahl einer Entität ist für jede Strategie mit Ausnahme des Playbooks erforderlich",
"fixed_mandatory": "Du musst mindestens Leistung, Leistungs-Template oder Leistung je Zustand angeben",
"fixed_states_power_only": "Diese Entität kann nur mit Leistung je Zustand und nicht mit Leistung arbeiten",
"group_mandatory": "Du musst mindestens Untergruppen oder Leistungs- und Energie-Entitäten definieren",
"linear_mandatory": "Du musst mindestens die minimale Leistung oder den Kalibrierungswert angeben",
"linear_min_higher_as_max": "Die maximale Leistung kann nicht niedriger als die minimale Leistung sein",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "Du musst entweder einen Energiepreis oder einen Energiepreissensor angeben",
"fixed_mandatory": "Sie müssen mindestens eines von Leistung, Leistung-Template oder Leistung je Zustand angeben",
"fixed_states_power_only": "Diese Entität kann nur mit Leistung je Zustand und nicht mit Leistung arbeiten",
"group_mandatory": "Sie müssen mindestens Untergruppen oder Leistung- und Energie-Entitäten definieren",
"linear_mandatory": "Sie müssen mindestens Maximale Leistung oder Kalibrierung angeben",
"linear_min_higher_as_max": "Maximale Leistung kann nicht niedriger sein als Minimale Leistung",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "You must supply at least one of Value or Value template",
"entity_mandatory": "Selecting an entity is required for any strategy other than playbook",
"fixed_mandatory": "You must supply at least one of Power, Power template or States power",
"fixed_states_power_only": "This entity can only work with 'states_power' not 'power'",
"group_mandatory": "You must define at least subgroups or power and energy-entities",
"linear_mandatory": "You must supply at least one of max_power or calibrate",
"linear_min_higher_as_max": "Max power cannot be lower than min power",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "You must supply either an energy price or an energy price sensor",
"fixed_mandatory": "You must supply at least one of Power, Power template or States power",
"fixed_states_power_only": "This entity can only work with 'states_power' not 'power'",
"group_mandatory": "You must define at least subgroups or power and energy-entities",
"linear_mandatory": "You must supply at least one of max_power or calibrate",
"linear_min_higher_as_max": "Max power cannot be lower than min power",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Debes proveer al menos un valor o valor en plantilla",
"entity_mandatory": "El sensor ya está configurado, especifique un identificador único",
"fixed_mandatory": "Debes proveer al menos uno de los siguientes: potencia, potencia en plantilla, o potencia en un estado",
"fixed_states_power_only": "Esta entidad solamente funciona con \"states_power\" y no con \"power\"",
"group_mandatory": "Debes definir como mínimo subgrupos o potencia y entidades de energía",
"linear_mandatory": "Debes proveer al menos potencia máxima (max_power) o calibrar",
"linear_min_higher_as_max": "La potencia máxima no puede ser menos que la potencia mínima",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "Debe proporcionar un precio de energía o un sensor de precio de energía.",
"fixed_mandatory": "Debe suministrar al menos uno de: potencia, plantilla de potencia o estados de potencia",
"fixed_states_power_only": "Esta entidad sólo puede funcionar con 'states_power' no 'power'",
"group_mandatory": "Debes definir como mínimo subgrupos o potencia y entidades de energía",
"linear_mandatory": "Debes proveer al menos potencia máxima (max_power) o calibrar",
"linear_min_higher_as_max": "La potencia máxima no puede ser menos que la potencia mínima",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Anna vähintään value tai value_template",
"entity_mandatory": "Entiteetti on valittava kaikille muille strategioille paitsi playbook-strategialle",
"fixed_mandatory": "Anna vähintään power, power_template tai states_power",
"fixed_states_power_only": "Tämä entiteetti toimii vain states_power-asetuksella, ei power-asetuksella",
"group_mandatory": "Määritä vähintään aliryhmät tai teho- ja energiaentiteetit",
"linear_mandatory": "Anna vähintään max_power tai calibrate",
"linear_min_higher_as_max": "Enimmäisteho ei voi olla pienempi kuin vähimmäisteho",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "Anna joko energian hinta tai energian hinta-anturi",
"fixed_mandatory": "Anna vähintään power-, power_template- tai states_power-arvo",
"fixed_states_power_only": "Tämä entiteetti voi toimia vain 'states_power', ei 'power':n kanssa",
"group_mandatory": "Sinun on määritettävä vähintään alaryhmät tai teho- ja energiaentiteetit",
"linear_mandatory": "Anna vähintään max_power tai calibrate",
"linear_min_higher_as_max": "Enimmäisteho ei voi olla pienempi kuin vähimmäisteho",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Vous devez fournir au moins une valeur ou un modèle de valeur",
"entity_mandatory": "La sélection d'une entité est requise pour toute stratégie autre que le playbook",
"fixed_mandatory": "Vous devez fournir au moins une alimentation, un modèle d'alimentation ou un état d'alimentation",
"fixed_states_power_only": "Cette entité ne peut fonctionner qu'avec 'states_power' et non 'power'",
"group_mandatory": "Vous devez définir au moins des sous-groupes ou des entités de puissance et d'énergie",
"linear_mandatory": "Vous devez spécifier au moins la puissance minimale ou la valeur d'étalonnage",
"linear_min_higher_as_max": "La puissance maximale ne peut pas être inférieure à la puissance minimale",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "Vous devez fournir soit un prix de l'énergie, soit un capteur de prix de l'énergie",
"fixed_mandatory": "Vous devez fournir au moins une alimentation, un modèle d'alimentation ou une alimentation des états",
"fixed_states_power_only": "Cette entité ne peut fonctionner qu'avec des 'Etat de puissance' et non 'Puissance'",
"group_mandatory": "Vous devez définir au moins des sous-groupes ou des entités de puissance et d'énergie",
"linear_mandatory": "Vous devez spécifier au moins la puissance minimale ou la valeur d'étalonnage",
"linear_min_higher_as_max": "La puissance maximale ne peut pas être inférieure à la puissance minimale",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Adja meg legalább a value vagy value_template egyikét",
"entity_mandatory": "A playbook stratégián kívül minden stratégiához entitást kell választani",
"fixed_mandatory": "Adja meg legalább a power, power_template vagy states_power egyikét",
"fixed_states_power_only": "Ez az entitás csak a „states_power” beállítással működik, a „power” beállítással nem",
"group_mandatory": "Adjon meg legalább alcsoportokat, illetve energia- vagy teljesítményentitásokat",
"linear_mandatory": "Adja meg legalább a max_power vagy a calibrate értéket",
"linear_min_higher_as_max": "A maximális teljesítmény nem lehet kisebb a minimális teljesítménynél",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "Adja meg az energiaárat vagy az energiaár-érzékelőt",
"fixed_mandatory": "Adja meg legalább a power, power_template vagy states_power egyikét",
"fixed_states_power_only": "Ez az entitás csak a 'states_power'-vel működik, nem a 'power'-val",
"group_mandatory": "Meg kell határoznia legalább alcsoportokat vagy teljesítmény- és energia-entitásokat",
"linear_mandatory": "A max_power vagy a calibrate közül legalább egyet meg kell adnia",
"linear_min_higher_as_max": "A maximális teljesítmény nem lehet kisebb a minimális teljesítménynél",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "È necessario fornire almeno o un Valore o un Modello valore",
"entity_mandatory": "La selezione di un'entità è richiesta per qualsiasi strategia diversa dal playbook",
"fixed_mandatory": "È necessario fornire almeno uno tra Potenza, Modello di potenza o Stati di potenza",
"fixed_states_power_only": "Questa entità può funzionare solo con 'states_power' non 'power'",
"group_mandatory": "È necessario definire almeno sottogruppi o entità di potenza e di energia",
"linear_mandatory": "È necessario fornire almeno valore di potenza massima o calibrare",
"linear_min_higher_as_max": "La potenza massima non può essere inferiore a quella minima",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "È necessario fornire un prezzo dell'energia o un sensore del prezzo dell'energia",
"fixed_mandatory": "È necessario fornire almeno uno tra Potenza, Modello di potenza o Stati di potenza",
"fixed_states_power_only": "Questa entità può funzionare solo con 'states_power' non 'power'",
"group_mandatory": "È necessario definire almeno sottogruppi o entità di potenza e entità di energia",
"linear_mandatory": "È necessario fornire almeno picco di massima potenza o calibrare",
"linear_min_higher_as_max": "La potenza massima non può essere inferiore a quella minima",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Du må oppgi minst én av value eller value_template",
"entity_mandatory": "Du må velge en entitet for alle strategier unntatt playbook",
"fixed_mandatory": "Du må oppgi minst én av power, power_template eller states_power",
"fixed_states_power_only": "Denne entiteten kan bare bruke «states_power», ikke «power»",
"group_mandatory": "Du må definere minst undergrupper eller effekt- og energientiteter",
"linear_mandatory": "Du må oppgi minst én av max_power eller calibrate",
"linear_min_higher_as_max": "Maksimal effekt kan ikke være lavere enn minimal effekt",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "Du må oppgi enten en energipris eller en energiprissensor",
"fixed_mandatory": "Du må oppgi minst én av power, power_template eller states_power",
"fixed_states_power_only": "Denne entiteten kan bare bruke 'states_power', ikke 'power'",
"group_mandatory": "Du må definere minst undergrupper eller effekt- og energientiteter",
"linear_mandatory": "Du må oppgi minst én av max_power eller calibrate",
"linear_min_higher_as_max": "Maks effekt kan ikke være lavere enn min effekt",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Je moet minimaal waarde of waarde template opgeven",
"entity_mandatory": "Je moet verplicht een entiteit opgeven voor iedere andere strategie dan playbook",
"fixed_mandatory": "Je dient minimaal een van de volgende velden te definiëren: Vermogen, Vermogen template of Vermogen per status",
"fixed_states_power_only": "Deze entiteit kan alleen functioneren met 'states_power', niet 'power'",
"group_mandatory": "Je dient minimaal een van de volgende velden te definiëren: sub groepen, vermogen entiteiten of energie entiteiten",
"linear_mandatory": "Je moet tenminste één van Max vermogen of Kalibratie waardes opgeven",
"linear_min_higher_as_max": "Maximale vermogen kan niet lager zijn dan minimale vermogen",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "U moet een energieprijs of een energieprijssensor aanleveren",
"fixed_mandatory": "Je dient minimaal een van de volgende velden te definieren: vermogen, vermogen template of vermogen per status",
"fixed_states_power_only": "Deze entiteit kan alleen functioneren met 'states_power', niet 'power'",
"group_mandatory": "Je dient minimaal een van de volgende velden te definieren: sub groepen, vermogen entiteiten of energie entiteiten",
"linear_mandatory": "Je moet mimimaal max vermogen of kalibratie waardes instellen",
"linear_min_higher_as_max": "Maximale vermogen kan niet lager zijn dan mimimale vermogen",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Musisz podać co najmniej jedną Wartość lub Szablon wartości",
"entity_mandatory": "Wybranie encji jest wymagane w przypadku każdej strategii innej niż playbook",
"fixed_mandatory": "Musisz podać co najmniej jedno z: Moc, Szablon mocy lub Moce stanów",
"fixed_states_power_only": "Ta encja może działać tylko ze 'states_power', a nie z 'power'",
"group_mandatory": "Musisz zdefiniować przynajmniej podgrupy lub encje mocy i energii",
"linear_mandatory": "Musisz dostarczyć co najmniej jedno z: Maksymalna moc lub Kalibracja",
"linear_min_higher_as_max": "Maksymalna moc nie może być mniejsza niż Minimalna moc",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "Należy podać cenę energii lub czujnik ceny energii",
"fixed_mandatory": "Musisz podać co najmniej jedno z: Moc, Szablon mocy lub Moce stanów",
"fixed_states_power_only": "Ta encja może działać tylko ze 'states_power', a nie z 'power'",
"group_mandatory": "Musisz zdefiniować przynajmniej podgrupy lub encje mocy i energii",
"linear_mandatory": "Musisz dostarczyć co najmniej jedno z: Maksymalna moc lub Kalibracja",
"linear_min_higher_as_max": "Maksymalna moc nie może być mniejsza niż Minimalna moc",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Você deve fornecer pelo menos um modelo de Valor ou Valor",
"entity_mandatory": "A seleção de uma entidade é necessária para qualquer estratégia que não seja roteiro",
"fixed_mandatory": "Você deve fornecer pelo menos uma potência, um modelo de potência ou potências por estado",
"fixed_states_power_only": "Esta entidade só pode trabalhar com 'states_power' e não 'power'",
"group_mandatory": "Você deve definir pelo menos subgrupos ou entidades de potência e consumo",
"linear_mandatory": "Você deve fornecer pelo menos um de max_power ou calibrar",
"linear_min_higher_as_max": "A potência máxima não pode ser inferior à potência mínima",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "Você deve fornecer um preço de energia ou um sensor de preço de energia",
"fixed_mandatory": "Você deve fornecer pelo menos uma potência, um modelo de potência ou potências por estado",
"fixed_states_power_only": "Esta entidade só pode trabalhar com 'states_power' e não 'power'",
"group_mandatory": "Você deve definir pelo menos subgrupos ou entidades de potência e consumo",
"linear_mandatory": "Você deve fornecer pelo menos um de max_power ou calibrar",
"linear_min_higher_as_max": "A potência máxima não pode ser inferior à potência mínima",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Deve fornecer pelo menos um modelo de Valor ou Valor",
"entity_mandatory": "A selecção de entidade é obrigatória para qualquer estratégia diferente de 'Sequência'",
"fixed_mandatory": "Deve fornecer pelo menos uma potência, um modelo de potência ou potências por estado",
"fixed_states_power_only": "Esta entidade só pode trabalhar com 'states_power' e não 'power'",
"group_mandatory": "Deve definir pelo menos subgrupos ou entidades de poder e energia",
"linear_mandatory": "Deve fornecer pelo menos um de max_power ou calibrar",
"linear_min_higher_as_max": "A potência máxima não pode ser inferior à potência mínima",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "Deve fornecer um preço de energia ou um sensor de preço da energia",
"fixed_mandatory": "Deve fornecer pelo menos uma potência, um modelo de potência ou potências por estado",
"fixed_states_power_only": "Esta entidade só pode trabalhar com 'states_power' e não 'power'",
"group_mandatory": "Deve definir pelo menos subgrupos ou entidades de poder e energia",
"linear_mandatory": "Deve fornecer pelo menos um de max_power ou calibrar",
"linear_min_higher_as_max": "A potência máxima não pode ser inferior à potência mínima",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Trebuie să furnizați cel puțin o Valoare sau șablonul Valoare",
"entity_mandatory": "Selectarea unei entități este necesară pentru orice altă strategie decât playbook",
"fixed_mandatory": "Trebuie să furnizați cel puțin o putere, șablon putere sau State power",
"fixed_states_power_only": "Această entitate poate funcționa numai cu 'states_power' nu cu 'power'",
"group_mandatory": "Trebuie să definiți cel puțin subgrupuri sau entități de putere și energie",
"linear_mandatory": "Trebuie să furnizați cel puțin unul dintre max_power sau să calibrați",
"linear_min_higher_as_max": "Puterea maximă nu poate fi mai mică decât puterea minimă",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "Trebuie să furnizați fie un preț al energiei, fie un senzor al prețului energiei",
"fixed_mandatory": "Trebuie să furnizați cel puțin o putere, șablon putere sau State power",
"fixed_states_power_only": "Această entitate poate funcționa numai cu 'states_power' nu cu 'power'",
"group_mandatory": "Trebuie să definiți cel puțin subgrupuri sau entități de putere și energie",
"linear_mandatory": "Trebuie să furnizați cel puțin unul dintre max_power sau să calibrați",
"linear_min_higher_as_max": "Puterea maximă nu poate fi mai mică decât puterea minimă",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Необходимо указать хотя бы одно из: Значение или Шаблон значения",
"entity_mandatory": "Выбор объекта обязателен для любой стратегии, кроме playbook",
"fixed_mandatory": "Необходимо указать хотя бы одно из: Мощность, Шаблон мощности или Мощность по состояниям",
"fixed_states_power_only": "Этот объект может работать только с 'states_power', а не 'power'",
"group_mandatory": "Необходимо определить хотя бы подгруппы или объекты мощности и энергии",
"linear_mandatory": "Необходимо указать хотя бы одно из: max_power или calibrate",
"linear_min_higher_as_max": "Максимальная мощность не может быть меньше минимальной",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "Вы должны предоставить либо цену на энергию, либо датчик цены на энергию.",
"fixed_mandatory": "Необходимо указать хотя бы одно из: Мощность, Шаблон мощности или Мощность по состояниям",
"fixed_states_power_only": "Этот объект может работать только с 'states_power', а не 'power'",
"group_mandatory": "Необходимо определить хотя бы подгруппы или объекты мощности и энергии",
"linear_mandatory": "Необходимо указать хотя бы одно из: max_power или calibrate",
"linear_min_higher_as_max": "Максимальная мощность не может быть меньше минимальной",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Musíte dodať aspoň jednu šablónu hodnoty alebo hodnoty",
"entity_mandatory": "Výber subjektu je potrebný pre akúkoľvek inú stratégiu, než je stratégia playbook",
"fixed_mandatory": "Musíte zadať aspoň jednu z možností: fixnú hodnotu, šablónu alebo mapovanie stavov",
"fixed_states_power_only": "Táto entita môže pracovať iba s 'states_power', nie 'power'",
"group_mandatory": "Musíte definovať aspoň podskupiny alebo silové a energetické entity",
"linear_mandatory": "Musíte dodať aspoň jeden z max_power alebo calibrate",
"linear_min_higher_as_max": "Maximálny výkon nemôže byť nižší ako minimálny výkon",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "Musíte dodať buď cenu energie, alebo snímač ceny energie",
"fixed_mandatory": "Musíte zadať aspoň jednu z možností: fixnú hodnotu, šablónu alebo mapovanie stavov",
"fixed_states_power_only": "Táto entita môže pracovať iba s 'states_power', nie 'power'",
"group_mandatory": "Musíte definovať aspoň podskupiny alebo výkonové a energetické entity",
"linear_mandatory": "Musíte dodať aspoň jeden z max_power alebo calibrate",
"linear_min_higher_as_max": "Maximálny výkon nemôže byť nižší ako minimálny výkon",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "Du måste ange minst ett värde eller en värdemall",
"entity_mandatory": "Att välja en entitet krävs för alla strategier utom playbook",
"fixed_mandatory": "Du måste ange minst en av Effekt, Effektmall eller effekttillstånd",
"fixed_states_power_only": "Den här entiteten kan bara jobba med 'state effekt' inte 'effekt'",
"group_mandatory": "Du måste ange minst en subgrupp eller effekt och energi entititeter",
"linear_mandatory": "Du måste ange minst en av max_effekt eller kalibrera",
"linear_min_higher_as_max": "Max effekt kan inte vara lägre än min effekt",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "Du måste ange antingen ett energipris eller en energiprissensor",
"fixed_mandatory": "Du måste ange minst en av Effekt, Effektmall eller effekttillstånd",
"fixed_states_power_only": "Den här entiteten kan bara jobba med 'state effekt' inte 'effekt'",
"group_mandatory": "Du måste ange minst en subgrupp eller effekt och energi entititeter",
"linear_mandatory": "Du måste ange minst en av max_effekt eller kalibrera",
"linear_min_higher_as_max": "Max effekt kan inte vara lägre än min effekt",
@@ -14,7 +14,6 @@
"daily_energy_mandatory": "必须至少提供“值”或“值模板”中的一项",
"entity_mandatory": "除运行方案 (playbook) 外,所有策略都必须选择实体",
"fixed_mandatory": "必须至少提供“功率”“功率模板”或“状态功率”中的一项",
"fixed_states_power_only": "此实体只能使用 'states_power',不能使用 'power'",
"group_mandatory": "必须定义至少一个子组,或功率和能源实体",
"linear_mandatory": "必须至少提供 max_power 或 calibrate 其中一项",
"linear_min_higher_as_max": "最大功率不能低于最小功率",
@@ -680,7 +679,6 @@
"error": {
"cost_price_mandatory": "必须提供能源价格或能源价格传感器",
"fixed_mandatory": "必须至少提供功率、功率模板或状态功率中的一项",
"fixed_states_power_only": "此实体只能使用 'states_power',不能使用 'power'",
"group_mandatory": "必须至少定义一个子组,或定义功率和能源实体",
"linear_mandatory": "必须至少提供 max_power 或 calibrate 中的一项",
"linear_min_higher_as_max": "最大功率不能低于最小功率",