From dd64fcd39b31ddd8acd92bee95d8db221fdaf800 Mon Sep 17 00:00:00 2001 From: Pedro Monteiro Date: Mon, 21 Sep 2026 11:19:34 +0100 Subject: [PATCH 1/7] Add integration code to python analysis so that autocomplete works for the new tools --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 160f55e..39b16a9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ "python.analysis.typeCheckingMode": "basic", "python.analysis.autoImportCompletions": true, + "python.analysis.extraPaths": ["${workspaceFolder}/custom_components"], "pylint.args": ["--rcfile=${workspaceFolder}/.pylintrc"], "mypy-type-checker.args": ["--config-file=${workspaceFolder}/mypy.ini"], From 06d2265365b98b207099af35fbdc13e5b3d5d198 Mon Sep 17 00:00:00 2001 From: Pedro Monteiro Date: Mon, 21 Sep 2026 11:20:29 +0100 Subject: [PATCH 2/7] Use VRF local discovery as in https://github.com/RobHofmann/HomeAssistant-GreeClimateComponent/pull/507 --- custom_components/gree_custom/aiogree/api.py | 43 ++++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/custom_components/gree_custom/aiogree/api.py b/custom_components/gree_custom/aiogree/api.py index 46fb6e4..69e479a 100755 --- a/custom_components/gree_custom/aiogree/api.py +++ b/custom_components/gree_custom/aiogree/api.py @@ -1245,12 +1245,12 @@ async def _get_sub_devices_list( json_payload = _create_payload( pack, "subList", - 1, + 0, mac_addr_controller, uid, ) - response = await gree_get_response_pack( + response = await gree_get_response( mac_addr_controller, json_payload, cipher, transport ) @@ -1262,7 +1262,25 @@ async def _get_sub_devices_list( else: # Response in format: # {"t":"subList","i":0,"c":6,"r":200,"list":[{"mac":"09c4a41d000000","mid":"6049"},...]} - sub_devs = response.get("list", []) + # The list may be at the top level (some firmwares) or inside a pack. + + sub_devs: list[dict[str, Any]] = [] + + if isinstance(response.get("list"), list): + sub_devs = response.get("list", []) + _LOGGER.debug( + "[%s] Found %d sub-units (top-level)", + mac_addr_controller, + len(sub_devs), + ) + else: + sub_devs = response.get("pack", {}).get("list", []) + _LOGGER.debug( + "[%s] Found %d sub-units (pack)", + mac_addr_controller, + len(sub_devs), + ) + if expected and (response.get("c") != expected or len(sub_devs) != expected): _LOGGER.warning( "[%s] Expected %d sub-devices and found %d", @@ -1281,11 +1299,11 @@ async def _get_sub_devices_list( ) else: new_dev = GreeDiscoveredDevice( - mac=sub_dev.get("mac"), + mac=sub_dev.get("mac", ""), mac_controller_local=mac_addr_controller, host=transport.ip_addr, port=transport.port, - mid=sub_dev.get("mid"), + mid=sub_dev.get("mid", ""), ) discovered_subdevices.append(new_dev) @@ -1321,21 +1339,30 @@ async def _process_local_scan_response( ver=device.ver or "", user_id=DEFAULT_DEVICE_USERID, ) - discovered_devices.append(discovered_device) if device.subCnt and device.subCnt > 0: # TODO: Ingest subdevices, need debugging. - # TODO: Is the device above also added, or only sub_devices? transport = GreeUdpTransport(ip_address, DEFAULT_DEVICE_PORT) + _LOGGER.debug("Obtaining device binding encryption info for the VRF controller") + binding_info = await gree_try_bind( + mac_addr_controller=mac_controller, + uid=DEFAULT_DEVICE_USERID, + version=None, + key=None, + transport=transport, + ) sub_devices = await _get_sub_devices_list( mac_controller, user_id, - get_cipher(EncryptionVersion.V1), + get_cipher(binding_info.encryption_version, binding_info.encryption_key), transport, discovered_device, device.subCnt, ) discovered_devices.extend(sub_devices) + else: + discovered_devices.append(discovered_device) + return discovered_devices From c96546d7b7a6fcf73eeef354a7aeda1fae0aec1f Mon Sep 17 00:00:00 2001 From: Pedro Monteiro Date: Mon, 21 Sep 2026 14:57:29 +0100 Subject: [PATCH 3/7] Remove unnecessary check for sub_devices lists length --- custom_components/gree_custom/aiogree/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/gree_custom/aiogree/api.py b/custom_components/gree_custom/aiogree/api.py index 69e479a..0381357 100755 --- a/custom_components/gree_custom/aiogree/api.py +++ b/custom_components/gree_custom/aiogree/api.py @@ -1260,9 +1260,9 @@ async def _get_sub_devices_list( ) from err else: - # Response in format: - # {"t":"subList","i":0,"c":6,"r":200,"list":[{"mac":"09c4a41d000000","mid":"6049"},...]} # The list may be at the top level (some firmwares) or inside a pack. + # Response pack in format: + # {"t":"subList","i":0,"c":6,"r":200,"list":[{"mac":"09c4a41d000000","mid":"6049"},...]} sub_devs: list[dict[str, Any]] = [] @@ -1281,7 +1281,7 @@ async def _get_sub_devices_list( len(sub_devs), ) - if expected and (response.get("c") != expected or len(sub_devs) != expected): + if expected and len(sub_devs) != expected: _LOGGER.warning( "[%s] Expected %d sub-devices and found %d", mac_addr_controller, From 0a634139373dd2f52ef62e8b6662d33ef064b314 Mon Sep 17 00:00:00 2001 From: Pedro Monteiro Date: Mon, 21 Sep 2026 15:19:07 +0100 Subject: [PATCH 4/7] Prevent one VRF gateway not connecting from breaking full discovery process Also properly use the discover timeout for getting the subdevices --- custom_components/gree_custom/aiogree/api.py | 43 ++++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/custom_components/gree_custom/aiogree/api.py b/custom_components/gree_custom/aiogree/api.py index 0381357..3649420 100755 --- a/custom_components/gree_custom/aiogree/api.py +++ b/custom_components/gree_custom/aiogree/api.py @@ -1251,9 +1251,15 @@ async def _get_sub_devices_list( ) response = await gree_get_response( - mac_addr_controller, json_payload, cipher, transport + mac_addr_controller, + json_payload, + cipher, + transport, ) + except GreeConnectionError: + raise + except Exception as err: raise GreeProtocolError( f"Error fetching sub-device list for '{mac_addr_controller}'" @@ -1311,15 +1317,14 @@ async def _get_sub_devices_list( async def _process_local_scan_response( - ip_address: str, pack: dict, user_id: int + ip_address: str, pack: dict, timeout: int, user_id: int ) -> list[GreeDiscoveredDevice]: - discovered_devices: list[GreeDiscoveredDevice] = [] device = DeviceScanInfoResponse.model_validate(pack) if not device.mac: _LOGGER.debug("No MAC address in response from %s", ip_address) - return discovered_devices + return [] mac, mac_controller = gree_extract_macs(device.mac) @@ -1340,9 +1345,12 @@ async def _process_local_scan_response( user_id=DEFAULT_DEVICE_USERID, ) - if device.subCnt and device.subCnt > 0: - # TODO: Ingest subdevices, need debugging. - transport = GreeUdpTransport(ip_address, DEFAULT_DEVICE_PORT) + if not device.subCnt or device.subCnt == 0: + return [discovered_device] + + # TODO: Ingest subdevices, need debugging. + transport = GreeUdpTransport(ip_address, DEFAULT_DEVICE_PORT, timeout=timeout) + try: _LOGGER.debug("Obtaining device binding encryption info for the VRF controller") binding_info = await gree_try_bind( mac_addr_controller=mac_controller, @@ -1359,11 +1367,20 @@ async def _process_local_scan_response( discovered_device, device.subCnt, ) - discovered_devices.extend(sub_devices) - else: - discovered_devices.append(discovered_device) - return discovered_devices + except GreeConnectionError: + # If we cannot connect, simply move on from this gateway as we cannot properly query subdevices + # Returning an empty list prevents discovery from complety fail because of one device + return [] + + except Exception as err: + # Other errors should break discovery and be investigated + raise GreeProtocolError( + f"Failed to discover sub-devices of {mac_controller}" + ) from err + + else: + return sub_devices async def gree_discover_device_local( @@ -1397,7 +1414,7 @@ async def gree_discover_device_local( return discovered_devices _LOGGER.debug("Got device info: %s", pack) - return await _process_local_scan_response(ip_address, pack, user_id) + return await _process_local_scan_response(ip_address, pack, timeout, user_id) async def gree_discover_devices_local( @@ -1430,7 +1447,7 @@ async def gree_discover_devices_local( pack = response.get("pack") if pack is not None and pack.get("t") == "dev": discovered_devices.extend( - await _process_local_scan_response(address, pack, user_id) + await _process_local_scan_response(address, pack, timeout, user_id) ) _LOGGER.info("Found total of %d local devices", len(discovered_devices)) From a92ee7cb51362fc842b7c25481394e70c94e29b6 Mon Sep 17 00:00:00 2001 From: Pedro Monteiro Date: Mon, 21 Sep 2026 15:30:35 +0100 Subject: [PATCH 5/7] Properly assign VRF subunits a name --- custom_components/gree_custom/aiogree/api.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/custom_components/gree_custom/aiogree/api.py b/custom_components/gree_custom/aiogree/api.py index 3649420..a85a102 100755 --- a/custom_components/gree_custom/aiogree/api.py +++ b/custom_components/gree_custom/aiogree/api.py @@ -1298,13 +1298,22 @@ async def _get_sub_devices_list( for sub_dev in sub_devs: new_dev: GreeDiscoveredDevice if parent_device: + # TODO: get real-data from VRF discovery to check the result list fields new_dev = replace( parent_device, - mac=sub_dev.get("mac"), - mid=sub_dev.get("mid"), + name=sub_dev.get( + "name", + f"{sub_dev.get('mac', '')[-5:]} VRF at {parent_device.name}", + ), + mac=sub_dev.get("mac", ""), + mid=sub_dev.get("mid", ""), ) else: new_dev = GreeDiscoveredDevice( + name=sub_dev.get( + "name", + f"{sub_dev.get('mac', '')[-5:]} VRF at {mac_addr_controller[-5:]}", + ), mac=sub_dev.get("mac", ""), mac_controller_local=mac_addr_controller, host=transport.ip_addr, @@ -1371,6 +1380,10 @@ async def _process_local_scan_response( except GreeConnectionError: # If we cannot connect, simply move on from this gateway as we cannot properly query subdevices # Returning an empty list prevents discovery from complety fail because of one device + _LOGGER.warning( + "Could not connect to VRF gateway %s. Its subdevices will be ignored", + mac_controller, + ) return [] except Exception as err: From 44246a58101c681c31fcaa85bc5800ba2ddb62ba Mon Sep 17 00:00:00 2001 From: Pedro Monteiro Date: Mon, 21 Sep 2026 15:37:26 +0100 Subject: [PATCH 6/7] Use the retireved key for sub-units --- custom_components/gree_custom/aiogree/api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/custom_components/gree_custom/aiogree/api.py b/custom_components/gree_custom/aiogree/api.py index a85a102..42637d5 100755 --- a/custom_components/gree_custom/aiogree/api.py +++ b/custom_components/gree_custom/aiogree/api.py @@ -1307,6 +1307,7 @@ async def _get_sub_devices_list( ), mac=sub_dev.get("mac", ""), mid=sub_dev.get("mid", ""), + key=cipher.key, ) else: new_dev = GreeDiscoveredDevice( @@ -1319,6 +1320,7 @@ async def _get_sub_devices_list( host=transport.ip_addr, port=transport.port, mid=sub_dev.get("mid", ""), + key=cipher.key, ) discovered_subdevices.append(new_dev) From da68eaeed4717c14a6d17c590c65d2b3143bee06 Mon Sep 17 00:00:00 2001 From: Pedro Monteiro Date: Mon, 21 Sep 2026 18:33:36 +0100 Subject: [PATCH 7/7] Reduce time waster discovering-subdevices when errors occur --- custom_components/gree_custom/aiogree/api.py | 32 ++++++++++++++------ custom_components/gree_custom/config_flow.py | 7 ++++- custom_components/gree_custom/const.py | 3 +- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/custom_components/gree_custom/aiogree/api.py b/custom_components/gree_custom/aiogree/api.py index 42637d5..bcefe1a 100755 --- a/custom_components/gree_custom/aiogree/api.py +++ b/custom_components/gree_custom/aiogree/api.py @@ -1292,7 +1292,7 @@ async def _get_sub_devices_list( "[%s] Expected %d sub-devices and found %d", mac_addr_controller, expected, - response.get("c"), + len(sub_devs), ) for sub_dev in sub_devs: @@ -1328,7 +1328,7 @@ async def _get_sub_devices_list( async def _process_local_scan_response( - ip_address: str, pack: dict, timeout: int, user_id: int + ip_address: str, pack: dict, timeout: int, max_retries: int, user_id: int ) -> list[GreeDiscoveredDevice]: device = DeviceScanInfoResponse.model_validate(pack) @@ -1359,8 +1359,11 @@ async def _process_local_scan_response( if not device.subCnt or device.subCnt == 0: return [discovered_device] - # TODO: Ingest subdevices, need debugging. - transport = GreeUdpTransport(ip_address, DEFAULT_DEVICE_PORT, timeout=timeout) + # If we are dealing wiht a VRF gateway, procceed by scaning its subdevices + # The gateway itselft is not a valid dicovered device + + # TODO: Ingest subdevices, need real-world tests. + transport = GreeUdpTransport(ip_address, DEFAULT_DEVICE_PORT, max_retries, timeout) try: _LOGGER.debug("Obtaining device binding encryption info for the VRF controller") binding_info = await gree_try_bind( @@ -1379,8 +1382,8 @@ async def _process_local_scan_response( device.subCnt, ) - except GreeConnectionError: - # If we cannot connect, simply move on from this gateway as we cannot properly query subdevices + except GreeBindingError, GreeConnectionError: + # If we cannot connect or bind, simply move on from this gateway as we cannot properly query subdevices # Returning an empty list prevents discovery from complety fail because of one device _LOGGER.warning( "Could not connect to VRF gateway %s. Its subdevices will be ignored", @@ -1399,13 +1402,14 @@ async def _process_local_scan_response( async def gree_discover_device_local( - ip_address: str, timeout: int, user_id: int + ip_address: str, timeout: int, max_retries: int, user_id: int ) -> list[GreeDiscoveredDevice]: """Target scan to a single Gree device. Args: ip_address: IP address of the target device timeout: Timeout (s) to wait for device responses + max_retries: Connection attempts to the device user_id: User ID for the request Returns: @@ -1429,17 +1433,20 @@ async def gree_discover_device_local( return discovered_devices _LOGGER.debug("Got device info: %s", pack) - return await _process_local_scan_response(ip_address, pack, timeout, user_id) + return await _process_local_scan_response( + ip_address, pack, timeout, max_retries, user_id + ) async def gree_discover_devices_local( - broadcast_addresses: list[str], timeout: int, user_id: int + broadcast_addresses: list[str], timeout: int, max_retries: int, user_id: int ) -> list[GreeDiscoveredDevice]: """Discover Gree devices on the network. Args: broadcast_addresses: List of broadcast addresses to search timeout: Timeout (s) to wait for device responses + max_retries: Connection attempts to the device user_id: User ID for the request Returns: @@ -1462,7 +1469,9 @@ async def gree_discover_devices_local( pack = response.get("pack") if pack is not None and pack.get("t") == "dev": discovered_devices.extend( - await _process_local_scan_response(address, pack, timeout, user_id) + await _process_local_scan_response( + address, pack, timeout, max_retries, user_id + ) ) _LOGGER.info("Found total of %d local devices", len(discovered_devices)) @@ -1511,6 +1520,7 @@ async def gree_discover_devices_cloud( async def gree_discover_devices( cloud_api: GreeCloudApi | None, broadcast_addresses: list[str] | None, + max_retries: int = 2, timeout: int = 3, ) -> list[GreeDiscoveredDevice]: """Discover Gree Devices. @@ -1519,6 +1529,7 @@ async def gree_discover_devices( cloud_api: The cloud API endpoint to get the devices from (Optional) broadcast_addresses: List of broadcast addresses to search (Optional) timeout: Timeout (s) to wait for device responses + max_retries: Connection attempts to the device Returns: De-duplicated list of discovered devices. @@ -1536,6 +1547,7 @@ async def gree_discover_devices( local_devices = await gree_discover_devices_local( broadcast_addresses, timeout, + max_retries, cloud_api.user_id if cloud_api and cloud_api.user_id else 0, ) diff --git a/custom_components/gree_custom/config_flow.py b/custom_components/gree_custom/config_flow.py index 4b6cc18..ea16166 100755 --- a/custom_components/gree_custom/config_flow.py +++ b/custom_components/gree_custom/config_flow.py @@ -114,6 +114,7 @@ DEFAULT_DEVICE_PORT, DEFAULT_DEVICE_UID, DEFAULT_DISABLE_AVAILABLE_CHECK, + DEFAULT_DISCOVERY_RETRIES, DEFAULT_DISCOVERY_TIMEOUT, DEFAULT_ENCRYPTION_KEY, DEFAULT_ENCRYPTION_VERSION, @@ -571,7 +572,10 @@ async def async_step_dhcp( # Check what's under that device: Main device and sub-devices # If it does not respond locally, there's no use of this information discover = await gree_discover_device_local( - discovery_info.ip, DEFAULT_DISCOVERY_TIMEOUT, DEFAULT_DEVICE_UID + discovery_info.ip, + DEFAULT_DISCOVERY_TIMEOUT, + DEFAULT_DISCOVERY_RETRIES, + DEFAULT_DEVICE_UID, ) entries_to_reload: list[GreeConfigEntry] = [] @@ -871,6 +875,7 @@ async def async_step_local_add( discovered = await gree_discover_devices_local( broadcast_addresses=await get_discovery_addresses(self.hass), timeout=DEFAULT_DISCOVERY_TIMEOUT, + max_retries=DEFAULT_DISCOVERY_RETRIES, user_id=0, ) diff --git a/custom_components/gree_custom/const.py b/custom_components/gree_custom/const.py index 3ea6d03..0f8b33d 100755 --- a/custom_components/gree_custom/const.py +++ b/custom_components/gree_custom/const.py @@ -63,7 +63,8 @@ DEFAULT_DEVICE_PORT = 7000 DEFAULT_CONNECTION_MAX_ATTEMPTS = 3 DEFAULT_CONNECTION_TIMEOUT = 10 -DEFAULT_DISCOVERY_TIMEOUT = 5 +DEFAULT_DISCOVERY_TIMEOUT = 2 +DEFAULT_DISCOVERY_RETRIES = 2 MAX_UNICAST_SCAN_HOSTS = 65536