BE+FE: Unstable devices list (3 status changes in 1h)

Signed-off-by: jokob-sk <jokob.sk@gmail.com>
This commit is contained in:
jokob-sk
2026-02-22 23:12:46 +11:00
parent a26137800d
commit 2f1e5068e3
28 changed files with 207 additions and 87 deletions

View File

@@ -338,7 +338,7 @@ class DeviceInstance:
for key, condition in conditions.items():
# Make sure the alias is SQL-safe (no spaces or special chars)
alias = key.replace(" ", "_").lower()
sub_queries.append(f'(SELECT COUNT(*) FROM Devices {condition}) AS "{alias}"')
sub_queries.append(f'(SELECT COUNT(*) FROM DevicesView {condition}) AS "{alias}"')
# Join all sub-selects with commas
query = "SELECT\n " + ",\n ".join(sub_queries)
@@ -360,7 +360,7 @@ class DeviceInstance:
for key, condition in conditions.items():
# Make sure the alias is SQL-safe (no spaces or special chars)
alias = key.replace(" ", "_").lower()
sub_queries.append(f'(SELECT COUNT(*) FROM Devices {condition}) AS "{alias}"')
sub_queries.append(f'(SELECT COUNT(*) FROM DevicesView {condition}) AS "{alias}"')
# Join all sub-selects with commas
query = "SELECT\n " + ",\n ".join(sub_queries)
@@ -381,7 +381,8 @@ class DeviceInstance:
# Build condition for SQL
condition = get_device_condition_by_status(status) if status else ""
query = f"SELECT * FROM Devices {condition}"
# Only DevicesView has devFlapping
query = f"SELECT * FROM DevicesView {condition}"
sql.execute(query)
table_data = []

View File

@@ -218,3 +218,50 @@ class EventInstance:
# Return as list
return [row[0], row[1], row[2], row[3], row[4], row[5]]
def get_unstable_devices(self, hours: int = 1, threshold: int = 3, macs_only: bool = True):
"""
Return unstable devices based on flap detection.
A device is considered unstable if it has >= threshold events within the last `hours`.
Events considered:
- Connected
- Disconnected
- Device Down
- Down Reconnected
Args:
hours (int): Time window in hours (default: 1)
threshold (int): Minimum number of events to be considered unstable (default: 3)
macs_only (bool): If True, return only MAC addresses (set). Otherwise return full rows.
Returns:
set[str] OR list[dict]
"""
if hours <= 0 or threshold <= 0:
mylog("warn", f"[Events] get_unstable_devices invalid params: hours={hours}, threshold={threshold}")
return set() if macs_only else []
conn = self._conn()
sql = """
SELECT eve_MAC, COUNT(*) as event_count
FROM Events
WHERE eve_EventType IN ('Connected','Disconnected','Device Down','Down Reconnected')
AND eve_DateTime >= datetime('now', ?)
GROUP BY eve_MAC
HAVING COUNT(*) >= ?
"""
# SQLite expects "-1 hours" format
window = f"-{hours} hours"
rows = conn.execute(sql, (window, threshold)).fetchall()
conn.close()
if macs_only:
return {row["eve_MAC"] for row in rows}
return [dict(row) for row in rows]

View File

@@ -49,7 +49,7 @@ class PluginObjectInstance:
"SELECT * FROM Plugins_Objects WHERE Plugin = ?", (plugin,)
)
def getLastNCreatedPerPLugin(self, plugin, entries=1):
def getLastNCreatedPerPlugin(self, plugin, entries=1):
return self._fetchall(
"""
SELECT *