diff --git a/custom_components/gree_custom/aiogree/api.py b/custom_components/gree_custom/aiogree/api.py index 498bb55..3b7ff5e 100755 --- a/custom_components/gree_custom/aiogree/api.py +++ b/custom_components/gree_custom/aiogree/api.py @@ -499,10 +499,12 @@ class DeviceScanInfoResponse(BaseModel): # Scan Responses format: # {"t":"dev","bc":"","catalog":"gree","series":"gree","model":"gree","lock":0,"vender":"1","mid":"60","name":"GR-Gcloud_60_0a_5ba3_EC","ver":"V3.2.M","mac":"9424b8fd5ba3","subCnt":6} # {'t': 'dev', 'cid': 'c03937b12280', 'bc': '00000000000000000000000000000000', 'brand': 'gree', 'catalog': 'gree', 'mac': 'c03937b12280', 'mid': '10001', 'model': 'gree', 'name': '', 'lock': 0, 'series': 'gree', 'vender': '1', 'ver': 'V3.4.M', 'ModelType': '32776', 'hid': '362001065279+U-WB05RT13V1.45.bin'} + # A zone controller sends no cid: + # {"t":"dev","bc":"gree","catalog":"gree","mid":"50","lock":0,"model":"gree","name":"GR-ZCntrlr_5000_02_ffee_EC","series":"gree","vender":"2","ver":"V1.2.1","brand":"gree","mac":"9424b8c0ffee","subCnt":5} model_config = ConfigDict(extra="ignore") t: str - cid: str + cid: str | None = None mac: str bc: str | None = None brand: str | None = None diff --git a/docs/protocol.md b/docs/protocol.md index d793c6a..9b3469e 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -37,6 +37,7 @@ Facts about how Gree devices behave on the wire. Most of these are not visible f ## Discovery and network - Local discovery is a plain text `{"t": "scan"}` UDP broadcast. The device answers with a `dev` pack encrypted with the generic key. +- Most devices put a `cid` in that `dev` pack. Some do not (seen: a Gree LE60-13/GH zone controller), so `DeviceScanInfoResponse` must not require it. Nothing reads it. - `extra_scan_networks` and `extra_scan_hosts` exist for devices on another subnet, where a broadcast does not reach. Those are probed with unicast. - A device that does not answer the scan cannot be bound. Setup retries the scan with the normal transport retries. diff --git a/tests/test_discovery_local.py b/tests/test_discovery_local.py index 97d8222..bd69476 100644 --- a/tests/test_discovery_local.py +++ b/tests/test_discovery_local.py @@ -14,6 +14,7 @@ # pytest pattern, not shadowing. from collections.abc import AsyncIterator, Callable +from typing import Any, override from aiogree.transport_udp import GreeUdpTransport import pytest @@ -58,6 +59,31 @@ async def test_one_device_answers_a_targeted_scan( assert found[0].host == device.host +class FakeDeviceWithoutCid(FakeGreeDevice): + """A unit whose scan reply has no cid (seen: a Gree LE60-13/GH zone controller).""" + + @override + def build_scan_info(self) -> dict[str, Any]: + """Answer the scan without the cid field.""" + info = super().build_scan_info() + del info["cid"] + return info + + +async def test_a_scan_reply_without_cid_is_accepted( + loopback_ip: str, discover_one: DiscoverOne +) -> None: + """The cid is not needed for anything, so a reply without it must not fail.""" + fake = FakeDeviceWithoutCid(name="Zone controller") + await fake.start(loopback_ip, DISCOVERY_PORT) + try: + found = await discover_one(fake.host, timeout=1) + finally: + fake.close() + + assert [(d.mac, d.name) for d in found] == [(DEFAULT_MAC, "Zone controller")] + + async def test_a_scan_closes_the_transport_it_opened( device: FakeGreeDevice, discover_one: DiscoverOne, monkeypatch: pytest.MonkeyPatch ) -> None: