From 42ee51da0638869fa49f6caf75284ba68d35d21c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 31 Oct 2023 11:50:08 -0400 Subject: [PATCH 1/9] Load parsed device info into application state --- tests/api/test_network_state.py | 6 +++++- tests/application/test_startup.py | 12 +++++++----- zigpy_znp/api.py | 13 +++++++++++-- zigpy_znp/zigbee/device.py | 11 +---------- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/tests/api/test_network_state.py b/tests/api/test_network_state.py index d8743f74..0bee1276 100644 --- a/tests/api/test_network_state.py +++ b/tests/api/test_network_state.py @@ -52,7 +52,11 @@ async def test_state_transfer(from_device, to_device, make_connected_znp): metadata=formed_znp.network_info.metadata ) - assert formed_znp.node_info == empty_znp.node_info + assert formed_znp.node_info == empty_znp.node_info.replace( + version=formed_znp.node_info.version, + model=formed_znp.node_info.model, + manufacturer=formed_znp.node_info.manufacturer, + ) @pytest.mark.parametrize("device", [FormedZStack3CC2531]) diff --git a/tests/application/test_startup.py b/tests/application/test_startup.py index ce55feee..828bae51 100644 --- a/tests/application/test_startup.py +++ b/tests/application/test_startup.py @@ -22,7 +22,7 @@ DEV_NETWORK_SETTINGS = { FormedLaunchpadCC26X2R1: ( - f"CC1352/CC2652, Z-Stack 3.30+ (build {FormedLaunchpadCC26X2R1.code_revision})", + "CC2652" f"Z-Stack {FormedLaunchpadCC26X2R1.code_revision}", 15, t.Channels.from_channel_list([15]), 0x4402, @@ -30,7 +30,7 @@ t.KeyData.convert("4C:4E:72:B8:41:22:51:79:9A:BF:35:25:12:88:CA:83"), ), FormedZStack3CC2531: ( - f"CC2531, Z-Stack 3.0.x (build {FormedZStack3CC2531.code_revision})", + "CC2531" f"Z-Stack 3.0.x {FormedZStack3CC2531.code_revision}", 15, t.Channels.from_channel_list([15]), 0xB6AB, @@ -38,7 +38,7 @@ t.KeyData.convert("6D:DE:24:EA:E2:85:52:B6:DE:29:56:EB:05:85:1A:FA"), ), FormedZStack1CC2531: ( - f"CC2531, Z-Stack Home 1.2 (build {FormedZStack1CC2531.code_revision})", + "CC2531" f"Z-Stack Home 1.2 {FormedZStack1CC2531.code_revision}", 11, t.Channels.from_channel_list([11]), 0x1A62, @@ -56,6 +56,7 @@ async def test_info( device, model, + version, channel, channels, pan_id, @@ -80,8 +81,9 @@ async def test_info( assert app.state.network_info.network_key.key == network_key assert app.state.network_info.network_key.seq == 0 - assert app._device.manufacturer == "Texas Instruments" - assert app._device.model == model + assert app.state.node_info.manufacturer == "Texas Instruments" + assert app.state.node_info.model == model + assert app.state.node_info.version == version # Anything to make sure it's set assert app._device.node_desc.maximum_outgoing_transfer_size == 160 diff --git a/zigpy_znp/api.py b/zigpy_znp/api.py index 0dfd260b..af979309 100644 --- a/zigpy_znp/api.py +++ b/zigpy_znp/api.py @@ -121,10 +121,21 @@ async def _load_network_info(self, *, load_devices=False): OsalNvIds.LOGICAL_TYPE, item_type=t.DeviceLogicalType ) + if self.version > 3.0: + model = "CC2652" + fw_variant = "Z-Stack" + else: + model = "CC2538" if self.nvram.align_structs else "CC2531" + fw_variant = "Z-Stack Home 1.2" if self.version == 1.2 else "Z-Stack 3.0.x" + + version = await self.request(c.SYS.Version.Req()) node_info = zigpy.state.NodeInfo( ieee=ieee, nwk=nib.nwkDevAddress, logical_type=zdo_t.LogicalType(logical_type), + manufacturer="Texas Instruments", + model=model, + version=f"{fw_variant} {version.CodeRevision or 0x00000000}", ) key_desc = await self.nvram.osal_read( @@ -135,8 +146,6 @@ async def _load_network_info(self, *, load_devices=False): self, ext_pan_id=nib.extendedPANID ) - version = await self.request(c.SYS.Version.Req()) - network_info = zigpy.state.NetworkInfo( source=f"zigpy-znp@{importlib.metadata.version('zigpy-znp')}", extended_pan_id=nib.extendedPANID, diff --git a/zigpy_znp/zigbee/device.py b/zigpy_znp/zigbee/device.py index 8adef8ec..f6c7a4c6 100644 --- a/zigpy_znp/zigbee/device.py +++ b/zigpy_znp/zigbee/device.py @@ -8,8 +8,6 @@ LOGGER = logging.getLogger(__name__) -NWK_UPDATE_LOOP_DELAY = 1 - class ZNPCoordinator(zigpy.device.Device): """ @@ -22,14 +20,7 @@ def manufacturer(self): @property def model(self): - if self.application._znp.version > 3.0: - model = "CC1352/CC2652" - version = "3.30+" - else: - model = "CC2538" if self.application._znp.nvram.align_structs else "CC2531" - version = "Home 1.2" if self.application._znp.version == 1.2 else "3.0.x" - - return f"{model}, Z-Stack {version} (build {self.application._zstack_build_id})" + return "Coordinator" def request( self, From 18d3ca8bc064835dd95e3051a2107ce3dcfd76bb Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 31 Oct 2023 11:50:28 -0400 Subject: [PATCH 2/9] Fix zigpy unit tests --- tests/application/test_zigpy_callbacks.py | 89 ++++++++++++++++------- 1 file changed, 61 insertions(+), 28 deletions(-) diff --git a/tests/application/test_zigpy_callbacks.py b/tests/application/test_zigpy_callbacks.py index c8465e72..815c4229 100644 --- a/tests/application/test_zigpy_callbacks.py +++ b/tests/application/test_zigpy_callbacks.py @@ -102,7 +102,7 @@ async def test_on_af_message_callback(device, make_application, mocker): app, znp_server = make_application(server_cls=device) await app.startup(auto_form=False) - mocker.patch.object(app, "handle_message") + mocker.patch.object(app, "packet_received") device = app.add_initialized_device(ieee=t.EUI64(range(8)), nwk=0xAABB) af_message = c.AF.IncomingMsg.Callback( @@ -125,49 +125,82 @@ async def test_on_af_message_callback(device, make_application, mocker): znp_server.send(af_message) await asyncio.sleep(0.1) - app.handle_message.assert_called_once_with( - sender=device, - profile=260, - cluster=2, - src_ep=4, - dst_ep=1, - message=b"test", - dst_addressing=zigpy_t.AddrMode.NWK, + app.packet_received.assert_called_once_with( + zigpy_t.ZigbeePacket( + profile_id=260, + cluster_id=0x0002, + src_ep=4, + dst_ep=1, + data=t.SerializableBytes(b"test"), + src=zigpy_t.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.NWK, + address=device.nwk, + ), + dst=zigpy_t.AddrModeAddress( + addr_mode=t.AddrMode.NWK, + address=app.state.node_info.nwk, + ), + lqi=19, + rssi=None, + radius=1, + ) ) - app.handle_message.reset_mock() + app.packet_received.reset_mock() # ZLL message znp_server.send(af_message.replace(DstEndpoint=2)) await asyncio.sleep(0.1) - app.handle_message.assert_called_once_with( - sender=device, - profile=49246, - cluster=2, - src_ep=4, - dst_ep=2, - message=b"test", - dst_addressing=zigpy_t.AddrMode.NWK, + app.packet_received.assert_called_once_with( + zigpy_t.ZigbeePacket( + profile_id=49246, # Profile ID is missing but inferred from registered EP + cluster_id=0x0002, + src_ep=4, + dst_ep=2, + data=t.SerializableBytes(b"test"), + src=zigpy_t.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.NWK, + address=device.nwk, + ), + dst=zigpy_t.AddrModeAddress( + addr_mode=t.AddrMode.NWK, + address=app.state.node_info.nwk, + ), + lqi=19, + rssi=None, + radius=1, + ) ) - app.handle_message.reset_mock() + app.packet_received.reset_mock() # Message on an unknown endpoint (is this possible?) znp_server.send(af_message.replace(DstEndpoint=3)) await asyncio.sleep(0.1) - app.handle_message.assert_called_once_with( - sender=device, - profile=260, - cluster=2, - src_ep=4, - dst_ep=3, - message=b"test", - dst_addressing=zigpy_t.AddrMode.NWK, + app.packet_received.assert_called_once_with( + zigpy_t.ZigbeePacket( + profile_id=260, + cluster_id=0x0002, + src_ep=4, + dst_ep=3, + data=t.SerializableBytes(b"test"), + src=zigpy_t.AddrModeAddress( + addr_mode=zigpy_t.AddrMode.NWK, + address=device.nwk, + ), + dst=zigpy_t.AddrModeAddress( + addr_mode=t.AddrMode.NWK, + address=app.state.node_info.nwk, + ), + lqi=19, + rssi=None, + radius=1, + ) ) - app.handle_message.reset_mock() + app.packet_received.reset_mock() @pytest.mark.parametrize("device", FORMED_DEVICES) From ac3026f5997b992217bdb1a16ef2938d06dd6662 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 31 Oct 2023 11:57:41 -0400 Subject: [PATCH 3/9] Bump all pre-commit dependencies so pre-commit runs --- .pre-commit-config.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 033bdf89..aa96b810 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,27 +1,27 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: debug-statements - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.10.1 hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 entry: pflake8 additional_dependencies: - - pyproject-flake8==6.0.0.post1 + - pyproject-flake8==6.1.0 - flake8-bugbear==23.1.20 - flake8-comprehensions==3.10.1 - flake8_2020==1.7.0 - mccabe==0.7.0 - - pycodestyle==2.10.0 - - pyflakes==3.0.1 + - pycodestyle==2.11.1 + - pyflakes==3.1.0 - repo: https://github.com/PyCQA/isort rev: 5.12.0 @@ -29,7 +29,7 @@ repos: - id: isort - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.0.0 + rev: v1.6.1 hooks: - id: mypy additional_dependencies: @@ -37,11 +37,11 @@ repos: - types-setuptools - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.15.0 hooks: - id: pyupgrade - repo: https://github.com/fsouza/autoflake8 - rev: v0.4.0 + rev: v0.4.1 hooks: - id: autoflake8 From 02ad475302c488a00b00d8c71568b4e920ce4b35 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 31 Oct 2023 12:00:07 -0400 Subject: [PATCH 4/9] Bump to unreleased zigpy --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4cac12d5..e0678bdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ readme = "README.md" license = {text = "GPL-3.0"} requires-python = ">=3.8" dependencies = [ - "zigpy>=0.56.3", + "zigpy>=0.60.0", "async_timeout", "voluptuous", "coloredlogs", From 917b11a0aeed6c64f09f3a43d32b575a2145aed1 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 15 Nov 2023 16:34:31 -0500 Subject: [PATCH 5/9] Use zigpy watchdog and connection closing --- zigpy_znp/zigbee/application.py | 55 ++------------------------------- 1 file changed, 2 insertions(+), 53 deletions(-) diff --git a/zigpy_znp/zigbee/application.py b/zigpy_znp/zigbee/application.py index 46ef99ed..8b28234b 100644 --- a/zigpy_znp/zigbee/application.py +++ b/zigpy_znp/zigbee/application.py @@ -39,7 +39,6 @@ DATA_CONFIRM_TIMEOUT = 8 EXTENDED_DATA_CONFIRM_TIMEOUT = 30 DEVICE_JOIN_MAX_DELAY = 5 -WATCHDOG_PERIOD = 30 REQUEST_MAX_RETRIES = 5 REQUEST_ERROR_RETRY_DELAY = 0.5 @@ -89,9 +88,6 @@ def __init__(self, config: conf.ConfigType): self._reconnect_task: asyncio.Future = asyncio.Future() self._reconnect_task.cancel() - self._watchdog_task: asyncio.Future = asyncio.Future() - self._watchdog_task.cancel() - self._version_rsp = None self._join_announce_tasks: dict[t.EUI64, asyncio.TimerHandle] = {} @@ -121,9 +117,6 @@ async def connect(self): self._bind_callbacks() async def disconnect(self): - self._reconnect_task.cancel() - self._watchdog_task.cancel() - if self._znp is not None: try: await self._znp.reset(wait_for_reset=False) @@ -133,15 +126,6 @@ async def disconnect(self): self._znp.close() self._znp = None - def close(self): - self._reconnect_task.cancel() - self._watchdog_task.cancel() - - # This will close the UART, which will then close the transport - if self._znp is not None: - self._znp.close() - self._znp = None - async def add_endpoint(self, descriptor: zdo_t.SimpleDescriptor) -> None: """ Registers a new endpoint on the device. @@ -253,8 +237,6 @@ async def start_network(self, *, read_only=False): "Your network is using the insecure Zigbee2MQTT network key!" ) - self._watchdog_task = asyncio.create_task(self._watchdog_loop()) - async def set_tx_power(self, dbm: int) -> None: """ Sets the radio TX power. @@ -411,19 +393,6 @@ async def _move_network_to_channel( RspStatus=t.Status.SUCCESS, ) - def connection_lost(self, exc): - """ - Propagated up from UART through ZNP when the connection is lost. - Spawns the auto-reconnect task. - """ - - LOGGER.debug("Connection lost: %s", exc) - - self.close() - - LOGGER.debug("Restarting background reconnection task") - self._reconnect_task = asyncio.create_task(self._reconnect()) - ##################################################### # Z-Stack message callbacks attached during startup # ##################################################### @@ -671,32 +640,12 @@ def znp_config(self) -> conf.ConfigType: return self.config[conf.CONF_ZNP_CONFIG] - async def _watchdog_loop(self): + async def _watchdog_feed(self): """ Watchdog loop to periodically test if Z-Stack is still running. """ - LOGGER.debug("Starting watchdog loop") - - while True: - await asyncio.sleep(WATCHDOG_PERIOD) - - # No point in trying to test the port if it's already disconnected - if self._znp is None: - break - - try: - await self._znp.request(c.SYS.Ping.Req()) - except Exception as e: - LOGGER.error( - "Watchdog check failed", - exc_info=e, - ) - - # Treat the watchdog failure as a disconnect - self.connection_lost(e) - - return + await self._znp.request(c.SYS.Ping.Req()) async def _set_led_mode(self, *, led: t.uint8_t, mode: c.util.LEDMode) -> None: """ From 19e4fc2754d3f5ccc4ffb1913f106a4e4a7795a5 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 15 Nov 2023 17:34:44 -0500 Subject: [PATCH 6/9] Use zigpy `device` config schema --- zigpy_znp/config.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/zigpy_znp/config.py b/zigpy_znp/config.py index f2e668bb..840eadcd 100644 --- a/zigpy_znp/config.py +++ b/zigpy_znp/config.py @@ -7,7 +7,6 @@ CONF_DEVICE, CONF_NWK_KEY, CONFIG_SCHEMA, - SCHEMA_DEVICE, CONF_NWK_PAN_ID, CONF_DEVICE_PATH, CONF_NWK_CHANNEL, @@ -26,18 +25,6 @@ VolPositiveNumber = vol.All(numbers.Real, vol.Range(min=0)) -CONF_DEVICE_BAUDRATE = "baudrate" -CONF_DEVICE_FLOW_CONTROL = "flow_control" - -SCHEMA_DEVICE = SCHEMA_DEVICE.extend( - { - vol.Optional(CONF_DEVICE_BAUDRATE, default=115_200): int, - vol.Optional(CONF_DEVICE_FLOW_CONTROL, default=None): vol.In( - ("hardware", "software", None) - ), - } -) - def EnumValue(enum, transformer=str): def validator(v): @@ -101,7 +88,6 @@ def validator(value: typing.Any) -> None: CONFIG_SCHEMA = CONFIG_SCHEMA.extend( { - vol.Required(CONF_DEVICE): SCHEMA_DEVICE, vol.Optional(CONF_ZNP_CONFIG, default={}): vol.Schema( vol.All( { From 7a6a56db8d738e7ebbc93ea98ba1713ab1de6987 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 15 Nov 2023 17:35:25 -0500 Subject: [PATCH 7/9] Drop `permit_with_key` --- zigpy_znp/zigbee/application.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/zigpy_znp/zigbee/application.py b/zigpy_znp/zigbee/application.py index 8b28234b..0d3cd60c 100644 --- a/zigpy_znp/zigbee/application.py +++ b/zigpy_znp/zigbee/application.py @@ -364,18 +364,6 @@ async def permit_with_link_key( RspStatus=t.Status.SUCCESS, ) - async def permit_with_key(self, node: t.EUI64, code: bytes, time_s=60): - """ - Permits a new device to join with the given IEEE and Install Code. - """ - - key = zigpy.util.convert_install_code(code) - - if key is None: - raise ValueError(f"Invalid install code: {code!r}") - - await self.permit_with_link_key(node=node, link_key=key, time_s=time_s) - async def _move_network_to_channel( self, new_channel: int, new_nwk_update_id: int ) -> None: From 7ba056eb3e154157145e1f4af8ea10f7ae0c608c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 15 Nov 2023 17:52:46 -0500 Subject: [PATCH 8/9] Remove unnecessary unit tests --- tests/application/test_connect.py | 138 --------------------- tests/application/test_joining.py | 19 +-- tests/application/test_startup.py | 11 +- tests/tools/test_network_backup_restore.py | 4 +- zigpy_znp/config.py | 1 + zigpy_znp/uart.py | 7 +- zigpy_znp/zigbee/application.py | 42 ------- 7 files changed, 18 insertions(+), 204 deletions(-) diff --git a/tests/application/test_connect.py b/tests/application/test_connect.py index 2d087cd8..ac2430f3 100644 --- a/tests/application/test_connect.py +++ b/tests/application/test_connect.py @@ -1,4 +1,3 @@ -import asyncio from unittest.mock import patch import pytest @@ -118,48 +117,6 @@ async def test_probe_multiple(device, make_znp_server): assert not any([t._is_connected for t in znp_server._transports]) -@pytest.mark.parametrize("device", FORMED_DEVICES) -async def test_reconnect(device, make_application): - app, znp_server = make_application( - server_cls=device, - client_config={ - # Make auto-reconnection happen really fast - conf.CONF_ZNP_CONFIG: { - conf.CONF_AUTO_RECONNECT_RETRY_DELAY: 0.01, - conf.CONF_SREQ_TIMEOUT: 0.1, - } - }, - shorten_delays=False, - ) - - # Start up the server - await app.startup(auto_form=False) - assert app._znp is not None - - # Don't reply to anything for a bit - with patch.object(znp_server, "frame_received", lambda _: None): - # Now that we're connected, have the server close the connection - znp_server._uart._transport.close() - - # ZNP should be closed - assert app._znp is None - - # Wait for more than the SREQ_TIMEOUT to pass, we should still fail to reconnect - await asyncio.sleep(0.3) - - assert not app._reconnect_task.done() - assert app._znp is None - - # Our reconnect task should complete a moment after we send the ping reply - while app._znp is None: - await asyncio.sleep(0.1) - - assert app._znp is not None - assert app._znp._uart is not None - - await app.shutdown() - - @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_shutdown_from_app(device, mocker, make_application): app, znp_server = make_application(server_cls=device) @@ -185,7 +142,6 @@ async def test_clean_shutdown(make_application): await app.shutdown() assert app._znp is None - assert app._reconnect_task.cancelled() async def test_multiple_shutdown(make_application): @@ -197,100 +153,6 @@ async def test_multiple_shutdown(make_application): await app.shutdown() -@pytest.mark.parametrize("device", FORMED_DEVICES) -async def test_reconnect_lockup(device, make_application, mocker): - mocker.patch("zigpy_znp.zigbee.application.WATCHDOG_PERIOD", 0.1) - - app, znp_server = make_application( - server_cls=device, - client_config={ - # Make auto-reconnection happen really fast - conf.CONF_ZNP_CONFIG: { - conf.CONF_AUTO_RECONNECT_RETRY_DELAY: 0.01, - conf.CONF_SREQ_TIMEOUT: 0.1, - } - }, - ) - - # Start up the server - await app.startup(auto_form=False) - - # Stop responding - with patch.object(znp_server, "frame_received", lambda _: None): - assert app._znp is not None - assert app._reconnect_task.done() - - # Wait for more than the SREQ_TIMEOUT to pass, the watchdog will notice - await asyncio.sleep(0.3) - - # We will treat this as a disconnect - assert app._znp is None - assert app._watchdog_task.done() - assert not app._reconnect_task.done() - - # Our reconnect task should complete after that - while app._znp is None: - await asyncio.sleep(0.1) - - assert app._znp is not None - assert app._znp._uart is not None - - await app.shutdown() - - -@pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) -async def test_reconnect_lockup_pyserial(device, make_application, mocker): - mocker.patch("zigpy_znp.zigbee.application.WATCHDOG_PERIOD", 0.1) - - app, znp_server = make_application( - server_cls=device, - client_config={ - conf.CONF_ZNP_CONFIG: { - conf.CONF_AUTO_RECONNECT_RETRY_DELAY: 0.01, - conf.CONF_SREQ_TIMEOUT: 0.1, - } - }, - ) - - # Start up the server - await app.startup(auto_form=False) - - # On Linux, a connection error during read with queued writes will cause PySerial to - # swallow the exception. This makes it appear like we intentionally closed the - # connection. - - # We are connected - assert app._znp is not None - - did_start_network = asyncio.get_running_loop().create_future() - - async def patched_start_network(old_start_network=app.start_network, **kwargs): - try: - return await old_start_network(**kwargs) - finally: - did_start_network.set_result(True) - - with patch.object(app, "start_network", patched_start_network): - # "Drop" the connection like PySerial - app._znp._uart.connection_lost(exc=None) - - # Wait until we are reconnecting - await did_start_network - - # "Drop" the connection like PySerial again, but during connect - app._znp._uart.connection_lost(exc=None) - - # We should reconnect soon - mocker.spy(app, "_watchdog_loop") - - while app._watchdog_loop.call_count == 0: - await asyncio.sleep(0.1) - - assert app._znp and app._znp._uart - - await app.shutdown() - - @pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) async def test_disconnect(device, make_application): app, znp_server = make_application( diff --git a/tests/application/test_joining.py b/tests/application/test_joining.py index bfe5a9a8..6db28283 100644 --- a/tests/application/test_joining.py +++ b/tests/application/test_joining.py @@ -140,12 +140,13 @@ async def test_permit_join_with_key(device, permit_result, make_application, moc # Consciot bulb ieee = t.EUI64.convert("EC:1B:BD:FF:FE:54:4F:40") code = bytes.fromhex("17D1856872570CEB7ACB53030C5D6DA368B1") + link_key = t.KeyData(zigpy.util.convert_install_code(code)) bdb_add_install_code = znp_server.reply_once_to( c.AppConfig.BDBAddInstallCode.Req( InstallCodeFormat=c.app_config.InstallCodeFormat.KeyDerivedFromInstallCode, IEEE=ieee, - InstallCode=t.Bytes(zigpy.util.convert_install_code(code)), + InstallCode=t.Bytes(link_key), ), responses=[c.AppConfig.BDBAddInstallCode.Rsp(Status=t.Status.SUCCESS)], ) @@ -171,7 +172,7 @@ async def test_permit_join_with_key(device, permit_result, make_application, moc with contextlib.nullcontext() if permit_result is None else pytest.raises( asyncio.TimeoutError ): - await app.permit_with_key(node=ieee, code=code, time_s=1) + await app.permit_with_link_key(node=ieee, link_key=link_key, time_s=1) await bdb_add_install_code await join_enable_install_code @@ -183,20 +184,6 @@ async def test_permit_join_with_key(device, permit_result, make_application, moc await app.shutdown() -@pytest.mark.parametrize("device", FORMED_ZSTACK3_DEVICES) -async def test_permit_join_with_invalid_key(device, make_application): - app, znp_server = make_application(server_cls=device) - - # Consciot bulb - ieee = t.EUI64.convert("EC:1B:BD:FF:FE:54:4F:40") - code = bytes.fromhex("17D1856872570CEB7ACB53030C5D6DA368B1")[:-1] # truncate it - - with pytest.raises(ValueError): - await app.permit_with_key(node=ieee, code=code, time_s=1) - - await app.shutdown() - - @pytest.mark.parametrize("device", FORMED_DEVICES) async def test_on_zdo_device_join(device, make_application, mocker): app, znp_server = make_application(server_cls=device) diff --git a/tests/application/test_startup.py b/tests/application/test_startup.py index 828bae51..d3098ffc 100644 --- a/tests/application/test_startup.py +++ b/tests/application/test_startup.py @@ -22,7 +22,8 @@ DEV_NETWORK_SETTINGS = { FormedLaunchpadCC26X2R1: ( - "CC2652" f"Z-Stack {FormedLaunchpadCC26X2R1.code_revision}", + "CC2652", + f"Z-Stack {FormedLaunchpadCC26X2R1.code_revision}", 15, t.Channels.from_channel_list([15]), 0x4402, @@ -30,7 +31,8 @@ t.KeyData.convert("4C:4E:72:B8:41:22:51:79:9A:BF:35:25:12:88:CA:83"), ), FormedZStack3CC2531: ( - "CC2531" f"Z-Stack 3.0.x {FormedZStack3CC2531.code_revision}", + "CC2531", + f"Z-Stack 3.0.x {FormedZStack3CC2531.code_revision}", 15, t.Channels.from_channel_list([15]), 0xB6AB, @@ -38,7 +40,8 @@ t.KeyData.convert("6D:DE:24:EA:E2:85:52:B6:DE:29:56:EB:05:85:1A:FA"), ), FormedZStack1CC2531: ( - "CC2531" f"Z-Stack Home 1.2 {FormedZStack1CC2531.code_revision}", + "CC2531", + f"Z-Stack Home 1.2 {FormedZStack1CC2531.code_revision}", 11, t.Channels.from_channel_list([11]), 0x1A62, @@ -50,7 +53,7 @@ # These settings were extracted from beacon requests and key exchanges in Wireshark @pytest.mark.parametrize( - "device,model,channel,channels,pan_id,ext_pan_id,network_key", + "device,model,version,channel,channels,pan_id,ext_pan_id,network_key", [(device_cls,) + settings for device_cls, settings in DEV_NETWORK_SETTINGS.items()], ) async def test_info( diff --git a/tests/tools/test_network_backup_restore.py b/tests/tools/test_network_backup_restore.py index 4f870302..646265dd 100644 --- a/tests/tools/test_network_backup_restore.py +++ b/tests/tools/test_network_backup_restore.py @@ -158,7 +158,9 @@ async def test_network_backup_formed(device, make_znp_server, tmp_path): znp_server = make_znp_server(server_cls=device) # We verified these settings with Wireshark - _, channel, channels, pan_id, ext_pan_id, network_key = DEV_NETWORK_SETTINGS[device] + _, _, channel, channels, pan_id, ext_pan_id, network_key = DEV_NETWORK_SETTINGS[ + device + ] backup_file = tmp_path / "backup.json" await network_backup([znp_server._port_path, "-o", str(backup_file)]) diff --git a/zigpy_znp/config.py b/zigpy_znp/config.py index 840eadcd..09183ac4 100644 --- a/zigpy_znp/config.py +++ b/zigpy_znp/config.py @@ -7,6 +7,7 @@ CONF_DEVICE, CONF_NWK_KEY, CONFIG_SCHEMA, + SCHEMA_DEVICE, CONF_NWK_PAN_ID, CONF_DEVICE_PATH, CONF_NWK_CHANNEL, diff --git a/zigpy_znp/uart.py b/zigpy_znp/uart.py index ec81a070..ea6adbf5 100644 --- a/zigpy_znp/uart.py +++ b/zigpy_znp/uart.py @@ -4,6 +4,7 @@ import asyncio import logging +import zigpy.config import zigpy.serial import zigpy_znp.config as conf @@ -161,9 +162,9 @@ def __repr__(self) -> str: async def connect(config: conf.ConfigType, api) -> ZnpMtProtocol: loop = asyncio.get_running_loop() - port = config[conf.CONF_DEVICE_PATH] - baudrate = config[conf.CONF_DEVICE_BAUDRATE] - flow_control = config[conf.CONF_DEVICE_FLOW_CONTROL] + port = config[zigpy.config.CONF_DEVICE_PATH] + baudrate = config[zigpy.config.CONF_DEVICE_BAUDRATE] + flow_control = config[zigpy.config.CONF_DEVICE_FLOW_CONTROL] LOGGER.debug("Connecting to %s at %s baud", port, baudrate) diff --git a/zigpy_znp/zigbee/application.py b/zigpy_znp/zigbee/application.py index 0d3cd60c..214e8241 100644 --- a/zigpy_znp/zigbee/application.py +++ b/zigpy_znp/zigbee/application.py @@ -3,7 +3,6 @@ import os import asyncio import logging -import itertools import zigpy.zcl import zigpy.zdo @@ -77,19 +76,12 @@ class RetryMethod(t.bitmap8): class ControllerApplication(zigpy.application.ControllerApplication): SCHEMA = conf.CONFIG_SCHEMA - SCHEMA_DEVICE = conf.SCHEMA_DEVICE def __init__(self, config: conf.ConfigType): super().__init__(config=conf.CONFIG_SCHEMA(config)) self._znp: ZNP | None = None - - # It's simpler to work with Task objects if they're never actually None - self._reconnect_task: asyncio.Future = asyncio.Future() - self._reconnect_task.cancel() - self._version_rsp = None - self._join_announce_tasks: dict[t.EUI64, asyncio.TimerHandle] = {} ################################################################## @@ -690,40 +682,6 @@ async def _write_stack_settings(self) -> bool: return any_changed - async def _reconnect(self) -> None: - """ - Endlessly tries to reconnect to the currently configured radio. - - Relies on the fact that `self.startup()` only modifies `self` upon a successful - connection to be essentially stateless. - """ - - for attempt in itertools.count(start=1): - LOGGER.debug( - "Trying to reconnect to %s, attempt %d", - self._config[conf.CONF_DEVICE][conf.CONF_DEVICE_PATH], - attempt, - ) - - try: - await self.connect() - await self.initialize() - return - except asyncio.CancelledError: - raise - except Exception as e: - LOGGER.error("Failed to reconnect", exc_info=e) - - if self._znp is not None: - self._znp.close() - self._znp = None - - await asyncio.sleep( - self._config[conf.CONF_ZNP_CONFIG][ - conf.CONF_AUTO_RECONNECT_RETRY_DELAY - ] - ) - def _find_endpoint(self, dst_ep: int, profile: int, cluster: int) -> int: """ Zigpy defaults to sending messages with src_ep == dst_ep. This does not work From 3a05f2e7c8934f0f6ee3a051e9de1af2c8d3defe Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 16 Nov 2023 10:58:13 -0500 Subject: [PATCH 9/9] Add a unit test for watchdog feeding --- tests/application/test_connect.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/application/test_connect.py b/tests/application/test_connect.py index ac2430f3..79fa9104 100644 --- a/tests/application/test_connect.py +++ b/tests/application/test_connect.py @@ -1,4 +1,5 @@ -from unittest.mock import patch +import asyncio +from unittest.mock import AsyncMock, patch import pytest @@ -197,3 +198,18 @@ async def test_disconnect_failure(device, make_application): await app.disconnect() assert app._znp is None + + +@pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1]) +async def test_watchdog(device, make_application): + app, znp_server = make_application(server_cls=device) + await app.startup(auto_form=False) + + app._watchdog_feed = AsyncMock(wraps=app._watchdog_feed) + + with patch("zigpy.application.ControllerApplication._watchdog_period", new=0.1): + await asyncio.sleep(0.6) + + assert len(app._watchdog_feed.mock_calls) >= 5 + + await app.shutdown()