Skip to content

Commit

Permalink
Time range should be treated as open ended (#77660)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
amitfin and emontnemery authored Sep 2, 2022
1 parent 6b361b7 commit 32e4a25
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 42 deletions.
27 changes: 17 additions & 10 deletions homeassistant/components/schedule/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,14 +291,17 @@ def _update(self, _: datetime | None = None) -> None:
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.
Expand All @@ -319,11 +322,15 @@ def _update(self, _: datetime | None = None) -> None:
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
Expand Down
175 changes: 143 additions & 32 deletions tests/components/schedule/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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"},
}
}
},
Expand All @@ -272,39 +261,164 @@ async def test_adjacent(
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.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-09-05T01: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-11T23: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_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-05T00:00:00-07:00"
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-05T01:00:00-07:00"
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-11T23:00:00-07:00"
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
for event in state_changes:
assert event.data["new_state"].state == STATE_ON
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(
Expand Down Expand Up @@ -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"


Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 32e4a25

Please sign in to comment.