Allow referencing sensor entities for before/after in time conditions (#51444)

* Allow referencing sensor entities for before/after in time conditions

* Fix typo in variable naming

* Improve test coverage
This commit is contained in:
Franck Nijhof 2021-06-07 14:50:31 +02:00 committed by GitHub
parent 88386a7f44
commit f35929ba63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 130 additions and 13 deletions

View File

@ -16,7 +16,9 @@ from homeassistant.components import zone as zone_cmp
from homeassistant.components.device_automation import ( from homeassistant.components.device_automation import (
async_get_device_automation_platform, async_get_device_automation_platform,
) )
from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_GPS_ACCURACY, ATTR_GPS_ACCURACY,
ATTR_LATITUDE, ATTR_LATITUDE,
ATTR_LONGITUDE, ATTR_LONGITUDE,
@ -736,11 +738,24 @@ def time(
after_entity = hass.states.get(after) after_entity = hass.states.get(after)
if not after_entity: if not after_entity:
raise ConditionErrorMessage("time", f"unknown 'after' entity {after}") raise ConditionErrorMessage("time", f"unknown 'after' entity {after}")
after = dt_util.dt.time( if after_entity.domain == "input_datetime":
after_entity.attributes.get("hour", 23), after = dt_util.dt.time(
after_entity.attributes.get("minute", 59), after_entity.attributes.get("hour", 23),
after_entity.attributes.get("second", 59), after_entity.attributes.get("minute", 59),
) after_entity.attributes.get("second", 59),
)
elif after_entity.attributes.get(
ATTR_DEVICE_CLASS
) == DEVICE_CLASS_TIMESTAMP and after_entity.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
after_datetime = dt_util.parse_datetime(after_entity.state)
if after_datetime is None:
return False
after = dt_util.as_local(after_datetime).time()
else:
return False
if before is None: if before is None:
before = dt_util.dt.time(23, 59, 59, 999999) before = dt_util.dt.time(23, 59, 59, 999999)
@ -748,11 +763,24 @@ def time(
before_entity = hass.states.get(before) before_entity = hass.states.get(before)
if not before_entity: if not before_entity:
raise ConditionErrorMessage("time", f"unknown 'before' entity {before}") raise ConditionErrorMessage("time", f"unknown 'before' entity {before}")
before = dt_util.dt.time( if before_entity.domain == "input_datetime":
before_entity.attributes.get("hour", 23), before = dt_util.dt.time(
before_entity.attributes.get("minute", 59), before_entity.attributes.get("hour", 23),
before_entity.attributes.get("second", 59), before_entity.attributes.get("minute", 59),
) before_entity.attributes.get("second", 59),
)
elif before_entity.attributes.get(
ATTR_DEVICE_CLASS
) == DEVICE_CLASS_TIMESTAMP and before_entity.state not in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
before_timedatime = dt_util.parse_datetime(before_entity.state)
if before_timedatime is None:
return False
before = dt_util.as_local(before_timedatime).time()
else:
return False
if after < before: if after < before:
condition_trace_update_result(after=after, now_time=now_time, before=before) condition_trace_update_result(after=after, now_time=now_time, before=before)

View File

@ -1014,8 +1014,12 @@ TIME_CONDITION_SCHEMA = vol.All(
{ {
**CONDITION_BASE_SCHEMA, **CONDITION_BASE_SCHEMA,
vol.Required(CONF_CONDITION): "time", vol.Required(CONF_CONDITION): "time",
"before": vol.Any(time, vol.All(str, entity_domain("input_datetime"))), "before": vol.Any(
"after": vol.Any(time, vol.All(str, entity_domain("input_datetime"))), time, vol.All(str, entity_domain(["input_datetime", "sensor"]))
),
"after": vol.Any(
time, vol.All(str, entity_domain(["input_datetime", "sensor"]))
),
"weekday": weekdays, "weekday": weekdays,
} }
), ),

View File

@ -6,7 +6,8 @@ import pytest
from homeassistant.components import sun from homeassistant.components import sun
import homeassistant.components.automation as automation import homeassistant.components.automation as automation
from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP
from homeassistant.const import ATTR_DEVICE_CLASS, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET
from homeassistant.exceptions import ConditionError, HomeAssistantError from homeassistant.exceptions import ConditionError, HomeAssistantError
from homeassistant.helpers import condition, trace from homeassistant.helpers import condition, trace
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
@ -826,6 +827,90 @@ async def test_time_using_input_datetime(hass):
condition.time(hass, before="input_datetime.not_existing") condition.time(hass, before="input_datetime.not_existing")
async def test_time_using_sensor(hass):
"""Test time conditions using sensor entities."""
hass.states.async_set(
"sensor.am",
"2021-06-03 13:00:00.000000+00:00", # 6 am local time
{ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP},
)
hass.states.async_set(
"sensor.pm",
"2020-06-01 01:00:00.000000+00:00", # 6 pm local time
{ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP},
)
hass.states.async_set(
"sensor.no_device_class",
"2020-06-01 01:00:00.000000+00:00",
)
hass.states.async_set(
"sensor.invalid_timestamp",
"This is not a timestamp",
{ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP},
)
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt_util.now().replace(hour=3),
):
assert not condition.time(hass, after="sensor.am", before="sensor.pm")
assert condition.time(hass, after="sensor.pm", before="sensor.am")
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt_util.now().replace(hour=9),
):
assert condition.time(hass, after="sensor.am", before="sensor.pm")
assert not condition.time(hass, after="sensor.pm", before="sensor.am")
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt_util.now().replace(hour=15),
):
assert condition.time(hass, after="sensor.am", before="sensor.pm")
assert not condition.time(hass, after="sensor.pm", before="sensor.am")
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt_util.now().replace(hour=21),
):
assert not condition.time(hass, after="sensor.am", before="sensor.pm")
assert condition.time(hass, after="sensor.pm", before="sensor.am")
# Trigger on PM time
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt_util.now().replace(hour=18, minute=0, second=0),
):
assert condition.time(hass, after="sensor.pm", before="sensor.am")
assert not condition.time(hass, after="sensor.am", before="sensor.pm")
assert condition.time(hass, after="sensor.pm")
assert not condition.time(hass, before="sensor.pm")
# Even though valid, the device class is missing
assert not condition.time(hass, after="sensor.no_device_class")
assert not condition.time(hass, before="sensor.no_device_class")
# Trigger on AM time
with patch(
"homeassistant.helpers.condition.dt_util.now",
return_value=dt_util.now().replace(hour=6, minute=0, second=0),
):
assert not condition.time(hass, after="sensor.pm", before="sensor.am")
assert condition.time(hass, after="sensor.am", before="sensor.pm")
assert condition.time(hass, after="sensor.am")
assert not condition.time(hass, before="sensor.am")
assert not condition.time(hass, after="sensor.invalid_timestamp")
assert not condition.time(hass, before="sensor.invalid_timestamp")
with pytest.raises(ConditionError):
condition.time(hass, after="sensor.not_existing")
with pytest.raises(ConditionError):
condition.time(hass, before="sensor.not_existing")
async def test_state_raises(hass): async def test_state_raises(hass):
"""Test that state raises ConditionError on errors.""" """Test that state raises ConditionError on errors."""
# No entity # No entity