diff --git a/.gitignore b/.gitignore index 9f2444e..82ff6f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules/ matrix_storage.json config.json +/matrix-storage.json diff --git a/index.js b/index.js index 002ce86..365947f 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ const mqtt = require('mqtt'); const { MatrixClient, SimpleFsStorageProvider, AutojoinRoomsMixin } = require('matrix-bot-sdk'); const config = require('./config.json'); +const rules = require('./rules.js'); // Matrix client const storage = new SimpleFsStorageProvider("matrix-storage.json"); @@ -8,93 +9,15 @@ const matrix = new MatrixClient(config.matrix.homeserverUrl, config.matrix.acces AutojoinRoomsMixin.setupOnClient(matrix); matrix.start().then(() => console.log("Matrix bot started")); -function formatMessageCQ(data) { - const { - callsign, - msg, - ccode, - country, - mode, - freq, - db, - dt, - locator - } = data; - - // Country flag emoji - const emojiFlag = countryCodeToFlagEmoji(ccode); - - // Convert frequency Hz to MHz with 3 decimal digits - const freqMHz = (freq / 1_000_000).toFixed(3); - - // RSSI visual bars - const rssiBars = snrToBars(db); - - // Google Maps locator search - const mapLink = `https://k7fry.com/grid/?qth=${locator}`; - - return `๐Ÿ“ก ${callsign} ${emojiFlag} ๐Ÿ“${locator} -๐Ÿ“ถ ${db} dB | ${dt} s | ${rssiBars} -๐Ÿ”Š ${freqMHz}MHz ${mode} `; +async function postMessage(msg) { + await matrix.sendMessage(config.matrix.roomId, { + msgtype: "m.text", + format: "org.matrix.custom.html", + formatted_body: msg, + body: msg, + }); } -function formatMessageWX(data) { - const { - temperature_F, - humidity, - rain_mm, - wind_dir_deg, - wind_avg_m_s, - wind_max_m_s, - light_lux, - uv, - mic, - mode, - timestamp - } = data; - - const emoji = { - temp: "๐ŸŒก๏ธ", - humidity: "๐Ÿ’ง", - rain: "๐ŸŒง๏ธ", - wind: "๐Ÿƒ", - light: "๐Ÿ’ก", - uv: "โ˜€๏ธ", - mic: "๐ŸŽ™๏ธ", - time: "๐Ÿ•’", - mode: "โš™๏ธ" - }; - - const dirEmoji = getWindDirectionEmoji(wind_dir_deg); - const readableTime = new Date(timestamp).toLocaleString(); - - return ` -${emoji.temp} Temperature: ${temperature_F.toFixed(1)} ยฐC -${emoji.humidity} Humidity: ${humidity}% (may be out of range!) -${emoji.rain} Rainfall: ${rain_mm} mm -${emoji.wind} Wind: ${wind_avg_m_s} m/s avg, ${wind_max_m_s} m/s max ${dirEmoji} (${wind_dir_deg}ยฐ) -${emoji.light} Light: ${light_lux} lux -${emoji.uv} UV Index: ${uv} -${emoji.time} Time: ${readableTime} -`.trim(); -} - - -// Helper: SNR (dB) to emoji "bar chart" -function snrToBars(db) { - const snr = Math.max(Math.min(db, 0), -30); // clamp -30 to 0 - const levels = 5; - const filled = Math.round(((snr + 30) / 30) * levels); - return 'โ–“'.repeat(filled) + 'โ–‘'.repeat(levels - filled); -} - -// Helper: ISO country code (e.g. "CA") โ†’ ๐Ÿ‡จ๐Ÿ‡ฆ emoji -function countryCodeToFlagEmoji(cc) { - if (!cc) return ''; - return cc.toUpperCase().replace(/./g, c => - String.fromCodePoint(0x1f1e6 - 65 + c.charCodeAt(0)) - ); -} // MQTT client const mqttClient = mqtt.connect(config.mqtt.url, { username: config.mqtt.username, @@ -114,30 +37,25 @@ mqttClient.on('connect', () => { mqttClient.on('message', async (topic, payload) => { const data = JSON.parse(payload.toString()); - console.log(`MQTT message on ${topic}: ${data?.msg}`); - // Simple filterโ€”customize as needed - if (data?.model?.includes('CQ')) { - await matrix.sendMessage(config.matrix.roomId, { - msgtype: "m.text", - format: "org.matrix.custom.html", - formatted_body: formatMessageCQ(data), - body: formatMessageCQ(data), - }); - } else if (data?.msg?.includes('Sharp-SPC775')) { - const msg = `๐ŸŒก๏ธ${data.temperature_C} | ๐Ÿ’ง${data.humidity}`; - await matrix.sendMessage(config.matrix.roomId, { - msgtype: "m.text", - format: "org.matrix.custom.html", - formatted_body: msg, - body: msg, - }); - } - else if (data?.msg?.includes('Cotech-367959')) { - await matrix.sendMessage(config.matrix.roomId, { - msgtype: "m.text", - format: "org.matrix.custom.html", - formatted_body: formatMessageWX(data), - body: formatMessageWX(data), - }); - } + // console.log(`MQTT message on ${topic}: ${data?.msg}`); + + rules.forEach((r) => { + if (r.rule(data)) { + console.log(`${topic} aplying ${r.name}`); + postMessage(r.msg(data)); + } + }); }); + + +// Simple filterโ€”customize as needed +/* if (data?.model?.includes('CQ')) { + * await postMessage(formatMessageCQ(data)); + + * } else if (data?.msg?.includes('Sharp-SPC775')) { + * const msg = `๐ŸŒก๏ธ${data.temperature_C} | ๐Ÿ’ง${data.humidity}`; + * await postMessage(msg); + * } + * else if (data?.msg?.includes('Cotech-367959')) { + * await postMessage(formatMessageCQWX(data)); + * } */ diff --git a/rules.js b/rules.js new file mode 100644 index 0000000..f6448b9 --- /dev/null +++ b/rules.js @@ -0,0 +1,141 @@ +// Helper: SNR (dB) to emoji "bar chart" +function snrToBars(db) { + const snr = Math.max(Math.min(db, 0), -30); // clamp -30 to 0 + const levels = 5; + const filled = Math.round(((snr + 30) / 30) * levels); + return 'โ–“'.repeat(filled) + 'โ–‘'.repeat(levels - filled); +} + +// Helper: ISO country code (e.g. "CA") โ†’ ๐Ÿ‡จ๐Ÿ‡ฆ emoji +function countryCodeToFlagEmoji(cc) { + if (!cc) return ''; + return cc.toUpperCase().replace(/./g, c => + String.fromCodePoint(0x1f1e6 - 65 + c.charCodeAt(0)) + ); +} +// Helper: emoji fr wind direction +function getWindDirectionEmoji(degree) { + // Normalize degree to 0-359 + const d = ((degree % 360) + 360) % 360; + + // Define ranges and corresponding emoji + const directions = [ + { min: 337.5, max: 360, emoji: 'โฌ‡๏ธ' }, // North -> Down (wind comes from North, goes South) + { min: 0, max: 22.5, emoji: 'โฌ‡๏ธ' }, // North + { min: 22.5, max: 67.5, emoji: 'โ†™๏ธ' }, // NE + { min: 67.5, max: 112.5, emoji: 'โฌ…๏ธ' }, // East -> Left + { min: 112.5, max: 157.5, emoji: 'โ†–๏ธ' }, // SE + { min: 157.5, max: 202.5, emoji: 'โฌ†๏ธ' }, // South -> Up + { min: 202.5, max: 247.5, emoji: 'โ†—๏ธ' }, // SW + { min: 247.5, max: 292.5, emoji: 'โžก๏ธ' }, // West -> Right + { min: 292.5, max: 337.5, emoji: 'โ†˜๏ธ' } // NW + ]; + + for (const dir of directions) { + if (d >= dir.min && d < dir.max) { + return dir.emoji; + } + } + + return 'โ“'; // fallback +} + +function formatMessageWX(data) { + const { + temperature_C, + temperature_F, + humidity, + rain_mm, + wind_dir_deg, + wind_avg_m_s, + wind_max_m_s, + light_lux, + uv, + mic, + mode, + model, + timestamp + } = data; + + const emoji = { + temp: "๐ŸŒก๏ธ", + humidity: "๐Ÿ’ง", + rain: "๐ŸŒง๏ธ", + wind: "๐Ÿƒ", + light: "๐Ÿ’ก", + uv: "โ˜€๏ธ", + mic: "๐ŸŽ™๏ธ", + time: "๐Ÿ•’", + mode: "โš™๏ธ" + }; + + const dirEmoji = getWindDirectionEmoji(wind_dir_deg); + const readableTime = new Date(timestamp).toLocaleString(); + // Acurite-986 is giving celsius under _F + const temperature = temperature_C || temperature_F; + + const response = + `${emoji.mic} reporting ${model}\n` + + (temperature !== undefined ? + `${emoji.temp} Temperature: ${temperature.toFixed(1)} ยฐC\n` : '') + + (humidity !== undefined ? + `${emoji.humidity} Humidity: ${humidity}%\n` : '') + + (rain_mm !==undefined ? + `${emoji.rain} Rainfall: ${rain_mm} mm\n` : '') + + (wind_dir_deg !==undefined ? + `${emoji.wind} Wind: ${wind_avg_m_s} m/s avg, ${wind_max_m_s}m/s max + ${dirEmoji} (${wind_dir_deg}ยฐ)\n` : '') + + (light_lux !== undefined ? + `${emoji.light} Light: ${light_lux} lux\n` : '') + + (uv !== undefined ? + `${emoji.uv} UV Index: ${uv}\n` : '') + + `${emoji.time} Time: ${readableTime}\n`; + + console.log(`hh response`, response); + return response.trim(); +} + +function formatMessageCQ(data) { + const { + callsign, + msg, + ccode, + country, + mode, + freq, + db, + dt, + locator + } = data; + + // Country flag emoji + const emojiFlag = countryCodeToFlagEmoji(ccode); + + // Convert frequency Hz to MHz with 3 decimal digits + const freqMHz = (freq / 1_000_000).toFixed(3); + + // RSSI visual bars + const rssiBars = snrToBars(db); + + // Google Maps locator search + const mapLink = `https://k7fry.com/grid/?qth=${locator}`; + + return `๐Ÿ“ก ${callsign} ${emojiFlag} ๐Ÿ“${locator} +๐Ÿ“ถ ${db} dB | ${dt} s | ${rssiBars} +๐Ÿ”Š ${freqMHz}MHz ${mode} `; +} + +module.exports = [ + { + name: "CQ", + rule: (data) => data?.msg?.includes('CQ'), + msg: formatMessageCQ, + }, + { + name: "WX", + // Not to miss the zero degree + rule: (data) => data?.temperature_C !== undefined + || data?.temperature_F !== undefined, + msg: formatMessageWX, + }, +];