Time range should be treated as open ended (#77660)

* Time range should be treated as open end

* Refactored the logic of calculating the state

* Improve tests

* Improve tests

Co-authored-by: Erik <erik@montnemery.com>
This commit is contained in:
amitfin 2022-09-02 09:14:06 +03:00 committed by GitHub
parent 6b361b70c9
commit 32e4a2515e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 166 additions and 48 deletions

View file

@ -291,14 +291,17 @@ class Schedule(Entity):
todays_schedule = self._config.get(WEEKDAY_TO_CONF[now.weekday()], [])
# Determine current schedule state
self._attr_state = next(
(
STATE_ON
for time_range in todays_schedule
if time_range[CONF_FROM] <= now.time() <= time_range[CONF_TO]
),
STATE_OFF,
)
for time_range in todays_schedule:
# The current time should be greater or equal to CONF_FROM.
if now.time() < time_range[CONF_FROM]:
continue
# The current time should be smaller (and not equal) to CONF_TO.
# Note that any time in the day is treated as smaller than time.max.
if now.time() < time_range[CONF_TO] or time_range[CONF_TO] == time.max:
self._attr_state = STATE_ON
break
else:
self._attr_state = STATE_OFF
# Find next event in the schedule, loop over each day (starting with
# the current day) until the next event has been found.
@ -319,11 +322,15 @@ class Schedule(Entity):
if next_event := next(
(
possible_next_event
for time in times
for timestamp in times
if (
possible_next_event := (
datetime.combine(now.date(), time, tzinfo=now.tzinfo)
datetime.combine(now.date(), timestamp, tzinfo=now.tzinfo)
+ timedelta(days=day)
if not timestamp == time.max
# Special case for midnight of the following day.
else datetime.combine(now.date(), time(), tzinfo=now.tzinfo)
+ timedelta(days=day + 1)
)
)
> now

View file

@ -221,25 +221,14 @@ async def test_events_one_day(
state = hass.states.get(f"{DOMAIN}.from_yaml")
assert state
assert state.state == STATE_ON
assert state.state == STATE_OFF
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-11T07:00:00-07:00"
@pytest.mark.parametrize(
"sun_schedule, mon_schedule",
(
(
{CONF_FROM: "23:00:00", CONF_TO: "24:00:00"},
{CONF_FROM: "00:00:00", CONF_TO: "01:00:00"},
),
),
)
async def test_adjacent(
async def test_adjacent_cross_midnight(
hass: HomeAssistant,
schedule_setup: Callable[..., Coroutine[Any, Any, bool]],
caplog: pytest.LogCaptureFixture,
sun_schedule: dict[str, str],
mon_schedule: dict[str, str],
freezer,
) -> None:
"""Test adjacent events don't toggle on->off->on."""
@ -251,8 +240,8 @@ async def test_adjacent(
"from_yaml": {
CONF_NAME: "from yaml",
CONF_ICON: "mdi:party-popper",
CONF_SUNDAY: sun_schedule,
CONF_MONDAY: mon_schedule,
CONF_SUNDAY: {CONF_FROM: "23:00:00", CONF_TO: "24:00:00"},
CONF_MONDAY: {CONF_FROM: "00:00:00", CONF_TO: "01:00:00"},
}
}
},
@ -269,17 +258,6 @@ async def test_adjacent(
freezer.move_to(state.attributes[ATTR_NEXT_EVENT])
async_fire_time_changed(hass)
state = hass.states.get(f"{DOMAIN}.from_yaml")
assert state
assert state.state == STATE_ON
assert (
state.attributes[ATTR_NEXT_EVENT].isoformat()
== "2022-09-04T23:59:59.999999-07:00"
)
freezer.move_to(state.attributes[ATTR_NEXT_EVENT])
async_fire_time_changed(hass)
state = hass.states.get(f"{DOMAIN}.from_yaml")
assert state
assert state.state == STATE_ON
@ -298,13 +276,149 @@ async def test_adjacent(
state = hass.states.get(f"{DOMAIN}.from_yaml")
assert state
assert state.state == STATE_ON
assert state.state == STATE_OFF
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-11T23:00:00-07:00"
await hass.async_block_till_done()
assert len(state_changes) == 4
for event in state_changes:
assert len(state_changes) == 3
for event in state_changes[:-1]:
assert event.data["new_state"].state == STATE_ON
assert state_changes[2].data["new_state"].state == STATE_OFF
async def test_adjacent_within_day(
hass: HomeAssistant,
schedule_setup: Callable[..., Coroutine[Any, Any, bool]],
caplog: pytest.LogCaptureFixture,
freezer,
) -> None:
"""Test adjacent events don't toggle on->off->on."""
freezer.move_to("2022-08-30 13:20:00-07:00")
assert await schedule_setup(
config={
DOMAIN: {
"from_yaml": {
CONF_NAME: "from yaml",
CONF_ICON: "mdi:party-popper",
CONF_SUNDAY: [
{CONF_FROM: "22:00:00", CONF_TO: "22:30:00"},
{CONF_FROM: "22:30:00", CONF_TO: "23:00:00"},
],
}
}
},
items=[],
)
state = hass.states.get(f"{DOMAIN}.from_yaml")
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T22:00:00-07:00"
state_changes = async_capture_events(hass, EVENT_STATE_CHANGED)
freezer.move_to(state.attributes[ATTR_NEXT_EVENT])
async_fire_time_changed(hass)
state = hass.states.get(f"{DOMAIN}.from_yaml")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T22:30:00-07:00"
freezer.move_to(state.attributes[ATTR_NEXT_EVENT])
async_fire_time_changed(hass)
state = hass.states.get(f"{DOMAIN}.from_yaml")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T23:00:00-07:00"
freezer.move_to(state.attributes[ATTR_NEXT_EVENT])
async_fire_time_changed(hass)
state = hass.states.get(f"{DOMAIN}.from_yaml")
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-11T22:00:00-07:00"
await hass.async_block_till_done()
assert len(state_changes) == 3
for event in state_changes[:-1]:
assert event.data["new_state"].state == STATE_ON
assert state_changes[2].data["new_state"].state == STATE_OFF
async def test_non_adjacent_within_day(
hass: HomeAssistant,
schedule_setup: Callable[..., Coroutine[Any, Any, bool]],
caplog: pytest.LogCaptureFixture,
freezer,
) -> None:
"""Test adjacent events don't toggle on->off->on."""
freezer.move_to("2022-08-30 13:20:00-07:00")
assert await schedule_setup(
config={
DOMAIN: {
"from_yaml": {
CONF_NAME: "from yaml",
CONF_ICON: "mdi:party-popper",
CONF_SUNDAY: [
{CONF_FROM: "22:00:00", CONF_TO: "22:15:00"},
{CONF_FROM: "22:30:00", CONF_TO: "23:00:00"},
],
}
}
},
items=[],
)
state = hass.states.get(f"{DOMAIN}.from_yaml")
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T22:00:00-07:00"
state_changes = async_capture_events(hass, EVENT_STATE_CHANGED)
freezer.move_to(state.attributes[ATTR_NEXT_EVENT])
async_fire_time_changed(hass)
state = hass.states.get(f"{DOMAIN}.from_yaml")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T22:15:00-07:00"
freezer.move_to(state.attributes[ATTR_NEXT_EVENT])
async_fire_time_changed(hass)
state = hass.states.get(f"{DOMAIN}.from_yaml")
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T22:30:00-07:00"
freezer.move_to(state.attributes[ATTR_NEXT_EVENT])
async_fire_time_changed(hass)
state = hass.states.get(f"{DOMAIN}.from_yaml")
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-04T23:00:00-07:00"
freezer.move_to(state.attributes[ATTR_NEXT_EVENT])
async_fire_time_changed(hass)
state = hass.states.get(f"{DOMAIN}.from_yaml")
assert state
assert state.state == STATE_OFF
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-11T22:00:00-07:00"
await hass.async_block_till_done()
assert len(state_changes) == 4
assert state_changes[0].data["new_state"].state == STATE_ON
assert state_changes[1].data["new_state"].state == STATE_OFF
assert state_changes[2].data["new_state"].state == STATE_ON
assert state_changes[3].data["new_state"].state == STATE_OFF
@pytest.mark.parametrize(
@ -348,17 +462,14 @@ async def test_to_midnight(
state = hass.states.get(f"{DOMAIN}.from_yaml")
assert state
assert state.state == STATE_ON
assert (
state.attributes[ATTR_NEXT_EVENT].isoformat()
== "2022-09-04T23:59:59.999999-07:00"
)
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-05T00:00:00-07:00"
freezer.move_to(state.attributes[ATTR_NEXT_EVENT])
async_fire_time_changed(hass)
state = hass.states.get(f"{DOMAIN}.from_yaml")
assert state
assert state.state == STATE_ON
assert state.state == STATE_OFF
assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-11T00:00:00-07:00"
@ -490,8 +601,8 @@ async def test_ws_delete(
"to, next_event, saved_to",
(
("23:59:59", "2022-08-10T23:59:59-07:00", "23:59:59"),
("24:00", "2022-08-10T23:59:59.999999-07:00", "24:00:00"),
("24:00:00", "2022-08-10T23:59:59.999999-07:00", "24:00:00"),
("24:00", "2022-08-11T00:00:00-07:00", "24:00:00"),
("24:00:00", "2022-08-11T00:00:00-07:00", "24:00:00"),
),
)
async def test_update(
@ -560,8 +671,8 @@ async def test_update(
"to, next_event, saved_to",
(
("14:00:00", "2022-08-15T14:00:00-07:00", "14:00:00"),
("24:00", "2022-08-15T23:59:59.999999-07:00", "24:00:00"),
("24:00:00", "2022-08-15T23:59:59.999999-07:00", "24:00:00"),
("24:00", "2022-08-16T00:00:00-07:00", "24:00:00"),
("24:00:00", "2022-08-16T00:00:00-07:00", "24:00:00"),
),
)
async def test_ws_create(