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

@@ -314,6 +314,9 @@
<li>
<a href="devices.php#archived" onclick="forceLoadUrl('devices.php#archived')" > <?= lang("Device_Shortcut_Archived");?> </a>
</li>
<li>
<a href="devices.php#unstable_devices" onclick="forceLoadUrl('devices.php#unstable_devices')" > <?= lang("Device_Shortcut_Unstable");?> </a>
</li>
<li>
<a href="devices.php#all_devices" onclick="forceLoadUrl('devices.php#all_devices')" > <?= lang("Gen_All_Devices");?> </a>
</li>

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "المفضلة",
"Device_Shortcut_NewDevices": "أجهزة جديدة",
"Device_Shortcut_OnlineChart": "مخطط الاتصال",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "تنبيه عدم الاتصال",
"Device_TableHead_Connected_Devices": "الأجهزة المتصلة",
"Device_TableHead_CustomProps": "خصائص مخصصة",
@@ -789,4 +790,4 @@
"settings_system_label": "نظام",
"settings_update_item_warning": "قم بتحديث القيمة أدناه. احرص على اتباع التنسيق السابق. <b>لم يتم إجراء التحقق.</b>",
"test_event_tooltip": "احفظ التغييرات أولاً قبل اختبار الإعدادات."
}
}

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "Favorits",
"Device_Shortcut_NewDevices": "Nous dispositius",
"Device_Shortcut_OnlineChart": "Dispositius detectats",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "Cancel·lar alerta",
"Device_TableHead_Connected_Devices": "Connexions",
"Device_TableHead_CustomProps": "Props / Accions",

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "",
"Device_Shortcut_NewDevices": "",
"Device_Shortcut_OnlineChart": "",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "",
"Device_TableHead_Connected_Devices": "",
"Device_TableHead_CustomProps": "",

View File

@@ -222,6 +222,7 @@
"Device_Shortcut_Favorites": "Favoriten",
"Device_Shortcut_NewDevices": "Neue Geräte",
"Device_Shortcut_OnlineChart": "Gerätepräsenz im Laufe der Zeit",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "Alarm aus",
"Device_TableHead_Connected_Devices": "Verbindungen",
"Device_TableHead_CustomProps": "Eigenschaften / Aktionen",

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "Favorites",
"Device_Shortcut_NewDevices": "New devices",
"Device_Shortcut_OnlineChart": "Device presence",
"Device_Shortcut_Unstable": "Unstable",
"Device_TableHead_AlertDown": "Alert Down",
"Device_TableHead_Connected_Devices": "Connections",
"Device_TableHead_CustomProps": "Props / Actions",

View File

