feat(monitoring): add edge-triggered threshold handling with group action orchestration and HA-aware Proxmox shutdowns
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user