Skip to content

SOTA API integration #31

@szporwolik

Description

@szporwolik

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_INTEGRATION and SOTA_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:

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions