Files
Paperwall/screens/weather.py
T
Honey 66464291e1 Initial commit: Paperwall, a standalone e-ink dashboard
Navidrome now-playing, clock, and weather for a Waveshare epd4in0e panel,
with a local web UI and install/uninstall scripts for the Pi.
2026-07-21 22:29:45 +02:00

109 lines
3.8 KiB
Python

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