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.
This commit is contained in:
commit
66464291e1
33 files changed
+2215
No files matched your search
@@ -0,0 +1,108 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user