Smart outlets: the panel can power-cycle a device through a SmartLife (Tuya) plug
settings/outlet.py talks to Tuya's cloud with the campus's own project keys (kept in outlets.json beside the config, 0600, never in the repo): list, status, on/off, and 'cycle <outlet> --ping <address>' which leaves a unit that answers alone, cuts power 8 s, and waits for the address to answer; refused while a HyperDeck is recording. A builder 'Power-cycle a smart outlet' makes the fix from fields; the TASCAM key's ping check now holds to it (verified live: the hung DA-6400 answered again 26 s after the cycle and Companion reconnected). Settings > Outlets: keys, the outlet list with live state and on/off, + Outlet from the account's devices. Calendar in 12-hour time. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ import status_panel as sp
|
||||
from settings import apply as applying
|
||||
from settings import builders
|
||||
from settings import scripts
|
||||
from settings import outlet as outlets
|
||||
from settings import keyguide, probes
|
||||
|
||||
log = logging.getLogger("status-settings")
|
||||
@@ -114,6 +115,7 @@ class Site:
|
||||
self.page_cache = {} # name -> (time, value)
|
||||
self.draft = applying.Draft(data_dir)
|
||||
self.applier = applying.Applier(self)
|
||||
outlets.DATA_DIR, outlets.FILE = self.data_dir, os.path.join(self.data_dir, "outlets.json")
|
||||
|
||||
def load(self):
|
||||
st = os.stat(self.config_path)
|
||||
@@ -666,6 +668,18 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
return self.send(200, {"local": snap.get("midi"), "remote": snap.get("remote_midi")})
|
||||
if path == "/api/site/names":
|
||||
return self.send(200, self.names_check((site.draft.read() or {}).get("config") or site.load()))
|
||||
if path == "/api/outlets":
|
||||
d = outlets.load()
|
||||
t = d.get("tuya") or {}
|
||||
return self.send(200, {"configured": bool(t.get("access_id") and t.get("access_secret")), "access_id": t.get("access_id", ""), "region": t.get("region", "us"),
|
||||
"outlets": d.get("outlets") or {}})
|
||||
if path == "/api/outlets/status":
|
||||
name = urllib.parse.parse_qs(url.query).get("name", [""])[0]
|
||||
try:
|
||||
d = outlets.load(); o = outlets.outlet(d, name); st = outlets.client(d).status(o["id"])
|
||||
return self.send(200, {"ok": True, "on": any(v for c, v in st.items() if c.startswith("switch")), "status": st})
|
||||
except RuntimeError as e:
|
||||
return self.send(200, {"ok": False, "error": str(e)})
|
||||
if path == "/api/setup":
|
||||
return self.send(200, {"computers": scripts.setup_all(site.load())})
|
||||
if path == "/api/program/staged":
|
||||
@@ -844,6 +858,47 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
return self.send(200 if ok else 502, {"ok": ok, "sent": text})
|
||||
if path == "/api/atem-reference":
|
||||
return self.send(200, self.take_atem(raw))
|
||||
if path == "/api/outlets/keys":
|
||||
d = outlets.load()
|
||||
d["tuya"] = {"access_id": str(body.get("access_id", "")).strip(), "access_secret": str(body.get("access_secret", "")).strip() or (d.get("tuya") or {}).get("access_secret", ""),
|
||||
"region": str(body.get("region", "us")).strip() or "us"}
|
||||
try:
|
||||
n = len(outlets.client(d).devices())
|
||||
except RuntimeError as e:
|
||||
return self.send(200, {"ok": False, "error": str(e)})
|
||||
outlets.save(d)
|
||||
log.info("%s set the outlet keys (%d devices on the account)", who, n)
|
||||
return self.send(200, {"ok": True, "devices": n})
|
||||
if path == "/api/outlets/devices":
|
||||
try:
|
||||
devs = [{k: v for k, v in dv.items() if k != "local_key"} for dv in outlets.client().devices()]
|
||||
return self.send(200, {"ok": True, "devices": devs})
|
||||
except RuntimeError as e:
|
||||
return self.send(200, {"ok": False, "error": str(e)})
|
||||
if path == "/api/outlets":
|
||||
d = outlets.load()
|
||||
name = re.sub(r"[^a-z0-9_]+", "_", str(body.get("name", "")).strip().lower()).strip("_")[:40]
|
||||
if not name:
|
||||
return self.send(400, {"error": "a short name is needed (letters, digits, _)"})
|
||||
if body.get("remove"):
|
||||
d.setdefault("outlets", {}).pop(name, None)
|
||||
else:
|
||||
if not body.get("id"):
|
||||
return self.send(400, {"error": "pick a device"})
|
||||
d.setdefault("outlets", {})[name] = {"id": str(body["id"])[:64], "name": str(body.get("device_name", ""))[:80]}
|
||||
outlets.save(d)
|
||||
log.info("%s %s outlet %s", who, "removed" if body.get("remove") else "set", name)
|
||||
return self.send(200, {"ok": True, "outlets": d["outlets"]})
|
||||
if path == "/api/outlets/switch":
|
||||
live, reason = site.applier.live()
|
||||
if live:
|
||||
return self.send(409, {"error": "live now (%s): outlets are not switched during a service" % reason})
|
||||
try:
|
||||
d = outlets.load(); o = outlets.outlet(d, str(body.get("name", ""))); outlets.client(d).switch(o["id"], bool(body.get("on")))
|
||||
log.info("%s switched outlet %s %s", who, body.get("name"), "on" if body.get("on") else "off")
|
||||
return self.send(200, {"ok": True})
|
||||
except RuntimeError as e:
|
||||
return self.send(200, {"ok": False, "error": str(e)})
|
||||
if path == "/api/program/install":
|
||||
live, reason = site.applier.live()
|
||||
if live:
|
||||
|
||||
Reference in New Issue
Block a user