Skip to content

Commit 2245e56

Browse files
gershamclaude
andcommitted
Add reboot and display power control
screen reboot (fleet requires --yes; reboot_unsupported explained), screen display --power on|off [--until|--for] and screen display clear, screen display-schedule show|set|clear, Display lines in screen show, and screen.reboot_requested / screen.display_changed rendering. Vendored from backend deaf865. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 0c1536d commit 2245e56

15 files changed

Lines changed: 1309 additions & 23 deletions

‎README.md‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,64 @@ cleared, and `reason`), and `screen.takeover_ended` (`reason`
422422
`screen.playlist_unavailable` (`playlist_id`, `entry_ids` comma-joined, and `code`
423423
`playlist_deleted|playlist_unavailable`).
424424

425+
## Reboot and display power
426+
427+
```sh
428+
screenrig screen reboot scr_LOBBY [--expect-rev REVISION]
429+
screenrig screen reboot scr_A scr_B --yes
430+
screenrig screen reboot --tag Lobby --yes
431+
screenrig screen display scr_LOBBY --power off [--until RFC3339 | --for 2h] [--expect-rev REVISION]
432+
screenrig screen display --tag Lobby --power on
433+
screenrig screen display clear scr_LOBBY [--expect-rev REVISION]
434+
screenrig screen display clear --tag Lobby
435+
screenrig screen display-schedule show scr_LOBBY
436+
screenrig screen display-schedule set scr_LOBBY --file hours.json [--expect-rev REVISION]
437+
screenrig screen display-schedule set --tag Cafe --file hours.json
438+
screenrig screen display-schedule clear scr_LOBBY
439+
```
440+
441+
`screen reboot` asks the Player to reboot the device. Only a Player that
442+
declares the `reboot` capability in its host report receives it; `screen show`
443+
lists the host capabilities. Any other screen is refused with
444+
`reboot_unsupported` (exit 5) and nothing is sent; `screen reload` refreshes
445+
content without a device reboot. A reboot request expires after ten minutes,
446+
and each screen accepts at most 2 reboots per 10 minutes. A refused or failed
447+
request and an exact replay of the same request do not count. The CLI never
448+
prompts, so rebooting several ids or a `--tag` fleet requires `--yes`.
449+
450+
`screen display` turns the display on or off now, as a manual override of the
451+
display schedule. `--power on|off` sets it (a trailing `on` or `off` after the
452+
ids is the same). `--until` takes a strict RFC 3339 instant, strictly in the
453+
future and at most 7 days ahead; `--for` takes a duration up to `6d23h59m`.
454+
Without either, the override ends at the display schedule's next boundary. A
455+
later display-schedule change or a timezone change moves that end to the new
456+
next boundary. With no schedule boundary in the next eight days (for example,
457+
no enabled schedule), the override holds until `screen display clear` or a
458+
replacement. `screen display clear` ends the override, so the display schedule
459+
applies again (without one, the display is on). The Player applies every change
460+
at once and reports what it achieved. A screen that is still waiting to pair
461+
answers `resource_conflict` (exit 5): it has no paired Player yet, though a
462+
display schedule can be set before pairing.
463+
464+
A display schedule lists 1 to 16 windows when the display is ON, in the screen
465+
timezone (set one first with `screen set-timezone`). Outside every window the
466+
display goes to standby. The device evaluates it offline.
467+
468+
```json
469+
{"enabled": true, "windows": [{"days": ["mon", "tue", "wed", "thu", "fri"], "start": "07:00", "end": "19:00"}, {"days": ["sat"]}]}
470+
```
471+
472+
A window whose `end` is at or before its `start` crosses midnight; a window
473+
without `start` and `end` covers the whole day. `enabled: false` keeps the
474+
windows and leaves the display on. The saved output of `display-schedule show`
475+
can be edited and sent back with `set --file`.
476+
477+
`screen show --human` prints `Display:` with the requested power, its source
478+
(manual override, display schedule, or default) and until when in the screen
479+
timezone, the schedule windows, and the power the Player reported, marked stale
480+
after 15 minutes without a report. `events list` prints
481+
`screen.reboot_requested` and `screen.display_changed`.
482+
425483
## Webhooks
426484

427485
A webhook POSTs this project's own durable events to an HTTPS endpoint you

‎assets/playlist-write-v2.schema.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$comment": "Generated by screenrig-protocol-gen from api/openapi.yaml; sha256=ec469ae74ca9fe82bacdd314fb67fc57b6b6d124fdba3584dbf75ae7c5b3808d. Shape validation for the adslot page union. Run canonical playlist-write-semantics.ts next. At most 16 adslot pages and one ordinary page with no visibility remain server checks.",
2+
"$comment": "Generated by screenrig-protocol-gen from api/openapi.yaml; sha256=6c916f85a19b7573dfa7af37d242a83eae074d5f924bc16230b937e25af58efe. Shape validation for the adslot page union. Run canonical playlist-write-semantics.ts next. At most 16 adslot pages and one ordinary page with no visibility remain server checks.",
33
"$defs": {
44
"AdslotPageWrite": {
55
"additionalProperties": false,

‎assets/playlist-write.schema.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$comment": "Generated by screenrig-protocol-gen from api/openapi.yaml; sha256=ec469ae74ca9fe82bacdd314fb67fc57b6b6d124fdba3584dbf75ae7c5b3808d. Shape validation only; run canonical playlist-write-semantics.ts next. Reference resolution and DNS remain server checks.",
2+
"$comment": "Generated by screenrig-protocol-gen from api/openapi.yaml; sha256=6c916f85a19b7573dfa7af37d242a83eae074d5f924bc16230b937e25af58efe. Shape validation only; run canonical playlist-write-semantics.ts next. Reference resolution and DNS remain server checks.",
33
"$defs": {
44
"CanvasBackground": {
55
"description": "Solid canonical uppercase #RRGGBBAA, or a top-to-bottom linear gradient.",

‎src/adapters/protocol.ts‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,40 @@ export interface ScreenHealth {
540540
renderer_restarts_24h?: number;
541541
}
542542

543+
export interface ScreenDisplaySchedule {
544+
enabled: boolean;
545+
windows: ScreenScheduleWindow[];
546+
updated_at: string;
547+
}
548+
549+
export interface ScreenDisplayOverride {
550+
override_id: string;
551+
power: "on" | "off";
552+
until: string | null;
553+
ends_at: string | null;
554+
set_at: string;
555+
}
556+
557+
/** Screen.display: requested power now, why, until when, and the Player's reported power. */
558+
export interface ScreenDisplay {
559+
requested: "on" | "off";
560+
source: "override" | "schedule" | "default";
561+
until?: string;
562+
schedule?: ScreenDisplaySchedule;
563+
override?: ScreenDisplayOverride;
564+
reported?: { power?: "on" | "off" | "standby" | "unknown"; connected?: boolean; reported_at: string; stale: boolean };
565+
}
566+
567+
export interface ScreenDisplayScheduleView {
568+
display_schedule: ScreenDisplaySchedule | null;
569+
display?: ScreenDisplay;
570+
}
571+
572+
export interface ScreenRebootAccepted {
573+
reboot_id: string;
574+
expires_at: string;
575+
}
576+
543577
export interface Screen {
544578
content_access_generation: number;
545579
created_at: string;
@@ -608,6 +642,7 @@ export interface Screen {
608642
*/
609643
storage?: ScreenStorage;
610644
health?: ScreenHealth;
645+
display?: ScreenDisplay;
611646
/**
612647
* Approximate steady-state target selection (no transition) from the last
613648
* reported capacity and the screen's desired manifest. Absent without a
@@ -669,7 +704,12 @@ export type ScreenAction =
669704
| { type: "takeover"; playlist_id: string; until?: string | null; reason?: string }
670705
| { type: "takeover_clear" }
671706
| { type: "set_playlist_schedule"; entries: ScreenScheduleEntryWrite[] }
672-
| { type: "clear_playlist_schedule" };
707+
| { type: "clear_playlist_schedule" }
708+
| { type: "reboot" }
709+
| { type: "display"; power: "on" | "off"; until?: string | null }
710+
| { type: "display_clear" }
711+
| { type: "set_display_schedule"; enabled: boolean; windows: ScreenScheduleWindow[] }
712+
| { type: "clear_display_schedule" };
673713

674714
export type ScreenActionType = ScreenAction["type"];
675715

@@ -686,6 +726,7 @@ export interface ScreenActionScreenResult {
686726
tags?: string[];
687727
reload?: ScreenReloadAccepted;
688728
toast?: ScreenToastAccepted;
729+
reboot?: ScreenRebootAccepted;
689730
problem?: { type?: string; title?: string; status?: number; detail?: string; code?: string; [key: string]: unknown };
690731
}
691732

‎src/cli-commands/screen.ts‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { addValueAlias } from "./aliases.js";
22
import { addCommandExamples, addCommandNotes, requireOptionGroup } from "./notes.js";
33
import { positiveInteger, revision, screenTagOption, screenTagListOption, toastDuration } from "./options.js";
44
import type { CommandActionBinder } from "./types.js";
5-
import { handleScreenPublish, handleScreenPair, handleScreenProvision, handleScreenUpdate, handleScreenList, handleScreenShow, handleScreenStorageForecast, handleScreenAssign, handleScreenSetTimezone, handleScreenArchive, handleScreenUnarchive, handleScreenDelete, handleScreenRotatePublicId, handleScreenRecover, handleScreenReload, handleScreenToast, handleScreenScreenshot, handleScreenTag, handleScreenScheduleShow, handleScreenScheduleSet, handleScreenScheduleClear, handleScreenTakeover, handleScreenTakeoverClear } from "../commands.js";
5+
import { handleScreenPublish, handleScreenPair, handleScreenProvision, handleScreenUpdate, handleScreenList, handleScreenShow, handleScreenStorageForecast, handleScreenAssign, handleScreenSetTimezone, handleScreenArchive, handleScreenUnarchive, handleScreenDelete, handleScreenRotatePublicId, handleScreenRecover, handleScreenReload, handleScreenToast, handleScreenScreenshot, handleScreenTag, handleScreenScheduleShow, handleScreenScheduleSet, handleScreenScheduleClear, handleScreenTakeover, handleScreenTakeoverClear, handleScreenReboot, handleScreenDisplay, handleScreenDisplayClear, handleScreenDisplayScheduleShow, handleScreenDisplayScheduleSet, handleScreenDisplayScheduleClear } from "../commands.js";
66
import { durationOption, takeoverReason, takeoverUntilOption } from "../screen-control.js";
7+
import { untilOption } from "../screen-display.js";
78
import { type Command, Option } from "commander";
89

910
export function registerScreenCommands(root: Command, bind: CommandActionBinder): void {
@@ -162,6 +163,54 @@ export function registerScreenCommands(root: Command, bind: CommandActionBinder)
162163
"screenrig screen takeover scr_A scr_B --playlist-id pl_NOTICE --until none");
163164
addCommandExamples(takeover.commands.find((command) => command.name() === "clear")!,
164165
"screenrig screen takeover clear scr_LOBBY", "screenrig screen takeover clear --tag Lobby");
166+
const reboot = screen.command("reboot").description("Reboot a screen's device, or a fleet's devices")
167+
.argument("[id...]", "Screen identifier; several ids target those screens in one fleet request")
168+
.option("--tag <TAG>", "Target every active screen carrying this tag", screenTagOption)
169+
.option("--yes", "Confirm a reboot of several screens or --tag (required for a fleet)")
170+
.option("--expect-rev <REVISION>", "Optionally require this screen revision (one screen id only)", revision)
171+
.action(bind(handleScreenReboot));
172+
addCommandNotes(reboot, "Reboot:\n The Player reboots the device, not just itself. Only a Player that declares\n the reboot capability in its host report receives it; any other screen is\n refused with reboot_unsupported (exit 5) and nothing is sent. The request\n expires after ten minutes, and each screen accepts at most 2 reboots per 10\n minutes; a refused or failed request and an exact replay spend none. A\n fleet (several ids or --tag) requires --yes: the CLI never prompts.");
173+
addCommandExamples(reboot, "screenrig screen reboot scr_LOBBY", "screenrig screen reboot --tag Lobby --yes");
174+
175+
const displayGroup = screen.command("display").description("Turn a screen's display on or off now, or clear that override");
176+
const display = displayGroup.command("set", { isDefault: true }).description("Turn a screen's display, or a fleet's displays, on or off now (default: screen display ID --power on|off)")
177+
.argument("[id...]", "Screen identifier; several ids target those screens in one fleet request; a trailing on|off sets the power")
178+
.option("--tag <TAG>", "Target every active screen carrying this tag", screenTagOption)
179+
.addOption(new Option("--power <POWER>", "on or off").choices(["on", "off"]))
180+
.addOption(new Option("--until <TIME>", "End at this RFC 3339 instant (strictly future, at most 7 days ahead)").argParser(untilOption).conflicts(["for"]))
181+
.addOption(new Option("--for <DURATION>", "End after this duration, such as 30m, 2h, or 3d (at most 6d23h59m)").argParser(durationOption).conflicts(["until"]))
182+
.option("--expect-rev <REVISION>", "Optionally require this screen revision (one screen id only)", revision)
183+
.action(bind(handleScreenDisplay));
184+
addCommandNotes(display, "Display power:\n A manual override of the display schedule. Without --until or --for it ends\n at the display schedule's next boundary; a later display-schedule change or\n timezone change moves that end to the new next boundary. With no schedule\n boundary in the next eight days (no enabled schedule) it holds until\n screen display clear or a replacement. The Player applies it at once and\n reports what it achieved (screen show: Display reported). At most 10\n display changes per screen per minute; refusals and replays spend none.");
185+
addCommandExamples(display, "screenrig screen display scr_LOBBY --power off --for 2h", "screenrig screen display --tag Lobby --power on", "screenrig screen display scr_A scr_B --power off --until 2026-10-01T07:00:00Z");
186+
const displayClear = displayGroup.command("clear").description("End the manual display override on a screen or a fleet; the schedule (else on) applies again")
187+
.argument("[id...]", "Screen identifier; several ids target those screens in one fleet request")
188+
.option("--tag <TAG>", "Target every active screen carrying this tag", screenTagOption)
189+
.option("--expect-rev <REVISION>", "Optionally require this screen revision (one screen id only)", revision)
190+
.action(bind(handleScreenDisplayClear));
191+
addCommandExamples(displayClear, "screenrig screen display clear scr_LOBBY", "screenrig screen display clear --tag Lobby");
192+
193+
const displaySchedule = screen.command("display-schedule").description("Show, set, or clear when a screen's display is on");
194+
displaySchedule.command("show").description("Show a screen's display schedule and its display state")
195+
.argument("<id>", "Screen identifier")
196+
.action(bind(handleScreenDisplayScheduleShow));
197+
displaySchedule.command("set").description("Replace the display schedule on a screen or a fleet of screens")
198+
.argument("[id...]", "Screen identifier; several ids target those screens in one fleet request")
199+
.option("--tag <TAG>", "Target every active screen carrying this tag", screenTagOption)
200+
.requiredOption("--file <FILE>", "Display schedule JSON {\"enabled\": true, \"windows\": [...]}, or - for stdin (required)")
201+
.option("--expect-rev <REVISION>", "Optionally require this screen revision (one screen id only)", revision)
202+
.action(bind(handleScreenDisplayScheduleSet));
203+
displaySchedule.command("clear").description("Remove the display schedule from a screen or a fleet of screens")
204+
.argument("[id...]", "Screen identifier; several ids target those screens in one fleet request")
205+
.option("--tag <TAG>", "Target every active screen carrying this tag", screenTagOption)
206+
.option("--expect-rev <REVISION>", "Optionally require this screen revision (one screen id only)", revision)
207+
.action(bind(handleScreenDisplayScheduleClear));
208+
addCommandNotes(displaySchedule, "Display schedule:\n 1 to 16 windows when the display is ON, in the screen timezone (required):\n {\"enabled\": true, \"windows\": [{\"days\": [\"mon\",\"tue\"], \"start\": \"07:00\", \"end\": \"19:00\"}]}.\n end <= start crosses midnight; omit start and end for the whole day. Outside\n every window the display goes to standby. enabled false keeps the windows and\n leaves the display on. The device evaluates it offline.");
209+
addCommandExamples(displaySchedule.commands.find((command) => command.name() === "show")!, "screenrig screen display-schedule show scr_LOBBY");
210+
addCommandExamples(displaySchedule.commands.find((command) => command.name() === "set")!,
211+
"screenrig screen display-schedule set scr_LOBBY --file hours.json", "screenrig screen display-schedule set --tag Cafe --file hours.json");
212+
addCommandExamples(displaySchedule.commands.find((command) => command.name() === "clear")!,
213+
"screenrig screen display-schedule clear scr_LOBBY", "screenrig screen display-schedule clear --tag Cafe");
165214
for (const name of ["pair", "provision"]) addValueAlias(screen.commands.find(command => command.name() === name)!, "--name", "--label", "Set the screen name");
166215
requireOptionGroup(screen.commands.find(command => command.name() === "update")!, "atLeastOne", ["--name", "--playlist-id", "--timezone"]);
167216
}

0 commit comments

Comments
 (0)