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

Co-authored-by: mib1185 <mail@mib85.de>
pull/86072/head
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
unit_of_measurement = self.unit_of_measurement
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
# 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}', "
"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 (
native_unit_of_measurement != unit_of_measurement

View File

@ -1,6 +1,9 @@
"""The test for sensor entity."""
from __future__ import annotations
from datetime import date, datetime, timezone
from decimal import Decimal
from typing import Any
import pytest
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 "
f"unit for the device class ('{device_class}') it is using"
) 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