NET blip flashes, clears itself after 5 minutes, hold NET clears it; Restart page is now page 20
- The remembered network blip is a flashing amber warning (new "notice" level, not counted as DOWN): press NET to stop the flashing, hold NET 2 s to clear it, otherwise it goes by itself after 5 minutes (was 3, solid). - The user moved the Restart page to page 20: config, the RESTART PAGE key on page 19 and both pages re-pushed. A scan of all pages finds no other button pointing at the old page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -66,7 +66,7 @@ Row 0 holds the group buttons. Each one shows the first problem in its group, pl
|
|||||||
It checks over SSH every 10 s that ProPresenter, Ableton and Church Translate are running, using NA Ingest's key `~/.ssh/id_status_panel`. That key was authorized on the Macs with `remote-access/mac-authorize.sh`.
|
It checks over SSH every 10 s that ProPresenter, Ableton and Church Translate are running, using NA Ingest's key `~/.ssh/id_status_panel`. That key was authorized on the Macs with `remote-access/mac-authorize.sh`.
|
||||||
- **ALARMS:** alarms are armed all the time (`service_windows` in `config.json`: every day, 00:00–23:59). Press ALARMS to mute the blinking, for example during maintenance. It re-arms itself at midnight.
|
- **ALARMS:** alarms are armed all the time (`service_windows` in `config.json`: every day, 00:00–23:59). Press ALARMS to mute the blinking, for example during maintenance. It re-arms itself at midnight.
|
||||||
|
|
||||||
## Restart page (page 23)
|
## Restart page (page 20)
|
||||||
|
|
||||||
Tap **RESTART PAGE** on the status page (row 2). Each key restarts one thing **even when the status page says it is fine**; hold it 2 s. It shows RESTARTING, then DONE (or FAILED / what to do). The same thing will not run twice within a minute. **STATUS PAGE** (bottom left) goes back.
|
Tap **RESTART PAGE** on the status page (row 2). Each key restarts one thing **even when the status page says it is fine**; hold it 2 s. It shows RESTARTING, then DONE (or FAILED / what to do). The same thing will not run twice within a minute. **STATUS PAGE** (bottom left) goes back.
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ The **page number key** on every page follows the ALL button. It's green when ev
|
|||||||
| APPS | ProPresenter API (offline or error only: its harmless yellow warning, every 15–20 min, is ignored with `"ignore_warning": true`); Ableton OSC; tracks Mac pings; Bible TV app answering on :8080 (every 5 s); Bible TV OSC. Messages name the app ("PROPRES OFFLINE", "BIBLE APP DOWN"). Hold 2 s restarts the ProPresenter, Ableton and Bible TV Companion connections. |
|
| APPS | ProPresenter API (offline or error only: its harmless yellow warning, every 15–20 min, is ignored with `"ignore_warning": true`); Ableton OSC; tracks Mac pings; Bible TV app answering on :8080 (every 5 s); Bible TV OSC. Messages name the app ("PROPRES OFFLINE", "BIBLE APP DOWN"). Hold 2 s restarts the ProPresenter, Ableton and Bible TV Companion connections. |
|
||||||
| VISTA | Vista on the Lighting PC: its Art-Net poll (sent every 0.5 s) must be heard within 5 s, otherwise **SILENT** (Vista closed or crashed, or the Lighting PC is off the network). Hold 2 s starts Vista if it is not running; Vista then opens **Prop Takeover** from its show folder (`E:\media\Documents\Vista 3\Shows`) by itself, through its own autoload preference (`<autoloadpath>` in `E:\media\Documents\Vista 3\.preferences`, set 2026-10-08). Resolume is the ARENA button. |
|
| VISTA | Vista on the Lighting PC: its Art-Net poll (sent every 0.5 s) must be heard within 5 s, otherwise **SILENT** (Vista closed or crashed, or the Lighting PC is off the network). Hold 2 s starts Vista if it is not running; Vista then opens **Prop Takeover** from its show folder (`E:\media\Documents\Vista 3\Shows`) by itself, through its own autoload preference (`<autoloadpath>` in `E:\media\Documents\Vista 3\.preferences`, set 2026-10-08). Resolume is the ARENA button. |
|
||||||
| RESI | decoder (RossTalk connection and ping, 10.1.10.203); **encoder** (ping and its web page, 10.1.10.202, LAOEncoder); resi-studio-cues; Resi cloud reachable. Only shows they are on the network, not that the encoder is streaming. |
|
| RESI | decoder (RossTalk connection and ping, 10.1.10.203); **encoder** (ping and its web page, 10.1.10.202, LAOEncoder); resi-studio-cues; Resi cloud reachable. Only shows they are on the network, not that the encoder is streaming. |
|
||||||
| NET | NA Ingest's link; gateway 10.1.0.1 pings; **4+ connections from 3+ groups dropping within 10 s** (what happened Sunday 10/5 at 10:21) |
|
| NET | NA Ingest's link; gateway 10.1.0.1 pings; **4+ connections from 3+ groups dropping within 10 s** (what happened Sunday 10/5 at 10:21). A blip **flashes amber** for 5 minutes, then goes by itself; press NET to stop the flashing, hold NET 2 s to clear it now. |
|
||||||
| INGEST | the bridge (makes ProPresenter's stage screen count down to the next Resi cue), lobby captions server and caption screen, **ProdCom** (transcription, answering on :24480), ProdDash (running and its page on :24500); free disk |
|
| INGEST | the bridge (makes ProPresenter's stage screen count down to the next Resi cue), lobby captions server and caption screen, **ProdCom** (transcription, answering on :24480), ProdDash (running and its page on :24500); free disk |
|
||||||
| RF (group) | Shure connections; battery ≤1 bar red, 2 bars amber; RF interference (only while that transmitter is on); PSM RF muted; each receiver pinged every 3 s ("AXIENT 1 NO PING" in about 6–9 s; Companion's Shure modules take 18–27 s to notice). The address is the one set on its Companion connection, read from Companion's settings every minute, so changing it there is enough; `"host"` in `config.json` is only the fallback. |
|
| RF (group) | Shure connections; battery ≤1 bar red, 2 bars amber; RF interference (only while that transmitter is on); PSM RF muted; each receiver pinged every 3 s ("AXIENT 1 NO PING" in about 6–9 s; Companion's Shure modules take 18–27 s to notice). The address is the one set on its Companion connection, read from Companion's settings every minute, so changing it there is enough; `"host"` in `config.json` is only the fallback. |
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -20,7 +20,7 @@
|
|||||||
"burst_connections": 4,
|
"burst_connections": 4,
|
||||||
"burst_groups": 3,
|
"burst_groups": 3,
|
||||||
"burst_window_seconds": 10,
|
"burst_window_seconds": 10,
|
||||||
"remember_seconds": 180
|
"remember_seconds": 300
|
||||||
},
|
},
|
||||||
|
|
||||||
"alert_webhook": "",
|
"alert_webhook": "",
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"restart_page": {
|
"restart_page": {
|
||||||
"page": 23, "page_name": "23\\nRestart", "jump_pos": [2, 6], "back_pos": [3, 0],
|
"page": 20, "page_name": "20\\nRestart", "jump_pos": [2, 6], "back_pos": [3, 0],
|
||||||
"keys": [
|
"keys": [
|
||||||
{ "id": "rs_dvs_prop", "label": "DVS PROP", "pos": [0, 1], "fix": "dvs_prop" },
|
{ "id": "rs_dvs_prop", "label": "DVS PROP", "pos": [0, 1], "fix": "dvs_prop" },
|
||||||
{ "id": "rs_dvs_tracks", "label": "DVS TRACKS", "pos": [0, 2], "fix": "dvs_tracks" },
|
{ "id": "rs_dvs_tracks", "label": "DVS TRACKS", "pos": [0, 2], "fix": "dvs_tracks" },
|
||||||
|
|||||||
+4
-2
@@ -166,8 +166,10 @@ def button(tid, tile, cfg):
|
|||||||
press = "ack " + tid
|
press = "ack " + tid
|
||||||
fixable = "members" in tile or any((c["type"] in RESTARTABLE and c.get("conn")) or c.get("fix")
|
fixable = "members" in tile or any((c["type"] in RESTARTABLE and c.get("conn")) or c.get("fix")
|
||||||
for c in tile.get("checks", []))
|
for c in tile.get("checks", []))
|
||||||
hold = ("restart " + tid) if fixable or tile.get("hold_always") else None
|
hold = ("restart " + tid) if fixable or tile.get("hold_always") or kind == "network" else None
|
||||||
if tile.get("hold_always"):
|
if kind == "network":
|
||||||
|
note = "Press: stop the flashing. Hold 2 s: clear a network blip (it also clears itself after 5 minutes)."
|
||||||
|
elif tile.get("hold_always"):
|
||||||
note = "Press: acknowledge. Hold 2 s: %s." % cfg["fixes"][tile["hold_always"]]["label"]
|
note = "Press: acknowledge. Hold 2 s: %s." % cfg["fixes"][tile["hold_always"]]["label"]
|
||||||
else:
|
else:
|
||||||
note = "Press: acknowledge." + (" Hold 2 s: fix what this button is failing on (nothing if it is green)."
|
note = "Press: acknowledge." + (" Hold 2 s: fix what this button is failing on (nothing if it is green)."
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Import the status page (19) or the Restart page (23) into Companion without clicking through the UI.
|
"""Import the status page (19) or the Restart page (20; both numbers come from config.json) into Companion without clicking through the UI.
|
||||||
|
|
||||||
Does exactly what Import/Export -> Import -> "import into page N" does, over the
|
Does exactly what Import/Export -> Import -> "import into page N" does, over the
|
||||||
same internal API the Companion web UI uses (tRPC over a WebSocket at /trpc):
|
same internal API the Companion web UI uses (tRPC over a WebSocket at /trpc):
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+21
-8
@@ -40,13 +40,14 @@ OK, WARN, ERR, OFF = "ok", "warn", "err", "off"
|
|||||||
|
|
||||||
# Display levels, least to most urgent. "idle" is a service-only fault while
|
# Display levels, least to most urgent. "idle" is a service-only fault while
|
||||||
# service mode is off (OBS PC off midweek); "quiet" is any other fault then.
|
# service mode is off (OBS PC off midweek); "quiet" is any other fault then.
|
||||||
ORDER = ["off", "ok", "idle", "warn", "quiet", "ack", "err"]
|
ORDER = ["off", "ok", "idle", "warn", "notice", "quiet", "ack", "err"]
|
||||||
RANK = {name: i for i, name in enumerate(ORDER)}
|
RANK = {name: i for i, name in enumerate(ORDER)}
|
||||||
|
|
||||||
# (background, text) for the flash-on phase and the flash-off phase.
|
# (background, text) for the flash-on phase and the flash-off phase.
|
||||||
COLORS = {
|
COLORS = {
|
||||||
"ok": ((0x007A2E, 0xFFFFFF), (0x007A2E, 0xFFFFFF)),
|
"ok": ((0x007A2E, 0xFFFFFF), (0x007A2E, 0xFFFFFF)),
|
||||||
"warn": ((0xFFA000, 0x000000), (0xFFA000, 0x000000)),
|
"warn": ((0xFFA000, 0x000000), (0xFFA000, 0x000000)),
|
||||||
|
"notice": ((0xFFA000, 0x000000), (0x000000, 0xFFA000)), # flashing amber: something happened (NET BLIP)
|
||||||
"quiet": ((0x3A0000, 0xFF8080), (0x3A0000, 0xFF8080)),
|
"quiet": ((0x3A0000, 0xFF8080), (0x3A0000, 0xFF8080)),
|
||||||
"ack": ((0x8B0000, 0xFFFFFF), (0x8B0000, 0xFFFFFF)),
|
"ack": ((0x8B0000, 0xFFFFFF), (0x8B0000, 0xFFFFFF)),
|
||||||
"err": ((0xFF0000, 0xFFFFFF), (0x000000, 0xFF3030)),
|
"err": ((0xFF0000, 0xFFFFFF), (0x000000, 0xFF3030)),
|
||||||
@@ -60,14 +61,15 @@ COLORS = {
|
|||||||
|
|
||||||
class Result:
|
class Result:
|
||||||
"""One check's verdict. `named` means msg already says which device it is."""
|
"""One check's verdict. `named` means msg already says which device it is."""
|
||||||
__slots__ = ("level", "msg", "info", "service_only", "named")
|
__slots__ = ("level", "msg", "info", "service_only", "named", "flash")
|
||||||
|
|
||||||
def __init__(self, level, msg="", info="", service_only=False, named=False):
|
def __init__(self, level, msg="", info="", service_only=False, named=False, flash=False):
|
||||||
self.level = level
|
self.level = level
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.info = info
|
self.info = info
|
||||||
self.service_only = service_only
|
self.service_only = service_only
|
||||||
self.named = named
|
self.named = named
|
||||||
|
self.flash = flash # a warning that flashes until acknowledged (NET BLIP)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "Result(%s, %r, info=%r)" % (self.level, self.msg, self.info)
|
return "Result(%s, %r, info=%r)" % (self.level, self.msg, self.info)
|
||||||
@@ -715,9 +717,10 @@ class Engine:
|
|||||||
else:
|
else:
|
||||||
self.burst["cleared"] = now
|
self.burst["cleared"] = now
|
||||||
if self.burst["cleared"] is not None:
|
if self.burst["cleared"] is not None:
|
||||||
if now - self.burst["cleared"] < self.net.get("remember_seconds", 180):
|
if now - self.burst["cleared"] < self.net.get("remember_seconds", 300):
|
||||||
stamp = datetime.fromtimestamp(self.burst["start"]).strftime("%H:%M")
|
stamp = datetime.fromtimestamp(self.burst["start"]).strftime("%H:%M")
|
||||||
out.append(("burst", Result(WARN, "BLIP " + stamp)))
|
# Flashes (to be noticed) until acknowledged, then goes by itself; a hold clears it.
|
||||||
|
out.append(("burst", Result(WARN, "BLIP " + stamp, flash=True)))
|
||||||
else:
|
else:
|
||||||
self.burst = None
|
self.burst = None
|
||||||
if not any(sub == "burst" for sub, _ in out):
|
if not any(sub == "burst" for sub, _ in out):
|
||||||
@@ -873,6 +876,16 @@ class Engine:
|
|||||||
self.notes[tile_id] = {"text": text, "level": level, "until": now + seconds}
|
self.notes[tile_id] = {"text": text, "level": level, "until": now + seconds}
|
||||||
|
|
||||||
def run_fixes(self, tile_id, snap, comp, now, cooldown=60):
|
def run_fixes(self, tile_id, snap, comp, now, cooldown=60):
|
||||||
|
if self.tiles.get(tile_id, {}).get("kind") == "network":
|
||||||
|
# NET: a hold clears a remembered network blip (link and gateway clear themselves).
|
||||||
|
if self.burst is None:
|
||||||
|
self.note(tile_id, "NOTHING\nTO CLEAR", "idle", now, 4)
|
||||||
|
else:
|
||||||
|
log.info("HOLD net: network blip cleared by hand")
|
||||||
|
self.burst = None
|
||||||
|
self.conn_drops.clear()
|
||||||
|
self.note(tile_id, "CLEARED", "ok", now, 3)
|
||||||
|
return []
|
||||||
plan = self.fix_plan(tile_id, snap)
|
plan = self.fix_plan(tile_id, snap)
|
||||||
always = self.tiles.get(tile_id, {}).get("hold_always")
|
always = self.tiles.get(tile_id, {}).get("hold_always")
|
||||||
if not plan and always:
|
if not plan and always:
|
||||||
@@ -988,7 +1001,7 @@ def display_level(res, acked, service_on):
|
|||||||
if res.level == OK:
|
if res.level == OK:
|
||||||
return "ok"
|
return "ok"
|
||||||
if res.level == WARN:
|
if res.level == WARN:
|
||||||
return "warn"
|
return "notice" if res.flash and not acked else "warn"
|
||||||
if res.service_only and not service_on:
|
if res.service_only and not service_on:
|
||||||
return "idle"
|
return "idle"
|
||||||
if not service_on:
|
if not service_on:
|
||||||
@@ -1003,7 +1016,7 @@ def worst(items):
|
|||||||
level = max((lv for lv, _, _ in items), key=lambda lv: RANK[lv])
|
level = max((lv for lv, _, _ in items), key=lambda lv: RANK[lv])
|
||||||
if all(lv == "off" for lv, _, _ in items):
|
if all(lv == "off" for lv, _, _ in items):
|
||||||
level = "off"
|
level = "off"
|
||||||
faults = [it for it in items if it[0] in ("idle", "warn", "quiet", "ack", "err")]
|
faults = [it for it in items if it[0] in ("idle", "warn", "notice", "quiet", "ack", "err")]
|
||||||
faults.sort(key=lambda it: (-RANK[it[0]], it[2]))
|
faults.sort(key=lambda it: (-RANK[it[0]], it[2]))
|
||||||
return level, faults
|
return level, faults
|
||||||
|
|
||||||
@@ -1075,7 +1088,7 @@ def render(engine, service_on):
|
|||||||
text, level = "ALL OK", "ok"
|
text, level = "ALL OK", "ok"
|
||||||
else:
|
else:
|
||||||
first = next(fault_text(t, r) for lv, r, s, t in items if (lv, r, s) == faults[0])
|
first = next(fault_text(t, r) for lv, r, s, t in items if (lv, r, s) == faults[0])
|
||||||
n_bad = sum(1 for f in faults if f[0] != "warn")
|
n_bad = sum(1 for f in faults if f[0] not in ("warn", "notice"))
|
||||||
text = ("%d DOWN" % n_bad if n_bad else "%d WARN" % len(faults)) + "\n" + first
|
text = ("%d DOWN" % n_bad if n_bad else "%d WARN" % len(faults)) + "\n" + first
|
||||||
out[tid] = colored(text, level)
|
out[tid] = colored(text, level)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ def run_steps(engine, snap, start, seconds, step=1.0):
|
|||||||
|
|
||||||
|
|
||||||
def level(tile):
|
def level(tile):
|
||||||
for name, ((b, f), _) in sp.COLORS.items():
|
for name, ((b, f), (b2, f2)) in sp.COLORS.items():
|
||||||
if (tile["b"], tile["f"]) == (b, f):
|
if (tile["b"], tile["f"], tile["b2"], tile["f2"]) == (b, f, b2, f2):
|
||||||
return name
|
return name
|
||||||
return "?"
|
return "?"
|
||||||
|
|
||||||
@@ -513,9 +513,32 @@ class TestBehaviour(unittest.TestCase):
|
|||||||
self.snap.conns[label]["category"] = "good"
|
self.snap.conns[label]["category"] = "good"
|
||||||
tiles, t = run_steps(engine, self.snap, t, 1)
|
tiles, t = run_steps(engine, self.snap, t, 1)
|
||||||
self.assertTrue(tiles["net"]["x"].startswith("NET\nBLIP "))
|
self.assertTrue(tiles["net"]["x"].startswith("NET\nBLIP "))
|
||||||
self.assertEqual(level(tiles["net"]), "warn")
|
self.assertEqual(level(tiles["net"]), "notice") # flashes amber
|
||||||
|
self.assertTrue(tiles["all"]["x"].startswith("1 WARN\nNET BLIP ")) # a warning, not a DOWN
|
||||||
tiles, _ = run_steps(engine, self.snap, t + 200, 1)
|
tiles, _ = run_steps(engine, self.snap, t + 200, 1)
|
||||||
self.assertEqual(tiles["net"]["x"], "NET\nOK")
|
self.assertTrue(tiles["net"]["x"].startswith("NET\nBLIP ")) # still up after 3+ minutes
|
||||||
|
tiles, _ = run_steps(engine, self.snap, t + 320, 1)
|
||||||
|
self.assertEqual(tiles["net"]["x"], "NET\nOK") # gone by itself after 5
|
||||||
|
|
||||||
|
def test_network_blip_press_and_hold(self):
|
||||||
|
engine = sp.Engine(CONFIG)
|
||||||
|
t = SUNDAY
|
||||||
|
run_steps(engine, self.snap, t, 2)
|
||||||
|
drop = ("x32", "VX1000", "UHDJr", "RESI_Decoder", "tascam")
|
||||||
|
for label in drop:
|
||||||
|
self.snap.conns[label]["category"] = "error"
|
||||||
|
_, t = run_steps(engine, self.snap, t + 3, 4)
|
||||||
|
for label in drop:
|
||||||
|
self.snap.conns[label]["category"] = "good"
|
||||||
|
tiles, t = run_steps(engine, self.snap, t, 1)
|
||||||
|
engine.ack("net") # press: stop the flashing
|
||||||
|
tiles, t = run_steps(engine, self.snap, t, 1)
|
||||||
|
self.assertEqual((tiles["net"]["x"].split("\n")[1][:5], level(tiles["net"])), ("BLIP ", "warn"))
|
||||||
|
engine.run_fixes("net", self.snap, None, t) # hold: gone now
|
||||||
|
tiles, t = run_steps(engine, self.snap, t + 4, 1)
|
||||||
|
self.assertEqual((tiles["net"]["x"], tiles["all"]["x"]), ("NET\nOK", "ALL OK"))
|
||||||
|
engine.run_fixes("net", self.snap, None, t)
|
||||||
|
self.assertEqual(engine.notes["net"]["text"], "NOTHING\nTO CLEAR")
|
||||||
|
|
||||||
def test_racks_powering_off_is_not_a_network_event(self):
|
def test_racks_powering_off_is_not_a_network_event(self):
|
||||||
engine = sp.Engine(CONFIG)
|
engine = sp.Engine(CONFIG)
|
||||||
@@ -1253,13 +1276,13 @@ class TestPage(unittest.TestCase):
|
|||||||
n = sum(1 for r in self.controls.values() for c in r.values() if c["type"] == "button-layered")
|
n = sum(1 for r in self.controls.values() for c in r.values() if c["type"] == "button-layered")
|
||||||
self.assertEqual(n, len(positioned) + 2) # + the page number and RESTART keys
|
self.assertEqual(n, len(positioned) + 2) # + the page number and RESTART keys
|
||||||
jump = self.controls["2"]["6"]["steps"]["0"]["action_sets"]["up"][0]
|
jump = self.controls["2"]["6"]["steps"]["0"]["action_sets"]["up"][0]
|
||||||
self.assertEqual((jump["definitionId"], jump["options"]["page"]["value"]), ("set_page", 23))
|
self.assertEqual((jump["definitionId"], jump["options"]["page"]["value"]), ("set_page", 20))
|
||||||
# The master is where the smallest Stream Deck can see it.
|
# The master is where the smallest Stream Deck can see it.
|
||||||
self.assertEqual(CONFIG["tiles"][0]["pos"], [0, 1])
|
self.assertEqual(CONFIG["tiles"][0]["pos"], [0, 1])
|
||||||
|
|
||||||
def test_restart_page(self):
|
def test_restart_page(self):
|
||||||
export = make_page.build_restart(CONFIG)
|
export = make_page.build_restart(CONFIG)
|
||||||
self.assertEqual((export["oldPageNumber"], export["page"]["name"]), (23, "23\\nRestart"))
|
self.assertEqual((export["oldPageNumber"], export["page"]["name"]), (20, "20\\nRestart"))
|
||||||
c = export["page"]["controls"]
|
c = export["page"]["controls"]
|
||||||
holds = {}
|
holds = {}
|
||||||
for r in c.values():
|
for r in c.values():
|
||||||
@@ -1310,7 +1333,8 @@ class TestPage(unittest.TestCase):
|
|||||||
self.assertIn("2000", ingest["action_sets"]) # restarts its services
|
self.assertIn("2000", ingest["action_sets"]) # restarts its services
|
||||||
self.assertIn("2000", self.controls["0"]["2"]["steps"]["0"]["action_sets"]) # REC group
|
self.assertIn("2000", self.controls["0"]["2"]["steps"]["0"]["action_sets"]) # REC group
|
||||||
net = self.controls["3"]["4"]["steps"]["0"]
|
net = self.controls["3"]["4"]["steps"]["0"]
|
||||||
self.assertNotIn("2000", net["action_sets"]) # nothing to fix there
|
self.assertEqual(net["action_sets"]["2000"][0]["options"]["value"]["value"], # hold: clear a blip
|
||||||
|
"$(custom:sp_cmd);restart net $(internal:time_unix)")
|
||||||
ids = [a["id"] for r in self.controls.values() for c in r.values()
|
ids = [a["id"] for r in self.controls.values() for c in r.values()
|
||||||
for s in c.get("steps", {}).values() for al in s["action_sets"].values() for a in al]
|
for s in c.get("steps", {}).values() for al in s["action_sets"].values() for a in al]
|
||||||
self.assertEqual(len(ids), len(set(ids)))
|
self.assertEqual(len(ids), len(set(ids)))
|
||||||
|
|||||||
Reference in New Issue
Block a user