nupst/ts/daemon.ts
2025-03-25 09:06:23 +00:00

261 lines
8.5 KiB
TypeScript

import * as fs from 'fs';
import * as path from 'path';
import { NupstSnmp, type SnmpConfig } from './snmp.js';
/**
* Configuration interface for the daemon
*/
export interface NupstConfig {
/** SNMP configuration settings */
snmp: SnmpConfig;
/** Threshold settings for initiating shutdown */
thresholds: {
/** Shutdown when battery below this percentage */
battery: number;
/** Shutdown when runtime below this minutes */
runtime: number;
};
/** Check interval in milliseconds */
checkInterval: number;
}
/**
* 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 */
private readonly DEFAULT_CONFIG: NupstConfig = {
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
upsModel: 'cyberpower'
},
thresholds: {
battery: 60, // Shutdown when battery below 60%
runtime: 20, // Shutdown when runtime below 20 minutes
},
checkInterval: 30000, // Check every 30 seconds
};
private config: NupstConfig;
private snmp: NupstSnmp;
private isRunning: boolean = false;
/**
* Create a new daemon instance with the given SNMP manager
*/
constructor(snmp: NupstSnmp) {
this.snmp = snmp;
this.config = this.DEFAULT_CONFIG;
}
/**
* Load configuration from file
* @throws Error if configuration file doesn't exist
*/
public async loadConfig(): Promise<NupstConfig> {
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);
}
// Read and parse config
const configData = fs.readFileSync(this.CONFIG_PATH, 'utf8');
this.config = JSON.parse(configData);
return this.config;
} catch (error) {
if (error.message.includes('No configuration found')) {
throw error; // Re-throw the no configuration error
}
this.logConfigError(`Error loading configuration: ${error.message}`);
throw new Error('Failed to load configuration');
}
}
/**
* Save configuration to file
*/
public async saveConfig(config: NupstConfig): Promise<void> {
try {
const configDir = path.dirname(this.CONFIG_PATH);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(config, null, 2));
this.config = config;
console.log('┌─ Configuration Saved ─────────────────────┐');
console.log(`│ Location: ${this.CONFIG_PATH}`);
console.log('└──────────────────────────────────────────┘');
} catch (error) {
console.error('Error saving configuration:', error);
}
}
/**
* Helper method to log configuration errors consistently
*/
private logConfigError(message: string): void {
console.error('┌─ Configuration Error ─────────────────────┐');
console.error(`${message}`);
console.error('│ Please run \'nupst setup\' first to create a configuration.');
console.error('└──────────────────────────────────────────┘');
}
/**
* Get the current configuration
*/
public getConfig(): NupstConfig {
return this.config;
}
/**
* Get the SNMP instance
*/
public getNupstSnmp(): NupstSnmp {
return this.snmp;
}
/**
* Start the monitoring daemon
*/
public async start(): Promise<void> {
if (this.isRunning) {
console.log('Daemon is already running');
return;
}
console.log('Starting NUPST daemon...');
try {
// Load configuration - this will throw an error if config doesn't exist
await this.loadConfig();
this.logConfigLoaded();
// Start UPS monitoring
this.isRunning = true;
await this.monitor();
} catch (error) {
this.isRunning = false;
console.error(`Daemon failed to start: ${error.message}`);
process.exit(1); // Exit with error
}
}
/**
* Log the loaded configuration settings
*/
private logConfigLoaded(): void {
console.log('┌─ Configuration Loaded ─────────────────────┐');
console.log('│ SNMP Settings:');
console.log(`│ Host: ${this.config.snmp.host}`);
console.log(`│ Port: ${this.config.snmp.port}`);
console.log(`│ Version: ${this.config.snmp.version}`);
console.log('│ Thresholds:');
console.log(`│ Battery: ${this.config.thresholds.battery}%`);
console.log(`│ Runtime: ${this.config.thresholds.runtime} minutes`);
console.log(`│ Check Interval: ${this.config.checkInterval / 1000} seconds`);
console.log('└──────────────────────────────────────────┘');
}
/**
* Stop the monitoring daemon
*/
public stop(): void {
console.log('Stopping NUPST daemon...');
this.isRunning = false;
}
/**
* Monitor the UPS status and trigger shutdown when necessary
*/
private async monitor(): Promise<void> {
console.log('Starting UPS monitoring...');
let lastStatus: 'online' | 'onBattery' | 'unknown' = 'unknown';
// Monitor continuously
while (this.isRunning) {
try {
const status = await this.snmp.getUpsStatus(this.config.snmp);
// Log status changes
if (status.powerStatus !== lastStatus) {
console.log('┌──────────────────────────────────────────┐');
console.log(`│ Power status changed: ${lastStatus}${status.powerStatus}`);
console.log('└──────────────────────────────────────────┘');
lastStatus = status.powerStatus;
}
// Handle battery power status
if (status.powerStatus === 'onBattery') {
await this.handleOnBatteryStatus(status);
}
// Wait before next check
await this.sleep(this.config.checkInterval);
} catch (error) {
console.error('Error during UPS monitoring:', error);
await this.sleep(this.config.checkInterval);
}
}
console.log('UPS monitoring stopped');
}
/**
* Handle UPS status when running on battery
*/
private async handleOnBatteryStatus(status: {
powerStatus: string,
batteryCapacity: number,
batteryRuntime: number
}): Promise<void> {
console.log('┌─ UPS Status ───────────────────────────────┐');
console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min │`);
console.log('└──────────────────────────────────────────┘');
// Check battery threshold
if (status.batteryCapacity < this.config.thresholds.battery) {
console.log('⚠️ WARNING: Battery capacity below threshold');
console.log(`Current: ${status.batteryCapacity}% | Threshold: ${this.config.thresholds.battery}%`);
await this.snmp.initiateShutdown('Battery capacity below threshold');
return;
}
// Check runtime threshold
if (status.batteryRuntime < this.config.thresholds.runtime) {
console.log('⚠️ WARNING: Runtime below threshold');
console.log(`Current: ${status.batteryRuntime} min | Threshold: ${this.config.thresholds.runtime} min`);
await this.snmp.initiateShutdown('Runtime below threshold');
return;
}
}
/**
* Sleep for the specified milliseconds
*/
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}