src.nth.io/

summaryrefslogtreecommitdiff
path: root/scrypted/README.md
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-06-14 10:53:37 -0500
committerLuke Hoersten <[email protected]>2026-06-14 10:53:37 -0500
commit52f4105aa059b9d7310d80cfb86abe844f7be05a (patch)
tree9c1dc09da9b77ec75334c0772d2179487b1f1f71 /scrypted/README.md
parent334455c8ab793e2303e5e3669fa6deba179412be (diff)
Path B: per-viewport Scrypted devices with UI-driven settings
Convert the Scripts-plugin script from a hardcoded BINDINGS constant into a DeviceProvider + DeviceCreator + HttpRequestHandler. Each viewport is now a child Scrypted device under the parent script with its own Settings page; operators add, edit, and delete viewports entirely through the Scrypted UI. Parent (ScryptedViewportProvider): - DeviceProvider: getDevice(nativeId) instantiates a Viewport, attaches its camera event listener, and posts /config. releaseDevice tears down stream + listener + child storage entry. - DeviceCreator: "+ Add Device" on the parent's page shows a small form (name / host / camera picker filtered to Camera interfaces / orientation choice). createDevice() pre-populates the child's storage via deviceManager.getDeviceStorage(nativeId) and registers it under the parent. - Tracks known child nativeIds in its own storage as a JSON array so it can eagerly instantiate every child on plugin start (each registration + camera subscription happens at load time, not lazily). - 5-min re-register loop catches devices that rebooted or got new DHCP leases. - HttpRequestHandler routes POST <base>/state on the parent's endpoint; body {viewport, state} is matched against child names. Honors the spec race rules: every callback cancels any prior stream + safety timer for that viewport before applying the new state, and a /frame 409 stops the stream without echoing sleep back. - Global tuning (frame_interval_ms) lives on the parent's Settings. Child (Viewport): - Settings: host (string), camera (type=device with deviceFilter for the Camera interface — the UX win), orientation (choices), idle timeout, brightness. All persisted via this.storage. - putSetting fires onBindingChanged() on the parent so re-register + re-subscribe happen immediately when any field changes. scrypted/README.md rewritten for the UI-driven flow — install + add device + edit + remove + global tuning + smoke test — no more "edit BINDINGS and re-save." scrypted/package.json + tsconfig.json: optional `npm install` so editors can resolve @scrypted/sdk types. Nothing here ships — install remains "paste into Scripts plugin." node_modules ignored.
Diffstat (limited to 'scrypted/README.md')
-rw-r--r--scrypted/README.md121
1 files changed, 67 insertions, 54 deletions
diff --git a/scrypted/README.md b/scrypted/README.md
index f279ca9..5f25c70 100644
--- a/scrypted/README.md
+++ b/scrypted/README.md
@@ -6,78 +6,91 @@ The ESP32 firmware needs something on the Scrypted side that:
- starts a JPEG stream to a viewport when its bound camera fires an event (doorbell ring, motion, person), and
- stops the stream when the viewport reports `sleep` or its own per-stream timer expires.
-`scrypted-viewport.ts` in this directory does all of that as a single-file TypeScript script for the **Scripts plugin**. v2 will replace this with a packaged plugin doing `POST /stream` over MJPEG; for now this is enough to bring up end-to-end functionality.
+`scrypted-viewport.ts` in this directory does all of that as a single-file TypeScript script for the **Scripts plugin**. Each viewport is a child Scrypted device under the script — you add, remove, and edit viewports entirely through the Scrypted UI; no script editing required after the initial paste.
+
+v2 will replace this with a packaged plugin doing `POST /stream` over MJPEG; for now this is enough to bring up end-to-end functionality.
## Install
1. In Scrypted's web UI: **Plugins → search "Scripts" → install** (if you don't already have it).
2. **+ Add Device → Scripts plugin → New Script**.
3. Paste the entire contents of `scrypted-viewport.ts` into the editor and **Save**.
-4. Edit the `BINDINGS` array at the top of the script for your setup:
-
- ```ts
- const BINDINGS: Binding[] = [
- {
- name: "mudroom", // matches /config viewport
- host: "192.168.1.42", // viewport IP (LAN-resolvable)
- cameraId: "abcdef0123456789", // Scrypted device id
- orientation: "portrait", // 480x800
- },
- ];
- ```
-
- - **`name`**: must match what the script POSTs to the device's `/config` (also drives `viewport-<name>.local`).
- - **`host`**: viewport's current IP or hostname. v1 has no mDNS-SD discovery — set it manually and update if DHCP renumbers (or use a DHCP reservation).
- - **`cameraId`**: the Scrypted device id (not the human name) of the camera to bind. Grab it from the URL bar on the camera's settings page in Scrypted.
- - **`orientation`**: `portrait` (480×800) or `landscape` (800×480). Sent to the device via `/config` and used to size the snapshots.
-
-5. Save. The script's `start()` runs immediately. Within a few seconds you should see lines in the Scrypted log:
-
- ```
- Scrypted Viewport script up. Callback URL base: http://scrypted.local:11080/endpoint/<scriptId>
- Bindings: 1 viewport(s)
- subscribed to "Front Door" events for viewport "mudroom"
- ```
-
-6. On the viewport, `GET /config` should now show the populated `scrypted` URL and the `viewport` name. `GET /state` should show `configured: true` and `state: "asleep"`.
-
-## Tuning constants
-
-Defined right above the class:
-
-| Constant | Default | What it controls |
-| --- | --- | --- |
-| `IDLE_TIMEOUT_MS` | 60000 | Sent to the device in `/config`. Both sides use this value independently. |
-| `STREAM_TIMEOUT_MS` | == `IDLE_TIMEOUT_MS` | Scrypted-side per-stream cutoff. Keep it the same as the device's idle timeout so both ends agree. |
-| `FRAME_INTERVAL_MS` | 1000 | Snapshot push cadence during an active stream. ~1 fps is fine for ambient camera viewing. |
-| `REREGISTER_INTERVAL_MS` | 300000 | Re-issue `/config` to every viewport every 5 minutes so a viewport that rebooted or moved IPs re-syncs without manual intervention. |
-| `HTTP_TIMEOUT_MS` | 1000 | Per-call timeout for outbound POSTs. Matches the device's own 1 s timeout. |
+4. Open the newly-created "Scrypted Viewport" device.
+
+The script is now running. Time to add viewports.
+
+## Adding a viewport
+
+On the "Scrypted Viewport" device page, click **+ Add Device**. You'll get a small form:
+
+| Field | What to enter |
+| --- | --- |
+| **Viewport name** | Lowercase routing key, e.g. `mudroom`. Becomes the device's mDNS hostname (`viewport-mudroom.local`) and the value the firmware sends back in callbacks. |
+| **IP or hostname** | Viewport's LAN address, e.g. `192.168.1.42`. Set a DHCP reservation if you want it to stay put. |
+| **Camera** | Dropdown — pick the camera whose events should wake this viewport. The dropdown is filtered to devices implementing `Camera`. |
+| **Orientation** | `portrait` (480×800, default) or `landscape` (800×480). Tells the device + script what dimensions to send. |
+
+Click **Create**. The script immediately POSTs `/config` to the device. Within a second or two, the viewport device should show up in Scrypted with its own page.
+
+## Editing a viewport
+
+Open the viewport device's page. It has its own Settings tab with the same four fields, plus two extras:
+
+- **Idle timeout (ms)** — sent to the device in `/config`. Both sides time independently. `0` disables the device-side idle timer; non-zero must be ≥ 5000. Default 60000.
+- **Brightness (0–100)** — gamma-corrected on the panel. Default 80.
+
+Changing any setting triggers an immediate re-register and re-subscribes the camera listener if the camera changed.
+
+## Removing a viewport
+
+Open the viewport device's page → **Settings** menu → **Delete Device**. The script stops any active stream, unsubscribes from the camera, and forgets the binding. The physical device keeps its NVS-stored config until it gets a fresh `/config` from somewhere — or hit **BOOT for 5 s** on the panel to factory-reset.
+
+## Global tuning
+
+Open the parent "Scrypted Viewport" device's Settings page:
+
+- **Frame push interval (ms)** — how often a snapshot is pushed during an active stream. 1000 = 1 fps. Lower for faster updates at higher CPU cost.
## What the script does on each event
-- **Camera event (doorbell ring / motion / person)** → call `startStream(binding.name)`:
- - cancel any previous stream for that viewport,
+- **Camera event (doorbell ring / motion / person)** → look up the viewport bound to that camera → `startStream(viewport)`:
+ - cancel any prior stream + safety timer for that viewport,
- POST `{state: "wake"}` to the device,
- start the snapshot interval,
- - arm a `STREAM_TIMEOUT_MS` safety timer.
+ - arm a per-stream safety timer at the viewport's `idle_timeout_ms`.
- **Device-initiated `{state: "wake"}` callback** (operator tapped the panel) → same `startStream` path.
-- **Device-initiated `{state: "sleep"}` callback** → `stopStream` with `sendSleep=false` (the device already knows).
-- **Per-stream safety timer fires** → `stopStream` with `sendSleep=true` (tell the device to sleep).
-- **`POST /frame` returns 409** → device went to sleep on its own (tap-to-sleep, or its idle timer); `stopStream` with `sendSleep=false`.
+- **Device-initiated `{state: "sleep"}` callback** → `stopStream` without echoing sleep back (the device already knows).
+- **Per-stream safety timer fires** → `stopStream` and POST `{state: "sleep"}` to the device.
+- **`POST /frame` returns 409** → device went to sleep on its own (tap-to-sleep, or its idle timer); `stopStream` without echo.
+
+## What the script does on script load + every 5 minutes
+
+- Re-POST `/config` to every known viewport with its current settings. A device that rebooted or got a new DHCP lease re-syncs within 5 minutes without manual intervention.
+
+## Local type-checking (optional)
+
+If you want red squigglies in your editor instead of just trusting the runtime:
+
+```bash
+cd scrypted
+npm install
+```
+
+That pulls in `@scrypted/sdk` and `@types/node` so the TS server can resolve everything. Nothing here is shipped — install is still "paste into Scrypted's web UI."
## v1 limitations
- Snapshot-rate only (~1 fps). Live MJPEG over `POST /stream` is v2.
-- Manual IP per viewport (no mDNS-SD discovery yet — that's v1.1).
-- Camera must respect `picture.width` / `picture.height` in `PictureOptions` or be paired with a snapshot plugin that resizes. If the camera returns the wrong size, the device rejects `/frame` with 400 and you'll see warning logs. Workaround: configure the camera plugin's snapshot size, or wait for v2 which will resize Scrypted-side via FFmpeg.
+- Manual IP per viewport (no mDNS-SD discovery yet). DHCP reservation is the simplest workaround.
+- Camera must respect `picture.width` / `picture.height` in `PictureOptions` or be paired with a snapshot plugin that resizes. If the camera returns the wrong size, the device rejects `/frame` with 400 and you'll see warning logs. Workaround: configure the camera plugin's snapshot size, or wait for v2 (FFmpeg-side resize).
- No retry on transport errors. Best-effort matches the device's own semantics; the next event or callback re-syncs.
## End-to-end smoke test
-Once installed and bindings are filled in:
+After installing the script and adding one viewport binding:
-1. **Viewport boots** with no `/config` yet → IP screen on panel.
-2. Script runs → `POST /config` lands → viewport → `state: asleep`, backlight off.
-3. Tap viewport → device POSTs `{viewport: "mudroom", state: "wake"}` → script logs `recv mudroom -> wake` → snapshots start flowing for `STREAM_TIMEOUT_MS`.
-4. Tap viewport again → device POSTs `{state: "sleep"}` → script stops streaming.
-5. Trigger the bound camera (doorbell, motion sensor, or person detection) → script POSTs `{state: "wake"}` to viewport → snapshots flow for `STREAM_TIMEOUT_MS`, then both sides time out and sleep.
+1. **Fresh viewport** boots → IP screen on panel.
+2. Script's `getDevice()` runs on script start → `POST /config` lands → viewport → `state: asleep`, backlight off.
+3. **Tap the viewport** → device POSTs `{viewport, state: "wake"}` → script logs `recv "<name>" -> wake` → snapshots start flowing for `idle_timeout_ms`.
+4. **Tap again** → device POSTs `{state: "sleep"}` → script stops streaming.
+5. **Trigger the bound camera** (doorbell, motion sensor, or person detection) → script POSTs `{state: "wake"}` → snapshots flow until either side's idle timer cuts off.