Skip to content

Fix VRF sub-device discovery: defer + concurrent subList, robust parsing - #507

Merged
RobHofmann merged 4 commits into
RobHofmann:masterfrom
meirlo:feat/vrf-discovery
Sep 23, 2026
Merged

RobHofmann merged 4 commits into
RobHofmann:masterfrom
meirlo:feat/vrf-discovery

Conversation

@meirlo

@meirlo meirlo commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Problem

On multi-split (VRF) setups — one WiFi gateway fronting several indoor units — discovery was unreliable:

  • Some units go missing. Sub-device (subList) fetching ran inline inside the UDP receive loop. That query has its own retries/timeouts, so it blocked the loop and consumed the discovery time budget while other devices' broadcast replies were still arriving. Result: intermittently finding e.g. 3 of 4 units.
  • Single-unit gateways were skipped. A device was only treated as a gateway when subCnt > 1, so a gateway reporting exactly one indoor unit was never expanded.
  • subList never returned usable data. The old code sent an unencrypted/generic-key query and pushed the reply through FetchResult, which failed on real gateways.

What the gateway actually expects

The gateway only answers a subList query when it is encrypted, and it encrypts the response with a different key depending on the request envelope:

  • i:0 / t:"pack" → response encrypted with the gateway's bound device key (e.g. GR-Gcloud firmware V3.2.M).
  • i:1 / t:"subList" (the classic Gree-app form) → response encrypted with the generic key, like scan/bind. Some modules only answer this form.

Different firmwares answer different forms, and some gateways even return a slightly different subset of units in each form.

Changes

  • Collect gateways during the receive loop and fetch their sub-device lists after the loop, concurrently (asyncio.gather). Keeps discovery fast and stops it dropping other devices' replies.
  • Treat any subCnt > 0 as a gateway.
  • Rewrite get_subunits_list: bind to the gateway, query both encrypted forms, and union the results by MAC. This handles both module families and never returns fewer units than either form alone. v2 (GCM) modules use the device-key form.

Scope

gree_protocol.py only. Standalone (non-VRF) devices report no subCnt and never enter the gateway path — verified on a mixed setup (VRF gateway + standalone unit).

Related

Part of a small VRF series. Complements #454 (per-unit device_info/unique_id), which makes each discovered sub-unit its own HA device. This PR is what makes all the sub-units reliably get discovered.

Testing

Validated on a GR-Gcloud V3.2.M gateway (v1/ECB) fronting 4 indoor units (subMAC@gatewayMAC) plus a standalone non-VRF unit:

get_subunits_list: <gw> device-key=4, generic-key=3, merged=4
Discovery completed, found 5 devices

The device-key form returned all 4 units; the generic-key form returned a subset of 3; the union is the full 4, with the standalone unit added normally. A second GR-Gcloud gateway (another tester) discovers its units the same way.

Note: at least one tester's module reports subCnt higher than the number of entries the gateway returns across both forms (e.g. c:6 with 5 unique units). That appears to be a gateway-firmware limitation of the local protocol — the missing unit is simply not returned — and is out of scope here.

@meirlo
meirlo marked this pull request as ready for review September 18, 2026 10:53
p-monteiro added a commit to p-monteiro/HomeAssistant-GreeClimateComponent-Rewrite that referenced this pull request Sep 18, 2026
@RobHofmann RobHofmann added help wanted Extra attention is needed to test This issue needs testing labels Sep 20, 2026
@RobHofmann

Copy link
Copy Markdown
Owner

I need some testers with VRF devices for this one!

p-monteiro added a commit to p-monteiro/HomeAssistant-GreeClimateComponent-Rewrite that referenced this pull request Sep 21, 2026
RobHofmann pushed a commit that referenced this pull request Sep 21, 2026
* Add integration code to python analysis so that autocomplete works for the new tools

* Use VRF local discovery as in #507

* Remove unnecessary check for sub_devices lists length

* Prevent one VRF gateway not connecting from breaking full discovery process

Also properly use the discover timeout for getting the subdevices

* Properly assign VRF subunits a name

* Use the retireved key for sub-units

* Reduce time waster discovering-subdevices when errors occur

---------

Co-authored-by: Pedro Monteiro <p-monteiro@users.noreply.github.com>
@meirlo

meirlo commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Hi @vellad1, @Ilya-Draigor and @ItayGo — you all run VRF setups. @RobHofmann needs a VRF owner to validate this PR. Could any of you give it a quick test and report back? Thanks!

Two problems made multi-split (VRF) gateways discover unreliably:

