Settings page: ON/OFF switches everywhere, the Auto switch screen, a Companion button picker
- ON/OFF on every key, check, fix, Restart and READY key, computer, MIDI link, HyperDeck, Dante device, watched Companion connection, switcher and Spotify check, outlet fix, and one main switch each for auto-fix, the auto switch and the READY page. Switched off = kept in the settings, grey OFF on the deck, never checked or run (effective_config in the watchdog). - Settings › Auto switch: a plain summary, what counts as down (each trigger editable in place: address, program and computer, Dante device, the button it presses, timings), the hours folded away, the buttons; + Button, + What counts as down, Remove. - Pick a Companion button by clicking it on its page, drawn like Companion (read-only API: /api/companion/pages and /api/companion/page). - STREAM STB / STREAM FOH Restart keys removed (the Studio B page has its own); the alert names the switch-back button instead. - Dante screen: shows the DANTE key's device list again (it picked STB AUDIO's one device). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+79
-1
@@ -148,6 +148,69 @@ class Site:
|
||||
self.page_cache[name] = (now, value)
|
||||
return value
|
||||
|
||||
# --- Companion's own pages, for picking a button by looking at it -----------------
|
||||
def companion_pages(self):
|
||||
"""[{"page", "name"}] for every page Companion has (read-only), all asked at once."""
|
||||
def one(n):
|
||||
try:
|
||||
status, raw = self.companion_get("/api/variable/internal/page_number_%d_name/value" % n, timeout=3)
|
||||
except (OSError, urllib.error.URLError):
|
||||
return None
|
||||
if status != 200:
|
||||
return None
|
||||
name = " ".join(raw.decode("utf-8", "replace").replace("\\n", " ").split())
|
||||
return {"page": n, "name": re.sub(r"^%d\s+" % n, "", name) or name} # "4 Audio" -> "Audio"
|
||||
|
||||
def look():
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
with ThreadPoolExecutor(max_workers=16) as pool:
|
||||
found = list(pool.map(one, range(1, 100)))
|
||||
out = []
|
||||
for f in found: # up to the first page Companion does not have
|
||||
if f is None:
|
||||
break
|
||||
out.append(f)
|
||||
return out
|
||||
return self.cached("companion-pages", 60, look)
|
||||
|
||||
def companion_page(self, n):
|
||||
"""One Companion page as its buttons look: text, colours and image, by row and column (read-only)."""
|
||||
def look():
|
||||
status, raw = self.companion_get("/int/export/page/%d?includeSecrets=false" % n, timeout=6)
|
||||
try:
|
||||
raw = gzip.decompress(raw)
|
||||
except OSError:
|
||||
pass
|
||||
page = (json.loads(raw).get("page") or {})
|
||||
val = lambda p: p.get("value") if isinstance(p, dict) and not p.get("isExpression") else None
|
||||
buttons = []
|
||||
for r, row in (page.get("controls") or {}).items():
|
||||
for c, ctl in (row or {}).items():
|
||||
if not ctl:
|
||||
continue
|
||||
b = {"row": int(r), "col": int(c), "type": ctl.get("type")}
|
||||
st = ctl.get("style") or {}
|
||||
if ctl.get("type") == "button-layered":
|
||||
lay = {}
|
||||
for l in st.get("layers") or []:
|
||||
lay.setdefault(l.get("type"), l)
|
||||
text = (lay.get("text") or {}).get("text") or {}
|
||||
b["text"] = val(text) if val(text) is not None else ("…" if text.get("isExpression") else "")
|
||||
b["bg"] = val((lay.get("box") or {}).get("color") or {})
|
||||
b["fg"] = val((lay.get("text") or {}).get("color") or {})
|
||||
img = val((lay.get("image") or {}).get("base64Image") or {})
|
||||
if img and len(img) < 60000:
|
||||
b["img"] = img if img.startswith("data:") else "data:image/png;base64," + img
|
||||
elif ctl.get("type") == "button":
|
||||
b["text"], b["bg"], b["fg"] = st.get("text") or "", st.get("bgcolor"), st.get("color")
|
||||
if st.get("png64") and len(st["png64"]) < 60000:
|
||||
b["img"] = st["png64"] if st["png64"].startswith("data:") else "data:image/png;base64," + st["png64"]
|
||||
if isinstance(b.get("text"), str):
|
||||
b["text"] = b["text"].replace("\\n", "\n")
|
||||
buttons.append(b)
|
||||
return {"page": n, "name": (page.get("name") or "").replace("\\n", " "), "grid": page.get("gridSize"), "buttons": buttons}
|
||||
return self.cached("companion-page-%d" % n, 10, look)
|
||||
|
||||
# --- what the screens show ---------------------------------------------
|
||||
|
||||
def overview(self):
|
||||
@@ -286,7 +349,9 @@ class Site:
|
||||
for f in fs:
|
||||
if f["level"] == "err" and alias in owners.get((tid, f.get("check")), set()):
|
||||
faults.append("%s %s" % (labels.get(tid, tid), f["msg"]))
|
||||
if probe and not probe.get("reachable") and not h.get("self"):
|
||||
if h.get("disabled"):
|
||||
word, reason = "Off", "switched off in the settings: not looked at, its checks and fixes skipped"
|
||||
elif probe and not probe.get("reachable") and not h.get("self"):
|
||||
word, reason = "Problem", "cannot be reached (%s)" % probe.get("error", "no answer")
|
||||
elif faults:
|
||||
word, reason = "Problem", "; ".join(faults[:2])
|
||||
@@ -629,6 +694,19 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
return self.send(200, site.overview())
|
||||
if path == "/api/companion":
|
||||
return self.send(200, site.companion_view())
|
||||
if path == "/api/companion/pages":
|
||||
try:
|
||||
return self.send(200, {"pages": site.companion_pages()})
|
||||
except (OSError, ValueError) as e:
|
||||
return self.send(200, {"pages": [], "error": "Companion did not answer: %s" % e})
|
||||
if path == "/api/companion/page":
|
||||
n = (urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query).get("n") or ["0"])[0]
|
||||
if not n.isdigit() or not 1 <= int(n) <= 99:
|
||||
return self.send(400, {"error": "which page?"})
|
||||
try:
|
||||
return self.send(200, site.companion_page(int(n)))
|
||||
except (OSError, ValueError) as e:
|
||||
return self.send(200, {"page": int(n), "buttons": [], "error": "Companion did not answer: %s" % e})
|
||||
if path == "/api/machines":
|
||||
return self.send(200, site.machines_view())
|
||||
if path == "/api/gear":
|
||||
|
||||
Reference in New Issue
Block a user