261 lines
8.5 KiB
TypeScript
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));
|
|
}
|
|
} |