Files
MIND/backend/features/tz_shifter.py
2025-08-25 21:11:10 +02:00

118 lines
3.6 KiB
Python

# -*- coding: utf-8 -*-
"""
Detector of timezone/DST changes and shifts reminder times to counter it.
"""
from __future__ import annotations
import time
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Union
from backend.base.definitions import Constants
from backend.base.logging import LOGGER
from backend.features.reminder_handler import ReminderHandler
from backend.internals.db_models import UserlessRemindersDB
from backend.internals.server import Server
from backend.internals.settings import Settings
if TYPE_CHECKING:
from threading import Timer
def get_timezone_offset() -> int:
"""Get the offset between the local timezone and UTC, in minutes.
Returns:
int: The offset in minutes.
"""
time.tzset()
if time.localtime().tm_isdst == 0:
return -time.timezone
return -time.altzone
class TimezoneChangeHandler:
detector_timer: Union[Timer, None] = None
@staticmethod
def __get_next_trigger_time() -> int:
"""Get the epoch timestamp of the next time a timezone change check
should be done.
Returns:
int: The epoch timestamp.
"""
# Get current time, but set hour and minute to desired trigger time
current_time = datetime.now()
target_time = current_time.replace(
hour=Constants.TZ_CHANGE_CHECK_TIME[0],
minute=Constants.TZ_CHANGE_CHECK_TIME[1]
)
if target_time < current_time:
# Current target time is in the past (trigger time has already been
# today), so run tomorrow at trigger time.
target_time += timedelta(days=1)
return int(target_time.timestamp())
@classmethod
def detect_and_handle_timezone_change(cls) -> None:
"""
Check whether a change in timezone/DST has happened and if so shift
all reminders. Also update DB on last measured timezone.
"""
settings = Settings()
measured_timezone = settings.sv.measured_timezone
current_timezone = get_timezone_offset()
if measured_timezone == -1:
# First measurement, so nothing to compare against
settings.update({"measured_timezone": current_timezone})
elif measured_timezone != current_timezone:
# Timezone change
settings.update({"measured_timezone": current_timezone})
shift_delta = measured_timezone - current_timezone
UserlessRemindersDB().shift(
reminder_id=None,
offset=shift_delta
)
ReminderHandler.set_reminder_timer()
LOGGER.info(
"Detected timezone/DST change (%s to %s), shifted reminders",
measured_timezone, current_timezone
)
cls.detector_timer = None
cls.set_detector_timer()
return
@classmethod
def set_detector_timer(cls) -> None:
"""
Set the timer for checking on a change in timezone and handling it if
so. Doesn't do anything if timer is already set.
"""
if cls.detector_timer is not None:
return
cls.detector_timer = Server().get_db_timer_thread(
cls.__get_next_trigger_time() - time.time(),
cls.detect_and_handle_timezone_change,
"TimezoneChangeHandler"
)
cls.detector_timer.start()
return
@classmethod
def stop_detector_timer(cls) -> None:
"If the timezone change detection timer is running, stop it"
if cls.detector_timer is not None:
cls.detector_timer.cancel()
return