Fix UPS functionality for multiple devices
This commit is contained in:
+79
-10
@@ -9,6 +9,7 @@ import socket
|
|||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
from bmspy import client
|
from bmspy import client
|
||||||
|
from bmspy.classes import UPS, BMSScalarField
|
||||||
|
|
||||||
DAEMON_UPDATE_PERIOD = 30
|
DAEMON_UPDATE_PERIOD = 30
|
||||||
|
|
||||||
@@ -18,6 +19,46 @@ warning_sent = False
|
|||||||
alert_sent = False
|
alert_sent = False
|
||||||
|
|
||||||
|
|
||||||
|
def _get_field_value(device: UPS, field_name: str) -> float | None:
|
||||||
|
"""Return the raw_value of a named BMSScalarField from a UPS, or None if not found."""
|
||||||
|
for name, field in device.items():
|
||||||
|
if name == field_name and isinstance(field, BMSScalarField):
|
||||||
|
return field.raw_value
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_ups_device(data: dict[str, UPS], requested: str | None) -> str | None:
|
||||||
|
"""Return the device name to monitor, or None and print an error if the selection is invalid.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- No devices → error.
|
||||||
|
- requested is set and found → use it.
|
||||||
|
- requested is set but not found → error listing available devices.
|
||||||
|
- requested is None and exactly one device → use it.
|
||||||
|
- requested is None and multiple devices → error; user must specify one.
|
||||||
|
"""
|
||||||
|
if not data:
|
||||||
|
print("Error: no UPS devices found")
|
||||||
|
return None
|
||||||
|
if requested is not None:
|
||||||
|
if requested not in data:
|
||||||
|
print(
|
||||||
|
"Error: UPS '{}' not found. Available: {}".format(
|
||||||
|
requested, ", ".join(sorted(data))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return requested
|
||||||
|
if len(data) > 1:
|
||||||
|
print(
|
||||||
|
"Error: multiple UPS devices found, specify one with --device: {}".format(
|
||||||
|
", ".join(sorted(data))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return next(iter(data))
|
||||||
|
|
||||||
|
|
||||||
def handle_shutdown(action: str = "cancel", delay: int = 0, debug: int = 0) -> None:
|
def handle_shutdown(action: str = "cancel", delay: int = 0, debug: int = 0) -> None:
|
||||||
"""Schedule or cancel a system shutdown; only schedules once per incident."""
|
"""Schedule or cancel a system shutdown; only schedules once per incident."""
|
||||||
global scheduled_shutdown
|
global scheduled_shutdown
|
||||||
@@ -157,6 +198,14 @@ def main() -> None:
|
|||||||
default="/run/bmspy/bms",
|
default="/run/bmspy/bms",
|
||||||
help="Socket to communicate with daemon",
|
help="Socket to communicate with daemon",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--device",
|
||||||
|
dest="device",
|
||||||
|
action="store",
|
||||||
|
default=None,
|
||||||
|
metavar="NAME",
|
||||||
|
help="Name of the UPS device to monitor (required if multiple devices are available)",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--verbose",
|
"--verbose",
|
||||||
"-v",
|
"-v",
|
||||||
@@ -173,10 +222,19 @@ def main() -> None:
|
|||||||
client.handle_registration(args.socket, "ups", debug)
|
client.handle_registration(args.socket, "ups", debug)
|
||||||
atexit.register(client.handle_registration, args.socket, "ups", debug)
|
atexit.register(client.handle_registration, args.socket, "ups", debug)
|
||||||
|
|
||||||
|
initial_data = client.read_data(args.socket, "ups")
|
||||||
|
device_name = _resolve_ups_device(initial_data, args.device)
|
||||||
|
if device_name is None:
|
||||||
|
return
|
||||||
|
|
||||||
history = deque()
|
history = deque()
|
||||||
while True:
|
while True:
|
||||||
data = client.read_data(args.socket, "ups")
|
data = client.read_data(args.socket, "ups", device=device_name)
|
||||||
history.append(data)
|
device_obj = data.get(device_name)
|
||||||
|
if device_obj is None:
|
||||||
|
time.sleep(DAEMON_UPDATE_PERIOD)
|
||||||
|
continue
|
||||||
|
history.append(device_obj)
|
||||||
|
|
||||||
# Remove the oldest data from the history
|
# Remove the oldest data from the history
|
||||||
while len(history) > 10:
|
while len(history) > 10:
|
||||||
@@ -192,19 +250,30 @@ def main() -> None:
|
|||||||
time.sleep(DAEMON_UPDATE_PERIOD)
|
time.sleep(DAEMON_UPDATE_PERIOD)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
current_amps = float(data["bms_current_amps"]["raw_value"])
|
current_amps = float(_get_field_value(device_obj, "bms_current_amps") or 0.0)
|
||||||
charge_ratio = float(data["bms_capacity_charge_ratio"]["raw_value"]) * 100
|
charge_ratio = (
|
||||||
comparison_1_current_amps = float(comparison_1["bms_current_amps"]["raw_value"])
|
float(_get_field_value(device_obj, "bms_capacity_charge_ratio") or 0.0) * 100
|
||||||
|
)
|
||||||
|
comparison_1_current_amps = float(
|
||||||
|
_get_field_value(comparison_1, "bms_current_amps") or 0.0
|
||||||
|
)
|
||||||
comparison_1_charge_ratio = (
|
comparison_1_charge_ratio = (
|
||||||
float(comparison_1["bms_capacity_charge_ratio"]["raw_value"]) * 100
|
float(_get_field_value(comparison_1, "bms_capacity_charge_ratio") or 0.0)
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
comparison_2_current_amps = float(
|
||||||
|
_get_field_value(comparison_2, "bms_current_amps") or 0.0
|
||||||
)
|
)
|
||||||
comparison_2_current_amps = float(comparison_2["bms_current_amps"]["raw_value"])
|
|
||||||
comparison_2_charge_ratio = (
|
comparison_2_charge_ratio = (
|
||||||
float(comparison_2["bms_capacity_charge_ratio"]["raw_value"]) * 100
|
float(_get_field_value(comparison_2, "bms_capacity_charge_ratio") or 0.0)
|
||||||
|
* 100
|
||||||
|
)
|
||||||
|
comparison_3_current_amps = float(
|
||||||
|
_get_field_value(comparison_3, "bms_current_amps") or 0.0
|
||||||
)
|
)
|
||||||
comparison_3_current_amps = float(comparison_3["bms_current_amps"]["raw_value"])
|
|
||||||
comparison_3_charge_ratio = (
|
comparison_3_charge_ratio = (
|
||||||
float(comparison_3["bms_capacity_charge_ratio"]["raw_value"]) * 100
|
float(_get_field_value(comparison_3, "bms_capacity_charge_ratio") or 0.0)
|
||||||
|
* 100
|
||||||
)
|
)
|
||||||
|
|
||||||
if debug > 1:
|
if debug > 1:
|
||||||
|
|||||||
+132
-30
@@ -4,7 +4,7 @@ from unittest.mock import patch, MagicMock
|
|||||||
|
|
||||||
import bmspy.ups as ups_mod
|
import bmspy.ups as ups_mod
|
||||||
from bmspy.classes import BMSScalarField, BMSMultiField, BMSInfoField, UPS
|
from bmspy.classes import BMSScalarField, BMSMultiField, BMSInfoField, UPS
|
||||||
from bmspy.ups import _get_field_value, handle_shutdown, handle_email
|
from bmspy.ups import _get_field_value, _resolve_ups_device, handle_shutdown, handle_email
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -204,6 +204,44 @@ class TestHandleEmail:
|
|||||||
assert args[1] == "root@myhost"
|
assert args[1] == "root@myhost"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# _resolve_ups_device
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _simple_ups_data(*names: str) -> dict[str, UPS]:
|
||||||
|
return {name: UPS.from_dict({
|
||||||
|
"bms_current_amps": {"help": "A", "raw_value": 0.0, "value": "0.00", "units": "A"},
|
||||||
|
}) for name in names}
|
||||||
|
|
||||||
|
|
||||||
|
class TestResolveUpsDevice:
|
||||||
|
def test_single_device_no_request_returns_it(self):
|
||||||
|
data = _simple_ups_data("myups")
|
||||||
|
assert _resolve_ups_device(data, None) == "myups"
|
||||||
|
|
||||||
|
def test_requested_device_found(self):
|
||||||
|
data = _simple_ups_data("ups1", "ups2")
|
||||||
|
assert _resolve_ups_device(data, "ups2") == "ups2"
|
||||||
|
|
||||||
|
def test_requested_device_not_found_returns_none(self, capsys):
|
||||||
|
data = _simple_ups_data("ups1")
|
||||||
|
result = _resolve_ups_device(data, "missing")
|
||||||
|
assert result is None
|
||||||
|
assert "missing" in capsys.readouterr().out
|
||||||
|
|
||||||
|
def test_multiple_devices_no_request_returns_none(self, capsys):
|
||||||
|
data = _simple_ups_data("ups1", "ups2")
|
||||||
|
result = _resolve_ups_device(data, None)
|
||||||
|
assert result is None
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "ups1" in out or "ups2" in out
|
||||||
|
|
||||||
|
def test_empty_data_returns_none(self, capsys):
|
||||||
|
result = _resolve_ups_device({}, None)
|
||||||
|
assert result is None
|
||||||
|
assert "no UPS" in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# ups main() - comprehensive loop testing
|
# ups main() - comprehensive loop testing
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -229,27 +267,26 @@ def _make_ups(current_amps: float, charge_ratio: float) -> UPS:
|
|||||||
class TestUpsMain:
|
class TestUpsMain:
|
||||||
"""Test ups.main() loop behavior by running a limited number of iterations."""
|
"""Test ups.main() loop behavior by running a limited number of iterations."""
|
||||||
|
|
||||||
def _run_main_with_data(self, data_sequence, argv=None, extra_patches=None):
|
def _run_main_with_data(self, data_sequence, argv=None):
|
||||||
"""Run main() with a sequence of UPS data, stopping when data runs out."""
|
"""Run main() with a sequence of UPS data, stopping when data runs out.
|
||||||
call_count = 0
|
|
||||||
|
The first call to read_data is the device-discovery call and returns
|
||||||
|
data_sequence[0]. Subsequent calls consume data_sequence in order.
|
||||||
|
"""
|
||||||
|
# discovery call returns data_sequence[0], then loop calls follow
|
||||||
|
discovery_done = [False]
|
||||||
|
loop_count = [0]
|
||||||
|
|
||||||
def _read_data(*args, **kwargs):
|
def _read_data(*args, **kwargs):
|
||||||
nonlocal call_count
|
if not discovery_done[0]:
|
||||||
if call_count >= len(data_sequence):
|
discovery_done[0] = True
|
||||||
|
return data_sequence[0]
|
||||||
|
if loop_count[0] >= len(data_sequence):
|
||||||
raise StopIteration("done")
|
raise StopIteration("done")
|
||||||
result = data_sequence[call_count]
|
result = data_sequence[loop_count[0]]
|
||||||
call_count += 1
|
loop_count[0] += 1
|
||||||
return result
|
return result
|
||||||
|
|
||||||
patches = {
|
|
||||||
"bmspy.ups.client.read_data": _read_data,
|
|
||||||
"bmspy.ups.client.handle_registration": MagicMock(),
|
|
||||||
"bmspy.ups.time.sleep": MagicMock(),
|
|
||||||
"bmspy.ups.os.system": MagicMock(),
|
|
||||||
}
|
|
||||||
if extra_patches:
|
|
||||||
patches.update(extra_patches)
|
|
||||||
|
|
||||||
with patch("sys.argv", ["bmspy-ups"] + (argv or [])):
|
with patch("sys.argv", ["bmspy-ups"] + (argv or [])):
|
||||||
with pytest.raises(StopIteration):
|
with pytest.raises(StopIteration):
|
||||||
with patch("bmspy.ups.client.read_data", side_effect=_read_data), \
|
with patch("bmspy.ups.client.read_data", side_effect=_read_data), \
|
||||||
@@ -257,7 +294,7 @@ class TestUpsMain:
|
|||||||
patch("bmspy.ups.time.sleep"), \
|
patch("bmspy.ups.time.sleep"), \
|
||||||
patch("bmspy.ups.os.system"):
|
patch("bmspy.ups.os.system"):
|
||||||
ups_mod.main()
|
ups_mod.main()
|
||||||
return call_count
|
return loop_count[0]
|
||||||
|
|
||||||
def _make_data(self, current_amps, charge_ratio):
|
def _make_data(self, current_amps, charge_ratio):
|
||||||
return {"testups": _make_ups(current_amps, charge_ratio)}
|
return {"testups": _make_ups(current_amps, charge_ratio)}
|
||||||
@@ -269,7 +306,7 @@ class TestUpsMain:
|
|||||||
|
|
||||||
with patch("sys.argv", ["bmspy-ups"]):
|
with patch("sys.argv", ["bmspy-ups"]):
|
||||||
with pytest.raises(StopIteration):
|
with pytest.raises(StopIteration):
|
||||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||||
patch("bmspy.ups.client.handle_registration"), \
|
patch("bmspy.ups.client.handle_registration"), \
|
||||||
patch("bmspy.ups.time.sleep"), \
|
patch("bmspy.ups.time.sleep"), \
|
||||||
patch("bmspy.ups.handle_shutdown"), \
|
patch("bmspy.ups.handle_shutdown"), \
|
||||||
@@ -285,7 +322,7 @@ class TestUpsMain:
|
|||||||
data = [self._make_data(0.0, 0.95)] * 5
|
data = [self._make_data(0.0, 0.95)] * 5
|
||||||
with patch("sys.argv", ["bmspy-ups", "-v", "-v"]):
|
with patch("sys.argv", ["bmspy-ups", "-v", "-v"]):
|
||||||
with pytest.raises(StopIteration):
|
with pytest.raises(StopIteration):
|
||||||
with patch("bmspy.ups.client.read_data", side_effect=data + [StopIteration]),\
|
with patch("bmspy.ups.client.read_data", side_effect=[data[0]] + data + [StopIteration]),\
|
||||||
patch("bmspy.ups.client.handle_registration"), \
|
patch("bmspy.ups.client.handle_registration"), \
|
||||||
patch("bmspy.ups.time.sleep"), \
|
patch("bmspy.ups.time.sleep"), \
|
||||||
patch("bmspy.ups.os.system"):
|
patch("bmspy.ups.os.system"):
|
||||||
@@ -305,7 +342,7 @@ class TestUpsMain:
|
|||||||
mock_email = MagicMock()
|
mock_email = MagicMock()
|
||||||
with patch("sys.argv", ["bmspy-ups", "--critical", "30"]):
|
with patch("sys.argv", ["bmspy-ups", "--critical", "30"]):
|
||||||
with pytest.raises(StopIteration):
|
with pytest.raises(StopIteration):
|
||||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||||
patch("bmspy.ups.client.handle_registration"), \
|
patch("bmspy.ups.client.handle_registration"), \
|
||||||
patch("bmspy.ups.time.sleep"), \
|
patch("bmspy.ups.time.sleep"), \
|
||||||
patch("bmspy.ups.handle_shutdown", mock_shutdown), \
|
patch("bmspy.ups.handle_shutdown", mock_shutdown), \
|
||||||
@@ -325,7 +362,7 @@ class TestUpsMain:
|
|||||||
mock_email = MagicMock()
|
mock_email = MagicMock()
|
||||||
with patch("sys.argv", ["bmspy-ups", "--warning", "75", "--critical", "30"]):
|
with patch("sys.argv", ["bmspy-ups", "--warning", "75", "--critical", "30"]):
|
||||||
with pytest.raises(StopIteration):
|
with pytest.raises(StopIteration):
|
||||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||||
patch("bmspy.ups.client.handle_registration"), \
|
patch("bmspy.ups.client.handle_registration"), \
|
||||||
patch("bmspy.ups.time.sleep"), \
|
patch("bmspy.ups.time.sleep"), \
|
||||||
patch("bmspy.ups.handle_shutdown"), \
|
patch("bmspy.ups.handle_shutdown"), \
|
||||||
@@ -352,7 +389,7 @@ class TestUpsMain:
|
|||||||
mock_email = MagicMock()
|
mock_email = MagicMock()
|
||||||
with patch("sys.argv", ["bmspy-ups"]):
|
with patch("sys.argv", ["bmspy-ups"]):
|
||||||
with pytest.raises(StopIteration):
|
with pytest.raises(StopIteration):
|
||||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||||
patch("bmspy.ups.client.handle_registration"), \
|
patch("bmspy.ups.client.handle_registration"), \
|
||||||
patch("bmspy.ups.time.sleep"), \
|
patch("bmspy.ups.time.sleep"), \
|
||||||
patch("bmspy.ups.handle_shutdown"), \
|
patch("bmspy.ups.handle_shutdown"), \
|
||||||
@@ -381,7 +418,7 @@ class TestUpsMain:
|
|||||||
mock_shutdown = MagicMock()
|
mock_shutdown = MagicMock()
|
||||||
with patch("sys.argv", ["bmspy-ups"]):
|
with patch("sys.argv", ["bmspy-ups"]):
|
||||||
with pytest.raises(StopIteration):
|
with pytest.raises(StopIteration):
|
||||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||||
patch("bmspy.ups.client.handle_registration"), \
|
patch("bmspy.ups.client.handle_registration"), \
|
||||||
patch("bmspy.ups.time.sleep"), \
|
patch("bmspy.ups.time.sleep"), \
|
||||||
patch("bmspy.ups.handle_shutdown", mock_shutdown), \
|
patch("bmspy.ups.handle_shutdown", mock_shutdown), \
|
||||||
@@ -394,7 +431,7 @@ class TestUpsMain:
|
|||||||
data_seq = [self._make_data(0.0, 0.95)] * 5
|
data_seq = [self._make_data(0.0, 0.95)] * 5
|
||||||
with patch("sys.argv", ["bmspy-ups", "-v", "-v"]):
|
with patch("sys.argv", ["bmspy-ups", "-v", "-v"]):
|
||||||
with pytest.raises(StopIteration):
|
with pytest.raises(StopIteration):
|
||||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||||
patch("bmspy.ups.client.handle_registration"), \
|
patch("bmspy.ups.client.handle_registration"), \
|
||||||
patch("bmspy.ups.time.sleep"), \
|
patch("bmspy.ups.time.sleep"), \
|
||||||
patch("bmspy.ups.handle_shutdown"), \
|
patch("bmspy.ups.handle_shutdown"), \
|
||||||
@@ -409,7 +446,7 @@ class TestUpsMain:
|
|||||||
data_seq = [self._make_data(0.0, 0.60)] * 5
|
data_seq = [self._make_data(0.0, 0.60)] * 5
|
||||||
with patch("sys.argv", ["bmspy-ups", "-v", "--warning", "75", "--critical", "30"]):
|
with patch("sys.argv", ["bmspy-ups", "-v", "--warning", "75", "--critical", "30"]):
|
||||||
with pytest.raises(StopIteration):
|
with pytest.raises(StopIteration):
|
||||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||||
patch("bmspy.ups.client.handle_registration"), \
|
patch("bmspy.ups.client.handle_registration"), \
|
||||||
patch("bmspy.ups.time.sleep"), \
|
patch("bmspy.ups.time.sleep"), \
|
||||||
patch("bmspy.ups.handle_shutdown"), \
|
patch("bmspy.ups.handle_shutdown"), \
|
||||||
@@ -430,7 +467,7 @@ class TestUpsMain:
|
|||||||
]
|
]
|
||||||
with patch("sys.argv", ["bmspy-ups", "-v"]):
|
with patch("sys.argv", ["bmspy-ups", "-v"]):
|
||||||
with pytest.raises(StopIteration):
|
with pytest.raises(StopIteration):
|
||||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||||
patch("bmspy.ups.client.handle_registration"), \
|
patch("bmspy.ups.client.handle_registration"), \
|
||||||
patch("bmspy.ups.time.sleep"), \
|
patch("bmspy.ups.time.sleep"), \
|
||||||
patch("bmspy.ups.handle_shutdown"), \
|
patch("bmspy.ups.handle_shutdown"), \
|
||||||
@@ -451,7 +488,7 @@ class TestUpsMain:
|
|||||||
]
|
]
|
||||||
with patch("sys.argv", ["bmspy-ups", "-v"]):
|
with patch("sys.argv", ["bmspy-ups", "-v"]):
|
||||||
with pytest.raises(StopIteration):
|
with pytest.raises(StopIteration):
|
||||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||||
patch("bmspy.ups.client.handle_registration"), \
|
patch("bmspy.ups.client.handle_registration"), \
|
||||||
patch("bmspy.ups.time.sleep"), \
|
patch("bmspy.ups.time.sleep"), \
|
||||||
patch("bmspy.ups.handle_shutdown"), \
|
patch("bmspy.ups.handle_shutdown"), \
|
||||||
@@ -465,7 +502,7 @@ class TestUpsMain:
|
|||||||
data_seq = [self._make_data(1.0, 0.80)] * 5
|
data_seq = [self._make_data(1.0, 0.80)] * 5
|
||||||
with patch("sys.argv", ["bmspy-ups", "-v", "-v"]):
|
with patch("sys.argv", ["bmspy-ups", "-v", "-v"]):
|
||||||
with pytest.raises(StopIteration):
|
with pytest.raises(StopIteration):
|
||||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||||
patch("bmspy.ups.client.handle_registration"), \
|
patch("bmspy.ups.client.handle_registration"), \
|
||||||
patch("bmspy.ups.time.sleep"), \
|
patch("bmspy.ups.time.sleep"), \
|
||||||
patch("bmspy.ups.handle_shutdown"), \
|
patch("bmspy.ups.handle_shutdown"), \
|
||||||
@@ -480,7 +517,7 @@ class TestUpsMain:
|
|||||||
data_seq = [self._make_data(0.0, 0.20)] * 5
|
data_seq = [self._make_data(0.0, 0.20)] * 5
|
||||||
with patch("sys.argv", ["bmspy-ups", "-v", "--critical", "30"]):
|
with patch("sys.argv", ["bmspy-ups", "-v", "--critical", "30"]):
|
||||||
with pytest.raises(StopIteration):
|
with pytest.raises(StopIteration):
|
||||||
with patch("bmspy.ups.client.read_data", side_effect=data_seq + [StopIteration("done")]), \
|
with patch("bmspy.ups.client.read_data", side_effect=[data_seq[0]] + data_seq + [StopIteration("done")]), \
|
||||||
patch("bmspy.ups.client.handle_registration"), \
|
patch("bmspy.ups.client.handle_registration"), \
|
||||||
patch("bmspy.ups.time.sleep"), \
|
patch("bmspy.ups.time.sleep"), \
|
||||||
patch("bmspy.ups.handle_shutdown"), \
|
patch("bmspy.ups.handle_shutdown"), \
|
||||||
@@ -488,3 +525,68 @@ class TestUpsMain:
|
|||||||
ups_mod.main()
|
ups_mod.main()
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert "critical" in captured.out.lower() or "threshold" in captured.out.lower()
|
assert "critical" in captured.out.lower() or "threshold" in captured.out.lower()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# main() — device selection errors and --device flag
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestUpsMainDeviceSelection:
|
||||||
|
def _make_data(self, name, current_amps=0.0, charge_ratio=0.8):
|
||||||
|
return {name: _make_ups(current_amps, charge_ratio)}
|
||||||
|
|
||||||
|
def test_main_no_devices_returns_early(self, capsys):
|
||||||
|
"""main() exits cleanly when no devices are found."""
|
||||||
|
with patch("sys.argv", ["bmspy-ups"]), \
|
||||||
|
patch("bmspy.ups.client.read_data", return_value={}), \
|
||||||
|
patch("bmspy.ups.client.handle_registration"):
|
||||||
|
ups_mod.main()
|
||||||
|
assert "no UPS" in capsys.readouterr().out
|
||||||
|
|
||||||
|
def test_main_multiple_devices_no_flag_returns_early(self, capsys):
|
||||||
|
"""main() exits with an error when multiple devices exist and --device is not set."""
|
||||||
|
two_devices = {
|
||||||
|
"ups1": _make_ups(0.0, 0.9),
|
||||||
|
"ups2": _make_ups(0.0, 0.8),
|
||||||
|
}
|
||||||
|
with patch("sys.argv", ["bmspy-ups"]), \
|
||||||
|
patch("bmspy.ups.client.read_data", return_value=two_devices), \
|
||||||
|
patch("bmspy.ups.client.handle_registration"):
|
||||||
|
ups_mod.main()
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "ups1" in out or "ups2" in out
|
||||||
|
|
||||||
|
def test_main_ups_flag_selects_device(self):
|
||||||
|
"""--device selects the correct device from multiple available ones."""
|
||||||
|
data_seq = [self._make_data("ups2")] * 6
|
||||||
|
with patch("sys.argv", ["bmspy-ups", "--device", "ups2"]):
|
||||||
|
with pytest.raises(StopIteration):
|
||||||
|
with patch("bmspy.ups.client.read_data",
|
||||||
|
side_effect=[{"ups1": _make_ups(0.0, 0.9), "ups2": _make_ups(0.0, 0.8)}]
|
||||||
|
+ data_seq + [StopIteration("done")]), \
|
||||||
|
patch("bmspy.ups.client.handle_registration"), \
|
||||||
|
patch("bmspy.ups.time.sleep"), \
|
||||||
|
patch("bmspy.ups.handle_shutdown"), \
|
||||||
|
patch("bmspy.ups.handle_email"):
|
||||||
|
ups_mod.main()
|
||||||
|
|
||||||
|
def test_main_ups_flag_unknown_device_returns_early(self, capsys):
|
||||||
|
"""--device with an unknown device name exits with an error."""
|
||||||
|
with patch("sys.argv", ["bmspy-ups", "--device", "ghost"]), \
|
||||||
|
patch("bmspy.ups.client.read_data", return_value={"ups1": _make_ups(0.0, 0.9)}), \
|
||||||
|
patch("bmspy.ups.client.handle_registration"):
|
||||||
|
ups_mod.main()
|
||||||
|
assert "ghost" in capsys.readouterr().out
|
||||||
|
|
||||||
|
def test_main_device_disappears_in_loop_sleeps(self):
|
||||||
|
"""When the named device is missing from a loop response, main() sleeps and retries."""
|
||||||
|
initial = {"testups": _make_ups(0.0, 0.8)}
|
||||||
|
# Second call returns empty dict (device gone), third raises StopIteration
|
||||||
|
with patch("sys.argv", ["bmspy-ups"]):
|
||||||
|
with pytest.raises(StopIteration):
|
||||||
|
with patch("bmspy.ups.client.read_data",
|
||||||
|
side_effect=[initial, {}, StopIteration("done")]), \
|
||||||
|
patch("bmspy.ups.client.handle_registration"), \
|
||||||
|
patch("bmspy.ups.time.sleep") as mock_sleep:
|
||||||
|
ups_mod.main()
|
||||||
|
mock_sleep.assert_called()
|
||||||
|
|||||||
Reference in New Issue
Block a user