HyperDeck hold switches its Companion connection off and on

After a deck restarts, Companion's HyperDeck connection does not come back
by itself and a plain restart of the connection does not help; switching
it off and on does. Each HD key's hold now does exactly that (even when it
looks green), waits 15 s and checks: DONE, STILL OFFLINE (deck pings) or
NO NETWORK. Decks are no longer rebooted by the panel; a recording deck is
left alone. Tested live on HD2 (off/on, reconnected in 17 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 05:20:07 -04:00
co-authored by Claude Opus 5.5
parent 0b38e9247b
commit ae28bb3c6d
4 changed files with 91 additions and 54 deletions
+44 -17
View File
@@ -893,6 +893,21 @@ class TestFixes(unittest.TestCase):
self.assertEqual(decks[0], ("HyperDeck_2", "10.1.10.12"))
self.assertEqual(self.plan("hd3"), [])
def test_hd_hold_works_even_when_it_looks_green(self):
ran = []
self.engine.executor = lambda cfg, fix_id, chk: ran.append((fix_id, chk.get("conn"), chk.get("ip"))) or True
run_steps(self.engine, self.snap, SUNDAY, 4) # every deck fine
self.engine.run_fixes("hd4", self.snap, None, SUNDAY + 5)
self.snap.vars[("HyperDeck_3", "status")] = "Record"
self.engine.run_fixes("hd3", self.snap, None, SUNDAY + 5) # recording: left alone
time.sleep(0.2)
self.assertEqual(ran, [("hyperdeck", "HyperDeck_4", "10.1.10.14")])
self.assertEqual(self.engine.notes["hd3"]["text"], "RECORDING\nLEFT ALONE")
self.engine.run_fixes("hd5", self.snap, None, SUNDAY + 6) # another deck: its own guard
self.engine.run_fixes("hd4", self.snap, None, SUNDAY + 20) # same deck within a minute
time.sleep(0.2)
self.assertEqual([r[1] for r in ran], ["HyperDeck_4", "HyperDeck_5"])
def test_disabled_connection_is_switched_on_never_rebooted(self):
# 2026-10-08: HyperDeck_1 disabled in Companion, a hold sent the deck a reboot.
self.snap.conns["HyperDeck_1"]["enabled"] = False
@@ -1082,28 +1097,40 @@ class TestHyperDeckProbe(unittest.TestCase):
deck.close()
self.assertEqual(sp.hyperdeck_probe("127.0.0.1", timeout=1, port=port), "unreachable")
def test_only_a_silent_deck_is_rebooted(self):
deck = FakeDeck("silent")
def test_the_hold_switches_the_connection_off_and_on_and_never_reboots(self):
from unittest import mock
deck = FakeDeck("silent") # even a deck that would once have been rebooted
try:
comp = mock.Mock()
comp.connections.side_effect = [
[{"label": "HyperDeck_1", "id": "h1", "enabled": True, "status": {"category": "error"}}],
[{"label": "HyperDeck_1", "id": "h1", "enabled": True, "status": {"category": "good"}}]]
comp.disable_connection.return_value = comp.enable_connection.return_value = True
chk = {"conn": "HyperDeck_1", "ip": "127.0.0.1", "port": deck.port}
self.assertTrue(sp.execute_fix(CONFIG, "hyperdeck", chk))
time.sleep(0.3)
self.assertIn(b"reboot", deck.received)
finally:
deck.close()
def test_a_recording_deck_is_never_rebooted(self):
deck = FakeDeck("record")
try:
cfg = json.loads(json.dumps(CONFIG))
cfg["companion"]["port"] = 1 # no Companion here: the reconnect just fails
chk = {"conn": "HyperDeck_1", "ip": "127.0.0.1", "port": deck.port}
sp.execute_fix(cfg, "hyperdeck", chk)
with mock.patch.object(sp, "Companion", return_value=comp), mock.patch.object(sp.time, "sleep"):
self.assertTrue(sp.execute_fix(CONFIG, "hyperdeck", chk))
self.assertEqual([c[0] for c in comp.method_calls if c[0].endswith("_connection")],
["disable_connection", "enable_connection"])
comp.restart_connection.assert_not_called()
time.sleep(0.3)
self.assertNotIn(b"reboot", deck.received)
finally:
deck.close()
def test_a_deck_that_stays_offline_says_so(self):
from unittest import mock
fix = CONFIG["fixes"]["hyperdeck"]
for pings, expect in ((True, "STILL OFFLINE\nHOLD AGAIN"), (False, "NO NETWORK\nCHECK POWER")):
comp = mock.Mock()
comp.connections.side_effect = [
[{"label": "HyperDeck_2", "id": "h2", "enabled": True, "status": {"category": "error"}}],
[{"label": "HyperDeck_2", "id": "h2", "enabled": True, "status": {"category": "error"}}]]
with mock.patch.object(sp, "Companion", return_value=comp), \
mock.patch.object(sp, "ping_once", return_value=pings) as ping:
self.assertEqual(sp.reconnect_and_verify(CONFIG, fix, "x", sleep=lambda s: None,
conn="HyperDeck_2", host="10.1.10.12", toggle=True), expect)
ping.assert_called_once_with("10.1.10.12") # the deck's own address, from Companion
class TestNetgearParsing(unittest.TestCase):
BIG = """ VLAN ID MAC Address Interface IfIndex Status
@@ -1233,8 +1260,8 @@ class TestAgainstFakeCompanion(unittest.TestCase):
sp.poll_connections(comp, snap)
run_steps(engine, snap, now, 4)
before = len(store["restarts"])
# hd3 is green, so its hold does nothing; LED is failing on VX1000, so that restarts.
sp.handle_commands(";restart hd3;restart led;service off;bogus", engine, comp, snap, now + 5)
# x32 is green, so its hold does nothing; LED is failing on VX1000, so that restarts.
sp.handle_commands(";restart x32;restart led;service off;bogus", engine, comp, snap, now + 5)
self.assertEqual(store["restarts"][before:], ["id-VX1000"])
finally:
store["conns"]["VX1000"]["category"] = "good"