Redesign weather with colorful hand-drawn icons, bigger forecast
New screens/weather_icons.py: vector icons drawn with PIL primitives using the panel's actual 6-color palette (black/white/yellow/red/blue/green, from epd4in0e's own getbuffer() quantization table) -- sun with rays, line-art clouds, blue rain/snow, yellow lightning bolt, fog bands. Replaces the old monochrome unicode-glyph approach entirely (also sidesteps font coverage gaps -- that's what caused the earlier tofu-box bug). Forecast row given significantly more vertical space per request. Caught two real bugs while building this: - Current-conditions block height was guessed independently of the space allocated to it, so it overflowed into the forecast row for real content. Now derives the space it needs from actual measured text/icon size instead of assuming both fit. - `scale * 0.045 if has_forecast else 0.05` -- conditional-expression precedence bug, silently evaluated to a bare 0.05 (not scale * 0.05) in the else branch, crashing font loading with size=0 when forecast is empty. Only surfaced by testing that specific case. - Sun icon's rays extended past the nominal icon_size box used for layout math, throckwing off vertical centering when no forecast is shown.
This commit is contained in:
1 parent
a5fd39f2af
commit
8798466e5b
2 files changed
+173
-56
No files matched your search
+52
-56
@@ -8,6 +8,7 @@ from PIL import Image, ImageDraw
|
||||
|
||||
from screens.base import Screen
|
||||
from screens.render_utils import load_font, text_width
|
||||
from screens.weather_icons import WMO_CODES, draw_weather_icon
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,34 +18,6 @@ FORECAST_DAY_CHOICES = (3, 5, 7)
|
||||
|
||||
WEEKDAY_ABBR_DE = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
|
||||
|
||||
# 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", "☁"), # DejaVu Sans has no ⛅ glyph, falls back to a tofu box
|
||||
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"
|
||||
@@ -107,7 +80,7 @@ class WeatherScreen(Screen):
|
||||
return None
|
||||
|
||||
code = int(current.get("weathercode", -1))
|
||||
description, symbol = WMO_CODES.get(code, ("Unbekannt", "?"))
|
||||
description, kind = WMO_CODES.get(code, ("Unbekannt", "clear"))
|
||||
|
||||
days = []
|
||||
daily = payload.get("daily") or {}
|
||||
@@ -117,10 +90,10 @@ class WeatherScreen(Screen):
|
||||
except ValueError:
|
||||
continue
|
||||
day_code = int(daily["weather_code"][i])
|
||||
_, day_symbol = WMO_CODES.get(day_code, ("Unbekannt", "?"))
|
||||
_, day_kind = WMO_CODES.get(day_code, ("Unbekannt", "clear"))
|
||||
days.append({
|
||||
"label": WEEKDAY_ABBR_DE[weekday],
|
||||
"symbol": day_symbol,
|
||||
"kind": day_kind,
|
||||
"high": daily["temperature_2m_max"][i],
|
||||
"low": daily["temperature_2m_min"][i],
|
||||
})
|
||||
@@ -129,7 +102,7 @@ class WeatherScreen(Screen):
|
||||
"temperature": current["temperature"],
|
||||
"windspeed": current.get("windspeed"),
|
||||
"description": description,
|
||||
"symbol": symbol,
|
||||
"kind": kind,
|
||||
"days": days,
|
||||
}
|
||||
|
||||
@@ -150,49 +123,72 @@ class WeatherScreen(Screen):
|
||||
|
||||
days = state.get("days") or []
|
||||
has_forecast = bool(days)
|
||||
current_h = height * (0.56 if has_forecast else 1.0)
|
||||
|
||||
symbol_font = load_font(size=int(scale * (0.20 if has_forecast else 0.28)))
|
||||
temp_font = load_font(size=int(scale * (0.15 if has_forecast else 0.22)), bold=True)
|
||||
desc_font = load_font(size=int(scale * 0.05))
|
||||
|
||||
symbol = state["symbol"]
|
||||
sw = text_width(draw, symbol, symbol_font)
|
||||
draw.text(((width - sw) / 2, current_h * 0.08), symbol, font=symbol_font, fill="black")
|
||||
# Icon + temperature + description, stacked. Sized from `scale`
|
||||
# directly (not a guessed fraction of current_h) and measured via
|
||||
# actual text bounding boxes, so current_h can be *derived* from
|
||||
# what the block actually needs -- rather than picking current_h
|
||||
# first and hoping the content fits in it, which overflowed into
|
||||
# the forecast row for some font metrics.
|
||||
icon_size = scale * (0.24 if has_forecast else 0.5)
|
||||
temp_font = load_font(size=int(scale * (0.13 if has_forecast else 0.22)), bold=True)
|
||||
desc_font = load_font(size=int(scale * (0.045 if has_forecast else 0.05)))
|
||||
|
||||
temp_str = f"{round(state['temperature'])}°"
|
||||
tw = text_width(draw, temp_str, temp_font)
|
||||
draw.text(((width - tw) / 2, current_h * 0.42), temp_str, font=temp_font, fill="black")
|
||||
|
||||
desc = state["description"]
|
||||
dw = text_width(draw, desc, desc_font)
|
||||
draw.text(((width - dw) / 2, current_h * 0.78), desc, font=desc_font, fill=(90, 90, 90))
|
||||
temp_left, temp_top, temp_right, temp_bottom = draw.textbbox((0, 0), temp_str, font=temp_font)
|
||||
temp_h = temp_bottom - temp_top
|
||||
desc_left, desc_top, desc_right, desc_bottom = draw.textbbox((0, 0), desc, font=desc_font)
|
||||
desc_h = desc_bottom - desc_top
|
||||
|
||||
icon_gap = icon_size * 0.12
|
||||
temp_gap = temp_h * 0.4
|
||||
block_h = icon_size + icon_gap + temp_h + temp_gap + desc_h
|
||||
|
||||
if has_forecast:
|
||||
top_pad = height * 0.04
|
||||
bottom_pad = height * 0.035
|
||||
current_h = block_h + top_pad + bottom_pad
|
||||
block_top = top_pad
|
||||
else:
|
||||
current_h = height
|
||||
block_top = (current_h - block_h) / 2
|
||||
|
||||
icon_cy = block_top + icon_size / 2
|
||||
draw_weather_icon(draw, state["kind"], width / 2, icon_cy, icon_size)
|
||||
|
||||
temp_y = block_top + icon_size + icon_gap
|
||||
tw = temp_right - temp_left
|
||||
draw.text(((width - tw) / 2, temp_y - temp_top), temp_str, font=temp_font, fill="black")
|
||||
|
||||
desc_y = temp_y + temp_h + temp_gap
|
||||
dw = desc_right - desc_left
|
||||
draw.text(((width - dw) / 2, desc_y - desc_top), desc, font=desc_font, fill=(90, 90, 90))
|
||||
|
||||
if has_forecast:
|
||||
forecast_y = current_h
|
||||
forecast_h = height - forecast_y
|
||||
draw.line((width * 0.06, forecast_y, width * 0.94, forecast_y), fill=(224, 224, 224), width=2)
|
||||
draw.rectangle((width * 0.06, forecast_y, width * 0.94, forecast_y + 2), fill=(0, 0, 0))
|
||||
|
||||
col_w = width / len(days)
|
||||
label_font = load_font(size=int(scale * 0.042), bold=True)
|
||||
day_symbol_font = load_font(size=int(scale * 0.085))
|
||||
day_temp_font = load_font(size=int(scale * 0.038))
|
||||
label_font = load_font(size=int(scale * 0.052), bold=True)
|
||||
day_temp_font = load_font(size=int(scale * 0.046), bold=True)
|
||||
day_icon_size = min(col_w * 0.75, forecast_h * 0.48)
|
||||
|
||||
for i, day in enumerate(days):
|
||||
cx = col_w * i + col_w / 2
|
||||
y = forecast_y + forecast_h * 0.12
|
||||
y = forecast_y + forecast_h * 0.1
|
||||
|
||||
label = day["label"]
|
||||
lw = text_width(draw, label, label_font)
|
||||
draw.text((cx - lw / 2, y), label, font=label_font, fill=(90, 90, 90))
|
||||
y += label_font.size * 1.5
|
||||
draw.text((cx - lw / 2, y), label, font=label_font, fill=(60, 60, 60))
|
||||
y += label_font.size * 1.4
|
||||
|
||||
dsw = text_width(draw, day["symbol"], day_symbol_font)
|
||||
draw.text((cx - dsw / 2, y), day["symbol"], font=day_symbol_font, fill="black")
|
||||
y += day_symbol_font.size * 1.2
|
||||
draw_weather_icon(draw, day["kind"], cx, y + day_icon_size / 2, day_icon_size)
|
||||
y += day_icon_size * 1.1
|
||||
|
||||
temps = f"{round(day['high'])}°/{round(day['low'])}°"
|
||||
dtw = text_width(draw, temps, day_temp_font)
|
||||
draw.text((cx - dtw / 2, y), temps, font=day_temp_font, fill=(110, 110, 110))
|
||||
draw.text((cx - dtw / 2, y), temps, font=day_temp_font, fill=(50, 50, 50))
|
||||
|
||||
return image
|
||||
Reference in new issue
Block a user