mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 01:38:02 +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,
|
||||
SpeedConverter,
|
||||
TemperatureConverter,
|
||||
UnitlessRatioConverter,
|
||||
VolumeConverter,
|
||||
)
|
||||
|
||||
@ -134,6 +135,7 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {
|
||||
**{unit: PressureConverter for unit in PressureConverter.VALID_UNITS},
|
||||
**{unit: SpeedConverter for unit in SpeedConverter.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},
|
||||
}
|
||||
|
||||
@ -155,9 +157,6 @@ def get_display_unit(
|
||||
) -> str | None:
|
||||
"""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:
|
||||
return statistic_unit
|
||||
|
||||
@ -183,9 +182,6 @@ def _get_statistic_to_display_unit_converter(
|
||||
"""Return val."""
|
||||
return val
|
||||
|
||||
if statistic_unit is None:
|
||||
return no_conversion
|
||||
|
||||
if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None:
|
||||
return no_conversion
|
||||
|
||||
@ -226,9 +222,6 @@ def _get_display_to_statistic_unit_converter(
|
||||
"""Return val."""
|
||||
return val
|
||||
|
||||
if statistic_unit is None:
|
||||
return no_conversion
|
||||
|
||||
if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None:
|
||||
return no_conversion
|
||||
|
||||
@ -1555,17 +1548,10 @@ def statistic_during_period(
|
||||
else:
|
||||
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"]
|
||||
if state := hass.states.get(statistic_id):
|
||||
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)
|
||||
else:
|
||||
convert = no_conversion
|
||||
convert = _get_statistic_to_display_unit_converter(unit, state_unit, units)
|
||||
|
||||
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"]
|
||||
if state := hass.states.get(statistic_id):
|
||||
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)
|
||||
else:
|
||||
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_platform import EntityPlatform
|
||||
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 .const import ( # noqa: F401
|
||||
@ -155,7 +155,7 @@ class SensorEntity(Entity):
|
||||
)
|
||||
_invalid_unit_of_measurement_reported = False
|
||||
_last_reset_reported = False
|
||||
_sensor_option_unit_of_measurement: str | None = None
|
||||
_sensor_option_unit_of_measurement: str | None | UndefinedType = UNDEFINED
|
||||
|
||||
@callback
|
||||
def add_to_platform_start(
|
||||
@ -371,7 +371,7 @@ class SensorEntity(Entity):
|
||||
"""Return the unit of measurement of the entity, after unit conversion."""
|
||||
# Highest priority, for registered entities: unit set by user,with fallback to
|
||||
# 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
|
||||
|
||||
# Second priority, for non registered entities: unit suggested by integration
|
||||
@ -481,8 +481,6 @@ class SensorEntity(Entity):
|
||||
native_unit_of_measurement != unit_of_measurement
|
||||
and device_class in UNIT_CONVERTERS
|
||||
):
|
||||
assert unit_of_measurement
|
||||
assert native_unit_of_measurement
|
||||
converter = UNIT_CONVERTERS[device_class]
|
||||
|
||||
value_s = str(value)
|
||||
@ -550,28 +548,31 @@ class SensorEntity(Entity):
|
||||
|
||||
return super().__repr__()
|
||||
|
||||
def _custom_unit_or_none(self, primary_key: str, secondary_key: str) -> str | None:
|
||||
"""Return a custom unit, or None if it's not compatible with the native unit."""
|
||||
def _custom_unit_or_undef(
|
||||
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
|
||||
if (
|
||||
(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 self.native_unit_of_measurement
|
||||
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 None
|
||||
return UNDEFINED
|
||||
|
||||
@callback
|
||||
def async_registry_entry_updated(self) -> None:
|
||||
"""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
|
||||
)
|
||||
if not self._sensor_option_unit_of_measurement:
|
||||
self._sensor_option_unit_of_measurement = self._custom_unit_or_none(
|
||||
if self._sensor_option_unit_of_measurement is UNDEFINED:
|
||||
self._sensor_option_unit_of_measurement = self._custom_unit_or_undef(
|
||||
f"{DOMAIN}.private", "suggested_unit_of_measurement"
|
||||
)
|
||||
|
||||
|
@ -46,6 +46,7 @@ from homeassistant.util.unit_conversion import (
|
||||
PressureConverter,
|
||||
SpeedConverter,
|
||||
TemperatureConverter,
|
||||
UnitlessRatioConverter,
|
||||
VolumeConverter,
|
||||
)
|
||||
|
||||
@ -421,6 +422,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] =
|
||||
SensorDeviceClass.CURRENT: ElectricCurrentConverter,
|
||||
SensorDeviceClass.ENERGY: EnergyConverter,
|
||||
SensorDeviceClass.GAS: VolumeConverter,
|
||||
SensorDeviceClass.POWER_FACTOR: UnitlessRatioConverter,
|
||||
SensorDeviceClass.PRECIPITATION: DistanceConverter,
|
||||
SensorDeviceClass.PRESSURE: PressureConverter,
|
||||
SensorDeviceClass.SPEED: SpeedConverter,
|
||||
|
@ -183,10 +183,7 @@ def _normalize_states(
|
||||
# We have seen this sensor before, use the unit from metadata
|
||||
statistics_unit = old_metadata["unit_of_measurement"]
|
||||
|
||||
if (
|
||||
not statistics_unit
|
||||
or statistics_unit not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER
|
||||
):
|
||||
if statistics_unit not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER:
|
||||
# The unit used by this sensor doesn't support unit conversion
|
||||
|
||||
all_units = _get_units(fstates)
|
||||
@ -721,7 +718,8 @@ def validate_statistics(
|
||||
)
|
||||
elif state_unit not in converter.VALID_UNITS:
|
||||
# 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(
|
||||
statistics.ValidationIssue(
|
||||
"units_changed",
|
||||
@ -729,7 +727,7 @@ def validate_statistics(
|
||||
"statistic_id": entity_id,
|
||||
"state_unit": state_unit,
|
||||
"metadata_unit": metadata_unit,
|
||||
"supported_unit": valid_units,
|
||||
"supported_unit": valid_units_str,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
@ -2,7 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
import subarulink.const as sc
|
||||
|
||||
@ -207,11 +207,11 @@ class SubaruSensor(
|
||||
return None
|
||||
|
||||
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:
|
||||
return round(
|
||||
unit_system.pressure(current_value, unit),
|
||||
unit_system.pressure(current_value, cast(str, unit)),
|
||||
1,
|
||||
)
|
||||
|
||||
|
@ -20,7 +20,7 @@ from homeassistant.helpers.frame import report
|
||||
from .unit_conversion import PressureConverter
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
|
@ -27,7 +27,7 @@ from .unit_conversion import ( # pylint: disable=unused-import # noqa: F401
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
UNIT_NOT_RECOGNIZED_TEMPLATE,
|
||||
UnitOfDataRate,
|
||||
UnitOfElectricCurrent,
|
||||
@ -56,13 +57,13 @@ class BaseUnitConverter:
|
||||
"""Define the format of a conversion utility."""
|
||||
|
||||
UNIT_CLASS: str
|
||||
NORMALIZED_UNIT: str
|
||||
VALID_UNITS: set[str]
|
||||
NORMALIZED_UNIT: str | None
|
||||
VALID_UNITS: set[str | None]
|
||||
|
||||
_UNIT_CONVERSION: dict[str, float]
|
||||
_UNIT_CONVERSION: dict[str | None, float]
|
||||
|
||||
@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."""
|
||||
if from_unit == to_unit:
|
||||
return value
|
||||
@ -85,7 +86,7 @@ class BaseUnitConverter:
|
||||
return new_value * to_ratio
|
||||
|
||||
@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."""
|
||||
return cls._UNIT_CONVERSION[from_unit] / cls._UNIT_CONVERSION[to_unit]
|
||||
|
||||
@ -96,7 +97,7 @@ class DataRateConverter(BaseUnitConverter):
|
||||
UNIT_CLASS = "data_rate"
|
||||
NORMALIZED_UNIT = UnitOfDataRate.BITS_PER_SECOND
|
||||
# Units in terms of bits
|
||||
_UNIT_CONVERSION: dict[str, float] = {
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
UnitOfDataRate.BITS_PER_SECOND: 1,
|
||||
UnitOfDataRate.KILOBITS_PER_SECOND: 1 / 1e3,
|
||||
UnitOfDataRate.MEGABITS_PER_SECOND: 1 / 1e6,
|
||||
@ -117,7 +118,7 @@ class DistanceConverter(BaseUnitConverter):
|
||||
|
||||
UNIT_CLASS = "distance"
|
||||
NORMALIZED_UNIT = UnitOfLength.METERS
|
||||
_UNIT_CONVERSION: dict[str, float] = {
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
UnitOfLength.METERS: 1,
|
||||
UnitOfLength.MILLIMETERS: 1 / _MM_TO_M,
|
||||
UnitOfLength.CENTIMETERS: 1 / _CM_TO_M,
|
||||
@ -144,7 +145,7 @@ class ElectricCurrentConverter(BaseUnitConverter):
|
||||
|
||||
UNIT_CLASS = "electric_current"
|
||||
NORMALIZED_UNIT = UnitOfElectricCurrent.AMPERE
|
||||
_UNIT_CONVERSION: dict[str, float] = {
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
UnitOfElectricCurrent.AMPERE: 1,
|
||||
UnitOfElectricCurrent.MILLIAMPERE: 1e3,
|
||||
}
|
||||
@ -156,7 +157,7 @@ class ElectricPotentialConverter(BaseUnitConverter):
|
||||
|
||||
UNIT_CLASS = "voltage"
|
||||
NORMALIZED_UNIT = UnitOfElectricPotential.VOLT
|
||||
_UNIT_CONVERSION: dict[str, float] = {
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
UnitOfElectricPotential.VOLT: 1,
|
||||
UnitOfElectricPotential.MILLIVOLT: 1e3,
|
||||
}
|
||||
@ -171,7 +172,7 @@ class EnergyConverter(BaseUnitConverter):
|
||||
|
||||
UNIT_CLASS = "energy"
|
||||
NORMALIZED_UNIT = UnitOfEnergy.KILO_WATT_HOUR
|
||||
_UNIT_CONVERSION: dict[str, float] = {
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
UnitOfEnergy.WATT_HOUR: 1 * 1000,
|
||||
UnitOfEnergy.KILO_WATT_HOUR: 1,
|
||||
UnitOfEnergy.MEGA_WATT_HOUR: 1 / 1000,
|
||||
@ -191,7 +192,7 @@ class InformationConverter(BaseUnitConverter):
|
||||
UNIT_CLASS = "information"
|
||||
NORMALIZED_UNIT = UnitOfInformation.BITS
|
||||
# Units in terms of bits
|
||||
_UNIT_CONVERSION: dict[str, float] = {
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
UnitOfInformation.BITS: 1,
|
||||
UnitOfInformation.KILOBITS: 1 / 1e3,
|
||||
UnitOfInformation.MEGABITS: 1 / 1e6,
|
||||
@ -222,7 +223,7 @@ class MassConverter(BaseUnitConverter):
|
||||
|
||||
UNIT_CLASS = "mass"
|
||||
NORMALIZED_UNIT = UnitOfMass.GRAMS
|
||||
_UNIT_CONVERSION: dict[str, float] = {
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
UnitOfMass.MICROGRAMS: 1 * 1000 * 1000,
|
||||
UnitOfMass.MILLIGRAMS: 1 * 1000,
|
||||
UnitOfMass.GRAMS: 1,
|
||||
@ -247,7 +248,7 @@ class PowerConverter(BaseUnitConverter):
|
||||
|
||||
UNIT_CLASS = "power"
|
||||
NORMALIZED_UNIT = UnitOfPower.WATT
|
||||
_UNIT_CONVERSION: dict[str, float] = {
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
UnitOfPower.WATT: 1,
|
||||
UnitOfPower.KILO_WATT: 1 / 1000,
|
||||
}
|
||||
@ -262,7 +263,7 @@ class PressureConverter(BaseUnitConverter):
|
||||
|
||||
UNIT_CLASS = "pressure"
|
||||
NORMALIZED_UNIT = UnitOfPressure.PA
|
||||
_UNIT_CONVERSION: dict[str, float] = {
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
UnitOfPressure.PA: 1,
|
||||
UnitOfPressure.HPA: 1 / 100,
|
||||
UnitOfPressure.KPA: 1 / 1000,
|
||||
@ -293,7 +294,7 @@ class SpeedConverter(BaseUnitConverter):
|
||||
|
||||
UNIT_CLASS = "speed"
|
||||
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_HOUR: _HRS_TO_SECS / _IN_TO_M,
|
||||
UnitOfVolumetricFlux.MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M,
|
||||
@ -334,7 +335,7 @@ class TemperatureConverter(BaseUnitConverter):
|
||||
}
|
||||
|
||||
@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.
|
||||
|
||||
eg. 10°C will return 50°F
|
||||
@ -411,13 +412,28 @@ class TemperatureConverter(BaseUnitConverter):
|
||||
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):
|
||||
"""Utility to convert volume values."""
|
||||
|
||||
UNIT_CLASS = "volume"
|
||||
NORMALIZED_UNIT = UnitOfVolume.CUBIC_METERS
|
||||
# Units in terms of m³
|
||||
_UNIT_CONVERSION: dict[str, float] = {
|
||||
_UNIT_CONVERSION: dict[str | None, float] = {
|
||||
UnitOfVolume.LITERS: 1 / _L_TO_CUBIC_METER,
|
||||
UnitOfVolume.MILLILITERS: 1 / _ML_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(
|
||||
"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(
|
||||
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)),
|
||||
("ft³", "ft³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "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(
|
||||
|
@ -17,6 +17,7 @@ from homeassistant.const import (
|
||||
LENGTH_YARD,
|
||||
MASS_GRAMS,
|
||||
MASS_OUNCES,
|
||||
PERCENTAGE,
|
||||
PRESSURE_HPA,
|
||||
PRESSURE_INHG,
|
||||
PRESSURE_KPA,
|
||||
@ -546,6 +547,31 @@ async def test_custom_unit(
|
||||
1000,
|
||||
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
|
||||
# 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)
|
||||
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", {"unit_of_measurement": custom_unit}
|
||||
@ -695,7 +721,7 @@ async def test_custom_unit_change(
|
||||
|
||||
state = hass.states.get(entity0.entity_id)
|
||||
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(
|
||||
"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)
|
||||
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)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity0.entity_id)
|
||||
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(
|
||||
|
@ -94,13 +94,13 @@ def set_time_zone():
|
||||
@pytest.mark.parametrize(
|
||||
"device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max",
|
||||
[
|
||||
(None, "%", "%", "%", None, 13.050847, -10, 30),
|
||||
("battery", "%", "%", "%", None, 13.050847, -10, 30),
|
||||
("battery", None, None, None, None, 13.050847, -10, 30),
|
||||
(None, "%", "%", "%", "unitless", 13.050847, -10, 30),
|
||||
("battery", "%", "%", "%", "unitless", 13.050847, -10, 30),
|
||||
("battery", None, None, None, "unitless", 13.050847, -10, 30),
|
||||
("distance", "m", "m", "m", "distance", 13.050847, -10, 30),
|
||||
("distance", "mi", "mi", "mi", "distance", 13.050847, -10, 30),
|
||||
("humidity", "%", "%", "%", None, 13.050847, -10, 30),
|
||||
("humidity", None, None, None, None, 13.050847, -10, 30),
|
||||
("humidity", "%", "%", "%", "unitless", 13.050847, -10, 30),
|
||||
("humidity", None, None, None, "unitless", 13.050847, -10, 30),
|
||||
("pressure", "Pa", "Pa", "Pa", "pressure", 13.050847, -10, 30),
|
||||
("pressure", "hPa", "hPa", "hPa", "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(
|
||||
"device_class, state_unit, display_unit, statistics_unit, unit_class",
|
||||
[
|
||||
(None, "%", "%", "%", None),
|
||||
(None, "%", "%", "%", "unitless"),
|
||||
],
|
||||
)
|
||||
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",
|
||||
"statistic_id": "sensor.test3",
|
||||
"statistics_unit_of_measurement": None,
|
||||
"unit_class": None,
|
||||
"unit_class": "unitless",
|
||||
},
|
||||
{
|
||||
"statistic_id": "sensor.test6",
|
||||
@ -1775,8 +1775,8 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog):
|
||||
@pytest.mark.parametrize(
|
||||
"state_class, device_class, state_unit, display_unit, statistics_unit, unit_class, statistic_type",
|
||||
[
|
||||
("measurement", "battery", "%", "%", "%", None, "mean"),
|
||||
("measurement", "battery", None, None, None, None, "mean"),
|
||||
("measurement", "battery", "%", "%", "%", "unitless", "mean"),
|
||||
("measurement", "battery", None, None, None, "unitless", "mean"),
|
||||
("measurement", "distance", "m", "m", "m", "distance", "mean"),
|
||||
("measurement", "distance", "mi", "mi", "mi", "distance", "mean"),
|
||||
("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"),
|
||||
("measurement", "energy", "Wh", "Wh", "Wh", "energy", "mean"),
|
||||
("measurement", "energy", "kWh", "kWh", "kWh", "energy", "mean"),
|
||||
("measurement", "humidity", "%", "%", "%", None, "mean"),
|
||||
("measurement", "humidity", None, None, None, None, "mean"),
|
||||
("measurement", "humidity", "%", "%", "%", "unitless", "mean"),
|
||||
("measurement", "humidity", None, None, None, "unitless", "mean"),
|
||||
("total", "monetary", "USD", "USD", "USD", None, "sum"),
|
||||
("total", "monetary", "None", "None", "None", None, "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(
|
||||
"device_class, state_unit, state_unit2, unit_class, mean, min, max",
|
||||
[
|
||||
(None, None, "cats", None, 13.050847, -10, 30),
|
||||
(None, "%", "cats", None, 13.050847, -10, 30),
|
||||
("battery", "%", "cats", None, 13.050847, -10, 30),
|
||||
("battery", None, "cats", None, 13.050847, -10, 30),
|
||||
(None, None, "cats", "unitless", 13.050847, -10, 30),
|
||||
(None, "%", "cats", "unitless", 13.050847, -10, 30),
|
||||
("battery", "%", "cats", "unitless", 13.050847, -10, 30),
|
||||
("battery", None, "cats", "unitless", 13.050847, -10, 30),
|
||||
(None, "kW", "Wh", "power", 13.050847, -10, 30),
|
||||
# Can't downgrade from ft³ to ft3 or from m³ to m3
|
||||
(None, "ft³", "ft3", "volume", 13.050847, -10, 30),
|
||||
@ -1919,7 +1919,10 @@ def test_compile_hourly_statistics_changing_units_1(
|
||||
min,
|
||||
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()
|
||||
hass = hass_recorder()
|
||||
setup_component(hass, "sensor", {})
|
||||
@ -2014,10 +2017,7 @@ def test_compile_hourly_statistics_changing_units_1(
|
||||
@pytest.mark.parametrize(
|
||||
"device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max",
|
||||
[
|
||||
(None, None, None, None, 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),
|
||||
(None, "dogs", "dogs", "dogs", None, 13.050847, -10, 30),
|
||||
],
|
||||
)
|
||||
def test_compile_hourly_statistics_changing_units_2(
|
||||
@ -2032,7 +2032,11 @@ def test_compile_hourly_statistics_changing_units_2(
|
||||
min,
|
||||
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()
|
||||
hass = hass_recorder()
|
||||
setup_component(hass, "sensor", {})
|
||||
@ -2077,10 +2081,7 @@ def test_compile_hourly_statistics_changing_units_2(
|
||||
@pytest.mark.parametrize(
|
||||
"device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max",
|
||||
[
|
||||
(None, None, None, None, 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),
|
||||
(None, "dogs", "dogs", "dogs", None, 13.050847, -10, 30),
|
||||
],
|
||||
)
|
||||
def test_compile_hourly_statistics_changing_units_3(
|
||||
@ -2095,7 +2096,11 @@ def test_compile_hourly_statistics_changing_units_3(
|
||||
min,
|
||||
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()
|
||||
hass = hass_recorder()
|
||||
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
|
||||
|
||||
|
||||
@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(
|
||||
"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,
|
||||
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()
|
||||
hass = hass_recorder()
|
||||
setup_component(hass, "sensor", {})
|
||||
@ -2586,7 +2720,10 @@ def test_compile_hourly_statistics_changing_device_class_2(
|
||||
min,
|
||||
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()
|
||||
hass = hass_recorder()
|
||||
setup_component(hass, "sensor", {})
|
||||
@ -2692,10 +2829,10 @@ def test_compile_hourly_statistics_changing_device_class_2(
|
||||
@pytest.mark.parametrize(
|
||||
"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,
|
||||
caplog,
|
||||
device_class,
|
||||
@ -2707,7 +2844,7 @@ def test_compile_hourly_statistics_changing_statistics(
|
||||
min,
|
||||
max,
|
||||
):
|
||||
"""Test compiling hourly statistics where units change during an hour."""
|
||||
"""Test compiling hourly statistics where state class changes."""
|
||||
period0 = dt_util.utcnow()
|
||||
period0_end = period1 = period0 + timedelta(minutes=5)
|
||||
period1_end = period0 + timedelta(minutes=10)
|
||||
@ -2737,7 +2874,7 @@ def test_compile_hourly_statistics_changing_statistics(
|
||||
"name": None,
|
||||
"source": "recorder",
|
||||
"statistics_unit_of_measurement": None,
|
||||
"unit_class": None,
|
||||
"unit_class": unit_class,
|
||||
},
|
||||
]
|
||||
metadata = get_metadata(hass, statistic_ids=("sensor.test1",))
|
||||
@ -2773,7 +2910,7 @@ def test_compile_hourly_statistics_changing_statistics(
|
||||
"name": None,
|
||||
"source": "recorder",
|
||||
"statistics_unit_of_measurement": None,
|
||||
"unit_class": None,
|
||||
"unit_class": unit_class,
|
||||
},
|
||||
]
|
||||
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,
|
||||
"source": "recorder",
|
||||
"statistics_unit_of_measurement": "%",
|
||||
"unit_class": None,
|
||||
"unit_class": "unitless",
|
||||
},
|
||||
{
|
||||
"statistic_id": "sensor.test2",
|
||||
@ -2975,7 +3112,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog):
|
||||
"name": None,
|
||||
"source": "recorder",
|
||||
"statistics_unit_of_measurement": "%",
|
||||
"unit_class": None,
|
||||
"unit_class": "unitless",
|
||||
},
|
||||
{
|
||||
"statistic_id": "sensor.test3",
|
||||
@ -2985,7 +3122,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog):
|
||||
"name": None,
|
||||
"source": "recorder",
|
||||
"statistics_unit_of_measurement": "%",
|
||||
"unit_class": None,
|
||||
"unit_class": "unitless",
|
||||
},
|
||||
{
|
||||
"statistic_id": "sensor.test4",
|
||||
@ -3496,6 +3633,13 @@ async def test_validate_statistics_unit_ignore_device_class(
|
||||
"bar",
|
||||
"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(
|
||||
@ -3851,8 +3995,8 @@ async def test_validate_statistics_sensor_removed(
|
||||
@pytest.mark.parametrize(
|
||||
"attributes, unit1, unit2",
|
||||
[
|
||||
(BATTERY_SENSOR_ATTRIBUTES, "%", "dogs"),
|
||||
(NONE_SENSOR_ATTRIBUTES, None, "dogs"),
|
||||
(BATTERY_SENSOR_ATTRIBUTES, "cats", "dogs"),
|
||||
(NONE_SENSOR_ATTRIBUTES, "cats", "dogs"),
|
||||
],
|
||||
)
|
||||
async def test_validate_statistics_unit_change_no_conversion(
|
||||
|
Loading…
x
Reference in New Issue
Block a user