READY page: the weekly worship checklist, checked against Planning Center
- readiness.py looks every 2 minutes (never while a HyperDeck records) at this weekend's PCO plan and what is in place for it: the Ableton set (songs, mix, routing, lighting cues), ProPresenter (every PCO item, MC announcements, baptism names, timelines, backgrounds), the LTC note of every song against the timecode the tracks play, the Band Lineup TV, Resolume's LED cross, and the tracks and ProP bundles for the locations. - READY key on the Status page (row 2, col 7; SPOTIFY now shows inside SHOW) opens page 24. Amber until done, blinking red from Saturday noon; not part of ALL. Tap keys for BOH mix, mics and stage reset every Monday. - Holds fix what can be fixed: re-sync the lineup, put the cross back up, write LTC into PCO, copy tracks to locations, look again. Hold READY fixes everything. - Settings page: a READY page tab (live keys, drag to move, why each key is amber), and Apply re-imports page 24. - Auto-fix: "while_muted" fixes keep running with ALARMS muted (Spotify playlist). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -111,6 +111,8 @@ field("auto_fix.windows", "list", [], "When auto-fix may act.", "Schedules")
|
||||
field("auto_fix.windows[].days", "days", None, "", "Schedules", required=True)
|
||||
field("auto_fix.windows[].start", "hhmm", None, "", "Schedules", required=True)
|
||||
field("auto_fix.windows[].end", "hhmm", None, "", "Schedules", required=True)
|
||||
field("auto_fix.while_muted", "list", [], "Auto-fixes that keep running inside the window even while ALARMS is "
|
||||
"muted (harmless ones, like playing the Spotify playlist).", "Schedules")
|
||||
field("auto_fix.after_seconds", "int", 20, "A fault must last this long before auto-fix acts.", "Schedules",
|
||||
advanced=True, lo=5, hi=600)
|
||||
field("auto_fix.max_tries", "int", 2, "Tries per fault, then it is yours to hold.", "Schedules", advanced=True,
|
||||
@@ -184,6 +186,8 @@ field("hosts.*.manual_done", "dict", {}, "Set-up steps ticked by hand (who, when
|
||||
field("gear", "dict", {}, "Devices that are not computers: where each one is.", "Computers", apply="none")
|
||||
field("gear.*.location", "str", None, "Where it is in the building.", "Computers", apply="none")
|
||||
field("gear.*.notes", "str", None, "Notes for operators.", "Computers", apply="none")
|
||||
field("gear.*.kind", "str", None, "What it is (recorder, switcher, wireless receiver...).", "Computers", apply="none")
|
||||
field("gear.*.address", "str", None, "Its network address, if it has one.", "Computers", apply="none")
|
||||
|
||||
# -- fixes --
|
||||
field("fixes", "dict", {}, "Everything a hold can run.", "Fixes")
|
||||
@@ -214,6 +218,7 @@ field("fixes.*.mac", "mac", None, "The device expected on that port.", "Fixes")
|
||||
field("fixes.*.playlist", "str", None, "A playlist other than the site's (rare).", "Fixes", advanced=True)
|
||||
field("fixes.*.template", "str", None, "The builder this fix was made with.", "Fixes", advanced=True, apply="none")
|
||||
field("fixes.*.params", "dict", None, "The builder's fields.", "Fixes", advanced=True, apply="none")
|
||||
field("fixes.*.action", "enum", None, "What a READY page fix does.", "Fixes", options=list(sp.READY_ACTIONS))
|
||||
|
||||
# -- tiles (keys) --
|
||||
field("tiles", "list", [], "The keys on the Status page.", "Keys", required=True)
|
||||
@@ -222,8 +227,9 @@ field("tiles[].id", "id", None, "Used inside Companion.", "Keys", advanced=True,
|
||||
field("tiles[].label", "str", None, "The first line on the key.", "Keys", required=True, apply="pages")
|
||||
field("tiles[].pos", "list", None, "[row, column]; absent = no key of its own (shown inside a group).", "Keys",
|
||||
apply="pages")
|
||||
field("tiles[].kind", "enum", None, "A special key: master (ALL), service (ALARMS), network (NET).", "Keys",
|
||||
advanced=True, options=["master", "service", "network"], apply="pages")
|
||||
field("tiles[].kind", "enum", None, "A special key: master (ALL), service (ALARMS), network (NET), ready (READY: "
|
||||
"opens the READY page and sums up its checklist).", "Keys", advanced=True,
|
||||
options=["master", "service", "network", "ready"], apply="pages")
|
||||
field("tiles[].members", "list", None, "A group: the keys it sums up.", "Keys")
|
||||
field("tiles[].checks", "list", [], "What the key watches.", "Keys")
|
||||
field("tiles[].hold_always", "str", None, "A fix a hold runs even when the key is green.", "Keys", apply="pages")
|
||||
@@ -275,6 +281,7 @@ field("tiles[].checks[].shell", "enum", None, "sh (Macs) or cmd (PCs); blank = f
|
||||
advanced=True, options=["sh", "cmd"])
|
||||
field("tiles[].checks[].part", "enum", None, "Which part of the switcher.", "Keys", options=["audio", "camera"])
|
||||
field("tiles[].checks[].template", "str", None, "The builder this check was made with.", "Keys", advanced=True, apply="none")
|
||||
field("tiles[].checks[].item", "str", None, "Which line of the weekly checklist (READY page).", "Ready")
|
||||
field("tiles[].checks[].params", "dict", None, "The builder's fields.", "Keys", advanced=True, apply="none")
|
||||
|
||||
# -- restart page --
|
||||
@@ -293,6 +300,60 @@ field("restart_page.keys[].fix", "str", None, "What holding it runs.", "Keys", r
|
||||
field("restart_page.keys[].watch", "list", [], "Keep flashing HOLD TO FIX while these fixes' checks or "
|
||||
"'conn:<label>' connections are down.", "Keys")
|
||||
|
||||
# -- the READY page (the weekly checklist) --
|
||||
field("ready_page", "dict", None, "The READY page: this weekend's checklist, checked against Planning Center; absent = "
|
||||
"this campus has none.", "Ready", apply="pages")
|
||||
field("ready_page.page", "int", 24, "Its number in Companion.", "Ready", lo=1, apply="pages")
|
||||
field("ready_page.page_name", "str", "24\\nReady", "Its name in Companion.", "Ready", advanced=True, apply="pages")
|
||||
field("ready_page.back_pos", "list", None, "Where the 'back to Status' key sits on the READY page.", "Ready",
|
||||
apply="pages")
|
||||
field("ready_page.keys", "list", [], "The checklist keys (each like a Status page key, placed on the READY page).",
|
||||
"Ready", apply="pages")
|
||||
field("ready", "dict", {}, "Where the READY page looks.", "Ready")
|
||||
field("ready.interval_seconds", "int", 120, "Look this often (never while a HyperDeck is recording; a hold looks at "
|
||||
"once).", "Ready", advanced=True, lo=30, hi=3600)
|
||||
field("ready.due", "list", [], "From when gaps are red instead of amber.", "Ready")
|
||||
field("ready.due[].days", "days", None, "", "Ready", required=True)
|
||||
field("ready.due[].start", "hhmm", None, "", "Ready", required=True)
|
||||
field("ready.due[].end", "hhmm", None, "", "Ready", required=True)
|
||||
field("ready.reset_day", "enum", "mon", "Taps (BOH mix, mics, stage) count from midnight on this day.", "Ready",
|
||||
options=list(sp.DAYS))
|
||||
field("ready.pco_credentials", "path", "~/Library/Application Support/ProdDash/modules.json", "ProdDash's settings "
|
||||
"file, which holds the Planning Center login.", "Ready", advanced=True)
|
||||
field("ready.pco_module", "str", "pco-plan", "The ProdDash module whose Planning Center login is used.", "Ready",
|
||||
advanced=True)
|
||||
field("ready.pco_service_type", "str", None, "The Planning Center service type; blank = ProdDash's first.", "Ready")
|
||||
field("ready.skip_songs", "list", [], "PCO songs that have no tracks or slides of their own.", "Ready")
|
||||
field("ready.ltc_note", "str", "LTC", "The PCO note category that holds a song's timecode (checked against the "
|
||||
"tracks, written from them by a hold); blank = not checked.",
|
||||
"Ready", advanced=True)
|
||||
field("ready.prop_url", "url", "", "ProPresenter's network API.", "Ready", example="http://10.1.14.225:1025")
|
||||
field("ready.prop_lists", "list", [], "PCO items whose description lists things the slides must have: "
|
||||
"{match, kind (count: one slide each / names: every name on a slide), short}.", "Ready", advanced=True)
|
||||
field("ready.tracks_host", "str", None, "The computer Ableton runs on.", "Ready")
|
||||
field("ready.projects", "path", "", "The shared Ableton Projects folder, as that computer sees it.", "Ready")
|
||||
field("ready.site_folder", "str", "", "This campus's folder inside it.", "Ready", example="NA")
|
||||
field("ready.folder_format", "str", "{date} {loc} Project", "A weekend's project folder ({date} = YYMMDD).",
|
||||
"Ready", advanced=True)
|
||||
field("ready.ltc_track", "str", "LTC", "The Ableton track that plays the timecode (an LTC file named like "
|
||||
"LTC_00000000_60mins_30fps: it starts at 00:00:00:00, 30 frames a second).", "Ready", advanced=True)
|
||||
field("ready.lighting_track", "str", "LIGHTING CUES", "The Ableton track that holds the lighting cues.", "Ready")
|
||||
field("ready.locations", "list", [], "The campuses that get this campus's tracks.", "Ready")
|
||||
field("ready.remote_timeout", "int", 90, "Seconds allowed for reading the Ableton set.", "Ready", advanced=True,
|
||||
lo=10, hi=600)
|
||||
field("ready.lineup_url", "url", "", "ProdDash's Band Lineup module.", "Ready")
|
||||
field("ready.lineup_teams", "list", ["Band", "Communication"], "PCO teams whose people must be on the lineup TV.",
|
||||
"Ready")
|
||||
field("ready.lineup_skip_positions", "list", [], "PCO positions the lineup TV does not show.", "Ready")
|
||||
field("ready.resolume_url", "url", "", "Resolume Arena's web server.", "Ready")
|
||||
field("ready.resolume_deck", "str", "", "The deck Resolume must be on.", "Ready")
|
||||
field("ready.led_clip", "str", "", "The cross background a hold plays; blank = the last one seen playing.", "Ready")
|
||||
field("ready.prop_bundle_host", "str", None, "The computer that saves the ProPresenter bundle.", "Ready")
|
||||
field("ready.prop_bundle_folder", "path", "", "Where the bundle for the locations goes.", "Ready")
|
||||
field("ready.prop_bundle_pattern", "str", "*.probundle", "The bundle's file name pattern.", "Ready", advanced=True)
|
||||
field("ready.vocal_positions", "list", ["Vocalist", "[Leader]"], "PCO positions that need a vocal mic.", "Ready")
|
||||
field("ready.mic_positions", "dict", {}, "Other PCO positions and the mic they need (MC -> MC).", "Ready")
|
||||
|
||||
# --- Check types ---------------------------------------------------------------
|
||||
# fields: the keys that type reads; subs: the sub-check names it returns (fix tables use them);
|
||||
# identity: what it is about; module: the Companion module its connection is expected to use.
|
||||
@@ -336,6 +397,10 @@ CHECKS = {
|
||||
"question": "Is the lighting console sending?", "words": "Art-Net is arriving from the lighting program."},
|
||||
"atem": {"fields": ["part", "msg"], "subs": ["audio", "camera"], "identity": ["part"], "module": "bmd-atem",
|
||||
"question": "Is a setting right?", "words": "The switcher matches the saved file."},
|
||||
"ready": {"fields": ["item"], "subs": ["ready", "<what is missing>"], "identity": ["item"], "module": None,
|
||||
"question": "Is the weekend ready?", "words": "A line of the weekly checklist, checked against Planning Center."},
|
||||
"ready_manual": {"fields": ["item", "msg"], "subs": ["done"], "identity": ["item"], "module": None,
|
||||
"question": "Is the weekend ready?", "words": "A line of the weekly checklist a person taps done."},
|
||||
}
|
||||
COMMON_CHECK_FIELDS = ["type", "fix", "tag", "service_only", "debounce_seconds"]
|
||||
FIX_KINDS = sp.FIX_KINDS
|
||||
@@ -343,7 +408,7 @@ FIX_KIND_FIELDS = {
|
||||
"ssh": ["host", "run", "timeout", "reconnect", "conn"], "local": ["run", "timeout", "reconnect", "conn"],
|
||||
"atem": ["part"], "reconnect": ["conn", "verify_seconds", "hung_note"], "midi_connect": [],
|
||||
"netgear_poe": ["switch", "port", "mac"], "hyperdeck": ["verify_seconds", "off_seconds", "hung_note"],
|
||||
"spotify": ["playlist"],
|
||||
"spotify": ["playlist"], "ready": ["action"],
|
||||
}
|
||||
EXPECTED_MODULE = {t: spec["module"] for t, spec in CHECKS.items() if spec["module"]}
|
||||
|
||||
@@ -361,6 +426,14 @@ GLOSSARY = {
|
||||
"RF MUTE", "HUNG - POWER CYCLE IT", "NO NETWORK CHECK POWER", "STILL DOWN", "REFUSED", "LOGIN", "BLIP",
|
||||
"NOT REACHABLE", "DISK",
|
||||
],
|
||||
"the READY page: what is not ready yet (hold the key to fix it or look again)": [
|
||||
"TO DO", "ALL DONE", "TAP WHEN DONE", "TAP WHEN SET", "NOT DONE", "DONE", "NOT SET UP", "CHECKING",
|
||||
"NO TRACKS", "EMPTY", "NOT MIXED", "TO MASTER", "NO LIGHTS", "NO CUE TRACK", "NO SET", "LIVE HAS OLD SET",
|
||||
"MISSING", "SLIDES OLD?", "SLIDES", "NO TC", "NO LTC", "NO BG", "NO PLAYLIST", "OLD PLAN", "SYNC ERROR",
|
||||
"NOT ON PLAN", "DECLINED", "WRONG DECK", "NOTHING ON", "NOT SENT", "COPYING TO", "NO PCO LOGIN",
|
||||
"PCO UNREACHABLE", "NO PLAN IN PCO", "PROP UNREACHABLE", "LINEUP UNREACHABLE", "RESOLUME UNREACHABLE",
|
||||
"UNREACHABLE", "CHECK FAILED", "NO CLIP KNOWN", "LTC WRONG", "PCO WRITE FAILED", "IN PCO", "NO SET TO COPY", "SYNC SLOW", "NO PLAN YET", "VOX",
|
||||
],
|
||||
"ask Chris": ["NOT IN COMPANION", "CHECK BUG", "NO DATA", "WATCHDOG DOWN", "ERR", "PROCESS", "WEB"],
|
||||
"what a key says while you hold it": [
|
||||
"NOTHING TO FIX", "NO AUTO FIX", "JUST TRIED", "WAIT 1 MIN", "WAIT", "MIN", "S", "CHECKING", "FIXING",
|
||||
@@ -399,6 +472,10 @@ def key_record():
|
||||
kind, parts = "restart_key", parts[2:]
|
||||
elif p.startswith("restart_page."):
|
||||
kind, parts = "restart_page", parts[1:]
|
||||
elif p.startswith("ready_page."):
|
||||
kind, parts = "ready_page", parts[1:]
|
||||
elif p.startswith("ready."):
|
||||
kind, parts = "ready", parts[1:]
|
||||
elif p.startswith("auto_fix."):
|
||||
kind, parts = "auto", parts[1:]
|
||||
elif p.startswith("network."):
|
||||
@@ -571,6 +648,9 @@ def validate(cfg, live=None):
|
||||
errors.append("auto-fix: %s is marked never to run by itself" % f)
|
||||
elif re.search(r"pkill|taskkill|killall", fixes[f].get("run", "")) and not fixes[f].get("restart_ok"):
|
||||
errors.append("auto-fix: %s closes a running program; tick 'may close a running program' or take it out" % f)
|
||||
for f in a.get("while_muted", []):
|
||||
if f not in a.get("fixes", []):
|
||||
errors.append("auto-fix: %s runs while ALARMS is muted but is not an auto-fix" % f)
|
||||
_windows(cfg.get("service_windows"), "alarms window", errors)
|
||||
_windows(a.get("windows"), "auto-fix window", errors)
|
||||
_windows((cfg.get("site") or {}).get("live_services"), "live services", errors)
|
||||
@@ -600,6 +680,51 @@ def validate(cfg, live=None):
|
||||
errors.append("the 'back to Status' key sits on %s" % rpos[tuple(rp["back_pos"])])
|
||||
if rp.get("page") == cfg.get("page"):
|
||||
errors.append("the Status page and the Restart page cannot be the same page")
|
||||
rd = cfg.get("ready_page")
|
||||
readies = [t for t in cfg["tiles"] if t.get("kind") == "ready"]
|
||||
if len(readies) > 1:
|
||||
errors.append("at most one READY key on the Status page; found %d" % len(readies))
|
||||
if rd:
|
||||
dpos = {}
|
||||
for k in rd.get("keys", []):
|
||||
kid = k.get("id", "")
|
||||
where = "READY key %s" % (kid or "?")
|
||||
if not re.fullmatch(r"[A-Za-z0-9_]+", str(kid)):
|
||||
errors.append("%s: the id may only hold letters, digits and _" % where)
|
||||
if kid in ids:
|
||||
errors.append("%s: another key has that id" % where)
|
||||
ids.add(kid)
|
||||
if not k.get("label"):
|
||||
errors.append("%s: no label" % where)
|
||||
pos = pos_ok(k.get("pos"), where)
|
||||
if pos in dpos:
|
||||
errors.append("%s and %s share row %d column %d" % (dpos[pos], where, pos[0], pos[1]))
|
||||
dpos[pos] = where
|
||||
for i, c in enumerate(k.get("checks", [])):
|
||||
cw = "%s check %d" % (where, i + 1)
|
||||
if c.get("type") not in CHECKS:
|
||||
errors.append("%s: unknown check type %r" % (cw, c.get("type")))
|
||||
continue
|
||||
if c["type"] in ("ready", "ready_manual") and not c.get("item"):
|
||||
errors.append("%s: which checklist line (item)?" % cw)
|
||||
if c["type"] == "ready" and c.get("item") and c["item"] not in sp.READY_ITEMS:
|
||||
errors.append("%s: unknown checklist line %s" % (cw, c["item"]))
|
||||
fx = c.get("fix")
|
||||
for f in (fx.values() if isinstance(fx, dict) else [fx]):
|
||||
if f and f not in fixes and f not in (sp.RESTART_CONNECTION, sp.DANTE_AUTO):
|
||||
errors.append("%s: unknown fix %s" % (cw, f))
|
||||
if "back_pos" in rd:
|
||||
bp = pos_ok(rd["back_pos"], "the READY page's back key")
|
||||
if bp in dpos:
|
||||
errors.append("the 'back to Status' key sits on %s" % dpos[bp])
|
||||
if rd.get("page") in (cfg.get("page"), (rp or {}).get("page")):
|
||||
errors.append("the READY page cannot be the Status or Restart page")
|
||||
if not readies:
|
||||
warnings.append("the READY page has no READY key on the Status page: nobody can open it from there")
|
||||
_windows((cfg.get("ready") or {}).get("due"), "READY due window", errors)
|
||||
for fid, fix in fixes.items():
|
||||
if fix.get("kind") == "ready" and fix.get("action") not in sp.READY_ACTIONS:
|
||||
errors.append("fix %s: unknown READY action %s" % (fid, fix.get("action")))
|
||||
spo = cfg.get("spotify") or {}
|
||||
if any(c.get("type") == "spotify" for t in cfg["tiles"] for c in t.get("checks", [])):
|
||||
if not re.fullmatch(r"spotify:playlist:[A-Za-z0-9]+", str(spo.get("playlist", ""))):
|
||||
|
||||
Reference in New Issue
Block a user