diff --git a/src/NetworkOptimizer.Audit/Services/Detectors/MacOuiDetector.cs b/src/NetworkOptimizer.Audit/Services/Detectors/MacOuiDetector.cs
index b803aa67e..e053b4b67 100644
--- a/src/NetworkOptimizer.Audit/Services/Detectors/MacOuiDetector.cs
+++ b/src/NetworkOptimizer.Audit/Services/Detectors/MacOuiDetector.cs
@@ -74,10 +74,16 @@ public class MacOuiDetector
{ "B8:3E:59", ("Roku", ClientDeviceCategory.StreamingDevice, 90) },
{ "C8:3A:6B", ("Roku", ClientDeviceCategory.StreamingDevice, 90) },
- // Apple TV (note: Apple devices can be many things)
+ // Apple TV /HomePods (note: Apple devices can be many things)
{ "40:CB:C0", ("Apple TV", ClientDeviceCategory.StreamingDevice, 75) },
{ "70:56:81", ("Apple TV", ClientDeviceCategory.StreamingDevice, 75) },
{ "68:D9:3C", ("Apple TV", ClientDeviceCategory.StreamingDevice, 75) },
+ { "A8:51:AB", ("Apple TV", ClientDeviceCategory.StreamingDevice, 75) },
+ { "C8:D0:83", ("Apple TV", ClientDeviceCategory.StreamingDevice, 75) },
+ { "9C:3E:53", ("Apple TV", ClientDeviceCategory.StreamingDevice, 75) },
+ { "E0:2B:96", ("Apple HomePod", ClientDeviceCategory.SmartSpeaker, 75) },
+ { "F4:34:F0", ("Apple HomePod", ClientDeviceCategory.SmartSpeaker, 75) },
+ { "D4:90:9C", ("Apple HomePod", ClientDeviceCategory.SmartSpeaker, 75) },
// Chromecast
{ "54:60:09", ("Chromecast", ClientDeviceCategory.StreamingDevice, 85) },
diff --git a/src/NetworkOptimizer.Audit/Services/DeviceTypeDetectionService.cs b/src/NetworkOptimizer.Audit/Services/DeviceTypeDetectionService.cs
index 7516445f5..932f1f64f 100644
--- a/src/NetworkOptimizer.Audit/Services/DeviceTypeDetectionService.cs
+++ b/src/NetworkOptimizer.Audit/Services/DeviceTypeDetectionService.cs
@@ -176,7 +176,8 @@ private DeviceDetectionResult DetectDeviceTypeCore(
// Priority 0.5: Check OUI for vendors that need special handling
// - Cync/Wyze/GE have camera fingerprints but most devices are actually plugs/bulbs
// - Apple with SmartSensor fingerprint is likely Apple Watch
- var vendorOverrideResult = CheckVendorDefaultOverride(client?.Oui, client?.Name, client?.Hostname, client?.DevCat);
+ // - Apple with generic fingerprints (SmartTV, IoTGeneric) should use MAC OUI for specific device type
+ var vendorOverrideResult = CheckVendorDefaultOverride(client?.Oui, client?.Name, client?.Hostname, client?.DevCat, client?.Mac);
if (vendorOverrideResult != null)
{
_logger?.LogDebug("[Detection] '{DisplayName}': Vendor override → {Category} (vendor defaults to plug unless camera indicated)",
@@ -442,7 +443,15 @@ private DeviceDetectionResult DetectFromUniFiOui(string ouiName, string? deviceN
// Media/Entertainment
if (name.Contains("roku")) return CreateOuiResult(ClientDeviceCategory.StreamingDevice, ouiName, OuiHighConfidence);
- if (name.Contains("apple") && name.Contains("tv")) return CreateOuiResult(ClientDeviceCategory.StreamingDevice, ouiName, OuiHighConfidence);
+
+ // Apple devices: Use device name to disambiguate between Apple TV and HomePod
+ if (name.Contains("apple"))
+ {
+ if (deviceNameLower.Contains("tv") || deviceNameLower.Contains("apple tv"))
+ return CreateOuiResult(ClientDeviceCategory.StreamingDevice, "Apple TV", OuiHighConfidence);
+ if (deviceNameLower.Contains("homepod") || deviceNameLower.Contains("siri"))
+ return CreateOuiResult(ClientDeviceCategory.SmartSpeaker, "Apple HomePod", OuiHighConfidence);
+ }
return DeviceDetectionResult.Unknown;
}
@@ -1185,7 +1194,8 @@ private static bool IsCloudSecurityVendor(string vendorLower)
System.Text.RegularExpressions.Regex.IsMatch(vendorLower, @"\barlo\b") ||
System.Text.RegularExpressions.Regex.IsMatch(vendorLower, @"\bsimplisafe\b") ||
System.Text.RegularExpressions.Regex.IsMatch(vendorLower, @"\btp-link\b") ||
- System.Text.RegularExpressions.Regex.IsMatch(vendorLower, @"\bcanary\b");
+ System.Text.RegularExpressions.Regex.IsMatch(vendorLower, @"\bcanary\b") ||
+ System.Text.RegularExpressions.Regex.IsMatch(vendorLower, @"\bfurbo\b");
}
///
@@ -1203,11 +1213,56 @@ private static bool IsThermostatName(string nameLower)
/// - Apple devices with SmartSensor fingerprint are usually Apple Watches (Smartphone).
/// - GoPro action cameras share devCat 106 with security cameras but aren't security devices.
///
- private DeviceDetectionResult? CheckVendorDefaultOverride(string? oui, string? name, string? hostname, int? devCat)
+ private DeviceDetectionResult? CheckVendorDefaultOverride(string? oui, string? name, string? hostname, int? devCat, string? mac)
{
var ouiLower = oui?.ToLowerInvariant() ?? "";
var nameLower = (name ?? hostname ?? "").ToLowerInvariant();
+ // Apple devices with generic fingerprints should check MAC OUI for specific device type
+ // Apple controls their hardware tightly, so MAC OUI is highly reliable for Apple devices
+ // This catches Apple TVs (SmartTV fingerprint) and HomePods (IoTGeneric) even without specific names
+ if (ouiLower.Contains("apple") && !string.IsNullOrEmpty(mac))
+ {
+ var isGenericFingerprint = devCat == 51 || // IoTGeneric
+ devCat == 7 || // SmartTV (generic)
+ devCat == 47; // SmartTV (alternative)
+
+ if (isGenericFingerprint)
+ {
+ _logger?.LogDebug("[VendorOverride] Apple device with generic fingerprint detected: OUI='{Oui}', DevCat={DevCat}, MAC={Mac}",
+ oui, devCat, mac);
+
+ var macOuiResult = _macOuiDetector.Detect(mac);
+ if (macOuiResult.Category != ClientDeviceCategory.Unknown)
+ {
+ _logger?.LogDebug("[VendorOverride] MAC OUI lookup successful: {MacPrefix} → {Category} ({VendorName})",
+ mac.Substring(0, Math.Min(8, mac.Length)), macOuiResult.Category, macOuiResult.VendorName);
+
+ // MAC OUI database has a specific match for this Apple device
+ return new DeviceDetectionResult
+ {
+ Category = macOuiResult.Category,
+ Source = DetectionSource.MacOui,
+ ConfidenceScore = 98, // Very high confidence - Apple OUI + specific device match
+ VendorName = macOuiResult.VendorName,
+ RecommendedNetwork = macOuiResult.RecommendedNetwork,
+ Metadata = new Dictionary
+ {
+ ["override_reason"] = "Apple device with generic fingerprint - MAC OUI provides specific device type",
+ ["oui"] = oui ?? "",
+ ["dev_cat"] = devCat ?? 0,
+ ["mac_oui_category"] = macOuiResult.Category.ToString()
+ }
+ };
+ }
+ else
+ {
+ _logger?.LogDebug("[VendorOverride] MAC OUI lookup found no match for {MacPrefix} - falling back to fingerprint",
+ mac.Substring(0, Math.Min(8, mac.Length)));
+ }
+ }
+ }
+
// Apple devices with SmartSensor fingerprint (DevCat=14) are likely Apple Watches
if (ouiLower.Contains("apple") && devCat == 14)
{
diff --git a/tests/NetworkOptimizer.Audit.Tests/Services/DeviceTypeDetectionServiceTests.cs b/tests/NetworkOptimizer.Audit.Tests/Services/DeviceTypeDetectionServiceTests.cs
index 45e0f3d66..e42cc9df2 100644
--- a/tests/NetworkOptimizer.Audit.Tests/Services/DeviceTypeDetectionServiceTests.cs
+++ b/tests/NetworkOptimizer.Audit.Tests/Services/DeviceTypeDetectionServiceTests.cs
@@ -228,6 +228,152 @@ public void DetectDeviceType_RingVendor_ReturnsCloudCamera()
result.Category.Should().Be(ClientDeviceCategory.CloudCamera);
}
+ [Fact]
+ public void DetectDeviceType_FurboVendor_ReturnsCloudCamera()
+ {
+ // Arrange - Furbo is a cloud camera vendor (dog camera with treat tossing)
+ var client = new UniFiClientResponse
+ {
+ Mac = "11:22:33:44:55:66",
+ Name = "Furbo Dog Camera",
+ Oui = "Furbo",
+ DevCat = 9 // Camera fingerprint (9 = IP Network Camera)
+ };
+
+ // Act
+ var result = _service.DetectDeviceType(client);
+
+ // Assert - Furbo cameras are cloud cameras (require internet for remote access)
+ result.Category.Should().Be(ClientDeviceCategory.CloudCamera);
+ }
+
+ #endregion
+
+ #region Apple Device Vendor Override Tests (Generic Fingerprints)
+
+ [Theory]
+ [InlineData("9C:3E:53:2A:72:5A", "Keeping-Room 72:5a", 47)] // SmartTV fingerprint, Apple TV OUI
+ [InlineData("C8:D0:83:B9:2B:A0", "Living-Room-Wireless", 7)] // SmartTV fingerprint, Apple TV OUI
+ [InlineData("A8:51:AB:13:F0:CD", "Guest-Media", 47)] // SmartTV fingerprint, Apple TV OUI (avoid "appletv" in name)
+ [InlineData("68:D9:3C:11:22:33", "Theater-Device", 47)] // SmartTV fingerprint, Apple TV OUI
+ public void DetectDeviceType_AppleOuiWithSmartTVFingerprint_UsesOuiForStreamingDevice(string mac, string deviceName, int devCat)
+ {
+ // Arrange - Apple devices with generic SmartTV fingerprint should use MAC OUI
+ // to get specific device type (Apple TV = StreamingDevice, not generic SmartTV)
+ var client = new UniFiClientResponse
+ {
+ Mac = mac,
+ Name = deviceName,
+ Oui = "Apple, Inc.",
+ DevCat = devCat // SmartTV fingerprint (generic)
+ };
+
+ // Act
+ var result = _service.DetectDeviceType(client);
+
+ // Assert - Should detect as StreamingDevice (from MAC OUI), not SmartTV (from fingerprint)
+ result.Category.Should().Be(ClientDeviceCategory.StreamingDevice);
+ result.VendorName.Should().Be("Apple TV");
+ result.ConfidenceScore.Should().Be(98); // High confidence - Apple OUI + specific MAC match
+ result.Source.Should().Be(DetectionSource.MacOui);
+ }
+
+ [Theory]
+ [InlineData("E0:2B:96:9C:03:1E", "Keeping-Room-Speaker", 51)] // IoTGeneric fingerprint, HomePod OUI (avoid "siri" in name)
+ [InlineData("F4:34:F0:3E:69:C2", "Guest-Speaker", 51)] // IoTGeneric fingerprint, HomePod OUI
+ public void DetectDeviceType_AppleOuiWithIoTGenericFingerprint_UsesOuiForSmartSpeaker(string mac, string deviceName, int devCat)
+ {
+ // Arrange - Apple devices with generic IoTGeneric fingerprint should use MAC OUI
+ // to get specific device type (HomePod = SmartSpeaker, not generic IoT)
+ var client = new UniFiClientResponse
+ {
+ Mac = mac,
+ Name = deviceName,
+ Oui = "Apple, Inc.",
+ DevCat = devCat // IoTGeneric fingerprint (generic)
+ };
+
+ // Act
+ var result = _service.DetectDeviceType(client);
+
+ // Assert - Should detect as SmartSpeaker (from MAC OUI), not IoTGeneric (from fingerprint)
+ result.Category.Should().Be(ClientDeviceCategory.SmartSpeaker);
+ result.VendorName.Should().Be("Apple HomePod");
+ result.ConfidenceScore.Should().Be(98); // High confidence - Apple OUI + specific MAC match
+ result.Source.Should().Be(DetectionSource.MacOui);
+ }
+
+ [Fact]
+ public void DetectDeviceType_AppleOuiWithGenericFingerprintButNoMacOuiMatch_UsesFingerprint()
+ {
+ // Arrange - Apple device with generic fingerprint but MAC prefix not in our OUI database
+ // Should fall back to normal fingerprint detection
+ var client = new UniFiClientResponse
+ {
+ Mac = "AA:BB:CC:DD:EE:FF", // Unknown MAC prefix
+ Name = "Some-Device",
+ Oui = "Apple, Inc.",
+ DevCat = 47 // SmartTV fingerprint
+ };
+
+ // Act
+ var result = _service.DetectDeviceType(client);
+
+ // Assert - No specific MAC OUI match, so should use fingerprint (SmartTV)
+ // This might be a generic Apple-compatible TV or other device
+ result.Category.Should().Be(ClientDeviceCategory.SmartTV);
+ result.ConfidenceScore.Should().Be(95); // Normal fingerprint confidence
+ }
+
+ [Theory]
+ [InlineData("E0:2B:96:9C:03:1E", "Office Siri", 51)] // Name-based detection takes priority
+ [InlineData("F4:34:F0:3E:69:C2", "Master HomePod", 51)] // Name-based detection takes priority
+ public void DetectDeviceType_AppleHomePodWithSiriOrHomePodInName_StillDetectsCorrectly(string mac, string deviceName, int devCat)
+ {
+ // Arrange - HomePods with "siri" or "homepod" in name should be detected
+ // through either name-based override OR MAC OUI override (both work)
+ var client = new UniFiClientResponse
+ {
+ Mac = mac,
+ Name = deviceName,
+ Oui = "Apple, Inc.",
+ DevCat = devCat // IoTGeneric fingerprint
+ };
+
+ // Act
+ var result = _service.DetectDeviceType(client);
+
+ // Assert
+ result.Category.Should().Be(ClientDeviceCategory.SmartSpeaker);
+ result.ConfidenceScore.Should().BeGreaterThanOrEqualTo(95); // High confidence (95% from name, or 98% from vendor override)
+ }
+
+ [Theory]
+ [InlineData(1)] // Laptop
+ [InlineData(2)] // Tablet
+ [InlineData(4)] // Smartphone
+ [InlineData(117)] // Desktop
+ public void DetectDeviceType_AppleOuiWithSpecificFingerprint_DoesNotOverride(int devCat)
+ {
+ // Arrange - Apple devices with specific (non-generic) fingerprints should NOT be overridden
+ // Only generic categories (SmartTV, IoTGeneric) trigger the MAC OUI override
+ var client = new UniFiClientResponse
+ {
+ Mac = "E0:2B:96:9C:03:1E", // HomePod OUI
+ Name = "Device",
+ Oui = "Apple, Inc.",
+ DevCat = devCat // Specific fingerprint
+ };
+
+ // Act
+ var result = _service.DetectDeviceType(client);
+
+ // Assert - Should use the specific fingerprint, not override to HomePod based on MAC
+ // (Even though MAC says HomePod, the specific fingerprint is more trustworthy for what's actually connected)
+ result.Category.Should().NotBe(ClientDeviceCategory.SmartSpeaker);
+ result.ConfidenceScore.Should().Be(95); // Normal fingerprint confidence
+ }
+
#endregion
#region Camera Name Override Tests (Nest/Google cameras)
@@ -258,6 +404,29 @@ public void DetectDeviceType_NestOrGoogleWithCameraName_ReturnsCloudCamera(strin
result.Category.Should().Be(ClientDeviceCategory.CloudCamera);
}
+ [Theory]
+ [InlineData("Furbo", "Living Room")]
+ [InlineData("FURBO", "Dog Camera")]
+ [InlineData("Furbo Inc", "Pet Camera")]
+ [InlineData("Furbo Dog Camera", "Kitchen Cam")]
+ public void DetectDeviceType_FurboVendorVariations_ReturnsCloudCamera(string vendor, string deviceName)
+ {
+ // Arrange - Test various Furbo vendor name formats (case-insensitive, with suffixes)
+ var client = new UniFiClientResponse
+ {
+ Mac = "11:22:33:44:55:66",
+ Name = deviceName,
+ Oui = vendor,
+ DevCat = 9 // Camera fingerprint
+ };
+
+ // Act
+ var result = _service.DetectDeviceType(client);
+
+ // Assert - All Furbo variations should be detected as cloud cameras
+ result.Category.Should().Be(ClientDeviceCategory.CloudCamera);
+ }
+
[Theory]
[InlineData("Nest Labs Inc.", "Living Room Thermostat", ClientDeviceCategory.SmartThermostat)]
[InlineData("Nest Labs Inc.", "Hallway Ecobee", ClientDeviceCategory.SmartThermostat)]
@@ -976,6 +1145,7 @@ public void SetClientHistory_FiltersEntriesWithEmptyMac()
[InlineData("Blink", ClientDeviceCategory.CloudCamera)]
[InlineData("TP-Link", ClientDeviceCategory.CloudCamera)]
[InlineData("Canary", ClientDeviceCategory.CloudCamera)]
+ [InlineData("Furbo", ClientDeviceCategory.CloudCamera)]
public void DetectFromMac_HistoryCameraFingerprint_WithCloudVendor_ReturnsCloudCamera(string vendor, ClientDeviceCategory expected)
{
// Arrange - history with camera fingerprint (DevCat 9) and cloud vendor via OUI
@@ -1157,6 +1327,36 @@ public void DetectDeviceType_GoogleNestSpeakers_SetsGoogleVendor(string deviceNa
result.VendorName.Should().Be("Google");
}
+ [Theory]
+ [InlineData("E0:2B:96:12:34:56", "Office Siri")]
+ [InlineData("F4:34:F0:AB:CD:EF", "Guest Room 1 Siri")]
+ [InlineData("D4:90:9C:11:22:33", "Living Room Siri")]
+ [InlineData("E0:2B:96:44:55:66", "Game Room Speaker 1")]
+ [InlineData("F4:34:F0:77:88:99", "Game Room Speaker 2")]
+ public void DetectFromMac_AppleSpeakerOui_ReturnsSmartSpeaker(string macAddress, string deviceName)
+ {
+ // Arrange - Test MAC OUI detection for Apple smart speakers using real device names
+ // These OUIs are specific to Apple's smart speaker product line
+ var history = new List
+ {
+ new()
+ {
+ Mac = macAddress,
+ Name = deviceName
+ }
+ };
+ _service.SetClientHistory(history);
+
+ // Act
+ var result = _service.DetectFromMac(macAddress);
+
+ // Assert - Should detect as SmartSpeaker via MAC OUI
+ result.Category.Should().Be(ClientDeviceCategory.SmartSpeaker);
+ result.VendorName.Should().Be("Apple HomePod");
+ result.Source.Should().Be(DetectionSource.MacOui);
+ result.ConfidenceScore.Should().Be(75);
+ }
+
#endregion
#region Vendor Preservation Tests - VR Headsets