From 991421d09720da0780b8c814c18cef7898ca1bf1 Mon Sep 17 00:00:00 2001 From: Janez T Date: Sun, 15 Mar 2026 20:58:12 +0100 Subject: [PATCH] Fix telemetry merge bug --- lib/providers/contacts_provider.dart | 62 ++++++++++++++++++++-- test/providers/contacts_provider_test.dart | 52 +++++++++++++++++- 2 files changed, 107 insertions(+), 7 deletions(-) diff --git a/lib/providers/contacts_provider.dart b/lib/providers/contacts_provider.dart index 07dbc25..9567bc0 100644 --- a/lib/providers/contacts_provider.dart +++ b/lib/providers/contacts_provider.dart @@ -878,7 +878,16 @@ class ContactsProvider with ChangeNotifier { ' ⚠️ Retaining last valid GPS. Incoming telemetry GPS is invalid/missing: $incomingGps', ); } - telemetry = mergedTelemetry; + telemetry = ContactTelemetry( + gpsLocation: telemetry.gpsLocation, + batteryPercentage: mergedTelemetry.batteryPercentage, + batteryMilliVolts: mergedTelemetry.batteryMilliVolts, + temperature: mergedTelemetry.temperature, + timestamp: mergedTelemetry.timestamp, + humidity: mergedTelemetry.humidity, + pressure: mergedTelemetry.pressure, + extraSensorData: telemetry.extraSensorData, + ); } // Update contact with new telemetry AND last seen time @@ -993,10 +1002,10 @@ class ContactsProvider with ChangeNotifier { // last known reading for any field that is omitted in the incoming update. final incomingGps = _getValidGpsOrNull(incomingTelemetry.gpsLocation); final previousGps = _getValidGpsOrNull(existingTelemetry?.gpsLocation); - final mergedExtraSensorData = { - ...?existingTelemetry?.extraSensorData, - ...?incomingTelemetry.extraSensorData, - }; + final mergedExtraSensorData = _mergeExtraSensorData( + existingTelemetry?.extraSensorData, + incomingTelemetry.extraSensorData, + ); return ContactTelemetry( gpsLocation: incomingGps ?? previousGps, @@ -1017,6 +1026,49 @@ class ContactsProvider with ChangeNotifier { ); } + Map _mergeExtraSensorData( + Map? existingExtraSensorData, + Map? incomingExtraSensorData, + ) { + final merged = {...?existingExtraSensorData}; + if (incomingExtraSensorData == null || incomingExtraSensorData.isEmpty) { + return merged; + } + + final incomingMetricFamilies = incomingExtraSensorData.keys + .map(_telemetryMetricFamilyForKey) + .whereType() + .toSet(); + if (incomingMetricFamilies.isNotEmpty) { + merged.removeWhere((key, _) { + final family = _telemetryMetricFamilyForKey(key); + return family != null && incomingMetricFamilies.contains(family); + }); + } + + merged.addAll(incomingExtraSensorData); + return merged; + } + + String? _telemetryMetricFamilyForKey(String key) { + const sourcePrefix = '__source_channel:'; + if (key.startsWith(sourcePrefix)) { + return key.substring(sourcePrefix.length); + } + + final separatorIndex = key.lastIndexOf('_'); + if (separatorIndex <= 0 || separatorIndex == key.length - 1) { + return key; + } + + final suffix = key.substring(separatorIndex + 1); + if (int.tryParse(suffix) == null) { + return key; + } + + return key.substring(0, separatorIndex); + } + int _coordinateToAdvertMicrodegrees(double coordinate) { return (coordinate * 1e6).round(); } diff --git a/test/providers/contacts_provider_test.dart b/test/providers/contacts_provider_test.dart index 384a17f..1544c36 100644 --- a/test/providers/contacts_provider_test.dart +++ b/test/providers/contacts_provider_test.dart @@ -336,7 +336,7 @@ void main() { expect(updated.telemetry!.extraSensorData, containsPair('co2', 415.0)); }); - test('retains prior telemetry fields across sparse telemetry updates', () { + test('retains scalar telemetry but wipes stale extra sensor fields on refresh', () { final fullTelemetry = ContactTelemetry( gpsLocation: const LatLng(46.0569, 14.5058), batteryPercentage: 54.0, @@ -366,7 +366,55 @@ void main() { expect(updated.telemetry!.temperature, equals(19.5)); expect(updated.telemetry!.humidity, equals(58.0)); expect(updated.telemetry!.pressure, equals(1011.2)); - expect(updated.telemetry!.extraSensorData, containsPair('pm25', 8.0)); + expect(updated.telemetry!.extraSensorData, isNull); + }); + + test('replaces old source-channel mappings when a metric moves channels', () { + final initialTelemetry = ContactTelemetry( + gpsLocation: null, + batteryPercentage: null, + batteryMilliVolts: null, + temperature: 21.5, + timestamp: DateTime.now().subtract(const Duration(minutes: 2)), + humidity: null, + pressure: null, + extraSensorData: const { + '__source_channel:temperature': 2, + 'temperature_2': 21.5, + 'humidity_4': 66.0, + }, + ); + + provider.addOrUpdateContact( + createContact( + key: publicKey, + type: ContactType.chat, + ).copyWith(telemetry: initialTelemetry), + ); + + final movedChannelTelemetry = CayenneLppParser.createTemperatureData( + 23.5, + channel: 3, + ); + + provider.updateTelemetry(publicKey.sublist(0, 6), movedChannelTelemetry); + + final updated = provider.findContactByKey(publicKey)!; + expect(updated.telemetry, isNotNull); + expect(updated.telemetry!.temperature, closeTo(23.5, 0.1)); + expect( + updated.telemetry!.extraSensorData, + containsPair('__source_channel:temperature', 3), + ); + expect( + updated.telemetry!.extraSensorData, + containsPair('temperature_3', closeTo(23.5, 0.1)), + ); + expect( + updated.telemetry!.extraSensorData, + isNot(contains('temperature_2')), + ); + expect(updated.telemetry!.extraSensorData, isNot(contains('humidity_4'))); }); test('builds message snapshot from latest valid telemetry', () {