Initial Commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,96 @@
|
||||
from collections.abc import Callable, Coroutine
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
||||
class Step(StrEnum):
|
||||
ADVANCED_OPTIONS = "advanced_options"
|
||||
ASSIGN_GROUPS = "assign_groups"
|
||||
AVAILABILITY_ENTITY = "availability_entity"
|
||||
BASIC_OPTIONS = "basic_options"
|
||||
GROUP_CUSTOM = "group_custom"
|
||||
GROUP_DOMAIN = "group_domain"
|
||||
GROUP_SUBTRACT = "group_subtract"
|
||||
GROUP_TRACKED_UNTRACKED = "group_tracked_untracked"
|
||||
GROUP_TRACKED_UNTRACKED_AUTO = "group_tracked_untracked_auto"
|
||||
GROUP_TRACKED_UNTRACKED_MANUAL = "group_tracked_untracked_manual"
|
||||
LIBRARY = "library"
|
||||
POST_LIBRARY = "post_library"
|
||||
LIBRARY_CUSTOM_FIELDS = "library_custom_fields"
|
||||
LIBRARY_MULTI_PROFILE = "library_multi_profile"
|
||||
LIBRARY_OPTIONS = "library_options"
|
||||
VIRTUAL_POWER = "virtual_power"
|
||||
FIXED = "fixed"
|
||||
LINEAR = "linear"
|
||||
MULTI_SWITCH = "multi_switch"
|
||||
PLAYBOOK = "playbook"
|
||||
WLED = "wled"
|
||||
POWER_ADVANCED = "power_advanced"
|
||||
DAILY_ENERGY = "daily_energy"
|
||||
REAL_POWER = "real_power"
|
||||
MANUFACTURER = "manufacturer"
|
||||
MENU_LIBRARY = "menu_library"
|
||||
MENU_GROUP = "menu_group"
|
||||
MODEL = "model"
|
||||
SUB_PROFILE = "sub_profile"
|
||||
SUB_PROFILE_PER_DEVICE = "sub_profile_per_device"
|
||||
USER = "user"
|
||||
SMART_SWITCH = "smart_switch"
|
||||
INIT = "init"
|
||||
ENERGY_OPTIONS = "energy_options"
|
||||
UTILITY_METER_OPTIONS = "utility_meter_options"
|
||||
GLOBAL_CONFIGURATION = "global_configuration"
|
||||
GLOBAL_CONFIGURATION_DISCOVERY = "global_configuration_discovery"
|
||||
GLOBAL_CONFIGURATION_ENERGY = "global_configuration_energy"
|
||||
GLOBAL_CONFIGURATION_THROTTLING = "global_configuration_throttling"
|
||||
GLOBAL_CONFIGURATION_UTILITY_METER = "global_configuration_utility_meter"
|
||||
|
||||
|
||||
class FlowType(StrEnum):
|
||||
VIRTUAL_POWER = "virtual_power"
|
||||
DAILY_ENERGY = "daily_energy"
|
||||
REAL_POWER = "real_power"
|
||||
LIBRARY = "library"
|
||||
GROUP = "group"
|
||||
GLOBAL_CONFIGURATION = "global_configuration"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PowercalcFormStep:
|
||||
schema: vol.Schema | Callable[[], Coroutine[Any, Any, vol.Schema | None]]
|
||||
step: Step
|
||||
validate_user_input: (
|
||||
Callable[
|
||||
[dict[str, Any]],
|
||||
Coroutine[Any, Any, dict[str, Any]],
|
||||
]
|
||||
| None
|
||||
) = None
|
||||
|
||||
next_step: Step | Callable[[dict[str, Any]], Coroutine[Any, Any, Step | None]] | None = None
|
||||
continue_utility_meter_options_step: bool = False
|
||||
continue_advanced_step: bool = False
|
||||
form_kwarg: dict[str, Any] | None = None
|
||||
form_data: dict[str, Any] | None = None
|
||||
|
||||
|
||||
def fill_schema_defaults(
|
||||
data_schema: vol.Schema,
|
||||
options: dict[str, Any],
|
||||
) -> vol.Schema:
|
||||
"""Make a copy of the schema with suggested values set to saved options."""
|
||||
schema = {}
|
||||
for key, val in data_schema.schema.items():
|
||||
new_key = key
|
||||
if key in options and isinstance(key, vol.Marker):
|
||||
if isinstance(key, vol.Optional) and callable(key.default) and key.default():
|
||||
new_key = vol.Optional(key.schema, default=options.get(key)) # type: ignore
|
||||
elif "suggested_value" not in (new_key.description or {}):
|
||||
new_key = copy.copy(key)
|
||||
new_key.description = {"suggested_value": options.get(key)} # type: ignore
|
||||
schema[new_key] = val
|
||||
return vol.Schema(schema)
|
||||
@@ -0,0 +1,29 @@
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.selector import selector
|
||||
import voluptuous as vol
|
||||
|
||||
from custom_components.powercalc.common import SourceEntity
|
||||
from custom_components.powercalc.power_profile.power_profile import PowerProfile
|
||||
|
||||
|
||||
def build_dynamic_field_schema(hass: HomeAssistant, profile: PowerProfile, source_entity: SourceEntity | None) -> vol.Schema:
|
||||
schema = {}
|
||||
for field in profile.custom_fields:
|
||||
field_description = field.description
|
||||
if not field_description:
|
||||
field_description = field.label
|
||||
|
||||
if field.default is not None:
|
||||
key = vol.Required(field.key, description=field_description, default=field.default)
|
||||
else:
|
||||
key = vol.Required(field.key, description=field_description)
|
||||
|
||||
if "entity" in field.selector and source_entity and source_entity.device_entry:
|
||||
entity_reg = er.async_get(hass)
|
||||
field.selector["entity"]["include_entities"] = [
|
||||
entity.entity_id for entity in entity_reg.entities.get_entries_for_device_id(source_entity.device_entry.id)
|
||||
]
|
||||
|
||||
schema[key] = selector(field.selector)
|
||||
return vol.Schema(schema)
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,113 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, UnitOfEnergy, UnitOfPower, UnitOfTime
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.schema_config_entry_flow import SchemaFlowError
|
||||
import voluptuous as vol
|
||||
|
||||
from custom_components.powercalc import CONF_CREATE_UTILITY_METERS
|
||||
from custom_components.powercalc.const import (
|
||||
CONF_DAILY_FIXED_ENERGY,
|
||||
CONF_GROUP,
|
||||
CONF_ON_TIME,
|
||||
CONF_UPDATE_FREQUENCY,
|
||||
CONF_VALUE,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
SensorType,
|
||||
)
|
||||
from custom_components.powercalc.flow_helper.common import PowercalcFormStep, Step, fill_schema_defaults
|
||||
from custom_components.powercalc.flow_helper.schema import SCHEMA_UTILITY_METER_TOGGLE
|
||||
from custom_components.powercalc.sensors.daily_energy import DEFAULT_DAILY_UPDATE_FREQUENCY
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from custom_components.powercalc.config_flow import PowercalcConfigFlow, PowercalcOptionsFlow
|
||||
|
||||
SCHEMA_DAILY_ENERGY_OPTIONS = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_VALUE): vol.Coerce(float),
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): selector.TemplateSelector(),
|
||||
vol.Optional(
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
default=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
): vol.In(
|
||||
[UnitOfEnergy.KILO_WATT_HOUR, UnitOfPower.WATT],
|
||||
),
|
||||
vol.Optional(CONF_ON_TIME): selector.DurationSelector(
|
||||
selector.DurationSelectorConfig(enable_day=False),
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_UPDATE_FREQUENCY,
|
||||
default=DEFAULT_DAILY_UPDATE_FREQUENCY,
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=10,
|
||||
unit_of_measurement=UnitOfTime.SECONDS,
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
SCHEMA_DAILY_ENERGY = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): selector.TextSelector(),
|
||||
**SCHEMA_UTILITY_METER_TOGGLE.schema,
|
||||
},
|
||||
).extend(SCHEMA_DAILY_ENERGY_OPTIONS.schema)
|
||||
|
||||
|
||||
def build_daily_energy_config(user_input: dict[str, Any], schema: vol.Schema) -> dict[str, Any]:
|
||||
"""Build the config under daily_energy: key."""
|
||||
config: dict[str, Any] = {
|
||||
CONF_DAILY_FIXED_ENERGY: {},
|
||||
}
|
||||
for key, val in user_input.items():
|
||||
if key in schema.schema and val is not None:
|
||||
if key in {CONF_CREATE_UTILITY_METERS, CONF_GROUP, CONF_NAME, CONF_UNIQUE_ID}:
|
||||
config[str(key)] = val
|
||||
continue
|
||||
|
||||
config[CONF_DAILY_FIXED_ENERGY][str(key)] = val
|
||||
return config
|
||||
|
||||
|
||||
class DailyEnergyConfigFlow:
|
||||
def __init__(self, flow: PowercalcConfigFlow) -> None:
|
||||
self.flow: PowercalcConfigFlow = flow
|
||||
|
||||
async def async_step_daily_energy(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> FlowResult:
|
||||
"""Handle the flow for daily energy sensor."""
|
||||
self.flow.selected_sensor_type = SensorType.DAILY_ENERGY
|
||||
|
||||
async def _validate(user_input: dict[str, Any]) -> dict[str, Any]:
|
||||
if CONF_VALUE not in user_input and CONF_VALUE_TEMPLATE not in user_input:
|
||||
raise SchemaFlowError("daily_energy_mandatory")
|
||||
return build_daily_energy_config(user_input, SCHEMA_DAILY_ENERGY)
|
||||
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.DAILY_ENERGY,
|
||||
schema=SCHEMA_DAILY_ENERGY,
|
||||
validate_user_input=_validate,
|
||||
next_step=Step.ASSIGN_GROUPS,
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
|
||||
class DailyEnergyOptionsFlow:
|
||||
def __init__(self, flow: PowercalcOptionsFlow) -> None:
|
||||
self.flow: PowercalcOptionsFlow = flow
|
||||
|
||||
async def async_step_daily_energy(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the daily energy options flow."""
|
||||
schema = fill_schema_defaults(
|
||||
SCHEMA_DAILY_ENERGY_OPTIONS,
|
||||
self.flow.sensor_config[CONF_DAILY_FIXED_ENERGY],
|
||||
)
|
||||
return await self.flow.async_handle_options_step(user_input, schema, Step.DAILY_ENERGY)
|
||||
@@ -0,0 +1,287 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.const import CONF_ENABLED, CONF_SENSORS, UnitOfTime
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
import voluptuous as vol
|
||||
|
||||
from custom_components.powercalc import DeviceType
|
||||
from custom_components.powercalc.const import (
|
||||
CONF_CREATE_ENERGY_SENSORS,
|
||||
CONF_CREATE_STANDBY_GROUP,
|
||||
CONF_CREATE_UTILITY_METERS,
|
||||
CONF_DISABLE_EXTENDED_ATTRIBUTES,
|
||||
CONF_DISABLE_LIBRARY_DOWNLOAD,
|
||||
CONF_DISCOVERY,
|
||||
CONF_ENABLE_ANALYTICS,
|
||||
CONF_ENERGY_SENSOR_CATEGORY,
|
||||
CONF_ENERGY_SENSOR_FRIENDLY_NAMING,
|
||||
CONF_ENERGY_SENSOR_NAMING,
|
||||
CONF_ENERGY_SENSOR_PRECISION,
|
||||
CONF_ENERGY_UPDATE_INTERVAL,
|
||||
CONF_EXCLUDE_DEVICE_TYPES,
|
||||
CONF_EXCLUDE_SELF_USAGE,
|
||||
CONF_GROUP_ENERGY_UPDATE_INTERVAL,
|
||||
CONF_GROUP_POWER_UPDATE_INTERVAL,
|
||||
CONF_IGNORE_UNAVAILABLE_STATE,
|
||||
CONF_INCLUDE_NON_POWERCALC_SENSORS,
|
||||
CONF_POWER_SENSOR_CATEGORY,
|
||||
CONF_POWER_SENSOR_FRIENDLY_NAMING,
|
||||
CONF_POWER_SENSOR_NAMING,
|
||||
CONF_POWER_SENSOR_PRECISION,
|
||||
CONF_POWER_UPDATE_INTERVAL,
|
||||
CONF_UTILITY_METER_OFFSET,
|
||||
DEFAULT_ENERGY_UPDATE_INTERVAL,
|
||||
DEFAULT_GROUP_ENERGY_UPDATE_INTERVAL,
|
||||
DEFAULT_GROUP_POWER_UPDATE_INTERVAL,
|
||||
DOMAIN,
|
||||
DOMAIN_CONFIG,
|
||||
ENTITY_CATEGORIES,
|
||||
ENTRY_GLOBAL_CONFIG_UNIQUE_ID,
|
||||
)
|
||||
from custom_components.powercalc.flow_helper.common import PowercalcFormStep, Step
|
||||
from custom_components.powercalc.flow_helper.schema import SCHEMA_ENERGY_OPTIONS, SCHEMA_UTILITY_METER_OPTIONS, SCHEMA_UTILITY_METER_TOGGLE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from custom_components.powercalc.config_flow import PowercalcCommonFlow, PowercalcConfigFlow, PowercalcOptionsFlow
|
||||
|
||||
SCHEMA_GLOBAL_CONFIGURATION = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_POWER_SENSOR_NAMING): selector.TextSelector(),
|
||||
vol.Optional(CONF_POWER_SENSOR_FRIENDLY_NAMING): selector.TextSelector(),
|
||||
vol.Optional(CONF_POWER_SENSOR_CATEGORY): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=list(filter(lambda item: item is not None, ENTITY_CATEGORIES)), # type: ignore
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_POWER_SENSOR_PRECISION): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(min=0, max=6, mode=selector.NumberSelectorMode.BOX, step=1),
|
||||
),
|
||||
vol.Optional(CONF_ENABLE_ANALYTICS, default=True): selector.BooleanSelector(),
|
||||
vol.Optional(CONF_IGNORE_UNAVAILABLE_STATE, default=False): selector.BooleanSelector(),
|
||||
vol.Optional(CONF_INCLUDE_NON_POWERCALC_SENSORS, default=True): selector.BooleanSelector(),
|
||||
vol.Optional(CONF_DISABLE_EXTENDED_ATTRIBUTES, default=False): selector.BooleanSelector(),
|
||||
vol.Optional(CONF_DISABLE_LIBRARY_DOWNLOAD, default=False): selector.BooleanSelector(),
|
||||
vol.Optional(CONF_CREATE_STANDBY_GROUP, default=True): selector.BooleanSelector(),
|
||||
vol.Optional(CONF_CREATE_ENERGY_SENSORS, default=True): selector.BooleanSelector(),
|
||||
**SCHEMA_UTILITY_METER_TOGGLE.schema,
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_GLOBAL_CONFIGURATION_DISCOVERY = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ENABLED, default=True): selector.BooleanSelector(),
|
||||
vol.Optional(CONF_EXCLUDE_DEVICE_TYPES): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[cls.value for cls in DeviceType],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
multiple=True,
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_EXCLUDE_SELF_USAGE, default=False): selector.BooleanSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_GLOBAL_CONFIGURATION_THROTTLING = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_POWER_UPDATE_INTERVAL, default=0): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(unit_of_measurement=UnitOfTime.SECONDS, mode=selector.NumberSelectorMode.BOX),
|
||||
),
|
||||
vol.Optional(CONF_ENERGY_UPDATE_INTERVAL, default=DEFAULT_ENERGY_UPDATE_INTERVAL): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(unit_of_measurement=UnitOfTime.SECONDS, mode=selector.NumberSelectorMode.BOX),
|
||||
),
|
||||
vol.Optional(CONF_GROUP_POWER_UPDATE_INTERVAL, default=DEFAULT_GROUP_POWER_UPDATE_INTERVAL): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(unit_of_measurement=UnitOfTime.SECONDS, mode=selector.NumberSelectorMode.BOX),
|
||||
),
|
||||
vol.Optional(CONF_GROUP_ENERGY_UPDATE_INTERVAL, default=DEFAULT_GROUP_ENERGY_UPDATE_INTERVAL): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(unit_of_measurement=UnitOfTime.SECONDS, mode=selector.NumberSelectorMode.BOX),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_GLOBAL_CONFIGURATION_ENERGY_SENSOR = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ENERGY_SENSOR_NAMING): selector.TextSelector(),
|
||||
vol.Optional(CONF_ENERGY_SENSOR_FRIENDLY_NAMING): selector.TextSelector(),
|
||||
vol.Optional(CONF_ENERGY_SENSOR_CATEGORY): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=list(filter(lambda item: item is not None, ENTITY_CATEGORIES)), # type: ignore
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
),
|
||||
**SCHEMA_ENERGY_OPTIONS.schema,
|
||||
vol.Optional(CONF_ENERGY_SENSOR_PRECISION): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(min=0, max=6, mode=selector.NumberSelectorMode.BOX, step=1),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_global_powercalc_config(flow: PowercalcCommonFlow) -> ConfigType:
|
||||
"""Get the global powercalc config."""
|
||||
if flow.global_config:
|
||||
return flow.global_config
|
||||
powercalc = flow.hass.data.get(DOMAIN) or {}
|
||||
global_config = dict.copy(powercalc.get(DOMAIN_CONFIG) or {})
|
||||
utility_meter_offset = global_config.get(CONF_UTILITY_METER_OFFSET)
|
||||
if isinstance(utility_meter_offset, timedelta):
|
||||
global_config[CONF_UTILITY_METER_OFFSET] = utility_meter_offset.days
|
||||
if CONF_SENSORS in global_config:
|
||||
global_config.pop(CONF_SENSORS)
|
||||
flow.global_config = global_config
|
||||
return global_config
|
||||
|
||||
|
||||
class GlobalConfigurationFlow:
|
||||
def __init__(self, flow: PowercalcCommonFlow) -> None:
|
||||
self.flow = flow
|
||||
|
||||
async def async_step_global_configuration_discovery(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the discovery configuration step."""
|
||||
|
||||
if user_input is not None:
|
||||
self.flow.global_config.update({CONF_DISCOVERY: user_input})
|
||||
if self.flow.is_options_flow:
|
||||
return self.flow.persist_config_entry()
|
||||
|
||||
global_config = get_global_powercalc_config(self.flow)
|
||||
discovery_options: dict[str, Any] = global_config.get(CONF_DISCOVERY, {})
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.GLOBAL_CONFIGURATION_DISCOVERY,
|
||||
schema=SCHEMA_GLOBAL_CONFIGURATION_DISCOVERY,
|
||||
next_step=Step.GLOBAL_CONFIGURATION_THROTTLING,
|
||||
form_data=discovery_options,
|
||||
form_kwarg={"description_placeholders": {"docs_uri": "https://docs.powercalc.nl/library/discovery/"}},
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_global_configuration_throttling(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the throttling related options."""
|
||||
|
||||
if user_input is not None:
|
||||
self.flow.global_config.update(user_input)
|
||||
if self.flow.is_options_flow:
|
||||
return self.flow.persist_config_entry()
|
||||
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.GLOBAL_CONFIGURATION_THROTTLING,
|
||||
schema=SCHEMA_GLOBAL_CONFIGURATION_THROTTLING,
|
||||
next_step=Step.GLOBAL_CONFIGURATION_ENERGY,
|
||||
form_kwarg={
|
||||
"description_placeholders": {
|
||||
"docs_uri": "https://docs.powercalc.nl/configuration/update-frequency/",
|
||||
},
|
||||
},
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_global_configuration_energy(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the global configuration step."""
|
||||
|
||||
if user_input is not None:
|
||||
self.flow.global_config.update(user_input)
|
||||
if self.flow.is_options_flow:
|
||||
return self.flow.persist_config_entry()
|
||||
|
||||
if not bool(self.flow.global_config.get(CONF_CREATE_ENERGY_SENSORS)) or user_input is not None:
|
||||
return await self.async_step_global_configuration_utility_meter()
|
||||
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.GLOBAL_CONFIGURATION_ENERGY,
|
||||
schema=SCHEMA_GLOBAL_CONFIGURATION_ENERGY_SENSOR,
|
||||
form_kwarg={"description_placeholders": {"docs_uri": "https://docs.powercalc.nl/configuration/global-configuration/"}},
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_global_configuration_utility_meter(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the global configuration step."""
|
||||
|
||||
if user_input is not None:
|
||||
self.flow.global_config.update(user_input)
|
||||
if self.flow.is_options_flow:
|
||||
return self.flow.persist_config_entry()
|
||||
|
||||
if not bool(self.flow.global_config.get(CONF_CREATE_UTILITY_METERS)) or user_input is not None:
|
||||
return self.flow.async_create_entry( # type: ignore
|
||||
title="Global Configuration",
|
||||
data=self.flow.global_config,
|
||||
)
|
||||
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.GLOBAL_CONFIGURATION_UTILITY_METER,
|
||||
schema=SCHEMA_UTILITY_METER_OPTIONS,
|
||||
form_kwarg={"description_placeholders": {"docs_uri": "https://docs.powercalc.nl/configuration/global-configuration/"}},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class GlobalConfigurationConfigFlow(GlobalConfigurationFlow):
|
||||
def __init__(self, flow: PowercalcConfigFlow) -> None:
|
||||
super().__init__(flow)
|
||||
self.flow: PowercalcConfigFlow = flow
|
||||
|
||||
async def async_step_global_configuration(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the global configuration step."""
|
||||
get_global_powercalc_config(self.flow)
|
||||
await self.flow.async_set_unique_id(ENTRY_GLOBAL_CONFIG_UNIQUE_ID)
|
||||
self.flow.abort_if_unique_id_configured()
|
||||
|
||||
if user_input is not None:
|
||||
self.flow.global_config.update(user_input)
|
||||
return await self.async_step_global_configuration_discovery()
|
||||
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.GLOBAL_CONFIGURATION,
|
||||
schema=SCHEMA_GLOBAL_CONFIGURATION,
|
||||
form_kwarg={
|
||||
"description_placeholders": {
|
||||
"docs_uri": "https://docs.powercalc.nl/configuration/global-configuration/",
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class GlobalConfigurationOptionsFlow(GlobalConfigurationFlow):
|
||||
def __init__(self, flow: PowercalcOptionsFlow) -> None:
|
||||
super().__init__(flow)
|
||||
self.flow = flow
|
||||
|
||||
def build_global_config_menu(self) -> dict[Step, str]:
|
||||
"""Build menu for global configuration"""
|
||||
menu = {
|
||||
Step.GLOBAL_CONFIGURATION: "Basic options",
|
||||
Step.GLOBAL_CONFIGURATION_DISCOVERY: "Discovery options",
|
||||
Step.GLOBAL_CONFIGURATION_THROTTLING: "Throttling options",
|
||||
}
|
||||
if self.flow.global_config.get(CONF_CREATE_ENERGY_SENSORS):
|
||||
menu[Step.GLOBAL_CONFIGURATION_ENERGY] = "Energy options"
|
||||
if self.flow.global_config.get(CONF_CREATE_UTILITY_METERS):
|
||||
menu[Step.GLOBAL_CONFIGURATION_UTILITY_METER] = "Utility meter options"
|
||||
return menu
|
||||
|
||||
async def async_step_global_configuration(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the global configuration step."""
|
||||
|
||||
if user_input is not None:
|
||||
self.flow.global_config.update(user_input)
|
||||
return self.flow.persist_config_entry()
|
||||
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.GLOBAL_CONFIGURATION,
|
||||
schema=SCHEMA_GLOBAL_CONFIGURATION,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,504 @@
|
||||
"""Group-related logic for the config flow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE,
|
||||
CONF_DOMAIN,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_NAME,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.schema_config_entry_flow import SchemaFlowError
|
||||
from homeassistant.helpers.selector import TextSelector
|
||||
import voluptuous as vol
|
||||
|
||||
from custom_components.powercalc.const import (
|
||||
CONF_AREA,
|
||||
CONF_EXCLUDE_ENTITIES,
|
||||
CONF_FLOOR,
|
||||
CONF_FORCE_CALCULATE_GROUP_ENERGY,
|
||||
CONF_GROUP,
|
||||
CONF_GROUP_ENERGY_ENTITIES,
|
||||
CONF_GROUP_ENERGY_START_AT_ZERO,
|
||||
CONF_GROUP_MEMBER_DEVICES,
|
||||
CONF_GROUP_MEMBER_SENSORS,
|
||||
CONF_GROUP_POWER_ENTITIES,
|
||||
CONF_GROUP_TRACKED_AUTO,
|
||||
CONF_GROUP_TRACKED_POWER_ENTITIES,
|
||||
CONF_GROUP_TYPE,
|
||||
CONF_HIDE_MEMBERS,
|
||||
CONF_INCLUDE_NON_POWERCALC_SENSORS,
|
||||
CONF_MAIN_POWER_SENSOR,
|
||||
CONF_NEW_GROUP,
|
||||
CONF_SENSOR_TYPE,
|
||||
CONF_SUB_GROUPS,
|
||||
CONF_SUBTRACT_ENTITIES,
|
||||
DOMAIN,
|
||||
GroupType,
|
||||
SensorType,
|
||||
)
|
||||
from custom_components.powercalc.flow_helper.common import PowercalcFormStep, Step, fill_schema_defaults
|
||||
from custom_components.powercalc.flow_helper.schema import SCHEMA_ENERGY_SENSOR_TOGGLE, SCHEMA_UTILITY_METER_TOGGLE
|
||||
from custom_components.powercalc.group_include.include import find_entities
|
||||
from custom_components.powercalc.sensors.group.config_entry_utils import get_group_entries
|
||||
from custom_components.powercalc.sensors.group.tracked_untracked import find_auto_tracked_power_entities
|
||||
from custom_components.powercalc.sensors.power import PowerSensor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from custom_components.powercalc.config_flow import PowercalcCommonFlow, PowercalcConfigFlow, PowercalcOptionsFlow
|
||||
|
||||
# Constants
|
||||
UNIQUE_ID_TRACKED_UNTRACKED = "pc_tracked_untracked"
|
||||
|
||||
# Schemas
|
||||
SCHEMA_GROUP = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): str,
|
||||
vol.Optional(CONF_DEVICE): selector.DeviceSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_GROUP_DOMAIN_OPTIONS = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_DOMAIN): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=["all"] + [cls.value for cls in Platform],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_EXCLUDE_ENTITIES): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=Platform.SENSOR,
|
||||
device_class=[SensorDeviceClass.ENERGY, SensorDeviceClass.POWER],
|
||||
multiple=True,
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_GROUP_DOMAIN = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): str,
|
||||
**SCHEMA_GROUP_DOMAIN_OPTIONS.schema,
|
||||
**SCHEMA_ENERGY_SENSOR_TOGGLE.schema,
|
||||
**SCHEMA_UTILITY_METER_TOGGLE.schema,
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_GROUP_SUBTRACT_OPTIONS = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=Platform.SENSOR,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
multiple=False,
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_SUBTRACT_ENTITIES): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=Platform.SENSOR,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
multiple=True,
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_GROUP_SUBTRACT = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): selector.TextSelector(),
|
||||
**SCHEMA_GROUP_SUBTRACT_OPTIONS.schema,
|
||||
**SCHEMA_ENERGY_SENSOR_TOGGLE.schema,
|
||||
**SCHEMA_UTILITY_METER_TOGGLE.schema,
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_GROUP_TRACKED_UNTRACKED = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_MAIN_POWER_SENSOR): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=Platform.SENSOR,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_GROUP_TRACKED_AUTO): selector.BooleanSelector(),
|
||||
**SCHEMA_ENERGY_SENSOR_TOGGLE.schema,
|
||||
**SCHEMA_UTILITY_METER_TOGGLE.schema,
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_GROUP_TRACKED_UNTRACKED_MANUAL = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_GROUP_TRACKED_POWER_ENTITIES): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=Platform.SENSOR,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
multiple=True,
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
MENU_GROUP = [
|
||||
Step.GROUP_CUSTOM,
|
||||
Step.GROUP_DOMAIN,
|
||||
Step.GROUP_SUBTRACT,
|
||||
Step.GROUP_TRACKED_UNTRACKED,
|
||||
]
|
||||
|
||||
# Mappings
|
||||
GROUP_SCHEMAS: dict[GroupType, vol.Schema] = {
|
||||
GroupType.CUSTOM: SCHEMA_GROUP,
|
||||
GroupType.DOMAIN: SCHEMA_GROUP_DOMAIN,
|
||||
GroupType.SUBTRACT: SCHEMA_GROUP_SUBTRACT,
|
||||
GroupType.TRACKED_UNTRACKED: SCHEMA_GROUP_TRACKED_UNTRACKED,
|
||||
}
|
||||
|
||||
GROUP_STEP_MAPPING: dict[GroupType, Step] = {
|
||||
GroupType.CUSTOM: Step.GROUP_CUSTOM,
|
||||
GroupType.DOMAIN: Step.GROUP_DOMAIN,
|
||||
GroupType.STANDBY: Step.GROUP_DOMAIN,
|
||||
GroupType.SUBTRACT: Step.GROUP_SUBTRACT,
|
||||
GroupType.TRACKED_UNTRACKED: Step.GROUP_TRACKED_UNTRACKED,
|
||||
}
|
||||
|
||||
|
||||
def validate_group_input(user_input: dict[str, Any] | None = None) -> None:
|
||||
"""Validate the group form."""
|
||||
required_keys = {
|
||||
CONF_SUB_GROUPS,
|
||||
CONF_GROUP_POWER_ENTITIES,
|
||||
CONF_GROUP_ENERGY_ENTITIES,
|
||||
CONF_GROUP_MEMBER_SENSORS,
|
||||
CONF_GROUP_MEMBER_DEVICES,
|
||||
CONF_AREA,
|
||||
CONF_FLOOR,
|
||||
}
|
||||
|
||||
if not any(key in (user_input or {}) for key in required_keys):
|
||||
raise SchemaFlowError("group_mandatory")
|
||||
|
||||
|
||||
def create_schema_group_custom(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry | None = None,
|
||||
is_option_flow: bool = False,
|
||||
) -> vol.Schema:
|
||||
"""Create config schema for groups."""
|
||||
member_sensors = [
|
||||
selector.SelectOptionDict(value=config_entry.entry_id, label=config_entry.title)
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.data.get(CONF_SENSOR_TYPE) in [SensorType.VIRTUAL_POWER, SensorType.REAL_POWER]
|
||||
and config_entry.unique_id is not None
|
||||
and config_entry.title is not None
|
||||
]
|
||||
member_sensor_selector = selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=member_sensors,
|
||||
multiple=True,
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
)
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_GROUP_MEMBER_SENSORS): member_sensor_selector,
|
||||
vol.Optional(CONF_GROUP_MEMBER_DEVICES): selector.DeviceSelector(
|
||||
selector.DeviceSelectorConfig(
|
||||
multiple=True,
|
||||
entity=selector.EntityFilterSelectorConfig(
|
||||
device_class=[SensorDeviceClass.POWER, SensorDeviceClass.ENERGY],
|
||||
),
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_GROUP_POWER_ENTITIES): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=Platform.SENSOR,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
multiple=True,
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_GROUP_ENERGY_ENTITIES): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=Platform.SENSOR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
multiple=True,
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_SUB_GROUPS): create_group_selector(hass, current_entry=config_entry),
|
||||
vol.Optional(CONF_AREA): selector.AreaSelector(),
|
||||
vol.Optional(CONF_FLOOR): selector.FloorSelector(),
|
||||
vol.Optional(CONF_DEVICE): selector.DeviceSelector(),
|
||||
vol.Optional(CONF_HIDE_MEMBERS, default=False): selector.BooleanSelector(),
|
||||
vol.Optional(CONF_INCLUDE_NON_POWERCALC_SENSORS, default=True): selector.BooleanSelector(),
|
||||
vol.Optional(CONF_FORCE_CALCULATE_GROUP_ENERGY, default=False): selector.BooleanSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
if not is_option_flow:
|
||||
schema = schema.extend(
|
||||
{
|
||||
vol.Optional(CONF_GROUP_ENERGY_START_AT_ZERO, default=True): selector.BooleanSelector(),
|
||||
**SCHEMA_ENERGY_SENSOR_TOGGLE.schema,
|
||||
**SCHEMA_UTILITY_METER_TOGGLE.schema,
|
||||
},
|
||||
)
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
def create_group_selector(
|
||||
hass: HomeAssistant,
|
||||
current_entry: ConfigEntry | None = None,
|
||||
group_entries: list[ConfigEntry] | None = None,
|
||||
) -> selector.SelectSelector:
|
||||
"""Create the group selector."""
|
||||
options = [
|
||||
selector.SelectOptionDict(
|
||||
value=config_entry.entry_id,
|
||||
label=config_entry.title,
|
||||
)
|
||||
for config_entry in (group_entries or get_group_entries(hass, GroupType.CUSTOM))
|
||||
if current_entry is None or config_entry.entry_id != current_entry.entry_id
|
||||
]
|
||||
|
||||
return selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=options,
|
||||
multiple=True,
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
custom_value=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def create_schema_tracked_untracked_auto(hass: HomeAssistant) -> vol.Schema:
|
||||
"""Handle the flow for tracked/untracked group sensor."""
|
||||
tracked_entities = await find_auto_tracked_power_entities(hass)
|
||||
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_EXCLUDE_ENTITIES): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
multiple=True,
|
||||
include_entities=list(tracked_entities),
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def create_schema_group_tracked_untracked_manual(
|
||||
hass: HomeAssistant,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
schema: vol.Schema | None = None,
|
||||
) -> vol.Schema:
|
||||
"""Handle the flow for tracked/untracked group sensor."""
|
||||
if not schema:
|
||||
schema = SCHEMA_GROUP_TRACKED_UNTRACKED_MANUAL
|
||||
|
||||
if not user_input:
|
||||
result = await find_entities(hass)
|
||||
tracked_entities = [entity.entity_id for entity in result.resolved if isinstance(entity, PowerSensor)]
|
||||
schema = fill_schema_defaults(schema, {CONF_GROUP_TRACKED_POWER_ENTITIES: tracked_entities})
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
class GroupFlow:
|
||||
def __init__(self, flow: PowercalcCommonFlow) -> None:
|
||||
self.flow = flow
|
||||
|
||||
async def async_step_assign_groups(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the flow for assigning groups."""
|
||||
group_entries = get_group_entries(self.flow.hass, GroupType.CUSTOM)
|
||||
if not group_entries:
|
||||
return await self.flow.handle_final_steps()
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_GROUP): create_group_selector(self.flow.hass, group_entries=group_entries),
|
||||
vol.Optional(CONF_NEW_GROUP): TextSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
async def _validate(user_input: dict[str, Any]) -> dict[str, Any]:
|
||||
groups = user_input.get(CONF_GROUP) or []
|
||||
new_group = user_input.get(CONF_NEW_GROUP)
|
||||
if new_group:
|
||||
groups.append(new_group)
|
||||
return {CONF_GROUP: groups}
|
||||
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.ASSIGN_GROUPS,
|
||||
schema=schema,
|
||||
continue_advanced_step=True,
|
||||
continue_utility_meter_options_step=True,
|
||||
validate_user_input=_validate,
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
|
||||
class GroupConfigFlow(GroupFlow):
|
||||
"""
|
||||
Encapsulates all group-related steps for config & options flows.
|
||||
Composition-based: call from ConfigFlow/OptionsFlow and delegate here.
|
||||
Expects the parent 'flow' to expose:
|
||||
- hass
|
||||
- sensor_config: dict
|
||||
- name: str | None
|
||||
- selected_sensor_type: str | None
|
||||
- async_set_unique_id(), _abort_if_unique_id_configured()
|
||||
- handle_form_step(PowercalcFormStep, user_input) -> FlowResult
|
||||
- async_show_menu(...), fill_schema_defaults(...),
|
||||
- create_group_selector(...), create_schema_group_custom(...)
|
||||
|
||||
We deliberately keep this controller dumb: it only handles group UX.
|
||||
"""
|
||||
|
||||
def __init__(self, flow: PowercalcConfigFlow) -> None:
|
||||
super().__init__(flow)
|
||||
self.flow: PowercalcConfigFlow = flow
|
||||
|
||||
async def async_step_menu_group(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||
menu = [Step.GROUP_CUSTOM, Step.GROUP_DOMAIN, Step.GROUP_SUBTRACT, Step.GROUP_TRACKED_UNTRACKED]
|
||||
# Hide tracked/untracked if already present
|
||||
entry = self.flow.hass.config_entries.async_entry_for_domain_unique_id(
|
||||
DOMAIN,
|
||||
UNIQUE_ID_TRACKED_UNTRACKED,
|
||||
)
|
||||
if entry:
|
||||
menu.remove(Step.GROUP_TRACKED_UNTRACKED)
|
||||
return self.flow.async_show_menu(step_id=Step.MENU_GROUP, menu_options=menu)
|
||||
|
||||
async def handle_group_step(
|
||||
self,
|
||||
group_type: GroupType,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
schema: vol.Schema | None = None,
|
||||
next_step: Callable[[dict[str, Any]], Coroutine[Any, Any, Step | None]] | None = None,
|
||||
) -> FlowResult:
|
||||
async def _validate(ui: dict[str, Any]) -> dict[str, Any]:
|
||||
if group_type == GroupType.CUSTOM:
|
||||
validate_group_input(ui)
|
||||
|
||||
self.flow.name = ui.get(CONF_NAME)
|
||||
self.flow.sensor_config.update(ui)
|
||||
self.flow.sensor_config.update({CONF_GROUP_TYPE: group_type})
|
||||
return ui
|
||||
|
||||
self.flow.selected_sensor_type = SensorType.GROUP
|
||||
step = GROUP_STEP_MAPPING[group_type]
|
||||
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=step,
|
||||
schema=schema or GROUP_SCHEMAS[group_type],
|
||||
validate_user_input=_validate,
|
||||
continue_utility_meter_options_step=True,
|
||||
next_step=next_step,
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_group_custom(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
schema = SCHEMA_GROUP.extend(create_schema_group_custom(self.flow.hass).schema)
|
||||
return await self.handle_group_step(GroupType.CUSTOM, user_input, schema)
|
||||
|
||||
async def async_step_group_domain(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
return await self.handle_group_step(GroupType.DOMAIN, user_input)
|
||||
|
||||
async def async_step_group_subtract(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
return await self.handle_group_step(GroupType.SUBTRACT, user_input)
|
||||
|
||||
async def async_step_group_tracked_untracked(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
await self.flow.async_set_unique_id(UNIQUE_ID_TRACKED_UNTRACKED)
|
||||
self.flow.abort_if_unique_id_configured()
|
||||
if user_input is not None:
|
||||
user_input[CONF_NAME] = "Tracked / Untracked"
|
||||
|
||||
async def _next(ui: dict[str, Any]) -> Step | None:
|
||||
return Step.GROUP_TRACKED_UNTRACKED_AUTO if bool(ui.get("group_tracked_auto", True)) else Step.GROUP_TRACKED_UNTRACKED_MANUAL
|
||||
|
||||
return await self.handle_group_step(
|
||||
GroupType.TRACKED_UNTRACKED,
|
||||
user_input,
|
||||
schema=SCHEMA_GROUP_TRACKED_UNTRACKED,
|
||||
next_step=_next,
|
||||
)
|
||||
|
||||
async def async_step_group_tracked_untracked_auto(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
schema = await create_schema_tracked_untracked_auto(self.flow.hass)
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.GROUP_TRACKED_UNTRACKED_AUTO,
|
||||
schema=schema,
|
||||
continue_utility_meter_options_step=True,
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_group_tracked_untracked_manual(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
schema = await create_schema_group_tracked_untracked_manual(self.flow.hass, user_input)
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.GROUP_TRACKED_UNTRACKED_MANUAL,
|
||||
schema=schema,
|
||||
continue_utility_meter_options_step=True,
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
|
||||
class GroupOptionsFlow(GroupFlow):
|
||||
"""Handle an option flow for PowerCalc."""
|
||||
|
||||
def __init__(self, flow: PowercalcOptionsFlow) -> None:
|
||||
super().__init__(flow)
|
||||
self.flow: PowercalcOptionsFlow = flow
|
||||
|
||||
async def async_step_group_custom(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the group options flow."""
|
||||
return await self.flow.async_handle_options_step(
|
||||
user_input, create_schema_group_custom(self.flow.hass, self.flow.config_entry, True), Step.GROUP_CUSTOM
|
||||
)
|
||||
|
||||
async def async_step_group_domain(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the group options flow."""
|
||||
return await self.flow.async_handle_options_step(user_input, SCHEMA_GROUP_DOMAIN_OPTIONS, Step.GROUP_DOMAIN)
|
||||
|
||||
async def async_step_group_subtract(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the group options flow."""
|
||||
return await self.flow.async_handle_options_step(user_input, SCHEMA_GROUP_SUBTRACT_OPTIONS, Step.GROUP_SUBTRACT)
|
||||
|
||||
async def async_step_group_tracked_untracked(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the group options flow."""
|
||||
schema = SCHEMA_GROUP_TRACKED_UNTRACKED
|
||||
if self.flow.sensor_config.get(CONF_GROUP_TRACKED_AUTO, True):
|
||||
schema = schema.extend((await create_schema_tracked_untracked_auto(self.flow.hass)).schema)
|
||||
else:
|
||||
schema = schema.extend(SCHEMA_GROUP_TRACKED_UNTRACKED_MANUAL.schema)
|
||||
|
||||
return await self.flow.async_handle_options_step(user_input, schema, Step.GROUP_TRACKED_UNTRACKED)
|
||||
|
||||
def build_group_menu(self) -> list[Step]:
|
||||
"""Build the group menu."""
|
||||
group_type = self.flow.sensor_config.get(CONF_GROUP_TYPE, GroupType.CUSTOM)
|
||||
if group_type == GroupType.DOMAIN:
|
||||
return [Step.GROUP_DOMAIN]
|
||||
if group_type == GroupType.SUBTRACT:
|
||||
return [Step.GROUP_SUBTRACT]
|
||||
if group_type == GroupType.TRACKED_UNTRACKED:
|
||||
return [Step.GROUP_TRACKED_UNTRACKED]
|
||||
return [Step.GROUP_CUSTOM]
|
||||
@@ -0,0 +1,554 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import selector, translation
|
||||
import voluptuous as vol
|
||||
|
||||
from custom_components.powercalc import (
|
||||
DOMAIN,
|
||||
DeviceType,
|
||||
)
|
||||
from custom_components.powercalc.const import (
|
||||
CONF_AVAILABILITY_ENTITY,
|
||||
CONF_FIXED,
|
||||
CONF_MANUFACTURER,
|
||||
CONF_MODE,
|
||||
CONF_MODEL,
|
||||
CONF_POWER,
|
||||
CONF_SELF_USAGE_INCLUDED,
|
||||
CONF_SUB_PROFILE,
|
||||
CONF_VARIABLES,
|
||||
DUMMY_ENTITY_ID,
|
||||
LIBRARY_URL,
|
||||
CalculationStrategy,
|
||||
)
|
||||
from custom_components.powercalc.discovery import (
|
||||
get_power_profile_by_source_device,
|
||||
get_power_profile_by_source_entity,
|
||||
)
|
||||
from custom_components.powercalc.flow_helper.common import FlowType, PowercalcFormStep, Step
|
||||
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_UTILITY_METER_TOGGLE, build_sub_profile_schema
|
||||
from custom_components.powercalc.helpers import collect_placeholders, iter_related_entity_placeholders, resolve_related_entity_placeholder
|
||||
from custom_components.powercalc.power_profile.library import ModelInfo, ProfileLibrary
|
||||
from custom_components.powercalc.power_profile.power_profile import DEVICE_TYPE_DOMAIN, DOMAIN_DEVICE_TYPE_MAPPING, DiscoveryBy, PowerProfile
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from custom_components.powercalc.config_flow import PowercalcCommonFlow, PowercalcConfigFlow, PowercalcOptionsFlow
|
||||
|
||||
CONF_CONFIRM_AUTODISCOVERED_MODEL = "confirm_autodisovered_model"
|
||||
|
||||
SCHEMA_POWER_AUTODISCOVERED = vol.Schema(
|
||||
{vol.Optional(CONF_CONFIRM_AUTODISCOVERED_MODEL, default=True): bool},
|
||||
)
|
||||
|
||||
SCHEMA_POWER_OPTIONS_LIBRARY = vol.Schema(
|
||||
{
|
||||
**SCHEMA_ENERGY_SENSOR_TOGGLE.schema,
|
||||
**SCHEMA_UTILITY_METER_TOGGLE.schema,
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_POWER_SMART_SWITCH = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_POWER): vol.Coerce(float),
|
||||
vol.Optional(CONF_SELF_USAGE_INCLUDED): selector.BooleanSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class LibraryFlow:
|
||||
def __init__(self, flow: PowercalcCommonFlow) -> None:
|
||||
self.flow = flow
|
||||
|
||||
async def async_step_manufacturer(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> FlowResult:
|
||||
"""Ask the user to select the manufacturer."""
|
||||
|
||||
async def _create_schema() -> vol.Schema:
|
||||
"""Create manufacturer schema."""
|
||||
library = await ProfileLibrary.factory(self.flow.hass)
|
||||
manufacturers = [
|
||||
selector.SelectOptionDict(value=manufacturer[0], label=manufacturer[1])
|
||||
for manufacturer in await library.get_manufacturer_listing(
|
||||
self._get_library_device_types(),
|
||||
self._get_library_discovery_by(),
|
||||
)
|
||||
]
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_MANUFACTURER, default=self.flow.sensor_config.get(CONF_MANUFACTURER)): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=manufacturers,
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.MANUFACTURER,
|
||||
schema=_create_schema,
|
||||
next_step=Step.MODEL,
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_model(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> FlowResult:
|
||||
"""Ask the user to select the model."""
|
||||
|
||||
def _build_model_label(model_id: str, model_name: str) -> str:
|
||||
if not model_name or model_name == model_id:
|
||||
return model_id
|
||||
return f"{model_id} ({model_name})"
|
||||
|
||||
async def _validate(user_input: dict[str, Any]) -> dict[str, str]:
|
||||
library = await ProfileLibrary.factory(self.flow.hass)
|
||||
profile = await library.get_profile(
|
||||
ModelInfo(
|
||||
str(self.flow.sensor_config.get(CONF_MANUFACTURER)),
|
||||
str(user_input.get(CONF_MODEL)),
|
||||
),
|
||||
self.flow.source_entity,
|
||||
process_variables=False,
|
||||
)
|
||||
self.flow.selected_profile = profile
|
||||
if self.flow.selected_profile and not await self.flow.selected_profile.needs_user_configuration:
|
||||
await self.flow.validate_strategy_config()
|
||||
return user_input
|
||||
|
||||
async def _create_schema() -> vol.Schema:
|
||||
"""Create model schema."""
|
||||
manufacturer = str(self.flow.sensor_config.get(CONF_MANUFACTURER))
|
||||
library = await ProfileLibrary.factory(self.flow.hass)
|
||||
models = [
|
||||
selector.SelectOptionDict(value=model_id, label=_build_model_label(model_id, model_name))
|
||||
for model_id, model_name in await library.get_model_listing(
|
||||
manufacturer,
|
||||
self._get_library_device_types(),
|
||||
self._get_library_discovery_by(),
|
||||
)
|
||||
]
|
||||
model = self.flow.selected_profile.model if self.flow.selected_profile else self.flow.sensor_config.get(CONF_MODEL)
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_MODEL, description={"suggested_value": model}, default=model): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=models,
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.MODEL,
|
||||
schema=_create_schema,
|
||||
next_step=Step.POST_LIBRARY,
|
||||
validate_user_input=_validate,
|
||||
form_kwarg={"description_placeholders": {"supported_models_link": LIBRARY_URL}},
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_post_library(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> FlowResult:
|
||||
"""
|
||||
Handles the logic after the user either selected manufacturer/model himself or confirmed autodiscovered.
|
||||
Forwards to the next step in the flow.
|
||||
"""
|
||||
if not self.flow.selected_profile:
|
||||
return self.flow.async_abort(reason="model_not_supported") # type:ignore # pragma: no cover
|
||||
|
||||
if Step.LIBRARY_CUSTOM_FIELDS not in self.flow.handled_steps and self.flow.selected_profile.has_custom_fields:
|
||||
return await self.async_step_library_custom_fields()
|
||||
|
||||
if Step.AVAILABILITY_ENTITY not in self.flow.handled_steps and self.flow.selected_profile.discovery_by == DiscoveryBy.DEVICE:
|
||||
result = await self.async_step_availability_entity()
|
||||
if result:
|
||||
return result
|
||||
|
||||
if Step.SUB_PROFILE not in self.flow.handled_steps and await self.flow.selected_profile.requires_manual_sub_profile_selection:
|
||||
return await self.async_step_sub_profile()
|
||||
|
||||
if (
|
||||
Step.SMART_SWITCH not in self.flow.handled_steps
|
||||
and self.flow.selected_profile.device_type == DeviceType.SMART_SWITCH
|
||||
and self.flow.selected_profile.calculation_strategy == CalculationStrategy.FIXED
|
||||
):
|
||||
return await self.async_step_smart_switch()
|
||||
|
||||
if Step.FIXED not in self.flow.handled_steps and self.flow.selected_profile.needs_fixed_config: # pragma: no cover
|
||||
return await self.flow.flow_handlers[FlowType.VIRTUAL_POWER].async_step_fixed() # type:ignore
|
||||
|
||||
if Step.LINEAR not in self.flow.handled_steps and self.flow.selected_profile.needs_linear_config:
|
||||
return await self.flow.flow_handlers[FlowType.VIRTUAL_POWER].async_step_linear() # type:ignore
|
||||
|
||||
if Step.MULTI_SWITCH not in self.flow.handled_steps and self.flow.selected_profile.calculation_strategy == CalculationStrategy.MULTI_SWITCH:
|
||||
return await self.flow.flow_handlers[FlowType.VIRTUAL_POWER].async_step_multi_switch() # type:ignore
|
||||
|
||||
return await self.flow.flow_handlers[FlowType.GROUP].async_step_assign_groups() # type:ignore
|
||||
|
||||
async def async_step_library_custom_fields(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the flow for custom fields."""
|
||||
|
||||
async def _process_user_input(user_input: dict[str, Any]) -> dict[str, Any]:
|
||||
return {CONF_VARIABLES: user_input}
|
||||
|
||||
form_kwarg: dict[str, Any] | None = None
|
||||
if self.flow.selected_profile and self.flow.selected_profile.documentation_url:
|
||||
form_kwarg = {
|
||||
"description_placeholders": {
|
||||
"documentation_url": self.flow.selected_profile.documentation_url,
|
||||
},
|
||||
}
|
||||
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.LIBRARY_CUSTOM_FIELDS,
|
||||
schema=build_dynamic_field_schema(
|
||||
self.flow.hass,
|
||||
self.flow.selected_profile, # type: ignore
|
||||
self.flow.source_entity,
|
||||
),
|
||||
next_step=Step.POST_LIBRARY,
|
||||
validate_user_input=_process_user_input,
|
||||
form_kwarg=form_kwarg,
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_sub_profile(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> FlowResult:
|
||||
"""Handle the flow for sub profile selection."""
|
||||
assert self.flow.selected_profile is not None
|
||||
|
||||
async def _validate(user_input: dict[str, Any]) -> dict[str, str]:
|
||||
return {CONF_MODEL: f"{self.flow.sensor_config.get(CONF_MODEL)}/{user_input.get(CONF_SUB_PROFILE)}"}
|
||||
|
||||
library = await ProfileLibrary.factory(self.flow.hass)
|
||||
profile = await library.get_profile(
|
||||
ModelInfo(
|
||||
str(self.flow.sensor_config.get(CONF_MANUFACTURER)),
|
||||
str(self.flow.sensor_config.get(CONF_MODEL)),
|
||||
),
|
||||
self.flow.source_entity,
|
||||
process_variables=False,
|
||||
)
|
||||
remarks = profile.config_flow_sub_profile_remarks
|
||||
if remarks:
|
||||
remarks = "\n\n" + remarks
|
||||
|
||||
step = Step.SUB_PROFILE_PER_DEVICE if self.flow.selected_profile.discovery_by == DiscoveryBy.DEVICE else Step.SUB_PROFILE
|
||||
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=step,
|
||||
schema=await build_sub_profile_schema(profile, self.flow.selected_sub_profile),
|
||||
next_step=Step.POWER_ADVANCED,
|
||||
validate_user_input=_validate,
|
||||
form_kwarg={
|
||||
"description_placeholders": {
|
||||
"entity_id": self.flow.source_entity_id,
|
||||
"remarks": remarks,
|
||||
},
|
||||
},
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_sub_profile_per_device(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> FlowResult:
|
||||
return await self.async_step_sub_profile(user_input)
|
||||
|
||||
async def async_step_smart_switch(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Asks the user for the power of connect appliance for the smart switch."""
|
||||
|
||||
if self.flow.selected_profile and not self.flow.selected_profile.needs_fixed_config:
|
||||
return self.flow.persist_config_entry()
|
||||
|
||||
async def _validate(user_input: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
CONF_SELF_USAGE_INCLUDED: user_input.get(CONF_SELF_USAGE_INCLUDED),
|
||||
CONF_MODE: CalculationStrategy.FIXED,
|
||||
CONF_FIXED: {CONF_POWER: user_input.get(CONF_POWER, 0)},
|
||||
}
|
||||
|
||||
self_usage_on = self.flow.selected_profile.standby_power_on if self.flow.selected_profile else 0
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.SMART_SWITCH,
|
||||
schema=SCHEMA_POWER_SMART_SWITCH,
|
||||
validate_user_input=_validate,
|
||||
next_step=Step.POWER_ADVANCED,
|
||||
form_kwarg={"description_placeholders": {"self_usage_power": str(self_usage_on)}},
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_availability_entity(self, user_input: dict[str, Any] | None = None) -> FlowResult | None:
|
||||
"""Handle the flow for availability entity."""
|
||||
# Auto-resolve availability entity from profile placeholders
|
||||
auto_entity = self._resolve_availability_entity()
|
||||
if auto_entity:
|
||||
self.flow.sensor_config[CONF_AVAILABILITY_ENTITY] = auto_entity
|
||||
self.flow.handled_steps.append(Step.AVAILABILITY_ENTITY)
|
||||
return None
|
||||
|
||||
domains = DEVICE_TYPE_DOMAIN[self.flow.selected_profile.device_type] # type: ignore
|
||||
entity_selector = self.flow.create_device_entity_selector(
|
||||
list(domains) if isinstance(domains, set) else [domains],
|
||||
)
|
||||
try:
|
||||
first_entity = entity_selector.config["include_entities"][0]
|
||||
except IndexError:
|
||||
# Skip step if no entities are available
|
||||
self.flow.handled_steps.append(Step.AVAILABILITY_ENTITY)
|
||||
return None
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.AVAILABILITY_ENTITY,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_AVAILABILITY_ENTITY, default=first_entity): entity_selector,
|
||||
},
|
||||
),
|
||||
next_step=Step.POST_LIBRARY,
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
def _resolve_availability_entity(self) -> str | None:
|
||||
"""Try to auto-resolve an availability entity from profile placeholders."""
|
||||
profile = self.flow.selected_profile
|
||||
device_entry = self.flow.source_entity.device_entry if self.flow.source_entity else None
|
||||
if not profile or not device_entry:
|
||||
return None
|
||||
|
||||
for placeholder in iter_related_entity_placeholders(collect_placeholders(profile.json_data)):
|
||||
entity = resolve_related_entity_placeholder(
|
||||
self.flow.hass,
|
||||
placeholder,
|
||||
source_entity=self.flow.source_entity,
|
||||
)
|
||||
if entity:
|
||||
return entity
|
||||
return None
|
||||
|
||||
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:
|
||||
return None
|
||||
|
||||
if self.flow.source_entity:
|
||||
return DOMAIN_DEVICE_TYPE_MAPPING.get(self.flow.source_entity.domain, set())
|
||||
|
||||
return None # pragma: no cover
|
||||
|
||||
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
|
||||
|
||||
|
||||
class LibraryConfigFlow(LibraryFlow):
|
||||
def __init__(self, flow: PowercalcConfigFlow) -> None:
|
||||
super().__init__(flow)
|
||||
self.flow: PowercalcConfigFlow = flow
|
||||
|
||||
async def async_step_library_multi_profile(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> FlowResult | ConfigFlowResult:
|
||||
"""This step gets executed when multiple profiles are found for the source entity."""
|
||||
if user_input is not None:
|
||||
selected_model: str = user_input.get(CONF_MODEL) # type: ignore
|
||||
selected_profile = self.flow.discovered_profiles.get(selected_model)
|
||||
if selected_profile is None: # pragma: no cover
|
||||
return self.flow.async_abort(reason="invalid_profile")
|
||||
self.flow.selected_profile = selected_profile
|
||||
self.flow.sensor_config.update(
|
||||
{
|
||||
CONF_MANUFACTURER: selected_profile.manufacturer,
|
||||
CONF_MODEL: selected_profile.model,
|
||||
},
|
||||
)
|
||||
return await self.async_step_post_library(user_input)
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_MODEL): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[
|
||||
selector.SelectOptionDict(
|
||||
value=profile.unique_id,
|
||||
label=profile.model,
|
||||
)
|
||||
for profile in self.flow.discovered_profiles.values()
|
||||
],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
manufacturer = str(self.flow.sensor_config.get(CONF_MANUFACTURER))
|
||||
model = str(self.flow.sensor_config.get(CONF_MODEL))
|
||||
return self.flow.async_show_form(
|
||||
step_id=Step.LIBRARY_MULTI_PROFILE,
|
||||
data_schema=schema,
|
||||
description_placeholders={
|
||||
"library_link": f"{LIBRARY_URL}/?manufacturer={manufacturer}",
|
||||
"manufacturer": manufacturer,
|
||||
"model": model,
|
||||
},
|
||||
last_step=False,
|
||||
)
|
||||
|
||||
async def async_step_library(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> FlowResult:
|
||||
"""Try to autodiscover manufacturer/model first.
|
||||
Ask the user to confirm this or forward to manual library selection.
|
||||
"""
|
||||
if user_input is not None:
|
||||
return await self._handle_library_confirmation(user_input)
|
||||
|
||||
await self._async_autodiscover_profile()
|
||||
if not self.flow.selected_profile:
|
||||
return await self.async_step_manufacturer()
|
||||
|
||||
return self._show_autodiscovered_profile_form()
|
||||
|
||||
async def _handle_library_confirmation(self, user_input: dict[str, Any]) -> FlowResult:
|
||||
"""Handle the user's response to an autodiscovered library profile."""
|
||||
if not user_input.get(CONF_CONFIRM_AUTODISCOVERED_MODEL) or not self.flow.selected_profile:
|
||||
return await self.async_step_manufacturer()
|
||||
|
||||
self.flow.sensor_config.update(
|
||||
{
|
||||
CONF_MANUFACTURER: self.flow.selected_profile.manufacturer,
|
||||
CONF_MODEL: self.flow.selected_profile.model,
|
||||
},
|
||||
)
|
||||
return await self.async_step_post_library(user_input)
|
||||
|
||||
async def _async_autodiscover_profile(self) -> None:
|
||||
"""Populate the selected profile from the source entity when possible."""
|
||||
if not self.flow.source_entity or not self.flow.source_entity.entity_entry or self.flow.selected_profile is not None:
|
||||
return
|
||||
|
||||
self.flow.selected_profile = await get_power_profile_by_source_entity(self.flow.hass, self.flow.source_entity)
|
||||
if self.flow.selected_profile is None and self.flow.source_entity.device_entry:
|
||||
self.flow.selected_profile = await get_power_profile_by_source_device(self.flow.hass, self.flow.source_entity)
|
||||
|
||||
def _show_autodiscovered_profile_form(self) -> FlowResult:
|
||||
"""Show the confirmation form for an autodiscovered library profile."""
|
||||
profile = self.flow.selected_profile
|
||||
assert profile is not None
|
||||
|
||||
return cast(
|
||||
FlowResult,
|
||||
self.flow.async_show_form(
|
||||
step_id=Step.LIBRARY,
|
||||
description_placeholders=self._build_library_description_placeholders(profile),
|
||||
data_schema=SCHEMA_POWER_AUTODISCOVERED,
|
||||
errors={},
|
||||
last_step=False,
|
||||
),
|
||||
)
|
||||
|
||||
def _build_library_description_placeholders(self, profile: PowerProfile) -> dict[str, Any]:
|
||||
"""Build the placeholders for the autodiscovered profile confirmation form."""
|
||||
return {
|
||||
"remarks": self._build_library_remarks(profile),
|
||||
"manufacturer": profile.manufacturer,
|
||||
"model": profile.model,
|
||||
"source": self._get_profile_source(profile),
|
||||
}
|
||||
|
||||
def _build_library_remarks(self, profile: PowerProfile) -> str | None:
|
||||
"""Build the remarks text for the autodiscovered profile confirmation form."""
|
||||
remarks = self._get_conditional_remarks()
|
||||
if remarks:
|
||||
remarks = "\n\n" + remarks
|
||||
|
||||
if profile.documentation_url:
|
||||
return (remarks or "") + f"\n\n[Documentation]({profile.documentation_url})"
|
||||
|
||||
return remarks
|
||||
|
||||
def _get_profile_source(self, profile: PowerProfile) -> str:
|
||||
"""Build the autodiscovery source description."""
|
||||
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:
|
||||
return f"{translations.get(f'component.{DOMAIN}.common.source_device')}: {self.flow.source_entity.device_entry.name}"
|
||||
|
||||
return f"{translations.get(f'component.{DOMAIN}.common.source_entity')}: {self.flow.source_entity_id}"
|
||||
|
||||
def _get_conditional_remarks(self) -> str | None:
|
||||
"""Get discovery remarks, only showing them if required entities are missing."""
|
||||
profile = self.flow.selected_profile
|
||||
if not profile:
|
||||
return None # pragma: no cover
|
||||
|
||||
remarks = profile.config_flow_discovery_remarks
|
||||
if not remarks:
|
||||
return None
|
||||
|
||||
# Check if all entity_by_* placeholders can be resolved from the device
|
||||
device_entry = self.flow.source_entity.device_entry if self.flow.source_entity else None
|
||||
if not device_entry:
|
||||
return remarks
|
||||
|
||||
related_placeholders = iter_related_entity_placeholders(collect_placeholders(profile.json_data))
|
||||
all_resolved = all(
|
||||
resolve_related_entity_placeholder(
|
||||
self.flow.hass,
|
||||
placeholder,
|
||||
source_entity=self.flow.source_entity,
|
||||
)
|
||||
for placeholder in related_placeholders
|
||||
)
|
||||
return None if all_resolved else remarks
|
||||
|
||||
|
||||
class LibraryOptionsFlow(LibraryFlow):
|
||||
def __init__(self, flow: PowercalcOptionsFlow) -> None:
|
||||
super().__init__(flow)
|
||||
self.flow: PowercalcOptionsFlow = flow
|
||||
|
||||
async def async_step_library_options(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the basic options flow."""
|
||||
self.flow.is_library_flow = True
|
||||
self.flow.selected_sub_profile = self.flow.selected_profile.sub_profile # type: ignore
|
||||
if user_input is not None:
|
||||
return await self.async_step_manufacturer()
|
||||
|
||||
return self.flow.async_show_form(
|
||||
step_id=Step.LIBRARY_OPTIONS,
|
||||
description_placeholders={
|
||||
"manufacturer": self.flow.selected_profile.manufacturer, # type: ignore
|
||||
"model": self.flow.selected_profile.model, # type: ignore
|
||||
},
|
||||
last_step=False,
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Real-power logic for the config flow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import CONF_DEVICE, CONF_ENTITY_ID, CONF_NAME
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import selector
|
||||
import voluptuous as vol
|
||||
|
||||
from custom_components.powercalc import SensorType
|
||||
from custom_components.powercalc.flow_helper.common import PowercalcFormStep, Step
|
||||
from custom_components.powercalc.flow_helper.schema import SCHEMA_UTILITY_METER_TOGGLE
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from custom_components.powercalc.config_flow import PowercalcConfigFlow, PowercalcOptionsFlow
|
||||
|
||||
SCHEMA_REAL_POWER_OPTIONS = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(device_class=SensorDeviceClass.POWER),
|
||||
),
|
||||
vol.Optional(CONF_DEVICE): selector.DeviceSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_REAL_POWER = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): selector.TextSelector(),
|
||||
**SCHEMA_REAL_POWER_OPTIONS.schema,
|
||||
**SCHEMA_UTILITY_METER_TOGGLE.schema,
|
||||
},
|
||||
).extend(SCHEMA_REAL_POWER_OPTIONS.schema)
|
||||
|
||||
|
||||
class RealPowerConfigFlow:
|
||||
def __init__(self, flow: PowercalcConfigFlow) -> None:
|
||||
self.flow: PowercalcConfigFlow = flow
|
||||
|
||||
async def async_step_real_power(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the flow for real power sensor"""
|
||||
|
||||
self.flow.selected_sensor_type = SensorType.REAL_POWER
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.REAL_POWER,
|
||||
schema=SCHEMA_REAL_POWER,
|
||||
next_step=Step.ENERGY_OPTIONS,
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
|
||||
class RealPowerOptionsFlow:
|
||||
def __init__(self, flow: PowercalcOptionsFlow) -> None:
|
||||
self.flow: PowercalcOptionsFlow = flow
|
||||
|
||||
async def async_step_real_power(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the real power options flow."""
|
||||
return await self.flow.async_handle_options_step(user_input, SCHEMA_REAL_POWER_OPTIONS, Step.REAL_POWER)
|
||||
@@ -0,0 +1,436 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.const import CONF_ATTRIBUTE, CONF_ENTITIES, CONF_ENTITY_ID, CONF_ID, CONF_NAME, CONF_PATH, Platform
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.schema_config_entry_flow import SchemaFlowError
|
||||
import voluptuous as vol
|
||||
|
||||
from custom_components.powercalc.common import create_source_entity
|
||||
from custom_components.powercalc.const import (
|
||||
CONF_AUTOSTART,
|
||||
CONF_CALCULATION_ENABLED_CONDITION,
|
||||
CONF_CALIBRATE,
|
||||
CONF_CREATE_ENERGY_SENSOR,
|
||||
CONF_CREATE_UTILITY_METERS,
|
||||
CONF_GAMMA_CURVE,
|
||||
CONF_IGNORE_UNAVAILABLE_STATE,
|
||||
CONF_MAX_POWER,
|
||||
CONF_MIN_POWER,
|
||||
CONF_MODE,
|
||||
CONF_MULTIPLY_FACTOR,
|
||||
CONF_MULTIPLY_FACTOR_STANDBY,
|
||||
CONF_PLAYBOOKS,
|
||||
CONF_POWER,
|
||||
CONF_POWER_OFF,
|
||||
CONF_POWER_TEMPLATE,
|
||||
CONF_REPEAT,
|
||||
CONF_STANDBY_POWER,
|
||||
CONF_STATE,
|
||||
CONF_STATE_TRIGGER,
|
||||
CONF_STATES_POWER,
|
||||
CONF_UNAVAILABLE_POWER,
|
||||
DUMMY_ENTITY_ID,
|
||||
CalculationStrategy,
|
||||
SensorType,
|
||||
)
|
||||
from custom_components.powercalc.flow_helper.common import FlowType, PowercalcFormStep, Step, fill_schema_defaults
|
||||
from custom_components.powercalc.flow_helper.flows.global_configuration import get_global_powercalc_config
|
||||
from custom_components.powercalc.flow_helper.flows.library import SCHEMA_POWER_OPTIONS_LIBRARY, SCHEMA_POWER_SMART_SWITCH
|
||||
from custom_components.powercalc.flow_helper.schema import (
|
||||
SCHEMA_ENERGY_SENSOR_TOGGLE,
|
||||
SCHEMA_SENSOR_ENERGY_OPTIONS,
|
||||
SCHEMA_UTILITY_METER_TOGGLE,
|
||||
)
|
||||
from custom_components.powercalc.power_profile.power_profile import DeviceType
|
||||
from custom_components.powercalc.strategy.wled import CONFIG_SCHEMA as SCHEMA_POWER_WLED
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from custom_components.powercalc.config_flow import PowercalcCommonFlow, PowercalcConfigFlow, PowercalcOptionsFlow
|
||||
|
||||
SCHEMA_POWER_ADVANCED = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CALCULATION_ENABLED_CONDITION): selector.TemplateSelector(),
|
||||
vol.Optional(CONF_IGNORE_UNAVAILABLE_STATE): selector.BooleanSelector(),
|
||||
vol.Optional(CONF_UNAVAILABLE_POWER): vol.Coerce(float),
|
||||
vol.Optional(CONF_MULTIPLY_FACTOR): vol.Coerce(float),
|
||||
vol.Optional(CONF_MULTIPLY_FACTOR_STANDBY): selector.BooleanSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_POWER_BASE = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME): selector.TextSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_POWER_OPTIONS = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_STANDBY_POWER): vol.Coerce(float),
|
||||
**SCHEMA_ENERGY_SENSOR_TOGGLE.schema,
|
||||
**SCHEMA_UTILITY_METER_TOGGLE.schema,
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_POWER_FIXED = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_POWER): vol.Coerce(float),
|
||||
vol.Optional(CONF_POWER_TEMPLATE): selector.TemplateSelector(),
|
||||
vol.Optional(CONF_STATES_POWER): selector.ObjectSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_POWER_LINEAR = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_MIN_POWER): vol.Coerce(float),
|
||||
vol.Optional(CONF_MAX_POWER): vol.Coerce(float),
|
||||
vol.Optional(CONF_GAMMA_CURVE): vol.Coerce(float),
|
||||
vol.Optional(CONF_CALIBRATE): selector.ObjectSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_POWER_MULTI_SWITCH_MANUAL = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_POWER): vol.Coerce(float),
|
||||
vol.Required(CONF_POWER_OFF): vol.Coerce(float),
|
||||
},
|
||||
)
|
||||
|
||||
STRATEGY_SCHEMAS: dict[CalculationStrategy, vol.Schema] = {
|
||||
CalculationStrategy.FIXED: SCHEMA_POWER_FIXED,
|
||||
CalculationStrategy.WLED: SCHEMA_POWER_WLED,
|
||||
}
|
||||
|
||||
STRATEGY_SELECTOR = selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[
|
||||
CalculationStrategy.FIXED,
|
||||
CalculationStrategy.LINEAR,
|
||||
CalculationStrategy.MULTI_SWITCH,
|
||||
CalculationStrategy.PLAYBOOK,
|
||||
CalculationStrategy.WLED,
|
||||
CalculationStrategy.LUT,
|
||||
],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
)
|
||||
|
||||
STRATEGY_STEP_MAPPING: dict[CalculationStrategy, Step] = {
|
||||
CalculationStrategy.FIXED: Step.FIXED,
|
||||
CalculationStrategy.LINEAR: Step.LINEAR,
|
||||
CalculationStrategy.MULTI_SWITCH: Step.MULTI_SWITCH,
|
||||
CalculationStrategy.PLAYBOOK: Step.PLAYBOOK,
|
||||
CalculationStrategy.WLED: Step.WLED,
|
||||
}
|
||||
|
||||
|
||||
class VirtualPowerFlow:
|
||||
def __init__(self, flow: PowercalcCommonFlow) -> None:
|
||||
self.flow = flow
|
||||
|
||||
async def create_strategy_schema(self) -> vol.Schema:
|
||||
"""Get the config schema for a given power calculation strategy."""
|
||||
if not self.flow.strategy:
|
||||
raise ValueError("No strategy selected") # pragma: no cover
|
||||
|
||||
create_schema_func = f"create_schema_{self.flow.strategy.lower()}"
|
||||
if hasattr(self, create_schema_func):
|
||||
return await getattr(self, create_schema_func)() # type: ignore
|
||||
|
||||
return STRATEGY_SCHEMAS[self.flow.strategy]
|
||||
|
||||
async def create_schema_linear(self) -> vol.Schema:
|
||||
"""Create the config schema for linear strategy."""
|
||||
return SCHEMA_POWER_LINEAR.extend( # type: ignore
|
||||
{
|
||||
vol.Optional(CONF_ATTRIBUTE): selector.AttributeSelector(
|
||||
selector.AttributeSelectorConfig(
|
||||
entity_id=self.flow.source_entity_id, # type: ignore
|
||||
hide_attributes=[],
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
async def create_schema_multi_switch(self) -> vol.Schema:
|
||||
"""Create the config schema for multi switch strategy."""
|
||||
|
||||
switch_domains = [str(Platform.SWITCH), str(Platform.LIGHT), str(Platform.COVER)]
|
||||
if self.flow.source_entity and self.flow.source_entity.device_entry:
|
||||
entity_selector = self.flow.create_device_entity_selector(switch_domains, multiple=True)
|
||||
else:
|
||||
entity_selector = selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(
|
||||
domain=switch_domains,
|
||||
multiple=True,
|
||||
),
|
||||
)
|
||||
|
||||
default_entities = entity_selector.config.get("include_entities", [])
|
||||
schema = vol.Schema({vol.Optional(CONF_ENTITIES, default=default_entities): entity_selector})
|
||||
|
||||
if not self.flow.is_library_flow:
|
||||
schema = schema.extend(SCHEMA_POWER_MULTI_SWITCH_MANUAL.schema)
|
||||
|
||||
return schema
|
||||
|
||||
async def create_schema_playbook(self) -> vol.Schema:
|
||||
"""Create the config schema for playbook strategy."""
|
||||
base_path = Path(self.flow.hass.config.path("powercalc/playbooks"))
|
||||
playbook_files = [str(p.relative_to(base_path)) for p in base_path.rglob("*") if p.is_file()]
|
||||
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_PLAYBOOKS): selector.ObjectSelector(
|
||||
{
|
||||
"multiple": True,
|
||||
"description_field": CONF_PATH,
|
||||
"label_field": CONF_ID,
|
||||
"fields": {
|
||||
CONF_ID: {
|
||||
"required": True,
|
||||
"selector": {"text": None},
|
||||
},
|
||||
CONF_PATH: {
|
||||
"required": True,
|
||||
"selector": {"select": {"options": playbook_files, "mode": "dropdown", "custom_value": True}},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
vol.Optional(CONF_REPEAT): selector.BooleanSelector(),
|
||||
vol.Optional(CONF_AUTOSTART): selector.TextSelector(),
|
||||
vol.Optional(CONF_STATE_TRIGGER): selector.ObjectSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
async def handle_strategy_step(
|
||||
self,
|
||||
strategy: CalculationStrategy,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
validate: Callable[[dict[str, Any]], None] | None = None,
|
||||
) -> FlowResult:
|
||||
self.flow.strategy = strategy
|
||||
|
||||
async def _validate(user_input: dict[str, Any]) -> dict[str, Any]:
|
||||
if validate:
|
||||
validate(user_input)
|
||||
# Convert states_power from dict to list to preserve order
|
||||
if CONF_STATES_POWER in user_input and isinstance(user_input[CONF_STATES_POWER], dict):
|
||||
user_input[CONF_STATES_POWER] = [{CONF_STATE: state, CONF_POWER: power} for state, power in user_input[CONF_STATES_POWER].items()]
|
||||
await self.flow.validate_strategy_config({strategy: user_input})
|
||||
return {strategy: user_input}
|
||||
|
||||
schema = await self.create_strategy_schema()
|
||||
|
||||
description_placeholders = {}
|
||||
if strategy == CalculationStrategy.WLED:
|
||||
description_placeholders = {
|
||||
"docs_uri": "https://docs.powercalc.nl/strategies/wled/",
|
||||
}
|
||||
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=STRATEGY_STEP_MAPPING[strategy],
|
||||
schema=schema,
|
||||
next_step=Step.ASSIGN_GROUPS,
|
||||
validate_user_input=_validate,
|
||||
form_kwarg={"description_placeholders": description_placeholders},
|
||||
),
|
||||
user_input,
|
||||
)
|
||||
|
||||
async def async_step_power_advanced(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the flow for advanced options."""
|
||||
|
||||
if self.flow.is_options_flow:
|
||||
return self.flow.persist_config_entry() # pragma: no cover
|
||||
|
||||
if user_input is not None or self.flow.skip_advanced_step:
|
||||
self.flow.sensor_config.update(user_input or {})
|
||||
if self.flow.sensor_config.get(CONF_CREATE_UTILITY_METERS):
|
||||
return await self.flow.async_step_utility_meter_options()
|
||||
return self.flow.persist_config_entry()
|
||||
|
||||
schema = SCHEMA_POWER_ADVANCED
|
||||
if self.flow.sensor_config.get(CONF_CREATE_ENERGY_SENSOR):
|
||||
schema = schema.extend(SCHEMA_SENSOR_ENERGY_OPTIONS.schema)
|
||||
|
||||
return await self.flow.handle_form_step(
|
||||
PowercalcFormStep(
|
||||
step=Step.POWER_ADVANCED,
|
||||
schema=fill_schema_defaults(
|
||||
schema,
|
||||
get_global_powercalc_config(self.flow),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class VirtualPowerConfigFlow(VirtualPowerFlow):
|
||||
def __init__(self, flow: PowercalcConfigFlow) -> None:
|
||||
super().__init__(flow)
|
||||
self.flow: PowercalcConfigFlow = flow
|
||||
|
||||
def create_schema_virtual_power(
|
||||
self,
|
||||
) -> vol.Schema:
|
||||
"""Create the config schema for virtual power sensor."""
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ENTITY_ID): self.flow.create_source_entity_selector(),
|
||||
},
|
||||
).extend(SCHEMA_POWER_BASE.schema)
|
||||
if not self.flow.is_library_flow:
|
||||
schema = schema.extend(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_MODE,
|
||||
default=CalculationStrategy.FIXED,
|
||||
): STRATEGY_SELECTOR,
|
||||
},
|
||||
)
|
||||
options_schema = SCHEMA_POWER_OPTIONS
|
||||
else:
|
||||
options_schema = SCHEMA_POWER_OPTIONS_LIBRARY
|
||||
|
||||
power_options = fill_schema_defaults(
|
||||
options_schema,
|
||||
get_global_powercalc_config(self.flow),
|
||||
)
|
||||
return schema.extend(power_options.schema) # type: ignore
|
||||
|
||||
async def async_step_virtual_power(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the flow for virtual power sensor."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
selected_strategy = CalculationStrategy(
|
||||
user_input.get(CONF_MODE) or CalculationStrategy.LUT,
|
||||
)
|
||||
entity_id = user_input.get(CONF_ENTITY_ID)
|
||||
if selected_strategy is not CalculationStrategy.PLAYBOOK and user_input.get(CONF_NAME) is None and entity_id is None:
|
||||
errors[CONF_ENTITY_ID] = "entity_mandatory"
|
||||
|
||||
if not errors:
|
||||
self.flow.source_entity_id = str(entity_id or DUMMY_ENTITY_ID)
|
||||
self.flow.source_entity = await create_source_entity(
|
||||
self.flow.source_entity_id,
|
||||
self.flow.hass,
|
||||
)
|
||||
|
||||
self.flow.name = user_input.get(CONF_NAME) or self.flow.source_entity.name
|
||||
self.flow.selected_sensor_type = SensorType.VIRTUAL_POWER
|
||||
self.flow.sensor_config.update(user_input)
|
||||
|
||||
return await self.forward_to_strategy_step(selected_strategy)
|
||||
|
||||
return self.flow.async_show_form( # type: ignore
|
||||
step_id=Step.VIRTUAL_POWER,
|
||||
data_schema=self.create_schema_virtual_power(),
|
||||
description_placeholders={
|
||||
"doc_uri_states_power": "https://docs.powercalc.nl/strategies/fixed/#power-per-state",
|
||||
},
|
||||
errors=errors,
|
||||
last_step=False,
|
||||
)
|
||||
|
||||
async def async_step_fixed(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the flow for fixed sensor."""
|
||||
return await self.handle_strategy_step(CalculationStrategy.FIXED, user_input)
|
||||
|
||||
async def async_step_linear(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the flow for fixed sensor."""
|
||||
return await self.handle_strategy_step(CalculationStrategy.LINEAR, user_input)
|
||||
|
||||
async def async_step_multi_switch(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the flow for multi switch strategy."""
|
||||
return await self.handle_strategy_step(CalculationStrategy.MULTI_SWITCH, user_input)
|
||||
|
||||
async def async_step_wled(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the flow for WLED sensor."""
|
||||
return await self.handle_strategy_step(CalculationStrategy.WLED, user_input)
|
||||
|
||||
async def async_step_playbook(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the flow for playbook sensor."""
|
||||
|
||||
def _validate(user_input: dict[str, Any]) -> None:
|
||||
if user_input.get(CONF_PLAYBOOKS) is None or len(user_input.get(CONF_PLAYBOOKS)) == 0: # type: ignore
|
||||
raise SchemaFlowError("playbook_mandatory")
|
||||
|
||||
return await self.handle_strategy_step(CalculationStrategy.PLAYBOOK, user_input, _validate)
|
||||
|
||||
async def forward_to_strategy_step(self, strategy: CalculationStrategy) -> FlowResult:
|
||||
"""Forward to the next step based on the selected strategy."""
|
||||
step = STRATEGY_STEP_MAPPING.get(strategy)
|
||||
if step is None:
|
||||
return await self.flow.flow_handlers[FlowType.LIBRARY].async_step_library() # type:ignore
|
||||
method = getattr(self.flow, f"async_step_{step}")
|
||||
return await method() # type: ignore
|
||||
|
||||
|
||||
class VirtualPowerOptionsFlow(VirtualPowerFlow):
|
||||
def __init__(self, flow: PowercalcOptionsFlow) -> None:
|
||||
super().__init__(flow)
|
||||
self.flow: PowercalcOptionsFlow = flow
|
||||
|
||||
async def build_strategy_config(
|
||||
self,
|
||||
user_input: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Build the config dict needed for the configured strategy."""
|
||||
strategy_schema = await self.create_strategy_schema()
|
||||
strategy_options: dict[str, Any] = {}
|
||||
for key in strategy_schema.schema:
|
||||
if user_input.get(key) is None:
|
||||
continue
|
||||
strategy_options[str(key)] = user_input.get(key)
|
||||
# Convert states_power from dict to list to preserve order
|
||||
if CONF_STATES_POWER in strategy_options and isinstance(strategy_options[CONF_STATES_POWER], dict):
|
||||
strategy_options[CONF_STATES_POWER] = [
|
||||
{CONF_STATE: state, CONF_POWER: power} for state, power in strategy_options[CONF_STATES_POWER].items()
|
||||
]
|
||||
return strategy_options
|
||||
|
||||
async def async_step_fixed(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the basic options flow."""
|
||||
return await self.async_handle_strategy_options_step(user_input)
|
||||
|
||||
async def async_step_linear(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the basic options flow."""
|
||||
return await self.async_handle_strategy_options_step(user_input)
|
||||
|
||||
async def async_step_wled(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the basic options flow."""
|
||||
return await self.async_handle_strategy_options_step(user_input)
|
||||
|
||||
async def async_step_multi_switch(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the basic options flow."""
|
||||
return await self.async_handle_strategy_options_step(user_input)
|
||||
|
||||
async def async_step_playbook(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the basic options flow."""
|
||||
return await self.async_handle_strategy_options_step(user_input)
|
||||
|
||||
async def async_handle_strategy_options_step(self, user_input: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Handle the option processing for the selected strategy."""
|
||||
step = STRATEGY_STEP_MAPPING.get(self.flow.strategy or CalculationStrategy.FIXED, Step.FIXED)
|
||||
|
||||
schema = await self.create_strategy_schema()
|
||||
if self.flow.selected_profile and self.flow.selected_profile.device_type == DeviceType.SMART_SWITCH:
|
||||
schema = SCHEMA_POWER_SMART_SWITCH
|
||||
|
||||
strategy_options = self.flow.sensor_config.get(str(self.flow.strategy)) or {}
|
||||
merged_options = {
|
||||
**self.flow.sensor_config,
|
||||
**{k: v for k, v in strategy_options.items() if k not in self.flow.sensor_config},
|
||||
}
|
||||
# Convert states_power from list to dict for display in ObjectSelector
|
||||
if CONF_STATES_POWER in merged_options and isinstance(merged_options[CONF_STATES_POWER], list):
|
||||
merged_options[CONF_STATES_POWER] = {item[CONF_STATE]: item[CONF_POWER] for item in merged_options[CONF_STATES_POWER]}
|
||||
schema = fill_schema_defaults(schema, merged_options)
|
||||
return await self.flow.async_handle_options_step(user_input, schema, step)
|
||||
@@ -0,0 +1,124 @@
|
||||
from homeassistant.components.utility_meter import CONF_METER_TYPE, METER_TYPES
|
||||
from homeassistant.const import UnitOfPower
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.selector import NumberSelector, NumberSelectorConfig, NumberSelectorMode
|
||||
import voluptuous as vol
|
||||
|
||||
from custom_components.powercalc.const import (
|
||||
CONF_CREATE_ENERGY_SENSOR,
|
||||
CONF_CREATE_UTILITY_METERS,
|
||||
CONF_ENERGY_FILTER_OUTLIER_ENABLED,
|
||||
CONF_ENERGY_FILTER_OUTLIER_MAX,
|
||||
CONF_ENERGY_INTEGRATION_METHOD,
|
||||
CONF_ENERGY_SENSOR_UNIT_PREFIX,
|
||||
CONF_SUB_PROFILE,
|
||||
CONF_UTILITY_METER_NET_CONSUMPTION,
|
||||
CONF_UTILITY_METER_OFFSET,
|
||||
CONF_UTILITY_METER_TARIFFS,
|
||||
CONF_UTILITY_METER_TYPES,
|
||||
ENERGY_INTEGRATION_METHOD_LEFT,
|
||||
ENERGY_INTEGRATION_METHODS,
|
||||
UnitPrefix,
|
||||
)
|
||||
from custom_components.powercalc.power_profile.power_profile import PowerProfile
|
||||
|
||||
SCHEMA_UTILITY_METER_TOGGLE = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CREATE_UTILITY_METERS, default=False): selector.BooleanSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_ENERGY_SENSOR_TOGGLE = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CREATE_ENERGY_SENSOR, default=True): selector.BooleanSelector(),
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_ENERGY_OPTIONS = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_ENERGY_INTEGRATION_METHOD,
|
||||
default=ENERGY_INTEGRATION_METHOD_LEFT,
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=ENERGY_INTEGRATION_METHODS,
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_ENERGY_SENSOR_UNIT_PREFIX): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[
|
||||
selector.SelectOptionDict(value=UnitPrefix.KILO, label="k (kilo)"),
|
||||
selector.SelectOptionDict(value=UnitPrefix.MEGA, label="M (mega)"),
|
||||
selector.SelectOptionDict(value=UnitPrefix.GIGA, label="G (giga)"),
|
||||
selector.SelectOptionDict(value=UnitPrefix.TERA, label="T (tera)"),
|
||||
],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
SCHEMA_SENSOR_ENERGY_OPTIONS = SCHEMA_ENERGY_OPTIONS.extend(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_ENERGY_FILTER_OUTLIER_ENABLED, default=False): selector.BooleanSelector(),
|
||||
vol.Optional(CONF_ENERGY_FILTER_OUTLIER_MAX): NumberSelector(
|
||||
NumberSelectorConfig(mode=NumberSelectorMode.BOX, unit_of_measurement=UnitOfPower.WATT),
|
||||
),
|
||||
},
|
||||
).schema,
|
||||
)
|
||||
|
||||
|
||||
SCHEMA_UTILITY_METER_OPTIONS = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_UTILITY_METER_TYPES): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=METER_TYPES,
|
||||
translation_key=CONF_METER_TYPE,
|
||||
multiple=True,
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_UTILITY_METER_TARIFFS, default=[]): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(options=[], custom_value=True, multiple=True),
|
||||
),
|
||||
vol.Optional(CONF_UTILITY_METER_NET_CONSUMPTION, default=False): selector.BooleanSelector(),
|
||||
vol.Required(CONF_UTILITY_METER_OFFSET, default=0): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0,
|
||||
max=28,
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
unit_of_measurement="days",
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def build_sub_profile_schema(
|
||||
profile: PowerProfile,
|
||||
selected_sub_profile: str | None,
|
||||
) -> vol.Schema:
|
||||
"""Create sub profile schema."""
|
||||
sub_profiles = [
|
||||
selector.SelectOptionDict(
|
||||
value=sub_profile[0],
|
||||
label=sub_profile[1]["name"] if "name" in sub_profile[1] else sub_profile[0],
|
||||
)
|
||||
for sub_profile in await profile.get_sub_profiles()
|
||||
]
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_SUB_PROFILE,
|
||||
description={"suggested_value": selected_sub_profile},
|
||||
default=selected_sub_profile,
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=sub_profiles,
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user