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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | <a href="./plugins/incoming-packages/"><img src="./docs/assets/incoming-packages/hero.png" width="240" alt="incoming-packages on an LED panel"></a> |

### Custom (3)

| Plugin | Description | Preview |
Expand Down
Binary file added docs/assets/incoming-packages/card-types.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/incoming-packages/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/incoming-packages/highlight-today.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/incoming-packages/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.
139 changes: 139 additions & 0 deletions docs/assets/incoming-packages/shots.json
Original file line number Diff line number Diff line change
@@ -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": []
}
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/incoming-packages/show-dashboard.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/incoming-packages/show-delivered.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/incoming-packages/survey.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions plugins.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": "1.0.0",
"last_updated": "2026-09-02",
"last_updated": "2026-09-03",
"plugins": [
{
"id": "cricket-scoreboard",
Expand Down Expand Up @@ -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"
},
{
Expand Down
110 changes: 99 additions & 11 deletions plugins/incoming-packages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 +
Expand Down Expand Up @@ -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/<slug>.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"
}
}
```
10 changes: 8 additions & 2 deletions plugins/incoming-packages/manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -27,14 +27,20 @@
"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",
"ledmatrix_min": "2.0.0",
"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,
Expand Down
62 changes: 62 additions & 0 deletions scripts/docs_render_support/sitecustomize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Loading
Loading