From b77856cb086b026d78b8742e2164ad324a5ba6ce Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 9 Nov 2023 20:03:55 -0500 Subject: [PATCH 01/22] Initial support for the Conbee III --- zigpy_deconz/api.py | 16 ++++++++++++++++ zigpy_deconz/zigbee/application.py | 14 ++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/zigpy_deconz/api.py b/zigpy_deconz/api.py index a6fa81c..d1d54e8 100644 --- a/zigpy_deconz/api.py +++ b/zigpy_deconz/api.py @@ -73,6 +73,7 @@ class DeviceState(t.Struct): class FirmwarePlatform(t.enum8): Conbee = 0x05 Conbee_II = 0x07 + Conbee_III = 0x09 class FirmwareVersion(t.Struct, t.uint32_t): @@ -713,6 +714,21 @@ def _handle_device_state_changed( self._device_state = device_state self._data_poller_event.set() + def _handle_device_state( + self, + status: t.Status, + device_state: DeviceState, + reserved1: t.uint8_t, + reserved2: t.uint8_t, + ) -> None: + if ( + self.firmware_version.platform == FirmwarePlatform.Conbee_III + and self.firmware_version == 0x26450900 + ): + # Initial Conbee III firmware used the wrong command to notify of network + # state changes + self._handle_device_state_changed(status=status, device_state=device_state) + async def version(self): self._protocol_version = await self.read_parameter( NetworkParameter.protocol_version diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index ff77633..fc17d2e 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -171,7 +171,10 @@ async def change_loop(): " 2.4GHz routers, motherboards, etc." ) - if self._api.protocol_version < PROTO_VER_WATCHDOG: + if self._api.protocol_version < PROTO_VER_WATCHDOG or ( + self._api.firmware_version.platform == FirmwarePlatform.Conbee_III + and self._api.firmware_version == 0x26450900 + ): return if self._reset_watchdog_task is not None: @@ -286,6 +289,9 @@ async def write_network_info(self, *, network_info, node_info): await self._change_network_state(NetworkState.CONNECTED) async def load_network_info(self, *, load_devices=False): + if self._api.firmware_version.platform == FirmwarePlatform.Conbee_III: + await self._change_network_state(NetworkState.CONNECTED) + network_info = self.state.network_info node_info = self.state.node_info @@ -601,7 +607,11 @@ def __init__(self, version: FirmwareVersion, device_path: str, *args): super().__init__(*args) is_gpio_device = re.match(r"/dev/tty(S|AMA|ACM)\d+", device_path) self._model = "RaspBee" if is_gpio_device else "ConBee" - self._model += " II" if version.platform == FirmwarePlatform.Conbee_II else "" + self._model += { + FirmwarePlatform.Conbee: "", + FirmwarePlatform.Conbee_II: " II", + FirmwarePlatform.Conbee_III: " III", + }[version.platform] async def add_to_group(self, grp_id: int, name: str = None) -> None: group = self.application.groups.add_group(grp_id, name) From 7833c04a9c6cb870332aa5c7f7e855cdfed81237 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:58:37 -0500 Subject: [PATCH 02/22] Add a delay when changing network state --- zigpy_deconz/zigbee/application.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index fc17d2e..caaebc8 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -45,7 +45,8 @@ LOGGER = logging.getLogger(__name__) -CHANGE_NETWORK_WAIT = 1 +CHANGE_NETWORK_POLL_TIME = 1 +CHANGE_NETWORK_STATE_DELAY = 2 DELAY_NEIGHBOUR_SCAN_S = 1500 SEND_CONFIRM_TIMEOUT = 60 @@ -145,7 +146,10 @@ async def start_network(self): ) async def _change_network_state( - self, target_state: NetworkState, *, timeout: int = 10 * CHANGE_NETWORK_WAIT + self, + target_state: NetworkState, + *, + timeout: int = 10 * CHANGE_NETWORK_POLL_TIME, ): async def change_loop(): while True: @@ -153,7 +157,7 @@ async def change_loop(): if NetworkState(device_state.network_state) == target_state: break - await asyncio.sleep(CHANGE_NETWORK_WAIT) + await asyncio.sleep(CHANGE_NETWORK_POLL_TIME) await self._api.change_network_state(target_state) @@ -286,6 +290,7 @@ async def write_network_info(self, *, network_info, node_info): # Note: Changed network configuration parameters become only affective after # sending a Leave Network Request followed by a Create or Join Network Request await self._change_network_state(NetworkState.OFFLINE) + await asyncio.sleep(CHANGE_NETWORK_STATE_DELAY) await self._change_network_state(NetworkState.CONNECTED) async def load_network_info(self, *, load_devices=False): @@ -399,6 +404,7 @@ async def _move_network_to_channel( ) await self._change_network_state(NetworkState.OFFLINE) + await asyncio.sleep(CHANGE_NETWORK_STATE_DELAY) await self._change_network_state(NetworkState.CONNECTED) async def add_endpoint(self, descriptor: zdo_t.SimpleDescriptor) -> None: From fc8946d7ee4c62088eb6ba0a380ab6699073ef94 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 10 Nov 2023 11:58:49 -0500 Subject: [PATCH 03/22] Do not write security mode for Conbee III --- zigpy_deconz/zigbee/application.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index caaebc8..a0227a4 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -278,14 +278,15 @@ async def write_network_info(self, *, network_info, node_info): ), ) - if network_info.security_level == 0x00: - await self._api.write_parameter( - NetworkParameter.security_mode, SecurityMode.NO_SECURITY - ) - else: - await self._api.write_parameter( - NetworkParameter.security_mode, SecurityMode.ONLY_TCLK - ) + if self._api.firmware_version.platform != FirmwarePlatform.Conbee_III: + if network_info.security_level == 0x00: + await self._api.write_parameter( + NetworkParameter.security_mode, SecurityMode.NO_SECURITY + ) + else: + await self._api.write_parameter( + NetworkParameter.security_mode, SecurityMode.ONLY_TCLK + ) # Note: Changed network configuration parameters become only affective after # sending a Leave Network Request followed by a Create or Join Network Request From a9dd5b2f26a1c716fbb0f813d1bdad76d0d6f35a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 13 Nov 2023 10:31:50 -0500 Subject: [PATCH 04/22] Skip restoring neighbors for current CB3 firmwares --- zigpy_deconz/zigbee/application.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index a0227a4..5edb64c 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -138,7 +138,10 @@ async def start_network(self): ) self.devices[self.state.node_info.ieee] = coordinator - if self._api.protocol_version >= PROTO_VER_NEIGBOURS: + if self._api.protocol_version >= PROTO_VER_NEIGBOURS and ( + self._api.firmware_version.platform == FirmwarePlatform.Conbee_III + and self._api.firmware_version >= 0x264D0900 + ): await self.restore_neighbours() self._delayed_neighbor_scan_task = asyncio.create_task( From d53d895893b0d33a8ec457d84cf1459a90322242 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 13 Nov 2023 10:34:54 -0500 Subject: [PATCH 05/22] Account for EmberZNet ZDO energy scanning bug --- zigpy_deconz/zigbee/application.py | 35 +++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 5edb64c..8907302 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -54,6 +54,8 @@ PROTO_VER_WATCHDOG = 0x0108 PROTO_VER_NEIGBOURS = 0x0107 +CONBEE_III_ENERGY_SCAN_ATTEMPTS = 5 + class ControllerApplication(zigpy.application.ControllerApplication): SCHEMA = CONFIG_SCHEMA @@ -390,12 +392,35 @@ async def force_remove(self, dev): async def energy_scan( self, channels: t.Channels.ALL_CHANNELS, duration_exp: int, count: int ) -> dict[int, float]: - results = await super().energy_scan( - channels=channels, duration_exp=duration_exp, count=count - ) + if self._api.firmware_version.platform in ( + FirmwarePlatform.Conbee, + FirmwarePlatform.Conbee_II, + ): + results = await super().energy_scan( + channels=channels, duration_exp=duration_exp, count=count + ) + + # The Conbee I/II seems to max out at an LQI of 85, which is exactly 255/3 + return {c: v * 3 for c, v in results.items()} + + for i in range(CONBEE_III_ENERGY_SCAN_ATTEMPTS): + # The Conbee III energy scan inherits the EmberZNet ZDO bug + try: + rsp = await self._device.zdo.Mgmt_NWK_Update_req( + zigpy.zdo.types.NwkUpdate( + ScanChannels=channels, + ScanDuration=duration_exp, + ScanCount=count, + ) + ) + break + except (asyncio.TimeoutError, zigpy.exceptions.DeliveryError): + continue + else: + raise - # The Conbee seems to max out at an LQI of 85, which is exactly 255/3 - return {c: v * 3 for c, v in results.items()} + _, scanned_channels, _, _, energy_values = rsp + return dict(zip(scanned_channels, energy_values)) async def _move_network_to_channel( self, new_channel: int, new_nwk_update_id: int From 0b6b061b761ae7b0ef0ee8c2556ce095b2943938 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 13 Nov 2023 10:56:26 -0500 Subject: [PATCH 06/22] Fix logic for neighbor restoration for CB2 --- zigpy_deconz/zigbee/application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 8907302..49aeef3 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -140,9 +140,9 @@ async def start_network(self): ) self.devices[self.state.node_info.ieee] = coordinator - if self._api.protocol_version >= PROTO_VER_NEIGBOURS and ( + if self._api.protocol_version >= PROTO_VER_NEIGBOURS and not ( self._api.firmware_version.platform == FirmwarePlatform.Conbee_III - and self._api.firmware_version >= 0x264D0900 + and self._api.firmware_version < 0x264D0900 ): await self.restore_neighbours() From 6ae50fe0121643afa2c8af21c08e3e452b2b1f7d Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:53:37 -0500 Subject: [PATCH 07/22] Fix existing unit tests --- tests/test_application.py | 17 +++++++++-------- tests/test_network_state.py | 8 +++++--- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/test_application.py b/tests/test_application.py index 45beee8..4023aca 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -41,6 +41,7 @@ def api(): return_value=deconz_api.DeviceState(deconz_api.NetworkState.CONNECTED) ) api.write_parameter = AsyncMock() + api.firmware_version = deconz_api.FirmwareVersion(0x26580700) # So the protocol version is effectively infinite api._protocol_version.__ge__.return_value = True @@ -112,7 +113,7 @@ def addr_nwk_and_ieee(nwk, ieee): return addr -@patch("zigpy_deconz.zigbee.application.CHANGE_NETWORK_WAIT", 0.001) +@patch("zigpy_deconz.zigbee.application.CHANGE_NETWORK_POLL_TIME", 0.001) @pytest.mark.parametrize( "proto_ver, target_state, returned_state", [ @@ -235,7 +236,7 @@ async def test_deconz_dev_add_to_group(app, nwk, device_path): app._groups.add_group.return_value = group deconz = application.DeconzDevice( - deconz_api.FirmwareVersion(0), device_path, app, sentinel.ieee, nwk + deconz_api.FirmwareVersion(0x26580700), device_path, app, sentinel.ieee, nwk ) deconz.endpoints = { 0: sentinel.zdo, @@ -255,7 +256,7 @@ async def test_deconz_dev_remove_from_group(app, nwk, device_path): group = MagicMock() app.groups[sentinel.grp_id] = group deconz = application.DeconzDevice( - deconz_api.FirmwareVersion(0), device_path, app, sentinel.ieee, nwk + deconz_api.FirmwareVersion(0x26580700), device_path, app, sentinel.ieee, nwk ) deconz.endpoints = { 0: sentinel.zdo, @@ -269,7 +270,7 @@ async def test_deconz_dev_remove_from_group(app, nwk, device_path): def test_deconz_props(nwk, device_path): deconz = application.DeconzDevice( - deconz_api.FirmwareVersion(0), device_path, app, sentinel.ieee, nwk + deconz_api.FirmwareVersion(0x26580700), device_path, app, sentinel.ieee, nwk ) assert deconz.manufacturer is not None assert deconz.model is not None @@ -298,7 +299,7 @@ async def test_deconz_new(app, nwk, device_path, monkeypatch): monkeypatch.setattr(zigpy.device.Device, "_initialize", mock_init) deconz = await application.DeconzDevice.new( - app, sentinel.ieee, nwk, deconz_api.FirmwareVersion(0), device_path + app, sentinel.ieee, nwk, deconz_api.FirmwareVersion(0x26580700), device_path ) assert isinstance(deconz, application.DeconzDevice) assert mock_init.call_count == 1 @@ -312,7 +313,7 @@ async def test_deconz_new(app, nwk, device_path, monkeypatch): } app.devices[sentinel.ieee] = mock_dev deconz = await application.DeconzDevice.new( - app, sentinel.ieee, nwk, deconz_api.FirmwareVersion(0), device_path + app, sentinel.ieee, nwk, deconz_api.FirmwareVersion(0x26580700), device_path ) assert isinstance(deconz, application.DeconzDevice) assert mock_init.call_count == 0 @@ -426,7 +427,7 @@ async def test_delayed_scan(): app.topology.scan.assert_called_once_with(devices=[coord]) -@patch("zigpy_deconz.zigbee.application.CHANGE_NETWORK_WAIT", 0.001) +@patch("zigpy_deconz.zigbee.application.CHANGE_NETWORK_POLL_TIME", 0.001) @pytest.mark.parametrize("support_watchdog", [False, True]) async def test_change_network_state(app, support_watchdog): app._reset_watchdog_task = MagicMock() @@ -588,7 +589,7 @@ async def test_reset_network_info(app): app.form_network.assert_called_once() -async def test_energy_scan(app): +async def test_energy_scan_conbee_2(app): with mock.patch.object( zigpy.application.ControllerApplication, "energy_scan", diff --git a/tests/test_network_state.py b/tests/test_network_state.py index 936818d..9040421 100644 --- a/tests/test_network_state.py +++ b/tests/test_network_state.py @@ -60,11 +60,12 @@ def network_info(node_info): nwk_addresses={}, stack_specific={}, source=f"zigpy-deconz@{importlib.metadata.version('zigpy-deconz')}", - metadata={"deconz": {"version": "0x00000001"}}, + metadata={"deconz": {"version": "0x26580700"}}, ) -@patch.object(application, "CHANGE_NETWORK_WAIT", 0.001) +@patch.object(application, "CHANGE_NETWORK_POLL_TIME", 0.001) +@patch.object(application, "CHANGE_NETWORK_STATE_DELAY", 0.001) @pytest.mark.parametrize( "channel_mask, channel, security_level, fw_supports_fc, logical_type", [ @@ -182,7 +183,8 @@ async def write_parameter(param, *args): assert params["security_mode"] == (zigpy_deconz.api.SecurityMode.ONLY_TCLK,) -@patch.object(application, "CHANGE_NETWORK_WAIT", 0.001) +@patch.object(application, "CHANGE_NETWORK_POLL_TIME", 0.001) +@patch.object(application, "CHANGE_NETWORK_STATE_DELAY", 0.001) @pytest.mark.parametrize( "error, param_overrides, nwk_state_changes, node_state_changes", [ From 1496e611ed75f97dc1e511d3e7e96f0b8850ce73 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:30:55 -0500 Subject: [PATCH 08/22] Add a new unit test for CB3 energy scanning --- tests/test_application.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_application.py b/tests/test_application.py index 4023aca..07a11a9 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -602,6 +602,40 @@ async def test_energy_scan_conbee_2(app): assert results == {c: c * 3 for c in Channels.ALL_CHANNELS} +async def test_energy_scan_conbee_3(app): + app._api.firmware_version = deconz_api.FirmwareVersion(0x26580900) + + type(app)._device = AsyncMock() + + app._device.zdo.Mgmt_NWK_Update_req = AsyncMock( + side_effect=zigpy.exceptions.DeliveryError() + ) + + with pytest.raises(zigpy.exceptions.DeliveryError): + await app.energy_scan(channels=Channels.ALL_CHANNELS, duration_exp=0, count=1) + + app._device.zdo.Mgmt_NWK_Update_req = AsyncMock( + side_effect=[ + asyncio.TimeoutError(), + list( + { + "Status": zdo_t.Status.SUCCESS, + "ScannedChannels": Channels.ALL_CHANNELS, + "TotalTransmissions": 0, + "TransmissionFailures": 0, + "EnergyValues": [i for i in range(11, 26 + 1)], + }.values() + ), + ] + ) + + results = await app.energy_scan( + channels=Channels.ALL_CHANNELS, duration_exp=0, count=1 + ) + + assert results == {c: c for c in Channels.ALL_CHANNELS} + + async def test_channel_migration(app): app._api.write_parameter = AsyncMock() app._change_network_state = AsyncMock() From 9205678507715b5b1ca279d3989c8d58f5daa332 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:39:00 -0500 Subject: [PATCH 09/22] Add unit test for wrong device state callback handling --- tests/test_api.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index 93997bb..fe8e5a0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -949,3 +949,47 @@ async def test_add_neighbour(api, mock_command_rsp): mac_capability_flags=0x12, ) ] + + +async def test_cb3_device_state_callback_bug(api, mock_command_rsp): + mock_command_rsp( + command_id=deconz_api.CommandId.version, + params={"reserved": t.uint8_t(0)}, + rsp={ + "status": deconz_api.Status.SUCCESS, + "frame_length": t.uint16_t(9), + "version": deconz_api.FirmwareVersion(0x26450900), + }, + replace=True, + ) + + await api.connect() + + device_state = deconz_api.DeviceState( + network_state=deconz_api.NetworkState2.CONNECTED, + device_state=deconz_api.DeviceStateFlags.APSDE_DATA_CONFIRM, + ) + + assert api._device_state != device_state + + _, rx_schema = deconz_api.COMMAND_SCHEMAS[deconz_api.CommandId.device_state] + api.data_received( + deconz_api.Command( + command_id=deconz_api.CommandId.device_state, + seq=api._seq, + payload=t.serialize_dict( + { + "status": deconz_api.Status.SUCCESS, + "frame_length": t.uint16_t(8), + "device_state": device_state, + "reserved1": t.uint8_t(0), + "reserved2": t.uint8_t(0), + }, + rx_schema, + ), + ).serialize() + ) + + await asyncio.sleep(0.01) + + assert api._device_state == device_state From f1dbb238635782472f40f6ad22d684ed4e1585f9 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:43:03 -0500 Subject: [PATCH 10/22] Fix new unit tests --- tests/test_application.py | 2 +- zigpy_deconz/zigbee/application.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_application.py b/tests/test_application.py index 07a11a9..bc50b72 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -608,7 +608,7 @@ async def test_energy_scan_conbee_3(app): type(app)._device = AsyncMock() app._device.zdo.Mgmt_NWK_Update_req = AsyncMock( - side_effect=zigpy.exceptions.DeliveryError() + side_effect=zigpy.exceptions.DeliveryError("error") ) with pytest.raises(zigpy.exceptions.DeliveryError): diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 49aeef3..ca8ddb3 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -415,9 +415,10 @@ async def energy_scan( ) break except (asyncio.TimeoutError, zigpy.exceptions.DeliveryError): + if i == CONBEE_III_ENERGY_SCAN_ATTEMPTS - 1: + raise + continue - else: - raise _, scanned_channels, _, _, energy_values = rsp return dict(zip(scanned_channels, energy_values)) From 3a496d0c66bc2dd3660fc01c38a12e3545527e25 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:46:58 -0500 Subject: [PATCH 11/22] Remove unnecessary `_change_network_state` from `load_network_info` --- zigpy_deconz/zigbee/application.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index ca8ddb3..51b03e3 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -300,9 +300,6 @@ async def write_network_info(self, *, network_info, node_info): await self._change_network_state(NetworkState.CONNECTED) async def load_network_info(self, *, load_devices=False): - if self._api.firmware_version.platform == FirmwarePlatform.Conbee_III: - await self._change_network_state(NetworkState.CONNECTED) - network_info = self.state.network_info node_info = self.state.node_info From 1565963de0cf270f8f50d63bc729cbfc290d35aa Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:55:06 -0500 Subject: [PATCH 12/22] Add a `probe` method --- zigpy_deconz/zigbee/application.py | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 51b03e3..85338ba 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -88,6 +88,41 @@ async def _reset_watchdog(self): await asyncio.sleep(self._config[CONF_WATCHDOG_TTL] * 0.75) + @classmethod + async def probe(cls, device_config: dict[str, Any]) -> bool | dict[str, Any]: + """Probes the device specified by `device_config` and returns valid settings. + + If the device is not supported, `False`. + """ + + device_config = zigpy.config.SCHEMA_DEVICE(device_config) + probe_configs = [device_config] + + # Probe the Conbee III with 115200 if we aren't already doing so + if device_config[zigpy.config.CONF_DEVICE_BAUDRATE] != 115200: + probe_configs.append( + {**device_config, zigpy.config.CONF_DEVICE_BAUDRATE: 115200} + ) + + for device_config in probe_configs: + config = cls.SCHEMA( + {zigpy.config.CONF_DEVICE: cls.SCHEMA_DEVICE(device_config)} + ) + app = cls(config) + + try: + await app.connect() + except Exception: + LOGGER.debug( + "Failed to probe with config %s", device_config, exc_info=True + ) + else: + return device_config + finally: + await app.disconnect() + + return False + async def connect(self): api = Deconz(self, self._config[zigpy.config.CONF_DEVICE]) From d8a8ba28d7536753f84b647d5523efa2d9410bbe Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 14 Nov 2023 13:05:12 -0500 Subject: [PATCH 13/22] Fix probing schema --- zigpy_deconz/zigbee/application.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 85338ba..16b782c 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -40,7 +40,12 @@ Status, TXStatus, ) -from zigpy_deconz.config import CONF_WATCHDOG_TTL, CONFIG_SCHEMA, SCHEMA_DEVICE +from zigpy_deconz.config import ( + CONF_DEVICE_BAUDRATE, + CONF_WATCHDOG_TTL, + CONFIG_SCHEMA, + SCHEMA_DEVICE, +) import zigpy_deconz.exception LOGGER = logging.getLogger(__name__) @@ -95,14 +100,12 @@ async def probe(cls, device_config: dict[str, Any]) -> bool | dict[str, Any]: If the device is not supported, `False`. """ - device_config = zigpy.config.SCHEMA_DEVICE(device_config) + device_config = cls.SCHEMA_DEVICE(device_config) probe_configs = [device_config] # Probe the Conbee III with 115200 if we aren't already doing so - if device_config[zigpy.config.CONF_DEVICE_BAUDRATE] != 115200: - probe_configs.append( - {**device_config, zigpy.config.CONF_DEVICE_BAUDRATE: 115200} - ) + if device_config[CONF_DEVICE_BAUDRATE] != 115200: + probe_configs.append({**device_config, CONF_DEVICE_BAUDRATE: 115200}) for device_config in probe_configs: config = cls.SCHEMA( From 1e6ac9298a1fd32c3070defbb85492f92581eddf Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 14 Nov 2023 13:21:16 -0500 Subject: [PATCH 14/22] Handle state change polling failures gracefully --- zigpy_deconz/zigbee/application.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 16b782c..bae01c5 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -196,10 +196,16 @@ async def _change_network_state( ): async def change_loop(): while True: - device_state = await self._api.get_device_state() + try: + device_state = await self._api.get_device_state() + except asyncio.TimeoutError: + # 0x264B0900 and earlier can reset during device state changes + # requiring a firmware reset, causing state polling to fail + LOGGER.debug("Failed to poll device state") + else: + if NetworkState(device_state.network_state) == target_state: + break - if NetworkState(device_state.network_state) == target_state: - break await asyncio.sleep(CHANGE_NETWORK_POLL_TIME) await self._api.change_network_state(target_state) From 8065517e3ed34a9370e8047b6ef9d2d8eac56a5a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 16 Nov 2023 10:35:59 -0500 Subject: [PATCH 15/22] Bump minimum zigpy version to 0.60.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ce7a737..e322a90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ license = {text = "GPL-3.0"} requires-python = ">=3.8" dependencies = [ "voluptuous", - "zigpy>=0.54.1", + "zigpy>=0.60.0", 'async-timeout; python_version<"3.11"', ] From 9cb3919d5d1eb04e5d9d3b7c0c6a38b10c2b2946 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 16 Nov 2023 10:59:47 -0500 Subject: [PATCH 16/22] Use new zigpy probing methods --- zigpy_deconz/zigbee/application.py | 38 ++++-------------------------- 1 file changed, 5 insertions(+), 33 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index bae01c5..fb6831c 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -66,6 +66,11 @@ class ControllerApplication(zigpy.application.ControllerApplication): SCHEMA = CONFIG_SCHEMA SCHEMA_DEVICE = SCHEMA_DEVICE + _probe_config_variants = [ + {CONF_DEVICE_BAUDRATE: 57600}, + {CONF_DEVICE_BAUDRATE: 115200}, + ] + def __init__(self, config: dict[str, Any]): """Initialize instance.""" @@ -93,39 +98,6 @@ async def _reset_watchdog(self): await asyncio.sleep(self._config[CONF_WATCHDOG_TTL] * 0.75) - @classmethod - async def probe(cls, device_config: dict[str, Any]) -> bool | dict[str, Any]: - """Probes the device specified by `device_config` and returns valid settings. - - If the device is not supported, `False`. - """ - - device_config = cls.SCHEMA_DEVICE(device_config) - probe_configs = [device_config] - - # Probe the Conbee III with 115200 if we aren't already doing so - if device_config[CONF_DEVICE_BAUDRATE] != 115200: - probe_configs.append({**device_config, CONF_DEVICE_BAUDRATE: 115200}) - - for device_config in probe_configs: - config = cls.SCHEMA( - {zigpy.config.CONF_DEVICE: cls.SCHEMA_DEVICE(device_config)} - ) - app = cls(config) - - try: - await app.connect() - except Exception: - LOGGER.debug( - "Failed to probe with config %s", device_config, exc_info=True - ) - else: - return device_config - finally: - await app.disconnect() - - return False - async def connect(self): api = Deconz(self, self._config[zigpy.config.CONF_DEVICE]) From 9a51a4f53984065be1e87c8f608ccb3cfff630f3 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:05:39 -0500 Subject: [PATCH 17/22] Use zigpy watchdog --- zigpy_deconz/config.py | 13 ------- zigpy_deconz/zigbee/application.py | 54 +++++++----------------------- 2 files changed, 13 insertions(+), 54 deletions(-) diff --git a/zigpy_deconz/config.py b/zigpy_deconz/config.py index 58911bc..6a53ac9 100644 --- a/zigpy_deconz/config.py +++ b/zigpy_deconz/config.py @@ -23,21 +23,8 @@ CONF_MAX_CONCURRENT_REQUESTS_DEFAULT = 8 -CONF_WATCHDOG_TTL = "watchdog_ttl" -CONF_WATCHDOG_TTL_DEFAULT = 600 - -CONF_DEVICE_BAUDRATE = "baudrate" - -SCHEMA_DEVICE = SCHEMA_DEVICE.extend( - {vol.Optional(CONF_DEVICE_BAUDRATE, default=38400): int} -) - CONFIG_SCHEMA = CONFIG_SCHEMA.extend( { - vol.Required(CONF_DEVICE): SCHEMA_DEVICE, - vol.Optional(CONF_WATCHDOG_TTL, default=CONF_WATCHDOG_TTL_DEFAULT): vol.All( - int, vol.Range(min=180) - ), vol.Optional( CONF_MAX_CONCURRENT_REQUESTS, default=CONF_MAX_CONCURRENT_REQUESTS_DEFAULT ): CONFIG_SCHEMA.schema[CONF_MAX_CONCURRENT_REQUESTS], diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index fb6831c..44fa680 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -40,12 +40,7 @@ Status, TXStatus, ) -from zigpy_deconz.config import ( - CONF_DEVICE_BAUDRATE, - CONF_WATCHDOG_TTL, - CONFIG_SCHEMA, - SCHEMA_DEVICE, -) +from zigpy_deconz.config import CONFIG_SCHEMA import zigpy_deconz.exception LOGGER = logging.getLogger(__name__) @@ -64,11 +59,10 @@ class ControllerApplication(zigpy.application.ControllerApplication): SCHEMA = CONFIG_SCHEMA - SCHEMA_DEVICE = SCHEMA_DEVICE _probe_config_variants = [ - {CONF_DEVICE_BAUDRATE: 57600}, - {CONF_DEVICE_BAUDRATE: 115200}, + {zigpy.config.CONF_DEVICE_BAUDRATE: 57600}, + {zigpy.config.CONF_DEVICE_BAUDRATE: 115200}, ] def __init__(self, config: dict[str, Any]): @@ -79,24 +73,22 @@ def __init__(self, config: dict[str, Any]): self._pending = zigpy.util.Requests() - self._reset_watchdog_task = None self._delayed_neighbor_scan_task = None self._reconnect_task = None self._written_endpoints = set() - async def _reset_watchdog(self): - while True: - try: - await self._api.write_parameter( - NetworkParameter.watchdog_ttl, self._config[CONF_WATCHDOG_TTL] - ) - except Exception as e: - LOGGER.warning("Failed to reset watchdog", exc_info=e) - self.connection_lost(e) - return + async def _watchdog_feed(self): + await self._api.get_device_state() - await asyncio.sleep(self._config[CONF_WATCHDOG_TTL] * 0.75) + if ( + self._api.protocol_version >= PROTO_VER_WATCHDOG + and self._api.firmware_version.platform != FirmwarePlatform.Conbee_III + and self._api.firmware_version > 0x26450900 + ): + await self._api.write_parameter( + NetworkParameter.watchdog_ttl, int(2 * self._watchdog_period) + ) async def connect(self): api = Deconz(self, self._config[zigpy.config.CONF_DEVICE]) @@ -115,14 +107,6 @@ def close(self): self._delayed_neighbor_scan_task.cancel() self._delayed_neighbor_scan_task = None - if self._reset_watchdog_task is not None: - self._reset_watchdog_task.cancel() - self._reset_watchdog_task = None - - if self._reconnect_task is not None: - self._reconnect_task.cancel() - self._reconnect_task = None - if self._api is not None: self._api.close() self._api = None @@ -196,18 +180,6 @@ async def change_loop(): " 2.4GHz routers, motherboards, etc." ) - if self._api.protocol_version < PROTO_VER_WATCHDOG or ( - self._api.firmware_version.platform == FirmwarePlatform.Conbee_III - and self._api.firmware_version == 0x26450900 - ): - return - - if self._reset_watchdog_task is not None: - self._reset_watchdog_task.cancel() - - if target_state == NetworkState.CONNECTED: - self._reset_watchdog_task = asyncio.create_task(self._reset_watchdog()) - async def reset_network_info(self): # TODO: There does not appear to be a way to factory reset a Conbee await self.form_network() From 68ac3b990ee529b9987276bd83ed4e1234ad702c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:32:29 -0500 Subject: [PATCH 18/22] Fix API using removed config --- zigpy_deconz/uart.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/zigpy_deconz/uart.py b/zigpy_deconz/uart.py index b243de5..f555787 100644 --- a/zigpy_deconz/uart.py +++ b/zigpy_deconz/uart.py @@ -5,10 +5,9 @@ import logging from typing import Callable, Dict +import zigpy.config import zigpy.serial -from zigpy_deconz.config import CONF_DEVICE_BAUDRATE, CONF_DEVICE_PATH - LOGGER = logging.getLogger(__name__) @@ -127,18 +126,18 @@ async def connect(config: Dict[str, any], api: Callable) -> Gateway: connected_future = loop.create_future() protocol = Gateway(api, connected_future) - LOGGER.debug("Connecting to %s", config[CONF_DEVICE_PATH]) + LOGGER.debug("Connecting to %s", config[zigpy.config.CONF_DEVICE_PATH]) _, protocol = await zigpy.serial.create_serial_connection( loop=loop, protocol_factory=lambda: protocol, - url=config[CONF_DEVICE_PATH], - baudrate=config[CONF_DEVICE_BAUDRATE], + url=config[zigpy.config.CONF_DEVICE_PATH], + baudrate=config[zigpy.config.CONF_DEVICE_BAUDRATE], xonxoff=False, ) await connected_future - LOGGER.debug("Connected to %s", config[CONF_DEVICE_PATH]) + LOGGER.debug("Connected to %s", config[zigpy.config.CONF_DEVICE_PATH]) return protocol From 3ffeb90f4cd8b1cb81efa0328252c82b3d9c6a47 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:32:35 -0500 Subject: [PATCH 19/22] Implement `permit_with_link_key` --- zigpy_deconz/zigbee/application.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 44fa680..a7727b5 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -117,8 +117,11 @@ async def disconnect(self): if self._api is not None: self._api.close() - async def permit_with_key(self, node: t.EUI64, code: bytes, time_s=60): - raise NotImplementedError() + async def permit_with_link_key(self, node: t.EUI64, link_key: t.KeyData, time_s=60): + await self._api.write_parameter( + NetworkParameter.link_key, + LinkKey(ieee=node, key=link_key), + ) async def start_network(self): await self.register_endpoints() From 23c076671e2027f686014f7618a738e64fcb2c07 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:34:59 -0500 Subject: [PATCH 20/22] Remove watchdog from unit tests --- tests/test_application.py | 75 +++++++++++++++--------------- tests/test_uart.py | 3 +- zigpy_deconz/zigbee/application.py | 56 ++-------------------- 3 files changed, 42 insertions(+), 92 deletions(-) diff --git a/tests/test_application.py b/tests/test_application.py index bc50b72..75dd44a 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -201,16 +201,12 @@ async def test_connect_failure(app): async def test_disconnect(app): - reset_watchdog_task = app._reset_watchdog_task = MagicMock() api_close = app._api.close = MagicMock() await app.disconnect() assert app._api is None - assert app._reset_watchdog_task is None - assert api_close.call_count == 1 - assert reset_watchdog_task.cancel.call_count == 1 async def test_disconnect_no_api(app): @@ -225,9 +221,26 @@ async def test_disconnect_close_error(app): await app.disconnect() -async def test_permit_with_key_not_implemented(app): - with pytest.raises(NotImplementedError): - await app.permit_with_key(node=MagicMock(), code=b"abcdef") +async def test_permit_with_link_key(app): + with patch.object(app._api, "write_parameter"): + await app.permit_with_link_key( + node=t.EUI64.convert("00:11:22:33:44:55:66:77"), + install_code=t.KeyData.convert( + "aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd" + ), + ) + + assert app._api.write_parameter.mock_calls == [ + mock.call( + deconz_api.NetworkParameter.link_key, + deconz_api.LinkKey( + ieee=t.EUI64.convert("00:11:22:33:44:55:66:77"), + key=t.KeyData.convert( + "aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd" + ), + ), + ) + ] async def test_deconz_dev_add_to_group(app, nwk, device_path): @@ -347,18 +360,21 @@ def test_tx_confirm_unexpcted(app, caplog): async def test_reset_watchdog(app): """Test watchdog.""" - with patch.object(app._api, "write_parameter") as mock_api: - dog = asyncio.create_task(app._reset_watchdog()) - await asyncio.sleep(0.3) - dog.cancel() - assert mock_api.call_count == 1 + app._api.protocol_version = application.PROTO_VER_WATCHDOG + app._api.get_device_state = AsyncMock() + app._api.write_parameter = AsyncMock() + + await app._watchdog_feed() + assert len(app._api.get_device_state.mock_calls) == 1 + assert len(app._api.write_parameter.mock_calls) == 1 + + app._api.protocol_version = application.PROTO_VER_WATCHDOG - 1 + app._api.get_device_state.reset_mock() + app._api.write_parameter.reset_mock() - with patch.object(app._api, "write_parameter") as mock_api: - mock_api.side_effect = zigpy_deconz.exception.CommandError - dog = asyncio.create_task(app._reset_watchdog()) - await asyncio.sleep(0.3) - dog.cancel() - assert mock_api.call_count == 1 + await app._watchdog_feed() + assert len(app._api.get_device_state.mock_calls) == 1 + assert len(app._api.write_parameter.mock_calls) == 0 async def test_force_remove(app): @@ -428,10 +444,7 @@ async def test_delayed_scan(): @patch("zigpy_deconz.zigbee.application.CHANGE_NETWORK_POLL_TIME", 0.001) -@pytest.mark.parametrize("support_watchdog", [False, True]) -async def test_change_network_state(app, support_watchdog): - app._reset_watchdog_task = MagicMock() - +async def test_change_network_state(app): app._api.get_device_state = AsyncMock( side_effect=[ deconz_api.DeviceState(deconz_api.NetworkState.OFFLINE), @@ -440,25 +453,11 @@ async def test_change_network_state(app, support_watchdog): ] ) - if support_watchdog: - app._api._protocol_version = application.PROTO_VER_WATCHDOG - app._api.protocol_version = application.PROTO_VER_WATCHDOG - else: - app._api._protocol_version = application.PROTO_VER_WATCHDOG - 1 - app._api.protocol_version = application.PROTO_VER_WATCHDOG - 1 - - old_watchdog_task = app._reset_watchdog_task - cancel_mock = app._reset_watchdog_task.cancel = MagicMock() + app._api._protocol_version = application.PROTO_VER_WATCHDOG + app._api.protocol_version = application.PROTO_VER_WATCHDOG await app._change_network_state(deconz_api.NetworkState.CONNECTED, timeout=0.01) - if support_watchdog: - assert cancel_mock.call_count == 1 - assert app._reset_watchdog_task is not old_watchdog_task - else: - assert cancel_mock.call_count == 0 - assert app._reset_watchdog_task is old_watchdog_task - ENDPOINT = zdo_t.SimpleDescriptor( endpoint=None, diff --git a/tests/test_uart.py b/tests/test_uart.py index 7082869..432c48f 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -4,11 +4,10 @@ from unittest import mock import pytest -from zigpy.config import CONF_DEVICE_PATH +from zigpy.config import CONF_DEVICE_BAUDRATE, CONF_DEVICE_PATH import zigpy.serial from zigpy_deconz import uart -from zigpy_deconz.config import CONF_DEVICE_BAUDRATE @pytest.fixture diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index a7727b5..7ac739b 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -81,10 +81,9 @@ def __init__(self, config: dict[str, Any]): async def _watchdog_feed(self): await self._api.get_device_state() - if ( - self._api.protocol_version >= PROTO_VER_WATCHDOG - and self._api.firmware_version.platform != FirmwarePlatform.Conbee_III - and self._api.firmware_version > 0x26450900 + if self._api.protocol_version >= PROTO_VER_WATCHDOG and not ( + self._api.firmware_version.platform == FirmwarePlatform.Conbee_III + and self._api.firmware_version <= 0x26450900 ): await self._api.write_parameter( NetworkParameter.watchdog_ttl, int(2 * self._watchdog_period) @@ -102,7 +101,7 @@ async def connect(self): self._api = api self._written_endpoints.clear() - def close(self): + async def disconnect(self): if self._delayed_neighbor_scan_task is not None: self._delayed_neighbor_scan_task.cancel() self._delayed_neighbor_scan_task = None @@ -111,12 +110,6 @@ def close(self): self._api.close() self._api = None - async def disconnect(self): - self.close() - - if self._api is not None: - self._api.close() - async def permit_with_link_key(self, node: t.EUI64, link_key: t.KeyData, time_s=60): await self._api.write_parameter( NetworkParameter.link_key, @@ -580,47 +573,6 @@ async def _delayed_neighbour_scan(self) -> None: coord = self.get_device(ieee=self.state.node_info.ieee) await self.topology.scan(devices=[coord]) - def connection_lost(self, exc: Exception) -> None: - """Lost connection.""" - - if exc is not None: - LOGGER.warning("Lost connection: %r", exc) - - self.close() - self._reconnect_task = asyncio.create_task(self._reconnect_loop()) - - async def _reconnect_loop(self) -> None: - attempt = 1 - - while True: - LOGGER.debug("Reconnecting, attempt %s", attempt) - - try: - async with asyncio_timeout(10): - await self.connect() - async with asyncio_timeout(10): - await self.initialize() - break - except Exception as exc: - wait = 2 ** min(attempt, 5) - attempt += 1 - LOGGER.debug( - "Couldn't re-open '%s' serial port, retrying in %ss: %s", - self._config[zigpy.config.CONF_DEVICE][ - zigpy.config.CONF_DEVICE_PATH - ], - wait, - str(exc), - exc_info=exc, - ) - await asyncio.sleep(wait) - - LOGGER.debug( - "Reconnected '%s' serial port after %s attempts", - self._config[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH], - attempt, - ) - class DeconzDevice(zigpy.device.Device): """Zigpy Device representing Coordinator.""" From a55def711ed78c0eccc7760dca74ee7c2786163b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:56:30 -0500 Subject: [PATCH 21/22] Fix unit tests --- tests/test_application.py | 51 +++++++----------------------- zigpy_deconz/zigbee/application.py | 1 + 2 files changed, 12 insertions(+), 40 deletions(-) diff --git a/tests/test_application.py b/tests/test_application.py index 75dd44a..7eddc63 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -8,7 +8,7 @@ import zigpy.application import zigpy.config import zigpy.device -from zigpy.types import EUI64, Channels +from zigpy.types import EUI64, Channels, KeyData import zigpy.zdo.types as zdo_t from zigpy_deconz import types as t @@ -222,26 +222,26 @@ async def test_disconnect_close_error(app): async def test_permit_with_link_key(app): - with patch.object(app._api, "write_parameter"): - await app.permit_with_link_key( - node=t.EUI64.convert("00:11:22:33:44:55:66:77"), - install_code=t.KeyData.convert( - "aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd" - ), - ) + app._api.write_parameter = AsyncMock() + app.permit = AsyncMock() + + await app.permit_with_link_key( + node=t.EUI64.convert("00:11:22:33:44:55:66:77"), + link_key=KeyData.convert("aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd"), + ) assert app._api.write_parameter.mock_calls == [ mock.call( deconz_api.NetworkParameter.link_key, deconz_api.LinkKey( ieee=t.EUI64.convert("00:11:22:33:44:55:66:77"), - key=t.KeyData.convert( - "aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd" - ), + key=KeyData.convert("aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd:aa:bb:cc:dd"), ), ) ] + assert app.permit.mock_calls == [mock.call(mock.ANY)] + async def test_deconz_dev_add_to_group(app, nwk, device_path): group = MagicMock() @@ -552,35 +552,6 @@ async def read_param(param_id, index): ) -@patch("zigpy_deconz.zigbee.application.asyncio.sleep", new_callable=AsyncMock) -@patch( - "zigpy_deconz.zigbee.application.ControllerApplication.initialize", - side_effect=[RuntimeError(), None], -) -@patch( - "zigpy_deconz.zigbee.application.ControllerApplication.connect", - side_effect=[RuntimeError(), None, None], -) -async def test_reconnect(mock_connect, mock_initialize, mock_sleep, app): - assert app._reconnect_task is None - app.connection_lost(RuntimeError()) - - assert app._reconnect_task is not None - await app._reconnect_task - - assert mock_connect.call_count == 3 - assert mock_initialize.call_count == 2 - - -async def test_disconnect_during_reconnect(app): - assert app._reconnect_task is None - app.connection_lost(RuntimeError()) - await asyncio.sleep(0) - await app.disconnect() - - assert app._reconnect_task is None - - async def test_reset_network_info(app): app.form_network = AsyncMock() await app.reset_network_info() diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 7ac739b..1b4cc68 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -115,6 +115,7 @@ async def permit_with_link_key(self, node: t.EUI64, link_key: t.KeyData, time_s= NetworkParameter.link_key, LinkKey(ieee=node, key=link_key), ) + await self.permit(time_s) async def start_network(self): await self.register_endpoints() From e56caad72e60a2a249442976c3209d26ec15f265 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 16 Nov 2023 12:10:16 -0500 Subject: [PATCH 22/22] Parse model info during `load_network_info` --- tests/test_application.py | 38 +++------------------ tests/test_network_state.py | 5 +++ zigpy_deconz/zigbee/application.py | 54 ++++++++++++++++++------------ 3 files changed, 42 insertions(+), 55 deletions(-) diff --git a/tests/test_application.py b/tests/test_application.py index 7eddc63..e1ef35e 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -248,9 +248,7 @@ async def test_deconz_dev_add_to_group(app, nwk, device_path): app._groups = MagicMock() app._groups.add_group.return_value = group - deconz = application.DeconzDevice( - deconz_api.FirmwareVersion(0x26580700), device_path, app, sentinel.ieee, nwk - ) + deconz = application.DeconzDevice("Conbee II", app, sentinel.ieee, nwk) deconz.endpoints = { 0: sentinel.zdo, 1: sentinel.ep1, @@ -268,9 +266,7 @@ async def test_deconz_dev_add_to_group(app, nwk, device_path): async def test_deconz_dev_remove_from_group(app, nwk, device_path): group = MagicMock() app.groups[sentinel.grp_id] = group - deconz = application.DeconzDevice( - deconz_api.FirmwareVersion(0x26580700), device_path, app, sentinel.ieee, nwk - ) + deconz = application.DeconzDevice("Conbee II", app, sentinel.ieee, nwk) deconz.endpoints = { 0: sentinel.zdo, 1: sentinel.ep1, @@ -282,38 +278,16 @@ async def test_deconz_dev_remove_from_group(app, nwk, device_path): def test_deconz_props(nwk, device_path): - deconz = application.DeconzDevice( - deconz_api.FirmwareVersion(0x26580700), device_path, app, sentinel.ieee, nwk - ) + deconz = application.DeconzDevice("Conbee II", app, sentinel.ieee, nwk) assert deconz.manufacturer is not None assert deconz.model is not None -@pytest.mark.parametrize( - "name, firmware_version, device_path", - [ - ("ConBee", deconz_api.FirmwareVersion(0x00000500), "/dev/ttyUSB0"), - ("ConBee II", deconz_api.FirmwareVersion(0x00000700), "/dev/ttyUSB0"), - ("RaspBee", deconz_api.FirmwareVersion(0x00000500), "/dev/ttyS0"), - ("RaspBee II", deconz_api.FirmwareVersion(0x00000700), "/dev/ttyS0"), - ("RaspBee", deconz_api.FirmwareVersion(0x00000500), "/dev/ttyAMA0"), - ("RaspBee II", deconz_api.FirmwareVersion(0x00000700), "/dev/ttyAMA0"), - ], -) -def test_deconz_name(nwk, name, firmware_version, device_path): - deconz = application.DeconzDevice( - firmware_version, device_path, app, sentinel.ieee, nwk - ) - assert deconz.model == name - - async def test_deconz_new(app, nwk, device_path, monkeypatch): mock_init = AsyncMock() monkeypatch.setattr(zigpy.device.Device, "_initialize", mock_init) - deconz = await application.DeconzDevice.new( - app, sentinel.ieee, nwk, deconz_api.FirmwareVersion(0x26580700), device_path - ) + deconz = await application.DeconzDevice.new(app, sentinel.ieee, nwk, "Conbee II") assert isinstance(deconz, application.DeconzDevice) assert mock_init.call_count == 1 mock_init.reset_mock() @@ -325,9 +299,7 @@ async def test_deconz_new(app, nwk, device_path, monkeypatch): 22: MagicMock(), } app.devices[sentinel.ieee] = mock_dev - deconz = await application.DeconzDevice.new( - app, sentinel.ieee, nwk, deconz_api.FirmwareVersion(0x26580700), device_path - ) + deconz = await application.DeconzDevice.new(app, sentinel.ieee, nwk, "Conbee II") assert isinstance(deconz, application.DeconzDevice) assert mock_init.call_count == 0 diff --git a/tests/test_network_state.py b/tests/test_network_state.py index 9040421..9cddade 100644 --- a/tests/test_network_state.py +++ b/tests/test_network_state.py @@ -32,6 +32,9 @@ def node_info(): nwk=t.NWK(0x0000), ieee=t.EUI64.convert("93:2C:A9:34:D9:D0:5D:12"), logical_type=zdo_t.LogicalType.Coordinator, + manufacturer="dresden elektronik", + model="Conbee II", + version="0x26580700", ) @@ -263,6 +266,7 @@ async def test_load_network_info( ieee=node_info.ieee, key=network_info.tc_link_key.key ), ("security_mode",): zigpy_deconz.api.SecurityMode.ONLY_TCLK, + ("protocol_version",): 0x010E, } params.update(param_overrides) @@ -280,6 +284,7 @@ async def read_param(param, *args): return value + app._api.firmware_version = zigpy_deconz.api.FirmwareVersion(0x26580700) app._api.read_parameter = AsyncMock(side_effect=read_param) if error is not None: diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 1b4cc68..4906dea 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -30,7 +30,6 @@ from zigpy_deconz.api import ( Deconz, FirmwarePlatform, - FirmwareVersion, IndexedEndpoint, IndexedKey, LinkKey, @@ -126,8 +125,7 @@ async def start_network(self): self, self.state.node_info.ieee, self.state.node_info.nwk, - self._api.firmware_version, - self._config[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH], + self.state.node_info.model, ) self.devices[self.state.node_info.ieee] = coordinator @@ -288,15 +286,6 @@ async def load_network_info(self, *, load_devices=False): network_info = self.state.network_info node_info = self.state.node_info - network_info.source = ( - f"zigpy-deconz@{importlib.metadata.version('zigpy-deconz')}" - ) - network_info.metadata = { - "deconz": { - "version": f"{int(self._api.firmware_version):#010x}", - } - } - ieee = await self._api.read_parameter(NetworkParameter.mac_address) node_info.ieee = zigpy.types.EUI64(ieee) designed_coord = await self._api.read_parameter( @@ -310,6 +299,33 @@ async def load_network_info(self, *, load_devices=False): node_info.nwk = await self._api.read_parameter(NetworkParameter.nwk_address) + node_info.manufacturer = "dresden elektronik" + + if re.match( + r"/dev/tty(S|AMA|ACM)\d+", + self._config[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH], + ): + node_info.model = "Raspbee" + else: + node_info.model = "Conbee" + + node_info.model += { + FirmwarePlatform.Conbee: "", + FirmwarePlatform.Conbee_II: " II", + FirmwarePlatform.Conbee_III: " III", + }[self._api.firmware_version.platform] + + node_info.version = f"{int(self._api.firmware_version):#010x}" + + network_info.source = ( + f"zigpy-deconz@{importlib.metadata.version('zigpy-deconz')}" + ) + network_info.metadata = { + "deconz": { + "version": node_info.version, + } + } + network_info.pan_id = await self._api.read_parameter(NetworkParameter.nwk_panid) network_info.extended_pan_id = await self._api.read_parameter( NetworkParameter.aps_extended_panid @@ -578,17 +594,11 @@ async def _delayed_neighbour_scan(self) -> None: class DeconzDevice(zigpy.device.Device): """Zigpy Device representing Coordinator.""" - def __init__(self, version: FirmwareVersion, device_path: str, *args): + def __init__(self, model: str, *args): """Initialize instance.""" super().__init__(*args) - is_gpio_device = re.match(r"/dev/tty(S|AMA|ACM)\d+", device_path) - self._model = "RaspBee" if is_gpio_device else "ConBee" - self._model += { - FirmwarePlatform.Conbee: "", - FirmwarePlatform.Conbee_II: " II", - FirmwarePlatform.Conbee_III: " III", - }[version.platform] + self._model = model async def add_to_group(self, grp_id: int, name: str = None) -> None: group = self.application.groups.add_group(grp_id, name) @@ -615,9 +625,9 @@ def model(self): return self._model @classmethod - async def new(cls, application, ieee, nwk, version: int, device_path: str): + async def new(cls, application, ieee, nwk, model: str): """Create or replace zigpy device.""" - dev = cls(version, device_path, application, ieee, nwk) + dev = cls(model, application, ieee, nwk) if ieee in application.devices: from_dev = application.get_device(ieee=ieee)