fix: fall back to CipherV2 when Discovery can't decrypt a scan reply with CipherV1 - #177
Open
jitsumi wants to merge 1 commit into
Open
fix: fall back to CipherV2 when Discovery can't decrypt a scan reply with CipherV1#177jitsumi wants to merge 1 commit into
jitsumi wants to merge 1 commit into
Conversation
jitsumi
marked this pull request as draft
July 21, 2026 15:04
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #177 +/- ##
==========================================
+ Coverage 95.71% 96.02% +0.31%
==========================================
Files 8 8
Lines 770 780 +10
==========================================
+ Hits 737 749 +12
+ Misses 33 31 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…with CipherV1 Discovery.datagram_received only ever decrypts scan replies with the generic CipherV1 (AES-ECB) cipher, unlike Device.bind() which already tries CipherV1 then falls back to CipherV2 (AES-GCM). Devices whose firmware uses the newer GCM-only protocol for their scan reply (observed on retrofitted and newer Gree Wi-Fi modules) fail to decrypt with CipherV1, raising an unhandled ValueError inside the asyncio UDP callback, and are silently dropped from discovery. Extract the one-line decrypt call in DeviceProtocolBase2.datagram_received (network.py) into a small _decrypt_pack() hook, so Discovery only needs to override that instead of duplicating the surrounding guard/parse/log/ dispatch logic. Discovery._decrypt_pack tries CipherV1 then CipherV2, returning None when a reply is undecryptable under both, which packet_received's existing "unexpected response" handling already covers. Device is unaffected: it doesn't override _decrypt_pack, so its code path through datagram_received is unchanged. Verified against 5 real Gree/Saunier Duval units on a live network: 4 of 5 were never discoverable before this fix (their scan replies are GCM-only), all 4 are found and bind successfully after it. Related reports of the same symptom: home-assistant/core#82452, home-assistant/core#148458.
jitsumi
force-pushed
the
fix/discovery-cipherv2-fallback
branch
from
July 21, 2026 16:04
afc7396 to
857e297
Compare
jitsumi
marked this pull request as ready for review
July 21, 2026 16:08
Author
|
@cmroche Hello |
Author
|
@cmroche |
|
My AC controls stopped working about six months ago after upgrading green firmware. If this piece of code would fix that, I would also be grateful if it would be published. |
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.
Behavior change
Discovery.datagram_receivedonly ever decrypts scan replies with thegeneric
CipherV1(AES-ECB) cipher, unlikeDevice.bind()which alreadytries
CipherV1then falls back toCipherV2(AES-GCM). Devices whosefirmware uses the newer GCM-only protocol for their scan reply (observed
on retrofitted and newer Gree Wi-Fi modules) fail to decrypt with
CipherV1, raising an unhandledValueErrorinside the asyncio UDPcallback (
Data must be aligned to block boundary in ECB mode), and aresilently dropped from discovery — they never appear in
discovery.deviceseven though they're on the network and respond to the scan.
This PR extracts the one-line decrypt call in
DeviceProtocolBase2.datagram_received(network.py) into a small_decrypt_pack()hook, then hasDiscoveryoverride just that hook totry
CipherV1thenCipherV2, mirroring the existing fallback already inDevice.bind().Discovery._decrypt_packreturnsNonewhen a reply isundecryptable under both ciphers, which
packet_received's existing"unexpected response" handling already covers — no new error-handling
code needed, and it's strictly better than today's behavior (a quiet,
already-existing log line instead of an unhandled exception).
Why scope the fix this way
Two narrower/differently-shaped fixes were considered and rejected:
CipherV1.decrypt()to silently retry asCipherV2onfailure. Rejected: it would change what "CipherV1" means for every
caller in the codebase, not just discovery.
DeviceProtocolBase2.datagram_received, rather than via anoverridable
_decrypt_pack()hook. Rejected: that method is used byboth
Discovery(scanning for unknown devices, where the correctcipher genuinely isn't known yet) and
Device(an already-bound devicewith an established, known-correct cipher). A bound
Deviceshouldn'tsilently retry with a different cipher on every decrypt failure on an
established connection — that risks masking real transmission errors
and adds an extra decrypt attempt to the hot path of every status poll,
for a case that should never legitimately happen post-bind.
_decrypt_pack()keepsDevice's code path provably unaffected (itdoesn't override the hook, so it still calls
self._cipher.decrypt()directly, identical to before) while letting
Discoveryoverride just thepart that varies. It also avoids duplicating
datagram_received'ssurrounding logic, so any future change to that shared logic (parsing,
logging, dispatch) automatically applies to
Discoverytoo instead ofneeding to be kept in sync by hand across two copies.
Context / device info
Verified against 5 real Gree/Saunier Duval AC units on a live home
network (1 built-in Wi-Fi unit, 4 retrofitted Wi-Fi modules). Before this
fix, only the 1 built-in-Wi-Fi unit was discoverable; the other 4 sent
valid scan replies but were never found. After the fix, all 5 are
discovered and bind successfully. This appears to be the same root cause
reported (without a fix) in:
Also deployed and verified end-to-end inside an actual Home Assistant
instance (as a drop-in-replacement custom_component built from this exact
patch): all 5 units discovered and controllable after the fix, confirmed
stable across a restart.
Test plan
test_discover_devices_cipherv2_fallback(reproduces the bug:fails with the exact
ValueErrorabove when the fix is reverted, passeswith it) and
test_discover_devices_undecryptable_reply_ignored(confirms a reply undecryptable under both ciphers is discarded
gracefully and doesn't block discovery of other devices).
flake8 --select=E9,F63,F7,F82: 0 errors on changed files. The--max-complexity=10 --max-line-length=127informational check reportsthe same pre-existing findings (same issue types, shifted line numbers)
as unpatched
master— nothing new introduced.above (discovery + bind + state read) with this exact patch, and inside
an actual Home Assistant instance.