199 lines
5.8 KiB
TypeScript
199 lines
5.8 KiB
TypeScript
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<IUpsStatus | undefined>((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,
|
|
};
|
|
}
|