Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) | <a href="./plugins/ledmatrix-weather/"><img src="./docs/assets/ledmatrix-weather/hero.png" width="240" alt="ledmatrix-weather on an LED panel"></a> |
| [Tide Display](./plugins/tide-display/) | Coastal tides with animated wave level, schedule & 24-hour chart (NOAA) | <a href="./plugins/tide-display/"><img src="./docs/assets/tide-display/hero.png" width="240" alt="tide-display on an LED panel"></a> |

### Media (2)
Expand Down
Binary file added docs/assets/ledmatrix-weather/hero.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/ledmatrix-weather/modes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/ledmatrix-weather/panel-sizes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
151 changes: 151 additions & 0 deletions docs/assets/ledmatrix-weather/shots.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
]
}
4 changes: 2 additions & 2 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": ""
},
Expand Down
28 changes: 26 additions & 2 deletions plugins/ledmatrix-weather/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 |
Expand All @@ -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 |
Expand All @@ -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

Expand All @@ -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
Expand Down
10 changes: 8 additions & 2 deletions plugins/ledmatrix-weather/manifest.json
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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",
Expand Down Expand Up @@ -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,
Expand Down
31 changes: 27 additions & 4 deletions plugins/ledmatrix-weather/weather_radar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <LEDMatrix>/plugin-repos/<id>, 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)
Expand Down Expand Up @@ -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

Expand Down
Loading