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()], []) todays_schedule = self._config.get(WEEKDAY_TO_CONF[now.weekday()], [])
# Determine current schedule state # Determine current schedule state
self._attr_state = next( for time_range in todays_schedule:
( # The current time should be greater or equal to CONF_FROM.
STATE_ON if now.time() < time_range[CONF_FROM]:
for time_range in todays_schedule continue
if time_range[CONF_FROM] <= now.time() <= time_range[CONF_TO] # 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.
STATE_OFF, 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 # Find next event in the schedule, loop over each day (starting with
# the current day) until the next event has been found. # the current day) until the next event has been found.
@ -319,11 +322,15 @@ class Schedule(Entity):
if next_event := next( if next_event := next(
( (
possible_next_event possible_next_event
for time in times for timestamp in times
if ( if (
possible_next_event := ( possible_next_event := (
datetime.combine(now.date(), time, tzinfo=now.tzinfo) datetime.combine(now.date(), timestamp, tzinfo=now.tzinfo)
+ timedelta(days=day) + 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 > now

View file

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