@@ -220,6 +220,7 @@
"Device_Shortcut_Favorites": "Favorito(s)",
"Device_Shortcut_NewDevices": "Nuevos dispositivos",
"Device_Shortcut_OnlineChart": "Presencia del dispositivo a lo largo del tiempo",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "Alerta desactivada",
"Device_TableHead_Connected_Devices": "Conexiones",
"Device_TableHead_CustomProps": "Propiedades / Acciones",

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "",
"Device_Shortcut_NewDevices": "",
"Device_Shortcut_OnlineChart": "",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "",
"Device_TableHead_Connected_Devices": "",
"Device_TableHead_CustomProps": "",

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "Favoris",
"Device_Shortcut_NewDevices": "Nouveaux appareils",
"Device_Shortcut_OnlineChart": "Présence de l'appareil",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "Alerter si En panne",
"Device_TableHead_Connected_Devices": "Connexions",
"Device_TableHead_CustomProps": "Champs / Actions",
@@ -789,4 +790,4 @@
"settings_system_label": "Système",
"settings_update_item_warning": "Mettre à jour la valeur ci-dessous. Veillez à bien suivre le même format qu'auparavant. <b>Il n'y a pas de pas de contrôle.</b>",
"test_event_tooltip": "Enregistrer d'abord vos modifications avant de tester vôtre paramétrage."
}
}

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "Preferiti",
"Device_Shortcut_NewDevices": "Nuovi dispositivi",
"Device_Shortcut_OnlineChart": "Presenza dispositivo",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "Avviso disconnessione",
"Device_TableHead_Connected_Devices": "Connessioni",
"Device_TableHead_CustomProps": "Proprietà/Azioni",
@@ -789,4 +790,4 @@
"settings_system_label": "Sistema",
"settings_update_item_warning": "Aggiorna il valore qui sotto. Fai attenzione a seguire il formato precedente. <b>La convalida non viene eseguita.</b>",
"test_event_tooltip": "Salva le modifiche prima di provare le nuove impostazioni."
}
}

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "お気に入り",
"Device_Shortcut_NewDevices": "新規デバイス",
"Device_Shortcut_OnlineChart": "デバイス検出",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "ダウンアラート",
"Device_TableHead_Connected_Devices": "接続",
"Device_TableHead_CustomProps": "属性 / アクション",
@@ -789,4 +790,4 @@
"settings_system_label": "システム",
"settings_update_item_warning": "以下の値を更新してください。以前のフォーマットに従うよう注意してください。<b>検証は行われません。</b>",
"test_event_tooltip": "設定をテストする前に、まず変更を保存してください。"
}
}

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "Favoritter",
"Device_Shortcut_NewDevices": "Nye Enheter",
"Device_Shortcut_OnlineChart": "Enhetens tilstedeværelse",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "",
"Device_TableHead_Connected_Devices": "Tilkoblinger",
"Device_TableHead_CustomProps": "",

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "Ulubione",
"Device_Shortcut_NewDevices": "Nowe urządzenia",
"Device_Shortcut_OnlineChart": "Obecność urządzenia",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "Alert niedostępny",
"Device_TableHead_Connected_Devices": "Połączenia",
"Device_TableHead_CustomProps": "Właściwości / Akcje",

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "Favoritos",
"Device_Shortcut_NewDevices": "Novos dispositivos",
"Device_Shortcut_OnlineChart": "Presença do dispositivo",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "Alerta em baixo",
"Device_TableHead_Connected_Devices": "Conexões",
"Device_TableHead_CustomProps": "",

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "Favoritos",
"Device_Shortcut_NewDevices": "Novo dispostivo",
"Device_Shortcut_OnlineChart": "Presença do dispositivo",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "Alerta em baixo",
"Device_TableHead_Connected_Devices": "Conexões",
"Device_TableHead_CustomProps": "Propriedades / Ações",

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "Избранные",
"Device_Shortcut_NewDevices": "Новые устройства",
"Device_Shortcut_OnlineChart": "Присутствие устройств",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "Оповещение о сост. ВЫКЛ",
"Device_TableHead_Connected_Devices": "Соединения",
"Device_TableHead_CustomProps": "Свойства / Действия",
@@ -789,4 +790,4 @@
"settings_system_label": "Система",
"settings_update_item_warning": "Обновить значение ниже. Будьте осторожны, следуя предыдущему формату. <b>Проверка не выполняется.</b>",
"test_event_tooltip": "Сначала сохраните изменения, прежде чем проверять настройки."
}
}

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "",
"Device_Shortcut_NewDevices": "",
"Device_Shortcut_OnlineChart": "",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "",
"Device_TableHead_Connected_Devices": "",
"Device_TableHead_CustomProps": "",

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "Favoriler",
"Device_Shortcut_NewDevices": "Yeni Cİhazlar",
"Device_Shortcut_OnlineChart": "Cihaz Durumu",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "Çalışmama Alarmı",
"Device_TableHead_Connected_Devices": "Bağlantılar",
"Device_TableHead_CustomProps": "Özellikler / Eylemler",

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "Вибране",
"Device_Shortcut_NewDevices": "Нові пристрої",
"Device_Shortcut_OnlineChart": "Наявність пристрою",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "Агент Вниз",
"Device_TableHead_Connected_Devices": "Зв'язки",
"Device_TableHead_CustomProps": "Реквізит / дії",
@@ -789,4 +790,4 @@
"settings_system_label": "Система",
"settings_update_item_warning": "Оновіть значення нижче. Слідкуйте за попереднім форматом. <b>Перевірка не виконана.</b>",
"test_event_tooltip": "Перш ніж перевіряти налаштування, збережіть зміни."
}
}

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "",
"Device_Shortcut_NewDevices": "",
"Device_Shortcut_OnlineChart": "",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "",
"Device_TableHead_Connected_Devices": "",
"Device_TableHead_CustomProps": "",

View File

@@ -218,6 +218,7 @@
"Device_Shortcut_Favorites": "收藏",
"Device_Shortcut_NewDevices": "新设备",
"Device_Shortcut_OnlineChart": "设备统计",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "提醒宕机",
"Device_TableHead_Connected_Devices": "链接",
"Device_TableHead_CustomProps": "属性",
@@ -789,4 +790,4 @@
"settings_system_label": "系统",
"settings_update_item_warning": "更新下面的值。请注意遵循先前的格式。<b>未执行验证。</b>",
"test_event_tooltip": "在测试设置之前,请先保存更改。"
}
}

