"""Current weather via Open-Meteo (free, no API key -- just a lat/long).""" import logging import requests from PIL import Image, ImageDraw from screens.base import Screen from screens.render_utils import load_font, text_width logger = logging.getLogger(__name__) HTTP_TIMEOUT = 10 # WMO weather codes -> (German description, symbol). Not exhaustive, covers # the common cases; unmapped codes fall back to a generic description. WMO_CODES = { 0: ("Klarer Himmel", "☀"), 1: ("Überwiegend klar", "☀"), 2: ("Teilweise bewölkt", "⛅"), 3: ("Bedeckt", "☁"), 45: ("Nebel", "☁"), 48: ("Nebel", "☁"), 51: ("Leichter Nieselregen", "☂"), 53: ("Nieselregen", "☂"), 55: ("Starker Nieselregen", "☂"), 61: ("Leichter Regen", "☔"), 63: ("Regen", "☔"), 65: ("Starker Regen", "☔"), 71: ("Leichter Schneefall", "❄"), 73: ("Schneefall", "❄"), 75: ("Starker Schneefall", "❄"), 80: ("Regenschauer", "☔"), 81: ("Regenschauer", "☔"), 82: ("Heftige Regenschauer", "☔"), 85: ("Schneeschauer", "❄"), 86: ("Schneeschauer", "❄"), 95: ("Gewitter", "⚡"), 96: ("Gewitter mit Hagel", "⚡"), 99: ("Gewitter mit Hagel", "⚡"), } class WeatherScreen(Screen): id = "weather" display_name = "Weather" poll_interval = 1800 # 30 minutes -- weather doesn't need to be fresher def default_settings(self) -> dict: return {"latitude": None, "longitude": None} def check(self, settings: dict): lat = settings.get("latitude") lon = settings.get("longitude") if lat is None or lon is None: logger.warning("Weather screen has no latitude/longitude configured") return None response = requests.get( "https://api.open-meteo.com/v1/forecast", params={"latitude": lat, "longitude": lon, "current_weather": "true"}, timeout=HTTP_TIMEOUT, ) response.raise_for_status() current = response.json().get("current_weather") or {} if "temperature" not in current: return None code = int(current.get("weathercode", -1)) description, symbol = WMO_CODES.get(code, ("Unbekannt", "?")) return { "temperature": current["temperature"], "windspeed": current.get("windspeed"), "description": description, "symbol": symbol, } def render(self, state, settings: dict, resolution: tuple[int, int]) -> Image.Image: width, height = resolution image = Image.new("RGB", (width, height), "white") draw = ImageDraw.Draw(image) if not state: font = load_font(size=int(min(width, height) * 0.06)) msg = "Kein Standort konfiguriert" if not ( settings.get("latitude") and settings.get("longitude") ) else "Wetterdaten nicht verfügbar" mw = text_width(draw, msg, font) draw.text(((width - mw) / 2, height / 2), msg, font=font, fill=(120, 120, 120)) return image symbol_font = load_font(size=int(min(width, height) * 0.28)) temp_font = load_font(size=int(min(width, height) * 0.22), bold=True) desc_font = load_font(size=int(min(width, height) * 0.055)) symbol = state["symbol"] sw = text_width(draw, symbol, symbol_font) draw.text(((width - sw) / 2, height * 0.14), symbol, font=symbol_font, fill="black") temp_str = f"{round(state['temperature'])}°" tw = text_width(draw, temp_str, temp_font) draw.text(((width - tw) / 2, height * 0.46), temp_str, font=temp_font, fill="black") desc = state["description"] dw = text_width(draw, desc, desc_font) draw.text(((width - dw) / 2, height * 0.68), desc, font=desc_font, fill=(90, 90, 90)) return image