224 files
This commit is contained in:
@@ -7,7 +7,7 @@ from enum import StrEnum
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import CONF_ATTRIBUTE, CONF_CONDITION, CONF_ENTITY_ID, STATE_OFF
|
||||
from homeassistant.const import CONF_ATTRIBUTE, CONF_CONDITION, CONF_CONDITIONS, CONF_ENTITY_ID, STATE_OFF
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.exceptions import ConditionError
|
||||
from homeassistant.helpers.condition import ConditionCheckerType
|
||||
@@ -41,14 +41,52 @@ class CompositeMode(StrEnum):
|
||||
SUM_ALL = "sum_all"
|
||||
|
||||
|
||||
class ConditionType(StrEnum):
|
||||
"""Condition types supported by the composite strategy.
|
||||
|
||||
Home Assistant has no constants for these, it uses string literals itself.
|
||||
"""
|
||||
|
||||
AND = "and"
|
||||
DEVICE = "device"
|
||||
NOT = "not"
|
||||
NUMERIC_STATE = "numeric_state"
|
||||
OR = "or"
|
||||
STATE = "state"
|
||||
TEMPLATE = "template"
|
||||
|
||||
|
||||
COMPOUND_CONDITIONS = (ConditionType.AND, ConditionType.OR, ConditionType.NOT)
|
||||
ENTITY_CONDITIONS = (ConditionType.STATE, ConditionType.NUMERIC_STATE)
|
||||
|
||||
DEFAULT_MODE = CompositeMode.STOP_AT_FIRST
|
||||
|
||||
|
||||
def make_entity_id_optional(schema: vol.Schema) -> vol.Schema:
|
||||
"""Make entity_id optional in schema."""
|
||||
schema = schema.schema
|
||||
schema[vol.Optional(CONF_ENTITY_ID)] = schema.pop(vol.Required(CONF_ENTITY_ID)) # type: ignore[index, attr-defined]
|
||||
return vol.Schema(schema)
|
||||
# Copy, the schemas we get passed here are module level globals of Home Assistant itself
|
||||
schema_dict = dict(schema.schema)
|
||||
schema_dict[vol.Optional(CONF_ENTITY_ID)] = schema_dict.pop(vol.Required(CONF_ENTITY_ID))
|
||||
return vol.Schema(schema_dict)
|
||||
|
||||
|
||||
def get_compound_schema(condition_type: ConditionType) -> vol.Schema:
|
||||
"""Return the schema for and/or/not conditions.
|
||||
|
||||
Home Assistant's own compound schemas recurse into `cv.CONDITION_SCHEMA`, which requires entity_id.
|
||||
We recurse into our own schema instead, so entity_id stays optional at any nesting level
|
||||
and can be defaulted to the source entity.
|
||||
"""
|
||||
return vol.Schema(
|
||||
{
|
||||
**cv.CONDITION_BASE_SCHEMA,
|
||||
vol.Required(CONF_CONDITION): condition_type.value,
|
||||
vol.Required(CONF_CONDITIONS): vol.All(
|
||||
cv.ensure_list,
|
||||
[lambda value: CONDITION_SCHEMA(value)],
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_numeric_state_schema() -> vol.Schema:
|
||||
@@ -86,13 +124,13 @@ CONDITION_SCHEMA: vol.Schema = vol.Schema(
|
||||
cv.key_value_schemas(
|
||||
CONF_CONDITION,
|
||||
{
|
||||
"and": cv.AND_CONDITION_SCHEMA,
|
||||
"device": cv.DEVICE_CONDITION_SCHEMA,
|
||||
"not": cv.NOT_CONDITION_SCHEMA,
|
||||
"numeric_state": get_numeric_state_schema(),
|
||||
"or": cv.OR_CONDITION_SCHEMA,
|
||||
"state": get_state_schema,
|
||||
"template": cv.TEMPLATE_CONDITION_SCHEMA,
|
||||
ConditionType.AND: get_compound_schema(ConditionType.AND),
|
||||
ConditionType.DEVICE: cv.DEVICE_CONDITION_SCHEMA,
|
||||
ConditionType.NOT: get_compound_schema(ConditionType.NOT),
|
||||
ConditionType.NUMERIC_STATE: get_numeric_state_schema(),
|
||||
ConditionType.OR: get_compound_schema(ConditionType.OR),
|
||||
ConditionType.STATE: get_state_schema,
|
||||
ConditionType.TEMPLATE: cv.TEMPLATE_CONDITION_SCHEMA,
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,12 +4,13 @@ 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.const import CONF_CONDITION, CONF_CONDITIONS, 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
|
||||
import voluptuous as vol
|
||||
|
||||
from custom_components.powercalc.common import SourceEntity
|
||||
from custom_components.powercalc.const import (
|
||||
@@ -31,7 +32,14 @@ from custom_components.powercalc.errors import (
|
||||
)
|
||||
from custom_components.powercalc.power_profile.power_profile import PowerProfile
|
||||
|
||||
from .composite import DEFAULT_MODE, CompositeStrategy, SubStrategy
|
||||
from .composite import (
|
||||
COMPOUND_CONDITIONS,
|
||||
CONFIG_SCHEMA as COMPOSITE_SCHEMA,
|
||||
DEFAULT_MODE,
|
||||
ENTITY_CONDITIONS,
|
||||
CompositeStrategy,
|
||||
SubStrategy,
|
||||
)
|
||||
from .fixed import FixedStrategy
|
||||
from .linear import LinearStrategy
|
||||
from .lut import LutRegistry, LutStrategy
|
||||
@@ -42,6 +50,24 @@ from .strategy_interface import PowerCalculationStrategyInterface
|
||||
from .wled import WledStrategy
|
||||
|
||||
|
||||
def resolve_condition_entity_ids(condition_config: ConfigType, source_entity: SourceEntity) -> ConfigType:
|
||||
"""Default entity_id to the source entity for conditions which omit it.
|
||||
|
||||
Recurses into and/or/not, so nested conditions are treated the same as top level ones.
|
||||
Returns a copy, to prevent mutating the profile configuration.
|
||||
"""
|
||||
resolved = dict(condition_config)
|
||||
condition_type = resolved.get(CONF_CONDITION)
|
||||
if condition_type in COMPOUND_CONDITIONS:
|
||||
resolved[CONF_CONDITIONS] = [
|
||||
resolve_condition_entity_ids(sub_condition, source_entity)
|
||||
for sub_condition in resolved.get(CONF_CONDITIONS, [])
|
||||
]
|
||||
elif condition_type in ENTITY_CONDITIONS and CONF_ENTITY_ID not in resolved:
|
||||
resolved[CONF_ENTITY_ID] = [source_entity.entity_id]
|
||||
return resolved
|
||||
|
||||
|
||||
class PowerCalculatorStrategyFactory:
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
self._hass = hass
|
||||
@@ -167,7 +193,7 @@ class PowerCalculatorStrategyFactory:
|
||||
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
|
||||
composite_config = self._validate_composite_config(power_profile.composite_config)
|
||||
else:
|
||||
raise StrategyConfigurationError("No composite configuration supplied")
|
||||
|
||||
@@ -183,11 +209,8 @@ class PowerCalculatorStrategyFactory:
|
||||
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_config = resolve_condition_entity_ids(condition_config, source_entity)
|
||||
condition_config = await condition.async_validate_condition_config(self._hass, condition_config)
|
||||
condition_instance = await condition.async_from_config(
|
||||
self._hass,
|
||||
condition_config,
|
||||
@@ -207,6 +230,19 @@ class PowerCalculatorStrategyFactory:
|
||||
strategies = [await _create_sub_strategy(config) for config in sub_strategies]
|
||||
return CompositeStrategy(self._hass, strategies, mode)
|
||||
|
||||
@staticmethod
|
||||
def _validate_composite_config(composite_config: list | dict) -> list | dict:
|
||||
"""Validate the composite configuration of a library profile.
|
||||
|
||||
Configuration from YAML and the config flow is already validated by the sensor schema.
|
||||
Library profiles are raw JSON, so validate them here as well to get the same normalization,
|
||||
for example entity_id to a list and value_template to a Template instance.
|
||||
"""
|
||||
try:
|
||||
return cast(list | dict, COMPOSITE_SCHEMA(composite_config))
|
||||
except vol.Invalid as err:
|
||||
raise StrategyConfigurationError(f"Invalid composite configuration in profile: {err}") from err
|
||||
|
||||
def _create_multi_switch(self, config: ConfigType, power_profile: PowerProfile | None) -> MultiSwitchStrategy:
|
||||
"""Create instance of multi switch strategy."""
|
||||
multi_switch_config: ConfigType = {}
|
||||
|
||||
Reference in New Issue
Block a user