Skip to content

Commit 130a416

Browse files
committed
driver/power/poe_netgear_plus: Add support
for NETGEAR Plus devices Signed-off-by: Burfeind, Jan-Niklas <jan-niklas.burfeind@sennheiser.com>
1 parent 81d768e commit 130a416

File tree

4 files changed

+123
-1
lines changed

4 files changed

+123
-1
lines changed

doc/configuration.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,9 @@ Currently available are:
216216
``poe_mib``
217217
Controls PoE switches using the PoE SNMP administration MiBs.
218218

219+
``poe_netgear_plus``
220+
Controls NETGEAR Plus switches using an HTTP interface.
221+
219222
``raritan``
220223
Controls *Raritan PDUs* via SNMP.
221224

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Control NETGEAR Plus devices via HTTP.
2+
3+
Available switch models:
4+
https://github.com/foxey/py-netgear-plus?tab=readme-ov-file#supported-and-tested-netgear-modelsproducts-and-firmware-versions
5+
6+
The password defaults to "P4ssword", but can be configured on a per-device basis like this:
7+
8+
NetworkPowerPort:
9+
model: poe_netgear_plus
10+
host: 'http://username_is_unused:AnotherP4ssword@192.168.0.239/'
11+
index: 7
12+
13+
Omitting the password defaults as described above.
14+
15+
NetworkPowerPort:
16+
model: poe_netgear_plus
17+
host: 'http://192.168.0.239/'
18+
index: 7
19+
20+
"""
21+
22+
from urllib.parse import urlparse
23+
24+
from py_netgear_plus import NetgearSwitchConnector
25+
26+
from ..exception import ExecutionError
27+
28+
29+
def _get_hostname_and_password(url: str) -> tuple[str, str]:
30+
"""Obtain credentials from url or default and return hostname and password.
31+
32+
If no password is in the URL return "P4ssword", which fulfills the minimal requirements from Netgear:
33+
- 8-20 characters
34+
- at least one upper case character
35+
- at least one lower case character
36+
- at least one number
37+
38+
Args:
39+
url: A URL with an optional basic auth prefix.
40+
41+
Returns:
42+
A tuple of the hostname, and the extracted or default password
43+
44+
"""
45+
parse_result = urlparse(url)
46+
if parse_result.scheme != "http":
47+
raise ExecutionError(f"URL must start with http://, found {parse_result.scheme} for {url}.")
48+
49+
password = "P4ssword" if parse_result.password is None else parse_result.password
50+
51+
return parse_result.hostname, password
52+
53+
54+
def power_set(host: str, _port: int, index: int, value: bool) -> None:
55+
"""Set the PoE output index based for a given host.
56+
57+
Args:
58+
host: The netloc with optional password e.g. "192.168.0.239" or ":P4ssword@192.168.0.239"
59+
_port: As the webserver of the switch is always on port 80, this is ignored
60+
index: Zero based access to the switches network ports
61+
value: Whether the port should enable PoE output
62+
63+
"""
64+
index = int(index)
65+
netgear_port_number = index + 1
66+
67+
(hostname, password) = _get_hostname_and_password(host)
68+
69+
sw = NetgearSwitchConnector(hostname, password)
70+
sw.autodetect_model()
71+
try:
72+
sw.get_login_cookie()
73+
sw._get_switch_metadata()
74+
if value:
75+
sw.turn_on_poe_port(netgear_port_number)
76+
else:
77+
sw.turn_off_poe_port(netgear_port_number)
78+
finally:
79+
sw.delete_login_cookie()
80+
81+
82+
def power_get(host: str, _port: int, index: int) -> bool:
83+
"""Determine whether a given Port has PoE enabled.
84+
85+
Args:
86+
host: The netloc with optional password e.g. "192.168.0.239" or ":P4ssword@192.168.0.239"
87+
_port: As the webserver of the switch is always on port 80, this is ignored
88+
index: Zero based access to the switches network ports
89+
90+
Returns:
91+
Whether the PoE output is enabled.
92+
93+
Raises:
94+
ExecutionError: In case the status dictionary contains unexpected PoE status values.
95+
96+
"""
97+
index = int(index)
98+
netgear_port_number = index + 1
99+
100+
(hostname, password) = _get_hostname_and_password(host)
101+
102+
sw = NetgearSwitchConnector(hostname, password)
103+
sw.autodetect_model()
104+
try:
105+
sw.get_login_cookie()
106+
sw._get_switch_metadata()
107+
data = sw._get_poe_port_config()
108+
key = f"port_{netgear_port_number}_poe_power_active"
109+
if data[key] == "on":
110+
return True
111+
if data[key] == "off":
112+
return False
113+
msg = f"Expected literal 'on'|'off', found {data[key]}"
114+
raise ExecutionError(msg)
115+
finally:
116+
sw.delete_login_cookie()

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ kasa = ["python-kasa>=0.7.0"]
6666
modbus = ["pyModbusTCP>=0.2.0"]
6767
modbusrtu = ["minimalmodbus>=1.0.2"]
6868
mqtt = ["paho-mqtt>=2.0.0"]
69+
netgear = ["py-netgear-plus>=0.4.7"]
6970
onewire = ["onewire>=0.2"]
7071
pyvisa = [
7172
"pyvisa>=1.11.3",
@@ -79,7 +80,7 @@ vxi11 = ["python-vxi11>=0.9"]
7980
xena = ["xenavalkyrie>=3.0.1"]
8081
deb = ["labgrid[modbus,onewire,snmp]"]
8182
dev = [
82-
"labgrid[doc,docker,graph,kasa,modbus,modbusrtu,mqtt,onewire,pyvisa,snmp,vxi11]",
83+
"labgrid[doc,docker,graph,kasa,modbus,modbusrtu,mqtt,netgear,onewire,pyvisa,snmp,vxi11]",
8384

8485
# additional dev dependencies
8586
"psutil>=5.8.0",
@@ -223,6 +224,7 @@ include = [
223224
"labgrid/driver/manualswitchdriver.py",
224225
"labgrid/driver/power/gude8031.py",
225226
"labgrid/driver/power/pe6216.py",
227+
"labgrid/driver/power/poe_netgear_plus.py",
226228
"labgrid/driver/power/shelly_gen2.py",
227229
"labgrid/driver/rawnetworkinterfacedriver.py",
228230
"labgrid/protocol/**/*.py",

tests/test_powerdriver.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ def test_import_backends(self):
292292
import labgrid.driver.power.netio
293293
import labgrid.driver.power.netio_kshell
294294
import labgrid.driver.power.pe6216
295+
import labgrid.driver.power.poe_netgear_plus
295296
import labgrid.driver.power.rest
296297
import labgrid.driver.power.sentry
297298
import labgrid.driver.power.eg_pms2_network

0 commit comments

Comments
 (0)