WellNuo/components/sensors/SensorHealthCard.tsx
Sergei d289dd79a1 Add comprehensive sensor health monitoring system
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>
2026-01-31 16:15:32 -08:00

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,
},
});