Update sensor parsing layout

This commit is contained in:
Janez T
2026-03-15 20:14:01 +01:00
parent 69377d90a4
commit 5d2f61749e
7 changed files with 1186 additions and 36 deletions

View File

@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/contact.dart';
import '../widgets/sensors/sensor_telemetry_card.dart';
import 'connection_provider.dart';
import 'contacts_provider.dart';
@@ -223,6 +224,22 @@ class SensorsProvider with ChangeNotifier {
_visibleFieldsBySensor[publicKeyHex] ?? _defaultVisibleFields,
);
Set<String> effectiveVisibleFieldsFor(
String publicKeyHex,
Iterable<String> availableFieldKeys,
) {
final storedVisibleFields =
_visibleFieldsBySensor[publicKeyHex] ?? _defaultVisibleFields;
if (!_shouldAutoIncludeAvailableFields(publicKeyHex, storedVisibleFields)) {
return Set<String>.unmodifiable(storedVisibleFields);
}
return Set<String>.unmodifiable(<String>{
...storedVisibleFields,
...availableFieldKeys,
});
}
bool showsField(String publicKeyHex, String fieldKey) =>
visibleFieldsFor(publicKeyHex).contains(fieldKey);
@@ -440,14 +457,14 @@ class SensorsProvider with ChangeNotifier {
}
_watchedSensorKeys.add(contact.publicKeyHex);
final initialVisibleFields = _initialVisibleFieldsForContact(contact);
await _persistWatchedSensors();
_visibleFieldsBySensor[contact.publicKeyHex] = Set<String>.from(
_defaultVisibleFields,
);
_visibleFieldsBySensor[contact.publicKeyHex] = initialVisibleFields;
_fieldSpansBySensor[contact.publicKeyHex] = <String, int>{'gps': 2};
_metricLabelsBySensor[contact.publicKeyHex] = <String, String>{};
_metricOrderBySensor[contact.publicKeyHex] = List<String>.from(
_defaultMetricOrder,
_metricOrderBySensor[contact.publicKeyHex] = metricOrderFor(
contact.publicKeyHex,
initialVisibleFields,
);
await _persistVisibleMetrics();
await _persistFieldSpans();
@@ -456,6 +473,37 @@ class SensorsProvider with ChangeNotifier {
notifyListeners();
}
bool _shouldAutoIncludeAvailableFields(
String publicKeyHex,
Set<String> storedVisibleFields,
) {
if (!setEquals(storedVisibleFields, _defaultVisibleFields)) {
return false;
}
final storedOrder = _metricOrderBySensor[publicKeyHex];
if (storedOrder != null && !listEquals(storedOrder, _defaultMetricOrder)) {
return false;
}
final storedLabels = _metricLabelsBySensor[publicKeyHex];
if (storedLabels != null && storedLabels.isNotEmpty) {
return false;
}
final storedSpans = _fieldSpansBySensor[publicKeyHex];
if (storedSpans != null &&
!(storedSpans.length == 1 && storedSpans['gps'] == 2)) {
return false;
}
return true;
}
Set<String> _initialVisibleFieldsForContact(Contact contact) {
return <String>{..._defaultVisibleFields, ...sensorMetricKeysFor(contact)};
}
Future<void> removeSensor(String publicKeyHex) async {
_watchedSensorKeys.remove(publicKeyHex);
_refreshStates.remove(publicKeyHex);

View File

@@ -294,19 +294,20 @@ class _SensorsTabState extends State<SensorsTab> {
break;
}
}
final availableFieldKeys = sensorMetricKeysFor(contact);
final visibleFields = sensorsProvider
.effectiveVisibleFieldsFor(key, availableFieldKeys);
return SensorTelemetryCard(
contact: contact,
state: sensorsProvider.stateFor(key),
visibleFields: sensorsProvider.visibleFieldsFor(key),
visibleFields: visibleFields,
fieldOrder: sensorsProvider.metricOrderFor(
key,
sensorsProvider.visibleFieldsFor(key),
availableFieldKeys,
),
labelOverrides: sensorsProvider.labelOverridesFor(key),
fieldSpans: {
for (final field in sensorsProvider.visibleFieldsFor(
key,
))
for (final field in visibleFields)
field: sensorsProvider.fieldSpanFor(key, field),
},
onRemove: () async {
@@ -359,14 +360,17 @@ class _SensorCustomizeView extends StatelessWidget {
}
}
final visibleFields = sensorsProvider.visibleFieldsFor(publicKeyHex);
final autoRefreshMinutes = sensorsProvider.autoRefreshMinutesFor(
publicKeyHex,
);
final options = sensorMetricOptionsFor(
contact,
labelOverrides: sensorsProvider.labelOverridesFor(publicKeyHex),
);
final visibleFields = sensorsProvider.effectiveVisibleFieldsFor(
publicKeyHex,
options.map((option) => option.key),
);
final autoRefreshMinutes = sensorsProvider.autoRefreshMinutesFor(
publicKeyHex,
);
final orderedFieldKeys = sensorsProvider.metricOrderFor(
publicKeyHex,
options.map((option) => option.key),

View File

@@ -19,7 +19,61 @@ class CayenneLppParser {
static const int _lppDirection = 132;
static const int _lppUnixTime = 133;
static const int _lppColour = 135;
static const int _lppGust = 137;
static const int _lppDewPoint = 138;
static const int _lppRain = 139;
static const int _lppSwitch = 142;
static const int _lppBinaryBool = 143;
static const int _lppBinaryPowerSwitch = 144;
static const int _lppBinaryOpen = 145;
static const int _lppBinaryBatteryLow = 146;
static const int _lppBinaryCharging = 147;
static const int _lppBinaryCarbonMonoxide = 148;
static const int _lppBinaryCold = 149;
static const int _lppBinaryConnectivity = 150;
static const int _lppBinaryDoor = 151;
static const int _lppBinaryGarageDoor = 152;
static const int _lppBinaryGas = 153;
static const int _lppBinaryHeat = 154;
static const int _lppBinaryLight = 155;
static const int _lppBinaryLock = 156;
static const int _lppBinaryMoisture = 157;
static const int _lppBinaryMotion = 158;
static const int _lppBinaryMoving = 159;
static const int _lppBinaryOccupancy = 160;
static const int _lppBinaryPlug = 161;
static const int _lppBinaryPresence = 162;
static const int _lppBinaryProblem = 163;
static const int _lppBinaryRunning = 164;
static const int _lppBinarySafety = 165;
static const int _lppBinarySmoke = 166;
static const int _lppBinarySound = 167;
static const int _lppBinaryTamper = 168;
static const int _lppBinaryVibration = 169;
static const int _lppBinaryWindow = 170;
static const int _lppButtonEvent = 171;
static const int _lppDimmer = 172;
static const int _lppUv = 173;
static const int _lppLightLevel = 174;
static const int _lppPm25 = 175;
static const int _lppPm10 = 176;
static const int _lppCo2 = 177;
static const int _lppTvoc = 178;
static const int _lppRpm = 179;
static const int _lppConductivity = 180;
static const int _lppRotation = 181;
static const int _lppDuration = 182;
static const int _lppAcceleration = 183;
static const int _lppGyroRate = 184;
static const int _lppVolume = 185;
static const int _lppFlowRate = 186;
static const int _lppVolumeStorage = 187;
static const int _lppWater = 188;
static const int _lppGasVolume = 189;
static const int _lppMass = 190;
static const int _lppSignedSpeed = 191;
static const int _lppSignedPower = 192;
static const int _lppSignedCurrent = 193;
/// Parse Cayenne LPP data into ContactTelemetry
static ContactTelemetry parse(Uint8List data) {
@@ -345,6 +399,212 @@ class CayenneLppParser {
};
break;
case _lppGust:
final rawValue = reader.readUInt16BE();
final value = rawValue / 100.0;
debugPrint(' Gust: ${value}m/s');
extraSensorData['gust_$channel'] = value;
break;
case _lppDewPoint:
final rawValue = reader.readInt16BE();
final value = rawValue / 10.0;
debugPrint(' Dew point: ${value.toStringAsFixed(1)}°C');
extraSensorData['dew_$channel'] = value;
break;
case _lppRain:
final rawValue = reader.readUInt16BE();
final value = rawValue / 10.0;
debugPrint(' Rain: ${value}mm');
extraSensorData['rain_$channel'] = value;
break;
case _lppBinaryBool:
case _lppBinaryPowerSwitch:
case _lppBinaryOpen:
case _lppBinaryBatteryLow:
case _lppBinaryCharging:
case _lppBinaryCarbonMonoxide:
case _lppBinaryCold:
case _lppBinaryConnectivity:
case _lppBinaryDoor:
case _lppBinaryGarageDoor:
case _lppBinaryGas:
case _lppBinaryHeat:
case _lppBinaryLight:
case _lppBinaryLock:
case _lppBinaryMoisture:
case _lppBinaryMotion:
case _lppBinaryMoving:
case _lppBinaryOccupancy:
case _lppBinaryPlug:
case _lppBinaryPresence:
case _lppBinaryProblem:
case _lppBinaryRunning:
case _lppBinarySafety:
case _lppBinarySmoke:
case _lppBinarySound:
case _lppBinaryTamper:
case _lppBinaryVibration:
case _lppBinaryWindow:
final value = reader.readByte();
debugPrint(' Binary state: $value');
extraSensorData['${_binaryMetricKeyForType(type)}_$channel'] =
value;
break;
case _lppButtonEvent:
final value = reader.readByte();
debugPrint(' Button event: $value');
extraSensorData['button_event_$channel'] = value;
break;
case _lppDimmer:
final value = _readInt8(reader);
debugPrint(' Dimmer: $value');
extraSensorData['dimmer_$channel'] = value;
break;
case _lppUv:
final value = reader.readByte() / 10.0;
debugPrint(' UV index: $value');
extraSensorData['uv_$channel'] = value;
break;
case _lppLightLevel:
final value = reader.readByte();
debugPrint(' Light level: $value');
extraSensorData['light_level_$channel'] = value;
break;
case _lppPm25:
final value = reader.readUInt16BE().toDouble();
debugPrint(' PM2.5: $value');
extraSensorData['pm25_$channel'] = value;
break;
case _lppPm10:
final value = reader.readUInt16BE().toDouble();
debugPrint(' PM10: $value');
extraSensorData['pm10_$channel'] = value;
break;
case _lppCo2:
final value = reader.readUInt16BE().toDouble();
debugPrint(' CO2: $value');
extraSensorData['co2_$channel'] = value;
break;
case _lppTvoc:
final value = reader.readUInt16BE().toDouble();
debugPrint(' TVOC: $value');
extraSensorData['tvoc_$channel'] = value;
break;
case _lppRpm:
final value = reader.readUInt16BE().toDouble();
debugPrint(' RPM: $value');
extraSensorData['rpm_$channel'] = value;
break;
case _lppConductivity:
final value = reader.readUInt16BE().toDouble();
debugPrint(' Conductivity: $value');
extraSensorData['conductivity_$channel'] = value;
break;
case _lppRotation:
final rawValue = reader.readInt16BE();
final value = rawValue / 10.0;
debugPrint(' Rotation: $value');
extraSensorData['rotation_$channel'] = value;
break;
case _lppDuration:
final rawValue = _readUInt32BE(reader);
final value = rawValue / 1000.0;
debugPrint(' Duration: $value s');
extraSensorData['duration_$channel'] = value;
break;
case _lppAcceleration:
final rawValue = _readInt32BE(reader);
final value = rawValue / 1000000.0;
debugPrint(' Acceleration: $value');
extraSensorData['acceleration_$channel'] = value;
break;
case _lppGyroRate:
final rawValue = _readInt32BE(reader);
final value = rawValue / 1000.0;
debugPrint(' Gyro rate: $value');
extraSensorData['gyro_rate_$channel'] = value;
break;
case _lppVolume:
final rawValue = _readUInt32BE(reader);
final value = rawValue / 1000.0;
debugPrint(' Volume: $value');
extraSensorData['volume_$channel'] = value;
break;
case _lppFlowRate:
final rawValue = _readUInt32BE(reader);
final value = rawValue / 1000.0;
debugPrint(' Flow rate: $value');
extraSensorData['flow_rate_$channel'] = value;
break;
case _lppVolumeStorage:
final rawValue = _readUInt32BE(reader);
final value = rawValue / 1000.0;
debugPrint(' Storage volume: $value');
extraSensorData['volume_storage_$channel'] = value;
break;
case _lppWater:
final rawValue = _readUInt32BE(reader);
final value = rawValue / 1000.0;
debugPrint(' Water: $value');
extraSensorData['water_$channel'] = value;
break;
case _lppGasVolume:
final rawValue = _readUInt32BE(reader);
final value = rawValue / 1000.0;
debugPrint(' Gas volume: $value');
extraSensorData['gas_volume_$channel'] = value;
break;
case _lppMass:
final rawValue = _readUInt32BE(reader);
final value = rawValue / 1000.0;
debugPrint(' Mass: $value');
extraSensorData['mass_$channel'] = value;
break;
case _lppSignedSpeed:
final rawValue = _readInt32BE(reader);
final value = rawValue / 1000000.0;
debugPrint(' Signed speed: $value');
extraSensorData['signed_speed_$channel'] = value;
break;
case _lppSignedPower:
final rawValue = _readInt32BE(reader);
final value = rawValue / 100.0;
debugPrint(' Signed power: $value');
extraSensorData['signed_power_$channel'] = value;
break;
case _lppSignedCurrent:
final rawValue = _readInt32BE(reader);
final value = rawValue / 1000.0;
debugPrint(' Signed current: $value');
extraSensorData['signed_current_$channel'] = value;
break;
case _lppSwitch:
final value = reader.readByte();
debugPrint(' Switch: $value');
@@ -352,11 +612,16 @@ class CayenneLppParser {
break;
default:
debugPrint(
' ⚠️ Unknown type, skipping remaining ${reader.remainingBytesCount} bytes',
);
// Unknown type, skip remaining to avoid parsing errors
reader.skip(reader.remainingBytesCount);
final size = _payloadSizeForType(type);
if (size == null || reader.remainingBytesCount < size) {
debugPrint(
' ⚠️ Unknown type $type with unsupported size, stopping parse',
);
reader.skip(reader.remainingBytesCount);
break;
}
debugPrint(' ⚠️ Unknown type $type, skipping $size bytes');
reader.skip(size);
break;
}
} catch (e) {
@@ -420,7 +685,154 @@ class CayenneLppParser {
return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];
}
static String _sourceChannelKey(String fieldKey) => '__source_channel:$fieldKey';
static int _readInt32BE(BufferReader reader) {
final value = _readUInt32BE(reader);
if ((value & 0x80000000) != 0) {
return value - 0x100000000;
}
return value;
}
static int _readInt8(BufferReader reader) {
final value = reader.readByte();
if ((value & 0x80) != 0) {
return value - 0x100;
}
return value;
}
static String _sourceChannelKey(String fieldKey) =>
'__source_channel:$fieldKey';
static String _binaryMetricKeyForType(int type) {
switch (type) {
case _lppBinaryBool:
return 'binary_bool';
case _lppBinaryPowerSwitch:
return 'binary_power_switch';
case _lppBinaryOpen:
return 'binary_open';
case _lppBinaryBatteryLow:
return 'binary_battery_low';
case _lppBinaryCharging:
return 'binary_charging';
case _lppBinaryCarbonMonoxide:
return 'binary_carbon_monoxide';
case _lppBinaryCold:
return 'binary_cold';
case _lppBinaryConnectivity:
return 'binary_connectivity';
case _lppBinaryDoor:
return 'binary_door';
case _lppBinaryGarageDoor:
return 'binary_garage_door';
case _lppBinaryGas:
return 'binary_gas';
case _lppBinaryHeat:
return 'binary_heat';
case _lppBinaryLight:
return 'binary_light';
case _lppBinaryLock:
return 'binary_lock';
case _lppBinaryMoisture:
return 'binary_moisture';
case _lppBinaryMotion:
return 'binary_motion';
case _lppBinaryMoving:
return 'binary_moving';
case _lppBinaryOccupancy:
return 'binary_occupancy';
case _lppBinaryPlug:
return 'binary_plug';
case _lppBinaryPresence:
return 'binary_presence';
case _lppBinaryProblem:
return 'binary_problem';
case _lppBinaryRunning:
return 'binary_running';
case _lppBinarySafety:
return 'binary_safety';
case _lppBinarySmoke:
return 'binary_smoke';
case _lppBinarySound:
return 'binary_sound';
case _lppBinaryTamper:
return 'binary_tamper';
case _lppBinaryVibration:
return 'binary_vibration';
case _lppBinaryWindow:
return 'binary_window';
}
return 'binary_state';
}
static int? _payloadSizeForType(int type) {
if (type >= _lppBinaryBool && type <= _lppBinaryWindow) {
return 1;
}
switch (type) {
case MeshCoreConstants.lppDigitalInput:
case MeshCoreConstants.lppDigitalOutput:
case MeshCoreConstants.lppPresenceSensor:
case MeshCoreConstants.lppHumiditySensor:
case _lppPercentage:
case _lppSwitch:
case _lppButtonEvent:
case _lppDimmer:
case _lppUv:
case _lppLightLevel:
return 1;
case MeshCoreConstants.lppAnalogInput:
case MeshCoreConstants.lppAnalogOutput:
case MeshCoreConstants.lppIlluminanceSensor:
case MeshCoreConstants.lppTemperatureSensor:
case MeshCoreConstants.lppBarometer:
case MeshCoreConstants.lppVoltageSensor:
case _lppCurrent:
case _lppAltitude:
case _lppConcentration:
case _lppPower:
case _lppSpeed:
case _lppDirection:
case _lppGust:
case _lppDewPoint:
case _lppRain:
case _lppPm25:
case _lppPm10:
case _lppCo2:
case _lppTvoc:
case _lppRpm:
case _lppConductivity:
case _lppRotation:
return 2;
case MeshCoreConstants.lppAccelerometer:
case MeshCoreConstants.lppGyrometer:
return 6;
case MeshCoreConstants.lppGps:
return 9;
case _lppGenericSensor:
case _lppFrequency:
case _lppDistance:
case _lppEnergy:
case _lppUnixTime:
case _lppDuration:
case _lppAcceleration:
case _lppGyroRate:
case _lppVolume:
case _lppFlowRate:
case _lppVolumeStorage:
case _lppWater:
case _lppGasVolume:
case _lppMass:
case _lppSignedSpeed:
case _lppSignedPower:
case _lppSignedCurrent:
return 4;
case _lppColour:
return 3;
}
return null;
}
static bool _isZeroPaddedTail(Uint8List data, int remainingBytes) {
final start = data.length - remainingBytes;

View File

@@ -253,6 +253,17 @@ SensorMetricCardData? _buildOptionPreviewCardData(
return null;
}
if (_isBinaryMetricBaseKey(metricKey.baseKey)) {
return SensorMetricCardData(
fieldKey: fieldKey,
icon: previewValue == 'On' ? Icons.toggle_on : Icons.toggle_off,
label: label,
value: previewValue,
accent: const Color(0xFF4B7B5A),
channel: metricKey.channel,
);
}
switch (metricKey.baseKey) {
case 'altitude':
return SensorMetricCardData(
@@ -350,6 +361,25 @@ SensorMetricCardData? _buildOptionPreviewCardData(
accent: const Color(0xFF3E657C),
channel: metricKey.channel,
);
case 'button_event':
return SensorMetricCardData(
fieldKey: fieldKey,
icon: Icons.touch_app_outlined,
label: label,
value: previewValue,
accent: const Color(0xFF6C4F96),
channel: metricKey.channel,
);
case 'dimmer':
case 'light_level':
return SensorMetricCardData(
fieldKey: fieldKey,
icon: Icons.tune,
label: label,
value: previewValue,
accent: const Color(0xFF5A6C84),
channel: metricKey.channel,
);
case 'current':
return SensorMetricCardData(
fieldKey: fieldKey,
@@ -359,6 +389,15 @@ SensorMetricCardData? _buildOptionPreviewCardData(
accent: const Color(0xFF1C7C54),
channel: metricKey.channel,
);
case 'signed_current':
return SensorMetricCardData(
fieldKey: fieldKey,
icon: Icons.electric_bolt,
label: label,
value: previewValue,
accent: const Color(0xFF1C7C54),
channel: metricKey.channel,
);
case 'frequency':
return SensorMetricCardData(
fieldKey: fieldKey,
@@ -389,6 +428,7 @@ SensorMetricCardData? _buildOptionPreviewCardData(
channel: metricKey.channel,
);
case 'power':
case 'signed_power':
return SensorMetricCardData(
fieldKey: fieldKey,
icon: Icons.flash_on_outlined,
@@ -406,6 +446,42 @@ SensorMetricCardData? _buildOptionPreviewCardData(
accent: const Color(0xFF2B78A0),
channel: metricKey.channel,
);
case 'signed_speed':
return SensorMetricCardData(
fieldKey: fieldKey,
icon: Icons.air,
label: label,
value: previewValue,
accent: const Color(0xFF2B78A0),
channel: metricKey.channel,
);
case 'gust':
return SensorMetricCardData(
fieldKey: fieldKey,
icon: Icons.air,
label: label,
value: previewValue,
accent: const Color(0xFF1E88A8),
channel: metricKey.channel,
);
case 'dew':
return SensorMetricCardData(
fieldKey: fieldKey,
icon: Icons.device_thermostat,
label: label,
value: previewValue,
accent: const Color(0xFF2F7AA1),
channel: metricKey.channel,
);
case 'rain':
return SensorMetricCardData(
fieldKey: fieldKey,
icon: Icons.grain,
label: label,
value: previewValue,
accent: const Color(0xFF2C6BA0),
channel: metricKey.channel,
);
case 'distance':
return SensorMetricCardData(
fieldKey: fieldKey,
@@ -415,6 +491,15 @@ SensorMetricCardData? _buildOptionPreviewCardData(
accent: const Color(0xFF577590),
channel: metricKey.channel,
);
case 'duration':
return SensorMetricCardData(
fieldKey: fieldKey,
icon: Icons.timer_outlined,
label: label,
value: previewValue,
accent: const Color(0xFF577590),
channel: metricKey.channel,
);
case 'energy':
return SensorMetricCardData(
fieldKey: fieldKey,
@@ -424,7 +509,22 @@ SensorMetricCardData? _buildOptionPreviewCardData(
accent: const Color(0xFF9C6644),
channel: metricKey.channel,
);
case 'volume':
case 'flow_rate':
case 'volume_storage':
case 'water':
case 'gas_volume':
case 'mass':
return SensorMetricCardData(
fieldKey: fieldKey,
icon: Icons.inventory_2_outlined,
label: label,
value: previewValue,
accent: const Color(0xFF5F7C8A),
channel: metricKey.channel,
);
case 'direction':
case 'rotation':
final degrees = _previewAsDouble(value);
return SensorMetricCardData(
fieldKey: fieldKey,
@@ -481,6 +581,33 @@ SensorMetricCardData? _buildOptionPreviewCardData(
accent: const Color(0xFF0A7D61),
channel: metricKey.channel,
);
case 'conductivity':
return SensorMetricCardData(
fieldKey: fieldKey,
icon: Icons.science_outlined,
label: label,
value: previewValue,
accent: const Color(0xFF4D6D9A),
channel: metricKey.channel,
);
case 'acceleration':
return SensorMetricCardData(
fieldKey: fieldKey,
icon: Icons.speed_outlined,
label: label,
value: previewValue,
accent: const Color(0xFF5A4C99),
channel: metricKey.channel,
);
case 'gyro_rate':
return SensorMetricCardData(
fieldKey: fieldKey,
icon: Icons.sync,
label: label,
value: previewValue,
accent: const Color(0xFF6C4F96),
channel: metricKey.channel,
);
case 'pm25':
case 'pm10':
return SensorMetricCardData(
@@ -543,6 +670,12 @@ String _previewFormatCardinalDirection(double degrees) {
String? _sensorMetricPreviewValue(String rawKey, dynamic value) {
final metricKey = _parseMetricKey(rawKey);
if (_isBinaryMetricBaseKey(metricKey.baseKey)) {
final isOn = _previewAsBool(value);
if (isOn == null) return null;
return isOn ? 'On' : 'Off';
}
switch (metricKey.baseKey) {
case 'altitude':
final meters = _previewAsDouble(value);
@@ -568,10 +701,21 @@ String? _sensorMetricPreviewValue(String rawKey, dynamic value) {
case 'analog_input':
case 'analog_output':
case 'generic_sensor':
case 'light_level':
final reading = _previewAsDouble(value);
if (reading == null) return null;
return _formatPreviewNumber(reading, maxFractionDigits: 3);
case 'button_event':
final eventCode = _previewAsInt(value);
if (eventCode == null) return null;
return _formatButtonEvent(eventCode);
case 'dimmer':
final reading = _previewAsInt(value);
if (reading == null) return null;
return reading.toString();
case 'accelerometer':
final vector = _previewAsVector3(value);
if (vector == null) return null;
@@ -591,6 +735,11 @@ String? _sensorMetricPreviewValue(String rawKey, dynamic value) {
if (amps == null) return null;
return _formatPreviewCurrent(amps);
case 'signed_current':
final amps = _previewAsDouble(value);
if (amps == null) return null;
return _formatPreviewCurrent(amps);
case 'frequency':
final hertz = _previewAsDouble(value);
if (hertz == null) return null;
@@ -613,26 +762,75 @@ String? _sensorMetricPreviewValue(String rawKey, dynamic value) {
if (watts == null) return null;
return _formatPreviewPower(watts);
case 'signed_power':
final watts = _previewAsDouble(value);
if (watts == null) return null;
return _formatPreviewPower(watts);
case 'speed':
final metersPerSecond = _previewAsDouble(value);
if (metersPerSecond == null) return null;
return '${_formatPreviewNumber(metersPerSecond, maxFractionDigits: 2)} m/s';
case 'signed_speed':
final metersPerSecond = _previewAsDouble(value);
if (metersPerSecond == null) return null;
return '${_formatPreviewNumber(metersPerSecond, maxFractionDigits: 2)} m/s';
case 'gust':
final metersPerSecond = _previewAsDouble(value);
if (metersPerSecond == null) return null;
return '${_formatPreviewNumber(metersPerSecond, maxFractionDigits: 2)} m/s';
case 'dew':
final degreesCelsius = _previewAsDouble(value);
if (degreesCelsius == null) return null;
return '${_formatPreviewNumber(degreesCelsius, maxFractionDigits: 1)}°C';
case 'rain':
final millimeters = _previewAsDouble(value);
if (millimeters == null) return null;
return '${_formatPreviewNumber(millimeters, maxFractionDigits: 1)} mm';
case 'distance':
final meters = _previewAsDouble(value);
if (meters == null) return null;
return _formatPreviewDistance(meters);
case 'duration':
final seconds = _previewAsDouble(value);
if (seconds == null) return null;
return '${_formatPreviewNumber(seconds, maxFractionDigits: 2)} s';
case 'energy':
final kilowattHours = _previewAsDouble(value);
if (kilowattHours == null) return null;
return _formatPreviewEnergy(kilowattHours);
case 'volume':
case 'flow_rate':
case 'volume_storage':
case 'water':
case 'gas_volume':
final liters = _previewAsDouble(value);
if (liters == null) return null;
return '${_formatPreviewNumber(liters, maxFractionDigits: 3)} L';
case 'mass':
final kilograms = _previewAsDouble(value);
if (kilograms == null) return null;
return '${_formatPreviewNumber(kilograms, maxFractionDigits: 3)} kg';
case 'direction':
final degrees = _previewAsDouble(value);
if (degrees == null) return null;
return '${_formatPreviewNumber(degrees, maxFractionDigits: 0)} deg';
case 'rotation':
final degrees = _previewAsDouble(value);
if (degrees == null) return null;
return '${_formatPreviewNumber(degrees, maxFractionDigits: 1)} deg';
case 'unixtime':
final seconds = _previewAsInt(value);
if (seconds == null) return null;
@@ -659,6 +857,21 @@ String? _sensorMetricPreviewValue(String rawKey, dynamic value) {
if (volts == null) return null;
return '${_formatPreviewNumber(volts, maxFractionDigits: 3)} V';
case 'conductivity':
final reading = _previewAsDouble(value);
if (reading == null) return null;
return _formatPreviewNumber(reading, maxFractionDigits: 0);
case 'acceleration':
final reading = _previewAsDouble(value);
if (reading == null) return null;
return '${_formatPreviewNumber(reading, maxFractionDigits: 3)} m/s2';
case 'gyro_rate':
final reading = _previewAsDouble(value);
if (reading == null) return null;
return '${_formatPreviewNumber(reading, maxFractionDigits: 3)} deg/s';
case 'pm25':
case 'pm10':
final reading = _previewAsDouble(value);
@@ -1179,6 +1392,19 @@ class SensorTelemetryCard extends StatelessWidget {
labelOverrides: labelOverrides,
);
if (_isBinaryMetricBaseKey(metricKey.baseKey)) {
final isOn = _asBool(value);
if (isOn == null) return null;
return SensorMetricCardData(
fieldKey: _extraFieldKey(rawKey),
icon: isOn ? Icons.toggle_on : Icons.toggle_off,
label: label,
value: isOn ? 'On' : 'Off',
accent: const Color(0xFF4B7B5A),
channel: metricKey.channel,
);
}
switch (metricKey.baseKey) {
case 'altitude':
final meters = _asDouble(value);
@@ -1310,6 +1536,31 @@ class SensorTelemetryCard extends StatelessWidget {
channel: metricKey.channel,
);
case 'button_event':
final reading = _asInt(value);
if (reading == null) return null;
return SensorMetricCardData(
fieldKey: _extraFieldKey(rawKey),
icon: Icons.touch_app_outlined,
label: label,
value: _formatButtonEvent(reading),
accent: const Color(0xFF6C4F96),
channel: metricKey.channel,
);
case 'dimmer':
case 'light_level':
final reading = _asDouble(value);
if (reading == null) return null;
return SensorMetricCardData(
fieldKey: _extraFieldKey(rawKey),
icon: Icons.tune,
label: label,
value: _formatNumber(reading, maxFractionDigits: 2),
accent: const Color(0xFF5A6C84),
channel: metricKey.channel,
);
case 'current':
final amps = _asDouble(value);
if (amps == null) return null;
@@ -1322,6 +1573,18 @@ class SensorTelemetryCard extends StatelessWidget {
channel: metricKey.channel,
);
case 'signed_current':
final amps = _asDouble(value);
if (amps == null) return null;
return SensorMetricCardData(
fieldKey: _extraFieldKey(rawKey),
icon: Icons.electric_bolt,
label: label,
value: _formatCurrent(amps),
accent: const Color(0xFF1C7C54),
channel: metricKey.channel,
);
case 'frequency':
final hz = _asDouble(value);
if (hz == null) return null;
@@ -1370,6 +1633,18 @@ class SensorTelemetryCard extends StatelessWidget {
channel: metricKey.channel,
);
case 'signed_power':
final watts = _asDouble(value);
if (watts == null) return null;
return SensorMetricCardData(
fieldKey: _extraFieldKey(rawKey),
icon: Icons.flash_on_outlined,
label: label,
value: _formatPower(watts),
accent: const Color(0xFFB5622E),
channel: metricKey.channel,
);
case 'speed':
final metersPerSecond = _asDouble(value);
if (metersPerSecond == null) return null;
@@ -1382,6 +1657,54 @@ class SensorTelemetryCard extends StatelessWidget {
channel: metricKey.channel,
);
case 'signed_speed':
final metersPerSecond = _asDouble(value);
if (metersPerSecond == null) return null;
return SensorMetricCardData(
fieldKey: _extraFieldKey(rawKey),
icon: Icons.air,
label: label,
value: '${_formatNumber(metersPerSecond, maxFractionDigits: 2)} m/s',
accent: const Color(0xFF2B78A0),
channel: metricKey.channel,
);
case 'gust':
final metersPerSecond = _asDouble(value);
if (metersPerSecond == null) return null;
return SensorMetricCardData(
fieldKey: _extraFieldKey(rawKey),
icon: Icons.air,
label: label,
value: '${_formatNumber(metersPerSecond, maxFractionDigits: 2)} m/s',
accent: const Color(0xFF1E88A8),
channel: metricKey.channel,
);
case 'dew':
final degreesCelsius = _asDouble(value);
if (degreesCelsius == null) return null;
return SensorMetricCardData(
fieldKey: _extraFieldKey(rawKey),
icon: Icons.device_thermostat,
label: label,
value: '${_formatNumber(degreesCelsius, maxFractionDigits: 1)}°C',
accent: const Color(0xFF2F7AA1),
channel: metricKey.channel,
);
case 'rain':
final millimeters = _asDouble(value);
if (millimeters == null) return null;
return SensorMetricCardData(
fieldKey: _extraFieldKey(rawKey),
icon: Icons.grain,
label: label,
value: '${_formatNumber(millimeters, maxFractionDigits: 1)} mm',
accent: const Color(0xFF2C6BA0),
channel: metricKey.channel,
);
case 'distance':
final meters = _asDouble(value);
if (meters == null) return null;
@@ -1394,6 +1717,18 @@ class SensorTelemetryCard extends StatelessWidget {
channel: metricKey.channel,
);
case 'duration':
final seconds = _asDouble(value);
if (seconds == null) return null;
return SensorMetricCardData(
fieldKey: _extraFieldKey(rawKey),
icon: Icons.timer_outlined,
label: label,
value: '${_formatNumber(seconds, maxFractionDigits: 2)} s',
accent: const Color(0xFF577590),
channel: metricKey.channel,
);
case 'energy':
final kwh = _asDouble(value);
if (kwh == null) return null;
@@ -1406,6 +1741,24 @@ class SensorTelemetryCard extends StatelessWidget {
channel: metricKey.channel,
);
case 'volume':
case 'flow_rate':
case 'volume_storage':
case 'water':
case 'gas_volume':
case 'mass':
final reading = _asDouble(value);
if (reading == null) return null;
final unit = metricKey.baseKey == 'mass' ? 'kg' : 'L';
return SensorMetricCardData(
fieldKey: _extraFieldKey(rawKey),
icon: Icons.inventory_2_outlined,
label: label,
value: '${_formatNumber(reading, maxFractionDigits: 3)} $unit',
accent: const Color(0xFF5F7C8A),
channel: metricKey.channel,
);
case 'direction':
final degrees = _asDouble(value);
if (degrees == null) return null;
@@ -1419,6 +1772,18 @@ class SensorTelemetryCard extends StatelessWidget {
channel: metricKey.channel,
);
case 'rotation':
final degrees = _asDouble(value);
if (degrees == null) return null;
return SensorMetricCardData(
fieldKey: _extraFieldKey(rawKey),
icon: Icons.explore_outlined,
label: label,
value: '${_formatNumber(degrees, maxFractionDigits: 1)} deg',
accent: const Color(0xFF8A5A44),
channel: metricKey.channel,
);
case 'unixtime':
final seconds = _asInt(value);
if (seconds == null) return null;
@@ -1475,6 +1840,42 @@ class SensorTelemetryCard extends StatelessWidget {
accent: const Color(0xFF0A7D61),
channel: metricKey.channel,
);
case 'conductivity':
final reading = _asDouble(value);
if (reading == null) return null;
return SensorMetricCardData(
fieldKey: _extraFieldKey(rawKey),
icon: Icons.science_outlined,
label: label,
value: _formatNumber(reading, maxFractionDigits: 0),
accent: const Color(0xFF4D6D9A),
channel: metricKey.channel,
);
case 'acceleration':
final reading = _asDouble(value);
if (reading == null) return null;
return SensorMetricCardData(
fieldKey: _extraFieldKey(rawKey),
icon: Icons.speed_outlined,
label: label,
value: '${_formatNumber(reading, maxFractionDigits: 3)} m/s2',
accent: const Color(0xFF5A4C99),
channel: metricKey.channel,
);
case 'gyro_rate':
final reading = _asDouble(value);
if (reading == null) return null;
return SensorMetricCardData(
fieldKey: _extraFieldKey(rawKey),
icon: Icons.sync,
label: label,
value: '${_formatNumber(reading, maxFractionDigits: 3)} deg/s',
accent: const Color(0xFF6C4F96),
channel: metricKey.channel,
);
}
switch (metricKey.baseKey) {
@@ -2135,6 +2536,32 @@ String _selectorMetricLabel(String label, int? channel) {
return '$label (ch $channel)';
}
bool _isBinaryMetricBaseKey(String baseKey) {
return baseKey.startsWith('binary_');
}
String _formatButtonEvent(int value) {
switch (value) {
case 0:
return 'None';
case 1:
return 'Press';
case 2:
return 'Double press';
case 3:
return 'Triple press';
case 4:
return 'Long press';
case 5:
return 'Long double';
case 6:
return 'Long triple';
case 128:
return 'Hold';
}
return value.toString();
}
String _formatExtraFieldLabel(String rawKey) {
final metricKey = _parseMetricKey(rawKey);
return _knownMetricLabels[metricKey.baseKey] ??
@@ -2168,13 +2595,60 @@ const List<String> _knownMetricBaseKeys = <String>[
'energy',
'power',
'speed',
'gust',
'dew',
'rain',
'pm25',
'pm10',
'tvoc',
'co2',
'rpm',
'cond',
'conductivity',
'uv',
'button_event',
'dimmer',
'light_level',
'rotation',
'duration',
'acceleration',
'gyro_rate',
'volume',
'flow_rate',
'volume_storage',
'water',
'gas_volume',
'mass',
'signed_speed',
'signed_power',
'signed_current',
'binary_bool',
'binary_power_switch',
'binary_open',
'binary_battery_low',
'binary_charging',
'binary_carbon_monoxide',
'binary_cold',
'binary_connectivity',
'binary_door',
'binary_garage_door',
'binary_gas',
'binary_heat',
'binary_light',
'binary_lock',
'binary_moisture',
'binary_motion',
'binary_moving',
'binary_occupancy',
'binary_plug',
'binary_presence',
'binary_problem',
'binary_running',
'binary_safety',
'binary_smoke',
'binary_sound',
'binary_tamper',
'binary_vibration',
'binary_window',
];
const Map<String, String> _knownMetricLabels = <String, String>{
@@ -2185,32 +2659,78 @@ const Map<String, String> _knownMetricLabels = <String, String>{
'co2': 'CO2',
'colour': 'Color',
'concentration': 'Concentration',
'cond': 'Conductivity',
'conductivity': 'Conductivity',
'current': 'Current',
'button_event': 'Button',
'dimmer': 'Dimmer',
'digital_input': 'Digital input',
'digital_output': 'Digital output',
'direction': 'Direction',
'distance': 'Distance',
'duration': 'Duration',
'energy': 'Energy',
'frequency': 'Frequency',
'flow_rate': 'Flow rate',
'gas_volume': 'Gas volume',
'generic_sensor': 'Generic sensor',
'gyro_rate': 'Gyro rate',
'gyrometer': 'Gyrometer',
'humidity': 'Humidity',
'illuminance': 'Illuminance',
'light_level': 'Light level',
'mass': 'Mass',
'percentage': 'Percentage',
'pm10': 'PM10',
'pm25': 'PM2.5',
'power': 'Power',
'presence': 'Presence',
'pressure': 'Pressure',
'rain': 'Rain',
'rotation': 'Rotation',
'rpm': 'RPM',
'gust': 'Wind gust',
'signed_current': 'Signed current',
'signed_power': 'Signed power',
'signed_speed': 'Signed speed',
'speed': 'Speed',
'switch': 'Switch',
'dew': 'Dew point',
'temperature': 'Temperature',
'tvoc': 'TVOC',
'unixtime': 'Time',
'uv': 'UV index',
'volume': 'Volume',
'volume_storage': 'Storage volume',
'voltage': 'Voltage',
'water': 'Water',
'binary_battery_low': 'Battery low',
'binary_bool': 'Binary',
'binary_carbon_monoxide': 'Carbon monoxide',
'binary_charging': 'Charging',
'binary_cold': 'Cold',
'binary_connectivity': 'Connectivity',
'binary_door': 'Door',
'binary_garage_door': 'Garage door',
'binary_gas': 'Gas',
'binary_heat': 'Heat',
'binary_light': 'Light',
'binary_lock': 'Lock',
'binary_moisture': 'Moisture',
'binary_motion': 'Motion',
'binary_moving': 'Moving',
'binary_occupancy': 'Occupancy',
'binary_open': 'Open',
'binary_plug': 'Plug',
'binary_power_switch': 'Power switch',
'binary_presence': 'Presence',
'binary_problem': 'Problem',
'binary_running': 'Running',
'binary_safety': 'Safety',
'binary_smoke': 'Smoke',
'binary_sound': 'Sound',
'binary_tamper': 'Tamper',
'binary_vibration': 'Vibration',
'binary_window': 'Window',
};
_ParsedMetricKey _parseMetricKey(String rawKey) {