From 3a73817048bd7e803bd378017c8b9aa1d0d8cf1b Mon Sep 17 00:00:00 2001 From: "Jokob @NetAlertX" <96159884+jokob-sk@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:05:37 +0000 Subject: [PATCH] Enhance device down event handling for sleeping devices and improve down alerts query --- server/messaging/reporting.py | 6 +- server/scan/device_handling.py | 6 +- server/scan/session_events.py | 25 ++- test/db_test_helpers.py | 4 +- test/scan/test_down_sleep_events.py | 242 ++++++++++++++++++++++------ 5 files changed, 229 insertions(+), 54 deletions(-) diff --git a/server/messaging/reporting.py b/server/messaging/reporting.py index 21c0d19e..a764fc27 100755 --- a/server/messaging/reporting.py +++ b/server/messaging/reporting.py @@ -126,6 +126,8 @@ def get_notifications(db): 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 [] mylog("verbose", ["[Notification] Included sections: ", sections]) @@ -172,7 +174,7 @@ def get_notifications(db): AND eve_EventType = 'New Device' {condition} ORDER BY eve_DateTime """, - "down_devices": """ + "down_devices": f""" SELECT devName, eve_MAC, @@ -183,7 +185,7 @@ def get_notifications(db): FROM Events_Devices AS down_events WHERE eve_PendingAlertEmail = 1 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 ( SELECT 1 FROM Events AS connected_events diff --git a/server/scan/device_handling.py b/server/scan/device_handling.py index 88090460..7d60c13d 100755 --- a/server/scan/device_handling.py +++ b/server/scan/device_handling.py @@ -534,7 +534,11 @@ def print_scan_stats(db): (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 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 = 1 AND NOT EXISTS (SELECT 1 FROM CurrentScan WHERE devMac = scanMac)) AS disconnections, (SELECT COUNT(*) FROM Devices, CurrentScan diff --git a/server/scan/session_events.py b/server/scan/session_events.py index bae9f053..6fe39ebf 100755 --- a/server/scan/session_events.py +++ b/server/scan/session_events.py @@ -169,20 +169,39 @@ def insert_events(db): sql = db.sql # TO-DO startTime = timeNowUTC() - # Check device down - mylog("debug", "[Events] - 1 - Devices down") + # Check device down – non-sleeping devices (immediate on first absence) + mylog("debug", "[Events] - 1a - Devices down (non-sleeping)") 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 devIsSleeping = 0 + AND devCanSleep = 0 AND devPresentLastScan = 1 AND NOT EXISTS (SELECT 1 FROM CurrentScan 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 mylog("debug", "[Events] - 2 - New Connections") sql.execute(f""" INSERT OR IGNORE INTO Events (eve_MAC, eve_IP, eve_DateTime, diff --git a/test/db_test_helpers.py b/test/db_test_helpers.py index d7880d2c..f49fd4b5 100644 --- a/test/db_test_helpers.py +++ b/test/db_test_helpers.py @@ -207,9 +207,9 @@ def insert_device( # --------------------------------------------------------------------------- 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'") - return {r["eve_MAC"] for r in cur.fetchall()} + return {r["eve_MAC"].lower() for r in cur.fetchall()} # --------------------------------------------------------------------------- diff --git a/test/scan/test_down_sleep_events.py b/test/scan/test_down_sleep_events.py index cc9067a6..295f06b6 100644 --- a/test/scan/test_down_sleep_events.py +++ b/test/scan/test_down_sleep_events.py @@ -4,14 +4,18 @@ Integration tests for the 'Device Down' event insertion and sleeping suppression Two complementary layers are tested: Layer 1 — insert_events() (session_events.py) - The "Device Down" event fires when: - devPresentLastScan = 1 (was online last scan) - AND device NOT in CurrentScan (absent this scan) - AND devAlertDown != 0 + Non-sleeping devices (devCanSleep=0): + The "Device Down" event fires when: + devPresentLastScan = 1 (was online last scan) + AND device NOT in CurrentScan (absent this scan) + AND devAlertDown != 0 - At this point devIsSleeping is always 0 (sleeping requires devPresentLastScan=0, - but insert_events runs before update_presence_from_CurrentScan flips it). - Tests here verify NULL-devAlertDown regression and normal down/no-down branching. + Sleeping devices (devCanSleep=1): + The "Device Down" event is DEFERRED until the sleep window + (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) 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 # -# Condition: devPresentLastScan = 1 (was online) AND not in CurrentScan (now absent) -# At this point devIsSleeping is always 0 (sleeping requires devPresentLastScan=0). +# Non-sleeping (devCanSleep=0): +# Condition: devPresentLastScan = 1 AND not in CurrentScan → immediate event. +# Sleeping (devCanSleep=1): +# No event until sleep window expires (see TestInsertEventsSleepSuppression). # --------------------------------------------------------------------------- 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. CurrentScan is left empty in all tests (all devices absent this scan). @@ -61,12 +67,12 @@ class TestInsertEventsDownDetection: """ conn = _make_db() 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() 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 " "(IFNULL coercion regression)" ) @@ -75,54 +81,54 @@ class TestInsertEventsDownDetection: """Explicit devAlertDown=0 must NOT fire a 'Device Down' event.""" conn = _make_db() 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() 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): """devAlertDown=1, was online last scan, absent now → 'Device Down' event.""" conn = _make_db() 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() 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): """A device present in CurrentScan (online now) must NOT get Down event.""" conn = _make_db() 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 cur.execute( "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() 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): """ devPresentLastScan=0 means device was already absent last scan. - The down event was already created then; it must not be created again. - (The INSERT query requires devPresentLastScan=1 — the down-transition moment.) + For non-sleeping devices (devCanSleep=0), the down event was already + created then; it must not be created again. """ conn = _make_db() 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() 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): """Archived devices should not produce Down events.""" @@ -133,7 +139,7 @@ class TestInsertEventsDownDetection: (devMac, devAlertDown, devPresentLastScan, devCanSleep, devLastConnection, devLastIP, devIsArchived, devIsNew) 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() @@ -145,17 +151,17 @@ class TestInsertEventsDownDetection: # The archived device DOES get a Down event today (no archive filter in # insert_events). This test documents the current behaviour. # 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): """Only devices with devAlertDown=1 that are absent fire Down events.""" conn = _make_db() cur = conn.cursor() cases = [ - ("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:03", 1, 1), # 1 → event - ("CC:00:00:00:00:04", 1, 0), # already absent → 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:03", 1, 1), # 1 → event + ("cc:00:00:00:00:04", 1, 0), # already absent → no event ] for mac, alert_down, present in cases: _insert_device(cur, mac, alert_down=alert_down, present_last_scan=present) @@ -164,10 +170,154 @@ class TestInsertEventsDownDetection: insert_events(DummyDB(conn)) 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: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:04" not in fired, "already-absent device must not fire again" + 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: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" + + +# --------------------------------------------------------------------------- +# 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.""" conn = _make_db() 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() cur.execute(self._DOWN_COUNT_SQL) 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): """devAlertDown=1 absent device must be counted as down.""" conn = _make_db() 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)) conn.commit() cur.execute(self._DOWN_COUNT_SQL) 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): """ @@ -226,13 +376,13 @@ class TestDownCountSleepingSuppression: """ conn = _make_db(sleep_minutes=30) 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)) conn.commit() cur.execute(self._DOWN_COUNT_SQL) 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" ) @@ -240,13 +390,13 @@ class TestDownCountSleepingSuppression: """Once the sleep window expires the device must appear in down count.""" conn = _make_db(sleep_minutes=30) 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)) conn.commit() cur.execute(self._DOWN_COUNT_SQL) 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" ) @@ -254,13 +404,13 @@ class TestDownCountSleepingSuppression: """devCanSleep=0 device that is absent is always counted as down.""" conn = _make_db(sleep_minutes=30) 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)) conn.commit() cur.execute(self._DOWN_COUNT_SQL) 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): """ @@ -271,13 +421,13 @@ class TestDownCountSleepingSuppression: cur = conn.cursor() # 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)) # 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)) # 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)) conn.commit()