Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2d52a10
feat(matrix): add MQTT matrix targets and ESPHome client
2shepard2 Jul 25, 2026
9adb9a6
fix(storage): tolerate transient SQLite locks
2shepard2 Jul 25, 2026
9bff16c
feat(matrix): add target management UI and compatibility checks
2shepard2 Jul 25, 2026
5405a5e
feat(matrix): improve status scenes and overflow rendering
2shepard2 Jul 26, 2026
60c8f1a
feat(matrix): add local effect scenes
2shepard2 Jul 26, 2026
82228ec
feat(matrix): apply themes to targets
2shepard2 Jul 26, 2026
8c44183
fix(matrix): preserve effective theme colors
2shepard2 Jul 26, 2026
e2a30bb
feat(matrix): add effect preview controls
2shepard2 Jul 26, 2026
3724249
feat(matrix): add weather effect scenes
2shepard2 Jul 26, 2026
ddd5fce
feat(matrix): improve effect controls
2shepard2 Jul 26, 2026
1ed6d0b
feat(matrix): add target self-registration
2shepard2 Jul 26, 2026
8d554de
feat(sendspin): add music visualizer support
2shepard2 Jul 26, 2026
a965e48
fix(sendspin): add pairing-capable adapter
2shepard2 Jul 26, 2026
0d7f4b5
fix(sendspin): install git in adapter image
2shepard2 Jul 26, 2026
6597d06
fix(sendspin): run adapter with data owner
2shepard2 Jul 26, 2026
310c562
fix(sendspin): disable legacy direct client
2shepard2 Jul 27, 2026
1d4690f
chore(sendspin): log visualizer handshake support
2shepard2 Jul 27, 2026
7952bb7
fix(sendspin): match Music Assistant 2.9 SDK
2shepard2 Jul 27, 2026
5ccefbe
fix(sendspin): serialize playback status
2shepard2 Jul 27, 2026
39baf57
fix(sendspin): retain server websocket session
2shepard2 Jul 27, 2026
1b8754b
fix(sendspin): preserve configured adapter roles
2shepard2 Jul 27, 2026
84880bb
docs: document Matrix and SendSpin integrations
2shepard2 Jul 27, 2026
c940a04
feat: configure SendSpin and package ESPHome base
2shepard2 Jul 27, 2026
c478a26
feat: add SendSpin music visualizer widgets
2shepard2 Jul 27, 2026
b342a7c
docs: use public-safe ESPHome examples
2shepard2 Jul 27, 2026
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ IMMICH_URL=
IMMICH_API_KEY=
IMMICH_ALBUM_ID=

# Optional Music Assistant / SendSpin display-client host port. This maps the
# adapter's internal listener (8928) to a port on the CoreView host; it is not
# a Music Assistant URL and CoreView does not need Music Assistant credentials.
SENDSPIN_CLIENT_PORT=8928

