diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 69b4d1cecd8..f3f415966ce 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -70,6 +70,7 @@ from .const import ( # noqa: F401 DEVICE_CLASSES, DEVICE_CLASSES_SCHEMA, DOMAIN, + NON_NUMERIC_DEVICE_CLASSES, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL, STATE_CLASS_TOTAL_INCREASING, @@ -246,6 +247,20 @@ class SensorEntity(Entity): return self.entity_description.device_class return None + @final + @property + def _numeric_state_expected(self) -> bool: + """Return true if the sensor must be numeric.""" + if ( + self.state_class is not None + or self.native_unit_of_measurement is not None + or self.native_precision is not None + ): + return True + # Sensors with custom device classes are not considered numeric + device_class = try_parse_enum(SensorDeviceClass, self.device_class) + return not (device_class is None or device_class in NON_NUMERIC_DEVICE_CLASSES) + @property def options(self) -> list[str] | None: """Return a set of possible options.""" @@ -471,15 +486,7 @@ class SensorEntity(Entity): # Sensors with device classes indicating a non-numeric value # should not have a unit of measurement - if ( - device_class - in { - SensorDeviceClass.DATE, - SensorDeviceClass.ENUM, - SensorDeviceClass.TIMESTAMP, - } - and unit_of_measurement - ): + if device_class in NON_NUMERIC_DEVICE_CLASSES and unit_of_measurement: raise ValueError( f"Sensor {self.entity_id} has a unit of measurement and thus " "indicating it has a numeric value; however, it has the " @@ -570,12 +577,7 @@ class SensorEntity(Entity): # If the sensor has neither a device class, a state class, a unit of measurement # nor a precision then there are no further checks or conversions - if ( - not device_class - and not state_class - and not unit_of_measurement - and precision is None - ): + if not self._numeric_state_expected: return value # From here on a numerical value is expected diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index c8402a28ffe..f1b158ab4dd 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -379,6 +379,12 @@ class SensorDeviceClass(StrEnum): """ +NON_NUMERIC_DEVICE_CLASSES = { + SensorDeviceClass.DATE, + SensorDeviceClass.ENUM, + SensorDeviceClass.TIMESTAMP, +} + DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorDeviceClass)) # DEVICE_CLASSES is deprecated as of 2021.12 diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index b0ab204bb57..b4da7f19b5a 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1640,3 +1640,48 @@ async def test_device_classes_with_invalid_state_class( "is using state class 'INVALID!' which is impossible considering device " f"class ('{device_class}') it is using" ) in caplog.text + + +@pytest.mark.parametrize( + "device_class,state_class,native_unit_of_measurement,native_precision,is_numeric", + [ + (SensorDeviceClass.ENUM, None, None, None, False), + (SensorDeviceClass.DATE, None, None, None, False), + (SensorDeviceClass.TIMESTAMP, None, None, None, False), + ("custom", None, None, None, False), + (SensorDeviceClass.POWER, None, "V", None, True), + (None, SensorStateClass.MEASUREMENT, None, None, True), + (None, None, PERCENTAGE, None, True), + (None, None, None, None, False), + ], +) +async def test_numeric_state_expected_helper( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, + device_class: SensorDeviceClass | None, + state_class: SensorStateClass | None, + native_unit_of_measurement: str | None, + native_precision: int | None, + is_numeric: bool, +) -> None: + """Test numeric_state_expected helper.""" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + native_value=None, + device_class=device_class, + state_class=state_class, + native_unit_of_measurement=native_unit_of_measurement, + native_precision=native_precision, + ) + + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + entity0 = platform.ENTITIES["0"] + state = hass.states.get(entity0.entity_id) + assert state is not None + + assert entity0._numeric_state_expected == is_numeric