From 7c5f086bd56b9c094be5111dab22fbd5042a3bcb Mon Sep 17 00:00:00 2001 From: christian Date: Thu, 8 Oct 2026 12:42:00 -0400 Subject: [PATCH] A DANTE hold on missing hardware restarts Companion's Dante module. Dante Virtual Soundcard on the four Macs is restarted directly; for hardware (consoles, racks, cards, AVIOs) nothing can be restarted from here, so a hold re-checks via a fresh Dante module connection (harmless to audio and routing). If the device is still gone it is really off or unplugged. A check's fix can now be "restart-connection", and a fix map can use "*" for every other sub-check. Co-Authored-By: Claude Opus 5.5 --- config.json | 2 +- status_panel.py | 16 +++++++++++++--- tests/test_status_panel.py | 14 ++++++-------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/config.json b/config.json index 83dec45..43d7d43 100644 --- a/config.json +++ b/config.json @@ -72,7 +72,7 @@ { "id": "dante", "label": "DANTE", "pos": [2, 2], "checks": [ { "type": "dante", "conn": "audinate-dantecontroller", "fix": { "NA-ProPresenter-Mac": "dvs_prop", "NA-Tracks": "dvs_tracks", "NA-Translation": "dvs_translation", - "STUDIO-A-Ingest": "dvs_ingest" }, + "STUDIO-A-Ingest": "dvs_ingest", "*": "restart-connection" }, "devices": { "FOH-DiGiCo-DMI-1": "DMI-1", "FOH-DiGiCo-DMI-2": "DMI-2", "BOH-WING": "WING", "BOH-DQ-Rack": "DQ RACK", "BOH-AD4Q-A": "AD4Q-A", "BOH-AD4Q-B": "AD4Q-B", "STUDIO-A-X32": "STA X32", diff --git a/status_panel.py b/status_panel.py index aba0e4a..f6a9389 100644 --- a/status_panel.py +++ b/status_panel.py @@ -452,6 +452,9 @@ CHECKS = { # Checks whose connection a long press may restart (Dante is shared, so not that one). RESTARTABLE = ("conn", "hyperdeck", "shure", "psm", "tascam") +# A check's "fix" may name this instead of an entry in "fixes": restart its own Companion +# connection. The Dante button uses it for hardware, which cannot be restarted from here. +RESTART_CONNECTION = "restart-connection" def needed_variables(cfg): @@ -750,7 +753,13 @@ class Engine: continue fix_id = chk.get("fix") if isinstance(fix_id, dict): - fix_id = fix_id.get(sub) + fix_id = fix_id.get(sub, fix_id.get("*")) # "*": every other sub-check + if fix_id == RESTART_CONNECTION and chk.get("conn"): + ident = ("restart", chk["conn"]) # e.g. Companion's Dante module + if ident not in seen: + seen.add(ident) + plan.append((ident, "restart", chk["conn"], chk)) + continue if fix_id: if chk["type"] == "hyperdeck" and hyperdeck_recording(snap, chk["conn"]): continue @@ -1233,9 +1242,10 @@ def load_config(path): for c in t.get("checks", []): fixes = c.get("fix") if isinstance(c.get("fix"), dict) else {"": c.get("fix")} for f in fixes.values(): - if f and f not in cfg.get("fixes", {}): + if f and f != RESTART_CONNECTION and f not in cfg.get("fixes", {}): raise ValueError("tile %s: unknown fix %s" % (t["id"], f)) - if f and cfg["fixes"][f].get("kind", "ssh") == "ssh" and cfg["fixes"][f]["host"] not in cfg.get("hosts", {}): + if (f and f != RESTART_CONNECTION and cfg["fixes"][f].get("kind", "ssh") == "ssh" + and cfg["fixes"][f]["host"] not in cfg.get("hosts", {})): raise ValueError("fix %s: unknown host" % f) if c["type"] == "remote" and c["host"] not in cfg.get("hosts", {}): raise ValueError("tile %s: unknown host %s" % (t["id"], c["host"])) diff --git a/tests/test_status_panel.py b/tests/test_status_panel.py index f7785ca..67648e3 100644 --- a/tests/test_status_panel.py +++ b/tests/test_status_panel.py @@ -461,9 +461,9 @@ class TestFixes(unittest.TestCase): def test_dante_reopens_dvs_on_the_right_mac(self): devices = json.loads(self.snap.vars[("audinate-dantecontroller", "devices")]) devices.remove("NA-ProPresenter-Mac") - devices.remove("BOH-WING") # no fix for a console + devices.remove("BOH-WING") # hardware: re-check via Companion's Dante module self.snap.vars[("audinate-dantecontroller", "devices")] = json.dumps(devices) - self.assertEqual(self.plan("dante"), [("fix", "dvs_prop")]) + self.assertEqual(self.plan("dante"), [("restart", "audinate-dantecontroller"), ("fix", "dvs_prop")]) def test_rec_reboots_only_hung_decks_not_recording_ones(self): for i in (2, 3, 5): @@ -543,13 +543,11 @@ class TestFixes(unittest.TestCase): devices.remove("STUDIO-A-Ingest") self.snap.vars[("audinate-dantecontroller", "devices")] = json.dumps(devices) self.assertEqual(self.plan("dante"), [("fix", "dvs_ingest")]) - # A failure with no fix (a console) must not claim there is nothing wrong. - devices.append("STUDIO-A-Ingest") - devices.remove("BOH-WING") - self.snap.vars[("audinate-dantecontroller", "devices")] = json.dumps(devices) + # A failure with no fix must not claim there is nothing wrong. + self.snap.midi["NA Ingest"]["peers"] = [] _, t = run_steps(self.engine, self.snap, SUNDAY, 4) - self.engine.run_fixes("dante", self.snap, None, t) - self.assertEqual(self.engine.notes["dante"]["text"], "NO AUTO\nFIX") + self.engine.run_fixes("midi", self.snap, None, t) + self.assertEqual(self.engine.notes["midi"]["text"], "NO AUTO\nFIX") def test_hold_on_a_green_button_says_so(self): _, t = run_steps(self.engine, self.snap, SUNDAY, 2)