# First-run hardening
# Set for first bootstrap claim, then clear if you no longer want env-managed bootstrap.
COREVIEW_BOOTSTRAP_TOKEN=
Expand Down
47 changes: 45 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ System flow:
Operator
-> Admin UI
-> CoreView Control Plane
-> WebSocket Runtime Channel
-> Screen Clients
-> WebSocket Runtime Channel -> Browser screens
-> MQTT desired-state channel -> Matrix Beacons
<- SendSpin display connection <- Music Assistant (optional)
```

Screen clients can run on any device capable of opening a modern web browser.
Expand All @@ -83,6 +84,10 @@ Examples include:

The client only needs a browser and a network connection to the CoreView server.

Matrix Beacons are a second, purpose-built target type. They receive a compact
MQTT scene description and render it locally, so they do not need a browser,
the Home Assistant native API, or a direct connection to CoreView.

---

## Core Data Model
Expand Down Expand Up @@ -150,6 +155,19 @@ Live entity state and automation data.
MQTT
Messaging and event distribution.

Pixel matrices
Capability-aware MQTT targets for constrained displays such as ESPHome/HUB75.
Matrix targets use a retained desired-state protocol instead of the browser
WebSocket runtime; see [the Matrix MQTT protocol](docs/matrix-mqtt-protocol.md).
The reference ESPHome firmware and setup guide live in
[`esphome/`](esphome/README.md).

Music Assistant / SendSpin
An optional local SendSpin display client for now-playing metadata and audio
visualizer data. The adapter does not require a Music Assistant URL. Instead,
it exposes an endpoint on the CoreView host which Music Assistant may discover
or add manually. See [SendSpin integration](docs/integrations/sendspin.md).

Frigate
Object detection events and camera triggers.

Expand Down Expand Up @@ -228,6 +246,31 @@ After starting the server:

Once a screen connects it will begin rendering its assigned View.

### Optional Matrix Beacon

Matrix Beacons require a reachable MQTT broker in addition to CoreView. Their
firmware reports the panel geometry and a physical claim code over MQTT; the
operator claims the pending Beacon in CoreView, selects a Matrix-compatible
View, and assigns it. Matrix dimensions are configured per Beacon—CoreView
does not assume a particular panel size.

See the [ESPHome Beacon guide](esphome/README.md) for firmware configuration
and the [MQTT protocol reference](docs/matrix-mqtt-protocol.md) for network and
broker ACL guidance.

### Optional Music Assistant visualizer

The Compose stack includes a small SendSpin adapter which exposes host port
`8928` by default. It is passive until a SendSpin server, such as Music
Assistant, connects. From CoreView's **System** page, copy the generated
endpoint and add it to Music Assistant's SendSpin manual discovery list. The
adapter does not require a Music Assistant address, credential, or library
data.

This integration only supplies a normalized stream of playback metadata and
visualizer frames to CoreView. It does not make CoreView an audio player and
does not proxy audio.

---

## Screen Registration
Expand Down
30 changes: 30 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ services:
- .env
environment:
TZ: ${TZ:-UTC}
SENDSPIN_ADAPTER_URL: http://sendspin-adapter:8929
ports:
- "${HOST_PORT:-3210}:3210"
read_only: true
Expand All @@ -26,3 +27,32 @@ services:
timeout: 5s
retries: 5
start_period: 20s

sendspin-adapter:
container_name: coreview-sendspin-adapter
init: true
# The persistent CoreView data directory is owned by UID/GID 100:101 on
# the Docker host. Run the unprivileged adapter with that identity so it
# can persist its SendSpin pairing key without broadening permissions.
user: "100:101"
build:
context: ./sendspin-adapter
environment:
SENDSPIN_CLIENT_PORT: 8928
SENDSPIN_STATUS_PORT: 8929
ports:
- "${SENDSPIN_CLIENT_PORT:-8928}:8928"
read_only: true
tmpfs:
- /tmp
volumes:
- ./data/sendspin:/app/data
security_opt:
- no-new-privileges:true
restart: unless-stopped
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8929/health')"]
interval: 30s
timeout: 5s
retries: 5
start_period: 20s
97 changes: 97 additions & 0 deletions docs/integrations/sendspin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Music Assistant and SendSpin

CoreView can act as a SendSpin **display client**. This optional integration
lets Music Assistant supply playback state, now-playing metadata, and
visualizer frames for SendSpin-aware CoreView widgets and renderers.

It is intentionally not an audio output or a Music Assistant API integration:

- CoreView does not play, relay, or store audio.
- CoreView does not require a Music Assistant URL, token, username, or
password.
- CoreView does not use a configured Music Assistant address for the adapter;
it waits for Music Assistant to connect inward.
- Music Assistant connects to the SendSpin endpoint exposed by CoreView.

This direction keeps CoreView portable: an installation without Music
Assistant can leave the integration unused, while an installation with Music
Assistant chooses whether and where to register the CoreView client.

## Architecture

```text
Music Assistant (SendSpin server)
-> WebSocket client connection
-> CoreView SendSpin adapter (:8928/sendspin by default)
-> local adapter status API
-> CoreView runtime
-> browser and Matrix-capable visualizer widgets
```

The adapter is a separate container so the Python SendSpin SDK is isolated from
the CoreView Node.js application. It persists only a generated client identity
under `data/sendspin/`, allowing Music Assistant to recognize the same
CoreView display client after a container recreation.

## Setup

1. Start CoreView normally with Docker Compose. The adapter is included in the
standard stack and exposes TCP port `8928` by default.
2. In CoreView **System** settings, enter the Music Assistant SendSpin server
URL (for example, `ws://music-assistant:8927/sendspin`). This is an optional
saved integration setting: it identifies the intended server in CoreView but
does not reverse the protocol's connection direction.
3. Copy the displayed CoreView SendSpin Client Endpoint. It has the form:

