Skip to content

refactor(radar): typed status fields + soft-fail GenState parser (fixes #129)#130

Merged
jacaudi merged 1 commit into
mainfrom
worktree-refactor-radar-typed-status
May 18, 2026
Merged

refactor(radar): typed status fields + soft-fail GenState parser (fixes #129)#130
jacaudi merged 1 commit into
mainfrom
worktree-refactor-radar-typed-status

Conversation

@jacaudi
Copy link
Copy Markdown
Owner

@jacaudi jacaudi commented May 18, 2026

Summary

Fixes #129. Refactors radar.Data's discrete-value fields onto named string types per go-standards.md §4.1/§4.3, and replaces simplifyGeneratorState with a soft-failing ParseGeneratorState so a degraded NWS payload no longer aborts the 5-minute poll cycle.

Combines Option 5 (typed enums + comparator skip-Unknown) and Option E (symmetric refactor of all status fields) from the design discussion. Option 2's sentinel-error pattern is folded into ParseGeneratorState, and Option 3's token-set parsing lives inside it so the soft-fail is rare in practice.

Changes

Typed enums (§4.1 / §4.3)

Six named string types with const blocks, Stringer methods, and <Type>Unknown sentinels:

Type Knowns Sentinel
VCP VCPR12 / R31 / R35 / R112 / R212 / R215 VCPUnknown
RadarMode ModeClearAir, ModePrecipitation ModeUnknown
RadarStatus StatusOperate / Standby / Test / OfflineOperate StatusUnknown
OperabilityStatus OpStatusOnline / Maintenance / Offline / Standby / Coasting OpStatusUnknown
PowerSource PowerSourceUtility / Generator PowerSourceUnknown
GeneratorState GenStateOn / Off GenStateUnknown

The underlying string type still accepts arbitrary NWS-supplied values — a surprise input is preserved verbatim for forensic logging rather than squashed into the Unknown bucket. Const blocks document the value space we've actually observed in the wild (per the API survey, though that doc isn't committed in this branch).

Parse<X> constructors

  • ParseVCP / ParseRadarStatus / ParseOperabilityStatus / ParsePowerSource — empty input → <Type>Unknown, non-empty passes through verbatim. No error possible.
  • ParseGeneratorState — semantic token-set parser. Generator On ∈ pipe-tokens → GenStateOn; only Utility PWR AvailableGenStateOff; everything else (including empty) → GenStateUnknown plus an error wrapping ErrUnknownGeneratorState.

FetchData soft-fail (the actual #129 fix)

Mirrors the ErrUnknownVCP shape from #121 (PR #123). When ParseGeneratorState reports ErrUnknownGeneratorState, FetchData logs at WARN with the raw input + station ID and continues with GenState = GenStateUnknown. The previous implementation returned ("", "unknown input") and the caller aborted the entire poll — three of five aborted polls observed in v2.13.1 production logs over 8 days hit that path.

Comparator skip-Unknown

CompareData now skips reporting a flip when the new value equals the field's Unknown sentinel. monitor.processStation only updates the cache when changed=true, so a degraded poll leaves the cache holding the last known-good value; recovery to the same value produces a true no-change. One-sided by design — Unknown→OK on recovery still fires (genuine state-clarification, not noise).

Tests

  • TestParseGeneratorState — table-driven. Covers all four canonical raw strings, token-order robustness, and three fallthrough cases (empty, totally novel, ambiguous) with errors.Is(err, ErrUnknownGeneratorState) assertions.
  • ExampleParseGeneratorState — §8.3 testable example, output-checked.
  • TestCompareData gains three sub-tests for skip-Unknown:
    • skips flip to Unknown on every field — full degraded payload, expect zero notifications
    • flip FROM Unknown is reported (recovery) — first-run-with-degraded-data recovery still fires
    • partial degradation: change on known field only — VCP→Unknown alone with other fields stable produces no notification
  • TestGetMode / TestGetVCPInfo updated to use the new typed values.
  • MockDataFetcher default response uses the new constants.

go test ./... green across all 9 packages; go vet ./... clean.

Notes

  • Data.Name remains string (identifying metadata, not a status).
  • simplifyGeneratorState (lowercase, package-private) is removed in favor of ParseGeneratorState. No external callers used the old name.
  • monitor.go needed no changes — Mode interpolation goes through %s (Stringer) and VCP equality is type-preserving.

Test plan

  • go test ./... — all 9 packages pass
  • go vet ./... clean
  • Comparator unit tests cover skip-Unknown happy + recovery paths
  • ExampleParseGeneratorState round-trips through go test
  • Deploy to Talos selfhosted and confirm no spurious notifications on next NWS-degraded-payload event (validation pending real-world recurrence; this is the same failure mode that hit 5 times in 8 days on v2.13.1)

Closes #129. Future symmetric extension targets — alarmSummary from #128, plus controlStatus, superResolutionStatus, and the performance.properties.* encoder-light fields — can layer onto this same shape.

🤖 Generated with Claude Code

#129)

Refactors radar.Data's discrete-value fields onto named string types per
go-standards.md §4.1/§4.3, and replaces simplifyGeneratorState with a
soft-failing ParseGeneratorState so a degraded NWS payload no longer
aborts the whole 5-minute poll cycle (issue #129).

## What changed

Typed enums for VCP, RadarMode, RadarStatus, OperabilityStatus,
PowerSource, GeneratorState — each a named string type with a
const block declaring known values, a String() method per §4.3, and
a <Type>Unknown sentinel for missing/unparseable data. Underlying
string type still accepts arbitrary NWS-supplied values so a surprise
input is preserved verbatim for forensic logging.

Parse<X> constructors per field:
  - ParseVCP / ParseRadarStatus / ParseOperabilityStatus /
    ParsePowerSource: empty input → <Type>Unknown, non-empty passes
    through verbatim. No error possible.
  - ParseGeneratorState: semantic token-set parser. `Generator On` in
    the pipe-separated input → GenStateOn; only `Utility PWR Available`
    → GenStateOff; everything else (including empty) → GenStateUnknown
    with ErrUnknownGeneratorState wrapping the raw input.

FetchData updated to use errors.Is on the new ErrUnknownGeneratorState
sentinel — mirrors the soft-fail shape established for ErrUnknownVCP in
#121 (PR #123). Three of five aborted polls observed in v2.13.1 prod
logs over 8 days were caused by simplifyGeneratorState returning
("", "unknown input") on degraded NWS payloads; this commit removes
that abort path.

Comparator (CompareData) now skips reporting a flip when the NEW value
equals the field's Unknown sentinel. Caller in monitor.processStation
only updates the cache when changed=true, so this leaves the cache
holding the last known-good value during a degraded poll; recovery to
the same value produces a real no-change. One-sided: Unknown→OK on
recovery still fires, since that's genuine state-clarification.

## Tests

  - TestParseGeneratorState — table-driven, covers all four canonical
    inputs plus empty / novel / ambiguous fallthrough cases with
    errors.Is(err, ErrUnknownGeneratorState) assertions.
  - ExampleParseGeneratorState — §8.3 testable example, output-checked.
  - TestCompareData adds three sub-tests for the skip-Unknown behavior:
    "skips flip to Unknown on every field", "flip FROM Unknown is
    reported (recovery)", and "partial degradation: change on known
    field only".
  - TestGetMode / TestGetVCPInfo updated to use the new typed values.
  - benchmark_test.go [] string of VCP codes → []VCP.
  - MockDataFetcher default response uses the new constants.

`go test ./...` green across all 9 packages; `go vet ./...` clean.

## Notes

  - Data.Name remains string (identifying metadata, not a status).
  - simplifyGeneratorState (lowercase, package-private) is removed in
    favor of ParseGeneratorState. No external callers used the old
    name; tests update in lockstep.
  - monitor.go needed no changes — Mode interpolation goes through %s
    (Stringer) and VCP equality is type-preserving.
  - Closes #129.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jacaudi jacaudi merged commit 66601ed into main May 18, 2026
10 checks passed
@jacaudi jacaudi deleted the worktree-refactor-radar-typed-status branch May 18, 2026 18:49
@wall-e-one
Copy link
Copy Markdown
Contributor

wall-e-one Bot commented May 18, 2026

🎉 This PR is included in version 2.14.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@wall-e-one wall-e-one Bot added the released label May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

radar: soft-fail simplifyGeneratorState on unknown input (mirror #121 VCP pattern)

1 participant