Files
espresso_frame/server/app/widgets/__init__.py
T
tfaour 52ebafab78
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
Add standalone weather widget (current/hourly/daily/multi-city, pluggable providers)
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.
2026-07-27 16:16:43 +00:00

50 lines
2.1 KiB
Python

"""Registry mapping a Widget's widget_type to its render/action module --
the widget-system's analogue of routers/device.py's old RENDERERS/
ADVANCE_RENDERERS/BACK_RENDERERS dicts, generalized from "one mode owns
the whole panel" to "each widget renders into its own region and
optionally responds to named button actions."
Each module in this package exposes:
render(db, frame, widget, target_w, target_h, is_normal_wake=True) -> Image.Image
An RGB image exactly target_w x target_h, unquantized -- the
widget's content composed into its own region. Never returns
packed panel bytes or raises for a foreseeable failure (a
widget's own fetch hiccup shows a small placeholder instead) --
image_pipeline.render_panel composites every widget's own
render() result onto one shared canvas and quantizes/packs the
whole thing once (see its own docstring). is_normal_wake
distinguishes an ordinary /frame/image GET from a button-
triggered re-render -- only app/widgets/calendar.py's render()
actually uses it (resetting browse_offset back to "today" on a
normal wake), but every module accepts it for one uniform call
signature regardless.
ACTIONS: dict[str, Callable[[Session, Frame, Widget], None]]
Named button actions this widget type supports (e.g. "advance",
"back", "check_now") -- see models.FrameButtonAction. Each
function mutates the widget's own state via db.widget_locked
internally; none return a value or re-render themselves --
whatever dispatches a button press (routers/device.py, once the
widget-system cutover lands) re-renders the whole panel once
after running every assigned action.
ACTION_LABELS: dict[str, str]
Human-readable labels for ACTIONS' keys, for the button-
assignment UI.
"""
from __future__ import annotations
from . import calendar, photos, static_image, tasks, text, weather, whiteboard
WIDGET_TYPES = {
"photos": photos,
"calendar": calendar,
"whiteboard": whiteboard,
"tasks": tasks,
"static": static_image,
"text": text,
"weather": weather,
}