150 lines
7.4 KiB
Python
150 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
"""A status panel a campus installs and runs on its own: no North Attleboro.
|
||
|
|
|
||
|
|
python3 tools/standalone.py ab "Apollo Beach" -> dist/Apollo Beach Status Panel.zip
|
||
|
|
|
||
|
|
Apollo Beach, 2026-10-21 (the user): "they want to take care of it on their own, so make a sorta
|
||
|
|
standalone version that i can send them. i dont need to monitor AB from NA like i do for SC and WS".
|
||
|
|
The zip holds the program (the same files a campus downloads from NA, never NA's settings, plug keys
|
||
|
|
or login keys), an installer and a read-me. Installed, the campus sets itself up from what it finds,
|
||
|
|
exactly as Southcoast and Woonsocket did, but it reports to nobody (campuses.json "standalone"), lets
|
||
|
|
no other Mac in, and backs itself up every night into ~/Documents/Status Panel Backups.
|
||
|
|
"""
|
||
|
|
import os
|
||
|
|
import re
|
||
|
|
import sys
|
||
|
|
import zipfile
|
||
|
|
|
||
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
|
|
sys.path.insert(0, ROOT)
|
||
|
|
from settings.campus import KIT_FILES, KIT_DIRS # noqa: E402
|
||
|
|
|
||
|
|
INSTALL = r"""#!/bin/zsh
|
||
|
|
# Status panel for %(name)s: installs on the Mac that runs Companion and sets itself up from what it finds.
|
||
|
|
# Safe to run again: it then updates the program and looks for new gear.
|
||
|
|
set -e
|
||
|
|
HERE=${0:A:h}
|
||
|
|
if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then
|
||
|
|
echo "First the Command Line Tools: a window opens - click Install, wait for it, then run this again."
|
||
|
|
/usr/bin/xcode-select --install 2>/dev/null || true
|
||
|
|
exit 1
|
||
|
|
fi
|
||
|
|
DIR="$HOME/status-panel-install"
|
||
|
|
rm -rf "$DIR" && mkdir -p "$DIR"
|
||
|
|
/usr/bin/ditto "$HERE/program" "$DIR"
|
||
|
|
/usr/bin/xattr -dr com.apple.quarantine "$DIR" 2>/dev/null || true
|
||
|
|
[[ -f "$HOME/.ssh/id_status_panel" ]] || { mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh" && /usr/bin/ssh-keygen -q -t ed25519 -N "" -C "status-panel@$(scutil --get LocalHostName)" -f "$HOME/.ssh/id_status_panel"; }
|
||
|
|
cd "$DIR"
|
||
|
|
zsh install.sh --site %(cid)s
|
||
|
|
cd "$HOME/Library/Application Support/status-panel"
|
||
|
|
/usr/bin/python3 -m settings.campus setup --id %(cid)s --name %(qname)s --standalone
|
||
|
|
# This Mac's wired address (the default route's port).
|
||
|
|
IF=$(/sbin/route -n get default 2>/dev/null | awk '/interface:/{print $2}')
|
||
|
|
ME=$(/usr/sbin/ipconfig getifaddr "$IF" 2>/dev/null || hostname)
|
||
|
|
echo
|
||
|
|
echo "Settings page: http://$ME:8020 (the editing code is printed above)"
|
||
|
|
echo
|
||
|
|
echo "Next, on each other computer here (ProPresenter, Tracks, Lighting, FOH...):"
|
||
|
|
echo " Mac: open Terminal and paste curl -fsSL http://$ME:8020/join | sh"
|
||
|
|
echo " (or send it to all of them at once in Apple Remote Desktop: Send UNIX Command, as root)"
|
||
|
|
echo " PC: open PowerShell as administrator and paste irm http://$ME:8020/join.ps1 | iex"
|
||
|
|
echo "Then on the settings page: Auto-setup > Look now."
|
||
|
|
"""
|
||
|
|
|
||
|
|
README = """%(name)s - Status Panel
|
||
|
|
%(rule)s
|
||
|
|
|
||
|
|
What it is
|
||
|
|
A status page on the Stream Deck (Bitfocus Companion) that watches your gear - ProPresenter, the
|
||
|
|
ATEM, Resi, consoles, Tracks, Lighting, MIDI links, the network - and fixes what it can. It runs on
|
||
|
|
the Mac that runs Companion. It is yours: nothing is sent to any other campus.
|
||
|
|
|
||
|
|
Before you start
|
||
|
|
- Companion 5.0.7 on that Mac, with its HTTP API on (Companion > Settings > Protocols).
|
||
|
|
If yours is older, the installer says so and how to upgrade (download 5.0.7 for Mac from
|
||
|
|
https://bitfocus.io/companion; your buttons come along).
|
||
|
|
- Remote Login on for the other Macs (System Settings > General > Sharing), so the panel can check
|
||
|
|
and restart programs on them.
|
||
|
|
|
||
|
|
Install (about 5 minutes)
|
||
|
|
1. Copy this folder to the Companion Mac (Downloads is fine).
|
||
|
|
2. Open Terminal, type zsh and a space, drag "install.command" from this folder into the
|
||
|
|
window, press Return.
|
||
|
|
3. It finds what you have, makes the Stream Deck pages (19 status, 20 restart, or the first free
|
||
|
|
pages), and prints the settings page address and its editing code (PIN). Keep the PIN.
|
||
|
|
4. On each other computer, paste the line it prints (Mac: Terminal; PC: PowerShell as
|
||
|
|
administrator). Then on the settings page: Auto-setup > Look now. New gear gets keys by itself.
|
||
|
|
|
||
|
|
Using it (the Stream Deck)
|
||
|
|
- Green means fine. Flashing red names the problem, for example "PROP OFFLINE".
|
||
|
|
- Press a red key to acknowledge it: it stops flashing and stays dark red.
|
||
|
|
- Hold any key for 2 seconds to fix it: it reconnects, restarts or reopens whatever is failing.
|
||
|
|
- Purple means the panel itself is not running: restart the Companion Mac, or run the installer again.
|
||
|
|
- ALARMS (bottom-left) silences alarms and stops automatic fixes until midnight. Use it during
|
||
|
|
maintenance.
|
||
|
|
- The page-number key on every page turns red when something is wrong. Hold it for 1 second to
|
||
|
|
jump to the status page.
|
||
|
|
- RESTART PAGE opens the restart page. Hold a key there for 2 seconds to restart that program even
|
||
|
|
if it looks fine.
|
||
|
|
- Automatic fixes run Tuesday 2-10 pm, Wednesday 9 am-9 pm and Sunday 5 am-2 pm. Change the hours
|
||
|
|
on the settings page (Auto-fix) to match your services.
|
||
|
|
- Nothing is ever restarted while the HyperDeck is recording.
|
||
|
|
|
||
|
|
The settings page (http://<Companion Mac>:8020)
|
||
|
|
Everything is changed there: keys, checks, fixes, auto-fix hours, the Restart page. Every Apply is
|
||
|
|
kept, and History puts back any earlier one. Changing gear? Auto-setup > Look now adds what is new.
|
||
|
|
|
||
|
|
Backups
|
||
|
|
Every night at 4:30 am the settings are copied to ~/Documents/Status Panel Backups/%(cid)s on the
|
||
|
|
Companion Mac (the newest 30 are kept). Copy that folder somewhere safe now and then, or point it at
|
||
|
|
a Google Drive folder (ask whoever installed it).
|
||
|
|
|
||
|
|
Run it again
|
||
|
|
Running install.command again updates the program and looks for new gear; your settings stay.
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
def build(cid, name, out_dir):
|
||
|
|
cid = re.match(r"[a-z0-9]{1,8}", cid.lower()).group(0)
|
||
|
|
top = "%s Status Panel" % name
|
||
|
|
os.makedirs(out_dir, exist_ok=True)
|
||
|
|
path = os.path.join(out_dir, top + ".zip")
|
||
|
|
|
||
|
|
def add_file(z, src, arc, mode=0o644):
|
||
|
|
info = zipfile.ZipInfo.from_file(src, arc)
|
||
|
|
info.external_attr = (0o100000 | mode) << 16
|
||
|
|
info.compress_type = zipfile.ZIP_DEFLATED
|
||
|
|
with open(src, "rb") as f:
|
||
|
|
z.writestr(info, f.read())
|
||
|
|
|
||
|
|
def add_text(z, text, arc, mode=0o644):
|
||
|
|
info = zipfile.ZipInfo(arc, date_time=(2026, 10, 21, 0, 0, 0))
|
||
|
|
info.external_attr = (0o100000 | mode) << 16
|
||
|
|
info.compress_type = zipfile.ZIP_DEFLATED
|
||
|
|
z.writestr(info, text)
|
||
|
|
|
||
|
|
with zipfile.ZipFile(path, "w") as z:
|
||
|
|
prog = top + "/program/"
|
||
|
|
for f in KIT_FILES + ("install.sh", "README.md"):
|
||
|
|
p = os.path.join(ROOT, f)
|
||
|
|
if os.path.exists(p):
|
||
|
|
add_file(z, p, prog + f, 0o755 if f.endswith(".sh") else 0o644)
|
||
|
|
for d in KIT_DIRS + ("sites/_template",):
|
||
|
|
for base, dirs, files in os.walk(os.path.join(ROOT, d)):
|
||
|
|
dirs[:] = [x for x in dirs if x != "__pycache__"]
|
||
|
|
for f in files:
|
||
|
|
if f.endswith(".pyc") or f.startswith("._") or f == ".DS_Store":
|
||
|
|
continue
|
||
|
|
p = os.path.join(base, f)
|
||
|
|
add_file(z, p, prog + os.path.relpath(p, ROOT), 0o755 if f.endswith(".sh") else 0o644)
|
||
|
|
q = "'" + name.replace("'", "'\"'\"'") + "'"
|
||
|
|
add_text(z, INSTALL % {"name": name, "cid": cid, "qname": q}, top + "/install.command", 0o755)
|
||
|
|
add_text(z, README % {"name": name, "cid": cid, "rule": "=" * (len(name) + 15)}, top + "/READ ME FIRST.txt")
|
||
|
|
return path
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
if len(sys.argv) < 3:
|
||
|
|
sys.exit(__doc__)
|
||
|
|
print(build(sys.argv[1], sys.argv[2], os.path.join(ROOT, "dist")))
|