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 { 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 { 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 { 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 { 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 { 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 { return new Promise(resolve => setTimeout(resolve, ms)); } }