diff --git a/devices.py b/devices.py index 5521388..f62ae58 100644 --- a/devices.py +++ b/devices.py @@ -5,7 +5,7 @@ import subprocess import threading import time from concurrent.futures import ThreadPoolExecutor, as_completed -from datetime import datetime, timedelta +from datetime import datetime from pathlib import Path import config @@ -152,12 +152,11 @@ def _get_budget(device: str) -> 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() if device not in state: state[device] = {"budget": float(BUDGET_S)} state[device]["budget"] = budget_seconds - state[device]["last_check"] = time.time() # track for elapsed calculation _save_state(state) @@ -270,17 +269,31 @@ def _shutdown_devices(device_ids: list[str]): _timer_running = False _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(): """Called every 10 seconds by the background timer.""" - now = datetime.now() - reset_time = now.replace(hour=7, minute=0, second=0) - - # 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) + # New day and we were offline at 7 AM? Reset all budgets to full. + _reset_budgets_if_new_day() # Check all devices in parallel online_status = {} @@ -299,20 +312,29 @@ def _budget_tick(): # Process results sequentially (budget operations need to be thread-safe) 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(): # Curfew in effect — turn off all online devices in parallel online_devices = [dev_id for dev_id, online in online_status.items() if online] _shutdown_devices(online_devices) + # Update last check times even during curfew + for dev_id in DEVICES: + _last_check_times[dev_id] = current_time return - current_time = time.time() # float timestamp for precise elapsed calculation devices_to_shutdown = [] for dev_id, online in online_status.items(): dev_info = DEVICES[dev_id] state = _load_state() 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 if current_budget <= 0 and online: @@ -331,8 +353,11 @@ def _budget_tick(): if new_budget <= 0: devices_to_shutdown.append(dev_id) else: - # Always update last_check even when offline - _set_budget(dev_id, current_budget) + # Always update last_check even when offline (in-memory only) + pass + + # Update last check time for next interval + _last_check_times[dev_id] = current_time # Shutdown all devices that hit zero budget in parallel if devices_to_shutdown: