Implemented a full sensor health monitoring system for WP sensors that tracks connectivity, WiFi signal strength, communication quality, and overall device health. Features: - Health status calculation (excellent/good/fair/poor/critical) - WiFi signal quality monitoring (RSSI-based) - Communication statistics tracking (success rate, response times) - BLE connection metrics (RSSI, attempt/failure counts) - Time-based status (online/warning/offline based on last seen) - Health data caching and retrieval Components: - Added SensorHealthMetrics types with detailed health indicators - Implemented getSensorHealth() and getAllSensorHealth() in BLEManager - Created SensorHealthCard UI component for health visualization - Added API endpoints for health history and reporting - Automatic communication stats tracking on every BLE command Testing: - 12 new tests for health monitoring functionality - All 89 BLE tests passing - No linting errors This enables proactive monitoring of sensor health to identify connectivity issues, poor WiFi signal, or failing devices before they impact user experience. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
262 lines
7.5 KiB
TypeScript
262 lines
7.5 KiB
TypeScript
import React from 'react';
|
|
import { View, Text, StyleSheet } from 'react-native';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import {
|
|
SensorHealthMetrics,
|
|
SensorHealthStatus,
|
|
WiFiSignalQuality,
|
|
} from '@/services/ble/types';
|
|
import { AppColors, FontSizes, FontWeights, Spacing, BorderRadius } from '@/constants/theme';
|
|
|
|
interface SensorHealthCardProps {
|
|
metrics: SensorHealthMetrics;
|
|
}
|
|
|
|
export function SensorHealthCard({ metrics }: SensorHealthCardProps) {
|
|
const getHealthColor = (status: SensorHealthStatus): string => {
|
|
switch (status) {
|
|
case SensorHealthStatus.EXCELLENT:
|
|
return AppColors.success;
|
|
case SensorHealthStatus.GOOD:
|
|
return '#4CAF50';
|
|
case SensorHealthStatus.FAIR:
|
|
return AppColors.warning;
|
|
case SensorHealthStatus.POOR:
|
|
return '#FF9800';
|
|
case SensorHealthStatus.CRITICAL:
|
|
return AppColors.error;
|
|
default:
|
|
return AppColors.textSecondary;
|
|
}
|
|
};
|
|
|
|
const getHealthIcon = (status: SensorHealthStatus): keyof typeof Ionicons.glyphMap => {
|
|
switch (status) {
|
|
case SensorHealthStatus.EXCELLENT:
|
|
return 'checkmark-circle';
|
|
case SensorHealthStatus.GOOD:
|
|
return 'checkmark-circle-outline';
|
|
case SensorHealthStatus.FAIR:
|
|
return 'alert-circle-outline';
|
|
case SensorHealthStatus.POOR:
|
|
return 'warning-outline';
|
|
case SensorHealthStatus.CRITICAL:
|
|
return 'close-circle';
|
|
default:
|
|
return 'help-circle-outline';
|
|
}
|
|
};
|
|
|
|
const getWiFiSignalIcon = (quality: WiFiSignalQuality): keyof typeof Ionicons.glyphMap => {
|
|
switch (quality) {
|
|
case WiFiSignalQuality.EXCELLENT:
|
|
return 'wifi';
|
|
case WiFiSignalQuality.GOOD:
|
|
return 'wifi';
|
|
case WiFiSignalQuality.FAIR:
|
|
return 'wifi-outline';
|
|
case WiFiSignalQuality.WEAK:
|
|
return 'wifi-outline';
|
|
default:
|
|
return 'help-circle-outline';
|
|
}
|
|
};
|
|
|
|
const getWiFiSignalColor = (quality: WiFiSignalQuality): string => {
|
|
switch (quality) {
|
|
case WiFiSignalQuality.EXCELLENT:
|
|
return AppColors.success;
|
|
case WiFiSignalQuality.GOOD:
|
|
return '#4CAF50';
|
|
case WiFiSignalQuality.FAIR:
|
|
return AppColors.warning;
|
|
case WiFiSignalQuality.WEAK:
|
|
return AppColors.error;
|
|
default:
|
|
return AppColors.textSecondary;
|
|
}
|
|
};
|
|
|
|
const successRate =
|
|
metrics.communication.successfulCommands + metrics.communication.failedCommands > 0
|
|
? (
|
|
(metrics.communication.successfulCommands /
|
|
(metrics.communication.successfulCommands + metrics.communication.failedCommands)) *
|
|
100
|
|
).toFixed(1)
|
|
: '100';
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
{/* Header with Overall Health */}
|
|
<View style={styles.header}>
|
|
<View style={styles.healthIndicator}>
|
|
<Ionicons
|
|
name={getHealthIcon(metrics.overallHealth)}
|
|
size={24}
|
|
color={getHealthColor(metrics.overallHealth)}
|
|
/>
|
|
<Text style={[styles.healthText, { color: getHealthColor(metrics.overallHealth) }]}>
|
|
{metrics.overallHealth.charAt(0).toUpperCase() + metrics.overallHealth.slice(1)}
|
|
</Text>
|
|
</View>
|
|
<Text style={styles.deviceName}>{metrics.deviceName}</Text>
|
|
</View>
|
|
|
|
{/* Metrics Grid */}
|
|
<View style={styles.metricsGrid}>
|
|
{/* Connection Status */}
|
|
<View style={styles.metricItem}>
|
|
<Text style={styles.metricLabel}>Status</Text>
|
|
<View style={styles.metricValue}>
|
|
<View
|
|
style={[
|
|
styles.statusDot,
|
|
{
|
|
backgroundColor:
|
|
metrics.connectionStatus === 'online'
|
|
? AppColors.success
|
|
: metrics.connectionStatus === 'warning'
|
|
? AppColors.warning
|
|
: AppColors.error,
|
|
},
|
|
]}
|
|
/>
|
|
<Text style={styles.metricText}>
|
|
{metrics.connectionStatus.charAt(0).toUpperCase() + metrics.connectionStatus.slice(1)}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* WiFi Signal */}
|
|
<View style={styles.metricItem}>
|
|
<Text style={styles.metricLabel}>WiFi Signal</Text>
|
|
<View style={styles.metricValue}>
|
|
<Ionicons
|
|
name={getWiFiSignalIcon(metrics.wifiSignalQuality)}
|
|
size={16}
|
|
color={getWiFiSignalColor(metrics.wifiSignalQuality)}
|
|
/>
|
|
<Text style={styles.metricText}>
|
|
{metrics.wifiRssi ? `${metrics.wifiRssi} dBm` : 'Unknown'}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Last Seen */}
|
|
<View style={styles.metricItem}>
|
|
<Text style={styles.metricLabel}>Last Seen</Text>
|
|
<Text style={styles.metricText}>{metrics.lastSeenMinutesAgo} min ago</Text>
|
|
</View>
|
|
|
|
{/* Success Rate */}
|
|
<View style={styles.metricItem}>
|
|
<Text style={styles.metricLabel}>Success Rate</Text>
|
|
<Text style={styles.metricText}>{successRate}%</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* WiFi Network Info */}
|
|
{metrics.wifiSsid && (
|
|
<View style={styles.wifiInfo}>
|
|
<Ionicons name="wifi" size={16} color={AppColors.textSecondary} />
|
|
<Text style={styles.wifiText}>
|
|
Connected to {metrics.wifiSsid}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{/* Communication Stats */}
|
|
<View style={styles.commStats}>
|
|
<Text style={styles.commStatsLabel}>Communication</Text>
|
|
<Text style={styles.commStatsText}>
|
|
✓ {metrics.communication.successfulCommands} successful · ✗{' '}
|
|
{metrics.communication.failedCommands} failed · Avg {metrics.communication.averageResponseTime}ms
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
backgroundColor: AppColors.cardBackground,
|
|
borderRadius: BorderRadius.medium,
|
|
padding: Spacing.medium,
|
|
marginBottom: Spacing.medium,
|
|
},
|
|
header: {
|
|
marginBottom: Spacing.medium,
|
|
},
|
|
healthIndicator: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.small,
|
|
marginBottom: Spacing.small,
|
|
},
|
|
healthText: {
|
|
fontSize: FontSizes.medium,
|
|
fontWeight: FontWeights.semibold,
|
|
},
|
|
deviceName: {
|
|
fontSize: FontSizes.small,
|
|
color: AppColors.textSecondary,
|
|
},
|
|
metricsGrid: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
gap: Spacing.medium,
|
|
marginBottom: Spacing.medium,
|
|
},
|
|
metricItem: {
|
|
width: '48%',
|
|
},
|
|
metricLabel: {
|
|
fontSize: FontSizes.tiny,
|
|
color: AppColors.textSecondary,
|
|
marginBottom: 4,
|
|
},
|
|
metricValue: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.tiny,
|
|
},
|
|
metricText: {
|
|
fontSize: FontSizes.small,
|
|
fontWeight: FontWeights.medium,
|
|
color: AppColors.text,
|
|
},
|
|
statusDot: {
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: 4,
|
|
},
|
|
wifiInfo: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: Spacing.tiny,
|
|
paddingTop: Spacing.small,
|
|
borderTopWidth: 1,
|
|
borderTopColor: AppColors.border,
|
|
marginBottom: Spacing.small,
|
|
},
|
|
wifiText: {
|
|
fontSize: FontSizes.tiny,
|
|
color: AppColors.textSecondary,
|
|
},
|
|
commStats: {
|
|
paddingTop: Spacing.small,
|
|
borderTopWidth: 1,
|
|
borderTopColor: AppColors.border,
|
|
},
|
|
commStatsLabel: {
|
|
fontSize: FontSizes.tiny,
|
|
color: AppColors.textSecondary,
|
|
marginBottom: 4,
|
|
},
|
|
commStatsText: {
|
|
fontSize: FontSizes.tiny,
|
|
color: AppColors.textSecondary,
|
|
},
|
|
});
|