Device Discovery Workflow

Overview

Django-micboard implements a comprehensive device discovery and deduplication system that maintains an authoritative registry of wireless microphone equipment across multi-vendor networks. The system detects new devices, tracks movements, identifies conflicts, and provides an approval workflow for manual review.

Architecture

┌──────────────────────────┐
│  Manufacturer APIs       │
│  (Shure, Sennheiser)     │
└────────────┬─────────────┘
             │ API polling (poll_devices)
             ▼
┌──────────────────────────┐
│ DeviceDeduplicationService│
│  • Priority matching      │
│  • Conflict detection     │
│  • Movement tracking      │
└────────────┬─────────────┘
             │
      ┌──────┴──────┐
      ▼             ▼
┌──────────┐  ┌──────────────┐
│ Receiver │  │ DiscoveryQueue│
│  Models  │  │  (staging)    │
└──────────┘  └──────────────┘
      │             │
      └──────┬──────┘
             ▼
    ┌──────────────────┐
    │DeviceMovementLog │
    │  (audit trail)   │
    └──────────────────┘

Deduplication Priority

The system uses a four-tier priority matching algorithm to identify devices:

Priority 1: Serial Number (Most Reliable)

  • Field: serial_number
  • Source: Manufacturer API (e.g., Shure firmware reports)
  • Reliability: Highest - unique hardware identifier
  • Use Case: Primary deduplication key
# Example: Shure ULXD4D with serial
{
    "serial_number": "TEST-001",
    "mac_address": "00:0e:dd:4c:43:78",
    "ip": "172.21.2.140"
}

Priority 2: MAC Address (Hardware Identity)

  • Field: mac_address
  • Source: Network interface (ARP/DHCP/API)
  • Reliability: High - unique network hardware
  • Use Case: Fallback when serial unavailable
# Example: Device without serial in API
{
    "mac_address": "00:0e:dd:4c:43:78",
    "ip": "172.21.2.140"
}

Priority 3: IP Address (Location)

  • Field: ip
  • Source: Network configuration
  • Reliability: Medium - can change with DHCP
  • Use Case: Detect IP conflicts
# Detects conflict when different device uses same IP
existing_device = {
    "serial_number": "TEST-001",
    "ip": "172.21.2.140"
}
new_device = {
    "serial_number": "TEST-002",  # Different device!
    "ip": "172.21.2.140"  # Same IP = conflict
}

Priority 4: API Device ID (Manufacturer-Specific)

  • Field: api_device_id
  • Source: Manufacturer's internal ID (e.g., Shure GUID)
  • Reliability: Variable - can change with firmware
  • Use Case: Last resort matching

Conflict Types

1. Device Movement (IP Changed)

Detection: Same serial/MAC, different IP Action: Update IP, log movement Status: Auto-resolved with audit log

# Before
receiver = Receiver(serial_number="TEST-001", ip="192.168.1.100")

# After discovery
# Detected: Same device, new IP
# Action: Update IP to 192.168.1.200
# Log: DeviceMovementLog created

Admin View:

Device: TEST-001
Movement: IP 192.168.1.100 → 192.168.1.200
Status: ⚠ PENDING ACKNOWLEDGMENT

2. IP Conflict (Different Device, Same IP)

Detection: Different serial/MAC, same IP Action: Queue for manual review Status: Requires admin approval

# Existing device
receiver_a = Receiver(serial_number="TEST-001", ip="192.168.1.100")

# New discovery
device_b = {
    "serial_number": "TEST-002",  # Different device
    "ip": "192.168.1.100"  # Same IP!
}
# → Queued to DiscoveryQueue with is_ip_conflict=True

Admin View:

Device: TEST-002
IP: 192.168.1.100
Conflict: ⛔ IP CONFLICT
Existing Device: TEST-001 (also at 192.168.1.100)
Status: Pending Review

3. Manufacturer Mismatch

Detection: Same serial, different manufacturer Action: Queue for review (likely duplicate serial across vendors) Status: Requires admin approval

4. Duplicate Device

Detection: Exact match on serial/MAC/API ID Action: Update metadata, refresh last_seen Status: Auto-resolved

Discovery Queue States

The DiscoveryQueue model manages staged devices with the following workflow:

pending → approved → imported
   ↓         ↓
