From c2bab03f42d1082ef4c624b76f4f481ceaa40b08 Mon Sep 17 00:00:00 2001 From: Roland Date: Fri, 13 Mar 2026 07:08:35 -0600 Subject: [PATCH] Checking local time for download window. --- app/src/components/admin/Settings.js | 19 ++++--- wrolpi/common.py | 14 ++++- wrolpi/downloader.py | 7 ++- wrolpi/test/test_downloader.py | 83 ++++++++++++++++++++-------- 4 files changed, 88 insertions(+), 35 deletions(-) diff --git a/app/src/components/admin/Settings.js b/app/src/components/admin/Settings.js index def32ab0..35ef48f5 100644 --- a/app/src/components/admin/Settings.js +++ b/app/src/components/admin/Settings.js @@ -508,15 +508,6 @@ export function SettingsPage() { -
- handleInputChange(null, 'download_on_startup', checked)} - /> -
-
+
Download settings
+
+ handleInputChange(null, 'download_on_startup', checked)} + /> +
+ diff --git a/wrolpi/common.py b/wrolpi/common.py index e270c5b5..debe6f47 100644 --- a/wrolpi/common.py +++ b/wrolpi/common.py @@ -1022,9 +1022,19 @@ class WROLPiConfig(ConfigFile): def download_wait(self, value: int): self.update({'download_wait': value}) + @staticmethod + def _normalize_time_value(value) -> str: + """YAML may parse HH:MM values as sexagesimal integers (e.g. 21:00 -> 1260). Convert back to HH:MM.""" + if value is None: + return None + if isinstance(value, int): + hours, minutes = divmod(value, 60) + return f'{hours:02d}:{minutes:02d}' + return str(value) + @property def download_window_start(self) -> str: - return self._config.get('download_window_start') + return self._normalize_time_value(self._config.get('download_window_start')) @download_window_start.setter def download_window_start(self, value: str): @@ -1032,7 +1042,7 @@ class WROLPiConfig(ConfigFile): @property def download_window_end(self) -> str: - return self._config.get('download_window_end') + return self._normalize_time_value(self._config.get('download_window_end')) @download_window_end.setter def download_window_end(self, value: str): diff --git a/wrolpi/downloader.py b/wrolpi/downloader.py index 008bdd44..9f644738 100644 --- a/wrolpi/downloader.py +++ b/wrolpi/downloader.py @@ -595,6 +595,11 @@ class DownloadManager: self.stopped.clear() self.disabled.clear() + @staticmethod + def _get_local_time(): + """Return the current local time, converting from UTC.""" + return now().astimezone().time() + def is_within_download_window(self) -> bool: """Returns True if current time is within the configured download window, or no window is configured.""" config = get_wrolpi_config() @@ -603,7 +608,7 @@ class DownloadManager: if not start or not end: return True - current = now().time() + current = self._get_local_time() start_time = datetime.strptime(start, "%H:%M").time() end_time = datetime.strptime(end, "%H:%M").time() diff --git a/wrolpi/test/test_downloader.py b/wrolpi/test/test_downloader.py index d1b27562..fdf9d286 100644 --- a/wrolpi/test/test_downloader.py +++ b/wrolpi/test/test_downloader.py @@ -2,7 +2,7 @@ import asyncio import json import pathlib from abc import ABC -from datetime import datetime +from datetime import datetime, time from http import HTTPStatus from itertools import zip_longest from unittest import mock @@ -1061,58 +1061,58 @@ def test_is_within_download_window_no_config(test_download_manager): assert test_download_manager.is_within_download_window() is True -def test_is_within_download_window_same_day(test_download_manager, fake_now): +def test_is_within_download_window_same_day(test_download_manager): """Same-day window (e.g. 08:00-17:00) boundary checks.""" config = get_wrolpi_config() config.download_window_start = '08:00' config.download_window_end = '17:00' # Before window - fake_now(datetime(2000, 1, 1, 7, 59)) - assert test_download_manager.is_within_download_window() is False + with mock.patch.object(type(test_download_manager), '_get_local_time', return_value=time(7, 59)): + assert test_download_manager.is_within_download_window() is False # Start of window - fake_now(datetime(2000, 1, 1, 8, 0)) - assert test_download_manager.is_within_download_window() is True + with mock.patch.object(type(test_download_manager), '_get_local_time', return_value=time(8, 0)): + assert test_download_manager.is_within_download_window() is True # Middle of window - fake_now(datetime(2000, 1, 1, 12, 0)) - assert test_download_manager.is_within_download_window() is True + with mock.patch.object(type(test_download_manager), '_get_local_time', return_value=time(12, 0)): + assert test_download_manager.is_within_download_window() is True # End of window (exclusive) - fake_now(datetime(2000, 1, 1, 17, 0)) - assert test_download_manager.is_within_download_window() is False + with mock.patch.object(type(test_download_manager), '_get_local_time', return_value=time(17, 0)): + assert test_download_manager.is_within_download_window() is False # After window - fake_now(datetime(2000, 1, 1, 23, 0)) - assert test_download_manager.is_within_download_window() is False + with mock.patch.object(type(test_download_manager), '_get_local_time', return_value=time(23, 0)): + assert test_download_manager.is_within_download_window() is False -def test_is_within_download_window_overnight(test_download_manager, fake_now): +def test_is_within_download_window_overnight(test_download_manager): """Overnight window (e.g. 22:00-06:00) wrapping.""" config = get_wrolpi_config() config.download_window_start = '22:00' config.download_window_end = '06:00' # Before window (daytime) - fake_now(datetime(2000, 1, 1, 12, 0)) - assert test_download_manager.is_within_download_window() is False + with mock.patch.object(type(test_download_manager), '_get_local_time', return_value=time(12, 0)): + assert test_download_manager.is_within_download_window() is False # Just before window start - fake_now(datetime(2000, 1, 1, 21, 59)) - assert test_download_manager.is_within_download_window() is False + with mock.patch.object(type(test_download_manager), '_get_local_time', return_value=time(21, 59)): + assert test_download_manager.is_within_download_window() is False # Start of window - fake_now(datetime(2000, 1, 1, 22, 0)) - assert test_download_manager.is_within_download_window() is True + with mock.patch.object(type(test_download_manager), '_get_local_time', return_value=time(22, 0)): + assert test_download_manager.is_within_download_window() is True # Middle of night - fake_now(datetime(2000, 1, 2, 3, 0)) - assert test_download_manager.is_within_download_window() is True + with mock.patch.object(type(test_download_manager), '_get_local_time', return_value=time(3, 0)): + assert test_download_manager.is_within_download_window() is True # End of window (exclusive) - fake_now(datetime(2000, 1, 2, 6, 0)) - assert test_download_manager.is_within_download_window() is False + with mock.patch.object(type(test_download_manager), '_get_local_time', return_value=time(6, 0)): + assert test_download_manager.is_within_download_window() is False def test_download_window_renews_download(test_session, test_download_manager, test_downloader): @@ -1138,3 +1138,40 @@ def test_download_window_renews_download(test_session, test_download_manager, te assert download.status == 'new' assert 'outside download window' in download.error + + +def test_download_window_yaml_sexagesimal(): + """YAML parses some HH:MM values as sexagesimal integers. The config normalizes them back to strings.""" + from wrolpi.common import WROLPiConfig + assert WROLPiConfig._normalize_time_value(None) is None + assert WROLPiConfig._normalize_time_value('07:00') == '07:00' + assert WROLPiConfig._normalize_time_value('21:00') == '21:00' + # YAML parses 21:00 as 1260 (sexagesimal) + assert WROLPiConfig._normalize_time_value(1260) == '21:00' + # YAML parses 17:00 as 1020 + assert WROLPiConfig._normalize_time_value(1020) == '17:00' + # YAML parses 22:30 as 1350 + assert WROLPiConfig._normalize_time_value(1350) == '22:30' + + +def test_is_within_download_window_uses_local_time(test_download_manager): + """Download window should compare against local time, not UTC. + + Bug: now() returns UTC. At 1:00 AM MDT (UTC-6), UTC is 07:00. + With a window of 07:00-20:00, the old code incorrectly returned True + because it compared UTC 07:00 against the window. The local time is + 01:00, which is outside the window. + """ + config = get_wrolpi_config() + config.download_window_start = '07:00' + config.download_window_end = '20:00' + + # Simulate local time of 01:00 AM (outside the 07:00-20:00 window). + with mock.patch.object(type(test_download_manager), '_get_local_time', + return_value=time(1, 0)): + assert test_download_manager.is_within_download_window() is False + + # Simulate local time of 12:00 PM (inside the window). + with mock.patch.object(type(test_download_manager), '_get_local_time', + return_value=time(12, 0)): + assert test_download_manager.is_within_download_window() is True