ATEM: alarm when ATEM Software Control's MIDI control is turned on

New 'pref' check reads an app setting on NA Ingest (defaults read) every
10 s; ATEM shows ATEM SW MIDI ON if midiEnabled is not 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 09:52:33 -04:00
co-authored by Claude Opus 5.5
parent 0a4352bb92
commit b468b42889
3 changed files with 26 additions and 2 deletions
+4 -1
View File
@@ -171,7 +171,10 @@
] }, ] },
{ "id": "service", "label": "ALARMS", "pos": [3, 0], "kind": "service" }, { "id": "service", "label": "ALARMS", "pos": [3, 0], "kind": "service" },
{ "id": "atem", "label": "ATEM", "pos": [3, 1], "checks": [{ "type": "conn", "conn": "ATEM" }] }, { "id": "atem", "label": "ATEM", "pos": [3, 1], "checks": [
{ "type": "conn", "conn": "ATEM" },
{ "type": "pref", "domain": "com.blackmagic-design.ATEM Software Control", "key": "midiEnabled", "want": "0", "msg": "ATEM SW MIDI ON" }
] },
{ "id": "obs", "label": "OBS", "pos": [3, 2], "checks": [{ "type": "conn", "conn": "OBS-Youtube", "service_only": true, "fix": "obs" }] }, { "id": "obs", "label": "OBS", "pos": [3, 2], "checks": [{ "type": "conn", "conn": "OBS-Youtube", "service_only": true, "fix": "obs" }] },
{ "id": "resi", "label": "RESI", "pos": [3, 3], "checks": [ { "id": "resi", "label": "RESI", "pos": [3, 3], "checks": [
{ "type": "conn", "conn": "RESI_Decoder", "tag": "DECODER" }, { "type": "conn", "conn": "RESI_Decoder", "tag": "DECODER" },
+14 -1
View File
@@ -149,6 +149,7 @@ class Snapshot:
self.remote_midi_time = {} # host -> when it last reported self.remote_midi_time = {} # host -> when it last reported
self.peer_addr = {} # (host or "", session, peer) -> address it was last seen at self.peer_addr = {} # (host or "", session, peer) -> address it was last seen at
self.conn_hosts = {} # Companion connection label -> its "host" setting self.conn_hosts = {} # Companion connection label -> its "host" setting
self.prefs = {} # (preferences domain, key) -> value as "defaults read" prints it
self.midi_time = 0.0 self.midi_time = 0.0
self.mdns = set() # Bonjour _apple-midi._udp instance names self.mdns = set() # Bonjour _apple-midi._udp instance names
self.mdns_time = 0.0 self.mdns_time = 0.0
@@ -415,6 +416,14 @@ def check_http(chk, snap, ctx):
return [("http", Result(OK))] return [("http", Result(OK))]
def check_pref(chk, snap, ctx):
"""An app setting on NA Ingest, e.g. ATEM Software Control's "Enable MIDI control" must stay off."""
value = snap.prefs.get((chk["domain"], chk["key"]))
if value is None or value == str(chk["want"]):
return [("pref", Result(OK))] # not read yet, or as it should be
return [("pref", Result(ERR, chk["msg"], named=True))]
def check_disk(chk, snap, ctx): def check_disk(chk, snap, ctx):
free = snap.disk.get(chk["path"]) free = snap.disk.get(chk["path"])
if free is None: if free is None:
@@ -496,7 +505,7 @@ def check_remote(chk, snap, ctx):
CHECKS = { CHECKS = {
"conn": check_conn, "hyperdeck": check_hyperdeck, "dante": check_dante, "shure": check_shure, "conn": check_conn, "hyperdeck": check_hyperdeck, "dante": check_dante, "shure": check_shure,
"psm": check_psm, "tascam": check_tascam, "ping": check_ping, "launchd": check_launchd, "psm": check_psm, "tascam": check_tascam, "ping": check_ping, "launchd": check_launchd,
"process": check_process, "http": check_http, "disk": check_disk, "midi_peer": check_midi_peer, "process": check_process, "http": check_http, "disk": check_disk, "midi_peer": check_midi_peer, "pref": check_pref,
"mdns": check_mdns, "midi_rx": check_midi_rx, "remote": check_remote, "artnet": check_artnet, "mdns": check_mdns, "midi_rx": check_midi_rx, "remote": check_remote, "artnet": check_artnet,
} }
@@ -1451,6 +1460,10 @@ def poll_system(snap, cfg):
elif c["type"] == "disk": elif c["type"] == "disk":
usage = shutil.disk_usage(c["path"]) usage = shutil.disk_usage(c["path"])
snap.disk = dict(snap.disk, **{c["path"]: int(usage.free / 1e9)}) snap.disk = dict(snap.disk, **{c["path"]: int(usage.free / 1e9)})
elif c["type"] == "pref":
r = subprocess.run(["/usr/bin/defaults", "read", c["domain"], c["key"]], capture_output=True,
text=True, timeout=5)
snap.prefs[(c["domain"], c["key"])] = r.stdout.strip() if r.returncode == 0 else None
snap.procs = procs snap.procs = procs
+8
View File
@@ -275,6 +275,14 @@ class TestChecks(unittest.TestCase):
self.assertIn("solo_afl", CONFIG["auto_fix"]["fixes"]) self.assertIn("solo_afl", CONFIG["auto_fix"]["fixes"])
self.assertIn('"bad":true', chk["cmd"]) self.assertIn('"bad":true', chk["cmd"])
def test_atem_software_midi_control_must_stay_off(self):
key = ("com.blackmagic-design.ATEM Software Control", "midiEnabled")
engine = sp.Engine(CONFIG)
self.snap.prefs[key] = "0"
self.assertEqual(run_steps(engine, self.snap, self.now, 4)[0]["atem"]["x"], "ATEM\nOK")
self.snap.prefs[key] = "1"
self.assertEqual(run_steps(engine, self.snap, self.now + 5, 4)[0]["atem"]["x"], "ATEM\nATEM SW MIDI ON")
def test_propresenter_yellow_warning_is_ignored(self): def test_propresenter_yellow_warning_is_ignored(self):
engine = sp.Engine(CONFIG) engine = sp.Engine(CONFIG)
self.snap.conns["Propresenter-API"].update(category="warning", message="Socket closed") self.snap.conns["Propresenter-API"].update(category="warning", message="Socket closed")