From e96385a4d5a929bd35931bab895597189a449050 Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 28 Oct 2025 20:45:09 +0100 Subject: [PATCH 1/4] Meteo alarm integration --- README.md | 24 +++++++++- config.json | 26 ++++++++++- index.mjs | 121 ++++++++++++++++++++++++++++++++------------------- package.json | 3 +- 4 files changed, 126 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 1dc0402..39329b4 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ npm install . }, "timers": { "blitzCollection": 600000, // how often should we aggregate thunder data for evaluation - "pollWeatherAlerts": 600000 // how often should we download weather altert data + "meteoAlerts": 600000 // how often should we download weather altert data }, "blitzArea": { // thunder reporting area. if there is storm detected inside, report it "minLat": 47.51, @@ -47,6 +47,28 @@ npm install . "SW": "South-West", "W": "West", "NW": "North-West" + }, + "meteoAlerts": { // meteo alarm config sections + "enabled": true, // enables or disables meteo alarm + "timeout": 60, // how long should the warning be muted after sending, in minutes + "url": "https://feeds.meteoalarm.org/feeds/meteoalarm-legacy-atom-slovakia", // atom feed with warnings + "regions": [ + "Bratislava" // list of monitored regions/ areas + ], + "severity": { + "unknown": "Unknown", + "minor": "Minor", + "moderate": "Moderate", + "severe": "Severe", + "extreme ": "Extreme" + }, + "certainty": { + "observed": "Observed", + "likely": "Likely (> 50%)", + "possible": "Possible (<= 50%)", + "unlikely": "Unlikely (~ 0%)", + "unknown": "Unknown" + } } } ``` diff --git a/config.json b/config.json index 7094264..f4384b1 100644 --- a/config.json +++ b/config.json @@ -11,7 +11,7 @@ }, "timers": { "blitzCollection": 600000, - "pollWeatherAlerts": 600000 + "meteoAlerts": 600000 }, "blitzArea": { "minLat": 47.51, @@ -28,5 +28,27 @@ "SW": "South-West", "W": "West", "NW": "North-West" + }, + "meteoAlerts": { + "enabled": true, + "timeout": 60, + "url": "https://feeds.meteoalarm.org/feeds/meteoalarm-legacy-atom-slovakia", + "regions": [ + "Bratislava" + ], + "severity": { + "unknown": "Unknown", + "minor": "Minor", + "moderate": "Moderate", + "severe": "Severe", + "extreme ": "Extreme" + }, + "certainty": { + "observed": "Observed", + "likely": "Likely (> 50%)", + "possible": "Possible (<= 50%)", + "unlikely": "Unlikely (~ 0%)", + "unknown": "Unknown" + } } -} +} \ No newline at end of file diff --git a/index.mjs b/index.mjs index bcf3773..eef1bd3 100644 --- a/index.mjs +++ b/index.mjs @@ -3,6 +3,15 @@ import { DOMParser } from 'linkedom'; import * as mqtt from 'mqtt'; import * as utils from './utils.mjs'; import config from './config.json' with { type: 'json' }; +import Parser from 'rss-parser'; + +const optionsShort = { + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false +}; const port = process.argv[2] ?? config.port; @@ -20,6 +29,8 @@ let geoCache = {}; let blitzBuffer = []; +let meteoAlerts = [] + console.log(`Connecting to ${port}`); const connection = new NodeJSSerialConnection(port); @@ -35,11 +46,15 @@ connection.on('connected', async () => { } } - await pollWeatherAlerts(); await registerBlitzortungMqtt(blitzHandler, config.blitzArea); utils.setAlarm(config.weatherAlarm, sendWeather); setInterval(blitzWarning, config.timers.blitzCollection); + if (config.meteoAlerts.enabled) { + await checkMeteoAlerts(); + setInterval(checkMeteoAlerts, config.timers.meteoAlerts); + } + console.log('weatherBot ready.'); }); @@ -59,6 +74,67 @@ connection.on(Constants.PushCodes.MsgWaiting, async () => { } }); +async function checkMeteoAlerts() { + Object.keys(meteoAlerts).forEach(key => { + if (meteoAlerts[key] < Date.now() - (config.meteoAlerts.timeout * 60 * 1000)) { + delete meteoAlerts[key]; + } + }); + + let parser = new Parser({ + headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9' }, + xml2jsOptions: { + explicitArray: false, + }, + customFields: { + item: [ + ['cap:areaDesc', 'area'], + ['cap:event', 'event'], + ['cap:certainty', 'certainty'], + ['cap:severity', 'severity'], + ['cap:expires', 'end'], + ['cap:identifier', 'identifier'], + ['cap:onset', 'start'] + ] + } + }); + let warnigs = []; + const feed = await parser.parseURL(config.meteoAlerts.url); + if (feed.items && feed.items.length > 0) { + feed.items.forEach((item) => { + if (config.meteoAlerts.regions.includes(item.area)) { + + if (meteoAlerts[item.identifier]) return; + + warnigs.push({ + id: item.identifier, + region: item.area, + certainty: config.meteoAlerts.certainty[item.certainty.toLowerCase()], + severity: config.meteoAlerts.severity[item.severity.toLowerCase()], + event: item.event, + start: item.start, + end: item.end + }); + } + }); + } + + if (warnigs.length > 0) { + const sorted = warnigs.sort((a, b) => new Date(a.start) - new Date(b.start)); + sorted.forEach(item => { + const message = `${item.region} ${formatDate(item.start)} - ${formatDate(item.end)}\n${item.event}\nCertainty: ${item.certainty}, Severity: ${item.severity}`; + sendAlert(message, channels.weather); + meteoAlerts[item.id] = Date.now(); + utils.sleep(30 * 1000); + }); + } +} +function formatDate(date) { + + const dt = new Date(date); + return dt.toLocaleString("sk-SK", optionsShort) +} + async function onContactMessageReceived(message) { console.log('Received contact message', message); } @@ -67,20 +143,6 @@ async function onChannelMessageReceived(message) { console.log(`Received channel message`, message); } -async function pollWeatherAlerts() { - const warnings = await getWarnings(); - - for (const warning of warnings) { - const hash = utils.shaSumHex(`${warning.type}_${warning.severity}_${warning.startTime}_${warning.endTime}`); - if (seen.warnings[hash]) continue; - - await sendAlert(`[${warning.severity}][${warning.type}]: ${warning.text}`, channels.alerts); - seen.warnings[hash] = true; - } - - setTimeout(pollWeatherAlerts, config.timers.pollWeatherAlerts); -} - async function sendWeather(date) { const chunks = utils.splitStringToByteChunks(await getWeather(), 130); if (chunks.length === 0) return; @@ -115,34 +177,6 @@ async function getWeather() { return weather; } -async function getWarnings() { - const warnings = []; - - try { - const res = await fetch('https://www.meteoblue.com/sk/počasie/warnings/bratislava_slovensko_3060972'); - const html = await res.text(); - console.debug(`downloaded ${html.length} bytes from meteoblue.com`); - const document = new DOMParser().parseFromString(html, 'text/html'); - //console.debug(document); - - for (const warnEl of document.querySelectorAll('.warning-wrapper[defaultlang="sk"]')) { - console.debug(warnEl); - const glyphClasses = warnEl.querySelector('.warning-logos > .glyph').className.split(' '); - const type = glyphClasses.find(c => c.startsWith('warning-type-')).replace('warning-type-', ''); - const severity = glyphClasses.find(c => c.startsWith('sev-')).replace('sev-', ''); - const text = warnEl.querySelector('.warning-heading .title').textContent.trim(); - const [startTime, endTime] = warnEl.querySelector('.warning .times').getAttribute('title').replaceAll(/(Štart|Koniec): /g, '').split('\n'); - - warnings.push({ type, severity, text, startTime, endTime }); - } - } - catch (e) { - console.error(e) - } - - return warnings; -} - async function registerBlitzortungMqtt(blitzCallback, blitzArea) { const client = await mqtt.connectAsync('mqtt://blitzortung.ha.sed.pl:1883'); const decoder = new TextDecoder(); @@ -203,7 +237,6 @@ async function blitzWarning() { await sendAlert(`🌩️ ${location} (${distance * 10}km ${config.compasNames[heading]})`, channels.alerts); seen.blitz[key] = 1; } - //if (messageParts.length == 0) return; blitzBuffer = []; } diff --git a/package.json b/package.json index 95ae1ac..4630844 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "dependencies": { "@liamcottle/meshcore.js": "1.5.0", "linkedom": "0.18.12", - "mqtt": "5.14.0" + "mqtt": "5.14.0", + "rss-parser": "^3.13.0" } } From 69ad99d1576aa2aae68d4273917985ac0c30c76f Mon Sep 17 00:00:00 2001 From: Michal OM1PU Date: Wed, 3 Dec 2025 08:48:26 +0100 Subject: [PATCH 2/4] Added ignore file --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f14cc4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules + +package-lock.json \ No newline at end of file From 77052420a21b34839ce308ef187c86489a41b224 Mon Sep 17 00:00:00 2001 From: Michal OM1PU Date: Wed, 3 Dec 2025 09:16:53 +0100 Subject: [PATCH 3/4] certainty and severity filter --- README.md | 9 ++++++--- config.json | 13 +++++++++++-- index.mjs | 12 +++++++----- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 39329b4..a2f26ae 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,8 @@ npm install . }, "timers": { "blitzCollection": 600000, // how often should we aggregate thunder data for evaluation - "meteoAlerts": 600000 // how often should we download weather altert data + "meteoAlerts": 600000, // how often should we download weather altert data + "meteoAlerts": 600000 // how often should we weather alterts checked }, "blitzArea": { // thunder reporting area. if there is storm detected inside, report it "minLat": 47.51, @@ -51,18 +52,20 @@ npm install . "meteoAlerts": { // meteo alarm config sections "enabled": true, // enables or disables meteo alarm "timeout": 60, // how long should the warning be muted after sending, in minutes + "severityFilter": ["severe", "extreme"], // severity levels which will send + "certaintyFilter": ["likely", "observed"], // certainty levels which will send "url": "https://feeds.meteoalarm.org/feeds/meteoalarm-legacy-atom-slovakia", // atom feed with warnings "regions": [ "Bratislava" // list of monitored regions/ areas ], - "severity": { + "severity": { // severity translations "unknown": "Unknown", "minor": "Minor", "moderate": "Moderate", "severe": "Severe", "extreme ": "Extreme" }, - "certainty": { + "certainty": { // ceverity translations "observed": "Observed", "likely": "Likely (> 50%)", "possible": "Possible (<= 50%)", diff --git a/config.json b/config.json index ca4ea27..859df6b 100644 --- a/config.json +++ b/config.json @@ -33,6 +33,14 @@ "meteoAlerts": { "enabled": true, "timeout": 180, + "severityFilter": [ + "severe", + "extreme" + ], + "certaintyFilter": [ + "likely", + "observed" + ], "url": "https://feeds.meteoalarm.org/feeds/meteoalarm-legacy-atom-slovakia", "regions": [ "Bratislava" @@ -58,7 +66,7 @@ "thunderstorm": "Thunderstorm", "fog": "Fog", "hightemperature": "High Temperature", - "lowtemperature": "Low Temperature", + "lowtemperature": "Low Temperature", "coastalevent": "Coastal Event", "forestfire": "Forest Fire", "avalanche": "Avalanche", @@ -66,7 +74,8 @@ "flood": "Flood ", "rainflood": "Rain Flood", "marinehazard": "Marine Hazard ", - "drought": "Drought " + "drought": "Drought ", + "icing": "Icing " } } } \ No newline at end of file diff --git a/index.mjs b/index.mjs index f7b13f6..a6b9602 100644 --- a/index.mjs +++ b/index.mjs @@ -108,14 +108,16 @@ async function checkMeteoAlerts() { if (endTime < Date.now()) return; - if (meteoAlerts[item.identifier]) + if (meteoAlerts[item.identifier] + || !config.meteoAlerts.certaintyFilter.includes(item.certainty.toLowerCase()) + || !config.meteoAlerts.severityFilter.includes(item.severity.toLowerCase())) return; warnigs.push({ id: item.identifier, region: item.area, - certainty: item.certainty, - severity: item.severity, + certainty: item.certainty.toLowerCase(), + severity: item.severity.toLowerCase(), event: parseEvent(item.event), start: item.start, end: item.end @@ -132,8 +134,8 @@ async function checkMeteoAlerts() { start: formatDate(item.start), end: formatDate(item.end), event: config.meteoAlerts.events[item.event] ?? item.event, - severity: config.meteoAlerts.severity[item.severity.toLowerCase()], - certainty: config.meteoAlerts.certainty[item.certainty.toLowerCase()] + severity: config.meteoAlerts.severity[item.severity], + certainty: config.meteoAlerts.certainty[item.certainty] }); sendAlert(message, channels.weather); meteoAlerts[item.id] = Date.now(); From d06a8bf18f12651703eeee961da9051f8c28aa6c Mon Sep 17 00:00:00 2001 From: Michal OM1PU Date: Mon, 15 Dec 2025 10:07:03 +0100 Subject: [PATCH 4/4] Minor fixes --- README.md | 4 ++-- config.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a2f26ae..d80556c 100644 --- a/README.md +++ b/README.md @@ -52,8 +52,8 @@ npm install . "meteoAlerts": { // meteo alarm config sections "enabled": true, // enables or disables meteo alarm "timeout": 60, // how long should the warning be muted after sending, in minutes - "severityFilter": ["severe", "extreme"], // severity levels which will send - "certaintyFilter": ["likely", "observed"], // certainty levels which will send + "severityFilter": ["severe", "extreme"], // severity levels which will be send + "certaintyFilter": ["likely", "observed"], // certainty levels which will be send "url": "https://feeds.meteoalarm.org/feeds/meteoalarm-legacy-atom-slovakia", // atom feed with warnings "regions": [ "Bratislava" // list of monitored regions/ areas diff --git a/config.json b/config.json index 859df6b..bfc79c7 100644 --- a/config.json +++ b/config.json @@ -51,7 +51,7 @@ "minor": "Minor", "moderate": "Moderate", "severe": "Severe", - "extreme ": "Extreme" + "extreme": "Extreme" }, "certainty": { "observed": "Observed",