UNPKG

@sailboat-computer/health-monitoring

Version:

Comprehensive health monitoring system for sailboat computer v3 with marine-specific health checks

370 lines (334 loc) 12 kB
/** * Battery Health Check for marine power systems */ import { BaseHealthCheck, HealthCheckResult, HealthCheckConfig, HealthCheckType, HealthStatus, HealthStatusType, OperationalContext, OperationalContextType, MarineSystemType, MarineSystemTypeType, MarineEnvironmentStatus } from '../index'; /** * Battery data interface for health checking */ export interface BatteryData { voltage: number; // Battery voltage in volts current: number; // Current in/out in amps (positive = charging, negative = discharging) temperature: number; // Battery temperature in Celsius stateOfCharge: number; // State of charge percentage (0-100) capacity: number; // Current capacity in Ah nominalCapacity: number; // Nominal capacity in Ah cycleCount: number; // Number of charge/discharge cycles timeToEmpty?: number; // Estimated time to empty in minutes (when discharging) timeToFull?: number; // Estimated time to full in minutes (when charging) timestamp: Date; batteryType: '12v' | '48v' | 'lithium' | 'agm' | 'gel'; } /** * Battery Health Check implementation */ export class BatteryHealthCheck extends BaseHealthCheck { private batteryDataProvider: () => Promise<BatteryData>; private voltageHistory: { voltage: number; timestamp: Date }[] = []; private maxHistoryLength = 100; constructor( config: HealthCheckConfig, batteryDataProvider: () => Promise<BatteryData> ) { super(config); this.batteryDataProvider = batteryDataProvider; } protected async performCheck( operationalContext: OperationalContextType, marineEnvironment?: MarineEnvironmentStatus ): Promise<HealthCheckResult> { try { const batteryData = await this.batteryDataProvider(); // Calculate health score based on multiple factors let score = 1.0; let status: HealthStatusType = HealthStatus.HEALTHY; const issues: string[] = []; const details: Record<string, any> = { voltage: batteryData.voltage, current: batteryData.current, temperature: batteryData.temperature, stateOfCharge: batteryData.stateOfCharge, capacity: batteryData.capacity, nominalCapacity: batteryData.nominalCapacity, capacityHealth: (batteryData.capacity / batteryData.nominalCapacity) * 100, cycleCount: batteryData.cycleCount, batteryType: batteryData.batteryType }; // Get voltage thresholds based on battery type const voltageThresholds = this.getVoltageThresholds(batteryData.batteryType); // Check voltage levels if (batteryData.voltage < voltageThresholds.critical) { score = 0.0; status = HealthStatus.CRITICAL; issues.push(`Critical low voltage: ${batteryData.voltage.toFixed(2)}V`); } else if (batteryData.voltage < voltageThresholds.warning) { score -= 0.4; issues.push(`Low voltage: ${batteryData.voltage.toFixed(2)}V`); } else if (batteryData.voltage > voltageThresholds.overcharge) { score -= 0.3; issues.push(`High voltage: ${batteryData.voltage.toFixed(2)}V`); } // Check state of charge if (batteryData.stateOfCharge < 20) { score -= 0.3; issues.push(`Low state of charge: ${batteryData.stateOfCharge}%`); } else if (batteryData.stateOfCharge < 30) { score -= 0.1; issues.push(`Marginal state of charge: ${batteryData.stateOfCharge}%`); } // Check battery capacity health const capacityHealth = (batteryData.capacity / batteryData.nominalCapacity) * 100; if (capacityHealth < 60) { score -= 0.4; issues.push(`Poor capacity health: ${capacityHealth.toFixed(1)}%`); } else if (capacityHealth < 80) { score -= 0.2; issues.push(`Reduced capacity: ${capacityHealth.toFixed(1)}%`); } // Check temperature const tempThresholds = this.getTemperatureThresholds(batteryData.batteryType); if (batteryData.temperature < tempThresholds.coldCritical || batteryData.temperature > tempThresholds.hotCritical) { score -= 0.5; issues.push(`Critical temperature: ${batteryData.temperature}°C`); } else if (batteryData.temperature < tempThresholds.coldWarning || batteryData.temperature > tempThresholds.hotWarning) { score -= 0.2; issues.push(`Temperature concern: ${batteryData.temperature}°C`); } // Check for excessive current draw if (Math.abs(batteryData.current) > batteryData.nominalCapacity * 0.5) { score -= 0.2; const direction = batteryData.current > 0 ? 'charging' : 'discharging'; issues.push(`High current ${direction}: ${Math.abs(batteryData.current).toFixed(1)}A`); } // Check cycle count (if available) if (batteryData.cycleCount > 0) { const expectedLifeCycles = this.getExpectedLifeCycles(batteryData.batteryType); const cycleHealth = Math.max(0, 1 - (batteryData.cycleCount / expectedLifeCycles)); score = Math.min(score, score * (0.7 + 0.3 * cycleHealth)); if (batteryData.cycleCount > expectedLifeCycles * 0.8) { issues.push(`High cycle count: ${batteryData.cycleCount}`); } } // Add voltage to history and check for voltage drop trends this.voltageHistory.push({ voltage: batteryData.voltage, timestamp: batteryData.timestamp }); // Keep history manageable if (this.voltageHistory.length > this.maxHistoryLength) { this.voltageHistory = this.voltageHistory.slice(-this.maxHistoryLength); } // Check for voltage drop trend (if we have enough history) if (this.voltageHistory.length >= 10) { const recentVoltages = this.voltageHistory.slice(-10); const voltageDropRate = this.calculateVoltageDropRate(recentVoltages); if (voltageDropRate > 0.1) { // More than 0.1V drop per hour score -= 0.2; issues.push(`Rapid voltage drop detected: ${voltageDropRate.toFixed(3)}V/hr`); details['voltageDropRate'] = voltageDropRate; } } // Environmental adjustments if (marineEnvironment) { if (marineEnvironment.powerStatus === 'critical') { // In critical power situations, battery health is even more important score = Math.min(score, score * 0.9); details['criticalPowerMode'] = true; } if (marineEnvironment.seaState === 'very_rough') { // Rough seas can affect battery performance score = Math.max(score - 0.05, 0); details['roughSeaImpact'] = true; } } // Operational context adjustments if (operationalContext === OperationalContext.EMERGENCY) { // In emergency situations, battery reliability is critical if (score < 0.7) { score = Math.min(score, 0.5); } } // Determine final status score = Math.max(0, Math.min(1, score)); if (score >= 0.8) { status = HealthStatus.HEALTHY; } else if (score >= 0.6) { status = HealthStatus.DEGRADED; } else if (score >= 0.3) { status = HealthStatus.UNHEALTHY; } else { status = HealthStatus.CRITICAL; } const message = issues.length > 0 ? `Battery issues detected: ${issues.join(', ')}` : 'Battery operating normally'; return { checkId: this.config.checkId, name: this.config.name, type: this.config.type, marineSystemType: this.config.marineSystemType, status, score, message, details, timestamp: new Date(), executionTime: 0, // Will be set by base class marineContext: { operationalContext, environmentalImpact: marineEnvironment?.seaState === 'very_rough', safetyImpact: true, // Battery is critical for all systems powerImpact: 0 // Battery monitoring uses minimal power }, thresholds: this.config.thresholds }; } catch (error) { throw new Error(`Battery health check failed: ${(error as Error).message}`); } } /** * Get voltage thresholds based on battery type */ private getVoltageThresholds(batteryType: string) { switch (batteryType) { case '12v': return { critical: 11.8, warning: 12.2, normal: 12.6, overcharge: 14.8 }; case '48v': return { critical: 47.2, warning: 48.8, normal: 50.4, overcharge: 59.2 }; case 'lithium': return { critical: 11.0, warning: 12.0, normal: 13.0, overcharge: 14.6 }; default: return { critical: 11.8, warning: 12.2, normal: 12.6, overcharge: 14.8 }; } } /** * Get temperature thresholds based on battery type */ private getTemperatureThresholds(batteryType: string) { switch (batteryType) { case 'lithium': return { coldCritical: -10, coldWarning: 0, hotWarning: 45, hotCritical: 60 }; case 'agm': case 'gel': return { coldCritical: -20, coldWarning: -10, hotWarning: 50, hotCritical: 65 }; default: return { coldCritical: -15, coldWarning: -5, hotWarning: 50, hotCritical: 60 }; } } /** * Get expected life cycles based on battery type */ private getExpectedLifeCycles(batteryType: string): number { switch (batteryType) { case 'lithium': return 3000; case 'agm': return 1000; case 'gel': return 1200; default: return 800; } } /** * Calculate voltage drop rate from recent history */ private calculateVoltageDropRate(voltageHistory: { voltage: number; timestamp: Date }[]): number { if (voltageHistory.length < 2) return 0; const first = voltageHistory[0]; const last = voltageHistory[voltageHistory.length - 1]; if (!first || !last) return 0; const voltageDrop = first.voltage - last.voltage; const timeDiffHours = (last.timestamp.getTime() - first.timestamp.getTime()) / (1000 * 60 * 60); return timeDiffHours > 0 ? voltageDrop / timeDiffHours : 0; } } /** * Factory function to create battery health check with standard marine configuration */ export function createBatteryHealthCheck( batteryDataProvider: () => Promise<BatteryData>, customConfig?: Partial<HealthCheckConfig> ): BatteryHealthCheck { const defaultConfig: HealthCheckConfig = { checkId: 'battery-health', name: 'Battery System Health', type: HealthCheckType.POWER, marineSystemType: MarineSystemType.POWER as MarineSystemTypeType, interval: 30000, // Check every 30 seconds timeout: 5000, // 5 second timeout retryCount: 2, thresholds: { warning: 0.6, critical: 0.3, unit: 'health_score' }, marineSettings: { operationalContexts: [ OperationalContext.SAILING, OperationalContext.MOTORING, OperationalContext.ANCHORED, OperationalContext.DOCKED, OperationalContext.EMERGENCY ] as OperationalContextType[], environmentalSensitivity: 0.2, // Somewhat sensitive to environmental conditions powerAware: false, // Battery monitoring is always critical safetyImpact: true }, advanced: { trendAnalysis: true, predictiveAlerts: true, adaptiveThresholds: true, historicalComparison: true }, ...customConfig }; return new BatteryHealthCheck(defaultConfig, batteryDataProvider); }