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 { // 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 { 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(); }); } }