From 6e52d0ddb19a7ff95bb6eacd610b02f02feef943 Mon Sep 17 00:00:00 2001 From: jbouwh Date: Thu, 22 May 2025 19:59:16 +0000 Subject: [PATCH] Allow sensor ambiguous native_unit_of_measurement for compatibility --- homeassistant/components/sensor/__init__.py | 27 +++++++++++++++------ homeassistant/components/sensor/const.py | 12 +++++++++ homeassistant/components/sensor/recorder.py | 17 ++----------- tests/components/sensor/test_init.py | 27 +++++++++++++++++++++ 4 files changed, 60 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 9948860fd5f..4917ee4191b 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -34,6 +34,7 @@ from homeassistant.util.enum import try_parse_enum from homeassistant.util.hass_dict import HassKey from .const import ( # noqa: F401 + AMBIGUOUS_UNITS, ATTR_LAST_RESET, ATTR_OPTIONS, ATTR_STATE_CLASS, @@ -314,7 +315,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return _numeric_state_expected( try_parse_enum(SensorDeviceClass, self.device_class), self.state_class, - self.native_unit_of_measurement, + self.__native_unit_of_measurement_compat, self.suggested_display_precision, ) @@ -366,7 +367,8 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): # Make sure we can convert the units if ( (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 ): 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: # Fallback to unit suggested by the unit conversion rules from device class 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 ( @@ -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, # fall back to the unit suggested by the unit converter's unit class. 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: @@ -468,6 +470,14 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return self.entity_description.native_unit_of_measurement 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 def suggested_unit_of_measurement(self) -> str | None: """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: 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 if not self.registry_entry and ( @@ -541,7 +551,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): @override def state(self) -> Any: """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 value = self.native_value # 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 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: 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)) and secondary_key in sensor_options 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 and (custom_unit := sensor_options[secondary_key]) in UNIT_CONVERTERS[device_class].VALID_UNITS diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 863388e4eb6..a158f777157 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -8,6 +8,7 @@ from typing import Final import voluptuous as vol from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, 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]] = { 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, +} diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 1f8caf75667..c20a3e2e1ae 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -28,13 +28,9 @@ from homeassistant.components.recorder.models import ( ) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, - CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, REVOLUTIONS_PER_MINUTE, - UnitOfElectricPotential, UnitOfIrradiance, - UnitOfMass, UnitOfSoundPressure, - UnitOfTime, UnitOfVolume, ) 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 .const import ( + AMBIGUOUS_UNITS, ATTR_LAST_RESET, ATTR_STATE_CLASS, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, DOMAIN, SensorStateClass, - UnitOfConductivity, UnitOfVolumeFlowRate, ) @@ -85,15 +80,7 @@ EQUIVALENT_UNITS = { "ft3": UnitOfVolume.CUBIC_FEET, "m3": UnitOfVolume.CUBIC_METERS, "ft³/m": UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, - "\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, -} +} | AMBIGUOUS_UNITS # Keep track of entities for which a warning about decreasing value has been logged diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 98fb9d6604a..e3a29291109 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -165,6 +165,33 @@ async def test_temperature_conversion_wrong_device_class( 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"]) async def test_deprecated_last_reset( hass: HomeAssistant,