diff --git a/README.md b/README.md index 9fae8532..9bd0c2f3 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,12 @@ curl -X POST http://your-pi-ip:5000/api/v3/plugins/install \ | [MQTT Notifications](./plugins/mqtt-notifications/) | HomeAssistant notifications via MQTT | | | [On Air Light](./plugins/on-air/) | Broadcast ON AIR tally light, triggered remotely via MQTT / Home Assistant | | +### Productivity (1) + +| Plugin | Description | Preview | +|--------|-------------|---------| +| [Incoming Packages](./plugins/incoming-packages/) | Rotating carrier cards for packages arriving today, via Home Assistant | incoming-packages on an LED panel | + ### Custom (3) | Plugin | Description | Preview | diff --git a/docs/assets/incoming-packages/card-types.png b/docs/assets/incoming-packages/card-types.png new file mode 100644 index 00000000..c20dc887 Binary files /dev/null and b/docs/assets/incoming-packages/card-types.png differ diff --git a/docs/assets/incoming-packages/hero.png b/docs/assets/incoming-packages/hero.png new file mode 100644 index 00000000..e88b1979 Binary files /dev/null and b/docs/assets/incoming-packages/hero.png differ diff --git a/docs/assets/incoming-packages/highlight-today.png b/docs/assets/incoming-packages/highlight-today.png new file mode 100644 index 00000000..03b565c7 Binary files /dev/null and b/docs/assets/incoming-packages/highlight-today.png differ diff --git a/docs/assets/incoming-packages/panel-sizes.png b/docs/assets/incoming-packages/panel-sizes.png new file mode 100644 index 00000000..b89cfc46 Binary files /dev/null and b/docs/assets/incoming-packages/panel-sizes.png differ diff --git a/docs/assets/incoming-packages/shots.json b/docs/assets/incoming-packages/shots.json new file mode 100644 index 00000000..224c59a9 --- /dev/null +++ b/docs/assets/incoming-packages/shots.json @@ -0,0 +1,139 @@ +{ + "plugin": "incoming-packages", + "defaults": { + "width": 128, + "height": 32, + "scale": 6, + "freeze_time": "2026-07-24T09:00:00+00:00", + "config": { + "enabled": true, + "provider": "mock" + }, + "attrs": { + "current_index": 0 + } + }, + "shots": [ + { + "name": "hero" + }, + { + "name": "card-dashboard", + "standalone": false + }, + { + "name": "card-today", + "attrs": { + "current_index": 1 + }, + "standalone": false + }, + { + "name": "card-transit", + "attrs": { + "current_index": 3 + }, + "standalone": false + }, + { + "name": "highlight-on", + "attrs": { + "current_index": 1 + }, + "standalone": false, + "config": { + "highlight_today": true + } + }, + { + "name": "highlight-off", + "attrs": { + "current_index": 1 + }, + "standalone": false, + "config": { + "highlight_today": false + } + }, + { + "name": "dashboard-on", + "standalone": false, + "config": { + "show_dashboard": true + } + }, + { + "name": "dashboard-off", + "standalone": false, + "config": { + "show_dashboard": false + } + }, + { + "name": "logo-on", + "attrs": { + "current_index": 1 + }, + "standalone": false, + "config": { + "show_carrier_logo": true + } + }, + { + "name": "logo-off", + "attrs": { + "current_index": 1 + }, + "standalone": false, + "config": { + "show_carrier_logo": false + } + }, + { + "name": "delivered-on", + "width": 128, + "height": 64, + "attrs": { + "current_index": 4 + }, + "standalone": false, + "config": { + "show_delivered": true + } + }, + { + "name": "delivered-off", + "width": 128, + "height": 64, + "attrs": { + "current_index": 4 + }, + "standalone": false, + "config": { + "show_delivered": false + } + }, + { + "name": "size-64x32", + "width": 64, + "standalone": false + }, + { + "name": "size-128x32", + "width": 128, + "standalone": false + }, + { + "name": "size-128x64", + "width": 128, + "height": 64, + "standalone": false + }, + { + "name": "size-256x32", + "width": 256, + "standalone": false + } + ], + "composites": [] +} diff --git a/docs/assets/incoming-packages/show-carrier-logo.png b/docs/assets/incoming-packages/show-carrier-logo.png new file mode 100644 index 00000000..5dfbec29 Binary files /dev/null and b/docs/assets/incoming-packages/show-carrier-logo.png differ diff --git a/docs/assets/incoming-packages/show-dashboard.png b/docs/assets/incoming-packages/show-dashboard.png new file mode 100644 index 00000000..6bd9b425 Binary files /dev/null and b/docs/assets/incoming-packages/show-dashboard.png differ diff --git a/docs/assets/incoming-packages/show-delivered.png b/docs/assets/incoming-packages/show-delivered.png new file mode 100644 index 00000000..00001d7f Binary files /dev/null and b/docs/assets/incoming-packages/show-delivered.png differ diff --git a/docs/assets/incoming-packages/survey.png b/docs/assets/incoming-packages/survey.png new file mode 100644 index 00000000..aa92a908 Binary files /dev/null and b/docs/assets/incoming-packages/survey.png differ diff --git a/plugins.json b/plugins.json index f900d57c..a4e5363b 100644 --- a/plugins.json +++ b/plugins.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "last_updated": "2026-09-02", + "last_updated": "2026-09-03", "plugins": [ { "id": "cricket-scoreboard", @@ -1140,10 +1140,10 @@ "plugin_path": "plugins/incoming-packages", "stars": 0, "downloads": 0, - "last_updated": "2026-07-27", + "last_updated": "2026-09-03", "verified": true, "screenshot": "https://raw.githubusercontent.com/ChuckBuilds/ledmatrix-plugins/main/plugins/incoming-packages/assets/screenshot.png", - "latest_version": "1.0.0", + "latest_version": "1.0.1", "icon": "fas fa-box" }, { diff --git a/plugins/incoming-packages/README.md b/plugins/incoming-packages/README.md index 66d46ad6..2800eaf7 100644 --- a/plugins/incoming-packages/README.md +++ b/plugins/incoming-packages/README.md @@ -4,6 +4,13 @@ Rotating cards on your LEDMatrix for packages headed your way — one per active carrier, with the count **arriving today** prioritized and highlighted, plus a lead summary card. +![The dashboard card on a 128x32 panel: "11 incoming - 3 today" above four +carrier badges with their counts](../../docs/assets/incoming-packages/hero.png) + +*Every image in this README is real plugin output from the built-in `mock` +provider, rendered at the true panel size and scaled up so the pixels stay +pixels.* + > **This plugin does not use the Shop app** — Shopify exposes no public API for a > consumer's package list. Instead it reads a normalized snapshot from a pluggable > **provider** (default: Home Assistant). The plugin only ever stores an API URL + @@ -83,20 +90,101 @@ count/image, and Home Assistant's own summary string. hiccup rather than blanking, with a small amber "Xm ago" freshness marker once the data is stale (`stale_after_minutes`). +![The three card types](../../docs/assets/incoming-packages/card-types.png) + +Carriers with something arriving today are sorted ahead of carriers that only +have packages in transit, and their count is drawn in `accent_color`. + Carrier badges are drawn (colored badge + abbreviation), so no trademarked logo images are bundled. Drop a `assets/carrier_logos/.png` (e.g. `ups.png`, `fedex.png`) to override a badge with your own image. -## Key options +![The same rotation on four panel sizes](../../docs/assets/incoming-packages/panel-sizes.png) + +The plugin reads the panel size every frame and picks a bitmap font tier to +match, shortening labels rather than overflowing — `in transit` becomes +`in trans.` when the card is narrow. + +## Configuration + +Settings live in the plugin's tab in the web UI and in `config/config.json` +under `incoming-packages`. The full schema is +[`config_schema.json`](config_schema.json). + +### Connection + +| Key | Default | Notes | +|---|---|---| +| `enabled` | `false` | Master switch | +| `provider` | `homeassistant` | `homeassistant`, `aftership`, or `mock` — see [Providers](#providers) | +| `ha_base_url` | `""` | Home Assistant URL, e.g. `http://homeassistant.local:8123`. Used by the `homeassistant` provider | +| `ha_token` | — | Home Assistant long-lived access token. Stored as a secret, so the web UI masks it | +| `api_key` | — | AfterShip API key. Only read when `provider` is `aftership`. Also a secret | +| `entity_prefix` | `sensor.mail_` | Mail and Packages entity prefix. Advanced; the provider auto-discovers the sensors, so this is rarely needed | +| `update_interval` | `600` | Seconds between provider refreshes (60–7200). Advanced | +| `stale_after_minutes` | `60` | Data older than this gets the amber "Xm ago" freshness marker (5–1440). Advanced | + +### What appears on the cards + +| Key | Default | Notes | +|---|---|---| +| `show_dashboard` | `true` | Lead with the badge grid rather than a compact text summary | +| `show_carrier_logo` | `true` | Draw the carrier badge on each card | +| `show_delivered` | `true` | Add the "N delivered" confirmation for packages delivered today | +| `include_delivered` | `false` | Also give already-delivered packages their own card | +| `show_usps_mail_image` | `true` | Include the USPS Informed Delivery mail card when there is mail | +| `show_delivery_images` | `true` | Show a carrier's scanned delivery photo when it is out for delivery today | +| `highlight_today` | `true` | Sort arriving-today carriers first and accent their count | +| `accent_color` | `[0, 220, 120]` | The accent colour used for that highlight | +| `customization.title_text.text_color` | `[255, 255, 255]` | Colour of the primary text | + +### Rotation and motion + +| Key | Default | Notes | +|---|---|---| +| `display_duration` | `30` | Seconds the plugin holds the screen per turn in the rotation (1–300) | +| `rotation_interval` | `6` | Seconds each card is shown before the next (1–60). Advanced | +| `max_cards` | `8` | Cap on how many cards are in the rotation (1–20). Advanced | +| `image_frame_seconds` | `1.5` | Seconds per frame when animating the USPS mail image (0.2–10). Advanced | +| `scroll_enabled` | `true` | Marquee-scroll text too long to fit rather than truncating. Advanced | +| `scroll_speed` | `5` | Frames between marquee steps; higher is slower (1–30). Advanced | +| `scroll_separator` | `" "` | Text inserted between marquee loops. Advanced | +| `timezone` | `""` | Override the timezone used to decide what "today" means. Empty follows the global LEDMatrix timezone. Advanced | + +Settings marked advanced sit in the collapsed **Advanced Settings** section of +the web UI form. + +### What the toggles look like + +`show_dashboard` decides whether the rotation opens with the badge grid or a +compact summary: + +![show_dashboard true and false](../../docs/assets/incoming-packages/show-dashboard.png) + +`highlight_today` controls both the accent colour and the sort order: + +![highlight_today true and false](../../docs/assets/incoming-packages/highlight-today.png) + +`show_carrier_logo` drops the badge and gives the text the full width: + +![show_carrier_logo true and false](../../docs/assets/incoming-packages/show-carrier-logo.png) + +`show_delivered` adds the confirmation line, which needs a panel tall enough +for a third row: + +![show_delivered true and false](../../docs/assets/incoming-packages/show-delivered.png) + +### Previewing without Home Assistant -| Option | Default | Notes | -|--------|---------|-------| -| `provider` | `homeassistant` | `homeassistant` \| `aftership` \| `mock` | -| `ha_base_url` / `ha_token` | — | Home Assistant URL + long-lived token | -| `highlight_today` | `true` | Prioritize + accent arriving-today | -| `accent_color` | `[0,220,120]` | RGB accent | -| `rotation_interval` | `6` | Seconds per card | -| `update_interval` | `600` | Provider refresh cadence (s) | -| `max_cards` | `8` | Cards in the rotation | +Set `provider` to `mock` to see the layout with built-in demo data and no +credentials at all — four carriers, two of them arriving today. Every image in +this README is that provider. -See `config_schema.json` for the full list. +```json +{ + "incoming-packages": { + "enabled": true, + "provider": "mock" + } +} +``` diff --git a/plugins/incoming-packages/manifest.json b/plugins/incoming-packages/manifest.json index 3cf209ef..6f7c1a87 100644 --- a/plugins/incoming-packages/manifest.json +++ b/plugins/incoming-packages/manifest.json @@ -1,7 +1,7 @@ { "id": "incoming-packages", "name": "Incoming Packages", - "version": "1.0.0", + "version": "1.0.1", "author": "ChuckBuilds", "description": "Rotating cards for packages headed your way - per carrier, with the count arriving today highlighted. Reads a normalized snapshot from a pluggable provider: the Home Assistant Mail and Packages integration (default), AfterShip, or a built-in demo. Email scanning stays inside Home Assistant; the plugin only stores an API URL + token.", "category": "productivity", @@ -27,6 +27,12 @@ "pillow" ], "versions": [ + { + "released": "2026-09-03", + "version": "1.0.1", + "notes": "Documentation. The README's 'Key options' table listed 7 of the 25 settings and said to read config_schema.json for the rest; it now documents all 25, grouped by connection, card contents, and rotation, with the advanced ones marked. Adds real rendered screenshots throughout -- the three card types, four panel sizes, and before/after pairs for show_dashboard, highlight_today, show_carrier_logo and show_delivered -- all produced by the built-in mock provider, so they need no credentials to reproduce.", + "ledmatrix_min": "2.0.0" + }, { "released": "2026-07-24", "version": "1.0.0", @@ -34,7 +40,7 @@ "notes": "Initial release. Provider-agnostic incoming-package display with a Home Assistant Mail and Packages provider (default, auto-discovers sensor.mail_* entities over the HA REST API), an AfterShip provider, and a built-in demo provider. Size-adaptive rotating cards: one per active carrier with a drawn carrier badge, the count arriving today (prioritized and shown in an accent color), the count in transit, and a lead summary card, plus optional USPS mail count. Renders on all panel sizes from 64x32 to 256x64 with adaptive font tiers and marquee text; on-panel setup/error messages." } ], - "last_updated": "2026-07-27", + "last_updated": "2026-09-03", "stars": 0, "downloads": 0, "verified": true, diff --git a/scripts/docs_render_support/sitecustomize.py b/scripts/docs_render_support/sitecustomize.py index 20844d5e..90fe76c1 100644 --- a/scripts/docs_render_support/sitecustomize.py +++ b/scripts/docs_render_support/sitecustomize.py @@ -52,3 +52,65 @@ def today(cls): _docs_http_replay.install() except Exception: # never let doc tooling break the render it is measuring pass + + +# Some plugins hold their interesting state in memory, put there by an event +# the renderer cannot produce -- an MQTT message, a webhook, a button press. +# No configuration reaches that state, so a documentation render would only +# ever show the idle frame. LEDMATRIX_DOCS_ATTRS names attributes to set on the +# plugin instance once the core's loader has built it, which is the same thing +# the event would have done, without the plugin knowing it is being rendered. +_ATTRS = os.environ.get("LEDMATRIX_DOCS_ATTRS") +if _ATTRS: + import json as _json + import sys as _sys + + _WANTED = _json.loads(_ATTRS) + _TARGET = "src.plugin_system.plugin_loader" + + def _apply(instance): + for name, value in _WANTED.items(): + if isinstance(value, list) and len(value) == 3 and all( + isinstance(v, int) for v in value): + value = tuple(value) # colours are tuples everywhere in the core + setattr(instance, name, value) + return instance + + class _PatchingLoader: + """Wraps the real loader so the module is patched right after it runs.""" + + def __init__(self, inner): + self._inner = inner + + def create_module(self, spec): + return self._inner.create_module(spec) + + def exec_module(self, module): + self._inner.exec_module(module) + real = module.PluginLoader.load_plugin + + def load_plugin(self, *args, **kwargs): + instance, mod = real(self, *args, **kwargs) + return _apply(instance), mod + + module.PluginLoader.load_plugin = load_plugin + + def __getattr__(self, name): + return getattr(self._inner, name) + + class _AttrFinder: + """Hands back the real spec with its loader wrapped, once.""" + + def find_spec(self, name, path=None, target=None): + if name != _TARGET: + return None + index = _sys.meta_path.index(self) + for finder in _sys.meta_path[index + 1:]: + find = getattr(finder, "find_spec", None) + spec = find(name, path, target) if find else None + if spec is not None and spec.loader is not None: + spec.loader = _PatchingLoader(spec.loader) + return spec + return None + + _sys.meta_path.insert(0, _AttrFinder()) diff --git a/scripts/render_docs_assets.py b/scripts/render_docs_assets.py index 42a1d93d..36c7bd4b 100644 --- a/scripts/render_docs_assets.py +++ b/scripts/render_docs_assets.py @@ -263,12 +263,16 @@ def render_shot( # clock, a countdown, or a "starts in 2h" line differs on every run. freeze_time = shot.get("freeze_time", defaults.get("freeze_time")) http_replay = shot.get("http_replay", defaults.get("http_replay")) - if freeze_time or http_replay: + # Runtime state that an event would normally have set; see the shim. + attrs = shot.get("attrs", defaults.get("attrs")) + if freeze_time or http_replay or attrs: support_dir = str(Path(__file__).resolve().parent / "docs_render_support") existing = env.get("PYTHONPATH") env["PYTHONPATH"] = f"{support_dir}{os.pathsep}{existing}" if existing else support_dir if freeze_time: env["LEDMATRIX_DOCS_FREEZE_TIME"] = str(freeze_time) + if attrs: + env["LEDMATRIX_DOCS_ATTRS"] = json.dumps(attrs) if http_replay: replay_path = (shot_list_dir / http_replay).resolve() if not replay_path.is_file():