Files
2026-07-14 23:57:03 -04:00

262 lines
10 KiB
Python

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")