style: configure deno fmt to use single quotes
- Add singleQuote: true to deno.json fmt configuration - Reformat all files with single quotes using deno fmt
This commit is contained in:
@@ -4,5 +4,5 @@
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/nupst',
|
||||
version: '3.1.2',
|
||||
description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices'
|
||||
}
|
||||
description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices',
|
||||
};
|
||||
|
53
ts/cli.ts
53
ts/cli.ts
@@ -1,4 +1,4 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { execSync } from 'node:child_process';
|
||||
import { Nupst } from './nupst.ts';
|
||||
import { logger } from './logger.ts';
|
||||
|
||||
@@ -62,7 +62,11 @@ export class NupstCli {
|
||||
* @param commandArgs Additional command arguments
|
||||
* @param debugMode Whether debug mode is enabled
|
||||
*/
|
||||
private async executeCommand(command: string, commandArgs: string[], debugMode: boolean): Promise<void> {
|
||||
private async executeCommand(
|
||||
command: string,
|
||||
commandArgs: string[],
|
||||
debugMode: boolean,
|
||||
): Promise<void> {
|
||||
// Get access to the handlers
|
||||
const upsHandler = this.nupst.getUpsHandler();
|
||||
const groupHandler = this.nupst.getGroupHandler();
|
||||
@@ -87,7 +91,7 @@ export class NupstCli {
|
||||
break;
|
||||
case 'restart':
|
||||
await serviceHandler.stop();
|
||||
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 2s
|
||||
await serviceHandler.start();
|
||||
break;
|
||||
case 'status':
|
||||
@@ -263,7 +267,9 @@ export class NupstCli {
|
||||
await serviceHandler.logs();
|
||||
break;
|
||||
case 'daemon-start':
|
||||
logger.log("Note: 'nupst daemon-start' is deprecated. Use 'nupst service start-daemon' instead.");
|
||||
logger.log(
|
||||
"Note: 'nupst daemon-start' is deprecated. Use 'nupst service start-daemon' instead.",
|
||||
);
|
||||
await serviceHandler.daemonStart(debugMode);
|
||||
break;
|
||||
|
||||
@@ -328,8 +334,12 @@ export class NupstCli {
|
||||
logger.logBoxLine(`${ups.name} (${ups.id}):`);
|
||||
logger.logBoxLine(` Host: ${ups.snmp.host}:${ups.snmp.port}`);
|
||||
logger.logBoxLine(` Model: ${ups.snmp.upsModel}`);
|
||||
logger.logBoxLine(` Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`);
|
||||
logger.logBoxLine(` Groups: ${ups.groups.length > 0 ? ups.groups.join(', ') : 'None'}`);
|
||||
logger.logBoxLine(
|
||||
` Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`,
|
||||
);
|
||||
logger.logBoxLine(
|
||||
` Groups: ${ups.groups.length > 0 ? ups.groups.join(', ') : 'None'}`,
|
||||
);
|
||||
logger.logBoxLine('');
|
||||
}
|
||||
logger.logBoxEnd();
|
||||
@@ -344,11 +354,16 @@ export class NupstCli {
|
||||
if (group.description) {
|
||||
logger.logBoxLine(` Description: ${group.description}`);
|
||||
}
|
||||
|
||||
|
||||
// List UPS devices in this group
|
||||
const upsInGroup = config.upsDevices.filter(ups => ups.groups && ups.groups.includes(group.id));
|
||||
logger.logBoxLine(` UPS Devices: ${upsInGroup.length > 0 ?
|
||||
upsInGroup.map(ups => ups.name).join(', ') : 'None'}`);
|
||||
const upsInGroup = config.upsDevices.filter((ups) =>
|
||||
ups.groups && ups.groups.includes(group.id)
|
||||
);
|
||||
logger.logBoxLine(
|
||||
` UPS Devices: ${
|
||||
upsInGroup.length > 0 ? upsInGroup.map((ups) => ups.name).join(', ') : 'None'
|
||||
}`,
|
||||
);
|
||||
logger.logBoxLine('');
|
||||
}
|
||||
logger.logBoxEnd();
|
||||
@@ -390,11 +405,15 @@ export class NupstCli {
|
||||
// Show OIDs if custom model is selected
|
||||
if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) {
|
||||
logger.logBoxLine('Custom OIDs:');
|
||||
logger.logBoxLine(` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`);
|
||||
logger.logBoxLine(
|
||||
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`
|
||||
` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`,
|
||||
);
|
||||
logger.logBoxLine(
|
||||
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
|
||||
);
|
||||
logger.logBoxLine(
|
||||
` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`,
|
||||
);
|
||||
logger.logBoxLine(` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,7 +454,11 @@ export class NupstCli {
|
||||
// Ignore errors checking service status
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to display configuration: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.error(
|
||||
`Failed to display configuration: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,4 +607,4 @@ Examples:
|
||||
nupst group remove dc-1 - Remove group with ID 'dc-1'
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -35,10 +35,10 @@ export class GroupHandler {
|
||||
logger.logBoxEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get current configuration
|
||||
const config = this.nupst.getDaemon().getConfig();
|
||||
|
||||
|
||||
// Check if multi-UPS config
|
||||
if (!config.groups || !Array.isArray(config.groups)) {
|
||||
// Legacy or missing groups configuration
|
||||
@@ -49,11 +49,11 @@ export class GroupHandler {
|
||||
logger.logBoxEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Display group list
|
||||
const boxWidth = 60;
|
||||
logger.logBoxTitle('UPS Groups', boxWidth);
|
||||
|
||||
|
||||
if (config.groups.length === 0) {
|
||||
logger.logBoxLine('No UPS groups configured.');
|
||||
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
|
||||
@@ -62,27 +62,29 @@ export class GroupHandler {
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('ID | Name | Mode | UPS Devices');
|
||||
logger.logBoxLine('-----------+----------------------+--------------+----------------');
|
||||
|
||||
|
||||
for (const group of config.groups) {
|
||||
const id = group.id.padEnd(10, ' ').substring(0, 10);
|
||||
const name = (group.name || '').padEnd(20, ' ').substring(0, 20);
|
||||
const mode = (group.mode || 'unknown').padEnd(12, ' ').substring(0, 12);
|
||||
|
||||
|
||||
// Count UPS devices in this group
|
||||
const upsInGroup = config.upsDevices.filter(ups => ups.groups.includes(group.id));
|
||||
const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id));
|
||||
const upsCount = upsInGroup.length;
|
||||
const upsNames = upsInGroup.map(ups => ups.name).join(', ');
|
||||
|
||||
const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
|
||||
|
||||
logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to list UPS groups: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.error(
|
||||
`Failed to list UPS groups: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a new UPS group
|
||||
*/
|
||||
@@ -110,54 +112,56 @@ export class GroupHandler {
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
} catch (error) {
|
||||
logger.error('No configuration found. Please run "nupst setup" first to create a configuration.');
|
||||
logger.error(
|
||||
'No configuration found. Please run "nupst setup" first to create a configuration.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get current configuration
|
||||
const config = this.nupst.getDaemon().getConfig();
|
||||
|
||||
|
||||
// Initialize groups array if not exists
|
||||
if (!config.groups) {
|
||||
config.groups = [];
|
||||
}
|
||||
|
||||
|
||||
// Check if upsDevices is initialized
|
||||
if (!config.upsDevices) {
|
||||
config.upsDevices = [];
|
||||
}
|
||||
|
||||
|
||||
logger.log('\nNUPST Add Group');
|
||||
logger.log('==============\n');
|
||||
logger.log('This will guide you through creating a new UPS group.\n');
|
||||
|
||||
|
||||
// Generate a new unique group ID
|
||||
const groupId = helpers.shortId();
|
||||
|
||||
|
||||
// Get group name
|
||||
const name = await prompt('Group Name: ');
|
||||
|
||||
|
||||
// Get group mode
|
||||
const modeInput = await prompt('Group Mode (redundant/nonRedundant) [redundant]: ');
|
||||
const mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
|
||||
|
||||
|
||||
// Get optional description
|
||||
const description = await prompt('Group Description (optional): ');
|
||||
|
||||
|
||||
// Create the new group
|
||||
const newGroup: IGroupConfig = {
|
||||
id: groupId,
|
||||
name: name || `Group-${groupId}`,
|
||||
mode,
|
||||
description: description || undefined
|
||||
description: description || undefined,
|
||||
};
|
||||
|
||||
|
||||
// Add the group to the configuration
|
||||
config.groups.push(newGroup);
|
||||
|
||||
|
||||
// Save the configuration
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
|
||||
|
||||
// Display summary
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Group Created', boxWidth);
|
||||
@@ -168,21 +172,23 @@ export class GroupHandler {
|
||||
logger.logBoxLine(`Description: ${newGroup.description}`);
|
||||
}
|
||||
logger.logBoxEnd();
|
||||
|
||||
|
||||
// Check if there are UPS devices to assign to this group
|
||||
if (config.upsDevices.length > 0) {
|
||||
const assignUps = await prompt('Would you like to assign UPS devices to this group now? (y/N): ');
|
||||
const assignUps = await prompt(
|
||||
'Would you like to assign UPS devices to this group now? (y/N): ',
|
||||
);
|
||||
if (assignUps.toLowerCase() === 'y') {
|
||||
await this.assignUpsToGroup(newGroup.id, config, prompt);
|
||||
|
||||
|
||||
// Save again after assigning UPS devices
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check if service is running and restart it if needed
|
||||
this.nupst.getUpsHandler().restartServiceIfRunning();
|
||||
|
||||
|
||||
logger.log('\nGroup setup complete!');
|
||||
} finally {
|
||||
rl.close();
|
||||
@@ -191,7 +197,7 @@ export class GroupHandler {
|
||||
logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Edit an existing UPS group
|
||||
* @param groupId ID of the group to edit
|
||||
@@ -220,57 +226,61 @@ export class GroupHandler {
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
} catch (error) {
|
||||
logger.error('No configuration found. Please run "nupst setup" first to create a configuration.');
|
||||
logger.error(
|
||||
'No configuration found. Please run "nupst setup" first to create a configuration.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get current configuration
|
||||
const config = this.nupst.getDaemon().getConfig();
|
||||
|
||||
|
||||
// Check if groups are initialized
|
||||
if (!config.groups || !Array.isArray(config.groups)) {
|
||||
logger.error('No groups configured. Please run "nupst group add" first to create a group.');
|
||||
logger.error(
|
||||
'No groups configured. Please run "nupst group add" first to create a group.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Find the group to edit
|
||||
const groupIndex = config.groups.findIndex(group => group.id === groupId);
|
||||
const groupIndex = config.groups.findIndex((group) => group.id === groupId);
|
||||
if (groupIndex === -1) {
|
||||
logger.error(`Group with ID "${groupId}" not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const group = config.groups[groupIndex];
|
||||
|
||||
|
||||
logger.log(`\nNUPST Edit Group: ${group.name} (${group.id})`);
|
||||
logger.log('==============================================\n');
|
||||
|
||||
|
||||
// Edit group name
|
||||
const newName = await prompt(`Group Name [${group.name}]: `);
|
||||
if (newName.trim()) {
|
||||
group.name = newName;
|
||||
}
|
||||
|
||||
|
||||
// Edit group mode
|
||||
const currentMode = group.mode || 'redundant';
|
||||
const modeInput = await prompt(`Group Mode (redundant/nonRedundant) [${currentMode}]: `);
|
||||
if (modeInput.trim()) {
|
||||
group.mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
|
||||
}
|
||||
|
||||
|
||||
// Edit description
|
||||
const currentDesc = group.description || '';
|
||||
const newDesc = await prompt(`Group Description [${currentDesc}]: `);
|
||||
if (newDesc.trim() || newDesc === '') {
|
||||
group.description = newDesc.trim() || undefined;
|
||||
}
|
||||
|
||||
|
||||
// Update the group in the configuration
|
||||
config.groups[groupIndex] = group;
|
||||
|
||||
|
||||
// Save the configuration
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
|
||||
|
||||
// Display summary
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Group Updated', boxWidth);
|
||||
@@ -281,19 +291,21 @@ export class GroupHandler {
|
||||
logger.logBoxLine(`Description: ${group.description}`);
|
||||
}
|
||||
logger.logBoxEnd();
|
||||
|
||||
|
||||
// Edit UPS assignments if requested
|
||||
const editAssignments = await prompt('Would you like to edit UPS assignments for this group? (y/N): ');
|
||||
const editAssignments = await prompt(
|
||||
'Would you like to edit UPS assignments for this group? (y/N): ',
|
||||
);
|
||||
if (editAssignments.toLowerCase() === 'y') {
|
||||
await this.assignUpsToGroup(group.id, config, prompt);
|
||||
|
||||
|
||||
// Save again after editing assignments
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
}
|
||||
|
||||
|
||||
// Check if service is running and restart it if needed
|
||||
this.nupst.getUpsHandler().restartServiceIfRunning();
|
||||
|
||||
|
||||
logger.log('\nGroup edit complete!');
|
||||
} finally {
|
||||
rl.close();
|
||||
@@ -302,7 +314,7 @@ export class GroupHandler {
|
||||
logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete an existing UPS group
|
||||
* @param groupId ID of the group to delete
|
||||
@@ -313,48 +325,53 @@ export class GroupHandler {
|
||||
try {
|
||||
await this.nupst.getDaemon().loadConfig();
|
||||
} catch (error) {
|
||||
logger.error('No configuration found. Please run "nupst setup" first to create a configuration.');
|
||||
logger.error(
|
||||
'No configuration found. Please run "nupst setup" first to create a configuration.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Get current configuration
|
||||
const config = this.nupst.getDaemon().getConfig();
|
||||
|
||||
|
||||
// Check if groups are initialized
|
||||
if (!config.groups || !Array.isArray(config.groups)) {
|
||||
logger.error('No groups configured.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Find the group to delete
|
||||
const groupIndex = config.groups.findIndex(group => group.id === groupId);
|
||||
const groupIndex = config.groups.findIndex((group) => group.id === groupId);
|
||||
if (groupIndex === -1) {
|
||||
logger.error(`Group with ID "${groupId}" not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const groupToDelete = config.groups[groupIndex];
|
||||
|
||||
|
||||
// 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 group "${groupToDelete.name}" (${groupId})? [y/N]: `, answer => {
|
||||
resolve(answer.toLowerCase());
|
||||
});
|
||||
|
||||
const confirm = await new Promise<string>((resolve) => {
|
||||
rl.question(
|
||||
`Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `,
|
||||
(answer) => {
|
||||
resolve(answer.toLowerCase());
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
rl.close();
|
||||
|
||||
|
||||
if (confirm !== 'y' && confirm !== 'yes') {
|
||||
logger.log('Deletion cancelled.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Remove this group from all UPS device group assignments
|
||||
if (config.upsDevices && Array.isArray(config.upsDevices)) {
|
||||
for (const ups of config.upsDevices) {
|
||||
@@ -364,19 +381,21 @@ export class GroupHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Remove the group from the array
|
||||
config.groups.splice(groupIndex, 1);
|
||||
|
||||
|
||||
// Save the configuration
|
||||
await this.nupst.getDaemon().saveConfig(config);
|
||||
|
||||
|
||||
logger.log(`Group "${groupToDelete.name}" (${groupId}) has been deleted.`);
|
||||
|
||||
|
||||
// Check if service is running and restart it if needed
|
||||
this.nupst.getUpsHandler().restartServiceIfRunning();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete group: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.error(
|
||||
`Failed to delete group: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,18 +408,18 @@ export class GroupHandler {
|
||||
public async assignUpsToGroups(
|
||||
ups: any,
|
||||
groups: any[],
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
// Initialize groups array if it doesn't exist
|
||||
if (!ups.groups) {
|
||||
ups.groups = [];
|
||||
}
|
||||
|
||||
|
||||
// Show current group assignments
|
||||
logger.log('\nCurrent Group Assignments:');
|
||||
if (ups.groups && ups.groups.length > 0) {
|
||||
for (const groupId of ups.groups) {
|
||||
const group = groups.find(g => g.id === groupId);
|
||||
const group = groups.find((g) => g.id === groupId);
|
||||
if (group) {
|
||||
logger.log(`- ${group.name} (${group.id})`);
|
||||
} else {
|
||||
@@ -410,52 +429,56 @@ export class GroupHandler {
|
||||
} else {
|
||||
logger.log('- None');
|
||||
}
|
||||
|
||||
|
||||
// Show available groups
|
||||
logger.log('\nAvailable Groups:');
|
||||
if (groups.length === 0) {
|
||||
logger.log('- No groups available. Use "nupst group add" to create groups.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
const group = groups[i];
|
||||
const assigned = ups.groups && ups.groups.includes(group.id);
|
||||
logger.log(`${i + 1}) ${group.name} (${group.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`);
|
||||
logger.log(
|
||||
`${i + 1}) ${group.name} (${group.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Prompt for group selection
|
||||
const selection = await prompt('\nSelect groups to assign/unassign (comma-separated numbers, or "clear" to remove all): ');
|
||||
|
||||
const selection = await prompt(
|
||||
'\nSelect groups to assign/unassign (comma-separated numbers, or "clear" to remove all): ',
|
||||
);
|
||||
|
||||
if (selection.toLowerCase() === 'clear') {
|
||||
// Clear all group assignments
|
||||
ups.groups = [];
|
||||
logger.log('All group assignments cleared.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!selection.trim()) {
|
||||
// No change if empty input
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Process selections
|
||||
const selections = selection.split(',').map(s => s.trim());
|
||||
|
||||
const selections = selection.split(',').map((s) => s.trim());
|
||||
|
||||
for (const sel of selections) {
|
||||
const index = parseInt(sel, 10) - 1;
|
||||
if (isNaN(index) || index < 0 || index >= groups.length) {
|
||||
logger.error(`Invalid selection: ${sel}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const group = groups[index];
|
||||
|
||||
|
||||
// Initialize groups array if needed (should already be done above)
|
||||
if (!ups.groups) {
|
||||
ups.groups = [];
|
||||
}
|
||||
|
||||
|
||||
// Toggle assignment
|
||||
const groupIndex = ups.groups.indexOf(group.id);
|
||||
if (groupIndex === -1) {
|
||||
@@ -469,7 +492,7 @@ export class GroupHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Assign UPS devices to a specific group
|
||||
* @param groupId Group ID to assign UPS devices to
|
||||
@@ -479,22 +502,24 @@ export class GroupHandler {
|
||||
public async assignUpsToGroup(
|
||||
groupId: string,
|
||||
config: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
if (!config.upsDevices || config.upsDevices.length === 0) {
|
||||
logger.log('No UPS devices available. Use "nupst add" to add UPS devices.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const group = config.groups.find((g: { id: string }) => g.id === groupId);
|
||||
if (!group) {
|
||||
logger.error(`Group with ID "${groupId}" not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Show current assignments
|
||||
logger.log(`\nUPS devices in group "${group.name}" (${group.id}):`);
|
||||
const upsInGroup = config.upsDevices.filter((ups: { groups?: string[] }) => ups.groups && ups.groups.includes(groupId));
|
||||
const upsInGroup = config.upsDevices.filter((ups: { groups?: string[] }) =>
|
||||
ups.groups && ups.groups.includes(groupId)
|
||||
);
|
||||
if (upsInGroup.length === 0) {
|
||||
logger.log('- None');
|
||||
} else {
|
||||
@@ -502,7 +527,7 @@ export class GroupHandler {
|
||||
logger.log(`- ${ups.name} (${ups.id})`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Show all UPS devices
|
||||
logger.log('\nAvailable UPS devices:');
|
||||
for (let i = 0; i < config.upsDevices.length; i++) {
|
||||
@@ -510,10 +535,12 @@ export class GroupHandler {
|
||||
const assigned = ups.groups && ups.groups.includes(groupId);
|
||||
logger.log(`${i + 1}) ${ups.name} (${ups.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`);
|
||||
}
|
||||
|
||||
|
||||
// Prompt for UPS selection
|
||||
const selection = await prompt('\nSelect UPS devices to assign/unassign (comma-separated numbers, or "clear" to remove all): ');
|
||||
|
||||
const selection = await prompt(
|
||||
'\nSelect UPS devices to assign/unassign (comma-separated numbers, or "clear" to remove all): ',
|
||||
);
|
||||
|
||||
if (selection.toLowerCase() === 'clear') {
|
||||
// Clear all UPS from this group
|
||||
for (const ups of config.upsDevices) {
|
||||
@@ -527,29 +554,29 @@ export class GroupHandler {
|
||||
logger.log(`All UPS devices removed from group "${group.name}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!selection.trim()) {
|
||||
// No change if empty input
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Process selections
|
||||
const selections = selection.split(',').map(s => s.trim());
|
||||
|
||||
const selections = selection.split(',').map((s) => s.trim());
|
||||
|
||||
for (const sel of selections) {
|
||||
const index = parseInt(sel, 10) - 1;
|
||||
if (isNaN(index) || index < 0 || index >= config.upsDevices.length) {
|
||||
logger.error(`Invalid selection: ${sel}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const ups = config.upsDevices[index];
|
||||
|
||||
|
||||
// Initialize groups array if needed
|
||||
if (!ups.groups) {
|
||||
ups.groups = [];
|
||||
}
|
||||
|
||||
|
||||
// Toggle assignment
|
||||
const groupIndex = ups.groups.indexOf(groupId);
|
||||
if (groupIndex === -1) {
|
||||
@@ -563,4 +590,4 @@ export class GroupHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import process from 'node:process';
|
||||
import { execSync } from "node:child_process";
|
||||
import { execSync } from 'node:child_process';
|
||||
import { Nupst } from '../nupst.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
|
||||
@@ -129,7 +129,7 @@ export class ServiceHandler {
|
||||
try {
|
||||
// Check if running as root
|
||||
this.checkRootAccess(
|
||||
'This command must be run as root to update NUPST and refresh the systemd service.'
|
||||
'This command must be run as root to update NUPST and refresh the systemd service.',
|
||||
);
|
||||
|
||||
const boxWidth = 45;
|
||||
@@ -243,7 +243,7 @@ export class ServiceHandler {
|
||||
|
||||
// Ask about removing configuration
|
||||
const removeConfig = await prompt(
|
||||
'Do you want to remove the NUPST configuration files? (y/N): '
|
||||
'Do you want to remove the NUPST configuration files? (y/N): ',
|
||||
);
|
||||
|
||||
// Find the uninstall.sh script location
|
||||
@@ -318,4 +318,4 @@ export class ServiceHandler {
|
||||
|
||||
return { debugMode, cleanedArgs };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import process from 'node:process';
|
||||
import { execSync } from "node:child_process";
|
||||
import { execSync } from 'node:child_process';
|
||||
import { Nupst } from '../nupst.ts';
|
||||
import { logger } from '../logger.ts';
|
||||
import * as helpers from '../helpers/index.ts';
|
||||
@@ -67,7 +67,7 @@ export class UpsHandler {
|
||||
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
|
||||
@@ -78,9 +78,9 @@ export class UpsHandler {
|
||||
name: 'Default UPS',
|
||||
snmp: config.snmp,
|
||||
thresholds: config.thresholds,
|
||||
groups: []
|
||||
groups: [],
|
||||
}],
|
||||
groups: []
|
||||
groups: [],
|
||||
};
|
||||
logger.log('Converting existing configuration to multi-UPS format.');
|
||||
}
|
||||
@@ -89,7 +89,7 @@ export class UpsHandler {
|
||||
config = {
|
||||
checkInterval: 30000, // Default check interval
|
||||
upsDevices: [],
|
||||
groups: []
|
||||
groups: [],
|
||||
};
|
||||
logger.log('No existing configuration found. Creating a new configuration.');
|
||||
}
|
||||
@@ -108,13 +108,13 @@ export class UpsHandler {
|
||||
community: 'public',
|
||||
version: 1,
|
||||
timeout: 5000,
|
||||
upsModel: 'cyberpower' as TUpsModel
|
||||
upsModel: 'cyberpower' as TUpsModel,
|
||||
},
|
||||
thresholds: {
|
||||
battery: 60,
|
||||
runtime: 20
|
||||
runtime: 20,
|
||||
},
|
||||
groups: []
|
||||
groups: [],
|
||||
};
|
||||
|
||||
// Gather SNMP settings
|
||||
@@ -144,10 +144,10 @@ export class UpsHandler {
|
||||
|
||||
// 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!');
|
||||
}
|
||||
|
||||
@@ -189,10 +189,13 @@ export class UpsHandler {
|
||||
* @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> {
|
||||
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();
|
||||
@@ -208,10 +211,10 @@ export class UpsHandler {
|
||||
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
|
||||
@@ -224,17 +227,17 @@ export class UpsHandler {
|
||||
name: 'Default UPS',
|
||||
snmp: config.snmp,
|
||||
thresholds: config.thresholds,
|
||||
groups: []
|
||||
groups: [],
|
||||
}];
|
||||
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);
|
||||
upsToEdit = config.upsDevices.find((ups) => ups.id === upsId);
|
||||
if (!upsToEdit) {
|
||||
logger.error(`UPS with ID "${upsId}" not found.`);
|
||||
return;
|
||||
@@ -249,41 +252,41 @@ export class UpsHandler {
|
||||
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 threshold settings
|
||||
await this.gatherThresholdSettings(upsToEdit.thresholds, 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);
|
||||
}
|
||||
|
||||
|
||||
// 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!');
|
||||
}
|
||||
|
||||
@@ -304,58 +307,63 @@ export class UpsHandler {
|
||||
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);
|
||||
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());
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
|
||||
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)}`);
|
||||
logger.error(
|
||||
`Failed to delete UPS: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,10 +383,10 @@ export class UpsHandler {
|
||||
logger.logBoxEnd();
|
||||
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
|
||||
@@ -395,41 +403,49 @@ export class UpsHandler {
|
||||
logger.logBoxLine('Default UPS:');
|
||||
logger.logBoxLine(` Host: ${config.snmp.host}:${config.snmp.port}`);
|
||||
logger.logBoxLine(` Model: ${config.snmp.upsModel || 'cyberpower'}`);
|
||||
logger.logBoxLine(` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`);
|
||||
logger.logBoxLine(
|
||||
` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`,
|
||||
);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('Use "nupst add" to add more UPS devices and migrate');
|
||||
logger.logBoxLine('to the multi-UPS configuration format.');
|
||||
logger.logBoxEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Display UPS list
|
||||
const boxWidth = 60;
|
||||
logger.logBoxTitle('UPS Devices', boxWidth);
|
||||
|
||||
|
||||
if (config.upsDevices.length === 0) {
|
||||
logger.logBoxLine('No UPS devices configured.');
|
||||
logger.logBoxLine('Use "nupst add" to add a UPS device.');
|
||||
} else {
|
||||
logger.logBoxLine(`Found ${config.upsDevices.length} UPS device(s)`);
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('ID | Name | Host | Mode | Groups');
|
||||
logger.logBoxLine('-----------+----------------------+-----------------+--------------+----------------');
|
||||
|
||||
logger.logBoxLine(
|
||||
'ID | Name | Host | Mode | Groups',
|
||||
);
|
||||
logger.logBoxLine(
|
||||
'-----------+----------------------+-----------------+--------------+----------------',
|
||||
);
|
||||
|
||||
for (const ups of config.upsDevices) {
|
||||
const id = ups.id.padEnd(10, ' ').substring(0, 10);
|
||||
const name = (ups.name || '').padEnd(20, ' ').substring(0, 20);
|
||||
const host = `${ups.snmp.host}:${ups.snmp.port}`.padEnd(15, ' ').substring(0, 15);
|
||||
const model = (ups.snmp.upsModel || 'cyberpower').padEnd(12, ' ').substring(0, 12);
|
||||
const groups = ups.groups.length > 0 ? ups.groups.join(', ') : 'None';
|
||||
|
||||
|
||||
logger.logBoxLine(`${id} | ${name} | ${host} | ${model} | ${groups}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to list UPS devices: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.error(
|
||||
`Failed to list UPS devices: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,7 +481,7 @@ export class UpsHandler {
|
||||
// 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})`);
|
||||
@@ -492,11 +508,11 @@ export class UpsHandler {
|
||||
const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {};
|
||||
const thresholds = isUpsConfig ? config.thresholds : config.thresholds || {};
|
||||
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}`);
|
||||
@@ -529,18 +545,22 @@ export class UpsHandler {
|
||||
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 Capacity: ${snmpConfig.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
|
||||
);
|
||||
logger.logBoxLine(` Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
|
||||
}
|
||||
logger.logBoxLine('Thresholds:');
|
||||
logger.logBoxLine(` Battery: ${thresholds.battery}%`);
|
||||
logger.logBoxLine(` Runtime: ${thresholds.runtime} minutes`);
|
||||
|
||||
|
||||
// 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(
|
||||
`Group Assignments: ${config.groups.length === 0 ? 'None' : config.groups.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
logger.logBoxLine(`Check Interval: ${checkInterval / 1000} seconds`);
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
@@ -553,12 +573,12 @@ export class UpsHandler {
|
||||
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 thresholds = config.thresholds ? config.thresholds : config.thresholds;
|
||||
|
||||
|
||||
const testConfig = {
|
||||
...snmpConfig,
|
||||
timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing
|
||||
@@ -599,26 +619,26 @@ export class UpsHandler {
|
||||
if (status.batteryCapacity < thresholds.battery) {
|
||||
logger.logBoxLine('⚠️ WARNING: Battery capacity below threshold');
|
||||
logger.logBoxLine(
|
||||
` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`
|
||||
` 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}%`
|
||||
` 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`
|
||||
` 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`
|
||||
` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -632,7 +652,7 @@ export class UpsHandler {
|
||||
*/
|
||||
private async gatherSnmpSettings(
|
||||
snmpConfig: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
// SNMP IP Address
|
||||
const defaultHost = snmpConfig.host || '127.0.0.1';
|
||||
@@ -653,10 +673,9 @@ export class UpsHandler {
|
||||
console.log(' 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;
|
||||
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)
|
||||
@@ -676,7 +695,7 @@ export class UpsHandler {
|
||||
*/
|
||||
private async gatherSnmpV3Settings(
|
||||
snmpConfig: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
console.log('\nSNMPv3 Security Settings:');
|
||||
|
||||
@@ -734,7 +753,7 @@ export class UpsHandler {
|
||||
// Allow customizing the timeout value
|
||||
const defaultTimeout = snmpConfig.timeout / 1000; // Convert from ms to seconds for display
|
||||
console.log(
|
||||
'\nSNMPv3 operations with authentication and privacy may require longer timeouts.'
|
||||
'\nSNMPv3 operations with authentication and privacy may require longer timeouts.',
|
||||
);
|
||||
const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `);
|
||||
const timeout = parseInt(timeoutInput, 10);
|
||||
@@ -751,7 +770,7 @@ export class UpsHandler {
|
||||
*/
|
||||
private async gatherAuthenticationSettings(
|
||||
snmpConfig: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
// Authentication protocol
|
||||
console.log('\nAuthentication Protocol:');
|
||||
@@ -759,7 +778,7 @@ export class UpsHandler {
|
||||
console.log(' 2) SHA');
|
||||
const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1;
|
||||
const authProtocolInput = await prompt(
|
||||
`Select Authentication Protocol [${defaultAuthProtocol}]: `
|
||||
`Select Authentication Protocol [${defaultAuthProtocol}]: `,
|
||||
);
|
||||
const authProtocol = parseInt(authProtocolInput, 10) || defaultAuthProtocol;
|
||||
snmpConfig.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5';
|
||||
@@ -777,7 +796,7 @@ export class UpsHandler {
|
||||
*/
|
||||
private async gatherPrivacySettings(
|
||||
snmpConfig: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
// Privacy protocol
|
||||
console.log('\nPrivacy Protocol:');
|
||||
@@ -801,31 +820,29 @@ export class UpsHandler {
|
||||
*/
|
||||
private async gatherThresholdSettings(
|
||||
thresholds: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
console.log('\nShutdown Thresholds:');
|
||||
|
||||
// Battery threshold
|
||||
const defaultBatteryThreshold = thresholds.battery || 60;
|
||||
const batteryThresholdInput = await prompt(
|
||||
`Battery percentage threshold [${defaultBatteryThreshold}%]: `
|
||||
`Battery percentage threshold [${defaultBatteryThreshold}%]: `,
|
||||
);
|
||||
const batteryThreshold = parseInt(batteryThresholdInput, 10);
|
||||
thresholds.battery =
|
||||
batteryThresholdInput.trim() && !isNaN(batteryThreshold)
|
||||
? batteryThreshold
|
||||
: defaultBatteryThreshold;
|
||||
thresholds.battery = batteryThresholdInput.trim() && !isNaN(batteryThreshold)
|
||||
? batteryThreshold
|
||||
: defaultBatteryThreshold;
|
||||
|
||||
// Runtime threshold
|
||||
const defaultRuntimeThreshold = thresholds.runtime || 20;
|
||||
const runtimeThresholdInput = await prompt(
|
||||
`Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: `
|
||||
`Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: `,
|
||||
);
|
||||
const runtimeThreshold = parseInt(runtimeThresholdInput, 10);
|
||||
thresholds.runtime =
|
||||
runtimeThresholdInput.trim() && !isNaN(runtimeThreshold)
|
||||
? runtimeThreshold
|
||||
: defaultRuntimeThreshold;
|
||||
thresholds.runtime = runtimeThresholdInput.trim() && !isNaN(runtimeThreshold)
|
||||
? runtimeThreshold
|
||||
: defaultRuntimeThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -835,7 +852,7 @@ export class UpsHandler {
|
||||
*/
|
||||
private async gatherUpsModelSettings(
|
||||
snmpConfig: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
console.log('\nUPS Model Selection:');
|
||||
console.log(' 1) CyberPower');
|
||||
@@ -845,20 +862,19 @@ export class UpsHandler {
|
||||
console.log(' 5) Liebert/Vertiv');
|
||||
console.log(' 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 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;
|
||||
@@ -905,7 +921,7 @@ export class UpsHandler {
|
||||
logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`);
|
||||
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`);
|
||||
logger.logBoxLine(
|
||||
`Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`
|
||||
`Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`,
|
||||
);
|
||||
if (ups.groups && ups.groups.length > 0) {
|
||||
logger.logBoxLine(`Groups: ${ups.groups.join(', ')}`);
|
||||
@@ -923,10 +939,10 @@ export class UpsHandler {
|
||||
*/
|
||||
private async optionallyTestConnection(
|
||||
snmpConfig: any,
|
||||
prompt: (question: string) => Promise<string>
|
||||
prompt: (question: string) => Promise<string>,
|
||||
): Promise<void> {
|
||||
const testConnection = await prompt(
|
||||
'Would you like to test the connection to your UPS? (y/N): '
|
||||
'Would you like to test the connection to your UPS? (y/N): ',
|
||||
);
|
||||
if (testConnection.toLowerCase() === 'y') {
|
||||
logger.log('\nTesting connection to UPS...');
|
||||
@@ -985,7 +1001,9 @@ export class UpsHandler {
|
||||
logger.logBoxLine(' sudo systemctl restart nupst.service');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.logBoxLine(`Error restarting service: ${error instanceof Error ? error.message : String(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');
|
||||
}
|
||||
@@ -996,4 +1014,4 @@ export class UpsHandler {
|
||||
// Ignore errors checking service status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
348
ts/daemon.ts
348
ts/daemon.ts
@@ -1,8 +1,8 @@
|
||||
import process from 'node:process';
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { exec, execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { exec, execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { NupstSnmp } from './snmp/manager.ts';
|
||||
import type { ISnmpConfig } from './snmp/types.ts';
|
||||
import { logger } from './logger.ts';
|
||||
@@ -55,7 +55,7 @@ export interface INupstConfig {
|
||||
groups: IGroupConfig[];
|
||||
/** Check interval in milliseconds */
|
||||
checkInterval: number;
|
||||
|
||||
|
||||
// Legacy fields for backward compatibility
|
||||
/** SNMP configuration settings (legacy) */
|
||||
snmp?: ISnmpConfig;
|
||||
@@ -109,14 +109,14 @@ export class NupstDaemon {
|
||||
privProtocol: 'AES',
|
||||
privKey: '',
|
||||
// UPS model for OID selection
|
||||
upsModel: 'cyberpower'
|
||||
upsModel: 'cyberpower',
|
||||
},
|
||||
thresholds: {
|
||||
battery: 60, // Shutdown when battery below 60%
|
||||
runtime: 20, // Shutdown when runtime below 20 minutes
|
||||
},
|
||||
groups: []
|
||||
}
|
||||
groups: [],
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
checkInterval: 30000, // Check every 30 seconds
|
||||
@@ -126,7 +126,7 @@ export class NupstDaemon {
|
||||
private snmp: NupstSnmp;
|
||||
private isRunning: boolean = false;
|
||||
private upsStatus: Map<string, IUpsStatus> = new Map();
|
||||
|
||||
|
||||
/**
|
||||
* Create a new daemon instance with the given SNMP manager
|
||||
*/
|
||||
@@ -148,11 +148,11 @@ export class NupstDaemon {
|
||||
this.logConfigError(errorMsg);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
|
||||
// Read and parse config
|
||||
const configData = fs.readFileSync(this.CONFIG_PATH, 'utf8');
|
||||
const parsedConfig = JSON.parse(configData);
|
||||
|
||||
|
||||
// Handle legacy configuration format
|
||||
if (!parsedConfig.upsDevices && parsedConfig.snmp) {
|
||||
// Convert legacy format to new format
|
||||
@@ -163,28 +163,32 @@ export class NupstDaemon {
|
||||
name: 'Default UPS',
|
||||
snmp: parsedConfig.snmp,
|
||||
thresholds: parsedConfig.thresholds,
|
||||
groups: []
|
||||
}
|
||||
groups: [],
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
checkInterval: parsedConfig.checkInterval
|
||||
checkInterval: parsedConfig.checkInterval,
|
||||
};
|
||||
|
||||
|
||||
logger.log('Legacy configuration format detected. Converting to multi-UPS format.');
|
||||
|
||||
|
||||
// Save the new format
|
||||
await this.saveConfig(this.config);
|
||||
} else {
|
||||
this.config = parsedConfig;
|
||||
}
|
||||
|
||||
|
||||
return this.config;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message && error.message.includes('No configuration found')) {
|
||||
if (
|
||||
error instanceof Error && error.message && error.message.includes('No configuration found')
|
||||
) {
|
||||
throw error; // Re-throw the no configuration error
|
||||
}
|
||||
|
||||
this.logConfigError(`Error loading configuration: ${error instanceof Error ? error.message : String(error)}`);
|
||||
|
||||
this.logConfigError(
|
||||
`Error loading configuration: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
throw new Error('Failed to load configuration');
|
||||
}
|
||||
}
|
||||
@@ -200,7 +204,7 @@ export class NupstDaemon {
|
||||
}
|
||||
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('└──────────────────────────────────────────┘');
|
||||
@@ -215,7 +219,7 @@ export class NupstDaemon {
|
||||
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("│ Please run 'nupst setup' first to create a configuration.");
|
||||
console.error('└───────────────────────────────────────────┘');
|
||||
}
|
||||
|
||||
@@ -225,7 +229,7 @@ export class NupstDaemon {
|
||||
public getConfig(): INupstConfig {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the SNMP instance
|
||||
*/
|
||||
@@ -243,15 +247,15 @@ export class NupstDaemon {
|
||||
}
|
||||
|
||||
logger.log('Starting NUPST daemon...');
|
||||
|
||||
|
||||
try {
|
||||
// Load configuration - this will throw an error if config doesn't exist
|
||||
await this.loadConfig();
|
||||
this.logConfigLoaded();
|
||||
|
||||
|
||||
// Log version information
|
||||
this.snmp.getNupst().logVersionInfo(false); // Don't check for updates immediately on startup
|
||||
|
||||
|
||||
// Check for updates in the background
|
||||
this.snmp.getNupst().checkForUpdates().then((updateAvailable: boolean) => {
|
||||
if (updateAvailable) {
|
||||
@@ -264,16 +268,18 @@ export class NupstDaemon {
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}).catch(() => {}); // Ignore errors checking for updates
|
||||
|
||||
|
||||
// Initialize UPS status tracking
|
||||
this.initializeUpsStatus();
|
||||
|
||||
|
||||
// Start UPS monitoring
|
||||
this.isRunning = true;
|
||||
await this.monitor();
|
||||
} catch (error) {
|
||||
this.isRunning = false;
|
||||
logger.error(`Daemon failed to start: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.error(
|
||||
`Daemon failed to start: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1); // Exit with error
|
||||
}
|
||||
}
|
||||
@@ -283,7 +289,7 @@ export class NupstDaemon {
|
||||
*/
|
||||
private initializeUpsStatus(): void {
|
||||
this.upsStatus.clear();
|
||||
|
||||
|
||||
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
|
||||
for (const ups of this.config.upsDevices) {
|
||||
this.upsStatus.set(ups.id, {
|
||||
@@ -293,10 +299,10 @@ export class NupstDaemon {
|
||||
batteryCapacity: 100,
|
||||
batteryRuntime: 999, // High value as default
|
||||
lastStatusChange: Date.now(),
|
||||
lastCheckTime: 0
|
||||
lastCheckTime: 0,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
logger.log(`Initialized status tracking for ${this.config.upsDevices.length} UPS devices`);
|
||||
} else {
|
||||
logger.error('No UPS devices found in configuration');
|
||||
@@ -309,7 +315,7 @@ export class NupstDaemon {
|
||||
private logConfigLoaded(): void {
|
||||
const boxWidth = 50;
|
||||
logger.logBoxTitle('Configuration Loaded', boxWidth);
|
||||
|
||||
|
||||
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
|
||||
logger.logBoxLine(`UPS Devices: ${this.config.upsDevices.length}`);
|
||||
for (const ups of this.config.upsDevices) {
|
||||
@@ -318,7 +324,7 @@ export class NupstDaemon {
|
||||
} else {
|
||||
logger.logBoxLine('No UPS devices configured');
|
||||
}
|
||||
|
||||
|
||||
if (this.config.groups && this.config.groups.length > 0) {
|
||||
logger.logBoxLine(`Groups: ${this.config.groups.length}`);
|
||||
for (const group of this.config.groups) {
|
||||
@@ -327,7 +333,7 @@ export class NupstDaemon {
|
||||
} else {
|
||||
logger.logBoxLine('No Groups configured');
|
||||
}
|
||||
|
||||
|
||||
logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`);
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
@@ -345,43 +351,45 @@ export class NupstDaemon {
|
||||
*/
|
||||
private async monitor(): Promise<void> {
|
||||
logger.log('Starting UPS monitoring...');
|
||||
|
||||
|
||||
if (!this.config.upsDevices || this.config.upsDevices.length === 0) {
|
||||
logger.error('No UPS devices found in configuration. Monitoring stopped.');
|
||||
this.isRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let lastLogTime = 0; // Track when we last logged status
|
||||
const LOG_INTERVAL = 5 * 60 * 1000; // Log at least every 5 minutes (300000ms)
|
||||
|
||||
|
||||
// Monitor continuously
|
||||
while (this.isRunning) {
|
||||
try {
|
||||
// Check all UPS devices
|
||||
await this.checkAllUpsDevices();
|
||||
|
||||
|
||||
// Log periodic status update
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastLogTime >= LOG_INTERVAL) {
|
||||
this.logAllUpsStatus();
|
||||
lastLogTime = currentTime;
|
||||
}
|
||||
|
||||
|
||||
// Check if shutdown is required based on group configurations
|
||||
await this.evaluateGroupShutdownConditions();
|
||||
|
||||
|
||||
// Wait before next check
|
||||
await this.sleep(this.config.checkInterval);
|
||||
} catch (error) {
|
||||
logger.error(`Error during UPS monitoring: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.error(
|
||||
`Error during UPS monitoring: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
await this.sleep(this.config.checkInterval);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
logger.log('UPS monitoring stopped');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check status of all UPS devices
|
||||
*/
|
||||
@@ -398,14 +406,14 @@ export class NupstDaemon {
|
||||
batteryCapacity: 100,
|
||||
batteryRuntime: 999,
|
||||
lastStatusChange: Date.now(),
|
||||
lastCheckTime: 0
|
||||
lastCheckTime: 0,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Check UPS status
|
||||
const status = await this.snmp.getUpsStatus(ups.snmp);
|
||||
const currentTime = Date.now();
|
||||
|
||||
|
||||
// Get the current status from the map
|
||||
const currentStatus = this.upsStatus.get(ups.id);
|
||||
|
||||
@@ -417,7 +425,7 @@ export class NupstDaemon {
|
||||
batteryCapacity: status.batteryCapacity,
|
||||
batteryRuntime: status.batteryRuntime,
|
||||
lastCheckTime: currentTime,
|
||||
lastStatusChange: currentStatus?.lastStatusChange || currentTime
|
||||
lastStatusChange: currentStatus?.lastStatusChange || currentTime,
|
||||
};
|
||||
|
||||
// Check if power status changed
|
||||
@@ -432,11 +440,15 @@ export class NupstDaemon {
|
||||
// Update the status in the map
|
||||
this.upsStatus.set(ups.id, updatedStatus);
|
||||
} catch (error) {
|
||||
logger.error(`Error checking UPS ${ups.name} (${ups.id}): ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.error(
|
||||
`Error checking UPS ${ups.name} (${ups.id}): ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log status of all UPS devices
|
||||
*/
|
||||
@@ -446,17 +458,19 @@ export class NupstDaemon {
|
||||
logger.logBoxTitle('Periodic Status Update', boxWidth);
|
||||
logger.logBoxLine(`Timestamp: ${timestamp}`);
|
||||
logger.logBoxLine('');
|
||||
|
||||
|
||||
for (const [id, status] of this.upsStatus.entries()) {
|
||||
logger.logBoxLine(`UPS: ${status.name} (${id})`);
|
||||
logger.logBoxLine(` Power Status: ${status.powerStatus}`);
|
||||
logger.logBoxLine(` Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
|
||||
logger.logBoxLine(
|
||||
` Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`,
|
||||
);
|
||||
logger.logBoxLine('');
|
||||
}
|
||||
|
||||
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Evaluate if shutdown is required based on group configurations
|
||||
*/
|
||||
@@ -466,7 +480,7 @@ export class NupstDaemon {
|
||||
for (const [id, status] of this.upsStatus.entries()) {
|
||||
if (status.powerStatus === 'onBattery') {
|
||||
// Find the UPS config
|
||||
const ups = this.config.upsDevices.find(u => u.id === id);
|
||||
const ups = this.config.upsDevices.find((u) => u.id === id);
|
||||
if (ups) {
|
||||
await this.evaluateUpsShutdownCondition(ups, status);
|
||||
}
|
||||
@@ -474,19 +488,19 @@ export class NupstDaemon {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Evaluate each group
|
||||
for (const group of this.config.groups) {
|
||||
// Find all UPS devices in this group
|
||||
const upsDevicesInGroup = this.config.upsDevices.filter(ups =>
|
||||
const upsDevicesInGroup = this.config.upsDevices.filter((ups) =>
|
||||
ups.groups && ups.groups.includes(group.id)
|
||||
);
|
||||
|
||||
|
||||
if (upsDevicesInGroup.length === 0) {
|
||||
// No UPS devices in this group
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (group.mode === 'redundant') {
|
||||
// Redundant mode: only shutdown if ALL UPS devices in the group are in critical condition
|
||||
await this.evaluateRedundantGroup(group, upsDevicesInGroup);
|
||||
@@ -496,72 +510,90 @@ export class NupstDaemon {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Evaluate a redundant group for shutdown conditions
|
||||
* In redundant mode, we only shut down if ALL UPS devices are in critical condition
|
||||
*/
|
||||
private async evaluateRedundantGroup(group: IGroupConfig, upsDevices: IUpsConfig[]): Promise<void> {
|
||||
private async evaluateRedundantGroup(
|
||||
group: IGroupConfig,
|
||||
upsDevices: IUpsConfig[],
|
||||
): Promise<void> {
|
||||
// Count UPS devices on battery and in critical condition
|
||||
let upsOnBattery = 0;
|
||||
let upsInCriticalCondition = 0;
|
||||
|
||||
|
||||
for (const ups of upsDevices) {
|
||||
const status = this.upsStatus.get(ups.id);
|
||||
if (!status) continue;
|
||||
|
||||
|
||||
if (status.powerStatus === 'onBattery') {
|
||||
upsOnBattery++;
|
||||
|
||||
|
||||
// Check if this UPS is in critical condition
|
||||
if (status.batteryCapacity < ups.thresholds.battery ||
|
||||
status.batteryRuntime < ups.thresholds.runtime) {
|
||||
if (
|
||||
status.batteryCapacity < ups.thresholds.battery ||
|
||||
status.batteryRuntime < ups.thresholds.runtime
|
||||
) {
|
||||
upsInCriticalCondition++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// All UPS devices must be online for a redundant group to be considered healthy
|
||||
const allUpsCount = upsDevices.length;
|
||||
|
||||
|
||||
// If all UPS are on battery and in critical condition, shutdown
|
||||
if (upsOnBattery === allUpsCount && upsInCriticalCondition === allUpsCount) {
|
||||
logger.logBoxTitle(`Group Shutdown Required: ${group.name}`, 50);
|
||||
logger.logBoxLine(`Mode: Redundant`);
|
||||
logger.logBoxLine(`All ${allUpsCount} UPS devices in critical condition`);
|
||||
logger.logBoxEnd();
|
||||
|
||||
await this.initiateShutdown(`All UPS devices in redundant group "${group.name}" in critical condition`);
|
||||
|
||||
await this.initiateShutdown(
|
||||
`All UPS devices in redundant group "${group.name}" in critical condition`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Evaluate a non-redundant group for shutdown conditions
|
||||
* In non-redundant mode, we shut down if ANY UPS device is in critical condition
|
||||
*/
|
||||
private async evaluateNonRedundantGroup(group: IGroupConfig, upsDevices: IUpsConfig[]): Promise<void> {
|
||||
private async evaluateNonRedundantGroup(
|
||||
group: IGroupConfig,
|
||||
upsDevices: IUpsConfig[],
|
||||
): Promise<void> {
|
||||
for (const ups of upsDevices) {
|
||||
const status = this.upsStatus.get(ups.id);
|
||||
if (!status) continue;
|
||||
|
||||
|
||||
if (status.powerStatus === 'onBattery') {
|
||||
// Check if this UPS is in critical condition
|
||||
if (status.batteryCapacity < ups.thresholds.battery ||
|
||||
status.batteryRuntime < ups.thresholds.runtime) {
|
||||
if (
|
||||
status.batteryCapacity < ups.thresholds.battery ||
|
||||
status.batteryRuntime < ups.thresholds.runtime
|
||||
) {
|
||||
logger.logBoxTitle(`Group Shutdown Required: ${group.name}`, 50);
|
||||
logger.logBoxLine(`Mode: Non-Redundant`);
|
||||
logger.logBoxLine(`UPS ${ups.name} in critical condition`);
|
||||
logger.logBoxLine(`Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`);
|
||||
logger.logBoxLine(`Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`);
|
||||
logger.logBoxLine(
|
||||
`Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`,
|
||||
);
|
||||
logger.logBoxLine(
|
||||
`Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`,
|
||||
);
|
||||
logger.logBoxEnd();
|
||||
|
||||
await this.initiateShutdown(`UPS "${ups.name}" in non-redundant group "${group.name}" in critical condition`);
|
||||
|
||||
await this.initiateShutdown(
|
||||
`UPS "${ups.name}" in non-redundant group "${group.name}" in critical condition`,
|
||||
);
|
||||
return; // Exit after initiating shutdown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Evaluate an individual UPS for shutdown conditions
|
||||
*/
|
||||
@@ -570,38 +602,44 @@ export class NupstDaemon {
|
||||
if (ups.groups && ups.groups.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check threshold conditions
|
||||
if (status.batteryCapacity < ups.thresholds.battery ||
|
||||
status.batteryRuntime < ups.thresholds.runtime) {
|
||||
if (
|
||||
status.batteryCapacity < ups.thresholds.battery ||
|
||||
status.batteryRuntime < ups.thresholds.runtime
|
||||
) {
|
||||
logger.logBoxTitle(`UPS Shutdown Required: ${ups.name}`, 50);
|
||||
logger.logBoxLine(`Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`);
|
||||
logger.logBoxLine(`Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`);
|
||||
logger.logBoxLine(
|
||||
`Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`,
|
||||
);
|
||||
logger.logBoxLine(
|
||||
`Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`,
|
||||
);
|
||||
logger.logBoxEnd();
|
||||
|
||||
|
||||
await this.initiateShutdown(`UPS "${ups.name}" battery or runtime below threshold`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initiate system shutdown with UPS monitoring during shutdown
|
||||
* @param reason Reason for shutdown
|
||||
*/
|
||||
public async initiateShutdown(reason: string): Promise<void> {
|
||||
logger.log(`Initiating system shutdown due to: ${reason}`);
|
||||
|
||||
|
||||
// Set a longer delay for shutdown to allow VMs and services to close
|
||||
const shutdownDelayMinutes = 5;
|
||||
|
||||
|
||||
try {
|
||||
// Find shutdown command in common system paths
|
||||
const shutdownPaths = [
|
||||
'/sbin/shutdown',
|
||||
'/usr/sbin/shutdown',
|
||||
'/bin/shutdown',
|
||||
'/usr/bin/shutdown'
|
||||
'/usr/bin/shutdown',
|
||||
];
|
||||
|
||||
|
||||
let shutdownCmd = '';
|
||||
for (const path of shutdownPaths) {
|
||||
try {
|
||||
@@ -614,14 +652,16 @@ export class NupstDaemon {
|
||||
// Continue checking other paths
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (shutdownCmd) {
|
||||
// Execute shutdown command with delay to allow for VM graceful shutdown
|
||||
logger.log(`Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`);
|
||||
logger.log(
|
||||
`Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`,
|
||||
);
|
||||
const { stdout } = await execFileAsync(shutdownCmd, [
|
||||
'-h',
|
||||
`+${shutdownDelayMinutes}`,
|
||||
`UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes`
|
||||
'-h',
|
||||
`+${shutdownDelayMinutes}`,
|
||||
`UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes`,
|
||||
]);
|
||||
logger.log(`Shutdown initiated: ${stdout}`);
|
||||
logger.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`);
|
||||
@@ -629,29 +669,34 @@ export class NupstDaemon {
|
||||
// Try using the PATH to find shutdown
|
||||
try {
|
||||
logger.log('Shutdown command not found in common paths, trying via PATH...');
|
||||
const { stdout } = await execAsync(`shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`, {
|
||||
env: process.env // Pass the current environment
|
||||
});
|
||||
const { stdout } = await execAsync(
|
||||
`shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`,
|
||||
{
|
||||
env: process.env, // Pass the current environment
|
||||
},
|
||||
);
|
||||
logger.log(`Shutdown initiated: ${stdout}`);
|
||||
} catch (e) {
|
||||
throw new Error(`Shutdown command not found: ${e instanceof Error ? e.message : String(e)}`);
|
||||
throw new Error(
|
||||
`Shutdown command not found: ${e instanceof Error ? e.message : String(e)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Monitor UPS during shutdown and force immediate shutdown if battery gets too low
|
||||
logger.log('Monitoring UPS during shutdown process...');
|
||||
await this.monitorDuringShutdown();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to initiate shutdown: ${error}`);
|
||||
|
||||
|
||||
// Try alternative shutdown methods
|
||||
const alternatives = [
|
||||
{ cmd: 'poweroff', args: ['--force'] },
|
||||
{ cmd: 'halt', args: ['-p'] },
|
||||
{ cmd: 'systemctl', args: ['poweroff'] },
|
||||
{ cmd: 'reboot', args: ['-p'] } // Some systems allow reboot -p for power off
|
||||
{ cmd: 'reboot', args: ['-p'] }, // Some systems allow reboot -p for power off
|
||||
];
|
||||
|
||||
|
||||
for (const alt of alternatives) {
|
||||
try {
|
||||
// First check if command exists in common system paths
|
||||
@@ -659,9 +704,9 @@ export class NupstDaemon {
|
||||
`/sbin/${alt.cmd}`,
|
||||
`/usr/sbin/${alt.cmd}`,
|
||||
`/bin/${alt.cmd}`,
|
||||
`/usr/bin/${alt.cmd}`
|
||||
`/usr/bin/${alt.cmd}`,
|
||||
];
|
||||
|
||||
|
||||
let cmdPath = '';
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
@@ -669,7 +714,7 @@ export class NupstDaemon {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (cmdPath) {
|
||||
logger.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`);
|
||||
await execFileAsync(cmdPath, alt.args);
|
||||
@@ -678,7 +723,7 @@ export class NupstDaemon {
|
||||
// Try using PATH environment
|
||||
logger.log(`Trying alternative via PATH: ${alt.cmd} ${alt.args.join(' ')}`);
|
||||
await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
|
||||
env: process.env // Pass the current environment
|
||||
env: process.env, // Pass the current environment
|
||||
});
|
||||
return; // Exit if successful
|
||||
}
|
||||
@@ -687,11 +732,11 @@ export class NupstDaemon {
|
||||
// Continue to next method
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
logger.error('All shutdown methods failed');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Monitor UPS during system shutdown
|
||||
* Force immediate shutdown if any UPS gets critically low
|
||||
@@ -701,48 +746,62 @@ export class NupstDaemon {
|
||||
const CHECK_INTERVAL = 30000; // Check every 30 seconds during shutdown
|
||||
const MAX_MONITORING_TIME = 5 * 60 * 1000; // Max 5 minutes of monitoring
|
||||
const startTime = Date.now();
|
||||
|
||||
logger.log(`Emergency shutdown threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes remaining battery runtime`);
|
||||
|
||||
|
||||
logger.log(
|
||||
`Emergency shutdown threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes remaining battery runtime`,
|
||||
);
|
||||
|
||||
// Continue monitoring until max monitoring time is reached
|
||||
while (Date.now() - startTime < MAX_MONITORING_TIME) {
|
||||
try {
|
||||
logger.log('Checking UPS status during shutdown...');
|
||||
|
||||
|
||||
// Check all UPS devices
|
||||
for (const ups of this.config.upsDevices) {
|
||||
try {
|
||||
const status = await this.snmp.getUpsStatus(ups.snmp);
|
||||
|
||||
logger.log(`UPS ${ups.name}: Battery ${status.batteryCapacity}%, Runtime: ${status.batteryRuntime} minutes`);
|
||||
|
||||
|
||||
logger.log(
|
||||
`UPS ${ups.name}: Battery ${status.batteryCapacity}%, Runtime: ${status.batteryRuntime} minutes`,
|
||||
);
|
||||
|
||||
// If any UPS battery runtime gets critically low, force immediate shutdown
|
||||
if (status.batteryRuntime < EMERGENCY_RUNTIME_THRESHOLD) {
|
||||
logger.logBoxTitle('EMERGENCY SHUTDOWN', 50);
|
||||
logger.logBoxLine(`UPS ${ups.name} runtime critically low: ${status.batteryRuntime} minutes`);
|
||||
logger.logBoxLine(
|
||||
`UPS ${ups.name} runtime critically low: ${status.batteryRuntime} minutes`,
|
||||
);
|
||||
logger.logBoxLine('Forcing immediate shutdown!');
|
||||
logger.logBoxEnd();
|
||||
|
||||
|
||||
// Force immediate shutdown
|
||||
await this.forceImmediateShutdown();
|
||||
return;
|
||||
}
|
||||
} catch (upsError) {
|
||||
logger.error(`Error checking UPS ${ups.name} during shutdown: ${upsError instanceof Error ? upsError.message : String(upsError)}`);
|
||||
logger.error(
|
||||
`Error checking UPS ${ups.name} during shutdown: ${
|
||||
upsError instanceof Error ? upsError.message : String(upsError)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Wait before checking again
|
||||
await this.sleep(CHECK_INTERVAL);
|
||||
} catch (error) {
|
||||
logger.error(`Error monitoring UPS during shutdown: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.error(
|
||||
`Error monitoring UPS during shutdown: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
await this.sleep(CHECK_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
logger.log('UPS monitoring during shutdown completed');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Force an immediate system shutdown
|
||||
*/
|
||||
@@ -753,9 +812,9 @@ export class NupstDaemon {
|
||||
'/sbin/shutdown',
|
||||
'/usr/sbin/shutdown',
|
||||
'/bin/shutdown',
|
||||
'/usr/bin/shutdown'
|
||||
'/usr/bin/shutdown',
|
||||
];
|
||||
|
||||
|
||||
let shutdownCmd = '';
|
||||
for (const path of shutdownPaths) {
|
||||
if (fs.existsSync(path)) {
|
||||
@@ -764,27 +823,34 @@ export class NupstDaemon {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (shutdownCmd) {
|
||||
logger.log(`Executing emergency shutdown: ${shutdownCmd} -h now`);
|
||||
await execFileAsync(shutdownCmd, ['-h', 'now', 'EMERGENCY: UPS battery critically low, shutting down NOW']);
|
||||
await execFileAsync(shutdownCmd, [
|
||||
'-h',
|
||||
'now',
|
||||
'EMERGENCY: UPS battery critically low, shutting down NOW',
|
||||
]);
|
||||
} else {
|
||||
// Try using the PATH to find shutdown
|
||||
logger.log('Shutdown command not found in common paths, trying via PATH...');
|
||||
await execAsync('shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"', {
|
||||
env: process.env // Pass the current environment
|
||||
});
|
||||
await execAsync(
|
||||
'shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"',
|
||||
{
|
||||
env: process.env, // Pass the current environment
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Emergency shutdown failed, trying alternative methods...');
|
||||
|
||||
|
||||
// Try alternative shutdown methods in sequence
|
||||
const alternatives = [
|
||||
{ cmd: 'poweroff', args: ['--force'] },
|
||||
{ cmd: 'halt', args: ['-p'] },
|
||||
{ cmd: 'systemctl', args: ['poweroff'] }
|
||||
{ cmd: 'systemctl', args: ['poweroff'] },
|
||||
];
|
||||
|
||||
|
||||
for (const alt of alternatives) {
|
||||
try {
|
||||
// Check common paths
|
||||
@@ -792,9 +858,9 @@ export class NupstDaemon {
|
||||
`/sbin/${alt.cmd}`,
|
||||
`/usr/sbin/${alt.cmd}`,
|
||||
`/bin/${alt.cmd}`,
|
||||
`/usr/bin/${alt.cmd}`
|
||||
`/usr/bin/${alt.cmd}`,
|
||||
];
|
||||
|
||||
|
||||
let cmdPath = '';
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
@@ -802,7 +868,7 @@ export class NupstDaemon {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (cmdPath) {
|
||||
logger.log(`Emergency: using ${cmdPath} ${alt.args.join(' ')}`);
|
||||
await execFileAsync(cmdPath, alt.args);
|
||||
@@ -811,7 +877,7 @@ export class NupstDaemon {
|
||||
// Try using PATH
|
||||
logger.log(`Emergency: trying ${alt.cmd} via PATH`);
|
||||
await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
|
||||
env: process.env
|
||||
env: process.env,
|
||||
});
|
||||
return; // Exit if successful
|
||||
}
|
||||
@@ -819,7 +885,7 @@ export class NupstDaemon {
|
||||
// Continue to next method
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
logger.error('All emergency shutdown methods failed');
|
||||
}
|
||||
}
|
||||
@@ -828,6 +894,6 @@ export class NupstDaemon {
|
||||
* Sleep for the specified milliseconds
|
||||
*/
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1 +1 @@
|
||||
export * from './shortid.ts';
|
||||
export * from './shortid.ts';
|
||||
|
@@ -5,11 +5,11 @@
|
||||
export function shortId(): string {
|
||||
// Define the character set: a-z, A-Z, 0-9
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
|
||||
|
||||
// Generate cryptographically secure random values
|
||||
const randomValues = new Uint8Array(6);
|
||||
crypto.getRandomValues(randomValues);
|
||||
|
||||
|
||||
// Map each random value to a character in our set
|
||||
let result = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
@@ -17,6 +17,6 @@ export function shortId(): string {
|
||||
const index = randomValues[i] % chars.length;
|
||||
result += chars[index];
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
import { NupstCli } from './cli.ts';
|
||||
import { logger } from './logger.ts';
|
||||
import process from 'node:process';
|
||||
|
||||
/**
|
||||
* Main entry point for NUPST
|
||||
@@ -13,7 +14,7 @@ async function main() {
|
||||
}
|
||||
|
||||
// Run the main function and handle any errors
|
||||
main().catch(error => {
|
||||
main().catch((error) => {
|
||||
logger.error(`Error: ${error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
24
ts/logger.ts
24
ts/logger.ts
@@ -5,7 +5,7 @@
|
||||
export class Logger {
|
||||
private currentBoxWidth: number | null = null;
|
||||
private static instance: Logger;
|
||||
|
||||
|
||||
/** Default width to use when no width is specified */
|
||||
private readonly DEFAULT_WIDTH = 60;
|
||||
|
||||
@@ -66,14 +66,14 @@ export class Logger {
|
||||
*/
|
||||
public logBoxTitle(title: string, width?: number): void {
|
||||
this.currentBoxWidth = width || this.DEFAULT_WIDTH;
|
||||
|
||||
|
||||
// Create the title line with appropriate padding
|
||||
const paddedTitle = ` ${title} `;
|
||||
const remainingSpace = this.currentBoxWidth - 3 - paddedTitle.length;
|
||||
|
||||
|
||||
// Title line: ┌─ Title ───┐
|
||||
const titleLine = `┌─${paddedTitle}${'─'.repeat(Math.max(0, remainingSpace))}┐`;
|
||||
|
||||
|
||||
console.log(titleLine);
|
||||
}
|
||||
|
||||
@@ -87,12 +87,12 @@ export class Logger {
|
||||
// No current width and no width provided, use default width
|
||||
this.logBoxTitle('', this.DEFAULT_WIDTH);
|
||||
}
|
||||
|
||||
|
||||
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
||||
|
||||
|
||||
// Calculate the available space for content
|
||||
const availableSpace = boxWidth - 2; // Account for left and right borders
|
||||
|
||||
|
||||
if (content.length <= availableSpace - 1) {
|
||||
// If content fits with at least one space for the right border stripe
|
||||
const padding = availableSpace - content.length - 1;
|
||||
@@ -109,10 +109,10 @@ export class Logger {
|
||||
*/
|
||||
public logBoxEnd(width?: number): void {
|
||||
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
|
||||
|
||||
|
||||
// Create the bottom border: └────────┘
|
||||
console.log(`└${'─'.repeat(boxWidth - 2)}┘`);
|
||||
|
||||
|
||||
// Reset the current box width
|
||||
this.currentBoxWidth = null;
|
||||
}
|
||||
@@ -125,11 +125,11 @@ export class Logger {
|
||||
*/
|
||||
public logBox(title: string, lines: string[], width?: number): void {
|
||||
this.logBoxTitle(title, width || this.DEFAULT_WIDTH);
|
||||
|
||||
|
||||
for (const line of lines) {
|
||||
this.logBoxLine(line);
|
||||
}
|
||||
|
||||
|
||||
this.logBoxEnd();
|
||||
}
|
||||
|
||||
@@ -144,4 +144,4 @@ export class Logger {
|
||||
}
|
||||
|
||||
// Export a singleton instance for easy use
|
||||
export const logger = Logger.getInstance();
|
||||
export const logger = Logger.getInstance();
|
||||
|
74
ts/nupst.ts
74
ts/nupst.ts
@@ -6,7 +6,7 @@ import { logger } from './logger.ts';
|
||||
import { UpsHandler } from './cli/ups-handler.ts';
|
||||
import { GroupHandler } from './cli/group-handler.ts';
|
||||
import { ServiceHandler } from './cli/service-handler.ts';
|
||||
import * as https from "node:https";
|
||||
import * as https from 'node:https';
|
||||
|
||||
/**
|
||||
* Main Nupst class that coordinates all components
|
||||
@@ -31,7 +31,7 @@ export class Nupst {
|
||||
this.snmp.setNupst(this); // Set up bidirectional reference
|
||||
this.daemon = new NupstDaemon(this.snmp);
|
||||
this.systemd = new NupstSystemd(this.daemon);
|
||||
|
||||
|
||||
// Initialize handlers
|
||||
this.upsHandler = new UpsHandler(this);
|
||||
this.groupHandler = new GroupHandler(this);
|
||||
@@ -58,28 +58,28 @@ export class Nupst {
|
||||
public getSystemd(): NupstSystemd {
|
||||
return this.systemd;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the UPS handler for UPS management
|
||||
*/
|
||||
public getUpsHandler(): UpsHandler {
|
||||
return this.upsHandler;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the Group handler for group management
|
||||
*/
|
||||
public getGroupHandler(): GroupHandler {
|
||||
return this.groupHandler;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the Service handler for service management
|
||||
*/
|
||||
public getServiceHandler(): ServiceHandler {
|
||||
return this.serviceHandler;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the current version of NUPST
|
||||
* @returns The current version string
|
||||
@@ -87,7 +87,7 @@ export class Nupst {
|
||||
public getVersion(): string {
|
||||
return commitinfo.version;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if an update is available
|
||||
* @returns Promise resolving to true if an update is available
|
||||
@@ -96,34 +96,36 @@ export class Nupst {
|
||||
try {
|
||||
const latestVersion = await this.getLatestVersion();
|
||||
const currentVersion = this.getVersion();
|
||||
|
||||
|
||||
// Compare versions
|
||||
this.updateAvailable = this.compareVersions(latestVersion, currentVersion) > 0;
|
||||
this.latestVersion = latestVersion;
|
||||
|
||||
|
||||
return this.updateAvailable;
|
||||
} catch (error) {
|
||||
logger.error(`Error checking for updates: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.error(
|
||||
`Error checking for updates: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get update status information
|
||||
* @returns Object with update status information
|
||||
*/
|
||||
public getUpdateStatus(): {
|
||||
currentVersion: string,
|
||||
latestVersion: string,
|
||||
updateAvailable: boolean
|
||||
public getUpdateStatus(): {
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
updateAvailable: boolean;
|
||||
} {
|
||||
return {
|
||||
currentVersion: this.getVersion(),
|
||||
latestVersion: this.latestVersion || this.getVersion(),
|
||||
updateAvailable: this.updateAvailable
|
||||
updateAvailable: this.updateAvailable,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the latest version from npm registry
|
||||
* @returns Promise resolving to the latest version string
|
||||
@@ -136,17 +138,17 @@ export class Nupst {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': `nupst/${this.getVersion()}`
|
||||
}
|
||||
'User-Agent': `nupst/${this.getVersion()}`,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let data = '';
|
||||
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
@@ -160,15 +162,15 @@ export class Nupst {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compare two semantic version strings
|
||||
* @param versionA First version
|
||||
@@ -176,39 +178,39 @@ export class Nupst {
|
||||
* @returns -1 if versionA < versionB, 0 if equal, 1 if versionA > versionB
|
||||
*/
|
||||
private compareVersions(versionA: string, versionB: string): number {
|
||||
const partsA = versionA.split('.').map(part => parseInt(part, 10));
|
||||
const partsB = versionB.split('.').map(part => parseInt(part, 10));
|
||||
|
||||
const partsA = versionA.split('.').map((part) => parseInt(part, 10));
|
||||
const partsB = versionB.split('.').map((part) => parseInt(part, 10));
|
||||
|
||||
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
||||
const partA = i < partsA.length ? partsA[i] : 0;
|
||||
const partB = i < partsB.length ? partsB[i] : 0;
|
||||
|
||||
|
||||
if (partA > partB) return 1;
|
||||
if (partA < partB) return -1;
|
||||
}
|
||||
|
||||
|
||||
return 0; // Versions are equal
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Log the current version and update status
|
||||
*/
|
||||
public logVersionInfo(checkForUpdates: boolean = true): void {
|
||||
const version = this.getVersion();
|
||||
const boxWidth = 45;
|
||||
|
||||
|
||||
logger.logBoxTitle('NUPST Version', boxWidth);
|
||||
logger.logBoxLine(`Current Version: ${version}`);
|
||||
|
||||
|
||||
if (this.updateAvailable && this.latestVersion) {
|
||||
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
|
||||
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||
logger.logBoxEnd();
|
||||
} else if (checkForUpdates) {
|
||||
logger.logBoxLine('Checking for updates...');
|
||||
|
||||
|
||||
// We can't end the box yet since we're in an async operation
|
||||
this.checkForUpdates().then(updateAvailable => {
|
||||
this.checkForUpdates().then((updateAvailable) => {
|
||||
if (updateAvailable) {
|
||||
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
|
||||
logger.logBoxLine('Run "sudo nupst update" to update');
|
||||
@@ -224,4 +226,4 @@ export class Nupst {
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
// Re-export all public types
|
||||
export type { IUpsStatus, IOidSet, TUpsModel, ISnmpConfig } from './types.ts';
|
||||
export type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts';
|
||||
|
||||
// Re-export the SNMP manager class
|
||||
export { NupstSnmp } from './manager.ts';
|
||||
export { NupstSnmp } from './manager.ts';
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import * as snmp from "npm:net-snmp@3.20.0";
|
||||
import { Buffer } from "node:buffer";
|
||||
import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.ts';
|
||||
import * as snmp from 'npm:net-snmp@3.20.0';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts';
|
||||
import { UpsOidSets } from './oid-sets.ts';
|
||||
|
||||
/**
|
||||
@@ -34,7 +34,7 @@ export class NupstSnmp {
|
||||
// Set default OID set
|
||||
this.activeOIDs = UpsOidSets.getOidSet('cyberpower');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set reference to the main Nupst instance
|
||||
* @param nupst Reference to the main Nupst instance
|
||||
@@ -42,14 +42,14 @@ export class NupstSnmp {
|
||||
public setNupst(nupst: any): void {
|
||||
this.nupst = nupst;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get reference to the main Nupst instance
|
||||
*/
|
||||
public getNupst(): any {
|
||||
return this.nupst;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enable debug mode
|
||||
*/
|
||||
@@ -71,11 +71,11 @@ export class NupstSnmp {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Use OIDs for the specified UPS model or default to Cyberpower
|
||||
const model = config.upsModel || 'cyberpower';
|
||||
this.activeOIDs = UpsOidSets.getOidSet(model);
|
||||
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`Using OIDs for UPS model: ${model}`);
|
||||
}
|
||||
@@ -89,13 +89,15 @@ export class NupstSnmp {
|
||||
* @returns Promise resolving to the SNMP response value
|
||||
*/
|
||||
public async snmpGet(
|
||||
oid: string,
|
||||
config = this.DEFAULT_CONFIG,
|
||||
retryCount = 0
|
||||
oid: string,
|
||||
config = this.DEFAULT_CONFIG,
|
||||
retryCount = 0,
|
||||
): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.debug) {
|
||||
console.log(`Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`);
|
||||
console.log(
|
||||
`Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`,
|
||||
);
|
||||
console.log('Using community:', config.community);
|
||||
}
|
||||
|
||||
@@ -106,7 +108,7 @@ export class NupstSnmp {
|
||||
timeout: config.timeout,
|
||||
transport: 'udp4',
|
||||
idBitsSize: 32,
|
||||
context: config.context || ''
|
||||
context: config.context || '',
|
||||
};
|
||||
|
||||
// Set version based on config
|
||||
@@ -120,23 +122,23 @@ export class NupstSnmp {
|
||||
|
||||
// Create appropriate session based on SNMP version
|
||||
let session;
|
||||
|
||||
|
||||
if (config.version === 3) {
|
||||
// For SNMPv3, we need to set up authentication and privacy
|
||||
// For SNMPv3, we need a valid security level
|
||||
const securityLevel = config.securityLevel || 'noAuthNoPriv';
|
||||
|
||||
|
||||
// Create the user object with required structure for net-snmp
|
||||
const user: any = {
|
||||
name: config.username || ''
|
||||
name: config.username || '',
|
||||
};
|
||||
|
||||
|
||||
// Set security level
|
||||
if (securityLevel === 'noAuthNoPriv') {
|
||||
user.level = snmp.SecurityLevel.noAuthNoPriv;
|
||||
} else if (securityLevel === 'authNoPriv') {
|
||||
user.level = snmp.SecurityLevel.authNoPriv;
|
||||
|
||||
|
||||
// Set auth protocol - must provide both protocol and key
|
||||
if (config.authProtocol && config.authKey) {
|
||||
if (config.authProtocol === 'MD5') {
|
||||
@@ -154,7 +156,7 @@ export class NupstSnmp {
|
||||
}
|
||||
} else if (securityLevel === 'authPriv') {
|
||||
user.level = snmp.SecurityLevel.authPriv;
|
||||
|
||||
|
||||
// Set auth protocol - must provide both protocol and key
|
||||
if (config.authProtocol && config.authKey) {
|
||||
if (config.authProtocol === 'MD5') {
|
||||
@@ -163,7 +165,7 @@ export class NupstSnmp {
|
||||
user.authProtocol = snmp.AuthProtocols.sha;
|
||||
}
|
||||
user.authKey = config.authKey;
|
||||
|
||||
|
||||
// Set privacy protocol - must provide both protocol and key
|
||||
if (config.privProtocol && config.privKey) {
|
||||
if (config.privProtocol === 'DES') {
|
||||
@@ -187,18 +189,20 @@ export class NupstSnmp {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.debug) {
|
||||
console.log('SNMPv3 user configuration:', {
|
||||
name: user.name,
|
||||
level: Object.keys(snmp.SecurityLevel).find(key => snmp.SecurityLevel[key] === user.level),
|
||||
level: Object.keys(snmp.SecurityLevel).find((key) =>
|
||||
snmp.SecurityLevel[key] === user.level
|
||||
),
|
||||
authProtocol: user.authProtocol ? 'Set' : 'Not Set',
|
||||
authKey: user.authKey ? 'Set' : 'Not Set',
|
||||
privProtocol: user.privProtocol ? 'Set' : 'Not Set',
|
||||
privKey: user.privKey ? 'Set' : 'Not Set'
|
||||
privKey: user.privKey ? 'Set' : 'Not Set',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
session = snmp.createV3Session(config.host, user, options);
|
||||
} else {
|
||||
// For SNMPv1/v2c, we use the community string
|
||||
@@ -230,9 +234,11 @@ export class NupstSnmp {
|
||||
}
|
||||
|
||||
// Check for SNMP errors in the response
|
||||
if (varbinds[0].type === snmp.ObjectType.NoSuchObject ||
|
||||
varbinds[0].type === snmp.ObjectType.NoSuchInstance ||
|
||||
varbinds[0].type === snmp.ObjectType.EndOfMibView) {
|
||||
if (
|
||||
varbinds[0].type === snmp.ObjectType.NoSuchObject ||
|
||||
varbinds[0].type === snmp.ObjectType.NoSuchInstance ||
|
||||
varbinds[0].type === snmp.ObjectType.EndOfMibView
|
||||
) {
|
||||
if (this.debug) {
|
||||
console.error('SNMP error:', snmp.ObjectType[varbinds[0].type]);
|
||||
}
|
||||
@@ -259,7 +265,7 @@ export class NupstSnmp {
|
||||
console.log('SNMP response:', {
|
||||
oid: varbinds[0].oid,
|
||||
type: varbinds[0].type,
|
||||
value: value
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -277,7 +283,7 @@ export class NupstSnmp {
|
||||
try {
|
||||
// Set active OID set based on UPS model in config
|
||||
this.setActiveOIDs(config);
|
||||
|
||||
|
||||
if (this.debug) {
|
||||
console.log('---------------------------------------');
|
||||
console.log('Getting UPS status with config:');
|
||||
@@ -300,18 +306,30 @@ export class NupstSnmp {
|
||||
console.log(' Battery Runtime:', this.activeOIDs.BATTERY_RUNTIME);
|
||||
console.log('---------------------------------------');
|
||||
}
|
||||
|
||||
|
||||
// Get all values with independent retry logic
|
||||
const powerStatusValue = await this.getSNMPValueWithRetry(this.activeOIDs.POWER_STATUS, 'power status', config);
|
||||
const batteryCapacity = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_CAPACITY, 'battery capacity', config) || 0;
|
||||
const batteryRuntime = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_RUNTIME, 'battery runtime', config) || 0;
|
||||
|
||||
const powerStatusValue = await this.getSNMPValueWithRetry(
|
||||
this.activeOIDs.POWER_STATUS,
|
||||
'power status',
|
||||
config,
|
||||
);
|
||||
const batteryCapacity = await this.getSNMPValueWithRetry(
|
||||
this.activeOIDs.BATTERY_CAPACITY,
|
||||
'battery capacity',
|
||||
config,
|
||||
) || 0;
|
||||
const batteryRuntime = await this.getSNMPValueWithRetry(
|
||||
this.activeOIDs.BATTERY_RUNTIME,
|
||||
'battery runtime',
|
||||
config,
|
||||
) || 0;
|
||||
|
||||
// Determine power status - handle different values for different UPS models
|
||||
const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue);
|
||||
|
||||
|
||||
// Convert to minutes for UPS models with different time units
|
||||
const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime);
|
||||
|
||||
|
||||
const result = {
|
||||
powerStatus,
|
||||
batteryCapacity,
|
||||
@@ -322,7 +340,7 @@ export class NupstSnmp {
|
||||
batteryRuntime,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
if (this.debug) {
|
||||
console.log('---------------------------------------');
|
||||
console.log('UPS status result:');
|
||||
@@ -331,15 +349,20 @@ export class NupstSnmp {
|
||||
console.log(' Battery Runtime:', result.batteryRuntime, 'minutes');
|
||||
console.log('---------------------------------------');
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (this.debug) {
|
||||
console.error('---------------------------------------');
|
||||
console.error('Error getting UPS status:', error instanceof Error ? error.message : String(error));
|
||||
console.error(
|
||||
'Error getting UPS status:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
console.error('---------------------------------------');
|
||||
}
|
||||
throw new Error(`Failed to get UPS status: ${error instanceof Error ? error.message : String(error)}`);
|
||||
throw new Error(
|
||||
`Failed to get UPS status: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,9 +374,9 @@ export class NupstSnmp {
|
||||
* @returns Promise resolving to the SNMP value
|
||||
*/
|
||||
private async getSNMPValueWithRetry(
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig,
|
||||
): Promise<any> {
|
||||
if (oid === '') {
|
||||
if (this.debug) {
|
||||
@@ -361,11 +384,11 @@ export class NupstSnmp {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`Getting ${description} OID: ${oid}`);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const value = await this.snmpGet(oid, config);
|
||||
if (this.debug) {
|
||||
@@ -374,19 +397,22 @@ export class NupstSnmp {
|
||||
return value;
|
||||
} catch (error) {
|
||||
if (this.debug) {
|
||||
console.error(`Error getting ${description}:`, error instanceof Error ? error.message : String(error));
|
||||
console.error(
|
||||
`Error getting ${description}:`,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// If we're using SNMPv3, try with different security levels
|
||||
if (config.version === 3) {
|
||||
return await this.tryFallbackSecurityLevels(oid, description, config);
|
||||
}
|
||||
|
||||
|
||||
// Try with standard OIDs as fallback
|
||||
if (config.upsModel !== 'custom') {
|
||||
return await this.tryStandardOids(oid, description, config);
|
||||
}
|
||||
|
||||
|
||||
// Return a default value if all attempts fail
|
||||
if (this.debug) {
|
||||
console.log(`Using default value 0 for ${description}`);
|
||||
@@ -403,14 +429,14 @@ export class NupstSnmp {
|
||||
* @returns Promise resolving to the SNMP value
|
||||
*/
|
||||
private async tryFallbackSecurityLevels(
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig,
|
||||
): Promise<any> {
|
||||
if (this.debug) {
|
||||
console.log(`Retrying ${description} with fallback security level...`);
|
||||
}
|
||||
|
||||
|
||||
// Try with authNoPriv if current level is authPriv
|
||||
if (config.securityLevel === 'authPriv') {
|
||||
const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' };
|
||||
@@ -425,7 +451,10 @@ export class NupstSnmp {
|
||||
return value;
|
||||
} catch (retryError) {
|
||||
if (this.debug) {
|
||||
console.error(`Retry failed for ${description}:`, retryError instanceof Error ? retryError.message : String(retryError));
|
||||
console.error(
|
||||
`Retry failed for ${description}:`,
|
||||
retryError instanceof Error ? retryError.message : String(retryError),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -444,7 +473,10 @@ export class NupstSnmp {
|
||||
return value;
|
||||
} catch (retryError) {
|
||||
if (this.debug) {
|
||||
console.error(`Retry failed for ${description}:`, retryError instanceof Error ? retryError.message : String(retryError));
|
||||
console.error(
|
||||
`Retry failed for ${description}:`,
|
||||
retryError instanceof Error ? retryError.message : String(retryError),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,18 +492,20 @@ export class NupstSnmp {
|
||||
* @returns Promise resolving to the SNMP value
|
||||
*/
|
||||
private async tryStandardOids(
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig
|
||||
oid: string,
|
||||
description: string,
|
||||
config: ISnmpConfig,
|
||||
): Promise<any> {
|
||||
try {
|
||||
// Try RFC 1628 standard UPS MIB OIDs
|
||||
const standardOIDs = UpsOidSets.getStandardOids();
|
||||
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`);
|
||||
console.log(
|
||||
`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const standardValue = await this.snmpGet(standardOIDs[description], config);
|
||||
if (this.debug) {
|
||||
console.log(`${description} standard OID value:`, standardValue);
|
||||
@@ -479,10 +513,13 @@ export class NupstSnmp {
|
||||
return standardValue;
|
||||
} catch (stdError) {
|
||||
if (this.debug) {
|
||||
console.error(`Standard OID retry failed for ${description}:`, stdError instanceof Error ? stdError.message : String(stdError));
|
||||
console.error(
|
||||
`Standard OID retry failed for ${description}:`,
|
||||
stdError instanceof Error ? stdError.message : String(stdError),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -493,8 +530,8 @@ export class NupstSnmp {
|
||||
* @returns Standardized power status
|
||||
*/
|
||||
private determinePowerStatus(
|
||||
upsModel: TUpsModel | undefined,
|
||||
powerStatusValue: number
|
||||
upsModel: TUpsModel | undefined,
|
||||
powerStatusValue: number,
|
||||
): 'online' | 'onBattery' | 'unknown' {
|
||||
if (upsModel === 'cyberpower') {
|
||||
// CyberPower RMCARD205: upsBaseOutputStatus values
|
||||
@@ -528,7 +565,7 @@ export class NupstSnmp {
|
||||
return 'onBattery';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
@@ -539,25 +576,29 @@ export class NupstSnmp {
|
||||
* @returns Processed runtime in minutes
|
||||
*/
|
||||
private processRuntimeValue(
|
||||
upsModel: TUpsModel | undefined,
|
||||
batteryRuntime: number
|
||||
upsModel: TUpsModel | undefined,
|
||||
batteryRuntime: number,
|
||||
): number {
|
||||
if (this.debug) {
|
||||
console.log('Raw runtime value:', batteryRuntime);
|
||||
}
|
||||
|
||||
|
||||
if (upsModel === 'cyberpower' && batteryRuntime > 0) {
|
||||
// CyberPower: TimeTicks is in 1/100 seconds, convert to minutes
|
||||
const minutes = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute
|
||||
if (this.debug) {
|
||||
console.log(`Converting CyberPower runtime from ${batteryRuntime} ticks to ${minutes} minutes`);
|
||||
console.log(
|
||||
`Converting CyberPower runtime from ${batteryRuntime} ticks to ${minutes} minutes`,
|
||||
);
|
||||
}
|
||||
return minutes;
|
||||
} else if (upsModel === 'eaton' && batteryRuntime > 0) {
|
||||
// Eaton: Runtime is in seconds, convert to minutes
|
||||
const minutes = Math.floor(batteryRuntime / 60);
|
||||
if (this.debug) {
|
||||
console.log(`Converting Eaton runtime from ${batteryRuntime} seconds to ${minutes} minutes`);
|
||||
console.log(
|
||||
`Converting Eaton runtime from ${batteryRuntime} seconds to ${minutes} minutes`,
|
||||
);
|
||||
}
|
||||
return minutes;
|
||||
} else if (batteryRuntime > 10000) {
|
||||
@@ -568,7 +609,7 @@ export class NupstSnmp {
|
||||
}
|
||||
return minutes;
|
||||
}
|
||||
|
||||
|
||||
return batteryRuntime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -15,41 +15,41 @@ export class UpsOidSets {
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage)
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks)
|
||||
},
|
||||
|
||||
|
||||
// APC OIDs
|
||||
apc: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // Power status (1=online, 2=on battery)
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes
|
||||
},
|
||||
|
||||
|
||||
// Eaton OIDs
|
||||
eaton: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource (3=normal/mains, 5=battery)
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage)
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds)
|
||||
},
|
||||
|
||||
|
||||
// TrippLite OIDs
|
||||
tripplite: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // Power status
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes
|
||||
},
|
||||
|
||||
|
||||
// Liebert/Vertiv OIDs
|
||||
liebert: {
|
||||
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // Power status
|
||||
BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage
|
||||
BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes
|
||||
},
|
||||
|
||||
|
||||
// Custom OIDs (to be provided by the user)
|
||||
custom: {
|
||||
POWER_STATUS: '',
|
||||
BATTERY_CAPACITY: '',
|
||||
BATTERY_RUNTIME: '',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -67,9 +67,9 @@ export class UpsOidSets {
|
||||
*/
|
||||
public static getStandardOids(): Record<string, string> {
|
||||
return {
|
||||
'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource
|
||||
'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining
|
||||
'battery runtime': '1.3.6.1.2.1.33.1.2.3.0' // upsEstimatedMinutesRemaining
|
||||
'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource
|
||||
'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining
|
||||
'battery runtime': '1.3.6.1.2.1.33.1.2.3.0', // upsEstimatedMinutesRemaining
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
* Type definitions for SNMP module
|
||||
*/
|
||||
|
||||
import { Buffer } from "node:buffer";
|
||||
import { Buffer } from 'node:buffer';
|
||||
|
||||
/**
|
||||
* UPS status interface
|
||||
@@ -49,11 +49,11 @@ export interface ISnmpConfig {
|
||||
timeout: number;
|
||||
|
||||
context?: string;
|
||||
|
||||
|
||||
// SNMPv1/v2c
|
||||
/** Community string for SNMPv1/v2c */
|
||||
community?: string;
|
||||
|
||||
|
||||
// SNMPv3
|
||||
/** Security level for SNMPv3 */
|
||||
securityLevel?: 'noAuthNoPriv' | 'authNoPriv' | 'authPriv';
|
||||
@@ -67,7 +67,7 @@ export interface ISnmpConfig {
|
||||
privProtocol?: 'DES' | 'AES';
|
||||
/** Privacy key for SNMPv3 */
|
||||
privKey?: string;
|
||||
|
||||
|
||||
// UPS model and custom OIDs
|
||||
/** UPS model for OID selection */
|
||||
upsModel?: TUpsModel;
|
||||
@@ -91,4 +91,4 @@ export interface ISnmpV3SecurityParams {
|
||||
msgAuthenticationParameters: Buffer;
|
||||
/** Privacy parameters */
|
||||
msgPrivacyParameters: Buffer;
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import process from 'node:process';
|
||||
import { promises as fs } from "node:fs";
|
||||
import { execSync } from "node:child_process";
|
||||
import { promises as fs } from 'node:fs';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { NupstDaemon } from './daemon.ts';
|
||||
import { logger } from './logger.ts';
|
||||
|
||||
@@ -66,7 +66,7 @@ WantedBy=multi-user.target
|
||||
try {
|
||||
// Check if configuration exists before installing
|
||||
await this.checkConfigExists();
|
||||
|
||||
|
||||
// Write the service file
|
||||
await fs.writeFile(this.serviceFilePath, this.serviceTemplate);
|
||||
const boxWidth = 50;
|
||||
@@ -99,7 +99,7 @@ WantedBy=multi-user.target
|
||||
try {
|
||||
// Check if configuration exists before starting
|
||||
await this.checkConfigExists();
|
||||
|
||||
|
||||
execSync('systemctl start nupst.service');
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Service Status', boxWidth);
|
||||
@@ -143,10 +143,10 @@ WantedBy=multi-user.target
|
||||
logger.logBoxEnd();
|
||||
this.daemon.getNupstSnmp().enableDebug();
|
||||
}
|
||||
|
||||
|
||||
// Display version information
|
||||
this.daemon.getNupstSnmp().getNupst().logVersionInfo();
|
||||
|
||||
|
||||
// Check if config exists first
|
||||
try {
|
||||
await this.checkConfigExists();
|
||||
@@ -154,11 +154,13 @@ WantedBy=multi-user.target
|
||||
// Error message already displayed by checkConfigExists
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await this.displayServiceStatus();
|
||||
await this.displayAllUpsStatus();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get status: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.error(
|
||||
`Failed to get status: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +174,7 @@ WantedBy=multi-user.target
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('Service Status', boxWidth);
|
||||
// Process each line of the status output
|
||||
serviceStatus.split('\n').forEach(line => {
|
||||
serviceStatus.split('\n').forEach((line) => {
|
||||
logger.logBoxLine(line);
|
||||
});
|
||||
logger.logBoxEnd();
|
||||
@@ -194,11 +196,11 @@ WantedBy=multi-user.target
|
||||
await this.daemon.loadConfig();
|
||||
const config = this.daemon.getConfig();
|
||||
const snmp = this.daemon.getNupstSnmp();
|
||||
|
||||
|
||||
// Check if we have the new multi-UPS config format
|
||||
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
|
||||
logger.log(`Found ${config.upsDevices.length} UPS device(s) in configuration`);
|
||||
|
||||
|
||||
// Show status for each UPS
|
||||
for (const ups of config.upsDevices) {
|
||||
await this.displaySingleUpsStatus(ups, snmp);
|
||||
@@ -210,9 +212,9 @@ WantedBy=multi-user.target
|
||||
name: 'Default UPS',
|
||||
snmp: config.snmp,
|
||||
thresholds: config.thresholds,
|
||||
groups: []
|
||||
groups: [],
|
||||
};
|
||||
|
||||
|
||||
await this.displaySingleUpsStatus(legacyUps, snmp);
|
||||
} else {
|
||||
logger.error('No UPS devices found in configuration');
|
||||
@@ -220,11 +222,13 @@ WantedBy=multi-user.target
|
||||
} catch (error) {
|
||||
const boxWidth = 45;
|
||||
logger.logBoxTitle('UPS Status', boxWidth);
|
||||
logger.logBoxLine(`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.logBoxLine(
|
||||
`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display status of a single UPS
|
||||
* @param ups UPS configuration
|
||||
@@ -236,7 +240,7 @@ WantedBy=multi-user.target
|
||||
logger.logBoxLine(`ID: ${ups.id}`);
|
||||
logger.logBoxLine(`Host: ${ups.snmp.host}:${ups.snmp.port}`);
|
||||
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel || 'cyberpower'}`);
|
||||
|
||||
|
||||
if (ups.groups && ups.groups.length > 0) {
|
||||
// Get group names if available
|
||||
const config = this.daemon.getConfig();
|
||||
@@ -246,37 +250,43 @@ WantedBy=multi-user.target
|
||||
});
|
||||
logger.logBoxLine(`Groups: ${groupNames.join(', ')}`);
|
||||
}
|
||||
|
||||
|
||||
logger.logBoxEnd();
|
||||
|
||||
|
||||
try {
|
||||
// Create a test config with a short timeout
|
||||
const testConfig = {
|
||||
...ups.snmp,
|
||||
timeout: Math.min(ups.snmp.timeout, 10000) // Use at most 10 seconds for status check
|
||||
timeout: Math.min(ups.snmp.timeout, 10000), // Use at most 10 seconds for status check
|
||||
};
|
||||
|
||||
|
||||
const status = await snmp.getUpsStatus(testConfig);
|
||||
|
||||
|
||||
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
|
||||
logger.logBoxLine(`Power Status: ${status.powerStatus}`);
|
||||
logger.logBoxLine(`Battery Capacity: ${status.batteryCapacity}%`);
|
||||
logger.logBoxLine(`Runtime Remaining: ${status.batteryRuntime} minutes`);
|
||||
|
||||
|
||||
// Show threshold status
|
||||
logger.logBoxLine('');
|
||||
logger.logBoxLine('Thresholds:');
|
||||
logger.logBoxLine(` Battery: ${status.batteryCapacity}% / ${ups.thresholds.battery}% ${
|
||||
status.batteryCapacity < ups.thresholds.battery ? '⚠️' : '✓'
|
||||
}`);
|
||||
logger.logBoxLine(` Runtime: ${status.batteryRuntime} min / ${ups.thresholds.runtime} min ${
|
||||
status.batteryRuntime < ups.thresholds.runtime ? '⚠️' : '✓'
|
||||
}`);
|
||||
|
||||
logger.logBoxLine(
|
||||
` Battery: ${status.batteryCapacity}% / ${ups.thresholds.battery}% ${
|
||||
status.batteryCapacity < ups.thresholds.battery ? '⚠️' : '✓'
|
||||
}`,
|
||||
);
|
||||
logger.logBoxLine(
|
||||
` Runtime: ${status.batteryRuntime} min / ${ups.thresholds.runtime} min ${
|
||||
status.batteryRuntime < ups.thresholds.runtime ? '⚠️' : '✓'
|
||||
}`,
|
||||
);
|
||||
|
||||
logger.logBoxEnd();
|
||||
} catch (error) {
|
||||
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
|
||||
logger.logBoxLine(`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`);
|
||||
logger.logBoxLine(
|
||||
`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
}
|
||||
@@ -290,7 +300,7 @@ WantedBy=multi-user.target
|
||||
await this.stopService();
|
||||
await this.disableService();
|
||||
await this.removeServiceFile();
|
||||
|
||||
|
||||
// Reload systemd daemon
|
||||
execSync('systemctl daemon-reload');
|
||||
logger.log('Systemd daemon reloaded');
|
||||
@@ -341,4 +351,4 @@ WantedBy=multi-user.target
|
||||
logger.log('Service file did not exist');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user