1. Sub-device (subList) fetching ran inline inside the UDP receive loop.
   The subList query has its own retries/timeouts, so it blocked the loop
   and ate the discovery time budget, causing other devices' broadcast
   replies to be dropped (e.g. finding 3 of 4 units). Gateways are now
   collected during the loop and queried afterwards, concurrently via
   asyncio.gather, keeping discovery fast and complete.

2. A gateway was only treated as such when subCnt > 1, so single-unit
   gateways were missed. Any subCnt > 0 is now handled as a gateway.

get_subunits_list is rewritten to send subList unencrypted and handle both
response shapes seen in the wild: the sub-unit list at the top level, or
wrapped in a pack encrypted with the gateway's bound device key (bind
first, then decrypt). A small self-contained _fetch_subunits_raw helper does
the raw UDP exchange since the response has no pack to feed through
FetchResult.
@meirlo
meirlo force-pushed the feat/vrf-discovery branch 3 times, most recently from 973cfe1 to cae8692 Compare September 23, 2026 12:35
The gateway only answers a subList query when it is encrypted, and it encrypts
the *response* with a different key depending on the request envelope:

 - i:0 / t:pack   -> response encrypted with the gateway's bound device key.
 - i:1 / t:subList (classic app form) -> response encrypted with the generic
                     key, like scan/bind.

Different WiFi-module firmwares answer different forms, and some gateways
return a slightly different subset of units in each form. get_subunits_list now
binds once, queries both forms, and unions the results by MAC. This handles
both module families and never returns fewer units than either form alone.

Verified on a GR-Gcloud V3.2.M gateway: device-key form returns 4 units,
generic-key form returns 3, merged result is the full 4, alongside a standalone
non-VRF unit discovered normally.
When several gateways are probed concurrently (asyncio.gather), the unconnected
UDP sockets could receive datagrams intended for another in-flight request -
observed in the wild as one gateway's bind call returning a different gateway's
key. Connecting the socket to the target peer makes the kernel drop datagrams
from any other address, correlating each reply to its request. Applied to both
FetchResult and the subList helper. FetchResult is only ever called with
unicast device IPs, so this does not affect broadcast discovery.
Comment thread custom_components/gree/gree_protocol.py Outdated
@meirlo

meirlo commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

@RobHofmann #507 is now validated end-to-end on real hardware across three testers and two different WiFi-module generations:

Tester Module Result
me GR-Gcloud V3.2.M 4 indoor units + a standalone AC, no regression
@Ilya-Draigor GR-Gcloud V3.2.M all 6 units (see below)
@vellad1 W06 V1.1.0.0 both gateways list their 3 units each

Two findings from testing shaped the final approach:

  1. Modules answer different sub-enumeration commands. GR-Gcloud answers subList (as an encrypted pack, with the response keyed to the request envelope); the older W06 only answers a subDev command. Fix VRF sub-device discovery: defer + concurrent subList, robust parsing #507 issues both and unions the results.
  2. Some gateways return a partial list per form. Ilya's gateway returned a different set of 5 in each form (device-key=5, generic-key=5), so merging by MAC was needed to recover the full 6.

