Weather: default to 5-day forecast, add weekday+date header line

- forecast_days now defaults to 5 (was 3) across default_settings(),
  check()'s fallback, and the settings form's pre-selected option.
- New show_date setting (on by default): weekday + date in the same
  "Samstag, 25. Juli" format Clock already used. Combined with the
  city name into one header line above the icon/temp block when both
  are on ("Münster · Samstag, 25. Juli"), each independently
  toggleable.
- Moved the weekday/month name lists and the "Weekday, D. Month"
  formatting out of clock.py into render_utils.format_date_de(), so
  Weather reuses the exact same formatting instead of a second copy
  that could drift.
This commit is contained in:
Honey committed 2026-07-25 10:38:14 +02:00
1 parent 78337a99d2
commit 7601f746fe
5 files changed
+50 -32

No files matched your search

+2 -8
View File
@@ -5,16 +5,10 @@ import time
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
from screens.base import Screen from screens.base import Screen
from screens.render_utils import load_font, text_width from screens.render_utils import format_date_de, load_font, text_width
LINE_GAP = 0.04 # fraction of min(width, height) between the time and date lines LINE_GAP = 0.04 # fraction of min(width, height) between the time and date lines
WEEKDAYS_DE = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
MONTHS_DE = [
"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember",
]
class ClockScreen(Screen): class ClockScreen(Screen):
id = "clock" id = "clock"
@@ -31,7 +25,7 @@ class ClockScreen(Screen):
now = time.localtime() now = time.localtime()
time_str = state or time.strftime("%H:%M", now) time_str = state or time.strftime("%H:%M", now)
date_str = f"{WEEKDAYS_DE[now.tm_wday]}, {now.tm_mday}. {MONTHS_DE[now.tm_mon - 1]}" date_str = format_date_de(now)
scale = min(width, height) scale = min(width, height)
time_font = load_font(size=int(scale * 0.32), bold=True) time_font = load_font(size=int(scale * 0.32), bold=True)
+12
View File
@@ -6,6 +6,7 @@ dithering by default) -- so screens can draw with normal anti-aliased RGB
text and full-color cover art without worrying about the palette themselves. text and full-color cover art without worrying about the palette themselves.
""" """
import time
from pathlib import Path from pathlib import Path
from PIL import Image, ImageFont from PIL import Image, ImageFont
@@ -14,6 +15,17 @@ FONTS_DIR = Path(__file__).resolve().parent.parent / "fonts"
_font_cache: dict[tuple[str, int], ImageFont.FreeTypeFont] = {} _font_cache: dict[tuple[str, int], ImageFont.FreeTypeFont] = {}
WEEKDAYS_DE = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
MONTHS_DE = [
"Januar", "Februar", "März", "April", "Mai", "Juni",
"Juli", "August", "September", "Oktober", "November", "Dezember",
]
def format_date_de(t: time.struct_time | None = None) -> str:
t = t or time.localtime()
return f"{WEEKDAYS_DE[t.tm_wday]}, {t.tm_mday}. {MONTHS_DE[t.tm_mon - 1]}"
def load_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont: def load_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont:
key = ("bold" if bold else "regular", size) key = ("bold" if bold else "regular", size)
+27 -20
View File
@@ -8,7 +8,7 @@ import requests
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
from screens.base import Screen from screens.base import Screen
from screens.render_utils import load_font, text_width, truncate_to_width from screens.render_utils import format_date_de, load_font, text_width, truncate_to_width
from screens.weather_icons import WMO_CODES, draw_weather_icon from screens.weather_icons import WMO_CODES, draw_weather_icon
BLACK = (0, 0, 0) BLACK = (0, 0, 0)
@@ -31,12 +31,13 @@ class WeatherScreen(Screen):
def default_settings(self) -> dict: def default_settings(self) -> dict:
return { return {
"city": "", "latitude": None, "longitude": None, "forecast_days": 3, "update_minutes": 30, "city": "", "latitude": None, "longitude": None, "forecast_days": 5, "update_minutes": 30,
"show_feels_like": True, "show_feels_like": True,
"show_high_low": True, "show_high_low": True,
"show_rain_chance": True, "show_rain_chance": True,
"show_hourly_graph": True, "show_hourly_graph": True,
"show_city": True, "show_city": True,
"show_date": True,
} }
def get_poll_interval(self, settings: dict) -> int: def get_poll_interval(self, settings: dict) -> int:
@@ -79,9 +80,9 @@ class WeatherScreen(Screen):
logger.warning("Weather screen has no latitude/longitude configured") logger.warning("Weather screen has no latitude/longitude configured")
return None return None
forecast_days = settings.get("forecast_days", 3) forecast_days = settings.get("forecast_days", 5)
if forecast_days not in FORECAST_DAY_CHOICES: if forecast_days not in FORECAST_DAY_CHOICES:
forecast_days = 3 forecast_days = 5
response = requests.get( response = requests.get(
"https://api.open-meteo.com/v1/forecast", "https://api.open-meteo.com/v1/forecast",
@@ -214,26 +215,32 @@ class WeatherScreen(Screen):
text_block_h = temp_h + text_gap + desc_h + ((text_gap + extra_h) if extra_line else 0) text_block_h = temp_h + text_gap + desc_h + ((text_gap + extra_h) if extra_line else 0)
current_h = max(icon_size, text_block_h) current_h = max(icon_size, text_block_h)
# City name, above the icon/temp block -- just the city itself, not # Header line above the icon/temp block: city name (just the city
# the full "City, State, Country" geocode label (too long/cluttered # itself, not the full "City, State, Country" geocode label -- too
# for a dashboard header). Nothing to show for manually-entered # cluttered for a dashboard) and/or today's weekday + date, same
# coordinates, since there's no resolved place name for those. # format as the Clock screen. Nothing to show for city specifically
city_font = load_font(size=int(scale * 0.045), bold=True) # with manually-entered coordinates, since there's no resolved place
city_label = "" # name for those.
city_h = city_gap = city_top = 0 header_font = load_font(size=int(scale * 0.045), bold=True)
header_parts = []
if settings.get("show_city", True) and settings.get("city"): if settings.get("show_city", True) and settings.get("city"):
city_label = truncate_to_width(draw, settings["city"].split(",")[0].strip(), city_font, width - 2 * pad) header_parts.append(settings["city"].split(",")[0].strip())
if city_label: if settings.get("show_date", True):
city_left, city_top, city_right, city_bottom = draw.textbbox((0, 0), city_label, font=city_font) header_parts.append(format_date_de())
city_h = city_bottom - city_top header_label = truncate_to_width(draw, " · ".join(header_parts), header_font, width - 2 * pad) if header_parts else ""
city_gap = height * 0.025
total_top_h = city_h + city_gap + current_h header_h = header_gap = header_top = 0
if header_label:
header_left, header_top, header_right, header_bottom = draw.textbbox((0, 0), header_label, font=header_font)
header_h = header_bottom - header_top
header_gap = height * 0.025
total_top_h = header_h + header_gap + current_h
top_pad = (height - total_top_h) / 2 if lower_sections == 0 else height * 0.05 top_pad = (height - total_top_h) / 2 if lower_sections == 0 else height * 0.05
if city_label: if header_label:
draw.text((pad, top_pad - city_top), city_label, font=city_font, fill="black") draw.text((pad, top_pad - header_top), header_label, font=header_font, fill="black")
block_top = top_pad + city_h + city_gap block_top = top_pad + header_h + header_gap
icon_cx = pad + icon_size / 2 icon_cx = pad + icon_size / 2
icon_cy = block_top + current_h / 2 icon_cy = block_top + current_h / 2
+3 -2
View File
@@ -66,12 +66,13 @@ def create_app(config, screens: dict, scheduler) -> Flask:
city = request.form.get("city", "").strip() city = request.form.get("city", "").strip()
lat_raw = request.form.get("latitude", "").strip() lat_raw = request.form.get("latitude", "").strip()
lon_raw = request.form.get("longitude", "").strip() lon_raw = request.form.get("longitude", "").strip()
forecast_days = int(request.form.get("forecast_days", 3) or 3) forecast_days = int(request.form.get("forecast_days", 5) or 5)
if forecast_days not in (3, 5, 7): if forecast_days not in (3, 5, 7):
forecast_days = 3 forecast_days = 5
update_minutes = max(1, int(request.form.get("update_minutes", 30) or 30)) update_minutes = max(1, int(request.form.get("update_minutes", 30) or 30))
display_toggles = { display_toggles = {
"show_city": "show_city" in request.form, "show_city": "show_city" in request.form,
"show_date": "show_date" in request.form,
"show_hourly_graph": "show_hourly_graph" in request.form, "show_hourly_graph": "show_hourly_graph" in request.form,
"show_feels_like": "show_feels_like" in request.form, "show_feels_like": "show_feels_like" in request.form,
"show_high_low": "show_high_low" in request.form, "show_high_low": "show_high_low" in request.form,
+6 -2
View File
@@ -24,8 +24,8 @@
<div class="field"> <div class="field">
<label for="forecast_days">Days to show</label> <label for="forecast_days">Days to show</label>
<select id="forecast_days" name="forecast_days"> <select id="forecast_days" name="forecast_days">
<option value="3" {{ "selected" if settings.forecast_days == 3 or not settings.forecast_days }}>3 days</option> <option value="3" {{ "selected" if settings.forecast_days == 3 }}>3 days</option>
<option value="5" {{ "selected" if settings.forecast_days == 5 }}>5 days</option> <option value="5" {{ "selected" if settings.forecast_days == 5 or not settings.forecast_days }}>5 days</option>
<option value="7" {{ "selected" if settings.forecast_days == 7 }}>7 days</option> <option value="7" {{ "selected" if settings.forecast_days == 7 }}>7 days</option>
</select> </select>
</div> </div>
@@ -39,6 +39,10 @@
<input type="checkbox" name="show_city" {{ "checked" if settings.show_city != False }}> <input type="checkbox" name="show_city" {{ "checked" if settings.show_city != False }}>
City name City name
</label> </label>
<label class="checkbox">
<input type="checkbox" name="show_date" {{ "checked" if settings.show_date != False }}>
Weekday + date
</label>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" name="show_hourly_graph" {{ "checked" if settings.show_hourly_graph != False }}> <input type="checkbox" name="show_hourly_graph" {{ "checked" if settings.show_hourly_graph != False }}>
Hourly temperature graph Hourly temperature graph