mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-31 07:12:23 -07:00
sleeping devices status #1519
This commit is contained in:
@@ -101,6 +101,8 @@ class Device(ObjectType):
|
||||
devParentRelTypeSource = String(description="Source tracking for devParentRelType (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devVlanSource = String(description="Source tracking for devVlan")
|
||||
devFlapping = Int(description="Indicates flapping device (device changing between online/offline states frequently)")
|
||||
devCanSleep = Int(description="Can this device sleep? (0 or 1). When enabled, offline periods within NTFPRCS_sleep_time are reported as Sleeping instead of Down.")
|
||||
devIsSleeping = Int(description="Computed: Is device currently in a sleep window? (0 or 1)")
|
||||
|
||||
|
||||
class DeviceResult(ObjectType):
|
||||
@@ -247,7 +249,7 @@ class Query(ObjectType):
|
||||
)
|
||||
|
||||
is_down = (
|
||||
device["devPresentLastScan"] == 0 and device["devAlertDown"] and "down" in allowed_statuses
|
||||
device["devPresentLastScan"] == 0 and device["devAlertDown"] and device.get("devIsSleeping", 0) == 0 and "down" in allowed_statuses
|
||||
)
|
||||
|
||||
is_offline = (
|
||||
@@ -282,11 +284,17 @@ class Query(ObjectType):
|
||||
devices_data = [
|
||||
device for device in devices_data if device["devIsNew"] == 1 and device["devIsArchived"] == 0
|
||||
]
|
||||
elif status == "sleeping":
|
||||
devices_data = [
|
||||
device
|
||||
for device in devices_data
|
||||
if device.get("devIsSleeping", 0) == 1 and device["devIsArchived"] == 0
|
||||
]
|
||||
elif status == "down":
|
||||
devices_data = [
|
||||
device
|
||||
for device in devices_data
|
||||
if device["devPresentLastScan"] == 0 and device["devAlertDown"] and device["devIsArchived"] == 0
|
||||
if device["devPresentLastScan"] == 0 and device["devAlertDown"] and device.get("devIsSleeping", 0) == 0 and device["devIsArchived"] == 0
|
||||
]
|
||||
elif status == "archived":
|
||||
devices_data = [
|
||||
|
||||
@@ -35,7 +35,7 @@ COLUMN_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9_]+$")
|
||||
ALLOWED_DEVICE_COLUMNS = Literal[
|
||||
"devName", "devOwner", "devType", "devVendor",
|
||||
"devGroup", "devLocation", "devComments", "devFavorite",
|
||||
"devParentMAC"
|
||||
"devParentMAC", "devCanSleep"
|
||||
]
|
||||
|
||||
ALLOWED_NMAP_MODES = Literal[
|
||||
@@ -204,9 +204,19 @@ class DeviceInfo(BaseModel):
|
||||
description="Present in last scan (0 or 1)",
|
||||
json_schema_extra={"enum": [0, 1]}
|
||||
)
|
||||
devStatus: Optional[Literal["online", "offline"]] = Field(
|
||||
devStatus: Optional[Literal["online", "offline", "sleeping"]] = Field(
|
||||
None,
|
||||
description="Online/Offline status"
|
||||
description="Online/Offline/Sleeping status"
|
||||
)
|
||||
devCanSleep: Optional[int] = Field(
|
||||
0,
|
||||
description="Can device sleep? (0=No, 1=Yes). When enabled, offline periods within NTFPRCS_sleep_time window are shown as Sleeping.",
|
||||
json_schema_extra={"enum": [0, 1]}
|
||||
)
|
||||
devIsSleeping: Optional[int] = Field(
|
||||
0,
|
||||
description="Computed: Is device currently in a sleep window? (0=No, 1=Yes)",
|
||||
json_schema_extra={"enum": [0, 1]}
|
||||
)
|
||||
devMacSource: Optional[str] = Field(None, description="Source of devMac (USER, LOCKED, or plugin prefix)")
|
||||
devNameSource: Optional[str] = Field(None, description="Source of devName")
|
||||
@@ -228,14 +238,15 @@ class DeviceSearchResponse(BaseResponse):
|
||||
class DeviceListRequest(BaseModel):
|
||||
"""Request for listing devices by status."""
|
||||
status: Optional[Literal[
|
||||
"connected", "down", "favorites", "new", "archived", "all", "my",
|
||||
"connected", "down", "sleeping", "favorites", "new", "archived", "all", "my",
|
||||
"offline"
|
||||
]] = Field(
|
||||
None,
|
||||
description=(
|
||||
"Filter devices by status:\n"
|
||||
"- connected: Active devices present in the last scan\n"
|
||||
"- down: Devices with active 'Device Down' alert\n"
|
||||
"- down: Devices with active 'Device Down' alert (excludes sleeping)\n"
|
||||
"- sleeping: Devices in a sleep window (devCanSleep=1, offline within NTFPRCS_sleep_time)\n"
|
||||
"- favorites: Devices marked as favorite\n"
|
||||
"- new: Devices flagged as new\n"
|
||||
"- archived: Devices moved to archive\n"
|
||||
|
||||
@@ -10,7 +10,6 @@ from db.db_helper import get_table_json, json_obj
|
||||
from workflows.app_events import AppEvent_obj
|
||||
from db.db_upgrade import (
|
||||
ensure_column,
|
||||
ensure_views,
|
||||
ensure_CurrentScan,
|
||||
ensure_plugins_tables,
|
||||
ensure_Parameters,
|
||||
@@ -192,6 +191,8 @@ class DB:
|
||||
raise RuntimeError("ensure_column(devParentRelTypeSource) failed")
|
||||
if not ensure_column(self.sql, "Devices", "devVlanSource", "TEXT"):
|
||||
raise RuntimeError("ensure_column(devVlanSource) failed")
|
||||
if not ensure_column(self.sql, "Devices", "devCanSleep", "INTEGER"):
|
||||
raise RuntimeError("ensure_column(devCanSleep) failed")
|
||||
|
||||
# Settings table setup
|
||||
ensure_Settings(self.sql)
|
||||
@@ -208,8 +209,9 @@ class DB:
|
||||
# CurrentScan table setup
|
||||
ensure_CurrentScan(self.sql)
|
||||
|
||||
# Views
|
||||
ensure_views(self.sql)
|
||||
# Views are created in importConfigs() after settings are committed,
|
||||
# so NTFPRCS_sleep_time is available when the view is built.
|
||||
# ensure_views is NOT called here.
|
||||
|
||||
# Indexes
|
||||
ensure_Indexes(self.sql)
|
||||
|
||||
@@ -24,7 +24,8 @@ def get_device_conditions():
|
||||
"connected": "WHERE devPresentLastScan=1",
|
||||
"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",
|
||||
"sleeping": f"WHERE {base_active} AND devIsSleeping=1",
|
||||
"down": f"WHERE {base_active} AND devAlertDown != 0 AND devPresentLastScan=0 AND devIsSleeping=0",
|
||||
"offline": f"WHERE {base_active} AND devPresentLastScan=0",
|
||||
"archived": "WHERE devIsArchived=1",
|
||||
"network_devices": f"WHERE {base_active} AND devType IN ({network_dev_types})",
|
||||
|
||||
@@ -28,6 +28,7 @@ EXPECTED_DEVICES_COLUMNS = [
|
||||
"devLogEvents",
|
||||
"devAlertEvents",
|
||||
"devAlertDown",
|
||||
"devCanSleep",
|
||||
"devSkipRepeated",
|
||||
"devLastNotification",
|
||||
"devPresentLastScan",
|
||||
@@ -235,8 +236,22 @@ def ensure_views(sql) -> bool:
|
||||
FLAP_THRESHOLD = 3
|
||||
FLAP_WINDOW_HOURS = 1
|
||||
|
||||
# Read sleep window from settings; fall back to 30 min if not yet configured.
|
||||
# Uses the same sql cursor (no separate connection) to avoid lock contention.
|
||||
# Note: changing NTFPRCS_sleep_time requires a restart to take effect,
|
||||
# same behaviour as FLAP_THRESHOLD / FLAP_WINDOW_HOURS.
|
||||
try:
|
||||
sql.execute("SELECT setValue FROM Settings WHERE setKey = 'NTFPRCS_sleep_time'")
|
||||
_sleep_row = sql.fetchone()
|
||||
SLEEP_MINUTES = int(_sleep_row[0]) if _sleep_row and _sleep_row[0] else 30
|
||||
except Exception:
|
||||
SLEEP_MINUTES = 30
|
||||
|
||||
sql.execute(""" DROP VIEW IF EXISTS DevicesView;""")
|
||||
sql.execute(f""" CREATE VIEW DevicesView AS
|
||||
-- CTE computes devIsSleeping and devFlapping so devStatus can
|
||||
-- reference them without duplicating the sub-expressions.
|
||||
WITH base AS (
|
||||
SELECT
|
||||
rowid,
|
||||
IFNULL(devMac, '') AS devMac,
|
||||
@@ -259,6 +274,7 @@ def ensure_views(sql) -> bool:
|
||||
IFNULL(devLogEvents, '') AS devLogEvents,
|
||||
IFNULL(devAlertEvents, '') AS devAlertEvents,
|
||||
IFNULL(devAlertDown, '') AS devAlertDown,
|
||||
IFNULL(devCanSleep, 0) AS devCanSleep,
|
||||
IFNULL(devSkipRepeated, '') AS devSkipRepeated,
|
||||
IFNULL(devLastNotification, '') AS devLastNotification,
|
||||
IFNULL(devPresentLastScan, 0) AS devPresentLastScan,
|
||||
@@ -287,14 +303,15 @@ def ensure_views(sql) -> bool:
|
||||
IFNULL(devParentPortSource, '') AS devParentPortSource,
|
||||
IFNULL(devParentRelTypeSource, '') AS devParentRelTypeSource,
|
||||
IFNULL(devVlanSource, '') AS devVlanSource,
|
||||
-- devIsSleeping: opted-in, absent, and still within the sleep window
|
||||
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,
|
||||
WHEN devCanSleep = 1
|
||||
AND devPresentLastScan = 0
|
||||
AND devLastConnection >= datetime('now', '-{SLEEP_MINUTES} minutes')
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END AS devIsSleeping,
|
||||
-- devFlapping: toggling online/offline frequently within the flap window
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
@@ -308,8 +325,20 @@ def ensure_views(sql) -> bool:
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END AS devFlapping
|
||||
|
||||
FROM Devices
|
||||
FROM Devices
|
||||
)
|
||||
SELECT *,
|
||||
-- devStatus references devIsSleeping from the CTE (no duplication)
|
||||
CASE
|
||||
WHEN devIsNew = 1 THEN 'New'
|
||||
WHEN devPresentLastScan = 1 THEN 'On-line'
|
||||
WHEN devIsSleeping = 1 THEN 'Sleeping'
|
||||
WHEN devAlertDown != 0 THEN 'Down'
|
||||
WHEN devIsArchived = 1 THEN 'Archived'
|
||||
WHEN devPresentLastScan = 0 THEN 'Off-line'
|
||||
ELSE 'Unknown status'
|
||||
END AS devStatus
|
||||
FROM base
|
||||
|
||||
""")
|
||||
|
||||
@@ -381,6 +410,10 @@ def ensure_Indexes(sql) -> bool:
|
||||
"idx_dev_alertdown",
|
||||
"CREATE INDEX idx_dev_alertdown ON Devices(devAlertDown)",
|
||||
),
|
||||
(
|
||||
"idx_dev_cansleep",
|
||||
"CREATE INDEX idx_dev_cansleep ON Devices(devCanSleep)",
|
||||
),
|
||||
("idx_dev_isnew", "CREATE INDEX idx_dev_isnew ON Devices(devIsNew)"),
|
||||
(
|
||||
"idx_dev_isarchived",
|
||||
|
||||
@@ -11,6 +11,7 @@ import uuid
|
||||
# Register NetAlertX libraries
|
||||
import conf
|
||||
from const import fullConfPath, fullConfFolder, default_tz, applicationPath
|
||||
from db.db_upgrade import ensure_views
|
||||
from helper import getBuildTimeStampAndVersion, collect_lang_strings, updateSubnets, generate_random_string
|
||||
from utils.datetime_utils import timeNowUTC
|
||||
from app_state import updateState
|
||||
@@ -733,6 +734,12 @@ def importConfigs(pm, db, all_plugins):
|
||||
|
||||
db.commitDB()
|
||||
|
||||
# Rebuild DevicesView now that settings (including NTFPRCS_sleep_time) are committed.
|
||||
# This is the single call site — initDB() deliberately skips it so the view
|
||||
# always gets the real user value, not an empty-Settings fallback.
|
||||
ensure_views(sql)
|
||||
db.commitDB()
|
||||
|
||||
# update only the settings datasource
|
||||
update_api(db, all_plugins, True, ["settings"])
|
||||
|
||||
|
||||
@@ -61,8 +61,8 @@ class DeviceInstance:
|
||||
|
||||
def getDown(self):
|
||||
return self._fetchall("""
|
||||
SELECT * FROM Devices
|
||||
WHERE devAlertDown = 1 AND devPresentLastScan = 0
|
||||
SELECT * FROM DevicesView
|
||||
WHERE devAlertDown = 1 AND devPresentLastScan = 0 AND devIsSleeping = 0
|
||||
""")
|
||||
|
||||
def getOffline(self):
|
||||
@@ -454,7 +454,9 @@ class DeviceInstance:
|
||||
"devPresenceHours": 0,
|
||||
"devFQDN": "",
|
||||
"devForceStatus" : "dont_force",
|
||||
"devVlan": ""
|
||||
"devVlan": "",
|
||||
"devCanSleep": 0,
|
||||
"devIsSleeping": 0
|
||||
}
|
||||
return device_data
|
||||
|
||||
@@ -467,11 +469,6 @@ class DeviceInstance:
|
||||
d.*,
|
||||
LOWER(d.devMac) AS devMac,
|
||||
LOWER(d.devParentMAC) AS devParentMAC,
|
||||
CASE
|
||||
WHEN d.devAlertDown != 0 AND d.devPresentLastScan = 0 THEN 'Down'
|
||||
WHEN d.devPresentLastScan = 1 THEN 'On-line'
|
||||
ELSE 'Off-line'
|
||||
END AS devStatus,
|
||||
|
||||
(SELECT COUNT(*) FROM Sessions
|
||||
WHERE LOWER(ses_MAC) = LOWER(d.devMac) AND (
|
||||
@@ -501,11 +498,10 @@ class DeviceInstance:
|
||||
OR ses_DateTimeDisconnection >= {period_date_sql} OR ses_StillConnected = 1)
|
||||
) AS devPresenceHours
|
||||
|
||||
FROM Devices d
|
||||
FROM DevicesView d
|
||||
WHERE LOWER(d.devMac) = LOWER(?) OR CAST(d.rowid AS TEXT) = ?
|
||||
"""
|
||||
|
||||
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute(sql, (mac, mac))
|
||||
@@ -571,7 +567,8 @@ class DeviceInstance:
|
||||
"devIsArchived",
|
||||
"devCustomProps",
|
||||
"devForceStatus",
|
||||
"devVlan"
|
||||
"devVlan",
|
||||
"devCanSleep"
|
||||
}
|
||||
|
||||
# Only mark USER for tracked fields that this method actually updates.
|
||||
@@ -617,12 +614,12 @@ class DeviceInstance:
|
||||
devMac, devName, devOwner, devType, devVendor, devIcon,
|
||||
devFavorite, devGroup, devLocation, devComments,
|
||||
devParentMAC, devParentPort, devSSID, devSite,
|
||||
devStaticIP, devScan, devAlertEvents, devAlertDown,
|
||||
devStaticIP, devScan, devAlertEvents, devAlertDown, devCanSleep,
|
||||
devParentRelType, devReqNicsOnline, devSkipRepeated,
|
||||
devIsNew, devIsArchived, devLastConnection,
|
||||
devFirstConnection, devLastIP, devGUID, devCustomProps,
|
||||
devSourcePlugin, devForceStatus, devVlan
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
values = (
|
||||
@@ -644,6 +641,7 @@ class DeviceInstance:
|
||||
data.get("devScan") or 0,
|
||||
data.get("devAlertEvents") or 0,
|
||||
data.get("devAlertDown") or 0,
|
||||
data.get("devCanSleep") or 0,
|
||||
data.get("devParentRelType") or "default",
|
||||
data.get("devReqNicsOnline") or 0,
|
||||
data.get("devSkipRepeated") or 0,
|
||||
@@ -665,7 +663,7 @@ class DeviceInstance:
|
||||
devName=?, devOwner=?, devType=?, devVendor=?, devIcon=?,
|
||||
devFavorite=?, devGroup=?, devLocation=?, devComments=?,
|
||||
devParentMAC=?, devParentPort=?, devSSID=?, devSite=?,
|
||||
devStaticIP=?, devScan=?, devAlertEvents=?, devAlertDown=?,
|
||||
devStaticIP=?, devScan=?, devAlertEvents=?, devAlertDown=?, devCanSleep=?,
|
||||
devParentRelType=?, devReqNicsOnline=?, devSkipRepeated=?,
|
||||
devIsNew=?, devIsArchived=?, devCustomProps=?, devForceStatus=?, devVlan=?
|
||||
WHERE devMac=?
|
||||
@@ -688,6 +686,7 @@ class DeviceInstance:
|
||||
data.get("devScan") or 0,
|
||||
data.get("devAlertEvents") or 0,
|
||||
data.get("devAlertDown") or 0,
|
||||
data.get("devCanSleep") or 0,
|
||||
data.get("devParentRelType") or "default",
|
||||
data.get("devReqNicsOnline") or 0,
|
||||
data.get("devSkipRepeated") or 0,
|
||||
@@ -834,7 +833,6 @@ class DeviceInstance:
|
||||
conn.close()
|
||||
return result
|
||||
|
||||
|
||||
def lockDeviceField(self, mac, field_name):
|
||||
"""Lock a device field so it won't be overwritten by plugins."""
|
||||
if field_name not in FIELD_SOURCE_MAP:
|
||||
|
||||
@@ -533,8 +533,8 @@ def print_scan_stats(db):
|
||||
SELECT
|
||||
(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 Devices WHERE devAlertDown != 0 AND NOT EXISTS (SELECT 1 FROM CurrentScan WHERE devMac = scanMac)) AS down_alerts,
|
||||
(SELECT COUNT(*) FROM Devices WHERE devAlertDown != 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 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 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
|
||||
|
||||
@@ -175,8 +175,9 @@ def insert_events(db):
|
||||
eve_EventType, eve_AdditionalInfo,
|
||||
eve_PendingAlertEmail)
|
||||
SELECT devMac, devLastIP, '{startTime}', 'Device Down', '', 1
|
||||
FROM Devices
|
||||
FROM DevicesView
|
||||
WHERE devAlertDown != 0
|
||||
AND devIsSleeping = 0
|
||||
AND devPresentLastScan = 1
|
||||
AND NOT EXISTS (SELECT 1 FROM CurrentScan
|
||||
WHERE devMac = scanMac
|
||||
@@ -242,8 +243,8 @@ def insertOnlineHistory(db):
|
||||
COUNT(*) AS allDevices,
|
||||
COALESCE(SUM(CASE WHEN devIsArchived = 1 THEN 1 ELSE 0 END), 0) AS archivedDevices,
|
||||
COALESCE(SUM(CASE WHEN devPresentLastScan = 1 THEN 1 ELSE 0 END), 0) AS onlineDevices,
|
||||
COALESCE(SUM(CASE WHEN devPresentLastScan = 0 AND devAlertDown = 1 THEN 1 ELSE 0 END), 0) AS downDevices
|
||||
FROM Devices
|
||||
COALESCE(SUM(CASE WHEN devPresentLastScan = 0 AND devAlertDown = 1 AND devIsSleeping = 0 THEN 1 ELSE 0 END), 0) AS downDevices
|
||||
FROM DevicesView
|
||||
"""
|
||||
|
||||
deviceCounts = db.read(query)[
|
||||
|
||||
Reference in New Issue
Block a user