"""Shared helpers for parsing numeric state values and converting between units.""" from __future__ import annotations from decimal import Decimal, DecimalException import logging from homeassistant.const import UnitOfEnergy from homeassistant.core import State from homeassistant.exceptions import HomeAssistantError, TemplateError from homeassistant.helpers.template import Template from homeassistant.util.unit_conversion import ( BaseUnitConverter, EnergyConverter, PowerConverter, ) from custom_components.powercalc.const import UNAVAILABLE_STATES, UnitPrefix _LOGGER = logging.getLogger(__name__) # Maps a configured energy unit prefix to the resulting energy unit of measurement. ENERGY_UNIT_PREFIX_MAPPING: dict[str, str] = { UnitPrefix.NONE: UnitOfEnergy.WATT_HOUR, UnitPrefix.KILO: UnitOfEnergy.KILO_WATT_HOUR, UnitPrefix.MEGA: UnitOfEnergy.MEGA_WATT_HOUR, UnitPrefix.GIGA: UnitOfEnergy.GIGA_WATT_HOUR, UnitPrefix.TERA: UnitOfEnergy.TERA_WATT_HOUR, } # Maps any energy or power unit of measurement to the converter that handles it. UNIT_CONVERTERS: dict[str | None, type[BaseUnitConverter]] = { **dict.fromkeys(EnergyConverter.VALID_UNITS, EnergyConverter), **dict.fromkeys(PowerConverter.VALID_UNITS, PowerConverter), } def parse_decimal(value: object) -> Decimal | None: """Parse a numeric value into a Decimal, returning None when it is not a usable number. Accepts a Home Assistant State (its ``state`` string is used) or a raw value of any type. Unknown or unavailable states and values that are not numeric yield None instead of raising. """ if isinstance(value, State): if value.state in UNAVAILABLE_STATES: return None value = value.state if not isinstance(value, (str, int, float, Decimal)): return None try: return Decimal(value) except DecimalException, ValueError: return None def evaluate_to_decimal(value: object) -> Decimal | None: """Evaluate a value into a Decimal, rendering it first when it is a template. Non-template values (strings, numbers) are parsed directly. Returns None when a template fails to render, or when the (rendered) value is not a usable number. Unknown/unavailable renders yield None silently; any other failure is logged. """ if isinstance(value, Template): try: value = value.async_render() except TemplateError as ex: _LOGGER.error("Could not render template %s: %s", value, ex) return None if value in UNAVAILABLE_STATES: return None result = parse_decimal(value) if result is None: _LOGGER.error("Could not convert value %s to a decimal", value) return result def convert_to_decimal( value: State | str | float | Decimal, from_unit: str | None, to_unit: str | None, ) -> Decimal | None: """Convert a value between energy/power units and return it as a Decimal. Returns None when the value is not numeric or the units cannot be converted. """ parsed = parse_decimal(value) if parsed is None: return None if from_unit == to_unit: return parsed converter = UNIT_CONVERTERS.get(from_unit) if converter is None: return None try: return Decimal(str(converter.convert(float(parsed), from_unit, to_unit))) except HomeAssistantError: return None