-
Notifications
You must be signed in to change notification settings - Fork 7
Open
Description
I modified all the DXClusterAPI code to run in native way at my shared hosting, so I cannot create pull request. But... I think you may like this as it's an easy patch, which will add SOTA spots from SOTA API into Wavelog.
This is part of the code to easy add SOTA spot's into the code, it ain't pretty, but it works 😉 :
"use strict";
const events = require("events");
// Prefer global fetch (Node 18+) or fall back to node-fetch
const fetch = (global.fetch ? global.fetch : require("node-fetch"));
const sleepNow = (delay) => new Promise((resolve) => setTimeout(resolve, delay));
module.exports = class SOTASpots extends events.EventEmitter {
constructor(opts = {}) {
super();
this.sotapollinterval = Math.max(30, Number(opts.sotapollinterval || 120)); // seconds
this.sotaspotcache = [];
this.apiUrl = "https://api2.sota.org.uk/api/spots/25/all";
}
getalloweddeviation(mode) {
// Allow FT8/FT4 ±3 kHz; others ±1 kHz (same approach as POTA)
switch ((mode || "").toUpperCase()) {
case "FT8":
case "FT4":
return 3;
default:
return 1;
}
}
/**
* Normalizes a frequency string from SOTA (typically MHz like "10.111")
* into a Number in kHz to match the implicit unit used by your POTA code.
*/
toKHz(freqStr) {
const mhz = Number(freqStr);
if (Number.isFinite(mhz)) return Math.round(mhz * 1000); // kHz
return NaN;
}
/**
* Start polling loop
*/
async run(opts = {}) {
while (true) {
// wait between polls
await sleepNow(this.sotapollinterval * 1000);
try {
// 10s timeout with AbortController
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), 10000);
const res = await fetch(this.apiUrl, { signal: controller.signal });
clearTimeout(t);
if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
/** @type {Array} */
const rawspots = await res.json();
const current = [];
for (const item of rawspots) {
const mode = item.mode || "";
const assoc = item.associationCode || "";
const summit = item.summitCode || "";
const summitDetails = item.summitDetails || "";
const comments = item.comments || "";
const spotter = item.callsign || ""; // "spotter"
const spotted = item.activatorCallsign || ""; // "activator"
// SOTA API provides MHz strings like "10.111"
const freqKHz = this.toKHz(item.frequency);
if (!Number.isFinite(freqKHz)) continue; // skip if no valid frequency
const ts = item.timeStamp;
const when = ts.endsWith('Z') ? new Date(ts) : new Date(ts + 'Z');
const msg =
(mode ? mode + " " : "") +
"SOTA @ " +
(assoc && summit ? `${assoc}/${summit}` : `${assoc}${summit}`) +
(summitDetails ? " " + summitDetails : "") +
(comments ? " (" + comments + ")" : "");
const dxSpot = {
spotter,
spotted,
frequency: freqKHz, // kHz to match your POTA emitter
message: msg,
when: when , // ISO timestamp from SOTA
additional_data: {
sota_ref: (assoc && summit) ? `${assoc}/${summit}` : (assoc || summit || ""),
sota_mode: mode
}
};
current.push(dxSpot);
// dedupe against previous cycle with kHz tolerance
const isNew = !this.sotaspotcache.some((s) =>
s.spotted === dxSpot.spotted &&
Math.abs(Number(s.frequency) - dxSpot.frequency) <= this.getalloweddeviation(mode) &&
s.message === dxSpot.message
);
if (isNew) this.emit("spot", dxSpot);
}
// Replace cache with the latest snapshot
this.sotaspotcache = current;
} catch (err) {
console.error("SOTA fetch failed:", err && err.stack ? err.stack : err);
}
}
}
};
There are some minor modifications of main app needed as well, the most important being:
- Setting
SOTA_INTEGRATIONandSOTA_POLLING_INTERVAL - adding at the beginning
const SOTASpots = require('./sota'); - adding (below similar POTA part)
if (config.includesotaspots || false) {
const sotapollinterval = config.sotapollinterval || 120;
const sota = new SOTASpots({ sotapollinterval });
sota.run();
// Tag as "pota" per your note; switch to "sota" if you prefer distinct source
sota.on('spot', async (spot) => {
await handlespot(spot, "sota"); // or "sota"
});
}
- adding (below similar POTA part)
if (spot_source === "sota" && spot.additional_data) {
dxSpot.dxcc_spotted = dxSpot.dxcc_spotted || {};
dxSpot.dxcc_spotted["sota_ref"] = spot.additional_data.sota_ref;
dxSpot.dxcc_spotted["sota_mode"] = spot.additional_data.sota_mode;
}
This is how it looks:

Metadata
Metadata
Assignees
Labels
No labels