from collections.abc import Awaitable, 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" type MaybeAwaitable[R] = R | Awaitable[R] @dataclass(slots=True) class PowercalcFormStep: schema: vol.Schema | Callable[[], Coroutine[Any, Any, vol.Schema | None]] step: Step validate_user_input: ( Callable[ [dict[str, Any]], MaybeAwaitable[dict[str, Any]], ] | None ) = None next_step: Step | Callable[[dict[str, Any]], MaybeAwaitable[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[call-overload] elif isinstance(key, vol.Required): new_key = vol.Required(key.schema, default=options.get(key)) # type: ignore[call-overload] new_key.description = {"suggested_value": options.get(key)} # type: ignore[call-overload] elif "suggested_value" not in (new_key.description or {}): new_key = copy.copy(key) new_key.description = {"suggested_value": options.get(key)} # type: ignore[call-overload] schema[new_key] = val return vol.Schema(schema) def unwrap_choose_selector( user_input: dict[str, Any], wrapper_key: str, value_key: str | Callable[[object], str] | None = None, ) -> dict[str, Any]: """ Unwrap a ChooseSelector value in user_input back into flat keys. A ChooseSelector value looks like {"active_choice": "", "": }. The wrapper key is dropped, and the active choice's value is merged back into user_input. Home Assistant schema validation returns the selected value directly; use ``value_key`` to map that validated value back to a config key. """ if wrapper_key not in user_input: return user_input raw = user_input.pop(wrapper_key) if not isinstance(raw, dict): if isinstance(value_key, str): user_input[value_key] = raw elif value_key is not None: user_input[value_key(raw)] = raw return user_input if "active_choice" not in raw: user_input.update(raw) return user_input active = raw["active_choice"] value = raw.get(active) if value is None: return user_input if isinstance(value, dict): user_input.update(value) else: user_input[active] = value return user_input def wrap_choose_selector( form_data: dict[str, Any], wrapper_key: str, choices: dict[str, list[str] | str], *, raw_value: bool = False, ) -> dict[str, Any]: """ Build the ChooseSelector value for ``wrapper_key`` from existing flat ``form_data``. ``choices`` maps the choice id to either a single key (the value of that key becomes the choice value) or a list of keys (a dict of those keys becomes the choice value). The first choice that has a matching key in form_data is used. """ for choice_id, mapping in choices.items(): keys = [mapping] if isinstance(mapping, str) else mapping present = {key: form_data[key] for key in keys if key in form_data} if not present: continue if isinstance(mapping, str): choice_value: Any = present[mapping] else: choice_value = present if raw_value: return {**form_data, wrapper_key: choice_value} return {**form_data, wrapper_key: {"active_choice": choice_id, choice_id: choice_value}} return form_data