Report decryption failure instead of "No HAN data received", and HAN as a service - #1241
Merged
Conversation
gskjold
force-pushed
the
fix/decrypt-error-not-no-data
branch
from
August 27, 2026 07:33
e94ab16 to
7058e38
Compare
A wrong encryption key made the device report "No HAN data received last
30s" while the meter was transmitting normally every 10 seconds. Two
separate causes:
The "no HAN data" watchdog is fed by getLastUpdateMillis(), which is only
assigned by the successful-decode paths. When decryption fails it stays 0
forever, so the check fires from 30s uptime onward on the errorBlink
cadence. PassiveMeterCommunicator now records the uptime of the last
checksum-verified HDLC/MBUS frame, and errorBlink distinguishes silence on
the HAN port from frames that arrive but cannot be decoded.
When no authentication key is configured, GCMParser decrypts without
verifying the GCM tag, so a wrong encryption key yields garbage plaintext
and unwrapData hits its default case with a different random tag every
frame. That was reported as DATA_PARSE_UNKNOWN_DATA ("Unknown data
received, check meter config"), pointing at the meter rather than the key.
It now returns the new GCM_DECRYPT_UNVERIFIED, which names the encryption
key as the thing to check and stays distinct from GCM_DECRYPT_FAILED, which
GCMParser also returns for malformed frames. With an authentication key set
the tag is verified, so the plaintext is genuine and the unknown-data
result is kept.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gskjold
force-pushed
the
fix/decrypt-error-not-no-data
branch
from
August 27, 2026 09:53
7058e38 to
3a6dec1
Compare
HAN was the last domain reporting only through the older per-domain error slots (data.he / data.hm), which allow one code per domain and carry no detail. It now also appears in the generic services array, so it gets the four-state colouring, the detail field and the Services badge aggregate for free. The older he/hm fields are untouched, so the header line and the cloud payload keep working unchanged. hanState() replaces the status logic that was inline in dataJson, and is now shared by the data feed, the services array and the aggregate. Its one behaviour change: waiting for the first frame after boot reports connecting rather than disabled, since state 0 renders as "Disabled" and the HAN port is never disabled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gskjold
force-pushed
the
fix/decrypt-error-not-no-data
branch
from
August 27, 2026 10:00
3a6dec1 to
95e45c8
Compare
🔧 PR Build ArtifactsVersion: All environments built successfully. Download the zip files:
|
This was referenced Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1245
Problem
A support case reported
"No HAN data received last 30s"filling the verbose telnet log while the meter was in fact transmitting perfectly. The attached log showed 21 consecutive GCM frames, invocation counter0xA680 → 0xA694with every delta exactly 1, roughly 10 s apart — and 34 "no HAN data" warnings in the same window. The real fault was a wrong encryption key. The misleading error sent the diagnosis toward HAN port, cable and hardware for a month.Two independent defects produce this.
1. The watchdog is fed by successful decode, not by frame reception
errorBlink()testslastErrorBlink - meterState.getLastUpdateMillis() > 30000.lastUpdateMillisis only ever assigned inside the successful-decode paths (IEC6205675.cpp,LNG.cpp,LNG2.cpp). If decryption or parsing fails it stays 0 forever, so the condition is true from 30 s uptime onward and fires on the 3 serrorBlinkcadence. The red LED gives the single blink that means "no data" while frames arrive every 10 s.2. A wrong encryption key is silently undetectable without an authentication key
The frames carried
SC = 0x30, so the authentication bit is set. ButGCMParseronly verifies the GCM tag when the configured authentication key is non-blank. With a blank key the ESP32 path takes thembedtls_gcm_starts/updatebranch — noauth_decrypt, no tag check. A wrong key therefore decrypts to noise, andunwrapDatahits itsdefault:case with a different random tag every frame (D5, 75, 90, 6F, 3E, D8, 04, …). That was reported asDATA_PARSE_UNKNOWN_DATA→ "Unknown data received, check meter config", which points at the meter instead of the key.Blanking the authentication key is standard advice for meters that don't use SC+AK authentication, so this path is commonly taken.
Commit 1 — report decryption failure instead of "No HAN data received"
MeterCommunicatorgainsgetLastFrameMillis(), defaulting to 0 (unsupported).PassiveMeterCommunicatorrecords the uptime of the last checksum-verified HDLC or MBUS frame — a verified link layer frame proves the meter is transmitting, regardless of what happens to the payload.errorBlink()case 0 now distinguishes the two situations. Silence on the HAN port keepsMETER_ERROR_NO_DATA(90) and the existing message. Frames arriving but nothing decoding logs"HAN frames received, but no data could be decoded"and falls back toMETER_ERROR_UNKNOWN_DATA(89) only if no more specific error is already set. The red single blink is unchanged in both cases — it is still an error.GCM_DECRYPT_UNVERIFIED(-54), returned byunwrapDatawhen garbage appears immediately after the GCM layer and no authentication key is configured. It stays distinct fromGCM_DECRYPT_FAILED(-52), whichGCMParseralso returns for malformed frames, so the new code specifically means "the key is what you should check". With an authentication key set the tag was verified, the plaintext is genuine, and an unrecognised payload really is unknown data —-9is kept for that.errors.han["-54"]added: "Decryption failed and could not be verified, check encryption key". It flows through the existing error table lookup, so the header line and the Services tile both pick it up with no new UI mechanism.Effect on the reported scenario
Before:
Header:
HAN: Unknown data received, check meter configAfter:
Header:
HAN: Decryption failed and could not be verified, check encryption keyCommit 2 — HAN as a service
There were two generations of error reporting. The older one is a hardcoded slot per domain —
data.he/me/eeplus status bytes, rendered by three copy-pastedbd-reddivs inHeader.svelte. The newer one is the genericsysinfo.services[]of{k, s, e, d, n}, built bybuildServicesJson(), rendered byServicesTile.svelte, aggregated intodata.sa. MQTT and price report through both. HAN was the last domain with only the older path.{"k":"han","s":…,"e":…,"d":"<meter model>"}, picking up the four-state colouring, the detail field and the Services badge aggregate. The olderhe/hmfields are untouched, soHeader.svelteand the cloud payload keep working unchanged.hanState()replaces the status logic that was inline indataJson(), now shared by the data feed, the services array and the aggregate.hanadded todefaultLabelsand toerrorNamespaceinServicesTile.svelte. No new rendering mechanism.Behaviour notes
hanState()returns connecting rather than disabled while waiting for the first frame after boot. State 0 renders as "Disabled" and the HAN port is never disabled. Visible effect: the header HAN badge is yellow instead of gray for at most 30 s post-boot, andsysinfo.bootingalready forces gray over most of that window.Translations
localazy/source/en.jsongainserrors.han["-54"]andstatus.services.han. Everything else reuses existing strings.Verification
pio test -e native— 17/17 pass, golden master unchanged.cd ui && npm run build— clean.pio run -e esp32— SUCCESS, flash 71.7%.🤖 Generated with Claude Code