```text
ws://<coreview-host>:8928/sendspin
```

4. In Music Assistant, add that host/port as a SendSpin manual discovery
address, then reload its SendSpin provider if needed. The CoreView adapter
deliberately does not advertise mDNS from its Docker network, so manual
registration is the reliable setup path.
5. Confirm CoreView reports **Connected** and lists the `metadata@v1` and
`visualizer@v1` roles.

The endpoint uses the browser's current CoreView hostname. When accessing
CoreView through a reverse proxy or a different hostname, make sure that name
and port resolve from the Music Assistant host. The endpoint must be reachable
from Music Assistant; the adapter status API is internal to the Compose
network and is not published.

## Configuration and security

`SENDSPIN_CLIENT_PORT` controls the external host port and defaults to `8928`.
Inside the Compose network the adapter always listens on `8928`; this setting
only changes the host-to-container port mapping. Set it in `.env` before
starting Compose if the host port is occupied, then use that same port in the
manual Music Assistant endpoint.

```dotenv
SENDSPIN_CLIENT_PORT=8928
```

The adapter listens only for a SendSpin server connection. Treat it like any
other trusted LAN integration: do not expose the port publicly, and restrict
network access to the Music Assistant host or trusted network where practical.

CoreView writes only its own stable SendSpin client identity under
`data/sendspin/`; it does not persist Music Assistant credentials or a target
Music Assistant address for this adapter flow.

## Troubleshooting

- **Waiting for Music Assistant to connect:** Verify the endpoint is added in
Music Assistant, port `8928` is reachable from its host/container, then
reload the SendSpin provider.
- **Adapter unavailable:** Check `coreview-sendspin-adapter` is running and
healthy with `docker compose ps` and inspect its logs with
`docker compose logs sendspin-adapter`.
- **Connected but no frames:** Start playback in Music Assistant and verify
its audio-analysis/visualizer capability is enabled. CoreView can remain
connected while playback is stopped.

The integration status is diagnostic only at this stage; it does not alter
existing CoreView screens or Matrix Beacons unless a SendSpin-aware widget is
added to a View.
110 changes: 110 additions & 0 deletions docs/matrix-mqtt-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# CoreView Matrix MQTT protocol

Status: draft v1. This is the public transport contract for constrained pixel
matrix targets such as ESPHome/HUB75 displays. It is intentionally separate
from CoreView's browser/WebSocket screen protocol.

## Topics

With `MQTT_TOPIC_ROOT=home/signage` and a target ID of `kitchen-matrix`:

| Direction | Topic | Retained |
| --- | --- | --- |
| CoreView → target | `home/signage/matrix/kitchen-matrix/state` | yes |
| CoreView → target | `home/signage/matrix/kitchen-matrix/event` | no |
| target → CoreView | `home/signage/matrix/kitchen-matrix/status` | yes |
| target → CoreView | `home/signage/matrix/kitchen-matrix/ack` | no |

