@sailboat-computer/health-monitoring
Version:
Comprehensive health monitoring system for sailboat computer v3 with marine-specific health checks
370 lines (334 loc) • 12 kB
text/typescript
/**
* 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);
}