screen-locker/screen_locker/_temperature.py
Krzysztof kuhy Rudnicki 67a8cf5b17 Add heat-skip feature and fix early-bird 9:00 re-check gap
Heat skip: if Warsaw temperature >= 32°C at locker startup, a fullscreen
dark-themed dialog asks the user to confirm skipping. Temperature is always
fetched from wttr.in automatically (user cannot self-report). Fail-closed:
API unavailable → no dialog, normal lock. Placed before skip-credit
consumption so credits are preserved when heat skip is used instead.
Logs a heat_skip entry (with temperature) to workout_log.json; does not
count toward weekly minimum. Visible in --status as "Heat skips (month)".

Early-bird gap fix: the re-check timer now fires at both 08:30 (normal
5:00–8:30 window) and 09:05 (extended 5:00–9:00 window earned by 5+
workout weeks). Previously the 08:30 run would see the window still active
on extended weeks and never re-check after 9:00.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QCx1roriuXzFgrzFXtugb
2026-06-29 11:23:19 +02:00

49 lines
1.6 KiB
Python

"""Temperature fetching via wttr.in for the heat-skip feature.
Pure logic — no Tk imports. Always fetches from the API; never trusts
user claims about the temperature.
"""
from __future__ import annotations
import json
import logging
import urllib.error
import urllib.request
_logger = logging.getLogger(__name__)
_WTTR_URL = "https://wttr.in/{city}?format=j1"
_TIMEOUT_SECONDS = 5
def fetch_current_temp_celsius(city: str) -> float | None:
"""Return the current temperature in °C for *city*, or None on failure.
Uses wttr.in's JSON API (no API key required). Returns None on network
errors, timeouts, or unexpected response shapes so callers can fail-closed.
"""
url = _WTTR_URL.format(city=urllib.request.quote(city, safe=""))
try:
with urllib.request.urlopen(url, timeout=_TIMEOUT_SECONDS) as resp:
data = json.loads(resp.read())
temp_str = data["current_condition"][0]["temp_C"]
return float(temp_str)
except (urllib.error.URLError, TimeoutError, OSError) as exc:
_logger.warning("Temperature fetch failed (network): %s", exc)
return None
except (KeyError, IndexError, ValueError, json.JSONDecodeError) as exc:
_logger.warning("Temperature fetch failed (parse): %s", exc)
return None
def is_too_hot(city: str, threshold: float) -> float | None:
"""Return the current temperature if it exceeds *threshold*, else None.
Fail-closed: API unavailable → returns None → no heat skip offered.
"""
temp = fetch_current_temp_celsius(city)
if temp is None:
return None
return temp if temp >= threshold else None