329 files

This commit is contained in:
Home Assistant Version Control
2026-08-06 13:56:25 +00:00
parent 0df89406fa
commit 7afe7add1d
330 changed files with 13098 additions and 5942 deletions
@@ -1,12 +1,11 @@
"""Config/options flow for a standalone cost sensor based on an existing energy sensor."""
from __future__ import annotations
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.core import HomeAssistant
from homeassistant.data_entry_flow import section
from homeassistant.helpers import selector
from homeassistant.helpers.schema_config_entry_flow import SchemaFlowError
@@ -21,28 +20,35 @@ from custom_components.powercalc.const import (
SensorType,
)
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
from custom_components.powercalc.flow_helper.schema import SECTION_COST_PRICING, build_cost_pricing_schema
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from custom_components.powercalc.config_flow import PowercalcConfigFlow, PowercalcOptionsFlow
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}),
},
)
SCHEMA_COST = vol.Schema(
{
vol.Required(CONF_NAME): selector.TextSelector(),
**SCHEMA_COST_OPTIONS.schema,
},
)
def build_cost_options_schema(hass: HomeAssistant, current_price_entity_id: str | None = None) -> vol.Schema:
"""Return the schema for the options of a standalone cost sensor."""
return vol.Schema(
{
vol.Required(CONF_ENERGY_SENSOR_ID): selector.EntitySelector(
selector.EntitySelectorConfig(domain="sensor", device_class=SensorDeviceClass.ENERGY),
),
vol.Optional(SECTION_COST_PRICING): section(
build_cost_pricing_schema(hass, current_price_entity_id),
{"collapsed": True},
),
},
)
def build_cost_schema(hass: HomeAssistant) -> vol.Schema:
"""Return the schema for creating a standalone cost sensor."""
return vol.Schema(
{
vol.Required(CONF_NAME): selector.TextSelector(),
**build_cost_options_schema(hass).schema,
},
)
def is_global_price_configured(hass: HomeAssistant) -> bool:
@@ -68,13 +74,17 @@ class CostConfigFlow:
"""Handle the flow for a standalone cost sensor."""
self.flow.selected_sensor_type = SensorType.COST
return await self.flow.handle_form_step(
PowercalcFormStep(step=Step.COST, schema=SCHEMA_COST, validate_user_input=self.validate),
PowercalcFormStep(
step=Step.COST,
schema=build_cost_schema(self.flow.hass),
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)
user_input = flatten_sections(user_input, build_cost_schema(self.flow.hass))
if validate_price_configured(self.flow.hass, user_input):
raise SchemaFlowError("cost_price_mandatory")
return user_input
@@ -88,7 +98,7 @@ class CostOptionsFlow:
"""Handle the cost sensor options flow."""
return await self.flow.async_handle_options_step(
user_input,
SCHEMA_COST_OPTIONS,
build_cost_options_schema(self.flow.hass, self.flow.sensor_config.get(CONF_ENERGY_PRICE_SENSOR)),
Step.COST,
validate=lambda flat_input: validate_price_configured(self.flow.hass, flat_input),
)
@@ -1,5 +1,3 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from homeassistant.config_entries import ConfigFlowResult
@@ -1,5 +1,3 @@
from __future__ import annotations
from datetime import timedelta
from typing import TYPE_CHECKING, Any
@@ -55,12 +53,12 @@ from custom_components.powercalc.flow_helper.schema import (
COST_DOCS_URI,
SCHEMA_COST_APPLY,
SCHEMA_ENERGY_OPTIONS,
SCHEMA_GLOBAL_COST,
SCHEMA_GLOBAL_COST_FLAT,
SCHEMA_UTILITY_METER_OPTIONS,
SCHEMA_UTILITY_METER_TOGGLE,
SECTION_COST_NAMING,
SECTION_COST_PRICING,
build_global_cost_schema,
)
from custom_components.powercalc.power_profile.power_profile import DeviceType
from custom_components.powercalc.service.gui_configuration import apply_field_to_config_entries
@@ -325,7 +323,7 @@ class GlobalConfigurationFlow:
form_step = PowercalcFormStep(
step=Step.GLOBAL_CONFIGURATION_COST,
schema=SCHEMA_GLOBAL_COST,
schema=build_global_cost_schema(self.flow.hass, self.flow.global_config.get(CONF_ENERGY_PRICE_SENSOR)),
form_kwarg={
"description_placeholders": {
"docs_uri": COST_DOCS_URI,
@@ -1,7 +1,5 @@
"""Group-related logic for the config flow."""
from __future__ import annotations
from collections.abc import Callable
from typing import TYPE_CHECKING, Any
@@ -1,8 +1,7 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from homeassistant.config_entries import ConfigFlowResult
from homeassistant.const import CONF_DEVICE
from homeassistant.helpers import selector, translation
import voluptuous as vol
@@ -21,6 +20,7 @@ from custom_components.powercalc.const import (
LIBRARY_URL,
CalculationStrategy,
)
from custom_components.powercalc.device_binding import get_devices_for_config_entry
from custom_components.powercalc.discovery import (
get_power_profile_by_source_device,
get_power_profile_by_source_entity,
@@ -29,6 +29,7 @@ from custom_components.powercalc.flow_helper.common import FlowType, PowercalcFo
from custom_components.powercalc.flow_helper.dynamic_field_builder import build_dynamic_field_schema
from custom_components.powercalc.flow_helper.schema import (
SCHEMA_ENERGY_SENSOR_TOGGLE,
SCHEMA_STANDBY_ENERGY_SENSOR_TOGGLE,
SCHEMA_UTILITY_METER_TOGGLE,
build_sub_profile_schema,
)
@@ -58,6 +59,7 @@ SCHEMA_POWER_AUTODISCOVERED = vol.Schema(
SCHEMA_POWER_OPTIONS_LIBRARY = vol.Schema(
{
**SCHEMA_ENERGY_SENSOR_TOGGLE.schema,
**SCHEMA_STANDBY_ENERGY_SENSOR_TOGGLE.schema,
**SCHEMA_UTILITY_METER_TOGGLE.schema,
},
)
@@ -208,6 +210,9 @@ class LibraryFlow:
if Step.LIBRARY_CUSTOM_FIELDS not in handled_steps and profile.has_custom_fields:
return await self.async_step_library_custom_fields()
if Step.SELECT_DEVICE not in handled_steps and profile.discovery_by == DiscoveryBy.CONFIG_ENTRY:
return await self.async_step_select_device()
if Step.AVAILABILITY_ENTITY not in handled_steps and profile.discovery_by == DiscoveryBy.DEVICE:
result = await self.async_step_availability_entity()
if result:
@@ -218,6 +223,35 @@ class LibraryFlow:
return None
async def async_step_select_device(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Ask which device should receive the entities created for a config-entry profile."""
assert self.flow.source_entity is not None
assert self.flow.source_entity.config_entry_id is not None
devices = get_devices_for_config_entry(self.flow.hass, self.flow.source_entity.config_entry_id)
return await self.flow.handle_form_step(
PowercalcFormStep(
step=Step.SELECT_DEVICE,
schema=vol.Schema(
{
vol.Required(CONF_DEVICE): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
selector.SelectOptionDict(
value=device.id,
label=device.name_by_user or device.name or device.id,
)
for device in devices
],
mode=selector.SelectSelectorMode.DROPDOWN,
),
),
},
),
next_step=Step.POST_LIBRARY,
),
user_input,
)
async def _async_next_strategy_step(self, profile: PowerProfile) -> ConfigFlowResult | None:
"""Return the next step needed to configure the calculation strategy, or None when nothing is left to ask."""
handled_steps = self.flow.handled_steps
@@ -397,7 +431,7 @@ class LibraryFlow:
def _get_library_device_types(self) -> set[DeviceType] | None:
"""Determine which device types should be shown in the library selectors."""
if self._get_library_discovery_by() == DiscoveryBy.DEVICE:
if self._get_library_discovery_by() in (DiscoveryBy.CONFIG_ENTRY, DiscoveryBy.DEVICE):
return None
if self.flow.source_entity:
@@ -407,9 +441,19 @@ class LibraryFlow:
def _get_library_discovery_by(self) -> DiscoveryBy | None:
"""Determine whether listing should be filtered by discovery mode."""
if self.flow.source_entity and self.flow.source_entity.entity_id == DUMMY_ENTITY_ID:
return DiscoveryBy.DEVICE
return None
source_entity = self.flow.source_entity
if not source_entity:
return None
if source_entity.config_entry_id:
return DiscoveryBy.CONFIG_ENTRY
if source_entity.entity_id != DUMMY_ENTITY_ID:
return None
profile = self.flow.selected_profile
if profile and profile.discovery_by == DiscoveryBy.CONFIG_ENTRY:
return DiscoveryBy.CONFIG_ENTRY
return DiscoveryBy.DEVICE
class LibraryConfigFlow(LibraryFlow):
@@ -546,19 +590,22 @@ class LibraryConfigFlow(LibraryFlow):
def _get_profile_source(self, profile: PowerProfile) -> str:
"""Build the autodiscovery source description."""
source_entity = self.flow.source_entity
if source_entity and profile.discovery_by == DiscoveryBy.CONFIG_ENTRY:
config_entry_id = source_entity.config_entry_id
if config_entry_id:
return source_entity.name or config_entry_id
translations = translation.async_get_cached_translations(
self.flow.hass,
self.flow.hass.config.language,
"common",
DOMAIN,
)
if (
profile.discovery_by == DiscoveryBy.DEVICE
and self.flow.source_entity
and self.flow.source_entity.device_entry
):
device_entry = source_entity.device_entry if source_entity else None
if profile.discovery_by == DiscoveryBy.DEVICE and device_entry:
label = translations.get(f"component.{DOMAIN}.common.source_device")
return f"{label}: {self.flow.source_entity.device_entry.name}"
return f"{label}: {device_entry.name}"
return f"{translations.get(f'component.{DOMAIN}.common.source_entity')}: {self.flow.source_entity_id}"
@@ -1,7 +1,5 @@
"""Real-power logic for the config flow."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from homeassistant.components.sensor import SensorDeviceClass
@@ -1,5 +1,3 @@
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
from typing import TYPE_CHECKING, Any
@@ -16,6 +14,7 @@ from custom_components.powercalc.const import (
CONF_CALCULATION_ENABLED_CONDITION,
CONF_CALIBRATE,
CONF_CREATE_ENERGY_SENSOR,
CONF_CREATE_STANDBY_ENERGY_SENSOR,
CONF_CREATE_UTILITY_METERS,
CONF_FIXED,
CONF_FIXED_VALUE,
@@ -57,6 +56,7 @@ from custom_components.powercalc.flow_helper.profile_preview import PREVIEW_NAME
from custom_components.powercalc.flow_helper.schema import (
SCHEMA_ENERGY_SENSOR_TOGGLE,
SCHEMA_SENSOR_ENERGY_OPTIONS,
SCHEMA_STANDBY_ENERGY_SENSOR_TOGGLE,
SCHEMA_UTILITY_METER_TOGGLE,
)
from custom_components.powercalc.flow_helper.strategy_form import (
@@ -92,6 +92,7 @@ SCHEMA_POWER_OPTIONS = vol.Schema(
{
vol.Optional(CONF_STANDBY_POWER): vol.Coerce(float),
**SCHEMA_ENERGY_SENSOR_TOGGLE.schema,
**SCHEMA_STANDBY_ENERGY_SENSOR_TOGGLE.schema,
**SCHEMA_UTILITY_METER_TOGGLE.schema,
},
)
@@ -358,7 +359,9 @@ class VirtualPowerFlow:
return self.flow.persist_config_entry()
schema = SCHEMA_POWER_ADVANCED
if self.flow.sensor_config.get(CONF_CREATE_ENERGY_SENSOR):
if self.flow.sensor_config.get(CONF_CREATE_ENERGY_SENSOR) or self.flow.sensor_config.get(
CONF_CREATE_STANDBY_ENERGY_SENSOR,
):
schema = schema.extend(SCHEMA_SENSOR_ENERGY_OPTIONS.schema)
return await self.flow.handle_form_step(