diff --git a/frigate/api/media.py b/frigate/api/media.py index c9755d5efe..2174d732b6 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -64,6 +64,7 @@ from frigate.util.recording_coverage import ( plan_clip, resolve_coverage, ) +from frigate.util.time import get_normalized_tz_name logger = logging.getLogger(__name__) @@ -837,7 +838,9 @@ async def vod_hour( parts = year_month.split("-") start_date = ( datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=UTC) - - datetime.now(ZoneInfo(tz_name.replace(",", "/"))).utcoffset() + - datetime.now( + ZoneInfo(get_normalized_tz_name(tz_name.replace(",", "/"))) + ).utcoffset() ) end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) start_ts = start_date.timestamp() diff --git a/frigate/api/preview.py b/frigate/api/preview.py index af84117fd6..81de23d003 100644 --- a/frigate/api/preview.py +++ b/frigate/api/preview.py @@ -22,6 +22,7 @@ from frigate.api.defs.response.preview_response import ( from frigate.api.defs.tags import Tags from frigate.const import BASE_DIR, CACHE_DIR, PREVIEW_FRAME_TYPE from frigate.models import Previews +from frigate.util.time import get_normalized_tz_name logger = logging.getLogger(__name__) @@ -126,7 +127,9 @@ def preview_hour( parts = year_month.split("-") start_date = ( datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=UTC) - - datetime.now(ZoneInfo(tz_name.replace(",", "/"))).utcoffset() + - datetime.now( + ZoneInfo(get_normalized_tz_name(tz_name.replace(",", "/"))) + ).utcoffset() ) end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1) start_ts = start_date.timestamp() diff --git a/frigate/record/export.py b/frigate/record/export.py index 5e21e4782b..2ec58d664a 100644 --- a/frigate/record/export.py +++ b/frigate/record/export.py @@ -49,7 +49,7 @@ from frigate.util.recording_coverage import ( stream_media_summary, ) from frigate.util.services import get_video_properties -from frigate.util.time import is_current_hour +from frigate.util.time import get_normalized_tz_name, is_current_hour logger = logging.getLogger(__name__) @@ -874,7 +874,7 @@ class RecordingExporter(threading.Thread): tz_name = self.config.ui.timezone if tz_name: try: - tz = ZoneInfo(tz_name) + tz = ZoneInfo(get_normalized_tz_name(tz_name)) except (ValueError, ZoneInfoNotFoundError): tz = None if tz is not None: @@ -1038,7 +1038,7 @@ class RecordingExporter(threading.Thread): tz: datetime.tzinfo | None = None if tz_name: try: - tz = ZoneInfo(tz_name) + tz = ZoneInfo(get_normalized_tz_name(tz_name)) except (ValueError, ZoneInfoNotFoundError): tz = None if tz is None: diff --git a/frigate/util/time.py b/frigate/util/time.py index 256d70a72a..49524f8683 100644 --- a/frigate/util/time.py +++ b/frigate/util/time.py @@ -3,16 +3,32 @@ import datetime import logging import math -from zoneinfo import ZoneInfo, ZoneInfoNotFoundError +from typing import Final +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError, available_timezones from tzlocal import get_localzone logger = logging.getLogger(__name__) +TIMEZONE_CASEFOLD_INDEX: Final = { + timezone.casefold(): timezone for timezone in available_timezones() +} + + +def get_normalized_tz_name(tz_name: str) -> str: + """Return the canonical name for a case-insensitive IANA timezone.""" + tz = TIMEZONE_CASEFOLD_INDEX.get(tz_name.casefold()) + if tz: + return tz + + raise ValueError(f"Unknown timezone: {tz_name}") + def get_tz_modifiers(tz_name: str) -> tuple[str, str, float]: seconds_offset = ( - datetime.datetime.now(ZoneInfo(tz_name)).utcoffset().total_seconds() + datetime.datetime.now(ZoneInfo(get_normalized_tz_name(tz_name))) + .utcoffset() + .total_seconds() ) hours_offset = int(seconds_offset / 60 / 60) minutes_offset = int(seconds_offset / 60 - hours_offset * 60) @@ -83,7 +99,7 @@ def get_dst_transitions( continuous periods with the same UTC offset """ try: - tz = ZoneInfo(tz_name) + tz = ZoneInfo(get_normalized_tz_name(tz_name)) except (ValueError, ZoneInfoNotFoundError): # If timezone is invalid, return single period with no offset return [(start_time, end_time, 0)]