diff --git a/README.md b/README.md index 1680c6f5..62d07d33 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ curl -X POST http://your-pi-ip:5000/api/v3/plugins/install \ | Plugin | Description | Preview | |--------|-------------|---------| -| [Weather Display](./plugins/ledmatrix-weather/) | Current conditions, hourly & daily forecasts (Open-Meteo) | | +| [Weather Display](./plugins/ledmatrix-weather/) | Current conditions, hourly & daily forecasts (Open-Meteo) | ledmatrix-weather on an LED panel | | [Tide Display](./plugins/tide-display/) | Coastal tides with animated wave level, schedule & 24-hour chart (NOAA) | tide-display on an LED panel | ### Media (2) diff --git a/docs/assets/ledmatrix-weather/hero.png b/docs/assets/ledmatrix-weather/hero.png new file mode 100644 index 00000000..7d4c9b4a Binary files /dev/null and b/docs/assets/ledmatrix-weather/hero.png differ diff --git a/docs/assets/ledmatrix-weather/modes.png b/docs/assets/ledmatrix-weather/modes.png new file mode 100644 index 00000000..b068290e Binary files /dev/null and b/docs/assets/ledmatrix-weather/modes.png differ diff --git a/docs/assets/ledmatrix-weather/panel-sizes.png b/docs/assets/ledmatrix-weather/panel-sizes.png new file mode 100644 index 00000000..d8b6e329 Binary files /dev/null and b/docs/assets/ledmatrix-weather/panel-sizes.png differ diff --git a/docs/assets/ledmatrix-weather/shots.json b/docs/assets/ledmatrix-weather/shots.json new file mode 100644 index 00000000..9865a5b2 --- /dev/null +++ b/docs/assets/ledmatrix-weather/shots.json @@ -0,0 +1,151 @@ +{ + "plugin": "ledmatrix-weather", + "defaults": { + "width": 128, + "height": 32, + "scale": 6, + "freeze_time": "2026-06-14T17:00:00", + "mock_data": "../../../plugins/ledmatrix-weather/test/fixtures/mock.json", + "config": { + "enabled": true, + "location_city": "Dallas", + "location_state": "Texas", + "location_country": "US", + "units": "imperial", + "show_current_weather": false, + "show_hourly_forecast": false, + "show_daily_forecast": false, + "show_almanac": false, + "show_radar": false + } + }, + "shots": [ + { + "name": "hero", + "config": { + "show_current_weather": true + } + }, + { + "name": "mode-current", + "standalone": false, + "config": { + "show_current_weather": true + } + }, + { + "name": "mode-hourly", + "standalone": false, + "config": { + "show_hourly_forecast": true + } + }, + { + "name": "mode-daily", + "standalone": false, + "config": { + "show_daily_forecast": true + } + }, + { + "name": "mode-almanac", + "standalone": false, + "config": { + "show_almanac": true + } + }, + { + "name": "mode-radar", + "standalone": false, + "config": { + "show_radar": true + } + }, + { + "name": "size-64x32", + "width": 64, + "height": 32, + "standalone": false, + "config": { + "show_current_weather": true + } + }, + { + "name": "size-128x32", + "width": 128, + "height": 32, + "standalone": false, + "config": { + "show_current_weather": true + } + }, + { + "name": "size-128x64", + "width": 128, + "height": 64, + "standalone": false, + "config": { + "show_current_weather": true + } + }, + { + "name": "size-256x32", + "width": 256, + "height": 32, + "standalone": false, + "config": { + "show_current_weather": true + } + } + ], + "composites": [ + { + "name": "modes", + "columns": 1, + "cells": [ + { + "shot": "mode-current", + "label": "Current conditions" + }, + { + "shot": "mode-hourly", + "label": "Hourly forecast" + }, + { + "shot": "mode-daily", + "label": "Daily forecast" + }, + { + "shot": "mode-almanac", + "label": "Almanac" + }, + { + "shot": "mode-radar", + "label": "Radar" + } + ] + }, + { + "name": "panel-sizes", + "columns": 2, + "cells": [ + { + "shot": "size-64x32", + "label": "64 x 32" + }, + { + "shot": "size-128x32", + "label": "128 x 32" + }, + { + "shot": "size-128x64", + "label": "128 x 64" + }, + { + "shot": "size-256x32", + "label": "256 x 32" + } + ] + } + ] +} diff --git a/plugins.json b/plugins.json index 532c8c15..f8240000 100644 --- a/plugins.json +++ b/plugins.json @@ -928,10 +928,10 @@ "repo": "https://github.com/ChuckBuilds/ledmatrix-plugins", "branch": "main", "plugin_path": "plugins/ledmatrix-weather", - "latest_version": "2.6.5", + "latest_version": "2.6.6", "stars": 0, "downloads": 0, - "last_updated": "2026-08-05", + "last_updated": "2026-09-03", "verified": true, "screenshot": "" }, diff --git a/plugins/ledmatrix-weather/README.md b/plugins/ledmatrix-weather/README.md index a22fad8f..81d55209 100644 --- a/plugins/ledmatrix-weather/README.md +++ b/plugins/ledmatrix-weather/README.md @@ -13,6 +13,13 @@ # Weather Display Plugin +![Current conditions on a 128x32 panel: a cloud icon, "Partly Cloudy", the +temperature, the day's high and low, and UV, humidity, wind and feels-like +readings](../../docs/assets/ledmatrix-weather/hero.png) + +*Every image in this README is real plugin output, rendered from a recorded +Open-Meteo response for Dallas on 2026-06-14 so it reproduces exactly.* + Comprehensive weather display plugin for LEDMatrix showing current conditions, hourly forecast, and daily forecast. Current Weather: @@ -60,6 +67,8 @@ generated from it. The keys you'll touch most often: | Key | Default | Notes | |---|---|---| | `enabled` | `false` | Master switch | +| `location_latitude` / `location_longitude` | `null` | Set both to skip geocoding and pin an exact position. Left null, the city/state/country above are geocoded once and cached. Advanced | +| `api_key` | `null` | Not needed for the default setup — Open-Meteo requires no key. Kept for deployments that proxy a keyed weather service. Advanced | | `location_city` | `"Dallas"` | City name | | `location_state` | `"Texas"` | State/province (optional, helps US disambiguation) | | `location_country` | `"US"` | ISO 3166-1 alpha-2 code | @@ -75,7 +84,7 @@ generated from it. The keys you'll touch most often: | `show_alerts` | `true` | Show active weather alerts (preempts rotation, US only) | | `show_feels_like` / `show_dew_point` / `show_visibility` / `show_pressure` | `true` | Extra current-conditions metrics (need height ≥ 48px) | | `radar_map_style` | `"osm"` | Basemap: `osm`, `carto`, `carto_dark`, `esri` (real tiles, worldwide) or `vector` (retro WeatherStar state outlines, US-only). Tile styles fall back to the vector map automatically when tiles can't be fetched | -| `radar_range_miles` | `50` | Distance from your location to the panel edge (10–500). Replaces the deprecated `radar_zoom`, which is still honored for old configs | +| `radar_range_miles` | `75` | Distance from your location to the panel edge (10–500). Replaces the deprecated `radar_zoom`, which is still honored for old configs | | `radar_show_nowcast` | `true` | Append ~30 min of predicted radar (`FCST +10m` … with yellow dots) after the observed frames | | `radar_tile_server` | `"https://maps.chuck-builds.com"` | Self-hosted OSM tile server for the `osm` style (`{server}/tile/{z}/{x}/{y}.png`); public mirrors are the fallback | | `radar_map_brightness` | `0.5` | Dim the tile basemap so precipitation pops on the matrix | @@ -84,7 +93,8 @@ generated from it. The keys you'll touch most often: | `radar_update_interval` | `180` | Seconds between new-frame checks (60–1800). Checks are cheap; tiles only download when RainViewer publishes a new frame | | `radar_past_frames` | `6` | Observed frames to animate (~10 min apart) | | `radar_frame_seconds` / `radar_loop_pause_seconds` | `0.5` / `1.5` | Animation pacing: per-frame time and the hold on the newest frame | -| `dynamic_duration` | `{"enabled": false}` | Opt-in: hold the radar until a full animation loop completes (capped by `max_duration_seconds`) | +| `dynamic_duration.enabled` | `false` | Opt-in: hold the radar until a full animation loop completes rather than cutting mid-loop | +| `dynamic_duration.max_duration_seconds` | `60` | Ceiling on that hold, in seconds (10–300), so a slow loop cannot monopolise the panel | ## Display modes @@ -99,6 +109,20 @@ controller rotates through them in order: | `almanac` | Sunrise, sunset, moon phase, day length | | `radar` | Animated precipitation radar (RainViewer) over an OSM or vector basemap, with optional nowcast frames | +![The five display modes](../../docs/assets/ledmatrix-weather/modes.png) + +The radar panel above is rendered with no network access, so it shows the +overlay — the location crosshair and the frame timestamp — over an empty +basemap. On a connected display the basemap tiles and the precipitation frames +fill it; those come from live tile servers, which is why this one screen cannot +be reproduced from a recording the way the others are. + +Every mode adapts to the panel. The current-conditions screen adds the +feels-like, dew point, visibility and pressure readings only when the panel is +at least 48px tall: + +![Current conditions on four panel sizes](../../docs/assets/ledmatrix-weather/panel-sizes.png) + When an active weather alert is available and `show_alerts` is true, the alert takes priority over the normal rotation. Alerts are sourced from the [NWS API](https://www.weather.gov/documentation/services-web-api) and are diff --git a/plugins/ledmatrix-weather/manifest.json b/plugins/ledmatrix-weather/manifest.json index 0284ec87..3ef199ce 100644 --- a/plugins/ledmatrix-weather/manifest.json +++ b/plugins/ledmatrix-weather/manifest.json @@ -1,7 +1,7 @@ { "id": "ledmatrix-weather", "name": "Weather Display", - "version": "2.6.5", + "version": "2.6.6", "author": "ChuckBuilds", "class_name": "WeatherPlugin", "update_interval": 60, @@ -26,6 +26,12 @@ "radar" ], "versions": [ + { + "version": "2.6.6", + "released": "2026-09-03", + "notes": "Radar overlay font, and documentation. weather_radar.py resolved the 4x6 face three levels above itself, which is only the LEDMatrix root when the plugins directory sits inside the core tree. Anywhere else it fell back to PIL's default font -- taller than the 6px the overlay is laid out for -- and the radar timestamp label was drawn past the bottom edge of the panel, where the safety harness cannot see it because PIL clips at draw time. It now asks the imported core where it lives. The README gains real rendered screenshots of all five display modes and four panel sizes, documents api_key, location_latitude/longitude and both dynamic_duration keys, and corrects the radar_range_miles default, which the table gave as 50 where the schema says 75.", + "ledmatrix_min_version": "2.0.0" + }, { "version": "2.6.5", "released": "2026-08-21", @@ -162,7 +168,7 @@ "ledmatrix_min_version": "2.0.0" } ], - "last_updated": "2026-08-05", + "last_updated": "2026-09-03", "stars": 0, "downloads": 0, "verified": true, diff --git a/plugins/ledmatrix-weather/weather_radar.py b/plugins/ledmatrix-weather/weather_radar.py index 307f6029..3be7be41 100644 --- a/plugins/ledmatrix-weather/weather_radar.py +++ b/plugins/ledmatrix-weather/weather_radar.py @@ -42,7 +42,26 @@ # Core-repo font, resolved relative to this file (plugin dir -> plugins -> root), # matching how manager.py locates assets. CWD-independent. -_FONT_PATH = Path(__file__).resolve().parent.parent.parent / "assets" / "fonts" / "4x6-font.ttf" +def _font_candidates(): + """Where the 4x6 face may live, best guess first. + + The plugin normally sits at /plugin-repos/, so three levels + up is the core root -- but the plugins directory is configurable, and from + anywhere else that guess finds nothing. The fallback is PIL's default font, + which is taller than the 6px this overlay is laid out for, so the timestamp + label ends up drawn past the bottom edge of the panel. Ask the imported + core where it is first. + """ + roots = [] + try: + import src # the LEDMatrix core package + if getattr(src, "__file__", None): + roots.append(Path(src.__file__).resolve().parent.parent) + except Exception: # nosec B110 - absence is expected off a real install + pass + roots.append(Path(__file__).resolve().parent.parent.parent) + roots.append(Path.cwd()) + return [r / "assets" / "fonts" / "4x6-font.ttf" for r in roots] _FETCH_BUDGET_SECONDS = 20 # per refresh_data() call; work resumes next tick _NOWCAST_FRAMES = 3 # RainViewer publishes 3 nowcast frames (10/20/30 min) @@ -560,9 +579,13 @@ def get_radar_image(self, width: int, height: int) -> Optional[Image.Image]: def _load_font(self): if self._font is None: - try: - self._font = ImageFont.truetype(str(_FONT_PATH), 6) - except (OSError, ValueError): + for candidate in _font_candidates(): + try: + self._font = ImageFont.truetype(str(candidate), 6) + break + except (OSError, ValueError): + continue + if self._font is None: self._font = ImageFont.load_default() return self._font