A DANTE hold power-cycles a missing Studio B PoE box on its Netgear switch port.
STUDIO-B-SOLO-MON, MON, TB and TB-MON map to ports 0/14-0/17 on the Netgear M4250 Dante switch 10.66.1.102. netgear.py poe_cycle first checks the port's MAC table and refuses if a different device is plugged in there, then runs poe reset. Tested 2026-10-08 13:10 on SOLO-MON: dropped off Dante and was back about 17 s later. install.sh now copies netgear.py too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+36
@@ -10,6 +10,8 @@ unlocked inside the logged-in session, so run this from a LaunchAgent or a
|
||||
Terminal there, not over SSH.
|
||||
|
||||
python3 netgear.py lookup 00:1d:c1:51:18:84 ... which switch / port each MAC is on (read-only)
|
||||
python3 netgear.py poe-status 10.66.1.102 0/14 PoE state of ports (read-only)
|
||||
python3 netgear.py poe-cycle 10.66.1.102 0/14 [MAC] power-cycle one PoE port (refuses if MAC is elsewhere)
|
||||
"""
|
||||
import re
|
||||
import subprocess
|
||||
@@ -102,10 +104,44 @@ def poe_status(host, ports):
|
||||
sw.close()
|
||||
|
||||
|
||||
MAC_RE = re.compile(r"(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}")
|
||||
|
||||
|
||||
def poe_cycle(host, port, expect_mac=None):
|
||||
"""Power-cycle PoE on ONE port ("poe reset" in Interface Config mode). Nothing is saved
|
||||
to the switch's startup config: the port's settings are unchanged afterwards.
|
||||
|
||||
With expect_mac, refuse if the port has been re-patched: if the switch has learned
|
||||
other MACs on that port and not this one, a different device is plugged in there.
|
||||
(No MACs at all is fine: a dead device ages out of the table.)"""
|
||||
if not re.fullmatch(r"\d+/\d+", port):
|
||||
raise ValueError("one port like 0/14, got %r" % port)
|
||||
user, password = credentials()
|
||||
sw = Switch(host, user, password)
|
||||
try:
|
||||
if expect_mac:
|
||||
seen = {m.lower() for m in MAC_RE.findall(sw.run("show mac-addr-table interface %s" % port))}
|
||||
if seen and expect_mac.lower() not in seen:
|
||||
raise RuntimeError("%s %s has %s, not %s - re-patched? not power-cycling"
|
||||
% (host, port, ", ".join(sorted(seen)), expect_mac.lower()))
|
||||
sw.run("configure")
|
||||
sw.run("interface %s" % port)
|
||||
answer = sw.run("poe reset")
|
||||
sw.run("exit")
|
||||
sw.run("exit")
|
||||
finally:
|
||||
sw.close()
|
||||
if re.search(r"(?i)invalid|error|fail|unrecognized", answer):
|
||||
raise RuntimeError("%s %s: switch refused poe reset: %s" % (host, port, answer.strip()))
|
||||
print("POE RESET %s %s ok%s" % (host, port, (": " + answer.strip()) if answer.strip() else ""))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) >= 3 and sys.argv[1] == "lookup":
|
||||
lookup(sys.argv[2:])
|
||||
elif len(sys.argv) >= 4 and sys.argv[1] == "poe-status":
|
||||
poe_status(sys.argv[2], sys.argv[3:])
|
||||
elif len(sys.argv) in (4, 5) and sys.argv[1] == "poe-cycle":
|
||||
poe_cycle(sys.argv[2], sys.argv[3], sys.argv[4] if len(sys.argv) == 5 else None)
|
||||
else:
|
||||
sys.exit(__doc__)
|
||||
|
||||
Reference in New Issue
Block a user