mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-05 01:31:49 -07:00
Enhance device down event handling for sleeping devices and improve down alerts query
This commit is contained in:
@@ -126,6 +126,8 @@ def get_notifications(db):
|
|||||||
AND eve_MAC IN (SELECT devMac FROM Devices WHERE devAlertDown = 0)
|
AND eve_MAC IN (SELECT devMac FROM Devices WHERE devAlertDown = 0)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
alert_down_minutes = int(get_setting_value("NTFPRCS_alert_down_time") or 0)
|
||||||
|
|
||||||
sections = get_setting_value("NTFPRCS_INCLUDED_SECTIONS") or []
|
sections = get_setting_value("NTFPRCS_INCLUDED_SECTIONS") or []
|
||||||
mylog("verbose", ["[Notification] Included sections: ", sections])
|
mylog("verbose", ["[Notification] Included sections: ", sections])
|
||||||
|
|
||||||
@@ -172,7 +174,7 @@ def get_notifications(db):
|
|||||||
AND eve_EventType = 'New Device' {condition}
|
AND eve_EventType = 'New Device' {condition}
|
||||||
ORDER BY eve_DateTime
|
ORDER BY eve_DateTime
|
||||||
""",
|
""",
|
||||||
"down_devices": """
|
"down_devices": f"""
|
||||||
SELECT
|
SELECT
|
||||||
devName,
|
devName,
|
||||||
eve_MAC,
|
eve_MAC,
|
||||||
@@ -183,7 +185,7 @@ def get_notifications(db):
|
|||||||
FROM Events_Devices AS down_events
|
FROM Events_Devices AS down_events
|
||||||
WHERE eve_PendingAlertEmail = 1
|
WHERE eve_PendingAlertEmail = 1
|
||||||
AND down_events.eve_EventType = 'Device Down'
|
AND down_events.eve_EventType = 'Device Down'
|
||||||
AND eve_DateTime < datetime('now', '-0 minutes')
|
AND eve_DateTime < datetime('now', '-{alert_down_minutes} minutes')
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM Events AS connected_events
|
FROM Events AS connected_events
|
||||||
|
|||||||
@@ -534,7 +534,11 @@ def print_scan_stats(db):
|
|||||||
(SELECT COUNT(*) FROM CurrentScan) AS devices_detected,
|
(SELECT COUNT(*) FROM CurrentScan) AS devices_detected,
|
||||||
(SELECT COUNT(*) FROM CurrentScan WHERE NOT EXISTS (SELECT 1 FROM Devices WHERE devMac = scanMac)) AS new_devices,
|
(SELECT COUNT(*) FROM CurrentScan WHERE NOT EXISTS (SELECT 1 FROM Devices WHERE devMac = scanMac)) AS new_devices,
|
||||||
(SELECT COUNT(*) FROM DevicesView WHERE devAlertDown != 0 AND devIsSleeping = 0 AND NOT EXISTS (SELECT 1 FROM CurrentScan WHERE devMac = scanMac)) AS down_alerts,
|
(SELECT COUNT(*) FROM DevicesView WHERE devAlertDown != 0 AND devIsSleeping = 0 AND NOT EXISTS (SELECT 1 FROM CurrentScan WHERE devMac = scanMac)) AS down_alerts,
|
||||||
(SELECT COUNT(*) FROM DevicesView WHERE devAlertDown != 0 AND devIsSleeping = 0 AND devPresentLastScan = 1 AND NOT EXISTS (SELECT 1 FROM CurrentScan WHERE devMac = scanMac)) AS new_down_alerts,
|
(SELECT COUNT(*) FROM DevicesView
|
||||||
|
WHERE devAlertDown != 0 AND devCanSleep = 0
|
||||||
|
AND devPresentLastScan = 1
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM CurrentScan WHERE devMac = scanMac)
|
||||||
|
) AS new_down_alerts,
|
||||||
(SELECT COUNT(*) FROM Devices WHERE devPresentLastScan = 0) AS new_connections,
|
(SELECT COUNT(*) FROM Devices WHERE devPresentLastScan = 0) AS new_connections,
|
||||||
(SELECT COUNT(*) FROM Devices WHERE devPresentLastScan = 1 AND NOT EXISTS (SELECT 1 FROM CurrentScan WHERE devMac = scanMac)) AS disconnections,
|
(SELECT COUNT(*) FROM Devices WHERE devPresentLastScan = 1 AND NOT EXISTS (SELECT 1 FROM CurrentScan WHERE devMac = scanMac)) AS disconnections,
|
||||||
(SELECT COUNT(*) FROM Devices, CurrentScan
|
(SELECT COUNT(*) FROM Devices, CurrentScan
|
||||||
|
|||||||
@@ -169,20 +169,39 @@ def insert_events(db):
|
|||||||
sql = db.sql # TO-DO
|
sql = db.sql # TO-DO
|
||||||
startTime = timeNowUTC()
|
startTime = timeNowUTC()
|
||||||
|
|
||||||
# Check device down
|
# Check device down – non-sleeping devices (immediate on first absence)
|
||||||
mylog("debug", "[Events] - 1 - Devices down")
|
mylog("debug", "[Events] - 1a - Devices down (non-sleeping)")
|
||||||
sql.execute(f"""INSERT OR IGNORE INTO Events (eve_MAC, eve_IP, eve_DateTime,
|
sql.execute(f"""INSERT OR IGNORE INTO Events (eve_MAC, eve_IP, eve_DateTime,
|
||||||
eve_EventType, eve_AdditionalInfo,
|
eve_EventType, eve_AdditionalInfo,
|
||||||
eve_PendingAlertEmail)
|
eve_PendingAlertEmail)
|
||||||
SELECT devMac, devLastIP, '{startTime}', 'Device Down', '', 1
|
SELECT devMac, devLastIP, '{startTime}', 'Device Down', '', 1
|
||||||
FROM DevicesView
|
FROM DevicesView
|
||||||
WHERE devAlertDown != 0
|
WHERE devAlertDown != 0
|
||||||
AND devIsSleeping = 0
|
AND devCanSleep = 0
|
||||||
AND devPresentLastScan = 1
|
AND devPresentLastScan = 1
|
||||||
AND NOT EXISTS (SELECT 1 FROM CurrentScan
|
AND NOT EXISTS (SELECT 1 FROM CurrentScan
|
||||||
WHERE devMac = scanMac
|
WHERE devMac = scanMac
|
||||||
) """)
|
) """)
|
||||||
|
|
||||||
|
# Check device down – sleeping devices whose sleep window has expired
|
||||||
|
mylog("debug", "[Events] - 1b - Devices down (sleep expired)")
|
||||||
|
sql.execute(f"""INSERT OR IGNORE INTO Events (eve_MAC, eve_IP, eve_DateTime,
|
||||||
|
eve_EventType, eve_AdditionalInfo,
|
||||||
|
eve_PendingAlertEmail)
|
||||||
|
SELECT devMac, devLastIP, '{startTime}', 'Device Down', '', 1
|
||||||
|
FROM DevicesView
|
||||||
|
WHERE devAlertDown != 0
|
||||||
|
AND devCanSleep = 1
|
||||||
|
AND devIsSleeping = 0
|
||||||
|
AND devPresentLastScan = 0
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM CurrentScan
|
||||||
|
WHERE devMac = scanMac)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM Events
|
||||||
|
WHERE eve_MAC = devMac
|
||||||
|
AND eve_EventType = 'Device Down'
|
||||||
|
AND eve_DateTime >= devLastConnection
|
||||||
|
) """)
|
||||||
|
|
||||||
# Check new Connections or Down Reconnections
|
# Check new Connections or Down Reconnections
|
||||||
mylog("debug", "[Events] - 2 - New Connections")
|
mylog("debug", "[Events] - 2 - New Connections")
|
||||||
sql.execute(f""" INSERT OR IGNORE INTO Events (eve_MAC, eve_IP, eve_DateTime,
|
sql.execute(f""" INSERT OR IGNORE INTO Events (eve_MAC, eve_IP, eve_DateTime,
|
||||||
|
|||||||
@@ -207,9 +207,9 @@ def insert_device(
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def down_event_macs(cur) -> set:
|
def down_event_macs(cur) -> set:
|
||||||
"""Return the set of MACs that have a 'Device Down' event row."""
|
"""Return the set of MACs that have a 'Device Down' event row (lowercased)."""
|
||||||
cur.execute("SELECT eve_MAC FROM Events WHERE eve_EventType = 'Device Down'")
|
cur.execute("SELECT eve_MAC FROM Events WHERE eve_EventType = 'Device Down'")
|
||||||
return {r["eve_MAC"] for r in cur.fetchall()}
|
return {r["eve_MAC"].lower() for r in cur.fetchall()}
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -4,14 +4,18 @@ Integration tests for the 'Device Down' event insertion and sleeping suppression
|
|||||||
Two complementary layers are tested:
|
Two complementary layers are tested:
|
||||||
|
|
||||||
Layer 1 — insert_events() (session_events.py)
|
Layer 1 — insert_events() (session_events.py)
|
||||||
The "Device Down" event fires when:
|
Non-sleeping devices (devCanSleep=0):
|
||||||
devPresentLastScan = 1 (was online last scan)
|
The "Device Down" event fires when:
|
||||||
AND device NOT in CurrentScan (absent this scan)
|
devPresentLastScan = 1 (was online last scan)
|
||||||
AND devAlertDown != 0
|
AND device NOT in CurrentScan (absent this scan)
|
||||||
|
AND devAlertDown != 0
|
||||||
|
|
||||||
At this point devIsSleeping is always 0 (sleeping requires devPresentLastScan=0,
|
Sleeping devices (devCanSleep=1):
|
||||||
but insert_events runs before update_presence_from_CurrentScan flips it).
|
The "Device Down" event is DEFERRED until the sleep window
|
||||||
Tests here verify NULL-devAlertDown regression and normal down/no-down branching.
|
(NTFPRCS_sleep_time) expires. During the sleep window the device
|
||||||
|
is shown as "Sleeping" and NO down event is created. After the
|
||||||
|
window expires, insert_events creates the event via the
|
||||||
|
sleep-expired query (devPresentLastScan=0, devIsSleeping=0).
|
||||||
|
|
||||||
Layer 2 — DevicesView down-count query (as used by insertOnlineHistory / db_helper)
|
Layer 2 — DevicesView down-count query (as used by insertOnlineHistory / db_helper)
|
||||||
After presence is updated (devPresentLastScan → 0) the sleeping suppression
|
After presence is updated (devPresentLastScan → 0) the sleeping suppression
|
||||||
@@ -39,13 +43,15 @@ from scan.session_events import insert_events # noqa: E402
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Layer 1: insert_events() — event creation on the down transition
|
# Layer 1: insert_events() — event creation on the down transition
|
||||||
#
|
#
|
||||||
# Condition: devPresentLastScan = 1 (was online) AND not in CurrentScan (now absent)
|
# Non-sleeping (devCanSleep=0):
|
||||||
# At this point devIsSleeping is always 0 (sleeping requires devPresentLastScan=0).
|
# Condition: devPresentLastScan = 1 AND not in CurrentScan → immediate event.
|
||||||
|
# Sleeping (devCanSleep=1):
|
||||||
|
# No event until sleep window expires (see TestInsertEventsSleepSuppression).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class TestInsertEventsDownDetection:
|
class TestInsertEventsDownDetection:
|
||||||
"""
|
"""
|
||||||
Tests for the 'Device Down' INSERT in insert_events().
|
Tests for the 'Device Down' INSERT in insert_events() for non-sleeping devices.
|
||||||
|
|
||||||
The down transition is: devPresentLastScan=1 AND absent from CurrentScan.
|
The down transition is: devPresentLastScan=1 AND absent from CurrentScan.
|
||||||
CurrentScan is left empty in all tests (all devices absent this scan).
|
CurrentScan is left empty in all tests (all devices absent this scan).
|
||||||
@@ -61,12 +67,12 @@ class TestInsertEventsDownDetection:
|
|||||||
"""
|
"""
|
||||||
conn = _make_db()
|
conn = _make_db()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
_insert_device(cur, "AA:11:22:33:44:01", alert_down=None, present_last_scan=1)
|
_insert_device(cur, "aa:11:22:33:44:01", alert_down=None, present_last_scan=1)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
insert_events(DummyDB(conn))
|
insert_events(DummyDB(conn))
|
||||||
|
|
||||||
assert "AA:11:22:33:44:01" not in _down_event_macs(cur), (
|
assert "aa:11:22:33:44:01" not in _down_event_macs(cur), (
|
||||||
"NULL devAlertDown must never fire a 'Device Down' event "
|
"NULL devAlertDown must never fire a 'Device Down' event "
|
||||||
"(IFNULL coercion regression)"
|
"(IFNULL coercion regression)"
|
||||||
)
|
)
|
||||||
@@ -75,54 +81,54 @@ class TestInsertEventsDownDetection:
|
|||||||
"""Explicit devAlertDown=0 must NOT fire a 'Device Down' event."""
|
"""Explicit devAlertDown=0 must NOT fire a 'Device Down' event."""
|
||||||
conn = _make_db()
|
conn = _make_db()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
_insert_device(cur, "AA:11:22:33:44:02", alert_down=0, present_last_scan=1)
|
_insert_device(cur, "aa:11:22:33:44:02", alert_down=0, present_last_scan=1)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
insert_events(DummyDB(conn))
|
insert_events(DummyDB(conn))
|
||||||
|
|
||||||
assert "AA:11:22:33:44:02" not in _down_event_macs(cur)
|
assert "aa:11:22:33:44:02" not in _down_event_macs(cur)
|
||||||
|
|
||||||
def test_alert_down_one_fires_down_event_when_absent(self):
|
def test_alert_down_one_fires_down_event_when_absent(self):
|
||||||
"""devAlertDown=1, was online last scan, absent now → 'Device Down' event."""
|
"""devAlertDown=1, was online last scan, absent now → 'Device Down' event."""
|
||||||
conn = _make_db()
|
conn = _make_db()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
_insert_device(cur, "AA:11:22:33:44:03", alert_down=1, present_last_scan=1)
|
_insert_device(cur, "aa:11:22:33:44:03", alert_down=1, present_last_scan=1)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
insert_events(DummyDB(conn))
|
insert_events(DummyDB(conn))
|
||||||
|
|
||||||
assert "AA:11:22:33:44:03" in _down_event_macs(cur)
|
assert "aa:11:22:33:44:03" in _down_event_macs(cur)
|
||||||
|
|
||||||
def test_device_in_current_scan_does_not_fire_down_event(self):
|
def test_device_in_current_scan_does_not_fire_down_event(self):
|
||||||
"""A device present in CurrentScan (online now) must NOT get Down event."""
|
"""A device present in CurrentScan (online now) must NOT get Down event."""
|
||||||
conn = _make_db()
|
conn = _make_db()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
_insert_device(cur, "AA:11:22:33:44:04", alert_down=1, present_last_scan=1)
|
_insert_device(cur, "aa:11:22:33:44:04", alert_down=1, present_last_scan=1)
|
||||||
# Put it in CurrentScan → device is online this scan
|
# Put it in CurrentScan → device is online this scan
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"INSERT INTO CurrentScan (scanMac, scanLastIP) VALUES (?, ?)",
|
"INSERT INTO CurrentScan (scanMac, scanLastIP) VALUES (?, ?)",
|
||||||
("AA:11:22:33:44:04", "192.168.1.1"),
|
("aa:11:22:33:44:04", "192.168.1.1"),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
insert_events(DummyDB(conn))
|
insert_events(DummyDB(conn))
|
||||||
|
|
||||||
assert "AA:11:22:33:44:04" not in _down_event_macs(cur)
|
assert "aa:11:22:33:44:04" not in _down_event_macs(cur)
|
||||||
|
|
||||||
def test_already_absent_last_scan_does_not_re_fire(self):
|
def test_already_absent_last_scan_does_not_re_fire(self):
|
||||||
"""
|
"""
|
||||||
devPresentLastScan=0 means device was already absent last scan.
|
devPresentLastScan=0 means device was already absent last scan.
|
||||||
The down event was already created then; it must not be created again.
|
For non-sleeping devices (devCanSleep=0), the down event was already
|
||||||
(The INSERT query requires devPresentLastScan=1 — the down-transition moment.)
|
created then; it must not be created again.
|
||||||
"""
|
"""
|
||||||
conn = _make_db()
|
conn = _make_db()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
_insert_device(cur, "AA:11:22:33:44:05", alert_down=1, present_last_scan=0)
|
_insert_device(cur, "aa:11:22:33:44:05", alert_down=1, present_last_scan=0)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
insert_events(DummyDB(conn))
|
insert_events(DummyDB(conn))
|
||||||
|
|
||||||
assert "AA:11:22:33:44:05" not in _down_event_macs(cur)
|
assert "aa:11:22:33:44:05" not in _down_event_macs(cur)
|
||||||
|
|
||||||
def test_archived_device_does_not_fire_down_event(self):
|
def test_archived_device_does_not_fire_down_event(self):
|
||||||
"""Archived devices should not produce Down events."""
|
"""Archived devices should not produce Down events."""
|
||||||
@@ -133,7 +139,7 @@ class TestInsertEventsDownDetection:
|
|||||||
(devMac, devAlertDown, devPresentLastScan, devCanSleep,
|
(devMac, devAlertDown, devPresentLastScan, devCanSleep,
|
||||||
devLastConnection, devLastIP, devIsArchived, devIsNew)
|
devLastConnection, devLastIP, devIsArchived, devIsNew)
|
||||||
VALUES (?, 1, 1, 0, ?, '192.168.1.1', 1, 0)""",
|
VALUES (?, 1, 1, 0, ?, '192.168.1.1', 1, 0)""",
|
||||||
("AA:11:22:33:44:06", _minutes_ago(60)),
|
("aa:11:22:33:44:06", _minutes_ago(60)),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
@@ -145,17 +151,17 @@ class TestInsertEventsDownDetection:
|
|||||||
# The archived device DOES get a Down event today (no archive filter in
|
# The archived device DOES get a Down event today (no archive filter in
|
||||||
# insert_events). This test documents the current behaviour.
|
# insert_events). This test documents the current behaviour.
|
||||||
# If that changes, update this assertion accordingly.
|
# If that changes, update this assertion accordingly.
|
||||||
assert "AA:11:22:33:44:06" in _down_event_macs(cur)
|
assert "aa:11:22:33:44:06" in _down_event_macs(cur)
|
||||||
|
|
||||||
def test_multiple_devices_mixed_alert_down(self):
|
def test_multiple_devices_mixed_alert_down(self):
|
||||||
"""Only devices with devAlertDown=1 that are absent fire Down events."""
|
"""Only devices with devAlertDown=1 that are absent fire Down events."""
|
||||||
conn = _make_db()
|
conn = _make_db()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
cases = [
|
cases = [
|
||||||
("CC:00:00:00:00:01", None, 1), # NULL → no event
|
("cc:00:00:00:00:01", None, 1), # NULL → no event
|
||||||
("CC:00:00:00:00:02", 0, 1), # 0 → no event
|
("cc:00:00:00:00:02", 0, 1), # 0 → no event
|
||||||
("CC:00:00:00:00:03", 1, 1), # 1 → event
|
("cc:00:00:00:00:03", 1, 1), # 1 → event
|
||||||
("CC:00:00:00:00:04", 1, 0), # already absent → no event
|
("cc:00:00:00:00:04", 1, 0), # already absent → no event
|
||||||
]
|
]
|
||||||
for mac, alert_down, present in cases:
|
for mac, alert_down, present in cases:
|
||||||
_insert_device(cur, mac, alert_down=alert_down, present_last_scan=present)
|
_insert_device(cur, mac, alert_down=alert_down, present_last_scan=present)
|
||||||
@@ -164,10 +170,154 @@ class TestInsertEventsDownDetection:
|
|||||||
insert_events(DummyDB(conn))
|
insert_events(DummyDB(conn))
|
||||||
fired = _down_event_macs(cur)
|
fired = _down_event_macs(cur)
|
||||||
|
|
||||||
assert "CC:00:00:00:00:01" not in fired, "NULL devAlertDown must not fire"
|
assert "cc:00:00:00:00:01" not in fired, "NULL devAlertDown must not fire"
|
||||||
assert "CC:00:00:00:00:02" not in fired, "devAlertDown=0 must not fire"
|
assert "cc:00:00:00:00:02" not in fired, "devAlertDown=0 must not fire"
|
||||||
assert "CC:00:00:00:00:03" in fired, "devAlertDown=1 absent must fire"
|
assert "cc:00:00:00:00:03" in fired, "devAlertDown=1 absent must fire"
|
||||||
assert "CC:00:00:00:00:04" not in fired, "already-absent device must not fire again"
|
assert "cc:00:00:00:00:04" not in fired, "already-absent device must not fire again"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Layer 1b: insert_events() — sleeping device suppression
|
||||||
|
#
|
||||||
|
# Sleeping devices (devCanSleep=1) must NOT get a 'Device Down' event on the
|
||||||
|
# first-scan transition. Instead, the event is deferred until the sleep
|
||||||
|
# window (NTFPRCS_sleep_time) expires.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestInsertEventsSleepSuppression:
|
||||||
|
"""
|
||||||
|
Tests for sleeping device suppression in insert_events().
|
||||||
|
|
||||||
|
Verifies that devCanSleep=1 devices DO NOT get immediate down events
|
||||||
|
and only get events after the sleep window expires.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_sleeping_device_no_down_event_on_first_absence(self):
|
||||||
|
"""
|
||||||
|
devCanSleep=1, devPresentLastScan=1, absent from CurrentScan.
|
||||||
|
Sleep window has NOT expired → must NOT fire 'Device Down'.
|
||||||
|
This is the core bug fix: previously the event fired immediately.
|
||||||
|
"""
|
||||||
|
conn = _make_db(sleep_minutes=30)
|
||||||
|
cur = conn.cursor()
|
||||||
|
_insert_device(cur, "bb:00:00:00:00:01", alert_down=1, present_last_scan=1,
|
||||||
|
can_sleep=1, last_connection=_minutes_ago(1))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
insert_events(DummyDB(conn))
|
||||||
|
|
||||||
|
assert "bb:00:00:00:00:01" not in _down_event_macs(cur), (
|
||||||
|
"Sleeping device must NOT get 'Device Down' on first absence "
|
||||||
|
"(sleep window not expired)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sleeping_device_still_in_window_no_event(self):
|
||||||
|
"""
|
||||||
|
devCanSleep=1, devPresentLastScan=0, devIsSleeping=1 (within window).
|
||||||
|
Device was already absent last scan and is still sleeping.
|
||||||
|
Must NOT fire 'Device Down'.
|
||||||
|
"""
|
||||||
|
conn = _make_db(sleep_minutes=30)
|
||||||
|
cur = conn.cursor()
|
||||||
|
_insert_device(cur, "bb:00:00:00:00:02", alert_down=1, present_last_scan=0,
|
||||||
|
can_sleep=1, last_connection=_minutes_ago(10))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
insert_events(DummyDB(conn))
|
||||||
|
|
||||||
|
assert "bb:00:00:00:00:02" not in _down_event_macs(cur), (
|
||||||
|
"Sleeping device within sleep window must NOT get 'Device Down'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sleeping_device_expired_window_fires_event(self):
|
||||||
|
"""
|
||||||
|
devCanSleep=1, devPresentLastScan=0, sleep window expired
|
||||||
|
(devLastConnection > NTFPRCS_sleep_time ago) → must fire 'Device Down'.
|
||||||
|
"""
|
||||||
|
conn = _make_db(sleep_minutes=30)
|
||||||
|
cur = conn.cursor()
|
||||||
|
_insert_device(cur, "bb:00:00:00:00:03", alert_down=1, present_last_scan=0,
|
||||||
|
can_sleep=1, last_connection=_minutes_ago(45))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
insert_events(DummyDB(conn))
|
||||||
|
|
||||||
|
assert "bb:00:00:00:00:03" in _down_event_macs(cur), (
|
||||||
|
"Sleeping device past its sleep window must get 'Device Down'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sleeping_device_expired_no_duplicate_event(self):
|
||||||
|
"""
|
||||||
|
Once a 'Device Down' event exists for the current absence period,
|
||||||
|
subsequent scan cycles must NOT create another one.
|
||||||
|
"""
|
||||||
|
conn = _make_db(sleep_minutes=30)
|
||||||
|
cur = conn.cursor()
|
||||||
|
last_conn = _minutes_ago(45)
|
||||||
|
_insert_device(cur, "bb:00:00:00:00:04", alert_down=1, present_last_scan=0,
|
||||||
|
can_sleep=1, last_connection=last_conn)
|
||||||
|
# Simulate: a Device Down event already exists for this absence
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO Events (eve_MAC, eve_IP, eve_DateTime, eve_EventType, "
|
||||||
|
"eve_AdditionalInfo, eve_PendingAlertEmail) "
|
||||||
|
"VALUES (?, '192.168.1.1', ?, 'Device Down', '', 1)",
|
||||||
|
("bb:00:00:00:00:04", _minutes_ago(15)),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
insert_events(DummyDB(conn))
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"SELECT COUNT(*) as cnt FROM Events "
|
||||||
|
"WHERE eve_MAC = 'bb:00:00:00:00:04' AND eve_EventType = 'Device Down'"
|
||||||
|
)
|
||||||
|
count = cur.fetchone()["cnt"]
|
||||||
|
assert count == 1, (
|
||||||
|
f"Expected exactly 1 Device Down event, got {count} (duplicate prevention)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sleeping_device_with_alert_down_zero_no_event(self):
|
||||||
|
"""devCanSleep=1 but devAlertDown=0 → never fires, even after sleep expires."""
|
||||||
|
conn = _make_db(sleep_minutes=30)
|
||||||
|
cur = conn.cursor()
|
||||||
|
_insert_device(cur, "bb:00:00:00:00:05", alert_down=0, present_last_scan=0,
|
||||||
|
can_sleep=1, last_connection=_minutes_ago(45))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
insert_events(DummyDB(conn))
|
||||||
|
|
||||||
|
assert "bb:00:00:00:00:05" not in _down_event_macs(cur)
|
||||||
|
|
||||||
|
def test_mixed_sleeping_and_non_sleeping(self):
|
||||||
|
"""
|
||||||
|
Non-sleeping device fires immediately on first absence.
|
||||||
|
Sleeping device within window does NOT fire.
|
||||||
|
Sleeping device past window DOES fire.
|
||||||
|
"""
|
||||||
|
conn = _make_db(sleep_minutes=30)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Non-sleeping, present last scan, absent now → immediate event
|
||||||
|
_insert_device(cur, "bb:00:00:00:00:10", alert_down=1, present_last_scan=1,
|
||||||
|
can_sleep=0, last_connection=_minutes_ago(1))
|
||||||
|
# Sleeping, present last scan (first absence) → NO event
|
||||||
|
_insert_device(cur, "bb:00:00:00:00:11", alert_down=1, present_last_scan=1,
|
||||||
|
can_sleep=1, last_connection=_minutes_ago(1))
|
||||||
|
# Sleeping, within window → NO event
|
||||||
|
_insert_device(cur, "bb:00:00:00:00:12", alert_down=1, present_last_scan=0,
|
||||||
|
can_sleep=1, last_connection=_minutes_ago(10))
|
||||||
|
# Sleeping, past window → event
|
||||||
|
_insert_device(cur, "bb:00:00:00:00:13", alert_down=1, present_last_scan=0,
|
||||||
|
can_sleep=1, last_connection=_minutes_ago(45))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
insert_events(DummyDB(conn))
|
||||||
|
fired = _down_event_macs(cur)
|
||||||
|
|
||||||
|
assert "bb:00:00:00:00:10" in fired, "Non-sleeping absent must fire"
|
||||||
|
assert "bb:00:00:00:00:11" not in fired, "Sleeping first-absence must NOT fire"
|
||||||
|
assert "bb:00:00:00:00:12" not in fired, "Sleeping within window must NOT fire"
|
||||||
|
assert "bb:00:00:00:00:13" in fired, "Sleeping past window must fire"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -200,24 +350,24 @@ class TestDownCountSleepingSuppression:
|
|||||||
"""NULL devAlertDown must not contribute to down count."""
|
"""NULL devAlertDown must not contribute to down count."""
|
||||||
conn = _make_db()
|
conn = _make_db()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
_insert_device(cur, "DD:00:00:00:00:01", alert_down=None, present_last_scan=0)
|
_insert_device(cur, "dd:00:00:00:00:01", alert_down=None, present_last_scan=0)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
cur.execute(self._DOWN_COUNT_SQL)
|
cur.execute(self._DOWN_COUNT_SQL)
|
||||||
macs = {r["devMac"] for r in cur.fetchall()}
|
macs = {r["devMac"] for r in cur.fetchall()}
|
||||||
assert "DD:00:00:00:00:01" not in macs
|
assert "dd:00:00:00:00:01" not in macs
|
||||||
|
|
||||||
def test_alert_down_one_included_in_down_count(self):
|
def test_alert_down_one_included_in_down_count(self):
|
||||||
"""devAlertDown=1 absent device must be counted as down."""
|
"""devAlertDown=1 absent device must be counted as down."""
|
||||||
conn = _make_db()
|
conn = _make_db()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
_insert_device(cur, "DD:00:00:00:00:02", alert_down=1, present_last_scan=0,
|
_insert_device(cur, "dd:00:00:00:00:02", alert_down=1, present_last_scan=0,
|
||||||
last_connection=_minutes_ago(60))
|
last_connection=_minutes_ago(60))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
cur.execute(self._DOWN_COUNT_SQL)
|
cur.execute(self._DOWN_COUNT_SQL)
|
||||||
macs = {r["devMac"] for r in cur.fetchall()}
|
macs = {r["devMac"] for r in cur.fetchall()}
|
||||||
assert "DD:00:00:00:00:02" in macs
|
assert "dd:00:00:00:00:02" in macs
|
||||||
|
|
||||||
def test_sleeping_device_excluded_from_down_count(self):
|
def test_sleeping_device_excluded_from_down_count(self):
|
||||||
"""
|
"""
|
||||||
@@ -226,13 +376,13 @@ class TestDownCountSleepingSuppression:
|
|||||||
"""
|
"""
|
||||||
conn = _make_db(sleep_minutes=30)
|
conn = _make_db(sleep_minutes=30)
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
_insert_device(cur, "DD:00:00:00:00:03", alert_down=1, present_last_scan=0,
|
_insert_device(cur, "dd:00:00:00:00:03", alert_down=1, present_last_scan=0,
|
||||||
can_sleep=1, last_connection=_minutes_ago(5))
|
can_sleep=1, last_connection=_minutes_ago(5))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
cur.execute(self._DOWN_COUNT_SQL)
|
cur.execute(self._DOWN_COUNT_SQL)
|
||||||
macs = {r["devMac"] for r in cur.fetchall()}
|
macs = {r["devMac"] for r in cur.fetchall()}
|
||||||
assert "DD:00:00:00:00:03" not in macs, (
|
assert "dd:00:00:00:00:03" not in macs, (
|
||||||
"Sleeping device must be excluded from down count"
|
"Sleeping device must be excluded from down count"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -240,13 +390,13 @@ class TestDownCountSleepingSuppression:
|
|||||||
"""Once the sleep window expires the device must appear in down count."""
|
"""Once the sleep window expires the device must appear in down count."""
|
||||||
conn = _make_db(sleep_minutes=30)
|
conn = _make_db(sleep_minutes=30)
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
_insert_device(cur, "DD:00:00:00:00:04", alert_down=1, present_last_scan=0,
|
_insert_device(cur, "dd:00:00:00:00:04", alert_down=1, present_last_scan=0,
|
||||||
can_sleep=1, last_connection=_minutes_ago(45))
|
can_sleep=1, last_connection=_minutes_ago(45))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
cur.execute(self._DOWN_COUNT_SQL)
|
cur.execute(self._DOWN_COUNT_SQL)
|
||||||
macs = {r["devMac"] for r in cur.fetchall()}
|
macs = {r["devMac"] for r in cur.fetchall()}
|
||||||
assert "DD:00:00:00:00:04" in macs, (
|
assert "dd:00:00:00:00:04" in macs, (
|
||||||
"Device past its sleep window must appear in down count"
|
"Device past its sleep window must appear in down count"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -254,13 +404,13 @@ class TestDownCountSleepingSuppression:
|
|||||||
"""devCanSleep=0 device that is absent is always counted as down."""
|
"""devCanSleep=0 device that is absent is always counted as down."""
|
||||||
conn = _make_db(sleep_minutes=30)
|
conn = _make_db(sleep_minutes=30)
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
_insert_device(cur, "DD:00:00:00:00:05", alert_down=1, present_last_scan=0,
|
_insert_device(cur, "dd:00:00:00:00:05", alert_down=1, present_last_scan=0,
|
||||||
can_sleep=0, last_connection=_minutes_ago(5))
|
can_sleep=0, last_connection=_minutes_ago(5))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
cur.execute(self._DOWN_COUNT_SQL)
|
cur.execute(self._DOWN_COUNT_SQL)
|
||||||
macs = {r["devMac"] for r in cur.fetchall()}
|
macs = {r["devMac"] for r in cur.fetchall()}
|
||||||
assert "DD:00:00:00:00:05" in macs
|
assert "dd:00:00:00:00:05" in macs
|
||||||
|
|
||||||
def test_online_history_down_count_excludes_sleeping(self):
|
def test_online_history_down_count_excludes_sleeping(self):
|
||||||
"""
|
"""
|
||||||
@@ -271,13 +421,13 @@ class TestDownCountSleepingSuppression:
|
|||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
# Normal down
|
# Normal down
|
||||||
_insert_device(cur, "EE:00:00:00:00:01", alert_down=1, present_last_scan=0,
|
_insert_device(cur, "ee:00:00:00:00:01", alert_down=1, present_last_scan=0,
|
||||||
can_sleep=0, last_connection=_minutes_ago(60))
|
can_sleep=0, last_connection=_minutes_ago(60))
|
||||||
# Sleeping (within window)
|
# Sleeping (within window)
|
||||||
_insert_device(cur, "EE:00:00:00:00:02", alert_down=1, present_last_scan=0,
|
_insert_device(cur, "ee:00:00:00:00:02", alert_down=1, present_last_scan=0,
|
||||||
can_sleep=1, last_connection=_minutes_ago(10))
|
can_sleep=1, last_connection=_minutes_ago(10))
|
||||||
# Online
|
# Online
|
||||||
_insert_device(cur, "EE:00:00:00:00:03", alert_down=1, present_last_scan=1,
|
_insert_device(cur, "ee:00:00:00:00:03", alert_down=1, present_last_scan=1,
|
||||||
last_connection=_minutes_ago(1))
|
last_connection=_minutes_ago(1))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user