Also included: deferred + concurrent sub-device fetch (so discovery no longer drops other devices' replies), and connecting the UDP sockets to their target so concurrent gateway probes don't receive each other's responses.

#507 is gree_protocol.py-only and doesn't touch the standalone path. It pairs with #454 (per-unit device_info, which the same testers confirmed) and #508 (stale-read/switch handling). Happy to split or adjust anything — thanks for maintaining this!

@meirlo

meirlo commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

@RobHofmann summary across the VRF series — all three PRs are now validated on real hardware by VRF owners:

The three are independent and additive, but together they make VRF/multi-split work end-to-end. Testing surfaced real module variance that #507 now handles (different sub-enumeration commands — subList vs subDev — and gateways returning a partial list per form, so results are unioned by MAC).

Given the confirmations from multiple VRF owners, would you be open to reviewing/merging the set? Happy to rebase, split, or adjust anything you'd like. Thanks!

Older gateway WiFi modules (e.g. W06, hid 362001067012+U-W06AV30.bin,
ver V1.1.0.0) do not answer the subList command at all - they enumerate
sub-units via a 'subDev' command wrapped in a device-key pack, returning the
same subList-shaped response. Captured from a real W06 gateway.

get_subunits_list now also issues the subDev form (with uid:0) and unions its
result with the subList forms. Confirmed on a GR-Gcloud V3.2.M gateway the
subDev form returns the full unit list too (device-key=4, generic-key=3,
subDev=4, merged=4), so it is a good universal path and adds no regression.
@RobHofmann
RobHofmann merged commit 0260bad into RobHofmann:master Sep 23, 2026
@RobHofmann

Copy link
Copy Markdown
Owner

https://github.com/RobHofmann/HomeAssistant-GreeClimateComponent/releases/tag/4.0.7

Thanks for contributing guys! amazing work!

I will do another PR towards the 5.0-dev branch for the future version with the same knowledge! Will ask you guys to help me test that one too later.

@RobHofmann

Copy link
Copy Markdown
Owner

@meirlo a follow-up question on the generic-key form, raised by @p-monteiro while porting this to 5.0 (#522). The request pack is encrypted with the device key but the reply comes back in the generic key. On my standalone unit, i:1 requests are only answered when the pack uses the generic key, so we suspect your gateway ignores the pack for t:"subList".

Could you run this read-only script against your GR-Gcloud? It sends one bind (the gateway returns its existing key) and three subList queries, and no commands.

python probe_vrf_sublist_keys.py <gateway-ip> <gateway-mac> (needs pip install pycryptodome)

probe_vrf_sublist_keys.py
"""Which key does a Gree VRF gateway expect in a subList request with i=1?

Usage: python probe_vrf_sublist_keys.py <gateway-ip> <gateway-mac>
Needs: pip install pycryptodome

Read-only. It sends one bind (the gateway returns its existing key) and a few
subList queries. It sends no command, so nothing on the units changes.
"""
import base64
import json
import socket
import sys

from Crypto.Cipher import AES

HOST, MAC, PORT = sys.argv[1], sys.argv[2].replace(":", "").lower(), 7000
GENERIC = b"a3K8Bx%2r8Y7#xDh"
RANDOM = b"0123456789abcdef"


def pad(s):
    n = 16 - len(s) % 16
    return s + chr(n) * n


def enc(key, obj):
    raw = AES.new(key, AES.MODE_ECB).encrypt(pad(json.dumps(obj)).encode())
    return base64.b64encode(raw).decode()


def dec(key, b64):
    try:
        t = AES.new(key, AES.MODE_ECB).decrypt(base64.b64decode(b64)).decode("utf-8", "ignore")
        return json.loads(t[: t.rindex("}") + 1])
    except Exception:
        return None


def send(payload, tries=3):
    for _ in range(tries):
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.settimeout(2)
        try:
            s.sendto(json.dumps(payload).encode(), (HOST, PORT))
            return json.loads(s.recvfrom(64000)[0])
        except socket.timeout:
            pass
        finally:
            s.close()
    return None


def env(t, i, pack):
    return {"cid": "app", "i": i, "pack": pack, "t": t, "tcid": MAC, "uid": 0}


def report(label, resp, key):
    if resp is None:
        print(f"{label}: no answer")
        return
    pack = resp.get("pack", "")
    for name, k in (("generic", GENERIC), ("device", key)):
        body = dec(k, pack) if pack else None
        if body is not None:
            n = len(body.get("list", [])) if isinstance(body.get("list"), list) else "no list"
            print(f"{label}: answer readable with the {name} key, units={n}")
            return
    print(f"{label}: answer, but readable with neither key")


bind = send(env("pack", 1, enc(GENERIC, {"mac": MAC, "t": "bind", "uid": 0})))
if bind is None or dec(GENERIC, bind["pack"]) is None:
    sys.exit("The bind got no usable answer. Check the IP and MAC.")
KEY = dec(GENERIC, bind["pack"])["key"].encode()
print("bind: ok")

inner = {"mac": MAC, "i": 1}
report("A  subList i=1, pack with device key ", send(env("subList", 1, enc(KEY, inner))), KEY)
report("B  subList i=1, pack with generic key", send(env("subList", 1, enc(GENERIC, inner))), KEY)
report("C  subList i=1, pack with random key ", send(env("subList", 1, enc(RANDOM, inner))), KEY)

If C (random key) also answers, the pack is ignored and we can use the generic key both ways. Thanks!

@meirlo

meirlo commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Ran the probe against my GR-Gcloud (580d0d30225f). It confirms @p-monteiro's theory — the gateway ignores the request pack entirely for the i:1 / t:"subList" form:

bind: ok
A  subList i=1, pack with device key : answer readable with the generic key, units=3
B  subList i=1, pack with generic key: answer readable with the generic key, units=3
C  subList i=1, pack with random key : answer readable with the generic key, units=3

All three request keys — device, generic, and even a random key (case C) — get the same reply, decryptable with the generic key, units=3. So the pack contents/encryption don't matter for this form; the gateway just answers the t:"subList" envelope in the generic key. For the 5.0 port you can safely encrypt this request with the generic key both ways — no device key needed on that path.

One thing worth carrying over: on my gateway this generic-key form returns a partial list (3 of 4), while the device-key i:0/t:"pack" form and the subDev form each return the full 4. On @Ilya-Draigor's gateway each form returned a different subset of 5, and only the union recovered all 6. So even though the request pack is ignored here, keeping this form in the union still adds coverage on some gateways.

Thanks for the clean read-only probe — made this easy to answer.

@RobHofmann

Copy link
Copy Markdown
Owner

Thanks for the input man! 4.0.10 has went out!

Also, working on the VRF PR towards the 5.0-dev right now. will be available shortly.

RobHofmann added a commit that referenced this pull request Sep 24, 2026
…until confirmed (#522)

* VRF gateways: ask all sub-device list forms, hold sent values until confirmed

Port of the VRF fixes that landed on master in #507 and #508. #454 needs no
port: 5.0 already gives each indoor unit its own device.

Sub-device list: a gateway answers one or more of three request forms,
depending on its WiFi module firmware (device key, generic key, subDev).
The old single hybrid form was never confirmed on hardware. V1 gateways
now get all three forms and the lists are joined by MAC; V2 gets the
device key form. The generic key form is encrypted with the device key
but answered with the generic key, so request_json() takes an optional
response_cipher. Gateways from a scan are handled side by side. On the
GR-Gcloud V3.2.M gateway from #507 the forms gave 4, 3 and 4 units,
joined 4.

Stale state: a gateway answers from a cache for a few seconds after a
command, so the read right after it showed the old value again. The
device state now holds sent values until the device reports them, for at
most 8 s. The coordinator polls again every 2 s while values are held. A
standalone unit confirms on the first read and gets no extra poll.

Suite: 247 passed (227 before).

* Only hold sent values for VRF sub-units

The stale cache was only seen on VRF gateways. For a standalone unit a hold
has a cost: when the unit corrects a value it cannot take, for example an
unsupported swing mode, it reports its own value, and the hold kept the
refused value on screen for up to 8 s. Standalone units now behave as they
did before the hold.

To find out whether a standalone unit or the MQTT transport caches too,
every device now logs at debug level when the read right after a command
does not report the sent value, with the transport and whether it is a
sub-unit.

Suite: 249 passed.

* Send the generic-key sub-device list request with the generic key

The generic-key form is answered with the generic key, but its request used
the bound device key, so transport.request_json() needed a separate
response_cipher for this one case. A probe on a GR-Gcloud V3.2.M gateway
(PR 507) showed the gateway ignores the request pack of this form: device,
generic and random keys all got the same answer, 3 units, readable with the
generic key. The request now uses the generic key too, and response_cipher
is removed again. The form stays in the union, because it adds units on some
gateways.
@RobHofmann

Copy link
Copy Markdown
Owner

@meirlo @ItayGo @Ilya-Draigor @vellad1 the VRF work is now ported to the 5.0 line, and I'd like your help to test it on real hardware.

What is in 5.0-dev now:

Before you start: 5.0 is an alpha (5.0.0-alpha.107) with a new domain, gree_custom. At the first start it moves your 4.x devices over. Take a Home Assistant backup first, because restoring that backup is the way back. The steps are in Coming from a 4.x release.

Install: download the 5.0-dev branch, copy custom_components/gree_custom into your custom_components folder, and restart.

What would help most:

  1. Are all your indoor units found? With debug logging on for custom_components.gree_custom, the line Sub-device list per form shows what each form returned.
  2. Does each unit sit under one VRF gateway device, and do the firmware values on that gateway look right?
  3. Do commands stick, without the UI jumping back?
  4. A device diagnostics download of one indoor unit. We want to confirm that all units behind one gateway report the same firmware.
  5. If you also use the Gree cloud for a VRF: the debug line Raw cloud device list (keys are redacted). With that, cloud units can be grouped under their gateway later too.

Thanks again for all the help on the 4.x side!

@meirlo

meirlo commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Sure I'll give it a try.
But since I don't have a test env, is it possible to create a version that can be installed alongside version 4?

@RobHofmann

Copy link
Copy Markdown
Owner

Yes and no. It kind of depends on whether you are using YAML or the config flow for the integration.

Config flow: 5.0-dev will automatically migrate the 4.x config into the 5.0 integration and remove the 4.x config.

YAML: It will give you the new YAML through the portal, which you have to copy in. Then you simply can let them co-exist (or comment out the YAML from 4.x for the testing period).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

help wanted Extra attention is needed to test This issue needs testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants