Agregar notificaciones y limite de acciones criticas
parent
8797cb4990
commit
7d8959f164
@ -0,0 +1,245 @@
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT_DIRECTORY = path.join(__dirname, '..');
|
||||
const NOTIFICATION_CONFIG_FILE = process.env.NOTIFICATION_CONFIG_FILE
|
||||
? path.resolve(process.env.NOTIFICATION_CONFIG_FILE)
|
||||
: path.join(ROOT_DIRECTORY, 'config', 'notifications.json');
|
||||
|
||||
const SEVERITY_RANK = {
|
||||
RECOVERY: 0,
|
||||
NORMAL: 0,
|
||||
WARNING: 1,
|
||||
OFFLINE: 2,
|
||||
CRITICAL: 3
|
||||
};
|
||||
|
||||
const DEFAULT_NOTIFICATION_CONFIG = {
|
||||
enabled: false,
|
||||
minSeverity: 'CRITICAL',
|
||||
channels: {
|
||||
webhook: {
|
||||
enabled: false,
|
||||
url: '',
|
||||
headers: {}
|
||||
},
|
||||
telegram: {
|
||||
enabled: false,
|
||||
botToken: '',
|
||||
chatId: ''
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function mergeNotificationConfig(config = {}) {
|
||||
return {
|
||||
enabled: config.enabled === true,
|
||||
minSeverity: SEVERITY_RANK[config.minSeverity] === undefined
|
||||
? DEFAULT_NOTIFICATION_CONFIG.minSeverity
|
||||
: config.minSeverity,
|
||||
channels: {
|
||||
webhook: {
|
||||
...DEFAULT_NOTIFICATION_CONFIG.channels.webhook,
|
||||
...(config.channels && config.channels.webhook)
|
||||
},
|
||||
telegram: {
|
||||
...DEFAULT_NOTIFICATION_CONFIG.channels.telegram,
|
||||
...(config.channels && config.channels.telegram)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function readNotificationConfig() {
|
||||
try {
|
||||
const rawConfig = await fs.readFile(NOTIFICATION_CONFIG_FILE, 'utf8');
|
||||
return mergeNotificationConfig(JSON.parse(rawConfig));
|
||||
} catch {
|
||||
return mergeNotificationConfig();
|
||||
}
|
||||
}
|
||||
|
||||
function validateNotificationConfig(rawConfig) {
|
||||
const config = mergeNotificationConfig(rawConfig);
|
||||
|
||||
if (SEVERITY_RANK[config.minSeverity] === undefined) {
|
||||
throw Object.assign(new Error('Severidad minima no valida.'), { status: 400 });
|
||||
}
|
||||
|
||||
if (config.channels.webhook.enabled && !isValidHttpUrl(config.channels.webhook.url)) {
|
||||
throw Object.assign(new Error('URL de webhook no valida.'), { status: 400 });
|
||||
}
|
||||
|
||||
if (config.channels.telegram.enabled) {
|
||||
if (!config.channels.telegram.botToken || !config.channels.telegram.chatId) {
|
||||
throw Object.assign(
|
||||
new Error('Telegram requiere botToken y chatId.'),
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
config.channels.webhook.headers &&
|
||||
(typeof config.channels.webhook.headers !== 'object' ||
|
||||
Array.isArray(config.channels.webhook.headers))
|
||||
) {
|
||||
throw Object.assign(new Error('Headers de webhook invalidos.'), { status: 400 });
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function writeNotificationConfig(rawConfig) {
|
||||
const config = validateNotificationConfig(rawConfig);
|
||||
|
||||
await fs.mkdir(path.dirname(NOTIFICATION_CONFIG_FILE), { recursive: true });
|
||||
await fs.writeFile(
|
||||
NOTIFICATION_CONFIG_FILE,
|
||||
`${JSON.stringify(config, null, 4)}\n`,
|
||||
'utf8'
|
||||
);
|
||||
return config;
|
||||
}
|
||||
|
||||
function redactNotificationConfig(config) {
|
||||
return {
|
||||
...config,
|
||||
channels: {
|
||||
webhook: {
|
||||
...config.channels.webhook,
|
||||
headers: Object.keys(config.channels.webhook.headers || {}).length > 0
|
||||
? '[configured]'
|
||||
: {}
|
||||
},
|
||||
telegram: {
|
||||
...config.channels.telegram,
|
||||
botToken: config.channels.telegram.botToken ? '[configured]' : ''
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function isValidHttpUrl(value) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return ['http:', 'https:'].includes(url.protocol);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldNotify(event, minSeverity) {
|
||||
const eventRank = SEVERITY_RANK[event.state] ?? 0;
|
||||
const minimumRank = SEVERITY_RANK[minSeverity] ?? SEVERITY_RANK.CRITICAL;
|
||||
|
||||
return eventRank >= minimumRank;
|
||||
}
|
||||
|
||||
function buildNotificationMessage(event) {
|
||||
const value = event.value === '' || event.value === null || event.value === undefined
|
||||
? 'N/A'
|
||||
: event.value;
|
||||
|
||||
return [
|
||||
`Photobioreactor alarm: ${event.state}`,
|
||||
`Sensor: ${event.sensorId || event.sensor}`,
|
||||
`Value: ${value}`,
|
||||
`Message: ${event.message}`,
|
||||
`Time: ${event.timestamp}`
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async function dispatchAlarmNotifications(events) {
|
||||
const config = await readNotificationConfig();
|
||||
|
||||
if (!config.enabled || !Array.isArray(events) || events.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const eligibleEvents = events.filter((event) =>
|
||||
shouldNotify(event, config.minSeverity)
|
||||
);
|
||||
const results = [];
|
||||
|
||||
for (const event of eligibleEvents) {
|
||||
if (config.channels.webhook.enabled) {
|
||||
results.push(await sendSafely('webhook', event, () =>
|
||||
sendWebhookNotification(event, config.channels.webhook)
|
||||
));
|
||||
}
|
||||
|
||||
if (config.channels.telegram.enabled) {
|
||||
results.push(await sendSafely('telegram', event, () =>
|
||||
sendTelegramNotification(event, config.channels.telegram)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function sendSafely(channel, event, send) {
|
||||
try {
|
||||
await send();
|
||||
return {
|
||||
channel,
|
||||
sensorId: event.sensorId || event.sensor,
|
||||
state: event.state,
|
||||
success: true
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
channel,
|
||||
sensorId: event.sensorId || event.sensor,
|
||||
state: event.state,
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function sendWebhookNotification(event, channelConfig) {
|
||||
await postJson(channelConfig.url, {
|
||||
event,
|
||||
text: buildNotificationMessage(event)
|
||||
}, channelConfig.headers || {});
|
||||
}
|
||||
|
||||
async function sendTelegramNotification(event, channelConfig) {
|
||||
const url = `https://api.telegram.org/bot${channelConfig.botToken}/sendMessage`;
|
||||
await postJson(url, {
|
||||
chat_id: channelConfig.chatId,
|
||||
text: buildNotificationMessage(event)
|
||||
});
|
||||
}
|
||||
|
||||
async function postJson(url, payload, headers = {}) {
|
||||
if (typeof fetch !== 'function') {
|
||||
throw new Error('fetch no esta disponible en este runtime.');
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_NOTIFICATION_CONFIG,
|
||||
NOTIFICATION_CONFIG_FILE,
|
||||
dispatchAlarmNotifications,
|
||||
readNotificationConfig,
|
||||
redactNotificationConfig,
|
||||
shouldNotify,
|
||||
validateNotificationConfig,
|
||||
writeNotificationConfig
|
||||
};
|
||||
@ -0,0 +1,16 @@
|
||||
{
|
||||
"enabled": false,
|
||||
"minSeverity": "CRITICAL",
|
||||
"channels": {
|
||||
"webhook": {
|
||||
"enabled": false,
|
||||
"url": "",
|
||||
"headers": {}
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": false,
|
||||
"botToken": "",
|
||||
"chatId": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue