2017-04-19 12:09:00 +00:00
|
|
|
"""Helper methods to handle the time in Home Assistant."""
|
2021-03-17 20:46:07 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2015-04-29 02:12:05 +00:00
|
|
|
import datetime as dt
|
2016-04-16 07:55:35 +00:00
|
|
|
import re
|
2021-03-17 20:46:07 +00:00
|
|
|
from typing import Any, cast
|
2016-07-23 18:07:08 +00:00
|
|
|
|
2020-02-24 16:33:10 +00:00
|
|
|
import ciso8601
|
2015-04-29 02:12:05 +00:00
|
|
|
import pytz
|
2018-07-13 10:24:51 +00:00
|
|
|
import pytz.exceptions as pytzexceptions
|
2019-09-04 03:36:04 +00:00
|
|
|
import pytz.tzinfo as pytzinfo
|
2018-10-09 08:14:18 +00:00
|
|
|
|
|
|
|
from homeassistant.const import MATCH_ALL
|
2015-04-29 02:12:05 +00:00
|
|
|
|
2015-06-15 05:56:55 +00:00
|
|
|
DATE_STR_FORMAT = "%Y-%m-%d"
|
2020-10-12 22:43:21 +00:00
|
|
|
NATIVE_UTC = dt.timezone.utc
|
2018-07-13 10:24:51 +00:00
|
|
|
UTC = pytz.utc
|
2019-09-04 03:36:04 +00:00
|
|
|
DEFAULT_TIME_ZONE: dt.tzinfo = pytz.utc
|
2015-04-29 02:12:05 +00:00
|
|
|
|
|
|
|
|
2016-04-16 07:55:35 +00:00
|
|
|
# Copyright (c) Django Software Foundation and individual contributors.
|
|
|
|
# All rights reserved.
|
|
|
|
# https://github.com/django/django/blob/master/LICENSE
|
|
|
|
DATETIME_RE = re.compile(
|
2019-07-31 19:25:30 +00:00
|
|
|
r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})"
|
|
|
|
r"[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
|
|
|
|
r"(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?"
|
|
|
|
r"(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$"
|
2016-04-16 07:55:35 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2016-07-23 18:07:08 +00:00
|
|
|
def set_default_time_zone(time_zone: dt.tzinfo) -> None:
|
2016-10-27 07:16:23 +00:00
|
|
|
"""Set a default time zone to be used when none is specified.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2020-04-04 18:10:55 +00:00
|
|
|
global DEFAULT_TIME_ZONE # pylint: disable=global-statement
|
2015-04-29 02:12:05 +00:00
|
|
|
|
2016-07-23 18:07:08 +00:00
|
|
|
# NOTE: Remove in the future in favour of typing
|
2015-04-29 02:12:05 +00:00
|
|
|
assert isinstance(time_zone, dt.tzinfo)
|
|
|
|
|
|
|
|
DEFAULT_TIME_ZONE = time_zone
|
|
|
|
|
|
|
|
|
2021-03-17 20:46:07 +00:00
|
|
|
def get_time_zone(time_zone_str: str) -> dt.tzinfo | None:
|
2016-10-27 07:16:23 +00:00
|
|
|
"""Get time zone from string. Return None if unable to determine.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
2015-04-29 02:12:05 +00:00
|
|
|
try:
|
|
|
|
return pytz.timezone(time_zone_str)
|
2018-07-13 10:24:51 +00:00
|
|
|
except pytzexceptions.UnknownTimeZoneError:
|
2015-04-29 02:12:05 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
2016-07-23 18:07:08 +00:00
|
|
|
def utcnow() -> dt.datetime:
|
2016-03-07 22:20:48 +00:00
|
|
|
"""Get now in UTC time."""
|
2020-10-12 22:43:21 +00:00
|
|
|
return dt.datetime.now(NATIVE_UTC)
|
2015-04-29 02:12:05 +00:00
|
|
|
|
|
|
|
|
2021-03-17 20:46:07 +00:00
|
|
|
def now(time_zone: dt.tzinfo | None = None) -> dt.datetime:
|
2016-03-07 22:20:48 +00:00
|
|
|
"""Get now in specified time zone."""
|
2015-04-29 05:38:43 +00:00
|
|
|
return dt.datetime.now(time_zone or DEFAULT_TIME_ZONE)
|
2015-04-29 02:12:05 +00:00
|
|
|
|
|
|
|
|
2016-07-23 18:07:08 +00:00
|
|
|
def as_utc(dattim: dt.datetime) -> dt.datetime:
|
2016-03-07 22:20:48 +00:00
|
|
|
"""Return a datetime as UTC time.
|
|
|
|
|
|
|
|
Assumes datetime without tzinfo to be in the DEFAULT_TIME_ZONE.
|
|
|
|
"""
|
2015-06-15 05:56:55 +00:00
|
|
|
if dattim.tzinfo == UTC:
|
2015-04-29 02:12:05 +00:00
|
|
|
return dattim
|
2018-07-23 08:16:05 +00:00
|
|
|
if dattim.tzinfo is None:
|
2018-07-13 10:24:51 +00:00
|
|
|
dattim = DEFAULT_TIME_ZONE.localize(dattim) # type: ignore
|
2015-04-29 02:12:05 +00:00
|
|
|
|
2015-06-15 05:56:55 +00:00
|
|
|
return dattim.astimezone(UTC)
|
2015-04-29 02:12:05 +00:00
|
|
|
|
|
|
|
|
2018-07-23 08:24:39 +00:00
|
|
|
def as_timestamp(dt_value: dt.datetime) -> float:
|
2016-05-07 01:33:46 +00:00
|
|
|
"""Convert a date/time into a unix time (seconds since 1970)."""
|
2016-05-07 17:16:14 +00:00
|
|
|
if hasattr(dt_value, "timestamp"):
|
2021-03-17 20:46:07 +00:00
|
|
|
parsed_dt: dt.datetime | None = dt_value
|
2016-05-07 01:33:46 +00:00
|
|
|
else:
|
|
|
|
parsed_dt = parse_datetime(str(dt_value))
|
2018-07-23 08:24:39 +00:00
|
|
|
if parsed_dt is None:
|
|
|
|
raise ValueError("not a valid date/time.")
|
2016-05-07 17:16:14 +00:00
|
|
|
return parsed_dt.timestamp()
|
2016-05-07 01:33:46 +00:00
|
|
|
|
|
|
|
|
2016-07-23 18:07:08 +00:00
|
|
|
def as_local(dattim: dt.datetime) -> dt.datetime:
|
2016-03-07 22:20:48 +00:00
|
|
|
"""Convert a UTC datetime object to local time zone."""
|
2015-04-29 02:12:05 +00:00
|
|
|
if dattim.tzinfo == DEFAULT_TIME_ZONE:
|
|
|
|
return dattim
|
2018-07-23 08:16:05 +00:00
|
|
|
if dattim.tzinfo is None:
|
2016-02-07 02:31:07 +00:00
|
|
|
dattim = UTC.localize(dattim)
|
2015-04-29 02:12:05 +00:00
|
|
|
|
|
|
|
return dattim.astimezone(DEFAULT_TIME_ZONE)
|
|
|
|
|
|
|
|
|
2016-07-23 18:07:08 +00:00
|
|
|
def utc_from_timestamp(timestamp: float) -> dt.datetime:
|
2016-03-07 22:20:48 +00:00
|
|
|
"""Return a UTC time from a timestamp."""
|
2018-07-13 10:24:51 +00:00
|
|
|
return UTC.localize(dt.datetime.utcfromtimestamp(timestamp))
|
2015-04-29 02:12:05 +00:00
|
|
|
|
|
|
|
|
2021-03-17 20:46:07 +00:00
|
|
|
def start_of_local_day(dt_or_d: dt.date | dt.datetime | None = None) -> dt.datetime:
|
2016-03-07 22:20:48 +00:00
|
|
|
"""Return local datetime object of start of day from date or datetime."""
|
2015-06-15 05:56:55 +00:00
|
|
|
if dt_or_d is None:
|
2019-09-04 03:36:04 +00:00
|
|
|
date: dt.date = now().date()
|
2015-06-15 05:56:55 +00:00
|
|
|
elif isinstance(dt_or_d, dt.datetime):
|
2016-08-07 23:26:35 +00:00
|
|
|
date = dt_or_d.date()
|
2020-11-26 19:20:10 +00:00
|
|
|
else:
|
|
|
|
date = dt_or_d
|
|
|
|
|
2019-07-31 20:08:31 +00:00
|
|
|
return DEFAULT_TIME_ZONE.localize( # type: ignore
|
|
|
|
dt.datetime.combine(date, dt.time())
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2015-06-15 05:56:55 +00:00
|
|
|
|
|
|
|
|
2016-04-16 07:55:35 +00:00
|
|
|
# Copyright (c) Django Software Foundation and individual contributors.
|
|
|
|
# All rights reserved.
|
|
|
|
# https://github.com/django/django/blob/master/LICENSE
|
2021-03-17 20:46:07 +00:00
|
|
|
def parse_datetime(dt_str: str) -> dt.datetime | None:
|
2016-04-16 07:55:35 +00:00
|
|
|
"""Parse a string and return a datetime.datetime.
|
2015-04-29 02:12:05 +00:00
|
|
|
|
2016-04-16 07:55:35 +00:00
|
|
|
This function supports time zone offsets. When the input contains one,
|
|
|
|
the output uses a timezone with a fixed offset from UTC.
|
|
|
|
Raises ValueError if the input is well formatted but not a valid datetime.
|
|
|
|
Returns None if the input isn't well formatted.
|
2015-04-29 02:12:05 +00:00
|
|
|
"""
|
2020-02-24 16:33:10 +00:00
|
|
|
try:
|
|
|
|
return ciso8601.parse_datetime(dt_str)
|
|
|
|
except (ValueError, IndexError):
|
|
|
|
pass
|
2016-04-16 07:55:35 +00:00
|
|
|
match = DATETIME_RE.match(dt_str)
|
|
|
|
if not match:
|
2015-06-15 05:56:55 +00:00
|
|
|
return None
|
2021-03-17 20:46:07 +00:00
|
|
|
kws: dict[str, Any] = match.groupdict()
|
2019-07-31 19:25:30 +00:00
|
|
|
if kws["microsecond"]:
|
|
|
|
kws["microsecond"] = kws["microsecond"].ljust(6, "0")
|
|
|
|
tzinfo_str = kws.pop("tzinfo")
|
2016-08-07 23:26:35 +00:00
|
|
|
|
2021-03-17 20:46:07 +00:00
|
|
|
tzinfo: dt.tzinfo | None = None
|
2019-07-31 19:25:30 +00:00
|
|
|
if tzinfo_str == "Z":
|
2016-04-16 07:55:35 +00:00
|
|
|
tzinfo = UTC
|
2016-07-23 18:07:08 +00:00
|
|
|
elif tzinfo_str is not None:
|
|
|
|
offset_mins = int(tzinfo_str[-2:]) if len(tzinfo_str) > 3 else 0
|
|
|
|
offset_hours = int(tzinfo_str[1:3])
|
2016-04-16 07:55:35 +00:00
|
|
|
offset = dt.timedelta(hours=offset_hours, minutes=offset_mins)
|
2019-07-31 19:25:30 +00:00
|
|
|
if tzinfo_str[0] == "-":
|
2016-04-16 07:55:35 +00:00
|
|
|
offset = -offset
|
|
|
|
tzinfo = dt.timezone(offset)
|
|
|
|
kws = {k: int(v) for k, v in kws.items() if v is not None}
|
2019-07-31 19:25:30 +00:00
|
|
|
kws["tzinfo"] = tzinfo
|
2016-04-16 07:55:35 +00:00
|
|
|
return dt.datetime(**kws)
|
|
|
|
|
|
|
|
|
2021-03-17 20:46:07 +00:00
|
|
|
def parse_date(dt_str: str) -> dt.date | None:
|
2016-03-07 22:20:48 +00:00
|
|
|
"""Convert a date string to a date object."""
|
2015-06-15 05:56:55 +00:00
|
|
|
try:
|
|
|
|
return dt.datetime.strptime(dt_str, DATE_STR_FORMAT).date()
|
2015-04-29 02:12:05 +00:00
|
|
|
except ValueError: # If dt_str did not match our format
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2021-03-17 20:46:07 +00:00
|
|
|
def parse_time(time_str: str) -> dt.time | None:
|
2016-03-07 22:20:48 +00:00
|
|
|
"""Parse a time string (00:20:00) into Time object.
|
|
|
|
|
|
|
|
Return None if invalid.
|
2015-09-15 15:56:06 +00:00
|
|
|
"""
|
2019-07-31 19:25:30 +00:00
|
|
|
parts = str(time_str).split(":")
|
2015-09-15 15:56:06 +00:00
|
|
|
if len(parts) < 2:
|
|
|
|
return None
|
|
|
|
try:
|
|
|
|
hour = int(parts[0])
|
|
|
|
minute = int(parts[1])
|
|
|
|
second = int(parts[2]) if len(parts) > 2 else 0
|
|
|
|
return dt.time(hour, minute, second)
|
|
|
|
except ValueError:
|
|
|
|
# ValueError if value cannot be converted to an int or not in range
|
|
|
|
return None
|
2016-05-10 07:04:53 +00:00
|
|
|
|
|
|
|
|
2016-07-23 18:07:08 +00:00
|
|
|
def get_age(date: dt.datetime) -> str:
|
2016-05-10 07:04:53 +00:00
|
|
|
"""
|
|
|
|
Take a datetime and return its "age" as a string.
|
|
|
|
|
|
|
|
The age can be in second, minute, hour, day, month or year. Only the
|
|
|
|
biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will
|
|
|
|
be returned.
|
|
|
|
Make sure date is not in the future, or else it won't work.
|
|
|
|
"""
|
2019-07-31 19:25:30 +00:00
|
|
|
|
2016-07-23 18:07:08 +00:00
|
|
|
def formatn(number: int, unit: str) -> str:
|
2016-05-10 07:04:53 +00:00
|
|
|
"""Add "unit" if it's plural."""
|
|
|
|
if number == 1:
|
2019-08-23 16:53:33 +00:00
|
|
|
return f"1 {unit}"
|
|
|
|
return f"{number:d} {unit}s"
|
2016-05-10 07:04:53 +00:00
|
|
|
|
2020-07-09 18:19:38 +00:00
|
|
|
delta = (now() - date).total_seconds()
|
|
|
|
rounded_delta = round(delta)
|
2016-05-10 07:04:53 +00:00
|
|
|
|
2020-07-09 18:19:38 +00:00
|
|
|
units = ["second", "minute", "hour", "day", "month"]
|
|
|
|
factors = [60, 60, 24, 30, 12]
|
|
|
|
selected_unit = "year"
|
2016-05-14 19:05:46 +00:00
|
|
|
|
2020-07-09 18:19:38 +00:00
|
|
|
for i, next_factor in enumerate(factors):
|
|
|
|
if rounded_delta < next_factor:
|
|
|
|
selected_unit = units[i]
|
|
|
|
break
|
|
|
|
delta /= next_factor
|
|
|
|
rounded_delta = round(delta)
|
2016-05-14 19:05:46 +00:00
|
|
|
|
2020-07-09 18:19:38 +00:00
|
|
|
return formatn(rounded_delta, selected_unit)
|
2018-10-09 08:14:18 +00:00
|
|
|
|
|
|
|
|
2021-03-17 20:46:07 +00:00
|
|
|
def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> list[int]:
|
2018-10-09 08:14:18 +00:00
|
|
|
"""Parse the time expression part and return a list of times to match."""
|
|
|
|
if parameter is None or parameter == MATCH_ALL:
|
2019-10-07 15:17:39 +00:00
|
|
|
res = list(range(min_value, max_value + 1))
|
2020-10-09 09:48:49 +00:00
|
|
|
elif isinstance(parameter, str):
|
|
|
|
if parameter.startswith("/"):
|
|
|
|
parameter = int(parameter[1:])
|
|
|
|
res = [x for x in range(min_value, max_value + 1) if x % parameter == 0]
|
|
|
|
else:
|
|
|
|
res = [int(parameter)]
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
elif not hasattr(parameter, "__iter__"):
|
2018-10-09 08:14:18 +00:00
|
|
|
res = [int(parameter)]
|
|
|
|
else:
|
2021-03-19 12:41:09 +00:00
|
|
|
res = sorted(int(x) for x in parameter)
|
2018-10-09 08:14:18 +00:00
|
|
|
|
|
|
|
for val in res:
|
|
|
|
if val < min_value or val > max_value:
|
|
|
|
raise ValueError(
|
2020-01-03 13:47:06 +00:00
|
|
|
f"Time expression '{parameter}': parameter {val} out of range "
|
|
|
|
f"({min_value} to {max_value})"
|
2018-10-09 08:14:18 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
def find_next_time_expression_time(
|
2020-05-09 11:08:40 +00:00
|
|
|
now: dt.datetime, # pylint: disable=redefined-outer-name
|
2021-03-17 20:46:07 +00:00
|
|
|
seconds: list[int],
|
|
|
|
minutes: list[int],
|
|
|
|
hours: list[int],
|
2019-07-31 19:25:30 +00:00
|
|
|
) -> dt.datetime:
|
2018-10-09 08:14:18 +00:00
|
|
|
"""Find the next datetime from now for which the time expression matches.
|
|
|
|
|
|
|
|
The algorithm looks at each time unit separately and tries to find the
|
|
|
|
next one that matches for each. If any of them would roll over, all
|
|
|
|
time units below that are reset to the first matching value.
|
|
|
|
|
|
|
|
Timezones are also handled (the tzinfo of the now object is used),
|
|
|
|
including daylight saving time.
|
|
|
|
"""
|
|
|
|
if not seconds or not minutes or not hours:
|
2020-01-02 19:17:10 +00:00
|
|
|
raise ValueError("Cannot find a next time: Time expression never matches!")
|
2018-10-09 08:14:18 +00:00
|
|
|
|
2021-03-17 20:46:07 +00:00
|
|
|
def _lower_bound(arr: list[int], cmp: int) -> int | None:
|
2018-10-09 08:14:18 +00:00
|
|
|
"""Return the first value in arr greater or equal to cmp.
|
|
|
|
|
|
|
|
Return None if no such value exists.
|
|
|
|
"""
|
|
|
|
left = 0
|
|
|
|
right = len(arr)
|
|
|
|
while left < right:
|
|
|
|
mid = (left + right) // 2
|
|
|
|
if arr[mid] < cmp:
|
|
|
|
left = mid + 1
|
|
|
|
else:
|
|
|
|
right = mid
|
|
|
|
|
|
|
|
if left == len(arr):
|
|
|
|
return None
|
|
|
|
return arr[left]
|
|
|
|
|
|
|
|
result = now.replace(microsecond=0)
|
|
|
|
|
|
|
|
# Match next second
|
|
|
|
next_second = _lower_bound(seconds, result.second)
|
|
|
|
if next_second is None:
|
|
|
|
# No second to match in this minute. Roll-over to next minute.
|
|
|
|
next_second = seconds[0]
|
|
|
|
result += dt.timedelta(minutes=1)
|
|
|
|
|
|
|
|
result = result.replace(second=next_second)
|
|
|
|
|
|
|
|
# Match next minute
|
|
|
|
next_minute = _lower_bound(minutes, result.minute)
|
|
|
|
if next_minute != result.minute:
|
|
|
|
# We're in the next minute. Seconds needs to be reset.
|
|
|
|
result = result.replace(second=seconds[0])
|
|
|
|
|
|
|
|
if next_minute is None:
|
|
|
|
# No minute to match in this hour. Roll-over to next hour.
|
|
|
|
next_minute = minutes[0]
|
|
|
|
result += dt.timedelta(hours=1)
|
|
|
|
|
|
|
|
result = result.replace(minute=next_minute)
|
|
|
|
|
|
|
|
# Match next hour
|
|
|
|
next_hour = _lower_bound(hours, result.hour)
|
|
|
|
if next_hour != result.hour:
|
|
|
|
# We're in the next hour. Seconds+minutes needs to be reset.
|
2019-06-22 11:39:33 +00:00
|
|
|
result = result.replace(second=seconds[0], minute=minutes[0])
|
2018-10-09 08:14:18 +00:00
|
|
|
|
|
|
|
if next_hour is None:
|
|
|
|
# No minute to match in this day. Roll-over to next day.
|
|
|
|
next_hour = hours[0]
|
|
|
|
result += dt.timedelta(days=1)
|
|
|
|
|
|
|
|
result = result.replace(hour=next_hour)
|
|
|
|
|
|
|
|
if result.tzinfo is None:
|
|
|
|
return result
|
|
|
|
|
|
|
|
# Now we need to handle timezones. We will make this datetime object
|
|
|
|
# "naive" first and then re-convert it to the target timezone.
|
|
|
|
# This is so that we can call pytz's localize and handle DST changes.
|
2020-10-12 22:43:21 +00:00
|
|
|
tzinfo: pytzinfo.DstTzInfo = UTC if result.tzinfo == NATIVE_UTC else result.tzinfo
|
2018-10-09 08:14:18 +00:00
|
|
|
result = result.replace(tzinfo=None)
|
|
|
|
|
|
|
|
try:
|
|
|
|
result = tzinfo.localize(result, is_dst=None)
|
|
|
|
except pytzexceptions.AmbiguousTimeError:
|
|
|
|
# This happens when we're leaving daylight saving time and local
|
|
|
|
# clocks are rolled back. In this case, we want to trigger
|
|
|
|
# on both the DST and non-DST time. So when "now" is in the DST
|
|
|
|
# use the DST-on time, and if not, use the DST-off time.
|
|
|
|
use_dst = bool(now.dst())
|
|
|
|
result = tzinfo.localize(result, is_dst=use_dst)
|
|
|
|
except pytzexceptions.NonExistentTimeError:
|
|
|
|
# This happens when we're entering daylight saving time and local
|
|
|
|
# clocks are rolled forward, thus there are local times that do
|
|
|
|
# not exist. In this case, we want to trigger on the next time
|
|
|
|
# that *does* exist.
|
|
|
|
# In the worst case, this will run through all the seconds in the
|
|
|
|
# time shift, but that's max 3600 operations for once per year
|
|
|
|
result = result.replace(tzinfo=tzinfo) + dt.timedelta(seconds=1)
|
|
|
|
return find_next_time_expression_time(result, seconds, minutes, hours)
|
|
|
|
|
|
|
|
result_dst = cast(dt.timedelta, result.dst())
|
2020-10-12 22:43:21 +00:00
|
|
|
now_dst = cast(dt.timedelta, now.dst()) or dt.timedelta(0)
|
2018-10-09 08:14:18 +00:00
|
|
|
if result_dst >= now_dst:
|
|
|
|
return result
|
|
|
|
|
|
|
|
# Another edge-case when leaving DST:
|
|
|
|
# When now is in DST and ambiguous *and* the next trigger time we *should*
|
|
|
|
# trigger is ambiguous and outside DST, the excepts above won't catch it.
|
|
|
|
# For example: if triggering on 2:30 and now is 28.10.2018 2:30 (in DST)
|
|
|
|
# we should trigger next on 28.10.2018 2:30 (out of DST), but our
|
|
|
|
# algorithm above would produce 29.10.2018 2:30 (out of DST)
|
|
|
|
|
|
|
|
# Step 1: Check if now is ambiguous
|
|
|
|
try:
|
|
|
|
tzinfo.localize(now.replace(tzinfo=None), is_dst=None)
|
|
|
|
return result
|
|
|
|
except pytzexceptions.AmbiguousTimeError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
# Step 2: Check if result of (now - DST) is ambiguous.
|
|
|
|
check = now - now_dst
|
2019-07-31 19:25:30 +00:00
|
|
|
check_result = find_next_time_expression_time(check, seconds, minutes, hours)
|
2018-10-09 08:14:18 +00:00
|
|
|
try:
|
|
|
|
tzinfo.localize(check_result.replace(tzinfo=None), is_dst=None)
|
|
|
|
return result
|
|
|
|
except pytzexceptions.AmbiguousTimeError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
# OK, edge case does apply. We must override the DST to DST-off
|
2019-07-31 19:25:30 +00:00
|
|
|
check_result = tzinfo.localize(check_result.replace(tzinfo=None), is_dst=False)
|
2018-10-09 08:14:18 +00:00
|
|
|
return check_result
|