Introduce UnitConverter protocol (#78888)
* Introduce ConversionUtility * Use ConversionUtility in number * Use ConversionUtility in sensor * Use ConversionUtility in sensor recorder * Add normalise to ConversionUtility * Revert changes to recorder.py * Reduce size of PR * Adjust recorder statistics * Rename variable * Rename * Apply suggestion Co-authored-by: Erik Montnemery <erik@montnemery.com> * Apply suggestion Co-authored-by: Erik Montnemery <erik@montnemery.com> * Apply suggestion Co-authored-by: Erik Montnemery <erik@montnemery.com> Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
e62e21ce46
commit
39315b7fe3
8 changed files with 72 additions and 66 deletions
|
@ -28,7 +28,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401
|
|||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.typing import ConfigType, UnitConverter
|
||||
from homeassistant.util import temperature as temperature_util
|
||||
|
||||
from .const import (
|
||||
|
@ -70,12 +70,8 @@ class NumberMode(StrEnum):
|
|||
SLIDER = "slider"
|
||||
|
||||
|
||||
UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = {
|
||||
NumberDeviceClass.TEMPERATURE: temperature_util.convert,
|
||||
}
|
||||
|
||||
VALID_UNITS: dict[str, tuple[str, ...]] = {
|
||||
NumberDeviceClass.TEMPERATURE: temperature_util.VALID_UNITS,
|
||||
UNIT_CONVERTERS: dict[str, UnitConverter] = {
|
||||
NumberDeviceClass.TEMPERATURE: temperature_util,
|
||||
}
|
||||
|
||||
# mypy: disallow-any-generics
|
||||
|
@ -436,7 +432,7 @@ class NumberEntity(Entity):
|
|||
|
||||
if (
|
||||
native_unit_of_measurement != unit_of_measurement
|
||||
and device_class in UNIT_CONVERSIONS
|
||||
and device_class in UNIT_CONVERTERS
|
||||
):
|
||||
assert native_unit_of_measurement
|
||||
assert unit_of_measurement
|
||||
|
@ -446,7 +442,7 @@ class NumberEntity(Entity):
|
|||
|
||||
# Suppress ValueError (Could not convert value to float)
|
||||
with suppress(ValueError):
|
||||
value_new: float = UNIT_CONVERSIONS[device_class](
|
||||
value_new: float = UNIT_CONVERTERS[device_class].convert(
|
||||
value,
|
||||
native_unit_of_measurement,
|
||||
unit_of_measurement,
|
||||
|
@ -467,12 +463,12 @@ class NumberEntity(Entity):
|
|||
if (
|
||||
value is not None
|
||||
and native_unit_of_measurement != unit_of_measurement
|
||||
and device_class in UNIT_CONVERSIONS
|
||||
and device_class in UNIT_CONVERTERS
|
||||
):
|
||||
assert native_unit_of_measurement
|
||||
assert unit_of_measurement
|
||||
|
||||
value = UNIT_CONVERSIONS[device_class](
|
||||
value = UNIT_CONVERTERS[device_class].convert(
|
||||
value,
|
||||
unit_of_measurement,
|
||||
native_unit_of_measurement,
|
||||
|
@ -500,9 +496,10 @@ class NumberEntity(Entity):
|
|||
if (
|
||||
(number_options := self.registry_entry.options.get(DOMAIN))
|
||||
and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT))
|
||||
and (device_class := self.device_class) in UNIT_CONVERSIONS
|
||||
and self.native_unit_of_measurement in VALID_UNITS[device_class]
|
||||
and custom_unit in VALID_UNITS[device_class]
|
||||
and (device_class := self.device_class) in UNIT_CONVERTERS
|
||||
and self.native_unit_of_measurement
|
||||
in UNIT_CONVERTERS[device_class].VALID_UNITS
|
||||
and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS
|
||||
):
|
||||
self._number_option_unit_of_measurement = custom_unit
|
||||
return
|
||||
|
|
|
@ -36,7 +36,7 @@ from homeassistant.exceptions import HomeAssistantError
|
|||
from homeassistant.helpers import entity_registry
|
||||
from homeassistant.helpers.json import JSONEncoder
|
||||
from homeassistant.helpers.storage import STORAGE_DIR
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType, UnitConverter
|
||||
from homeassistant.util import (
|
||||
dt as dt_util,
|
||||
energy as energy_util,
|
||||
|
@ -186,12 +186,12 @@ STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = {
|
|||
VOLUME_CUBIC_METERS: "volume",
|
||||
}
|
||||
|
||||
STATISTIC_UNIT_TO_VALID_UNITS: dict[str | None, Iterable[str | None]] = {
|
||||
ENERGY_KILO_WATT_HOUR: energy_util.VALID_UNITS,
|
||||
POWER_WATT: power_util.VALID_UNITS,
|
||||
PRESSURE_PA: pressure_util.VALID_UNITS,
|
||||
TEMP_CELSIUS: temperature_util.VALID_UNITS,
|
||||
VOLUME_CUBIC_METERS: volume_util.VALID_UNITS,
|
||||
STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, UnitConverter] = {
|
||||
ENERGY_KILO_WATT_HOUR: energy_util,
|
||||
POWER_WATT: power_util,
|
||||
PRESSURE_PA: pressure_util,
|
||||
TEMP_CELSIUS: temperature_util,
|
||||
VOLUME_CUBIC_METERS: volume_util,
|
||||
}
|
||||
|
||||
# Convert energy power, pressure, temperature and volume statistics from the
|
||||
|
@ -243,7 +243,8 @@ def _get_statistic_to_display_unit_converter(
|
|||
else:
|
||||
display_unit = state_unit
|
||||
|
||||
if display_unit not in STATISTIC_UNIT_TO_VALID_UNITS[statistic_unit]:
|
||||
unit_converter = STATISTIC_UNIT_TO_UNIT_CONVERTER[statistic_unit]
|
||||
if display_unit not in unit_converter.VALID_UNITS:
|
||||
# Guard against invalid state unit in the DB
|
||||
return no_conversion
|
||||
|
||||
|
@ -1514,9 +1515,11 @@ def _validate_units(statistics_unit: str | None, state_unit: str | None) -> None
|
|||
"""Raise if the statistics unit and state unit are not compatible."""
|
||||
if statistics_unit == state_unit:
|
||||
return
|
||||
if (valid_units := STATISTIC_UNIT_TO_VALID_UNITS.get(statistics_unit)) is None:
|
||||
if (
|
||||
unit_converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistics_unit)
|
||||
) is None:
|
||||
raise HomeAssistantError(f"Invalid units {statistics_unit},{state_unit}")
|
||||
if state_unit not in valid_units:
|
||||
if state_unit not in unit_converter.VALID_UNITS:
|
||||
raise HomeAssistantError(f"Invalid units {statistics_unit},{state_unit}")
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Component to interface with various sensors that can be monitored."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from collections.abc import Mapping
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
|
@ -56,7 +56,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401
|
|||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType, StateType
|
||||
from homeassistant.helpers.typing import ConfigType, StateType, UnitConverter
|
||||
from homeassistant.util import (
|
||||
dt as dt_util,
|
||||
pressure as pressure_util,
|
||||
|
@ -207,9 +207,9 @@ STATE_CLASS_TOTAL: Final = "total"
|
|||
STATE_CLASS_TOTAL_INCREASING: Final = "total_increasing"
|
||||
STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass]
|
||||
|
||||
UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = {
|
||||
SensorDeviceClass.PRESSURE: pressure_util.convert,
|
||||
SensorDeviceClass.TEMPERATURE: temperature_util.convert,
|
||||
UNIT_CONVERTERS: dict[str, UnitConverter] = {
|
||||
SensorDeviceClass.PRESSURE: pressure_util,
|
||||
SensorDeviceClass.TEMPERATURE: temperature_util,
|
||||
}
|
||||
|
||||
UNIT_RATIOS: dict[str, dict[str, float]] = {
|
||||
|
@ -221,11 +221,6 @@ UNIT_RATIOS: dict[str, dict[str, float]] = {
|
|||
},
|
||||
}
|
||||
|
||||
VALID_UNITS: dict[str, tuple[str, ...]] = {
|
||||
SensorDeviceClass.PRESSURE: pressure_util.VALID_UNITS,
|
||||
SensorDeviceClass.TEMPERATURE: temperature_util.VALID_UNITS,
|
||||
}
|
||||
|
||||
# mypy: disallow-any-generics
|
||||
|
||||
|
||||
|
@ -431,7 +426,7 @@ class SensorEntity(Entity):
|
|||
if (
|
||||
value is not None
|
||||
and native_unit_of_measurement != unit_of_measurement
|
||||
and device_class in UNIT_CONVERSIONS
|
||||
and device_class in UNIT_CONVERTERS
|
||||
):
|
||||
assert unit_of_measurement
|
||||
assert native_unit_of_measurement
|
||||
|
@ -453,7 +448,7 @@ class SensorEntity(Entity):
|
|||
# Suppress ValueError (Could not convert sensor_value to float)
|
||||
with suppress(ValueError):
|
||||
value_f = float(value) # type: ignore[arg-type]
|
||||
value_f_new = UNIT_CONVERSIONS[device_class](
|
||||
value_f_new = UNIT_CONVERTERS[device_class].convert(
|
||||
value_f,
|
||||
native_unit_of_measurement,
|
||||
unit_of_measurement,
|
||||
|
@ -482,9 +477,10 @@ class SensorEntity(Entity):
|
|||
if (
|
||||
(sensor_options := self.registry_entry.options.get(DOMAIN))
|
||||
and (custom_unit := sensor_options.get(CONF_UNIT_OF_MEASUREMENT))
|
||||
and (device_class := self.device_class) in UNIT_CONVERSIONS
|
||||
and self.native_unit_of_measurement in VALID_UNITS[device_class]
|
||||
and custom_unit in VALID_UNITS[device_class]
|
||||
and (device_class := self.device_class) in UNIT_CONVERTERS
|
||||
and self.native_unit_of_measurement
|
||||
in UNIT_CONVERTERS[device_class].VALID_UNITS
|
||||
and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS
|
||||
):
|
||||
self._sensor_option_unit_of_measurement = custom_unit
|
||||
return
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Typing Helpers for Home Assistant."""
|
||||
from collections.abc import Mapping
|
||||
from enum import Enum
|
||||
from typing import Any, Optional, Union
|
||||
from typing import Any, Optional, Protocol, Union
|
||||
|
||||
import homeassistant.core
|
||||
|
||||
|
@ -26,6 +26,16 @@ class UndefinedType(Enum):
|
|||
|
||||
UNDEFINED = UndefinedType._singleton # pylint: disable=protected-access
|
||||
|
||||
|
||||
class UnitConverter(Protocol):
|
||||
"""Define the format of a conversion utility."""
|
||||
|
||||
VALID_UNITS: tuple[str, ...]
|
||||
|
||||
def convert(self, value: float, from_unit: str, to_unit: str) -> float:
|
||||
"""Convert one unit of measurement to another."""
|
||||
|
||||
|
||||
# The following types should not used and
|
||||
# are not present in the core code base.
|
||||
# They are kept in order not to break custom integrations
|
||||
|
|
|
@ -23,18 +23,18 @@ UNIT_CONVERSION: dict[str, float] = {
|
|||
}
|
||||
|
||||
|
||||
def convert(value: float, unit_1: str, unit_2: str) -> float:
|
||||
def convert(value: float, from_unit: str, to_unit: str) -> float:
|
||||
"""Convert one unit of measurement to another."""
|
||||
if unit_1 not in VALID_UNITS:
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, "energy"))
|
||||
if unit_2 not in VALID_UNITS:
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, "energy"))
|
||||
if from_unit not in VALID_UNITS:
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, "energy"))
|
||||
if to_unit not in VALID_UNITS:
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, "energy"))
|
||||
|
||||
if not isinstance(value, Number):
|
||||
raise TypeError(f"{value} is not of numeric type")
|
||||
|
||||
if unit_1 == unit_2:
|
||||
if from_unit == to_unit:
|
||||
return value
|
||||
|
||||
watts = value / UNIT_CONVERSION[unit_1]
|
||||
return watts * UNIT_CONVERSION[unit_2]
|
||||
watthours = value / UNIT_CONVERSION[from_unit]
|
||||
return watthours * UNIT_CONVERSION[to_unit]
|
||||
|
|
|
@ -20,18 +20,18 @@ UNIT_CONVERSION: dict[str, float] = {
|
|||
}
|
||||
|
||||
|
||||
def convert(value: float, unit_1: str, unit_2: str) -> float:
|
||||
def convert(value: float, from_unit: str, to_unit: str) -> float:
|
||||
"""Convert one unit of measurement to another."""
|
||||
if unit_1 not in VALID_UNITS:
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, "power"))
|
||||
if unit_2 not in VALID_UNITS:
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, "power"))
|
||||
if from_unit not in VALID_UNITS:
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, "power"))
|
||||
if to_unit not in VALID_UNITS:
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, "power"))
|
||||
|
||||
if not isinstance(value, Number):
|
||||
raise TypeError(f"{value} is not of numeric type")
|
||||
|
||||
if unit_1 == unit_2:
|
||||
if from_unit == to_unit:
|
||||
return value
|
||||
|
||||
watts = value / UNIT_CONVERSION[unit_1]
|
||||
return watts * UNIT_CONVERSION[unit_2]
|
||||
watts = value / UNIT_CONVERSION[from_unit]
|
||||
return watts * UNIT_CONVERSION[to_unit]
|
||||
|
|
|
@ -42,18 +42,18 @@ UNIT_CONVERSION: dict[str, float] = {
|
|||
}
|
||||
|
||||
|
||||
def convert(value: float, unit_1: str, unit_2: str) -> float:
|
||||
def convert(value: float, from_unit: str, to_unit: str) -> float:
|
||||
"""Convert one unit of measurement to another."""
|
||||
if unit_1 not in VALID_UNITS:
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, PRESSURE))
|
||||
if unit_2 not in VALID_UNITS:
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, PRESSURE))
|
||||
if from_unit not in VALID_UNITS:
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, PRESSURE))
|
||||
if to_unit not in VALID_UNITS:
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, PRESSURE))
|
||||
|
||||
if not isinstance(value, Number):
|
||||
raise TypeError(f"{value} is not of numeric type")
|
||||
|
||||
if unit_1 == unit_2:
|
||||
if from_unit == to_unit:
|
||||
return value
|
||||
|
||||
pascals = value / UNIT_CONVERSION[unit_1]
|
||||
return pascals * UNIT_CONVERSION[unit_2]
|
||||
pascals = value / UNIT_CONVERSION[from_unit]
|
||||
return pascals * UNIT_CONVERSION[to_unit]
|
||||
|
|
|
@ -46,9 +46,9 @@ def convert(
|
|||
temperature: float, from_unit: str, to_unit: str, interval: bool = False
|
||||
) -> float:
|
||||
"""Convert a temperature from one unit to another."""
|
||||
if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN):
|
||||
if from_unit not in VALID_UNITS:
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, TEMPERATURE))
|
||||
if to_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN):
|
||||
if to_unit not in VALID_UNITS:
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, TEMPERATURE))
|
||||
|
||||
if from_unit == to_unit:
|
||||
|
|
Loading…
Add table
Reference in a new issue