from __future__ import annotations from collections.abc import Callable from decimal import Decimal from typing import Any, cast from homeassistant.const import CONF_CONDITION, CONF_ENTITIES, CONF_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import condition from homeassistant.helpers.singleton import singleton from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType from custom_components.powercalc.common import SourceEntity from custom_components.powercalc.const import ( CONF_COMPOSITE, CONF_MODE, CONF_MULTI_SWITCH, CONF_POWER, CONF_POWER_OFF, CONF_POWER_TEMPLATE, CONF_STANDBY_POWER, CONF_STATE, CONF_STATES_POWER, CONF_STRATEGIES, CalculationStrategy, ) from custom_components.powercalc.errors import ( StrategyConfigurationError, UnsupportedStrategyError, ) from custom_components.powercalc.power_profile.power_profile import PowerProfile from .composite import DEFAULT_MODE, CompositeStrategy, SubStrategy from .fixed import FixedStrategy from .linear import LinearStrategy from .lut import LutRegistry, LutStrategy from .multi_switch import MultiSwitchStrategy from .playbook import PlaybookStrategy from .selector import detect_calculation_strategy from .strategy_interface import PowerCalculationStrategyInterface from .wled import WledStrategy class PowerCalculatorStrategyFactory: def __init__(self, hass: HomeAssistant) -> None: self._hass = hass self._lut_registry = LutRegistry(hass) @staticmethod @singleton("powercalc_strategy_factory") def get_instance(hass: HomeAssistant) -> PowerCalculatorStrategyFactory: return PowerCalculatorStrategyFactory(hass) async def create( self, config: dict, strategy: str, power_profile: PowerProfile | None, source_entity: SourceEntity, ) -> PowerCalculationStrategyInterface: """Create instance of calculation strategy based on configuration.""" strategy_mapping: dict[str, Callable[[], PowerCalculationStrategyInterface]] = { CalculationStrategy.LINEAR: lambda: self._create_linear(source_entity, config, power_profile), CalculationStrategy.FIXED: lambda: self._create_fixed(source_entity, config, power_profile), CalculationStrategy.LUT: lambda: self._create_lut(source_entity, power_profile), CalculationStrategy.MULTI_SWITCH: lambda: self._create_multi_switch(config, power_profile), CalculationStrategy.PLAYBOOK: lambda: self._create_playbook(config, power_profile), CalculationStrategy.WLED: lambda: self._create_wled(source_entity, config), } if strategy == CalculationStrategy.COMPOSITE: return await self._prepare( await self._create_composite(config, source_entity, power_profile), ) if strategy in strategy_mapping: return await self._prepare( strategy_mapping[strategy](), ) raise UnsupportedStrategyError("Invalid calculation strategy", strategy) @staticmethod async def _prepare(instance: PowerCalculationStrategyInterface) -> PowerCalculationStrategyInterface: await instance.validate_config() await instance.initialize() return instance def _create_linear( self, source_entity: SourceEntity, config: dict, power_profile: PowerProfile | None, ) -> LinearStrategy: """Create the linear strategy.""" linear_config = self._get_strategy_config(CalculationStrategy.LINEAR, config, power_profile) return LinearStrategy( linear_config, self._hass, source_entity, config.get(CONF_STANDBY_POWER), ) def _create_fixed( self, source_entity: SourceEntity, config: dict, power_profile: PowerProfile | None, ) -> FixedStrategy: """Create the fixed strategy.""" fixed_config = self._get_strategy_config(CalculationStrategy.FIXED, config, power_profile) power = fixed_config.get(CONF_POWER) if power is None: power = fixed_config.get(CONF_POWER_TEMPLATE) power = self._resolve_template(power) states_power = fixed_config.get(CONF_STATES_POWER) if states_power: # Handle both list format (config flow) and dict format (YAML) if isinstance(states_power, list): states_power = {item[CONF_STATE]: item[CONF_POWER] for item in states_power} states_power = {state: self._resolve_template(value) for state, value in states_power.items()} return FixedStrategy(source_entity, power, states_power) def _create_lut( self, source_entity: SourceEntity, power_profile: PowerProfile | None, ) -> LutStrategy: """Create the lut strategy.""" if power_profile is None: raise StrategyConfigurationError( "You must supply a valid manufacturer and model to use the LUT mode", ) return LutStrategy(source_entity, self._lut_registry, power_profile) def _create_wled(self, source_entity: SourceEntity, config: dict) -> WledStrategy: """Create the WLED strategy.""" wled_config = self._get_strategy_config(CalculationStrategy.WLED, config, None) return WledStrategy( config=wled_config, light_entity=source_entity, hass=self._hass, standby_power=config.get(CONF_STANDBY_POWER), ) def _create_playbook(self, config: ConfigType, power_profile: PowerProfile | None) -> PlaybookStrategy: playbook_config = self._get_strategy_config(CalculationStrategy.PLAYBOOK, config, power_profile) directory = None if power_profile: directory = power_profile.get_model_directory() return PlaybookStrategy(self._hass, playbook_config, directory) async def _create_composite( self, config: ConfigType, source_entity: SourceEntity, power_profile: PowerProfile | None, ) -> CompositeStrategy: composite_config: list | dict | None = config.get(CONF_COMPOSITE) if composite_config is None: if power_profile and power_profile.composite_config: composite_config = power_profile.composite_config else: raise StrategyConfigurationError("No composite configuration supplied") sub_strategies: list[ConfigType] mode = DEFAULT_MODE if isinstance(composite_config, dict): mode = composite_config.get(CONF_MODE, DEFAULT_MODE) sub_strategies = composite_config.get(CONF_STRATEGIES, []) else: sub_strategies = composite_config async def _create_sub_strategy(strategy_config: ConfigType) -> SubStrategy: condition_instance = None condition_config = strategy_config.get(CONF_CONDITION) if condition_config: condition_type = condition_config.get(CONF_CONDITION) if condition_type in ["state", "numeric_state"] and CONF_ENTITY_ID not in condition_config: condition_config[CONF_ENTITY_ID] = [source_entity.entity_id] if condition_type == "state": condition_config = condition.state_validate_config(self._hass, condition_config) condition_instance = await condition.async_from_config( self._hass, condition_config, ) strategy = detect_calculation_strategy(strategy_config, power_profile) strategy_instance = await self.create( strategy_config, strategy, power_profile, source_entity, ) return SubStrategy(condition_config, condition_instance, strategy_instance) # type: ignore if not sub_strategies: raise StrategyConfigurationError("No strategies configured for composite strategy") strategies = [await _create_sub_strategy(config) for config in sub_strategies] return CompositeStrategy(self._hass, strategies, mode) def _create_multi_switch(self, config: ConfigType, power_profile: PowerProfile | None) -> MultiSwitchStrategy: """Create instance of multi switch strategy.""" multi_switch_config: ConfigType = {} if power_profile and power_profile.multi_switch_config: # Copy to avoid mutating the (potentially cached) profile config with the user's config below. multi_switch_config = dict(power_profile.multi_switch_config) multi_switch_config.update(config.get(CONF_MULTI_SWITCH, {})) if not multi_switch_config: raise StrategyConfigurationError("No multi_switch configuration supplied") entities: list[str] = multi_switch_config.get(CONF_ENTITIES, []) on_power: Decimal | None = multi_switch_config.get(CONF_POWER) off_power: Decimal | None = multi_switch_config.get(CONF_POWER_OFF) if on_power is None: raise StrategyConfigurationError("No power configuration supplied") return MultiSwitchStrategy( self._hass, entities, on_power=Decimal(on_power), off_power=Decimal(off_power) if off_power else None, ) def _resolve_template(self, value: Any) -> Any: # noqa: ANN401 """ Process the input to ensure it is a Template if applicable. Otherwise, return the original value. """ if isinstance(value, str) and value.startswith("{{"): return Template(value, self._hass) if isinstance(value, Template): value.hass = self._hass return value return value @staticmethod def _get_strategy_config( strategy: CalculationStrategy, config: ConfigType, power_profile: PowerProfile | None, ) -> ConfigType: """Get the strategy configuration.""" if strategy in config: return cast(ConfigType, config[strategy]) prop = f"{strategy}_config" if power_profile and getattr(power_profile, prop): return cast(ConfigType, getattr(power_profile, prop)) raise StrategyConfigurationError(f"No {strategy} configuration supplied")