View File

@@ -100,6 +100,7 @@ class Device(ObjectType):
devParentPortSource = String(description="Source tracking for devParentPort (USER, LOCKED, NEWDEV, or plugin prefix)")
devParentRelTypeSource = String(description="Source tracking for devParentRelType (USER, LOCKED, NEWDEV, or plugin prefix)")
devVlanSource = String(description="Source tracking for devVlan")
devFlapping = String(description="ndicates flapping device (device changing between online/offline states frequently)")
class DeviceResult(ObjectType):
@@ -266,7 +267,7 @@ class Query(ObjectType):
filtered.append(device)
devices_data = filtered
# 🔻 START If you change anything here, also update get_device_condition_by_status
# 🔻 START If you change anything here, also update get_device_conditions
elif status == "connected":
devices_data = [
device
@@ -323,7 +324,25 @@ class Query(ObjectType):
for device in devices_data
if device["devType"] in network_dev_types and device["devPresentLastScan"] == 0 and device["devIsArchived"] == 0
]
# 🔺 END If you change anything here, also update get_device_condition_by_status
elif status == "unstable_devices":
devices_data = [
device
for device in devices_data
if device["devIsArchived"] == 0 and device["devFlapping"] == 1
]
elif status == "unstable_favorites":
devices_data = [
device
for device in devices_data
if device["devIsArchived"] == 0 and device["devFavorite"] == 1 and device["devFlapping"] == 1
]
elif status == "unstable_network_devices":
devices_data = [
device
for device in devices_data
if device["devIsArchived"] == 0 and device["devType"] in network_dev_types and device["devFlapping"] == 1
]
# 🔺 END If you change anything here, also update get_device_conditions
elif status == "all_devices":
devices_data = devices_data # keep all

View File

