mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 02:07:09 +00:00
Add unitless unit converter (#85694)
* Add unitless unit converter * Adjust type hints * Adjust tests * Rename to UnitlessRatioConverter
This commit is contained in:
parent
a176de6d4b
commit
b0d4b73874
@ -43,6 +43,7 @@ from homeassistant.util.unit_conversion import (
|
|||||||
PressureConverter,
|
PressureConverter,
|
||||||
SpeedConverter,
|
SpeedConverter,
|
||||||
TemperatureConverter,
|
TemperatureConverter,
|
||||||
|
UnitlessRatioConverter,
|
||||||
VolumeConverter,
|
VolumeConverter,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -134,6 +135,7 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {
|
|||||||
**{unit: PressureConverter for unit in PressureConverter.VALID_UNITS},
|
**{unit: PressureConverter for unit in PressureConverter.VALID_UNITS},
|
||||||
**{unit: SpeedConverter for unit in SpeedConverter.VALID_UNITS},
|
**{unit: SpeedConverter for unit in SpeedConverter.VALID_UNITS},
|
||||||
**{unit: TemperatureConverter for unit in TemperatureConverter.VALID_UNITS},
|
**{unit: TemperatureConverter for unit in TemperatureConverter.VALID_UNITS},
|
||||||
|
**{unit: UnitlessRatioConverter for unit in UnitlessRatioConverter.VALID_UNITS},
|
||||||
**{unit: VolumeConverter for unit in VolumeConverter.VALID_UNITS},
|
**{unit: VolumeConverter for unit in VolumeConverter.VALID_UNITS},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,9 +157,6 @@ def get_display_unit(
|
|||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Return the unit which the statistic will be displayed in."""
|
"""Return the unit which the statistic will be displayed in."""
|
||||||
|
|
||||||
if statistic_unit is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None:
|
if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None:
|
||||||
return statistic_unit
|
return statistic_unit
|
||||||
|
|
||||||
@ -183,9 +182,6 @@ def _get_statistic_to_display_unit_converter(
|
|||||||
"""Return val."""
|
"""Return val."""
|
||||||
return val
|
return val
|
||||||
|
|
||||||
if statistic_unit is None:
|
|
||||||
return no_conversion
|
|
||||||
|
|
||||||
if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None:
|
if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None:
|
||||||
return no_conversion
|
return no_conversion
|
||||||
|
|
||||||
@ -226,9 +222,6 @@ def _get_display_to_statistic_unit_converter(
|
|||||||
"""Return val."""
|
"""Return val."""
|
||||||
return val
|
return val
|
||||||
|
|
||||||
if statistic_unit is None:
|
|
||||||
return no_conversion
|
|
||||||
|
|
||||||
if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None:
|
if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None:
|
||||||
return no_conversion
|
return no_conversion
|
||||||
|
|
||||||
@ -1555,17 +1548,10 @@ def statistic_during_period(
|
|||||||
else:
|
else:
|
||||||
result["change"] = None
|
result["change"] = None
|
||||||
|
|
||||||
def no_conversion(val: float | None) -> float | None:
|
|
||||||
"""Return val."""
|
|
||||||
return val
|
|
||||||
|
|
||||||
state_unit = unit = metadata[statistic_id][1]["unit_of_measurement"]
|
state_unit = unit = metadata[statistic_id][1]["unit_of_measurement"]
|
||||||
if state := hass.states.get(statistic_id):
|
if state := hass.states.get(statistic_id):
|
||||||
state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
if unit is not None:
|
convert = _get_statistic_to_display_unit_converter(unit, state_unit, units)
|
||||||
convert = _get_statistic_to_display_unit_converter(unit, state_unit, units)
|
|
||||||
else:
|
|
||||||
convert = no_conversion
|
|
||||||
|
|
||||||
return {key: convert(value) for key, value in result.items()}
|
return {key: convert(value) for key, value in result.items()}
|
||||||
|
|
||||||
@ -1916,7 +1902,7 @@ def _sorted_statistics_to_dict(
|
|||||||
statistic_id = metadata[meta_id]["statistic_id"]
|
statistic_id = metadata[meta_id]["statistic_id"]
|
||||||
if state := hass.states.get(statistic_id):
|
if state := hass.states.get(statistic_id):
|
||||||
state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||||
if unit is not None and convert_units:
|
if convert_units:
|
||||||
convert = _get_statistic_to_display_unit_converter(unit, state_unit, units)
|
convert = _get_statistic_to_display_unit_converter(unit, state_unit, units)
|
||||||
else:
|
else:
|
||||||
convert = no_conversion
|
convert = no_conversion
|
||||||
|
@ -56,7 +56,7 @@ from homeassistant.helpers.entity import Entity, EntityDescription
|
|||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||||
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
|
from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity
|
||||||
from homeassistant.helpers.typing import ConfigType, StateType
|
from homeassistant.helpers.typing import UNDEFINED, ConfigType, StateType, UndefinedType
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import ( # noqa: F401
|
from .const import ( # noqa: F401
|
||||||
@ -155,7 +155,7 @@ class SensorEntity(Entity):
|
|||||||
)
|
)
|
||||||
_invalid_unit_of_measurement_reported = False
|
_invalid_unit_of_measurement_reported = False
|
||||||
_last_reset_reported = False
|
_last_reset_reported = False
|
||||||
_sensor_option_unit_of_measurement: str | None = None
|
_sensor_option_unit_of_measurement: str | None | UndefinedType = UNDEFINED
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def add_to_platform_start(
|
def add_to_platform_start(
|
||||||
@ -371,7 +371,7 @@ class SensorEntity(Entity):
|
|||||||
"""Return the unit of measurement of the entity, after unit conversion."""
|
"""Return the unit of measurement of the entity, after unit conversion."""
|
||||||
# Highest priority, for registered entities: unit set by user,with fallback to
|
# Highest priority, for registered entities: unit set by user,with fallback to
|
||||||
# unit suggested by integration or secondary fallback to unit conversion rules
|
# unit suggested by integration or secondary fallback to unit conversion rules
|
||||||
if self._sensor_option_unit_of_measurement:
|
if self._sensor_option_unit_of_measurement is not UNDEFINED:
|
||||||
return self._sensor_option_unit_of_measurement
|
return self._sensor_option_unit_of_measurement
|
||||||
|
|
||||||
# Second priority, for non registered entities: unit suggested by integration
|
# Second priority, for non registered entities: unit suggested by integration
|
||||||
@ -481,8 +481,6 @@ class SensorEntity(Entity):
|
|||||||
native_unit_of_measurement != unit_of_measurement
|
native_unit_of_measurement != unit_of_measurement
|
||||||
and device_class in UNIT_CONVERTERS
|
and device_class in UNIT_CONVERTERS
|
||||||
):
|
):
|
||||||
assert unit_of_measurement
|
|
||||||
assert native_unit_of_measurement
|
|
||||||
converter = UNIT_CONVERTERS[device_class]
|
converter = UNIT_CONVERTERS[device_class]
|
||||||
|
|
||||||
value_s = str(value)
|
value_s = str(value)
|
||||||
@ -550,28 +548,31 @@ class SensorEntity(Entity):
|
|||||||
|
|
||||||
return super().__repr__()
|
return super().__repr__()
|
||||||
|
|
||||||
def _custom_unit_or_none(self, primary_key: str, secondary_key: str) -> str | None:
|
def _custom_unit_or_undef(
|
||||||
"""Return a custom unit, or None if it's not compatible with the native unit."""
|
self, primary_key: str, secondary_key: str
|
||||||
|
) -> str | None | UndefinedType:
|
||||||
|
"""Return a custom unit, or UNDEFINED if not compatible with the native unit."""
|
||||||
assert self.registry_entry
|
assert self.registry_entry
|
||||||
if (
|
if (
|
||||||
(sensor_options := self.registry_entry.options.get(primary_key))
|
(sensor_options := self.registry_entry.options.get(primary_key))
|
||||||
and (custom_unit := sensor_options.get(secondary_key))
|
and secondary_key in sensor_options
|
||||||
and (device_class := self.device_class) in UNIT_CONVERTERS
|
and (device_class := self.device_class) in UNIT_CONVERTERS
|
||||||
and self.native_unit_of_measurement
|
and self.native_unit_of_measurement
|
||||||
in UNIT_CONVERTERS[device_class].VALID_UNITS
|
in UNIT_CONVERTERS[device_class].VALID_UNITS
|
||||||
and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS
|
and (custom_unit := sensor_options[secondary_key])
|
||||||
|
in UNIT_CONVERTERS[device_class].VALID_UNITS
|
||||||
):
|
):
|
||||||
return cast(str, custom_unit)
|
return cast(str, custom_unit)
|
||||||
return None
|
return UNDEFINED
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_registry_entry_updated(self) -> None:
|
def async_registry_entry_updated(self) -> None:
|
||||||
"""Run when the entity registry entry has been updated."""
|
"""Run when the entity registry entry has been updated."""
|
||||||
self._sensor_option_unit_of_measurement = self._custom_unit_or_none(
|
self._sensor_option_unit_of_measurement = self._custom_unit_or_undef(
|
||||||
DOMAIN, CONF_UNIT_OF_MEASUREMENT
|
DOMAIN, CONF_UNIT_OF_MEASUREMENT
|
||||||
)
|
)
|
||||||
if not self._sensor_option_unit_of_measurement:
|
if self._sensor_option_unit_of_measurement is UNDEFINED:
|
||||||
self._sensor_option_unit_of_measurement = self._custom_unit_or_none(
|
self._sensor_option_unit_of_measurement = self._custom_unit_or_undef(
|
||||||
f"{DOMAIN}.private", "suggested_unit_of_measurement"
|
f"{DOMAIN}.private", "suggested_unit_of_measurement"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@ from homeassistant.util.unit_conversion import (
|
|||||||
PressureConverter,
|
PressureConverter,
|
||||||
SpeedConverter,
|
SpeedConverter,
|
||||||
TemperatureConverter,
|
TemperatureConverter,
|
||||||
|
UnitlessRatioConverter,
|
||||||
VolumeConverter,
|
VolumeConverter,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -421,6 +422,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] =
|
|||||||
SensorDeviceClass.CURRENT: ElectricCurrentConverter,
|
SensorDeviceClass.CURRENT: ElectricCurrentConverter,
|
||||||
SensorDeviceClass.ENERGY: EnergyConverter,
|
SensorDeviceClass.ENERGY: EnergyConverter,
|
||||||
SensorDeviceClass.GAS: VolumeConverter,
|
SensorDeviceClass.GAS: VolumeConverter,
|
||||||
|
SensorDeviceClass.POWER_FACTOR: UnitlessRatioConverter,
|
||||||
SensorDeviceClass.PRECIPITATION: DistanceConverter,
|
SensorDeviceClass.PRECIPITATION: DistanceConverter,
|
||||||
SensorDeviceClass.PRESSURE: PressureConverter,
|
SensorDeviceClass.PRESSURE: PressureConverter,
|
||||||
SensorDeviceClass.SPEED: SpeedConverter,
|
SensorDeviceClass.SPEED: SpeedConverter,
|
||||||
|
@ -183,10 +183,7 @@ def _normalize_states(
|
|||||||
# We have seen this sensor before, use the unit from metadata
|
# We have seen this sensor before, use the unit from metadata
|
||||||
statistics_unit = old_metadata["unit_of_measurement"]
|
statistics_unit = old_metadata["unit_of_measurement"]
|
||||||
|
|
||||||
if (
|
if statistics_unit not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER:
|
||||||
not statistics_unit
|
|
||||||
or statistics_unit not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER
|
|
||||||
):
|
|
||||||
# The unit used by this sensor doesn't support unit conversion
|
# The unit used by this sensor doesn't support unit conversion
|
||||||
|
|
||||||
all_units = _get_units(fstates)
|
all_units = _get_units(fstates)
|
||||||
@ -721,7 +718,8 @@ def validate_statistics(
|
|||||||
)
|
)
|
||||||
elif state_unit not in converter.VALID_UNITS:
|
elif state_unit not in converter.VALID_UNITS:
|
||||||
# The state unit can't be converted to the unit in metadata
|
# The state unit can't be converted to the unit in metadata
|
||||||
valid_units = ", ".join(sorted(converter.VALID_UNITS))
|
valid_units = (unit or "<None>" for unit in converter.VALID_UNITS)
|
||||||
|
valid_units_str = ", ".join(sorted(valid_units))
|
||||||
validation_result[entity_id].append(
|
validation_result[entity_id].append(
|
||||||
statistics.ValidationIssue(
|
statistics.ValidationIssue(
|
||||||
"units_changed",
|
"units_changed",
|
||||||
@ -729,7 +727,7 @@ def validate_statistics(
|
|||||||
"statistic_id": entity_id,
|
"statistic_id": entity_id,
|
||||||
"state_unit": state_unit,
|
"state_unit": state_unit,
|
||||||
"metadata_unit": metadata_unit,
|
"metadata_unit": metadata_unit,
|
||||||
"supported_unit": valid_units,
|
"supported_unit": valid_units_str,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
import subarulink.const as sc
|
import subarulink.const as sc
|
||||||
|
|
||||||
@ -207,11 +207,11 @@ class SubaruSensor(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if unit in LENGTH_UNITS:
|
if unit in LENGTH_UNITS:
|
||||||
return round(unit_system.length(current_value, unit), 1)
|
return round(unit_system.length(current_value, cast(str, unit)), 1)
|
||||||
|
|
||||||
if unit in PRESSURE_UNITS and unit_system == US_CUSTOMARY_SYSTEM:
|
if unit in PRESSURE_UNITS and unit_system == US_CUSTOMARY_SYSTEM:
|
||||||
return round(
|
return round(
|
||||||
unit_system.pressure(current_value, unit),
|
unit_system.pressure(current_value, cast(str, unit)),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ from homeassistant.helpers.frame import report
|
|||||||
from .unit_conversion import PressureConverter
|
from .unit_conversion import PressureConverter
|
||||||
|
|
||||||
# pylint: disable-next=protected-access
|
# pylint: disable-next=protected-access
|
||||||
UNIT_CONVERSION: dict[str, float] = PressureConverter._UNIT_CONVERSION
|
UNIT_CONVERSION: dict[str | None, float] = PressureConverter._UNIT_CONVERSION
|
||||||
VALID_UNITS = PressureConverter.VALID_UNITS
|
VALID_UNITS = PressureConverter.VALID_UNITS
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ from .unit_conversion import ( # pylint: disable=unused-import # noqa: F401
|
|||||||
)
|
)
|
||||||
|
|
||||||
# pylint: disable-next=protected-access
|
# pylint: disable-next=protected-access
|
||||||
UNIT_CONVERSION: dict[str, float] = SpeedConverter._UNIT_CONVERSION
|
UNIT_CONVERSION: dict[str | None, float] = SpeedConverter._UNIT_CONVERSION
|
||||||
VALID_UNITS = SpeedConverter.VALID_UNITS
|
VALID_UNITS = SpeedConverter.VALID_UNITS
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
PERCENTAGE,
|
||||||
UNIT_NOT_RECOGNIZED_TEMPLATE,
|
UNIT_NOT_RECOGNIZED_TEMPLATE,
|
||||||
UnitOfDataRate,
|
UnitOfDataRate,
|
||||||
UnitOfElectricCurrent,
|
UnitOfElectricCurrent,
|
||||||
@ -56,13 +57,13 @@ class BaseUnitConverter:
|
|||||||
"""Define the format of a conversion utility."""
|
"""Define the format of a conversion utility."""
|
||||||
|
|
||||||
UNIT_CLASS: str
|
UNIT_CLASS: str
|
||||||
NORMALIZED_UNIT: str
|
NORMALIZED_UNIT: str | None
|
||||||
VALID_UNITS: set[str]
|
VALID_UNITS: set[str | None]
|
||||||
|
|
||||||
_UNIT_CONVERSION: dict[str, float]
|
_UNIT_CONVERSION: dict[str | None, float]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def convert(cls, value: float, from_unit: str, to_unit: str) -> float:
|
def convert(cls, value: float, from_unit: str | None, to_unit: str | None) -> float:
|
||||||
"""Convert one unit of measurement to another."""
|
"""Convert one unit of measurement to another."""
|
||||||
if from_unit == to_unit:
|
if from_unit == to_unit:
|
||||||
return value
|
return value
|
||||||
@ -85,7 +86,7 @@ class BaseUnitConverter:
|
|||||||
return new_value * to_ratio
|
return new_value * to_ratio
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_unit_ratio(cls, from_unit: str, to_unit: str) -> float:
|
def get_unit_ratio(cls, from_unit: str | None, to_unit: str | None) -> float:
|
||||||
"""Get unit ratio between units of measurement."""
|
"""Get unit ratio between units of measurement."""
|
||||||
return cls._UNIT_CONVERSION[from_unit] / cls._UNIT_CONVERSION[to_unit]
|
return cls._UNIT_CONVERSION[from_unit] / cls._UNIT_CONVERSION[to_unit]
|
||||||
|
|
||||||
@ -96,7 +97,7 @@ class DataRateConverter(BaseUnitConverter):
|
|||||||
UNIT_CLASS = "data_rate"
|
UNIT_CLASS = "data_rate"
|
||||||
NORMALIZED_UNIT = UnitOfDataRate.BITS_PER_SECOND
|
NORMALIZED_UNIT = UnitOfDataRate.BITS_PER_SECOND
|
||||||
# Units in terms of bits
|
# Units in terms of bits
|
||||||
_UNIT_CONVERSION: dict[str, float] = {
|
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||||
UnitOfDataRate.BITS_PER_SECOND: 1,
|
UnitOfDataRate.BITS_PER_SECOND: 1,
|
||||||
UnitOfDataRate.KILOBITS_PER_SECOND: 1 / 1e3,
|
UnitOfDataRate.KILOBITS_PER_SECOND: 1 / 1e3,
|
||||||
UnitOfDataRate.MEGABITS_PER_SECOND: 1 / 1e6,
|
UnitOfDataRate.MEGABITS_PER_SECOND: 1 / 1e6,
|
||||||
@ -117,7 +118,7 @@ class DistanceConverter(BaseUnitConverter):
|
|||||||
|
|
||||||
UNIT_CLASS = "distance"
|
UNIT_CLASS = "distance"
|
||||||
NORMALIZED_UNIT = UnitOfLength.METERS
|
NORMALIZED_UNIT = UnitOfLength.METERS
|
||||||
_UNIT_CONVERSION: dict[str, float] = {
|
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||||
UnitOfLength.METERS: 1,
|
UnitOfLength.METERS: 1,
|
||||||
UnitOfLength.MILLIMETERS: 1 / _MM_TO_M,
|
UnitOfLength.MILLIMETERS: 1 / _MM_TO_M,
|
||||||
UnitOfLength.CENTIMETERS: 1 / _CM_TO_M,
|
UnitOfLength.CENTIMETERS: 1 / _CM_TO_M,
|
||||||
@ -144,7 +145,7 @@ class ElectricCurrentConverter(BaseUnitConverter):
|
|||||||
|
|
||||||
UNIT_CLASS = "electric_current"
|
UNIT_CLASS = "electric_current"
|
||||||
NORMALIZED_UNIT = UnitOfElectricCurrent.AMPERE
|
NORMALIZED_UNIT = UnitOfElectricCurrent.AMPERE
|
||||||
_UNIT_CONVERSION: dict[str, float] = {
|
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||||
UnitOfElectricCurrent.AMPERE: 1,
|
UnitOfElectricCurrent.AMPERE: 1,
|
||||||
UnitOfElectricCurrent.MILLIAMPERE: 1e3,
|
UnitOfElectricCurrent.MILLIAMPERE: 1e3,
|
||||||
}
|
}
|
||||||
@ -156,7 +157,7 @@ class ElectricPotentialConverter(BaseUnitConverter):
|
|||||||
|
|
||||||
UNIT_CLASS = "voltage"
|
UNIT_CLASS = "voltage"
|
||||||
NORMALIZED_UNIT = UnitOfElectricPotential.VOLT
|
NORMALIZED_UNIT = UnitOfElectricPotential.VOLT
|
||||||
_UNIT_CONVERSION: dict[str, float] = {
|
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||||
UnitOfElectricPotential.VOLT: 1,
|
UnitOfElectricPotential.VOLT: 1,
|
||||||
UnitOfElectricPotential.MILLIVOLT: 1e3,
|
UnitOfElectricPotential.MILLIVOLT: 1e3,
|
||||||
}
|
}
|
||||||
@ -171,7 +172,7 @@ class EnergyConverter(BaseUnitConverter):
|
|||||||
|
|
||||||
UNIT_CLASS = "energy"
|
UNIT_CLASS = "energy"
|
||||||
NORMALIZED_UNIT = UnitOfEnergy.KILO_WATT_HOUR
|
NORMALIZED_UNIT = UnitOfEnergy.KILO_WATT_HOUR
|
||||||
_UNIT_CONVERSION: dict[str, float] = {
|
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||||
UnitOfEnergy.WATT_HOUR: 1 * 1000,
|
UnitOfEnergy.WATT_HOUR: 1 * 1000,
|
||||||
UnitOfEnergy.KILO_WATT_HOUR: 1,
|
UnitOfEnergy.KILO_WATT_HOUR: 1,
|
||||||
UnitOfEnergy.MEGA_WATT_HOUR: 1 / 1000,
|
UnitOfEnergy.MEGA_WATT_HOUR: 1 / 1000,
|
||||||
@ -191,7 +192,7 @@ class InformationConverter(BaseUnitConverter):
|
|||||||
UNIT_CLASS = "information"
|
UNIT_CLASS = "information"
|
||||||
NORMALIZED_UNIT = UnitOfInformation.BITS
|
NORMALIZED_UNIT = UnitOfInformation.BITS
|
||||||
# Units in terms of bits
|
# Units in terms of bits
|
||||||
_UNIT_CONVERSION: dict[str, float] = {
|
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||||
UnitOfInformation.BITS: 1,
|
UnitOfInformation.BITS: 1,
|
||||||
UnitOfInformation.KILOBITS: 1 / 1e3,
|
UnitOfInformation.KILOBITS: 1 / 1e3,
|
||||||
UnitOfInformation.MEGABITS: 1 / 1e6,
|
UnitOfInformation.MEGABITS: 1 / 1e6,
|
||||||
@ -222,7 +223,7 @@ class MassConverter(BaseUnitConverter):
|
|||||||
|
|
||||||
UNIT_CLASS = "mass"
|
UNIT_CLASS = "mass"
|
||||||
NORMALIZED_UNIT = UnitOfMass.GRAMS
|
NORMALIZED_UNIT = UnitOfMass.GRAMS
|
||||||
_UNIT_CONVERSION: dict[str, float] = {
|
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||||
UnitOfMass.MICROGRAMS: 1 * 1000 * 1000,
|
UnitOfMass.MICROGRAMS: 1 * 1000 * 1000,
|
||||||
UnitOfMass.MILLIGRAMS: 1 * 1000,
|
UnitOfMass.MILLIGRAMS: 1 * 1000,
|
||||||
UnitOfMass.GRAMS: 1,
|
UnitOfMass.GRAMS: 1,
|
||||||
@ -247,7 +248,7 @@ class PowerConverter(BaseUnitConverter):
|
|||||||
|
|
||||||
UNIT_CLASS = "power"
|
UNIT_CLASS = "power"
|
||||||
NORMALIZED_UNIT = UnitOfPower.WATT
|
NORMALIZED_UNIT = UnitOfPower.WATT
|
||||||
_UNIT_CONVERSION: dict[str, float] = {
|
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||||
UnitOfPower.WATT: 1,
|
UnitOfPower.WATT: 1,
|
||||||
UnitOfPower.KILO_WATT: 1 / 1000,
|
UnitOfPower.KILO_WATT: 1 / 1000,
|
||||||
}
|
}
|
||||||
@ -262,7 +263,7 @@ class PressureConverter(BaseUnitConverter):
|
|||||||
|
|
||||||
UNIT_CLASS = "pressure"
|
UNIT_CLASS = "pressure"
|
||||||
NORMALIZED_UNIT = UnitOfPressure.PA
|
NORMALIZED_UNIT = UnitOfPressure.PA
|
||||||
_UNIT_CONVERSION: dict[str, float] = {
|
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||||
UnitOfPressure.PA: 1,
|
UnitOfPressure.PA: 1,
|
||||||
UnitOfPressure.HPA: 1 / 100,
|
UnitOfPressure.HPA: 1 / 100,
|
||||||
UnitOfPressure.KPA: 1 / 1000,
|
UnitOfPressure.KPA: 1 / 1000,
|
||||||
@ -293,7 +294,7 @@ class SpeedConverter(BaseUnitConverter):
|
|||||||
|
|
||||||
UNIT_CLASS = "speed"
|
UNIT_CLASS = "speed"
|
||||||
NORMALIZED_UNIT = UnitOfSpeed.METERS_PER_SECOND
|
NORMALIZED_UNIT = UnitOfSpeed.METERS_PER_SECOND
|
||||||
_UNIT_CONVERSION: dict[str, float] = {
|
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||||
UnitOfVolumetricFlux.INCHES_PER_DAY: _DAYS_TO_SECS / _IN_TO_M,
|
UnitOfVolumetricFlux.INCHES_PER_DAY: _DAYS_TO_SECS / _IN_TO_M,
|
||||||
UnitOfVolumetricFlux.INCHES_PER_HOUR: _HRS_TO_SECS / _IN_TO_M,
|
UnitOfVolumetricFlux.INCHES_PER_HOUR: _HRS_TO_SECS / _IN_TO_M,
|
||||||
UnitOfVolumetricFlux.MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M,
|
UnitOfVolumetricFlux.MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M,
|
||||||
@ -334,7 +335,7 @@ class TemperatureConverter(BaseUnitConverter):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def convert(cls, value: float, from_unit: str, to_unit: str) -> float:
|
def convert(cls, value: float, from_unit: str | None, to_unit: str | None) -> float:
|
||||||
"""Convert a temperature from one unit to another.
|
"""Convert a temperature from one unit to another.
|
||||||
|
|
||||||
eg. 10°C will return 50°F
|
eg. 10°C will return 50°F
|
||||||
@ -411,13 +412,28 @@ class TemperatureConverter(BaseUnitConverter):
|
|||||||
return celsius + 273.15
|
return celsius + 273.15
|
||||||
|
|
||||||
|
|
||||||
|
class UnitlessRatioConverter(BaseUnitConverter):
|
||||||
|
"""Utility to convert unitless ratios."""
|
||||||
|
|
||||||
|
UNIT_CLASS = "unitless"
|
||||||
|
NORMALIZED_UNIT = None
|
||||||
|
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||||
|
None: 1,
|
||||||
|
PERCENTAGE: 100,
|
||||||
|
}
|
||||||
|
VALID_UNITS = {
|
||||||
|
None,
|
||||||
|
PERCENTAGE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class VolumeConverter(BaseUnitConverter):
|
class VolumeConverter(BaseUnitConverter):
|
||||||
"""Utility to convert volume values."""
|
"""Utility to convert volume values."""
|
||||||
|
|
||||||
UNIT_CLASS = "volume"
|
UNIT_CLASS = "volume"
|
||||||
NORMALIZED_UNIT = UnitOfVolume.CUBIC_METERS
|
NORMALIZED_UNIT = UnitOfVolume.CUBIC_METERS
|
||||||
# Units in terms of m³
|
# Units in terms of m³
|
||||||
_UNIT_CONVERSION: dict[str, float] = {
|
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||||
UnitOfVolume.LITERS: 1 / _L_TO_CUBIC_METER,
|
UnitOfVolume.LITERS: 1 / _L_TO_CUBIC_METER,
|
||||||
UnitOfVolume.MILLILITERS: 1 / _ML_TO_CUBIC_METER,
|
UnitOfVolume.MILLILITERS: 1 / _ML_TO_CUBIC_METER,
|
||||||
UnitOfVolume.GALLONS: 1 / _GALLON_TO_CUBIC_METER,
|
UnitOfVolume.GALLONS: 1 / _GALLON_TO_CUBIC_METER,
|
||||||
|
@ -1690,7 +1690,7 @@ async def test_clear_statistics(recorder_mock, hass, hass_ws_client):
|
|||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"new_unit, new_unit_class, new_display_unit",
|
"new_unit, new_unit_class, new_display_unit",
|
||||||
[("dogs", None, "dogs"), (None, None, None), ("W", "power", "kW")],
|
[("dogs", None, "dogs"), (None, "unitless", None), ("W", "power", "kW")],
|
||||||
)
|
)
|
||||||
async def test_update_statistics_metadata(
|
async def test_update_statistics_metadata(
|
||||||
recorder_mock, hass, hass_ws_client, new_unit, new_unit_class, new_display_unit
|
recorder_mock, hass, hass_ws_client, new_unit, new_unit_class, new_display_unit
|
||||||
@ -2986,7 +2986,7 @@ async def test_adjust_sum_statistics_gas(
|
|||||||
("m³", "m³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)),
|
("m³", "m³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)),
|
||||||
("ft³", "ft³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)),
|
("ft³", "ft³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)),
|
||||||
("dogs", "dogs", None, 1, ("dogs",), ("cats", None)),
|
("dogs", "dogs", None, 1, ("dogs",), ("cats", None)),
|
||||||
(None, None, None, 1, (None,), ("cats",)),
|
(None, None, "unitless", 1, (None,), ("cats",)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async def test_adjust_sum_statistics_errors(
|
async def test_adjust_sum_statistics_errors(
|
||||||
|
@ -17,6 +17,7 @@ from homeassistant.const import (
|
|||||||
LENGTH_YARD,
|
LENGTH_YARD,
|
||||||
MASS_GRAMS,
|
MASS_GRAMS,
|
||||||
MASS_OUNCES,
|
MASS_OUNCES,
|
||||||
|
PERCENTAGE,
|
||||||
PRESSURE_HPA,
|
PRESSURE_HPA,
|
||||||
PRESSURE_INHG,
|
PRESSURE_INHG,
|
||||||
PRESSURE_KPA,
|
PRESSURE_KPA,
|
||||||
@ -546,6 +547,31 @@ async def test_custom_unit(
|
|||||||
1000,
|
1000,
|
||||||
SensorDeviceClass.ENERGY,
|
SensorDeviceClass.ENERGY,
|
||||||
),
|
),
|
||||||
|
# Power factor
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
PERCENTAGE,
|
||||||
|
PERCENTAGE,
|
||||||
|
1.0,
|
||||||
|
100,
|
||||||
|
SensorDeviceClass.POWER_FACTOR,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
PERCENTAGE,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
100,
|
||||||
|
1,
|
||||||
|
SensorDeviceClass.POWER_FACTOR,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Cos φ",
|
||||||
|
None,
|
||||||
|
"Cos φ",
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
SensorDeviceClass.POWER_FACTOR,
|
||||||
|
),
|
||||||
# Pressure
|
# Pressure
|
||||||
# Smaller to larger unit, InHg is ~33x larger than hPa -> 1 more decimal
|
# Smaller to larger unit, InHg is ~33x larger than hPa -> 1 more decimal
|
||||||
(
|
(
|
||||||
@ -686,7 +712,7 @@ async def test_custom_unit_change(
|
|||||||
|
|
||||||
state = hass.states.get(entity0.entity_id)
|
state = hass.states.get(entity0.entity_id)
|
||||||
assert float(state.state) == approx(float(native_value))
|
assert float(state.state) == approx(float(native_value))
|
||||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit
|
||||||
|
|
||||||
entity_registry.async_update_entity_options(
|
entity_registry.async_update_entity_options(
|
||||||
"sensor.test", "sensor", {"unit_of_measurement": custom_unit}
|
"sensor.test", "sensor", {"unit_of_measurement": custom_unit}
|
||||||
@ -695,7 +721,7 @@ async def test_custom_unit_change(
|
|||||||
|
|
||||||
state = hass.states.get(entity0.entity_id)
|
state = hass.states.get(entity0.entity_id)
|
||||||
assert float(state.state) == approx(float(custom_value))
|
assert float(state.state) == approx(float(custom_value))
|
||||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == state_unit
|
||||||
|
|
||||||
entity_registry.async_update_entity_options(
|
entity_registry.async_update_entity_options(
|
||||||
"sensor.test", "sensor", {"unit_of_measurement": native_unit}
|
"sensor.test", "sensor", {"unit_of_measurement": native_unit}
|
||||||
@ -704,14 +730,14 @@ async def test_custom_unit_change(
|
|||||||
|
|
||||||
state = hass.states.get(entity0.entity_id)
|
state = hass.states.get(entity0.entity_id)
|
||||||
assert float(state.state) == approx(float(native_value))
|
assert float(state.state) == approx(float(native_value))
|
||||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit
|
||||||
|
|
||||||
entity_registry.async_update_entity_options("sensor.test", "sensor", None)
|
entity_registry.async_update_entity_options("sensor.test", "sensor", None)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
state = hass.states.get(entity0.entity_id)
|
state = hass.states.get(entity0.entity_id)
|
||||||
assert float(state.state) == approx(float(native_value))
|
assert float(state.state) == approx(float(native_value))
|
||||||
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit
|
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -94,13 +94,13 @@ def set_time_zone():
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max",
|
"device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max",
|
||||||
[
|
[
|
||||||
(None, "%", "%", "%", None, 13.050847, -10, 30),
|
(None, "%", "%", "%", "unitless", 13.050847, -10, 30),
|
||||||
("battery", "%", "%", "%", None, 13.050847, -10, 30),
|
("battery", "%", "%", "%", "unitless", 13.050847, -10, 30),
|
||||||
("battery", None, None, None, None, 13.050847, -10, 30),
|
("battery", None, None, None, "unitless", 13.050847, -10, 30),
|
||||||
("distance", "m", "m", "m", "distance", 13.050847, -10, 30),
|
("distance", "m", "m", "m", "distance", 13.050847, -10, 30),
|
||||||
("distance", "mi", "mi", "mi", "distance", 13.050847, -10, 30),
|
("distance", "mi", "mi", "mi", "distance", 13.050847, -10, 30),
|
||||||
("humidity", "%", "%", "%", None, 13.050847, -10, 30),
|
("humidity", "%", "%", "%", "unitless", 13.050847, -10, 30),
|
||||||
("humidity", None, None, None, None, 13.050847, -10, 30),
|
("humidity", None, None, None, "unitless", 13.050847, -10, 30),
|
||||||
("pressure", "Pa", "Pa", "Pa", "pressure", 13.050847, -10, 30),
|
("pressure", "Pa", "Pa", "Pa", "pressure", 13.050847, -10, 30),
|
||||||
("pressure", "hPa", "hPa", "hPa", "pressure", 13.050847, -10, 30),
|
("pressure", "hPa", "hPa", "hPa", "pressure", 13.050847, -10, 30),
|
||||||
("pressure", "mbar", "mbar", "mbar", "pressure", 13.050847, -10, 30),
|
("pressure", "mbar", "mbar", "mbar", "pressure", 13.050847, -10, 30),
|
||||||
@ -178,7 +178,7 @@ def test_compile_hourly_statistics(
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"device_class, state_unit, display_unit, statistics_unit, unit_class",
|
"device_class, state_unit, display_unit, statistics_unit, unit_class",
|
||||||
[
|
[
|
||||||
(None, "%", "%", "%", None),
|
(None, "%", "%", "%", "unitless"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_compile_hourly_statistics_purged_state_changes(
|
def test_compile_hourly_statistics_purged_state_changes(
|
||||||
@ -317,7 +317,7 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes)
|
|||||||
"source": "recorder",
|
"source": "recorder",
|
||||||
"statistic_id": "sensor.test3",
|
"statistic_id": "sensor.test3",
|
||||||
"statistics_unit_of_measurement": None,
|
"statistics_unit_of_measurement": None,
|
||||||
"unit_class": None,
|
"unit_class": "unitless",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"statistic_id": "sensor.test6",
|
"statistic_id": "sensor.test6",
|
||||||
@ -1775,8 +1775,8 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog):
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"state_class, device_class, state_unit, display_unit, statistics_unit, unit_class, statistic_type",
|
"state_class, device_class, state_unit, display_unit, statistics_unit, unit_class, statistic_type",
|
||||||
[
|
[
|
||||||
("measurement", "battery", "%", "%", "%", None, "mean"),
|
("measurement", "battery", "%", "%", "%", "unitless", "mean"),
|
||||||
("measurement", "battery", None, None, None, None, "mean"),
|
("measurement", "battery", None, None, None, "unitless", "mean"),
|
||||||
("measurement", "distance", "m", "m", "m", "distance", "mean"),
|
("measurement", "distance", "m", "m", "m", "distance", "mean"),
|
||||||
("measurement", "distance", "mi", "mi", "mi", "distance", "mean"),
|
("measurement", "distance", "mi", "mi", "mi", "distance", "mean"),
|
||||||
("total", "distance", "m", "m", "m", "distance", "sum"),
|
("total", "distance", "m", "m", "m", "distance", "sum"),
|
||||||
@ -1785,8 +1785,8 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog):
|
|||||||
("total", "energy", "kWh", "kWh", "kWh", "energy", "sum"),
|
("total", "energy", "kWh", "kWh", "kWh", "energy", "sum"),
|
||||||
("measurement", "energy", "Wh", "Wh", "Wh", "energy", "mean"),
|
("measurement", "energy", "Wh", "Wh", "Wh", "energy", "mean"),
|
||||||
("measurement", "energy", "kWh", "kWh", "kWh", "energy", "mean"),
|
("measurement", "energy", "kWh", "kWh", "kWh", "energy", "mean"),
|
||||||
("measurement", "humidity", "%", "%", "%", None, "mean"),
|
("measurement", "humidity", "%", "%", "%", "unitless", "mean"),
|
||||||
("measurement", "humidity", None, None, None, None, "mean"),
|
("measurement", "humidity", None, None, None, "unitless", "mean"),
|
||||||
("total", "monetary", "USD", "USD", "USD", None, "sum"),
|
("total", "monetary", "USD", "USD", "USD", None, "sum"),
|
||||||
("total", "monetary", "None", "None", "None", None, "sum"),
|
("total", "monetary", "None", "None", "None", None, "sum"),
|
||||||
("total", "gas", "m³", "m³", "m³", "volume", "sum"),
|
("total", "gas", "m³", "m³", "m³", "volume", "sum"),
|
||||||
@ -1898,10 +1898,10 @@ def test_list_statistic_ids_unsupported(hass_recorder, caplog, _attributes):
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"device_class, state_unit, state_unit2, unit_class, mean, min, max",
|
"device_class, state_unit, state_unit2, unit_class, mean, min, max",
|
||||||
[
|
[
|
||||||
(None, None, "cats", None, 13.050847, -10, 30),
|
(None, None, "cats", "unitless", 13.050847, -10, 30),
|
||||||
(None, "%", "cats", None, 13.050847, -10, 30),
|
(None, "%", "cats", "unitless", 13.050847, -10, 30),
|
||||||
("battery", "%", "cats", None, 13.050847, -10, 30),
|
("battery", "%", "cats", "unitless", 13.050847, -10, 30),
|
||||||
("battery", None, "cats", None, 13.050847, -10, 30),
|
("battery", None, "cats", "unitless", 13.050847, -10, 30),
|
||||||
(None, "kW", "Wh", "power", 13.050847, -10, 30),
|
(None, "kW", "Wh", "power", 13.050847, -10, 30),
|
||||||
# Can't downgrade from ft³ to ft3 or from m³ to m3
|
# Can't downgrade from ft³ to ft3 or from m³ to m3
|
||||||
(None, "ft³", "ft3", "volume", 13.050847, -10, 30),
|
(None, "ft³", "ft3", "volume", 13.050847, -10, 30),
|
||||||
@ -1919,7 +1919,10 @@ def test_compile_hourly_statistics_changing_units_1(
|
|||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
):
|
):
|
||||||
"""Test compiling hourly statistics where units change from one hour to the next."""
|
"""Test compiling hourly statistics where units change from one hour to the next.
|
||||||
|
|
||||||
|
This tests the case where the recorder can not convert between the units.
|
||||||
|
"""
|
||||||
zero = dt_util.utcnow()
|
zero = dt_util.utcnow()
|
||||||
hass = hass_recorder()
|
hass = hass_recorder()
|
||||||
setup_component(hass, "sensor", {})
|
setup_component(hass, "sensor", {})
|
||||||
@ -2014,10 +2017,7 @@ def test_compile_hourly_statistics_changing_units_1(
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max",
|
"device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max",
|
||||||
[
|
[
|
||||||
(None, None, None, None, None, 13.050847, -10, 30),
|
(None, "dogs", "dogs", "dogs", None, 13.050847, -10, 30),
|
||||||
(None, "%", "%", "%", None, 13.050847, -10, 30),
|
|
||||||
("battery", "%", "%", "%", None, 13.050847, -10, 30),
|
|
||||||
("battery", None, None, None, None, 13.050847, -10, 30),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_compile_hourly_statistics_changing_units_2(
|
def test_compile_hourly_statistics_changing_units_2(
|
||||||
@ -2032,7 +2032,11 @@ def test_compile_hourly_statistics_changing_units_2(
|
|||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
):
|
):
|
||||||
"""Test compiling hourly statistics where units change during an hour."""
|
"""Test compiling hourly statistics where units change during an hour.
|
||||||
|
|
||||||
|
This tests the behaviour when the sensor units are note supported by any unit
|
||||||
|
converter.
|
||||||
|
"""
|
||||||
zero = dt_util.utcnow()
|
zero = dt_util.utcnow()
|
||||||
hass = hass_recorder()
|
hass = hass_recorder()
|
||||||
setup_component(hass, "sensor", {})
|
setup_component(hass, "sensor", {})
|
||||||
@ -2077,10 +2081,7 @@ def test_compile_hourly_statistics_changing_units_2(
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max",
|
"device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max",
|
||||||
[
|
[
|
||||||
(None, None, None, None, None, 13.050847, -10, 30),
|
(None, "dogs", "dogs", "dogs", None, 13.050847, -10, 30),
|
||||||
(None, "%", "%", "%", None, 13.050847, -10, 30),
|
|
||||||
("battery", "%", "%", "%", None, 13.050847, -10, 30),
|
|
||||||
("battery", None, None, None, None, 13.050847, -10, 30),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_compile_hourly_statistics_changing_units_3(
|
def test_compile_hourly_statistics_changing_units_3(
|
||||||
@ -2095,7 +2096,11 @@ def test_compile_hourly_statistics_changing_units_3(
|
|||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
):
|
):
|
||||||
"""Test compiling hourly statistics where units change from one hour to the next."""
|
"""Test compiling hourly statistics where units change from one hour to the next.
|
||||||
|
|
||||||
|
This tests the behaviour when the sensor units are note supported by any unit
|
||||||
|
converter.
|
||||||
|
"""
|
||||||
zero = dt_util.utcnow()
|
zero = dt_util.utcnow()
|
||||||
hass = hass_recorder()
|
hass = hass_recorder()
|
||||||
setup_component(hass, "sensor", {})
|
setup_component(hass, "sensor", {})
|
||||||
@ -2187,6 +2192,132 @@ def test_compile_hourly_statistics_changing_units_3(
|
|||||||
assert "Error while processing event StatisticsTask" not in caplog.text
|
assert "Error while processing event StatisticsTask" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"state_unit_1, state_unit_2, unit_class, mean, min, max, factor",
|
||||||
|
[
|
||||||
|
(None, "%", "unitless", 13.050847, -10, 30, 100),
|
||||||
|
("%", None, "unitless", 13.050847, -10, 30, 0.01),
|
||||||
|
("W", "kW", "power", 13.050847, -10, 30, 0.001),
|
||||||
|
("kW", "W", "power", 13.050847, -10, 30, 1000),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_compile_hourly_statistics_convert_units_1(
|
||||||
|
hass_recorder,
|
||||||
|
caplog,
|
||||||
|
state_unit_1,
|
||||||
|
state_unit_2,
|
||||||
|
unit_class,
|
||||||
|
mean,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
factor,
|
||||||
|
):
|
||||||
|
"""Test compiling hourly statistics where units change from one hour to the next.
|
||||||
|
|
||||||
|
This tests the case where the recorder can convert between the units.
|
||||||
|
"""
|
||||||
|
zero = dt_util.utcnow()
|
||||||
|
hass = hass_recorder()
|
||||||
|
setup_component(hass, "sensor", {})
|
||||||
|
wait_recording_done(hass) # Wait for the sensor recorder platform to be added
|
||||||
|
attributes = {
|
||||||
|
"device_class": None,
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": state_unit_1,
|
||||||
|
}
|
||||||
|
four, states = record_states(hass, zero, "sensor.test1", attributes)
|
||||||
|
four, _states = record_states(
|
||||||
|
hass, zero + timedelta(minutes=5), "sensor.test1", attributes, seq=[0, 1, None]
|
||||||
|
)
|
||||||
|
states["sensor.test1"] += _states["sensor.test1"]
|
||||||
|
|
||||||
|
do_adhoc_statistics(hass, start=zero)
|
||||||
|
wait_recording_done(hass)
|
||||||
|
assert "does not match the unit of already compiled" not in caplog.text
|
||||||
|
statistic_ids = list_statistic_ids(hass)
|
||||||
|
assert statistic_ids == [
|
||||||
|
{
|
||||||
|
"statistic_id": "sensor.test1",
|
||||||
|
"display_unit_of_measurement": state_unit_1,
|
||||||
|
"has_mean": True,
|
||||||
|
"has_sum": False,
|
||||||
|
"name": None,
|
||||||
|
"source": "recorder",
|
||||||
|
"statistics_unit_of_measurement": state_unit_1,
|
||||||
|
"unit_class": unit_class,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
stats = statistics_during_period(hass, zero, period="5minute")
|
||||||
|
assert stats == {
|
||||||
|
"sensor.test1": [
|
||||||
|
{
|
||||||
|
"start": process_timestamp(zero),
|
||||||
|
"end": process_timestamp(zero + timedelta(minutes=5)),
|
||||||
|
"mean": approx(mean),
|
||||||
|
"min": approx(min),
|
||||||
|
"max": approx(max),
|
||||||
|
"last_reset": None,
|
||||||
|
"state": None,
|
||||||
|
"sum": None,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes["unit_of_measurement"] = state_unit_2
|
||||||
|
four, _states = record_states(
|
||||||
|
hass, zero + timedelta(minutes=10), "sensor.test1", attributes
|
||||||
|
)
|
||||||
|
states["sensor.test1"] += _states["sensor.test1"]
|
||||||
|
hist = history.get_significant_states(hass, zero, four)
|
||||||
|
assert dict(states) == dict(hist)
|
||||||
|
do_adhoc_statistics(hass, start=zero + timedelta(minutes=10))
|
||||||
|
wait_recording_done(hass)
|
||||||
|
assert "The unit of sensor.test1 is changing" not in caplog.text
|
||||||
|
assert (
|
||||||
|
f"matches the unit of already compiled statistics ({state_unit_1})"
|
||||||
|
not in caplog.text
|
||||||
|
)
|
||||||
|
statistic_ids = list_statistic_ids(hass)
|
||||||
|
assert statistic_ids == [
|
||||||
|
{
|
||||||
|
"statistic_id": "sensor.test1",
|
||||||
|
"display_unit_of_measurement": state_unit_2,
|
||||||
|
"has_mean": True,
|
||||||
|
"has_sum": False,
|
||||||
|
"name": None,
|
||||||
|
"source": "recorder",
|
||||||
|
"statistics_unit_of_measurement": state_unit_1,
|
||||||
|
"unit_class": unit_class,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
stats = statistics_during_period(hass, zero, period="5minute")
|
||||||
|
assert stats == {
|
||||||
|
"sensor.test1": [
|
||||||
|
{
|
||||||
|
"start": process_timestamp(zero),
|
||||||
|
"end": process_timestamp(zero + timedelta(minutes=5)),
|
||||||
|
"mean": approx(mean * factor),
|
||||||
|
"min": approx(min * factor),
|
||||||
|
"max": approx(max * factor),
|
||||||
|
"last_reset": None,
|
||||||
|
"state": None,
|
||||||
|
"sum": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": process_timestamp(zero + timedelta(minutes=10)),
|
||||||
|
"end": process_timestamp(zero + timedelta(minutes=15)),
|
||||||
|
"mean": approx(mean),
|
||||||
|
"min": approx(min),
|
||||||
|
"max": approx(max),
|
||||||
|
"last_reset": None,
|
||||||
|
"state": None,
|
||||||
|
"sum": None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
assert "Error while processing event StatisticsTask" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"device_class, state_unit, state_unit2, unit_class, unit_class2, mean, mean2, min, max",
|
"device_class, state_unit, state_unit2, unit_class, unit_class2, mean, mean2, min, max",
|
||||||
[
|
[
|
||||||
@ -2400,7 +2531,10 @@ def test_compile_hourly_statistics_changing_device_class_1(
|
|||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
):
|
):
|
||||||
"""Test compiling hourly statistics where device class changes from one hour to the next."""
|
"""Test compiling hourly statistics where device class changes from one hour to the next.
|
||||||
|
|
||||||
|
Device class is ignored, meaning changing device class should not influence the statistics.
|
||||||
|
"""
|
||||||
zero = dt_util.utcnow()
|
zero = dt_util.utcnow()
|
||||||
hass = hass_recorder()
|
hass = hass_recorder()
|
||||||
setup_component(hass, "sensor", {})
|
setup_component(hass, "sensor", {})
|
||||||
@ -2586,7 +2720,10 @@ def test_compile_hourly_statistics_changing_device_class_2(
|
|||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
):
|
):
|
||||||
"""Test compiling hourly statistics where device class changes from one hour to the next."""
|
"""Test compiling hourly statistics where device class changes from one hour to the next.
|
||||||
|
|
||||||
|
Device class is ignored, meaning changing device class should not influence the statistics.
|
||||||
|
"""
|
||||||
zero = dt_util.utcnow()
|
zero = dt_util.utcnow()
|
||||||
hass = hass_recorder()
|
hass = hass_recorder()
|
||||||
setup_component(hass, "sensor", {})
|
setup_component(hass, "sensor", {})
|
||||||
@ -2692,10 +2829,10 @@ def test_compile_hourly_statistics_changing_device_class_2(
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max",
|
"device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max",
|
||||||
[
|
[
|
||||||
(None, None, None, None, None, 13.050847, -10, 30),
|
(None, None, None, None, "unitless", 13.050847, -10, 30),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_compile_hourly_statistics_changing_statistics(
|
def test_compile_hourly_statistics_changing_state_class(
|
||||||
hass_recorder,
|
hass_recorder,
|
||||||
caplog,
|
caplog,
|
||||||
device_class,
|
device_class,
|
||||||
@ -2707,7 +2844,7 @@ def test_compile_hourly_statistics_changing_statistics(
|
|||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
):
|
):
|
||||||
"""Test compiling hourly statistics where units change during an hour."""
|
"""Test compiling hourly statistics where state class changes."""
|
||||||
period0 = dt_util.utcnow()
|
period0 = dt_util.utcnow()
|
||||||
period0_end = period1 = period0 + timedelta(minutes=5)
|
period0_end = period1 = period0 + timedelta(minutes=5)
|
||||||
period1_end = period0 + timedelta(minutes=10)
|
period1_end = period0 + timedelta(minutes=10)
|
||||||
@ -2737,7 +2874,7 @@ def test_compile_hourly_statistics_changing_statistics(
|
|||||||
"name": None,
|
"name": None,
|
||||||
"source": "recorder",
|
"source": "recorder",
|
||||||
"statistics_unit_of_measurement": None,
|
"statistics_unit_of_measurement": None,
|
||||||
"unit_class": None,
|
"unit_class": unit_class,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
metadata = get_metadata(hass, statistic_ids=("sensor.test1",))
|
metadata = get_metadata(hass, statistic_ids=("sensor.test1",))
|
||||||
@ -2773,7 +2910,7 @@ def test_compile_hourly_statistics_changing_statistics(
|
|||||||
"name": None,
|
"name": None,
|
||||||
"source": "recorder",
|
"source": "recorder",
|
||||||
"statistics_unit_of_measurement": None,
|
"statistics_unit_of_measurement": None,
|
||||||
"unit_class": None,
|
"unit_class": unit_class,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
metadata = get_metadata(hass, statistic_ids=("sensor.test1",))
|
metadata = get_metadata(hass, statistic_ids=("sensor.test1",))
|
||||||
@ -2965,7 +3102,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog):
|
|||||||
"name": None,
|
"name": None,
|
||||||
"source": "recorder",
|
"source": "recorder",
|
||||||
"statistics_unit_of_measurement": "%",
|
"statistics_unit_of_measurement": "%",
|
||||||
"unit_class": None,
|
"unit_class": "unitless",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"statistic_id": "sensor.test2",
|
"statistic_id": "sensor.test2",
|
||||||
@ -2975,7 +3112,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog):
|
|||||||
"name": None,
|
"name": None,
|
||||||
"source": "recorder",
|
"source": "recorder",
|
||||||
"statistics_unit_of_measurement": "%",
|
"statistics_unit_of_measurement": "%",
|
||||||
"unit_class": None,
|
"unit_class": "unitless",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"statistic_id": "sensor.test3",
|
"statistic_id": "sensor.test3",
|
||||||
@ -2985,7 +3122,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog):
|
|||||||
"name": None,
|
"name": None,
|
||||||
"source": "recorder",
|
"source": "recorder",
|
||||||
"statistics_unit_of_measurement": "%",
|
"statistics_unit_of_measurement": "%",
|
||||||
"unit_class": None,
|
"unit_class": "unitless",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"statistic_id": "sensor.test4",
|
"statistic_id": "sensor.test4",
|
||||||
@ -3496,6 +3633,13 @@ async def test_validate_statistics_unit_ignore_device_class(
|
|||||||
"bar",
|
"bar",
|
||||||
"Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi",
|
"Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
METRIC_SYSTEM,
|
||||||
|
BATTERY_SENSOR_ATTRIBUTES,
|
||||||
|
"%",
|
||||||
|
None,
|
||||||
|
"%, <None>",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_validate_statistics_unit_change_no_device_class(
|
async def test_validate_statistics_unit_change_no_device_class(
|
||||||
@ -3851,8 +3995,8 @@ async def test_validate_statistics_sensor_removed(
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"attributes, unit1, unit2",
|
"attributes, unit1, unit2",
|
||||||
[
|
[
|
||||||
(BATTERY_SENSOR_ATTRIBUTES, "%", "dogs"),
|
(BATTERY_SENSOR_ATTRIBUTES, "cats", "dogs"),
|
||||||
(NONE_SENSOR_ATTRIBUTES, None, "dogs"),
|
(NONE_SENSOR_ATTRIBUTES, "cats", "dogs"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_validate_statistics_unit_change_no_conversion(
|
async def test_validate_statistics_unit_change_no_conversion(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user