mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 16:57:10 +00:00
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:
parent
fb55933b0a
commit
2e16b7e2df
@ -70,6 +70,7 @@ from .const import ( # noqa: F401
|
|||||||
DEVICE_CLASSES,
|
DEVICE_CLASSES,
|
||||||
DEVICE_CLASSES_SCHEMA,
|
DEVICE_CLASSES_SCHEMA,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
NON_NUMERIC_DEVICE_CLASSES,
|
||||||
STATE_CLASS_MEASUREMENT,
|
STATE_CLASS_MEASUREMENT,
|
||||||
STATE_CLASS_TOTAL,
|
STATE_CLASS_TOTAL,
|
||||||
STATE_CLASS_TOTAL_INCREASING,
|
STATE_CLASS_TOTAL_INCREASING,
|
||||||
@ -246,6 +247,20 @@ class SensorEntity(Entity):
|
|||||||
return self.entity_description.device_class
|
return self.entity_description.device_class
|
||||||
return None
|
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
|
@property
|
||||||
def options(self) -> list[str] | None:
|
def options(self) -> list[str] | None:
|
||||||
"""Return a set of possible options."""
|
"""Return a set of possible options."""
|
||||||
@ -471,15 +486,7 @@ class SensorEntity(Entity):
|
|||||||
|
|
||||||
# Sensors with device classes indicating a non-numeric value
|
# Sensors with device classes indicating a non-numeric value
|
||||||
# should not have a unit of measurement
|
# should not have a unit of measurement
|
||||||
if (
|
if device_class in NON_NUMERIC_DEVICE_CLASSES and unit_of_measurement:
|
||||||
device_class
|
|
||||||
in {
|
|
||||||
SensorDeviceClass.DATE,
|
|
||||||
SensorDeviceClass.ENUM,
|
|
||||||
SensorDeviceClass.TIMESTAMP,
|
|
||||||
}
|
|
||||||
and unit_of_measurement
|
|
||||||
):
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Sensor {self.entity_id} has a unit of measurement and thus "
|
f"Sensor {self.entity_id} has a unit of measurement and thus "
|
||||||
"indicating it has a numeric value; however, it has the "
|
"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
|
# 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
|
# nor a precision then there are no further checks or conversions
|
||||||
if (
|
if not self._numeric_state_expected:
|
||||||
not device_class
|
|
||||||
and not state_class
|
|
||||||
and not unit_of_measurement
|
|
||||||
and precision is None
|
|
||||||
):
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
# From here on a numerical value is expected
|
# From here on a numerical value is expected
|
||||||
|
@ -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_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorDeviceClass))
|
||||||
|
|
||||||
# DEVICE_CLASSES is deprecated as of 2021.12
|
# DEVICE_CLASSES is deprecated as of 2021.12
|
||||||
|
@ -1640,3 +1640,48 @@ async def test_device_classes_with_invalid_state_class(
|
|||||||
"is using state class 'INVALID!' which is impossible considering device "
|
"is using state class 'INVALID!' which is impossible considering device "
|
||||||
f"class ('{device_class}') it is using"
|
f"class ('{device_class}') it is using"
|
||||||
) in caplog.text
|
) 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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user