parent
3666af0b10
commit
82ce5e56b5
|
@ -226,6 +226,21 @@ class TodSensor(BinarySensorEntity):
|
|||
self._time_after += self._after_offset
|
||||
self._time_before += self._before_offset
|
||||
|
||||
def _add_one_dst_aware_day(self, a_date: datetime, target_time: time) -> datetime:
|
||||
"""Add 24 hours (1 day) but account for DST."""
|
||||
tentative_new_date = a_date + timedelta(days=1)
|
||||
tentative_new_date = dt_util.as_local(tentative_new_date)
|
||||
tentative_new_date = tentative_new_date.replace(
|
||||
hour=target_time.hour, minute=target_time.minute
|
||||
)
|
||||
# The following call addresses missing time during DST jumps
|
||||
return dt_util.find_next_time_expression_time(
|
||||
tentative_new_date,
|
||||
dt_util.parse_time_expression("*", 0, 59),
|
||||
dt_util.parse_time_expression("*", 0, 59),
|
||||
dt_util.parse_time_expression("*", 0, 23),
|
||||
)
|
||||
|
||||
def _turn_to_next_day(self) -> None:
|
||||
"""Turn to to the next day."""
|
||||
if TYPE_CHECKING:
|
||||
|
@ -238,7 +253,9 @@ class TodSensor(BinarySensorEntity):
|
|||
self._time_after += self._after_offset
|
||||
else:
|
||||
# Offset is already there
|
||||
self._time_after += timedelta(days=1)
|
||||
self._time_after = self._add_one_dst_aware_day(
|
||||
self._time_after, self._after
|
||||
)
|
||||
|
||||
if _is_sun_event(self._before):
|
||||
self._time_before = get_astral_event_next(
|
||||
|
@ -247,7 +264,9 @@ class TodSensor(BinarySensorEntity):
|
|||
self._time_before += self._before_offset
|
||||
else:
|
||||
# Offset is already there
|
||||
self._time_before += timedelta(days=1)
|
||||
self._time_before = self._add_one_dst_aware_day(
|
||||
self._time_before, self._before
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity about to be added to Home Assistant."""
|
||||
|
|
|
@ -614,21 +614,62 @@ async def test_sun_offset(
|
|||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_dst(
|
||||
async def test_dst1(
|
||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory, hass_tz_info
|
||||
) -> None:
|
||||
"""Test sun event with offset."""
|
||||
"""Test DST when time falls in non-existent hour. Also check 48 hours later."""
|
||||
hass.config.time_zone = "CET"
|
||||
dt_util.set_default_time_zone(dt_util.get_time_zone("CET"))
|
||||
test_time = datetime(2019, 3, 30, 3, 0, 0, tzinfo=hass_tz_info)
|
||||
test_time1 = datetime(2019, 3, 30, 3, 0, 0, tzinfo=dt_util.get_time_zone("CET"))
|
||||
test_time2 = datetime(2019, 3, 31, 3, 0, 0, tzinfo=dt_util.get_time_zone("CET"))
|
||||
config = {
|
||||
"binary_sensor": [
|
||||
{"platform": "tod", "name": "Day", "after": "2:30", "before": "2:40"}
|
||||
]
|
||||
}
|
||||
# Test DST:
|
||||
# Test DST #1:
|
||||
# after 2019-03-30 03:00 CET the next update should ge scheduled
|
||||
# at 3:30 not 2:30 local time
|
||||
# at 2:30am, but on 2019-03-31, that hour does not exist. That means
|
||||
# the start/end will end up happning on the next available second (3am)
|
||||
# Essentially, the ToD sensor never turns on that day.
|
||||
entity_id = "binary_sensor.day"
|
||||
freezer.move_to(test_time1)
|
||||
await async_setup_component(hass, "binary_sensor", config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes["after"] == "2019-03-31T03:00:00+02:00"
|
||||
assert state.attributes["before"] == "2019-03-31T03:00:00+02:00"
|
||||
assert state.attributes["next_update"] == "2019-03-31T03:00:00+02:00"
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
# But the following day, the sensor should resume it normal operation.
|
||||
freezer.move_to(test_time2)
|
||||
async_fire_time_changed(hass, dt_util.utcnow())
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes["after"] == "2019-04-01T02:30:00+02:00"
|
||||
assert state.attributes["before"] == "2019-04-01T02:40:00+02:00"
|
||||
assert state.attributes["next_update"] == "2019-04-01T02:30:00+02:00"
|
||||
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
async def test_dst2(hass, freezer, hass_tz_info):
|
||||
"""Test DST when there's a time switch in the East."""
|
||||
hass.config.time_zone = "CET"
|
||||
dt_util.set_default_time_zone(dt_util.get_time_zone("CET"))
|
||||
test_time = datetime(2019, 3, 30, 5, 0, 0, tzinfo=dt_util.get_time_zone("CET"))
|
||||
config = {
|
||||
"binary_sensor": [
|
||||
{"platform": "tod", "name": "Day", "after": "4:30", "before": "4:40"}
|
||||
]
|
||||
}
|
||||
# Test DST #2:
|
||||
# after 2019-03-30 05:00 CET the next update should ge scheduled
|
||||
# at 4:30+02 not 4:30+01
|
||||
entity_id = "binary_sensor.day"
|
||||
freezer.move_to(test_time)
|
||||
await async_setup_component(hass, "binary_sensor", config)
|
||||
|
@ -636,12 +677,150 @@ async def test_dst(
|
|||
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes["after"] == "2019-03-31T03:30:00+02:00"
|
||||
assert state.attributes["before"] == "2019-03-31T03:40:00+02:00"
|
||||
assert state.attributes["next_update"] == "2019-03-31T03:30:00+02:00"
|
||||
assert state.attributes["after"] == "2019-03-31T04:30:00+02:00"
|
||||
assert state.attributes["before"] == "2019-03-31T04:40:00+02:00"
|
||||
assert state.attributes["next_update"] == "2019-03-31T04:30:00+02:00"
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
async def test_dst3(hass, freezer, hass_tz_info):
|
||||
"""Test DST when there's a time switch forward in the West."""
|
||||
hass.config.time_zone = "US/Pacific"
|
||||
dt_util.set_default_time_zone(dt_util.get_time_zone("US/Pacific"))
|
||||
test_time = datetime(
|
||||
2023, 3, 11, 5, 0, 0, tzinfo=dt_util.get_time_zone("US/Pacific")
|
||||
)
|
||||
config = {
|
||||
"binary_sensor": [
|
||||
{"platform": "tod", "name": "Day", "after": "4:30", "before": "4:40"}
|
||||
]
|
||||
}
|
||||
# Test DST #3:
|
||||
# after 2023-03-11 05:00 Pacific the next update should ge scheduled
|
||||
# at 4:30-07 not 4:30-08
|
||||
entity_id = "binary_sensor.day"
|
||||
freezer.move_to(test_time)
|
||||
await async_setup_component(hass, "binary_sensor", config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes["after"] == "2023-03-12T04:30:00-07:00"
|
||||
assert state.attributes["before"] == "2023-03-12T04:40:00-07:00"
|
||||
assert state.attributes["next_update"] == "2023-03-12T04:30:00-07:00"
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
async def test_dst4(hass, freezer, hass_tz_info):
|
||||
"""Test DST when there's a time switch backward in the West."""
|
||||
hass.config.time_zone = "US/Pacific"
|
||||
dt_util.set_default_time_zone(dt_util.get_time_zone("US/Pacific"))
|
||||
test_time = datetime(
|
||||
2023, 11, 4, 5, 0, 0, tzinfo=dt_util.get_time_zone("US/Pacific")
|
||||
)
|
||||
config = {
|
||||
"binary_sensor": [
|
||||
{"platform": "tod", "name": "Day", "after": "4:30", "before": "4:40"}
|
||||
]
|
||||
}
|
||||
# Test DST #4:
|
||||
# after 2023-11-04 05:00 Pacific the next update should ge scheduled
|
||||
# at 4:30-08 not 4:30-07
|
||||
entity_id = "binary_sensor.day"
|
||||
freezer.move_to(test_time)
|
||||
await async_setup_component(hass, "binary_sensor", config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes["after"] == "2023-11-05T04:30:00-08:00"
|
||||
assert state.attributes["before"] == "2023-11-05T04:40:00-08:00"
|
||||
assert state.attributes["next_update"] == "2023-11-05T04:30:00-08:00"
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
|
||||
async def test_dst5(
|
||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory, hass_tz_info
|
||||
) -> None:
|
||||
"""Test DST when end time falls in non-existent hour (1:50am-2:10am)."""
|
||||
hass.config.time_zone = "CET"
|
||||
dt_util.set_default_time_zone(dt_util.get_time_zone("CET"))
|
||||
test_time1 = datetime(2019, 3, 30, 3, 0, 0, tzinfo=dt_util.get_time_zone("CET"))
|
||||
test_time2 = datetime(2019, 3, 31, 1, 51, 0, tzinfo=dt_util.get_time_zone("CET"))
|
||||
config = {
|
||||
"binary_sensor": [
|
||||
{"platform": "tod", "name": "Day", "after": "1:50", "before": "2:10"}
|
||||
]
|
||||
}
|
||||
# Test DST #5:
|
||||
# Test the case where the end time does not exist (roll out to the next available time)
|
||||
# First test before the sensor is turned on
|
||||
entity_id = "binary_sensor.day"
|
||||
freezer.move_to(test_time1)
|
||||
await async_setup_component(hass, "binary_sensor", config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes["after"] == "2019-03-31T01:50:00+01:00"
|
||||
assert state.attributes["before"] == "2019-03-31T03:00:00+02:00"
|
||||
assert state.attributes["next_update"] == "2019-03-31T01:50:00+01:00"
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
# Seconds, test state when sensor is ON but end time has rolled out to next available time.
|
||||
freezer.move_to(test_time2)
|
||||
async_fire_time_changed(hass, dt_util.utcnow())
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes["after"] == "2019-03-31T01:50:00+01:00"
|
||||
assert state.attributes["before"] == "2019-03-31T03:00:00+02:00"
|
||||
assert state.attributes["next_update"] == "2019-03-31T03:00:00+02:00"
|
||||
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_dst6(
|
||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory, hass_tz_info
|
||||
) -> None:
|
||||
"""Test DST when start time falls in non-existent hour (2:50am 3:10am)."""
|
||||
hass.config.time_zone = "CET"
|
||||
dt_util.set_default_time_zone(dt_util.get_time_zone("CET"))
|
||||
test_time1 = datetime(2019, 3, 30, 4, 0, 0, tzinfo=dt_util.get_time_zone("CET"))
|
||||
test_time2 = datetime(2019, 3, 31, 3, 1, 0, tzinfo=dt_util.get_time_zone("CET"))
|
||||
config = {
|
||||
"binary_sensor": [
|
||||
{"platform": "tod", "name": "Day", "after": "2:50", "before": "3:10"}
|
||||
]
|
||||
}
|
||||
# Test DST #6:
|
||||
# Test the case where the end time does not exist (roll out to the next available time)
|
||||
# First test before the sensor is turned on
|
||||
entity_id = "binary_sensor.day"
|
||||
freezer.move_to(test_time1)
|
||||
await async_setup_component(hass, "binary_sensor", config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes["after"] == "2019-03-31T03:00:00+02:00"
|
||||
assert state.attributes["before"] == "2019-03-31T03:10:00+02:00"
|
||||
assert state.attributes["next_update"] == "2019-03-31T03:00:00+02:00"
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
# Seconds, test state when sensor is ON but end time has rolled out to next available time.
|
||||
freezer.move_to(test_time2)
|
||||
async_fire_time_changed(hass, dt_util.utcnow())
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes["after"] == "2019-03-31T03:00:00+02:00"
|
||||
assert state.attributes["before"] == "2019-03-31T03:10:00+02:00"
|
||||
assert state.attributes["next_update"] == "2019-03-31T03:10:00+02:00"
|
||||
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2019-01-10 18:43:00")
|
||||
@pytest.mark.parametrize("hass_time_zone", ("UTC",))
|
||||
async def test_simple_before_after_does_not_loop_utc_not_in_range(
|
||||
|
|
Loading…
Reference in New Issue