updated apps

This commit is contained in:
2026-07-14 23:57:03 -04:00
parent 6cc7212cef
commit 010e828e9c
797 changed files with 45153 additions and 4246 deletions
@@ -0,0 +1,130 @@
"""Convert config entry data to runtime sensor configuration."""
from __future__ import annotations
import copy
from datetime import timedelta
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType
from custom_components.powercalc.configuration.normalization import normalize_playbooks, normalize_states_power
from custom_components.powercalc.const import (
CONF_CALCULATION_ENABLED_CONDITION,
CONF_CREATE_GROUP,
CONF_DAILY_FIXED_ENERGY,
CONF_FIXED,
CONF_FORCE_ENERGY_SENSOR_CREATION,
CONF_LINEAR,
CONF_ON_TIME,
CONF_PLAYBOOK,
CONF_PLAYBOOKS,
CONF_POWER,
CONF_POWER_SENSOR_ID,
CONF_POWER_TEMPLATE,
CONF_SENSOR_TYPE,
CONF_STATES_POWER,
CONF_UTILITY_METER_OFFSET,
CONF_VALUE,
CONF_VALUE_TEMPLATE,
SensorType,
)
def convert_config_entry_to_sensor_config(config_entry: ConfigEntry, hass: HomeAssistant) -> ConfigType: # noqa: C901
"""Convert the config entry structure to the sensor config used to create the entities."""
sensor_config = dict(config_entry.data.copy())
sensor_type = sensor_config.get(CONF_SENSOR_TYPE)
def handle_sensor_type() -> None:
"""Handle sensor type-specific configuration."""
if sensor_type == SensorType.GROUP:
sensor_config[CONF_CREATE_GROUP] = sensor_config.get(CONF_NAME)
elif sensor_type == SensorType.REAL_POWER:
sensor_config[CONF_POWER_SENSOR_ID] = sensor_config.get(CONF_ENTITY_ID)
sensor_config[CONF_FORCE_ENERGY_SENSOR_CREATION] = True
def process_template(config: dict[str, Any], template_key: str, target_key: str) -> None:
"""Convert a template key in the config to a Template object."""
if template_key in config:
config[target_key] = Template(config[template_key], hass)
del config[template_key]
def process_on_time(config: dict[str, Any]) -> None:
"""Convert on_time dictionary to timedelta."""
on_time = config.get(CONF_ON_TIME)
config[CONF_ON_TIME] = (
timedelta(hours=on_time["hours"], minutes=on_time["minutes"], seconds=on_time["seconds"])
if on_time
else timedelta(days=1)
)
def process_states_power(states_power: dict[str, Any] | list[dict[str, Any]]) -> dict[str, Any]:
"""Convert state power values to Template objects where necessary."""
return {
key: Template(value, hass) if isinstance(value, str) and "{{" in value else value
for key, value in normalize_states_power(states_power).items()
}
def process_daily_fixed_energy() -> None:
"""Process daily fixed energy configuration."""
if CONF_DAILY_FIXED_ENERGY not in sensor_config:
return
daily_fixed_config = copy.copy(sensor_config[CONF_DAILY_FIXED_ENERGY])
process_template(daily_fixed_config, CONF_VALUE_TEMPLATE, CONF_VALUE)
process_on_time(daily_fixed_config)
sensor_config[CONF_DAILY_FIXED_ENERGY] = daily_fixed_config
def process_fixed_config() -> None:
"""Process fixed energy configuration."""
if CONF_FIXED not in sensor_config:
return
fixed_config = copy.copy(sensor_config[CONF_FIXED])
process_template(fixed_config, CONF_POWER_TEMPLATE, CONF_POWER)
if CONF_STATES_POWER in fixed_config:
fixed_config[CONF_STATES_POWER] = process_states_power(fixed_config[CONF_STATES_POWER])
sensor_config[CONF_FIXED] = fixed_config
def process_linear_config() -> None:
"""Process linear energy configuration."""
if CONF_LINEAR not in sensor_config:
return
linear_config = copy.copy(sensor_config[CONF_LINEAR])
sensor_config[CONF_LINEAR] = linear_config
def process_calculation_enabled_condition() -> None:
"""Process calculation enabled condition template."""
if CONF_CALCULATION_ENABLED_CONDITION in sensor_config:
sensor_config[CONF_CALCULATION_ENABLED_CONDITION] = Template(
sensor_config[CONF_CALCULATION_ENABLED_CONDITION],
hass,
)
def process_utility_meter_offset() -> None:
if CONF_UTILITY_METER_OFFSET in sensor_config:
sensor_config[CONF_UTILITY_METER_OFFSET] = timedelta(days=sensor_config[CONF_UTILITY_METER_OFFSET])
def process_playbook_config() -> None:
if CONF_PLAYBOOK not in sensor_config:
return
playbook_config = copy.copy(sensor_config[CONF_PLAYBOOK])
playbook_config[CONF_PLAYBOOKS] = normalize_playbooks(playbook_config[CONF_PLAYBOOKS])
sensor_config[CONF_PLAYBOOK] = playbook_config
handle_sensor_type()
process_daily_fixed_energy()
process_fixed_config()
process_linear_config()
process_playbook_config()
process_calculation_enabled_condition()
process_utility_meter_offset()
return sensor_config
@@ -0,0 +1,34 @@
"""Normalize discovery info into sensor configuration."""
from __future__ import annotations
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from custom_components.powercalc.const import (
CONF_GROUP_TYPE,
CONF_SENSOR_TYPE,
DISCOVERY_TYPE,
DUMMY_ENTITY_ID,
GroupType,
PowercalcDiscoveryType,
SensorType,
)
GROUP_DISCOVERY_TYPES = {
PowercalcDiscoveryType.DOMAIN_GROUP: GroupType.DOMAIN,
PowercalcDiscoveryType.STANDBY_GROUP: GroupType.STANDBY,
}
def convert_discovery_info_to_sensor_config(
discovery_info: DiscoveryInfoType,
) -> ConfigType:
"""Convert discovery info to sensor config."""
group_type = GROUP_DISCOVERY_TYPES.get(discovery_info[DISCOVERY_TYPE])
if group_type:
discovery_info[CONF_GROUP_TYPE] = group_type
discovery_info[CONF_SENSOR_TYPE] = SensorType.GROUP
discovery_info[CONF_ENTITY_ID] = DUMMY_ENTITY_ID
return discovery_info
@@ -8,6 +8,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from custom_components.powercalc.const import (
CONF_COST_SENSOR_NAMING,
CONF_CREATE_COST_SENSORS,
CONF_CREATE_DOMAIN_GROUPS,
CONF_CREATE_ENERGY_SENSORS,
CONF_CREATE_STANDBY_GROUP,
@@ -32,6 +34,7 @@ from custom_components.powercalc.const import (
CONF_POWER_SENSOR_PRECISION,
CONF_UTILITY_METER_OFFSET,
CONF_UTILITY_METER_TYPES,
DEFAULT_COST_NAME_PATTERN,
DEFAULT_ENERGY_INTEGRATION_METHOD,
DEFAULT_ENERGY_NAME_PATTERN,
DEFAULT_ENERGY_SENSOR_PRECISION,
@@ -72,6 +75,8 @@ def get_global_configuration(hass: HomeAssistant, config: ConfigType) -> ConfigT
CONF_IGNORE_UNAVAILABLE_STATE: False,
CONF_CREATE_DOMAIN_GROUPS: [],
CONF_CREATE_ENERGY_SENSORS: True,
CONF_CREATE_COST_SENSORS: False,
CONF_COST_SENSOR_NAMING: DEFAULT_COST_NAME_PATTERN,
CONF_CREATE_STANDBY_GROUP: True,
CONF_CREATE_UTILITY_METERS: False,
CONF_DISCOVERY: {
@@ -0,0 +1,30 @@
"""Normalize persisted configuration shapes to runtime mappings."""
from __future__ import annotations
from typing import Any
from homeassistant.const import CONF_ID, CONF_PATH
from custom_components.powercalc.const import CONF_PLAYBOOK_ID, CONF_POWER, CONF_STATE
def normalize_states_power(states_power: dict[str, Any] | list[dict[str, Any]]) -> dict[str, Any]:
"""Normalize state-power config to the runtime mapping shape."""
if isinstance(states_power, list):
return {item[CONF_STATE]: item[CONF_POWER] for item in states_power}
return dict(states_power)
def normalize_playbooks(playbooks: dict[str, str] | list[dict[str, str]]) -> dict[str, str]:
"""Normalize playbook config to the runtime id-path mapping shape."""
if isinstance(playbooks, list):
return {item[CONF_ID]: item[CONF_PATH] for item in playbooks}
return dict(playbooks)
def normalize_state_trigger(state_trigger: dict[str, str] | list[dict[str, str]]) -> dict[str, str]:
"""Normalize playbook state-trigger config to the runtime state-playbook mapping shape."""
if isinstance(state_trigger, list):
return {item[CONF_STATE]: item[CONF_PLAYBOOK_ID] for item in state_trigger}
return dict(state_trigger)
@@ -0,0 +1,184 @@
"""Sensor configuration schemas."""
from __future__ import annotations
from homeassistant.components.sensor import PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA
from homeassistant.components.utility_meter import max_28_days
from homeassistant.components.utility_meter.const import METER_TYPES
from homeassistant.const import CONF_ENTITIES, CONF_ENTITY_ID, CONF_NAME, CONF_UNIQUE_ID
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from custom_components.powercalc.common import validate_name_pattern
from custom_components.powercalc.const import (
CONF_AND,
CONF_AVAILABILITY_ENTITY,
CONF_CALCULATION_ENABLED_CONDITION,
CONF_COMPOSITE,
CONF_COST,
CONF_CREATE_COST_SENSOR,
CONF_CREATE_ENERGY_SENSOR,
CONF_CREATE_GROUP,
CONF_CREATE_UTILITY_METERS,
CONF_CUSTOM_MODEL_DIRECTORY,
CONF_DAILY_FIXED_ENERGY,
CONF_DELAY,
CONF_DISABLE_STANDBY_POWER,
CONF_ENERGY_FILTER_OUTLIER_ENABLED,
CONF_ENERGY_FILTER_OUTLIER_MAX,
CONF_ENERGY_INTEGRATION_METHOD,
CONF_ENERGY_SENSOR_CATEGORY,
CONF_ENERGY_SENSOR_ID,
CONF_ENERGY_SENSOR_NAMING,
CONF_ENERGY_SENSOR_UNIT_PREFIX,
CONF_FILTER,
CONF_FIXED,
CONF_FORCE_CALCULATE_GROUP_ENERGY,
CONF_FORCE_ENERGY_SENSOR_CREATION,
CONF_GROUP_ENERGY_START_AT_ZERO,
CONF_GROUP_TYPE,
CONF_HIDE_MEMBERS,
CONF_IGNORE_UNAVAILABLE_STATE,
CONF_INCLUDE,
CONF_INCLUDE_NON_POWERCALC_SENSORS,
CONF_LINEAR,
CONF_MANUFACTURER,
CONF_MODE,
CONF_MODEL,
CONF_MULTI_SWITCH,
CONF_MULTIPLY_FACTOR,
CONF_MULTIPLY_FACTOR_STANDBY,
CONF_NOT,
CONF_OR,
CONF_PLAYBOOK,
CONF_POWER,
CONF_POWER_SENSOR_CATEGORY,
CONF_POWER_SENSOR_ID,
CONF_POWER_SENSOR_NAMING,
CONF_SLEEP_POWER,
CONF_STANDBY_POWER,
CONF_SUBTRACT_ENTITIES,
CONF_UNAVAILABLE_POWER,
CONF_UTILITY_METER_NET_CONSUMPTION,
CONF_UTILITY_METER_OFFSET,
CONF_UTILITY_METER_TARIFFS,
CONF_UTILITY_METER_TYPES,
CONF_VARIABLES,
CONF_WLED,
ENERGY_INTEGRATION_METHODS,
ENTITY_CATEGORIES,
CalculationStrategy,
GroupType,
UnitPrefix,
)
from custom_components.powercalc.group_include.filter import FILTER_CONFIG
from custom_components.powercalc.sensors.daily_energy import DAILY_FIXED_ENERGY_SCHEMA
from custom_components.powercalc.strategy.composite import CONFIG_SCHEMA as COMPOSITE_SCHEMA
from custom_components.powercalc.strategy.fixed import CONFIG_SCHEMA as FIXED_SCHEMA
from custom_components.powercalc.strategy.linear import CONFIG_SCHEMA as LINEAR_SCHEMA
from custom_components.powercalc.strategy.multi_switch import CONFIG_SCHEMA as MULTI_SWITCH_SCHEMA
from custom_components.powercalc.strategy.playbook import CONFIG_SCHEMA as PLAYBOOK_SCHEMA
from custom_components.powercalc.strategy.wled import CONFIG_SCHEMA as WLED_SCHEMA
MAX_GROUP_NESTING_LEVEL = 5
SENSOR_CONFIG = {
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_AVAILABILITY_ENTITY): cv.entity_id,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_MODEL): cv.string,
vol.Optional(CONF_MANUFACTURER): cv.string,
vol.Optional(CONF_MODE): vol.In([cls.value for cls in CalculationStrategy]),
vol.Optional(CONF_STANDBY_POWER): vol.Any(vol.Coerce(float), cv.template),
vol.Optional(CONF_DISABLE_STANDBY_POWER): cv.boolean,
vol.Optional(CONF_CUSTOM_MODEL_DIRECTORY): cv.string,
vol.Optional(CONF_POWER_SENSOR_ID): cv.entity_id,
vol.Optional(CONF_COST): vol.Schema({vol.Required(CONF_ENERGY_SENSOR_ID): cv.entity_id}),
vol.Optional(CONF_FORCE_ENERGY_SENSOR_CREATION): cv.boolean,
vol.Optional(CONF_FORCE_CALCULATE_GROUP_ENERGY): cv.boolean,
vol.Optional(CONF_FIXED): FIXED_SCHEMA,
vol.Optional(CONF_LINEAR): LINEAR_SCHEMA,
vol.Optional(CONF_MULTI_SWITCH): MULTI_SWITCH_SCHEMA,
vol.Optional(CONF_WLED): WLED_SCHEMA,
vol.Optional(CONF_PLAYBOOK): PLAYBOOK_SCHEMA,
vol.Optional(CONF_DAILY_FIXED_ENERGY): DAILY_FIXED_ENERGY_SCHEMA,
vol.Optional(CONF_CREATE_ENERGY_SENSOR): cv.boolean,
vol.Optional(CONF_CREATE_COST_SENSOR): cv.boolean,
vol.Optional(CONF_CREATE_UTILITY_METERS): cv.boolean,
vol.Optional(CONF_UTILITY_METER_NET_CONSUMPTION): cv.boolean,
vol.Optional(CONF_UTILITY_METER_TARIFFS): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_UTILITY_METER_TYPES): vol.All(cv.ensure_list, [vol.In(METER_TYPES)]),
vol.Optional(CONF_UTILITY_METER_OFFSET): vol.All(cv.time_period, cv.positive_timedelta, max_28_days),
vol.Optional(CONF_MULTIPLY_FACTOR): vol.Coerce(float),
vol.Optional(CONF_MULTIPLY_FACTOR_STANDBY): cv.boolean,
vol.Optional(CONF_POWER_SENSOR_NAMING): validate_name_pattern,
vol.Optional(CONF_POWER_SENSOR_CATEGORY): vol.In(ENTITY_CATEGORIES),
vol.Optional(CONF_ENERGY_SENSOR_ID): cv.entity_id,
vol.Optional(CONF_ENERGY_SENSOR_NAMING): validate_name_pattern,
vol.Optional(CONF_ENERGY_SENSOR_CATEGORY): vol.In(ENTITY_CATEGORIES),
vol.Optional(CONF_ENERGY_INTEGRATION_METHOD): vol.In(ENERGY_INTEGRATION_METHODS),
vol.Optional(CONF_ENERGY_FILTER_OUTLIER_ENABLED): cv.boolean,
vol.Optional(CONF_ENERGY_FILTER_OUTLIER_MAX): cv.positive_int,
vol.Optional(CONF_ENERGY_SENSOR_UNIT_PREFIX): vol.In([cls.value for cls in UnitPrefix]),
vol.Optional(CONF_CREATE_GROUP): cv.string,
vol.Optional(CONF_GROUP_ENERGY_START_AT_ZERO): cv.boolean,
vol.Optional(CONF_GROUP_TYPE): vol.In([cls.value for cls in GroupType]),
vol.Optional(CONF_SUBTRACT_ENTITIES): vol.All(cv.ensure_list, [cv.entity_id]),
vol.Optional(CONF_HIDE_MEMBERS): cv.boolean,
vol.Optional(CONF_INCLUDE): vol.Schema(
{
**FILTER_CONFIG.schema,
vol.Optional(CONF_FILTER): vol.Schema(
{
**FILTER_CONFIG.schema,
vol.Optional(CONF_OR): vol.All(cv.ensure_list, [FILTER_CONFIG]),
vol.Optional(CONF_AND): vol.All(cv.ensure_list, [FILTER_CONFIG]),
vol.Optional(CONF_NOT): vol.All(cv.ensure_list, [FILTER_CONFIG]),
},
),
vol.Optional(CONF_INCLUDE_NON_POWERCALC_SENSORS, default=True): cv.boolean,
},
),
vol.Optional(CONF_IGNORE_UNAVAILABLE_STATE): cv.boolean,
vol.Optional(CONF_CALCULATION_ENABLED_CONDITION): cv.template,
vol.Optional(CONF_SLEEP_POWER): vol.Schema(
{
vol.Required(CONF_POWER): vol.Coerce(float),
vol.Required(CONF_DELAY): cv.positive_int,
},
),
vol.Optional(CONF_UNAVAILABLE_POWER): vol.Coerce(float),
vol.Optional(CONF_COMPOSITE): COMPOSITE_SCHEMA,
vol.Optional(CONF_VARIABLES): vol.Schema({cv.string: cv.string}),
}
def build_nested_configuration_schema(schema: dict, iteration: int = 0) -> dict:
if iteration == MAX_GROUP_NESTING_LEVEL:
return schema
iteration += 1
schema.update(
{
vol.Optional(CONF_ENTITIES): vol.All(
cv.ensure_list,
[build_nested_configuration_schema(schema.copy(), iteration)],
),
},
)
return schema
SENSOR_CONFIG = build_nested_configuration_schema(SENSOR_CONFIG)
PLATFORM_SCHEMA = vol.All(
cv.has_at_least_one_key(
CONF_ENTITY_ID,
CONF_POWER_SENSOR_ID,
CONF_ENTITIES,
CONF_INCLUDE,
CONF_DAILY_FIXED_ENERGY,
CONF_COST,
),
SENSOR_PLATFORM_SCHEMA.extend(SENSOR_CONFIG),
)