Navidrome now-playing, clock, and weather for a Waveshare epd4in0e panel, with a local web UI and install/uninstall scripts for the Pi.
109 lines
3.8 KiB
Python
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
|