From 6002377d4f7389f3f7415f3395d8481eff0ea502 Mon Sep 17 00:00:00 2001
From: epenet <6771947+epenet@users.noreply.github.com>
Date: Thu, 22 Sep 2022 14:15:22 +0200
Subject: [PATCH] Convert UnitConverter protocol to a class (#78934)

* Convert UnitConverter protocl to a class

* Remove logic change

* Use TypeVar

* Remove NORMALIZED_UNIT from pressure

* Reduce size of PR

* Reduce some more

* Once more

* Once more

* Remove DEVICE_CLASS
---
 homeassistant/components/number/__init__.py   |  8 +--
 .../components/recorder/statistics.py         | 22 ++++---
 homeassistant/components/sensor/__init__.py   | 17 +++---
 homeassistant/components/sensor/recorder.py   | 21 ++++---
 homeassistant/helpers/typing.py               | 12 +---
 homeassistant/util/unit_conversion.py         | 60 +++++++++++++++++++
 6 files changed, 103 insertions(+), 37 deletions(-)
 create mode 100644 homeassistant/util/unit_conversion.py

diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py
index f3e9a1d9da1..0012a4b77ff 100644
--- a/homeassistant/components/number/__init__.py
+++ b/homeassistant/components/number/__init__.py
@@ -28,8 +28,8 @@ 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, UnitConverter
-from homeassistant.util import temperature as temperature_util
+from homeassistant.helpers.typing import ConfigType
+from homeassistant.util.unit_conversion import BaseUnitConverter, TemperatureConverter
 
 from .const import (
     ATTR_MAX,
@@ -70,8 +70,8 @@ class NumberMode(StrEnum):
     SLIDER = "slider"
 
 
-UNIT_CONVERTERS: dict[str, UnitConverter] = {
-    NumberDeviceClass.TEMPERATURE: temperature_util,
+UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = {
+    NumberDeviceClass.TEMPERATURE: TemperatureConverter,
 }
 
 # mypy: disallow-any-generics
diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py
index d1d9dd2a658..bb38f35cad2 100644
--- a/homeassistant/components/recorder/statistics.py
+++ b/homeassistant/components/recorder/statistics.py
@@ -29,7 +29,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, UnitConverter
+from homeassistant.helpers.typing import UNDEFINED, UndefinedType
 from homeassistant.util import (
     dt as dt_util,
     energy as energy_util,
@@ -38,6 +38,14 @@ from homeassistant.util import (
     temperature as temperature_util,
     volume as volume_util,
 )
+from homeassistant.util.unit_conversion import (
+    BaseUnitConverter,
+    EnergyConverter,
+    PowerConverter,
+    PressureConverter,
+    TemperatureConverter,
+    VolumeConverter,
+)
 
 from .const import DOMAIN, MAX_ROWS_TO_PURGE, SupportedDialect
 from .db_schema import Statistics, StatisticsMeta, StatisticsRuns, StatisticsShortTerm
@@ -179,12 +187,12 @@ STATISTIC_UNIT_TO_UNIT_CLASS: dict[str | None, str] = {
     volume_util.NORMALIZED_UNIT: "volume",
 }
 
-STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, UnitConverter] = {
-    energy_util.NORMALIZED_UNIT: energy_util,
-    power_util.NORMALIZED_UNIT: power_util,
-    pressure_util.NORMALIZED_UNIT: pressure_util,
-    temperature_util.NORMALIZED_UNIT: temperature_util,
-    volume_util.NORMALIZED_UNIT: volume_util,
+STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {
+    energy_util.NORMALIZED_UNIT: EnergyConverter,
+    power_util.NORMALIZED_UNIT: PowerConverter,
+    pressure_util.NORMALIZED_UNIT: PressureConverter,
+    temperature_util.NORMALIZED_UNIT: TemperatureConverter,
+    volume_util.NORMALIZED_UNIT: VolumeConverter,
 }
 
 # Convert energy power, pressure, temperature and volume statistics from the
diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py
index 530769f2873..e15bd851952 100644
--- a/homeassistant/components/sensor/__init__.py
+++ b/homeassistant/components/sensor/__init__.py
@@ -56,11 +56,12 @@ 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, UnitConverter
-from homeassistant.util import (
-    dt as dt_util,
-    pressure as pressure_util,
-    temperature as temperature_util,
+from homeassistant.helpers.typing import ConfigType, StateType
+from homeassistant.util import dt as dt_util, pressure as pressure_util
+from homeassistant.util.unit_conversion import (
+    BaseUnitConverter,
+    PressureConverter,
+    TemperatureConverter,
 )
 
 from .const import CONF_STATE_CLASS  # noqa: F401
@@ -207,9 +208,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_CONVERTERS: dict[str, UnitConverter] = {
-    SensorDeviceClass.PRESSURE: pressure_util,
-    SensorDeviceClass.TEMPERATURE: temperature_util,
+UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = {
+    SensorDeviceClass.PRESSURE: PressureConverter,
+    SensorDeviceClass.TEMPERATURE: TemperatureConverter,
 }
 
 UNIT_RATIOS: dict[str, dict[str, float]] = {
diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py
index 9bd068aa469..c7ace30af6e 100644
--- a/homeassistant/components/sensor/recorder.py
+++ b/homeassistant/components/sensor/recorder.py
@@ -47,7 +47,6 @@ from homeassistant.const import (
 from homeassistant.core import HomeAssistant, State
 from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers.entity import entity_sources
-from homeassistant.helpers.typing import UnitConverter
 from homeassistant.util import (
     dt as dt_util,
     energy as energy_util,
@@ -56,6 +55,14 @@ from homeassistant.util import (
     temperature as temperature_util,
     volume as volume_util,
 )
+from homeassistant.util.unit_conversion import (
+    BaseUnitConverter,
+    EnergyConverter,
+    PowerConverter,
+    PressureConverter,
+    TemperatureConverter,
+    VolumeConverter,
+)
 
 from . import (
     ATTR_LAST_RESET,
@@ -76,12 +83,12 @@ DEFAULT_STATISTICS = {
     STATE_CLASS_TOTAL_INCREASING: {"sum"},
 }
 
-UNIT_CONVERTERS: dict[str, UnitConverter] = {
-    SensorDeviceClass.ENERGY: energy_util,
-    SensorDeviceClass.POWER: power_util,
-    SensorDeviceClass.PRESSURE: pressure_util,
-    SensorDeviceClass.TEMPERATURE: temperature_util,
-    SensorDeviceClass.GAS: volume_util,
+UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = {
+    SensorDeviceClass.ENERGY: EnergyConverter,
+    SensorDeviceClass.POWER: PowerConverter,
+    SensorDeviceClass.PRESSURE: PressureConverter,
+    SensorDeviceClass.TEMPERATURE: TemperatureConverter,
+    SensorDeviceClass.GAS: VolumeConverter,
 }
 
 UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = {
diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py
index a6b4862f03b..0e3edab71b0 100644
--- a/homeassistant/helpers/typing.py
+++ b/homeassistant/helpers/typing.py
@@ -1,7 +1,7 @@
 """Typing Helpers for Home Assistant."""
 from collections.abc import Mapping
 from enum import Enum
-from typing import Any, Optional, Protocol, Union
+from typing import Any, Optional, Union
 
 import homeassistant.core
 
@@ -27,16 +27,6 @@ class UndefinedType(Enum):
 UNDEFINED = UndefinedType._singleton  # pylint: disable=protected-access
 
 
-class UnitConverter(Protocol):
-    """Define the format of a conversion utility."""
-
-    VALID_UNITS: tuple[str, ...]
-    NORMALIZED_UNIT: 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
diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py
new file mode 100644
index 00000000000..bc0ec09794f
--- /dev/null
+++ b/homeassistant/util/unit_conversion.py
@@ -0,0 +1,60 @@
+"""Typing Helpers for Home Assistant."""
+from __future__ import annotations
+
+from collections.abc import Callable
+
+from . import (
+    energy as energy_util,
+    power as power_util,
+    pressure as pressure_util,
+    temperature as temperature_util,
+    volume as volume_util,
+)
+
+
+class BaseUnitConverter:
+    """Define the format of a conversion utility."""
+
+    NORMALIZED_UNIT: str
+    VALID_UNITS: tuple[str, ...]
+    convert: Callable[[float, str, str], float]
+
+
+class EnergyConverter(BaseUnitConverter):
+    """Utility to convert energy values."""
+
+    NORMALIZED_UNIT = energy_util.NORMALIZED_UNIT
+    VALID_UNITS = energy_util.VALID_UNITS
+    convert = energy_util.convert
+
+
+class PowerConverter(BaseUnitConverter):
+    """Utility to convert power values."""
+
+    NORMALIZED_UNIT = power_util.NORMALIZED_UNIT
+    VALID_UNITS = power_util.VALID_UNITS
+    convert = power_util.convert
+
+
+class PressureConverter(BaseUnitConverter):
+    """Utility to convert pressure values."""
+
+    NORMALIZED_UNIT = pressure_util.NORMALIZED_UNIT
+    VALID_UNITS = pressure_util.VALID_UNITS
+    convert = pressure_util.convert
+
+
+class TemperatureConverter(BaseUnitConverter):
+    """Utility to convert temperature values."""
+
+    NORMALIZED_UNIT = temperature_util.NORMALIZED_UNIT
+    VALID_UNITS = temperature_util.VALID_UNITS
+    convert = temperature_util.convert
+
+
+class VolumeConverter(BaseUnitConverter):
+    """Utility to convert volume values."""
+
+    NORMALIZED_UNIT = volume_util.NORMALIZED_UNIT
+    VALID_UNITS = volume_util.VALID_UNITS
+    convert = volume_util.convert