The target must only subscribe to its own `state` and `event` topics. Broker
ACLs should likewise limit its publish rights to its own `status` and `ack`
topics.

## Desired state

CoreView publishes this payload whenever the assigned View or its effective
runtime override changes. `revision` strictly increases per target; a client
must ignore an older revision. A retained state lets a restarted target render
without waiting for another CoreView change.

```json
{
"schema": "coreview.matrix.state.v1",
"target": "kitchen-matrix",
"revision": 42,
"issuedAt": "2026-07-25T19:30:00.000Z",
"display": { "width": 96, "height": 32, "colorDepth": 4, "rotation": 0 },
"theme": {
"background": "#000000",
"primary": "#e6e8ed",
"secondary": "#0080ff",
"accent": "#ffb000",
"critical": "#ff2000",
"effectPalette": "neon",
"brightness": 64
},
"scene": {
"kind": "clock",
"title": "Kitchen",
"ticker": { "message": "Trash night" }
}
}
```

`scene.kind` is one of `clock`, `status`, `notification`, `effect`, `music`, or `unsupported`.
`unsupported` is an explicit, renderable fallback for browser-only View types
(for example maps and photo slideshows); it is never a silent failure.

Matrix-only custom widgets may emit an `effect` scene with an effect name,
palette, speed, intensity, and optional text. Effects are generated locally by
the target; CoreView sends parameters, never a pixel stream. The reference
firmware supports `scanner`, `rainbow_waves`, `aurora`, `digital_rain`, `fire`,
`twinkle`, `color_vortex`, and `confetti`.

Notification scenes may also include an optional `icon` (`info`, `warning`,
`success`, `door`, `lock`, `motion`, `water`, or `fire`) and `flashBorder`.
These are presentation hints: clients that do not support them can render the
same title, detail, and severity without loss of meaning.

Music scenes carry a `mode` (`spectrum`, `meter`, or `pulse`) and a
`visualizer` object containing normalized 0–255 `bands`, `level`, `peak`, and
an `active` flag. They may include now-playing `title` and `detail`. This is
live display data only; CoreView never sends or relays audio.

`theme` is the effective CoreView Theme after normal assignment, schedules, and
manual overrides are resolved. All colors are six-digit CSS hex values and
`brightness` is an integer from 1 through 255. Clients that do not implement
theme support may ignore this object. Effects use `theme.effectPalette`, so a
theme change restyles both normal scenes and local animations consistently.

Transient events use `coreview.matrix.event.v1`, carry a UUID `eventId`, a
`kind` of `notification` or `clear`, an optional RFC 3339 `expiresAt`, and a
severity (`info`, `success`, `warning`, `critical`). Events do not replace the
retained desired state.

## Target status and acknowledgement

Targets publish `coreview.matrix.status.v1` with `online`, `width`, `height`,
`colorDepth`, `rotation`, `features`, and optional firmware version. CoreView
uses it for observability; the server-side target configuration remains the
authority for compilation. After applying a state or event, targets publish
`coreview.matrix.ack.v1` with its `revision` or `eventId` and either `ok: true`
or a bounded error code.

An unclaimed reference Beacon also includes a six-digit `claimCode` in its
status payload. CoreView uses this only to confirm physical possession while an
administrator claims the pending target; it is not a replacement for MQTT ACLs.

## Capability policy

Matrix dimensions are per target, never hard-coded in CoreView. Target records
also declare color depth, rotation, and supported features. The reference
firmware may render a richer local idle animation, but CoreView scenes must use
only this protocol's portable primitives: text, short status fields, ticker,
severity, and the standard icon names.

The reference renderer currently presents one custom status widget at a time.
It centers text that fits and marquees text that does not; profiles with
multiple custom widgets are intentionally treated as browser-only until Matrix
paging is introduced. The reference 32-pixel-tall hardware profile reserves a
middle ticker lane when possible, temporarily hiding the status detail so the
ticker remains legible.
Loading
Loading