Allow sensor ambiguous native_unit_of_measurement for compatibility

This commit is contained in:
jbouwh 2025-05-22 19:59:16 +00:00
parent 2d4265fbe1
commit 6e52d0ddb1
4 changed files with 60 additions and 23 deletions

View File

@ -34,6 +34,7 @@ from homeassistant.util.enum import try_parse_enum
from homeassistant.util.hass_dict import HassKey from homeassistant.util.hass_dict import HassKey
from .const import ( # noqa: F401 from .const import ( # noqa: F401
AMBIGUOUS_UNITS,
ATTR_LAST_RESET, ATTR_LAST_RESET,
ATTR_OPTIONS, ATTR_OPTIONS,
ATTR_STATE_CLASS, ATTR_STATE_CLASS,
@ -314,7 +315,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return _numeric_state_expected( return _numeric_state_expected(
try_parse_enum(SensorDeviceClass, self.device_class), try_parse_enum(SensorDeviceClass, self.device_class),
self.state_class, self.state_class,
self.native_unit_of_measurement, self.__native_unit_of_measurement_compat,
self.suggested_display_precision, self.suggested_display_precision,
) )
@ -366,7 +367,8 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
# Make sure we can convert the units # Make sure we can convert the units
if ( if (
(unit_converter := UNIT_CONVERTERS.get(self.device_class)) is None (unit_converter := UNIT_CONVERTERS.get(self.device_class)) is None
or self.native_unit_of_measurement not in unit_converter.VALID_UNITS or self.__native_unit_of_measurement_compat
not in unit_converter.VALID_UNITS
or suggested_unit_of_measurement not in unit_converter.VALID_UNITS or suggested_unit_of_measurement not in unit_converter.VALID_UNITS
): ):
if not self._invalid_suggested_unit_of_measurement_reported: if not self._invalid_suggested_unit_of_measurement_reported:
@ -387,7 +389,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if suggested_unit_of_measurement is None: if suggested_unit_of_measurement is None:
# Fallback to unit suggested by the unit conversion rules from device class # Fallback to unit suggested by the unit conversion rules from device class
suggested_unit_of_measurement = self.hass.config.units.get_converted_unit( suggested_unit_of_measurement = self.hass.config.units.get_converted_unit(
self.device_class, self.native_unit_of_measurement self.device_class, self.__native_unit_of_measurement_compat
) )
if suggested_unit_of_measurement is None and ( if suggested_unit_of_measurement is None and (
@ -396,7 +398,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
# If the device class is not known by the unit system but has a unit converter, # If the device class is not known by the unit system but has a unit converter,
# fall back to the unit suggested by the unit converter's unit class. # fall back to the unit suggested by the unit converter's unit class.
suggested_unit_of_measurement = self.hass.config.units.get_converted_unit( suggested_unit_of_measurement = self.hass.config.units.get_converted_unit(
unit_converter.UNIT_CLASS, self.native_unit_of_measurement unit_converter.UNIT_CLASS, self.__native_unit_of_measurement_compat
) )
if suggested_unit_of_measurement is None: if suggested_unit_of_measurement is None:
@ -468,6 +470,14 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return self.entity_description.native_unit_of_measurement return self.entity_description.native_unit_of_measurement
return None return None
@cached_property
def __native_unit_of_measurement_compat(self) -> str | None:
"""Process ambiguous units."""
native_unit_of_measurement = self.native_unit_of_measurement
return AMBIGUOUS_UNITS.get(
native_unit_of_measurement, native_unit_of_measurement
)
@cached_property @cached_property
def suggested_unit_of_measurement(self) -> str | None: def suggested_unit_of_measurement(self) -> str | None:
"""Return the unit which should be used for the sensor's state. """Return the unit which should be used for the sensor's state.
@ -503,7 +513,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if self._sensor_option_unit_of_measurement is not UNDEFINED: if self._sensor_option_unit_of_measurement is not UNDEFINED:
return self._sensor_option_unit_of_measurement return self._sensor_option_unit_of_measurement
native_unit_of_measurement = self.native_unit_of_measurement native_unit_of_measurement = self.__native_unit_of_measurement_compat
# Second priority, for non registered entities: unit suggested by integration # Second priority, for non registered entities: unit suggested by integration
if not self.registry_entry and ( if not self.registry_entry and (
@ -541,7 +551,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@override @override
def state(self) -> Any: def state(self) -> Any:
"""Return the state of the sensor and perform unit conversions, if needed.""" """Return the state of the sensor and perform unit conversions, if needed."""
native_unit_of_measurement = self.native_unit_of_measurement native_unit_of_measurement = self.__native_unit_of_measurement_compat
unit_of_measurement = self.unit_of_measurement unit_of_measurement = self.unit_of_measurement
value = self.native_value value = self.native_value
# For the sake of validation, we can ignore custom device classes # For the sake of validation, we can ignore custom device classes
@ -763,7 +773,8 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
return display_precision return display_precision
default_unit_of_measurement = ( default_unit_of_measurement = (
self.suggested_unit_of_measurement or self.native_unit_of_measurement self.suggested_unit_of_measurement
or self.__native_unit_of_measurement_compat
) )
if default_unit_of_measurement is None: if default_unit_of_measurement is None:
return display_precision return display_precision
@ -841,7 +852,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
(sensor_options := self.registry_entry.options.get(primary_key)) (sensor_options := self.registry_entry.options.get(primary_key))
and secondary_key in sensor_options 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_compat
in UNIT_CONVERTERS[device_class].VALID_UNITS in UNIT_CONVERTERS[device_class].VALID_UNITS
and (custom_unit := sensor_options[secondary_key]) and (custom_unit := sensor_options[secondary_key])
in UNIT_CONVERTERS[device_class].VALID_UNITS in UNIT_CONVERTERS[device_class].VALID_UNITS

View File

@ -8,6 +8,7 @@ from typing import Final
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_BILLION,
@ -770,3 +771,14 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = {
STATE_CLASS_UNITS: dict[SensorStateClass | str, set[type[StrEnum] | str | None]] = { STATE_CLASS_UNITS: dict[SensorStateClass | str, set[type[StrEnum] | str | None]] = {
SensorStateClass.MEASUREMENT_ANGLE: {DEGREE}, SensorStateClass.MEASUREMENT_ANGLE: {DEGREE},
} }
AMBIGUOUS_UNITS: dict[str | None, str | None] = {
"\u00b5Sv/h": "μSv/h", # aranet: radiation rate
"\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM,
"\u00b5V": UnitOfElectricPotential.MICROVOLT,
"\u00b5g/ft³": CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
"\u00b5g/m³": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
"\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light
"\u00b5g": UnitOfMass.MICROGRAMS,
"\u00b5s": UnitOfTime.MICROSECONDS,
}

View File

@ -28,13 +28,9 @@ from homeassistant.components.recorder.models import (
) )
from homeassistant.const import ( from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
REVOLUTIONS_PER_MINUTE, REVOLUTIONS_PER_MINUTE,
UnitOfElectricPotential,
UnitOfIrradiance, UnitOfIrradiance,
UnitOfMass,
UnitOfSoundPressure, UnitOfSoundPressure,
UnitOfTime,
UnitOfVolume, UnitOfVolume,
) )
from homeassistant.core import HomeAssistant, State, callback, split_entity_id from homeassistant.core import HomeAssistant, State, callback, split_entity_id
@ -49,12 +45,11 @@ from homeassistant.util.enum import try_parse_enum
from homeassistant.util.hass_dict import HassKey from homeassistant.util.hass_dict import HassKey
from .const import ( from .const import (
AMBIGUOUS_UNITS,
ATTR_LAST_RESET, ATTR_LAST_RESET,
ATTR_STATE_CLASS, ATTR_STATE_CLASS,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
DOMAIN, DOMAIN,
SensorStateClass, SensorStateClass,
UnitOfConductivity,
UnitOfVolumeFlowRate, UnitOfVolumeFlowRate,
) )
@ -85,15 +80,7 @@ EQUIVALENT_UNITS = {
"ft3": UnitOfVolume.CUBIC_FEET, "ft3": UnitOfVolume.CUBIC_FEET,
"m3": UnitOfVolume.CUBIC_METERS, "m3": UnitOfVolume.CUBIC_METERS,
"ft³/m": UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, "ft³/m": UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE,
"\u00b5Sv/h": "μSv/h", # aranet: radiation rate } | AMBIGUOUS_UNITS
"\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM,
"\u00b5V": UnitOfElectricPotential.MICROVOLT,
"\u00b5g/ft³": CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
"\u00b5g/m³": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
"\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light
"\u00b5g": UnitOfMass.MICROGRAMS,
"\u00b5s": UnitOfTime.MICROSECONDS,
}
# Keep track of entities for which a warning about decreasing value has been logged # Keep track of entities for which a warning about decreasing value has been logged

View File

@ -165,6 +165,33 @@ async def test_temperature_conversion_wrong_device_class(
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.FAHRENHEIT assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.FAHRENHEIT
@pytest.mark.parametrize(
("ambiguous_unit", "normalized_unit"),
[
(ambiguous_unit, normalized_unit)
for ambiguous_unit, normalized_unit in sensor.AMBIGUOUS_UNITS.items()
],
)
async def test_ambiguous_unit_of_measurement_compat(
hass: HomeAssistant, ambiguous_unit: str, normalized_unit: str
) -> None:
"""Test ambiguous native_unit_of_measurement values are corrected."""
entity0 = MockSensor(
name="Test",
native_value="0.0",
native_unit_of_measurement=ambiguous_unit,
)
setup_test_component_platform(hass, sensor.DOMAIN, [entity0])
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
await hass.async_block_till_done()
# Check temperature is not converted
state = hass.states.get(entity0.entity_id)
assert state.state == "0.0"
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == normalized_unit
@pytest.mark.parametrize("state_class", ["measurement", "total_increasing"]) @pytest.mark.parametrize("state_class", ["measurement", "total_increasing"])
async def test_deprecated_last_reset( async def test_deprecated_last_reset(
hass: HomeAssistant, hass: HomeAssistant,