A Dante device Companion's module never listed before gets the Dante Controller connection restarted once
The module only reads a device's channels when it starts, so a receiver added later is missing from the crosspoint lists until the connection restarts (the two AD4Q receivers today). The watchdog now remembers every Dante device it has seen in state.json and, when a newcomer appears, restarts that connection once: never during a live service, at most every ten minutes, and never for a device that merely dropped out and came back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -2470,6 +2470,44 @@ def dante_label(cfg):
|
||||
return None
|
||||
|
||||
|
||||
def watch_dante_newcomers(snap, cfg, comp, engine, now=None):
|
||||
"""A Dante device Companion's module has never listed before: restart that connection once, so
|
||||
the module reads the newcomer's channels (its crosspoint lists skip a device it only discovered
|
||||
after it started). Devices seen so far are kept in state.json; never during a live service;
|
||||
at most once every ten minutes."""
|
||||
label = dante_label(cfg)
|
||||
if not label:
|
||||
return False
|
||||
try:
|
||||
present = set(json.loads(snap.var(label, "devices") or "[]"))
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
if not present:
|
||||
return False
|
||||
now = now or time.time()
|
||||
known = set(engine.state.get("dante_known", []) or [])
|
||||
if not known:
|
||||
engine.state.set("dante_known", sorted(present))
|
||||
return False
|
||||
new = present - known
|
||||
if not new:
|
||||
return False
|
||||
if engine.live_now(now)[0]:
|
||||
return False # after the service; the device stays new until then
|
||||
if now - (engine.state.get("dante_known_restart", 0) or 0) < 600:
|
||||
return False
|
||||
c = snap.conns.get(label)
|
||||
if not (c and c.get("enabled")):
|
||||
return False
|
||||
ok = comp.restart_connection(c["id"])
|
||||
log.info("AUTO dante: new device%s %s - reconnecting %s so its channels are known -> %s",
|
||||
"s" if len(new) > 1 else "", ", ".join(sorted(new)), label, "ok" if ok else "failed")
|
||||
engine.state.set("dante_known_restart", now)
|
||||
if ok:
|
||||
engine.state.set("dante_known", sorted(known | present))
|
||||
return ok
|
||||
|
||||
|
||||
def poll_dante_map(snap, cfg, comp, path, arp=arp_table, survey=None):
|
||||
"""Every Dante device that is present now: where it is plugged in and whether that port
|
||||
powers it. Kept for devices that later disappear, so a hold can still find them."""
|
||||
@@ -3201,6 +3239,11 @@ def run(cfg, data_dir, dry_run=False, stop=None, config_sha=None):
|
||||
except ValueError: # not the main thread (tests)
|
||||
pass
|
||||
threads = start_pollers(cfg, snap, stop, data_dir)
|
||||
if not dry_run:
|
||||
c5 = make_companion(cfg)
|
||||
t = Periodic("dante-newcomers", 60.0, lambda: watch_dante_newcomers(snap, cfg, c5, engine), stop)
|
||||
t.start()
|
||||
threads.append(t)
|
||||
if cfg.get("ready_page") and not (cfg.get("ready") or {}).get("disabled"):
|
||||
import readiness
|
||||
poller = readiness.Poller(cfg, snap, stop, data_dir, is_live=lambda: engine.live_now(time.time())[0])
|
||||
|
||||
@@ -2255,3 +2255,52 @@ class TestCoreMIDI(unittest.TestCase):
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
class TestDanteNewcomers(unittest.TestCase):
|
||||
"""A Dante device the module never listed before gets the Dante Controller connection restarted once."""
|
||||
|
||||
def setUp(self):
|
||||
self.snap = healthy_snapshot(time.time())
|
||||
self.engine = sp.Engine(CONFIG, None)
|
||||
self.restarts = []
|
||||
me = self
|
||||
|
||||
class Comp:
|
||||
def restart_connection(self, cid):
|
||||
me.restarts.append(cid); return True
|
||||
self.comp = Comp()
|
||||
self.label = sp.dante_label(CONFIG)
|
||||
self.snap.conns[self.label] = {"id": "dante-1", "enabled": True, "category": "good"}
|
||||
|
||||
def devices(self, names):
|
||||
self.snap.vars[(self.label, "devices")] = json.dumps(names)
|
||||
|
||||
def test_first_list_is_remembered_then_a_newcomer_restarts_once(self):
|
||||
now = time.time()
|
||||
self.devices(["A", "B"])
|
||||
self.assertFalse(sp.watch_dante_newcomers(self.snap, CONFIG, self.comp, self.engine, now))
|
||||
self.assertEqual(self.engine.state.get("dante_known"), ["A", "B"])
|
||||
self.assertFalse(sp.watch_dante_newcomers(self.snap, CONFIG, self.comp, self.engine, now + 60)) # nothing new
|
||||
self.devices(["A", "B", "BOH-AD4Q-A"])
|
||||
self.assertTrue(sp.watch_dante_newcomers(self.snap, CONFIG, self.comp, self.engine, now + 120))
|
||||
self.assertEqual(self.restarts, ["dante-1"])
|
||||
self.assertEqual(self.engine.state.get("dante_known"), ["A", "B", "BOH-AD4Q-A"])
|
||||
self.devices(["A", "B", "BOH-AD4Q-A", "X"])
|
||||
self.assertFalse(sp.watch_dante_newcomers(self.snap, CONFIG, self.comp, self.engine, now + 180)) # ten-minute cooldown
|
||||
self.assertTrue(sp.watch_dante_newcomers(self.snap, CONFIG, self.comp, self.engine, now + 800))
|
||||
self.assertEqual(len(self.restarts), 2)
|
||||
self.devices(["A", "B"]) # devices going away never restart
|
||||
self.assertFalse(sp.watch_dante_newcomers(self.snap, CONFIG, self.comp, self.engine, now + 2000))
|
||||
self.devices(["A", "B", "BOH-AD4Q-A"]) # nor coming back
|
||||
self.assertFalse(sp.watch_dante_newcomers(self.snap, CONFIG, self.comp, self.engine, now + 2600))
|
||||
|
||||
def test_waits_for_the_service_to_end(self):
|
||||
now = time.time()
|
||||
self.devices(["A"])
|
||||
sp.watch_dante_newcomers(self.snap, CONFIG, self.comp, self.engine, now)
|
||||
self.devices(["A", "NEW"])
|
||||
self.engine.any_recording = True
|
||||
self.assertFalse(sp.watch_dante_newcomers(self.snap, CONFIG, self.comp, self.engine, now + 100))
|
||||
self.engine.any_recording = False
|
||||
self.assertTrue(sp.watch_dante_newcomers(self.snap, CONFIG, self.comp, self.engine, now + 200))
|
||||
|
||||
Reference in New Issue
Block a user