Add numeric_state_expected property to Sensor class (#87013)

* Add is_numeric property to Sensor class

* Follw up comment

* Update homeassistant/components/sensor/__init__.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* Update homeassistant/components/sensor/__init__.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* Tests and corrections

* Simplify converion check

* Correct custom device class handling

* Update homeassistant/components/sensor/__init__.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* rename to numeric_state_expected

* Replace with new const

* Adjust docstr

* Update homeassistant/components/sensor/__init__.py

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

* Move to const

* Correct logic

* Do not use bool

* Adjust docstr must be numeric

* remote state from docstr

* protect numeric_state_expected

* Use try_parse_enum for custom class check

* Remove redundant type hints

---------

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
Jan Bouwhuis 2023-02-01 18:45:13 +01:00 committed by GitHub
parent fb55933b0a
commit 2e16b7e2df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 68 additions and 15 deletions

View file

@ -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

View file

@ -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

View file

@ -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