From c3d38264483ba92aedcc74d7a0ee4460f1a5026c Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Mon, 26 Jan 2026 10:35:35 +1100 Subject: [PATCH] PLG: add PIHOLEAPI_CONSIDER_ONLINE #1436 Signed-off-by: jokob-sk --- front/plugins/pihole_api_scan/config.json | 28 +++++++ .../pihole_api_scan/pihole_api_scan.py | 75 ++++++++++++------- 2 files changed, 74 insertions(+), 29 deletions(-) diff --git a/front/plugins/pihole_api_scan/config.json b/front/plugins/pihole_api_scan/config.json index 2988d11e..2ed11f33 100644 --- a/front/plugins/pihole_api_scan/config.json +++ b/front/plugins/pihole_api_scan/config.json @@ -263,6 +263,34 @@ } ] }, + { + "function": "CONSIDER_ONLINE", + "type": { + "dataType": "integer", + "elements": [ + { + "elementType": "input", + "elementOptions": [{ "type": "number" }], + "transformers": [] + } + ] + }, + "default_value": 300, + "options": [], + "localized": ["name", "description"], + "name": [ + { + "language_code": "en_us", + "string": "Online if last seen" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Time in seconds to consider a device online." + } + ] + }, { "function": "VERIFY_SSL", "type": { diff --git a/front/plugins/pihole_api_scan/pihole_api_scan.py b/front/plugins/pihole_api_scan/pihole_api_scan.py index 56c92692..65cda801 100644 --- a/front/plugins/pihole_api_scan/pihole_api_scan.py +++ b/front/plugins/pihole_api_scan/pihole_api_scan.py @@ -196,57 +196,69 @@ def get_pihole_network_devices(): # ------------------------------------------------------------------ def gather_device_entries(): """ - Build a list of device entries suitable for Plugin_Objects.add_object. - Each entry is a dict with: mac, ip, name, macVendor, lastQuery + Build a list of device entries. + Online status is determined by comparing lastSeen (in seconds) vs the current time. """ entries = [] - - iface_map = get_pihole_interface_data() # mac -> [ips] devices = get_pihole_network_devices() now_ts = int(datetime.datetime.now().timestamp()) for device in devices: hwaddr = device.get('hwaddr') - if not hwaddr or hwaddr == "00:00:00:00:00:00": + + # Filter out invalid MACs/interfaces + if not hwaddr or hwaddr in ["00:00:00:00:00:00", "ip-::"]: continue - macVendor = device.get('macVendor', '') - lastQuery = device.get('lastQuery') - - # collect all IPs for this device device_ips = device.get('ips', []) if not device_ips: continue + # 1. Find the freshest timestamp across all IPs for this MAC + # This ensures if the device is active on ANY IP, the MAC is considered online. + max_last_seen = 0 + for ip_info in device_ips: + ls = ip_info.get('lastSeen', 0) + if ls > max_last_seen: + max_last_seen = ls + + # 2. Determine online status: (Current Time - Last Seen) <= PIHOLEAPI_CONSIDER_ONLINE + # Math is in seconds. + if (now_ts - max_last_seen) <= PIHOLEAPI_CONSIDER_ONLINE: + is_online = True + else: + is_online = False + + # 3. Skip if offline (and user doesn't want offline devices) + if not is_online and not PIHOLEAPI_GET_OFFLINE: + mylog('verbose', [f'[{pluginName}] Not online in the last {PIHOLEAPI_CONSIDER_ONLINE}s, import of offline disabled (PIHOLEAPI_GET_OFFLINE) skipping device: {device}.']) + continue + + mac_vendor = device.get('macVendor', '') + + # 4. Process each valid IP for the device for ip_info in device_ips: ip = ip_info.get('ip') - if not ip: + + # Skip internal Pi-hole placeholders + if not ip or ip in ["0.0.0.0", "::"]: + mylog('verbose', [f'[{pluginName}] Not a valid ip ({ip}), skipping device: {device}.']) continue name = ip_info.get('name') or '(unknown)' + tmp_mac = hwaddr.lower() - # Determine if this device is "online" - online = any(ip in iplist for iplist in iface_map.values()) - - # Skip offline devices unless PIHOLEAPI_GET_OFFLINE=True - if not online and not PIHOLEAPI_GET_OFFLINE: - continue - - tmpMac = hwaddr.lower() - - # ensure fake mac if enabled - if PIHOLEAPI_FAKE_MAC and not is_mac(tmpMac): - tmpMac = string_to_fake_mac(ip) - - # mark lastQuery as now if online, else keep original - last_query_val = str(now_ts) if online else str(lastQuery) if lastQuery else '' + # Handle Fake MAC logic for non-standard hardware addresses + if PIHOLEAPI_FAKE_MAC and not is_mac(tmp_mac): + tmp_mac = string_to_fake_mac(ip) entries.append({ - 'mac': tmpMac, + 'mac': tmp_mac, 'ip': ip, 'name': name, - 'macVendor': macVendor, - 'lastQuery': last_query_val + 'macVendor': mac_vendor, + # Pass the Unix timestamp as a string for NAX tracking + 'lastQuery': str(max_last_seen) if max_last_seen > 0 else "" }) return entries @@ -255,7 +267,7 @@ def gather_device_entries(): # ------------------------------------------------------------------ def main(): """Main plugin entrypoint.""" - global PIHOLEAPI_URL, PIHOLEAPI_PASSWORD, PIHOLEAPI_API_MAXCLIENTS, PIHOLEAPI_VERIFY_SSL, PIHOLEAPI_RUN_TIMEOUT, PIHOLEAPI_GET_OFFLINE + global PIHOLEAPI_URL, PIHOLEAPI_PASSWORD, PIHOLEAPI_API_MAXCLIENTS, PIHOLEAPI_VERIFY_SSL, PIHOLEAPI_RUN_TIMEOUT, PIHOLEAPI_GET_OFFLINE, PIHOLEAPI_CONSIDER_ONLINE mylog('verbose', [f'[{pluginName}] start script.']) @@ -272,6 +284,11 @@ def main(): PIHOLEAPI_VERIFY_SSL = get_setting_value('PIHOLEAPI_SSL_VERIFY') PIHOLEAPI_RUN_TIMEOUT = get_setting_value('PIHOLEAPI_RUN_TIMEOUT') PIHOLEAPI_GET_OFFLINE = get_setting_value('PIHOLEAPI_GET_OFFLINE') + PIHOLEAPI_CONSIDER_ONLINE = get_setting_value('PIHOLEAPI_CONSIDER_ONLINE') + + # Fallback in case the setting is missing or returned as an empty string + if not isinstance(PIHOLEAPI_CONSIDER_ONLINE, int): + PIHOLEAPI_CONSIDER_ONLINE = 300 # Authenticate if not pihole_api_auth():