142 lines
4.2 KiB
TypeScript
142 lines
4.2 KiB
TypeScript
|
import * as http from 'node:http';
|
||
|
import * as https from 'node:https';
|
||
|
import { URL } from 'node:url';
|
||
|
import { Action, type IActionConfig, type IActionContext } from './base-action.ts';
|
||
|
import { logger } from '../logger.ts';
|
||
|
|
||
|
/**
|
||
|
* WebhookAction - Calls an HTTP webhook with UPS state information
|
||
|
*
|
||
|
* Sends UPS status to a configured webhook URL via GET or POST.
|
||
|
* This is useful for remote notifications and integrations with external systems.
|
||
|
*/
|
||
|
export class WebhookAction extends Action {
|
||
|
readonly type = 'webhook';
|
||
|
|
||
|
/**
|
||
|
* Execute the webhook action
|
||
|
* @param context Action context with UPS state
|
||
|
*/
|
||
|
async execute(context: IActionContext): Promise<void> {
|
||
|
// Check if we should execute based on trigger mode
|
||
|
if (!this.shouldExecute(context)) {
|
||
|
logger.info(`Webhook action skipped (trigger mode: ${this.config.triggerMode || 'powerChangesAndThresholds'})`);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!this.config.webhookUrl) {
|
||
|
logger.error('Webhook URL not configured');
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const method = this.config.webhookMethod || 'POST';
|
||
|
const timeout = this.config.webhookTimeout || 10000;
|
||
|
|
||
|
logger.info(`Calling webhook: ${method} ${this.config.webhookUrl}`);
|
||
|
|
||
|
try {
|
||
|
await this.callWebhook(context, method, timeout);
|
||
|
logger.success('Webhook call successful');
|
||
|
} catch (error) {
|
||
|
logger.error(
|
||
|
`Webhook call failed: ${error instanceof Error ? error.message : String(error)}`,
|
||
|
);
|
||
|
// Don't throw - webhook failures shouldn't stop other actions
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Call the webhook with UPS state data
|
||
|
* @param context Action context
|
||
|
* @param method HTTP method (GET or POST)
|
||
|
* @param timeout Request timeout in milliseconds
|
||
|
*/
|
||
|
private async callWebhook(
|
||
|
context: IActionContext,
|
||
|
method: 'GET' | 'POST',
|
||
|
timeout: number,
|
||
|
): Promise<void> {
|
||
|
const payload: any = {
|
||
|
upsId: context.upsId,
|
||
|
upsName: context.upsName,
|
||
|
powerStatus: context.powerStatus,
|
||
|
batteryCapacity: context.batteryCapacity,
|
||
|
batteryRuntime: context.batteryRuntime,
|
||
|
triggerReason: context.triggerReason,
|
||
|
timestamp: context.timestamp,
|
||
|
};
|
||
|
|
||
|
// Include action's own thresholds if configured
|
||
|
if (this.config.thresholds) {
|
||
|
payload.thresholds = {
|
||
|
battery: this.config.thresholds.battery,
|
||
|
runtime: this.config.thresholds.runtime,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
const url = new URL(this.config.webhookUrl!);
|
||
|
|
||
|
if (method === 'GET') {
|
||
|
// Append payload as query parameters for GET
|
||
|
url.searchParams.append('upsId', payload.upsId);
|
||
|
url.searchParams.append('upsName', payload.upsName);
|
||
|
url.searchParams.append('powerStatus', payload.powerStatus);
|
||
|
url.searchParams.append('batteryCapacity', String(payload.batteryCapacity));
|
||
|
url.searchParams.append('batteryRuntime', String(payload.batteryRuntime));
|
||
|
|
||
|
url.searchParams.append('triggerReason', payload.triggerReason);
|
||
|
url.searchParams.append('timestamp', String(payload.timestamp));
|
||
|
}
|
||
|
|
||
|
return new Promise((resolve, reject) => {
|
||
|
const protocol = url.protocol === 'https:' ? https : http;
|
||
|
|
||
|
const options: http.RequestOptions = {
|
||
|
method,
|
||
|
headers: method === 'POST'
|
||
|
? {
|
||
|
'Content-Type': 'application/json',
|
||
|
'User-Agent': 'nupst',
|
||
|
}
|
||
|
: {
|
||
|
'User-Agent': 'nupst',
|
||
|
},
|
||
|
timeout,
|
||
|
};
|
||
|
|
||
|
const req = protocol.request(url, options, (res) => {
|
||
|
let data = '';
|
||
|
|
||
|
res.on('data', (chunk) => {
|
||
|
data += chunk;
|
||
|
});
|
||
|
|
||
|
res.on('end', () => {
|
||
|
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||
|
logger.dim(`Webhook response (${res.statusCode}): ${data.substring(0, 100)}`);
|
||
|
resolve();
|
||
|
} else {
|
||
|
reject(new Error(`Webhook returned status ${res.statusCode}`));
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
req.on('error', (error) => {
|
||
|
reject(error);
|
||
|
});
|
||
|
|
||
|
req.on('timeout', () => {
|
||
|
req.destroy();
|
||
|
reject(new Error(`Webhook request timed out after ${timeout}ms`));
|
||
|
});
|
||
|
|
||
|
// Send POST data if applicable
|
||
|
if (method === 'POST') {
|
||
|
req.write(JSON.stringify(payload));
|
||
|
}
|
||
|
|
||
|
req.end();
|
||
|
});
|
||
|
}
|
||
|
}
|