Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions config_clean.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ config.wwff = {
listUrl: 'http://wwff.co/wwff-data/wwff_directory.csv'
};

config.wwtota = {
apiKey: '<redacted>',
spotsUrl: 'https://wwtota.com/apidata/cluster.php',
listUrl: 'https://wwtota.com/apidata/tower.php',
refreshInterval: 60*1000,
spotMaxAge: 5*60*1000
};

config.mongodb = {
url: 'mongodb://hamalert:<redacted>@localhost:27017/hamalert',
dbName: 'hamalert'
Expand Down Expand Up @@ -292,6 +300,7 @@ config.matcher = {
'summitRegion',
'summitRef',
'wwffRef',
'wwtotaRef',
'mode',
'band',
'spotter',
Expand Down Expand Up @@ -327,6 +336,7 @@ config.matcher = {
'summitRegion',
'summitRef',
'wwffRef',
'wwtotaRef',
'spotter',
'spotterPrefix',
'daysOfWeek',
Expand Down
2 changes: 2 additions & 0 deletions hamutil.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ exports.makeSpotParams = function(spot, comment, actions) {
wwffRef: spot.wwffRef,
wwffDivision: spot.wwffDivision,
wwffName: spot.wwffName,
wwtotaRef: spot.wwtotaRef,
wwtotaName: spot.wwtotaName,
iotaGroupRef: spot.iotaGroupRef,
iotaGroupName: spot.iotaGroupName
};
Expand Down
10 changes: 9 additions & 1 deletion notify/email.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,15 @@ class EmailNotifier extends Notifier {
text += `Park name: ${spot.wwffName}\n`;
}
}


if (spot.wwtotaRef) {
text += "\n";
text += `Tower ref: ${spot.wwtotaRef}\n`;
if (spot.wwtotaName) {
text += `Tower name: ${spot.wwtotaName}\n`;
}
}

if (spot.iotaGroupRef) {
text += "\n";
text += `IOTA ref: ${spot.iotaGroupRef}\n`;
Expand Down
3 changes: 3 additions & 0 deletions notify/telnetconn.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ class TelnetConnection extends EventEmitter {
if (spot.wwffRef) {
commentElements.push(spot.wwffRef);
}
if (spot.wwtotaRef) {
commentElements.push(spot.wwtotaRef);
}
if (spot.iotaGroupRef) {
commentElements.push(spot.iotaGroupRef);
}
Expand Down
49 changes: 46 additions & 3 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const util = require('util');
const SotaSpotReceiver = require('./sotaspots');
const PotaSpotReceiver = require('./potaspots');
const WwtotaSpotReceiver = require('./wwtotaspots');
const RbnReceiver = require('./rbn');
const PskReporterReceiver = require('./pskreporter');
const ClusterReceiver = require('./cluster');
Expand Down Expand Up @@ -30,6 +31,7 @@ const config = require('./config');
const summitRefRegex = /([a-zA-Z0-9]{1,8}\/[a-zA-Z]{2})\-?((?:[0-9][0-9][1-9])|(?:[0-9][1-9][0])|(?:[1-9][0-9][0]))/;
const sotaRefRegex = /^(.+)\/(.+)\-(\d+)$/;
const wwffRefRegex = /\b([a-z0-9]{1,5})-(\d{4})\b/i;
const wwtotaRefRegex = /\b([a-z]{3})-(\d{4})\b/i;
const iotaRefRegex = /(?:^|\s)(AF|AN|AS|EU|NA|OC|SA)[ -]?(\d{3})\b/i;
const potaLocationStateRegex = /^((US|CA)-..,?)+$/;

Expand Down Expand Up @@ -85,7 +87,11 @@ function startReceivers() {
let potaSpotReceiver = new PotaSpotReceiver(db);
potaSpotReceiver.on('spot', notifySpot);
potaSpotReceiver.start();


let wwtotaSpotReceiver = new WwtotaSpotReceiver(db);
wwtotaSpotReceiver.on('spot', notifySpot);
wwtotaSpotReceiver.start();

config.rbn.forEach(rbnConfig => {
let rbnReceiver = new RbnReceiver(rbnConfig);
rbnReceiver.on('spot', notifySpot);
Expand Down Expand Up @@ -136,7 +142,7 @@ function runMatcher(spot) {
// Find matching triggers using matcher via JSON-RPC
let conditions = {};

let fields = ['source', 'callsign', 'fullCallsign', 'summitAssociation', 'summitRegion', 'summitPoints', 'summitActivations', 'summitRef', 'wwffRef', 'iotaGroupRef', 'mode', 'time', 'spotter', 'state', 'spotterState', 'qsl', 'prefix', 'spotterPrefix', 'speed', 'snr'];
let fields = ['source', 'callsign', 'fullCallsign', 'summitAssociation', 'summitRegion', 'summitPoints', 'summitActivations', 'summitRef', 'wwffRef', 'wwtotaRef', 'iotaGroupRef', 'mode', 'time', 'spotter', 'state', 'spotterState', 'qsl', 'prefix', 'spotterPrefix', 'speed', 'snr'];
for (let field of fields) {
if (spot[field] !== undefined) {
conditions[field] = spot[field];
Expand All @@ -163,7 +169,11 @@ function runMatcher(spot) {
if (spot.wwffDivision) {
conditions.wwffDivision = [spot.wwffDivision, "*"];
}


if (spot.wwtotaRef) {
conditions.wwtotaRef = [spot.wwtotaRef, "*"];
}

if (spot.iotaGroupRef) {
conditions.iotaGroupRef = [spot.iotaGroupRef, "*"];
}
Expand Down Expand Up @@ -418,6 +428,11 @@ function normalizeSpot(spot, callback) {
findIotaGroupRef(spot, callback);
},

// Find WWTOTA ref
(callback) => {
findWwtotaRef(spot, callback);
},

// Find callsign info
(callback) => {
findCallsignInfo(spot, callback);
Expand Down Expand Up @@ -532,6 +547,34 @@ function findIotaGroupRef(spot, callback) {
}
}

function findWwtotaRef(spot, callback) {
// Find a WWTOTA reference in the spot comment, and populate the WWTOTA fields if
// a valid reference has been found
if (spot.wwtotaRef) {
// Already have a WWTOTA reference
callback();
return;
}

let matches = wwtotaRefRegex.exec(spot.rawText);
if (matches) {
// Look up tower ref in database to be sure
let towerRef = matches[1].toUpperCase() + '-' + matches[2];
db.collection('wwtotaTowers').findOne({Ref: towerRef}, {}, (err, tower) => {
if (tower) {
spot.wwtotaRef = towerRef;
spot.wwtotaName = tower.Name;
} else {
console.info(`Tower ${towerRef} not found in database`);
}

callback();
});
} else {
callback();
}
}

function findCallsignInfo(spot, callback) {
db.collection('callsignInfo').findOne({callsign: spot.fullCallsign}, {}, (err, callsignInfo) => {
if (callsignInfo) {
Expand Down
45 changes: 45 additions & 0 deletions tools/updateWwtota.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const axios = require('axios');
const MongoClient = require('mongodb').MongoClient;
const config = require('../config');
const assert = require('assert');

let client = new MongoClient(config.mongodb.url)
client.connect(async (err) => {
assert.equal(null, err);
let db = client.db(config.mongodb.dbName);

await processWwtotaList(db);
client.close();
});

async function processWwtotaList(db) {
let response = await axios.get(config.wwtota.listUrl, {
headers: {
'User-Agent': 'HamAlert/1.0 (+https://hamalert.org)'
},
params: {
key: config.wwtota.apiKey,
}
});

// Loop through towers
let towers = [];
for (let towerInfo of response.data) {
let tower = {
'Ref': towerInfo.ref,
'Name': towerInfo.name,
'Lat': towerInfo.lat,
'Lon': towerInfo.lon,
};

towers.push(tower);
}

if (towers.length < 2000) {
throw new Error("Bad number of WWTOTA towers, expecting more than 2000");
}

let collection = db.collection('wwtotaTowers');
await collection.deleteMany({manual: {$not: {$eq: true}}});
await collection.insertMany(towers);
}
107 changes: 107 additions & 0 deletions wwtotaspots.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
const axios = require('axios');
const config = require('./config');
const EventEmitter = require('events');
const util = require('util');
const crypto = require('crypto');
const sprintf = require('sprintf');

class WwtotaSpotReceiver extends EventEmitter {
constructor(db) {
super();
this.db = db;
this.lastProcessedTime = null;
}

start() {
setInterval(() => {
this.refreshSpots();
}, config.wwtota.refreshInterval);
this.refreshSpots();
}

async refreshSpots() {
console.log("Refreshing WWTOTA JSON feed");

let response = await axios.get(config.wwtota.spotsUrl, {
headers: {
'User-Agent': 'HamAlert/1.0 (+https://hamalert.org)'
},
params: {
key: config.wwtota.apiKey,
}
})
if (response.status !== 200) {
throw `Bad status code ${response.statusCode} from WWTOTA`;
}

if (!Array.isArray(response.data.spots)) {
throw `Expected array from WWTOTA, but got something else`;
}

// reverse to process oldest to newest
response.data.spots.reverse()
response.data.spots.forEach(spot => {
this.processJsonSpot(spot)
});
if (response.data.spots.length > 0) {
this.lastProcessedTime = response.data.spots[response.data.spots.length - 1].time; // Previously reversed, so newest last
}
}

processJsonSpot(jsonSpot) {
try {
jsonSpot.time = new Date(jsonSpot.time_utc);

// Check if spot newer that last batch processed
// edited spot have new timestamp as well
if (this.lastProcessedTime && this.lastProcessedTime >= jsonSpot.time) {
return;
}

// Ignore old spots
if ((new Date() - jsonSpot.spotTime) > config.wwtota.spotMaxAge) {
return;
}

jsonSpot.callsign = jsonSpot.callsign.toUpperCase().replace(/\s/g, '');

// Clean up frequency
let frequency = sprintf("%.4f", jsonSpot.frequency / 1000);
let spot = {
source: 'wwtota',
time: jsonSpot.time.toISOString().substring(11, 16),
fullCallsign: jsonSpot.callsign,
wwtotaRef: jsonSpot.tower_ref,
wwtotaName: jsonSpot.tower_name,
frequency,
mode: jsonSpot.mode.toLowerCase().trim(),
comment: "",
spotter: jsonSpot.spotter.toUpperCase().trim().replace('-#', '')
};

spot.rawText = `${spot.time} ${spot.fullCallsign} in ${spot.wwtotaRef} (${spot.wwtotaName}) ${spot.frequency}`;
if (spot.mode) {
spot.rawText += ` ${spot.mode.toUpperCase()}`;
}
if (spot.comment) {
spot.rawText += `: ${spot.comment}`;
}
spot.title = `WWTOTA ${spot.fullCallsign} in ${spot.wwtotaRef} (${spot.frequency}`;
if (spot.mode) {
spot.title += " " + spot.mode.toUpperCase();
}
spot.title += ")";

//console.log(spot);
this.emit("spot", spot);

} catch (e) {
console.error("Exception while processing WWTOTA spot", e);
}
}
}

let rec = new WwtotaSpotReceiver();
rec.start();

module.exports = WwtotaSpotReceiver;