diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 3e9d3448af2..42b252b5fdd 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -31,6 +31,7 @@ from homeassistant.loader import async_suggest_report_issue from homeassistant.util.hass_dict import HassKey from .const import ( # noqa: F401 + AMBIGUOUS_UNITS, ATTR_MAX, ATTR_MIN, ATTR_STEP, @@ -367,6 +368,15 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return self.entity_description.native_unit_of_measurement return None + @final + @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 + ) + @property @final def unit_of_measurement(self) -> str | None: @@ -374,7 +384,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if self._number_option_unit_of_measurement: return self._number_option_unit_of_measurement - native_unit_of_measurement = self.native_unit_of_measurement + native_unit_of_measurement = self.__native_unit_of_measurement_compat # device_class is checked after native_unit_of_measurement since most # of the time we can avoid the device_class check if ( @@ -441,7 +451,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if device_class not in UNIT_CONVERTERS: return value - 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 if native_unit_of_measurement != unit_of_measurement: if TYPE_CHECKING: @@ -470,7 +480,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if value is None or (device_class := self.device_class) not in UNIT_CONVERTERS: return value - 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 if native_unit_of_measurement != unit_of_measurement: if TYPE_CHECKING: @@ -493,7 +503,7 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): (number_options := self.registry_entry.options.get(DOMAIN)) and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT)) 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 in UNIT_CONVERTERS[device_class].VALID_UNITS ): diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index eb082ea2d99..f3003db29c6 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/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, @@ -546,3 +547,14 @@ UNIT_CONVERTERS: dict[NumberDeviceClass, type[BaseUnitConverter]] = { NumberDeviceClass.TEMPERATURE: TemperatureConverter, NumberDeviceClass.VOLUME_FLOW_RATE: VolumeFlowRateConverter, } + +AMBIGUOUS_UNITS: dict[str | None, str] = { + "\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/__init__.py b/homeassistant/components/sensor/__init__.py index bca0727ffba..1a0895100fb 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -476,7 +476,8 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Process ambiguous units.""" native_unit_of_measurement = self.native_unit_of_measurement return AMBIGUOUS_UNITS.get( - native_unit_of_measurement, native_unit_of_measurement + native_unit_of_measurement, + native_unit_of_measurement, ) @cached_property diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 089fe6b37be..c96fc15e8a4 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -772,7 +772,7 @@ STATE_CLASS_UNITS: dict[SensorStateClass | str, set[type[StrEnum] | str | None]] SensorStateClass.MEASUREMENT_ANGLE: {DEGREE}, } -AMBIGUOUS_UNITS: dict[str, str] = { +AMBIGUOUS_UNITS: dict[str | None, str] = { "\u00b5Sv/h": "μSv/h", # aranet: radiation rate "\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM, "\u00b5V": UnitOfElectricPotential.MICROVOLT, diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 4ccf8f69c42..b5e5e18f664 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -7,6 +7,7 @@ from unittest.mock import MagicMock, patch import pytest from homeassistant.components.number import ( + AMBIGUOUS_UNITS, ATTR_MAX, ATTR_MIN, ATTR_MODE, @@ -48,6 +49,7 @@ from . import common from tests.common import ( MockConfigEntry, + MockEntity, MockModule, MockPlatform, async_mock_restore_state_shutdown_restart, @@ -61,6 +63,25 @@ from tests.common import ( TEST_DOMAIN = "test" +class MockNumber(MockEntity, NumberEntity): + """Mock NumberEntity class to test unit of measurement.""" + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._handle("device_class") + + @property + def native_unit_of_measurement(self): + """Return the native unit_of_measurement of this sensor.""" + return self._handle("native_unit_of_measurement") + + @property + def native_value(self): + """Return the native value of this sensor.""" + return self._handle("native_value") + + class MockDefaultNumberEntity(NumberEntity): """Mock NumberEntity device to use in tests. @@ -900,6 +921,33 @@ async def test_translated_unit_with_native_unit_raises( assert entity0.entity_id is None +@pytest.mark.parametrize( + ("ambiguous_unit", "normalized_unit"), + [ + (ambiguous_unit, normalized_unit) + for ambiguous_unit, normalized_unit in 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 = MockNumber( + name="Test", + native_value="0.0", + native_unit_of_measurement=ambiguous_unit, + ) + setup_test_component_platform(hass, DOMAIN, [entity0]) + + assert await async_setup_component(hass, "number", {"number": {"platform": "test"}}) + await hass.async_block_till_done() + + # Check compatible unit is applied + state = hass.states.get(entity0.entity_id) + assert state.state == "0.0" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == normalized_unit + + def test_device_classes_aligned() -> None: """Make sure all sensor device classes are also available in NumberDeviceClass.""" diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index e3a29291109..fd79e0dda66 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -159,7 +159,7 @@ async def test_temperature_conversion_wrong_device_class( assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() - # Check temperature is not converted + # Check compatible unit is applied state = hass.states.get(entity0.entity_id) assert state.state == "0.0" assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.FAHRENHEIT