Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
992a776fd2 | |||
3e15a2d52f | |||
d1a3576d31 | |||
1ca05e879b | |||
9c6fa37eb8 | |||
ff433b2256 | |||
263d69aef1 | |||
b6b7b43161 | |||
316c66c344 | |||
4debda856b | |||
0e7bcab499 | |||
7bf65d8495 | |||
f2ce0180d3 | |||
8c1be6555f |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/nupst",
|
"name": "@serve.zone/nupst",
|
||||||
"version": "4.2.1",
|
"version": "4.3.2",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"dev": "deno run --allow-all mod.ts",
|
"dev": "deno run --allow-all mod.ts",
|
||||||
|
65
ts/cli.ts
65
ts/cli.ts
@@ -72,6 +72,7 @@ export class NupstCli {
|
|||||||
const upsHandler = this.nupst.getUpsHandler();
|
const upsHandler = this.nupst.getUpsHandler();
|
||||||
const groupHandler = this.nupst.getGroupHandler();
|
const groupHandler = this.nupst.getGroupHandler();
|
||||||
const serviceHandler = this.nupst.getServiceHandler();
|
const serviceHandler = this.nupst.getServiceHandler();
|
||||||
|
const actionHandler = this.nupst.getActionHandler();
|
||||||
|
|
||||||
// Handle service subcommands
|
// Handle service subcommands
|
||||||
if (command === 'service') {
|
if (command === 'service') {
|
||||||
@@ -193,6 +194,38 @@ export class NupstCli {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle action subcommands
|
||||||
|
if (command === 'action') {
|
||||||
|
const subcommand = commandArgs[0] || 'list';
|
||||||
|
const subcommandArgs = commandArgs.slice(1);
|
||||||
|
|
||||||
|
switch (subcommand) {
|
||||||
|
case 'add': {
|
||||||
|
const upsId = subcommandArgs[0];
|
||||||
|
await actionHandler.add(upsId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'remove':
|
||||||
|
case 'rm': // Alias
|
||||||
|
case 'delete': { // Backward compatibility
|
||||||
|
const upsId = subcommandArgs[0];
|
||||||
|
const actionIndex = subcommandArgs[1];
|
||||||
|
await actionHandler.remove(upsId, actionIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'list':
|
||||||
|
case 'ls': { // Alias
|
||||||
|
const upsId = subcommandArgs[0];
|
||||||
|
await actionHandler.list(upsId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
this.showActionHelp();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle config subcommand
|
// Handle config subcommand
|
||||||
if (command === 'config') {
|
if (command === 'config') {
|
||||||
const subcommand = commandArgs[0] || 'show';
|
const subcommand = commandArgs[0] || 'show';
|
||||||
@@ -499,6 +532,7 @@ export class NupstCli {
|
|||||||
this.printCommand('service <subcommand>', 'Manage systemd service');
|
this.printCommand('service <subcommand>', 'Manage systemd service');
|
||||||
this.printCommand('ups <subcommand>', 'Manage UPS devices');
|
this.printCommand('ups <subcommand>', 'Manage UPS devices');
|
||||||
this.printCommand('group <subcommand>', 'Manage UPS groups');
|
this.printCommand('group <subcommand>', 'Manage UPS groups');
|
||||||
|
this.printCommand('action <subcommand>', 'Manage UPS actions');
|
||||||
this.printCommand('config [show]', 'Display current configuration');
|
this.printCommand('config [show]', 'Display current configuration');
|
||||||
this.printCommand('update', 'Update NUPST from repository', theme.dim('(requires root)'));
|
this.printCommand('update', 'Update NUPST from repository', theme.dim('(requires root)'));
|
||||||
this.printCommand('uninstall', 'Completely remove NUPST', theme.dim('(requires root)'));
|
this.printCommand('uninstall', 'Completely remove NUPST', theme.dim('(requires root)'));
|
||||||
@@ -535,6 +569,13 @@ export class NupstCli {
|
|||||||
this.printCommand('nupst group list (or ls)', 'List all UPS groups');
|
this.printCommand('nupst group list (or ls)', 'List all UPS groups');
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
// Action subcommands
|
||||||
|
logger.log(theme.info('Action Subcommands:'));
|
||||||
|
this.printCommand('nupst action add <target-id>', 'Add a new action to a UPS or group');
|
||||||
|
this.printCommand('nupst action remove <target-id> <index>', 'Remove an action by index');
|
||||||
|
this.printCommand('nupst action list [target-id]', 'List all actions (optionally for specific target)');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
// Options
|
// Options
|
||||||
logger.log(theme.info('Options:'));
|
logger.log(theme.info('Options:'));
|
||||||
this.printCommand('--debug, -d', 'Enable debug mode for detailed SNMP logging');
|
this.printCommand('--debug, -d', 'Enable debug mode for detailed SNMP logging');
|
||||||
@@ -639,6 +680,30 @@ Examples:
|
|||||||
nupst group add - Create a new group
|
nupst group add - Create a new group
|
||||||
nupst group edit dc-1 - Edit group with ID 'dc-1'
|
nupst group edit dc-1 - Edit group with ID 'dc-1'
|
||||||
nupst group remove dc-1 - Remove group with ID 'dc-1'
|
nupst group remove dc-1 - Remove group with ID 'dc-1'
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private showActionHelp(): void {
|
||||||
|
logger.log(`
|
||||||
|
NUPST - Action Management Commands
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
nupst action <subcommand> [arguments]
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
add <ups-id|group-id> - Add a new action to a UPS or group interactively
|
||||||
|
remove <ups-id|group-id> <index> - Remove an action by index (alias: rm, delete)
|
||||||
|
list [ups-id|group-id] - List all actions (optionally for specific target) (alias: ls)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--debug, -d - Enable debug mode for detailed logging
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
nupst action list - List actions for all UPS devices and groups
|
||||||
|
nupst action list default - List actions for UPS or group with ID 'default'
|
||||||
|
nupst action add default - Add a new action to UPS or group 'default'
|
||||||
|
nupst action remove default 0 - Remove action at index 0 from UPS or group 'default'
|
||||||
|
nupst action add dc-rack-1 - Add a new action to group 'dc-rack-1'
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
357
ts/cli/action-handler.ts
Normal file
357
ts/cli/action-handler.ts
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
import process from 'node:process';
|
||||||
|
import { Nupst } from '../nupst.ts';
|
||||||
|
import { logger, type ITableColumn } from '../logger.ts';
|
||||||
|
import { theme, symbols } from '../colors.ts';
|
||||||
|
import type { IActionConfig } from '../actions/base-action.ts';
|
||||||
|
import type { IUpsConfig, IGroupConfig } from '../daemon.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for handling action-related CLI commands
|
||||||
|
* Provides interface for managing UPS actions
|
||||||
|
*/
|
||||||
|
export class ActionHandler {
|
||||||
|
private readonly nupst: Nupst;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new action handler
|
||||||
|
* @param nupst Reference to the main Nupst instance
|
||||||
|
*/
|
||||||
|
constructor(nupst: Nupst) {
|
||||||
|
this.nupst = nupst;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new action to a UPS or group
|
||||||
|
*/
|
||||||
|
public async add(targetId?: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!targetId) {
|
||||||
|
logger.error('Target ID is required');
|
||||||
|
logger.log(
|
||||||
|
` ${theme.dim('Usage:')} ${theme.command('nupst action add <ups-id|group-id>')}`,
|
||||||
|
);
|
||||||
|
logger.log('');
|
||||||
|
logger.log(` ${theme.dim('List UPS devices:')} ${theme.command('nupst ups list')}`);
|
||||||
|
logger.log(` ${theme.dim('List groups:')} ${theme.command('nupst group list')}`);
|
||||||
|
logger.log('');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await this.nupst.getDaemon().loadConfig();
|
||||||
|
|
||||||
|
// Check if it's a UPS
|
||||||
|
const ups = config.upsDevices.find((u) => u.id === targetId);
|
||||||
|
// Check if it's a group
|
||||||
|
const group = config.groups?.find((g) => g.id === targetId);
|
||||||
|
|
||||||
|
if (!ups && !group) {
|
||||||
|
logger.error(`UPS or Group with ID '${targetId}' not found`);
|
||||||
|
logger.log('');
|
||||||
|
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
|
||||||
|
logger.log(` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
|
||||||
|
logger.log('');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = ups || group;
|
||||||
|
const targetType = ups ? 'UPS' : 'Group';
|
||||||
|
const targetName = ups ? ups.name : group!.name;
|
||||||
|
|
||||||
|
const readline = await import('node:readline');
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
|
||||||
|
const prompt = (question: string): Promise<string> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(question, (answer: string) => {
|
||||||
|
resolve(answer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.log('');
|
||||||
|
logger.info(`Add Action to ${targetType} ${theme.highlight(targetName)}`);
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
|
// Action type (currently only shutdown is supported)
|
||||||
|
const type = 'shutdown';
|
||||||
|
logger.log(` ${theme.dim('Action type:')} ${theme.highlight('shutdown')}`);
|
||||||
|
|
||||||
|
// Battery threshold
|
||||||
|
const batteryStr = await prompt(
|
||||||
|
` ${theme.dim('Battery threshold')} ${theme.dim('(%):')} `,
|
||||||
|
);
|
||||||
|
const battery = parseInt(batteryStr, 10);
|
||||||
|
if (isNaN(battery) || battery < 0 || battery > 100) {
|
||||||
|
logger.error('Invalid battery threshold. Must be 0-100.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runtime threshold
|
||||||
|
const runtimeStr = await prompt(
|
||||||
|
` ${theme.dim('Runtime threshold')} ${theme.dim('(minutes):')} `,
|
||||||
|
);
|
||||||
|
const runtime = parseInt(runtimeStr, 10);
|
||||||
|
if (isNaN(runtime) || runtime < 0) {
|
||||||
|
logger.error('Invalid runtime threshold. Must be >= 0.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger mode
|
||||||
|
logger.log('');
|
||||||
|
logger.log(` ${theme.dim('Trigger mode:')}`);
|
||||||
|
logger.log(` ${theme.dim('1)')} onlyPowerChanges - Trigger only when power status changes`);
|
||||||
|
logger.log(
|
||||||
|
` ${theme.dim('2)')} onlyThresholds - Trigger only when thresholds are violated`,
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
` ${theme.dim('3)')} powerChangesAndThresholds - Trigger on power change AND thresholds`,
|
||||||
|
);
|
||||||
|
logger.log(` ${theme.dim('4)')} anyChange - Trigger on any status change`);
|
||||||
|
const triggerChoice = await prompt(` ${theme.dim('Choice')} ${theme.dim('[2]:')} `);
|
||||||
|
const triggerModeMap: Record<string, string> = {
|
||||||
|
'1': 'onlyPowerChanges',
|
||||||
|
'2': 'onlyThresholds',
|
||||||
|
'3': 'powerChangesAndThresholds',
|
||||||
|
'4': 'anyChange',
|
||||||
|
'': 'onlyThresholds', // Default
|
||||||
|
};
|
||||||
|
const triggerMode = triggerModeMap[triggerChoice] || 'onlyThresholds';
|
||||||
|
|
||||||
|
// Shutdown delay
|
||||||
|
const delayStr = await prompt(
|
||||||
|
` ${theme.dim('Shutdown delay')} ${theme.dim('(seconds) [5]:')} `,
|
||||||
|
);
|
||||||
|
const shutdownDelay = delayStr ? parseInt(delayStr, 10) : 5;
|
||||||
|
if (isNaN(shutdownDelay) || shutdownDelay < 0) {
|
||||||
|
logger.error('Invalid shutdown delay. Must be >= 0.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the action
|
||||||
|
const newAction: IActionConfig = {
|
||||||
|
type,
|
||||||
|
thresholds: {
|
||||||
|
battery,
|
||||||
|
runtime,
|
||||||
|
},
|
||||||
|
triggerMode: triggerMode as IActionConfig['triggerMode'],
|
||||||
|
shutdownDelay,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to target (UPS or group)
|
||||||
|
if (!target!.actions) {
|
||||||
|
target!.actions = [];
|
||||||
|
}
|
||||||
|
target!.actions.push(newAction);
|
||||||
|
|
||||||
|
await this.nupst.getDaemon().saveConfig(config);
|
||||||
|
|
||||||
|
logger.log('');
|
||||||
|
logger.success(`Action added to ${targetType} ${targetName}`);
|
||||||
|
logger.log(` ${theme.dim('Changes saved and will be applied automatically')}`);
|
||||||
|
logger.log('');
|
||||||
|
} finally {
|
||||||
|
rl.close();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to add action: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an action from a UPS or group
|
||||||
|
*/
|
||||||
|
public async remove(targetId?: string, actionIndexStr?: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!targetId || !actionIndexStr) {
|
||||||
|
logger.error('Target ID and action index are required');
|
||||||
|
logger.log(
|
||||||
|
` ${theme.dim('Usage:')} ${theme.command('nupst action remove <ups-id|group-id> <action-index>')}`,
|
||||||
|
);
|
||||||
|
logger.log('');
|
||||||
|
logger.log(` ${theme.dim('List actions:')} ${theme.command('nupst action list')}`);
|
||||||
|
logger.log('');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionIndex = parseInt(actionIndexStr, 10);
|
||||||
|
if (isNaN(actionIndex) || actionIndex < 0) {
|
||||||
|
logger.error('Invalid action index. Must be >= 0.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await this.nupst.getDaemon().loadConfig();
|
||||||
|
|
||||||
|
// Check if it's a UPS
|
||||||
|
const ups = config.upsDevices.find((u) => u.id === targetId);
|
||||||
|
// Check if it's a group
|
||||||
|
const group = config.groups?.find((g) => g.id === targetId);
|
||||||
|
|
||||||
|
if (!ups && !group) {
|
||||||
|
logger.error(`UPS or Group with ID '${targetId}' not found`);
|
||||||
|
logger.log('');
|
||||||
|
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
|
||||||
|
logger.log(` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
|
||||||
|
logger.log('');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = ups || group;
|
||||||
|
const targetType = ups ? 'UPS' : 'Group';
|
||||||
|
const targetName = ups ? ups.name : group!.name;
|
||||||
|
|
||||||
|
if (!target!.actions || target!.actions.length === 0) {
|
||||||
|
logger.error(`No actions configured for ${targetType} '${targetName}'`);
|
||||||
|
logger.log('');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionIndex >= target!.actions.length) {
|
||||||
|
logger.error(
|
||||||
|
`Invalid action index. ${targetType} '${targetName}' has ${target!.actions.length} action(s) (index 0-${target!.actions.length - 1})`,
|
||||||
|
);
|
||||||
|
logger.log('');
|
||||||
|
logger.log(
|
||||||
|
` ${theme.dim('List actions:')} ${theme.command(`nupst action list ${targetId}`)}`,
|
||||||
|
);
|
||||||
|
logger.log('');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedAction = target!.actions[actionIndex];
|
||||||
|
target!.actions.splice(actionIndex, 1);
|
||||||
|
|
||||||
|
await this.nupst.getDaemon().saveConfig(config);
|
||||||
|
|
||||||
|
logger.log('');
|
||||||
|
logger.success(`Action removed from ${targetType} ${targetName}`);
|
||||||
|
logger.log(` ${theme.dim('Type:')} ${removedAction.type}`);
|
||||||
|
if (removedAction.thresholds) {
|
||||||
|
logger.log(
|
||||||
|
` ${theme.dim('Thresholds:')} Battery: ${removedAction.thresholds.battery}%, Runtime: ${removedAction.thresholds.runtime}min`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
logger.log(` ${theme.dim('Changes saved and will be applied automatically')}`);
|
||||||
|
logger.log('');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to remove action: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all actions for a specific UPS/group or all devices
|
||||||
|
*/
|
||||||
|
public async list(targetId?: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const config = await this.nupst.getDaemon().loadConfig();
|
||||||
|
|
||||||
|
if (targetId) {
|
||||||
|
// List actions for specific UPS or group
|
||||||
|
const ups = config.upsDevices.find((u) => u.id === targetId);
|
||||||
|
const group = config.groups?.find((g) => g.id === targetId);
|
||||||
|
|
||||||
|
if (!ups && !group) {
|
||||||
|
logger.error(`UPS or Group with ID '${targetId}' not found`);
|
||||||
|
logger.log('');
|
||||||
|
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
|
||||||
|
logger.log(` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
|
||||||
|
logger.log('');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ups) {
|
||||||
|
this.displayTargetActions(ups, 'UPS');
|
||||||
|
} else {
|
||||||
|
this.displayTargetActions(group!, 'Group');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// List actions for all UPS devices and groups
|
||||||
|
logger.log('');
|
||||||
|
logger.info('Actions for All UPS Devices and Groups');
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
|
let hasAnyActions = false;
|
||||||
|
|
||||||
|
// Display UPS actions
|
||||||
|
for (const ups of config.upsDevices) {
|
||||||
|
if (ups.actions && ups.actions.length > 0) {
|
||||||
|
hasAnyActions = true;
|
||||||
|
this.displayTargetActions(ups, 'UPS');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display Group actions
|
||||||
|
for (const group of config.groups || []) {
|
||||||
|
if (group.actions && group.actions.length > 0) {
|
||||||
|
hasAnyActions = true;
|
||||||
|
this.displayTargetActions(group, 'Group');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasAnyActions) {
|
||||||
|
logger.log(` ${theme.dim('No actions configured')}`);
|
||||||
|
logger.log('');
|
||||||
|
logger.log(
|
||||||
|
` ${theme.dim('Add an action:')} ${theme.command('nupst action add <ups-id|group-id>')}`,
|
||||||
|
);
|
||||||
|
logger.log('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to list actions: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display actions for a single UPS or Group
|
||||||
|
*/
|
||||||
|
private displayTargetActions(
|
||||||
|
target: IUpsConfig | IGroupConfig,
|
||||||
|
targetType: 'UPS' | 'Group',
|
||||||
|
): void {
|
||||||
|
logger.log(
|
||||||
|
`${symbols.info} ${targetType} ${theme.highlight(target.name)} ${theme.dim(`(${target.id})`)}`,
|
||||||
|
);
|
||||||
|
logger.log('');
|
||||||
|
|
||||||
|
if (!target.actions || target.actions.length === 0) {
|
||||||
|
logger.log(` ${theme.dim('No actions configured')}`);
|
||||||
|
logger.log('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: ITableColumn[] = [
|
||||||
|
{ header: 'Index', key: 'index', align: 'right' },
|
||||||
|
{ header: 'Type', key: 'type', align: 'left' },
|
||||||
|
{ header: 'Battery', key: 'battery', align: 'right' },
|
||||||
|
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
||||||
|
{ header: 'Trigger Mode', key: 'triggerMode', align: 'left' },
|
||||||
|
{ header: 'Delay', key: 'delay', align: 'right' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rows = target.actions.map((action, index) => ({
|
||||||
|
index: theme.dim(index.toString()),
|
||||||
|
type: theme.highlight(action.type),
|
||||||
|
battery: action.thresholds ? `${action.thresholds.battery}%` : theme.dim('N/A'),
|
||||||
|
runtime: action.thresholds ? `${action.thresholds.runtime}min` : theme.dim('N/A'),
|
||||||
|
triggerMode: theme.dim(action.triggerMode || 'onlyThresholds'),
|
||||||
|
delay: `${action.shutdownDelay || 5}s`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
logger.logTable(columns, rows);
|
||||||
|
logger.log('');
|
||||||
|
}
|
||||||
|
}
|
14
ts/daemon.ts
14
ts/daemon.ts
@@ -4,7 +4,7 @@ import * as path from 'node:path';
|
|||||||
import { exec, execFile } from 'node:child_process';
|
import { exec, execFile } from 'node:child_process';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import { NupstSnmp } from './snmp/manager.ts';
|
import { NupstSnmp } from './snmp/manager.ts';
|
||||||
import type { ISnmpConfig } from './snmp/types.ts';
|
import type { ISnmpConfig, IUpsStatus as ISnmpUpsStatus } from './snmp/types.ts';
|
||||||
import { logger, type ITableColumn } from './logger.ts';
|
import { logger, type ITableColumn } from './logger.ts';
|
||||||
import { MigrationRunner } from './migrations/index.ts';
|
import { MigrationRunner } from './migrations/index.ts';
|
||||||
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
||||||
@@ -76,7 +76,7 @@ export interface INupstConfig {
|
|||||||
/**
|
/**
|
||||||
* UPS status tracking interface
|
* UPS status tracking interface
|
||||||
*/
|
*/
|
||||||
interface IUpsStatus {
|
export interface IUpsStatus {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
powerStatus: 'online' | 'onBattery' | 'unknown';
|
powerStatus: 'online' | 'onBattery' | 'unknown';
|
||||||
@@ -96,7 +96,7 @@ export class NupstDaemon {
|
|||||||
|
|
||||||
/** Default configuration */
|
/** Default configuration */
|
||||||
private readonly DEFAULT_CONFIG: INupstConfig = {
|
private readonly DEFAULT_CONFIG: INupstConfig = {
|
||||||
version: '4.1',
|
version: '4.2',
|
||||||
upsDevices: [
|
upsDevices: [
|
||||||
{
|
{
|
||||||
id: 'default',
|
id: 'default',
|
||||||
@@ -171,11 +171,13 @@ export class NupstDaemon {
|
|||||||
const { config: migratedConfig, migrated } = await migrationRunner.run(parsedConfig);
|
const { config: migratedConfig, migrated } = await migrationRunner.run(parsedConfig);
|
||||||
|
|
||||||
// Save migrated config back to disk if any migrations ran
|
// Save migrated config back to disk if any migrations ran
|
||||||
|
// Cast to INupstConfig since migrations ensure the output is valid
|
||||||
|
const validConfig = migratedConfig as unknown as INupstConfig;
|
||||||
if (migrated) {
|
if (migrated) {
|
||||||
this.config = migratedConfig;
|
this.config = validConfig;
|
||||||
await this.saveConfig(this.config);
|
await this.saveConfig(this.config);
|
||||||
} else {
|
} else {
|
||||||
this.config = migratedConfig;
|
this.config = validConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.config;
|
return this.config;
|
||||||
@@ -760,7 +762,7 @@ export class NupstDaemon {
|
|||||||
|
|
||||||
const rows: Array<Record<string, string>> = [];
|
const rows: Array<Record<string, string>> = [];
|
||||||
let emergencyDetected = false;
|
let emergencyDetected = false;
|
||||||
let emergencyUps: any = null;
|
let emergencyUps: { ups: IUpsConfig; status: ISnmpUpsStatus } | null = null;
|
||||||
|
|
||||||
// Check all UPS devices
|
// Check all UPS devices
|
||||||
for (const ups of this.config.upsDevices) {
|
for (const ups of this.config.upsDevices) {
|
||||||
|
@@ -28,18 +28,18 @@ export abstract class BaseMigration {
|
|||||||
/**
|
/**
|
||||||
* Check if this migration should run on the given config
|
* Check if this migration should run on the given config
|
||||||
*
|
*
|
||||||
* @param config - Raw configuration object to check
|
* @param config - Raw configuration object to check (unknown schema for migrations)
|
||||||
* @returns True if migration should run, false otherwise
|
* @returns True if migration should run, false otherwise
|
||||||
*/
|
*/
|
||||||
abstract shouldRun(config: any): Promise<boolean>;
|
abstract shouldRun(config: Record<string, unknown>): Promise<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform the migration on the given config
|
* Perform the migration on the given config
|
||||||
*
|
*
|
||||||
* @param config - Raw configuration object to migrate
|
* @param config - Raw configuration object to migrate (unknown schema for migrations)
|
||||||
* @returns Migrated configuration object
|
* @returns Migrated configuration object
|
||||||
*/
|
*/
|
||||||
abstract migrate(config: any): Promise<any>;
|
abstract migrate(config: Record<string, unknown>): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get human-readable name for this migration
|
* Get human-readable name for this migration
|
||||||
|
@@ -19,7 +19,7 @@ export class MigrationRunner {
|
|||||||
new MigrationV1ToV2(),
|
new MigrationV1ToV2(),
|
||||||
new MigrationV3ToV4(),
|
new MigrationV3ToV4(),
|
||||||
new MigrationV4_0ToV4_1(),
|
new MigrationV4_0ToV4_1(),
|
||||||
// Add future migrations here (v4.2, v4.3, etc.)
|
// Add future migrations here (v4.3, v4.4, etc.)
|
||||||
];
|
];
|
||||||
|
|
||||||
// Sort by version order to ensure they run in sequence
|
// Sort by version order to ensure they run in sequence
|
||||||
@@ -32,7 +32,9 @@ export class MigrationRunner {
|
|||||||
* @param config - Raw configuration object to migrate
|
* @param config - Raw configuration object to migrate
|
||||||
* @returns Migrated configuration and whether migrations ran
|
* @returns Migrated configuration and whether migrations ran
|
||||||
*/
|
*/
|
||||||
async run(config: any): Promise<{ config: any; migrated: boolean }> {
|
async run(
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
): Promise<{ config: Record<string, unknown>; migrated: boolean }> {
|
||||||
let currentConfig = config;
|
let currentConfig = config;
|
||||||
let anyMigrationsRan = false;
|
let anyMigrationsRan = false;
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ import { logger } from '../logger.ts';
|
|||||||
* 3. Adds empty actions array to UPS devices without actions
|
* 3. Adds empty actions array to UPS devices without actions
|
||||||
* 4. Adds empty actions array to groups
|
* 4. Adds empty actions array to groups
|
||||||
*
|
*
|
||||||
* Transforms v4.0 format:
|
* Transforms v4.0 format (with UPS-level thresholds):
|
||||||
* {
|
* {
|
||||||
* version: "4.0",
|
* version: "4.0",
|
||||||
* upsDevices: [
|
* upsDevices: [
|
||||||
@@ -24,7 +24,7 @@ import { logger } from '../logger.ts';
|
|||||||
* ]
|
* ]
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* To v4.1 format:
|
* To v4.1 format (with action-level thresholds):
|
||||||
* {
|
* {
|
||||||
* version: "4.1",
|
* version: "4.1",
|
||||||
* upsDevices: [
|
* upsDevices: [
|
||||||
@@ -37,7 +37,7 @@ import { logger } from '../logger.ts';
|
|||||||
* {
|
* {
|
||||||
* type: "shutdown",
|
* type: "shutdown",
|
||||||
* thresholds: { battery: 60, runtime: 20 },
|
* thresholds: { battery: 60, runtime: 20 },
|
||||||
* onlyOnThresholdViolation: true,
|
* triggerMode: "onlyThresholds",
|
||||||
* shutdownDelay: 5
|
* shutdownDelay: 5
|
||||||
* }
|
* }
|
||||||
* ]
|
* ]
|
||||||
@@ -49,30 +49,31 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
|
|||||||
readonly fromVersion = '4.0';
|
readonly fromVersion = '4.0';
|
||||||
readonly toVersion = '4.1';
|
readonly toVersion = '4.1';
|
||||||
|
|
||||||
async shouldRun(config: any): Promise<boolean> {
|
async shouldRun(config: Record<string, unknown>): Promise<boolean> {
|
||||||
// Run if config is version 4.0 or missing version with v4 structure
|
// Run if config is version 4.0
|
||||||
if (config.version === '4.0') {
|
if (config.version === '4.0') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also run if config has upsDevices with thresholds at UPS level (v4.0 format)
|
// Also run if config has upsDevices with thresholds at UPS level (v4.0 format)
|
||||||
if (config.upsDevices && config.upsDevices.length > 0) {
|
if (Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
|
||||||
const firstDevice = config.upsDevices[0];
|
const firstDevice = config.upsDevices[0] as Record<string, unknown>;
|
||||||
// v4.0 has thresholds at UPS level, v4.1 has them in actions
|
// v4.0 has thresholds at UPS level, v4.1 has them in actions
|
||||||
return firstDevice.thresholds !== undefined && firstDevice.actions === undefined;
|
return firstDevice.thresholds !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async migrate(config: any): Promise<any> {
|
async migrate(config: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||||
logger.info(`${this.getName()}: Migrating v4.0 config to v4.1 format...`);
|
logger.info(`${this.getName()}: Migrating v4.0 config to v4.1 format...`);
|
||||||
logger.dim(` - Moving thresholds from UPS level to action level`);
|
logger.dim(` - Moving thresholds from UPS level to action level`);
|
||||||
logger.dim(` - Creating default shutdown actions from existing thresholds`);
|
logger.dim(` - Creating default shutdown actions from existing thresholds`);
|
||||||
|
|
||||||
// Migrate UPS devices
|
// Migrate UPS devices
|
||||||
const migratedDevices = (config.upsDevices || []).map((device: any) => {
|
const devices = (config.upsDevices as Array<Record<string, unknown>>) || [];
|
||||||
const migrated: any = {
|
const migratedDevices = devices.map((device) => {
|
||||||
|
const migrated: Record<string, unknown> = {
|
||||||
id: device.id,
|
id: device.id,
|
||||||
name: device.name,
|
name: device.name,
|
||||||
snmp: device.snmp,
|
snmp: device.snmp,
|
||||||
@@ -80,20 +81,21 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// If device has thresholds at UPS level, convert to shutdown action
|
// If device has thresholds at UPS level, convert to shutdown action
|
||||||
if (device.thresholds) {
|
const deviceThresholds = device.thresholds as { battery: number; runtime: number } | undefined;
|
||||||
|
if (deviceThresholds) {
|
||||||
migrated.actions = [
|
migrated.actions = [
|
||||||
{
|
{
|
||||||
type: 'shutdown',
|
type: 'shutdown',
|
||||||
thresholds: {
|
thresholds: {
|
||||||
battery: device.thresholds.battery,
|
battery: deviceThresholds.battery,
|
||||||
runtime: device.thresholds.runtime,
|
runtime: deviceThresholds.runtime,
|
||||||
},
|
},
|
||||||
triggerMode: 'onlyThresholds', // Preserve old behavior (only on threshold violation)
|
triggerMode: 'onlyThresholds', // Preserve old behavior (only on threshold violation)
|
||||||
shutdownDelay: 5, // Default delay
|
shutdownDelay: 5, // Default delay
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
logger.dim(
|
logger.dim(
|
||||||
` → ${device.name}: Created shutdown action (battery: ${device.thresholds.battery}%, runtime: ${device.thresholds.runtime}min)`,
|
` → ${device.name}: Created shutdown action (battery: ${deviceThresholds.battery}%, runtime: ${deviceThresholds.runtime}min)`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// No thresholds, just add empty actions array
|
// No thresholds, just add empty actions array
|
||||||
@@ -104,7 +106,8 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add actions to groups
|
// Add actions to groups
|
||||||
const migratedGroups = (config.groups || []).map((group: any) => ({
|
const groups = (config.groups as Array<Record<string, unknown>>) || [];
|
||||||
|
const migratedGroups = groups.map((group) => ({
|
||||||
...group,
|
...group,
|
||||||
actions: group.actions || [],
|
actions: group.actions || [],
|
||||||
}));
|
}));
|
||||||
|
10
ts/nupst.ts
10
ts/nupst.ts
@@ -6,6 +6,7 @@ import { logger } from './logger.ts';
|
|||||||
import { UpsHandler } from './cli/ups-handler.ts';
|
import { UpsHandler } from './cli/ups-handler.ts';
|
||||||
import { GroupHandler } from './cli/group-handler.ts';
|
import { GroupHandler } from './cli/group-handler.ts';
|
||||||
import { ServiceHandler } from './cli/service-handler.ts';
|
import { ServiceHandler } from './cli/service-handler.ts';
|
||||||
|
import { ActionHandler } from './cli/action-handler.ts';
|
||||||
import * as https from 'node:https';
|
import * as https from 'node:https';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,6 +20,7 @@ export class Nupst {
|
|||||||
private readonly upsHandler: UpsHandler;
|
private readonly upsHandler: UpsHandler;
|
||||||
private readonly groupHandler: GroupHandler;
|
private readonly groupHandler: GroupHandler;
|
||||||
private readonly serviceHandler: ServiceHandler;
|
private readonly serviceHandler: ServiceHandler;
|
||||||
|
private readonly actionHandler: ActionHandler;
|
||||||
private updateAvailable: boolean = false;
|
private updateAvailable: boolean = false;
|
||||||
private latestVersion: string = '';
|
private latestVersion: string = '';
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ export class Nupst {
|
|||||||
this.upsHandler = new UpsHandler(this);
|
this.upsHandler = new UpsHandler(this);
|
||||||
this.groupHandler = new GroupHandler(this);
|
this.groupHandler = new GroupHandler(this);
|
||||||
this.serviceHandler = new ServiceHandler(this);
|
this.serviceHandler = new ServiceHandler(this);
|
||||||
|
this.actionHandler = new ActionHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,6 +83,13 @@ export class Nupst {
|
|||||||
return this.serviceHandler;
|
return this.serviceHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Action handler for action management
|
||||||
|
*/
|
||||||
|
public getActionHandler(): ActionHandler {
|
||||||
|
return this.actionHandler;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current version of NUPST
|
* Get the current version of NUPST
|
||||||
* @returns The current version string
|
* @returns The current version string
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { promises as fs } from 'node:fs';
|
import { promises as fs } from 'node:fs';
|
||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import { NupstDaemon } from './daemon.ts';
|
import { NupstDaemon, type IUpsConfig } from './daemon.ts';
|
||||||
|
import { NupstSnmp } from './snmp/manager.ts';
|
||||||
import { logger } from './logger.ts';
|
import { logger } from './logger.ts';
|
||||||
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
||||||
|
|
||||||
@@ -277,14 +278,23 @@ WantedBy=multi-user.target
|
|||||||
await this.displaySingleUpsStatus(ups, snmp);
|
await this.displaySingleUpsStatus(ups, snmp);
|
||||||
}
|
}
|
||||||
} else if (config.snmp) {
|
} else if (config.snmp) {
|
||||||
// Legacy single UPS configuration
|
// Legacy single UPS configuration (v1/v2 format)
|
||||||
logger.info('UPS Devices (1):');
|
logger.info('UPS Devices (1):');
|
||||||
const legacyUps = {
|
const legacyUps: IUpsConfig = {
|
||||||
id: 'default',
|
id: 'default',
|
||||||
name: 'Default UPS',
|
name: 'Default UPS',
|
||||||
snmp: config.snmp,
|
snmp: config.snmp,
|
||||||
thresholds: config.thresholds,
|
|
||||||
groups: [],
|
groups: [],
|
||||||
|
actions: config.thresholds
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: 'shutdown',
|
||||||
|
thresholds: config.thresholds,
|
||||||
|
triggerMode: 'onlyThresholds',
|
||||||
|
shutdownDelay: 5,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.displaySingleUpsStatus(legacyUps, snmp);
|
await this.displaySingleUpsStatus(legacyUps, snmp);
|
||||||
@@ -307,7 +317,7 @@ WantedBy=multi-user.target
|
|||||||
* @param ups UPS configuration
|
* @param ups UPS configuration
|
||||||
* @param snmp SNMP manager
|
* @param snmp SNMP manager
|
||||||
*/
|
*/
|
||||||
private async displaySingleUpsStatus(ups: any, snmp: any): Promise<void> {
|
private async displaySingleUpsStatus(ups: IUpsConfig, snmp: NupstSnmp): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Create a test config with a short timeout
|
// Create a test config with a short timeout
|
||||||
const testConfig = {
|
const testConfig = {
|
||||||
@@ -330,7 +340,16 @@ WantedBy=multi-user.target
|
|||||||
|
|
||||||
// Display battery with color coding
|
// Display battery with color coding
|
||||||
const batteryColor = getBatteryColor(status.batteryCapacity);
|
const batteryColor = getBatteryColor(status.batteryCapacity);
|
||||||
const batterySymbol = status.batteryCapacity >= ups.thresholds.battery ? symbols.success : symbols.warning;
|
|
||||||
|
// Get threshold from actions (if any action has thresholds defined)
|
||||||
|
const actionWithThresholds = ups.actions?.find((action) => action.thresholds);
|
||||||
|
const batteryThreshold = actionWithThresholds?.thresholds?.battery;
|
||||||
|
const batterySymbol = batteryThreshold !== undefined && status.batteryCapacity >= batteryThreshold
|
||||||
|
? symbols.success
|
||||||
|
: batteryThreshold !== undefined
|
||||||
|
? symbols.warning
|
||||||
|
: '';
|
||||||
|
|
||||||
logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
|
logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
|
||||||
|
|
||||||
// Display host info
|
// Display host info
|
||||||
|
Reference in New Issue
Block a user