From de61ce89e3d82599b880483d475ef9b832155fca Mon Sep 17 00:00:00 2001 From: Cobra Date: Tue, 14 Apr 2026 15:01:49 -0400 Subject: [PATCH] Add Garmin/Fitbit OUIs; consolidate personal device detection into is_personal_device() Garmin and Fitbit wearables use globally-assigned OUI MACs (no private addressing) so the LAA bit check misses them. Added their OUIs to MOBILE_DEVICE_OUIS. Consolidated all three detection signals into is_personal_device(): 1. Explicit config (device_labels, personal_devices) 2. OUI table (phones + Garmin/Fitbit) 3. LAA bit (iOS 14+/Android 10+ private MACs) Occupancy _is_personal now delegates to is_personal_device instead of duplicating logic. --- net_alerter/net_alerter.py | 42 +++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/net_alerter/net_alerter.py b/net_alerter/net_alerter.py index 4766ab1..6c8c8b0 100644 --- a/net_alerter/net_alerter.py +++ b/net_alerter/net_alerter.py @@ -116,6 +116,10 @@ MOBILE_DEVICE_OUIS = { "001555", "001620", "001645", "001693", "0016B8", "001730", "0017F6", "001822", "001888", "0018F8", "001913", "001985", "001A45", "001A92", "001AFC", + # Garmin (GPS watches/wearables with WiFi — do not use private MACs) + "086698", "F8B5AB", "E87941", "1430C6", "801F02", "68F3B1", "283B96", "24DEC6", + # Fitbit (wearables with WiFi sync — do not use private MACs) + "E890AC", "88A70E", "E407AA", "A41566", "008B17", } # Personal device tracking @@ -728,26 +732,31 @@ def load_env(): def is_personal_device(mac: str) -> bool: """ - Check if MAC is a personal device (mobile OUI or manually configured). - Returns True if device OUI is in MOBILE_DEVICE_OUIS OR mac is in personal_devices set. + Check if MAC belongs to a phone or wearable. + + Three signals, checked in order: + 1. Explicit config — device_labels or personal_devices (operator-configured) + 2. OUI table — MOBILE_DEVICE_OUIS covers phones + Garmin/Fitbit (no private MACs) + 3. LAA bit — locally-administered bit set = iOS 14+/Android 10+ private MAC = phone/watch """ - # Normalize MAC mac_normalized = _normalize_mac(mac) - if not mac_normalized: # Skip invalid MACs + if not mac_normalized: return False - + oui = mac_normalized[:6] - # Check personal_devices set first (manually configured) with personal_lock: - if mac_normalized in personal_devices: + if mac_normalized in personal_devices or mac_normalized in device_labels: return True - # Check against MOBILE_DEVICE_OUIS if oui in MOBILE_DEVICE_OUIS: return True - return False + # Locally-administered bit: modern phones randomize MACs; TVs/infra don't + try: + return bool(int(mac_normalized[:2], 16) & 0x02) + except ValueError: + return False def lookup_oui(mac: str) -> str: @@ -901,18 +910,9 @@ def _update_occupancy_state_unlocked() -> None: """ global location_occupancy - # Detect phones/wearables via LAA MAC (locally-administered bit set in first octet). - # iOS 14+, Android 10+ all use private/random MACs — this bit is always set. - # TVs, gateways, smart home devices use globally-assigned OUI MACs (bit clear). - # Explicit device_labels / personal_devices also qualify (older phones, pre-config). - def _is_personal(mac: str) -> bool: - n = _normalize_mac(mac) - if n in device_labels or n in personal_devices: - return True - try: - return bool(int(n[:2], 16) & 0x02) # locally-administered bit - except ValueError: - return False + # Delegate to is_personal_device: checks explicit config, OUI table (phones + Garmin/Fitbit), + # and LAA bit (iOS 14+/Android 10+ private MACs). No pre-config needed for modern phones. + _is_personal = is_personal_device active_enrolled_count = 0 for mac, dev in known_devices.items():