Add standalone weather widget (current/hourly/daily/multi-city, pluggable providers)
Build and push server image / test (push) Successful in 1m11s
Build and push server image / build-and-push (push) Successful in 2m3s
Build and push server image / deploy (push) Successful in 52s

New widget type with four display modes -- current conditions, an
hourly forecast strip, a multi-day forecast, and several cities' current
day side by side -- backed by a pluggable provider registry (app/weather/,
mirroring the app/widgets/ dispatch pattern): Open-Meteo (worldwide) and
NWS (US-only) both wired up now, Environment Canada documented as the
next one to add given its more involved station/grid-lookup API.

The calendar widget's existing embedded weather strip is untouched and
still Open-Meteo-only; this lifts the same underlying icon-drawing
primitives (now shared via app/weather_render.py, calendar_render.py
still imports draw_weather_row unchanged) into a widget that can be
placed and sized on its own. Icons are redrawn in the panel's actual ink
colors (yellow sun/bolt, blue rain/snow) instead of flat black, and
build_multi_city's icon/font sizing now scales with how many cities need
to fit rather than the box's height alone -- both fixed after catching
them via live browser verification, along with a mode-switch cache-shape
crash and a mobile-width dialog overflow.

New WeatherWidgetConfig table (migration 24), grid footprint, widget
module, common.py fetch/cache helper, router endpoints (location set/
clear, city add/remove, preview), dialog template + JS, and full test
coverage (providers, widget render, HTTP endpoints, migration replay).
docs/widgets.md and CLAUDE.md's TODO updated accordingly.
This commit is contained in:
2026-07-27 16:16:43 +00:00
parent 6118705c37
commit 52ebafab78
25 changed files with 2036 additions and 152 deletions
+2 -1
View File
@@ -36,7 +36,7 @@ Each module in this package exposes:
from __future__ import annotations
from . import calendar, photos, static_image, tasks, text, whiteboard
from . import calendar, photos, static_image, tasks, text, weather, whiteboard
WIDGET_TYPES = {
"photos": photos,
@@ -45,4 +45,5 @@ WIDGET_TYPES = {
"tasks": tasks,
"static": static_image,
"text": text,
"weather": weather,
}
+40
View File
@@ -0,0 +1,40 @@
"""Weather widget: one of four display modes (see models.
WeatherWidgetConfig) backed by a pluggable provider (app/weather/'s
PROVIDERS registry -- Open-Meteo or NWS) and drawn by app/weather_render.
py's build() dispatch. No real "next"/"back" concept (same as
whiteboard) -- a single "check now" action forces a re-fetch bypassing
the normal throttle."""
from __future__ import annotations
from PIL import Image
from sqlalchemy.orm import Session
from .. import weather_render
from ..models import Frame, WeatherWidgetConfig, Widget
from ..routers.common import get_or_refresh_weather_widget_data
from ._shared import placeholder_image
ACTION_LABELS = {"check_now": "Check for updates"}
def render(db: Session, frame: Frame, widget: Widget, target_w: int, target_h: int,
is_normal_wake: bool = True) -> Image.Image:
"""is_normal_wake is unused here -- see app/widgets/photos.py's
identical note; every widget type's render() shares one call
signature regardless of which ones actually care."""
cfg = db.get(WeatherWidgetConfig, widget.id)
data = get_or_refresh_weather_widget_data(db, frame, widget)
if data is None:
return placeholder_image(target_w, target_h, ["Weather widget", "not configured yet"])
return weather_render.build(
cfg.mode, data, target_w, target_h, frame.palette_rgb, cfg.units,
city_label=cfg.city_label or "", interval_hours=cfg.hourly_interval_hours,
)
def _check_now(db: Session, frame: Frame, widget: Widget) -> None:
get_or_refresh_weather_widget_data(db, frame, widget, force=True)
ACTIONS = {"check_now": _check_now}