Fix telemetry merge bug

This commit is contained in:
Janez T
2026-03-15 20:58:12 +01:00
parent 1dcd85f178
commit 991421d097
2 changed files with 107 additions and 7 deletions

View File

@@ -878,7 +878,16 @@ class ContactsProvider with ChangeNotifier {
' ⚠️ Retaining last valid GPS. Incoming telemetry GPS is invalid/missing: $incomingGps', ' ⚠️ 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 // 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. // last known reading for any field that is omitted in the incoming update.
final incomingGps = _getValidGpsOrNull(incomingTelemetry.gpsLocation); final incomingGps = _getValidGpsOrNull(incomingTelemetry.gpsLocation);
final previousGps = _getValidGpsOrNull(existingTelemetry?.gpsLocation); final previousGps = _getValidGpsOrNull(existingTelemetry?.gpsLocation);
final mergedExtraSensorData = <String, dynamic>{ final mergedExtraSensorData = _mergeExtraSensorData(
...?existingTelemetry?.extraSensorData, existingTelemetry?.extraSensorData,
...?incomingTelemetry.extraSensorData, incomingTelemetry.extraSensorData,
}; );
return ContactTelemetry( return ContactTelemetry(
gpsLocation: incomingGps ?? previousGps, gpsLocation: incomingGps ?? previousGps,
@@ -1017,6 +1026,49 @@ class ContactsProvider with ChangeNotifier {
); );
} }
Map<String, dynamic> _mergeExtraSensorData(
Map<String, dynamic>? existingExtraSensorData,
Map<String, dynamic>? incomingExtraSensorData,
) {
final merged = <String, dynamic>{...?existingExtraSensorData};
if (incomingExtraSensorData == null || incomingExtraSensorData.isEmpty) {
return merged;
}
final incomingMetricFamilies = incomingExtraSensorData.keys
.map(_telemetryMetricFamilyForKey)
.whereType<String>()
.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) { int _coordinateToAdvertMicrodegrees(double coordinate) {
return (coordinate * 1e6).round(); return (coordinate * 1e6).round();
} }

View File

@@ -336,7 +336,7 @@ void main() {
expect(updated.telemetry!.extraSensorData, containsPair('co2', 415.0)); 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( final fullTelemetry = ContactTelemetry(
gpsLocation: const LatLng(46.0569, 14.5058), gpsLocation: const LatLng(46.0569, 14.5058),
batteryPercentage: 54.0, batteryPercentage: 54.0,
@@ -366,7 +366,55 @@ void main() {
expect(updated.telemetry!.temperature, equals(19.5)); expect(updated.telemetry!.temperature, equals(19.5));
expect(updated.telemetry!.humidity, equals(58.0)); expect(updated.telemetry!.humidity, equals(58.0));
expect(updated.telemetry!.pressure, equals(1011.2)); 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', () { test('builds message snapshot from latest valid telemetry', () {