Settings page: ON/OFF switches everywhere, the Auto switch screen, a Companion button picker
- ON/OFF on every key, check, fix, Restart and READY key, computer, MIDI link, HyperDeck, Dante device, watched Companion connection, switcher and Spotify check, outlet fix, and one main switch each for auto-fix, the auto switch and the READY page. Switched off = kept in the settings, grey OFF on the deck, never checked or run (effective_config in the watchdog). - Settings › Auto switch: a plain summary, what counts as down (each trigger editable in place: address, program and computer, Dante device, the button it presses, timings), the hours folded away, the buttons; + Button, + What counts as down, Remove. - Pick a Companion button by clicking it on its page, drawn like Companion (read-only API: /api/companion/pages and /api/companion/page). - STREAM STB / STREAM FOH Restart keys removed (the Studio B page has its own); the alert names the switch-back button instead. - Dante screen: shows the DANTE key's device list again (it picked STB AUDIO's one device). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,8 @@ def collect(path):
|
||||
return step(base, key)
|
||||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr in ("items", "values"):
|
||||
return kind_of(node.func.value, local)
|
||||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "effective_config":
|
||||
return "top" # the config with switched-off keys and checks taken out
|
||||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in ("all_tiles", "ready_keys"):
|
||||
return "tiles" # the Status page's tiles and the READY page's keys
|
||||
if isinstance(node, ast.Call) and dotted(node.func) == "json.load":
|
||||
|
||||
+107
-11
@@ -1536,6 +1536,108 @@ def run_failover(engine, snap, start, seconds, step=0.5):
|
||||
STB_PING, STB_PT = "10.1.13.175", "studiob\0pgrep -xq 'Pro Tools'"
|
||||
|
||||
|
||||
class TestSwitchedOff(unittest.TestCase):
|
||||
"""Anything can be switched off in the settings and stay there, like a connection in Companion:
|
||||
a key goes grey OFF and checks nothing, a check is skipped, a fix never runs."""
|
||||
|
||||
def cfg(self, **changes):
|
||||
cfg = json.loads(json.dumps(CONFIG))
|
||||
for path, value in changes.items():
|
||||
node = cfg
|
||||
*parts, last = path.split("/")
|
||||
for p in parts:
|
||||
node = node[int(p)] if isinstance(node, list) else node[p]
|
||||
node[last] = value
|
||||
return cfg
|
||||
|
||||
def test_a_key_switched_off_is_grey_and_checks_nothing(self):
|
||||
i = next(n for n, t in enumerate(CONFIG["tiles"]) if t["id"] == "obs")
|
||||
engine = sp.Engine(self.cfg(**{"tiles/%d/disabled" % i: True}))
|
||||
snap = healthy_snapshot(SUNDAY)
|
||||
snap.conns.pop("OBS-Youtube", None) # OBS gone: would be red
|
||||
tiles, _ = run_steps(engine, snap, SUNDAY, 5)
|
||||
self.assertEqual(tiles["obs"]["x"], "OBS\nOFF")
|
||||
self.assertEqual(level(tiles["obs"]), "off")
|
||||
self.assertEqual(tiles["all"]["x"], "ALL OK") # not part of ALL any more
|
||||
self.assertEqual(tiles["stream"]["x"], "STREAM\nOK") # nor of its group
|
||||
|
||||
def test_a_check_switched_off_is_skipped(self):
|
||||
i = next(n for n, t in enumerate(CONFIG["tiles"]) if t["id"] == "stb_audio")
|
||||
engine = sp.Engine(self.cfg(**{"tiles/%d/checks/0/disabled" % i: True}))
|
||||
snap = healthy_snapshot(SUNDAY)
|
||||
snap.ping[STB_PING] = 5 # the ping check would fail
|
||||
tiles, _ = run_steps(engine, snap, SUNDAY, 5)
|
||||
self.assertEqual(tiles["stb_audio"]["x"], "STB AUDIO\nOK")
|
||||
self.assertEqual(len(engine.tiles["stb_audio"]["checks"]), 2)
|
||||
self.assertEqual(len(CONFIG["tiles"][i]["checks"]), 3) # the settings keep all three
|
||||
|
||||
def test_a_fix_switched_off_never_runs(self):
|
||||
cfg = self.cfg(**{"fixes/obs/disabled": True, "fixes/stream_foh_backup/disabled": True})
|
||||
self.assertEqual(sp.execute_fix(cfg, "obs", {}), "TURNED\nOFF")
|
||||
engine = sp.Engine(cfg)
|
||||
engine.executor = lambda c, fix_id, chk: self.fail("ran " + fix_id)
|
||||
engine.notify = lambda url, text: None
|
||||
snap = healthy_snapshot(SUNDAY)
|
||||
snap.ping[STB_PING] = 5 # Studio B dies on a Sunday
|
||||
snap.conns.pop("OBS-Youtube", None)
|
||||
run_failover(engine, snap, SUNDAY, 40)
|
||||
self.assertEqual(engine.failed, {}) # no auto switch
|
||||
self.assertEqual(engine.auto_fix(snap, mock_comp(), SUNDAY + 41), []) # no auto-fix
|
||||
|
||||
def test_a_restart_key_switched_off(self):
|
||||
rp = self.cfg()["restart_page"]
|
||||
j = next(n for n, k in enumerate(rp["keys"]) if k["id"] == "rs_tascam")
|
||||
engine = sp.Engine(self.cfg(**{"restart_page/keys/%d/disabled" % j: True}))
|
||||
tiles = engine.step(healthy_snapshot(SUNDAY), SUNDAY)
|
||||
self.assertEqual(tiles["rs_tascam"]["x"], "TASCAM\nOFF")
|
||||
self.assertIsNone(engine.run_force("rs_tascam", healthy_snapshot(SUNDAY), None, SUNDAY))
|
||||
self.assertEqual(engine.notes["rs_tascam"]["text"], "TURNED\nOFF")
|
||||
|
||||
def test_a_computer_switched_off(self):
|
||||
cfg = self.cfg(**{"hosts/studiob/disabled": True})
|
||||
engine = sp.Engine(cfg)
|
||||
stb = engine.tiles["stb_audio"]["checks"]
|
||||
self.assertEqual([c["type"] for c in stb], ["dante"]) # its ping and Pro Tools checks are gone
|
||||
self.assertTrue(engine.cfg["fixes"]["solo_afl"]["disabled"]) # and a fix that logs in to it
|
||||
self.assertFalse(any(c.get("host") == "studiob" for t in sp.all_tiles(engine.cfg) for c in t.get("checks", [])))
|
||||
self.assertNotIn("studiob", sp.remote_midi_hosts(engine.cfg))
|
||||
engine = sp.Engine(self.cfg(**{"hosts/bibletv/disabled": True})) # its web page is not fetched either
|
||||
self.assertNotIn("http://10.6.7.13:8080/", [u for u, _ in sp.http_checks(engine.cfg)])
|
||||
self.assertTrue(engine.cfg["fixes"]["bibleapp"]["disabled"])
|
||||
|
||||
def test_a_dante_device_switched_off(self):
|
||||
i = next(n for n, t in enumerate(CONFIG["tiles"]) if t["id"] == "dante")
|
||||
engine = sp.Engine(self.cfg(**{"tiles/%d/checks/0/devices_off" % i: ["STUDIO-B-Tascam"]}))
|
||||
snap = healthy_snapshot(SUNDAY)
|
||||
devices = json.loads(snap.vars[("audinate-dantecontroller", "devices")])
|
||||
snap.vars[("audinate-dantecontroller", "devices")] = json.dumps([d for d in devices if d != "STUDIO-B-Tascam"])
|
||||
tiles, _ = run_steps(engine, snap, SUNDAY, 5)
|
||||
self.assertNotIn("GONE", tiles["dante"]["x"]) # gone, but switched off: no alarm
|
||||
self.assertIn("STUDIO-B-Tascam", CONFIG["tiles"][i]["checks"][0]["devices"]) # still in the list
|
||||
|
||||
def test_whole_features_switched_off(self):
|
||||
cfg = self.cfg(**{"auto_fix/disabled": True, "failover/disabled": True, "ready/disabled": True})
|
||||
engine = sp.Engine(cfg)
|
||||
self.assertEqual(engine.cfg["auto_fix"]["windows"], [])
|
||||
self.assertEqual(engine.failover_windows, [])
|
||||
self.assertTrue(all(k.get("disabled") for k in sp.ready_keys(engine.cfg)))
|
||||
snap = healthy_snapshot(SUNDAY)
|
||||
snap.ping[STB_PING] = 5
|
||||
engine.executor = lambda c, fix_id, chk: self.fail("ran " + fix_id)
|
||||
engine.notify = lambda url, text: None
|
||||
tiles, _ = run_failover(engine, snap, SUNDAY, 20)
|
||||
self.assertEqual(engine.failed, {})
|
||||
self.assertEqual(tiles["rd_tracks"]["x"], "TRACKS\nOFF")
|
||||
|
||||
def test_the_rules_accept_switched_off_things(self):
|
||||
import schema
|
||||
i = next(n for n, t in enumerate(CONFIG["tiles"]) if t["id"] == "obs")
|
||||
cfg = self.cfg(**{"tiles/%d/disabled" % i: True, "tiles/%d/checks/0/disabled" % i: True, "fixes/obs/disabled": True})
|
||||
errors, warnings = schema.validate(cfg)
|
||||
self.assertEqual(errors, [])
|
||||
self.assertFalse(any("disabled" in w for w in warnings), warnings)
|
||||
|
||||
|
||||
class TestFailover(unittest.TestCase):
|
||||
"""Studio B dies on a Sunday: the FOH Backup button is pressed by itself, once, and the keys say so.
|
||||
Nothing here presses a real button, posts a real alert or writes a real log."""
|
||||
@@ -1567,8 +1669,6 @@ class TestFailover(unittest.TestCase):
|
||||
self.assertEqual(fix["undo"], "stream_studio_b")
|
||||
self.assertFalse(CONFIG["fixes"]["stream_studio_b"].get("failover"))
|
||||
self.assertNotIn("stream_foh_backup", CONFIG["auto_fix"]["fixes"])
|
||||
keys = {k["id"]: k["fix"] for k in CONFIG["restart_page"]["keys"]}
|
||||
self.assertEqual((keys["rs_stream_stb"], keys["rs_stream_foh"]), ("stream_studio_b", "stream_foh_backup"))
|
||||
self.assertEqual(CONFIG["failover"]["after_seconds"], 2)
|
||||
self.assertEqual(self.engine.failover_windows, [{"days": ["sun"], "start": "06:30", "end": "13:30"}])
|
||||
|
||||
@@ -1600,11 +1700,9 @@ class TestFailover(unittest.TestCase):
|
||||
self.assertTrue(any("-> done, checking it comes back" in l for l in body), body)
|
||||
self.assertEqual(len(self.sent), 1)
|
||||
self.assertTrue(self.sent[0].startswith("NA Ingest: STB AUDIO STUDIO B NO PING - switched over by itself: press the FOH Backup"))
|
||||
self.assertIn("hold STREAM STB on the Restart page", self.sent[0])
|
||||
self.assertIn("Switch back by hand (press the Studio B button in Companion", self.sent[0])
|
||||
self.engine.run_fixes("audio", self.snap, None, at + 10)
|
||||
self.assertEqual(self.engine.notes["audio"]["text"], "JUST TRIED\nWAIT 1 MIN")
|
||||
self.assertIsNone(self.engine.run_force("rs_stream_foh", self.snap, None, at + 11))
|
||||
self.assertEqual(self.engine.notes["rs_stream_foh"]["text"], "JUST DONE\nWAIT 1 MIN")
|
||||
out = sp.snapshot_summary(self.snap, self.engine, t)
|
||||
json.dumps(out)
|
||||
self.assertEqual(out["failover"]["stream_foh_backup"]["note"], "ON FOH BACKUP")
|
||||
@@ -1664,9 +1762,6 @@ class TestFailover(unittest.TestCase):
|
||||
self.snap.remote[STB_PT] = False # the answer is back: still gone
|
||||
tiles, t = run_failover(self.engine, self.snap, t, 10)
|
||||
self.assertEqual(self.ran, ["stream_foh_backup"]) # no second press
|
||||
self.assertEqual(tiles["rs_stream_foh"]["x"], "STREAM FOH\nON FOH BACKUP") # pressed already: no HOLD TO FIX
|
||||
self.assertEqual(level(tiles["rs_stream_foh"]), "ok")
|
||||
self.assertEqual(tiles["rs_stream_stb"]["x"], "STREAM STB\nHOLD 2s")
|
||||
|
||||
def test_studio_b_rednet_is_never_power_cycled_by_itself(self):
|
||||
"""Only STUDIO-B-RedNet-TNX vanishes: the failover presses FOH Backup; Sunday auto-fix on the DANTE
|
||||
@@ -1697,11 +1792,12 @@ class TestFailover(unittest.TestCase):
|
||||
self.snap.ping[STB_PING] = 2
|
||||
tiles, t = run_failover(self.engine, self.snap, t0, 6)
|
||||
self.assertIn("ON FOH BACKUP", tiles["audio"]["x"])
|
||||
self.assertEqual(self.engine.run_force("rs_stream_stb", self.snap, None, t), "stream_studio_b")
|
||||
self.assertEqual(self.engine.notes["rs_stream_stb"]["text"], "PRESSING")
|
||||
# the undo, run through the watchdog (a Restart key or the settings page's Run the fix)
|
||||
self.assertEqual(self.engine.run_force("stb_audio", self.snap, None, t, fix_id="stream_studio_b", chk={}), "stream_studio_b")
|
||||
self.assertEqual(self.engine.notes["stb_audio"]["text"], "PRESSING")
|
||||
gate.set()
|
||||
time.sleep(0.2)
|
||||
self.assertEqual(self.engine.notes["rs_stream_stb"]["text"], "DONE")
|
||||
self.assertEqual(self.engine.notes["stb_audio"]["text"], "DONE")
|
||||
self.assertEqual(self.engine.failed, {})
|
||||
tiles = self.engine.step(self.snap, t + 0.5)
|
||||
self.assertEqual(tiles["audio"]["x"], "AUDIO\nSTUDIO B NO PING")
|
||||
|
||||
Reference in New Issue
Block a user