feat: HAMLOG.online upload and confirmations, Yaesu antenna, a named port holder

HAMLOG.online is a seventh external service: one API key, one ADIF record per
QSO, immediate / delayed / on-close like the rest. They publish no API
documentation, so the protocol is read from THEIR OWN client — the HAMLOG Agent
(github.com/hamlogonline/Agent), which is the authoritative source short of
asking them:

	POST https://hamlog.online/api/agent/
	{"ADIFADD": {"APIKEY": k, "ADIFDATA": record}}   → {"STATUS":"OK"}
	{"KEYSTATUS": {"APIKEY": k}}                     → {"STATUS":"OK","CALLSIGN":…}

Success is STATUS == OK, not "no ERROR field": their failure carries ERROR and
no STATUS, and reading an unknown reply — a proxy page, a maintenance notice —
as an acceptance is how a contact goes missing without anyone noticing.

KEYSTATUS buys something no other service here offers: the key can be checked
BEFORE the first QSO, and the answer names the account. A key pasted from
another callsign is caught in the settings panel rather than through a week of
silent refusals.

Their confirmations are also an award source now, ticked like LoTW rather than
expressed through "custom". It reads the ADIF extras, not a column: the standard
names a field for hamlog.EU and none for hamlog.ONLINE, and borrowing the other
site's field would write a falsehood into every exported log. Three plausible
key names from their own export are accepted too, so nobody has to rename a
column by hand after an export.

Yaesu gains antenna selection (AN), remembered per band — the antenna picked on
a band comes back with it, with no table to fill in anywhere. Rigs with one
socket never answer AN and never show the row; the log says which case it is.

And a serial port that is refused now names its likely holder. OmniRig stays
resident once activated and keeps the port of the rig configured in it, so a
native backend never gets it — "Serial port busy" alone accused nobody, and an
FTDX10 spent a morning being blamed for it.
This commit is contained in:
2026-08-23 12:18:27 +02:00
parent 3014796c1d
commit ee148732dd
21 changed files with 603 additions and 18 deletions
+31
View File
@@ -64,3 +64,34 @@ func TestConfirmedSources(t *testing.T) {
}
}
}
// HAMLOG.online is a named confirmation source, not something to express through
// "custom": an award ticks it the way it ticks LoTW.
func TestHamlogConfirmationSource(t *testing.T) {
def := Def{Confirm: []string{"hamlog"}, Validate: []string{"hamlog"}}
yes := func(extras map[string]string) bool {
return Confirmed(&qso.QSO{Extras: extras}, def, def.Confirm)
}
if !yes(map[string]string{HamlogQSLKey: "Y"}) {
t.Error("our own key did not confirm")
}
// Their export's field name is unpublished, so the plausible shapes count too.
if !yes(map[string]string{"APP_HAMLOG_QSL": "20260823"}) {
t.Error("a date in an alternative key did not confirm")
}
// A value is not a confirmation when it says no.
if yes(map[string]string{HamlogQSLKey: "N"}) {
t.Error(`"N" was taken as a confirmation`)
}
if yes(map[string]string{HamlogQSLKey: " "}) {
t.Error("blank was taken as a confirmation")
}
if yes(nil) || yes(map[string]string{"SOMETHING_ELSE": "Y"}) {
t.Error("confirmed with nothing to confirm it")
}
// And it does not leak into an award that never asked for it.
other := Def{Confirm: []string{"lotw"}}
if Confirmed(&qso.QSO{Extras: map[string]string{HamlogQSLKey: "Y"}}, other, other.Confirm) {
t.Error("a hamlog confirmation counted for an award that only accepts LoTW")
}
}