Warn if numeric sensors have an invalid value (#85863)

Co-authored-by: mib1185 <mail@mib85.de>
This commit is contained in:
epenet 2023-01-16 11:00:07 +01:00 committed by GitHub
parent ccd8bc14e0
commit 3179101fbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 163 additions and 1 deletions

View File

@ -401,7 +401,12 @@ class SensorEntity(Entity):
native_unit_of_measurement = self.native_unit_of_measurement native_unit_of_measurement = self.native_unit_of_measurement
unit_of_measurement = self.unit_of_measurement unit_of_measurement = self.unit_of_measurement
value = self.native_value value = self.native_value
device_class = self.device_class device_class: SensorDeviceClass | None = None
with suppress(ValueError):
# For the sake of validation, we can ignore custom device classes
# (customization and legacy style translations)
device_class = SensorDeviceClass(str(self.device_class))
state_class = self.state_class
# Sensors with device classes indicating a non-numeric value # Sensors with device classes indicating a non-numeric value
# should not have a state class or unit of measurement # should not have a state class or unit of measurement
@ -478,6 +483,29 @@ class SensorEntity(Entity):
f"Sensor {self.entity_id} provides state value '{value}', " f"Sensor {self.entity_id} provides state value '{value}', "
"which is not in the list of options provided" "which is not in the list of options provided"
) )
return value
# If the sensor has neither a device class, a state class nor
# a unit_of measurement then there are no further checks or conversions
if not device_class and not state_class and not unit_of_measurement:
return value
if not isinstance(value, (int, float, Decimal)):
try:
_ = float(value) # type: ignore[arg-type]
except (TypeError, ValueError):
_LOGGER.warning(
"Sensor %s has device class %s, state class %s and unit %s "
"thus indicating it has a numeric value; however, it has the "
"non-numeric value: %s (%s). This will stop working in 2023.4",
self.entity_id,
device_class,
state_class,
unit_of_measurement,
value,
type(value),
)
return value
if ( if (
native_unit_of_measurement != unit_of_measurement native_unit_of_measurement != unit_of_measurement

View File

@ -1,6 +1,9 @@
"""The test for sensor entity.""" """The test for sensor entity."""
from __future__ import annotations
from datetime import date, datetime, timezone from datetime import date, datetime, timezone
from decimal import Decimal from decimal import Decimal
from typing import Any
import pytest import pytest
from pytest import approx from pytest import approx
@ -1231,3 +1234,134 @@ async def test_device_classes_with_invalid_unit_of_measurement(
"is using native unit of measurement 'INVALID!' which is not a valid " "is using native unit of measurement 'INVALID!' which is not a valid "
f"unit for the device class ('{device_class}') it is using" f"unit for the device class ('{device_class}') it is using"
) in caplog.text ) in caplog.text
@pytest.mark.parametrize(
"device_class,state_class,unit",
[
(SensorDeviceClass.AQI, None, None),
(None, SensorStateClass.MEASUREMENT, None),
(None, None, UnitOfTemperature.CELSIUS),
],
)
@pytest.mark.parametrize(
"native_value,expected",
[
("abc", "abc"),
("13.7.1", "13.7.1"),
(datetime(2012, 11, 10, 7, 35, 1), "2012-11-10 07:35:01"),
(date(2012, 11, 10), "2012-11-10"),
],
)
async def test_non_numeric_validation(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
enable_custom_integrations: None,
native_value: Any,
expected: str,
device_class: SensorDeviceClass | None,
state_class: SensorStateClass | None,
unit: str | None,
) -> None:
"""Test error on expected numeric entities."""
platform = getattr(hass.components, "test.sensor")
platform.init(empty=True)
platform.ENTITIES["0"] = platform.MockSensor(
name="Test",
native_value=native_value,
device_class=device_class,
native_unit_of_measurement=unit,
state_class=state_class,
)
entity0 = platform.ENTITIES["0"]
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
await hass.async_block_till_done()
state = hass.states.get(entity0.entity_id)
assert state.state == expected
assert (
"thus indicating it has a numeric value; "
f"however, it has the non-numeric value: {native_value}"
) in caplog.text
@pytest.mark.parametrize(
"device_class,state_class,unit",
[
(SensorDeviceClass.AQI, None, None),
(None, SensorStateClass.MEASUREMENT, None),
(None, None, UnitOfTemperature.CELSIUS),
],
)
@pytest.mark.parametrize(
"native_value,expected",
[
(13, "13"),
(17.50, "17.5"),
(Decimal(18.50), "18.5"),
("19.70", "19.70"),
(None, STATE_UNKNOWN),
],
)
async def test_numeric_validation(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
enable_custom_integrations: None,
native_value: Any,
expected: str,
device_class: SensorDeviceClass | None,
state_class: SensorStateClass | None,
unit: str | None,
) -> None:
"""Test does not error on expected numeric entities."""
platform = getattr(hass.components, "test.sensor")
platform.init(empty=True)
platform.ENTITIES["0"] = platform.MockSensor(
name="Test",
native_value=native_value,
device_class=device_class,
native_unit_of_measurement=unit,
state_class=state_class,
)
entity0 = platform.ENTITIES["0"]
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
await hass.async_block_till_done()
state = hass.states.get(entity0.entity_id)
assert state.state == expected
assert (
"thus indicating it has a numeric value; "
f"however, it has the non-numeric value: {native_value}"
) not in caplog.text
async def test_numeric_validation_ignores_custom_device_class(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
enable_custom_integrations: None,
) -> None:
"""Test does not error on expected numeric entities."""
native_value = "Three elephants"
platform = getattr(hass.components, "test.sensor")
platform.init(empty=True)
platform.ENTITIES["0"] = platform.MockSensor(
name="Test",
native_value=native_value,
device_class="custom__deviceclass",
)
entity0 = platform.ENTITIES["0"]
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
await hass.async_block_till_done()
state = hass.states.get(entity0.entity_id)
assert state.state == "Three elephants"
assert (
"thus indicating it has a numeric value; "
f"however, it has the non-numeric value: {native_value}"
) not in caplog.text