312 lines
10 KiB
TypeScript
312 lines
10 KiB
TypeScript
|
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 } 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
|
||
|
*/
|
||
|
public async add(upsId?: string): Promise<void> {
|
||
|
try {
|
||
|
if (!upsId) {
|
||
|
logger.error('UPS ID is required');
|
||
|
logger.log(` ${theme.dim('Usage:')} ${theme.command('nupst action add <ups-id>')}`);
|
||
|
logger.log('');
|
||
|
logger.log(` ${theme.dim('List UPS devices:')} ${theme.command('nupst ups list')}`);
|
||
|
logger.log('');
|
||
|
process.exit(1);
|
||
|
}
|
||
|
|
||
|
const config = await this.nupst.getDaemon().loadConfig();
|
||
|
const ups = config.upsDevices.find((u) => u.id === upsId);
|
||
|
|
||
|
if (!ups) {
|
||
|
logger.error(`UPS with ID '${upsId}' not found`);
|
||
|
logger.log('');
|
||
|
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
|
||
|
logger.log('');
|
||
|
process.exit(1);
|
||
|
}
|
||
|
|
||
|
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 ${theme.highlight(ups.name)}`);
|
||
|
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 UPS
|
||
|
if (!ups.actions) {
|
||
|
ups.actions = [];
|
||
|
}
|
||
|
ups.actions.push(newAction);
|
||
|
|
||
|
await this.nupst.getDaemon().saveConfig(config);
|
||
|
|
||
|
logger.log('');
|
||
|
logger.success(`Action added to ${ups.name}`);
|
||
|
logger.log('');
|
||
|
logger.log(` ${theme.dim('Restart service to apply changes:')} ${theme.command('nupst service restart')}`);
|
||
|
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
|
||
|
*/
|
||
|
public async remove(upsId?: string, actionIndexStr?: string): Promise<void> {
|
||
|
try {
|
||
|
if (!upsId || !actionIndexStr) {
|
||
|
logger.error('UPS ID and action index are required');
|
||
|
logger.log(
|
||
|
` ${theme.dim('Usage:')} ${theme.command('nupst action remove <ups-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();
|
||
|
const ups = config.upsDevices.find((u) => u.id === upsId);
|
||
|
|
||
|
if (!ups) {
|
||
|
logger.error(`UPS with ID '${upsId}' not found`);
|
||
|
logger.log('');
|
||
|
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
|
||
|
logger.log('');
|
||
|
process.exit(1);
|
||
|
}
|
||
|
|
||
|
if (!ups.actions || ups.actions.length === 0) {
|
||
|
logger.error(`No actions configured for UPS '${ups.name}'`);
|
||
|
logger.log('');
|
||
|
process.exit(1);
|
||
|
}
|
||
|
|
||
|
if (actionIndex >= ups.actions.length) {
|
||
|
logger.error(
|
||
|
`Invalid action index. UPS '${ups.name}' has ${ups.actions.length} action(s) (index 0-${ups.actions.length - 1})`,
|
||
|
);
|
||
|
logger.log('');
|
||
|
logger.log(` ${theme.dim('List actions:')} ${theme.command(`nupst action list ${upsId}`)}`);
|
||
|
logger.log('');
|
||
|
process.exit(1);
|
||
|
}
|
||
|
|
||
|
const removedAction = ups.actions[actionIndex];
|
||
|
ups.actions.splice(actionIndex, 1);
|
||
|
|
||
|
await this.nupst.getDaemon().saveConfig(config);
|
||
|
|
||
|
logger.log('');
|
||
|
logger.success(`Action removed from ${ups.name}`);
|
||
|
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('');
|
||
|
logger.log(` ${theme.dim('Restart service to apply changes:')} ${theme.command('nupst service restart')}`);
|
||
|
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 or all UPS devices
|
||
|
*/
|
||
|
public async list(upsId?: string): Promise<void> {
|
||
|
try {
|
||
|
const config = await this.nupst.getDaemon().loadConfig();
|
||
|
|
||
|
if (upsId) {
|
||
|
// List actions for specific UPS
|
||
|
const ups = config.upsDevices.find((u) => u.id === upsId);
|
||
|
|
||
|
if (!ups) {
|
||
|
logger.error(`UPS with ID '${upsId}' not found`);
|
||
|
logger.log('');
|
||
|
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
|
||
|
logger.log('');
|
||
|
process.exit(1);
|
||
|
}
|
||
|
|
||
|
this.displayUpsActions(ups);
|
||
|
} else {
|
||
|
// List actions for all UPS devices
|
||
|
logger.log('');
|
||
|
logger.info('Actions for All UPS Devices');
|
||
|
logger.log('');
|
||
|
|
||
|
let hasAnyActions = false;
|
||
|
for (const ups of config.upsDevices) {
|
||
|
if (ups.actions && ups.actions.length > 0) {
|
||
|
hasAnyActions = true;
|
||
|
this.displayUpsActions(ups);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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>')}`);
|
||
|
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
|
||
|
*/
|
||
|
private displayUpsActions(ups: IUpsConfig): void {
|
||
|
logger.log(`${symbols.info} ${theme.highlight(ups.name)} ${theme.dim(`(${ups.id})`)}`);
|
||
|
logger.log('');
|
||
|
|
||
|
if (!ups.actions || ups.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 = ups.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('');
|
||
|
}
|
||
|
}
|