@@ -58,70 +58,13 @@ NULL_EQUIVALENTS = ["", "null", "(unknown)", "(Unknown)", "(name not found)"]
# Convert list to SQL string: wrap each value in single quotes and escape single quotes if needed
NULL_EQUIVALENTS_SQL = ",".join("'" + v.replace("'", "''") + "'" for v in NULL_EQUIVALENTS)
# ===============================================================================
# SQL queries
# ===============================================================================
sql_devices_all = """
SELECT
rowid,
IFNULL(devMac, '') AS devMac,
IFNULL(devName, '') AS devName,
IFNULL(devOwner, '') AS devOwner,
IFNULL(devType, '') AS devType,
IFNULL(devVendor, '') AS devVendor,
IFNULL(devFavorite, '') AS devFavorite,
IFNULL(devGroup, '') AS devGroup,
IFNULL(devComments, '') AS devComments,
IFNULL(devFirstConnection, '') AS devFirstConnection,
IFNULL(devLastConnection, '') AS devLastConnection,
IFNULL(devLastIP, '') AS devLastIP,
IFNULL(devPrimaryIPv4, '') AS devPrimaryIPv4,
IFNULL(devPrimaryIPv6, '') AS devPrimaryIPv6,
IFNULL(devVlan, '') AS devVlan,
IFNULL(devForceStatus, '') AS devForceStatus,
IFNULL(devStaticIP, '') AS devStaticIP,
IFNULL(devScan, '') AS devScan,
IFNULL(devLogEvents, '') AS devLogEvents,
IFNULL(devAlertEvents, '') AS devAlertEvents,
IFNULL(devAlertDown, '') AS devAlertDown,
IFNULL(devSkipRepeated, '') AS devSkipRepeated,
IFNULL(devLastNotification, '') AS devLastNotification,
IFNULL(devPresentLastScan, 0) AS devPresentLastScan,
IFNULL(devIsNew, '') AS devIsNew,
IFNULL(devLocation, '') AS devLocation,
IFNULL(devIsArchived, '') AS devIsArchived,
IFNULL(devParentMAC, '') AS devParentMAC,
IFNULL(devParentPort, '') AS devParentPort,
IFNULL(devIcon, '') AS devIcon,
IFNULL(devGUID, '') AS devGUID,
IFNULL(devSite, '') AS devSite,
IFNULL(devSSID, '') AS devSSID,
IFNULL(devSyncHubNode, '') AS devSyncHubNode,
IFNULL(devSourcePlugin, '') AS devSourcePlugin,
IFNULL(devCustomProps, '') AS devCustomProps,
IFNULL(devFQDN, '') AS devFQDN,
IFNULL(devParentRelType, '') AS devParentRelType,
IFNULL(devReqNicsOnline, '') AS devReqNicsOnline,
IFNULL(devMacSource, '') AS devMacSource,
IFNULL(devNameSource, '') AS devNameSource,
IFNULL(devFQDNSource, '') AS devFQDNSource,
IFNULL(devLastIPSource, '') AS devLastIPSource,
IFNULL(devVendorSource, '') AS devVendorSource,
IFNULL(devSSIDSource, '') AS devSSIDSource,
IFNULL(devParentMACSource, '') AS devParentMACSource,
IFNULL(devParentPortSource, '') AS devParentPortSource,
IFNULL(devParentRelTypeSource, '') AS devParentRelTypeSource,
IFNULL(devVlanSource, '') AS devVlanSource,
CASE
WHEN devIsNew = 1 THEN 'New'
WHEN devPresentLastScan = 1 THEN 'On-line'
WHEN devPresentLastScan = 0 AND devAlertDown != 0 THEN 'Down'
WHEN devIsArchived = 1 THEN 'Archived'
WHEN devPresentLastScan = 0 THEN 'Off-line'
ELSE 'Unknown status'
END AS devStatus
FROM Devices
sql_devices_all = """
SELECT
*
FROM DevicesView
"""
sql_appevents = """select * from AppEvents order by DateTimeCreated desc"""

View File

@@ -14,22 +14,28 @@ from const import NULL_EQUIVALENTS_SQL # noqa: E402 [flake8 lint suppression]
def get_device_conditions():
network_dev_types = ",".join("'" + v.replace("'", "''") + "'" for v in get_setting_value("NETWORK_DEVICE_TYPES"))
# DO NOT CHANGE ORDER
# Base archived condition
base_active = "devIsArchived=0"
# DO NOT CHANGE ORDER - if you add or change something update graphql endpoint as well
conditions = {
"all": "WHERE devIsArchived=0",
"my": "WHERE devIsArchived=0",
"all": f"WHERE {base_active}",
"my": f"WHERE {base_active}",
"connected": "WHERE devPresentLastScan=1",
"favorites": "WHERE devIsArchived=0 AND devFavorite=1",
"new": "WHERE devIsArchived=0 AND devIsNew=1",
"down": "WHERE devIsArchived=0 AND devAlertDown != 0 AND devPresentLastScan=0",
"offline": "WHERE devIsArchived=0 AND devPresentLastScan=0",
"favorites": f"WHERE {base_active} AND devFavorite=1",
"new": f"WHERE {base_active} AND devIsNew=1",
"down": f"WHERE {base_active} AND devAlertDown != 0 AND devPresentLastScan=0",
"offline": f"WHERE {base_active} AND devPresentLastScan=0",
"archived": "WHERE devIsArchived=1",
"network_devices": f"WHERE devIsArchived=0 AND devType in ({network_dev_types})",
"network_devices_down": f"WHERE devIsArchived=0 AND devType in ({network_dev_types}) AND devPresentLastScan=0",
"unknown": f"WHERE devIsArchived=0 AND devName in ({NULL_EQUIVALENTS_SQL})",
"known": f"WHERE devIsArchived=0 AND devName not in ({NULL_EQUIVALENTS_SQL})",
"favorites_offline": "WHERE devIsArchived=0 AND devFavorite=1 AND devPresentLastScan=0",
"new_online": "WHERE devIsArchived=0 AND devIsNew=1 AND devPresentLastScan=0",
"network_devices": f"WHERE {base_active} AND devType IN ({network_dev_types})",
"network_devices_down": f"WHERE {base_active} AND devType IN ({network_dev_types}) AND devPresentLastScan=0",
"unknown": f"WHERE {base_active} AND devName IN ({NULL_EQUIVALENTS_SQL})",
"known": f"WHERE {base_active} AND devName NOT IN ({NULL_EQUIVALENTS_SQL})",
"favorites_offline": f"WHERE {base_active} AND devFavorite=1 AND devPresentLastScan=0",
"new_online": f"WHERE {base_active} AND devIsNew=1 AND devPresentLastScan=0",
"unstable_devices": f"WHERE {base_active} AND devFlapping=1",
"unstable_favorites": f"WHERE {base_active} AND devFavorite=1 AND devFlapping=1",
"unstable_network_devices": f"WHERE {base_active} AND devType IN ({network_dev_types}) AND devFlapping=1",
}
return conditions

View File

@@ -232,6 +232,87 @@ def ensure_views(sql) -> bool:
""")
FLAP_THRESHOLD = 3
FLAP_WINDOW_HOURS = 1
sql.execute(""" DROP VIEW IF EXISTS DevicesView;""")
sql.execute(f""" CREATE VIEW DevicesView AS
SELECT
rowid,
IFNULL(devMac, '') AS devMac,
IFNULL(devName, '') AS devName,
IFNULL(devOwner, '') AS devOwner,
IFNULL(devType, '') AS devType,
IFNULL(devVendor, '') AS devVendor,
IFNULL(devFavorite, '') AS devFavorite,
IFNULL(devGroup, '') AS devGroup,
IFNULL(devComments, '') AS devComments,
IFNULL(devFirstConnection, '') AS devFirstConnection,
IFNULL(devLastConnection, '') AS devLastConnection,
IFNULL(devLastIP, '') AS devLastIP,
IFNULL(devPrimaryIPv4, '') AS devPrimaryIPv4,
IFNULL(devPrimaryIPv6, '') AS devPrimaryIPv6,
IFNULL(devVlan, '') AS devVlan,
IFNULL(devForceStatus, '') AS devForceStatus,
IFNULL(devStaticIP, '') AS devStaticIP,
IFNULL(devScan, '') AS devScan,
IFNULL(devLogEvents, '') AS devLogEvents,
IFNULL(devAlertEvents, '') AS devAlertEvents,
IFNULL(devAlertDown, '') AS devAlertDown,
IFNULL(devSkipRepeated, '') AS devSkipRepeated,
IFNULL(devLastNotification, '') AS devLastNotification,
IFNULL(devPresentLastScan, 0) AS devPresentLastScan,
IFNULL(devIsNew, '') AS devIsNew,
IFNULL(devLocation, '') AS devLocation,
IFNULL(devIsArchived, '') AS devIsArchived,
IFNULL(devParentMAC, '') AS devParentMAC,
IFNULL(devParentPort, '') AS devParentPort,
IFNULL(devIcon, '') AS devIcon,
IFNULL(devGUID, '') AS devGUID,
IFNULL(devSite, '') AS devSite,
IFNULL(devSSID, '') AS devSSID,
IFNULL(devSyncHubNode, '') AS devSyncHubNode,
IFNULL(devSourcePlugin, '') AS devSourcePlugin,
IFNULL(devCustomProps, '') AS devCustomProps,
IFNULL(devFQDN, '') AS devFQDN,
IFNULL(devParentRelType, '') AS devParentRelType,
IFNULL(devReqNicsOnline, '') AS devReqNicsOnline,
IFNULL(devMacSource, '') AS devMacSource,
IFNULL(devNameSource, '') AS devNameSource,
IFNULL(devFQDNSource, '') AS devFQDNSource,
IFNULL(devLastIPSource, '') AS devLastIPSource,
IFNULL(devVendorSource, '') AS devVendorSource,
IFNULL(devSSIDSource, '') AS devSSIDSource,
IFNULL(devParentMACSource, '') AS devParentMACSource,
IFNULL(devParentPortSource, '') AS devParentPortSource,
IFNULL(devParentRelTypeSource, '') AS devParentRelTypeSource,
IFNULL(devVlanSource, '') AS devVlanSource,
CASE
WHEN devIsNew = 1 THEN 'New'
WHEN devPresentLastScan = 1 THEN 'On-line'
WHEN devPresentLastScan = 0 AND devAlertDown != 0 THEN 'Down'
WHEN devIsArchived = 1 THEN 'Archived'
WHEN devPresentLastScan = 0 THEN 'Off-line'
ELSE 'Unknown status'
END AS devStatus,
CASE
WHEN EXISTS (
SELECT 1
FROM Events e
WHERE e.eve_MAC = Devices.devMac
AND e.eve_EventType IN ('Connected','Disconnected','Device Down','Down Reconnected')
AND e.eve_DateTime >= datetime('now', '-{FLAP_WINDOW_HOURS} hours')
GROUP BY e.eve_MAC
HAVING COUNT(*) >= {FLAP_THRESHOLD}
)
THEN 1
ELSE 0
END AS devFlapping
FROM Devices
""")
return True

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 *