From 51a14140cc0905f49b24d9f70ac86d45424fd158 Mon Sep 17 00:00:00 2001 From: christian Date: Wed, 21 Oct 2026 20:52:42 -0400 Subject: [PATCH] MIDI hold and auto-fix move a link joined by name to the owning Mac's wired address - WS 2026-10-21: FOH Mac mini joined ProPresenter's session by name; hold said "no automatic fix". Now the address comes from the link's own check, else the Mac that owns that session (wired SSH / ingest address + session port); auto-fix does it too, never while a HyperDeck records - WS: FOH Mac mini <-> ProPresenter watched (FOH PROP); auto-fix moved it to 10.2.1.12:5004 by itself Co-Authored-By: Claude Opus 5.5 --- sites/ws/config.json | 8 ++++++++ status_panel.py | 35 ++++++++++++++++++++++++++++++----- tests/test_status_panel.py | 38 +++++++++++++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/sites/ws/config.json b/sites/ws/config.json index 66c2492..98d1843 100644 --- a/sites/ws/config.json +++ b/sites/ws/config.json @@ -537,6 +537,14 @@ "tag": "PROP LIGHTS", "type": "midi_peer" }, + { + "type": "midi_peer", + "session": "ProPresenter", + "peer": "WS FOH Mac mini", + "address": "10.2.1.12:5004", + "fix": "midi_reconnect", + "tag": "FOH PROP" + }, { "fix": "midi_reconnect", "nets": [ diff --git a/status_panel.py b/status_panel.py index bf7c4dc..6b92798 100644 --- a/status_panel.py +++ b/status_panel.py @@ -687,6 +687,27 @@ def by_name(address): return str(address or "").startswith("\t") +def owner_wired_address(cfg, snap, session_name, nets): + """"ip:port" of the Mac whose network MIDI session is called `session_name`, at its wired address: + this Mac (its wired ingest address), or another computer the settings know (its SSH address), using + that session's own port. None when no known Mac owns it or none of its addresses is wired.""" + def wired(ip): + try: + return any(ipaddress.ip_address(ip) in n for n in nets) + except ValueError: + return False + if snap.midi and session_name in snap.midi: + ip = next((i for i in (cfg.get("site") or {}).get("ingest_ips") or [] if wired(i)), None) + if ip: + return "%s:%s" % (ip, snap.midi[session_name].get("port") or 5004) + for alias, sessions in (snap.remote_midi or {}).items(): + if session_name in (sessions or {}): + ip = str((cfg.get("hosts") or {}).get(alias, {}).get("ssh", "")).split("@")[-1] + if wired(ip): + return "%s:%s" % (ip, sessions[session_name].get("port") or 5004) + return None + + def wired_nets(cfg): """The wired network(s) the MIDI key's wired check names (empty: no rule).""" nets = [] @@ -1602,16 +1623,20 @@ class Engine: target = chk["conn"] if fix_kind == "midi_connect" and chk["type"] == "midi_wired": # A peer off the wired network or joined by name: join it again at the wired - # address its own MIDI check names. One no check names (ProdDash's own) is left - # to its program: the key says which. - if errors_only: - continue # moving a working link drops it a second: a hold only + # address its own MIDI check names, else at the wired address of the Mac whose + # session it is (WS, 2026-10-21: FOH Mac mini joined ProPresenter by name and a hold + # did nothing). Moving a working link drops it for a second: auto-fix does it too, + # but never while a HyperDeck records. A peer no known Mac owns (ProdDash's own) is + # left to its program: the key says which. + if errors_only and self.any_recording: + continue host, session, peer = (sub.split("|", 2) + ["", ""])[:3] nets = wired_nets(self.cfg) want = next((c.get("address") for t2 in all_tiles(self.cfg) for c in t2.get("checks", []) if c.get("type") == "midi_peer" and c.get("host", "") == host and c.get("session") == session and c.get("peer") == peer and peer_ip(c.get("address")) is not None - and any(peer_ip(c["address"]) in n for n in nets)), None) + and any(peer_ip(c["address"]) in n for n in nets)), None) \ + or owner_wired_address(self.cfg, snap, peer, nets) if not want: continue chk = dict({"host": host} if host else {}, type="midi_peer", session=session, peer=peer, address=want, repoint=True) diff --git a/tests/test_status_panel.py b/tests/test_status_panel.py index 918883b..65dfd43 100644 --- a/tests/test_status_panel.py +++ b/tests/test_status_panel.py @@ -1293,7 +1293,10 @@ class TestFixes(unittest.TestCase): self.assertEqual([(c["host"], c["session"], c["peer"], c["address"]) for c in moves], [("tracks", "NA Tracks", "Studio B Mac Studio", "10.1.13.175:5004")]) # ProdDash's own: left to it auto = engine.fix_plan("midi", self.snap, SUNDAY + 30, 0, True) - self.assertFalse(any(c.get("repoint") for _, _, _, c in auto)) # never by itself: a hold + self.assertTrue(any(c.get("repoint") for _, _, _, c in auto)) # auto-fix moves it too (user, 2026-10-21) ... + engine.any_recording = True + auto = engine.fix_plan("midi", self.snap, SUNDAY + 30, 0, True) + self.assertFalse(any(c.get("repoint") for _, _, _, c in auto)) # ... but never while a HyperDeck records def test_an_off_pc_is_woken(self): from unittest import mock @@ -2663,3 +2666,36 @@ class TestMidiNoState(unittest.TestCase): self.assertEqual(self.check(s, 110.0).msg, "MIDI CONNECTING") # put back: not trusted yet self.assertEqual(self.check(s, 125.0).msg, "MIDI CONNECTING") self.assertEqual(self.check(s, 131.0).level, sp.OK) # stayed 20 s: it answered + + +class TestByNameMove(unittest.TestCase): + """WS 2026-10-21: FOH Mac mini joined ProPresenter's session by name; a hold said "no automatic fix".""" + + def setUp(self): + self.cfg = sp.load_config(os.path.join(ROOT, "sites", "ws", "config.json")) + self.engine = sp.Engine(self.cfg) + self.snap = healthy_snapshot(SUNDAY) + self.snap.midi = {"ProPresenter": {"port": 5004, "peers": [ + {"name": "WS FOH Mac mini", "address": "\tWS FOH Mac mini\tlocal.", "state": "running"}, + {"name": "Lighting", "address": "10.2.2.2:5004", "state": "running"}]}} + self.snap.midi_time = SUNDAY + foh = next(a for a, h in self.cfg["hosts"].items() if str(h.get("ssh", "")).endswith("@10.2.1.12")) + self.snap.remote_midi = {foh: {"WS FOH Mac mini": {"port": 5004, "peers": []}}} + self.snap.remote_midi_time = {foh: SUNDAY} + self.midi = next(t["id"] for t in self.cfg["tiles"] if t["label"] == "MIDI") + + def moves(self, errors_only): + run_steps(self.engine, self.snap, SUNDAY, 3) + return [chk for _, kind, what, chk in self.engine.fix_plan(self.midi, self.snap, now=SUNDAY + 3, errors_only=errors_only) + if chk.get("repoint")] + + def test_a_hold_moves_it_to_the_owning_macs_wired_address(self): + moves = self.moves(False) + self.assertEqual([(c["session"], c["peer"], c["address"]) for c in moves], + [("ProPresenter", "WS FOH Mac mini", "10.2.1.12:5004")]) + + def test_auto_fix_moves_it_too_but_never_while_recording(self): + self.assertEqual(len(self.moves(True)), 1) + self.engine.any_recording = True # a HyperDeck records: left alone + plan = self.engine.fix_plan(self.midi, self.snap, now=SUNDAY + 3, errors_only=True) + self.assertEqual([c for _, k, w, c in plan if c.get("repoint")], [])