Add weather to calendar mode; fix CalDAV events never showing
Build and push server image / build-and-push (push) Successful in 48s
Build and push server image / build-and-push (push) Successful in 48s
Weather: multiple cities per frame, geocoded via Open-Meteo (no API key), shown above the event list on agenda/today & tomorrow/week views -- never month, no room for it there. Hand-drawn sun/cloud/rain/snow/ thunderstorm icons (no new font/icon asset, same primitives-only approach the rest of calendar_render.py already uses). City geocoding handles "City, State" qualifiers Open-Meteo's own search doesn't (disambiguates same-named cities, e.g. the three "Portland"s). CalDAV fix: events were never showing despite calendars discovering fine, because fetch_calendar_events relied on the calendar-query REPORT's server-side time-range filter, which real servers implement inconsistently (confirmed against a real server, not just guessed -- reproduced locally with Radicale). Switched to fetching every event unfiltered and doing all date-window filtering/expansion client-side, same approach already used for plain ICS feeds.
This commit is contained in:
@@ -25,7 +25,7 @@ from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .. import calendar_render, gitea_releases, photo_queue, quiet_hours
|
||||
from .. import calendar_render, gitea_releases, photo_queue, quiet_hours, weather
|
||||
from ..auth import require_frame_control, require_frame_view, require_user_api
|
||||
from ..db import frame_locked, get_db
|
||||
from ..image_pipeline import (
|
||||
@@ -44,6 +44,7 @@ from .common import (
|
||||
calendar_sources_for_frame,
|
||||
fetch_source_and_faces,
|
||||
get_or_refresh_calendar_events,
|
||||
get_or_refresh_weather,
|
||||
immich_client_for,
|
||||
immich_creds,
|
||||
list_assets,
|
||||
@@ -100,6 +101,8 @@ def api_config_save(
|
||||
calendar_view: str | None = Form(None),
|
||||
calendar_photo_inlay: bool | None = Form(None),
|
||||
calendar_week_start: int | None = Form(None),
|
||||
calendar_weather_enabled: bool | None = Form(None),
|
||||
calendar_weather_units: str | None = Form(None),
|
||||
frame: Frame = Depends(require_frame_control),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -177,6 +180,14 @@ def api_config_save(
|
||||
cfg.calendar_photo_inlay = calendar_photo_inlay
|
||||
if calendar_week_start is not None:
|
||||
cfg.calendar_week_start = max(0, min(6, calendar_week_start))
|
||||
if calendar_weather_enabled is not None:
|
||||
cfg.calendar_weather_enabled = calendar_weather_enabled
|
||||
if calendar_weather_units is not None and calendar_weather_units in ("fahrenheit", "celsius"):
|
||||
if calendar_weather_units != cfg.calendar_weather_units:
|
||||
# Cached forecasts are in the old unit -- force a refetch
|
||||
# rather than showing stale numbers under a new unit label.
|
||||
cfg.calendar_weather_checked_at = 0.0
|
||||
cfg.calendar_weather_units = calendar_weather_units
|
||||
cfg.stats_config_saves += 1
|
||||
return {"status": "saved"}
|
||||
|
||||
@@ -497,14 +508,62 @@ def api_preview_calendar(frame: Frame = Depends(require_frame_view), db: Session
|
||||
events, summary = get_or_refresh_calendar_events(db, frame)
|
||||
photo_inlay = _calendar_photo_inlay(frame, db)
|
||||
view = frame.calendar_view if frame.calendar_view in calendar_render.CALENDAR_VIEWS else "agenda"
|
||||
weather_cities = get_or_refresh_weather(db, frame)
|
||||
png = calendar_render.render_calendar_preview_png(
|
||||
events, view=view, browse_offset=frame.calendar_browse_offset, orientation=frame.orientation,
|
||||
palette_rgb=frame.palette_rgb, timezone=frame.timezone, photo_inlay=photo_inlay, fetch_summary=summary,
|
||||
week_start=frame.calendar_week_start,
|
||||
weather_cities=weather_cities, weather_units=frame.calendar_weather_units,
|
||||
)
|
||||
return Response(content=png, media_type="image/png")
|
||||
|
||||
|
||||
class WeatherCityAddRequest(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
@router.post("/api/frames/{frame_id}/weather-cities/add")
|
||||
def api_weather_city_add(
|
||||
body: WeatherCityAddRequest,
|
||||
frame: Frame = Depends(require_frame_control),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Geocodes a free-text city name (e.g. "Portland, OR") and adds it
|
||||
to this frame's weather strip -- a frame-wide display setting (like
|
||||
calendar_view), not personal data, so this is gated the same way as
|
||||
api_config_save rather than the calendar-select owner/mute split."""
|
||||
try:
|
||||
city = weather.geocode_city(body.name)
|
||||
except weather.WeatherFetchError as e:
|
||||
raise HTTPException(400, str(e)) from e
|
||||
with frame_locked(db, frame.id) as cfg:
|
||||
cities = list(cfg.calendar_weather_cities or [])
|
||||
if any(c["label"] == city["label"] for c in cities):
|
||||
raise HTTPException(400, f"{city['label']} is already on this frame's list")
|
||||
cities.append(city)
|
||||
cfg.calendar_weather_cities = cities
|
||||
cfg.calendar_weather_checked_at = 0.0 # pick up the new city promptly
|
||||
return {"status": "saved", "city": city}
|
||||
|
||||
|
||||
class WeatherCityRemoveRequest(BaseModel):
|
||||
label: str
|
||||
|
||||
|
||||
@router.post("/api/frames/{frame_id}/weather-cities/remove")
|
||||
def api_weather_city_remove(
|
||||
body: WeatherCityRemoveRequest,
|
||||
frame: Frame = Depends(require_frame_control),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
with frame_locked(db, frame.id) as cfg:
|
||||
cities = [c for c in (cfg.calendar_weather_cities or []) if c["label"] != body.label]
|
||||
cfg.calendar_weather_cities = cities
|
||||
cached = [c for c in (cfg.calendar_weather_cached or []) if c["label"] != body.label]
|
||||
cfg.calendar_weather_cached = cached
|
||||
return {"status": "saved"}
|
||||
|
||||
|
||||
@router.post("/api/frames/{frame_id}/firmware")
|
||||
def api_firmware_upload(
|
||||
file: UploadFile = File(...),
|
||||
|
||||
Reference in New Issue
Block a user