Energy distance units (#136933)

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
pull/122818/head^2
Jakob Schlyter 2025-01-31 15:22:25 +01:00 committed by GitHub
parent 21ffcf853b
commit 84ae476b67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 117 additions and 0 deletions

View File

@ -23,6 +23,7 @@ from homeassistant.const import (
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfEnergyDistance,
UnitOfFrequency,
UnitOfInformation,
UnitOfIrradiance,
@ -166,6 +167,15 @@ class NumberDeviceClass(StrEnum):
Unit of measurement: `J`, `kJ`, `MJ`, `GJ`, `mWh`, `Wh`, `kWh`, `MWh`, `GWh`, `TWh`, `cal`, `kcal`, `Mcal`, `Gcal`
"""
ENERGY_DISTANCE = "energy_distance"
"""Energy distance.
Use this device class for sensors measuring energy by distance, for example the amount
of electric energy consumed by an electric car.
Unit of measurement: `kWh/100km`, `mi/kWh`, `km/kWh`
"""
ENERGY_STORAGE = "energy_storage"
"""Stored energy.
@ -447,6 +457,7 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
UnitOfTime.MILLISECONDS,
},
NumberDeviceClass.ENERGY: set(UnitOfEnergy),
NumberDeviceClass.ENERGY_DISTANCE: set(UnitOfEnergyDistance),
NumberDeviceClass.ENERGY_STORAGE: set(UnitOfEnergy),
NumberDeviceClass.FREQUENCY: set(UnitOfFrequency),
NumberDeviceClass.GAS: {

View File

@ -38,6 +38,7 @@ from homeassistant.util.unit_conversion import (
ElectricCurrentConverter,
ElectricPotentialConverter,
EnergyConverter,
EnergyDistanceConverter,
InformationConverter,
MassConverter,
PowerConverter,
@ -147,6 +148,7 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {
for unit in ElectricPotentialConverter.VALID_UNITS
},
**{unit: EnergyConverter for unit in EnergyConverter.VALID_UNITS},
**{unit: EnergyDistanceConverter for unit in EnergyDistanceConverter.VALID_UNITS},
**{unit: InformationConverter for unit in InformationConverter.VALID_UNITS},
**{unit: MassConverter for unit in MassConverter.VALID_UNITS},
**{unit: PowerConverter for unit in PowerConverter.VALID_UNITS},

View File

@ -25,6 +25,7 @@ from homeassistant.util.unit_conversion import (
ElectricCurrentConverter,
ElectricPotentialConverter,
EnergyConverter,
EnergyDistanceConverter,
InformationConverter,
MassConverter,
PowerConverter,
@ -67,6 +68,7 @@ UNIT_SCHEMA = vol.Schema(
vol.Optional("electric_current"): vol.In(ElectricCurrentConverter.VALID_UNITS),
vol.Optional("voltage"): vol.In(ElectricPotentialConverter.VALID_UNITS),
vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS),
vol.Optional("energy_distance"): vol.In(EnergyDistanceConverter.VALID_UNITS),
vol.Optional("information"): vol.In(InformationConverter.VALID_UNITS),
vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS),
vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS),

View File

@ -23,6 +23,7 @@ from homeassistant.const import (
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfEnergyDistance,
UnitOfFrequency,
UnitOfInformation,
UnitOfIrradiance,
@ -51,6 +52,7 @@ from homeassistant.util.unit_conversion import (
ElectricCurrentConverter,
ElectricPotentialConverter,
EnergyConverter,
EnergyDistanceConverter,
InformationConverter,
MassConverter,
PowerConverter,
@ -194,6 +196,15 @@ class SensorDeviceClass(StrEnum):
Unit of measurement: `J`, `kJ`, `MJ`, `GJ`, `mWh`, `Wh`, `kWh`, `MWh`, `GWh`, `TWh`, `cal`, `kcal`, `Mcal`, `Gcal`
"""
ENERGY_DISTANCE = "energy_distance"
"""Energy distance.
Use this device class for sensors measuring energy by distance, for example the amount
of electric energy consumed by an electric car.
Unit of measurement: `kWh/100km`, `mi/kWh`, `km/kWh`
"""
ENERGY_STORAGE = "energy_storage"
"""Stored energy.
@ -500,6 +511,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] =
SensorDeviceClass.DISTANCE: DistanceConverter,
SensorDeviceClass.DURATION: DurationConverter,
SensorDeviceClass.ENERGY: EnergyConverter,
SensorDeviceClass.ENERGY_DISTANCE: EnergyDistanceConverter,
SensorDeviceClass.ENERGY_STORAGE: EnergyConverter,
SensorDeviceClass.GAS: VolumeConverter,
SensorDeviceClass.POWER: PowerConverter,
@ -541,6 +553,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
UnitOfTime.MILLISECONDS,
},
SensorDeviceClass.ENERGY: set(UnitOfEnergy),
SensorDeviceClass.ENERGY_DISTANCE: set(UnitOfEnergyDistance),
SensorDeviceClass.ENERGY_STORAGE: set(UnitOfEnergy),
SensorDeviceClass.FREQUENCY: set(UnitOfFrequency),
SensorDeviceClass.GAS: {
@ -622,6 +635,7 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = {
SensorStateClass.TOTAL,
SensorStateClass.TOTAL_INCREASING,
},
SensorDeviceClass.ENERGY_DISTANCE: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.ENERGY_STORAGE: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.ENUM: set(),
SensorDeviceClass.FREQUENCY: {SensorStateClass.MEASUREMENT},

View File

@ -48,6 +48,7 @@ CONF_IS_DATA_SIZE = "is_data_size"
CONF_IS_DISTANCE = "is_distance"
CONF_IS_DURATION = "is_duration"
CONF_IS_ENERGY = "is_energy"
CONF_IS_ENERGY_DISTANCE = "is_energy_distance"
CONF_IS_FREQUENCY = "is_frequency"
CONF_IS_HUMIDITY = "is_humidity"
CONF_IS_GAS = "is_gas"
@ -102,6 +103,7 @@ ENTITY_CONDITIONS = {
SensorDeviceClass.DISTANCE: [{CONF_TYPE: CONF_IS_DISTANCE}],
SensorDeviceClass.DURATION: [{CONF_TYPE: CONF_IS_DURATION}],
SensorDeviceClass.ENERGY: [{CONF_TYPE: CONF_IS_ENERGY}],
SensorDeviceClass.ENERGY_DISTANCE: [{CONF_TYPE: CONF_IS_ENERGY_DISTANCE}],
SensorDeviceClass.ENERGY_STORAGE: [{CONF_TYPE: CONF_IS_ENERGY}],
SensorDeviceClass.FREQUENCY: [{CONF_TYPE: CONF_IS_FREQUENCY}],
SensorDeviceClass.GAS: [{CONF_TYPE: CONF_IS_GAS}],
@ -168,6 +170,7 @@ CONDITION_SCHEMA = vol.All(
CONF_IS_DISTANCE,
CONF_IS_DURATION,
CONF_IS_ENERGY,
CONF_IS_ENERGY_DISTANCE,
CONF_IS_FREQUENCY,
CONF_IS_GAS,
CONF_IS_HUMIDITY,

View File

@ -47,6 +47,7 @@ CONF_DATA_SIZE = "data_size"
CONF_DISTANCE = "distance"
CONF_DURATION = "duration"
CONF_ENERGY = "energy"
CONF_ENERGY_DISTANCE = "energy_distance"
CONF_FREQUENCY = "frequency"
CONF_GAS = "gas"
CONF_HUMIDITY = "humidity"
@ -101,6 +102,7 @@ ENTITY_TRIGGERS = {
SensorDeviceClass.DISTANCE: [{CONF_TYPE: CONF_DISTANCE}],
SensorDeviceClass.DURATION: [{CONF_TYPE: CONF_DURATION}],
SensorDeviceClass.ENERGY: [{CONF_TYPE: CONF_ENERGY}],
SensorDeviceClass.ENERGY_DISTANCE: [{CONF_TYPE: CONF_ENERGY_DISTANCE}],
SensorDeviceClass.ENERGY_STORAGE: [{CONF_TYPE: CONF_ENERGY}],
SensorDeviceClass.FREQUENCY: [{CONF_TYPE: CONF_FREQUENCY}],
SensorDeviceClass.GAS: [{CONF_TYPE: CONF_GAS}],
@ -168,6 +170,7 @@ TRIGGER_SCHEMA = vol.All(
CONF_DISTANCE,
CONF_DURATION,
CONF_ENERGY,
CONF_ENERGY_DISTANCE,
CONF_FREQUENCY,
CONF_GAS,
CONF_HUMIDITY,

View File

@ -17,6 +17,7 @@
"is_distance": "Current {entity_name} distance",
"is_duration": "Current {entity_name} duration",
"is_energy": "Current {entity_name} energy",
"is_energy_distance": "Current {entity_name} energy per distance",
"is_frequency": "Current {entity_name} frequency",
"is_gas": "Current {entity_name} gas",
"is_humidity": "Current {entity_name} humidity",
@ -69,6 +70,7 @@
"distance": "{entity_name} distance changes",
"duration": "{entity_name} duration changes",
"energy": "{entity_name} energy changes",
"energy_distance": "{entity_name} energy per distance changes",
"frequency": "{entity_name} frequency changes",
"gas": "{entity_name} gas changes",
"humidity": "{entity_name} humidity changes",
@ -183,6 +185,9 @@
"energy": {
"name": "Energy"
},
"energy_distance": {
"name": "Energy per distance"
},
"energy_storage": {
"name": "Stored energy"
},

View File

@ -632,6 +632,15 @@ class UnitOfEnergy(StrEnum):
GIGA_CALORIE = "Gcal"
# Energy Distance units
class UnitOfEnergyDistance(StrEnum):
"""Energy Distance units."""
KILO_WATT_HOUR_PER_100_KM = "kWh/100km"
MILES_PER_KILO_WATT_HOUR = "mi/kWh"
KM_PER_KILO_WATT_HOUR = "km/kWh"
# Electric_current units
class UnitOfElectricCurrent(StrEnum):
"""Electric current units."""

View File

@ -17,6 +17,7 @@ from homeassistant.const import (
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfEnergyDistance,
UnitOfInformation,
UnitOfLength,
UnitOfMass,
@ -90,6 +91,7 @@ class BaseUnitConverter:
VALID_UNITS: set[str | None]
_UNIT_CONVERSION: dict[str | None, float]
_UNIT_INVERSES: set[str] = set()
@classmethod
def convert(cls, value: float, from_unit: str | None, to_unit: str | None) -> float:
@ -105,6 +107,8 @@ class BaseUnitConverter:
if from_unit == to_unit:
return lambda value: value
from_ratio, to_ratio = cls._get_from_to_ratio(from_unit, to_unit)
if cls._are_unit_inverses(from_unit, to_unit):
return lambda val: to_ratio / (val / from_ratio)
return lambda val: (val / from_ratio) * to_ratio
@classmethod
@ -129,6 +133,8 @@ class BaseUnitConverter:
if from_unit == to_unit:
return lambda value: value
from_ratio, to_ratio = cls._get_from_to_ratio(from_unit, to_unit)
if cls._are_unit_inverses(from_unit, to_unit):
return lambda val: None if val is None else to_ratio / (val / from_ratio)
return lambda val: None if val is None else (val / from_ratio) * to_ratio
@classmethod
@ -138,6 +144,12 @@ class BaseUnitConverter:
from_ratio, to_ratio = cls._get_from_to_ratio(from_unit, to_unit)
return from_ratio / to_ratio
@classmethod
@lru_cache
def _are_unit_inverses(cls, from_unit: str | None, to_unit: str | None) -> bool:
"""Return true if one unit is an inverse but not the other."""
return (from_unit in cls._UNIT_INVERSES) != (to_unit in cls._UNIT_INVERSES)
class DataRateConverter(BaseUnitConverter):
"""Utility to convert data rate values."""
@ -284,6 +296,22 @@ class EnergyConverter(BaseUnitConverter):
VALID_UNITS = set(UnitOfEnergy)
class EnergyDistanceConverter(BaseUnitConverter):
"""Utility to convert vehicle energy consumption values."""
UNIT_CLASS = "energy_distance"
_UNIT_CONVERSION: dict[str | None, float] = {
UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM: 1,
UnitOfEnergyDistance.MILES_PER_KILO_WATT_HOUR: 100 * _KM_TO_M / _MILE_TO_M,
UnitOfEnergyDistance.KM_PER_KILO_WATT_HOUR: 100,
}
_UNIT_INVERSES: set[str] = {
UnitOfEnergyDistance.MILES_PER_KILO_WATT_HOUR,
UnitOfEnergyDistance.KM_PER_KILO_WATT_HOUR,
}
VALID_UNITS = set(UnitOfEnergyDistance)
class InformationConverter(BaseUnitConverter):
"""Utility to convert information values."""

View File

@ -18,6 +18,7 @@ from homeassistant.const import (
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfEnergyDistance,
UnitOfInformation,
UnitOfLength,
UnitOfMass,
@ -43,6 +44,7 @@ from homeassistant.util.unit_conversion import (
ElectricCurrentConverter,
ElectricPotentialConverter,
EnergyConverter,
EnergyDistanceConverter,
InformationConverter,
MassConverter,
PowerConverter,
@ -79,6 +81,7 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = {
SpeedConverter,
TemperatureConverter,
UnitlessRatioConverter,
EnergyDistanceConverter,
VolumeConverter,
VolumeFlowRateConverter,
)
@ -115,6 +118,11 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo
1000,
),
EnergyConverter: (UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000),
EnergyDistanceConverter: (
UnitOfEnergyDistance.MILES_PER_KILO_WATT_HOUR,
UnitOfEnergyDistance.KM_PER_KILO_WATT_HOUR,
0.621371,
),
InformationConverter: (UnitOfInformation.BITS, UnitOfInformation.BYTES, 8),
MassConverter: (UnitOfMass.STONES, UnitOfMass.KILOGRAMS, 0.157473),
PowerConverter: (UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000),
@ -486,6 +494,38 @@ _CONVERTED_VALUE: dict[
(10, UnitOfEnergy.GIGA_CALORIE, 10000, UnitOfEnergy.MEGA_CALORIE),
(10, UnitOfEnergy.GIGA_CALORIE, 11.622222, UnitOfEnergy.MEGA_WATT_HOUR),
],
EnergyDistanceConverter: [
(
10,
UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM,
6.213712,
UnitOfEnergyDistance.MILES_PER_KILO_WATT_HOUR,
),
(
25,
UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM,
4,
UnitOfEnergyDistance.KM_PER_KILO_WATT_HOUR,
),
(
20,
UnitOfEnergyDistance.MILES_PER_KILO_WATT_HOUR,
3.106856,
UnitOfEnergyDistance.KILO_WATT_HOUR_PER_100_KM,
),
(
10,
UnitOfEnergyDistance.MILES_PER_KILO_WATT_HOUR,
16.09344,
UnitOfEnergyDistance.KM_PER_KILO_WATT_HOUR,
),
(
16.09344,
UnitOfEnergyDistance.KM_PER_KILO_WATT_HOUR,
10,
UnitOfEnergyDistance.MILES_PER_KILO_WATT_HOUR,
),
],
InformationConverter: [
(8e3, UnitOfInformation.BITS, 8, UnitOfInformation.KILOBITS),
(8e6, UnitOfInformation.BITS, 8, UnitOfInformation.MEGABITS),