Skip to content

Commit c52abcc

Browse files
committed
driver/power: Add sentry_sequential backend for single-bank models
Add a new power backend for Sentry PDUs that use sequential OID mapping (1.1.1 through 1.1.16) instead of the multi-bank mapping used by the existing sentry backend. This is suitable for single-bank models like the CW-16V1. The original sentry backend remains unchanged for multi-bank models like CW-24VDD and 4805-XLS-16. Includes unit tests with 100% coverage for the new backend.
1 parent 2f0bdf1 commit c52abcc

3 files changed

Lines changed: 201 additions & 2 deletions

File tree

doc/configuration.rst

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,15 @@ Currently available are:
222222
for details.
223223

224224
``sentry``
225-
Controls *Sentry PDUs* via SNMP using Sentry3-MIB.
226-
It was tested on *CW-24VDD* and *4805-XLS-16*.
225+
Controls *Sentry PDUs* via SNMP using Sentry3-MIB with multi-bank OID mapping.
226+
Supports up to 48 outlets organized as 6 banks of 8 outlets each.
227+
Tested on *CW-24VDD* and *4805-XLS-16*.
228+
For single-bank sequential models like *CW-16V1*, use ``sentry_sequential`` instead.
229+
230+
``sentry_sequential``
231+
Controls *Sentry PDUs* via SNMP using Sentry3-MIB with sequential OID mapping.
232+
Supports up to 16 outlets numbered sequentially (1.1.1 through 1.1.16).
233+
Suitable for single-bank models like *CW-16V1*.
227234

