BE+FE: refactor totals retrieval + LUCIRPC old field name

Signed-off-by: jokob-sk <jokob.sk@gmail.com>
This commit is contained in:
jokob-sk
2026-02-10 07:39:11 +11:00
parent cedbd59897
commit e899f657c5
10 changed files with 142 additions and 72 deletions

View File

@@ -529,7 +529,7 @@
}, },
{ {
"column": "Watched_Value2", "column": "Watched_Value2",
"mapped_to_column": "cur_NAME", "mapped_to_column": "scanName",
"css_classes": "col-sm-2", "css_classes": "col-sm-2",
"show": true, "show": true,
"type": "label", "type": "label",

View File

@@ -10,8 +10,8 @@ sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from plugin_helper import Plugin_Objects, handleEmpty # noqa: E402 [flake8 lint suppression] from plugin_helper import Plugin_Objects, handleEmpty # noqa: E402 [flake8 lint suppression]
from logger import mylog, Logger # noqa: E402 [flake8 lint suppression] from logger import mylog, Logger # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value # noqa: E402 [flake8 lint suppression] from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
from const import logPath, applicationPath # noqa: E402 [flake8 lint suppression] from const import logPath, applicationPath, NULL_EQUIVALENTS_SQL # noqa: E402 [flake8 lint suppression]
from scan.device_handling import query_MAC_vendor # noqa: E402 [flake8 lint suppression] from scan.device_handling import query_MAC_vendor # noqa: E402 [flake8 lint suppression]
import conf # noqa: E402 [flake8 lint suppression] import conf # noqa: E402 [flake8 lint suppression]
from pytz import timezone # noqa: E402 [flake8 lint suppression] from pytz import timezone # noqa: E402 [flake8 lint suppression]
@@ -83,17 +83,16 @@ def update_vendors(plugin_objects):
mylog('verbose', [' Searching devices vendor']) mylog('verbose', [' Searching devices vendor'])
# Get devices without a vendor # Get devices without a vendor
cursor.execute("""SELECT query = f"""
devMac, SELECT
devLastIP, devMac,
devName, devLastIP,
devVendor devName,
FROM Devices devVendor
WHERE devVendor = '(unknown)' FROM Devices
OR devVendor = '(Unknown)' WHERE devVendor IN ({NULL_EQUIVALENTS_SQL}) OR devVendor IS NULL
OR devVendor = '' """
OR devVendor IS NULL cursor.execute(query)
""")
devices = cursor.fetchall() devices = cursor.fetchall()
conn.commit() conn.commit()

View File

@@ -11,7 +11,7 @@ INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
sys.path.extend([f"{INSTALL_PATH}/server"]) sys.path.extend([f"{INSTALL_PATH}/server"])
from logger import mylog # noqa: E402 [flake8 lint suppression] from logger import mylog # noqa: E402 [flake8 lint suppression]
from const import apiPath # noqa: E402 [flake8 lint suppression] from const import apiPath, NULL_EQUIVALENTS # noqa: E402 [flake8 lint suppression]
from helper import ( # noqa: E402 [flake8 lint suppression] from helper import ( # noqa: E402 [flake8 lint suppression]
is_random_mac, is_random_mac,
get_number_of_children, get_number_of_children,
@@ -266,7 +266,7 @@ class Query(ObjectType):
filtered.append(device) filtered.append(device)
devices_data = filtered devices_data = filtered
# 🔻 START If you change anything here, also update get_device_condition_by_status
elif status == "connected": elif status == "connected":
devices_data = [ devices_data = [
device device
@@ -275,17 +275,17 @@ class Query(ObjectType):
] ]
elif status == "favorites": elif status == "favorites":
devices_data = [ devices_data = [
device for device in devices_data if device["devFavorite"] == 1 device for device in devices_data if device["devFavorite"] == 1 and device["devIsArchived"] == 0
] ]
elif status == "new": elif status == "new":
devices_data = [ devices_data = [
device for device in devices_data if device["devIsNew"] == 1 device for device in devices_data if device["devIsNew"] == 1 and device["devIsArchived"] == 0
] ]
elif status == "down": elif status == "down":
devices_data = [ devices_data = [
device device
for device in devices_data for device in devices_data
if device["devPresentLastScan"] == 0 and device["devAlertDown"] if device["devPresentLastScan"] == 0 and device["devAlertDown"] and device["devIsArchived"] == 0
] ]
elif status == "archived": elif status == "archived":
devices_data = [ devices_data = [
@@ -297,14 +297,33 @@ class Query(ObjectType):
devices_data = [ devices_data = [
device device
for device in devices_data for device in devices_data
if device["devPresentLastScan"] == 0 if device["devPresentLastScan"] == 0 and device["devIsArchived"] == 0
]
elif status == "unknown":
devices_data = [
device
for device in devices_data
if device["devName"] in NULL_EQUIVALENTS and device["devIsArchived"] == 0
]
elif status == "known":
devices_data = [
device
for device in devices_data
if device["devName"] not in NULL_EQUIVALENTS and device["devIsArchived"] == 0
] ]
elif status == "network_devices": elif status == "network_devices":
devices_data = [ devices_data = [
device device
for device in devices_data for device in devices_data
if device["devType"] in network_dev_types if device["devType"] in network_dev_types and device["devIsArchived"] == 0
] ]
elif status == "network_devices_down":
devices_data = [
device
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 == "all_devices": elif status == "all_devices":
devices_data = devices_data # keep all devices_data = devices_data # keep all

View File

@@ -49,6 +49,15 @@ NATIVE_SPEEDTEST_PATH = os.getenv("NATIVE_SPEEDTEST_PATH", "/usr/bin/speedtest")
default_tz = "Europe/Berlin" default_tz = "Europe/Berlin"
# ===============================================================================
# Magic strings
# ===============================================================================
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(f"'{v.replace('\'', '\'\'')}'" for v in NULL_EQUIVALENTS)
# =============================================================================== # ===============================================================================
# SQL queries # SQL queries
@@ -186,10 +195,19 @@ sql_devices_filters = """
FROM Devices WHERE devSSID NOT IN ('', 'null') AND devSSID IS NOT NULL FROM Devices WHERE devSSID NOT IN ('', 'null') AND devSSID IS NOT NULL
ORDER BY columnName; ORDER BY columnName;
""" """
sql_devices_stats = """SELECT Online_Devices as online, Down_Devices as down, All_Devices as 'all', Archived_Devices as archived,
(select count(*) from Devices a where devIsNew = 1 ) as new, sql_devices_stats = f"""
(select count(*) from Devices a where devName = '(unknown)' or devName = '(name not found)' ) as unknown SELECT
from Online_History order by Scan_Date desc limit 1""" Online_Devices as online,
Down_Devices as down,
All_Devices as 'all',
Archived_Devices as archived,
(SELECT COUNT(*) FROM Devices a WHERE devIsNew = 1) as new,
(SELECT COUNT(*) FROM Devices a WHERE devName IN ({NULL_EQUIVALENTS_SQL}) OR devName IS NULL) as unknown
FROM Online_History
ORDER BY Scan_Date DESC
LIMIT 1
"""
sql_events_pending_alert = "SELECT * FROM Events where eve_PendingAlertEmail is not 0" sql_events_pending_alert = "SELECT * FROM Events where eve_PendingAlertEmail is not 0"
sql_settings = "SELECT * FROM Settings" sql_settings = "SELECT * FROM Settings"
sql_plugins_objects = "SELECT * FROM Plugins_Objects" sql_plugins_objects = "SELECT * FROM Plugins_Objects"

View File

@@ -19,6 +19,7 @@ from logger import mylog # noqa: E402 [flake8 lint suppression]
from helper import get_setting_value # noqa: E402 [flake8 lint suppression] from helper import get_setting_value # noqa: E402 [flake8 lint suppression]
from db.db_helper import row_to_json # noqa: E402 [flake8 lint suppression] from db.db_helper import row_to_json # noqa: E402 [flake8 lint suppression]
from plugin_helper import normalize_mac # noqa: E402 [flake8 lint suppression] from plugin_helper import normalize_mac # noqa: E402 [flake8 lint suppression]
from const import NULL_EQUIVALENTS # noqa: E402 [flake8 lint suppression]
# Map of field to its source tracking field # Map of field to its source tracking field
@@ -96,7 +97,7 @@ def can_overwrite_field(field_name, current_value, current_source, plugin_prefix
bool: True if overwrite allowed. bool: True if overwrite allowed.
""" """
empty_values = ("0.0.0.0", "", "null", "(unknown)", "(name not found)", None) empty_values = ("0.0.0.0", *NULL_EQUIVALENTS, None)
# Rule 1: USER/LOCKED protected # Rule 1: USER/LOCKED protected
if current_source in ("USER", "LOCKED"): if current_source in ("USER", "LOCKED"):
@@ -188,9 +189,7 @@ def get_source_for_field_update_with_value(
if isinstance(field_value, str): if isinstance(field_value, str):
stripped = field_value.strip() stripped = field_value.strip()
if stripped in ("", "null"): if stripped.lower() in NULL_EQUIVALENTS:
return "NEWDEV"
if stripped.lower() in ("(unknown)", "(name not found)"):
return "NEWDEV" return "NEWDEV"
return plugin_prefix return plugin_prefix

View File

@@ -6,14 +6,34 @@ import os
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app") INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
sys.path.extend([f"{INSTALL_PATH}/server"]) sys.path.extend([f"{INSTALL_PATH}/server"])
from helper import if_byte_then_to_str # noqa: E402 [flake8 lint suppression] from helper import if_byte_then_to_str, get_setting_value # noqa: E402 [flake8 lint suppression]
from logger import mylog # noqa: E402 [flake8 lint suppression] from logger import mylog # noqa: E402 [flake8 lint suppression]
from const import NULL_EQUIVALENTS_SQL # noqa: E402 [flake8 lint suppression]
def get_device_conditions():
network_dev_types = ",".join(f"'{v.replace('\'', '\'\'')}'" for v in get_setting_value("NETWORK_DEVICE_TYPES"))
conditions = {
"all": "WHERE devIsArchived=0",
"my": "WHERE devIsArchived=0",
"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",
"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})",
}
return conditions
# ------------------------------------------------------------------------------- # -------------------------------------------------------------------------------
# Return the SQL WHERE clause for filtering devices based on their status. # Return the SQL WHERE clause for filtering devices based on their status.
def get_device_condition_by_status(device_status): def get_device_condition_by_status(device_status):
""" """
Return the SQL WHERE clause for filtering devices based on their status. Return the SQL WHERE clause for filtering devices based on their status.
@@ -32,17 +52,8 @@ def get_device_condition_by_status(device_status):
str: SQL WHERE clause corresponding to the device status. str: SQL WHERE clause corresponding to the device status.
Defaults to 'WHERE 1=0' for unrecognized statuses. Defaults to 'WHERE 1=0' for unrecognized statuses.
""" """
conditions = {
"all": "WHERE devIsArchived=0", return get_device_conditions().get(device_status, "WHERE 1=0")
"my": "WHERE devIsArchived=0",
"connected": "WHERE devIsArchived=0 AND 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",
"archived": "WHERE devIsArchived=1",
}
return conditions.get(device_status, "WHERE 1=0")
# ------------------------------------------------------------------------------- # -------------------------------------------------------------------------------

View File

@@ -228,7 +228,7 @@ def ensure_views(sql) -> bool:
) )
SELECT SELECT
d.*, -- all Device fields d.*, -- all Device fields
r.* -- all CurrentScan fields (cur_*) r.* -- all CurrentScan fields
FROM Devices d FROM Devices d
LEFT JOIN RankedScans r LEFT JOIN RankedScans r
ON d.devMac = r.scanMac ON d.devMac = r.scanMac

View File

@@ -8,7 +8,7 @@ from front.plugins.plugin_helper import is_mac, normalize_mac
from logger import mylog from logger import mylog
from models.plugin_object_instance import PluginObjectInstance from models.plugin_object_instance import PluginObjectInstance
from database import get_temp_db_connection from database import get_temp_db_connection
from db.db_helper import get_table_json, get_device_condition_by_status, row_to_json, get_date_from_period from db.db_helper import get_table_json, get_device_conditions, get_device_condition_by_status, row_to_json, get_date_from_period
from db.authoritative_handler import ( from db.authoritative_handler import (
enforce_source_on_user_update, enforce_source_on_user_update,
get_locked_field_overrides, get_locked_field_overrides,
@@ -331,22 +331,45 @@ class DeviceInstance:
conn = get_temp_db_connection() conn = get_temp_db_connection()
sql = conn.cursor() sql = conn.cursor()
# Build a combined query with sub-selects for each status conditions = get_device_conditions()
query = f"""
SELECT # Build sub-selects dynamically for all dictionary entries
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status("my")}) AS devices, sub_queries = []
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status("connected")}) AS connected, for key, condition in conditions.items():
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status("favorites")}) AS favorites, # Make sure the alias is SQL-safe (no spaces or special chars)
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status("new")}) AS new, alias = key.replace(" ", "_").lower()
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status("down")}) AS down, sub_queries.append(f'(SELECT COUNT(*) FROM Devices {condition}) AS "{alias}"')
(SELECT COUNT(*) FROM Devices {get_device_condition_by_status("archived")}) AS archived
""" # Join all sub-selects with commas
query = "SELECT\n " + ",\n ".join(sub_queries)
sql.execute(query) sql.execute(query)
row = sql.fetchone() row = sql.fetchone()
conn.close() conn.close()
return list(row) if row else [] return list(row) if row else []
def getNamedTotals(self):
"""Get device totals by status."""
conn = get_temp_db_connection()
sql = conn.cursor()
conditions = get_device_conditions()
# Build sub-selects dynamically for all dictionary entries
sub_queries = []
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}"')
# Join all sub-selects with commas
query = "SELECT\n " + ",\n ".join(sub_queries)
mylog('none', [f'[getNamedTotals] query {query}'])
json_obj = get_table_json(sql, query, parameters=None)
return json_obj
def getByStatus(self, status=None): def getByStatus(self, status=None):
""" """
Return devices filtered by status. Returns all if no status provided. Return devices filtered by status. Returns all if no status provided.

View File

@@ -5,7 +5,7 @@ import ipaddress
from helper import get_setting_value, check_IP_format from helper import get_setting_value, check_IP_format
from utils.datetime_utils import timeNowDB, normalizeTimeStamp from utils.datetime_utils import timeNowDB, normalizeTimeStamp
from logger import mylog, Logger from logger import mylog, Logger
from const import vendorsPath, vendorsPathNewest, sql_generateGuid from const import vendorsPath, vendorsPathNewest, sql_generateGuid, NULL_EQUIVALENTS
from models.device_instance import DeviceInstance from models.device_instance import DeviceInstance
from scan.name_resolution import NameResolver from scan.name_resolution import NameResolver
from scan.device_heuristics import guess_icon, guess_type from scan.device_heuristics import guess_icon, guess_type
@@ -97,7 +97,7 @@ FIELD_SPECS = {
"devName": { "devName": {
"scan_col": "scanName", "scan_col": "scanName",
"source_col": "devNameSource", "source_col": "devNameSource",
"empty_values": ["", "null", "(unknown)", "(name not found)"], "empty_values": NULL_EQUIVALENTS,
"default_value": "(unknown)", "default_value": "(unknown)",
"priority": ["NSLOOKUP", "AVAHISCAN", "NBTSCAN", "DIGSCAN", "ARPSCAN", "DHCPLSS", "NEWDEV", "N/A"], "priority": ["NSLOOKUP", "AVAHISCAN", "NBTSCAN", "DIGSCAN", "ARPSCAN", "DHCPLSS", "NEWDEV", "N/A"],
}, },
@@ -108,7 +108,7 @@ FIELD_SPECS = {
"devLastIP": { "devLastIP": {
"scan_col": "scanLastIP", "scan_col": "scanLastIP",
"source_col": "devLastIPSource", "source_col": "devLastIPSource",
"empty_values": ["", "null", "(unknown)", "(Unknown)"], "empty_values": NULL_EQUIVALENTS,
"priority": ["ARPSCAN", "NEWDEV", "N/A"], "priority": ["ARPSCAN", "NEWDEV", "N/A"],
"default_value": "0.0.0.0", "default_value": "0.0.0.0",
"allow_override_if_changed": True, "allow_override_if_changed": True,
@@ -120,7 +120,7 @@ FIELD_SPECS = {
"devVendor": { "devVendor": {
"scan_col": "scanVendor", "scan_col": "scanVendor",
"source_col": "devVendorSource", "source_col": "devVendorSource",
"empty_values": ["", "null", "(unknown)", "(Unknown)"], "empty_values": NULL_EQUIVALENTS,
"priority": ["VNDRPDT", "ARPSCAN", "NEWDEV", "N/A"], "priority": ["VNDRPDT", "ARPSCAN", "NEWDEV", "N/A"],
}, },
@@ -131,7 +131,7 @@ FIELD_SPECS = {
"devSyncHubNode": { "devSyncHubNode": {
"scan_col": "scanSyncHubNode", "scan_col": "scanSyncHubNode",
"source_col": None, "source_col": None,
"empty_values": ["", "null"], "empty_values": NULL_EQUIVALENTS,
"priority": None, "priority": None,
}, },
@@ -141,7 +141,7 @@ FIELD_SPECS = {
"devSite": { "devSite": {
"scan_col": "scanSite", "scan_col": "scanSite",
"source_col": None, "source_col": None,
"empty_values": ["", "null"], "empty_values": NULL_EQUIVALENTS,
"priority": None, "priority": None,
}, },
@@ -151,7 +151,7 @@ FIELD_SPECS = {
"devVlan": { "devVlan": {
"scan_col": "scanVlan", "scan_col": "scanVlan",
"source_col": "devVlanSource", "source_col": "devVlanSource",
"empty_values": ["", "null"], "empty_values": NULL_EQUIVALENTS,
"priority": None, "priority": None,
}, },
@@ -161,7 +161,7 @@ FIELD_SPECS = {
"devType": { "devType": {
"scan_col": "scanType", "scan_col": "scanType",
"source_col": None, "source_col": None,
"empty_values": ["", "null"], "empty_values": NULL_EQUIVALENTS,
"priority": None, "priority": None,
}, },
@@ -171,14 +171,14 @@ FIELD_SPECS = {
"devParentMAC": { "devParentMAC": {
"scan_col": "scanParentMAC", "scan_col": "scanParentMAC",
"source_col": "devParentMACSource", "source_col": "devParentMACSource",
"empty_values": ["", "null"], "empty_values": NULL_EQUIVALENTS,
"priority": ["SNMPDSC", "UNIFIAPI", "UNFIMP", "NEWDEV", "N/A"], "priority": ["SNMPDSC", "UNIFIAPI", "UNFIMP", "NEWDEV", "N/A"],
}, },
"devParentPort": { "devParentPort": {
"scan_col": "scanParentPort", "scan_col": "scanParentPort",
"source_col": None, "source_col": None,
"empty_values": ["", "null"], "empty_values": NULL_EQUIVALENTS,
"priority": ["SNMPDSC", "UNIFIAPI", "UNFIMP", "NEWDEV", "N/A"], "priority": ["SNMPDSC", "UNIFIAPI", "UNFIMP", "NEWDEV", "N/A"],
}, },
@@ -188,7 +188,7 @@ FIELD_SPECS = {
"devSSID": { "devSSID": {
"scan_col": "scanSSID", "scan_col": "scanSSID",
"source_col": None, "source_col": None,
"empty_values": ["", "null"], "empty_values": NULL_EQUIVALENTS,
"priority": ["SNMPDSC", "UNIFIAPI", "UNFIMP", "NEWDEV", "N/A"], "priority": ["SNMPDSC", "UNIFIAPI", "UNFIMP", "NEWDEV", "N/A"],
}, },
} }
@@ -708,16 +708,16 @@ def create_new_devices(db):
raw_name = str(scanName).strip() if scanName else "" raw_name = str(scanName).strip() if scanName else ""
raw_vendor = str(scanVendor).strip() if scanVendor else "" raw_vendor = str(scanVendor).strip() if scanVendor else ""
raw_ip = str(scanLastIP).strip() if scanLastIP else "" raw_ip = str(scanLastIP).strip() if scanLastIP else ""
if raw_ip.lower() in ("null", "(unknown)"): if raw_ip.lower() in NULL_EQUIVALENTS:
raw_ip = "" raw_ip = ""
raw_ssid = str(scanSSID).strip() if scanSSID else "" raw_ssid = str(scanSSID).strip() if scanSSID else ""
if raw_ssid.lower() in ("null", "(unknown)"): if raw_ssid.lower() in NULL_EQUIVALENTS:
raw_ssid = "" raw_ssid = ""
raw_parent_mac = str(scanParentMAC).strip() if scanParentMAC else "" raw_parent_mac = str(scanParentMAC).strip() if scanParentMAC else ""
if raw_parent_mac.lower() in ("null", "(unknown)"): if raw_parent_mac.lower() in NULL_EQUIVALENTS:
raw_parent_mac = "" raw_parent_mac = ""
raw_parent_port = str(scanParentPort).strip() if scanParentPort else "" raw_parent_port = str(scanParentPort).strip() if scanParentPort else ""
if raw_parent_port.lower() in ("null", "(unknown)"): if raw_parent_port.lower() in NULL_EQUIVALENTS:
raw_parent_port = "" raw_parent_port = ""
# Handle NoneType # Handle NoneType

View File

@@ -18,6 +18,7 @@ from utils.datetime_utils import timeNowDB
from logger import mylog, Logger from logger import mylog, Logger
from messaging.reporting import skip_repeated_notifications from messaging.reporting import skip_repeated_notifications
from messaging.in_app import update_unread_notifications_count from messaging.in_app import update_unread_notifications_count
from const import NULL_EQUIVALENTS, NULL_EQUIVALENTS_SQL
# Make sure log level is initialized correctly # Make sure log level is initialized correctly
@@ -222,7 +223,7 @@ def insert_events(db):
FROM Devices, CurrentScan FROM Devices, CurrentScan
WHERE devMac = scanMac WHERE devMac = scanMac
AND scanLastIP IS NOT NULL AND scanLastIP IS NOT NULL
AND scanLastIP NOT IN ('', 'null', '(unknown)', '(Unknown)') AND scanLastIP NOT IN ({NULL_EQUIVALENTS_SQL})
AND scanLastIP <> COALESCE(devPrimaryIPv4, '') AND scanLastIP <> COALESCE(devPrimaryIPv4, '')
AND scanLastIP <> COALESCE(devPrimaryIPv6, '') AND scanLastIP <> COALESCE(devPrimaryIPv6, '')
AND scanLastIP <> COALESCE(devLastIP, '') """) AND scanLastIP <> COALESCE(devLastIP, '') """)