Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/core/constants/device_field_sets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class DeviceFieldSets {
'mac', // Access points/ONTs use 'mac'
'scratch', // Switches store MAC in 'scratch'
'pms_room', // Full nested object
'pms_room_id', // Flat field (switches may not have nested pms_room)
'switch_ports', // Switch port list — used to correlate switches to PMS rooms
'location',
'last_seen',
'signal_strength',
Expand Down
8 changes: 4 additions & 4 deletions lib/core/services/device_normalizer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class DeviceNormalizer {
/// Normalize raw JSON to APModel
APModel normalizeToAP(Map<String, dynamic> data) {
return APModel(
id: (data['id'] ?? '').toString(),
id: 'ap_${data['id'] ?? ''}',
name: data['name']?.toString() ?? 'Unknown AP',
status: determineStatus(data),
pmsRoomId: extractPmsRoomId(data),
Expand Down Expand Up @@ -41,7 +41,7 @@ class DeviceNormalizer {
/// Normalize raw JSON to ONTModel
ONTModel normalizeToONT(Map<String, dynamic> data) {
return ONTModel(
id: (data['id'] ?? '').toString(),
id: 'ont_${data['id'] ?? ''}',
name: data['name']?.toString() ?? 'Unknown ONT',
status: determineStatus(data),
pmsRoomId: extractPmsRoomId(data),
Expand Down Expand Up @@ -70,7 +70,7 @@ class DeviceNormalizer {
/// Normalize raw JSON to SwitchModel
SwitchModel normalizeToSwitch(Map<String, dynamic> data) {
return SwitchModel(
id: (data['id'] ?? '').toString(),
id: 'sw_${data['id'] ?? ''}',
name: data['name']?.toString() ?? 'Unknown Switch',
status: determineStatus(data),
pmsRoomId: extractPmsRoomId(data),
Expand All @@ -94,7 +94,7 @@ class DeviceNormalizer {
/// Normalize raw JSON to WLANModel
WLANModel normalizeToWLAN(Map<String, dynamic> data) {
return WLANModel(
id: (data['id'] ?? '').toString(),
id: 'wlan_${data['id'] ?? ''}',
name: data['name']?.toString() ?? 'Unknown WLAN',
status: determineStatus(data),
pmsRoomId: extractPmsRoomId(data),
Expand Down
206 changes: 206 additions & 0 deletions lib/core/services/websocket_cache_integration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ class WebSocketCacheIntegration {
/// Cached room data.
final List<Map<String, dynamic>> _roomCache = [];

/// Port ID → switch raw int ID, built from switch devices' embedded ports.
/// Used to correlate room-embedded port IDs back to their parent switch.
final Map<int, int> _portToSwitchIndex = {};

/// Callbacks for when room data is received.
final List<void Function(List<Map<String, dynamic>>)> _roomDataCallbacks = [];

Expand Down Expand Up @@ -1271,9 +1275,150 @@ class WebSocketCacheIntegration {
return null;
}

int? _parseIntId(dynamic value) {
if (value is int) return value;
if (value is String) return int.tryParse(value);
if (value is num) return value.toInt();
return null;
}

/// Build switch device ID → room ID index using port ID overlap.
///
/// Rooms embed switch_ports as [{id, name}] — only the port's own ID.
/// Switch devices embed the same ports with the same IDs.
/// [_portToSwitchIndex] (portId → switchId) bridges the two.
Map<int, int> _buildSwitchToRoomIndex() {
final index = <int, int>{};
final sampleSwitchIds = <int>[];

var switchPortEntries = 0;
var switchDeviceEntries = 0;
for (final room in _roomCache) {
final roomId = _parseIntId(room['id']);
if (roomId == null) {
continue;
}

final switchPorts = room['switch_ports'];
if (switchPorts is List && switchPorts.isNotEmpty) {
for (final entry in switchPorts) {
if (entry is! Map<String, dynamic>) continue;

// Try direct FK first (future-proof if server ever embeds it)
final sw = entry['switch_device'];
final directId = _parseIntId(
sw is Map<String, dynamic> ? sw['id'] : entry['switch_device_id'],
);
if (directId != null) {
index[directId] = roomId;
switchPortEntries++;
if (sampleSwitchIds.length < 5) sampleSwitchIds.add(directId);
continue;
}

// Primary: look up port ID in port→switch index
final portId = _parseIntId(entry['id']);
if (portId != null) {
final swId = _portToSwitchIndex[portId];
if (swId != null) {
index[swId] = roomId;
switchPortEntries++;
if (sampleSwitchIds.length < 5) sampleSwitchIds.add(swId);
}
}
}
} else {
final switchDevices = room['switch_devices'];
if (switchDevices is List) {
for (final entry in switchDevices) {
if (entry is! Map<String, dynamic>) continue;
final swId = _parseIntId(entry['id']);
if (swId != null) {
index[swId] = roomId;
switchDeviceEntries++;
if (sampleSwitchIds.length < 5) sampleSwitchIds.add(swId);
}
}
}
}
}
_logger.i(
'WebSocketCacheIntegration: switch→room index built '
'(rooms=${_roomCache.length}, index=${index.length}, '
'portIndex=${_portToSwitchIndex.length}, '
'switch_ports=$switchPortEntries, switch_devices=$switchDeviceEntries, '
'sample=${sampleSwitchIds.join(', ')})',
);
return index;
}

/// Stamp pms_room_id onto cached switch devices using room data.
/// Returns the number of devices stamped.
int _backPopulateSwitchRoomIds() {
final switches = _deviceCache['switch_devices'];
if (switches == null || switches.isEmpty) {
_logger.i(
'WebSocketCacheIntegration: Back-populate skipped (no switch cache)',
);
return 0;
}
if (_roomCache.isEmpty) {
_logger.i(
'WebSocketCacheIntegration: Back-populate skipped (no room cache)',
);
return 0;
}

final index = _buildSwitchToRoomIndex();
if (index.isEmpty) {
_logger.i(
'WebSocketCacheIntegration: Back-populate skipped (empty index)',
);
return 0;
}

var stamped = 0;
var missingMapping = 0;
var alreadySet = 0;
var invalidIds = 0;
final sampleMissing = <int>[];
for (final sw in switches) {
final swId = _parseIntId(sw['id']);
if (swId == null) {
invalidIds++;
continue;
}
final roomId = index[swId];
if (roomId == null) {
missingMapping++;
if (sampleMissing.length < 5) {
sampleMissing.add(swId);
}
continue;
}
if (sw['pms_room_id'] == null) {
sw['pms_room_id'] = roomId;
stamped++;
} else {
alreadySet++;
}
}

_logger.i(
'WebSocketCacheIntegration: Back-populate summary '
'(switches=${switches.length}, stamped=$stamped, '
'alreadySet=$alreadySet, missingMap=$missingMapping, '
'invalidIds=$invalidIds, sampleMissing=${sampleMissing.join(', ')})',
);
return stamped;
}

void _applySnapshot(String resourceType, List<Map<String, dynamic>> items) {
if (resourceType == _roomResourceType) {
// Handle room data
_logger.i(
'WebSocketCacheIntegration: rooms snapshot - ${items.length} items',
);
_roomCache
..clear()
..addAll(items);
Expand All @@ -1283,6 +1428,18 @@ class WebSocketCacheIntegration {
for (final callback in _roomDataCallbacks) {
callback(items);
}

// Back-populate pms_room_id onto switch devices now that rooms are updated
final stamped = _backPopulateSwitchRoomIds();
if (stamped > 0) {
_bumpDeviceUpdate();
final switchCache = _deviceCache['switch_devices'];
if (switchCache != null) {
for (final callback in _deviceDataCallbacks) {
callback('switch_devices', switchCache);
}
}
}
} else if (resourceType == _speedTestConfigResourceType) {
// Handle speed test config data
_speedTestConfigCache
Expand Down Expand Up @@ -1317,6 +1474,35 @@ class WebSocketCacheIntegration {
_bumpLastUpdate();
_bumpDeviceUpdate();

// Back-populate pms_room_id onto switch devices from room data
if (resourceType == 'switch_devices') {
// Build portId → switchId index from each switch device's embedded ports.
// Room snapshots only embed port {id, name} — this index lets us look up
// the owning switch from that port ID.
_portToSwitchIndex.clear();
var switchesWithPorts = 0;
for (final sw in items) {
final swRawId = _parseIntId(sw['id']);
if (swRawId == null) continue;
final ports = sw['switch_ports'];
if (ports is List && ports.isNotEmpty) {
switchesWithPorts++;
for (final port in ports) {
if (port is! Map<String, dynamic>) continue;
final portId = _parseIntId(port['id']);
if (portId != null) _portToSwitchIndex[portId] = swRawId;
}
}
}
LoggerService.info(
'🔌 [SWITCH-PORT] CacheIntegration switch_devices snapshot — '
'${items.length} items, switchesWithPorts=$switchesWithPorts, '
'portIndexSize=${_portToSwitchIndex.length}',
tag: 'SwitchPort',
);
_backPopulateSwitchRoomIds();
}

// Debug: Log first device's keys to see what fields backend is sending
if (items.isNotEmpty) {
final firstItem = items.first;
Expand Down Expand Up @@ -1446,6 +1632,18 @@ class WebSocketCacheIntegration {
for (final callback in _roomDataCallbacks) {
callback(_roomCache);
}

// Back-populate pms_room_id onto switch devices now that room data changed
final stamped = _backPopulateSwitchRoomIds();
if (stamped > 0) {
_bumpDeviceUpdate();
final switchCache = _deviceCache['switch_devices'];
if (switchCache != null) {
for (final callback in _deviceDataCallbacks) {
callback('switch_devices', switchCache);
}
}
}
} else if (resourceType == _speedTestConfigResourceType) {
// Handle speed test config upsert
final index = _speedTestConfigCache.indexWhere((item) => item['id'] == id);
Expand Down Expand Up @@ -1482,6 +1680,11 @@ class WebSocketCacheIntegration {
_bumpLastUpdate();
_bumpDeviceUpdate();

// Back-populate pms_room_id onto switch devices from room data
if (resourceType == 'switch_devices') {
_backPopulateSwitchRoomIds();
}

// Notify device callbacks
for (final callback in _deviceDataCallbacks) {
callback(resourceType, cache);
Expand Down Expand Up @@ -1631,6 +1834,7 @@ class WebSocketCacheIntegration {
// Clear device, room, and speed test caches
_deviceCache.clear();
_roomCache.clear();
_portToSwitchIndex.clear();
_speedTestConfigCache.clear();
_speedTestResultCache.clear();

Expand All @@ -1653,6 +1857,7 @@ class WebSocketCacheIntegration {
// Clear device, room, and speed test caches
_deviceCache.clear();
_roomCache.clear();
_portToSwitchIndex.clear();
_speedTestConfigCache.clear();
_speedTestResultCache.clear();

Expand Down Expand Up @@ -1697,6 +1902,7 @@ class WebSocketCacheIntegration {
_speedTestResultCallbacks.clear();
_deviceCache.clear();
_roomCache.clear();
_portToSwitchIndex.clear();
_speedTestConfigCache.clear();
_speedTestResultCache.clear();
}
Expand Down
Loading