- Remove call to gatherThresholdSettings in runAddProcess - Delete entire gatherThresholdSettings method - Thresholds are now configured per-action in gatherActionSettings Fixes: Cannot read properties of undefined (reading 'battery')
		
			
				
	
	
		
			1135 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			1135 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import process from 'node:process';
 | |
| import { execSync } from 'node:child_process';
 | |
| import { Nupst } from '../nupst.ts';
 | |
| import { logger, type ITableColumn } from '../logger.ts';
 | |
| import { theme } from '../colors.ts';
 | |
| import * as helpers from '../helpers/index.ts';
 | |
| import type { TUpsModel } from '../snmp/types.ts';
 | |
| import type { INupstConfig } from '../daemon.ts';
 | |
| 
 | |
| /**
 | |
|  * Class for handling UPS-related CLI commands
 | |
|  * Provides interface for managing UPS devices
 | |
|  */
 | |
| export class UpsHandler {
 | |
|   private readonly nupst: Nupst;
 | |
| 
 | |
|   /**
 | |
|    * Create a new UPS handler
 | |
|    * @param nupst Reference to the main Nupst instance
 | |
|    */
 | |
|   constructor(nupst: Nupst) {
 | |
|     this.nupst = nupst;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Add a new UPS configuration
 | |
|    */
 | |
|   public async add(): Promise<void> {
 | |
|     try {
 | |
|       // Import readline module for user input
 | |
|       const readline = await import('node:readline');
 | |
| 
 | |
|       const rl = readline.createInterface({
 | |
|         input: process.stdin,
 | |
|         output: process.stdout,
 | |
|       });
 | |
| 
 | |
|       // Helper function to prompt for input
 | |
|       const prompt = (question: string): Promise<string> => {
 | |
|         return new Promise((resolve) => {
 | |
|           rl.question(question, (answer: string) => {
 | |
|             resolve(answer);
 | |
|           });
 | |
|         });
 | |
|       };
 | |
| 
 | |
|       try {
 | |
|         await this.runAddProcess(prompt);
 | |
|       } finally {
 | |
|         rl.close();
 | |
|         process.stdin.destroy();
 | |
|       }
 | |
|     } catch (error) {
 | |
|       logger.error(`Add UPS error: ${error instanceof Error ? error.message : String(error)}`);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Run the interactive process to add a new UPS
 | |
|    * @param prompt Function to prompt for user input
 | |
|    */
 | |
|   public async runAddProcess(prompt: (question: string) => Promise<string>): Promise<void> {
 | |
|     logger.log('\nNUPST Add UPS');
 | |
|     logger.log('=============\n');
 | |
|     logger.log('This will guide you through configuring a new UPS.\n');
 | |
| 
 | |
|     // Try to load existing config if available
 | |
|     let config;
 | |
|     try {
 | |
|       await this.nupst.getDaemon().loadConfig();
 | |
|       config = this.nupst.getDaemon().getConfig();
 | |
| 
 | |
|       // Convert old format to new format if needed
 | |
|       if (!config.upsDevices) {
 | |
|         // Initialize with the current config as the first UPS
 | |
|         config = {
 | |
|           checkInterval: config.checkInterval,
 | |
|           upsDevices: [{
 | |
|             id: 'default',
 | |
|         name: 'Default UPS',
 | |
|         snmp: config.snmp,
 | |
|         groups: [],
 | |
|         actions: [],
 | |
|           }],
 | |
|           groups: [],
 | |
|         };
 | |
|         logger.log('Converting existing configuration to multi-UPS format.');
 | |
|       }
 | |
|     } catch (error) {
 | |
|       // If config doesn't exist, initialize with empty config
 | |
|       config = {
 | |
|         checkInterval: 30000, // Default check interval
 | |
|         upsDevices: [],
 | |
|         groups: [],
 | |
|       };
 | |
|       logger.log('No existing configuration found. Creating a new configuration.');
 | |
|     }
 | |
| 
 | |
|     // Get UPS ID and name
 | |
|     const upsId = helpers.shortId();
 | |
|     const name = await prompt('UPS Name: ');
 | |
| 
 | |
|     // Create a new UPS configuration object with defaults
 | |
|     const newUps = {
 | |
|       id: upsId,
 | |
|       name: name || `UPS-${upsId}`,
 | |
|       snmp: {
 | |
|         host: '127.0.0.1',
 | |
|         port: 161,
 | |
|         community: 'public',
 | |
|         version: 1,
 | |
|         timeout: 5000,
 | |
|         upsModel: 'cyberpower' as TUpsModel,
 | |
|       },
 | |
|       thresholds: {
 | |
|         battery: 60,
 | |
|         runtime: 20,
 | |
|       },
 | |
|       groups: [],
 | |
|       actions: [],
 | |
|     };
 | |
| 
 | |
|     // Gather SNMP settings
 | |
|     await this.gatherSnmpSettings(newUps.snmp, prompt);
 | |
| 
 | |
|     // Gather UPS model settings
 | |
|     await this.gatherUpsModelSettings(newUps.snmp, prompt);
 | |
| 
 | |
|     // Get access to GroupHandler for group assignments
 | |
|     const groupHandler = this.nupst.getGroupHandler();
 | |
| 
 | |
|     // Assign to groups if any exist
 | |
|     if (config.groups && config.groups.length > 0) {
 | |
|       await groupHandler.assignUpsToGroups(newUps, config.groups, prompt);
 | |
|     }
 | |
| 
 | |
| // Gather action settings
 | |
|     await this.gatherActionSettings(newUps.actions, prompt);
 | |
| 
 | |
|     // Add the new UPS to the config
 | |
|     config.upsDevices.push(newUps);
 | |
| 
 | |
|     // Save the configuration
 | |
|     await this.nupst.getDaemon().saveConfig(config as INupstConfig);
 | |
| 
 | |
|     this.displayUpsConfigSummary(newUps);
 | |
| 
 | |
|     // Test the connection if requested
 | |
|     await this.optionallyTestConnection(newUps.snmp, prompt);
 | |
| 
 | |
|     // Check if service is running and restart it if needed
 | |
|     await this.restartServiceIfRunning();
 | |
| 
 | |
|     logger.log('\nSetup complete!');
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Edit an existing UPS configuration
 | |
|    * @param upsId ID of the UPS to edit (undefined for default UPS)
 | |
|    */
 | |
|   public async edit(upsId?: string): Promise<void> {
 | |
|     try {
 | |
|       // Import readline module for user input
 | |
|       const readline = await import('node:readline');
 | |
| 
 | |
|       const rl = readline.createInterface({
 | |
|         input: process.stdin,
 | |
|         output: process.stdout,
 | |
|       });
 | |
| 
 | |
|       // Helper function to prompt for input
 | |
|       const prompt = (question: string): Promise<string> => {
 | |
|         return new Promise((resolve) => {
 | |
|           rl.question(question, (answer: string) => {
 | |
|             resolve(answer);
 | |
|           });
 | |
|         });
 | |
|       };
 | |
| 
 | |
|       try {
 | |
|         await this.runEditProcess(upsId, prompt);
 | |
|       } finally {
 | |
|         rl.close();
 | |
|         process.stdin.destroy();
 | |
|       }
 | |
|     } catch (error) {
 | |
|       logger.error(`Edit UPS error: ${error instanceof Error ? error.message : String(error)}`);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Run the interactive process to edit a UPS
 | |
|    * @param upsId ID of the UPS to edit (undefined for default UPS)
 | |
|    * @param prompt Function to prompt for user input
 | |
|    */
 | |
|   public async runEditProcess(
 | |
|     upsId: string | undefined,
 | |
|     prompt: (question: string) => Promise<string>,
 | |
|   ): Promise<void> {
 | |
|     logger.log('\nNUPST Edit UPS');
 | |
|     logger.log('=============\n');
 | |
| 
 | |
|     // Try to load existing config
 | |
|     try {
 | |
|       await this.nupst.getDaemon().loadConfig();
 | |
|     } catch (error) {
 | |
|       if (!upsId) {
 | |
|         // For default UPS (no ID specified), run setup if no config exists
 | |
|         logger.log('No existing configuration found. Running setup for new UPS.');
 | |
|         await this.runAddProcess(prompt);
 | |
|         return;
 | |
|       } else {
 | |
|         // For specific UPS ID, error if config doesn't exist
 | |
|         logger.error('No configuration found. Please run "nupst setup" first.');
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Get the config
 | |
|     const config = this.nupst.getDaemon().getConfig();
 | |
| 
 | |
|     // Convert old format to new format if needed
 | |
|     if (!config.upsDevices) {
 | |
|       // Initialize with the current config as the first UPS
 | |
|       if (!config.snmp) {
 | |
|         logger.error('Legacy configuration is missing required SNMP settings');
 | |
|         return;
 | |
|       }
 | |
|       config.upsDevices = [{
 | |
|         id: 'default',
 | |
|         name: 'Default UPS',
 | |
|         snmp: config.snmp,
 | |
|         groups: [],
 | |
|         actions: [],
 | |
|       }];
 | |
|       config.groups = [];
 | |
|       logger.log('Converting existing configuration to multi-UPS format.');
 | |
|     }
 | |
| 
 | |
|     // Find the UPS to edit
 | |
|     let upsToEdit;
 | |
|     if (upsId) {
 | |
|       // Find specific UPS by ID
 | |
|       upsToEdit = config.upsDevices.find((ups) => ups.id === upsId);
 | |
|       if (!upsToEdit) {
 | |
|         logger.error(`UPS with ID "${upsId}" not found.`);
 | |
|         return;
 | |
|       }
 | |
|       logger.log(`Editing UPS: ${upsToEdit.name} (${upsToEdit.id})\n`);
 | |
|     } else {
 | |
|       // For backward compatibility, edit the first UPS if no ID specified
 | |
|       if (config.upsDevices.length === 0) {
 | |
|         logger.error('No UPS devices configured. Please run "nupst add" to add a UPS.');
 | |
|         return;
 | |
|       }
 | |
|       upsToEdit = config.upsDevices[0];
 | |
|       logger.log(`Editing default UPS: ${upsToEdit.name} (${upsToEdit.id})\n`);
 | |
|     }
 | |
| 
 | |
|     // Allow editing UPS name
 | |
|     const newName = await prompt(`UPS Name [${upsToEdit.name}]: `);
 | |
|     if (newName.trim()) {
 | |
|       upsToEdit.name = newName;
 | |
|     }
 | |
| 
 | |
|     // Edit SNMP settings
 | |
|     await this.gatherSnmpSettings(upsToEdit.snmp, prompt);
 | |
| 
 | |
|     // Edit UPS model settings
 | |
|     await this.gatherUpsModelSettings(upsToEdit.snmp, prompt);
 | |
| 
 | |
|     // Get access to GroupHandler for group assignments
 | |
|     const groupHandler = this.nupst.getGroupHandler();
 | |
| 
 | |
|     // Edit group assignments
 | |
|     if (config.groups && config.groups.length > 0) {
 | |
|       await groupHandler.assignUpsToGroups(upsToEdit, config.groups, prompt);
 | |
|     }
 | |
| 
 | |
|     // Initialize actions array if not exists
 | |
|     if (!upsToEdit.actions) {
 | |
|       upsToEdit.actions = [];
 | |
|     }
 | |
| 
 | |
|     // Edit action settings
 | |
|     await this.gatherActionSettings(upsToEdit.actions, prompt);
 | |
| 
 | |
|     // Save the configuration
 | |
|     await this.nupst.getDaemon().saveConfig(config);
 | |
| 
 | |
|     this.displayUpsConfigSummary(upsToEdit);
 | |
| 
 | |
|     // Test the connection if requested
 | |
|     await this.optionallyTestConnection(upsToEdit.snmp, prompt);
 | |
| 
 | |
|     // Check if service is running and restart it if needed
 | |
|     await this.restartServiceIfRunning();
 | |
| 
 | |
|     logger.log('\nEdit complete!');
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Delete a UPS by ID
 | |
|    * @param upsId ID of the UPS to delete
 | |
|    */
 | |
|   public async remove(upsId: string): Promise<void> {
 | |
|     try {
 | |
|       // Try to load configuration
 | |
|       try {
 | |
|         await this.nupst.getDaemon().loadConfig();
 | |
|       } catch (error) {
 | |
|         const errorBoxWidth = 45;
 | |
|         logger.logBoxTitle('Configuration Error', errorBoxWidth);
 | |
|         logger.logBoxLine('No configuration found.');
 | |
|         logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
 | |
|         logger.logBoxEnd();
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // Get current configuration
 | |
|       const config = this.nupst.getDaemon().getConfig();
 | |
| 
 | |
|       // Check if multi-UPS config
 | |
|       if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
 | |
|         logger.error('Legacy single-UPS configuration detected. Cannot delete UPS.');
 | |
|         logger.log('Use "nupst add" to migrate to multi-UPS configuration format first.');
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // Find the UPS to delete
 | |
|       const upsIndex = config.upsDevices.findIndex((ups) => ups.id === upsId);
 | |
|       if (upsIndex === -1) {
 | |
|         logger.error(`UPS with ID "${upsId}" not found.`);
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       const upsToDelete = config.upsDevices[upsIndex];
 | |
| 
 | |
|       // Get confirmation before deleting
 | |
|       const readline = await import('node:readline');
 | |
|       const rl = readline.createInterface({
 | |
|         input: process.stdin,
 | |
|         output: process.stdout,
 | |
|       });
 | |
| 
 | |
|       const confirm = await new Promise<string>((resolve) => {
 | |
|         rl.question(
 | |
|           `Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `,
 | |
|           (answer) => {
 | |
|             resolve(answer.toLowerCase());
 | |
|           },
 | |
|         );
 | |
|       });
 | |
| 
 | |
|       rl.close();
 | |
|       process.stdin.destroy();
 | |
| 
 | |
|       if (confirm !== 'y' && confirm !== 'yes') {
 | |
|         logger.log('Deletion cancelled.');
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // Remove the UPS from the array
 | |
|       config.upsDevices.splice(upsIndex, 1);
 | |
| 
 | |
|       // Save the configuration
 | |
|       await this.nupst.getDaemon().saveConfig(config);
 | |
| 
 | |
|       logger.log(`UPS "${upsToDelete.name}" (${upsId}) has been deleted.`);
 | |
| 
 | |
|       // Check if service is running and restart it if needed
 | |
|       await this.restartServiceIfRunning();
 | |
|     } catch (error) {
 | |
|       logger.error(
 | |
|         `Failed to delete UPS: ${error instanceof Error ? error.message : String(error)}`,
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * List all configured UPS devices
 | |
|    */
 | |
|   public async list(): Promise<void> {
 | |
|     try {
 | |
|       // Try to load configuration
 | |
|       try {
 | |
|         await this.nupst.getDaemon().loadConfig();
 | |
|       } catch (error) {
 | |
|         logger.logBox('Configuration Error', [
 | |
|           'No configuration found.',
 | |
|           "Please run 'nupst ups add' first to create a configuration.",
 | |
|         ], 50, 'error');
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // Get current configuration
 | |
|       const config = this.nupst.getDaemon().getConfig();
 | |
| 
 | |
|       // Check if multi-UPS config
 | |
|       if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
 | |
|         // Legacy single UPS configuration
 | |
|         logger.logBox('UPS Devices', [
 | |
|           'Legacy single-UPS configuration detected.',
 | |
|           '',
 | |
|           ...(!config.snmp 
 | |
|             ? ['Error: Configuration missing SNMP settings']
 | |
|             : [
 | |
|                 'Default UPS:',
 | |
|                 `  Host: ${config.snmp.host}:${config.snmp.port}`,
 | |
|                 `  Model: ${config.snmp.upsModel || 'cyberpower'}`,
 | |
|                 '',
 | |
|                 'Use "nupst ups add" to add more UPS devices and migrate',
 | |
|                 'to the multi-UPS configuration format.',
 | |
|               ]
 | |
|           ),
 | |
|         ], 60, 'warning');
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // Display UPS list with modern table
 | |
|       if (config.upsDevices.length === 0) {
 | |
|         logger.logBox('UPS Devices', [
 | |
|           'No UPS devices configured.',
 | |
|           '',
 | |
|           `${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`,
 | |
|         ], 60, 'info');
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // Prepare table data
 | |
|       const rows = config.upsDevices.map((ups) => ({
 | |
|         id: ups.id,
 | |
|         name: ups.name || '',
 | |
|         host: `${ups.snmp.host}:${ups.snmp.port}`,
 | |
|         model: ups.snmp.upsModel || 'cyberpower',
 | |
|         groups: ups.groups.length > 0 ? ups.groups.join(', ') : theme.dim('None'),
 | |
|       }));
 | |
| 
 | |
|       const columns: ITableColumn[] = [
 | |
|         { header: 'ID', key: 'id', align: 'left', color: theme.highlight },
 | |
|         { header: 'Name', key: 'name', align: 'left' },
 | |
|         { header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
 | |
|         { header: 'Model', key: 'model', align: 'left' },
 | |
|         { header: 'Groups', key: 'groups', align: 'left' },
 | |
|       ];
 | |
| 
 | |
|       logger.log('');
 | |
|       logger.info(`UPS Devices (${config.upsDevices.length}):`);
 | |
|       logger.log('');
 | |
|       logger.logTable(columns, rows);
 | |
|       logger.log('');
 | |
|     } catch (error) {
 | |
|       logger.error(
 | |
|         `Failed to list UPS devices: ${error instanceof Error ? error.message : String(error)}`,
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Test the current configuration by connecting to the UPS
 | |
|    * @param debugMode Whether to enable debug mode
 | |
|    */
 | |
|   public async test(debugMode: boolean = false): Promise<void> {
 | |
|     try {
 | |
|       // Debug mode is now handled in parseAndExecute
 | |
|       if (debugMode) {
 | |
|         const boxWidth = 45;
 | |
|         logger.logBoxTitle('Debug Mode', boxWidth);
 | |
|         logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown');
 | |
|         logger.logBoxEnd();
 | |
|       }
 | |
| 
 | |
|       // Try to load the configuration
 | |
|       try {
 | |
|         await this.nupst.getDaemon().loadConfig();
 | |
|       } catch (error) {
 | |
|         const errorBoxWidth = 45;
 | |
|         logger.logBoxTitle('Configuration Error', errorBoxWidth);
 | |
|         logger.logBoxLine('No configuration found.');
 | |
|         logger.logBoxLine("Please run 'nupst setup' first to create a configuration.");
 | |
|         logger.logBoxEnd();
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // Get current configuration
 | |
|       const config = this.nupst.getDaemon().getConfig();
 | |
| 
 | |
|       // Handle new multi-UPS configuration format
 | |
|       if (config.upsDevices && config.upsDevices.length > 0) {
 | |
|         logger.log(`Found ${config.upsDevices.length} UPS devices in configuration.`);
 | |
| 
 | |
|         for (let i = 0; i < config.upsDevices.length; i++) {
 | |
|           const ups = config.upsDevices[i];
 | |
|           logger.log(`\nTesting UPS: ${ups.name} (${ups.id})`);
 | |
|           this.displayTestConfig(ups);
 | |
|           await this.testConnection(ups);
 | |
|         }
 | |
|       } else {
 | |
|         // Legacy configuration format
 | |
|         this.displayTestConfig(config);
 | |
|         await this.testConnection(config);
 | |
|       }
 | |
|     } catch (error) {
 | |
|       logger.error(`Test failed: ${error instanceof Error ? error.message : String(error)}`);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Display the configuration for testing
 | |
|    * @param config Current configuration or individual UPS configuration
 | |
|    */
 | |
|   private displayTestConfig(config: any): void {
 | |
|     // Check if this is a UPS device or full configuration
 | |
|     const isUpsConfig = config.snmp;
 | |
|     const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {};
 | |
|     const checkInterval = config.checkInterval || 30000;
 | |
| 
 | |
|     // Get UPS name and ID if available
 | |
|     const upsName = config.name ? config.name : 'Default UPS';
 | |
|     const upsId = config.id ? config.id : 'default';
 | |
| 
 | |
|     const boxWidth = 45;
 | |
|     logger.logBoxTitle(`Testing Configuration: ${upsName}`, boxWidth);
 | |
|     logger.logBoxLine(`UPS ID: ${upsId}`);
 | |
|     logger.logBoxLine('SNMP Settings:');
 | |
|     logger.logBoxLine(`  Host: ${snmpConfig.host}`);
 | |
|     logger.logBoxLine(`  Port: ${snmpConfig.port}`);
 | |
|     logger.logBoxLine(`  Version: ${snmpConfig.version}`);
 | |
|     logger.logBoxLine(`  UPS Model: ${snmpConfig.upsModel || 'cyberpower'}`);
 | |
| 
 | |
|     if (snmpConfig.version === 1 || snmpConfig.version === 2) {
 | |
|       logger.logBoxLine(`  Community: ${snmpConfig.community}`);
 | |
|     } else if (snmpConfig.version === 3) {
 | |
|       logger.logBoxLine(`  Security Level: ${snmpConfig.securityLevel}`);
 | |
|       logger.logBoxLine(`  Username: ${snmpConfig.username}`);
 | |
| 
 | |
|       // Show auth and privacy details based on security level
 | |
|       if (snmpConfig.securityLevel === 'authNoPriv' || snmpConfig.securityLevel === 'authPriv') {
 | |
|         logger.logBoxLine(`  Auth Protocol: ${snmpConfig.authProtocol || 'None'}`);
 | |
|       }
 | |
| 
 | |
|       if (snmpConfig.securityLevel === 'authPriv') {
 | |
|         logger.logBoxLine(`  Privacy Protocol: ${snmpConfig.privProtocol || 'None'}`);
 | |
|       }
 | |
| 
 | |
|       // Show timeout value
 | |
|       logger.logBoxLine(`  Timeout: ${snmpConfig.timeout / 1000} seconds`);
 | |
|     }
 | |
| 
 | |
|     // Show OIDs if custom model is selected
 | |
|     if (snmpConfig.upsModel === 'custom' && snmpConfig.customOIDs) {
 | |
|       logger.logBoxLine('Custom OIDs:');
 | |
|       logger.logBoxLine(`  Power Status: ${snmpConfig.customOIDs.POWER_STATUS || 'Not set'}`);
 | |
|       logger.logBoxLine(
 | |
|         `  Battery Capacity: ${snmpConfig.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
 | |
|       );
 | |
|       logger.logBoxLine(`  Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
 | |
|     }
 | |
|     // Show group assignments if this is a UPS config
 | |
|     if (config.groups && Array.isArray(config.groups)) {
 | |
|       logger.logBoxLine(
 | |
|         `Group Assignments: ${config.groups.length === 0 ? 'None' : config.groups.join(', ')}`,
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     logger.logBoxLine(`Check Interval: ${checkInterval / 1000} seconds`);
 | |
|     logger.logBoxEnd();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Test connection to the UPS
 | |
|    * @param config Current UPS configuration or legacy config
 | |
|    */
 | |
|   private async testConnection(config: any): Promise<void> {
 | |
|     const upsId = config.id || 'default';
 | |
|     const upsName = config.name || 'Default UPS';
 | |
|     logger.log(`\nTesting connection to UPS: ${upsName} (${upsId})...`);
 | |
| 
 | |
|     try {
 | |
|       // Create a test config with a short timeout
 | |
|       const snmpConfig = config.snmp ? config.snmp : config.snmp;
 | |
| 
 | |
|       const testConfig = {
 | |
|         ...snmpConfig,
 | |
|         timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing
 | |
|       };
 | |
| 
 | |
|       const status = await this.nupst.getSnmp().getUpsStatus(testConfig);
 | |
| 
 | |
|       const boxWidth = 45;
 | |
|       logger.logBoxTitle(`Connection Successful: ${upsName}`, boxWidth);
 | |
|       logger.logBoxLine('UPS Status:');
 | |
|       logger.logBoxLine(`  Power Status: ${status.powerStatus}`);
 | |
|       logger.logBoxLine(`  Battery Capacity: ${status.batteryCapacity}%`);
 | |
|       logger.logBoxLine(`  Runtime Remaining: ${status.batteryRuntime} minutes`);
 | |
|       logger.logBoxEnd();
 | |
| 
 | |
|       
 | |
|     } catch (error) {
 | |
|       const errorBoxWidth = 45;
 | |
|       logger.logBoxTitle(`Connection Failed: ${upsName}`, errorBoxWidth);
 | |
|       logger.logBoxLine(`Error: ${error instanceof Error ? error.message : String(error)}`);
 | |
|       logger.logBoxEnd();
 | |
|       logger.log("\nPlease check your settings and run 'nupst edit' to reconfigure this UPS.");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Analyze UPS status against thresholds
 | |
|    * @param status UPS status
 | |
|    * @param thresholds Threshold configuration
 | |
|    */
 | |
|   private analyzeThresholds(status: any, thresholds: any): void {
 | |
|     const boxWidth = 45;
 | |
|     logger.logBoxTitle('Threshold Analysis', boxWidth);
 | |
| 
 | |
|     if (status.batteryCapacity < thresholds.battery) {
 | |
|       logger.logBoxLine('⚠️ WARNING: Battery capacity below threshold');
 | |
|       logger.logBoxLine(
 | |
|         `  Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`,
 | |
|       );
 | |
|       logger.logBoxLine('  System would initiate shutdown');
 | |
|     } else {
 | |
|       logger.logBoxLine('✓ Battery capacity above threshold');
 | |
|       logger.logBoxLine(
 | |
|         `  Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`,
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (status.batteryRuntime < thresholds.runtime) {
 | |
|       logger.logBoxLine('⚠️ WARNING: Runtime below threshold');
 | |
|       logger.logBoxLine(
 | |
|         `  Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`,
 | |
|       );
 | |
|       logger.logBoxLine('  System would initiate shutdown');
 | |
|     } else {
 | |
|       logger.logBoxLine('✓ Runtime above threshold');
 | |
|       logger.logBoxLine(
 | |
|         `  Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`,
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     logger.logBoxEnd();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gather SNMP settings
 | |
|    * @param snmpConfig SNMP configuration object to update
 | |
|    * @param prompt Function to prompt for user input
 | |
|    */
 | |
|   private async gatherSnmpSettings(
 | |
|     snmpConfig: any,
 | |
|     prompt: (question: string) => Promise<string>,
 | |
|   ): Promise<void> {
 | |
|     // SNMP IP Address
 | |
|     const defaultHost = snmpConfig.host || '127.0.0.1';
 | |
|     const host = await prompt(`UPS IP Address [${defaultHost}]: `);
 | |
|     snmpConfig.host = host.trim() || defaultHost;
 | |
| 
 | |
|     // SNMP Port
 | |
|     const defaultPort = snmpConfig.port || 161;
 | |
|     const portInput = await prompt(`SNMP Port [${defaultPort}]: `);
 | |
|     const port = parseInt(portInput, 10);
 | |
|     snmpConfig.port = portInput.trim() && !isNaN(port) ? port : defaultPort;
 | |
| 
 | |
|     // SNMP Version
 | |
|     const defaultVersion = snmpConfig.version || 1;
 | |
|     logger.log('');
 | |
|     logger.info('SNMP Version:');
 | |
|     logger.dim('  1) SNMPv1');
 | |
|     logger.dim('  2) SNMPv2c');
 | |
|     logger.dim('  3) SNMPv3 (with security features)');
 | |
|     const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `);
 | |
|     const version = parseInt(versionInput, 10);
 | |
|     snmpConfig.version = versionInput.trim() && (version === 1 || version === 2 || version === 3)
 | |
|       ? version
 | |
|       : defaultVersion;
 | |
| 
 | |
|     if (snmpConfig.version === 1 || snmpConfig.version === 2) {
 | |
|       // SNMP Community String (for v1/v2c)
 | |
|       const defaultCommunity = snmpConfig.community || 'public';
 | |
|       const community = await prompt(`SNMP Community String [${defaultCommunity}]: `);
 | |
|       snmpConfig.community = community.trim() || defaultCommunity;
 | |
|     } else if (snmpConfig.version === 3) {
 | |
|       // SNMP v3 settings
 | |
|       await this.gatherSnmpV3Settings(snmpConfig, prompt);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gather SNMPv3 specific settings
 | |
|    * @param snmpConfig SNMP configuration object to update
 | |
|    * @param prompt Function to prompt for user input
 | |
|    */
 | |
|   private async gatherSnmpV3Settings(
 | |
|     snmpConfig: any,
 | |
|     prompt: (question: string) => Promise<string>,
 | |
|   ): Promise<void> {
 | |
|     logger.log('');
 | |
|     logger.info('SNMPv3 Security Settings:');
 | |
| 
 | |
|     // Security Level
 | |
|     logger.log('');
 | |
|     logger.info('Security Level:');
 | |
|     logger.dim('  1) noAuthNoPriv (No Authentication, No Privacy)');
 | |
|     logger.dim('  2) authNoPriv (Authentication, No Privacy)');
 | |
|     logger.dim('  3) authPriv (Authentication and Privacy)');
 | |
|     const defaultSecLevel = snmpConfig.securityLevel
 | |
|       ? snmpConfig.securityLevel === 'noAuthNoPriv'
 | |
|         ? 1
 | |
|         : snmpConfig.securityLevel === 'authNoPriv'
 | |
|         ? 2
 | |
|         : 3
 | |
|       : 3;
 | |
|     const secLevelInput = await prompt(`Select Security Level [${defaultSecLevel}]: `);
 | |
|     const secLevel = parseInt(secLevelInput, 10) || defaultSecLevel;
 | |
| 
 | |
|     if (secLevel === 1) {
 | |
|       snmpConfig.securityLevel = 'noAuthNoPriv';
 | |
|       // No auth, no priv - clear out authentication and privacy settings
 | |
|       snmpConfig.authProtocol = '';
 | |
|       snmpConfig.authKey = '';
 | |
|       snmpConfig.privProtocol = '';
 | |
|       snmpConfig.privKey = '';
 | |
|       // Set appropriate timeout for security level
 | |
|       snmpConfig.timeout = 5000; // 5 seconds for basic security
 | |
|     } else if (secLevel === 2) {
 | |
|       snmpConfig.securityLevel = 'authNoPriv';
 | |
|       // Auth, no priv - clear out privacy settings
 | |
|       snmpConfig.privProtocol = '';
 | |
|       snmpConfig.privKey = '';
 | |
|       // Set appropriate timeout for security level
 | |
|       snmpConfig.timeout = 10000; // 10 seconds for authentication
 | |
|     } else {
 | |
|       snmpConfig.securityLevel = 'authPriv';
 | |
|       // Set appropriate timeout for security level
 | |
|       snmpConfig.timeout = 15000; // 15 seconds for full encryption
 | |
|     }
 | |
| 
 | |
|     // Username
 | |
|     const defaultUsername = snmpConfig.username || '';
 | |
|     const username = await prompt(`SNMPv3 Username [${defaultUsername}]: `);
 | |
|     snmpConfig.username = username.trim() || defaultUsername;
 | |
| 
 | |
|     if (secLevel >= 2) {
 | |
|       // Authentication settings
 | |
|       await this.gatherAuthenticationSettings(snmpConfig, prompt);
 | |
| 
 | |
|       if (secLevel === 3) {
 | |
|         // Privacy settings
 | |
|         await this.gatherPrivacySettings(snmpConfig, prompt);
 | |
|       }
 | |
| 
 | |
|       // Allow customizing the timeout value
 | |
|       const defaultTimeout = snmpConfig.timeout / 1000; // Convert from ms to seconds for display
 | |
|       logger.log('');
 | |
|       logger.info(
 | |
|         'SNMPv3 operations with authentication and privacy may require longer timeouts.',
 | |
|       );
 | |
|       const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `);
 | |
|       const timeout = parseInt(timeoutInput, 10);
 | |
|       if (timeoutInput.trim() && !isNaN(timeout)) {
 | |
|         snmpConfig.timeout = timeout * 1000; // Convert to ms
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gather authentication settings for SNMPv3
 | |
|    * @param snmpConfig SNMP configuration object to update
 | |
|    * @param prompt Function to prompt for user input
 | |
|    */
 | |
|   private async gatherAuthenticationSettings(
 | |
|     snmpConfig: any,
 | |
|     prompt: (question: string) => Promise<string>,
 | |
|   ): Promise<void> {
 | |
|     // Authentication protocol
 | |
|     logger.log('');
 | |
|     logger.info('Authentication Protocol:');
 | |
|     logger.dim('  1) MD5');
 | |
|     logger.dim('  2) SHA');
 | |
|     const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1;
 | |
|     const authProtocolInput = await prompt(
 | |
|       `Select Authentication Protocol [${defaultAuthProtocol}]: `,
 | |
|     );
 | |
|     const authProtocol = parseInt(authProtocolInput, 10) || defaultAuthProtocol;
 | |
|     snmpConfig.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5';
 | |
| 
 | |
|     // Authentication Key/Password
 | |
|     const defaultAuthKey = snmpConfig.authKey || '';
 | |
|     const authKey = await prompt(`Authentication Password ${defaultAuthKey ? '[*****]' : ''}: `);
 | |
|     snmpConfig.authKey = authKey.trim() || defaultAuthKey;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gather privacy settings for SNMPv3
 | |
|    * @param snmpConfig SNMP configuration object to update
 | |
|    * @param prompt Function to prompt for user input
 | |
|    */
 | |
|   private async gatherPrivacySettings(
 | |
|     snmpConfig: any,
 | |
|     prompt: (question: string) => Promise<string>,
 | |
|   ): Promise<void> {
 | |
|     // Privacy protocol
 | |
|     logger.log('');
 | |
|     logger.info('Privacy Protocol:');
 | |
|     logger.dim('  1) DES');
 | |
|     logger.dim('  2) AES');
 | |
|     const defaultPrivProtocol = snmpConfig.privProtocol === 'AES' ? 2 : 1;
 | |
|     const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `);
 | |
|     const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol;
 | |
|     snmpConfig.privProtocol = privProtocol === 2 ? 'AES' : 'DES';
 | |
| 
 | |
|     // Privacy Key/Password
 | |
|     const defaultPrivKey = snmpConfig.privKey || '';
 | |
|     const privKey = await prompt(`Privacy Password ${defaultPrivKey ? '[*****]' : ''}: `);
 | |
|     snmpConfig.privKey = privKey.trim() || defaultPrivKey;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gather UPS model settings
 | |
|    * @param snmpConfig SNMP configuration object to update
 | |
|    * @param prompt Function to prompt for user input
 | |
|    */
 | |
|   private async gatherUpsModelSettings(
 | |
|     snmpConfig: any,
 | |
|     prompt: (question: string) => Promise<string>,
 | |
|   ): Promise<void> {
 | |
|     logger.log('');
 | |
|     logger.info('UPS Model Selection:');
 | |
|     logger.dim('  1) CyberPower');
 | |
|     logger.dim('  2) APC');
 | |
|     logger.dim('  3) Eaton');
 | |
|     logger.dim('  4) TrippLite');
 | |
|     logger.dim('  5) Liebert/Vertiv');
 | |
|     logger.dim('  6) Custom (Advanced)');
 | |
| 
 | |
|     const defaultModelValue = snmpConfig.upsModel === 'cyberpower'
 | |
|       ? 1
 | |
|       : snmpConfig.upsModel === 'apc'
 | |
|       ? 2
 | |
|       : snmpConfig.upsModel === 'eaton'
 | |
|       ? 3
 | |
|       : snmpConfig.upsModel === 'tripplite'
 | |
|       ? 4
 | |
|       : snmpConfig.upsModel === 'liebert'
 | |
|       ? 5
 | |
|       : snmpConfig.upsModel === 'custom'
 | |
|       ? 6
 | |
|       : 1;
 | |
| 
 | |
|     const modelInput = await prompt(`Select UPS model [${defaultModelValue}]: `);
 | |
|     const modelValue = parseInt(modelInput, 10) || defaultModelValue;
 | |
| 
 | |
|     if (modelValue === 1) {
 | |
|       snmpConfig.upsModel = 'cyberpower';
 | |
|     } else if (modelValue === 2) {
 | |
|       snmpConfig.upsModel = 'apc';
 | |
|     } else if (modelValue === 3) {
 | |
|       snmpConfig.upsModel = 'eaton';
 | |
|     } else if (modelValue === 4) {
 | |
|       snmpConfig.upsModel = 'tripplite';
 | |
|     } else if (modelValue === 5) {
 | |
|       snmpConfig.upsModel = 'liebert';
 | |
|     } else if (modelValue === 6) {
 | |
|       snmpConfig.upsModel = 'custom';
 | |
|       logger.log('');
 | |
|       logger.info('Enter custom OIDs for your UPS:');
 | |
|       logger.dim('(Leave blank to use standard RFC 1628 OIDs as fallback)');
 | |
| 
 | |
|       // Custom OIDs
 | |
|       const powerStatusOID = await prompt('Power Status OID: ');
 | |
|       const batteryCapacityOID = await prompt('Battery Capacity OID: ');
 | |
|       const batteryRuntimeOID = await prompt('Battery Runtime OID: ');
 | |
| 
 | |
|       // Create custom OIDs object
 | |
|       snmpConfig.customOIDs = {
 | |
|         POWER_STATUS: powerStatusOID.trim(),
 | |
|         BATTERY_CAPACITY: batteryCapacityOID.trim(),
 | |
|         BATTERY_RUNTIME: batteryRuntimeOID.trim(),
 | |
|       };
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Gather action configuration settings
 | |
|    * @param actions Actions array to configure
 | |
|    * @param prompt Function to prompt for user input
 | |
|    */
 | |
|   private async gatherActionSettings(
 | |
|     actions: any[],
 | |
|     prompt: (question: string) => Promise<string>,
 | |
|   ): Promise<void> {
 | |
|     logger.log('');
 | |
|     logger.info('Action Configuration (Optional):');
 | |
|     logger.dim('Actions are triggered on power status changes and threshold violations.');
 | |
|     logger.dim('Leave empty to use default shutdown behavior on threshold violations.');
 | |
| 
 | |
|     const configureActions = await prompt('Configure custom actions? (y/N): ');
 | |
|     if (configureActions.toLowerCase() !== 'y') {
 | |
|       return; // Keep existing actions or use default
 | |
|     }
 | |
| 
 | |
|     // Clear existing actions
 | |
|     actions.length = 0;
 | |
| 
 | |
|     let addMore = true;
 | |
|     while (addMore) {
 | |
|       logger.log('');
 | |
|       logger.info('Action Type:');
 | |
|       logger.dim('  1) Shutdown (system shutdown)');
 | |
|       logger.dim('  2) Webhook (HTTP notification)');
 | |
|       logger.dim('  3) Custom Script (run .sh file from /etc/nupst)');
 | |
| 
 | |
|       const typeInput = await prompt('Select action type [1]: ');
 | |
|       const typeValue = parseInt(typeInput, 10) || 1;
 | |
| 
 | |
|       const action: any = {};
 | |
| 
 | |
|       if (typeValue === 1) {
 | |
|         // Shutdown action
 | |
|         action.type = 'shutdown';
 | |
| 
 | |
|         const delayInput = await prompt('Shutdown delay in minutes [5]: ');
 | |
|         const delay = parseInt(delayInput, 10);
 | |
|         if (delayInput.trim() && !isNaN(delay)) {
 | |
|           action.shutdownDelay = delay;
 | |
|         }
 | |
|       } else if (typeValue === 2) {
 | |
|         // Webhook action
 | |
|         action.type = 'webhook';
 | |
| 
 | |
|         const url = await prompt('Webhook URL: ');
 | |
|         if (!url.trim()) {
 | |
|           logger.warn('Webhook URL required, skipping action');
 | |
|           continue;
 | |
|         }
 | |
|         action.webhookUrl = url.trim();
 | |
| 
 | |
|         logger.log('');
 | |
|         logger.info('HTTP Method:');
 | |
|         logger.dim('  1) POST (JSON body)');
 | |
|         logger.dim('  2) GET (query parameters)');
 | |
|         const methodInput = await prompt('Select method [1]: ');
 | |
|         action.webhookMethod = methodInput === '2' ? 'GET' : 'POST';
 | |
| 
 | |
|         const timeoutInput = await prompt('Timeout in seconds [10]: ');
 | |
|         const timeout = parseInt(timeoutInput, 10);
 | |
|         if (timeoutInput.trim() && !isNaN(timeout)) {
 | |
|           action.webhookTimeout = timeout * 1000; // Convert to ms
 | |
|         }
 | |
|       } else if (typeValue === 3) {
 | |
|         // Script action
 | |
|         action.type = 'script';
 | |
| 
 | |
|         const scriptPath = await prompt('Script filename (in /etc/nupst/, must end with .sh): ');
 | |
|         if (!scriptPath.trim() || !scriptPath.trim().endsWith('.sh')) {
 | |
|           logger.warn('Script path must end with .sh, skipping action');
 | |
|           continue;
 | |
|         }
 | |
|         action.scriptPath = scriptPath.trim();
 | |
| 
 | |
|         const timeoutInput = await prompt('Script timeout in seconds [60]: ');
 | |
|         const timeout = parseInt(timeoutInput, 10);
 | |
|         if (timeoutInput.trim() && !isNaN(timeout)) {
 | |
|           action.scriptTimeout = timeout * 1000; // Convert to ms
 | |
|         }
 | |
|       } else {
 | |
|         logger.warn('Invalid action type, skipping');
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       // Configure trigger mode (applies to all action types)
 | |
|       logger.log('');
 | |
|       logger.info('Trigger Mode:');
 | |
|       logger.dim('  1) Power changes + thresholds (default)');
 | |
|       logger.dim('  2) Only power status changes');
 | |
|       logger.dim('  3) Only threshold violations');
 | |
|       logger.dim('  4) Any change (every ~30s check)');
 | |
|       const triggerInput = await prompt('Select trigger mode [1]: ');
 | |
|       const triggerValue = parseInt(triggerInput, 10) || 1;
 | |
|       
 | |
|       switch (triggerValue) {
 | |
|         case 2:
 | |
|           action.triggerMode = 'onlyPowerChanges';
 | |
|           break;
 | |
|         case 3:
 | |
|           action.triggerMode = 'onlyThresholds';
 | |
|           break;
 | |
|         case 4:
 | |
|           action.triggerMode = 'anyChange';
 | |
|           break;
 | |
|         default:
 | |
|           action.triggerMode = 'powerChangesAndThresholds';
 | |
|       }
 | |
| 
 | |
|       // Configure thresholds if needed for onlyThresholds or powerChangesAndThresholds modes
 | |
|       if (action.triggerMode === 'onlyThresholds' || action.triggerMode === 'powerChangesAndThresholds') {
 | |
|         logger.log('');
 | |
|         logger.info('Action Thresholds:');
 | |
|         logger.dim('Action will trigger when battery or runtime falls below these values (while on battery)');
 | |
|         
 | |
|         const batteryInput = await prompt('Battery threshold percentage [60]: ');
 | |
|         const battery = parseInt(batteryInput, 10);
 | |
|         const batteryThreshold = (batteryInput.trim() && !isNaN(battery)) ? battery : 60;
 | |
| 
 | |
|         const runtimeInput = await prompt('Runtime threshold in minutes [20]: ');
 | |
|         const runtime = parseInt(runtimeInput, 10);
 | |
|         const runtimeThreshold = (runtimeInput.trim() && !isNaN(runtime)) ? runtime : 20;
 | |
| 
 | |
|         action.thresholds = {
 | |
|           battery: batteryThreshold,
 | |
|           runtime: runtimeThreshold,
 | |
|         };
 | |
|       }
 | |
| 
 | |
|       actions.push(action);
 | |
|       logger.success(`${action.type.charAt(0).toUpperCase() + action.type.slice(1)} action added (mode: ${action.triggerMode || 'powerChangesAndThresholds'})`);
 | |
| 
 | |
|       const more = await prompt('Add another action? (y/N): ');
 | |
|       addMore = more.toLowerCase() === 'y';
 | |
|     }
 | |
| 
 | |
|     if (actions.length > 0) {
 | |
|       logger.log('');
 | |
|       logger.success(`${actions.length} action(s) configured`);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Display UPS configuration summary
 | |
|    * @param ups UPS configuration
 | |
|    */
 | |
|   private displayUpsConfigSummary(ups: any): void {
 | |
|     const boxWidth = 45;
 | |
|     logger.log('');
 | |
|     logger.logBoxTitle(`UPS Configuration: ${ups.name}`, boxWidth);
 | |
|     logger.logBoxLine(`UPS ID: ${ups.id}`);
 | |
|     logger.logBoxLine(`SNMP Host: ${ups.snmp.host}:${ups.snmp.port}`);
 | |
|     logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`);
 | |
|     logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`);
 | |
|     
 | |
|     if (ups.groups && ups.groups.length > 0) {
 | |
|       logger.logBoxLine(`Groups: ${ups.groups.join(', ')}`);
 | |
|     } else {
 | |
|       logger.logBoxLine('Groups: None');
 | |
|     }
 | |
|     logger.logBoxEnd();
 | |
|     logger.log('');
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Optionally test connection to UPS
 | |
|    * @param snmpConfig SNMP configuration to test
 | |
|    * @param prompt Function to prompt for user input
 | |
|    */
 | |
|   private async optionallyTestConnection(
 | |
|     snmpConfig: any,
 | |
|     prompt: (question: string) => Promise<string>,
 | |
|   ): Promise<void> {
 | |
|     const testConnection = await prompt(
 | |
|       'Would you like to test the connection to your UPS? (y/N): ',
 | |
|     );
 | |
|     if (testConnection.toLowerCase() === 'y') {
 | |
|       logger.log('\nTesting connection to UPS...');
 | |
|       try {
 | |
|         // Create a test config with a short timeout
 | |
|         const testConfig = {
 | |
|           ...snmpConfig,
 | |
|           timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing
 | |
|         };
 | |
| 
 | |
|         const status = await this.nupst.getSnmp().getUpsStatus(testConfig);
 | |
|         const boxWidth = 45;
 | |
|         logger.log('');
 | |
|         logger.logBoxTitle('Connection Successful!', boxWidth);
 | |
|         logger.logBoxLine('UPS Status:');
 | |
|         logger.logBoxLine(`✓ Power Status: ${status.powerStatus}`);
 | |
|         logger.logBoxLine(`✓ Battery Capacity: ${status.batteryCapacity}%`);
 | |
|         logger.logBoxLine(`✓ Runtime Remaining: ${status.batteryRuntime} minutes`);
 | |
|         logger.logBoxEnd();
 | |
|       } catch (error) {
 | |
|         const errorBoxWidth = 45;
 | |
|         logger.log('');
 | |
|         logger.logBoxTitle('Connection Failed!', errorBoxWidth);
 | |
|         logger.logBoxLine(`Error: ${error instanceof Error ? error.message : String(error)}`);
 | |
|         logger.logBoxEnd();
 | |
|         logger.log('\nPlease check your settings and try again.');
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Check if the systemd service is running and restart it if it is
 | |
|    * This is useful after configuration changes
 | |
|    */
 | |
|   public restartServiceIfRunning(): void {
 | |
|     try {
 | |
|       // Check if the service is active
 | |
|       const isActive =
 | |
|         execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
 | |
| 
 | |
|       if (isActive) {
 | |
|         // Service is running, restart it
 | |
|         const boxWidth = 45;
 | |
|         logger.logBoxTitle('Service Update', boxWidth);
 | |
|         logger.logBoxLine('Configuration has changed.');
 | |
|         logger.logBoxLine('Restarting NUPST service to apply changes...');
 | |
| 
 | |
|         try {
 | |
|           if (process.getuid && process.getuid() === 0) {
 | |
|             // We have root access, restart directly
 | |
|             execSync('systemctl restart nupst.service');
 | |
|             logger.logBoxLine('Service restarted successfully.');
 | |
|           } else {
 | |
|             // No root access, show instructions
 | |
|             logger.logBoxLine('Please restart the service with:');
 | |
|             logger.logBoxLine('  sudo systemctl restart nupst.service');
 | |
|           }
 | |
|         } catch (error) {
 | |
|           logger.logBoxLine(
 | |
|             `Error restarting service: ${error instanceof Error ? error.message : String(error)}`,
 | |
|           );
 | |
|           logger.logBoxLine('You may need to restart the service manually:');
 | |
|           logger.logBoxLine('  sudo systemctl restart nupst.service');
 | |
|         }
 | |
| 
 | |
|         logger.logBoxEnd();
 | |
|       }
 | |
|     } catch (error) {
 | |
|       // Ignore errors checking service status
 | |
|     }
 | |
|   }
 | |
| }
 |