diff --git a/make_page.py b/make_page.py
index 742f3cd..6ea2469 100644
--- a/make_page.py
+++ b/make_page.py
@@ -208,8 +208,8 @@ def button(tid, tile, cfg):
elif kind == "autofix":
stale_text = "'%s\\n?'" % label
press, hold = "autofix toggle", None
- note = ("Press: pause auto-fix (alarms stay on); press again to turn it back on - or on outside its hours. It goes "
- "back to its hours by itself when they next start or end. Holds always fix.")
+ note = ("Press: pause auto-fix (alarms stay on) through the hours it is in now, or through the next ones when pressed "
+ "outside them; press again to resume. It resumes by itself when those hours end. Holds always fix.")
elif kind == "bypass":
stale_text = "'%s\\n?'" % label
press, hold = "ack " + tid, "restart " + tid
diff --git a/ready-page.companionconfig b/ready-page.companionconfig
index bd293a0..657322c 100644
Binary files a/ready-page.companionconfig and b/ready-page.companionconfig differ
diff --git a/schema.py b/schema.py
index aa22f61..e74831e 100644
--- a/schema.py
+++ b/schema.py
@@ -502,7 +502,7 @@ GLOSSARY = {
"ON FOH BACKUP", "BUSY", "TAKING MIDI OUT", "PUTTING MIDI BACK", "TAKING", "PUTTING", "MIDI OUT", "MIDI BACK",
],
"a switch key (AUTO-FIX, or one like LIGHTS that hands a device to an operator)": [
- "OPERATOR", "MIDI OFF", "OFF IN SETTINGS", "SETTINGS", "TIL",
+ "OPERATOR", "MIDI OFF", "OFF IN SETTINGS", "SETTINGS", "TIL", "PAUSED",
],
}
diff --git a/settings/campuses.py b/settings/campuses.py
index 0021313..c45e3c0 100644
--- a/settings/campuses.py
+++ b/settings/campuses.py
@@ -225,7 +225,7 @@ def all_autofix_key(local_cfg):
def autofix_paused(state_json):
- """True when that location's AUTO-FIX key says it was paused by hand ("AUTO-FIX\\nOFF\\nTIL ..."), None without one."""
+ """True when that location's AUTO-FIX key says it is paused ("AUTO-FIX\\nPAUSED\\nTIL ..."), None without one."""
try:
af = json.loads(state_json or "{}").get("autofix")
except ValueError:
@@ -233,7 +233,7 @@ def autofix_paused(state_json):
if not af:
return None
lines = str(af.get("x") or "").split("\n")
- return len(lines) > 2 and lines[1] == "OFF" and lines[2].startswith("TIL")
+ return len(lines) > 1 and lines[1] == "PAUSED"
def all_autofix_summary(states, now):
diff --git a/settings/keyguide.py b/settings/keyguide.py
index f364343..ecc365e 100644
--- a/settings/keyguide.py
+++ b/settings/keyguide.py
@@ -164,8 +164,8 @@ def key_guide(cfg):
if t.get("kind") == "service":
parts.append("
%s
" % e(ph["alarms"]))
if t.get("kind") == "autofix":
- parts.append("
Press: pause auto-fix, or run it outside its hours (auto-fix %s). It goes back to its hours by itself "
- "when they next start or end. Holds always fix.
" % e(ph["auto_fix"]))
+ parts.append("
Press: pause auto-fix through the hours it is in now, or through the next ones (auto-fix %s); press "
+ "again to resume. It resumes by itself when those hours end. Holds always fix.
" % e(ph["auto_fix"]))
if t.get("kind") == "bypass":
b = t.get("bypass") or {}
parts.append("
Hold: hand it to an operator - the MIDI links to %s are taken out%s, and the panel stops putting them "
diff --git a/settings/ui/app.js b/settings/ui/app.js
index bbc7f7f..7e4d52d 100644
--- a/settings/ui/app.js
+++ b/settings/ui/app.js
@@ -282,7 +282,7 @@
${chip(w.running, w.running ? "Watchdog ✓" : "Watchdog down", (w.running ? "Running since " + when(w.since) : "Not running") + (w.version ? " · code " + w.version : "") + (o.heartbeat_age != null ? " · heartbeat " + ago(o.heartbeat_age) : ""))}
${chip(o.companion_ok, o.companion_ok ? "Companion ✓" : "Companion down", o.pages && o.pages.status ? (o.pages.status.ok ? "Pages OK" : "Page " + o.pages.status.page + " is called " + (o.pages.status.live || "?")) : "")}
${chip(o.service_on == null ? null : o.service_on, o.service_on == null ? "Alarms ?" : o.service_on ? "Alarms on" : "Alarms silenced", o.phrases.alarms)}
- ${chip(null, "Auto-fix " + (af.override === false ? "paused" : af.on ? "active" : "waiting"), o.auto_fix.phrase + (af.override === false ? " (paused with the AUTO-FIX key)" : af.paused ? " (waiting out a network blip)" : ""))}
+ ${chip(null, "Auto-fix " + (af.paused === true ? "paused" : af.on ? "active" : "waiting"), o.auto_fix.phrase + (af.paused === true ? " (paused with the AUTO-FIX key)" : af.net_wait ? " (waiting out a network blip)" : ""))}
${Object.keys(o.bypass || {}).length ? chip(null, Object.keys(o.bypass).map((k) => ((cfg().tiles.find((t) => t.id === k) || {}).label || k)).join(", ") + ": operator", "Handed to an operator with its key: its MIDI is out until the key is held again") : ""}
${Object.keys(o.running || {}).length ? chip(null, "Fix running", Object.values(o.running).map((r) => r.what + " on " + r.key).join(", ")) : ""}
${o.last_change ? `Last change ${esc(o.last_change.when || "")}` : ""}
@@ -292,7 +292,7 @@
@@ -301,7 +301,7 @@
main.querySelector("#restartwd").onclick = () => confirmBox(w.running ? "Restart the watchdog? The deck goes purple for about 5 seconds; a fix that is running finishes first." : "Start the watchdog?", w.running ? "Restart" : "Start", async () => { try { const r = await post(w.running ? "/api/watchdog/restart" : "/api/watchdog/start"); alertBox(r.ok ? "Done: " + r.detail : "Not done: " + r.detail); } catch (e) { alertBox(e.message); } });
const stop = main.querySelector("#stopwd"); if (stop) stop.onclick = () => confirmBox("Stop the watchdog? The deck goes purple and every alarm is off until it is started again.", "Stop", async () => { try { const r = await post("/api/watchdog/stop", { word: "STOP" }); alertBox(r.ok ? "Stopped." : "Not stopped: " + r.detail); } catch (e) { alertBox(e.message); } }, "STOP");
main.querySelector("#alarms").onclick = async () => { try { await post("/api/command", { text: o.service_on === false ? "service on" : "service off" }); setTimeout(() => overview(_, true), 1500); } catch (e) { alertBox(e.message); } };
- const afb = main.querySelector("#autofixbtn"); if (afb) afb.onclick = async () => { try { await post("/api/command", { text: af.switch ? "autofix off" : "autofix on" }); setTimeout(() => overview(_, true), 1500); } catch (e) { alertBox(e.message); } };
+ const afb = main.querySelector("#autofixbtn"); if (afb) afb.onclick = async () => { try { await post("/api/command", { text: af.paused ? "autofix resume" : "autofix pause" }); setTimeout(() => overview(_, true), 1500); } catch (e) { alertBox(e.message); } };
for (const b of main.querySelectorAll("[data-run]")) b.onclick = () => runNow(...b.dataset.run.split("|"));
if (!quiet) window.scrollTo(0, 0);
}
diff --git a/sites/sc/config.json b/sites/sc/config.json
index 9928607..c3e857a 100644
--- a/sites/sc/config.json
+++ b/sites/sc/config.json
@@ -151,7 +151,8 @@
{
"type": "conn",
"conn": "Propresenter-API",
- "fix": "prop_port_propresenter_api"
+ "fix": "prop_port_propresenter_api",
+ "ignore_warning": true
},
{
"type": "process",
diff --git a/sites/ws/config.json b/sites/ws/config.json
index c2fb05c..c9b661c 100644
--- a/sites/ws/config.json
+++ b/sites/ws/config.json
@@ -486,7 +486,8 @@
"checks": [
{
"conn": "Propresenter-API",
- "type": "conn"
+ "type": "conn",
+ "ignore_warning": true
},
{
"fix": "propresenter_ingest",
diff --git a/status_panel.py b/status_panel.py
index 7450d7b..bf7c4dc 100644
--- a/status_panel.py
+++ b/status_panel.py
@@ -499,17 +499,56 @@ def check_midi_peer(chk, snap, ctx):
sess = sessions.get(chk["session"])
if sess is None:
return [("peer", Result(ERR, "NO SESSION"))]
+ seen = midi_presence(snap, chk.get("host", ""), sessions, ctx["now"])
for p in sess["peers"]:
if p["name"] == chk["peer"]:
# Measured 2026-10-07: a joining peer reads "connected" for ~6 s until
# clock sync, then "running". A peer that dies is removed from the list:
# at once if it says goodbye, after ~60 s if it crashes or its cable is pulled.
- if (p.get("state") or "running") in ("running", "connected"): # no state: older macOS, configured
+ state = p.get("state") or ""
+ if state in ("running", "connected"):
return [("peer", Result(OK))]
- return [("peer", Result(ERR, "MIDI " + (p["state"] or "?").upper()[:8]))]
+ if state:
+ return [("peer", Result(ERR, "MIDI " + state.upper()[:8]))]
+ # No state. macOS 26 reports one for every live link, so there it is not answering; macOS 14/15
+ # report none, so a link put back after it dropped only counts once it has stayed: macOS drops
+ # an unanswered one after ~12 s (Southcoast's Lighting flickered OK/LOST all afternoon, 2026-10-21).
+ since, dropped = seen.get((chk["session"], chk["peer"]), (-1e18, None))
+ young = ctx["now"] - since < MIDI_SETTLE
+ if seen.get("reports"):
+ return [("peer", Result(ERR, "MIDI CONNECTING" if young else "MIDI NO ANSWER"))]
+ if young and dropped is not None and since - dropped < 300:
+ return [("peer", Result(ERR, "MIDI CONNECTING"))]
+ return [("peer", Result(OK))]
return [("peer", Result(ERR, "MIDI LOST"))]
+MIDI_SETTLE = 20 # seconds a re-added link must stay listed before it counts (macOS drops a dead one at ~12 s)
+
+
+def midi_presence(snap, host, sessions, now):
+ """For one Mac's sessions: when each peer appeared {(session, peer): (since, dropped_at)}, and "reports":
+ whether that Mac gives links a state (any non-empty one seen there, ever). Peers there at the first
+ reading count as long established."""
+ mem = snap.__dict__.setdefault("midi_mem", {})
+ m = mem.setdefault(host, {"since": {}, "dropped": {}, "reports": False, "first": True, "read": None})
+ if m["read"] is not sessions: # a new reading: update once, not per check
+ m["read"] = sessions
+ present = {(s, p.get("name")) for s, sess in sessions.items() for p in (sess or {}).get("peers") or []}
+ if any(p.get("state") for sess in sessions.values() for p in (sess or {}).get("peers") or []):
+ m["reports"] = True
+ for key in present:
+ if key not in m["since"]:
+ m["since"][key] = -1e18 if m["first"] else now
+ for key in [k for k in m["since"] if k not in present]:
+ del m["since"][key]
+ m["dropped"][key] = now
+ m["first"] = False
+ out = {k: (v, m["dropped"].get(k)) for k, v in m["since"].items()}
+ out["reports"] = m["reports"]
+ return out
+
+
def midi_on(cfg, host, call, jxa):
"""Run a coremidi call on another Mac (python there, else the JavaScript for Automation `jxa`):
(result, error text)."""
@@ -1071,6 +1110,7 @@ class ServiceMode:
saved = self.state.get(name) or {}
self.override, self.last_sched = saved.get("override"), saved.get("last_sched")
self.edge = saved.get("edge")
+ self.hold = bool(saved.get("hold")) # a pause that lasts through the hours, not just to the next edge
def scheduled(self, now):
return in_windows(self.windows, now)
@@ -1086,6 +1126,12 @@ class ServiceMode:
def on(self, now):
sched = self.scheduled(now)
+ if self.hold: # paused: until its edge, whatever the schedule does meanwhile
+ if self.edge is not None and now >= self.edge:
+ self.override, self.hold, self.edge, self.last_sched = None, False, None, sched
+ self._save()
+ return sched
+ return False
if self.override is not None and self.edge is None:
self.edge = self.next_change(now) # an override saved before edges were kept
self._save()
@@ -1101,6 +1147,8 @@ class ServiceMode:
Looked up once a minute."""
if self.override is None:
return None
+ if self.hold:
+ return self.edge
minute = int(now // 60)
cached = getattr(self, "_back", None)
if cached and cached[0] == (minute, self.override):
@@ -1122,10 +1170,29 @@ class ServiceMode:
self.last_sched = sched
self.override = None if value == sched else value
self.edge = self.next_change(now) if self.override is not None else None
+ self.hold = False
self._save()
+ def pause(self, now):
+ """AUTO-FIX paused: through the hours it is in now, or, pressed outside them, through the next ones
+ (2026-10-21: a press on Saturday night used to do nothing for Sunday)."""
+ sched = self.scheduled(now)
+ start = now if sched else self.next_change(now)
+ self.override, self.hold, self.last_sched = False, True, sched
+ self.edge = self.next_change(start) if start else None
+ self._save()
+
+ def resume(self, now):
+ """Back to the hours."""
+ self.override, self.hold, self.edge, self.last_sched = None, False, None, self.scheduled(now)
+ self._save()
+
+ @property
+ def paused(self):
+ return self.hold and self.override is False
+
def _save(self):
- self.state.set(self.name, {"override": self.override, "last_sched": self.last_sched, "edge": self.edge})
+ self.state.set(self.name, {"override": self.override, "last_sched": self.last_sched, "edge": self.edge, "hold": self.hold})
def until(self, now):
"""When the state shown now next changes by itself (an override's end, else the schedule's next edge),
@@ -1686,10 +1753,13 @@ class Engine:
self.note(tile_id, "RESTARTING\nSOON", "idle", now, 4)
return []
kind = self.tiles.get(tile_id, {}).get("kind")
- if kind in ("service", "autofix"): # from North Attleboro's LOCATION page: the hold is the press
- mode = self.service if kind == "service" else self.autofix
- mode.set(not mode.on(now), now)
- log.info("%s %s (hold)", "SERVICE" if kind == "service" else "AUTO-FIX", "ON" if mode.on(now) else "OFF")
+ if kind == "service": # from North Attleboro's LOCATION page: the hold is the press
+ self.service.set(not self.service.on(now), now)
+ log.info("SERVICE %s (hold)", "ON" if self.service.on(now) else "OFF")
+ return []
+ if kind == "autofix":
+ (self.autofix.resume if self.autofix.paused else self.autofix.pause)(now)
+ log.info("AUTO-FIX %s (hold)", "PAUSED" if self.autofix.paused else "back to its hours")
return []
if kind == "bypass":
self.toggle_bypass(tile_id, comp, now)
@@ -1824,7 +1894,7 @@ class Engine:
# The AUTO-FIX key: paused, nothing is fixed by itself (holds still work); pressed outside
# the hours, the gentle fixes run until the hours next start or end.
hours = self.autofix.on(now)
- muted = self.autofix.override is False
+ muted = self.autofix.paused
else:
# The ALARMS key is the on-the-fly switch: muted, nothing is fixed by itself at all (holds
# still work). It comes back by itself with the alarms (2026-10-19: the user's choice).
@@ -2278,10 +2348,16 @@ def render(engine, service_on):
continue
on = engine.autofix.on(engine.now)
until = engine.autofix.until(engine.now)
- text = "%s\n%s" % (tile["label"], "ON" if on else "OFF")
- if until and (on or engine.autofix.override is not None):
+ when = ""
+ if until:
same_day = datetime.fromtimestamp(until).date() == datetime.fromtimestamp(engine.now).date()
- text += "\nTIL " + ("" if same_day else datetime.fromtimestamp(until).strftime("%a ").upper()) + clock12(until)
+ when = ("" if same_day else datetime.fromtimestamp(until).strftime("%a ").upper()) + clock12(until)
+ if engine.autofix.paused:
+ text = "%s\nPAUSED" % tile["label"] + ("\nTIL " + when if when else "")
+ elif on:
+ text = "%s\nON" % tile["label"] + ("\nTIL " + when if when else "")
+ else:
+ text = "%s\nOFF" % tile["label"] + ("\nON " + when if when else "") # outside the hours: when they start
out[tid] = colored(text, "service" if on else "off")
continue
if kind == "bypass":
@@ -3649,8 +3725,8 @@ def snapshot_summary(snap, engine, now):
for name, l in snap.listeners.items()},
"service_on": engine.service.on(now),
"alarms_override": engine.service.override,
- "auto_fix": {"on": engine.auto_on, "paused": engine.auto_paused, "split": engine.split,
- "switch": engine.autofix.on(now), "override": engine.autofix.override},
+ "auto_fix": {"on": engine.auto_on, "net_wait": engine.auto_paused, "split": engine.split,
+ "switch": engine.autofix.on(now), "override": engine.autofix.override, "paused": engine.autofix.paused},
"bypass": {tid: {"since": b.get("since")} for tid, b in engine.bypass.items()},
"failover": dict(engine.failed),
"burst": engine.burst and {"start": engine.burst["start"], "conns": sorted(engine.burst["conns"]),
@@ -3697,10 +3773,11 @@ def handle_commands(text, engine, comp, snap, now):
engine.service.set(target, now)
log.info("SERVICE %s (button)", "ON" if target else "OFF")
elif verb == "autofix":
- # "auto": back to the auto-fix hours (NA's ALL AUTO-FIX key resuming every location).
- target = {"on": True, "off": False, "auto": engine.autofix.scheduled(now)}.get(arg, not engine.autofix.on(now))
- engine.autofix.set(target, now)
- log.info("AUTO-FIX %s (%s)", "ON" if target else "OFF", "back to its hours" if arg == "auto" else "button")
+ # The AUTO-FIX key pauses and resumes; "off"/"pause" and "on"/"auto"/"resume" say which (NA's ALL AUTO-FIX key).
+ pause = {"off": True, "pause": True, "on": False, "auto": False, "resume": False}.get(arg, not engine.autofix.paused)
+ (engine.autofix.pause if pause else engine.autofix.resume)(now)
+ log.info("AUTO-FIX %s", ("PAUSED until %s" % datetime.fromtimestamp(engine.autofix.edge).strftime("%a %H:%M")
+ if engine.autofix.edge else "PAUSED") if pause else "back to its hours")
elif verb in ("restart", "force"):
# Holds carry Companion's clock ("force rs_x 1791496225"). One pressed while the watchdog
# was down must not fire minutes later in a burst with the others.
diff --git a/tests/test_campus.py b/tests/test_campus.py
index 39e7745..acefcf2 100644
--- a/tests/test_campus.py
+++ b/tests/test_campus.py
@@ -608,7 +608,7 @@ class TestAllAutoFix(unittest.TestCase):
def test_the_key_names_the_paused_locations_and_sits_where_auto_fix_does(self):
from settings import campuses as cv
st = lambda x: json.dumps({"autofix": {"x": x}})
- states = {"na": st("AUTO-FIX\nON\nTIL 9PM"), "sc": st("AUTO-FIX\nOFF\nTIL SUN 5AM"), "ws": st("AUTO-FIX\nOFF"), "ab": None}
+ states = {"na": st("AUTO-FIX\nON\nTIL 9PM"), "sc": st("AUTO-FIX\nPAUSED\nTIL SUN 2PM"), "ws": st("AUTO-FIX\nOFF\nON SUN 5AM"), "ab": None}
self.assertEqual(cv.all_autofix_summary(states, 100.0), {"t": 100, "paused": 1, "x": "SC\nPAUSED"}) # WS: outside its hours
self.assertEqual(cv.all_autofix_summary({"na": st("AUTO-FIX\nON\nTIL 9PM")}, 100.0)["x"], "ALL ON")
na = json.load(open(os.path.join(ROOT, "config.json")))
diff --git a/tests/test_status_panel.py b/tests/test_status_panel.py
index 2ef43e9..4ade477 100644
--- a/tests/test_status_panel.py
+++ b/tests/test_status_panel.py
@@ -2493,16 +2493,25 @@ class TestSwitches(unittest.TestCase):
self.engine.auto_fix(self.snap, mock_comp(), SUNDAY + 30)
time.sleep(0.2)
self.assertEqual(ran, ["ableton"]) # auto-fix carried on
- self.engine.autofix.set(False, SUNDAY + 40) # AUTO-FIX pressed: paused
+ self.engine.autofix.pause(SUNDAY + 40) # AUTO-FIX pressed in the hours: paused
tiles = self.engine.step(self.snap, SUNDAY + 41)
- self.assertEqual(tiles["autofix"]["x"], "AUTO-FIX\nOFF\nTIL WED 4PM") # back on with Wednesday's hours
+ self.assertEqual(tiles["autofix"]["x"], "AUTO-FIX\nPAUSED\nTIL 1:30PM") # through Sunday's hours
self.assertEqual(level(tiles["autofix"]), "off")
self.assertEqual(self.engine.auto_fix(self.snap, mock_comp(), SUNDAY + 200), [])
- self.assertFalse(self.new_engine().autofix.on(SUNDAY + 300)) # survives a restart
+ self.assertTrue(self.new_engine().autofix.paused) # survives a restart
self.assertTrue(self.engine.autofix.on(datetime(2026, 10, 14, 16, 30).timestamp())) # Wednesday's hours: back by itself
- weekday = SUNDAY - 3 * 86400 # Thursday: pressed = runs anyway
- self.engine.autofix.set(True, weekday)
- self.assertTrue(self.engine.autofix.on(weekday + 60))
+ self.assertFalse(self.engine.autofix.paused)
+ # Pressed outside the hours (Thursday): the key said OFF / ON SUN 6:30AM; the pause holds through Sunday's hours.
+ thursday = SUNDAY - 3 * 86400
+ tiles = self.engine.step(self.snap, thursday)
+ self.assertEqual(tiles["autofix"]["x"], "AUTO-FIX\nOFF\nON SUN 6:30AM")
+ self.engine.autofix.pause(thursday)
+ self.assertEqual(self.engine.step(self.snap, thursday + 1)["autofix"]["x"], "AUTO-FIX\nPAUSED\nTIL SUN 1:30PM")
+ self.assertFalse(self.engine.autofix.on(SUNDAY)) # Sunday 8:00, inside the hours: still paused
+ self.assertTrue(self.engine.autofix.on(datetime(2026, 10, 14, 16, 30).timestamp())) # and over after them
+ self.engine.autofix.pause(thursday)
+ self.engine.autofix.resume(thursday + 60) # pressed again: back to the hours
+ self.assertTrue(self.engine.autofix.on(SUNDAY))
page = make_page.build(self.cfg)["page"]["controls"]
notes = {(ctl.get("options") or {}).get("notes", "") for row in page.values() for ctl in row.values() if ctl}
self.assertTrue(any("Auto-fix is the AUTO-FIX key" in n for n in notes))
@@ -2510,8 +2519,9 @@ class TestSwitches(unittest.TestCase):
def test_a_hold_from_north_attleboro_flips_a_switch(self):
self.engine.run_fixes("autofix", self.snap, mock_comp(), SUNDAY)
- self.assertFalse(self.engine.autofix.on(SUNDAY + 1))
+ self.assertTrue(self.engine.autofix.paused)
self.engine.run_fixes("autofix", self.snap, mock_comp(), SUNDAY + 2)
+ self.assertFalse(self.engine.autofix.paused)
self.assertTrue(self.engine.autofix.on(SUNDAY + 3))
def test_the_operator_key_takes_the_midi_out_and_puts_it_back(self):
@@ -2622,3 +2632,33 @@ class TestCompanionKeeper(unittest.TestCase):
open(os.path.join(self.data, "companion-upgrading"), "w").write("x")
self.assertIsNone(self.keeper().check(self.now + 30)) # an upgrade is going on
self.assertEqual(self.opened, [])
+
+
+class TestMidiNoState(unittest.TestCase):
+ """macOS 26 gives each live link a state, macOS 14/15 none: a link with no state is judged by which (2026-10-21)."""
+
+ def snap(self, peers, now):
+ s = healthy_snapshot(now)
+ s.remote_midi = {"tracks": {"T": {"port": 5004, "peers": peers}}}
+ s.remote_midi_time = {"tracks": now}
+ return s
+
+ def check(self, s, now):
+ chk = {"type": "midi_peer", "host": "tracks", "session": "T", "peer": "Lighting", "address": "10.5.1.168:5004"}
+ s.remote_midi_time = {"tracks": now} # a fresh reading every 2 s
+ return sp.check_midi_peer(chk, s, {"now": now})[0][1]
+
+ def test_on_a_mac_that_reports_states_no_state_means_no_answer(self):
+ s = self.snap([{"name": "FOH", "address": "x", "state": "running"}, {"name": "Lighting", "address": "y", "state": ""}], 100.0)
+ self.assertEqual(self.check(s, 100.0).level, sp.ERR)
+ self.assertEqual(self.check(s, 100.0).msg, "MIDI NO ANSWER") # there at the first reading: long-standing
+
+ def test_on_an_older_mac_a_link_put_back_counts_only_once_it_stays(self):
+ s = self.snap([{"name": "Lighting", "address": "y"}], 100.0)
+ self.assertEqual(self.check(s, 100.0).level, sp.OK) # there from the start: configured, fine
+ s.remote_midi = {"tracks": {"T": {"port": 5004, "peers": []}}}
+ self.assertEqual(self.check(s, 101.0).msg, "MIDI LOST") # dropped
+ s.remote_midi = {"tracks": {"T": {"port": 5004, "peers": [{"name": "Lighting", "address": "y"}]}}}
+ 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