Correct rest sensor configured to generate timestamps (#61429)

This commit is contained in:
Erik Montnemery 2021-12-10 18:59:27 +01:00 committed by GitHub
parent e0cb7dad31
commit aa36dde148
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 155 additions and 33 deletions

View file

@ -11,8 +11,10 @@ from homeassistant.components.sensor import (
CONF_STATE_CLASS, CONF_STATE_CLASS,
DOMAIN as SENSOR_DOMAIN, DOMAIN as SENSOR_DOMAIN,
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity, SensorEntity,
) )
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE, CONF_FORCE_UPDATE,
@ -186,4 +188,13 @@ class RestSensor(RestEntity, SensorEntity):
value, None value, None
) )
self._state = value if value is None or self.device_class not in (
SensorDeviceClass.DATE,
SensorDeviceClass.TIMESTAMP,
):
self._state = value
return
self._state = async_parse_date_datetime(
value, self.entity_id, self.device_class
)

View file

@ -0,0 +1,38 @@
"""Helpers for sensor entities."""
from __future__ import annotations
from datetime import date, datetime
import logging
from homeassistant.core import callback
from homeassistant.util import dt as dt_util
from . import SensorDeviceClass
_LOGGER = logging.getLogger(__name__)
@callback
def async_parse_date_datetime(
value: str, entity_id: str, device_class: SensorDeviceClass | str | None
) -> datetime | date | None:
"""Parse datetime string to a data or datetime."""
if device_class == SensorDeviceClass.TIMESTAMP:
if (parsed_timestamp := dt_util.parse_datetime(value)) is None:
_LOGGER.warning("%s rendered invalid timestamp: %s", entity_id, value)
return None
if parsed_timestamp.tzinfo is None:
_LOGGER.warning(
"%s rendered timestamp without timezone: %s", entity_id, value
)
return None
return parsed_timestamp
# Date device class
if (parsed_date := dt_util.parse_date(value)) is not None:
return parsed_date
_LOGGER.warning("%s rendered invalid date %s", entity_id, value)
return None

View file

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import date, datetime from datetime import date, datetime
import logging
from typing import Any from typing import Any
import voluptuous as vol import voluptuous as vol
@ -17,6 +16,7 @@ from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
) )
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_ENTITY_ID,
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
@ -35,7 +35,6 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers import config_validation as cv, template
from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.util import dt as dt_util
from .const import ( from .const import (
CONF_ATTRIBUTE_TEMPLATES, CONF_ATTRIBUTE_TEMPLATES,
@ -89,7 +88,6 @@ LEGACY_SENSOR_SCHEMA = vol.All(
} }
), ),
) )
_LOGGER = logging.getLogger(__name__)
def extra_validation_checks(val): def extra_validation_checks(val):
@ -184,32 +182,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
) )
@callback
def _async_parse_date_datetime(
value: str, entity_id: str, device_class: SensorDeviceClass | str | None
) -> datetime | date | None:
"""Parse datetime."""
if device_class == SensorDeviceClass.TIMESTAMP:
if (parsed_timestamp := dt_util.parse_datetime(value)) is None:
_LOGGER.warning("%s rendered invalid timestamp: %s", entity_id, value)
return None
if parsed_timestamp.tzinfo is None:
_LOGGER.warning(
"%s rendered timestamp without timezone: %s", entity_id, value
)
return None
return parsed_timestamp
# Date device class
if (parsed_date := dt_util.parse_date(value)) is not None:
return parsed_date
_LOGGER.warning("%s rendered invalid date %s", entity_id, value)
return None
class SensorTemplate(TemplateEntity, SensorEntity): class SensorTemplate(TemplateEntity, SensorEntity):
"""Representation of a Template Sensor.""" """Representation of a Template Sensor."""
@ -269,7 +241,7 @@ class SensorTemplate(TemplateEntity, SensorEntity):
self._attr_native_value = result self._attr_native_value = result
return return
self._attr_native_value = _async_parse_date_datetime( self._attr_native_value = async_parse_date_datetime(
result, self.entity_id, self.device_class result, self.entity_id, self.device_class
) )
@ -303,6 +275,6 @@ class TriggerSensorEntity(TriggerEntity, SensorEntity):
): ):
return return
self._rendered[CONF_STATE] = _async_parse_date_datetime( self._rendered[CONF_STATE] = async_parse_date_datetime(
state, self.entity_id, self.device_class state, self.entity_id, self.device_class
) )

View file

