feat(actions): implement action system for UPS state management with shutdown, webhook, and script actions

This commit is contained in:
2025-10-20 11:47:51 +00:00
parent a7113d0387
commit 32bd27b849
11 changed files with 1238 additions and 166 deletions

View File

@@ -77,10 +77,10 @@ export class UpsHandler {
checkInterval: config.checkInterval,
upsDevices: [{
id: 'default',
name: 'Default UPS',
snmp: config.snmp,
thresholds: config.thresholds,
groups: [],
name: 'Default UPS',
snmp: config.snmp,
groups: [],
actions: [],
}],
groups: [],
};
@@ -117,6 +117,7 @@ export class UpsHandler {
runtime: 20,
},
groups: [],
actions: [],
};
// Gather SNMP settings
@@ -136,6 +137,9 @@ export class UpsHandler {
await groupHandler.assignUpsToGroups(newUps, config.groups, prompt);
}
// Gather action settings
await this.gatherActionSettings(newUps.actions, prompt);
// Add the new UPS to the config
config.upsDevices.push(newUps);
@@ -221,16 +225,16 @@ export class UpsHandler {
// Convert old format to new format if needed
if (!config.upsDevices) {
// Initialize with the current config as the first UPS
if (!config.snmp || !config.thresholds) {
logger.error('Legacy configuration is missing required SNMP or threshold settings');
if (!config.snmp) {
logger.error('Legacy configuration is missing required SNMP settings');
return;
}
config.upsDevices = [{
id: 'default',
name: 'Default UPS',
snmp: config.snmp,
thresholds: config.thresholds,
groups: [],
actions: [],
}];
config.groups = [];
logger.log('Converting existing configuration to multi-UPS format.');
@@ -265,9 +269,6 @@ export class UpsHandler {
// 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);
@@ -279,6 +280,14 @@ export class UpsHandler {
await groupHandler.assignUpsToGroups(upsToEdit, config.groups, prompt);
}
// Initialize actions array if not exists
if (!upsToEdit.actions) {
upsToEdit.actions = [];
}
// Edit action settings
await this.gatherActionSettings(upsToEdit.actions, prompt);
// Save the configuration
await this.nupst.getDaemon().saveConfig(config);
@@ -396,13 +405,12 @@ export class UpsHandler {
logger.logBox('UPS Devices', [
'Legacy single-UPS configuration detected.',
'',
...((!config.snmp || !config.thresholds)
? ['Error: Configuration missing SNMP or threshold settings']
...(!config.snmp
? ['Error: Configuration missing SNMP settings']
: [
'Default UPS:',
` Host: ${config.snmp.host}:${config.snmp.port}`,
` Model: ${config.snmp.upsModel || 'cyberpower'}`,
` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`,
'',
'Use "nupst ups add" to add more UPS devices and migrate',
'to the multi-UPS configuration format.',
@@ -506,9 +514,8 @@ export class UpsHandler {
*/
private displayTestConfig(config: any): void {
// Check if this is a UPS device or full configuration
const isUpsConfig = config.snmp && config.thresholds;
const isUpsConfig = config.snmp;
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
@@ -919,6 +926,151 @@ export class UpsHandler {
}
}
/**
* Gather action configuration settings
* @param actions Actions array to configure
* @param prompt Function to prompt for user input
*/
private async gatherActionSettings(
actions: any[],
prompt: (question: string) => Promise<string>,
): Promise<void> {
logger.log('');
logger.info('Action Configuration (Optional):');
logger.dim('Actions are triggered on power status changes and threshold violations.');
logger.dim('Leave empty to use default shutdown behavior on threshold violations.');
const configureActions = await prompt('Configure custom actions? (y/N): ');
if (configureActions.toLowerCase() !== 'y') {
return; // Keep existing actions or use default
}
// Clear existing actions
actions.length = 0;
let addMore = true;
while (addMore) {
logger.log('');
logger.info('Action Type:');
logger.dim(' 1) Shutdown (system shutdown)');
logger.dim(' 2) Webhook (HTTP notification)');
logger.dim(' 3) Custom Script (run .sh file from /etc/nupst)');
const typeInput = await prompt('Select action type [1]: ');
const typeValue = parseInt(typeInput, 10) || 1;
const action: any = {};
if (typeValue === 1) {
// Shutdown action
action.type = 'shutdown';
const delayInput = await prompt('Shutdown delay in minutes [5]: ');
const delay = parseInt(delayInput, 10);
if (delayInput.trim() && !isNaN(delay)) {
action.shutdownDelay = delay;
}
} else if (typeValue === 2) {
// Webhook action
action.type = 'webhook';
const url = await prompt('Webhook URL: ');
if (!url.trim()) {
logger.warn('Webhook URL required, skipping action');
continue;
}
action.webhookUrl = url.trim();
logger.log('');
logger.info('HTTP Method:');
logger.dim(' 1) POST (JSON body)');
logger.dim(' 2) GET (query parameters)');
const methodInput = await prompt('Select method [1]: ');
action.webhookMethod = methodInput === '2' ? 'GET' : 'POST';
const timeoutInput = await prompt('Timeout in seconds [10]: ');
const timeout = parseInt(timeoutInput, 10);
if (timeoutInput.trim() && !isNaN(timeout)) {
action.webhookTimeout = timeout * 1000; // Convert to ms
}
} else if (typeValue === 3) {
// Script action
action.type = 'script';
const scriptPath = await prompt('Script filename (in /etc/nupst/, must end with .sh): ');
if (!scriptPath.trim() || !scriptPath.trim().endsWith('.sh')) {
logger.warn('Script path must end with .sh, skipping action');
continue;
}
action.scriptPath = scriptPath.trim();
const timeoutInput = await prompt('Script timeout in seconds [60]: ');
const timeout = parseInt(timeoutInput, 10);
if (timeoutInput.trim() && !isNaN(timeout)) {
action.scriptTimeout = timeout * 1000; // Convert to ms
}
} else {
logger.warn('Invalid action type, skipping');
continue;
}
// Configure trigger mode (applies to all action types)
logger.log('');
logger.info('Trigger Mode:');
logger.dim(' 1) Power changes + thresholds (default)');
logger.dim(' 2) Only power status changes');
logger.dim(' 3) Only threshold violations');
logger.dim(' 4) Any change (every ~30s check)');
const triggerInput = await prompt('Select trigger mode [1]: ');
const triggerValue = parseInt(triggerInput, 10) || 1;
switch (triggerValue) {
case 2:
action.triggerMode = 'onlyPowerChanges';
break;
case 3:
action.triggerMode = 'onlyThresholds';
break;
case 4:
action.triggerMode = 'anyChange';
break;
default:
action.triggerMode = 'powerChangesAndThresholds';
}
// Configure thresholds if needed for onlyThresholds or powerChangesAndThresholds modes
if (action.triggerMode === 'onlyThresholds' || action.triggerMode === 'powerChangesAndThresholds') {
logger.log('');
logger.info('Action Thresholds:');
logger.dim('Action will trigger when battery or runtime falls below these values (while on battery)');
const batteryInput = await prompt('Battery threshold percentage [60]: ');
const battery = parseInt(batteryInput, 10);
const batteryThreshold = (batteryInput.trim() && !isNaN(battery)) ? battery : 60;
const runtimeInput = await prompt('Runtime threshold in minutes [20]: ');
const runtime = parseInt(runtimeInput, 10);
const runtimeThreshold = (runtimeInput.trim() && !isNaN(runtime)) ? runtime : 20;
action.thresholds = {
battery: batteryThreshold,
runtime: runtimeThreshold,
};
}
actions.push(action);
logger.success(`${action.type.charAt(0).toUpperCase() + action.type.slice(1)} action added (mode: ${action.triggerMode || 'powerChangesAndThresholds'})`);
const more = await prompt('Add another action? (y/N): ');
addMore = more.toLowerCase() === 'y';
}
if (actions.length > 0) {
logger.log('');
logger.success(`${actions.length} action(s) configured`);
}
}
/**
* Display UPS configuration summary
* @param ups UPS configuration