224 files

This commit is contained in:
Home Assistant Version Control
2026-07-27 13:30:34 +00:00
parent b5723aa856
commit e36b8a1a22
224 changed files with 9967 additions and 2663 deletions
+32
View File
@@ -15,6 +15,7 @@ from homeassistant.components.utility_meter import max_28_days
from homeassistant.components.utility_meter.const import METER_TYPES
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import (
CONF_DEVICE,
CONF_DOMAIN,
CONF_ENABLED,
EVENT_HOMEASSISTANT_STARTED,
@@ -22,6 +23,7 @@ from homeassistant.const import (
__version__ as HA_VERSION, # noqa: N812
)
from homeassistant.core import Event, HassJob, HomeAssistant, ServiceCall, callback
from homeassistant.helpers import issue_registry as ir
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.entity_platform import async_get_platforms
@@ -98,6 +100,7 @@ from .const import (
ENERGY_INTEGRATION_METHODS,
ENTITY_CATEGORIES,
ENTRY_GLOBAL_CONFIG_UNIQUE_ID,
ISSUE_COMPOSITE_DEVICE_ID,
MIN_HA_VERSION,
SERVICE_CHANGE_GUI_CONFIGURATION,
SERVICE_RELOAD,
@@ -106,6 +109,7 @@ from .const import (
SensorType,
UnitPrefix,
)
from .device_binding import is_composite_device_id
from .discovery import DiscoveryManager, DiscoveryStatus, get_discovery_manager
from .migrate import async_fix_legacy_profile_config_entry, async_migrate_config_entry
from .power_profile.power_profile import DeviceType
@@ -443,6 +447,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Powercalc integration from a config entry."""
await async_fix_legacy_profile_config_entry(hass, entry)
_async_handle_composite_device_issue(hass, entry)
await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR, Platform.SELECT])
entry.async_on_unload(entry.add_update_listener(async_update_entry))
@@ -512,6 +517,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
async def async_remove_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Called after a config entry is removed."""
ir.async_delete_issue(hass, DOMAIN, _composite_device_issue_id(config_entry.entry_id))
discovery_manager = get_discovery_manager(hass)
discovery_manager.remove_initialized_flow(config_entry)
@@ -529,6 +536,31 @@ async def async_remove_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
await hass.config_entries.async_reload(entry.entry_id)
def _composite_device_issue_id(config_entry_id: str) -> str:
"""Return the repair issue ID for a config entry using a composite device ID."""
return f"{ISSUE_COMPOSITE_DEVICE_ID}_{config_entry_id}"
def _async_handle_composite_device_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Report ambiguous legacy device IDs."""
issue_id = _composite_device_issue_id(entry.entry_id)
device_id = entry.data.get(CONF_DEVICE)
if not device_id or not is_composite_device_id(hass, str(device_id)):
ir.async_delete_issue(hass, DOMAIN, issue_id)
return
ir.async_create_issue(
hass,
DOMAIN,
issue_id,
data={"config_entry_id": entry.entry_id},
is_fixable=True,
severity=ir.IssueSeverity.WARNING,
translation_key=ISSUE_COMPOSITE_DEVICE_ID,
translation_placeholders={"name": entry.title},
)
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
await async_migrate_config_entry(hass, config_entry)
+19 -1
View File
@@ -18,6 +18,8 @@ from .const import (
CONF_CREATE_ENERGY_SENSORS,
CONF_CREATE_GROUP,
CONF_DAILY_FIXED_ENERGY,
CONF_ENERGY_PRICE,
CONF_ENERGY_PRICE_SENSOR,
CONF_FORCE_ENERGY_SENSOR_CREATION,
CONF_MULTI_SWITCH,
CONF_POWER_SENSOR_ID,
@@ -48,6 +50,11 @@ EXCLUDE_FROM_PARENT_CONFIG = (
)
ENTITY_ID_OPTIONAL_KEYS = (CONF_DAILY_FIXED_ENERGY, CONF_POWER_SENSOR_ID, CONF_MULTI_SWITCH)
# Groups of keys where a deeper config level replaces the whole group, instead of merging
# key by key. A sensor defining `energy_price` must fully override a global
# `energy_price_sensor`, otherwise the merged config would contain both price sources.
MUTUALLY_EXCLUSIVE_KEY_GROUPS = ((CONF_ENERGY_PRICE, CONF_ENERGY_PRICE_SENSOR),)
def is_number(value: str) -> bool:
"""Return whether the value can be converted to a finite float."""
@@ -167,17 +174,28 @@ def _merge_config_levels(configs: tuple[dict, ...]) -> dict:
"""Merge config levels while keeping deepest-level-only fields local."""
num_configs = len(configs)
merged_config = {}
merged_config: dict = {}
for i, config in enumerate(configs, 1):
config_copy = config.copy()
if i < num_configs:
for key in EXCLUDE_FROM_PARENT_CONFIG:
config_copy.pop(key, None)
_drop_overridden_alternatives(merged_config, config_copy)
merged_config.update(config_copy)
return merged_config
def _drop_overridden_alternatives(merged_config: dict, config: dict) -> None:
"""Drop the alternatives of a mutually exclusive key group set by a shallower config level."""
for group in MUTUALLY_EXCLUSIVE_KEY_GROUPS:
if not any(key in config for key in group):
continue
for key in group:
if key not in config:
merged_config.pop(key, None)
def _apply_sensor_creation_defaults(config: dict) -> None:
config.setdefault(CONF_CREATE_ENERGY_SENSOR, config.get(CONF_CREATE_ENERGY_SENSORS))
config.setdefault(CONF_CREATE_COST_SENSOR, config.get(CONF_CREATE_COST_SENSORS))
+23 -2
View File
@@ -31,6 +31,7 @@ import voluptuous as vol
from .common import SourceEntity, create_source_entity
from .const import (
CONF_CREATE_COST_SENSOR,
CONF_CREATE_UTILITY_METERS,
CONF_MANUFACTURER,
CONF_MODE,
@@ -47,7 +48,7 @@ from .const import (
SensorType,
)
from .errors import ModelNotSupportedError, StrategyConfigurationError
from .flow_helper.common import FlowType, PowercalcFormStep, Step, fill_schema_defaults
from .flow_helper.common import FlowType, PowercalcFormStep, Step, fill_schema_defaults, flatten_sections
from .flow_helper.flows.cost import CostConfigFlow, CostOptionsFlow
from .flow_helper.flows.daily_energy import (
SCHEMA_DAILY_ENERGY_OPTIONS,
@@ -74,6 +75,8 @@ from .flow_helper.flows.virtual_power import (
)
from .flow_helper.profile_preview import async_setup_preview as async_setup_powercalc_preview
from .flow_helper.schema import (
COST_DOCS_URI,
SCHEMA_COST_PRICING,
SCHEMA_COST_SENSOR_TOGGLE,
SCHEMA_ENERGY_SENSOR_TOGGLE,
SCHEMA_SENSOR_ENERGY_OPTIONS,
@@ -554,6 +557,9 @@ class PowercalcOptionsFlow(PowercalcCommonFlow, OptionsFlow):
if self.selected_sensor_type == SensorType.GROUP:
menu.extend(self.flow_handlers[FlowType.GROUP].build_group_menu())
if self.sensor_config.get(CONF_CREATE_COST_SENSOR):
menu.append(Step.COST_OPTIONS)
if self.sensor_config.get(CONF_CREATE_UTILITY_METERS):
menu.append(Step.UTILITY_METER_OPTIONS)
@@ -589,22 +595,37 @@ class PowercalcOptionsFlow(PowercalcCommonFlow, OptionsFlow):
Step.UTILITY_METER_OPTIONS,
)
async def async_step_cost_options(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Handle the per sensor energy price override."""
return await self.async_handle_options_step(
user_input,
SCHEMA_COST_PRICING,
Step.COST_OPTIONS,
form_kwarg={"description_placeholders": {"docs_uri": COST_DOCS_URI}},
)
async def async_handle_options_step(
self,
user_input: dict[str, Any] | None,
schema: vol.Schema,
step: Step,
form_kwarg: dict[str, Any] | None = None,
validate: Callable[[dict[str, Any]], dict[str, str] | None] | None = None,
) -> ConfigFlowResult:
"""
Generic handler for all the option steps.
processes user input against the select schema.
And finally persist the changes on the config entry
An optional `validate` callback receives the user input with any collapsible sections
flattened, and returns errors to show on the form.
"""
errors: dict[str, str] | None = {}
schema = fill_schema_defaults(schema, self.sensor_config)
if user_input is not None:
errors = await self.process_all_options(user_input, schema)
errors = validate(flatten_sections(user_input, schema)) if validate else None
if not errors:
errors = await self.process_all_options(user_input, schema)
if not errors:
return self.persist_config_entry()
return self.async_show_form(step_id=step, data_schema=schema, errors=errors, **(form_kwarg or {}))
@@ -27,6 +27,10 @@ from custom_components.powercalc.const import (
CONF_ENERGY_FILTER_OUTLIER_ENABLED,
CONF_ENERGY_FILTER_OUTLIER_MAX,
CONF_ENERGY_INTEGRATION_METHOD,
CONF_ENERGY_PRICE,
CONF_ENERGY_PRICE_MULTIPLIER,
CONF_ENERGY_PRICE_SENSOR,
CONF_ENERGY_PRICE_SURCHARGE,
CONF_ENERGY_SENSOR_CATEGORY,
CONF_ENERGY_SENSOR_ID,
CONF_ENERGY_SENSOR_NAMING,
@@ -114,6 +118,10 @@ SENSOR_CONFIG = {
vol.Optional(CONF_MULTIPLY_FACTOR_STANDBY): cv.boolean,
vol.Optional(CONF_POWER_SENSOR_NAMING): validate_name_pattern,
vol.Optional(CONF_POWER_SENSOR_CATEGORY): vol.In(ENTITY_CATEGORIES),
vol.Optional(CONF_ENERGY_PRICE): vol.Coerce(float),
vol.Optional(CONF_ENERGY_PRICE_SENSOR): cv.entity_id,
vol.Optional(CONF_ENERGY_PRICE_SURCHARGE): vol.Coerce(float),
vol.Optional(CONF_ENERGY_PRICE_MULTIPLIER): vol.Coerce(float),
vol.Optional(CONF_ENERGY_SENSOR_ID): cv.entity_id,
vol.Optional(CONF_ENERGY_SENSOR_NAMING): validate_name_pattern,
vol.Optional(CONF_ENERGY_SENSOR_CATEGORY): vol.In(ENTITY_CATEGORIES),
+2
View File
@@ -25,6 +25,8 @@ BUILT_IN_LIBRARY_DIR = "powercalc_profiles"
DOMAIN = "powercalc"
DOMAIN_CONFIG = "config"
ISSUE_COMPOSITE_DEVICE_ID = "composite_device_id"
DATA_CONFIGURED_ENTITIES = "configured_entities"
DATA_DISCOVERY_MANAGER = "discovery_manager"
DATA_DOMAIN_ENTITIES = "domain_entities"
+30 -1
View File
@@ -12,11 +12,38 @@ 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
from custom_components.powercalc.const import CONF_AREA, DUMMY_ENTITY_ID
_LOGGER = logging.getLogger(__name__)
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
"""
device_reg = device_registry.async_get(hass)
is_composite = getattr(device_reg, "async_is_composite_device_id", None)
if not callable(is_composite):
return False
return bool(is_composite(device_id))
def attach_configured_device_entry(
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:
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 source_entity
async def attach_entities_to_resolved_device(
config_entry: ConfigEntry | None,
entities_to_add: list[Entity],
@@ -53,6 +80,8 @@ def get_device_entry(
if device_id is None and config_entry is not None:
device_id = config_entry.data.get(CONF_DEVICE)
if device_id is not None:
if is_composite_device_id(hass, device_id):
return None
return device_registry.async_get(hass).async_get(device_id)
if source_entity:
@@ -34,6 +34,7 @@ class Step(StrEnum):
DAILY_ENERGY = "daily_energy"
REAL_POWER = "real_power"
COST = "cost"
COST_OPTIONS = "cost_options"
MANUFACTURER = "manufacturer"
MENU_LIBRARY = "menu_library"
MENU_GROUP = "menu_group"
@@ -7,7 +7,9 @@ from typing import TYPE_CHECKING, Any
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.config_entries import ConfigFlowResult
from homeassistant.const import CONF_NAME
from homeassistant.data_entry_flow import section
from homeassistant.helpers import selector
from homeassistant.helpers.schema_config_entry_flow import SchemaFlowError
import voluptuous as vol
from custom_components.powercalc.const import (
@@ -18,7 +20,8 @@ from custom_components.powercalc.const import (
DOMAIN_CONFIG,
SensorType,
)
from custom_components.powercalc.flow_helper.common import PowercalcFormStep, Step
from custom_components.powercalc.flow_helper.common import PowercalcFormStep, Step, flatten_sections
from custom_components.powercalc.flow_helper.schema import SCHEMA_COST_PRICING, SECTION_COST_PRICING
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
@@ -30,6 +33,7 @@ SCHEMA_COST_OPTIONS = vol.Schema(
vol.Required(CONF_ENERGY_SENSOR_ID): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor", device_class=SensorDeviceClass.ENERGY),
),
vol.Optional(SECTION_COST_PRICING): section(SCHEMA_COST_PRICING, {"collapsed": True}),
},
)
@@ -47,24 +51,34 @@ def is_global_price_configured(hass: HomeAssistant) -> bool:
return bool(global_config.get(CONF_ENERGY_PRICE) or global_config.get(CONF_ENERGY_PRICE_SENSOR))
def validate_price_configured(hass: HomeAssistant, user_input: dict[str, Any]) -> dict[str, str] | None:
"""Ensure a price is available, either overridden on the sensor itself or globally."""
if user_input.get(CONF_ENERGY_PRICE) or user_input.get(CONF_ENERGY_PRICE_SENSOR):
return None
if is_global_price_configured(hass):
return None
return {"base": "cost_price_mandatory"}
class CostConfigFlow:
def __init__(self, flow: PowercalcConfigFlow) -> None:
self.flow: PowercalcConfigFlow = flow
async def async_step_cost(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Handle the flow for a standalone cost sensor."""
if not is_global_price_configured(self.flow.hass):
return self.flow.async_abort(
reason="cost_no_global_price",
description_placeholders={"url": "https://docs.powercalc.nl/sensor-types/cost-sensor/"},
)
self.flow.selected_sensor_type = SensorType.COST
return await self.flow.handle_form_step(
PowercalcFormStep(step=Step.COST, schema=SCHEMA_COST),
PowercalcFormStep(step=Step.COST, schema=SCHEMA_COST, validate_user_input=self.validate),
user_input,
)
def validate(self, user_input: dict[str, Any]) -> dict[str, Any]:
"""Flatten the pricing section and reject the input when no price is available."""
user_input = flatten_sections(user_input, SCHEMA_COST)
if validate_price_configured(self.flow.hass, user_input):
raise SchemaFlowError("cost_price_mandatory")
return user_input
class CostOptionsFlow:
def __init__(self, flow: PowercalcOptionsFlow) -> None:
@@ -72,4 +86,9 @@ class CostOptionsFlow:
async def async_step_cost(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Handle the cost sensor options flow."""
return await self.flow.async_handle_options_step(user_input, SCHEMA_COST_OPTIONS, Step.COST)
return await self.flow.async_handle_options_step(
user_input,
SCHEMA_COST_OPTIONS,
Step.COST,
validate=lambda flat_input: validate_price_configured(self.flow.hass, flat_input),
)
@@ -53,6 +53,7 @@ from custom_components.powercalc.const import (
)
from custom_components.powercalc.flow_helper.common import PowercalcFormStep, Step, flatten_sections
from custom_components.powercalc.flow_helper.schema import (
COST_DOCS_URI,
SCHEMA_COST_APPLY,
SCHEMA_ENERGY_OPTIONS,
SCHEMA_GLOBAL_COST,
@@ -327,7 +328,7 @@ class GlobalConfigurationFlow:
schema=SCHEMA_GLOBAL_COST,
form_kwarg={
"description_placeholders": {
"docs_uri": "https://docs.powercalc.nl/sensor-types/cost-sensor/",
"docs_uri": COST_DOCS_URI,
},
},
)
@@ -52,8 +52,11 @@ SCHEMA_COST_SENSOR_TOGGLE = vol.Schema(
SECTION_COST_PRICING = "cost_pricing"
SECTION_COST_NAMING = "cost_naming"
COST_DOCS_URI = "https://docs.powercalc.nl/sensor-types/cost-sensor/"
SCHEMA_GLOBAL_COST_PRICING = vol.Schema(
# The pricing fields are used both for the global energy price and for the per sensor
# price override, so they are defined once and reused by both schemas.
SCHEMA_COST_PRICING = vol.Schema(
{
vol.Optional(CONF_ENERGY_PRICE): NumberSelector(
selector.NumberSelectorConfig(mode=NumberSelectorMode.BOX, step="any"),
@@ -80,13 +83,13 @@ SCHEMA_GLOBAL_COST_NAMING = vol.Schema(
# Presented in the GUI as two collapsible sections (pricing and naming).
SCHEMA_GLOBAL_COST = vol.Schema(
{
vol.Required(SECTION_COST_PRICING): section(SCHEMA_GLOBAL_COST_PRICING),
vol.Required(SECTION_COST_PRICING): section(SCHEMA_COST_PRICING),
vol.Required(SECTION_COST_NAMING): section(SCHEMA_GLOBAL_COST_NAMING, {"collapsed": True}),
},
)
# Flat variant with all cost keys, used to merge/clear the (un)nested user input.
SCHEMA_GLOBAL_COST_FLAT = SCHEMA_GLOBAL_COST_PRICING.extend(SCHEMA_GLOBAL_COST_NAMING.schema)
SCHEMA_GLOBAL_COST_FLAT = SCHEMA_COST_PRICING.extend(SCHEMA_GLOBAL_COST_NAMING.schema)
# Shown when the global create_cost_sensors toggle is flipped, to optionally propagate the
# change to all existing GUI sensors.
+1 -1
View File
@@ -22,5 +22,5 @@
"requirements": [
"numpy>=1.21.1"
],
"version": "v1.23.0"
"version": "v1.23.1"
}
+19 -6
View File
@@ -1,10 +1,10 @@
from datetime import timedelta
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENABLED, CONF_ID, CONF_PATH, EntityCategory
from homeassistant.const import CONF_DEVICE, CONF_ENABLED, CONF_ID, CONF_PATH, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry, issue_registry as ir
from homeassistant.helpers.helper_integration import async_remove_helper_config_entry_from_source_device
import homeassistant.helpers.helper_integration as helper_integration
from homeassistant.helpers.issue_registry import async_create_issue
from custom_components.powercalc.const import (
@@ -74,13 +74,26 @@ def _remove_config_entry_from_devices(hass: HomeAssistant, config_entry: ConfigE
Remove powercalc config entry from devices.
See: https://developers.home-assistant.io/blog/2025/07/18/updated-pattern-for-helpers-linking-to-devices/
"""
device_reg = device_registry.async_get(hass)
for device_entry in device_registry.async_entries_for_config_entry(device_reg, config_entry.entry_id):
async_remove_helper_config_entry_from_source_device(
remove_helper_devices = getattr(helper_integration, "async_remove_helper_devices", None)
if remove_helper_devices is not None:
configured_device_id = config_entry.data.get(CONF_DEVICE, None)
remove_helper_devices(
hass,
helper_config_entry_id=config_entry.entry_id,
source_device_id=device_entry.id,
source_device_id=configured_device_id,
remove_all_devices=True,
)
return
remove_legacy_link = getattr(helper_integration, "async_remove_helper_config_entry_from_source_device", None)
if remove_legacy_link is not None:
device_reg = device_registry.async_get(hass)
for device_entry in device_registry.async_entries_for_config_entry(device_reg, config_entry.entry_id):
remove_legacy_link(
hass,
helper_config_entry_id=config_entry.entry_id,
source_device_id=device_entry.id,
)
def _migrate_power_template(data: dict) -> None:
+73 -5
View File
@@ -1,13 +1,18 @@
from __future__ import annotations
from typing import Any
from homeassistant import data_entry_flow
from homeassistant.components.repairs import RepairsFlow
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.const import CONF_DEVICE, CONF_ENTITY_ID
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.const import CONF_MODEL, CONF_SUB_PROFILE
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
from custom_components.powercalc.power_profile.factory import get_power_profile
@@ -52,14 +57,77 @@ class SubProfileRepairFlow(RepairsFlow):
)
class CompositeDeviceIdRepairFlow(RepairsFlow):
"""Handle selection of a device after Home Assistant split a legacy device."""
def __init__(self, entry_id: str, entry_title: str) -> None:
"""Initialize the repair flow."""
self._entry_id = entry_id
self._entry_title = entry_title
async def async_step_init(self, _: dict[str, Any] | None = None) -> data_entry_flow.FlowResult:
"""Handle the first step of the repair flow."""
return await self.async_step_select_device()
async def async_step_select_device(self, user_input: dict[str, Any] | None = None) -> data_entry_flow.FlowResult:
"""Select a concrete split device, or unlink the Powercalc entities."""
errors: dict[str, str] = {}
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]
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 is_composite_device_id(self.hass, selected_device_id)
):
errors["base"] = "invalid_device"
if not errors:
new_data = dict(entry.data)
if selected_device_id is None:
new_data.pop(CONF_DEVICE, None)
else:
new_data[CONF_DEVICE] = selected_device_id
self.hass.config_entries.async_update_entry(entry, data=new_data)
ir.async_delete_issue(
self.hass,
DOMAIN,
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_show_form( # type: ignore[no-any-return]
step_id="select_device",
data_schema=vol.Schema({vol.Optional(CONF_DEVICE): selector.DeviceSelector()}),
errors=errors,
description_placeholders={"name": self._entry_title},
)
async def async_create_fix_flow(
hass: HomeAssistant,
_: str,
issue_id: str,
data: dict[str, str | int | float | None] | None,
) -> RepairsFlow:
"""Create flow."""
assert data
if not data or "config_entry_id" not in data:
raise ValueError("Missing config entry ID for repair flow")
config_entry_id = str(data["config_entry_id"])
config_entry = hass.config_entries.async_get_entry(config_entry_id)
assert config_entry
if issue_id == f"{ISSUE_COMPOSITE_DEVICE_ID}_{config_entry_id}":
return CompositeDeviceIdRepairFlow(
config_entry_id,
config_entry.title if config_entry else "Unknown configuration",
)
if config_entry is None:
raise ValueError(f"Unknown config entry: {config_entry_id}")
return SubProfileRepairFlow(hass, config_entry)
+3 -19
View File
@@ -20,7 +20,6 @@ from homeassistant.const import (
from homeassistant.core import Event, HomeAssistant, SupportsResponse, callback
from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.helpers.entity_registry as er
@@ -88,7 +87,7 @@ from .const import (
PowercalcDiscoveryType,
SensorType,
)
from .device_binding import attach_entities_to_resolved_device
from .device_binding import attach_configured_device_entry, attach_entities_to_resolved_device
from .errors import (
PowercalcSetupError,
SensorAlreadyConfiguredError,
@@ -636,7 +635,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 = attach_configured_device_entry(hass, sensor_config, source_entity)
used_unique_ids = hass.data[DOMAIN].get(DATA_USED_UNIQUE_IDS, [])
@@ -712,23 +711,8 @@ async def _create_daily_fixed_energy_sensors(
return energy_sensor
def _attach_configured_device_entry(
hass: HomeAssistant,
sensor_config: dict,
source_entity: SourceEntity,
) -> SourceEntity:
if source_entity.entity_id != DUMMY_ENTITY_ID or "device" not in sensor_config:
return source_entity
device_registry = dr.async_get(hass)
device_entry = device_registry.async_get(sensor_config["device"])
if device_entry:
return source_entity._replace(device_entry=device_entry)
return source_entity
def check_entity_not_already_configured(
sensor_config: dict,
sensor_config: ConfigType,
source_entity: SourceEntity,
hass: HomeAssistant,
used_unique_ids: list[str],
+73 -40
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from decimal import Decimal
import logging
from typing import TYPE_CHECKING
@@ -109,6 +110,56 @@ def _currency_from_price_unit(unit: str | None) -> str | None:
return currency or None
@dataclass(frozen=True, slots=True)
class PriceConfig:
"""The resolved energy price configuration for a single cost sensor."""
fixed_price: Decimal | None = None
price_entity_id: str | None = None
surcharge: Decimal = Decimal(0)
multiplier: Decimal = Decimal(1)
@property
def has_price_source(self) -> bool:
"""Return whether a price source (fixed price or price sensor) is configured."""
return self.fixed_price is not None or bool(self.price_entity_id)
def effective_price(self, price: Decimal | None) -> Decimal | None:
"""Apply the surcharge and multiplier to a raw price per kWh."""
if price is None:
return None
return (price + self.surcharge) * self.multiplier
def resolve_price_config(hass: HomeAssistant, sensor_config: ConfigType) -> PriceConfig:
"""Resolve the energy price for a sensor, preferring its own config over the global one.
The price source is resolved as a whole: a sensor which defines a fixed price or a price
sensor of its own fully replaces the globally configured price source, rather than mixing
the two. The surcharge and multiplier are independent and fall back to the global value
individually.
"""
global_config: ConfigType = hass.data[DOMAIN].get(DOMAIN_CONFIG, {})
if CONF_ENERGY_PRICE in sensor_config or CONF_ENERGY_PRICE_SENSOR in sensor_config:
fixed_price = sensor_config.get(CONF_ENERGY_PRICE)
price_entity_id = sensor_config.get(CONF_ENERGY_PRICE_SENSOR)
else:
fixed_price = global_config.get(CONF_ENERGY_PRICE)
price_entity_id = global_config.get(CONF_ENERGY_PRICE_SENSOR)
def resolve(key: str, default: float) -> Decimal:
value = sensor_config.get(key, global_config.get(key))
return Decimal(str(default if value is None else value))
return PriceConfig(
fixed_price=Decimal(str(fixed_price)) if fixed_price is not None else None,
price_entity_id=price_entity_id,
surcharge=resolve(CONF_ENERGY_PRICE_SURCHARGE, 0),
multiplier=resolve(CONF_ENERGY_PRICE_MULTIPLIER, 1),
)
def create_cost_sensor(
hass: HomeAssistant,
sensor_config: ConfigType,
@@ -120,21 +171,18 @@ def create_cost_sensor(
) -> CostSensor | None:
"""Create a cost sensor tracking the cost of the given energy sensor.
The energy sensor can be a regular energy sensor or a utility meter. The energy
price is defined globally, either as a fixed price or a price sensor. When no price
is configured, no cost sensor is created. A ``unique_id`` can be provided to override
the id derived from the energy sensor (used for standalone cost sensor config entries).
The energy sensor can be a regular energy sensor or a utility meter. The energy price is
taken from the sensor configuration when it defines one, otherwise from the global
configuration, either as a fixed price or a price sensor. When no price is configured,
no cost sensor is created. A ``unique_id`` can be provided to override the id derived
from the energy sensor (used for standalone cost sensor config entries).
"""
global_config: ConfigType = hass.data[DOMAIN].get(DOMAIN_CONFIG, {})
fixed_price = global_config.get(CONF_ENERGY_PRICE)
price_entity_id = global_config.get(CONF_ENERGY_PRICE_SENSOR)
price_surcharge = Decimal(str(global_config.get(CONF_ENERGY_PRICE_SURCHARGE, 0) or 0))
price_multiplier = Decimal(str(global_config.get(CONF_ENERGY_PRICE_MULTIPLIER, 1) or 1))
if fixed_price is None and not price_entity_id:
price_config = resolve_price_config(hass, sensor_config)
if not price_config.has_price_source:
_LOGGER.warning(
"Cost sensor creation is enabled but no energy price is configured. "
"Define `energy_price` or `energy_price_sensor` in the global powercalc configuration",
"Define `energy_price` or `energy_price_sensor` on the sensor itself "
"or in the global powercalc configuration",
)
return None
@@ -151,16 +199,10 @@ def create_cost_sensor(
)
_LOGGER.debug(
(
"Creating cost sensor (entity_id=%s, source_entity=%s, fixed_price=%s, "
"price_entity=%s, price_surcharge=%s, price_multiplier=%s)"
),
"Creating cost sensor (entity_id=%s, source_entity=%s, %s)",
entity_id,
energy_sensor.entity_id,
fixed_price,
price_entity_id,
price_surcharge,
price_multiplier,
price_config,
)
return CostSensor(
@@ -170,10 +212,7 @@ def create_cost_sensor(
unique_id=unique_id,
name=cost_name,
sensor_config=sensor_config,
fixed_price=Decimal(str(fixed_price)) if fixed_price is not None else None,
price_entity_id=price_entity_id,
price_surcharge=price_surcharge,
price_multiplier=price_multiplier,
price_config=price_config,
reset_on_source_reset=reset_on_source_reset,
)
@@ -215,10 +254,7 @@ class CostSensor(BaseEntity, RestoreEntity, SensorEntity):
sensor_config: ConfigType,
name: str | None = None,
unique_id: str | None = None,
fixed_price: Decimal | None = None,
price_entity_id: str | None = None,
price_surcharge: Decimal = Decimal(0),
price_multiplier: Decimal = Decimal(1),
price_config: PriceConfig | None = None,
reset_on_source_reset: bool = False,
) -> None:
self._source_energy_entity = source_energy_entity
@@ -227,15 +263,13 @@ class CostSensor(BaseEntity, RestoreEntity, SensorEntity):
self._attr_name = name
self._attr_unique_id = unique_id
self._attr_native_unit_of_measurement = hass.config.currency
self._fixed_price = fixed_price
self._price_entity_id = price_entity_id
self._price_surcharge = price_surcharge
self._price_multiplier = price_multiplier
self._price_config = price_config or PriceConfig()
self._price_entity_id = self._price_config.price_entity_id
self._rounding_digits = int(sensor_config.get(CONF_COST_SENSOR_PRECISION, DEFAULT_COST_SENSOR_PRECISION))
self._attr_suggested_display_precision = self._rounding_digits
self._state: Decimal = Decimal(0)
self._last_energy: Decimal | None = None
self._current_price: Decimal | None = self._effective_price(fixed_price)
self._current_price: Decimal | None = self._price_config.effective_price(self._price_config.fixed_price)
# Only a resetting (per utility meter cycle) sensor uses last_reset; a lifetime cost
# sensor accumulates monotonically and leaves it None.
self._attr_last_reset: datetime | None = None
@@ -266,7 +300,9 @@ class CostSensor(BaseEntity, RestoreEntity, SensorEntity):
price_unit = price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if price_state else None
if (currency := _currency_from_price_unit(price_unit)) is not None:
self._attr_native_unit_of_measurement = currency
self._current_price = self._effective_price(_parse_scaled(price_state, _price_per_kwh_factor))
self._current_price = self._price_config.effective_price(
_parse_scaled(price_state, _price_per_kwh_factor),
)
self.async_on_remove(
async_track_state_change_event(
self.hass,
@@ -306,12 +342,9 @@ class CostSensor(BaseEntity, RestoreEntity, SensorEntity):
if current_energy is not None:
self._accumulate(current_energy, self._current_price)
self._current_price = self._effective_price(_parse_scaled(event.data["new_state"], _price_per_kwh_factor))
def _effective_price(self, price: Decimal | None) -> Decimal | None:
if price is None:
return None
return (price + self._price_surcharge) * self._price_multiplier
self._current_price = self._price_config.effective_price(
_parse_scaled(event.data["new_state"], _price_per_kwh_factor),
)
def _accumulate(self, new_energy: Decimal, price: Decimal | None) -> None:
"""Add the cost of the consumed energy at the given price and advance the baseline."""
@@ -7,7 +7,7 @@ from enum import StrEnum
import logging
from typing import Any
from homeassistant.const import CONF_ATTRIBUTE, CONF_CONDITION, CONF_ENTITY_ID, STATE_OFF
from homeassistant.const import CONF_ATTRIBUTE, CONF_CONDITION, CONF_CONDITIONS, CONF_ENTITY_ID, STATE_OFF
from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import ConditionError
from homeassistant.helpers.condition import ConditionCheckerType
@@ -41,14 +41,52 @@ class CompositeMode(StrEnum):
SUM_ALL = "sum_all"
class ConditionType(StrEnum):
"""Condition types supported by the composite strategy.
Home Assistant has no constants for these, it uses string literals itself.
"""
AND = "and"
DEVICE = "device"
NOT = "not"
NUMERIC_STATE = "numeric_state"
OR = "or"
STATE = "state"
TEMPLATE = "template"
COMPOUND_CONDITIONS = (ConditionType.AND, ConditionType.OR, ConditionType.NOT)
ENTITY_CONDITIONS = (ConditionType.STATE, ConditionType.NUMERIC_STATE)
DEFAULT_MODE = CompositeMode.STOP_AT_FIRST
def make_entity_id_optional(schema: vol.Schema) -> vol.Schema:
"""Make entity_id optional in schema."""
schema = schema.schema
schema[vol.Optional(CONF_ENTITY_ID)] = schema.pop(vol.Required(CONF_ENTITY_ID)) # type: ignore[index, attr-defined]
return vol.Schema(schema)
# Copy, the schemas we get passed here are module level globals of Home Assistant itself
schema_dict = dict(schema.schema)
schema_dict[vol.Optional(CONF_ENTITY_ID)] = schema_dict.pop(vol.Required(CONF_ENTITY_ID))
return vol.Schema(schema_dict)
def get_compound_schema(condition_type: ConditionType) -> vol.Schema:
"""Return the schema for and/or/not conditions.
Home Assistant's own compound schemas recurse into `cv.CONDITION_SCHEMA`, which requires entity_id.
We recurse into our own schema instead, so entity_id stays optional at any nesting level
and can be defaulted to the source entity.
"""
return vol.Schema(
{
**cv.CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): condition_type.value,
vol.Required(CONF_CONDITIONS): vol.All(
cv.ensure_list,
[lambda value: CONDITION_SCHEMA(value)],
),
},
)
def get_numeric_state_schema() -> vol.Schema:
@@ -86,13 +124,13 @@ CONDITION_SCHEMA: vol.Schema = vol.Schema(
cv.key_value_schemas(
CONF_CONDITION,
{
"and": cv.AND_CONDITION_SCHEMA,
"device": cv.DEVICE_CONDITION_SCHEMA,
"not": cv.NOT_CONDITION_SCHEMA,
"numeric_state": get_numeric_state_schema(),
"or": cv.OR_CONDITION_SCHEMA,
"state": get_state_schema,
"template": cv.TEMPLATE_CONDITION_SCHEMA,
ConditionType.AND: get_compound_schema(ConditionType.AND),
ConditionType.DEVICE: cv.DEVICE_CONDITION_SCHEMA,
ConditionType.NOT: get_compound_schema(ConditionType.NOT),
ConditionType.NUMERIC_STATE: get_numeric_state_schema(),
ConditionType.OR: get_compound_schema(ConditionType.OR),
ConditionType.STATE: get_state_schema,
ConditionType.TEMPLATE: cv.TEMPLATE_CONDITION_SCHEMA,
},
),
),
@@ -4,12 +4,13 @@ from collections.abc import Callable
from decimal import Decimal
from typing import Any, cast
from homeassistant.const import CONF_CONDITION, CONF_ENTITIES, CONF_ENTITY_ID
from homeassistant.const import CONF_CONDITION, CONF_CONDITIONS, CONF_ENTITIES, CONF_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.helpers import condition
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType
import voluptuous as vol
from custom_components.powercalc.common import SourceEntity
from custom_components.powercalc.const import (
@@ -31,7 +32,14 @@ from custom_components.powercalc.errors import (
)
from custom_components.powercalc.power_profile.power_profile import PowerProfile
from .composite import DEFAULT_MODE, CompositeStrategy, SubStrategy
from .composite import (
COMPOUND_CONDITIONS,
CONFIG_SCHEMA as COMPOSITE_SCHEMA,
DEFAULT_MODE,
ENTITY_CONDITIONS,
CompositeStrategy,
SubStrategy,
)
from .fixed import FixedStrategy
from .linear import LinearStrategy
from .lut import LutRegistry, LutStrategy
@@ -42,6 +50,24 @@ from .strategy_interface import PowerCalculationStrategyInterface
from .wled import WledStrategy
def resolve_condition_entity_ids(condition_config: ConfigType, source_entity: SourceEntity) -> ConfigType:
"""Default entity_id to the source entity for conditions which omit it.
Recurses into and/or/not, so nested conditions are treated the same as top level ones.
Returns a copy, to prevent mutating the profile configuration.
"""
resolved = dict(condition_config)
condition_type = resolved.get(CONF_CONDITION)
if condition_type in COMPOUND_CONDITIONS:
resolved[CONF_CONDITIONS] = [
resolve_condition_entity_ids(sub_condition, source_entity)
for sub_condition in resolved.get(CONF_CONDITIONS, [])
]
elif condition_type in ENTITY_CONDITIONS and CONF_ENTITY_ID not in resolved:
resolved[CONF_ENTITY_ID] = [source_entity.entity_id]
return resolved
class PowerCalculatorStrategyFactory:
def __init__(self, hass: HomeAssistant) -> None:
self._hass = hass
@@ -167,7 +193,7 @@ class PowerCalculatorStrategyFactory:
composite_config: list | dict | None = config.get(CONF_COMPOSITE)
if composite_config is None:
if power_profile and power_profile.composite_config:
composite_config = power_profile.composite_config
composite_config = self._validate_composite_config(power_profile.composite_config)
else:
raise StrategyConfigurationError("No composite configuration supplied")
@@ -183,11 +209,8 @@ class PowerCalculatorStrategyFactory:
condition_instance = None
condition_config = strategy_config.get(CONF_CONDITION)
if condition_config:
condition_type = condition_config.get(CONF_CONDITION)
if condition_type in ["state", "numeric_state"] and CONF_ENTITY_ID not in condition_config:
condition_config[CONF_ENTITY_ID] = [source_entity.entity_id]
if condition_type == "state":
condition_config = condition.state_validate_config(self._hass, condition_config)
condition_config = resolve_condition_entity_ids(condition_config, source_entity)
condition_config = await condition.async_validate_condition_config(self._hass, condition_config)
condition_instance = await condition.async_from_config(
self._hass,
condition_config,
@@ -207,6 +230,19 @@ class PowerCalculatorStrategyFactory:
strategies = [await _create_sub_strategy(config) for config in sub_strategies]
return CompositeStrategy(self._hass, strategies, mode)
@staticmethod
def _validate_composite_config(composite_config: list | dict) -> list | dict:
"""Validate the composite configuration of a library profile.
Configuration from YAML and the config flow is already validated by the sensor schema.
Library profiles are raw JSON, so validate them here as well to get the same normalization,
for example entity_id to a list and value_template to a Template instance.
"""
try:
return cast(list | dict, COMPOSITE_SCHEMA(composite_config))
except vol.Invalid as err:
raise StrategyConfigurationError(f"Invalid composite configuration in profile: {err}") from err
def _create_multi_switch(self, config: ConfigType, power_profile: PowerProfile | None) -> MultiSwitchStrategy:
"""Create instance of multi switch strategy."""
multi_switch_config: ConfigType = {}
@@ -562,6 +562,29 @@
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
"abort": {
"entry_removed": "Položka konfigurace Powercalc byla odstraněna"
},
"error": {
"invalid_device": "Vyberte existující zařízení nebo ponechte pole prázdné"
},
"step": {
"select_device": {
"data": {
"device": "Zařízení"
},
"data_description": {
"device": "Zařízení, ke kterému mají být připojeny entity vytvořené touto konfigurací Powercalc"
},
"description": "Konfigurace Powercalc {name} byla připojena k zařízení, které Home Assistant rozdělil na více zařízení. Její entity nelze připojit ke starému ID zařízení.\n\nVyberte zařízení, ke kterému mají být tyto entity připojeny, nebo ponechte pole prázdné, aby zůstaly bez zařízení.",
"title": "Vyberte zařízení pro {name}"
}
}
},
"title": "Zařízení pro konfiguraci Powercalc {name} je třeba vybrat znovu"
},
"deprecated_platform_yaml": {
"description": "Configuring sensors using `sensor->platform` has been deprecated. You need to change your configuration to `powercalc->sensors`. Click on 'Learn more' for further instructions.",
"title": "Nastavení Powercalc YAML bylo přesunuto"
@@ -562,6 +562,29 @@
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
"abort": {
"entry_removed": "Der Powercalc-Konfigurationseintrag wurde entfernt"
},
"error": {
"invalid_device": "Wähle ein vorhandenes Gerät aus oder lasse das Feld leer"
},
"step": {
"select_device": {
"data": {
"device": "Gerät"
},
"data_description": {
"device": "Das Gerät, mit dem die von dieser Powercalc-Konfiguration erstellten Entitäten verknüpft werden sollen"
},
"description": "Die Powercalc-Konfiguration {name} war mit einem Gerät verknüpft, das Home Assistant in mehrere Geräte aufgeteilt hat. Ihre Entitäten können nicht mit der alten Geräte-ID verknüpft werden.\n\nWähle das Gerät aus, mit dem diese Entitäten verknüpft werden sollen, oder lasse das Feld leer, damit sie keinem Gerät zugeordnet werden.",
"title": "Wähle ein Gerät für {name}"
}
}
},
"title": "Das Gerät für die Powercalc-Konfiguration {name} muss erneut ausgewählt werden"
},
"deprecated_platform_yaml": {
"description": "Das Konfigurieren von Sensoren mit 'sensor->platform' ist veraltet. Sie müssen Ihre Konfiguration auf 'powercalc->sensors' umstellen. Klicken Sie auf 'Mehr erfahren' für weitere Anweisungen.",
"title": "Powercalc YAML Konfiguration wurde verschoben"
@@ -7,8 +7,7 @@
},
"config": {
"abort": {
"already_configured": "Sensor is already configured, specify a unique_id",
"cost_no_global_price": "No energy price is configured yet. Set up an energy price in the Powercalc global configuration before creating a cost sensor. See [documentation]({url})."
"already_configured": "Sensor is already configured, specify a unique_id"
},
"error": {
"cost_price_mandatory": "You must supply either an energy price or an energy price sensor",
@@ -472,7 +471,24 @@
"energy_sensor_id": "The existing energy sensor (kWh) to calculate the cost for. This can be a sensor from any integration, not only Powercalc",
"name": "Base name for the cost sensor. The full entity name is set according to the cost_sensor_naming setting"
},
"description": "Create a cost sensor for an existing energy sensor. The energy price is taken from the Powercalc global configuration.",
"description": "Create a cost sensor for an existing energy sensor. The energy price is taken from the Powercalc global configuration, unless you override it below.",
"sections": {
"cost_pricing": {
"name": "Price override",
"data": {
"energy_price": "Fixed energy price",
"energy_price_multiplier": "Energy price multiplier",
"energy_price_sensor": "Energy price sensor",
"energy_price_surcharge": "Energy price surcharge"
},
"data_description": {
"energy_price": "A fixed price per kWh in your local currency. Leave empty to use the global energy price",
"energy_price_multiplier": "Multiplier applied after the surcharge. Use this for percentage-based taxes or fees, for example 1.21 for 21%. Leave empty to use the global multiplier",
"energy_price_sensor": "A sensor which provides the current energy price per kWh (e.g. from a dynamic tariff integration). Leave the fixed price empty to use this sensor",
"energy_price_surcharge": "Additional fixed amount per kWh added to the fixed price or price sensor value. Leave empty to use the global surcharge"
}
}
},
"title": "Create a cost sensor"
},
"sub_profile": {
@@ -562,6 +578,29 @@
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
"abort": {
"entry_removed": "The Powercalc configuration entry has been removed"
},
"error": {
"invalid_device": "Select an existing device or leave the field empty"
},
"step": {
"select_device": {
"data": {
"device": "Device"
},
"data_description": {
"device": "The device the entities created by this Powercalc configuration should be linked to"
},
"description": "The Powercalc configuration {name} was linked to a device that Home Assistant has split into multiple devices. Its entities cannot be linked to the old device ID.\n\nSelect the device these entities should be linked to, or leave the field empty to leave them without a device.",
"title": "Select device for {name}"
}
}
},
"title": "Device for Powercalc configuration {name} needs to be selected again"
},
"deprecated_platform_yaml": {
"description": "Configuring sensors using `sensor->platform` has been deprecated. You need to change your configuration to `powercalc->sensors`. Click on 'Learn more' for further instructions.",
"title": "Powercalc YAML configuration has moved"
@@ -905,7 +944,8 @@
"multi_switch": "Multi switch options",
"real_power": "Real power options",
"utility_meter_options": "Utility meter options",
"wled": "WLED options"
"wled": "WLED options",
"cost_options": "Cost options"
}
},
"library_options": {
@@ -963,13 +1003,30 @@
}
},
"cost": {
"title": "Cost options",
"data": {
"energy_sensor_id": "Energy sensor"
},
"data_description": {
"energy_sensor_id": "The existing energy sensor (kWh) to calculate the cost for"
}
},
"sections": {
"cost_pricing": {
"name": "Price override",
"data": {
"energy_price": "Fixed energy price",
"energy_price_multiplier": "Energy price multiplier",
"energy_price_sensor": "Energy price sensor",
"energy_price_surcharge": "Energy price surcharge"
},
"data_description": {
"energy_price": "A fixed price per kWh in your local currency. Leave empty to use the global energy price",
"energy_price_multiplier": "Multiplier applied after the surcharge. Use this for percentage-based taxes or fees, for example 1.21 for 21%. Leave empty to use the global multiplier",
"energy_price_sensor": "A sensor which provides the current energy price per kWh (e.g. from a dynamic tariff integration). Leave the fixed price empty to use this sensor",
"energy_price_surcharge": "Additional fixed amount per kWh added to the fixed price or price sensor value. Leave empty to use the global surcharge"
}
}
},
"title": "Cost options"
},
"utility_meter_options": {
"title": "Utility meter options",
@@ -990,6 +1047,22 @@
"power_factor": "Power factor",
"voltage": "Voltage"
}
},
"cost_options": {
"data": {
"energy_price": "Fixed energy price",
"energy_price_multiplier": "Energy price multiplier",
"energy_price_sensor": "Energy price sensor",
"energy_price_surcharge": "Energy price surcharge"
},
"data_description": {
"energy_price": "A fixed price per kWh in your local currency. Leave empty to use the global energy price",
"energy_price_multiplier": "Multiplier applied after the surcharge. Use this for percentage-based taxes or fees, for example 1.21 for 21%. Leave empty to use the global multiplier",
"energy_price_sensor": "A sensor which provides the current energy price per kWh (e.g. from a dynamic tariff integration). Leave the fixed price empty to use this sensor",
"energy_price_surcharge": "Additional fixed amount per kWh added to the fixed price or price sensor value. Leave empty to use the global surcharge"
},
"description": "Override the globally configured energy price for this sensor only. Leave all fields empty to keep using the global price. See [documentation]({docs_uri}) for more info",
"title": "Cost options"
}
}
},
@@ -562,6 +562,29 @@
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
"abort": {
"entry_removed": "La entrada de configuración de Powercalc se ha eliminado"
},
"error": {
"invalid_device": "Selecciona un dispositivo existente o deja el campo vacío"
},
"step": {
"select_device": {
"data": {
"device": "Dispositivo"
},
"data_description": {
"device": "El dispositivo al que deben vincularse las entidades creadas por esta configuración de Powercalc"
},
"description": "La configuración de Powercalc {name} estaba vinculada a un dispositivo que Home Assistant ha dividido en varios dispositivos. Sus entidades no se pueden vincular al identificador de dispositivo anterior.\n\nSelecciona el dispositivo al que deben vincularse estas entidades o deja el campo vacío para dejarlas sin dispositivo.",
"title": "Seleccionar dispositivo para {name}"
}
}
},
"title": "Es necesario volver a seleccionar el dispositivo de la configuración de Powercalc {name}"
},
"deprecated_platform_yaml": {
"description": "La configuración de los sensores usando `sensor->plataform` está ahora obsoleta. Necesita cambiar su configuración a `powercalc->sensores`. Haga clic en 'Learn more' para más instrucciones.",
"title": "La configuración de Powercalc YAML se ha movido"
@@ -562,6 +562,29 @@
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
"abort": {
"entry_removed": "L'entrée de configuration Powercalc a été supprimée"
},
"error": {
"invalid_device": "Sélectionnez un appareil existant ou laissez le champ vide"
},
"step": {
"select_device": {
"data": {
"device": "Appareil"
},
"data_description": {
"device": "L'appareil auquel les entités créées par cette configuration Powercalc doivent être liées"
},
"description": "La configuration Powercalc {name} était liée à un appareil que Home Assistant a divisé en plusieurs appareils. Ses entités ne peuvent pas être liées à l'ancien identifiant de l'appareil.\n\nSélectionnez l'appareil auquel ces entités doivent être liées, ou laissez le champ vide pour ne les lier à aucun appareil.",
"title": "Sélectionner un appareil pour {name}"
}
}
},
"title": "L'appareil de la configuration Powercalc {name} doit être sélectionné à nouveau"
},
"deprecated_platform_yaml": {
"description": "La configuration des capteurs à l'aide de `sensor->platform` est obsolète. Vous devez changer votre configuration en `powercalc->sensors`. Cliquez sur 'En savoir plus' pour plus d'instructions.",
"title": "La configuration Powercalc YAML a été déplacée"
@@ -562,6 +562,29 @@
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
"abort": {
"entry_removed": "La voce di configurazione Powercalc è stata rimossa"
},
"error": {
"invalid_device": "Seleziona un dispositivo esistente o lascia il campo vuoto"
},
"step": {
"select_device": {
"data": {
"device": "Dispositivo"
},
"data_description": {
"device": "Il dispositivo a cui devono essere collegate le entità create da questa configurazione Powercalc"
},
"description": "La configurazione Powercalc {name} era collegata a un dispositivo che Home Assistant ha suddiviso in più dispositivi. Le sue entità non possono essere collegate al vecchio ID dispositivo.\n\nSeleziona il dispositivo a cui collegare queste entità oppure lascia il campo vuoto per lasciarle senza dispositivo.",
"title": "Seleziona il dispositivo per {name}"
}
}
},
"title": "Il dispositivo per la configurazione Powercalc {name} deve essere selezionato di nuovo"
},
"deprecated_platform_yaml": {
"description": "Configurare i sensori usando `sensor->piattaforma` è stato deprecato. È necessario modificare la configurazione in `powercalc->sensor`. Fare clic su 'Ulteriori informazioni' per ulteriori istruzioni.",
"title": "La configurazione di Powercalc YAML è stata spostata"
@@ -562,6 +562,29 @@
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
"abort": {
"entry_removed": "Het Powercalc-configuratie-item is verwijderd"
},
"error": {
"invalid_device": "Selecteer een bestaand apparaat of laat het veld leeg"
},
"step": {
"select_device": {
"data": {
"device": "Apparaat"
},
"data_description": {
"device": "Het apparaat waaraan de entiteiten die door deze Powercalc-configuratie zijn aangemaakt, moeten worden gekoppeld"
},
"description": "De Powercalc-configuratie {name} was gekoppeld aan een apparaat dat Home Assistant heeft opgesplitst in meerdere apparaten. De entiteiten kunnen niet aan de oude apparaat-ID worden gekoppeld.\n\nSelecteer het apparaat waaraan deze entiteiten moeten worden gekoppeld, of laat het veld leeg om ze niet aan een apparaat te koppelen.",
"title": "Selecteer apparaat voor {name}"
}
}
},
"title": "Het apparaat voor Powercalc-configuratie {name} moet opnieuw worden geselecteerd"
},
"deprecated_platform_yaml": {
"description": "Configuratie middels `sensor->platform` is verouderd. Verander je configratie naar `powercalc->sensors`. Klik op 'Meer informatie' voor verder uitleg.",
"title": "Powercalc YAML configuratie is verplaatst"
@@ -562,6 +562,29 @@
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
"abort": {
"entry_removed": "Wpis konfiguracji Powercalc został usunięty"
},
"error": {
"invalid_device": "Wybierz istniejące urządzenie lub pozostaw pole puste"
},
"step": {
"select_device": {
"data": {
"device": "Urządzenie"
},
"data_description": {
"device": "Urządzenie, z którym mają zostać powiązane encje utworzone przez tę konfigurację Powercalc"
},
"description": "Konfiguracja Powercalc {name} była powiązana z urządzeniem, które Home Assistant podzielił na wiele urządzeń. Jej encji nie można powiązać ze starym identyfikatorem urządzenia.\n\nWybierz urządzenie, z którym mają zostać powiązane te encje, lub pozostaw pole puste, aby pozostawić je bez urządzenia.",
"title": "Wybierz urządzenie dla {name}"
}
}
},
"title": "Urządzenie dla konfiguracji Powercalc {name} musi zostać wybrane ponownie"
},
"deprecated_platform_yaml": {
"description": "Konfigurowanie czujników przy użyciu `sensor->platform` zostało uznane za przestarzałe. Musisz zmienić konfigurację na `powercalc->sensors`. Aby uzyskać dalsze instrukcje, kliknij 'Dowiedz się więcej'.",
"title": "Konfiguracja Powercalc w trybie YAML została zmieniona (breaking change)"
@@ -562,6 +562,29 @@
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
"abort": {
"entry_removed": "A entrada de configuração do Powercalc foi removida"
},
"error": {
"invalid_device": "Selecione um dispositivo existente ou deixe o campo vazio"
},
"step": {
"select_device": {
"data": {
"device": "Dispositivo"
},
"data_description": {
"device": "O dispositivo ao qual as entidades criadas por esta configuração do Powercalc devem ser vinculadas"
},
"description": "A configuração do Powercalc {name} estava vinculada a um dispositivo que o Home Assistant dividiu em vários dispositivos. Suas entidades não podem ser vinculadas ao ID antigo do dispositivo.\n\nSelecione o dispositivo ao qual essas entidades devem ser vinculadas ou deixe o campo vazio para mantê-las sem um dispositivo.",
"title": "Selecionar dispositivo para {name}"
}
}
},
"title": "O dispositivo da configuração do Powercalc {name} precisa ser selecionado novamente"
},
"deprecated_platform_yaml": {
"description": "A configuração de sensores usando `sensor->platform` foi descontinuada. Você precisa alterar sua configuração para `powercalc->sensores`. Clique em 'Saiba mais' para mais instruções.",
"title": "Configuração YAML do Powercalc foi movida"
@@ -562,6 +562,29 @@
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
"abort": {
"entry_removed": "A entrada de configuração do Powercalc foi removida"
},
"error": {
"invalid_device": "Selecione um dispositivo existente ou deixe o campo vazio"
},
"step": {
"select_device": {
"data": {
"device": "Dispositivo"
},
"data_description": {
"device": "O dispositivo ao qual as entidades criadas por esta configuração do Powercalc devem ser associadas"
},
"description": "A configuração do Powercalc {name} estava associada a um dispositivo que o Home Assistant dividiu em vários dispositivos. As respetivas entidades não podem ser associadas ao ID antigo do dispositivo.\n\nSelecione o dispositivo ao qual estas entidades devem ser associadas ou deixe o campo vazio para as deixar sem dispositivo.",
"title": "Selecionar dispositivo para {name}"
}
}
},
"title": "O dispositivo da configuração do Powercalc {name} tem de ser selecionado novamente"
},
"deprecated_platform_yaml": {
"description": "A configuração de sensores usando `sensor->platform` foi descontinuada. Deve mudar a sua configuração para `powercalc->sensors`. Clicar em 'Saber mais' para instruções mais detalhadas.",
"title": "A configuração YAML do Powercalc foi movida"
@@ -562,6 +562,29 @@
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
"abort": {
"entry_removed": "Intrarea de configurare Powercalc a fost eliminată"
},
"error": {
"invalid_device": "Selectează un dispozitiv existent sau lasă câmpul necompletat"
},
"step": {
"select_device": {
"data": {
"device": "Dispozitiv"
},
"data_description": {
"device": "Dispozitivul la care trebuie asociate entitățile create de această configurație Powercalc"
},
"description": "Configurația Powercalc {name} era asociată unui dispozitiv pe care Home Assistant l-a împărțit în mai multe dispozitive. Entitățile sale nu pot fi asociate vechiului ID de dispozitiv.\n\nSelectează dispozitivul la care trebuie asociate aceste entități sau lasă câmpul necompletat pentru a le lăsa fără dispozitiv.",
"title": "Selectează dispozitivul pentru {name}"
}
}
},
"title": "Dispozitivul pentru configurația Powercalc {name} trebuie selectat din nou"
},
"deprecated_platform_yaml": {
"description": "Configurarea senzorilor folosind `senzor->platform` a fost retrasă. Trebuie să vă schimbați configurația la `powercalc->senzori`. Faceți clic pe 'Aflați mai multe' pentru instrucțiuni suplimentare.",
"title": "Configurația Powercalc YAML a fost mutată"
@@ -562,6 +562,29 @@
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
"abort": {
"entry_removed": "Запись конфигурации Powercalc была удалена"
},
"error": {
"invalid_device": "Выберите существующее устройство или оставьте поле пустым"
},
"step": {
"select_device": {
"data": {
"device": "Устройство"
},
"data_description": {
"device": "Устройство, к которому следует привязать сущности, созданные этой конфигурацией Powercalc"
},
"description": "Конфигурация Powercalc {name} была привязана к устройству, которое Home Assistant разделил на несколько устройств. Её сущности нельзя привязать к старому идентификатору устройства.\n\nВыберите устройство, к которому следует привязать эти сущности, или оставьте поле пустым, чтобы не привязывать их к устройству.",
"title": "Выберите устройство для {name}"
}
}
},
"title": "Необходимо повторно выбрать устройство для конфигурации Powercalc {name}"
},
"deprecated_platform_yaml": {
"description": "Конфигурация сенсоров через `sensor->platform` устарела. Необходимо изменить конфигурацию на `powercalc->sensors`. Нажмите 'Подробнее' для инструкций.",
"title": "YAML-конфигурация Powercalc перемещена"
@@ -562,6 +562,29 @@
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
"abort": {
"entry_removed": "Konfiguračný záznam Powercalc bol odstránený"
},
"error": {
"invalid_device": "Vyberte existujúce zariadenie alebo nechajte pole prázdne"
},
"step": {
"select_device": {
"data": {
"device": "Zariadenie"
},
"data_description": {
"device": "Zariadenie, ku ktorému majú byť priradené entity vytvorené touto konfiguráciou Powercalc"
},
"description": "Konfigurácia Powercalc {name} bola priradená k zariadeniu, ktoré Home Assistant rozdelil na viacero zariadení. Jej entity nemožno priradiť k starému ID zariadenia.\n\nVyberte zariadenie, ku ktorému majú byť tieto entity priradené, alebo nechajte pole prázdne, aby zostali bez zariadenia.",
"title": "Vyberte zariadenie pre {name}"
}
}
},
"title": "Zariadenie pre konfiguráciu Powercalc {name} je potrebné vybrať znova"
},
"deprecated_platform_yaml": {
"description": "Konfigurácia senzorov pomocou `sensor->platform` bola zastaraná. Musíte zmeniť konfiguráciu na `powercalc->sensors`. Kliknite na 'Viac informácií' pre ďalšie pokyny.",
"title": "Konfigurácia Powercalc YAML bola presunutá"
@@ -562,6 +562,29 @@
}
},
"issues": {
"composite_device_id": {
"fix_flow": {
"abort": {
"entry_removed": "Konfigurationsposten för Powercalc har tagits bort"
},
"error": {
"invalid_device": "Välj en befintlig enhet eller lämna fältet tomt"
},
"step": {
"select_device": {
"data": {
"device": "Enhet"
},
"data_description": {
"device": "Enheten som entiteterna som skapats av denna Powercalc-konfiguration ska länkas till"
},
"description": "Powercalc-konfigurationen {name} var länkad till en enhet som Home Assistant har delat upp i flera enheter. Dess entiteter kan inte länkas till det gamla enhets-ID:t.\n\nVälj den enhet som dessa entiteter ska länkas till, eller lämna fältet tomt för att lämna dem utan en enhet.",
"title": "Välj enhet för {name}"
}
}
},
"title": "Enheten för Powercalc-konfigurationen {name} måste väljas igen"
},
"deprecated_platform_yaml": {
"description": "Att konfigurera sensorer med sensor->plattform har fasats ut. Du behöver ändra din konfiguration till powercalc->sensorer. Klicka på 'Läs mer' för ytterligare instruktioner.",
"title": "Powercalc YAML konfiguration har flyttats"