@ -16,6 +16,7 @@ from homeassistant.const import (
CONTENT_TYPE_JSON, CONTENT_TYPE_JSON,
DATA_MEGABYTES, DATA_MEGABYTES,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
SERVICE_RELOAD, SERVICE_RELOAD,
STATE_UNKNOWN, STATE_UNKNOWN,
TEMP_CELSIUS, TEMP_CELSIUS,
@ -218,6 +219,68 @@ async def test_setup_get(hass):
assert state.attributes[sensor.ATTR_STATE_CLASS] == sensor.STATE_CLASS_MEASUREMENT assert state.attributes[sensor.ATTR_STATE_CLASS] == sensor.STATE_CLASS_MEASUREMENT
@respx.mock
async def test_setup_timestamp(hass, caplog):
"""Test setup with valid configuration."""
respx.get("http://localhost").respond(
status_code=HTTPStatus.OK, json={"key": "2021-11-11 11:39Z"}
)
assert await async_setup_component(
hass,
"sensor",
{
"sensor": {
"platform": "rest",
"resource": "http://localhost",
"method": "GET",
"value_template": "{{ value_json.key }}",
"device_class": DEVICE_CLASS_TIMESTAMP,
"state_class": sensor.STATE_CLASS_MEASUREMENT,
}
},
)
await async_setup_component(hass, "homeassistant", {})
await hass.async_block_till_done()
assert len(hass.states.async_all("sensor")) == 1
state = hass.states.get("sensor.rest_sensor")
assert state.state == "2021-11-11T11:39:00+00:00"
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TIMESTAMP
assert "sensor.rest_sensor rendered invalid timestamp" not in caplog.text
assert "sensor.rest_sensor rendered timestamp without timezone" not in caplog.text
# Bad response: Not a timestamp
respx.get("http://localhost").respond(
status_code=HTTPStatus.OK, json={"key": "invalid time stamp"}
)
await hass.services.async_call(
"homeassistant",
"update_entity",
{ATTR_ENTITY_ID: ["sensor.rest_sensor"]},
blocking=True,
)
state = hass.states.get("sensor.rest_sensor")
assert state.state == "unknown"
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TIMESTAMP
assert "sensor.rest_sensor rendered invalid timestamp" in caplog.text
# Bad response: No timezone
respx.get("http://localhost").respond(
status_code=HTTPStatus.OK, json={"key": "2021-10-11 11:39"}
)
await hass.services.async_call(
"homeassistant",
"update_entity",
{ATTR_ENTITY_ID: ["sensor.rest_sensor"]},
blocking=True,
)
state = hass.states.get("sensor.rest_sensor")
assert state.state == "unknown"
assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TIMESTAMP
assert "sensor.rest_sensor rendered timestamp without timezone" in caplog.text
@respx.mock @respx.mock
async def test_setup_get_templated_headers_params(hass): async def test_setup_get_templated_headers_params(hass):
"""Test setup with valid configuration.""" """Test setup with valid configuration."""

View file

@ -0,0 +1,38 @@
"""The test for sensor helpers."""
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.sensor.helpers import async_parse_date_datetime
def test_async_parse_datetime(caplog):
"""Test async_parse_date_datetime."""
entity_id = "sensor.timestamp"
device_class = SensorDeviceClass.TIMESTAMP
assert (
async_parse_date_datetime(
"2021-12-12 12:12Z", entity_id, device_class
).isoformat()
== "2021-12-12T12:12:00+00:00"
)
assert not caplog.text
# No timezone
assert (
async_parse_date_datetime("2021-12-12 12:12", entity_id, device_class) is None
)
assert "sensor.timestamp rendered timestamp without timezone" in caplog.text
# Invalid timestamp
assert async_parse_date_datetime("12 past 12", entity_id, device_class) is None
assert "sensor.timestamp rendered invalid timestamp: 12 past 12" in caplog.text
device_class = SensorDeviceClass.DATE
caplog.clear()
assert (
async_parse_date_datetime("2021-12-12", entity_id, device_class).isoformat()
== "2021-12-12"
)
assert not caplog.text
# Invalid date
assert async_parse_date_datetime("December 12th", entity_id, device_class) is None
assert "sensor.timestamp rendered invalid date December 12th" in caplog.text

View file

@ -1,4 +1,4 @@
"""The test for sensor device automation.""" """The test for sensor entity."""
from datetime import date, datetime, timezone from datetime import date, datetime, timezone
import pytest import pytest