Files
HomeAssistantVS/custom_components/powercalc/unit.py
T

97 lines
3.3 KiB
Python

"""Shared helpers for parsing numeric state values and converting between units."""
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