Report a device that sends no data for 5 minutes
If the server had no data for the device, bmspy-ups slept and read again with no report. A battery that stopped answering during a power failure gave no email and no shutdown. bmspy-ups now sends one email after 5 minutes with no data, and one more when the data returns. A new email for a loss needs another 5 minutes with no data after that return. The same rule applies to the wait at startup.
This commit is contained in:
+64
-6
@@ -469,13 +469,18 @@ class TestUpsMain:
|
||||
def _events(self, readings, argv=None):
|
||||
"""Run main() over (current, charge ratio) readings, 30 seconds apart.
|
||||
|
||||
Return (reading number, event) for each email and each shutdown call.
|
||||
A reading of None is a read in which the server has no data for the
|
||||
device. Return (reading number, event) for each email and each
|
||||
shutdown call.
|
||||
"""
|
||||
sequence = [self._make_data(*reading) for reading in readings]
|
||||
for data, (current, _) in zip(sequence, readings):
|
||||
if current is None:
|
||||
sequence = [{} if reading is None else self._make_data(*reading) for reading in readings]
|
||||
for data, reading in zip(sequence, readings):
|
||||
if reading is not None and reading[0] is None:
|
||||
del data["testups"]._fields["bms_current_amps"]
|
||||
events, number = [], [0]
|
||||
events, number, clock = [], [0], [1000.0]
|
||||
|
||||
def sleep(seconds):
|
||||
clock[0] += seconds
|
||||
|
||||
def read(*args, **kwargs):
|
||||
if number[0] > len(sequence):
|
||||
@@ -487,7 +492,8 @@ class TestUpsMain:
|
||||
with patch("sys.argv", ["bmspy-ups"] + (argv or [])), \
|
||||
patch("bmspy.ups.client.read_data", side_effect=read), \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep"), \
|
||||
patch("bmspy.ups.time.sleep", side_effect=sleep), \
|
||||
patch("bmspy.ups.time.monotonic", side_effect=lambda: clock[0]), \
|
||||
patch("bmspy.ups.handle_shutdown",
|
||||
side_effect=lambda action, **kw: events.append((number[0] - 1, action))), \
|
||||
patch("bmspy.ups.handle_email",
|
||||
@@ -555,6 +561,34 @@ class TestUpsMain:
|
||||
events = self._events([(None, 0.95)] * 5 + [(None, 0.20)] * 2)
|
||||
assert events == [(7, "shutdown"), (7, "Critical alert")]
|
||||
|
||||
def test_no_data_for_five_minutes_is_reported_once_and_the_return_once(self):
|
||||
"""Readings are 30 seconds apart, so the tenth read with no data is at 5 minutes."""
|
||||
readings = (
|
||||
[(1.0, 0.95)] * 3 # readings 1 to 3: data
|
||||
+ [None] * 25 # 4 to 28: no data for 12.5 minutes
|
||||
+ [(1.0, 0.95)] * 3 # 29 to 31: the data returns
|
||||
+ [None] * 9 # 32 to 40: 4.5 minutes, below the limit
|
||||
+ [(1.0, 0.95)] * 2 # 41 and 42
|
||||
+ [None] * 12 # 43 to 54: no data for 6 minutes
|
||||
)
|
||||
assert self._events(readings) == [
|
||||
(13, "Data loss alert"), (29, "Data recovery alert"), (52, "Data loss alert"),
|
||||
]
|
||||
|
||||
def test_data_loss_email_names_the_device(self):
|
||||
send_email = MagicMock()
|
||||
with patch("bmspy.ups.time.monotonic", side_effect=[0, 299, 300, 900, 901]):
|
||||
monitor = ups_mod._DataMonitor(send_email)
|
||||
monitor.update("network", False) # 299 seconds: below the limit
|
||||
send_email.assert_not_called()
|
||||
monitor.update("network", False) # 300 seconds
|
||||
monitor.update("network", False) # no second email
|
||||
monitor.update("network", True)
|
||||
assert [c.kwargs for c in send_email.call_args_list] == [
|
||||
{"text": "no data from UPS 'network' for 5 minutes", "level": "Data loss alert"},
|
||||
{"text": "data from UPS 'network' regained", "level": "Data recovery alert"},
|
||||
]
|
||||
|
||||
def test_main_not_enough_history_debug_2(self, capsys):
|
||||
"""With debug=2 and < 4 readings, prints 'not enough readings'."""
|
||||
data_seq = [self._make_data(0.0, 0.95)] * 5
|
||||
@@ -680,6 +714,30 @@ class TestUpsMainDeviceSelection:
|
||||
# The reason is reported once, not on each retry.
|
||||
assert capsys.readouterr().out.count("UPS 'ups1' has no data yet") == 1
|
||||
|
||||
def test_no_data_at_startup_for_five_minutes_is_reported(self):
|
||||
"""The startup wait reads every 5 seconds, so read 61 is at 5 minutes."""
|
||||
clock = [1000.0]
|
||||
|
||||
def sleep(seconds):
|
||||
clock[0] += seconds
|
||||
|
||||
reads = [{}] * 70 + [self._make_data("ups1"), self._make_data("ups1"), StopIteration("done")]
|
||||
mock_email = MagicMock()
|
||||
with patch("sys.argv", ["bmspy-ups", "--to", "admin@example.com"]), \
|
||||
patch("bmspy.ups.client.list_devices", return_value=["ups1"]), \
|
||||
patch("bmspy.ups.client.read_data", side_effect=reads) as read_data, \
|
||||
patch("bmspy.ups.client.handle_registration"), \
|
||||
patch("bmspy.ups.time.sleep", side_effect=sleep), \
|
||||
patch("bmspy.ups.time.monotonic", side_effect=lambda: clock[0]), \
|
||||
patch("bmspy.ups.handle_email", mock_email):
|
||||
with pytest.raises(StopIteration):
|
||||
ups_mod.main()
|
||||
assert [(c.kwargs["level"], c.kwargs["recipient"]) for c in mock_email.call_args_list] == [
|
||||
("Data loss alert", "admin@example.com"), ("Data recovery alert", "admin@example.com"),
|
||||
]
|
||||
# The loss email goes out during the wait, and the other one after it.
|
||||
assert read_data.call_count == 73
|
||||
|
||||
def test_main_selects_from_configured_devices_not_from_those_with_data(self):
|
||||
"""One device with data must not be picked when the server has several."""
|
||||
with patch("sys.argv", ["bmspy-ups", "--device", "ups2"]), \
|
||||
|
||||
Reference in New Issue
Block a user