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

99 lines
3.4 KiB
Python

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