feat(wavelog): push station frequency/mode to Wavelog per operator
Add a CAT integration so the station's current frequency and mode are
reported to each operator's personal Wavelog logbook via Wavelog's radio
API (POST <baseUrl>/index.php/api/radio).
- New plugin rms.wavelog.cat (capability logging.cat.publish): posts
{key, radio, frequency, mode}. Base URL / radio name come from plugin
settings or env (WAVELOG_BASE_URL, WAVELOG_RADIO_NAME); no secrets in repo.
- Core hook in the OpenWebRX band-select flow pushes centerFreqHz + startMod
for the active operator, best-effort (never breaks the band change).
- Per-user Wavelog API key stored on the user record; PUT /v1/me/wavelog-key
to set it, returned by GET /v1/me and the OpenWebRX session issue response.
- UI: Wavelog API key field in the OpenWebRX panel (visible when generating
the session link), with i18n (de/en).
- Docs, .env.example entries, and a unit test for the mode mapping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
116
plugins/rms.wavelog.cat/index.js
Normal file
116
plugins/rms.wavelog.cat/index.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// rms.wavelog.cat
|
||||
//
|
||||
// Publishes the station's current frequency/mode to a Wavelog logbook via its
|
||||
// CAT radio API (`POST <baseUrl>/index.php/api/radio`). The caller passes the
|
||||
// per-operator Wavelog API key, so each operator's frequency lands in their own
|
||||
// Wavelog account (cat.user_id = key owner).
|
||||
//
|
||||
// Generic by design (public repo): the Wavelog base URL and radio name come from
|
||||
// plugin settings or runtime env, never hardcoded. No secrets are stored here.
|
||||
|
||||
const ADIF_MODE_BY_STARTMOD = {
|
||||
usb: "SSB",
|
||||
lsb: "SSB",
|
||||
ssb: "SSB",
|
||||
cw: "CW",
|
||||
cwr: "CW",
|
||||
fm: "FM",
|
||||
nfm: "FM",
|
||||
wfm: "FM",
|
||||
am: "AM",
|
||||
sam: "AM",
|
||||
dmr: "DMR",
|
||||
rtty: "RTTY",
|
||||
ft8: "FT8",
|
||||
ft4: "FT4",
|
||||
psk: "PSK",
|
||||
psk31: "PSK"
|
||||
};
|
||||
|
||||
function toAdifMode(mode) {
|
||||
const key = String(mode || "").trim().toLowerCase();
|
||||
if (!key) {
|
||||
return "";
|
||||
}
|
||||
if (ADIF_MODE_BY_STARTMOD[key]) {
|
||||
return ADIF_MODE_BY_STARTMOD[key];
|
||||
}
|
||||
// Fall back to the raw value in upper case; Wavelog maps common rig aliases itself.
|
||||
return String(mode).trim().toUpperCase();
|
||||
}
|
||||
|
||||
async function createPlugin(ctx) {
|
||||
return {
|
||||
async execute(action, input) {
|
||||
if (action !== "publish") {
|
||||
throw new Error(`Unknown action: ${action}`);
|
||||
}
|
||||
return publish(ctx, input || {});
|
||||
},
|
||||
async health() {
|
||||
const baseUrl = resolveBaseUrl(ctx, {});
|
||||
return { ok: true, configured: Boolean(baseUrl) };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function resolveBaseUrl(ctx, input) {
|
||||
return String(input.baseUrl || ctx.getSetting("baseUrl", ctx.env.WAVELOG_BASE_URL || ""))
|
||||
.trim()
|
||||
.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
async function publish(ctx, input) {
|
||||
const baseUrl = resolveBaseUrl(ctx, input);
|
||||
const apiKey = String(input.apiKey || "").trim();
|
||||
const radio = String(input.radioName || ctx.getSetting("radioName", ctx.env.WAVELOG_RADIO_NAME || "RMS Remote")).trim();
|
||||
const frequencyHz = Math.round(Number(input.frequencyHz));
|
||||
const mode = toAdifMode(input.mode);
|
||||
|
||||
if (!baseUrl) {
|
||||
return { ok: false, skipped: true, reason: "wavelog.baseUrl_missing" };
|
||||
}
|
||||
if (!apiKey) {
|
||||
return { ok: false, skipped: true, reason: "wavelog.apiKey_missing" };
|
||||
}
|
||||
if (!Number.isFinite(frequencyHz) || frequencyHz <= 0) {
|
||||
return { ok: false, skipped: true, reason: "wavelog.frequency_missing" };
|
||||
}
|
||||
|
||||
const payload = {
|
||||
key: apiKey,
|
||||
radio: radio || "RMS Remote",
|
||||
frequency: frequencyHz
|
||||
};
|
||||
if (mode) {
|
||||
payload.mode = mode;
|
||||
}
|
||||
|
||||
const timeoutMs = Number(ctx.getSetting("timeoutMs", ctx.env.WAVELOG_TIMEOUT_MS || 8000));
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), Number.isFinite(timeoutMs) ? timeoutMs : 8000);
|
||||
try {
|
||||
const resp = await fetch(`${baseUrl}/index.php/api/radio`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
signal: controller.signal
|
||||
});
|
||||
const text = await resp.text().catch(() => "");
|
||||
if (!resp.ok) {
|
||||
// Never surface the API key; only status + a short body excerpt.
|
||||
return { ok: false, status: resp.status, body: String(text).slice(0, 200) };
|
||||
}
|
||||
return { ok: true, status: resp.status, radio: payload.radio, frequency: frequencyHz, mode: mode || null };
|
||||
} catch (error) {
|
||||
return { ok: false, error: String(error && error.message ? error.message : error) };
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createPlugin,
|
||||
// exported for unit tests
|
||||
toAdifMode
|
||||
};
|
||||
18
plugins/rms.wavelog.cat/manifest.json
Normal file
18
plugins/rms.wavelog.cat/manifest.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "rms.wavelog.cat",
|
||||
"name": "Wavelog CAT",
|
||||
"version": "1.0.0",
|
||||
"apiVersion": "1.0",
|
||||
"capabilities": [
|
||||
"logging.cat.publish"
|
||||
],
|
||||
"settingsSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"baseUrl": { "type": "string" },
|
||||
"radioName": { "type": "string" },
|
||||
"timeoutMs": { "type": "number", "minimum": 1000, "maximum": 60000 }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user