mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Enhance sensor telemetry cards
This commit is contained in:
@@ -15,6 +15,10 @@ class SensorsProvider with ChangeNotifier {
|
||||
static const String _watchedSensorsKey = 'watched_sensor_keys';
|
||||
static const String _visibleSensorMetricsKey = 'visible_sensor_metrics';
|
||||
static const String _fieldSpanKey = 'sensor_field_spans';
|
||||
static const String _metricLabelKey = 'sensor_metric_labels';
|
||||
static const String _metricOrderKey = 'sensor_metric_order';
|
||||
static const String _autoRefreshMinutesKey = 'sensor_auto_refresh_minutes';
|
||||
static const List<int> supportedAutoRefreshIntervals = <int>[0, 1, 5, 15];
|
||||
static const Set<String> _defaultVisibleFields = <String>{
|
||||
'voltage',
|
||||
'battery',
|
||||
@@ -23,6 +27,14 @@ class SensorsProvider with ChangeNotifier {
|
||||
'pressure',
|
||||
'gps',
|
||||
};
|
||||
static const List<String> _defaultMetricOrder = <String>[
|
||||
'voltage',
|
||||
'battery',
|
||||
'temperature',
|
||||
'humidity',
|
||||
'pressure',
|
||||
'gps',
|
||||
];
|
||||
|
||||
final List<String> _watchedSensorKeys = <String>[];
|
||||
final Map<String, SensorRefreshState> _refreshStates =
|
||||
@@ -32,8 +44,15 @@ class SensorsProvider with ChangeNotifier {
|
||||
<String, Set<String>>{};
|
||||
final Map<String, Map<String, int>> _fieldSpansBySensor =
|
||||
<String, Map<String, int>>{};
|
||||
final Map<String, Map<String, String>> _metricLabelsBySensor =
|
||||
<String, Map<String, String>>{};
|
||||
final Map<String, List<String>> _metricOrderBySensor =
|
||||
<String, List<String>>{};
|
||||
final Map<String, int> _autoRefreshMinutesBySensor = <String, int>{};
|
||||
final Map<String, DateTime> _lastRefreshAttemptAt = <String, DateTime>{};
|
||||
bool _isLoaded = false;
|
||||
bool _isRefreshingAll = false;
|
||||
bool _isRunningAutoRefreshTick = false;
|
||||
|
||||
SensorsProvider() {
|
||||
unawaited(_loadWatchedSensors());
|
||||
@@ -52,11 +71,17 @@ class SensorsProvider with ChangeNotifier {
|
||||
final stored = prefs.getStringList(_watchedSensorsKey) ?? <String>[];
|
||||
final storedMetricsJson = prefs.getString(_visibleSensorMetricsKey);
|
||||
final storedSpansJson = prefs.getString(_fieldSpanKey);
|
||||
final storedLabelsJson = prefs.getString(_metricLabelKey);
|
||||
final storedOrderJson = prefs.getString(_metricOrderKey);
|
||||
final storedAutoRefreshJson = prefs.getString(_autoRefreshMinutesKey);
|
||||
_watchedSensorKeys
|
||||
..clear()
|
||||
..addAll(stored);
|
||||
_visibleFieldsBySensor.clear();
|
||||
_fieldSpansBySensor.clear();
|
||||
_metricLabelsBySensor.clear();
|
||||
_metricOrderBySensor.clear();
|
||||
_autoRefreshMinutesBySensor.clear();
|
||||
if (storedMetricsJson != null && storedMetricsJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(storedMetricsJson) as Map<String, dynamic>;
|
||||
for (final entry in decoded.entries) {
|
||||
@@ -72,12 +97,47 @@ class SensorsProvider with ChangeNotifier {
|
||||
.map((key, value) => MapEntry(key, value as int));
|
||||
}
|
||||
}
|
||||
if (storedLabelsJson != null && storedLabelsJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(storedLabelsJson) as Map<String, dynamic>;
|
||||
for (final entry in decoded.entries) {
|
||||
_metricLabelsBySensor[entry.key] =
|
||||
(entry.value as Map<String, dynamic>).map(
|
||||
(key, value) => MapEntry(key, value as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (storedOrderJson != null && storedOrderJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(storedOrderJson) as Map<String, dynamic>;
|
||||
for (final entry in decoded.entries) {
|
||||
_metricOrderBySensor[entry.key] = (entry.value as List<dynamic>)
|
||||
.cast<String>()
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
if (storedAutoRefreshJson != null && storedAutoRefreshJson.isNotEmpty) {
|
||||
final decoded =
|
||||
jsonDecode(storedAutoRefreshJson) as Map<String, dynamic>;
|
||||
for (final entry in decoded.entries) {
|
||||
final minutes = (entry.value as num).toInt();
|
||||
if (minutes > 0) {
|
||||
_autoRefreshMinutesBySensor[entry.key] = minutes;
|
||||
}
|
||||
}
|
||||
}
|
||||
_autoRefreshMinutesBySensor.removeWhere(
|
||||
(key, _) => !_watchedSensorKeys.contains(key),
|
||||
);
|
||||
for (final key in _watchedSensorKeys) {
|
||||
_visibleFieldsBySensor.putIfAbsent(
|
||||
key,
|
||||
() => Set<String>.from(_defaultVisibleFields),
|
||||
);
|
||||
_fieldSpansBySensor.putIfAbsent(key, () => <String, int>{});
|
||||
_metricLabelsBySensor.putIfAbsent(key, () => <String, String>{});
|
||||
_metricOrderBySensor.putIfAbsent(
|
||||
key,
|
||||
() => List<String>.from(_defaultMetricOrder),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading watched sensors: $e');
|
||||
@@ -118,6 +178,36 @@ class SensorsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistMetricLabels() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_metricLabelKey, jsonEncode(_metricLabelsBySensor));
|
||||
} catch (e) {
|
||||
debugPrint('Error saving sensor metric labels: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistMetricOrder() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_metricOrderKey, jsonEncode(_metricOrderBySensor));
|
||||
} catch (e) {
|
||||
debugPrint('Error saving sensor metric order: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistAutoRefreshMinutes() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_autoRefreshMinutesKey,
|
||||
jsonEncode(_autoRefreshMinutesBySensor),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving sensor auto refresh minutes: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> visibleFieldsFor(String publicKeyHex) => Set<String>.unmodifiable(
|
||||
_visibleFieldsBySensor[publicKeyHex] ?? _defaultVisibleFields,
|
||||
);
|
||||
@@ -131,6 +221,78 @@ class SensorsProvider with ChangeNotifier {
|
||||
return span == 2 ? 2 : 1;
|
||||
}
|
||||
|
||||
List<String> metricOrderFor(
|
||||
String publicKeyHex,
|
||||
Iterable<String> availableFieldKeys,
|
||||
) {
|
||||
final available = availableFieldKeys.toList();
|
||||
final availableSet = available.toSet();
|
||||
final ordered = <String>[];
|
||||
final seen = <String>{};
|
||||
final stored =
|
||||
_metricOrderBySensor[publicKeyHex] ??
|
||||
List<String>.from(_defaultMetricOrder);
|
||||
|
||||
for (final fieldKey in stored) {
|
||||
if (availableSet.contains(fieldKey) && seen.add(fieldKey)) {
|
||||
ordered.add(fieldKey);
|
||||
}
|
||||
}
|
||||
for (final fieldKey in available) {
|
||||
if (seen.add(fieldKey)) {
|
||||
ordered.add(fieldKey);
|
||||
}
|
||||
}
|
||||
return List<String>.unmodifiable(ordered);
|
||||
}
|
||||
|
||||
Map<String, String> labelOverridesFor(String publicKeyHex) =>
|
||||
Map<String, String>.unmodifiable(
|
||||
_metricLabelsBySensor[publicKeyHex] ?? const <String, String>{},
|
||||
);
|
||||
|
||||
String? labelOverrideFor(String publicKeyHex, String fieldKey) =>
|
||||
_metricLabelsBySensor[publicKeyHex]?[fieldKey];
|
||||
|
||||
int autoRefreshMinutesFor(String publicKeyHex) =>
|
||||
_autoRefreshMinutesBySensor[publicKeyHex] ?? 0;
|
||||
|
||||
Future<void> setAutoRefreshMinutes(String publicKeyHex, int minutes) async {
|
||||
final normalizedMinutes = minutes <= 0 ? 0 : minutes;
|
||||
final currentMinutes = autoRefreshMinutesFor(publicKeyHex);
|
||||
if (currentMinutes == normalizedMinutes) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizedMinutes == 0) {
|
||||
_autoRefreshMinutesBySensor.remove(publicKeyHex);
|
||||
} else {
|
||||
_autoRefreshMinutesBySensor[publicKeyHex] = normalizedMinutes;
|
||||
}
|
||||
await _persistAutoRefreshMinutes();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<String> dueAutoRefreshSensorKeys({DateTime? now}) {
|
||||
final refreshTime = now ?? DateTime.now();
|
||||
final dueKeys = <String>[];
|
||||
|
||||
for (final key in _watchedSensorKeys) {
|
||||
final minutes = autoRefreshMinutesFor(key);
|
||||
if (minutes <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final lastRefreshAt = _lastRefreshAttemptAt[key];
|
||||
if (lastRefreshAt == null ||
|
||||
refreshTime.difference(lastRefreshAt) >= Duration(minutes: minutes)) {
|
||||
dueKeys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return List<String>.unmodifiable(dueKeys);
|
||||
}
|
||||
|
||||
Future<void> toggleMetric(
|
||||
String publicKeyHex,
|
||||
String fieldKey,
|
||||
@@ -140,8 +302,17 @@ class SensorsProvider with ChangeNotifier {
|
||||
publicKeyHex,
|
||||
() => Set<String>.from(_defaultVisibleFields),
|
||||
);
|
||||
final metricOrder = _metricOrderBySensor.putIfAbsent(
|
||||
publicKeyHex,
|
||||
() => List<String>.from(_defaultMetricOrder),
|
||||
);
|
||||
var shouldPersistOrder = false;
|
||||
if (visible) {
|
||||
visibleFields.add(fieldKey);
|
||||
if (!metricOrder.contains(fieldKey)) {
|
||||
metricOrder.add(fieldKey);
|
||||
shouldPersistOrder = true;
|
||||
}
|
||||
} else {
|
||||
if (visibleFields.length == 1 && visibleFields.contains(fieldKey)) {
|
||||
return;
|
||||
@@ -149,6 +320,9 @@ class SensorsProvider with ChangeNotifier {
|
||||
visibleFields.remove(fieldKey);
|
||||
}
|
||||
await _persistVisibleMetrics();
|
||||
if (shouldPersistOrder) {
|
||||
await _persistMetricOrder();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -166,6 +340,57 @@ class SensorsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setMetricLabel(
|
||||
String publicKeyHex,
|
||||
String fieldKey,
|
||||
String? label,
|
||||
) async {
|
||||
final sensorLabels = _metricLabelsBySensor.putIfAbsent(
|
||||
publicKeyHex,
|
||||
() => <String, String>{},
|
||||
);
|
||||
final trimmed = label?.trim();
|
||||
if (trimmed == null || trimmed.isEmpty) {
|
||||
sensorLabels.remove(fieldKey);
|
||||
} else {
|
||||
sensorLabels[fieldKey] = trimmed;
|
||||
}
|
||||
if (sensorLabels.isEmpty) {
|
||||
_metricLabelsBySensor.remove(publicKeyHex);
|
||||
}
|
||||
await _persistMetricLabels();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> moveMetric(
|
||||
String publicKeyHex, {
|
||||
required List<String> availableFieldKeys,
|
||||
required int oldIndex,
|
||||
required int newIndex,
|
||||
}) async {
|
||||
if (oldIndex < 0 ||
|
||||
newIndex < 0 ||
|
||||
oldIndex >= availableFieldKeys.length ||
|
||||
newIndex >= availableFieldKeys.length ||
|
||||
oldIndex == newIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
final reordered = List<String>.from(
|
||||
metricOrderFor(publicKeyHex, availableFieldKeys),
|
||||
);
|
||||
final fieldKey = reordered.removeAt(oldIndex);
|
||||
reordered.insert(newIndex, fieldKey);
|
||||
|
||||
final storedTail =
|
||||
(_metricOrderBySensor[publicKeyHex] ?? _defaultMetricOrder).where(
|
||||
(key) => !reordered.contains(key),
|
||||
);
|
||||
_metricOrderBySensor[publicKeyHex] = <String>[...reordered, ...storedTail];
|
||||
await _persistMetricOrder();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isWatched(String publicKeyHex) =>
|
||||
_watchedSensorKeys.contains(publicKeyHex);
|
||||
|
||||
@@ -183,8 +408,14 @@ class SensorsProvider with ChangeNotifier {
|
||||
_defaultVisibleFields,
|
||||
);
|
||||
_fieldSpansBySensor[contact.publicKeyHex] = <String, int>{'gps': 2};
|
||||
_metricLabelsBySensor[contact.publicKeyHex] = <String, String>{};
|
||||
_metricOrderBySensor[contact.publicKeyHex] = List<String>.from(
|
||||
_defaultMetricOrder,
|
||||
);
|
||||
await _persistVisibleMetrics();
|
||||
await _persistFieldSpans();
|
||||
await _persistMetricLabels();
|
||||
await _persistMetricOrder();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -194,9 +425,16 @@ class SensorsProvider with ChangeNotifier {
|
||||
_refreshStateUpdatedAt.remove(publicKeyHex);
|
||||
_visibleFieldsBySensor.remove(publicKeyHex);
|
||||
_fieldSpansBySensor.remove(publicKeyHex);
|
||||
_metricLabelsBySensor.remove(publicKeyHex);
|
||||
_metricOrderBySensor.remove(publicKeyHex);
|
||||
_autoRefreshMinutesBySensor.remove(publicKeyHex);
|
||||
_lastRefreshAttemptAt.remove(publicKeyHex);
|
||||
await _persistWatchedSensors();
|
||||
await _persistVisibleMetrics();
|
||||
await _persistFieldSpans();
|
||||
await _persistMetricLabels();
|
||||
await _persistMetricOrder();
|
||||
await _persistAutoRefreshMinutes();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -240,7 +478,14 @@ class SensorsProvider with ChangeNotifier {
|
||||
required String publicKeyHex,
|
||||
required ContactsProvider contactsProvider,
|
||||
required ConnectionProvider connectionProvider,
|
||||
DateTime? requestedAt,
|
||||
}) async {
|
||||
if (stateFor(publicKeyHex) == SensorRefreshState.refreshing) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastRefreshAttemptAt[publicKeyHex] = requestedAt ?? DateTime.now();
|
||||
|
||||
Contact? contact;
|
||||
for (final entry in contactsProvider.contacts) {
|
||||
if (entry.publicKeyHex == publicKeyHex) {
|
||||
@@ -267,6 +512,38 @@ class SensorsProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> refreshDueSensors({
|
||||
required ContactsProvider contactsProvider,
|
||||
required ConnectionProvider connectionProvider,
|
||||
DateTime? now,
|
||||
}) async {
|
||||
if (!connectionProvider.deviceInfo.isConnected ||
|
||||
_isRefreshingAll ||
|
||||
_isRunningAutoRefreshTick) {
|
||||
return;
|
||||
}
|
||||
|
||||
final refreshTime = now ?? DateTime.now();
|
||||
final dueKeys = dueAutoRefreshSensorKeys(now: refreshTime);
|
||||
if (dueKeys.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isRunningAutoRefreshTick = true;
|
||||
try {
|
||||
for (final key in dueKeys) {
|
||||
await refreshSensor(
|
||||
publicKeyHex: key,
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
requestedAt: refreshTime,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
_isRunningAutoRefreshTick = false;
|
||||
}
|
||||
}
|
||||
|
||||
void clearExpiredRefreshStates({DateTime? now}) {
|
||||
final cutoff = (now ?? DateTime.now()).subtract(_successStateRetention);
|
||||
final keysToClear = <String>[];
|
||||
|
||||
Reference in New Issue
Block a user