Checking local time for download window.
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Controller Tests / controller-tests (push) Waiting to run
Jest Tests / jest-tests (push) Waiting to run

This commit is contained in:
Roland 2026-03-13 07:08:35 -06:00
parent 35994bd363
commit c2bab03f42
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
4 changed files with 88 additions and 35 deletions

View file

@ -508,15 +508,6 @@ export function SettingsPage() {
<Grid columns={2} stackable>
<Grid.Row>
<Grid.Column>
<div style={{margin: '0.5em'}}>
<Toggle
label='Download on Startup'
disabled={disabled || state.download_on_startup === null}
checked={state.download_on_startup === true}
onChange={checked => handleInputChange(null, 'download_on_startup', checked)}
/>
</div>
<div style={{margin: '0.5em'}}>
<Toggle
label='CPU Power-save on Startup'
@ -653,8 +644,18 @@ export function SettingsPage() {
<Divider/>
<Header as='h3'>Download settings</Header>
<div style={{margin: '0.5em'}}>
<Toggle
label='Download on Startup'
disabled={disabled || state.download_on_startup === null}
checked={state.download_on_startup === true}
onChange={checked => handleInputChange(null, 'download_on_startup', checked)}
/>
</div>
<Form.Group inline>
<Form.Input
label={<>

View file

@ -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):

View file

@ -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()

View file

@ -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