mirror of
https://github.com/home-assistant/core.git
synced 2025-07-11 23:37:18 +00:00
Simplify sensor state validation (#85513)
This commit is contained in:
parent
b86c58b0ea
commit
9eb06fd59d
@ -401,41 +401,6 @@ class SensorEntity(Entity):
|
|||||||
value = self.native_value
|
value = self.native_value
|
||||||
device_class = self.device_class
|
device_class = self.device_class
|
||||||
|
|
||||||
# Received a datetime
|
|
||||||
if value is not None and device_class == SensorDeviceClass.TIMESTAMP:
|
|
||||||
try:
|
|
||||||
# We cast the value, to avoid using isinstance, but satisfy
|
|
||||||
# typechecking. The errors are guarded in this try.
|
|
||||||
value = cast(datetime, value)
|
|
||||||
if value.tzinfo is None:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid datetime: {self.entity_id} provides state '{value}', "
|
|
||||||
"which is missing timezone information"
|
|
||||||
)
|
|
||||||
|
|
||||||
if value.tzinfo != timezone.utc:
|
|
||||||
value = value.astimezone(timezone.utc)
|
|
||||||
|
|
||||||
return value.isoformat(timespec="seconds")
|
|
||||||
except (AttributeError, OverflowError, TypeError) as err:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid datetime: {self.entity_id} has timestamp device class "
|
|
||||||
f"but provides state {value}:{type(value)} resulting in '{err}'"
|
|
||||||
) from err
|
|
||||||
|
|
||||||
# Received a date value
|
|
||||||
if value is not None and device_class == SensorDeviceClass.DATE:
|
|
||||||
try:
|
|
||||||
# We cast the value, to avoid using isinstance, but satisfy
|
|
||||||
# typechecking. The errors are guarded in this try.
|
|
||||||
value = cast(date, value)
|
|
||||||
return value.isoformat()
|
|
||||||
except (AttributeError, TypeError) as err:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid date: {self.entity_id} has date device class "
|
|
||||||
f"but provides state {value}:{type(value)} resulting in '{err}'"
|
|
||||||
) from err
|
|
||||||
|
|
||||||
# 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
|
||||||
if device_class in {
|
if device_class in {
|
||||||
@ -457,10 +422,47 @@ class SensorEntity(Entity):
|
|||||||
f"non-numeric device class: {device_class}"
|
f"non-numeric device class: {device_class}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Checks below only apply if there is a value
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Received a datetime
|
||||||
|
if device_class == SensorDeviceClass.TIMESTAMP:
|
||||||
|
try:
|
||||||
|
# We cast the value, to avoid using isinstance, but satisfy
|
||||||
|
# typechecking. The errors are guarded in this try.
|
||||||
|
value = cast(datetime, value)
|
||||||
|
if value.tzinfo is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid datetime: {self.entity_id} provides state '{value}', "
|
||||||
|
"which is missing timezone information"
|
||||||
|
)
|
||||||
|
|
||||||
|
if value.tzinfo != timezone.utc:
|
||||||
|
value = value.astimezone(timezone.utc)
|
||||||
|
|
||||||
|
return value.isoformat(timespec="seconds")
|
||||||
|
except (AttributeError, OverflowError, TypeError) as err:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid datetime: {self.entity_id} has timestamp device class "
|
||||||
|
f"but provides state {value}:{type(value)} resulting in '{err}'"
|
||||||
|
) from err
|
||||||
|
|
||||||
|
# Received a date value
|
||||||
|
if device_class == SensorDeviceClass.DATE:
|
||||||
|
try:
|
||||||
|
# We cast the value, to avoid using isinstance, but satisfy
|
||||||
|
# typechecking. The errors are guarded in this try.
|
||||||
|
value = cast(date, value)
|
||||||
|
return value.isoformat()
|
||||||
|
except (AttributeError, TypeError) as err:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid date: {self.entity_id} has date device class "
|
||||||
|
f"but provides state {value}:{type(value)} resulting in '{err}'"
|
||||||
|
) from err
|
||||||
|
|
||||||
# Enum checks
|
# Enum checks
|
||||||
if value is not None and (
|
if device_class == SensorDeviceClass.ENUM or self.options is not None:
|
||||||
device_class == SensorDeviceClass.ENUM or self.options is not None
|
|
||||||
):
|
|
||||||
if device_class != SensorDeviceClass.ENUM:
|
if device_class != SensorDeviceClass.ENUM:
|
||||||
reason = "is missing the enum device class"
|
reason = "is missing the enum device class"
|
||||||
if device_class is not None:
|
if device_class is not None:
|
||||||
@ -476,8 +478,7 @@ class SensorEntity(Entity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
value is not None
|
native_unit_of_measurement != unit_of_measurement
|
||||||
and native_unit_of_measurement != unit_of_measurement
|
|
||||||
and device_class in UNIT_CONVERTERS
|
and device_class in UNIT_CONVERTERS
|
||||||
):
|
):
|
||||||
assert unit_of_measurement
|
assert unit_of_measurement
|
||||||
@ -514,7 +515,6 @@ class SensorEntity(Entity):
|
|||||||
# Validate unit of measurement used for sensors with a device class
|
# Validate unit of measurement used for sensors with a device class
|
||||||
if (
|
if (
|
||||||
not self._invalid_unit_of_measurement_reported
|
not self._invalid_unit_of_measurement_reported
|
||||||
and value is not None
|
|
||||||
and device_class
|
and device_class
|
||||||
and (units := DEVICE_CLASS_UNITS.get(device_class)) is not None
|
and (units := DEVICE_CLASS_UNITS.get(device_class)) is not None
|
||||||
and native_unit_of_measurement not in units
|
and native_unit_of_measurement not in units
|
||||||
|
@ -230,25 +230,25 @@ async def test_reject_timezoneless_datetime_str(
|
|||||||
|
|
||||||
|
|
||||||
RESTORE_DATA = {
|
RESTORE_DATA = {
|
||||||
"str": {"native_unit_of_measurement": "°F", "native_value": "abc123"},
|
"str": {"native_unit_of_measurement": None, "native_value": "abc123"},
|
||||||
"int": {"native_unit_of_measurement": "°F", "native_value": 123},
|
"int": {"native_unit_of_measurement": "°F", "native_value": 123},
|
||||||
"float": {"native_unit_of_measurement": "°F", "native_value": 123.0},
|
"float": {"native_unit_of_measurement": "°F", "native_value": 123.0},
|
||||||
"date": {
|
"date": {
|
||||||
"native_unit_of_measurement": "°F",
|
"native_unit_of_measurement": None,
|
||||||
"native_value": {
|
"native_value": {
|
||||||
"__type": "<class 'datetime.date'>",
|
"__type": "<class 'datetime.date'>",
|
||||||
"isoformat": date(2020, 2, 8).isoformat(),
|
"isoformat": date(2020, 2, 8).isoformat(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"datetime": {
|
"datetime": {
|
||||||
"native_unit_of_measurement": "°F",
|
"native_unit_of_measurement": None,
|
||||||
"native_value": {
|
"native_value": {
|
||||||
"__type": "<class 'datetime.datetime'>",
|
"__type": "<class 'datetime.datetime'>",
|
||||||
"isoformat": datetime(2020, 2, 8, 15, tzinfo=timezone.utc).isoformat(),
|
"isoformat": datetime(2020, 2, 8, 15, tzinfo=timezone.utc).isoformat(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"Decimal": {
|
"Decimal": {
|
||||||
"native_unit_of_measurement": "°F",
|
"native_unit_of_measurement": "kWh",
|
||||||
"native_value": {
|
"native_value": {
|
||||||
"__type": "<class 'decimal.Decimal'>",
|
"__type": "<class 'decimal.Decimal'>",
|
||||||
"decimal_str": "123.4",
|
"decimal_str": "123.4",
|
||||||
@ -266,19 +266,38 @@ RESTORE_DATA = {
|
|||||||
|
|
||||||
# None | str | int | float | date | datetime | Decimal:
|
# None | str | int | float | date | datetime | Decimal:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"native_value, native_value_type, expected_extra_data, device_class",
|
"native_value, native_value_type, expected_extra_data, device_class, uom",
|
||||||
[
|
[
|
||||||
("abc123", str, RESTORE_DATA["str"], None),
|
("abc123", str, RESTORE_DATA["str"], None, None),
|
||||||
(123, int, RESTORE_DATA["int"], SensorDeviceClass.TEMPERATURE),
|
(
|
||||||
(123.0, float, RESTORE_DATA["float"], SensorDeviceClass.TEMPERATURE),
|
123,
|
||||||
(date(2020, 2, 8), dict, RESTORE_DATA["date"], SensorDeviceClass.DATE),
|
int,
|
||||||
|
RESTORE_DATA["int"],
|
||||||
|
SensorDeviceClass.TEMPERATURE,
|
||||||
|
UnitOfTemperature.FAHRENHEIT,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
123.0,
|
||||||
|
float,
|
||||||
|
RESTORE_DATA["float"],
|
||||||
|
SensorDeviceClass.TEMPERATURE,
|
||||||
|
UnitOfTemperature.FAHRENHEIT,
|
||||||
|
),
|
||||||
|
(date(2020, 2, 8), dict, RESTORE_DATA["date"], SensorDeviceClass.DATE, None),
|
||||||
(
|
(
|
||||||
datetime(2020, 2, 8, 15, tzinfo=timezone.utc),
|
datetime(2020, 2, 8, 15, tzinfo=timezone.utc),
|
||||||
dict,
|
dict,
|
||||||
RESTORE_DATA["datetime"],
|
RESTORE_DATA["datetime"],
|
||||||
SensorDeviceClass.TIMESTAMP,
|
SensorDeviceClass.TIMESTAMP,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Decimal("123.4"),
|
||||||
|
dict,
|
||||||
|
RESTORE_DATA["Decimal"],
|
||||||
|
SensorDeviceClass.ENERGY,
|
||||||
|
UnitOfEnergy.KILO_WATT_HOUR,
|
||||||
),
|
),
|
||||||
(Decimal("123.4"), dict, RESTORE_DATA["Decimal"], SensorDeviceClass.ENERGY),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_restore_sensor_save_state(
|
async def test_restore_sensor_save_state(
|
||||||
@ -289,6 +308,7 @@ async def test_restore_sensor_save_state(
|
|||||||
native_value_type,
|
native_value_type,
|
||||||
expected_extra_data,
|
expected_extra_data,
|
||||||
device_class,
|
device_class,
|
||||||
|
uom,
|
||||||
):
|
):
|
||||||
"""Test RestoreSensor."""
|
"""Test RestoreSensor."""
|
||||||
platform = getattr(hass.components, "test.sensor")
|
platform = getattr(hass.components, "test.sensor")
|
||||||
@ -296,7 +316,7 @@ async def test_restore_sensor_save_state(
|
|||||||
platform.ENTITIES["0"] = platform.MockRestoreSensor(
|
platform.ENTITIES["0"] = platform.MockRestoreSensor(
|
||||||
name="Test",
|
name="Test",
|
||||||
native_value=native_value,
|
native_value=native_value,
|
||||||
native_unit_of_measurement=TEMP_FAHRENHEIT,
|
native_unit_of_measurement=uom,
|
||||||
device_class=device_class,
|
device_class=device_class,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -318,23 +338,23 @@ async def test_restore_sensor_save_state(
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"native_value, native_value_type, extra_data, device_class, uom",
|
"native_value, native_value_type, extra_data, device_class, uom",
|
||||||
[
|
[
|
||||||
("abc123", str, RESTORE_DATA["str"], None, "°F"),
|
("abc123", str, RESTORE_DATA["str"], None, None),
|
||||||
(123, int, RESTORE_DATA["int"], SensorDeviceClass.TEMPERATURE, "°F"),
|
(123, int, RESTORE_DATA["int"], SensorDeviceClass.TEMPERATURE, "°F"),
|
||||||
(123.0, float, RESTORE_DATA["float"], SensorDeviceClass.TEMPERATURE, "°F"),
|
(123.0, float, RESTORE_DATA["float"], SensorDeviceClass.TEMPERATURE, "°F"),
|
||||||
(date(2020, 2, 8), date, RESTORE_DATA["date"], SensorDeviceClass.DATE, "°F"),
|
(date(2020, 2, 8), date, RESTORE_DATA["date"], SensorDeviceClass.DATE, None),
|
||||||
(
|
(
|
||||||
datetime(2020, 2, 8, 15, tzinfo=timezone.utc),
|
datetime(2020, 2, 8, 15, tzinfo=timezone.utc),
|
||||||
datetime,
|
datetime,
|
||||||
RESTORE_DATA["datetime"],
|
RESTORE_DATA["datetime"],
|
||||||
SensorDeviceClass.TIMESTAMP,
|
SensorDeviceClass.TIMESTAMP,
|
||||||
"°F",
|
None,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Decimal("123.4"),
|
Decimal("123.4"),
|
||||||
Decimal,
|
Decimal,
|
||||||
RESTORE_DATA["Decimal"],
|
RESTORE_DATA["Decimal"],
|
||||||
SensorDeviceClass.ENERGY,
|
SensorDeviceClass.ENERGY,
|
||||||
"°F",
|
"kWh",
|
||||||
),
|
),
|
||||||
(None, type(None), None, None, None),
|
(None, type(None), None, None, None),
|
||||||
(None, type(None), {}, None, None),
|
(None, type(None), {}, None, None),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user