mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
Core's own HTTP fetch paths (APIHelper, fetch_espn_scoreboard and its date chunks, BackgroundDataService, BaseOddsManager.get_odds) go through one service in src/common/fetch_service.py: shared connection pools per retry policy, merged identical in-flight GETs, per-host token-bucket budgets (fetch_service.rate_limits), and per-plugin request counters published to GET /api/v3/plugins/fetch-stats. Return values, exceptions, cache keys, TTLs and retry policies are unchanged. Core-internal in this release; plugins should not import it directly yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2511 lines
66 KiB
Markdown
2511 lines
66 KiB
Markdown
# LEDMatrix REST API Reference
|
|
|
|
Reference for the REST API served by the LEDMatrix web interface.
|
|
|
|
**Base URL**: `http://your-pi-ip:5000/api/v3`
|
|
|
|
Most endpoints answer JSON in this envelope:
|
|
```json
|
|
{
|
|
"status": "success" | "error",
|
|
"data": { ... },
|
|
"message": "Optional message"
|
|
}
|
|
```
|
|
|
|
Not every endpoint follows it exactly. Where a response puts fields at the
|
|
top level instead of under `data` (install-from-url, registry-from-url, the
|
|
auth endpoints, upload endpoints, `system/git-info`, `system/check-update`),
|
|
the entry below says so.
|
|
|
|
**Cross-site requests are refused.** A `POST`, `PUT`, `PATCH` or `DELETE`
|
|
carrying an `Origin` header (or, without one, a `Referer`) that is not the
|
|
host the request was sent to gets `403` with `"error_code":
|
|
"CROSS_SITE_REQUEST"`; so does `Origin: null`. This stops other websites from
|
|
driving the Pi through a LAN user's browser. Scripts, curl, Home Assistant and
|
|
the MQTT bridge send neither header and are unaffected. A browser page on
|
|
another origin (a dashboard you host elsewhere, say) can no longer call the
|
|
API; call it server-side instead. Behind a reverse proxy, pass the original
|
|
`Host` through, port included (nginx: `proxy_set_header Host $http_host;`;
|
|
`$host` drops the port) -- `X-Forwarded-Host` is not read.
|
|
|
|
**Authentication (optional, off by default).** With no web password set,
|
|
nothing below needs credentials. Once one is set (General > Security, or
|
|
[`POST /auth/password`](#web-login-and-api-tokens)), every route needs a login
|
|
session or an API token:
|
|
|
|
```bash
|
|
curl -H "Authorization: Bearer lmx_..." http://your-pi-ip:5000/api/v3/display/current
|
|
```
|
|
|
|
Without either, an API route answers `401` with
|
|
`{"status": "error", "error_code": "AUTH_REQUIRED", "message": ...}`, a
|
|
`WWW-Authenticate: Bearer realm="LEDMatrix"` header and the login page's URL
|
|
in `X-LEDMatrix-Login`; an unknown or revoked token gets `"error_code":
|
|
"INVALID_TOKEN"`. Browser page loads are redirected to `/login` instead, and
|
|
HTMX requests get `401` with `HX-Redirect: /login?...`. Never asked for
|
|
credentials: requests from the Pi itself (loopback, with no `X-Forwarded-For`,
|
|
`X-Real-IP`, `Forwarded` or `X-Forwarded-Host` header), `/static/*`, the
|
|
captive-portal probe URLs, `/login`, `/api/v3/health` (status only, see
|
|
[Health Check](#health-check)), and -- only while the Pi is in access-point
|
|
mode -- `/setup`, `GET /wifi/status`, `GET /wifi/scan` and
|
|
`POST /wifi/connect`.
|
|
|
|
## Table of Contents
|
|
|
|
- [Configuration](#configuration)
|
|
- [Display Control](#display-control)
|
|
- [Plugins](#plugins)
|
|
- [Plugin Store](#plugin-store)
|
|
- [System](#system)
|
|
- [Backup and Restore](#backup-and-restore)
|
|
- [Fonts](#fonts)
|
|
- [Cache](#cache)
|
|
- [WiFi](#wifi)
|
|
- [Streams](#streams)
|
|
- [Logs](#logs)
|
|
- [Error tracking](#error-tracking)
|
|
- [Health and Status](#health-and-status)
|
|
- [Schedule (dim/power)](#schedule-dimpower)
|
|
- [Integrations](#integrations)
|
|
- [Web login and API tokens](#web-login-and-api-tokens)
|
|
- [Plugin-specific endpoints](#plugin-specific-endpoints)
|
|
- [Starlark Apps](#starlark-apps)
|
|
|
|
> The API blueprint is the `api_v3` package in
|
|
> `web_interface/blueprints/api_v3/` (one module per area: `config.py`,
|
|
> `display.py`, `system.py`, `backup.py`, `fonts.py`, `misc.py`, `wifi.py`,
|
|
> `starlark.py`, and `plugins.py` plus the `plugin_*.py` modules for the
|
|
> plugin routes). `web_interface/app.py` registers it
|
|
> at `/api/v3` (`app.register_blueprint(api_v3, url_prefix='/api/v3')`).
|
|
> The three SSE endpoints (`/api/v3/stream/*`) are defined directly on the
|
|
> Flask app in `app.py` (`stream_stats`, `stream_display`, `stream_logs`).
|
|
> `test/fixtures/api_v3_url_map.json` is the canonical list of blueprint
|
|
> routes; a test fails if the code and that fixture differ.
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
### Get Main Configuration
|
|
|
|
**GET** `/api/v3/config/main`
|
|
|
|
Return `config/config.json`. Fields whose names look like credentials
|
|
(`api_key`, `token`, `password`, `secret`, ...) are blanked to `""` in the
|
|
response; `config/config_secrets.json` values are never included.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"timezone": "America/New_York",
|
|
"location": {
|
|
"city": "New York",
|
|
"state": "NY",
|
|
"country": "US"
|
|
},
|
|
"display": { ... },
|
|
"plugin_system": { ... }
|
|
}
|
|
}
|
|
```
|
|
|
|
### Save Main Configuration
|
|
|
|
**POST** `/api/v3/config/main`
|
|
|
|
Update the main configuration. Accepts JSON (`Content-Type: application/json`)
|
|
or form data. The body uses the web UI's flat field names, which the handler
|
|
maps into the nested config:
|
|
|
|
| Fields | Stored at |
|
|
|--------|-----------|
|
|
| `timezone`, `city`, `state`, `country` | `timezone`, `location.*` |
|
|
| `web_display_autostart`, `auto_update_enabled` | `web_display_autostart`, `auto_update.enabled` |
|
|
| `plugins_directory` (and the unused legacy flags `auto_discover`, `auto_load_enabled`, `development_mode`, stored only when sent) | `plugin_system.*` |
|
|
| `target_fps` (30-200) | `target_fps` |
|
|
| `rows`, `cols`, `chain_length`, `parallel`, `brightness`, `hardware_mapping`, `pwm_bits`, `led_rgb_sequence`, `panel_type`, `pixel_mapper_config`, `disable_hardware_pulsing`, `inverse_colors`, `show_refresh_rate`, ... | `display.hardware.*` |
|
|
| `gpio_slowdown`, `rp1_rio` | `display.runtime.*` |
|
|
| `use_short_date_format` | `display.use_short_date_format` |
|
|
| `max_dynamic_duration_seconds` | `display.dynamic_duration.max_duration_seconds` |
|
|
| `double_sided_*`, `vegas_*`, `sync_*` | `display.double_sided`, `display.vegas_scroll`, `sync` |
|
|
| `<name>_duration`, `default_duration`, `duration__<mode>` | `display.display_durations.*` |
|
|
| `plugin_rotation_order` (list of plugin ids) | `display.plugin_rotation_order` |
|
|
|
|
Any other top-level key is deep-merged into the config as given.
|
|
|
|
A JSON body changes only the keys it contains; everything else keeps its
|
|
stored value. (Form posts from the web UI send every field of a tab, and
|
|
there an unchecked checkbox — which the browser omits — is saved as
|
|
`false`.)
|
|
|
|
**Request Body** (JSON):
|
|
```json
|
|
{
|
|
"timezone": "America/New_York",
|
|
"city": "New York",
|
|
"brightness": 90
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Configuration saved successfully",
|
|
"restart_required": true
|
|
}
|
|
```
|
|
|
|
`restart_required` is always true here: display hardware, rotation,
|
|
durations and general settings take effect when the display restarts, and
|
|
the web UI shows its restart banner on the flag. (Plugin sections saved
|
|
through this route reach the running plugin live, like
|
|
`POST /plugins/config`.)
|
|
|
|
Invalid values (e.g. an out-of-range `target_fps`, a hardware option the
|
|
Raspberry Pi 5 driver cannot use) are rejected with `400` and nothing is
|
|
saved.
|
|
|
|
### Get Schedule Configuration
|
|
|
|
**GET** `/api/v3/config/schedule`
|
|
|
|
Retrieve the current on/off schedule.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"enabled": true,
|
|
"mode": "global",
|
|
"start_time": "07:00",
|
|
"end_time": "23:00"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Per-day mode response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"enabled": true,
|
|
"mode": "per-day",
|
|
"days": {
|
|
"monday": {
|
|
"enabled": true,
|
|
"start_time": "07:00",
|
|
"end_time": "23:00"
|
|
},
|
|
"tuesday": { ... }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Save Schedule Configuration
|
|
|
|
**POST** `/api/v3/config/schedule`
|
|
|
|
Replace the schedule configuration.
|
|
|
|
**Request Body** (Global mode):
|
|
```json
|
|
{
|
|
"enabled": true,
|
|
"mode": "global",
|
|
"start_time": "07:00",
|
|
"end_time": "23:00"
|
|
}
|
|
```
|
|
|
|
**Request Body** (Per-day mode, flat form-field names):
|
|
```json
|
|
{
|
|
"enabled": true,
|
|
"mode": "per-day",
|
|
"monday_enabled": true,
|
|
"monday_start": "07:00",
|
|
"monday_end": "23:00",
|
|
"tuesday_enabled": true,
|
|
"tuesday_start": "08:00",
|
|
"tuesday_end": "22:00"
|
|
}
|
|
```
|
|
|
|
A day whose `<day>_enabled` key is absent counts as enabled, with default
|
|
times `07:00`-`23:00`. At least one day must be enabled.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Schedule configuration saved successfully"
|
|
}
|
|
```
|
|
|
|
### Get Secrets Configuration
|
|
|
|
**GET** `/api/v3/config/secrets`
|
|
|
|
Retrieve `config/config_secrets.json` with every set value replaced by eight
|
|
bullet characters (`"••••••••"`). Empty values and `YOUR_*` placeholders
|
|
are returned as-is, so a client can tell "set" from "not set". The
|
|
`web_auth` section (the web login's password hash, API-token hashes and
|
|
cookie key) is left out entirely; [Get Main Configuration](#get-main-configuration)
|
|
leaves it out too.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"ledmatrix-weather": {
|
|
"api_key": "••••••••"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Save Raw Configuration
|
|
|
|
**POST** `/api/v3/config/raw/main`
|
|
|
|
Replace `config/config.json` with the JSON body (advanced use only).
|
|
|
|
**POST** `/api/v3/config/raw/secrets`
|
|
|
|
Save the secrets file (advanced use only). Masked values (`"••••••••"`)
|
|
and blank strings in the body are dropped, and the rest is merged onto the
|
|
stored secrets, so posting back the GET response unchanged changes nothing.
|
|
A secret cannot be cleared by blanking it here. A `web_auth` key in the body
|
|
is ignored; the stored login settings are kept.
|
|
|
|
---
|
|
|
|
## Display Control
|
|
|
|
### Get Current Display
|
|
|
|
**GET** `/api/v3/display/current`
|
|
|
|
Get the latest display snapshot as a base64 PNG (`image` is `null` when no
|
|
snapshot is available).
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"timestamp": 1234567890.123,
|
|
"width": 128,
|
|
"height": 32,
|
|
"image": "base64_encoded_image_data"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Get Current Display Status
|
|
|
|
**GET** `/api/v3/display/current-status`
|
|
|
|
The mode and plugin the display service is currently showing, as published
|
|
by the display process (stale after 120 seconds).
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"mode": "nfl_live",
|
|
"plugin_id": "football-scoreboard",
|
|
"last_updated": 1234567890.123
|
|
}
|
|
}
|
|
```
|
|
|
|
When nothing has been published, every field is `null`.
|
|
|
|
### List Display Modes
|
|
|
|
**GET** `/api/v3/display/modes`
|
|
|
|
Every display mode that can be requested on-demand, with the plugin that owns
|
|
it. This is the list the force-display dialog offers.
|
|
|
|
Send the reported `plugin_id` alongside `mode` when starting an on-demand
|
|
display: `/display/on-demand/start` falls back to `find_plugin_for_mode` when
|
|
`plugin_id` is omitted, and that lookup only sees modes declared in a static
|
|
manifest — a plugin whose modes are generated (each installed Starlark app is
|
|
one) returns 404 there.
|
|
|
|
Triggers plugin discovery, which is otherwise lazy — so a caller that never
|
|
opens the dashboard still gets the full list.
|
|
|
|
**Query Parameters**:
|
|
- `include_disabled` (optional): `1` to include modes belonging to disabled
|
|
plugins. They are still valid on-demand targets — the controller enables the
|
|
plugin for the duration of the request — and are reported with
|
|
`"enabled": false`.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"modes": [
|
|
{
|
|
"mode": "nfl_live",
|
|
"plugin_id": "football-scoreboard",
|
|
"plugin_name": "Football Scoreboard",
|
|
"name": "nfl_live",
|
|
"enabled": true
|
|
},
|
|
{
|
|
"mode": "clock-simple",
|
|
"plugin_id": "clock-simple",
|
|
"plugin_name": "Simple Clock",
|
|
"name": "Simple Clock",
|
|
"enabled": true
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
`name` is a label for a dropdown: a single-mode plugin's own name, or the raw
|
|
mode string for a multi-mode plugin, since there is no per-mode name anywhere.
|
|
|
|
### On-Demand Display Status
|
|
|
|
**GET** `/api/v3/display/on-demand/status`
|
|
|
|
Get the current on-demand display state.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"state": {
|
|
"active": true,
|
|
"plugin_id": "football-scoreboard",
|
|
"mode": "nfl_live",
|
|
"duration": 45,
|
|
"pinned": true,
|
|
"status": "running",
|
|
"last_updated": 1234567890.123
|
|
},
|
|
"service": {
|
|
"active": true,
|
|
"returncode": 0,
|
|
"stdout": "active",
|
|
"stderr": ""
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
With no on-demand request, `state` is
|
|
`{"active": false, "status": "idle", "last_updated": null}`.
|
|
|
|
### Start On-Demand Display
|
|
|
|
**POST** `/api/v3/display/on-demand/start`
|
|
|
|
Request a specific plugin to display on-demand.
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"plugin_id": "football-scoreboard",
|
|
"mode": "nfl_live",
|
|
"duration": 45,
|
|
"pinned": true,
|
|
"start_service": true
|
|
}
|
|
```
|
|
|
|
**Parameters** (at least one of `plugin_id` and `mode` is required):
|
|
- `plugin_id` (string, optional): Plugin identifier
|
|
- `mode` (string, optional): Display mode name (plugin_id inferred if not provided)
|
|
- `duration` (number, optional): Duration in seconds (0 = until stopped)
|
|
- `pinned` (boolean, optional): Pin display (pause rotation)
|
|
- `start_service` (boolean, optional): Start the display service if it is not running (default: true). A running service is never restarted: it picks the request up within about a quarter of a second. When false and the service is stopped, the route returns 400.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"request_id": "uuid-here",
|
|
"plugin_id": "football-scoreboard",
|
|
"mode": "nfl_live",
|
|
"duration": 45,
|
|
"pinned": true,
|
|
"service": { "active": true, "returncode": 0, "stdout": "", "stderr": "" },
|
|
"transport": "socket"
|
|
}
|
|
}
|
|
```
|
|
|
|
`service` is `null` when `start_service` is false.
|
|
|
|
`transport` says how the request reached the display: `"socket"` means the
|
|
display's control socket acknowledged it (it is queued for the render thread;
|
|
see [IPC_CONTROL_SOCKET.md](IPC_CONTROL_SOCKET.md)), `"mailbox"` means it was
|
|
written to the cache mailbox the display polls, as before the socket existed.
|
|
With `"mailbox"`, `socket_error` gives the reason the socket was not used
|
|
(`no_socket` when the display is stopped or predates the socket, `timeout`,
|
|
`refused`, `busy`, ...). Either way the request is applied the same way;
|
|
`request_id` is the same id in both.
|
|
|
|
### Stop On-Demand Display
|
|
|
|
**POST** `/api/v3/display/on-demand/stop`
|
|
|
|
Stop the current on-demand display.
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"stop_service": false
|
|
}
|
|
```
|
|
|
|
**Parameters**:
|
|
- `stop_service` (boolean, optional): Also stop the display service (default: false)
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"request_id": "uuid-here",
|
|
"service": null,
|
|
"transport": "socket"
|
|
}
|
|
}
|
|
```
|
|
|
|
`transport` and `socket_error` are as for start.
|
|
|
|
---
|
|
|
|
## Plugins
|
|
|
|
### Get Installed Plugins
|
|
|
|
**GET** `/api/v3/plugins/installed`
|
|
|
|
List all installed plugins with their status and metadata.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"plugins": [
|
|
{
|
|
"id": "football-scoreboard",
|
|
"name": "Football Scoreboard",
|
|
"version": "1.2.3",
|
|
"latest_version": "1.2.4",
|
|
"update_available": true,
|
|
"author": "ChuckBuilds",
|
|
"category": "Sports",
|
|
"description": "NFL and NCAA Football scores",
|
|
"tags": ["sports", "football", "nfl"],
|
|
"enabled": true,
|
|
"verified": true,
|
|
"loaded": true,
|
|
"state": "enabled",
|
|
"error_info": null,
|
|
"loaded_version": "1.2.3",
|
|
"loaded_at": 1790000000.0,
|
|
"last_updated": "2025-01-15T10:30:00Z",
|
|
"last_commit": "abc1234",
|
|
"last_commit_message": "feat: Add live game updates",
|
|
"branch": "main",
|
|
"web_ui_actions": [],
|
|
"vegas_mode": null,
|
|
"vegas_content_type": null,
|
|
"vegas_participation": "scroll",
|
|
"vegas_participation_source": "manifest"
|
|
}
|
|
],
|
|
"runtime": {
|
|
"status": "live",
|
|
"published_at": 1790000030.0,
|
|
"age_seconds": 12.4,
|
|
"stale_after": 180.0
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Metadata comes from each plugin's files on disk; `enabled` is the plugin's
|
|
`enabled` flag in `config.json` (missing means disabled, as the display
|
|
reads it). `vegas_mode` is the plugin's configured `vegas_mode`, or `null`.
|
|
|
|
`loaded`, `state`, `error_info`, `loaded_version` and `loaded_at` come from
|
|
the runtime snapshot the display publishes (the web process runs no plugin
|
|
code). `state` is the display's lifecycle state (`loaded` while loading,
|
|
`enabled`, `disabled`, `error`, `unloaded`); `error_info` is `null` or
|
|
`{"type", "message", "at", "recoverable"}`, with the message redacted and at
|
|
most 200 characters (the full error is at `/errors/*`). `loaded_version` is
|
|
the version the display loaded, which differs from `version` after an update
|
|
until the display restarts. A plugin a live snapshot does not list is
|
|
`loaded: false`, `state: "unloaded"`.
|
|
|
|
`runtime.status` says whether to believe them: `live` (fresh snapshot from
|
|
a running display), `stale` (not refreshed within `stale_after` seconds: the
|
|
display is hung or died), `stopped` (the display shut down) or `unknown`
|
|
(nothing published yet). Unless it is `live`, every one of those fields is
|
|
`null`. Health and metrics are at [`/plugins/health`](#get-plugin-health)
|
|
and `/plugins/metrics`.
|
|
|
|
`vegas_participation` is what Vegas mode does with the plugin: `"scroll"`,
|
|
`"pause"` or `"exclude"` (see
|
|
[PLUGIN_API_REFERENCE.md](PLUGIN_API_REFERENCE.md#vegas-participation)),
|
|
and `vegas_participation_source` says where it came from. The web reads it
|
|
the way the display resolves it, as far as files can tell: the user's own
|
|
`vegas_participation` setting (`"config"`), else the manifest's declared
|
|
`vegas_participation` (`"manifest"`). Past those the display derives it from
|
|
the plugin's code -- a `get_vegas_participation()` override or the legacy
|
|
Vegas hooks -- which the web process never runs, so `vegas_participation`
|
|
is `null` and the source is `"runtime"`. A plugin that overrides
|
|
`get_vegas_participation()` decides at run time and can differ from its
|
|
manifest's declaration. `vegas_content_type` is always `null`.
|
|
|
|
### Get Plugin Configuration
|
|
|
|
**GET** `/api/v3/plugins/config?plugin_id=<plugin_id>`
|
|
|
|
Get a plugin's configuration, with schema defaults filled in for keys that
|
|
are not stored. `data` is the configuration object itself.
|
|
|
|
**Query Parameters**:
|
|
- `plugin_id` (required): Plugin identifier
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"enabled": true,
|
|
"display_duration": 30,
|
|
"favorite_teams": ["TB", "DAL"]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Save Plugin Configuration
|
|
|
|
**POST** `/api/v3/plugins/config`
|
|
|
|
Update a plugin's configuration. With a JSON body, the keys in `config` are
|
|
merged onto the plugin's stored configuration: keys you do not send keep
|
|
their stored values. Fields the schema marks `"x-secret": true` are written
|
|
to `config/config_secrets.json` instead of `config.json`. The web UI posts
|
|
form data instead (`?plugin_id=` in the query string, fields as form fields).
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"plugin_id": "football-scoreboard",
|
|
"config": {
|
|
"display_duration": 30,
|
|
"favorite_teams": ["TB", "DAL"]
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Plugin football-scoreboard configuration saved successfully"
|
|
}
|
|
```
|
|
|
|
A config that fails schema validation is rejected with `400` and nothing is
|
|
saved.
|
|
|
|
### Get Plugin Schema
|
|
|
|
**GET** `/api/v3/plugins/schema?plugin_id=<plugin_id>`
|
|
|
|
Get the JSON schema for a plugin's configuration. A plugin without a
|
|
`config_schema.json` gets a minimal default schema.
|
|
|
|
**Query Parameters**:
|
|
- `plugin_id` (required): Plugin identifier
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"default": true
|
|
},
|
|
"display_duration": {
|
|
"type": "number",
|
|
"minimum": 1,
|
|
"maximum": 300
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Reset Plugin Configuration
|
|
|
|
**POST** `/api/v3/plugins/config/reset`
|
|
|
|
Reset a plugin's configuration to its schema defaults.
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"plugin_id": "football-scoreboard",
|
|
"preserve_secrets": true
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Plugin football-scoreboard configuration reset to defaults",
|
|
"data": { "config": { ... } }
|
|
}
|
|
```
|
|
|
|
### Toggle Plugin
|
|
|
|
**POST** `/api/v3/plugins/toggle`
|
|
|
|
Enable or disable a plugin. A `plugin_id` of the form `starlark:<app_id>`
|
|
toggles a Starlark app.
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"plugin_id": "football-scoreboard",
|
|
"enabled": true
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Plugin football-scoreboard enabled successfully"
|
|
}
|
|
```
|
|
|
|
### Install Plugin
|
|
|
|
**POST** `/api/v3/plugins/install`
|
|
|
|
Install a plugin from the plugin store.
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"plugin_id": "football-scoreboard",
|
|
"branch": "main"
|
|
}
|
|
```
|
|
|
|
`branch` is optional.
|
|
|
|
**Response** (queued; poll `/plugins/operation/<operation_id>`):
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": { "operation_id": "uuid-here" },
|
|
"message": "Plugin football-scoreboard installation queued"
|
|
}
|
|
```
|
|
|
|
When the operation queue is unavailable the install runs synchronously and
|
|
the response has only a `message` and the restart fields below.
|
|
|
|
The finished operation's `result` (from `/plugins/operation/<operation_id>`)
|
|
carries `restart_required`: true when the plugin is already enabled in
|
|
`config.json`, because the running display does not load newly installed
|
|
files by itself; `restart_message` then holds the restart banner's wording.
|
|
A plugin that is not enabled needs no restart: enabling it loads it.
|
|
|
|
A plugin whose registry entry (or downloaded manifest) needs a newer
|
|
LEDMatrix is refused: the synchronous install answers `409` with a message
|
|
such as `Failed to install plugin x: X requires LEDMatrix 3.8.0 or newer…`,
|
|
and a queued one fails with that message. Nothing already installed is
|
|
changed.
|
|
|
|
### Uninstall Plugin
|
|
|
|
**POST** `/api/v3/plugins/uninstall`
|
|
|
|
Remove an installed plugin.
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"plugin_id": "football-scoreboard",
|
|
"preserve_config": false
|
|
}
|
|
```
|
|
|
|
**Response** (queued):
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": { "operation_id": "uuid-here" },
|
|
"message": "Plugin uninstallation queued"
|
|
}
|
|
```
|
|
|
|
The finished operation's `result` carries `restart_required`. Removing the
|
|
plugin's config (the default) lets the display unload it by itself, so it is
|
|
false; with `preserve_config: true` an enabled plugin keeps running until
|
|
the display restarts, and it is true.
|
|
|
|
### Update Plugin
|
|
|
|
**POST** `/api/v3/plugins/update`
|
|
|
|
Update a plugin to the latest version. Runs synchronously.
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"plugin_id": "football-scoreboard"
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Plugin football-scoreboard updated ...",
|
|
"data": {
|
|
"last_updated": "2025-01-15T10:30:00Z",
|
|
"commit": "abc1234...",
|
|
"update_status": "updated"
|
|
},
|
|
"restart_required": true,
|
|
"restart_message": "Plugin updated — restart the display to run the new version"
|
|
}
|
|
```
|
|
|
|
`update_status` is `updated`, `up_to_date` or `local_only`.
|
|
`restart_required` is true when the plugin changed and is enabled: the
|
|
running display keeps the code it loaded until it restarts.
|
|
|
|
An update this core cannot run answers `409` with `Plugin update refused:`
|
|
and the reason; the installed version is left as it was.
|
|
|
|
### Install Plugin from URL
|
|
|
|
**POST** `/api/v3/plugins/install-from-url`
|
|
|
|
Install a plugin directly from a GitHub repository URL. Runs synchronously.
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"repo_url": "https://github.com/user/ledmatrix-my-plugin",
|
|
"branch": "main",
|
|
"plugin_path": null
|
|
}
|
|
```
|
|
|
|
**Parameters**:
|
|
- `repo_url` (required): GitHub repository URL
|
|
- `branch` (optional): Branch name (default: `main`, then `master`)
|
|
- `plugin_path` (optional): Path within the repository, for monorepo plugins
|
|
- `plugin_id` (optional): Plugin id, for monorepo installations
|
|
|
|
**Response** (fields at the top level):
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Plugin my-plugin installed successfully",
|
|
"plugin_id": "my-plugin",
|
|
"name": "My Plugin",
|
|
"branch": "main",
|
|
"restart_required": false
|
|
}
|
|
```
|
|
|
|
`restart_required` follows the same rule as `/plugins/install`.
|
|
|
|
### Load Registry from URL
|
|
|
|
**POST** `/api/v3/plugins/registry-from-url`
|
|
|
|
Load a `plugins.json` registry from a GitHub repository URL.
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"repo_url": "https://github.com/user/ledmatrix-plugins"
|
|
}
|
|
```
|
|
|
|
**Response** (fields at the top level):
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"plugins": [
|
|
{
|
|
"id": "plugin-1",
|
|
"name": "Plugin One",
|
|
"description": "..."
|
|
}
|
|
],
|
|
"registry_url": "https://github.com/user/ledmatrix-plugins"
|
|
}
|
|
```
|
|
|
|
### Get Plugin Health
|
|
|
|
**GET** `/api/v3/plugins/health`
|
|
|
|
Get health state for all installed plugins, keyed by plugin id.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"football-scoreboard": {
|
|
"plugin_id": "football-scoreboard",
|
|
"circuit_state": "closed",
|
|
"consecutive_failures": 0,
|
|
"total_failures": 2,
|
|
"total_successes": 1500,
|
|
"success_rate": 99.87,
|
|
"last_success_time": 1234567890.123,
|
|
"last_failure_time": 1234560000.0,
|
|
"last_error": null,
|
|
"is_healthy": true,
|
|
"degraded": false,
|
|
"degraded_reason": null,
|
|
"circuit_opened_time": null,
|
|
"half_open_start_time": null
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Get Plugin Health (Single)
|
|
|
|
**GET** `/api/v3/plugins/health/<plugin_id>`
|
|
|
|
Health state for one plugin; `data` has the same fields as one entry above.
|
|
Answers `503` when health tracking is unavailable.
|
|
|
|
### Reset Plugin Health
|
|
|
|
**POST** `/api/v3/plugins/health/<plugin_id>/reset`
|
|
|
|
Reset health state for a plugin (manual recovery).
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Health state reset for plugin football-scoreboard"
|
|
}
|
|
```
|
|
|
|
### Get Plugin Metrics
|
|
|
|
**GET** `/api/v3/plugins/metrics`
|
|
|
|
Get resource usage metrics for all installed plugins, keyed by plugin id.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"football-scoreboard": {
|
|
"plugin_id": "football-scoreboard",
|
|
"memory_mb": 24.5,
|
|
"cpu_percent": 3.2,
|
|
"execution_time": 0.12,
|
|
"avg_execution_time": 0.1,
|
|
"min_execution_time": 0.05,
|
|
"max_execution_time": 0.9,
|
|
"call_count": 500,
|
|
"last_update_time": 1234567890.123,
|
|
"limits": {
|
|
"max_memory_mb": 50,
|
|
"max_cpu_percent": 50,
|
|
"max_execution_time": 5.0,
|
|
"warning_threshold": 0.8
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
`limits` (and usage percentages derived from it) appear only when limits
|
|
are configured for the plugin.
|
|
|
|
### Get Plugin Metrics (Single)
|
|
|
|
**GET** `/api/v3/plugins/metrics/<plugin_id>`
|
|
|
|
Metrics for one plugin; `data` has the same fields as one entry above.
|
|
|
|
### Reset Plugin Metrics
|
|
|
|
**POST** `/api/v3/plugins/metrics/<plugin_id>/reset`
|
|
|
|
Reset metrics for a plugin.
|
|
|
|
### Get Fetch Statistics
|
|
|
|
**GET** `/api/v3/plugins/fetch-stats`
|
|
|
|
Network requests made through the core fetch service
|
|
(`src/common/fetch_service.py`), per plugin and per host, cumulative since
|
|
the display started. Read-only. The display publishes the counters at most
|
|
once a minute when they change (every 10 minutes otherwise), so they can be
|
|
up to a minute old. Requests a plugin makes with its own `requests` calls,
|
|
outside `APIHelper`, `espn_dates`, `BackgroundDataService` and
|
|
`BaseOddsManager`, are not counted yet.
|
|
|
|
`data.status` is `live`, `stale` (no publish for longer than
|
|
`stale_after`), `stopped` (the display exited; the last counters are kept)
|
|
or `unknown` (nothing published; `data.data` is `null`).
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"status": "live",
|
|
"age_seconds": 12.4,
|
|
"data": {
|
|
"schema": 1,
|
|
"running": true,
|
|
"published_at": 1790000000.0,
|
|
"stale_after": 720.0,
|
|
"since": 1789990000.0,
|
|
"totals": {"requests": 412, "merged": 3, "not_modified": 0,
|
|
"errors": 1, "http_errors": 2, "retries": 0,
|
|
"throttled": 0, "overruns": 0, "bytes": 18234011,
|
|
"wait_seconds": 0.0},
|
|
"plugins": {
|
|
"football-scoreboard": {"requests": 240, "merged": 2, "bytes": 9120330,
|
|
"hosts": {"site.api.espn.com": 180,
|
|
"sports.core.api.espn.com": 62},
|
|
"...": "the other counters, as in totals"}
|
|
},
|
|
"hosts": {
|
|
"site.api.espn.com": {"requests": 301, "...": "as in totals"}
|
|
},
|
|
"validators": {"entries": 0, "bytes": 0},
|
|
"config": {"enabled": true, "single_flight": true,
|
|
"conditional_get": true, "max_wait_seconds": 2.0,
|
|
"rate_limits": {"*.espn.com": {"per_second": 20.0, "burst": 200.0}}}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
`requests` counts round trips sent (retries inside the HTTP adapter are in
|
|
`retries`), `merged` requests answered by an identical one already in
|
|
flight, `not_modified` 304s served from the stored body, `errors` transport
|
|
failures and `http_errors` responses with status 400 or above. `bytes` is the
|
|
decoded body size. `core` is everything no plugin made.
|
|
|
|
### Get/Set Plugin Limits
|
|
|
|
**GET** `/api/v3/plugins/limits/<plugin_id>`
|
|
|
|
Get a plugin's resource limits. `data` is `null` when none are configured.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"max_memory_mb": 50,
|
|
"max_cpu_percent": 50,
|
|
"max_execution_time": 5.0,
|
|
"warning_threshold": 0.8
|
|
}
|
|
}
|
|
```
|
|
|
|
**POST** `/api/v3/plugins/limits/<plugin_id>`
|
|
|
|
Set a plugin's resource limits. The body replaces all four limits: a key you
|
|
omit is stored as no limit (`warning_threshold` defaults to `0.8`). Each value
|
|
must be a non-negative number or `null`; anything else is a 400.
|
|
|
|
The limits are stored in the shared cache. A display service that has already
|
|
read limits for the plugin keeps using those until it restarts; likewise the
|
|
health and metrics reset routes clear the stored record and the web process's
|
|
copy, not the display service's in-memory state.
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"max_memory_mb": 50,
|
|
"max_cpu_percent": 50,
|
|
"max_execution_time": 5.0,
|
|
"warning_threshold": 0.8
|
|
}
|
|
```
|
|
|
|
### Get Plugin State
|
|
|
|
**GET** `/api/v3/plugins/state`
|
|
|
|
Every plugin that is installed or configured, keyed by plugin id: desired
|
|
state from `config.json` and the plugins directory, observed state from the
|
|
display's runtime snapshot. Built per request; there is no state file.
|
|
Pass `?plugin_id=<id>` for one plugin (`data` is then that record; 404 if
|
|
it is neither installed nor configured).
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"football-scoreboard": {
|
|
"plugin_id": "football-scoreboard",
|
|
"status": "enabled",
|
|
"installed": true,
|
|
"in_config": true,
|
|
"enabled": true,
|
|
"version": "1.2.3",
|
|
"loaded": true,
|
|
"state": "enabled",
|
|
"error_info": null,
|
|
"loaded_version": "1.2.3",
|
|
"loaded_at": 1790000000.0,
|
|
"installed_at": "2025-01-15T10:30:00",
|
|
"last_updated": "2025-01-15T10:30:00"
|
|
}
|
|
},
|
|
"runtime": {"status": "live", "published_at": 1790000030.0, "age_seconds": 12.4, "stale_after": 180.0}
|
|
}
|
|
```
|
|
|
|
`status` is `enabled` / `disabled` for an installed plugin, `unknown` for
|
|
one that is configured but not installed, and `error` when the display
|
|
reports its state as `error`. `installed_at` and `last_updated` are the
|
|
newest successful install, and install or update, in the operation history
|
|
(`null` when it has none). The runtime fields follow the same rule as
|
|
[`/plugins/installed`](#get-installed-plugins): `null` unless
|
|
`runtime.status` is `live`.
|
|
|
|
### Reconcile Plugin State
|
|
|
|
**POST** `/api/v3/plugins/state/reconcile`
|
|
|
|
Reconcile desired state (`config.json` plus the plugins on disk) with the
|
|
display's runtime snapshot. Desired-state gaps are fixed (a plugin on disk
|
|
with no config section is added disabled); observed-state gaps -- enabled
|
|
but not loaded, loaded at an older version than is installed -- are
|
|
reported with `fix_action: "no_action"`.
|
|
|
|
**Request Body** (optional):
|
|
```json
|
|
{
|
|
"force": false
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "...",
|
|
"data": {
|
|
"inconsistencies_found": 1,
|
|
"inconsistencies_fixed": 1,
|
|
"inconsistencies_manual": 0,
|
|
"inconsistencies": [
|
|
{"plugin_id": "...", "type": "...", "description": "...", "fix_action": "..."}
|
|
],
|
|
"fixed": [ ... ],
|
|
"manual_fix_required": [ ... ]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Get Reconciliation Status
|
|
|
|
**GET** `/api/v3/plugins/reconciliation-status`
|
|
|
|
Result of the last startup reconciliation, as written by the display service.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"done": true,
|
|
"unresolved": []
|
|
}
|
|
}
|
|
```
|
|
|
|
Before a run has finished, `data` is `{"done": false, "unresolved": []}`.
|
|
|
|
### Get Plugin Operation
|
|
|
|
**GET** `/api/v3/plugins/operation/<operation_id>`
|
|
|
|
Get status of a queued plugin operation (install, uninstall).
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"operation_id": "uuid-here",
|
|
"operation_type": "install",
|
|
"plugin_id": "football-scoreboard",
|
|
"parameters": {},
|
|
"status": "completed",
|
|
"progress": 100,
|
|
"message": "Installation completed successfully",
|
|
"error": null,
|
|
"result": { ... },
|
|
"created_at": "2025-01-15T10:30:00",
|
|
"started_at": "2025-01-15T10:30:01",
|
|
"completed_at": "2025-01-15T10:30:20"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Get Operation History
|
|
|
|
**GET** `/api/v3/plugins/operation/history?limit=50`
|
|
|
|
Get the plugin operation audit log. `data` is a list.
|
|
|
|
**Query Parameters**:
|
|
- `limit` (optional): Maximum number of records (default: 50)
|
|
- `plugin_id` (optional): Only records for this plugin
|
|
- `operation_type` (optional): Only records of this type (`install`, `update`, `enable`, ...)
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": [
|
|
{
|
|
"operation_id": "uuid-here",
|
|
"operation_type": "install",
|
|
"plugin_id": "football-scoreboard",
|
|
"timestamp": "2025-01-15T10:30:00",
|
|
"status": "success",
|
|
"user": null,
|
|
"details": null,
|
|
"error": null
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Clear Operation History
|
|
|
|
**DELETE** `/api/v3/plugins/operation/history`
|
|
|
|
Clear the operation audit log.
|
|
|
|
### Execute Plugin Action
|
|
|
|
**POST** `/api/v3/plugins/action`
|
|
|
|
Execute an action declared in the plugin manifest's `web_ui_actions`. See
|
|
[PLUGIN_WEB_UI_ACTIONS.md](PLUGIN_WEB_UI_ACTIONS.md).
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"plugin_id": "football-scoreboard",
|
|
"action_id": "refresh_games",
|
|
"params": {}
|
|
}
|
|
```
|
|
|
|
**Response** (fields at the top level; a script that prints JSON can return
|
|
its own object instead):
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Action completed successfully",
|
|
"output": "script stdout"
|
|
}
|
|
```
|
|
|
|
### Upload Plugin Assets
|
|
|
|
**POST** `/api/v3/plugins/assets/upload`
|
|
|
|
Upload images for a plugin. Stored under
|
|
`assets/plugins/<plugin_id>/uploads/`.
|
|
|
|
**Request**: Multipart form data
|
|
- `plugin_id` (required): Plugin identifier
|
|
- `files` (required, repeatable, up to 10): PNG, JPEG, BMP or GIF images, 5 MB each, 50 MB total per plugin
|
|
|
|
**Response** (fields at the top level):
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"uploaded_files": [
|
|
{
|
|
"id": "uuid-here",
|
|
"filename": "image_1700000000_abcd1234.png",
|
|
"path": "assets/plugins/football-scoreboard/uploads/image_1700000000_abcd1234.png",
|
|
"size": 1024,
|
|
"uploaded_at": "2025-01-15T10:30:00Z"
|
|
}
|
|
],
|
|
"total_files": 3
|
|
}
|
|
```
|
|
|
|
### Delete Plugin Asset
|
|
|
|
**POST** `/api/v3/plugins/assets/delete`
|
|
|
|
Delete an uploaded plugin image by its id (the `id` from upload or list).
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"plugin_id": "football-scoreboard",
|
|
"image_id": "uuid-here"
|
|
}
|
|
```
|
|
|
|
### List Plugin Assets
|
|
|
|
**GET** `/api/v3/plugins/assets/list?plugin_id=<plugin_id>`
|
|
|
|
List uploaded images for a plugin.
|
|
|
|
**Query Parameters**:
|
|
- `plugin_id` (required): Plugin identifier
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"assets": [
|
|
{
|
|
"id": "uuid-here",
|
|
"filename": "image_1700000000_abcd1234.png",
|
|
"path": "assets/plugins/football-scoreboard/uploads/image_1700000000_abcd1234.png",
|
|
"size": 1024,
|
|
"uploaded_at": "2025-01-15T10:30:00Z",
|
|
"original_filename": "logo.png"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Upload Calendar Credentials
|
|
|
|
**POST** `/api/v3/plugins/calendar/upload-credentials`
|
|
|
|
Upload the Google OAuth client file for the calendar plugin.
|
|
|
|
**Request**: Multipart form data
|
|
- `file` (required): `credentials.json` (JSON, max 1 MB)
|
|
|
|
### Authenticate Calendar
|
|
|
|
**POST** `/api/v3/plugins/calendar/authenticate`
|
|
|
|
Google OAuth for the calendar plugin, in two steps. Step 1 (no body) returns
|
|
the consent URL. Step 2 posts the URL Google redirected to (it fails to load
|
|
in the browser, but its address carries the authorization code):
|
|
|
|
```json
|
|
{
|
|
"redirect_url": "http://localhost/?code=..."
|
|
}
|
|
```
|
|
|
|
Requires `credentials.json` to have been uploaded first (`400` otherwise).
|
|
|
|
---
|
|
|
|
## Plugin Store
|
|
|
|
### List / Search Store Plugins
|
|
|
|
**GET** `/api/v3/plugins/store/list`
|
|
|
|
List plugins from the registry and saved repositories. The same endpoint
|
|
searches.
|
|
|
|
**Query Parameters**:
|
|
- `query` (optional): Text search over name, description, id
|
|
- `category` (optional): Category filter
|
|
- `tags` (optional, repeatable): Tag filter
|
|
- `fetch_commit_info` (optional): `false` to skip fetching commit metadata from GitHub (default: fetched)
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"plugins": [
|
|
{
|
|
"id": "football-scoreboard",
|
|
"name": "Football Scoreboard",
|
|
"author": "ChuckBuilds",
|
|
"category": "Sports",
|
|
"description": "NFL and NCAA Football scores",
|
|
"tags": ["sports"],
|
|
"stars": 0,
|
|
"verified": true,
|
|
"repo": "https://github.com/ChuckBuilds/ledmatrix-plugins",
|
|
"last_updated": "2025-01-15",
|
|
"last_updated_iso": "2025-01-15T10:30:00Z",
|
|
"last_commit": "abc1234",
|
|
"last_commit_message": "...",
|
|
"last_commit_author": "...",
|
|
"version": "1.2.3",
|
|
"branch": "main",
|
|
"default_branch": "main",
|
|
"plugin_path": "plugins/football-scoreboard",
|
|
"commit": "843588025a81197056f8d96779ccb2be19337ab8",
|
|
"ledmatrix_min_version": "3.7.0",
|
|
"aliases": [],
|
|
"incompatible_reason": null
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
`commit` (the monorepo commit that introduced `version`),
|
|
`ledmatrix_min_version` and `aliases` come from the registry entry and are
|
|
`null` / `[]` when an older registry lacks them. `incompatible_reason` is the
|
|
message an install would be refused with on this core, or `null`.
|
|
|
|
### Get GitHub Status
|
|
|
|
**GET** `/api/v3/plugins/store/github-status`
|
|
|
|
Whether a GitHub token is configured and valid.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"token_status": "valid",
|
|
"authenticated": true,
|
|
"rate_limit": 5000,
|
|
"message": "GitHub API authenticated",
|
|
"error": null
|
|
}
|
|
}
|
|
```
|
|
|
|
`token_status` is `none`, `valid` or `invalid`; `rate_limit` is the nominal
|
|
hourly limit (60 unauthenticated), not a live count.
|
|
|
|
### Refresh Plugin Store
|
|
|
|
**POST** `/api/v3/plugins/store/refresh`
|
|
|
|
Force refresh of the registry cache.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Plugin store refreshed",
|
|
"plugin_count": 42
|
|
}
|
|
```
|
|
|
|
### Get Saved Repositories
|
|
|
|
**GET** `/api/v3/plugins/saved-repositories`
|
|
|
|
Get the list of saved custom plugin repositories.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"repositories": [
|
|
{
|
|
"url": "https://github.com/user/ledmatrix-plugins",
|
|
"name": "ledmatrix-plugins",
|
|
"type": "registry"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Save Repository
|
|
|
|
**POST** `/api/v3/plugins/saved-repositories`
|
|
|
|
Save a custom plugin repository. Returns the updated list in
|
|
`data.repositories`.
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"repo_url": "https://github.com/user/ledmatrix-plugins",
|
|
"name": "Custom Plugins"
|
|
}
|
|
```
|
|
|
|
### Delete Saved Repository
|
|
|
|
**DELETE** `/api/v3/plugins/saved-repositories`
|
|
|
|
Remove a saved repository. Returns the updated list in `data.repositories`.
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"repo_url": "https://github.com/user/ledmatrix-plugins"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## System
|
|
|
|
### Get System Status
|
|
|
|
**GET** `/api/v3/system/status`
|
|
|
|
Get system status and metrics (cached for 10 seconds).
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"timestamp": 1234567890.123,
|
|
"uptime": "3d 4h",
|
|
"uptime_seconds": 273600,
|
|
"service_active": true,
|
|
"cpu_percent": 25.5,
|
|
"memory_used_percent": 45.2,
|
|
"memory_total_mb": 3794.0,
|
|
"memory_used_mb": 1715.0,
|
|
"memory_available_mb": 1900.0,
|
|
"cpu_temp": 45.0,
|
|
"disk_used_percent": 60.0,
|
|
"disk_total_gb": 29.0,
|
|
"disk_used_gb": 17.4
|
|
}
|
|
}
|
|
```
|
|
|
|
### Get System Version
|
|
|
|
**GET** `/api/v3/system/version`
|
|
|
|
Get LEDMatrix repository version.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"version": "v2.4-10-g1234567"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Check for Update
|
|
|
|
**GET** `/api/v3/system/check-update`
|
|
|
|
Whether newer code is available on this device's update channel. On
|
|
`stable` that is a newer release tag than the checkout (`target_version`
|
|
names it); on `beta`, and on `stable` while it waits on a branch for a
|
|
release that contains the current commit, it is commits on `origin/main`
|
|
the checkout lacks. A detached checkout newer than the newest release is
|
|
never offered an update: Update Code leaves it where it is until a release
|
|
includes it, and `channel_message` says so in the General tab's words.
|
|
Cached briefly. Fields at the top level (no envelope):
|
|
|
|
```json
|
|
{
|
|
"update_available": true,
|
|
"remote_sha": "abc123...",
|
|
"commits_behind": 3,
|
|
"target_version": "v3.8.0",
|
|
"channel": "stable",
|
|
"configured_channel": "stable",
|
|
"waiting": false,
|
|
"newest_release": "v3.8.0",
|
|
"current_release": null,
|
|
"channel_message": "Stable: release v3.8.0 is available."
|
|
}
|
|
```
|
|
|
|
When git cannot run the check, the response also carries
|
|
`"check_failed": true` and an `error` explaining why.
|
|
|
|
### Update Channel
|
|
|
|
**GET** `/api/v3/system/update-channel`
|
|
|
|
The update channel and what the next Update Code or weekly update would do
|
|
(in `data`): `configured` (`"stable"`, `"beta"` or `null` for a config from
|
|
before channels), `channel` (the one in effect), `waiting` (stable, but the
|
|
device is newer than the newest release, so it follows `main` for now),
|
|
`action` (`none`, `checkout_tag`, `pull` or `switch_to_beta`),
|
|
`newest_release`, `current_release`, `branch` (`""` when on a release tag),
|
|
`message`. Reads local refs; `?fetch=1` fetches from origin first.
|
|
|
|
**POST** `/api/v3/system/update-channel`
|
|
|
|
```json
|
|
{
|
|
"channel": "beta"
|
|
}
|
|
```
|
|
|
|
Saves `auto_update.channel`. The next update applies it; switching to
|
|
`stable` never installs an older version than the one running, and the
|
|
`message` says when the device keeps following `main` until a newer release.
|
|
400 for anything but `stable` or `beta`. The General tab form also accepts
|
|
`auto_update_channel` on `POST /api/v3/config/main`.
|
|
|
|
### Automatic Update Status
|
|
|
|
**GET** `/api/v3/system/auto-update`
|
|
|
|
Weekly automatic-update status for the General tab and the Overview banner:
|
|
`last_run`, `summary`, `status`, `next_due`, `alert`, `alert_id`,
|
|
`verifier_installed`, `setup_status`, `setup_message`, `verifying` (in
|
|
`data`).
|
|
|
|
**POST** `/api/v3/system/auto-update/dismiss`
|
|
|
|
Hide the current automatic-update alert until a new one replaces it.
|
|
|
|
```json
|
|
{
|
|
"alert_id": "..."
|
|
}
|
|
```
|
|
|
|
### Git Info
|
|
|
|
**GET** `/api/v3/system/git-info`
|
|
|
|
Branch, dirty state, recent commits and remote for the Tools tab. Fields at
|
|
the top level: `branch`, `dirty`, `status`, `recent_commits`, `remote_url`
|
|
(credentials scrubbed), `upstream`, `can_pull`, and for the update channel
|
|
`detached`, `version` (`git describe`), `current_release` (the release tag
|
|
HEAD is exactly on, else `null`) and `channel_message` (detached only).
|
|
|
|
### Git Branches
|
|
|
|
**GET** `/api/v3/system/git-branches`
|
|
|
|
Fetches `origin` and lists branches to switch to: `current`, `upstream`,
|
|
`local` (list), `remote_only` (list), at the top level.
|
|
|
|
### Execute System Action
|
|
|
|
**POST** `/api/v3/system/action`
|
|
|
|
Execute system-level actions. Send JSON (`Content-Type: application/json`).
|
|
A form-encoded or `text/plain` body is accepted only with an `HX-Request`
|
|
header (HTMX sends it; a cross-site HTML form cannot) and is otherwise
|
|
refused with `415`.
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"action": "restart_display_service"
|
|
}
|
|
```
|
|
|
|
**Available Actions**:
|
|
- `start_display`: Start the display service
|
|
- `stop_display`: Stop the display service
|
|
- `restart_display_service`: Restart the display service
|
|
- `restart_web_service`: Restart the web interface service
|
|
- `enable_autostart`: Enable display service autostart
|
|
- `disable_autostart`: Disable display service autostart
|
|
- `reboot_system`: Reboot the Raspberry Pi
|
|
- `shutdown_system`: Power off the Raspberry Pi
|
|
- `git_pull`: Update LEDMatrix from git (the Update button)
|
|
- `checkout_branch`: Switch branch; takes `branch` and optional `stash`
|
|
- `force_git_reset`: `git reset --hard origin/main`
|
|
- `install_base_requirements`: pip install `requirements.txt` and `web_interface/requirements.txt`
|
|
- `install_plugin_requirements`: pip install every plugin's `requirements.txt`
|
|
- `clear_pycache`: Delete `__pycache__` directories
|
|
|
|
**Response** (service actions):
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Action completed"
|
|
}
|
|
```
|
|
|
|
A failed service action returns `"status": "error"` with `returncode` and
|
|
`stderr`. `git_pull` returns `message`, `restart_required` and
|
|
`dependency_failures`; the install actions return `output` or `details`.
|
|
|
|
---
|
|
|
|
## Backup and Restore
|
|
|
|
Backups are ZIP files kept in the backup export directory.
|
|
|
|
### Preview
|
|
|
|
**GET** `/api/v3/backup/preview`
|
|
|
|
Summary of what a new backup would include.
|
|
|
|
### List
|
|
|
|
**GET** `/api/v3/backup/list`
|
|
|
|
Stored backups, newest first. `data` is a list of
|
|
`{"filename", "size", "created_at"}`.
|
|
|
|
### Export
|
|
|
|
**POST** `/api/v3/backup/export`
|
|
|
|
Create a backup. Returns `{"status": "success", "filename": "..."}`.
|
|
|
|
### Validate
|
|
|
|
**POST** `/api/v3/backup/validate`
|
|
|
|
Check an uploaded backup and return its manifest in `data`.
|
|
|
|
**Request**: Multipart form data
|
|
- `backup_file` (required): the ZIP
|
|
|
|
### Restore
|
|
|
|
**POST** `/api/v3/backup/restore`
|
|
|
|
Restore an uploaded backup.
|
|
|
|
**Request**: Multipart form data
|
|
- `backup_file` (required): the ZIP
|
|
- `options` (optional): JSON object; keys `restore_config`, `restore_secrets`,
|
|
`restore_wifi`, `restore_fonts`, `restore_plugin_uploads`,
|
|
`reinstall_plugins` (each defaults to `true`; unknown keys are rejected)
|
|
|
|
A partial restore answers `500` with `"status": "error"`, a message listing
|
|
what did and didn't restore, and the result in `data`.
|
|
|
|
### Download
|
|
|
|
**GET** `/api/v3/backup/download/<filename>`
|
|
|
|
Download a stored backup.
|
|
|
|
### Delete
|
|
|
|
**DELETE** `/api/v3/backup/<filename>`
|
|
|
|
Delete a stored backup.
|
|
|
|
---
|
|
|
|
## Fonts
|
|
|
|
### Get Font Catalog
|
|
|
|
**GET** `/api/v3/fonts/catalog`
|
|
|
|
Fonts in `assets/fonts/`, keyed by file name without extension.
|
|
|
|
`used_by` lists the plugins that use the font, as the display service last
|
|
reported: the loaded plugins that registered it through
|
|
`FontManager.register_manager_font()`, under whatever family, alias
|
|
(`press_start`, `four_by_six`, ...) or path they gave. It is `[]` when no
|
|
loaded plugin registered it and `null` when the display service has not
|
|
reported yet (`font_usage.available` is then `false`). A plugin that opens a
|
|
font file directly is not counted, so `[]` does not prove a font is unused.
|
|
The file scan is cached for 5 minutes; `used_by` is read on every request.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"catalog": {
|
|
"PressStart2P-Regular": {
|
|
"filename": "PressStart2P-Regular.ttf",
|
|
"family_name": "Press Start 2P",
|
|
"display_name": "Press Start 2P",
|
|
"path": "assets/fonts/PressStart2P-Regular.ttf",
|
|
"type": "ttf",
|
|
"is_system": true,
|
|
"scalable": true,
|
|
"native_size": null,
|
|
"metadata": { ... },
|
|
"used_by": ["calendar", "hello-world"]
|
|
}
|
|
},
|
|
"font_usage": {
|
|
"available": true,
|
|
"generated_at": "2026-09-23T10:00:00"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Get Font Tokens
|
|
|
|
**GET** `/api/v3/fonts/tokens`
|
|
|
|
Get font size token definitions.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"tokens": {
|
|
"xs": 6,
|
|
"sm": 8,
|
|
"md": 10,
|
|
"lg": 12,
|
|
"xl": 14,
|
|
"xxl": 16
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Upload Font
|
|
|
|
**POST** `/api/v3/fonts/upload`
|
|
|
|
Upload a custom font file. It is saved as `assets/fonts/<font_family><ext>`.
|
|
|
|
**Request**: Multipart form data
|
|
- `font_file` (required): `.ttf`, `.otf` or `.bdf`, max 10 MB
|
|
- `font_family` (required): name for the font (letters, numbers, `_`, `-`)
|
|
|
|
**Response** (fields at the top level):
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"message": "Font custom_font uploaded successfully",
|
|
"font_family": "custom_font",
|
|
"filename": "custom_font.ttf",
|
|
"path": "assets/fonts/custom_font.ttf"
|
|
}
|
|
```
|
|
|
|
### Delete Font
|
|
|
|
**DELETE** `/api/v3/fonts/<font_family>`
|
|
|
|
Delete an uploaded font (`<font_family>` is the file name without
|
|
extension). System fonts answer `403`. A font a plugin uses is not refused;
|
|
the Fonts tab names those plugins (the catalog's `used_by`) in its
|
|
confirmation first.
|
|
|
|
### Font Preview
|
|
|
|
**GET** `/api/v3/fonts/preview?font=<filename>&text=<sample>&size=12`
|
|
|
|
Render text in a font, for the web UI font picker. BDF fonts are not
|
|
previewed (`400`).
|
|
|
|
**Query Parameters**:
|
|
- `font` (required): font file name in `assets/fonts/` (e.g. `press_start.ttf`)
|
|
- `text` (optional): up to 100 characters (default `Sample Text 123`)
|
|
- `size` (optional): 4-72 (default 12)
|
|
- `bg`, `fg` (optional): hex colours without `#` (default `000000` / `ffffff`)
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"image": "data:image/png;base64,...",
|
|
"width": 140,
|
|
"height": 32
|
|
}
|
|
}
|
|
```
|
|
|
|
> Font overrides (`/api/v3/fonts/overrides`) were removed. Per-plugin font
|
|
> choices are made in each plugin's own settings.
|
|
|
|
---
|
|
|
|
## Cache
|
|
|
|
### List Cache Entries
|
|
|
|
**GET** `/api/v3/cache/list`
|
|
|
|
List cache files.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"cache_files": [ ... ],
|
|
"cache_dir": "/var/cache/ledmatrix",
|
|
"total_files": 12
|
|
}
|
|
}
|
|
```
|
|
|
|
### Delete Cache Entry
|
|
|
|
**POST** `/api/v3/cache/delete`
|
|
|
|
Delete one cache entry by key. There is no clear-all option here; use
|
|
`scripts/utils/clear_cache.py --clear-all` on the Pi for that.
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"key": "weather_current_12345"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## WiFi
|
|
|
|
### Get WiFi Status
|
|
|
|
**GET** `/api/v3/wifi/status`
|
|
|
|
Get current WiFi connection status.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"connected": true,
|
|
"ssid": "MyNetwork",
|
|
"ip_address": "192.168.1.100",
|
|
"signal": 70,
|
|
"ap_mode_active": false,
|
|
"auto_enable_ap_mode": true,
|
|
"last_connect_attempt": null
|
|
}
|
|
}
|
|
```
|
|
|
|
### Scan WiFi Networks
|
|
|
|
**GET** `/api/v3/wifi/scan`
|
|
|
|
Scan for available WiFi networks. `data` is a list. If AP mode is active it is
|
|
turned off for the scan and back on afterwards, and `message` says so.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": [
|
|
{
|
|
"ssid": "MyNetwork",
|
|
"signal": 70,
|
|
"security": "WPA2",
|
|
"frequency": 2437
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Connect to WiFi
|
|
|
|
**POST** `/api/v3/wifi/connect`
|
|
|
|
Connect to a WiFi network.
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"ssid": "MyNetwork",
|
|
"password": "mypassword"
|
|
}
|
|
```
|
|
|
|
**Response**: `"status": "success"` when connected; `"status": "pending"`
|
|
(with `data.ssid`) when the connection continues in the background — poll
|
|
`/wifi/status` and read `last_connect_attempt`. A wrong password answers
|
|
`400` with `"error_type": "wrong_password"`.
|
|
|
|
### Disconnect from WiFi
|
|
|
|
**POST** `/api/v3/wifi/disconnect`
|
|
|
|
Disconnect from current WiFi network.
|
|
|
|
### Enable Access Point Mode
|
|
|
|
**POST** `/api/v3/wifi/ap/enable`
|
|
|
|
Enable WiFi access point mode. Optional body `{"force": true}`.
|
|
|
|
### Disable Access Point Mode
|
|
|
|
**POST** `/api/v3/wifi/ap/disable`
|
|
|
|
Disable WiFi access point mode.
|
|
|
|
### Get Auto-Enable AP Setting
|
|
|
|
**GET** `/api/v3/wifi/ap/auto-enable`
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"auto_enable_ap_mode": true
|
|
}
|
|
}
|
|
```
|
|
|
|
### Set Auto-Enable AP
|
|
|
|
**POST** `/api/v3/wifi/ap/auto-enable`
|
|
|
|
**Request Body**:
|
|
```json
|
|
{
|
|
"auto_enable_ap_mode": true
|
|
}
|
|
```
|
|
|
|
### WiFi Radio
|
|
|
|
**GET** `/api/v3/wifi/radio`
|
|
|
|
Radio state: `data.enabled` (`null` if unknown), `data.ethernet_connected`,
|
|
`data.available`.
|
|
|
|
**POST** `/api/v3/wifi/radio`
|
|
|
|
Turn the WiFi radio on or off. Turning it off is refused unless Ethernet is
|
|
connected or `force` is true, so you don't cut off your own connection.
|
|
|
|
```json
|
|
{
|
|
"enabled": false,
|
|
"force": false
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Streams
|
|
|
|
Server-Sent Events, defined in `web_interface/app.py`. Each event is one
|
|
`data: <json>` line; idle connections get `: heartbeat` comments.
|
|
|
|
### System Statistics Stream
|
|
|
|
**GET** `/api/v3/stream/stats`
|
|
|
|
```
|
|
data: {"timestamp": 1234567890.1, "uptime": "Running", "service_active": true, "cpu_percent": 25.5, "memory_used_percent": 45.2, "memory_available_mb": 1900.0, "cpu_temp": 45.0, "disk_used_percent": 60.0, "power": {...}}
|
|
```
|
|
|
|
### Display Preview Stream
|
|
|
|
**GET** `/api/v3/stream/display`
|
|
|
|
```
|
|
data: {"timestamp": 1234567890.123, "width": 128, "height": 32, "image": "base64_data_here"}
|
|
```
|
|
|
|
### Service Logs Stream
|
|
|
|
**GET** `/api/v3/stream/logs`
|
|
|
|
Each event carries the latest journal lines for `ledmatrix` and
|
|
`ledmatrix-web` as one text block:
|
|
|
|
```
|
|
data: {"timestamp": 1234567890.123, "logs": "2025-01-15T10:30:00+0000 host python[123]: ..."}
|
|
```
|
|
|
|
---
|
|
|
|
## Logs
|
|
|
|
### Get Logs
|
|
|
|
**GET** `/api/v3/logs`
|
|
|
|
The last 100 journal lines for `ledmatrix.service` and
|
|
`ledmatrix-web.service`, as one text block. Takes no parameters.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"logs": "2025-01-15T10:30:00+0000 host python[123]: Plugin loaded: football-scoreboard\n..."
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Error tracking
|
|
|
|
Plugin errors are recorded by the display service (`ledmatrix.service`),
|
|
which runs the plugins. It publishes a snapshot to the shared cache directory
|
|
(`plugin_error_snapshot`) at most every 10 seconds, and only when something
|
|
changed, so these endpoints lag the display by up to about 15 seconds. The
|
|
counts cover the display service's current run: they start at zero when it
|
|
restarts. Error messages and stack traces have credentials redacted, and
|
|
messages, traces and context values are truncated in the snapshot.
|
|
|
|
Every response below adds three fields to the shape it always had:
|
|
|
|
| Field | Meaning |
|
|
|---|---|
|
|
| `snapshot_available` | `false` until the display service has reported (for example, it is not running). Counts are then zero. |
|
|
| `generated_at` | When the display service produced the snapshot (ISO, the Pi's local time), or `null`. |
|
|
| `clear_pending` | A clear has been requested and the display service has not applied it yet. |
|
|
|
|
### Get Error Summary
|
|
|
|
**GET** `/api/v3/errors/summary`
|
|
|
|
Aggregated counts, detected patterns and recent errors (the last 20).
|
|
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"session_start": "2026-09-23T09:40:02.118000",
|
|
"total_errors": 13,
|
|
"error_rate_per_hour": 41.2,
|
|
"error_counts_by_type": {"ConnectionError": 12, "ValueError": 1},
|
|
"plugin_error_counts": {"weather": {"ConnectionError": 12}, "stocks": {"ValueError": 1}},
|
|
"active_patterns": {
|
|
"ConnectionError": {
|
|
"error_type": "ConnectionError", "count": 12,
|
|
"first_seen": "2026-09-23T09:41:10.500000", "last_seen": "2026-09-23T09:58:36.020000",
|
|
"affected_plugins": ["weather"], "sample_messages": ["Read timed out."],
|
|
"severity": "error"
|
|
}
|
|
},
|
|
"recent_errors": [
|
|
{"error_type": "ValueError", "message": "could not parse price",
|
|
"timestamp": "2026-09-23T09:58:36.100000", "context": {},
|
|
"plugin_id": "stocks", "operation": "update", "stack_trace": "Traceback ..."}
|
|
],
|
|
"generated_at": "2026-09-23T09:58:40.000000",
|
|
"snapshot_available": true,
|
|
"clear_pending": false
|
|
},
|
|
"message": "Error summary retrieved"
|
|
}
|
|
```
|
|
|
|
### Get Plugin Errors
|
|
|
|
**GET** `/api/v3/errors/plugin/<plugin_id>`
|
|
|
|
Error health and statistics for one plugin: `plugin_id`, `status`
|
|
(`healthy`, `degraded` or `unhealthy`), `total_errors`, `error_types`,
|
|
`recent_error_count`, `last_error` (a `recent_errors` entry or `null`), plus
|
|
the three fields above. A plugin with no recorded errors is `healthy`.
|
|
|
|
### Clear Errors
|
|
|
|
**POST** `/api/v3/errors/clear`
|
|
|
|
Clear error records older than `max_age_hours` (default 24, 1-8760), or every
|
|
error with `"all": true` (`max_age_hours` is then ignored).
|
|
|
|
```json
|
|
{
|
|
"max_age_hours": 24
|
|
}
|
|
```
|
|
|
|
The clear is asynchronous. The web interface records a request
|
|
(`plugin_error_clear_request` in the shared cache), and the display service
|
|
applies it within about 5 seconds, rebuilding its counts from the errors it
|
|
keeps and republishing. Reads hide the cleared errors from the moment the
|
|
request is recorded. Until the display service applies an age-based clear,
|
|
`recent_errors` and `active_patterns` are already filtered but the counts
|
|
are the old ones, and `clear_pending` is `true`.
|
|
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"cleared_count": 13,
|
|
"clear_requested": true,
|
|
"request_id": "5f0c1e...",
|
|
"cutoff": "2026-09-23T09:59:02.310000"
|
|
},
|
|
"message": "Clear of all errors requested; the display service applies it within about 5 seconds"
|
|
}
|
|
```
|
|
|
|
`cleared_count` is how many of the reported errors the clear hides. It is
|
|
`null` when that cannot be known before the display service applies it (an
|
|
age-based clear over more errors than the report lists). A request that
|
|
could not be written to the shared cache answers `500`.
|
|
|
|
---
|
|
|
|
## Health and Status
|
|
|
|
### Health Check
|
|
|
|
**GET** `/api/v3/health`
|
|
|
|
Health of the web interface, display service, config file, plugin system and
|
|
display snapshot. `data.status` is `healthy` or `degraded`, with
|
|
`data.services` and `data.checks`.
|
|
|
|
`data.checks.display_loop` is the display's render-loop heartbeat: `running`
|
|
(with `heartbeat_age_seconds`), `stalled` (no heartbeat for 60s: the panel is
|
|
frozen even if the service is active; the status turns `degraded`), or
|
|
`not_reported` when the display writes none (not started yet, the dev server,
|
|
Windows), which does not affect the status.
|
|
|
|
Open even when the web login is on, for uptime monitors; a caller that is not
|
|
logged in (and has no token) then gets only `{"status": "success", "data":
|
|
{"status": "healthy" | "degraded"}}`. A stalled render loop still shows there
|
|
as `degraded`; the `checks` detail is only for logged-in callers, tokens and
|
|
requests from the Pi itself.
|
|
|
|
### Hardware Status
|
|
|
|
**GET** `/api/v3/hardware/status`
|
|
|
|
LED matrix initialization result written by the display service at startup.
|
|
Before the service has written it, `data` is
|
|
`{"ok": null, "error": "Display service not yet started"}`.
|
|
|
|
### Sync Status
|
|
|
|
**GET** `/api/v3/sync/status`
|
|
|
|
Live multi-display sync status from the display process; before it has
|
|
written one, `data` is `{"role", "port", "state": "starting"}` from config.
|
|
|
|
---
|
|
|
|
## Schedule (dim/power)
|
|
|
|
### Get Dim Schedule
|
|
|
|
**GET** `/api/v3/config/dim-schedule`
|
|
|
|
Read the schedule that lowers brightness at configured times.
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"enabled": true,
|
|
"dim_brightness": 30,
|
|
"mode": "per-day",
|
|
"days": {
|
|
"monday": { "enabled": true, "start_time": "20:00", "end_time": "07:00" },
|
|
"tuesday": { ... }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
In `global` mode, `start_time` and `end_time` sit at the top level instead
|
|
of `days`.
|
|
|
|
### Update Dim Schedule
|
|
|
|
**POST** `/api/v3/config/dim-schedule`
|
|
|
|
Replace the dim schedule. `dim_brightness` is 0-100 (default 30). In
|
|
`per-day` mode the days can be sent either as the `days` object that GET
|
|
returns, or as the web form's flat fields (`monday_enabled`,
|
|
`monday_start`, `monday_end`, ...). A day that is not sent counts as
|
|
enabled with default times `20:00`-`07:00`; at least one day must be
|
|
enabled.
|
|
|
|
---
|
|
|
|
## Integrations
|
|
|
|
### MQTT Bridge
|
|
|
|
**GET** `/api/v3/integrations/mqtt-bridge`
|
|
|
|
Home Assistant MQTT bridge service state and settings: `data.service`,
|
|
`data.config_exists`, `data.config_path`, `data.config` (password
|
|
omitted), `data.password_set`, `data.api_token_set`, `data.env_override_prefix`.
|
|
|
|
**PUT** `/api/v3/integrations/mqtt-bridge/config`
|
|
|
|
Write `integrations/mqtt_bridge/bridge_config.json`. Only the keys you send
|
|
change. The password is write-only: omit `mqtt_password` to keep it, send a
|
|
value to replace it, or send `"clear_password": true`. The web-login API token
|
|
the bridge sends (`ledmatrix_api_token`, needed only when login is on and the
|
|
bridge runs on another machine) is write-only the same way, cleared with
|
|
`"clear_api_token": true`. A password with
|
|
`mqtt_tls` off is refused unless `allow_insecure_mqtt` is true. Returns
|
|
`data.password_set`, `data.api_token_set` and `data.restart_required` (the
|
|
bridge must be restarted to pick up changes). See
|
|
[integrations/mqtt_bridge/README.md](../integrations/mqtt_bridge/README.md).
|
|
|
|
---
|
|
|
|
## Web login and API tokens
|
|
|
|
The optional password and API tokens (`web_auth` in
|
|
`config/config_secrets.json`, `web_interface/auth.py`). No route here returns
|
|
the password hash, a token hash or the cookie key. A request authenticated by
|
|
an API token gets `403` `TOKEN_NOT_ALLOWED` from every route in this section:
|
|
tokens are for integrations, not for changing who can log in. Wrong current
|
|
passwords (`403` `WRONG_PASSWORD`) count against the same per-address limit as
|
|
the login page: 5 a minute, 30 an hour, then `429`.
|
|
|
|
Lost password: run `sudo python3 scripts/reset_web_password.py` on the Pi.
|
|
|
|
### Login status
|
|
|
|
**GET** `/api/v3/auth/status`
|
|
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"enabled": true,
|
|
"signed_in": true,
|
|
"access": "session",
|
|
"min_password_length": 8,
|
|
"tokens": [
|
|
{"id": "3f9c1a2b4d5e6f70", "name": "Home Assistant", "prefix": "lmx_Ab3d",
|
|
"created_at": "2026-09-29T20:14:03+00:00"}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
`access` is how this request got in: `open` (login off), `session`,
|
|
`localhost`, `ap-setup` or `token`.
|
|
|
|
### Set or change the password
|
|
|
|
**POST** `/api/v3/auth/password`
|
|
|
|
Body: `{"new_password": "...", "current_password": "..."}`.
|
|
`current_password` is required once login is on. At least 8 characters, no
|
|
leading or trailing space (`400` `WEAK_PASSWORD`). Setting the first password
|
|
turns login on. Every existing login session ends; the caller's own browser
|
|
is signed in again with the answer.
|
|
|
|
### Turn login off
|
|
|
|
**POST** `/api/v3/auth/disable`
|
|
|
|
Body: `{"current_password": "..."}`. Removes the password; API tokens are
|
|
kept (and are needed again if login is turned back on).
|
|
|
|
### API tokens
|
|
|
|
**GET** `/api/v3/auth/tokens` — `data.tokens`, as in the status answer.
|
|
|
|
**POST** `/api/v3/auth/tokens` — body `{"name": "Home Assistant"}` (1-60
|
|
characters). Answers `201` with `data.token`, the token itself (`lmx_` plus 43
|
|
characters), and `data.record`. **The token is never shown again**; only its
|
|
SHA-256 is stored. At most 50 tokens.
|
|
|
|
**DELETE** `/api/v3/auth/tokens/<id>` — revoke; it stops working on the next
|
|
request. `404` for an unknown id.
|
|
|
|
Send a token as `Authorization: Bearer <token>`.
|
|
|
|
### Login page
|
|
|
|
`GET /login` shows the login form (and redirects home when login is off or
|
|
this browser is already signed in); `POST /login` with a form field `password`
|
|
(and optional `next`, a path on this server) signs in and redirects to `next`,
|
|
or answers `401` with the form again. `POST /logout` ends the session. Both
|
|
are outside `/api/v3` and go through the cross-site check like every other
|
|
`POST`.
|
|
|
|
---
|
|
|
|
## Plugin-specific endpoints
|
|
|
|
A handful of endpoints belong to individual plugins. The music plugin's
|
|
Spotify and YouTube Music sign-in and the Of-The-Day data files go through the
|
|
plugin's own web UI actions ([Execute Plugin Action](#execute-plugin-action))
|
|
rather than dedicated routes.
|
|
|
|
### Calendar
|
|
|
|
**GET** `/api/v3/plugins/calendar/list-calendars`
|
|
|
|
List the calendars on the authenticated Google account. Used by the calendar
|
|
plugin's config UI. Returns `calendars` at the top level. The upload and
|
|
authenticate endpoints are under [Plugins](#upload-calendar-credentials).
|
|
|
|
### Plugin Static Assets
|
|
|
|
**GET** `/api/v3/plugins/<plugin_id>/static/<path:file_path>`
|
|
|
|
Serve a static file from a plugin's directory. Used internally by the web UI
|
|
to render plugin previews and icons.
|
|
|
|
---
|
|
|
|
## Starlark Apps
|
|
|
|
The Starlark plugin lets you run [Tronbyt](https://github.com/tronbyt/apps)
|
|
Starlark apps on the matrix. These endpoints expose its UI.
|
|
|
|
### Status
|
|
|
|
**GET** `/api/v3/starlark/status`
|
|
|
|
Returns whether the Pixlet binary is installed and the Starlark plugin
|
|
is operational.
|
|
|
|
### Install Pixlet
|
|
|
|
**POST** `/api/v3/starlark/install-pixlet`
|
|
|
|
Download and install the Pixlet binary on the Pi.
|
|
|
|
### Apps
|
|
|
|
**GET** `/api/v3/starlark/apps` — list installed Starlark apps
|
|
**GET** `/api/v3/starlark/apps/<app_id>` — get app details
|
|
**DELETE** `/api/v3/starlark/apps/<app_id>` — uninstall an app
|
|
**GET** `/api/v3/starlark/apps/<app_id>/config` — get app config schema
|
|
**PUT** `/api/v3/starlark/apps/<app_id>/config` — update app config
|
|
**POST** `/api/v3/starlark/apps/<app_id>/render` — render app to a frame
|
|
**POST** `/api/v3/starlark/apps/<app_id>/toggle` — enable/disable app (`{"enabled": bool}`; omit to flip)
|
|
|
|
### Repository (Tronbyt community apps)
|
|
|
|
**GET** `/api/v3/starlark/repository/categories` — list categories
|
|
**GET** `/api/v3/starlark/repository/browse` — every app with metadata (filtering happens client-side; cached for 2 hours)
|
|
**POST** `/api/v3/starlark/repository/install` — install an app: `{"app_id": "...", "render_interval": 300, "display_duration": 15}`
|
|
|
|
### Upload custom app
|
|
|
|
**POST** `/api/v3/starlark/upload`
|
|
|
|
Upload a custom Starlark `.star` file as a new app. Multipart fields: `file`
|
|
(required, max 5 MB), `name`, `app_id`, `render_interval`,
|
|
`display_duration`.
|
|
|
|
### Editor
|
|
|
|
A Pixlet editing session for one app. The display is stopped while a session
|
|
runs.
|
|
|
|
**GET** `/api/v3/starlark/editor/apps` — apps the editor can open (`data.apps`, `data.apps_dir`, `data.pixlet_available`)
|
|
**GET** `/api/v3/starlark/editor/status` — `data.running`, plus `app_id`, `port`, `pid`, `started_at`, `timeout`, `seconds_remaining`, `host_bound` while running
|
|
**POST** `/api/v3/starlark/editor/start` — `{"app_id": "...", "timeout": 1800, "port": 8080}` (`timeout` and `port` optional)
|
|
**POST** `/api/v3/starlark/editor/stop` — end the session and restart the display
|
|
|
|
---
|
|
|
|
## Error Responses
|
|
|
|
Errors use one of two shapes. Most endpoints answer:
|
|
|
|
```json
|
|
{
|
|
"status": "error",
|
|
"message": "Error description",
|
|
"details": "Additional error details (optional)"
|
|
}
|
|
```
|
|
|
|
An exception no route anticipated gets this shape too, with a 500, the
|
|
message `An error occurred; see logs for details`, and `details` naming the
|
|
exception type and text (credentials redacted). The api_v3 blueprint's
|
|
error handler produces it, so it is the same for every `/api/v3` route.
|
|
|
|
Endpoints built on the structured error helper add a code, and usually
|
|
suggested fixes (the web UI's error dialog lists them):
|
|
|
|
```json
|
|
{
|
|
"status": "error",
|
|
"error_code": "CONFIG_SAVE_FAILED",
|
|
"message": "Error description",
|
|
"details": "optional",
|
|
"context": { },
|
|
"suggested_fixes": [ ]
|
|
}
|
|
```
|
|
|
|
**Common HTTP Status Codes**:
|
|
- `200`: Success
|
|
- `400`: Bad Request (invalid parameters)
|
|
- `403`: Forbidden (e.g. deleting a system font)
|
|
- `404`: Not Found (resource doesn't exist)
|
|
- `408`: Timed out (plugin actions, auth scripts)
|
|
- `500`: Internal Server Error
|
|
- `503`: Service Unavailable (feature not available)
|
|
|
|
---
|
|
|
|
## See Also
|
|
|
|
- [Plugin API Reference](PLUGIN_API_REFERENCE.md) - API for plugin developers
|
|
- [Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md) - Complete plugin development guide
|
|
- [Web Interface README](../web_interface/README.md) - Web interface documentation
|