Classify a device using rule-based logic first, then GPT-5 Nano if confidence is below threshold. Args: vendor: MAC OUI vendor string ports: list of port strings or ints hostname: device hostname (mDNS, DHCP, NetBIOS) mac: full MAC address ai_service:
(vendor, ports, hostname, mac, ai_service=None,
gateway_ip=None, device_ip=None)
| 701 | # AI-enhanced classification via GPT-5.4 Nano (optional) |
| 702 | # --------------------------------------------------------------------------- |
| 703 | def classify_device_ai(vendor, ports, hostname, mac, ai_service=None, |
| 704 | gateway_ip=None, device_ip=None): |
| 705 | """Classify a device using rule-based logic first, then GPT-5.4 Nano if |
| 706 | confidence is below threshold. |
| 707 | |
| 708 | Args: |
| 709 | vendor: MAC OUI vendor string |
| 710 | ports: list of port strings or ints |
| 711 | hostname: device hostname (mDNS, DHCP, NetBIOS) |
| 712 | mac: full MAC address |
| 713 | ai_service: an AIService instance (or None to skip AI) |
| 714 | gateway_ip: default gateway IP |
| 715 | device_ip: this device IP |
| 716 | |
| 717 | Returns: |
| 718 | dict with keys: device_type, label, confidence, ai_enhanced (bool) |
| 719 | """ |
| 720 | # Step 1: rule-based classification |
| 721 | result = classify_device(vendor, ports, gateway_ip=gateway_ip, device_ip=device_ip) |
| 722 | result["ai_enhanced"] = False |
| 723 | |
| 724 | # Step 2: hostname-based refinement |
| 725 | # Strong hostname hints override even high-confidence vendor matches because |
| 726 | # hostnames are user-visible names that are very specific (e.g. "RE200" is |
| 727 | # always a TP-Link extender even though TP-Link vendor → router). |
| 728 | if hostname: |
| 729 | hostname_lower = hostname.lower() |
| 730 | # Priority hostname hints — these ALWAYS override (even at confidence 0.8+) |
| 731 | # because the hostname is more specific than a broad vendor match. |
| 732 | _STRONG_HOSTNAME_HINTS = { |
| 733 | "re200": "extender", "re300": "extender", "re450": "extender", |
| 734 | "re505x": "extender", "re605x": "extender", "re650": "extender", |
| 735 | "range-ext": "extender", "repeater": "extender", "ty_wr": "extender", |
| 736 | "raspberry": "sbc", "raspberrypi": "sbc", "pi-hole": "sbc", |
| 737 | "nest-audio": "speaker", "nest-mini": "speaker", "nest-hub": "speaker", |
| 738 | "google-home": "speaker", "homepod": "speaker", |
| 739 | "bosch-wat": "appliance", "bosch-wt": "appliance", |
| 740 | "appletv": "smart_tv", "edvinsappletv": "smart_tv", |
| 741 | "apple-tv": "smart_tv", |
| 742 | "homepod": "speaker", "apple-homepod": "speaker", |
| 743 | "macbook": "laptop", "imac": "workstation", "mac-mini": "workstation", |
| 744 | "mac-pro": "workstation", "mac-studio": "workstation", |
| 745 | "iphone": "phone", "ipad": "tablet", |
| 746 | "apple-watch": "wearable", "applewatch": "wearable", |
| 747 | # ASUS router hostnames (product lines that are always routers) |
| 748 | "rt-ax": "router", "rt-ac": "router", "rt-n": "router", |
| 749 | "gt-ax": "router", "gt-ac": "router", |
| 750 | "zenwifi": "router", "asus router": "router", |
| 751 | # Ragnar devices |
| 752 | "ragnar": "ragnar", |
| 753 | } |
| 754 | for hint, dtype in _STRONG_HOSTNAME_HINTS.items(): |
| 755 | if hint in hostname_lower: |
| 756 | result["device_type"] = dtype |
| 757 | result["label"] = DEVICE_TYPE_LABELS.get(dtype, "Unknown") |
| 758 | result["confidence"] = 0.85 # hostname is strong signal |
| 759 | return result # short circuit — hostname is definitive |
| 760 |
no test coverage detected