Files
status-indicators/settings/autoconf.py
T
christianandClaude Opus 5.5 457335bc96 Woonsocket's startup list covered: M32, SoundGrid link, MIDI on Macs without python
From the WS Production Startup checklist (check.watersmedia.org):
- M32 (the "X32" there is a Midas M32 at 10.2.1.146): Woonsocket's Companion got the X32/M32
  module (imported from NA's copy through Companion's own API) and an m32 connection; auto-setup
  names a single M32 connection's key M32.
- SuperRack's link to the Waves SoundGrid server: a soundgrid check builder (SuperRack's control
  connection on port 19000) inside every SUPERRACK key.
- MIDI ("Open Audio MIDI"): Macs without the Command Line Tools have their sessions read with
  JavaScript for Automation (coremidi.JXA_SESSIONS) - by the watchdog every 2 s and by discovery. A
  peer with no state (older macOS) counts as configured. A peer joined by name or IPv6 gets the
  wired address of the Mac whose session it is (matched by session name), at that session's port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:48:28 -04:00

530 lines
28 KiB
Python

"""Auto-setup: what a campus has (settings/discover.py's inventory) becomes its keys, checks, fixes
and page layout - the same building blocks North Attleboro's were made of by hand.
Pure functions, no I/O:
build(base, inv, campus, pin) a whole config for a new campus, from the empty template
extend(cfg, inv, seen) a running campus: only what is new (never removes anything)
Everything that comes out passes schema.validate and can be changed on the settings page like any
hand-made setting. Gear auto-setup does not know (a generic connection, an app it has no recipe
for) is reported, not guessed at.
"""
import copy
import ipaddress
import re
from settings import PARENT # noqa: F401 (puts the project folder on sys.path)
from settings import builders as b
import status_panel as sp
# --- what auto-setup knows -------------------------------------------------------------
# Group keys sum up their members on a big campus, in this order along the top row.
GROUPS = ["REC", "AUDIO", "RF", "SHOW", "VIDEO", "STREAM"]
# Who gets a cell of its own when not everything fits (the rest show inside their group key only):
# a row of wireless receivers folds first, as at North Attleboro.
PRIORITY = ["REC", "SHOW", "AUDIO", "STREAM", "VIDEO", "RF"]
# Companion modules: which group, what kind of key, the key's text when there is only one.
COMPANION = {
"bmd-hyperdeck": {"group": "REC", "kind": "hyperdeck", "label": "HD"},
"tascam-da-6400": {"group": "REC", "kind": "tascam", "label": "TASCAM"},
"audinate-dantecontroller": {"group": "AUDIO", "kind": "dante", "label": "DANTE"},
"behringer-x32": {"group": "AUDIO", "kind": "conn", "label": "X32"},
"behringer-wing": {"group": "AUDIO", "kind": "conn", "label": "WING"},
"midas-m32": {"group": "AUDIO", "kind": "conn", "label": "M32"},
"allenheath-sq": {"group": "AUDIO", "kind": "conn", "label": "SQ"},
"allenheath-dlive": {"group": "AUDIO", "kind": "conn", "label": "DLIVE"},
"allenheath-avantis": {"group": "AUDIO", "kind": "conn", "label": "AVANTIS"},
"yamaha-rcp": {"group": "AUDIO", "kind": "conn", "label": "YAMAHA"},
"soundcraft-ui": {"group": "AUDIO", "kind": "conn", "label": "UI24"},
"digico-sd": {"group": "AUDIO", "kind": "conn", "label": "DIGICO"},
"shure-wireless": {"group": "RF", "kind": "shure", "label": "MICS"},
"shure-psm1000": {"group": "RF", "kind": "psm", "label": "PSM"},
"renewedvision-propresenter-api": {"group": "SHOW", "kind": "conn", "label": "PROP", "app": "propresenter"},
"ableton-osc": {"group": "SHOW", "kind": "conn", "label": "ABLETON", "app": "ableton"},
"leolabs-ableset": {"group": "SHOW", "kind": "conn", "label": "ABLESET", "app": "ableton"},
"bmd-atem": {"group": "VIDEO", "kind": "conn", "label": "ATEM"},
"bmd-videohub": {"group": "VIDEO", "kind": "conn", "label": "VIDEOHUB"},
"resolume-arena": {"group": "VIDEO", "kind": "conn", "label": "ARENA", "app": "arena"},
"novastar-controller": {"group": "VIDEO", "kind": "conn", "label": "LED", "ping": True},
"obs-studio": {"group": "STREAM", "kind": "conn", "label": "OBS", "app": "obs"},
"resi-decoders": {"group": "STREAM", "kind": "conn", "label": "RESI", "ping": True},
}
# Programs on the computers: by OS, the process (Mac, as pgrep sees it) or program file (Windows).
# "app" ties a program to a Companion connection of the same name; "restart" adds a Restart page key.
MAC_APPS = [
{"app": "propresenter", "process": "ProPresenter", "label": "PROP", "open": "/Applications/ProPresenter*.app", "restart": True},
{"app": "ableton", "process": "Live", "label": "ABLETON", "open": "/Applications/Ableton Live*.app", "ableton": True, "restart": True},
{"app": "arena", "process": "Arena", "label": "ARENA", "open": "/Applications/Resolume Arena*/Arena.app", "restart": True},
{"app": "obs", "process": "obs", "label": "OBS", "open": "/Applications/OBS.app", "restart": True},
{"app": "translate", "process": "Church Translate", "label": "TRANSLATE", "open": "/Applications/Church Translate.app", "restart": True},
{"app": "protools", "process": "Pro Tools", "label": "PRO TOOLS", "open": "/Applications/Pro Tools.app", "restart": True},
{"app": "vista", "process": "Vista", "label": "VISTA", "open": "/Applications/Vista*.app", "restart": True},
{"app": "superrack", "process": "SuperRack SoundGrid", "label": "SUPERRACK", "open": "/Applications/*SuperRack*.app", "restart": True, "group": "AUDIO"},
{"app": "x32edit", "process": "X32-Edit", "label": "X32 EDIT", "open": "/Applications/X32-Edit.app", "restart": False, "group": "AUDIO"},
]
WIN_APPS = [
{"app": "vista", "exe": "Vista.exe", "label": "VISTA", "task": "Start Vista", "restart": True},
{"app": "arena", "exe": "Arena.exe", "label": "ARENA", "task": "Start Arena", "restart": True},
{"app": "obs", "exe": "obs64.exe", "label": "OBS", "task": "Start OBS", "restart": True},
{"app": "propresenter", "exe": "ProPresenter.exe", "label": "PROP", "task": "Start ProPresenter", "restart": True},
{"app": "rtpmidi", "exe": "rtpMIDI.exe", "label": "RTPMIDI", "task": "Start rtpMIDI", "restart": False, "fold": "midi_wired"},
]
# The task names the Windows join script makes, by program file (it reads WIN_APPS through campus.py).
WIN_TASKS = {a["exe"]: a["task"] for a in WIN_APPS}
# Fixes shared by every key of a kind (the same words North Attleboro's carry).
SHARED_FIXES = {
"hyperdeck": {"kind": "hyperdeck", "template": "hyperdeck", "params": {}, "short": "HYPERDECK", "verify_seconds": 15,
"hung_note": "STILL OFFLINE\nHOLD AGAIN",
"label": "turn Companion's connection to that HyperDeck off and on (what gets a restarted deck green); never reboots the deck"},
"midi_reconnect": {"kind": "midi_connect", "template": "midi_connect", "params": {}, "short": "RECONNECT",
"label": "reconnect the peer to the network MIDI session it dropped out of (like Connect in Audio MIDI Setup), at its wired address"},
}
STATUS_ROWS, COLS = 4, 8
DEFAULT_AUTO_FIX = {"after_seconds": 20, "max_tries": 2, "retry_seconds": 90, "pause_after_network_seconds": 60,
"restart_connections": True, "windows": [{"days": ["sun"], "start": "05:00", "end": "14:00"}]}
# --- small helpers ---------------------------------------------------------------------
def slug(text, taken=()):
base = re.sub(r"[^a-z0-9]+", "_", str(text).lower()).strip("_") or "key"
out, n = base, 2
while out in taken:
out, n = "%s_%d" % (base, n), n + 1
return out
def key_text(label, limit=10):
t = re.sub(r"[_\s]+", " ", str(label)).strip().upper()
return t if len(t) <= limit else t[:limit].rstrip()
def tag_for(name):
return key_text(name, 12)
def ip_of(address):
ip = sp.peer_ip(address)
return str(ip) if ip else None
def in_nets(nets, address):
ip = sp.peer_ip(address)
return ip is not None and any(ip in ipaddress.ip_network(n, strict=False) for n in nets)
def computer_by_ip(inv, ip):
for alias, c in (inv.get("computers") or {}).items():
if ip and ip in (c.get("ips") or []):
return alias
return None
def referenced(cfg):
"""Everything a config already watches, by the same identities discovery uses."""
out = set()
for t in sp.all_tiles(cfg):
for c in t.get("checks", []):
if c.get("conn"):
out.add("conn:" + c["conn"])
if c.get("type") == "dante":
out.update("dante:" + d for d in (c.get("devices") or {}))
if c.get("type") == "midi_peer":
out.add("midi:%s|%s|%s" % (c.get("host", ""), c.get("session"), c.get("peer")))
if c.get("type") == "midi_wired":
out.add("midi_wired")
p = c.get("params") or {}
if c.get("type") == "remote" and p.get("host") and (p.get("process") or p.get("exe")):
out.add("app:%s|%s" % (p["host"], p.get("process") or p.get("exe")))
for alias, h in (cfg.get("hosts") or {}).items():
out.add("host:" + alias)
ip = str(h.get("ssh", "")).split("@")[-1]
if ip:
out.add("hostip:" + ip)
return out
# --- from the inventory to items ------------------------------------------------------
def items(inv, nets):
"""[{ident, group, label, checks, fixes, restart, also_idents}] for everything auto-setup can watch,
plus the computer records ({alias: host}). Each item is one key."""
out, hosts = [], {}
comps = inv.get("computers") or {}
for alias, c in comps.items():
if not c.get("reachable"):
continue
hosts[alias] = {"name": c.get("name") or alias, "os": c.get("os", "mac"), "ssh": "%s@%s" % (c["user"], c["ip"]),
"role": "found by auto-setup", "runs": [{"kind": "sshd"}]}
if c.get("macs"):
hosts[alias]["mac"] = c["macs"][0]
# Programs on the computers, by app, so a Companion connection to the same program joins its key.
apps_at = {} # (app, ip) -> item
for alias, c in comps.items():
if not c.get("reachable"):
continue
procs = set(c.get("procs") or [])
catalog = MAC_APPS if c.get("os", "mac") == "mac" else WIN_APPS
for a in catalog:
if c.get("os", "mac") == "mac":
if a["process"] not in procs:
continue
app_path = (c.get("paths") or {}).get(a["process"]) or newest_app(c.get("apps") or [], a["open"])
if not app_path:
continue
check = {"type": "remote", "template": "mac_program", "params": {"host": alias, "process": a["process"]},
"msg": "%s NOT RUNNING" % a["label"]}
if a.get("ableton"):
start = {"template": "ableton", "params": {"host": alias, "app": app_path.rsplit("/", 1)[-1][:-4], "restart": False}}
again = {"template": "ableton", "params": {"host": alias, "app": app_path.rsplit("/", 1)[-1][:-4], "restart": True}}
else:
start = {"template": "mac_start", "params": {"host": alias, "process": a["process"], "app": app_path}}
again = {"template": "mac_restart", "params": {"host": alias, "process": a["process"], "app": app_path,
"wait_seconds": 10, "force": True}}
ident = "app:%s|%s" % (alias, a["process"])
else:
if a["exe"] not in procs or a["task"] not in (c.get("tasks") or []):
continue # no start task on that PC: a hold could not start it
check = {"type": "remote", "template": "win_program", "params": {"host": alias, "exe": a["exe"]},
"msg": "%s NOT RUNNING" % a["label"]}
start = {"template": "win_start", "params": {"host": alias, "task": a["task"], "exe": a["exe"]}}
again = {"template": "win_restart", "params": {"host": alias, "task": a["task"], "exe": a["exe"]}}
ident = "app:%s|%s" % (alias, a["exe"])
fid = slug("%s_%s" % (a["app"], alias))
pretty = {"propresenter": "ProPresenter", "ableton": "Ableton", "arena": "Resolume Arena", "obs": "OBS", "translate": "Church Translate",
"protools": "Pro Tools", "vista": "Vista", "rtpmidi": "rtpMIDI", "superrack": "SuperRack", "x32edit": "X32-Edit"}.get(a["app"], a["label"].title())
where = c.get("name") or alias
start = dict(start, label="start %s on %s if it is not running" % (pretty, where), short=a["label"])
again = dict(again, label="restart %s on %s (closes it first)" % (pretty, where), short=a["label"])
item = {"ident": ident, "group": "SHOW" if a["app"] not in ("obs",) else "STREAM", "label": a["label"],
"checks": [dict(check, fix=fid)], "fixes": {fid: start}, "restart": [], "also": [], "fold": a.get("fold")}
if a.get("restart"):
rid = "restart_" + fid
item["fixes"][rid] = again
item["restart"].append({"label": a["label"], "fix": rid})
if a["app"] == "arena":
item["group"] = "VIDEO"
if a.get("group"):
item["group"] = a["group"]
if a["app"] == "superrack": # and its link to the Waves SoundGrid server
item["checks"].append({"type": "remote", "template": "soundgrid", "params": {"host": alias}, "msg": "SOUNDGRID LOST"})
out.append(item)
apps_at[(a["app"], c.get("ip"))] = item
for ip in c.get("ips") or []:
apps_at.setdefault((a["app"], ip), item)
# Companion connections.
counts = {}
for conn in inv.get("connections") or []:
counts[conn.get("module")] = counts.get(conn.get("module"), 0) + 1
hd_n = 0
for conn in sorted(inv.get("connections") or [], key=lambda c: c["label"]):
spec = COMPANION.get(conn.get("module"))
if not spec or not conn.get("enabled", True):
continue
label = conn["label"]
ident = "conn:" + label
many = counts.get(conn["module"], 0) > 1
text = spec["label"] if not many else key_text(label)
if conn.get("module") == "behringer-x32" and "m32" in label.lower() and not many:
text = "M32" # a Midas M32 on the same module (Woonsocket)
if spec["kind"] == "hyperdeck":
hd_n += 1
text = "HD%d" % hd_n if many else "HD"
out.append({"ident": ident, "group": "REC", "label": text, "restart": [], "also": [],
"checks": [{"type": "hyperdeck", "conn": label, "fix": {"conn": "hyperdeck"}}],
"fixes": {"hyperdeck": dict(SHARED_FIXES["hyperdeck"])}})
continue
if spec["kind"] == "tascam":
fid = slug("reconnect_" + label)
out.append({"ident": ident, "group": "REC", "label": text, "restart": [], "also": [],
"checks": [{"type": "tascam", "conn": label, "fix": fid}, {"type": "ping", "conn": label, "name": text}],
"fixes": {fid: {"kind": "reconnect", "template": "reconnect", "params": {}, "conn": label, "short": text,
"verify_seconds": 20, "label": "reconnect Companion to %s" % label}}})
continue
if spec["kind"] == "dante":
devices = {d: key_text(d, 12) for d in (inv.get("dante") or {}).get("devices") or []}
out.append({"ident": ident, "group": "AUDIO", "label": "DANTE", "restart": [], "also": ["dante:" + d for d in devices],
"checks": [{"type": "dante", "conn": label, "devices": devices, "fix": {"*": sp.DANTE_AUTO}}], "fixes": {}})
continue
if spec["kind"] in ("shure", "psm"):
n = int(conn.get("channels") or (4 if spec["kind"] == "shure" else 2))
out.append({"ident": ident, "group": "RF", "label": text if many else ("MICS" if spec["kind"] == "shure" else "PSM"),
"restart": [], "also": [],
"checks": [{"type": spec["kind"], "conn": label, "channels": n}, {"type": "ping", "conn": label}], "fixes": {}})
continue
checks = [{"type": "conn", "conn": label}]
if spec.get("ping") and conn.get("host"):
checks.append({"type": "ping", "conn": label})
app_item = apps_at.get((spec.get("app"), ip_of(conn.get("host")))) if spec.get("app") else None
if app_item: # the program's own key carries its connection too
app_item["checks"] = checks + app_item["checks"]
app_item["also"].append(ident)
continue
out.append({"ident": ident, "group": spec["group"], "label": text, "checks": checks, "fixes": {}, "restart": [], "also": []})
# Network MIDI: every peer in the sessions here and on the other Macs, at its wired address.
midi_checks = []
# Whose session a peer is, by its name: a peer joined by name or by an IPv6 address gets that Mac's wired address.
owner = {sname: alias for alias, sessions in (inv.get("midi") or {}).items() if alias for sname in (sessions or {})}
for host, sessions in (inv.get("midi") or {}).items():
for sname, sess in (sessions or {}).items():
for p in sess.get("peers") or []:
addr = p.get("address") or ""
if not in_nets(nets, addr):
alias = owner.get(p.get("name")) or computer_named(inv, p.get("name"))
ip = alias and next((i for i in comps[alias].get("ips") or [] if in_nets(nets, i)), None)
if not ip:
continue # no wired address known: the wired check will name it
port = (((inv.get("midi") or {}).get(alias) or {}).get(p.get("name")) or {}).get("port") or 5004
addr = "%s:%d" % (ip, port)
chk = {"type": "midi_peer", "session": sname, "peer": p["name"], "address": addr, "fix": "midi_reconnect",
"tag": tag_for(p["name"])}
if host:
chk["host"] = host
midi_checks.append(("midi:%s|%s|%s" % (host, sname, p["name"]), chk))
if midi_checks:
checks = [c for _, c in midi_checks] + [{"type": "midi_wired", "nets": list(nets), "fix": "midi_reconnect"}]
out.append({"ident": "midi_wired", "group": "SHOW", "label": "MIDI", "checks": checks,
"fixes": {"midi_reconnect": dict(SHARED_FIXES["midi_reconnect"])}, "restart": [],
"also": [i for i, _ in midi_checks]})
# A helper program (rtpMIDI) rides inside the key it serves when that key exists.
for it in [x for x in out if x.get("fold")]:
home = next((x for x in out if x["ident"] == it["fold"]), None)
if home is not None:
home["checks"] += it["checks"]
home["fixes"].update(it["fixes"])
home["also"].append(it["ident"])
out.remove(it)
return out, hosts
def computer_named(inv, name):
n = str(name or "").lower()
for alias, c in (inv.get("computers") or {}).items():
if n and n in (str(c.get("name", "")).lower(), str(c.get("hostname", "")).lower().replace(".local", "")):
return alias
return None
def newest_app(apps, pattern):
"""The newest /Applications entry matching a glob-ish pattern ("/Applications/ProPresenter*.app")."""
rx = re.compile("^" + re.escape(pattern).replace(r"\*", ".*") + "$")
hits = sorted(a for a in apps if rx.match(a))
return hits[-1] if hits else None
# --- layout ----------------------------------------------------------------------------
def free_cells(taken, rows=STATUS_ROWS):
return [(r, c) for r in range(rows) for c in range(1, COLS) if (r, c) not in taken]
def layout(cfg, new_tiles, restart_items):
"""Put the keys on the Status page: everything fits, so no group keys; otherwise group keys along
the top row and the members after them (a member with no room left shows inside its group only)."""
fixed = {tuple(t["pos"]) for t in cfg["tiles"] if t.get("pos")}
if restart_items:
fixed.add(tuple(cfg["restart_page"]["jump_pos"]))
cells = free_cells(fixed)
groups = [g for g in GROUPS if any(t["_group"] == g for t in new_tiles)]
if len(new_tiles) > len(cells) and len(groups) > 1:
for g in groups:
members = [t["id"] for t in new_tiles if t["_group"] == g]
cfg["tiles"].append({"id": slug(g, {t["id"] for t in cfg["tiles"]} | {t["id"] for t in new_tiles}),
"label": g, "members": members, "_group": g})
group_tiles = [t for t in cfg["tiles"] if t.get("_group")]
for t in group_tiles:
if cells:
t["pos"] = list(cells.pop(0))
# Who gets a cell: by priority while cells last; then placed in group order, so a group reads as a block.
chosen = set()
for g in PRIORITY:
for t in new_tiles:
if t["_group"] == g and len(chosen) < len(cells):
chosen.add(t["id"])
for t in new_tiles:
if t["id"] in chosen:
t["pos"] = list(cells.pop(0))
cfg["tiles"].append(t)
for t in cfg["tiles"]:
t.pop("_group", None)
def place_new(cfg, tile):
"""One more key on a running campus: the first free cell, else inside its group, else on ALL only."""
taken = {tuple(t["pos"]) for t in cfg["tiles"] if t.get("pos")}
if cfg.get("restart_page"):
taken.add(tuple(cfg["restart_page"]["jump_pos"]))
cells = free_cells(taken)
group = next((t for t in cfg["tiles"] if t.get("label") == tile.get("_group") and t.get("members") is not None), None)
if cells:
tile["pos"] = list(cells[0])
if group is not None:
group["members"].append(tile["id"])
tile.pop("_group", None)
cfg["tiles"].append(tile)
def restart_layout(cfg, restart_items):
if not restart_items:
cfg.pop("restart_page", None)
return
rp = cfg["restart_page"]
taken = {tuple(rp["back_pos"])} | {tuple(k["pos"]) for k in rp.get("keys", [])}
cells = free_cells(taken)
ids = {k["id"] for k in rp.get("keys", [])}
for it in restart_items:
kid = slug("rs_" + it["label"], ids)
ids.add(kid)
if not cells:
break
rp.setdefault("keys", []).append({"id": kid, "label": key_text(it["label"]), "fix": it["fix"], "pos": list(cells.pop(0))})
# --- the two entry points --------------------------------------------------------------
def nets_of(inv):
w = (inv.get("ingest") or {}).get("wired") or {}
return [w["net"]] if w.get("net") else []
def build(base, inv, campus, pin, pages=(19, 20)):
"""A whole config for a new campus: the template, this Mac, and a key for everything found."""
cfg = copy.deepcopy(base)
ing = inv.get("ingest") or {}
wired = ing.get("wired") or {}
site = cfg["site"]
site.update({"id": campus["id"], "name": campus["name"], "ingest_name": ing.get("name") or "%s Ingest" % campus["name"],
"ingest_hostname": ing.get("hostname") or "", "ingest_ips": [ip for ip in [wired.get("ip")] + list(ing.get("ips") or []) if ip],
"editing_code": pin})
site["ingest_ips"] = list(dict.fromkeys(site["ingest_ips"]))
cfg["discover"] = {"auto": True}
cfg["page"], cfg["page_name"] = pages[0], "%d\\nStatus" % pages[0]
cfg["restart_page"] = {"page": pages[1], "page_name": "%d\\nRestart" % pages[1], "jump_pos": [3, 6], "back_pos": [3, 0], "keys": []}
cfg["network"] = dict(cfg.get("network") or {}, interface=wired.get("interface") or "en0", gateway=wired.get("gateway") or "")
cfg["service_windows"] = [{"days": list(sp.DAYS), "start": "00:00", "end": "23:59"}]
cfg["hosts"]["ingest"]["name"] = site["ingest_name"]
for t in cfg["tiles"]:
if t.get("kind") == "network":
t["pos"] = [3, 7]
found, hosts = items(inv, nets_of(inv))
cfg["hosts"].update(hosts)
tiles, restart, taken = [], [], {t["id"] for t in cfg["tiles"]}
for it in found:
tid = slug(it["label"], taken)
taken.add(tid)
tiles.append({"id": tid, "label": key_text(it["label"]), "checks": it["checks"], "_group": it["group"]})
for fid, f in it["fixes"].items():
cfg["fixes"].setdefault(fid, f)
restart += it["restart"]
tiles.sort(key=lambda t: GROUPS.index(t["_group"]))
layout(cfg, tiles, restart)
restart_layout(cfg, restart)
finish(cfg)
a = dict(DEFAULT_AUTO_FIX, **{k: v for k, v in (cfg.get("auto_fix") or {}).items() if k not in ("windows", "fixes")})
a["fixes"] = gentle(cfg)
cfg["auto_fix"] = a
return cfg, [it["ident"] for it in found] + ["host:" + h for h in hosts] + [x for it in found for x in it["also"]]
def extend(cfg, inv, seen):
"""What is new since the last look, added to a running campus. Never removes or changes what is
there; a thing seen before (and so deliberately taken off by someone) is not added again.
Returns (new config, [what was added, in words], [identities now seen])."""
cfg = copy.deepcopy(cfg)
have = referenced(cfg)
skip = have | set(seen)
nets = nets_of(inv) or [n for t in sp.all_tiles(cfg) for c in t.get("checks", []) if c.get("type") == "midi_wired" for n in c.get("nets", [])]
found, hosts = items(inv, nets)
added, now_seen = [], []
for alias, h in hosts.items():
if "host:" + alias in skip or "hostip:" + h["ssh"].split("@")[-1] in have:
continue
cfg["hosts"][alias] = h
added.append("computer %s" % h["name"])
now_seen.append("host:" + alias)
taken = {t["id"] for t in sp.all_tiles(cfg)}
for it in found:
# A key that already exists for this thing: only its new parts (a Dante device, a MIDI peer).
if it["ident"] in skip:
fresh = [x for x in it["also"] if x not in skip]
if fresh:
added += merge_parts(cfg, it, fresh)
now_seen += fresh
continue
if any(c.get("params", {}).get("host") not in cfg["hosts"] for c in it["checks"] if c.get("template")):
continue
tid = slug(it["label"], taken)
taken.add(tid)
for fid, f in it["fixes"].items():
cfg["fixes"].setdefault(fid, f)
place_new(cfg, {"id": tid, "label": key_text(it["label"]), "checks": it["checks"], "_group": it["group"]})
if it["restart"] and cfg.get("restart_page"):
restart_layout(cfg, it["restart"])
added.append("%s key" % key_text(it["label"]))
now_seen += [it["ident"]] + it["also"]
if added:
finish(cfg)
auto = cfg.setdefault("auto_fix", dict(DEFAULT_AUTO_FIX, fixes=[]))
auto["fixes"] = list(dict.fromkeys(list(auto.get("fixes") or []) + [f for f in gentle(cfg) if f not in auto.get("fixes", [])
and any(f in it["fixes"] for it in found)]))
return cfg, added, now_seen
def merge_parts(cfg, it, fresh):
"""New Dante devices and MIDI peers go into the key that already watches the rest."""
words = []
for t in sp.all_tiles(cfg):
for c in t.get("checks", []):
if c.get("type") == "dante" and it["ident"] == "conn:" + c.get("conn", ""):
for x in fresh:
if x.startswith("dante:"):
name = x[6:]
c.setdefault("devices", {})[name] = key_text(name, 12)
words.append("Dante device %s" % name)
if it["ident"] == "midi_wired":
midi = next((t for t in cfg["tiles"] if any(c.get("type") == "midi_wired" for c in t.get("checks", []))), None)
if midi is not None:
wired_at = next(i for i, c in enumerate(midi["checks"]) if c.get("type") == "midi_wired")
for c in it["checks"]:
if c["type"] == "midi_peer" and "midi:%s|%s|%s" % (c.get("host", ""), c["session"], c["peer"]) in fresh:
midi["checks"].insert(wired_at, c)
wired_at += 1
words.append("MIDI link %s" % c["peer"])
cfg["fixes"].setdefault("midi_reconnect", dict(SHARED_FIXES["midi_reconnect"]))
return words
def finish(cfg):
"""Fixes and remote checks built by their builders, words filled in, as the page would."""
for fid, f in list(cfg["fixes"].items()):
if f.get("template") and "run" not in f and b.TEMPLATES.get(f["template"], {}).get("render"):
built = b.build(cfg, f)
built.setdefault("label", b.suggest_label(cfg, built) or fid)
short = key_text(fid.replace("restart_", "").split("_")[0], 10)
built.setdefault("short", short)
cfg["fixes"][fid] = built
elif f.get("template") and not f.get("label"):
f["label"] = b.suggest_label(cfg, f) or fid
for t in cfg["tiles"]:
for i, c in enumerate(t.get("checks", [])):
if c.get("template"):
t["checks"][i] = b.build_check(cfg, c)
def gentle(cfg):
"""The fixes auto-fix may run by itself: starting what is closed and reconnecting - never a restart."""
out = []
for fid, f in cfg["fixes"].items():
if fid.startswith("restart_") or f.get("restart_ok") or f.get("never_auto"):
continue
if f.get("kind") in ("hyperdeck", "midi_connect", "reconnect") or (f.get("template") in ("mac_start", "win_start", "ableton")
and not (f.get("params") or {}).get("restart")):
out.append(fid)
return out