@ecopoesis/homebridge-dmx 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -0
- package/ENCRYPTION.md +207 -0
- package/PROTOCOL.md +277 -0
- package/README.md +25 -2
- package/config.schema.json +53 -5
- package/dist/config.d.ts +5 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -2
- package/dist/config.js.map +1 -1
- package/dist/controller.d.ts +16 -4
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +41 -5
- package/dist/controller.js.map +1 -1
- package/dist/dmxController.d.ts +11 -0
- package/dist/dmxController.d.ts.map +1 -0
- package/dist/dmxController.js +4 -0
- package/dist/dmxController.js.map +1 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +5 -2
- package/dist/platform.js.map +1 -1
- package/dist/platformAccessory.d.ts +2 -2
- package/dist/platformAccessory.d.ts.map +1 -1
- package/dist/platformAccessory.js.map +1 -1
- package/dist/sacn.d.ts +29 -0
- package/dist/sacn.d.ts.map +1 -0
- package/dist/sacn.js +142 -0
- package/dist/sacn.js.map +1 -0
- package/dist/settings.d.ts +23 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +24 -0
- package/dist/settings.js.map +1 -1
- package/dist/zoneAccessory.d.ts +2 -2
- package/dist/zoneAccessory.d.ts.map +1 -1
- package/dist/zoneAccessory.js.map +1 -1
- package/examples/dmx.yaml +13 -4
- package/package.json +1 -1
- package/src/config.ts +18 -3
- package/src/controller.ts +41 -6
- package/src/dmxController.ts +14 -0
- package/src/platform.ts +8 -4
- package/src/platformAccessory.ts +2 -2
- package/src/sacn.ts +149 -0
- package/src/settings.ts +32 -0
- package/src/zoneAccessory.ts +2 -2
- package/tools/stick-power-cycle.py +121 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# stick-power-cycle — hard power-cycle the Nicolaudie Stick-DE3 by turning
|
|
3
|
+
# PoE off/on on its switch port (sw01 port 15, via PoE splitter).
|
|
4
|
+
#
|
|
5
|
+
# Why: the Stick can wedge into a state where TCP sessions and the crypto
|
|
6
|
+
# handshake succeed but live DMX output is blackout (seen 2026-08-22..24);
|
|
7
|
+
# only a real power removal recovers it. The UniFi UI "Power Cycle" bounce
|
|
8
|
+
# is too short — the splitter rides through it. This holds power off for
|
|
9
|
+
# OFF_SECONDS, then restores, then waits for the Stick to answer ping.
|
|
10
|
+
#
|
|
11
|
+
# Credentials: read at runtime from the homebridge config.json (same local
|
|
12
|
+
# UniFi user the AP-RGB plugin uses). Nothing secret is stored here.
|
|
13
|
+
#
|
|
14
|
+
# Scheduled via systemd user timer (host has no cron):
|
|
15
|
+
# ~/.config/systemd/user/stick-power-cycle.{service,timer} — daily 08:30 UTC
|
|
16
|
+
# (= 4:30am EDT; lights are always off then). Lingering enabled for miker so
|
|
17
|
+
# the timer fires without a login session. Logs: ~/stick-power-cycle.log
|
|
18
|
+
# Manual run: systemctl --user start stick-power-cycle.service
|
|
19
|
+
|
|
20
|
+
import json, socket, ssl, sys, time, urllib.request, http.cookiejar
|
|
21
|
+
|
|
22
|
+
CONTROLLER = 'https://192.168.1.1'
|
|
23
|
+
CONFIG_JSON = '/opt/containers/homebridge/config.json'
|
|
24
|
+
SWITCH_MAC = '94:2a:6f:94:f1:da' # sw01
|
|
25
|
+
PORT_IDX = 15 # Stick-DE3 PoE splitter, "dmx" VLAN 96
|
|
26
|
+
STICK_IP = '192.168.96.2'
|
|
27
|
+
OFF_SECONDS = 30
|
|
28
|
+
BOOT_WAIT_S = 180
|
|
29
|
+
|
|
30
|
+
def log(*a):
|
|
31
|
+
print(time.strftime('%Y-%m-%d %H:%M:%S%z'), *a, flush=True)
|
|
32
|
+
|
|
33
|
+
def load_creds():
|
|
34
|
+
cfg = json.load(open(CONFIG_JSON))
|
|
35
|
+
p = next(x for x in cfg['platforms'] if x.get('platform') == 'UnifiAPLight')
|
|
36
|
+
return p['username'], p['password']
|
|
37
|
+
|
|
38
|
+
class Unifi:
|
|
39
|
+
def __init__(self):
|
|
40
|
+
ctx = ssl.create_default_context()
|
|
41
|
+
ctx.check_hostname = False
|
|
42
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
43
|
+
self.op = urllib.request.build_opener(
|
|
44
|
+
urllib.request.HTTPSHandler(context=ctx),
|
|
45
|
+
urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
|
|
46
|
+
self.csrf = ''
|
|
47
|
+
|
|
48
|
+
def req(self, method, path, body=None):
|
|
49
|
+
rq = urllib.request.Request(CONTROLLER + path, method=method,
|
|
50
|
+
data=json.dumps(body).encode() if body is not None else None,
|
|
51
|
+
headers={'Content-Type': 'application/json'})
|
|
52
|
+
if self.csrf:
|
|
53
|
+
rq.add_header('x-csrf-token', self.csrf)
|
|
54
|
+
r = self.op.open(rq, timeout=15)
|
|
55
|
+
self.csrf = r.headers.get('x-csrf-token', self.csrf)
|
|
56
|
+
return json.load(r)
|
|
57
|
+
|
|
58
|
+
def login(self, user, pw):
|
|
59
|
+
self.req('POST', '/api/auth/login', {'username': user, 'password': pw})
|
|
60
|
+
|
|
61
|
+
def get_switch(self):
|
|
62
|
+
data = self.req('GET', '/proxy/network/api/s/default/stat/device')['data']
|
|
63
|
+
return next(d for d in data if d.get('mac') == SWITCH_MAC)
|
|
64
|
+
|
|
65
|
+
def set_poe(self, poe_mode):
|
|
66
|
+
# port_overrides is replaced wholesale on PUT — merge, never clobber.
|
|
67
|
+
sw = self.get_switch()
|
|
68
|
+
overrides = sw.get('port_overrides', [])
|
|
69
|
+
entry = next((o for o in overrides if o.get('port_idx') == PORT_IDX), None)
|
|
70
|
+
if entry is None:
|
|
71
|
+
entry = {'port_idx': PORT_IDX}
|
|
72
|
+
overrides.append(entry)
|
|
73
|
+
entry['poe_mode'] = poe_mode
|
|
74
|
+
self.req('PUT', f"/proxy/network/api/s/default/rest/device/{sw['_id']}",
|
|
75
|
+
{'port_overrides': overrides})
|
|
76
|
+
log(f'port {PORT_IDX} poe_mode -> {poe_mode}')
|
|
77
|
+
|
|
78
|
+
def stick_up():
|
|
79
|
+
# TCP probe of the Stick's own control port — proves the device booted,
|
|
80
|
+
# not just that the link is up. (The host has no ping binary anyway.)
|
|
81
|
+
try:
|
|
82
|
+
socket.create_connection((STICK_IP, 2431), timeout=2).close()
|
|
83
|
+
return True
|
|
84
|
+
except OSError:
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
def main():
|
|
88
|
+
log(f'power-cycling Stick-DE3 (sw {SWITCH_MAC} port {PORT_IDX}, {OFF_SECONDS}s off)')
|
|
89
|
+
api = Unifi()
|
|
90
|
+
api.login(*load_creds())
|
|
91
|
+
api.set_poe('off')
|
|
92
|
+
try:
|
|
93
|
+
time.sleep(OFF_SECONDS)
|
|
94
|
+
finally:
|
|
95
|
+
# restore is sacred: retry hard so the port can never stay dark
|
|
96
|
+
for attempt in range(5):
|
|
97
|
+
try:
|
|
98
|
+
api.set_poe('auto')
|
|
99
|
+
break
|
|
100
|
+
except Exception as e:
|
|
101
|
+
log(f'restore attempt {attempt + 1} failed: {e}')
|
|
102
|
+
time.sleep(5)
|
|
103
|
+
try:
|
|
104
|
+
api.login(*load_creds())
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
else:
|
|
108
|
+
log('FATAL: could not restore PoE — port may be off!')
|
|
109
|
+
sys.exit(2)
|
|
110
|
+
|
|
111
|
+
deadline = time.time() + BOOT_WAIT_S
|
|
112
|
+
while time.time() < deadline:
|
|
113
|
+
if stick_up():
|
|
114
|
+
log(f'Stick back up ({STICK_IP}:2431 accepting connections)')
|
|
115
|
+
return
|
|
116
|
+
time.sleep(5)
|
|
117
|
+
log(f'WARNING: Stick not reachable within {BOOT_WAIT_S}s of power restore')
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
|
|
120
|
+
if __name__ == '__main__':
|
|
121
|
+
main()
|