rejected   duplicate

State Descriptions

State Description Next Actions
pending Awaiting admin review Approve, Reject, Mark Duplicate
approved Admin approved, ready for import Auto-import to Receiver
rejected Admin rejected device Remove from queue
imported Successfully imported to Receiver Link to created device
duplicate Marked as duplicate, skip import Keep for audit trail

Admin Workflow

1. Viewing Discovered Devices

Navigate to Admin > Micboard > Discovery Queue

List Filters: - Status (pending, approved, rejected, imported, duplicate) - Manufacturer (Shure, Sennheiser, etc.) - Device Type (receiver, transmitter) - Conflict Flags (is_duplicate, is_ip_conflict) - Discovered Date

List Display:

Name              | Manufacturer | Serial      | IP            | Status   | Conflicts        | Discovered
------------------|--------------|-------------|---------------|----------|------------------|------------
ULX-D Quad        | Shure        | TEST-001    | 172.21.2.140 | PENDING  | ⚠ DUPLICATE     | 2025-01-22
ULXD4D            | Shure        | TEST-002    | 172.21.2.141 | PENDING  | ⛔ IP CONFLICT  | 2025-01-22
SK 6000           | Sennheiser   | SN-5678     | 10.0.1.50    | APPROVED | —                | 2025-01-22

2. Reviewing Conflict Details

Click on a device to view detailed conflict analysis:

Device Information:
  Name: ULXD4D
  Serial Number: TEST-002
  MAC Address: 00:0e:dd:4c:43:78
  IP: 172.21.2.141

Conflict Analysis:
  ⛔ IP Conflict Detected
  existing_device: TEST-001 (Shure Receiver at 172.21.2.141)
  conflict_type: ip_conflict
  details: Different device using same IP address

3. Approving Devices

Action: Select devices → Actions dropdownApprove selected devices for import

This will: 1. Create Receiver object with all metadata 2. Set status to imported 3. Link existing_device to created receiver 4. Set reviewed_by to current admin user 5. Show success message: "Approved and imported 3 device(s)."

4. Rejecting Devices

Action: Select devices → Actions dropdownReject selected devices

This will: 1. Set status to rejected 2. Prevent auto-import 3. Keep record for audit trail

5. Acknowledging Movements

Navigate to Admin > Micboard > Device Movement Log

List Display:

Device      | Manufacturer | Movement                                 | Detected    | Status
------------|--------------|------------------------------------------|-------------|---------------------
TEST-001    | Shure        | IP: 192.168.1.100 → 192.168.1.200       | 2025-01-22  | ⚠ PENDING
TEST-003    | Shure        | Location: Studio A → Studio B            | 2025-01-22  | ✓ ACKNOWLEDGED

Action: Select movements → Actions dropdownAcknowledge selected movements

Example Scenarios

Scenario 1: New Device Discovery

API Data (Shure ULXD4D):

{
    "api_device_id": "guog244",
    "serial_number": "ULXD4D-12345",
    "mac_address": "00:0e:dd:4c:43:78",
    "ip": "172.21.2.140",
    "subnet_mask": "255.255.255.0",
    "gateway": "172.21.0.1",
    "model": "ULXD4D",
    "firmware_version": "2.7.6.0",
    "hosted_firmware_version": "2.7.3.0",
    "interface_id": "0.6.0"
}

Deduplication Result: - Priority 1 (Serial): No match - Priority 2 (MAC): No match - Priority 3 (IP): No match - Priority 4 (API ID): No match - Result: is_new=True

Action: Create new Receiver with all metadata:

receiver = Receiver.objects.create(
    manufacturer=manufacturer,
    api_device_id="guog244",
    serial_number="ULXD4D-12345",
    mac_address="00:0e:dd:4c:43:78",
    ip="172.21.2.140",
    subnet_mask="255.255.255.0",
    gateway="172.21.0.1",
    model="ULXD4D",
    firmware_version="2.7.6.0",
    hosted_firmware_version="2.7.3.0",
    interface_id="0.6.0",
)

Scenario 2: Device Movement Detection

Existing Device:

receiver = Receiver(
    serial_number="ULXD4D-12345",
    ip="172.21.2.140"
)

New API Data (same device, new IP):

{
    "serial_number": "ULXD4D-12345",  # Same serial
    "ip": "172.21.3.100"  # New IP!
}

