Step 0 of the settings page: registry, safe restart, remembered state, test verb, page markers
- schema.py: every setting with type, default, help, owner screen and the rules a config must pass; tests/test_schema.py keeps it in step with the code (every key the code reads is declared, every default matches, check types and fix kinds agree, the glossary covers every word a key can show, today's config still builds today's pages apart from the key notes) - status_panel.py: VERSION + started.json; SIGTERM lets a running fix finish and starts nothing new (RESTARTING SOON); fixes-running.json and live-now.json for the settings page; state.json keeps the ALARMS override, acknowledgements, auto-fix tries and fix times across restarts, keyed by what a check is about rather than its position; 'test' and 'resurvey' commands; queued commands dropped at start; overnight windows; a HyperDeck switched off is always switched back on; the spotify fix kind (playlist set once, in config 'spotify'); Windows remote checks batched for cmd.exe with conditional echoes; every fix's kind and host validated; never_auto; wording and numbers from config (site block, timing, page_hold_ms, auto-fix keep_months); hyperdeck_probe removed; --check and --exit-timeout - make_page.py: numbers from config, markers on every key, nav templates per Companion version, --out; push_page.py: a page is overwritten only when every key carries a marker (foreign keys named by position), secret-free fetch, host from the site; nav_keys.py: host from the site; netgear.py: switches and the shared-port limit from config - install.sh: validates with the new code first, keeps prev/ for --rollback, --keep-config, derived ExitTimeOut, wider deploy list, waits for the start stamp - config.json: site, timing, spotify sections; Windows hosts marked; deployed to NA Ingest and both pages re-imported once (--force) so every key carries its marker Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+85
-44
@@ -17,6 +17,7 @@ Buttons write commands into sp_cmd, which status_panel.py reads:
|
||||
Restart page force <key> (hold 2 s: restart that thing even when it looks fine)
|
||||
|
||||
python3 make_page.py -> status-page.companionconfig and restart-page.companionconfig
|
||||
python3 make_page.py --out DIR -> the same files in DIR (the data dir on the Ingest Mac)
|
||||
"""
|
||||
import gzip
|
||||
import hashlib
|
||||
@@ -28,12 +29,25 @@ HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
from status_panel import RESTARTABLE, load_config # noqa: E402
|
||||
|
||||
# Defaults; "companion", "page_grid", "page_stale_seconds" and "page_hold_ms" in config.json override them.
|
||||
EXPORT_VERSION = 12 # Companion 5.0.7's export format
|
||||
COMPANION_BUILD = "5.0.7+9763-stable-cec2f88e6f"
|
||||
GRID = {"minColumn": 0, "maxColumn": 8, "minRow": 0, "maxRow": 5}
|
||||
STALE_SECONDS = 15
|
||||
PURPLE = 0x6A1B9A
|
||||
HOLD_MS = 2000
|
||||
# Every key this tool makes carries one of these in its notes; push_page.py overwrites a page only
|
||||
# when every key on it (besides Companion's own page keys) does.
|
||||
MARKERS = ("STATUS tile", "RESTART key", "STATUS jump key", "RESTART back key", "STATUS nav key")
|
||||
|
||||
|
||||
def settings(cfg):
|
||||
"""The page-building numbers, from config.json where set."""
|
||||
comp = cfg.get("companion", {})
|
||||
return {"export_version": comp.get("export_version", EXPORT_VERSION), "build": comp.get("build", COMPANION_BUILD),
|
||||
"grid": cfg.get("page_grid", GRID), "stale": cfg.get("page_stale_seconds", STALE_SECONDS),
|
||||
"hold_ms": cfg.get("page_hold_ms", HOLD_MS), "flash": cfg.get("variables", {}).get("flash", "flash_tick"),
|
||||
"ingest": cfg.get("site", {}).get("ingest_name", "NA Ingest")}
|
||||
|
||||
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"
|
||||
|
||||
@@ -48,9 +62,10 @@ def prop(value, expression=False):
|
||||
return {"value": value, "isExpression": expression}
|
||||
|
||||
|
||||
def layers(tid, tile, names, stale_text):
|
||||
state, hb = names["state"], names["heartbeat"]
|
||||
stale = "$(internal:time_unix) - $(custom:%s) > %d" % (hb, STALE_SECONDS)
|
||||
def layers(tid, tile, names, stale_text, opts=None):
|
||||
opts = opts or settings({"variables": names})
|
||||
state, hb, flash = names["state"], names["heartbeat"], opts["flash"]
|
||||
stale = "$(internal:time_unix) - $(custom:%s) > %d" % (hb, opts["stale"])
|
||||
|
||||
def pick(on_key, off_key):
|
||||
return "jsonpath($(custom:%s), '$.%s.%s')" % (state, tid, on_key), \
|
||||
@@ -58,8 +73,8 @@ def layers(tid, tile, names, stale_text):
|
||||
|
||||
b_on, b_off = pick("b", "b2")
|
||||
f_on, f_off = pick("f", "f2")
|
||||
bg = "%s ? %d : ($(custom:flash_tick) == 1 ? %s : %s)" % (stale, PURPLE, b_on, b_off)
|
||||
fg = "%s ? %d : ($(custom:flash_tick) == 1 ? %s : %s)" % (stale, 0xFFFFFF, f_on, f_off)
|
||||
bg = "%s ? %d : ($(custom:%s) == 1 ? %s : %s)" % (stale, PURPLE, flash, b_on, b_off)
|
||||
fg = "%s ? %d : ($(custom:%s) == 1 ? %s : %s)" % (stale, 0xFFFFFF, flash, f_on, f_off)
|
||||
text = "%s ? %s : jsonpath($(custom:%s), '$.%s.x')" % (stale, stale_text, state, tid)
|
||||
# The watchdog picks a size per key so words never break mid-word.
|
||||
size = "%s ? 20 : jsonpath($(custom:%s), '$.%s.s')" % (stale, state, tid)
|
||||
@@ -102,8 +117,8 @@ def set_page_action(tid, role, page):
|
||||
"options": {"surfaceId": prop("self"), "page": prop(page)}, "children": {}}
|
||||
|
||||
|
||||
def static_button(tid, text, bg, fg, page, note):
|
||||
"""A plain key (no watchdog state): tap goes to `page`."""
|
||||
def static_button(tid, text, bg, fg, page, note, marker):
|
||||
"""A plain key (no watchdog state): tap goes to `page`. `marker` names it as ours in its notes."""
|
||||
geometry = {"enabled": prop(True), "opacity": prop(100), "x": prop(0), "y": prop(0),
|
||||
"width": prop(100), "height": prop(100), "rotation": prop(0)}
|
||||
layers = [
|
||||
@@ -120,7 +135,7 @@ def static_button(tid, text, bg, fg, page, note):
|
||||
"type": "button-layered",
|
||||
"style": {"layers": layers},
|
||||
"options": {"stepProgression": "auto", "stepExpression": "", "rotaryActions": False,
|
||||
"canModifyStyleInApis": False, "notes": note},
|
||||
"canModifyStyleInApis": False, "notes": "%s - %s" % (marker, note)},
|
||||
"feedbacks": [],
|
||||
"steps": {"0": {"action_sets": {"down": [], "up": [set_page_action(tid, "go", page)]},
|
||||
"options": {"runWhileHeld": []}}},
|
||||
@@ -129,27 +144,29 @@ def static_button(tid, text, bg, fg, page, note):
|
||||
|
||||
|
||||
def restart_key(key, cfg):
|
||||
"""A Restart page key: drawn by the watchdog like a tile; hold 2 s = "force <id>"."""
|
||||
names = cfg["variables"]
|
||||
"""A Restart page key: drawn by the watchdog like a tile; hold = "force <id>"."""
|
||||
names, opts = cfg["variables"], settings(cfg)
|
||||
hold_ms, secs = opts["hold_ms"], "%g" % (opts["hold_ms"] / 1000.0)
|
||||
stale_text = "'%s\\n?'" % key["label"]
|
||||
return {
|
||||
"type": "button-layered",
|
||||
"style": {"layers": layers(key["id"], key, names, stale_text)},
|
||||
"style": {"layers": layers(key["id"], key, names, stale_text, opts)},
|
||||
"options": {"stepProgression": "auto", "stepExpression": "", "rotaryActions": False,
|
||||
"canModifyStyleInApis": True,
|
||||
"notes": "RESTART key '%s' - hold 2 s: restart it even when the status page says it is fine "
|
||||
"(status_panel.py on NA Ingest runs fix '%s')." % (key["id"], key["fix"])},
|
||||
"notes": "RESTART key '%s' - hold %s s: restart it even when the status page says it is fine "
|
||||
"(status_panel.py on %s runs fix '%s')." % (key["id"], secs, opts["ingest"], key["fix"])},
|
||||
"feedbacks": [],
|
||||
"steps": {"0": {"action_sets": {"down": [], "up": [],
|
||||
str(HOLD_MS): [command_action(key["id"], "hold", names["command"],
|
||||
str(hold_ms): [command_action(key["id"], "hold", names["command"],
|
||||
"force %s $(internal:time_unix)" % key["id"])]},
|
||||
"options": {"runWhileHeld": [HOLD_MS]}}},
|
||||
"options": {"runWhileHeld": [hold_ms]}}},
|
||||
"localVariables": [],
|
||||
}
|
||||
|
||||
|
||||
def button(tid, tile, cfg):
|
||||
names = cfg["variables"]
|
||||
names, opts = cfg["variables"], settings(cfg)
|
||||
hold_ms, secs = opts["hold_ms"], "%g" % (opts["hold_ms"] / 1000.0)
|
||||
kind = tile.get("kind")
|
||||
label = tile["label"]
|
||||
if kind == "master":
|
||||
@@ -168,24 +185,26 @@ def button(tid, tile, cfg):
|
||||
for c in tile.get("checks", []))
|
||||
hold = ("restart " + tid) if fixable or tile.get("hold_always") or kind == "network" else None
|
||||
if kind == "network":
|
||||
note = "Press: stop the flashing. Hold 2 s: clear a network blip (it also clears itself after 5 minutes)."
|
||||
minutes = cfg.get("network", {}).get("remember_seconds", 300) // 60
|
||||
note = ("Press: stop the flashing. Hold %s s: clear a network blip (it also clears itself after %d minutes)."
|
||||
% (secs, minutes))
|
||||
elif tile.get("hold_always"):
|
||||
note = "Press: acknowledge. Hold 2 s: %s." % cfg["fixes"][tile["hold_always"]]["label"]
|
||||
note = "Press: acknowledge. Hold %s s: %s." % (secs, cfg["fixes"][tile["hold_always"]]["label"])
|
||||
else:
|
||||
note = "Press: acknowledge." + (" Hold 2 s: fix what this button is failing on (nothing if it is green)."
|
||||
if hold else "")
|
||||
note = "Press: acknowledge." + (" Hold %s s: fix what this button is failing on (nothing if it is green)."
|
||||
% secs if hold else "")
|
||||
action_sets = {"down": [], "up": [command_action(tid, "press", names["command"], press)]}
|
||||
run_while_held = []
|
||||
if hold:
|
||||
# Stamped with Companion's clock, so a hold made while the watchdog was down is dropped later.
|
||||
action_sets[str(HOLD_MS)] = [command_action(tid, "hold", names["command"], hold + " $(internal:time_unix)")]
|
||||
run_while_held = [HOLD_MS]
|
||||
action_sets[str(hold_ms)] = [command_action(tid, "hold", names["command"], hold + " $(internal:time_unix)")]
|
||||
run_while_held = [hold_ms]
|
||||
return {
|
||||
"type": "button-layered",
|
||||
"style": {"layers": layers(tid, tile, names, stale_text)},
|
||||
"style": {"layers": layers(tid, tile, names, stale_text, opts)},
|
||||
"options": {"stepProgression": "auto", "stepExpression": "", "rotaryActions": False,
|
||||
"canModifyStyleInApis": True,
|
||||
"notes": "STATUS tile '%s' - drawn by status_panel.py on NA Ingest. %s" % (tid, note)},
|
||||
"notes": "STATUS tile '%s' - drawn by status_panel.py on %s. %s" % (tid, opts["ingest"], note)},
|
||||
"feedbacks": [],
|
||||
"steps": {"0": {"action_sets": action_sets, "options": {"runWhileHeld": run_while_held}}},
|
||||
"localVariables": [],
|
||||
@@ -196,7 +215,18 @@ NAV_LOCATIONS = {"pageup": ("0", "0"), "pagenum": ("1", "0"), "pagedown": ("2",
|
||||
NAV_MARKER = "STATUS nav key"
|
||||
|
||||
|
||||
def nav_key(kind, names, status_page=None, hold_ms=1000, tap_page=1):
|
||||
def nav_templates(cfg):
|
||||
"""Companion's own nav key designs for the version in use: companion-objects/nav_templates/<major.minor>.json
|
||||
when that exists, else this folder's nav_templates.json (captured from 5.0.7)."""
|
||||
version = ".".join(str(settings(cfg)["build"]).split("+")[0].split(".")[:2])
|
||||
for path in (os.path.join(HERE, "companion-objects", "nav_templates", version + ".json"),
|
||||
os.path.join(HERE, "nav_templates.json")):
|
||||
if os.path.exists(path):
|
||||
return json.load(open(path))
|
||||
raise FileNotFoundError("no nav key templates for Companion %s" % version)
|
||||
|
||||
|
||||
def nav_key(kind, names, status_page=None, hold_ms=1000, tap_page=1, cfg=None):
|
||||
"""Companion's own UP / page number / DOWN key, coloured like the ALL button.
|
||||
|
||||
For the page number key, status_page adds: tap = go to tap_page (on release,
|
||||
@@ -204,12 +234,13 @@ def nav_key(kind, names, status_page=None, hold_ms=1000, tap_page=1):
|
||||
status page. The tap names the page outright: the converted key's "startup"
|
||||
means the current page on surfaces set to "use last page".
|
||||
"""
|
||||
templates = json.load(open(os.path.join(HERE, "nav_templates.json")))
|
||||
key = json.loads(json.dumps(templates[kind]))
|
||||
stale = "$(internal:time_unix) - $(custom:%s) > %d" % (names["heartbeat"], STALE_SECONDS)
|
||||
cfg = cfg or {"variables": names}
|
||||
opts = settings(cfg)
|
||||
key = json.loads(json.dumps(nav_templates(cfg)[kind]))
|
||||
stale = "$(internal:time_unix) - $(custom:%s) > %d" % (names["heartbeat"], opts["stale"])
|
||||
pick = lambda field: "jsonpath($(custom:%s), '$.all.%s')" % (names["state"], field)
|
||||
bg = "%s ? %d : ($(custom:flash_tick) == 1 ? %s : %s)" % (stale, PURPLE, pick("b"), pick("b2"))
|
||||
fg = "%s ? %d : ($(custom:flash_tick) == 1 ? %s : %s)" % (stale, 0xFFFFFF, pick("f"), pick("f2"))
|
||||
bg = "%s ? %d : ($(custom:%s) == 1 ? %s : %s)" % (stale, PURPLE, opts["flash"], pick("b"), pick("b2"))
|
||||
fg = "%s ? %d : ($(custom:%s) == 1 ? %s : %s)" % (stale, 0xFFFFFF, opts["flash"], pick("f"), pick("f2"))
|
||||
|
||||
def paint(layers):
|
||||
for layer in layers:
|
||||
@@ -240,7 +271,7 @@ def nav_controls(cfg):
|
||||
coloured = cfg.get("nav_keys", list(NAV_LOCATIONS)) # which nav keys follow the ALL button
|
||||
for kind, (r, c) in NAV_LOCATIONS.items():
|
||||
controls.setdefault(r, {})[c] = (nav_key(kind, cfg["variables"], cfg["page"], cfg.get("nav_hold_ms", 1000),
|
||||
cfg.get("nav_tap_page", 1))
|
||||
cfg.get("nav_tap_page", 1), cfg)
|
||||
if kind in coloured else {"type": kind})
|
||||
return controls
|
||||
|
||||
@@ -252,12 +283,13 @@ def place(controls, pos, control):
|
||||
controls.setdefault(r, {})[c] = control
|
||||
|
||||
|
||||
def page_export(name, number, controls):
|
||||
def page_export(name, number, controls, cfg=None):
|
||||
opts = settings(cfg or {})
|
||||
return {
|
||||
"version": EXPORT_VERSION,
|
||||
"version": opts["export_version"],
|
||||
"type": "page",
|
||||
"companionBuild": COMPANION_BUILD,
|
||||
"page": {"id": stable_id("page", name), "name": name, "controls": controls, "gridSize": GRID},
|
||||
"companionBuild": opts["build"],
|
||||
"page": {"id": stable_id("page", name), "name": name, "controls": controls, "gridSize": opts["grid"]},
|
||||
"instances": {},
|
||||
"connectionCollections": [],
|
||||
"oldPageNumber": number,
|
||||
@@ -274,18 +306,20 @@ def build(cfg):
|
||||
rp = cfg.get("restart_page")
|
||||
if rp:
|
||||
place(controls, rp["jump_pos"], static_button("restart_jump", "RESTART\\nPAGE", 0x37474F, 0xFFFFFF,
|
||||
rp["page"], "Tap: open the Restart page (page %d)." % rp["page"]))
|
||||
return page_export(cfg["page_name"], cfg["page"], controls)
|
||||
rp["page"], "Tap: open the Restart page (page %d)." % rp["page"],
|
||||
"STATUS jump key"))
|
||||
return page_export(cfg["page_name"], cfg["page"], controls, cfg)
|
||||
|
||||
|
||||
def build_restart(cfg):
|
||||
rp = cfg["restart_page"]
|
||||
controls = nav_controls(cfg)
|
||||
place(controls, rp["back_pos"], static_button("restart_back", "STATUS\\nPAGE", 0x37474F, 0xFFFFFF, cfg["page"],
|
||||
"Tap: back to the status page (page %d)." % cfg["page"]))
|
||||
"Tap: back to the status page (page %d)." % cfg["page"],
|
||||
"RESTART back key"))
|
||||
for key in rp["keys"]:
|
||||
place(controls, key["pos"], restart_key(key, cfg))
|
||||
return page_export(rp["page_name"], rp["page"], controls)
|
||||
return page_export(rp["page_name"], rp["page"], controls, cfg)
|
||||
|
||||
|
||||
def write(path, export):
|
||||
@@ -293,13 +327,20 @@ def write(path, export):
|
||||
f.write(json.dumps(export, indent=1).encode())
|
||||
|
||||
|
||||
def main():
|
||||
cfg = load_config(os.path.join(HERE, "config.json"))
|
||||
out = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "status-page.companionconfig")
|
||||
def main(argv=None):
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("--config", default=os.path.join(HERE, "config.json"))
|
||||
ap.add_argument("--out", help="folder for the page files (default: this folder)")
|
||||
ap.add_argument("status_file", nargs="?", help="write only the status page, to this file")
|
||||
args = ap.parse_args(argv)
|
||||
cfg = load_config(args.config)
|
||||
folder = args.out or HERE
|
||||
out = args.status_file or os.path.join(folder, "status-page.companionconfig")
|
||||
write(out, build(cfg))
|
||||
print("wrote %s (%d buttons)" % (out, sum(1 for t in cfg["tiles"] if "pos" in t)))
|
||||
if cfg.get("restart_page") and len(sys.argv) <= 1:
|
||||
out = os.path.join(HERE, "restart-page.companionconfig")
|
||||
if cfg.get("restart_page") and not args.status_file:
|
||||
out = os.path.join(folder, "restart-page.companionconfig")
|
||||
write(out, build_restart(cfg))
|
||||
print("wrote %s (%d restart keys)" % (out, len(cfg["restart_page"]["keys"])))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user