228235
``shelly_gen1``
229236
Controls relays of *Shelly* devices using the Gen 1 Device API.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
Sentry PDU driver with sequential OID mapping for single-bank models.
3+
4+
This driver was tested on CW-16V1 but should work on all devices
5+
implementing Sentry3-MIB with sequential OID numbering.
6+
7+
This driver uses sequential OID mapping with 16 outlets numbered
8+
1.1.1 through 1.1.16. For multi-bank models like CW-24VDD or
9+
4805-XLS-16, use 'sentry' instead.
10+
"""
11+
12+
from ..exception import ExecutionError
13+
from ...util.helper import processwrapper
14+
15+
INDEX_TO_OID = {
16+
1: "1.1.1", 2: "1.1.2", 3: "1.1.3", 4: "1.1.4",
17+
5: "1.1.5", 6: "1.1.6", 7: "1.1.7", 8: "1.1.8",
18+
9: "1.1.9", 10: "1.1.10", 11: "1.1.11", 12: "1.1.12",
19+
13: "1.1.13", 14: "1.1.14", 15: "1.1.15", 16: "1.1.16",
20+
}
21+
22+
BASE_STATUS_OID = ".1.3.6.1.4.1.1718.3.2.3.1.10"
23+
BASE_CTRL_OID = ".1.3.6.1.4.1.1718.3.2.3.1.11"
24+
25+
def _snmp_get(host, oid):
26+
out = processwrapper.check_output(
27+
f"snmpget -v1 -c private -O qn {host} {oid}".split()
28+
).decode('ascii')
29+
out_oid, value = out.strip().split(' ', 1)
30+
assert oid == out_oid
31+
if value == "3" or value == "5":
32+
return True
33+
if value == "4":
34+
return False
35+
36+
def _snmp_set(host, oid, value):
37+
try:
38+
processwrapper.check_output(
39+
f"snmpset -v1 -c private {host} {oid} {value}".split()
40+
)
41+
except Exception as e:
42+
raise ExecutionError("failed to set SNMP value") from e
43+
44+
def power_set(host, port, index, value):
45+
assert port is None
46+
47+
index = int(index)
48+
value = 1 if value else 2
49+
assert 1 <= index <= 16
50+
51+
_snmp_set(host, f"{BASE_CTRL_OID}.{INDEX_TO_OID[index]}", f"int {value}")
52+
53+
54+
def power_get(host, port, index):
55+
assert port is None
56+
57+
index = int(index)
58+
assert 1 <= index <= 16
59+
60+
return _snmp_get(host, f"{BASE_STATUS_OID}.{INDEX_TO_OID[index]}")

tests/test_powerdriver.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ def test_import_backends(self):
287287
import labgrid.driver.power.netio_kshell
288288
import labgrid.driver.power.rest
289289
import labgrid.driver.power.sentry
290+
import labgrid.driver.power.sentry_sequential
290291
import labgrid.driver.power.eg_pms2_network
291292
import labgrid.driver.power.shelly_gen1
292293
import labgrid.driver.power.ubus
@@ -307,3 +308,134 @@ def test_import_backend_siglent(self):
307308
def test_import_backend_poe_mib(self):
308309
pytest.importorskip("pysnmp")
309310
import labgrid.driver.power.poe_mib
311+
312+
313+
class TestSentrySequentialBackend:
314+
def test_power_get_on_value_3(self, mocker):
315+
from labgrid.driver.power import sentry_sequential
316+
317+
mock_output = mocker.patch(
318+
'labgrid.driver.power.sentry_sequential.processwrapper.check_output'
319+
)
320+
mock_output.return_value = b'.1.3.6.1.4.1.1718.3.2.3.1.10.1.1.5 3'
321+
322+
result = sentry_sequential.power_get('host', None, 5)
323+
assert result is True
324+
325+
def test_power_get_on_value_5(self, mocker):
326+
from labgrid.driver.power import sentry_sequential
327+
328+
mock_output = mocker.patch(
329+
'labgrid.driver.power.sentry_sequential.processwrapper.check_output'
330+
)
331+
mock_output.return_value = b'.1.3.6.1.4.1.1718.3.2.3.1.10.1.1.5 5'
332+
333+
result = sentry_sequential.power_get('host', None, 5)
334+
assert result is True
335+
336+
def test_power_get_off(self, mocker):
337+
from labgrid.driver.power import sentry_sequential
338+
339+
mock_output = mocker.patch(
340+
'labgrid.driver.power.sentry_sequential.processwrapper.check_output'
341+
)
342+
mock_output.return_value = b'.1.3.6.1.4.1.1718.3.2.3.1.10.1.1.5 4'
343+
344+
result = sentry_sequential.power_get('host', None, 5)
345+
assert result is False
346+
347+
def test_power_set_on(self, mocker):
348+
from labgrid.driver.power import sentry_sequential
349+
350+
mock_output = mocker.patch(
351+
'labgrid.driver.power.sentry_sequential.processwrapper.check_output'
352+
)
353+
354+
sentry_sequential.power_set('host', None, 5, True)
355+
356+
mock_output.assert_called_once()
357+
call_args = mock_output.call_args[0][0]
358+
assert 'int' in call_args
359+
assert '1' in call_args
360+
361+
def test_power_set_off(self, mocker):
362+
from labgrid.driver.power import sentry_sequential
363+
364+
mock_output = mocker.patch(
365+
'labgrid.driver.power.sentry_sequential.processwrapper.check_output'
366+
)
367+
368+
sentry_sequential.power_set('host', None, 5, False)
369+
370+
call_args = mock_output.call_args[0][0]
371+
assert 'int' in call_args
372+
assert '2' in call_args
373+
374+
def test_power_set_index_bounds(self):
375+
from labgrid.driver.power import sentry_sequential
376+
377+
with pytest.raises(AssertionError):
378+
sentry_sequential.power_set('host', None, 0, True)
379+
380+
with pytest.raises(AssertionError):
381+
sentry_sequential.power_set('host', None, 17, True)
382+
383+
def test_power_get_index_bounds(self):
384+
from labgrid.driver.power import sentry_sequential
385+
386+
with pytest.raises(AssertionError):
387+
sentry_sequential.power_get('host', None, 0)
388+
389+
with pytest.raises(AssertionError):
390+
sentry_sequential.power_get('host', None, 17)
391+
392+
def test_power_set_port_must_be_none(self):
393+
from labgrid.driver.power import sentry_sequential
394+
395+
with pytest.raises(AssertionError):
396+
sentry_sequential.power_set('host', 'port', 5, True)
397+
398+
def test_power_get_port_must_be_none(self):
399+
from labgrid.driver.power import sentry_sequential
400+
401+
with pytest.raises(AssertionError):
402+
sentry_sequential.power_get('host', 'port', 5)
403+
404+
def test_power_set_error_handling(self, mocker):
405+
from labgrid.driver.power import sentry_sequential
406+
from labgrid.driver.exception import ExecutionError
407+
408+
mock_output = mocker.patch(
409+
'labgrid.driver.power.sentry_sequential.processwrapper.check_output'
410+
)
411+
mock_output.side_effect = Exception("SNMP failed")
412+
413+
with pytest.raises(ExecutionError):
414+
sentry_sequential.power_set('host', None, 5, True)
415+
416+
def test_power_get_oid_mapping(self, mocker):
417+
from labgrid.driver.power import sentry_sequential
418+
419+
mock_output = mocker.patch(
420+
'labgrid.driver.power.sentry_sequential.processwrapper.check_output'
421+
)
422+
mock_output.return_value = b'.1.3.6.1.4.1.1718.3.2.3.1.10.1.1.1 3'
423+
424+
sentry_sequential.power_get('host', None, 1)
425+
426+
call_args = mock_output.call_args[0][0]
427+
# Verify OID ends with .1.1.1 for index 1
428+
assert '.1.3.6.1.4.1.1718.3.2.3.1.10.1.1.1' in call_args
429+
430+
def test_power_set_oid_mapping(self, mocker):
431+
from labgrid.driver.power import sentry_sequential
432+
433+
mock_output = mocker.patch(
434+
'labgrid.driver.power.sentry_sequential.processwrapper.check_output'
435+
)
436+
437+
sentry_sequential.power_set('host', None, 16, True)
438+
439+
call_args = mock_output.call_args[0][0]
440+
# Verify OID ends with .1.1.16 for index 16
441+
assert '.1.3.6.1.4.1.1718.3.2.3.1.11.1.1.16' in call_args

0 commit comments

Comments
 (0)