Add datetime object as valid StateType (#52671)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>pull/59896/head
parent
92ca94e915
commit
01efe1eba2
|
@ -1,5 +1,7 @@
|
|||
"""Counter for the days until an HTTPS (TLS) certificate will expire."""
|
||||
from datetime import timedelta
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -85,8 +87,8 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity):
|
|||
self._attr_unique_id = f"{coordinator.host}:{coordinator.port}-timestamp"
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
def native_value(self) -> datetime | None:
|
||||
"""Return the state of the sensor."""
|
||||
if self.coordinator.data:
|
||||
return self.coordinator.data.isoformat()
|
||||
return self.coordinator.data
|
||||
return None
|
||||
|
|
|
@ -4,11 +4,12 @@ from __future__ import annotations
|
|||
from collections.abc import Mapping
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
import inspect
|
||||
import logging
|
||||
from typing import Any, Final, cast, final
|
||||
|
||||
import ciso8601
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
@ -182,6 +183,9 @@ class SensorEntity(Entity):
|
|||
_last_reset_reported = False
|
||||
_temperature_conversion_reported = False
|
||||
|
||||
# Temporary private attribute to track if deprecation has been logged.
|
||||
__datetime_as_string_deprecation_logged = False
|
||||
|
||||
@property
|
||||
def state_class(self) -> str | None:
|
||||
"""Return the state class of this entity, from STATE_CLASSES, if any."""
|
||||
|
@ -236,7 +240,7 @@ class SensorEntity(Entity):
|
|||
return None
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
def native_value(self) -> StateType | date | datetime:
|
||||
"""Return the value reported by the sensor."""
|
||||
return self._attr_native_value
|
||||
|
||||
|
@ -273,6 +277,61 @@ class SensorEntity(Entity):
|
|||
"""Return the state of the sensor and perform unit conversions, if needed."""
|
||||
unit_of_measurement = self.native_unit_of_measurement
|
||||
value = self.native_value
|
||||
device_class = self.device_class
|
||||
|
||||
# We have an old non-datetime value, warn about it and convert it during
|
||||
# the deprecation period.
|
||||
if (
|
||||
value is not None
|
||||
and device_class in (DEVICE_CLASS_DATE, DEVICE_CLASS_TIMESTAMP)
|
||||
and not isinstance(value, (date, datetime))
|
||||
):
|
||||
# Deprecation warning for date/timestamp device classes
|
||||
if not self.__datetime_as_string_deprecation_logged:
|
||||
report_issue = self._suggest_report_issue()
|
||||
_LOGGER.warning(
|
||||
"%s is providing a string for its state, while the device "
|
||||
"class is '%s', this is not valid and will be unsupported "
|
||||
"from Home Assistant 2022.2. Please %s",
|
||||
self.entity_id,
|
||||
device_class,
|
||||
report_issue,
|
||||
)
|
||||
self.__datetime_as_string_deprecation_logged = True
|
||||
|
||||
# Anyways, lets validate the date at least..
|
||||
try:
|
||||
value = ciso8601.parse_datetime(str(value))
|
||||
except (ValueError, IndexError) as error:
|
||||
raise ValueError(
|
||||
f"Invalid date/datetime: {self.entity_id} provide state '{value}', "
|
||||
f"while it has device class '{device_class}'"
|
||||
) from error
|
||||
|
||||
# Convert the date object to a standardized state string.
|
||||
if device_class == DEVICE_CLASS_DATE:
|
||||
return value.date().isoformat()
|
||||
return value.isoformat(timespec="seconds")
|
||||
|
||||
# Received a datetime
|
||||
if value is not None and device_class == DEVICE_CLASS_TIMESTAMP:
|
||||
try:
|
||||
return value.isoformat(timespec="seconds") # type: ignore
|
||||
except (AttributeError, TypeError) as err:
|
||||
raise ValueError(
|
||||
f"Invalid datetime: {self.entity_id} has a timestamp device class"
|
||||
f"but does not provide a datetime state but {type(value)}"
|
||||
) from err
|
||||
|
||||
# Received a date value
|
||||
if value is not None and device_class == DEVICE_CLASS_DATE:
|
||||
try:
|
||||
return value.isoformat() # type: ignore
|
||||
except (AttributeError, TypeError) as err:
|
||||
raise ValueError(
|
||||
f"Invalid date: {self.entity_id} has a date device class"
|
||||
f"but does not provide a date state but {type(value)}"
|
||||
) from err
|
||||
|
||||
units = self.hass.config.units
|
||||
if (
|
||||
|
@ -304,7 +363,7 @@ class SensorEntity(Entity):
|
|||
prec = len(value_s) - value_s.index(".") - 1 if "." in value_s else 0
|
||||
# Suppress ValueError (Could not convert sensor_value to float)
|
||||
with suppress(ValueError):
|
||||
temp = units.temperature(float(value), unit_of_measurement)
|
||||
temp = units.temperature(float(value), unit_of_measurement) # type: ignore
|
||||
value = round(temp) if prec == 0 else round(temp, prec)
|
||||
|
||||
return value
|
||||
|
|
|
@ -210,44 +210,44 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
|
|||
)
|
||||
self._assert_sensor(
|
||||
"sensor.picnic_selected_slot_start",
|
||||
"2021-03-03T14:45:00.000+01:00",
|
||||
"2021-03-03T14:45:00+01:00",
|
||||
cls=DEVICE_CLASS_TIMESTAMP,
|
||||
)
|
||||
self._assert_sensor(
|
||||
"sensor.picnic_selected_slot_end",
|
||||
"2021-03-03T15:45:00.000+01:00",
|
||||
"2021-03-03T15:45:00+01:00",
|
||||
cls=DEVICE_CLASS_TIMESTAMP,
|
||||
)
|
||||
self._assert_sensor(
|
||||
"sensor.picnic_selected_slot_max_order_time",
|
||||
"2021-03-02T22:00:00.000+01:00",
|
||||
"2021-03-02T22:00:00+01:00",
|
||||
cls=DEVICE_CLASS_TIMESTAMP,
|
||||
)
|
||||
self._assert_sensor("sensor.picnic_selected_slot_min_order_value", "35.0")
|
||||
self._assert_sensor(
|
||||
"sensor.picnic_last_order_slot_start",
|
||||
"2021-02-26T20:15:00.000+01:00",
|
||||
"2021-02-26T20:15:00+01:00",
|
||||
cls=DEVICE_CLASS_TIMESTAMP,
|
||||
)
|
||||
self._assert_sensor(
|
||||
"sensor.picnic_last_order_slot_end",
|
||||
"2021-02-26T21:15:00.000+01:00",
|
||||
"2021-02-26T21:15:00+01:00",
|
||||
cls=DEVICE_CLASS_TIMESTAMP,
|
||||
)
|
||||
self._assert_sensor("sensor.picnic_last_order_status", "COMPLETED")
|
||||
self._assert_sensor(
|
||||
"sensor.picnic_last_order_eta_start",
|
||||
"2021-02-26T20:54:00.000+01:00",
|
||||
"2021-02-26T20:54:00+01:00",
|
||||
cls=DEVICE_CLASS_TIMESTAMP,
|
||||
)
|
||||
self._assert_sensor(
|
||||
"sensor.picnic_last_order_eta_end",
|
||||
"2021-02-26T21:14:00.000+01:00",
|
||||
"2021-02-26T21:14:00+01:00",
|
||||
cls=DEVICE_CLASS_TIMESTAMP,
|
||||
)
|
||||
self._assert_sensor(
|
||||
"sensor.picnic_last_order_delivery_time",
|
||||
"2021-02-26T20:54:05.221+01:00",
|
||||
"2021-02-26T20:54:05+01:00",
|
||||
cls=DEVICE_CLASS_TIMESTAMP,
|
||||
)
|
||||
self._assert_sensor(
|
||||
|
@ -305,10 +305,10 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
|
|||
# Assert delivery time is not available, but eta is
|
||||
self._assert_sensor("sensor.picnic_last_order_delivery_time", STATE_UNAVAILABLE)
|
||||
self._assert_sensor(
|
||||
"sensor.picnic_last_order_eta_start", "2021-02-26T20:54:00.000+01:00"
|
||||
"sensor.picnic_last_order_eta_start", "2021-02-26T20:54:00+01:00"
|
||||
)
|
||||
self._assert_sensor(
|
||||
"sensor.picnic_last_order_eta_end", "2021-02-26T21:14:00.000+01:00"
|
||||
"sensor.picnic_last_order_eta_end", "2021-02-26T21:14:00+01:00"
|
||||
)
|
||||
|
||||
async def test_sensors_use_detailed_eta_if_available(self):
|
||||
|
@ -333,10 +333,10 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase):
|
|||
delivery_response["delivery_id"]
|
||||
)
|
||||
self._assert_sensor(
|
||||
"sensor.picnic_last_order_eta_start", "2021-03-05T11:19:20.452+01:00"
|
||||
"sensor.picnic_last_order_eta_start", "2021-03-05T11:19:20+01:00"
|
||||
)
|
||||
self._assert_sensor(
|
||||
"sensor.picnic_last_order_eta_end", "2021-03-05T11:39:20.452+01:00"
|
||||
"sensor.picnic_last_order_eta_end", "2021-03-05T11:39:20+01:00"
|
||||
)
|
||||
|
||||
async def test_sensors_no_data(self):
|
||||
|
|
|
@ -147,7 +147,7 @@ def _check_state(hass, category, entity_id):
|
|||
event_index = CATEGORIES_TO_EVENTS[category]
|
||||
event = TEST_EVENTS[event_index]
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == event.time
|
||||
assert state.state == dt.parse_datetime(event.time).isoformat()
|
||||
assert state.attributes["category_id"] == event.category_id
|
||||
assert state.attributes["category_name"] == event.category_name
|
||||
assert state.attributes["type_id"] == event.type_id
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
"""The test for sensor device automation."""
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
import pytest
|
||||
from pytest import approx
|
||||
|
||||
from homeassistant.components.sensor import SensorEntityDescription
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_CLASS_DATE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_TIMESTAMP,
|
||||
STATE_UNKNOWN,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
|
@ -107,3 +112,68 @@ async def test_deprecated_unit_of_measurement(hass, caplog, enable_custom_integr
|
|||
"tests.components.sensor.test_init is setting 'unit_of_measurement' on an "
|
||||
"instance of SensorEntityDescription"
|
||||
) in caplog.text
|
||||
|
||||
|
||||
async def test_datetime_conversion(hass, caplog, enable_custom_integrations):
|
||||
"""Test conversion of datetime."""
|
||||
test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=timezone.utc)
|
||||
test_date = date(2017, 12, 19)
|
||||
platform = getattr(hass.components, "test.sensor")
|
||||
platform.init(empty=True)
|
||||
platform.ENTITIES["0"] = platform.MockSensor(
|
||||
name="Test", native_value=test_timestamp, device_class=DEVICE_CLASS_TIMESTAMP
|
||||
)
|
||||
platform.ENTITIES["1"] = platform.MockSensor(
|
||||
name="Test", native_value=test_date, device_class=DEVICE_CLASS_DATE
|
||||
)
|
||||
platform.ENTITIES["2"] = platform.MockSensor(
|
||||
name="Test", native_value=None, device_class=DEVICE_CLASS_TIMESTAMP
|
||||
)
|
||||
platform.ENTITIES["3"] = platform.MockSensor(
|
||||
name="Test", native_value=None, device_class=DEVICE_CLASS_DATE
|
||||
)
|
||||
|
||||
assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(platform.ENTITIES["0"].entity_id)
|
||||
assert state.state == test_timestamp.isoformat()
|
||||
|
||||
state = hass.states.get(platform.ENTITIES["1"].entity_id)
|
||||
assert state.state == test_date.isoformat()
|
||||
|
||||
state = hass.states.get(platform.ENTITIES["2"].entity_id)
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
state = hass.states.get(platform.ENTITIES["3"].entity_id)
|
||||
assert state.state == STATE_UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_class,native_value",
|
||||
[
|
||||
(DEVICE_CLASS_DATE, "2021-11-09"),
|
||||
(DEVICE_CLASS_TIMESTAMP, "2021-01-09T12:00:00+00:00"),
|
||||
],
|
||||
)
|
||||
async def test_deprecated_datetime_str(
|
||||
hass, caplog, enable_custom_integrations, device_class, native_value
|
||||
):
|
||||
"""Test warning on deprecated str for a date(time) value."""
|
||||
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
|
||||
)
|
||||
|
||||
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 == native_value
|
||||
assert (
|
||||
"is providing a string for its state, while the device class is "
|
||||
f"'{device_class}', this is not valid and will be unsupported "
|
||||
"from Home Assistant 2022.2."
|
||||
) in caplog.text
|
||||
|
|
|
@ -78,6 +78,12 @@ def mirobo_is_got_error_fixture():
|
|||
mock_vacuum.status().battery = 82
|
||||
mock_vacuum.status().clean_area = 123.43218
|
||||
mock_vacuum.status().clean_time = timedelta(hours=2, minutes=35, seconds=34)
|
||||
mock_vacuum.last_clean_details().start = datetime(
|
||||
2020, 4, 1, 13, 21, 10, tzinfo=dt_util.UTC
|
||||
)
|
||||
mock_vacuum.last_clean_details().end = datetime(
|
||||
2020, 4, 1, 13, 21, 10, tzinfo=dt_util.UTC
|
||||
)
|
||||
mock_vacuum.consumable_status().main_brush_left = timedelta(
|
||||
hours=12, minutes=35, seconds=34
|
||||
)
|
||||
|
@ -136,6 +142,12 @@ def mirobo_old_speeds_fixture(request):
|
|||
mock_vacuum.status().battery = 32
|
||||
mock_vacuum.fan_speed_presets.return_value = request.param
|
||||
mock_vacuum.status().fanspeed = list(request.param.values())[0]
|
||||
mock_vacuum.last_clean_details().start = datetime(
|
||||
2020, 4, 1, 13, 21, 10, tzinfo=dt_util.UTC
|
||||
)
|
||||
mock_vacuum.last_clean_details().end = datetime(
|
||||
2020, 4, 1, 13, 21, 10, tzinfo=dt_util.UTC
|
||||
)
|
||||
|
||||
with patch("homeassistant.components.xiaomi_miio.Vacuum") as mock_vacuum_cls:
|
||||
mock_vacuum_cls.return_value = mock_vacuum
|
||||
|
|
Loading…
Reference in New Issue