fix: reset budgets using state file mtime when server was stopped across the daily reset
The old per-device last_check timestamp was only persisted while a device was consuming budget, so a stop/restart across days left stale budgets. Check the state file mtime instead: if it predates today's 7 AM and we're past 7 AM, the server was offline during the daily reset, so give every device a fresh budget. Also drops the redundant 2-minute reset window and the persisted last_check field.
This commit is contained in:
53
devices.py
53
devices.py
@ -5,7 +5,7 @@ import subprocess
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import config
|
import config
|
||||||
@ -152,12 +152,11 @@ def _get_budget(device: str) -> float:
|
|||||||
|
|
||||||
|
|
||||||
def _set_budget(device: str, budget_seconds: float):
|
def _set_budget(device: str, budget_seconds: float):
|
||||||
"""Set budget and track last check time for accurate elapsed calculation."""
|
"""Persist the remaining budget for a device."""
|
||||||
state = _load_state()
|
state = _load_state()
|
||||||
if device not in state:
|
if device not in state:
|
||||||
state[device] = {"budget": float(BUDGET_S)}
|
state[device] = {"budget": float(BUDGET_S)}
|
||||||
state[device]["budget"] = budget_seconds
|
state[device]["budget"] = budget_seconds
|
||||||
state[device]["last_check"] = time.time() # track for elapsed calculation
|
|
||||||
_save_state(state)
|
_save_state(state)
|
||||||
|
|
||||||
|
|
||||||
@ -270,17 +269,31 @@ def _shutdown_devices(device_ids: list[str]):
|
|||||||
|
|
||||||
_timer_running = False
|
_timer_running = False
|
||||||
_timer_lock = threading.Lock()
|
_timer_lock = threading.Lock()
|
||||||
|
_last_check_times: dict[str, float] = {} # in-memory only, not persisted
|
||||||
|
|
||||||
|
|
||||||
|
def _reset_budgets_if_new_day():
|
||||||
|
"""Reset all budgets if today's 7 AM daily reset was missed.
|
||||||
|
|
||||||
|
The state file is only written while the server is running, so its
|
||||||
|
mtime is the last time the server did anything. If that's before
|
||||||
|
today's 7 AM (and it's now past 7 AM), the server was stopped across
|
||||||
|
the daily reset — give every device a fresh budget.
|
||||||
|
"""
|
||||||
|
if not STATE_FILE.exists():
|
||||||
|
return
|
||||||
|
now = datetime.now()
|
||||||
|
today_reset = now.replace(hour=7, minute=0, second=0)
|
||||||
|
mtime = datetime.fromtimestamp(STATE_FILE.stat().st_mtime)
|
||||||
|
if mtime < today_reset <= now:
|
||||||
|
for dev_id in DEVICES:
|
||||||
|
_reset_budget(dev_id)
|
||||||
|
|
||||||
|
|
||||||
def _budget_tick():
|
def _budget_tick():
|
||||||
"""Called every 10 seconds by the background timer."""
|
"""Called every 10 seconds by the background timer."""
|
||||||
now = datetime.now()
|
# New day and we were offline at 7 AM? Reset all budgets to full.
|
||||||
reset_time = now.replace(hour=7, minute=0, second=0)
|
_reset_budgets_if_new_day()
|
||||||
|
|
||||||
# Reset all budgets at 7:00 AM each day
|
|
||||||
if now >= reset_time and now < reset_time + timedelta(minutes=2):
|
|
||||||
for dev_id in DEVICES:
|
|
||||||
_reset_budget(dev_id)
|
|
||||||
|
|
||||||
# Check all devices in parallel
|
# Check all devices in parallel
|
||||||
online_status = {}
|
online_status = {}
|
||||||
@ -299,20 +312,29 @@ def _budget_tick():
|
|||||||
|
|
||||||
# Process results sequentially (budget operations need to be thread-safe)
|
# Process results sequentially (budget operations need to be thread-safe)
|
||||||
with _timer_lock:
|
with _timer_lock:
|
||||||
|
current_time = time.time() # float timestamp for precise elapsed calculation
|
||||||
|
|
||||||
|
# Initialize last check times on first run (server restart)
|
||||||
|
if not _last_check_times:
|
||||||
|
for dev_id in DEVICES:
|
||||||
|
_last_check_times[dev_id] = current_time
|
||||||
|
|
||||||
if not is_curfew_allowed():
|
if not is_curfew_allowed():
|
||||||
# Curfew in effect — turn off all online devices in parallel
|
# Curfew in effect — turn off all online devices in parallel
|
||||||
online_devices = [dev_id for dev_id, online in online_status.items() if online]
|
online_devices = [dev_id for dev_id, online in online_status.items() if online]
|
||||||
_shutdown_devices(online_devices)
|
_shutdown_devices(online_devices)
|
||||||
|
# Update last check times even during curfew
|
||||||
|
for dev_id in DEVICES:
|
||||||
|
_last_check_times[dev_id] = current_time
|
||||||
return
|
return
|
||||||
|
|
||||||
current_time = time.time() # float timestamp for precise elapsed calculation
|
|
||||||
devices_to_shutdown = []
|
devices_to_shutdown = []
|
||||||
|
|
||||||
for dev_id, online in online_status.items():
|
for dev_id, online in online_status.items():
|
||||||
dev_info = DEVICES[dev_id]
|
dev_info = DEVICES[dev_id]
|
||||||
state = _load_state()
|
state = _load_state()
|
||||||
current_budget = state.get(dev_id, {}).get("budget", float(BUDGET_S))
|
current_budget = state.get(dev_id, {}).get("budget", float(BUDGET_S))
|
||||||
last_check = state.get(dev_id, {}).get("last_check", current_time)
|
last_check = _last_check_times.get(dev_id, current_time)
|
||||||
|
|
||||||
# Expired budget + device back online → shut it down
|
# Expired budget + device back online → shut it down
|
||||||
if current_budget <= 0 and online:
|
if current_budget <= 0 and online:
|
||||||
@ -331,8 +353,11 @@ def _budget_tick():
|
|||||||
if new_budget <= 0:
|
if new_budget <= 0:
|
||||||
devices_to_shutdown.append(dev_id)
|
devices_to_shutdown.append(dev_id)
|
||||||
else:
|
else:
|
||||||
# Always update last_check even when offline
|
# Always update last_check even when offline (in-memory only)
|
||||||
_set_budget(dev_id, current_budget)
|
pass
|
||||||
|
|
||||||
|
# Update last check time for next interval
|
||||||
|
_last_check_times[dev_id] = current_time
|
||||||
|
|
||||||
# Shutdown all devices that hit zero budget in parallel
|
# Shutdown all devices that hit zero budget in parallel
|
||||||
if devices_to_shutdown:
|
if devices_to_shutdown:
|
||||||
|
|||||||
Reference in New Issue
Block a user