Deduplication Result: - Priority 1 (Serial): Match! But IP changed. - Result: is_moved=True, conflict_type="ip_changed"

Actions: 1. Update receiver IP: receiver.ip = "172.21.3.100" 2. Create DeviceMovementLog: python DeviceMovementLog.objects.create( device=receiver, old_ip="172.21.2.140", new_ip="172.21.3.100", detected_by="sync", reason="Detected during shure sync", acknowledged=False, ) 3. Admin sees: ⚠ PENDING acknowledgment in movement log

Scenario 3: IP Conflict

Existing Device A:

receiver_a = Receiver(
    serial_number="DEVICE-A",
    ip="192.168.1.100"
)

New API Data (Device B, same IP):

{
    "serial_number": "DEVICE-B",  # Different device
    "mac_address": "AA:BB:CC:DD:EE:FF",  # Different MAC
    "ip": "192.168.1.100"  # Same IP as Device A!
}

Deduplication Result: - Priority 1 (Serial): No match (different serial) - Priority 2 (MAC): No match (different MAC) - Priority 3 (IP): Match! But serial/MAC differ. - Result: is_conflict=True, conflict_type="ip_conflict"

Actions: 1. Do NOT auto-create receiver 2. Create DiscoveryQueue entry: python DiscoveryQueue.objects.create( manufacturer=manufacturer, serial_number="DEVICE-B", mac_address="AA:BB:CC:DD:EE:FF", ip="192.168.1.100", status="pending", is_ip_conflict=True, existing_device=receiver_a, # Link to conflicting device ) 3. Admin sees: ⛔ IP CONFLICT badge in discovery queue 4. Admin reviews and decides: - Option A: Reject (network misconfiguration) - Option B: Approve (Device A moved, Device B legit)

Integration with poll_devices

The deduplication service is integrated into the polling command:

# micboard/management/commands/poll_devices.py

for manufacturer in Manufacturer.objects.filter(api_enabled=True):
    service = get_device_service(manufacturer)
    created, updated = service.sync_devices_from_api()

    # sync_devices_from_api now uses DeviceDeduplicationService:
    # 1. Fetches device list from API
    # 2. For each device:
    #    a. Extract serial, MAC, IP, network metadata
    #    b. Call dedup_service.check_device()
    #    c. Handle result (new/duplicate/moved/conflict)
    # 3. Returns (created_count, updated_count)

API Endpoints

Get Pending Approvals

GET /api/discovery/pending/

Response:

{
  "count": 2,
  "results": [
    {
      "id": 1,
      "manufacturer": "Shure",
      "serial_number": "TEST-001",
      "ip": "172.21.2.140",
      "status": "pending",
      "is_duplicate": false,
      "is_ip_conflict": true,
      "discovered_at": "2025-01-22T15:30:00Z"
    }
  ]
}

Get Unacknowledged Movements

GET /api/movements/unacknowledged/

Response:

{
  "count": 1,
  "results": [
    {
      "id": 1,
      "device": {
        "id": 5,
        "name": "ULXD4D Receiver",
        "serial_number": "TEST-001"
      },
      "old_ip": "192.168.1.100",
      "new_ip": "192.168.1.200",
      "movement_type": "ip_only",
      "detected_at": "2025-01-22T15:30:00Z",
      "acknowledged": false
    }
  ]
}

Database Schema

Receiver (Enhanced)

Field Type Description
serial_number CharField [Indexed] Hardware serial
mac_address CharField [Indexed] Network MAC address
ip GenericIPAddress Device IP address
subnet_mask GenericIPAddress Network subnet
gateway GenericIPAddress Network gateway
network_mode CharField auto/manual/dhcp/static
interface_id CharField Network interface version
model CharField Manufacturer model (e.g., ULXD4D)
description TextField Device description
firmware_version CharField Device firmware version
hosted_firmware_version CharField Transmitter firmware (for receivers)

DiscoveryQueue

Field Type Description
manufacturer ForeignKey Manufacturer reference
api_device_id CharField Manufacturer device ID
serial_number CharField [Indexed] Hardware serial
mac_address CharField Network MAC address
ip GenericIPAddress [Indexed] Device IP
device_type CharField receiver/transmitter
status CharField [Indexed] pending/approved/rejected
is_duplicate Boolean Duplicate device flag
is_ip_conflict Boolean IP conflict flag
existing_device ForeignKey Link to conflicting receiver
reviewed_by ForeignKey Admin user who reviewed
discovered_at DateTimeField [Indexed] Discovery timestamp
metadata JSONField Full API response

DeviceMovementLog

Field Type Description
device ForeignKey [Indexed] Receiver reference
old_ip GenericIPAddress Previous IP address
new_ip GenericIPAddress New IP address
old_location ForeignKey Previous location
new_location ForeignKey New location
detected_at DateTimeField [Indexed] Detection timestamp
detected_by CharField Detection source (sync/manual)
reason TextField Movement reason/description
acknowledged Boolean [Indexed] Admin acknowledged flag
acknowledged_by ForeignKey Admin user who acknowledged

Performance Considerations

Indexes

All deduplication keys are indexed for fast lookups:

class Receiver(models.Model):
    class Meta:
        indexes = [
            models.Index(fields=['serial_number']),
            models.Index(fields=['mac_address']),
            models.Index(fields=['ip']),
            models.Index(fields=['api_device_id', 'manufacturer']),
        ]

Query Optimization

Deduplication checks use select_related() and efficient queries:

# Check by serial (single query)
existing = Receiver.objects.filter(
    serial_number=serial_number,
    manufacturer=manufacturer
).select_related('location').first()

# Priority cascade: serial → MAC → IP → API ID
# Stops at first match (no redundant queries)

Testing

Run the deduplication test suite:

python scripts/test_deduplication.py

Tests: 1. ✅ New device detection 2. ✅ Duplicate detection (by serial number) 3. ✅ Device movement detection (IP change) 4. ✅ IP conflict detection 5. ✅ MAC address matching

Expected Output:

============================================================
Device Deduplication Service Test Suite
============================================================

=== Test 1: New Device Detection ===
  Is New: True
  ✓ PASSED

=== Test 2: Duplicate Detection (Serial Number) ===
  Is Duplicate: True
  ✓ PASSED

=== Test 3: Device Movement Detection (IP Change) ===
  Is Moved: True
  ✓ PASSED

=== Test 4: IP Conflict Detection ===
  Is Conflict: True
  ✓ PASSED

=== Test 5: MAC Address Matching ===
  Is Duplicate: True (matched by MAC)
  ✓ PASSED

============================================================
✅ ALL TESTS PASSED
============================================================

Future Enhancements

Phase 2 (Notifications)

  • [ ] Email alerts for pending approvals
  • [ ] Dashboard widget for unacknowledged movements
  • [ ] Slack/Teams integration for conflict notifications
  • [ ] Daily summary reports

Phase 3 (Advanced Deduplication)

  • [ ] Fuzzy matching for similar names
  • [ ] Historical tracking (device seen at multiple IPs over time)
  • [ ] Auto-approve patterns (trusted subnets, known devices)
  • [ ] Bulk import from CSV with conflict detection

Phase 4 (Multi-Manufacturer)

  • [ ] Cross-manufacturer deduplication (same physical device, different APIs)
  • [ ] Vendor-specific deduplication rules
  • [ ] API priority (prefer Shure data over Sennheiser if conflict)

Troubleshooting

Issue: Devices Keep Getting Queued as Conflicts

Cause: API not returning serial numbers consistently

Solution: Check API response in DiscoveryQueue.metadata field:

queue_item = DiscoveryQueue.objects.filter(status='pending').first()
print(queue_item.metadata)  # Full API response

If serial missing, check manufacturer API configuration.

Issue: Device Moved But Not Detected

Cause: Serial number or MAC not populated

Solution: Ensure manufacturer plugin extracts identity fields:

# micboard/integrations/shure/plugin.py
def get_devices(self):
    return [
        {
            "id": device["id"],
            "serial_number": device.get("serialNumber"),  # Must extract
            "mac_address": device.get("macAddress"),      # Must extract
            "ip": device.get("ipAddress"),
        }
    ]

Issue: Too Many Pending Approvals

Cause: Legitimate device movements queued as conflicts

Solution: Use "Approve" action to batch-import: 1. Select all pending items 2. Actions → "Approve selected devices for import" 3. Devices will be created and status set to imported

References