2025-10-19 12:57:17 +00:00
|
|
|
import process from 'node:process';
|
2025-10-19 13:14:18 +00:00
|
|
|
import * as fs from 'node:fs';
|
|
|
|
|
import * as path from 'node:path';
|
2025-10-18 11:59:55 +00:00
|
|
|
import { NupstSnmp } from './snmp/manager.ts';
|
2025-10-20 12:27:02 +00:00
|
|
|
import type { ISnmpConfig, IUpsStatus as ISnmpUpsStatus } from './snmp/types.ts';
|
2026-02-20 11:51:59 +00:00
|
|
|
import { NupstUpsd } from './upsd/client.ts';
|
|
|
|
|
import type { IUpsdConfig } from './upsd/types.ts';
|
|
|
|
|
import type { TProtocol } from './protocol/types.ts';
|
|
|
|
|
import { ProtocolResolver } from './protocol/resolver.ts';
|
2026-01-29 17:46:23 +00:00
|
|
|
import { logger } from './logger.ts';
|
2025-10-19 20:41:09 +00:00
|
|
|
import { MigrationRunner } from './migrations/index.ts';
|
2026-01-29 17:46:23 +00:00
|
|
|
import { formatPowerStatus, getBatteryColor, getRuntimeColor, theme } from './colors.ts';
|
2025-10-20 11:47:51 +00:00
|
|
|
import type { IActionConfig } from './actions/base-action.ts';
|
2026-04-14 14:27:29 +00:00
|
|
|
import { ActionManager } from './actions/index.ts';
|
2026-04-14 18:47:37 +00:00
|
|
|
import {
|
|
|
|
|
applyDefaultShutdownDelay,
|
2026-04-16 02:54:16 +00:00
|
|
|
buildUpsActionContext,
|
2026-04-14 18:47:37 +00:00
|
|
|
decideUpsActionExecution,
|
|
|
|
|
type TUpsTriggerReason,
|
|
|
|
|
} from './action-orchestration.ts';
|
2025-10-23 12:57:58 +00:00
|
|
|
import { NupstHttpServer } from './http-server.ts';
|
2026-04-14 18:47:37 +00:00
|
|
|
import { NETWORK, PAUSE, SHUTDOWN, THRESHOLDS, TIMING, UI } from './constants.ts';
|
2026-04-14 14:27:29 +00:00
|
|
|
import {
|
|
|
|
|
analyzeConfigReload,
|
|
|
|
|
shouldRefreshPauseState,
|
|
|
|
|
shouldReloadConfig,
|
|
|
|
|
} from './config-watch.ts';
|
|
|
|
|
import { type IPauseState, loadPauseSnapshot } from './pause-state.ts';
|
|
|
|
|
import { ShutdownExecutor } from './shutdown-executor.ts';
|
2026-04-16 02:54:16 +00:00
|
|
|
import {
|
|
|
|
|
buildGroupStatusSnapshot,
|
|
|
|
|
buildGroupThresholdContextStatus,
|
|
|
|
|
evaluateGroupActionThreshold,
|
|
|
|
|
} from './group-monitoring.ts';
|
2026-04-14 14:27:29 +00:00
|
|
|
import {
|
|
|
|
|
buildFailedUpsPollSnapshot,
|
|
|
|
|
buildSuccessfulUpsPollSnapshot,
|
|
|
|
|
ensureUpsStatus,
|
2026-04-16 02:54:16 +00:00
|
|
|
getActionThresholdStates,
|
|
|
|
|
getEnteredThresholdIndexes,
|
2026-04-14 14:27:29 +00:00
|
|
|
} from './ups-monitoring.ts';
|
|
|
|
|
import {
|
|
|
|
|
buildShutdownErrorRow,
|
|
|
|
|
buildShutdownStatusRow,
|
|
|
|
|
selectEmergencyCandidate,
|
|
|
|
|
} from './shutdown-monitoring.ts';
|
|
|
|
|
import { createInitialUpsStatus, type IUpsStatus } from './ups-status.ts';
|
2025-03-25 11:49:50 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
/**
|
2025-03-28 16:19:43 +00:00
|
|
|
* UPS configuration interface
|
2025-03-25 09:06:23 +00:00
|
|
|
*/
|
2025-03-28 16:19:43 +00:00
|
|
|
export interface IUpsConfig {
|
|
|
|
|
/** Unique ID for the UPS */
|
|
|
|
|
id: string;
|
|
|
|
|
/** Friendly name for the UPS */
|
|
|
|
|
name: string;
|
2026-02-20 11:51:59 +00:00
|
|
|
/** Communication protocol (defaults to 'snmp') */
|
|
|
|
|
protocol?: TProtocol;
|
|
|
|
|
/** SNMP configuration settings (required for 'snmp' protocol) */
|
|
|
|
|
snmp?: ISnmpConfig;
|
|
|
|
|
/** UPSD/NIS configuration settings (required for 'upsd' protocol) */
|
|
|
|
|
upsd?: IUpsdConfig;
|
2025-03-28 16:19:43 +00:00
|
|
|
/** Group IDs this UPS belongs to */
|
|
|
|
|
groups: string[];
|
2025-10-20 11:47:51 +00:00
|
|
|
/** Actions to trigger on power status changes and threshold violations */
|
|
|
|
|
actions?: IActionConfig[];
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Group configuration interface
|
|
|
|
|
*/
|
|
|
|
|
export interface IGroupConfig {
|
|
|
|
|
/** Unique ID for the group */
|
|
|
|
|
id: string;
|
|
|
|
|
/** Friendly name for the group */
|
|
|
|
|
name: string;
|
|
|
|
|
/** Group operation mode */
|
|
|
|
|
mode: 'redundant' | 'nonRedundant';
|
|
|
|
|
/** Optional description */
|
|
|
|
|
description?: string;
|
2025-10-20 11:47:51 +00:00
|
|
|
/** Actions to trigger on power status changes and threshold violations */
|
|
|
|
|
actions?: IActionConfig[];
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
|
|
|
|
|
2025-10-23 12:57:58 +00:00
|
|
|
/**
|
|
|
|
|
* HTTP Server configuration interface
|
|
|
|
|
*/
|
|
|
|
|
export interface IHttpServerConfig {
|
|
|
|
|
/** Whether HTTP server is enabled */
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
/** Port to listen on */
|
|
|
|
|
port: number;
|
|
|
|
|
/** URL path for the endpoint */
|
|
|
|
|
path: string;
|
|
|
|
|
/** Authentication token */
|
|
|
|
|
authToken: string;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
/**
|
|
|
|
|
* Configuration interface for the daemon
|
|
|
|
|
*/
|
|
|
|
|
export interface INupstConfig {
|
2025-10-19 20:41:09 +00:00
|
|
|
/** Configuration format version */
|
|
|
|
|
version?: string;
|
2025-03-28 16:19:43 +00:00
|
|
|
/** UPS devices configuration */
|
|
|
|
|
upsDevices: IUpsConfig[];
|
|
|
|
|
/** Groups configuration */
|
|
|
|
|
groups: IGroupConfig[];
|
2025-03-25 09:06:23 +00:00
|
|
|
/** Check interval in milliseconds */
|
|
|
|
|
checkInterval: number;
|
2026-04-14 18:47:37 +00:00
|
|
|
/** Default delay in minutes for shutdown actions without an override */
|
|
|
|
|
defaultShutdownDelay?: number;
|
2025-10-23 12:57:58 +00:00
|
|
|
/** HTTP Server configuration */
|
|
|
|
|
httpServer?: IHttpServerConfig;
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-10-19 20:41:09 +00:00
|
|
|
// Legacy fields for backward compatibility (will be migrated away)
|
|
|
|
|
/** UPS list (v3 format - legacy) */
|
|
|
|
|
upsList?: IUpsConfig[];
|
|
|
|
|
/** SNMP configuration settings (v1 format - legacy) */
|
2025-03-28 16:19:43 +00:00
|
|
|
snmp?: ISnmpConfig;
|
2025-10-19 20:41:09 +00:00
|
|
|
/** Threshold settings (v1 format - legacy) */
|
2025-03-28 16:19:43 +00:00
|
|
|
thresholds?: {
|
|
|
|
|
/** Shutdown when battery below this percentage */
|
|
|
|
|
battery: number;
|
|
|
|
|
/** Shutdown when runtime below this minutes */
|
|
|
|
|
runtime: number;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
/**
|
|
|
|
|
* Daemon class for monitoring UPS and handling shutdown
|
|
|
|
|
* Responsible for loading/saving config and monitoring the UPS status
|
|
|
|
|
*/
|
|
|
|
|
export class NupstDaemon {
|
|
|
|
|
/** Default configuration path */
|
|
|
|
|
private readonly CONFIG_PATH = '/etc/nupst/config.json';
|
|
|
|
|
|
|
|
|
|
/** Default configuration */
|
2025-03-25 10:21:21 +00:00
|
|
|
private readonly DEFAULT_CONFIG: INupstConfig = {
|
2026-04-16 09:44:30 +00:00
|
|
|
version: '4.4',
|
2026-04-14 18:47:37 +00:00
|
|
|
defaultShutdownDelay: SHUTDOWN.DEFAULT_DELAY_MINUTES,
|
2025-03-28 16:19:43 +00:00
|
|
|
upsDevices: [
|
|
|
|
|
{
|
|
|
|
|
id: 'default',
|
|
|
|
|
name: 'Default UPS',
|
|
|
|
|
snmp: {
|
|
|
|
|
host: '127.0.0.1',
|
|
|
|
|
port: 161,
|
|
|
|
|
community: 'public',
|
|
|
|
|
version: 1,
|
|
|
|
|
timeout: 5000,
|
|
|
|
|
// SNMPv3 defaults (used only if version === 3)
|
|
|
|
|
securityLevel: 'authPriv',
|
|
|
|
|
username: '',
|
|
|
|
|
authProtocol: 'SHA',
|
|
|
|
|
authKey: '',
|
|
|
|
|
privProtocol: 'AES',
|
|
|
|
|
privKey: '',
|
|
|
|
|
// UPS model for OID selection
|
2025-10-19 13:14:18 +00:00
|
|
|
upsModel: 'cyberpower',
|
2026-03-30 06:46:28 +00:00
|
|
|
runtimeUnit: 'ticks',
|
2025-03-28 16:19:43 +00:00
|
|
|
},
|
2025-10-19 13:14:18 +00:00
|
|
|
groups: [],
|
2025-10-20 11:47:51 +00:00
|
|
|
actions: [
|
|
|
|
|
{
|
|
|
|
|
type: 'shutdown',
|
|
|
|
|
triggerMode: 'onlyThresholds',
|
|
|
|
|
thresholds: {
|
2026-01-29 17:04:12 +00:00
|
|
|
battery: THRESHOLDS.DEFAULT_BATTERY_PERCENT, // Shutdown when battery below 60%
|
|
|
|
|
runtime: THRESHOLDS.DEFAULT_RUNTIME_MINUTES, // Shutdown when runtime below 20 minutes
|
2025-10-20 11:47:51 +00:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
2025-10-19 13:14:18 +00:00
|
|
|
},
|
2025-03-28 16:19:43 +00:00
|
|
|
],
|
|
|
|
|
groups: [],
|
2026-01-29 17:04:12 +00:00
|
|
|
checkInterval: TIMING.CHECK_INTERVAL_MS, // Check every 30 seconds
|
2026-01-29 17:10:17 +00:00
|
|
|
};
|
2025-03-25 09:06:23 +00:00
|
|
|
|
2025-03-25 10:21:21 +00:00
|
|
|
private config: INupstConfig;
|
2025-03-25 09:06:23 +00:00
|
|
|
private snmp: NupstSnmp;
|
2026-02-20 11:51:59 +00:00
|
|
|
private upsd: NupstUpsd;
|
|
|
|
|
private protocolResolver: ProtocolResolver;
|
2025-03-25 09:06:23 +00:00
|
|
|
private isRunning: boolean = false;
|
2026-02-20 11:51:59 +00:00
|
|
|
private isPaused: boolean = false;
|
|
|
|
|
private pauseState: IPauseState | null = null;
|
2025-03-28 16:19:43 +00:00
|
|
|
private upsStatus: Map<string, IUpsStatus> = new Map();
|
2026-04-16 02:54:16 +00:00
|
|
|
private groupStatus: Map<string, IUpsStatus> = new Map();
|
|
|
|
|
private thresholdState: Map<string, boolean[]> = new Map();
|
2025-10-23 12:57:58 +00:00
|
|
|
private httpServer?: NupstHttpServer;
|
2026-04-14 14:27:29 +00:00
|
|
|
private readonly shutdownExecutor: ShutdownExecutor;
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
/**
|
2026-02-20 11:51:59 +00:00
|
|
|
* Create a new daemon instance with the given protocol managers
|
2025-03-25 09:06:23 +00:00
|
|
|
*/
|
2026-02-20 11:51:59 +00:00
|
|
|
constructor(snmp: NupstSnmp, upsd: NupstUpsd) {
|
2025-03-25 09:06:23 +00:00
|
|
|
this.snmp = snmp;
|
2026-02-20 11:51:59 +00:00
|
|
|
this.upsd = upsd;
|
|
|
|
|
this.protocolResolver = new ProtocolResolver(snmp, upsd);
|
2026-04-14 14:27:29 +00:00
|
|
|
this.shutdownExecutor = new ShutdownExecutor();
|
2025-03-25 09:06:23 +00:00
|
|
|
this.config = this.DEFAULT_CONFIG;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Load configuration from file
|
|
|
|
|
* @throws Error if configuration file doesn't exist
|
|
|
|
|
*/
|
2025-03-25 10:21:21 +00:00
|
|
|
public async loadConfig(): Promise<INupstConfig> {
|
2025-03-25 09:06:23 +00:00
|
|
|
try {
|
|
|
|
|
// Check if config file exists
|
|
|
|
|
const configExists = fs.existsSync(this.CONFIG_PATH);
|
|
|
|
|
if (!configExists) {
|
|
|
|
|
const errorMsg = `No configuration found at ${this.CONFIG_PATH}`;
|
|
|
|
|
this.logConfigError(errorMsg);
|
|
|
|
|
throw new Error(errorMsg);
|
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
// Read and parse config
|
|
|
|
|
const configData = fs.readFileSync(this.CONFIG_PATH, 'utf8');
|
2025-03-28 16:19:43 +00:00
|
|
|
const parsedConfig = JSON.parse(configData);
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-10-19 20:41:09 +00:00
|
|
|
// Run migrations to upgrade config format if needed
|
|
|
|
|
const migrationRunner = new MigrationRunner();
|
|
|
|
|
const { config: migratedConfig, migrated } = await migrationRunner.run(parsedConfig);
|
|
|
|
|
|
2026-04-14 18:47:37 +00:00
|
|
|
// Save migrated or normalized config back to disk when needed.
|
|
|
|
|
// Cast to INupstConfig since migrations ensure the output is valid.
|
2025-10-20 12:27:02 +00:00
|
|
|
const validConfig = migratedConfig as unknown as INupstConfig;
|
2026-04-14 18:47:37 +00:00
|
|
|
const normalizedShutdownDelay = this.normalizeShutdownDelay(validConfig.defaultShutdownDelay);
|
2026-04-16 02:54:16 +00:00
|
|
|
const shouldPersistNormalizedConfig =
|
|
|
|
|
validConfig.defaultShutdownDelay !== normalizedShutdownDelay;
|
2026-04-14 18:47:37 +00:00
|
|
|
validConfig.defaultShutdownDelay = normalizedShutdownDelay;
|
|
|
|
|
if (migrated || shouldPersistNormalizedConfig) {
|
2025-10-20 12:27:02 +00:00
|
|
|
this.config = validConfig;
|
2025-03-28 16:19:43 +00:00
|
|
|
await this.saveConfig(this.config);
|
|
|
|
|
} else {
|
2025-10-20 12:27:02 +00:00
|
|
|
this.config = validConfig;
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
return this.config;
|
|
|
|
|
} catch (error) {
|
2025-10-19 13:14:18 +00:00
|
|
|
if (
|
|
|
|
|
error instanceof Error && error.message && error.message.includes('No configuration found')
|
|
|
|
|
) {
|
2025-03-25 09:06:23 +00:00
|
|
|
throw error; // Re-throw the no configuration error
|
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
|
|
|
|
this.logConfigError(
|
|
|
|
|
`Error loading configuration: ${error instanceof Error ? error.message : String(error)}`,
|
|
|
|
|
);
|
2025-03-25 09:06:23 +00:00
|
|
|
throw new Error('Failed to load configuration');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Save configuration to file
|
|
|
|
|
*/
|
2025-10-19 13:25:01 +00:00
|
|
|
public saveConfig(config: INupstConfig): void {
|
2025-03-25 09:06:23 +00:00
|
|
|
try {
|
|
|
|
|
const configDir = path.dirname(this.CONFIG_PATH);
|
|
|
|
|
if (!fs.existsSync(configDir)) {
|
|
|
|
|
fs.mkdirSync(configDir, { recursive: true });
|
|
|
|
|
}
|
2025-10-19 20:41:09 +00:00
|
|
|
|
|
|
|
|
// Ensure version is always set and remove legacy fields before saving
|
|
|
|
|
const configToSave: INupstConfig = {
|
2026-04-16 09:44:30 +00:00
|
|
|
version: '4.4',
|
2025-10-19 20:41:09 +00:00
|
|
|
upsDevices: config.upsDevices,
|
|
|
|
|
groups: config.groups,
|
|
|
|
|
checkInterval: config.checkInterval,
|
2026-04-14 18:47:37 +00:00
|
|
|
defaultShutdownDelay: this.normalizeShutdownDelay(config.defaultShutdownDelay),
|
2026-02-20 11:51:59 +00:00
|
|
|
...(config.httpServer ? { httpServer: config.httpServer } : {}),
|
2025-10-19 20:41:09 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(configToSave, null, 2));
|
|
|
|
|
this.config = configToSave;
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-10-20 00:32:06 +00:00
|
|
|
logger.logBox('Configuration Saved', [`Location: ${this.CONFIG_PATH}`], 45, 'success');
|
2025-03-25 09:06:23 +00:00
|
|
|
} catch (error) {
|
2025-10-20 00:32:06 +00:00
|
|
|
logger.error(`Error saving configuration: ${error}`);
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Helper method to log configuration errors consistently
|
|
|
|
|
*/
|
|
|
|
|
private logConfigError(message: string): void {
|
2026-01-29 17:10:17 +00:00
|
|
|
logger.logBox(
|
|
|
|
|
'Configuration Error',
|
2026-04-14 14:27:29 +00:00
|
|
|
[message, "Please run 'nupst ups add' first to create a configuration."],
|
2026-01-29 17:10:17 +00:00
|
|
|
45,
|
|
|
|
|
'error',
|
|
|
|
|
);
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the current configuration
|
|
|
|
|
*/
|
2025-03-25 10:21:21 +00:00
|
|
|
public getConfig(): INupstConfig {
|
2025-03-25 09:06:23 +00:00
|
|
|
return this.config;
|
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2026-04-14 18:47:37 +00:00
|
|
|
private normalizeShutdownDelay(delayMinutes: number | undefined): number {
|
|
|
|
|
if (
|
|
|
|
|
typeof delayMinutes !== 'number' ||
|
|
|
|
|
!Number.isFinite(delayMinutes) ||
|
|
|
|
|
delayMinutes < 0
|
|
|
|
|
) {
|
|
|
|
|
return SHUTDOWN.DEFAULT_DELAY_MINUTES;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return delayMinutes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getDefaultShutdownDelayMinutes(): number {
|
|
|
|
|
return this.normalizeShutdownDelay(this.config.defaultShutdownDelay);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
/**
|
|
|
|
|
* Get the SNMP instance
|
|
|
|
|
*/
|
|
|
|
|
public getNupstSnmp(): NupstSnmp {
|
|
|
|
|
return this.snmp;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 11:51:59 +00:00
|
|
|
/**
|
|
|
|
|
* Get the UPSD instance
|
|
|
|
|
*/
|
|
|
|
|
public getNupstUpsd(): NupstUpsd {
|
|
|
|
|
return this.upsd;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
/**
|
|
|
|
|
* Start the monitoring daemon
|
|
|
|
|
*/
|
|
|
|
|
public async start(): Promise<void> {
|
|
|
|
|
if (this.isRunning) {
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log('Daemon is already running');
|
2025-03-25 09:06:23 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log('Starting NUPST daemon...');
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
try {
|
|
|
|
|
// Load configuration - this will throw an error if config doesn't exist
|
|
|
|
|
await this.loadConfig();
|
|
|
|
|
this.logConfigLoaded();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:27:44 +00:00
|
|
|
// Log version information
|
2026-01-29 17:04:12 +00:00
|
|
|
const nupst = this.snmp.getNupst();
|
|
|
|
|
if (nupst) {
|
|
|
|
|
nupst.logVersionInfo(false); // Don't check for updates immediately on startup
|
|
|
|
|
|
|
|
|
|
// Check for updates in the background
|
|
|
|
|
nupst.checkForUpdates().then((updateAvailable: boolean) => {
|
|
|
|
|
if (updateAvailable) {
|
|
|
|
|
const updateStatus = nupst.getUpdateStatus();
|
|
|
|
|
const boxWidth = 45;
|
|
|
|
|
logger.logBoxTitle('Update Available', boxWidth);
|
|
|
|
|
logger.logBoxLine(`Current Version: ${updateStatus.currentVersion}`);
|
|
|
|
|
logger.logBoxLine(`Latest Version: ${updateStatus.latestVersion}`);
|
2026-03-15 12:04:05 +00:00
|
|
|
logger.logBoxLine('Run "sudo nupst upgrade" to upgrade');
|
2026-01-29 17:04:12 +00:00
|
|
|
logger.logBoxEnd();
|
|
|
|
|
}
|
|
|
|
|
}).catch(() => {}); // Ignore errors checking for updates
|
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Initialize UPS status tracking
|
|
|
|
|
this.initializeUpsStatus();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-10-23 12:57:58 +00:00
|
|
|
// Start HTTP server if configured
|
|
|
|
|
if (this.config.httpServer?.enabled && this.config.httpServer.authToken) {
|
|
|
|
|
try {
|
|
|
|
|
this.httpServer = new NupstHttpServer(
|
|
|
|
|
this.config.httpServer.port,
|
|
|
|
|
this.config.httpServer.path,
|
|
|
|
|
this.config.httpServer.authToken,
|
2026-01-29 17:10:17 +00:00
|
|
|
() => this.upsStatus,
|
2026-02-20 11:51:59 +00:00
|
|
|
() => this.pauseState,
|
2025-10-23 12:57:58 +00:00
|
|
|
);
|
|
|
|
|
this.httpServer.start();
|
|
|
|
|
} catch (error) {
|
2026-01-29 17:10:17 +00:00
|
|
|
logger.error(
|
|
|
|
|
`Failed to start HTTP server: ${
|
|
|
|
|
error instanceof Error ? error.message : String(error)
|
|
|
|
|
}`,
|
|
|
|
|
);
|
2025-10-23 12:57:58 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
// Start UPS monitoring
|
|
|
|
|
this.isRunning = true;
|
|
|
|
|
await this.monitor();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.isRunning = false;
|
2025-10-19 13:14:18 +00:00
|
|
|
logger.error(
|
|
|
|
|
`Daemon failed to start: ${error instanceof Error ? error.message : String(error)}`,
|
|
|
|
|
);
|
2025-03-25 09:06:23 +00:00
|
|
|
process.exit(1); // Exit with error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
/**
|
|
|
|
|
* Initialize UPS status tracking for all UPS devices
|
|
|
|
|
*/
|
|
|
|
|
private initializeUpsStatus(): void {
|
|
|
|
|
this.upsStatus.clear();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
|
|
|
|
|
for (const ups of this.config.upsDevices) {
|
2026-04-14 14:27:29 +00:00
|
|
|
this.upsStatus.set(ups.id, createInitialUpsStatus(ups));
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.log(`Initialized status tracking for ${this.config.upsDevices.length} UPS devices`);
|
|
|
|
|
} else {
|
|
|
|
|
logger.error('No UPS devices found in configuration');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
/**
|
|
|
|
|
* Log the loaded configuration settings
|
|
|
|
|
*/
|
|
|
|
|
private logConfigLoaded(): void {
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.log('');
|
|
|
|
|
logger.logBoxTitle('Configuration Loaded', 70, 'success');
|
|
|
|
|
logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`);
|
|
|
|
|
logger.logBoxEnd();
|
|
|
|
|
logger.log('');
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-10-20 01:38:44 +00:00
|
|
|
// Display UPS devices in a table
|
2025-03-28 16:19:43 +00:00
|
|
|
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.info(`UPS Devices (${this.config.upsDevices.length}):`);
|
2026-01-29 17:10:17 +00:00
|
|
|
|
|
|
|
|
const upsColumns: Array<
|
|
|
|
|
{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }
|
|
|
|
|
> = [
|
2025-10-20 01:38:44 +00:00
|
|
|
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
|
|
|
|
|
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
|
2026-02-20 11:51:59 +00:00
|
|
|
{ header: 'Protocol', key: 'protocol', align: 'left' },
|
2025-10-20 01:38:44 +00:00
|
|
|
{ header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
|
2025-10-20 11:47:51 +00:00
|
|
|
{ header: 'Actions', key: 'actions', align: 'left' },
|
2025-10-20 01:38:44 +00:00
|
|
|
];
|
|
|
|
|
|
2026-02-20 11:51:59 +00:00
|
|
|
const upsRows: Array<Record<string, string>> = this.config.upsDevices.map((ups) => {
|
|
|
|
|
const protocol = ups.protocol || 'snmp';
|
|
|
|
|
let host = 'N/A';
|
|
|
|
|
if (protocol === 'upsd' && ups.upsd) {
|
|
|
|
|
host = `${ups.upsd.host}:${ups.upsd.port}`;
|
|
|
|
|
} else if (ups.snmp) {
|
|
|
|
|
host = `${ups.snmp.host}:${ups.snmp.port}`;
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
name: ups.name,
|
|
|
|
|
id: ups.id,
|
|
|
|
|
protocol: protocol.toUpperCase(),
|
|
|
|
|
host,
|
|
|
|
|
actions: `${(ups.actions || []).length} configured`,
|
|
|
|
|
};
|
|
|
|
|
});
|
2025-10-20 01:38:44 +00:00
|
|
|
|
|
|
|
|
logger.logTable(upsColumns, upsRows);
|
|
|
|
|
logger.log('');
|
2025-03-28 16:19:43 +00:00
|
|
|
} else {
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.warn('No UPS devices configured');
|
|
|
|
|
logger.log('');
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-10-20 01:38:44 +00:00
|
|
|
// Display groups in a table
|
2025-03-28 16:19:43 +00:00
|
|
|
if (this.config.groups && this.config.groups.length > 0) {
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.info(`Groups (${this.config.groups.length}):`);
|
2026-01-29 17:10:17 +00:00
|
|
|
|
|
|
|
|
const groupColumns: Array<
|
|
|
|
|
{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }
|
|
|
|
|
> = [
|
2025-10-20 01:38:44 +00:00
|
|
|
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
|
|
|
|
|
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
|
|
|
|
|
{ header: 'Mode', key: 'mode', align: 'left', color: theme.info },
|
|
|
|
|
];
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-10-20 01:38:44 +00:00
|
|
|
const groupRows: Array<Record<string, string>> = this.config.groups.map((group) => ({
|
|
|
|
|
name: group.name,
|
|
|
|
|
id: group.id,
|
|
|
|
|
mode: group.mode,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
logger.logTable(groupColumns, groupRows);
|
|
|
|
|
logger.log('');
|
|
|
|
|
}
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Stop the monitoring daemon
|
|
|
|
|
*/
|
|
|
|
|
public stop(): void {
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log('Stopping NUPST daemon...');
|
2025-10-23 12:57:58 +00:00
|
|
|
|
|
|
|
|
// Stop HTTP server if running
|
|
|
|
|
if (this.httpServer) {
|
|
|
|
|
this.httpServer.stop();
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
this.isRunning = false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 11:51:59 +00:00
|
|
|
/**
|
|
|
|
|
* Get the current pause state
|
|
|
|
|
*/
|
|
|
|
|
public getPauseState(): IPauseState | null {
|
|
|
|
|
return this.pauseState;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check and update pause state from the pause file
|
|
|
|
|
*/
|
|
|
|
|
private checkPauseState(): void {
|
2026-04-14 14:27:29 +00:00
|
|
|
const snapshot = loadPauseSnapshot(PAUSE.FILE_PATH, this.isPaused);
|
2026-02-20 11:51:59 +00:00
|
|
|
|
2026-04-14 14:27:29 +00:00
|
|
|
if (snapshot.transition === 'autoResumed') {
|
|
|
|
|
logger.log('');
|
|
|
|
|
logger.logBoxTitle('Auto-Resume', 45, 'success');
|
|
|
|
|
logger.logBoxLine('Pause duration expired, resuming action monitoring');
|
|
|
|
|
logger.logBoxEnd();
|
|
|
|
|
logger.log('');
|
|
|
|
|
} else if (snapshot.transition === 'paused' && snapshot.pauseState) {
|
|
|
|
|
logger.log('');
|
|
|
|
|
logger.logBoxTitle('Actions Paused', 45, 'warning');
|
|
|
|
|
logger.logBoxLine(`Paused by: ${snapshot.pauseState.pausedBy}`);
|
|
|
|
|
if (snapshot.pauseState.reason) {
|
|
|
|
|
logger.logBoxLine(`Reason: ${snapshot.pauseState.reason}`);
|
|
|
|
|
}
|
|
|
|
|
if (snapshot.pauseState.resumeAt) {
|
|
|
|
|
const remaining = Math.round((snapshot.pauseState.resumeAt - Date.now()) / 1000);
|
|
|
|
|
logger.logBoxLine(`Auto-resume in: ${remaining} seconds`);
|
2026-02-20 11:51:59 +00:00
|
|
|
} else {
|
2026-04-14 14:27:29 +00:00
|
|
|
logger.logBoxLine('Duration: Indefinite (run "nupst resume" to resume)');
|
2026-02-20 11:51:59 +00:00
|
|
|
}
|
2026-04-14 14:27:29 +00:00
|
|
|
logger.logBoxEnd();
|
|
|
|
|
logger.log('');
|
|
|
|
|
} else if (snapshot.transition === 'resumed') {
|
|
|
|
|
logger.log('');
|
|
|
|
|
logger.logBoxTitle('Actions Resumed', 45, 'success');
|
|
|
|
|
logger.logBoxLine('Action monitoring has been resumed');
|
|
|
|
|
logger.logBoxEnd();
|
|
|
|
|
logger.log('');
|
2026-02-20 11:51:59 +00:00
|
|
|
}
|
2026-04-14 14:27:29 +00:00
|
|
|
|
|
|
|
|
this.isPaused = snapshot.isPaused;
|
|
|
|
|
this.pauseState = snapshot.pauseState;
|
2026-02-20 11:51:59 +00:00
|
|
|
}
|
|
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
/**
|
|
|
|
|
* Monitor the UPS status and trigger shutdown when necessary
|
|
|
|
|
*/
|
|
|
|
|
private async monitor(): Promise<void> {
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log('Starting UPS monitoring...');
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
if (!this.config.upsDevices || this.config.upsDevices.length === 0) {
|
feat(cli): add beautiful colored output and fix daemon exit bug
Major improvements:
- Created color theme system (ts/colors.ts) with ANSI colors
- Enhanced logger with colors, table formatting, and styled boxes
- Fixed daemon exit bug - now stays running when no UPS configured
- Added config hot-reload with file watcher for live updates
- Beautified CLI help output with color-coded commands
- Added showcase test demonstrating all output features
- Fixed ANSI code handling for perfect table/box alignment
Features:
- Color-coded messages (success=green, error=red, warning=yellow, info=cyan)
- Status symbols (●○◐◯ for running/stopped/starting/unknown)
- Battery level colors (green>60%, yellow 30-60%, red<30%)
- Table formatting with auto-sizing and column alignment
- Styled boxes (success, error, warning, info styles)
- Hot-reload: daemon watches config file and reloads automatically
- Idle mode: daemon stays alive when no devices, checks periodically
Daemon improvements:
- No longer exits when no UPS devices configured
- Enters idle monitoring loop waiting for config
- File watcher detects config changes in real-time
- Auto-reloads and starts monitoring when devices added
- Logs warnings instead of errors for missing devices
Technical fixes:
- Strip ANSI codes when calculating text width for alignment
- Use visible length for padding calculations in tables and boxes
- Properly handle colored text in table cells and box lines
Breaking changes: None (backward compatible)
2025-10-19 15:08:30 +00:00
|
|
|
logger.warn('No UPS devices found in configuration. Daemon will remain idle...');
|
|
|
|
|
// Don't exit - enter idle monitoring mode instead
|
|
|
|
|
await this.idleMonitoring();
|
2025-03-28 16:19:43 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:20:55 +00:00
|
|
|
let lastLogTime = 0; // Track when we last logged status
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
// Monitor continuously
|
|
|
|
|
while (this.isRunning) {
|
|
|
|
|
try {
|
2026-02-20 11:51:59 +00:00
|
|
|
// Check pause state before each cycle
|
|
|
|
|
this.checkPauseState();
|
|
|
|
|
|
|
|
|
|
// Check all UPS devices (polling continues even when paused for visibility)
|
2025-03-28 16:19:43 +00:00
|
|
|
await this.checkAllUpsDevices();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Log periodic status update
|
|
|
|
|
const currentTime = Date.now();
|
2026-01-29 17:04:12 +00:00
|
|
|
if (currentTime - lastLogTime >= TIMING.LOG_INTERVAL_MS) {
|
2025-03-28 16:19:43 +00:00
|
|
|
this.logAllUpsStatus();
|
2025-03-25 09:20:55 +00:00
|
|
|
lastLogTime = currentTime;
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
// Wait before next check
|
|
|
|
|
await this.sleep(this.config.checkInterval);
|
|
|
|
|
} catch (error) {
|
2025-10-19 13:14:18 +00:00
|
|
|
logger.error(
|
|
|
|
|
`Error during UPS monitoring: ${error instanceof Error ? error.message : String(error)}`,
|
|
|
|
|
);
|
2025-03-25 09:06:23 +00:00
|
|
|
await this.sleep(this.config.checkInterval);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.log('UPS monitoring stopped');
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
/**
|
|
|
|
|
* Check status of all UPS devices
|
|
|
|
|
*/
|
|
|
|
|
private async checkAllUpsDevices(): Promise<void> {
|
|
|
|
|
for (const ups of this.config.upsDevices) {
|
|
|
|
|
try {
|
2026-04-14 14:27:29 +00:00
|
|
|
const initialStatus = ensureUpsStatus(this.upsStatus.get(ups.id), ups);
|
|
|
|
|
this.upsStatus.set(ups.id, initialStatus);
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2026-02-20 11:51:59 +00:00
|
|
|
// Check UPS status via configured protocol
|
|
|
|
|
const protocol = ups.protocol || 'snmp';
|
|
|
|
|
const status = protocol === 'upsd' && ups.upsd
|
|
|
|
|
? await this.protocolResolver.getUpsStatus('upsd', undefined, ups.upsd)
|
|
|
|
|
: await this.protocolResolver.getUpsStatus('snmp', ups.snmp);
|
2025-03-28 16:19:43 +00:00
|
|
|
const currentTime = Date.now();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
const currentStatus = this.upsStatus.get(ups.id);
|
2026-04-14 14:27:29 +00:00
|
|
|
const pollSnapshot = buildSuccessfulUpsPollSnapshot(
|
|
|
|
|
ups,
|
|
|
|
|
status,
|
|
|
|
|
currentStatus,
|
|
|
|
|
currentTime,
|
|
|
|
|
);
|
2025-10-18 21:07:57 +00:00
|
|
|
|
2026-04-14 14:27:29 +00:00
|
|
|
if (pollSnapshot.transition === 'recovered' && pollSnapshot.previousStatus) {
|
2026-02-20 11:51:59 +00:00
|
|
|
logger.log('');
|
|
|
|
|
logger.logBoxTitle(`UPS Recovered: ${ups.name}`, 60, 'success');
|
2026-04-14 14:27:29 +00:00
|
|
|
logger.logBoxLine(`UPS is reachable again after ${pollSnapshot.downtimeSeconds} seconds`);
|
2026-02-20 11:51:59 +00:00
|
|
|
logger.logBoxLine(`Current Status: ${formatPowerStatus(status.powerStatus)}`);
|
|
|
|
|
logger.logBoxLine(`Time: ${new Date().toISOString()}`);
|
|
|
|
|
logger.logBoxEnd();
|
|
|
|
|
logger.log('');
|
|
|
|
|
|
|
|
|
|
// Trigger power status change action for recovery
|
2026-04-14 14:27:29 +00:00
|
|
|
await this.triggerUpsActions(
|
|
|
|
|
ups,
|
|
|
|
|
pollSnapshot.updatedStatus,
|
|
|
|
|
pollSnapshot.previousStatus,
|
|
|
|
|
'powerStatusChange',
|
|
|
|
|
);
|
|
|
|
|
} else if (pollSnapshot.transition === 'powerStatusChange' && pollSnapshot.previousStatus) {
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.log('');
|
|
|
|
|
logger.logBoxTitle(`Power Status Change: ${ups.name}`, 60, 'warning');
|
2026-04-14 14:27:29 +00:00
|
|
|
logger.logBoxLine(
|
|
|
|
|
`Previous: ${formatPowerStatus(pollSnapshot.previousStatus.powerStatus)}`,
|
|
|
|
|
);
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.logBoxLine(`Current: ${formatPowerStatus(status.powerStatus)}`);
|
|
|
|
|
logger.logBoxLine(`Time: ${new Date().toISOString()}`);
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.logBoxEnd();
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.log('');
|
2025-10-18 21:07:57 +00:00
|
|
|
|
2025-10-20 11:47:51 +00:00
|
|
|
// Trigger actions for power status change
|
2026-04-14 14:27:29 +00:00
|
|
|
await this.triggerUpsActions(
|
|
|
|
|
ups,
|
|
|
|
|
pollSnapshot.updatedStatus,
|
|
|
|
|
pollSnapshot.previousStatus,
|
|
|
|
|
'powerStatusChange',
|
|
|
|
|
);
|
2025-10-20 11:47:51 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-16 02:54:16 +00:00
|
|
|
const thresholdStates = getActionThresholdStates(
|
|
|
|
|
status.powerStatus,
|
|
|
|
|
status.batteryCapacity,
|
|
|
|
|
status.batteryRuntime,
|
|
|
|
|
ups.actions,
|
|
|
|
|
);
|
|
|
|
|
const enteredThresholdIndexes = this.trackEnteredThresholdIndexes(
|
|
|
|
|
`ups:${ups.id}`,
|
|
|
|
|
thresholdStates,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (enteredThresholdIndexes.length > 0) {
|
2026-04-14 14:27:29 +00:00
|
|
|
await this.triggerUpsActions(
|
|
|
|
|
ups,
|
|
|
|
|
pollSnapshot.updatedStatus,
|
|
|
|
|
pollSnapshot.previousStatus,
|
|
|
|
|
'thresholdViolation',
|
2026-04-16 02:54:16 +00:00
|
|
|
enteredThresholdIndexes,
|
2026-04-14 14:27:29 +00:00
|
|
|
);
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
2025-10-18 21:07:57 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Update the status in the map
|
2026-04-14 14:27:29 +00:00
|
|
|
this.upsStatus.set(ups.id, pollSnapshot.updatedStatus);
|
2025-03-28 16:19:43 +00:00
|
|
|
} catch (error) {
|
2026-04-14 14:27:29 +00:00
|
|
|
const currentTime = Date.now();
|
2026-02-20 11:51:59 +00:00
|
|
|
const currentStatus = this.upsStatus.get(ups.id);
|
2026-04-14 14:27:29 +00:00
|
|
|
const failureSnapshot = buildFailedUpsPollSnapshot(ups, currentStatus, currentTime);
|
2026-02-20 11:51:59 +00:00
|
|
|
|
2025-10-19 13:14:18 +00:00
|
|
|
logger.error(
|
2026-04-14 14:27:29 +00:00
|
|
|
`Error checking UPS ${ups.name} (${ups.id}) [failure ${failureSnapshot.failures}/${NETWORK.CONSECUTIVE_FAILURE_THRESHOLD}]: ${
|
2025-10-19 13:14:18 +00:00
|
|
|
error instanceof Error ? error.message : String(error)
|
|
|
|
|
}`,
|
|
|
|
|
);
|
2026-02-20 11:51:59 +00:00
|
|
|
|
2026-04-14 14:27:29 +00:00
|
|
|
if (failureSnapshot.transition === 'unreachable' && failureSnapshot.previousStatus) {
|
2026-02-20 11:51:59 +00:00
|
|
|
logger.log('');
|
|
|
|
|
logger.logBoxTitle(`UPS Unreachable: ${ups.name}`, 60, 'error');
|
2026-04-14 14:27:29 +00:00
|
|
|
logger.logBoxLine(`${failureSnapshot.failures} consecutive communication failures`);
|
|
|
|
|
logger.logBoxLine(
|
|
|
|
|
`Last known status: ${formatPowerStatus(failureSnapshot.previousStatus.powerStatus)}`,
|
|
|
|
|
);
|
2026-02-20 11:51:59 +00:00
|
|
|
logger.logBoxLine(`Time: ${new Date().toISOString()}`);
|
|
|
|
|
logger.logBoxEnd();
|
|
|
|
|
logger.log('');
|
|
|
|
|
|
|
|
|
|
// Trigger power status change action for unreachable
|
2026-04-14 14:27:29 +00:00
|
|
|
await this.triggerUpsActions(
|
|
|
|
|
ups,
|
|
|
|
|
failureSnapshot.updatedStatus,
|
|
|
|
|
failureSnapshot.previousStatus,
|
|
|
|
|
'powerStatusChange',
|
|
|
|
|
);
|
2026-02-20 11:51:59 +00:00
|
|
|
}
|
2026-04-14 14:27:29 +00:00
|
|
|
|
|
|
|
|
this.upsStatus.set(ups.id, failureSnapshot.updatedStatus);
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-16 02:54:16 +00:00
|
|
|
|
|
|
|
|
await this.checkGroupActions();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private trackEnteredThresholdIndexes(sourceKey: string, currentStates: boolean[]): number[] {
|
|
|
|
|
const previousStates = this.thresholdState.get(sourceKey);
|
|
|
|
|
const enteredIndexes = getEnteredThresholdIndexes(previousStates, currentStates);
|
|
|
|
|
this.thresholdState.set(sourceKey, [...currentStates]);
|
|
|
|
|
return enteredIndexes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getGroupActionIdentity(group: IGroupConfig): { id: string; name: string } {
|
|
|
|
|
return {
|
|
|
|
|
id: group.id,
|
|
|
|
|
name: `Group ${group.name}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async checkGroupActions(): Promise<void> {
|
|
|
|
|
for (const group of this.config.groups || []) {
|
|
|
|
|
const groupIdentity = this.getGroupActionIdentity(group);
|
|
|
|
|
const memberStatuses = this.config.upsDevices
|
|
|
|
|
.filter((ups) => ups.groups?.includes(group.id))
|
|
|
|
|
.map((ups) => this.upsStatus.get(ups.id))
|
|
|
|
|
.filter((status): status is IUpsStatus => !!status);
|
|
|
|
|
|
|
|
|
|
if (memberStatuses.length === 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentTime = Date.now();
|
|
|
|
|
const pollSnapshot = buildGroupStatusSnapshot(
|
|
|
|
|
groupIdentity,
|
|
|
|
|
group.mode,
|
|
|
|
|
memberStatuses,
|
|
|
|
|
this.groupStatus.get(group.id),
|
|
|
|
|
currentTime,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (pollSnapshot.transition === 'powerStatusChange' && pollSnapshot.previousStatus) {
|
|
|
|
|
logger.log('');
|
|
|
|
|
logger.logBoxTitle(`Group Power Status Change: ${group.name}`, 60, 'warning');
|
|
|
|
|
logger.logBoxLine(
|
|
|
|
|
`Previous: ${formatPowerStatus(pollSnapshot.previousStatus.powerStatus)}`,
|
|
|
|
|
);
|
|
|
|
|
logger.logBoxLine(`Current: ${formatPowerStatus(pollSnapshot.updatedStatus.powerStatus)}`);
|
|
|
|
|
logger.logBoxLine(`Members: ${memberStatuses.map((status) => status.name).join(', ')}`);
|
|
|
|
|
logger.logBoxLine(`Time: ${new Date().toISOString()}`);
|
|
|
|
|
logger.logBoxEnd();
|
|
|
|
|
logger.log('');
|
|
|
|
|
|
|
|
|
|
await this.triggerGroupActions(
|
|
|
|
|
group,
|
|
|
|
|
pollSnapshot.updatedStatus,
|
|
|
|
|
pollSnapshot.previousStatus,
|
|
|
|
|
'powerStatusChange',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const thresholdEvaluations = (group.actions || []).map((action) =>
|
|
|
|
|
evaluateGroupActionThreshold(action, group.mode, memberStatuses)
|
|
|
|
|
);
|
|
|
|
|
const thresholdStates = thresholdEvaluations.map((evaluation) =>
|
|
|
|
|
evaluation.exceedsThreshold && !evaluation.blockedByUnreachable
|
|
|
|
|
);
|
|
|
|
|
const enteredThresholdIndexes = this.trackEnteredThresholdIndexes(
|
|
|
|
|
`group:${group.id}`,
|
|
|
|
|
thresholdStates,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (enteredThresholdIndexes.length > 0) {
|
|
|
|
|
const thresholdStatus = buildGroupThresholdContextStatus(
|
|
|
|
|
groupIdentity,
|
|
|
|
|
thresholdEvaluations,
|
|
|
|
|
enteredThresholdIndexes,
|
|
|
|
|
pollSnapshot.updatedStatus,
|
|
|
|
|
currentTime,
|
|
|
|
|
);
|
|
|
|
|
await this.triggerGroupActions(
|
|
|
|
|
group,
|
|
|
|
|
thresholdStatus,
|
|
|
|
|
pollSnapshot.previousStatus,
|
|
|
|
|
'thresholdViolation',
|
|
|
|
|
enteredThresholdIndexes,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.groupStatus.set(group.id, pollSnapshot.updatedStatus);
|
|
|
|
|
}
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
/**
|
2025-03-28 16:19:43 +00:00
|
|
|
* Log status of all UPS devices
|
2025-03-25 09:06:23 +00:00
|
|
|
*/
|
2025-03-28 16:19:43 +00:00
|
|
|
private logAllUpsStatus(): void {
|
|
|
|
|
const timestamp = new Date().toISOString();
|
2026-01-29 17:10:17 +00:00
|
|
|
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.log('');
|
2026-02-20 11:51:59 +00:00
|
|
|
const pauseLabel = this.isPaused ? ' [PAUSED]' : '';
|
2026-04-14 14:27:29 +00:00
|
|
|
logger.logBoxTitle(
|
|
|
|
|
`Periodic Status Update${pauseLabel}`,
|
|
|
|
|
70,
|
|
|
|
|
this.isPaused ? 'warning' : 'info',
|
|
|
|
|
);
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.logBoxLine(`Timestamp: ${timestamp}`);
|
2026-02-20 11:51:59 +00:00
|
|
|
if (this.isPaused && this.pauseState) {
|
|
|
|
|
logger.logBoxLine(`Actions paused by: ${this.pauseState.pausedBy}`);
|
|
|
|
|
if (this.pauseState.resumeAt) {
|
|
|
|
|
const remaining = Math.round((this.pauseState.resumeAt - Date.now()) / 1000);
|
|
|
|
|
logger.logBoxLine(`Auto-resume in: ${remaining > 0 ? remaining : 0} seconds`);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.logBoxEnd();
|
|
|
|
|
logger.log('');
|
|
|
|
|
|
|
|
|
|
// Build table data
|
2026-01-29 17:10:17 +00:00
|
|
|
const columns: Array<
|
|
|
|
|
{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }
|
|
|
|
|
> = [
|
2025-10-20 01:38:44 +00:00
|
|
|
{ header: 'UPS Name', key: 'name', align: 'left', color: theme.highlight },
|
|
|
|
|
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
|
|
|
|
|
{ header: 'Power Status', key: 'powerStatus', align: 'left' },
|
|
|
|
|
{ header: 'Battery', key: 'battery', align: 'right' },
|
|
|
|
|
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
|
|
|
|
];
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-10-20 01:38:44 +00:00
|
|
|
const rows: Array<Record<string, string>> = [];
|
2025-03-28 16:19:43 +00:00
|
|
|
for (const [id, status] of this.upsStatus.entries()) {
|
2025-10-20 01:38:44 +00:00
|
|
|
const batteryColor = getBatteryColor(status.batteryCapacity);
|
|
|
|
|
const runtimeColor = getRuntimeColor(status.batteryRuntime);
|
2026-01-29 17:10:17 +00:00
|
|
|
|
2025-10-20 01:38:44 +00:00
|
|
|
rows.push({
|
|
|
|
|
name: status.name,
|
|
|
|
|
id: id,
|
|
|
|
|
powerStatus: formatPowerStatus(status.powerStatus),
|
|
|
|
|
battery: batteryColor(status.batteryCapacity + '%'),
|
|
|
|
|
runtime: runtimeColor(status.batteryRuntime + ' min'),
|
|
|
|
|
});
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.logTable(columns, rows);
|
|
|
|
|
logger.log('');
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
/**
|
2025-10-20 11:47:51 +00:00
|
|
|
* Trigger actions for a UPS device
|
|
|
|
|
* @param ups UPS configuration
|
|
|
|
|
* @param status Current UPS status
|
|
|
|
|
* @param previousStatus Previous UPS status (for determining previousPowerStatus)
|
|
|
|
|
* @param triggerReason Why actions are being triggered
|
2025-03-28 16:19:43 +00:00
|
|
|
*/
|
2025-10-20 11:47:51 +00:00
|
|
|
private async triggerUpsActions(
|
|
|
|
|
ups: IUpsConfig,
|
|
|
|
|
status: IUpsStatus,
|
|
|
|
|
previousStatus: IUpsStatus | undefined,
|
2026-04-14 14:27:29 +00:00
|
|
|
triggerReason: TUpsTriggerReason,
|
2026-04-16 02:54:16 +00:00
|
|
|
actionIndexes?: number[],
|
2025-10-19 13:14:18 +00:00
|
|
|
): Promise<void> {
|
2026-04-14 14:27:29 +00:00
|
|
|
const decision = decideUpsActionExecution(
|
|
|
|
|
this.isPaused,
|
|
|
|
|
ups,
|
|
|
|
|
status,
|
|
|
|
|
previousStatus,
|
|
|
|
|
triggerReason,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (decision.type === 'suppressed') {
|
|
|
|
|
logger.info(decision.message);
|
2026-02-20 11:51:59 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 14:27:29 +00:00
|
|
|
if (decision.type === 'legacyShutdown') {
|
|
|
|
|
await this.initiateShutdown(decision.reason);
|
2025-10-20 11:47:51 +00:00
|
|
|
return;
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2026-04-14 14:27:29 +00:00
|
|
|
if (decision.type === 'skip') {
|
|
|
|
|
return;
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2026-04-16 02:54:16 +00:00
|
|
|
const selectedActions = actionIndexes
|
|
|
|
|
? decision.actions.filter((_action, index) => actionIndexes.includes(index))
|
|
|
|
|
: decision.actions;
|
|
|
|
|
|
|
|
|
|
if (selectedActions.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-14 18:47:37 +00:00
|
|
|
const actions = applyDefaultShutdownDelay(
|
2026-04-16 02:54:16 +00:00
|
|
|
selectedActions,
|
2026-04-14 18:47:37 +00:00
|
|
|
this.getDefaultShutdownDelayMinutes(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await ActionManager.executeActions(actions, decision.context);
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2026-04-16 02:54:16 +00:00
|
|
|
private async triggerGroupActions(
|
|
|
|
|
group: IGroupConfig,
|
|
|
|
|
status: IUpsStatus,
|
|
|
|
|
previousStatus: IUpsStatus | undefined,
|
|
|
|
|
triggerReason: TUpsTriggerReason,
|
|
|
|
|
actionIndexes?: number[],
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
if (this.isPaused) {
|
|
|
|
|
logger.info(
|
|
|
|
|
`[PAUSED] Actions suppressed for Group ${group.name} (trigger: ${triggerReason})`,
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const configuredActions = group.actions || [];
|
|
|
|
|
if (configuredActions.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const selectedActions = actionIndexes
|
|
|
|
|
? configuredActions.filter((_action, index) => actionIndexes.includes(index))
|
|
|
|
|
: configuredActions;
|
|
|
|
|
|
|
|
|
|
if (selectedActions.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const actions = applyDefaultShutdownDelay(
|
|
|
|
|
selectedActions,
|
|
|
|
|
this.getDefaultShutdownDelayMinutes(),
|
|
|
|
|
);
|
|
|
|
|
const context = buildUpsActionContext(
|
|
|
|
|
this.getGroupActionIdentity(group),
|
|
|
|
|
status,
|
|
|
|
|
previousStatus,
|
|
|
|
|
triggerReason,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await ActionManager.executeActions(actions, context);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-25 11:49:50 +00:00
|
|
|
/**
|
|
|
|
|
* Initiate system shutdown with UPS monitoring during shutdown
|
|
|
|
|
* @param reason Reason for shutdown
|
|
|
|
|
*/
|
|
|
|
|
public async initiateShutdown(reason: string): Promise<void> {
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log(`Initiating system shutdown due to: ${reason}`);
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2026-04-14 18:47:37 +00:00
|
|
|
const shutdownDelayMinutes = this.getDefaultShutdownDelayMinutes();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 11:49:50 +00:00
|
|
|
try {
|
2026-04-14 14:27:29 +00:00
|
|
|
await this.shutdownExecutor.scheduleShutdown(shutdownDelayMinutes);
|
|
|
|
|
logger.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`);
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 11:49:50 +00:00
|
|
|
// Monitor UPS during shutdown and force immediate shutdown if battery gets too low
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log('Monitoring UPS during shutdown process...');
|
2025-03-25 11:49:50 +00:00
|
|
|
await this.monitorDuringShutdown();
|
|
|
|
|
} catch (error) {
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.error(`Failed to initiate shutdown: ${error}`);
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2026-04-14 14:27:29 +00:00
|
|
|
const shutdownTriggered = await this.shutdownExecutor.tryScheduledAlternatives();
|
|
|
|
|
if (!shutdownTriggered) {
|
|
|
|
|
logger.error('All shutdown methods failed');
|
2025-03-25 11:49:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 11:49:50 +00:00
|
|
|
/**
|
|
|
|
|
* Monitor UPS during system shutdown
|
2025-03-28 16:19:43 +00:00
|
|
|
* Force immediate shutdown if any UPS gets critically low
|
2025-03-25 11:49:50 +00:00
|
|
|
*/
|
|
|
|
|
private async monitorDuringShutdown(): Promise<void> {
|
|
|
|
|
const startTime = Date.now();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.log('');
|
2026-01-29 17:04:12 +00:00
|
|
|
logger.logBoxTitle('Shutdown Monitoring Active', UI.WIDE_BOX_WIDTH, 'warning');
|
2026-01-29 17:10:17 +00:00
|
|
|
logger.logBoxLine(
|
|
|
|
|
`Emergency threshold: ${THRESHOLDS.EMERGENCY_RUNTIME_MINUTES} minutes runtime`,
|
|
|
|
|
);
|
2026-01-29 17:04:12 +00:00
|
|
|
logger.logBoxLine(`Check interval: ${TIMING.SHUTDOWN_CHECK_INTERVAL_MS / 1000} seconds`);
|
|
|
|
|
logger.logBoxLine(`Max monitoring time: ${TIMING.MAX_SHUTDOWN_MONITORING_MS / 1000} seconds`);
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.logBoxEnd();
|
|
|
|
|
logger.log('');
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 11:49:50 +00:00
|
|
|
// Continue monitoring until max monitoring time is reached
|
2026-01-29 17:04:12 +00:00
|
|
|
while (Date.now() - startTime < TIMING.MAX_SHUTDOWN_MONITORING_MS) {
|
2025-03-25 11:49:50 +00:00
|
|
|
try {
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.info('Checking UPS status during shutdown...');
|
|
|
|
|
|
|
|
|
|
// Build table for UPS status during shutdown
|
2026-01-29 17:10:17 +00:00
|
|
|
const columns: Array<
|
|
|
|
|
{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }
|
|
|
|
|
> = [
|
2025-10-20 01:38:44 +00:00
|
|
|
{ header: 'UPS Name', key: 'name', align: 'left', color: theme.highlight },
|
|
|
|
|
{ header: 'Battery', key: 'battery', align: 'right' },
|
|
|
|
|
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
|
|
|
|
{ header: 'Status', key: 'status', align: 'left' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const rows: Array<Record<string, string>> = [];
|
2025-10-20 12:27:02 +00:00
|
|
|
let emergencyUps: { ups: IUpsConfig; status: ISnmpUpsStatus } | null = null;
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Check all UPS devices
|
|
|
|
|
for (const ups of this.config.upsDevices) {
|
2025-03-25 11:49:50 +00:00
|
|
|
try {
|
2026-02-20 11:51:59 +00:00
|
|
|
const protocol = ups.protocol || 'snmp';
|
|
|
|
|
const status = protocol === 'upsd' && ups.upsd
|
|
|
|
|
? await this.protocolResolver.getUpsStatus('upsd', undefined, ups.upsd)
|
|
|
|
|
: await this.protocolResolver.getUpsStatus('snmp', ups.snmp);
|
2026-04-14 14:27:29 +00:00
|
|
|
const rowSnapshot = buildShutdownStatusRow(
|
|
|
|
|
ups.name,
|
|
|
|
|
status,
|
|
|
|
|
THRESHOLDS.EMERGENCY_RUNTIME_MINUTES,
|
|
|
|
|
{
|
|
|
|
|
battery: (batteryCapacity) =>
|
|
|
|
|
getBatteryColor(batteryCapacity)(`${batteryCapacity}%`),
|
|
|
|
|
runtime: (batteryRuntime) =>
|
|
|
|
|
getRuntimeColor(batteryRuntime)(`${batteryRuntime} min`),
|
|
|
|
|
ok: theme.success,
|
|
|
|
|
critical: theme.error,
|
|
|
|
|
error: theme.error,
|
|
|
|
|
},
|
|
|
|
|
);
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2026-04-14 14:27:29 +00:00
|
|
|
rows.push(rowSnapshot.row);
|
|
|
|
|
emergencyUps = selectEmergencyCandidate(
|
|
|
|
|
emergencyUps,
|
|
|
|
|
ups,
|
|
|
|
|
status,
|
|
|
|
|
THRESHOLDS.EMERGENCY_RUNTIME_MINUTES,
|
|
|
|
|
);
|
2025-03-28 16:19:43 +00:00
|
|
|
} catch (upsError) {
|
2026-04-14 14:27:29 +00:00
|
|
|
rows.push(buildShutdownErrorRow(ups.name, theme.error));
|
2026-01-29 17:10:17 +00:00
|
|
|
|
2025-10-19 13:14:18 +00:00
|
|
|
logger.error(
|
|
|
|
|
`Error checking UPS ${ups.name} during shutdown: ${
|
|
|
|
|
upsError instanceof Error ? upsError.message : String(upsError)
|
|
|
|
|
}`,
|
|
|
|
|
);
|
2025-03-25 11:49:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-10-20 01:38:44 +00:00
|
|
|
// Display the table
|
|
|
|
|
logger.logTable(columns, rows);
|
|
|
|
|
logger.log('');
|
|
|
|
|
|
|
|
|
|
// If emergency detected, trigger immediate shutdown
|
2026-04-14 14:27:29 +00:00
|
|
|
if (emergencyUps) {
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.log('');
|
|
|
|
|
logger.logBoxTitle('EMERGENCY SHUTDOWN', 60, 'error');
|
|
|
|
|
logger.logBoxLine(
|
|
|
|
|
`UPS ${emergencyUps.ups.name} runtime critically low: ${emergencyUps.status.batteryRuntime} minutes`,
|
|
|
|
|
);
|
2026-01-29 17:04:12 +00:00
|
|
|
logger.logBoxLine(`Emergency threshold: ${THRESHOLDS.EMERGENCY_RUNTIME_MINUTES} minutes`);
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.logBoxLine('Forcing immediate shutdown!');
|
|
|
|
|
logger.logBoxEnd();
|
|
|
|
|
logger.log('');
|
|
|
|
|
|
|
|
|
|
// Force immediate shutdown
|
|
|
|
|
await this.forceImmediateShutdown();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-25 11:49:50 +00:00
|
|
|
// Wait before checking again
|
2026-01-29 17:04:12 +00:00
|
|
|
await this.sleep(TIMING.SHUTDOWN_CHECK_INTERVAL_MS);
|
2025-03-25 11:49:50 +00:00
|
|
|
} catch (error) {
|
2025-10-19 13:14:18 +00:00
|
|
|
logger.error(
|
|
|
|
|
`Error monitoring UPS during shutdown: ${
|
|
|
|
|
error instanceof Error ? error.message : String(error)
|
|
|
|
|
}`,
|
|
|
|
|
);
|
2026-01-29 17:04:12 +00:00
|
|
|
await this.sleep(TIMING.SHUTDOWN_CHECK_INTERVAL_MS);
|
2025-03-25 11:49:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-10-20 01:38:44 +00:00
|
|
|
logger.log('');
|
|
|
|
|
logger.success('UPS monitoring during shutdown completed');
|
|
|
|
|
logger.log('');
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
/**
|
|
|
|
|
* Force an immediate system shutdown
|
|
|
|
|
*/
|
|
|
|
|
private async forceImmediateShutdown(): Promise<void> {
|
|
|
|
|
try {
|
2026-04-14 14:27:29 +00:00
|
|
|
await this.shutdownExecutor.forceImmediateShutdown();
|
2025-03-28 16:19:43 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('Emergency shutdown failed, trying alternative methods...');
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2026-04-14 14:27:29 +00:00
|
|
|
const shutdownTriggered = await this.shutdownExecutor.tryEmergencyAlternatives();
|
|
|
|
|
if (!shutdownTriggered) {
|
|
|
|
|
logger.error('All emergency shutdown methods failed');
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-03-25 11:49:50 +00:00
|
|
|
}
|
2025-03-25 09:06:23 +00:00
|
|
|
|
feat(cli): add beautiful colored output and fix daemon exit bug
Major improvements:
- Created color theme system (ts/colors.ts) with ANSI colors
- Enhanced logger with colors, table formatting, and styled boxes
- Fixed daemon exit bug - now stays running when no UPS configured
- Added config hot-reload with file watcher for live updates
- Beautified CLI help output with color-coded commands
- Added showcase test demonstrating all output features
- Fixed ANSI code handling for perfect table/box alignment
Features:
- Color-coded messages (success=green, error=red, warning=yellow, info=cyan)
- Status symbols (●○◐◯ for running/stopped/starting/unknown)
- Battery level colors (green>60%, yellow 30-60%, red<30%)
- Table formatting with auto-sizing and column alignment
- Styled boxes (success, error, warning, info styles)
- Hot-reload: daemon watches config file and reloads automatically
- Idle mode: daemon stays alive when no devices, checks periodically
Daemon improvements:
- No longer exits when no UPS devices configured
- Enters idle monitoring loop waiting for config
- File watcher detects config changes in real-time
- Auto-reloads and starts monitoring when devices added
- Logs warnings instead of errors for missing devices
Technical fixes:
- Strip ANSI codes when calculating text width for alignment
- Use visible length for padding calculations in tables and boxes
- Properly handle colored text in table cells and box lines
Breaking changes: None (backward compatible)
2025-10-19 15:08:30 +00:00
|
|
|
/**
|
|
|
|
|
* Idle monitoring loop when no UPS devices are configured
|
|
|
|
|
* Watches for config changes and reloads when detected
|
|
|
|
|
*/
|
|
|
|
|
private async idleMonitoring(): Promise<void> {
|
|
|
|
|
let lastConfigCheck = Date.now();
|
|
|
|
|
|
|
|
|
|
logger.log('Entering idle monitoring mode...');
|
2026-01-29 17:10:17 +00:00
|
|
|
logger.log(
|
|
|
|
|
`Daemon will check for config changes every ${TIMING.IDLE_CHECK_INTERVAL_MS / 1000} seconds`,
|
|
|
|
|
);
|
feat(cli): add beautiful colored output and fix daemon exit bug
Major improvements:
- Created color theme system (ts/colors.ts) with ANSI colors
- Enhanced logger with colors, table formatting, and styled boxes
- Fixed daemon exit bug - now stays running when no UPS configured
- Added config hot-reload with file watcher for live updates
- Beautified CLI help output with color-coded commands
- Added showcase test demonstrating all output features
- Fixed ANSI code handling for perfect table/box alignment
Features:
- Color-coded messages (success=green, error=red, warning=yellow, info=cyan)
- Status symbols (●○◐◯ for running/stopped/starting/unknown)
- Battery level colors (green>60%, yellow 30-60%, red<30%)
- Table formatting with auto-sizing and column alignment
- Styled boxes (success, error, warning, info styles)
- Hot-reload: daemon watches config file and reloads automatically
- Idle mode: daemon stays alive when no devices, checks periodically
Daemon improvements:
- No longer exits when no UPS devices configured
- Enters idle monitoring loop waiting for config
- File watcher detects config changes in real-time
- Auto-reloads and starts monitoring when devices added
- Logs warnings instead of errors for missing devices
Technical fixes:
- Strip ANSI codes when calculating text width for alignment
- Use visible length for padding calculations in tables and boxes
- Properly handle colored text in table cells and box lines
Breaking changes: None (backward compatible)
2025-10-19 15:08:30 +00:00
|
|
|
|
|
|
|
|
// Start file watcher for hot-reload
|
|
|
|
|
this.watchConfigFile();
|
|
|
|
|
|
|
|
|
|
while (this.isRunning) {
|
|
|
|
|
try {
|
|
|
|
|
const currentTime = Date.now();
|
|
|
|
|
|
|
|
|
|
// Periodically check if config has been updated
|
2026-01-29 17:04:12 +00:00
|
|
|
if (currentTime - lastConfigCheck >= TIMING.CONFIG_CHECK_INTERVAL_MS) {
|
feat(cli): add beautiful colored output and fix daemon exit bug
Major improvements:
- Created color theme system (ts/colors.ts) with ANSI colors
- Enhanced logger with colors, table formatting, and styled boxes
- Fixed daemon exit bug - now stays running when no UPS configured
- Added config hot-reload with file watcher for live updates
- Beautified CLI help output with color-coded commands
- Added showcase test demonstrating all output features
- Fixed ANSI code handling for perfect table/box alignment
Features:
- Color-coded messages (success=green, error=red, warning=yellow, info=cyan)
- Status symbols (●○◐◯ for running/stopped/starting/unknown)
- Battery level colors (green>60%, yellow 30-60%, red<30%)
- Table formatting with auto-sizing and column alignment
- Styled boxes (success, error, warning, info styles)
- Hot-reload: daemon watches config file and reloads automatically
- Idle mode: daemon stays alive when no devices, checks periodically
Daemon improvements:
- No longer exits when no UPS devices configured
- Enters idle monitoring loop waiting for config
- File watcher detects config changes in real-time
- Auto-reloads and starts monitoring when devices added
- Logs warnings instead of errors for missing devices
Technical fixes:
- Strip ANSI codes when calculating text width for alignment
- Use visible length for padding calculations in tables and boxes
- Properly handle colored text in table cells and box lines
Breaking changes: None (backward compatible)
2025-10-19 15:08:30 +00:00
|
|
|
try {
|
|
|
|
|
// Try to load config
|
|
|
|
|
const newConfig = await this.loadConfig();
|
|
|
|
|
|
|
|
|
|
// Check if we now have UPS devices configured
|
|
|
|
|
if (newConfig.upsDevices && newConfig.upsDevices.length > 0) {
|
|
|
|
|
logger.success('Configuration updated! UPS devices found. Starting monitoring...');
|
|
|
|
|
this.initializeUpsStatus();
|
|
|
|
|
// Exit idle mode and start monitoring
|
|
|
|
|
await this.monitor();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Config still doesn't exist or invalid, continue waiting
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lastConfigCheck = currentTime;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 17:04:12 +00:00
|
|
|
await this.sleep(TIMING.IDLE_CHECK_INTERVAL_MS);
|
feat(cli): add beautiful colored output and fix daemon exit bug
Major improvements:
- Created color theme system (ts/colors.ts) with ANSI colors
- Enhanced logger with colors, table formatting, and styled boxes
- Fixed daemon exit bug - now stays running when no UPS configured
- Added config hot-reload with file watcher for live updates
- Beautified CLI help output with color-coded commands
- Added showcase test demonstrating all output features
- Fixed ANSI code handling for perfect table/box alignment
Features:
- Color-coded messages (success=green, error=red, warning=yellow, info=cyan)
- Status symbols (●○◐◯ for running/stopped/starting/unknown)
- Battery level colors (green>60%, yellow 30-60%, red<30%)
- Table formatting with auto-sizing and column alignment
- Styled boxes (success, error, warning, info styles)
- Hot-reload: daemon watches config file and reloads automatically
- Idle mode: daemon stays alive when no devices, checks periodically
Daemon improvements:
- No longer exits when no UPS devices configured
- Enters idle monitoring loop waiting for config
- File watcher detects config changes in real-time
- Auto-reloads and starts monitoring when devices added
- Logs warnings instead of errors for missing devices
Technical fixes:
- Strip ANSI codes when calculating text width for alignment
- Use visible length for padding calculations in tables and boxes
- Properly handle colored text in table cells and box lines
Breaking changes: None (backward compatible)
2025-10-19 15:08:30 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(
|
|
|
|
|
`Error during idle monitoring: ${error instanceof Error ? error.message : String(error)}`,
|
|
|
|
|
);
|
2026-01-29 17:04:12 +00:00
|
|
|
await this.sleep(TIMING.IDLE_CHECK_INTERVAL_MS);
|
feat(cli): add beautiful colored output and fix daemon exit bug
Major improvements:
- Created color theme system (ts/colors.ts) with ANSI colors
- Enhanced logger with colors, table formatting, and styled boxes
- Fixed daemon exit bug - now stays running when no UPS configured
- Added config hot-reload with file watcher for live updates
- Beautified CLI help output with color-coded commands
- Added showcase test demonstrating all output features
- Fixed ANSI code handling for perfect table/box alignment
Features:
- Color-coded messages (success=green, error=red, warning=yellow, info=cyan)
- Status symbols (●○◐◯ for running/stopped/starting/unknown)
- Battery level colors (green>60%, yellow 30-60%, red<30%)
- Table formatting with auto-sizing and column alignment
- Styled boxes (success, error, warning, info styles)
- Hot-reload: daemon watches config file and reloads automatically
- Idle mode: daemon stays alive when no devices, checks periodically
Daemon improvements:
- No longer exits when no UPS devices configured
- Enters idle monitoring loop waiting for config
- File watcher detects config changes in real-time
- Auto-reloads and starts monitoring when devices added
- Logs warnings instead of errors for missing devices
Technical fixes:
- Strip ANSI codes when calculating text width for alignment
- Use visible length for padding calculations in tables and boxes
- Properly handle colored text in table cells and box lines
Breaking changes: None (backward compatible)
2025-10-19 15:08:30 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.log('Idle monitoring stopped');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Watch config file for changes and reload automatically
|
|
|
|
|
*/
|
|
|
|
|
private watchConfigFile(): void {
|
|
|
|
|
try {
|
|
|
|
|
// Use Deno's file watcher to monitor config file
|
|
|
|
|
const configDir = path.dirname(this.CONFIG_PATH);
|
|
|
|
|
|
|
|
|
|
// Spawn a background watcher (non-blocking)
|
|
|
|
|
(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const watcher = Deno.watchFs(configDir);
|
|
|
|
|
|
|
|
|
|
logger.log('Config file watcher started');
|
|
|
|
|
|
|
|
|
|
for await (const event of watcher) {
|
2026-02-20 11:51:59 +00:00
|
|
|
// Respond to modify events on config file
|
2026-04-14 14:27:29 +00:00
|
|
|
if (shouldReloadConfig(event)) {
|
feat(cli): add beautiful colored output and fix daemon exit bug
Major improvements:
- Created color theme system (ts/colors.ts) with ANSI colors
- Enhanced logger with colors, table formatting, and styled boxes
- Fixed daemon exit bug - now stays running when no UPS configured
- Added config hot-reload with file watcher for live updates
- Beautified CLI help output with color-coded commands
- Added showcase test demonstrating all output features
- Fixed ANSI code handling for perfect table/box alignment
Features:
- Color-coded messages (success=green, error=red, warning=yellow, info=cyan)
- Status symbols (●○◐◯ for running/stopped/starting/unknown)
- Battery level colors (green>60%, yellow 30-60%, red<30%)
- Table formatting with auto-sizing and column alignment
- Styled boxes (success, error, warning, info styles)
- Hot-reload: daemon watches config file and reloads automatically
- Idle mode: daemon stays alive when no devices, checks periodically
Daemon improvements:
- No longer exits when no UPS devices configured
- Enters idle monitoring loop waiting for config
- File watcher detects config changes in real-time
- Auto-reloads and starts monitoring when devices added
- Logs warnings instead of errors for missing devices
Technical fixes:
- Strip ANSI codes when calculating text width for alignment
- Use visible length for padding calculations in tables and boxes
- Properly handle colored text in table cells and box lines
Breaking changes: None (backward compatible)
2025-10-19 15:08:30 +00:00
|
|
|
logger.info('Config file changed, reloading...');
|
|
|
|
|
await this.reloadConfig();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 11:51:59 +00:00
|
|
|
// Detect pause file changes
|
2026-04-14 14:27:29 +00:00
|
|
|
if (shouldRefreshPauseState(event)) {
|
2026-02-20 11:51:59 +00:00
|
|
|
this.checkPauseState();
|
|
|
|
|
}
|
|
|
|
|
|
feat(cli): add beautiful colored output and fix daemon exit bug
Major improvements:
- Created color theme system (ts/colors.ts) with ANSI colors
- Enhanced logger with colors, table formatting, and styled boxes
- Fixed daemon exit bug - now stays running when no UPS configured
- Added config hot-reload with file watcher for live updates
- Beautified CLI help output with color-coded commands
- Added showcase test demonstrating all output features
- Fixed ANSI code handling for perfect table/box alignment
Features:
- Color-coded messages (success=green, error=red, warning=yellow, info=cyan)
- Status symbols (●○◐◯ for running/stopped/starting/unknown)
- Battery level colors (green>60%, yellow 30-60%, red<30%)
- Table formatting with auto-sizing and column alignment
- Styled boxes (success, error, warning, info styles)
- Hot-reload: daemon watches config file and reloads automatically
- Idle mode: daemon stays alive when no devices, checks periodically
Daemon improvements:
- No longer exits when no UPS devices configured
- Enters idle monitoring loop waiting for config
- File watcher detects config changes in real-time
- Auto-reloads and starts monitoring when devices added
- Logs warnings instead of errors for missing devices
Technical fixes:
- Strip ANSI codes when calculating text width for alignment
- Use visible length for padding calculations in tables and boxes
- Properly handle colored text in table cells and box lines
Breaking changes: None (backward compatible)
2025-10-19 15:08:30 +00:00
|
|
|
// Stop watching if daemon stopped
|
|
|
|
|
if (!this.isRunning) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Watcher error - not critical, just log it
|
|
|
|
|
logger.dim(
|
|
|
|
|
`Config watcher stopped: ${error instanceof Error ? error.message : String(error)}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// If we can't start the watcher, just log and continue
|
|
|
|
|
// The periodic check will still work
|
|
|
|
|
logger.dim('Could not start config file watcher, using periodic checks only');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Reload configuration and restart monitoring if needed
|
|
|
|
|
*/
|
|
|
|
|
private async reloadConfig(): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
const oldDeviceCount = this.config.upsDevices?.length || 0;
|
|
|
|
|
|
|
|
|
|
// Load the new configuration
|
|
|
|
|
await this.loadConfig();
|
2026-04-16 02:54:16 +00:00
|
|
|
this.thresholdState.clear();
|
|
|
|
|
this.groupStatus.clear();
|
feat(cli): add beautiful colored output and fix daemon exit bug
Major improvements:
- Created color theme system (ts/colors.ts) with ANSI colors
- Enhanced logger with colors, table formatting, and styled boxes
- Fixed daemon exit bug - now stays running when no UPS configured
- Added config hot-reload with file watcher for live updates
- Beautified CLI help output with color-coded commands
- Added showcase test demonstrating all output features
- Fixed ANSI code handling for perfect table/box alignment
Features:
- Color-coded messages (success=green, error=red, warning=yellow, info=cyan)
- Status symbols (●○◐◯ for running/stopped/starting/unknown)
- Battery level colors (green>60%, yellow 30-60%, red<30%)
- Table formatting with auto-sizing and column alignment
- Styled boxes (success, error, warning, info styles)
- Hot-reload: daemon watches config file and reloads automatically
- Idle mode: daemon stays alive when no devices, checks periodically
Daemon improvements:
- No longer exits when no UPS devices configured
- Enters idle monitoring loop waiting for config
- File watcher detects config changes in real-time
- Auto-reloads and starts monitoring when devices added
- Logs warnings instead of errors for missing devices
Technical fixes:
- Strip ANSI codes when calculating text width for alignment
- Use visible length for padding calculations in tables and boxes
- Properly handle colored text in table cells and box lines
Breaking changes: None (backward compatible)
2025-10-19 15:08:30 +00:00
|
|
|
const newDeviceCount = this.config.upsDevices?.length || 0;
|
|
|
|
|
|
2026-04-14 14:27:29 +00:00
|
|
|
const reloadSnapshot = analyzeConfigReload(oldDeviceCount, newDeviceCount);
|
|
|
|
|
logger.success(reloadSnapshot.message);
|
|
|
|
|
|
|
|
|
|
if (reloadSnapshot.shouldLogMonitoringStart) {
|
feat(cli): add beautiful colored output and fix daemon exit bug
Major improvements:
- Created color theme system (ts/colors.ts) with ANSI colors
- Enhanced logger with colors, table formatting, and styled boxes
- Fixed daemon exit bug - now stays running when no UPS configured
- Added config hot-reload with file watcher for live updates
- Beautified CLI help output with color-coded commands
- Added showcase test demonstrating all output features
- Fixed ANSI code handling for perfect table/box alignment
Features:
- Color-coded messages (success=green, error=red, warning=yellow, info=cyan)
- Status symbols (●○◐◯ for running/stopped/starting/unknown)
- Battery level colors (green>60%, yellow 30-60%, red<30%)
- Table formatting with auto-sizing and column alignment
- Styled boxes (success, error, warning, info styles)
- Hot-reload: daemon watches config file and reloads automatically
- Idle mode: daemon stays alive when no devices, checks periodically
Daemon improvements:
- No longer exits when no UPS devices configured
- Enters idle monitoring loop waiting for config
- File watcher detects config changes in real-time
- Auto-reloads and starts monitoring when devices added
- Logs warnings instead of errors for missing devices
Technical fixes:
- Strip ANSI codes when calculating text width for alignment
- Use visible length for padding calculations in tables and boxes
- Properly handle colored text in table cells and box lines
Breaking changes: None (backward compatible)
2025-10-19 15:08:30 +00:00
|
|
|
logger.info('Monitoring will start automatically...');
|
2026-04-14 14:27:29 +00:00
|
|
|
}
|
feat(cli): add beautiful colored output and fix daemon exit bug
Major improvements:
- Created color theme system (ts/colors.ts) with ANSI colors
- Enhanced logger with colors, table formatting, and styled boxes
- Fixed daemon exit bug - now stays running when no UPS configured
- Added config hot-reload with file watcher for live updates
- Beautified CLI help output with color-coded commands
- Added showcase test demonstrating all output features
- Fixed ANSI code handling for perfect table/box alignment
Features:
- Color-coded messages (success=green, error=red, warning=yellow, info=cyan)
- Status symbols (●○◐◯ for running/stopped/starting/unknown)
- Battery level colors (green>60%, yellow 30-60%, red<30%)
- Table formatting with auto-sizing and column alignment
- Styled boxes (success, error, warning, info styles)
- Hot-reload: daemon watches config file and reloads automatically
- Idle mode: daemon stays alive when no devices, checks periodically
Daemon improvements:
- No longer exits when no UPS devices configured
- Enters idle monitoring loop waiting for config
- File watcher detects config changes in real-time
- Auto-reloads and starts monitoring when devices added
- Logs warnings instead of errors for missing devices
Technical fixes:
- Strip ANSI codes when calculating text width for alignment
- Use visible length for padding calculations in tables and boxes
- Properly handle colored text in table cells and box lines
Breaking changes: None (backward compatible)
2025-10-19 15:08:30 +00:00
|
|
|
|
2026-04-14 14:27:29 +00:00
|
|
|
if (reloadSnapshot.shouldInitializeUpsStatus) {
|
feat(cli): add beautiful colored output and fix daemon exit bug
Major improvements:
- Created color theme system (ts/colors.ts) with ANSI colors
- Enhanced logger with colors, table formatting, and styled boxes
- Fixed daemon exit bug - now stays running when no UPS configured
- Added config hot-reload with file watcher for live updates
- Beautified CLI help output with color-coded commands
- Added showcase test demonstrating all output features
- Fixed ANSI code handling for perfect table/box alignment
Features:
- Color-coded messages (success=green, error=red, warning=yellow, info=cyan)
- Status symbols (●○◐◯ for running/stopped/starting/unknown)
- Battery level colors (green>60%, yellow 30-60%, red<30%)
- Table formatting with auto-sizing and column alignment
- Styled boxes (success, error, warning, info styles)
- Hot-reload: daemon watches config file and reloads automatically
- Idle mode: daemon stays alive when no devices, checks periodically
Daemon improvements:
- No longer exits when no UPS devices configured
- Enters idle monitoring loop waiting for config
- File watcher detects config changes in real-time
- Auto-reloads and starts monitoring when devices added
- Logs warnings instead of errors for missing devices
Technical fixes:
- Strip ANSI codes when calculating text width for alignment
- Use visible length for padding calculations in tables and boxes
- Properly handle colored text in table cells and box lines
Breaking changes: None (backward compatible)
2025-10-19 15:08:30 +00:00
|
|
|
// Reinitialize UPS status tracking
|
|
|
|
|
this.initializeUpsStatus();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.warn(
|
|
|
|
|
`Failed to reload config: ${error instanceof Error ? error.message : String(error)}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
/**
|
|
|
|
|
* Sleep for the specified milliseconds
|
|
|
|
|
*/
|
|
|
|
|
private sleep(ms: number): Promise<void> {
|
2025-10-19 13:14:18 +00:00
|
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
}
|