import type { IActionConfig, TPowerStatus } from './actions/base-action.ts'; import { createInitialUpsStatus, type IUpsIdentity, type IUpsStatus } from './ups-status.ts'; export interface IGroupStatusSnapshot { updatedStatus: IUpsStatus; transition: 'none' | 'powerStatusChange'; previousStatus?: IUpsStatus; } export interface IGroupThresholdEvaluation { exceedsThreshold: boolean; blockedByUnreachable: boolean; representativeStatus?: IUpsStatus; } const destructiveActionTypes = new Set(['shutdown', 'proxmox']); function getStatusSeverity(powerStatus: TPowerStatus): number { switch (powerStatus) { case 'unreachable': return 3; case 'onBattery': return 2; case 'unknown': return 1; case 'online': default: return 0; } } export function selectWorstStatus(statuses: IUpsStatus[]): IUpsStatus | undefined { return statuses.reduce((worst, status) => { if (!worst) { return status; } const severityDiff = getStatusSeverity(status.powerStatus) - getStatusSeverity(worst.powerStatus); if (severityDiff > 0) { return status; } if (severityDiff < 0) { return worst; } if (status.batteryRuntime !== worst.batteryRuntime) { return status.batteryRuntime < worst.batteryRuntime ? status : worst; } if (status.batteryCapacity !== worst.batteryCapacity) { return status.batteryCapacity < worst.batteryCapacity ? status : worst; } return worst; }, undefined); } function deriveGroupPowerStatus( mode: 'redundant' | 'nonRedundant', memberStatuses: IUpsStatus[], ): TPowerStatus { if (memberStatuses.length === 0) { return 'unknown'; } if (memberStatuses.some((status) => status.powerStatus === 'unreachable')) { return 'unreachable'; } if (mode === 'redundant') { if (memberStatuses.every((status) => status.powerStatus === 'onBattery')) { return 'onBattery'; } } else if (memberStatuses.some((status) => status.powerStatus === 'onBattery')) { return 'onBattery'; } if (memberStatuses.some((status) => status.powerStatus === 'unknown')) { return 'unknown'; } return 'online'; } function pickRepresentativeStatus( powerStatus: TPowerStatus, memberStatuses: IUpsStatus[], ): IUpsStatus | undefined { const matchingStatuses = memberStatuses.filter((status) => status.powerStatus === powerStatus); return selectWorstStatus(matchingStatuses.length > 0 ? matchingStatuses : memberStatuses); } export function buildGroupStatusSnapshot( group: IUpsIdentity, mode: 'redundant' | 'nonRedundant', memberStatuses: IUpsStatus[], currentStatus: IUpsStatus | undefined, currentTime: number, ): IGroupStatusSnapshot { const previousStatus = currentStatus || createInitialUpsStatus(group, currentTime); const powerStatus = deriveGroupPowerStatus(mode, memberStatuses); const representative = pickRepresentativeStatus(powerStatus, memberStatuses) || previousStatus; const updatedStatus: IUpsStatus = { ...previousStatus, id: group.id, name: group.name, powerStatus, batteryCapacity: representative.batteryCapacity, batteryRuntime: representative.batteryRuntime, outputLoad: representative.outputLoad, outputPower: representative.outputPower, outputVoltage: representative.outputVoltage, outputCurrent: representative.outputCurrent, lastCheckTime: currentTime, consecutiveFailures: 0, unreachableSince: powerStatus === 'unreachable' ? previousStatus.unreachableSince || currentTime : 0, lastStatusChange: previousStatus.lastStatusChange || currentTime, }; if (previousStatus.powerStatus !== powerStatus) { updatedStatus.lastStatusChange = currentTime; if (powerStatus === 'unreachable') { updatedStatus.unreachableSince = currentTime; } return { updatedStatus, transition: 'powerStatusChange', previousStatus, }; } return { updatedStatus, transition: 'none', previousStatus: currentStatus, }; } export function evaluateGroupActionThreshold( actionConfig: IActionConfig, mode: 'redundant' | 'nonRedundant', memberStatuses: IUpsStatus[], ): IGroupThresholdEvaluation { if (!actionConfig.thresholds || memberStatuses.length === 0) { return { exceedsThreshold: false, blockedByUnreachable: false, }; } const criticalMembers = memberStatuses.filter((status) => status.powerStatus === 'onBattery' && (status.batteryCapacity < actionConfig.thresholds!.battery || status.batteryRuntime < actionConfig.thresholds!.runtime) ); const exceedsThreshold = mode === 'redundant' ? criticalMembers.length === memberStatuses.length : criticalMembers.length > 0; return { exceedsThreshold, blockedByUnreachable: exceedsThreshold && destructiveActionTypes.has(actionConfig.type) && memberStatuses.some((status) => status.powerStatus === 'unreachable'), representativeStatus: selectWorstStatus(criticalMembers), }; } export function buildGroupThresholdContextStatus( group: IUpsIdentity, evaluations: IGroupThresholdEvaluation[], enteredActionIndexes: number[], fallbackStatus: IUpsStatus, currentTime: number, ): IUpsStatus { const representativeStatuses = enteredActionIndexes .map((index) => evaluations[index]?.representativeStatus) .filter((status): status is IUpsStatus => !!status); const representative = selectWorstStatus(representativeStatuses) || fallbackStatus; return { ...fallbackStatus, id: group.id, name: group.name, powerStatus: 'onBattery', batteryCapacity: representative.batteryCapacity, batteryRuntime: representative.batteryRuntime, outputLoad: representative.outputLoad, outputPower: representative.outputPower, outputVoltage: representative.outputVoltage, outputCurrent: representative.outputCurrent, lastCheckTime: currentTime, }; }