From d9725311893343a93fd5f8ad6d299f347e60cb9c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 13 Nov 2021 17:55:17 -0500 Subject: [PATCH 01/27] Implement new core methods --- zigpy_deconz/api.py | 7 + zigpy_deconz/zigbee/application.py | 274 +++++++++++++++++------------ 2 files changed, 173 insertions(+), 108 deletions(-) diff --git a/zigpy_deconz/api.py b/zigpy_deconz/api.py index 756f4bf..59ea902 100644 --- a/zigpy_deconz/api.py +++ b/zigpy_deconz/api.py @@ -63,6 +63,13 @@ class NetworkState(t.uint8_t, enum.Enum): LEAVING = 3 +class SecurityMode(t.uint8_t, enum.Enum): + NO_SECURITY = 0x00 + PRECONFIGURED_NETWORK_KEY = 0x01 + NETWORK_KEY_FROM_TC = 0x02 + ONLY_TCLK = 0x03 + + class Command(t.uint8_t, enum.Enum): aps_data_confirm = 0x04 device_state = 0x07 diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 2502f50..fa3da62 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -1,23 +1,33 @@ """ControllerApplication for deCONZ protocol based adapters.""" +from __future__ import annotations + import asyncio import binascii import logging import re -from typing import Any, Dict +from typing import Any import zigpy.application import zigpy.config import zigpy.device import zigpy.endpoint import zigpy.exceptions +from zigpy.exceptions import FormationFailure, NetworkNotFormed import zigpy.neighbor import zigpy.state import zigpy.types import zigpy.util +import zigpy.zdo.types as zdo_t from zigpy_deconz import types as t -from zigpy_deconz.api import Deconz, NetworkParameter, NetworkState, Status +from zigpy_deconz.api import ( + Deconz, + NetworkParameter, + NetworkState, + SecurityMode, + Status, +) from zigpy_deconz.config import CONF_WATCHDOG_TTL, CONFIG_SCHEMA, SCHEMA_DEVICE import zigpy_deconz.exception @@ -35,9 +45,7 @@ class ControllerApplication(zigpy.application.ControllerApplication): SCHEMA = CONFIG_SCHEMA SCHEMA_DEVICE = SCHEMA_DEVICE - probe = Deconz.probe - - def __init__(self, config: Dict[str, Any]): + def __init__(self, config: dict[str, Any]): """Initialize instance.""" super().__init__(config=zigpy.config.ZIGPY_SCHEMA(config)) @@ -56,148 +64,198 @@ async def _reset_watchdog(self): LOGGER.warning("No watchdog response") await asyncio.sleep(self._config[CONF_WATCHDOG_TTL] * 0.75) - async def shutdown(self): - """Shutdown application.""" - self._api.close() + async def connect(self): + api = Deconz(self, self._config[zigpy.config.CONF_DEVICE]) + await api.connect() + self.version = await api.version() + self._api = api - async def startup(self, auto_form=False): - """Perform a complete application startup.""" - self._api = Deconz(self, self._config[zigpy.config.CONF_DEVICE]) - await self._api.connect() - self.version = await self._api.version() - await self._api.device_state() - (ieee,) = await self._api[NetworkParameter.mac_address] - self.state.node_information.ieee = zigpy.types.EUI64(ieee) + async def disconnect(self): + try: + if self._api.protocol_version >= PROTO_VER_WATCHDOG: + await self._api.write_parameter(NetworkParameter.watchdog_ttl, 0) + finally: + self._api.close() + async def permit_with_key(self, node: t.EUI64, code: bytes, time_s=60): + raise NotImplementedError() + + async def start_network(self): if self._api.protocol_version >= PROTO_VER_WATCHDOG: asyncio.ensure_future(self._reset_watchdog()) - (designed_coord,) = await self._api[NetworkParameter.aps_designed_coordinator] - device_state, _, _ = await self._api.device_state() - should_form = ( - device_state.network_state != NetworkState.CONNECTED or designed_coord != 1 + coordinator = await DeconzDevice.new( + self, + self.ieee, + self.nwk, + self.version, + self._config[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH], ) - if auto_form and should_form: - await self.form_network() - (self.state.node_information.nwk,) = await self._api[ - NetworkParameter.nwk_address - ] - (self.state.network_information.pan_id,) = await self._api[ - NetworkParameter.nwk_panid - ] - (self.state.network_information.extended_pan_id,) = await self._api[ + coordinator.neighbors.add_context_listener(self._dblistener) + self.devices[self.ieee] = coordinator + if self._api.protocol_version >= PROTO_VER_NEIGBOURS: + await self.restore_neighbours() + asyncio.create_task(self._delayed_neighbour_scan()) + + async def write_network_info(self, *, network_info, node_info): + await self._api.change_network_state(NetworkState.OFFLINE) + + if node_info.logical_type == zdo_t.LogicalType.Coordinator: + await self._api.write_parameter( + NetworkParameter.aps_designed_coordinator, 1 + ) + else: + await self._api.write_parameter( + NetworkParameter.aps_designed_coordinator, 0 + ) + + await self._api.write_parameter(NetworkParameter.nwk_address, node_info.nwk) + await self._api.write_parameter(NetworkParameter.mac_address, node_info.ieee) + + # No way to specify both a mask and the logical channel + logical_channel_mask = zigpy.types.Channels.from_channel_list( + [network_info.channel] + ) + + if logical_channel_mask != network_info.channel_mask: + LOGGER.warning( + "Channel mask %s will be replaced with current logical channel %s", + network_info.channel_mask, + logical_channel_mask, + ) + + await self._api.write_parameter( + NetworkParameter.channel_mask, logical_channel_mask + ) + await self._api.write_parameter(NetworkParameter.nwk_panid, network_info.pan_id) + await self._api.write_parameter( + NetworkParameter.aps_extended_panid, network_info.extended_pan_id + ) + await self._api.write_parameter( + NetworkParameter.nwk_update_id, network_info.nwk_update_id + ) + + await self._api.write_parameter( + NetworkParameter.network_key, 0, network_info.network_key.key + ) + + try: + await self._api.write_parameter( + NetworkParameter.nwk_frame_counter, network_info.network_key.tx_counter + ) + except zigpy_deconz.exception.CommandError as ex: + assert ex.status == Status.UNSUPPORTED + LOGGER.warning( + "Writing network frame counter is not supported with this firmware" + ) + + if network_info.tc_link_key is not None: + await self._api.write_parameter( + NetworkParameter.trust_center_address, + network_info.tc_link_key.partner_ieee, + ) + await self._api.write_parameter( + NetworkParameter.link_key, + network_info.tc_link_key.partner_ieee, + network_info.tc_link_key.key, + ) + + if self.state.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.PRECONFIGURED_NETWORK_KEY + ) + + # bring network up + await self._api.change_network_state(NetworkState.CONNECTED) + + for _ in range(10): + (state, _, _) = await self._api.device_state() + if state.network_state == NetworkState.CONNECTED: + break + await asyncio.sleep(CHANGE_NETWORK_WAIT) + else: + raise FormationFailure("Could not form network.") + + async def load_network_info(self, *, load_devices=False): + device_state, _, _ = await self._api.device_state() + + if device_state.network_state != NetworkState.CONNECTED: + raise NetworkNotFormed() + + (ieee,) = await self._api[NetworkParameter.mac_address] + self.state.node_info.ieee = zigpy.types.EUI64(ieee) + + (designed_coord,) = await self._api[NetworkParameter.aps_designed_coordinator] + + if designed_coord == 0x01: + self.state.node_info.logical_type = zdo_t.LogicalType.Coordinator + else: + self.state.node_info.logical_type = zdo_t.LogicalType.Router + + (self.state.node_info.nwk,) = await self._api[NetworkParameter.nwk_address] + + (self.state.network_info.pan_id,) = await self._api[NetworkParameter.nwk_panid] + (self.state.network_info.extended_pan_id,) = await self._api[ NetworkParameter.nwk_extended_panid ] - (self.state.network_information.channel_mask,) = await self._api[ + (self.state.network_info.channel_mask,) = await self._api[ NetworkParameter.channel_mask ] await self._api[NetworkParameter.aps_extended_panid] - if self.state.network_information.network_key is None: - self.state.network_information.network_key = zigpy.state.Key() + if self.state.network_info.network_key is None: + self.state.network_info.network_key = zigpy.state.Key() ( _, - self.state.network_information.network_key.key, + self.state.network_info.network_key.key, ) = await self._api.read_parameter(NetworkParameter.network_key, 0) - self.state.network_information.network_key.seq = 0 - self.state.network_information.network_key.rx_counter = None - self.state.network_information.network_key.partner_ieee = None + self.state.network_info.network_key.seq = 0 + self.state.network_info.network_key.rx_counter = None + self.state.network_info.network_key.partner_ieee = None try: - (self.state.network_information.network_key.tx_counter,) = await self._api[ + (self.state.network_info.network_key.tx_counter,) = await self._api[ NetworkParameter.nwk_frame_counter ] except zigpy_deconz.exception.CommandError as ex: assert ex.status == Status.UNSUPPORTED - self.state.network_information.network_key.tx_counter = None + self.state.network_info.network_key.tx_counter = None - if self.state.network_information.tc_link_key is None: - self.state.network_information.tc_link_key = zigpy.state.Key() + if self.state.network_info.tc_link_key is None: + self.state.network_info.tc_link_key = zigpy.state.Key() - (self.state.network_information.tc_link_key.partner_ieee,) = await self._api[ + (self.state.network_info.tc_link_key.partner_ieee,) = await self._api[ NetworkParameter.trust_center_address ] - ( - _, - self.state.network_information.tc_link_key.key, - ) = await self._api.read_parameter( + (_, self.state.network_info.tc_link_key.key,) = await self._api.read_parameter( NetworkParameter.link_key, - self.state.network_information.tc_link_key.partner_ieee, + self.state.network_info.tc_link_key.partner_ieee, ) - (self.state.network_information.security_level,) = await self._api[ - NetworkParameter.security_mode - ] - (self.state.network_information.channel,) = await self._api[ + (security_mode,) = await self._api[NetworkParameter.security_mode] + + if security_mode == SecurityMode.NO_SECURITY: + self.state.network_info.security_level = 0x00 + else: + self.state.network_info.security_level = 0x05 + + (self.state.network_info.channel,) = await self._api[ NetworkParameter.current_channel ] - await self._api[NetworkParameter.protocol_version] - (self.state.network_information.nwk_update_id,) = await self._api[ + (self.state.network_info.nwk_update_id,) = await self._api[ NetworkParameter.nwk_update_id ] - coordinator = await DeconzDevice.new( - self, - self.ieee, - self.nwk, - self.version, - self._config[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH], - ) - - coordinator.neighbors.add_context_listener(self._dblistener) - self.devices[self.ieee] = coordinator - if self._api.protocol_version >= PROTO_VER_NEIGBOURS: - await self.restore_neighbours() - asyncio.create_task(self._delayed_neighbour_scan()) - async def force_remove(self, dev): """Forcibly remove device from NCP.""" pass - async def form_network(self): - LOGGER.info("Forming network") - await self._api.change_network_state(NetworkState.OFFLINE) - await self._api.write_parameter(NetworkParameter.aps_designed_coordinator, 1) - - nwk_config = self.config[zigpy.config.CONF_NWK] - - # set channel - channel = nwk_config.get(zigpy.config.CONF_NWK_CHANNEL) - if channel is not None: - channel_mask = zigpy.types.Channels.from_channel_list([channel]) - else: - channel_mask = nwk_config[zigpy.config.CONF_NWK_CHANNELS] - await self._api.write_parameter(NetworkParameter.channel_mask, channel_mask) - - pan_id = nwk_config[zigpy.config.CONF_NWK_PAN_ID] - if pan_id is not None: - await self._api.write_parameter(NetworkParameter.nwk_panid, pan_id) - - ext_pan_id = nwk_config[zigpy.config.CONF_NWK_EXTENDED_PAN_ID] - if ext_pan_id is not None: - await self._api.write_parameter( - NetworkParameter.aps_extended_panid, ext_pan_id - ) - - nwk_update_id = nwk_config[zigpy.config.CONF_NWK_UPDATE_ID] - await self._api.write_parameter(NetworkParameter.nwk_update_id, nwk_update_id) - - nwk_key = nwk_config[zigpy.config.CONF_NWK_KEY] - if nwk_key is not None: - await self._api.write_parameter(NetworkParameter.network_key, 0, nwk_key) - - # bring network up - await self._api.change_network_state(NetworkState.CONNECTED) - - for _ in range(10): - (state, _, _) = await self._api.device_state() - if state.network_state == NetworkState.CONNECTED: - return - await asyncio.sleep(CHANGE_NETWORK_WAIT) - raise Exception("Could not form network.") - async def mrequest( self, group_id, From 2effc402c5d88e07c8d0e812d0dbd4b2e8700764 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 13 Nov 2021 18:07:53 -0500 Subject: [PATCH 02/27] Start the watchdog task on connect --- zigpy_deconz/zigbee/application.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index fa3da62..4fb352f 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -53,6 +53,7 @@ def __init__(self, config: dict[str, Any]): self._pending = zigpy.util.Requests() self._nwk = 0 self.version = 0 + self._reset_watchdog_task = None async def _reset_watchdog(self): while True: @@ -70,7 +71,13 @@ async def connect(self): self.version = await api.version() self._api = api + if self._api.protocol_version >= PROTO_VER_WATCHDOG: + self._reset_watchdog_task = asyncio.create_task(self._reset_watchdog()) + async def disconnect(self): + if self._reset_watchdog_task is not None: + self._reset_watchdog_task.cancel() + try: if self._api.protocol_version >= PROTO_VER_WATCHDOG: await self._api.write_parameter(NetworkParameter.watchdog_ttl, 0) @@ -81,8 +88,6 @@ async def permit_with_key(self, node: t.EUI64, code: bytes, time_s=60): raise NotImplementedError() async def start_network(self): - if self._api.protocol_version >= PROTO_VER_WATCHDOG: - asyncio.ensure_future(self._reset_watchdog()) coordinator = await DeconzDevice.new( self, From 3fe31b3a13cc935d15ca2c9b7078e34c6d2cca51 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 13 Nov 2021 18:08:27 -0500 Subject: [PATCH 03/27] Use `zigpy.state` attributes instead of old properties --- zigpy_deconz/zigbee/application.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 4fb352f..5db8cdd 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -88,17 +88,18 @@ async def permit_with_key(self, node: t.EUI64, code: bytes, time_s=60): raise NotImplementedError() async def start_network(self): + await self.load_network_info(load_devices=False) coordinator = await DeconzDevice.new( self, - self.ieee, - self.nwk, + self.state.node_info.ieee, + self.state.node_info.nwk, self.version, self._config[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH], ) coordinator.neighbors.add_context_listener(self._dblistener) - self.devices[self.ieee] = coordinator + self.devices[self.state.node_info.ieee] = coordinator if self._api.protocol_version >= PROTO_VER_NEIGBOURS: await self.restore_neighbours() asyncio.create_task(self._delayed_neighbour_scan()) From 713690d95e86ca19df11960c2b9b83cde0aba85f Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 24 Nov 2021 13:26:21 -0500 Subject: [PATCH 04/27] Use the channel mask if the logical channel is `None` --- zigpy_deconz/zigbee/application.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 5db8cdd..e6c2ca7 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -120,20 +120,21 @@ async def write_network_info(self, *, network_info, node_info): await self._api.write_parameter(NetworkParameter.mac_address, node_info.ieee) # No way to specify both a mask and the logical channel - logical_channel_mask = zigpy.types.Channels.from_channel_list( - [network_info.channel] - ) - - if logical_channel_mask != network_info.channel_mask: - LOGGER.warning( - "Channel mask %s will be replaced with current logical channel %s", - network_info.channel_mask, - logical_channel_mask, + if network_info.channel is not None: + channel_mask = zigpy.types.Channels.from_channel_list( + [network_info.channel] ) - await self._api.write_parameter( - NetworkParameter.channel_mask, logical_channel_mask - ) + if channel_mask != network_info.channel_mask: + LOGGER.warning( + "Channel mask %s will be replaced with current logical channel %s", + network_info.channel_mask, + channel_mask, + ) + else: + channel_mask = network_info.channel_mask + + await self._api.write_parameter(NetworkParameter.channel_mask, channel_mask) await self._api.write_parameter(NetworkParameter.nwk_panid, network_info.pan_id) await self._api.write_parameter( NetworkParameter.aps_extended_panid, network_info.extended_pan_id From f81a6fad06190da3f74fac7f43a73e26e69fc150 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 24 Nov 2021 13:26:53 -0500 Subject: [PATCH 05/27] Wait for the network state to change when it is written --- zigpy_deconz/zigbee/application.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index e6c2ca7..785f584 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -104,8 +104,19 @@ async def start_network(self): await self.restore_neighbours() asyncio.create_task(self._delayed_neighbour_scan()) + async def _wait_for_network_state(self, target_state: NetworkState): + while True: + (state, _, _) = await self._api.device_state() + if state.network_state == target_state: + break + await asyncio.sleep(CHANGE_NETWORK_WAIT) + async def write_network_info(self, *, network_info, node_info): await self._api.change_network_state(NetworkState.OFFLINE) + await asyncio.wait_for( + self._wait_for_network_state(NetworkState.OFFLINE), + timeout=10 * CHANGE_NETWORK_WAIT, + ) if node_info.logical_type == zdo_t.LogicalType.Coordinator: await self._api.write_parameter( @@ -180,13 +191,13 @@ async def write_network_info(self, *, network_info, node_info): # bring network up await self._api.change_network_state(NetworkState.CONNECTED) - for _ in range(10): - (state, _, _) = await self._api.device_state() - if state.network_state == NetworkState.CONNECTED: - break - await asyncio.sleep(CHANGE_NETWORK_WAIT) - else: - raise FormationFailure("Could not form network.") + try: + await asyncio.wait_for( + self._wait_for_network_state(NetworkState.CONNECTED), + timeout=10 * CHANGE_NETWORK_WAIT, + ) + except asyncio.TimeoutError as e: + raise FormationFailure() from e async def load_network_info(self, *, load_devices=False): device_state, _, _ = await self._api.device_state() From ed90a0ec1421280f40d88edefcf1d3fc145e575d Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 24 Nov 2021 13:28:18 -0500 Subject: [PATCH 06/27] Shorten network and node info variable names --- zigpy_deconz/zigbee/application.py | 57 ++++++++++++++---------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 785f584..4ceaf98 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -205,70 +205,67 @@ async def load_network_info(self, *, load_devices=False): if device_state.network_state != NetworkState.CONNECTED: raise NetworkNotFormed() + network_info = self.state.network_info + node_info = self.state.node_info + (ieee,) = await self._api[NetworkParameter.mac_address] - self.state.node_info.ieee = zigpy.types.EUI64(ieee) + node_info.ieee = zigpy.types.EUI64(ieee) (designed_coord,) = await self._api[NetworkParameter.aps_designed_coordinator] if designed_coord == 0x01: - self.state.node_info.logical_type = zdo_t.LogicalType.Coordinator + node_info.logical_type = zdo_t.LogicalType.Coordinator else: - self.state.node_info.logical_type = zdo_t.LogicalType.Router + node_info.logical_type = zdo_t.LogicalType.Router - (self.state.node_info.nwk,) = await self._api[NetworkParameter.nwk_address] + (node_info.nwk,) = await self._api[NetworkParameter.nwk_address] - (self.state.network_info.pan_id,) = await self._api[NetworkParameter.nwk_panid] - (self.state.network_info.extended_pan_id,) = await self._api[ + (network_info.pan_id,) = await self._api[NetworkParameter.nwk_panid] + (network_info.extended_pan_id,) = await self._api[ NetworkParameter.nwk_extended_panid ] - (self.state.network_info.channel_mask,) = await self._api[ - NetworkParameter.channel_mask - ] + (network_info.channel_mask,) = await self._api[NetworkParameter.channel_mask] await self._api[NetworkParameter.aps_extended_panid] - if self.state.network_info.network_key is None: - self.state.network_info.network_key = zigpy.state.Key() + if network_info.network_key is None: + network_info.network_key = zigpy.state.Key() ( _, - self.state.network_info.network_key.key, + network_info.network_key.key, ) = await self._api.read_parameter(NetworkParameter.network_key, 0) - self.state.network_info.network_key.seq = 0 - self.state.network_info.network_key.rx_counter = None - self.state.network_info.network_key.partner_ieee = None + network_info.network_key.seq = 0 + network_info.network_key.rx_counter = None + network_info.network_key.partner_ieee = None try: - (self.state.network_info.network_key.tx_counter,) = await self._api[ + (network_info.network_key.tx_counter,) = await self._api[ NetworkParameter.nwk_frame_counter ] except zigpy_deconz.exception.CommandError as ex: assert ex.status == Status.UNSUPPORTED - self.state.network_info.network_key.tx_counter = None + network_info.network_key.tx_counter = None - if self.state.network_info.tc_link_key is None: - self.state.network_info.tc_link_key = zigpy.state.Key() + if network_info.tc_link_key is None: + network_info.tc_link_key = zigpy.state.Key() - (self.state.network_info.tc_link_key.partner_ieee,) = await self._api[ + (network_info.tc_link_key.partner_ieee,) = await self._api[ NetworkParameter.trust_center_address ] - (_, self.state.network_info.tc_link_key.key,) = await self._api.read_parameter( + (_, network_info.tc_link_key.key,) = await self._api.read_parameter( NetworkParameter.link_key, - self.state.network_info.tc_link_key.partner_ieee, + network_info.tc_link_key.partner_ieee, ) (security_mode,) = await self._api[NetworkParameter.security_mode] if security_mode == SecurityMode.NO_SECURITY: - self.state.network_info.security_level = 0x00 + network_info.security_level = 0x00 else: - self.state.network_info.security_level = 0x05 + network_info.security_level = 0x05 - (self.state.network_info.channel,) = await self._api[ - NetworkParameter.current_channel - ] - (self.state.network_info.nwk_update_id,) = await self._api[ - NetworkParameter.nwk_update_id - ] + (network_info.channel,) = await self._api[NetworkParameter.current_channel] + (network_info.nwk_update_id,) = await self._api[NetworkParameter.nwk_update_id] async def force_remove(self, dev): """Forcibly remove device from NCP.""" From 3a184794dda38a5471429213ea802565179dfd39 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 25 Nov 2021 01:22:36 -0500 Subject: [PATCH 07/27] Fix references to old zigpy properties --- 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 4ceaf98..efffc53 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -462,7 +462,7 @@ def handle_tx_confirm(self, req_id, status): async def restore_neighbours(self) -> None: """Restore children.""" - coord = self.get_device(ieee=self.ieee) + coord = self.get_device(ieee=self.state.node_info.ieee) devices = (nei.device for nei in coord.neighbors) for device in devices: if device is None: @@ -494,7 +494,7 @@ async def restore_neighbours(self) -> None: async def _delayed_neighbour_scan(self) -> None: """Scan coordinator's neighbours.""" await asyncio.sleep(DELAY_NEIGHBOUR_SCAN_S) - coord = self.get_device(ieee=self.ieee) + coord = self.get_device(ieee=self.state.node_info.ieee) await coord.neighbors.scan() From ba4ea80d2debbc4ec181a3c3a6095c111aac4e54 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 28 Nov 2021 18:35:07 -0500 Subject: [PATCH 08/27] Implement new radio state schema --- zigpy_deconz/zigbee/application.py | 115 +++++++++++++++++++---------- 1 file changed, 76 insertions(+), 39 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index efffc53..6cedd42 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -90,6 +90,19 @@ async def permit_with_key(self, node: t.EUI64, code: bytes, time_s=60): async def start_network(self): await self.load_network_info(load_devices=False) + device_state, _, _ = await self._api.device_state() + + if device_state.network_state != NetworkState.CONNECTED: + await self._api.change_network_state(NetworkState.CONNECTED) + + try: + await asyncio.wait_for( + self._wait_for_network_state(NetworkState.CONNECTED), + timeout=10 * CHANGE_NETWORK_WAIT, + ) + except asyncio.TimeoutError as e: + raise FormationFailure() from e + coordinator = await DeconzDevice.new( self, self.state.node_info.ieee, @@ -112,12 +125,26 @@ async def _wait_for_network_state(self, target_state: NetworkState): await asyncio.sleep(CHANGE_NETWORK_WAIT) async def write_network_info(self, *, network_info, node_info): - await self._api.change_network_state(NetworkState.OFFLINE) + + # Note: Changed network configuration parameters become only affective after + # sending a Leave Network Request followed by a Create or Join Network Request + await self._api.change_network_state(NetworkState.CONNECTED) await asyncio.wait_for( - self._wait_for_network_state(NetworkState.OFFLINE), + self._wait_for_network_state(NetworkState.CONNECTED), timeout=10 * CHANGE_NETWORK_WAIT, ) + # TODO: this works maybe 1% of the time + try: + await self._api.write_parameter( + NetworkParameter.nwk_frame_counter, network_info.network_key.tx_counter + ) + except zigpy_deconz.exception.CommandError as ex: + assert ex.status == Status.UNSUPPORTED + LOGGER.warning( + "Writing network frame counter is not supported with this firmware" + ) + if node_info.logical_type == zdo_t.LogicalType.Coordinator: await self._api.write_parameter( NetworkParameter.aps_designed_coordinator, 1 @@ -146,6 +173,7 @@ async def write_network_info(self, *, network_info, node_info): channel_mask = network_info.channel_mask await self._api.write_parameter(NetworkParameter.channel_mask, channel_mask) + await self._api.write_parameter(NetworkParameter.use_predefined_nwk_panid, True) await self._api.write_parameter(NetworkParameter.nwk_panid, network_info.pan_id) await self._api.write_parameter( NetworkParameter.aps_extended_panid, network_info.extended_pan_id @@ -158,14 +186,10 @@ async def write_network_info(self, *, network_info, node_info): NetworkParameter.network_key, 0, network_info.network_key.key ) - try: - await self._api.write_parameter( - NetworkParameter.nwk_frame_counter, network_info.network_key.tx_counter - ) - except zigpy_deconz.exception.CommandError as ex: - assert ex.status == Status.UNSUPPORTED + if network_info.network_key.seq != 0: LOGGER.warning( - "Writing network frame counter is not supported with this firmware" + "Non-zero network key sequence number is not supported: %s", + network_info.network_key.seq, ) if network_info.tc_link_key is not None: @@ -185,32 +209,43 @@ async def write_network_info(self, *, network_info, node_info): ) else: await self._api.write_parameter( - NetworkParameter.security_mode, SecurityMode.PRECONFIGURED_NETWORK_KEY + NetworkParameter.security_mode, SecurityMode.ONLY_TCLK ) - # bring network up - await self._api.change_network_state(NetworkState.CONNECTED) + # Note: Changed network configuration parameters become only affective after + # sending a Leave Network Request followed by a Create or Join Network Request + await self._api.change_network_state(NetworkState.OFFLINE) + await asyncio.wait_for( + self._wait_for_network_state(NetworkState.OFFLINE), + timeout=10 * CHANGE_NETWORK_WAIT, + ) - try: - await asyncio.wait_for( - self._wait_for_network_state(NetworkState.CONNECTED), - timeout=10 * CHANGE_NETWORK_WAIT, - ) - except asyncio.TimeoutError as e: - raise FormationFailure() from e + await asyncio.sleep(1) - async def load_network_info(self, *, load_devices=False): - device_state, _, _ = await self._api.device_state() + await self._api.change_network_state(NetworkState.CONNECTED) + await asyncio.wait_for( + self._wait_for_network_state(NetworkState.CONNECTED), + timeout=10 * CHANGE_NETWORK_WAIT, + ) - if device_state.network_state != NetworkState.CONNECTED: - raise NetworkNotFormed() + await asyncio.sleep(1) + + await self._api.change_network_state(NetworkState.OFFLINE) + await asyncio.wait_for( + self._wait_for_network_state(NetworkState.OFFLINE), + timeout=10 * CHANGE_NETWORK_WAIT, + ) + async def load_network_info(self, *, load_devices=False): network_info = self.state.network_info node_info = self.state.node_info (ieee,) = await self._api[NetworkParameter.mac_address] node_info.ieee = zigpy.types.EUI64(ieee) + if node_info.ieee == zigpy.types.EUI64.convert("00:00:00:00:00:00:00:00"): + raise NetworkNotFormed(f"Node IEEE address is invalid: {node_info.ieee}") + (designed_coord,) = await self._api[NetworkParameter.aps_designed_coordinator] if designed_coord == 0x01: @@ -220,23 +255,30 @@ async def load_network_info(self, *, load_devices=False): (node_info.nwk,) = await self._api[NetworkParameter.nwk_address] + if designed_coord == 0x01 and node_info.nwk != 0x0000: + raise NetworkNotFormed( + f"Coordinator NWK is not 0x0000: 0x{node_info.nwk:04X}" + ) + (network_info.pan_id,) = await self._api[NetworkParameter.nwk_panid] (network_info.extended_pan_id,) = await self._api[ NetworkParameter.nwk_extended_panid ] + + (network_info.channel,) = await self._api[NetworkParameter.current_channel] (network_info.channel_mask,) = await self._api[NetworkParameter.channel_mask] - await self._api[NetworkParameter.aps_extended_panid] + (network_info.nwk_update_id,) = await self._api[NetworkParameter.nwk_update_id] + + if not 11 <= network_info.channel <= 26: + raise NetworkNotFormed(f"Invalid network channel: {network_info.channel}") - if network_info.network_key is None: - network_info.network_key = zigpy.state.Key() + await self._api[NetworkParameter.aps_extended_panid] + network_info.network_key = zigpy.state.Key() ( _, network_info.network_key.key, ) = await self._api.read_parameter(NetworkParameter.network_key, 0) - network_info.network_key.seq = 0 - network_info.network_key.rx_counter = None - network_info.network_key.partner_ieee = None try: (network_info.network_key.tx_counter,) = await self._api[ @@ -244,17 +286,15 @@ async def load_network_info(self, *, load_devices=False): ] except zigpy_deconz.exception.CommandError as ex: assert ex.status == Status.UNSUPPORTED - network_info.network_key.tx_counter = None - - if network_info.tc_link_key is None: - network_info.tc_link_key = zigpy.state.Key() - (network_info.tc_link_key.partner_ieee,) = await self._api[ + (network_info.tc_address,) = await self._api[ NetworkParameter.trust_center_address ] + + network_info.tc_link_key = zigpy.state.Key() (_, network_info.tc_link_key.key,) = await self._api.read_parameter( NetworkParameter.link_key, - network_info.tc_link_key.partner_ieee, + network_info.tc_address, ) (security_mode,) = await self._api[NetworkParameter.security_mode] @@ -264,9 +304,6 @@ async def load_network_info(self, *, load_devices=False): else: network_info.security_level = 0x05 - (network_info.channel,) = await self._api[NetworkParameter.current_channel] - (network_info.nwk_update_id,) = await self._api[NetworkParameter.nwk_update_id] - async def force_remove(self, dev): """Forcibly remove device from NCP.""" pass @@ -281,7 +318,7 @@ async def mrequest( data, *, hops=0, - non_member_radius=3 + non_member_radius=3, ): """Submit and send data out as a multicast transmission. From 4f52658f01c505fbcd1106506ddfe5a25b32edb1 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 15 Dec 2021 21:31:13 -0500 Subject: [PATCH 09/27] Correctly read link key --- zigpy_deconz/zigbee/application.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 6cedd42..4bb5b57 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -287,14 +287,14 @@ async def load_network_info(self, *, load_devices=False): except zigpy_deconz.exception.CommandError as ex: assert ex.status == Status.UNSUPPORTED - (network_info.tc_address,) = await self._api[ + network_info.tc_link_key = zigpy.state.Key() + (network_info.tc_link_key.partner_ieee,) = await self._api[ NetworkParameter.trust_center_address ] - network_info.tc_link_key = zigpy.state.Key() (_, network_info.tc_link_key.key,) = await self._api.read_parameter( NetworkParameter.link_key, - network_info.tc_address, + network_info.tc_link_key.partner_ieee, ) (security_mode,) = await self._api[NetworkParameter.security_mode] From c408d38a48c6f9cf70e94abbe00bcbe4a2319195 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 15 Dec 2021 21:50:55 -0500 Subject: [PATCH 10/27] Rewrite `_wait_for_network_state` into `_change_network_state` --- zigpy_deconz/zigbee/application.py | 67 ++++++++++++------------------ 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 4bb5b57..40ffa3a 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -71,9 +71,6 @@ async def connect(self): self.version = await api.version() self._api = api - if self._api.protocol_version >= PROTO_VER_WATCHDOG: - self._reset_watchdog_task = asyncio.create_task(self._reset_watchdog()) - async def disconnect(self): if self._reset_watchdog_task is not None: self._reset_watchdog_task.cancel() @@ -93,13 +90,8 @@ async def start_network(self): device_state, _, _ = await self._api.device_state() if device_state.network_state != NetworkState.CONNECTED: - await self._api.change_network_state(NetworkState.CONNECTED) - try: - await asyncio.wait_for( - self._wait_for_network_state(NetworkState.CONNECTED), - timeout=10 * CHANGE_NETWORK_WAIT, - ) + await self._change_network_state(NetworkState.CONNECTED) except asyncio.TimeoutError as e: raise FormationFailure() from e @@ -117,22 +109,33 @@ async def start_network(self): await self.restore_neighbours() asyncio.create_task(self._delayed_neighbour_scan()) - async def _wait_for_network_state(self, target_state: NetworkState): - while True: - (state, _, _) = await self._api.device_state() - if state.network_state == target_state: - break - await asyncio.sleep(CHANGE_NETWORK_WAIT) + async def _change_network_state( + self, target_state: NetworkState, *, timeout: int = 10 * CHANGE_NETWORK_WAIT + ): + async def change_loop(): + while True: + (state, _, _) = await self._api.device_state() + if state.network_state == target_state: + break + await asyncio.sleep(CHANGE_NETWORK_WAIT) - async def write_network_info(self, *, network_info, node_info): + await self._api.change_network_state(target_state) + await asyncio.wait_for(change_loop(), timeout=timeout) + + if self._api.protocol_version < PROTO_VER_WATCHDOG: + 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 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._api.change_network_state(NetworkState.CONNECTED) - await asyncio.wait_for( - self._wait_for_network_state(NetworkState.CONNECTED), - timeout=10 * CHANGE_NETWORK_WAIT, - ) + await self._change_network_state(NetworkState.OFFLINE) + await self._change_network_state(NetworkState.CONNECTED) # TODO: this works maybe 1% of the time try: @@ -214,27 +217,9 @@ 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._api.change_network_state(NetworkState.OFFLINE) - await asyncio.wait_for( - self._wait_for_network_state(NetworkState.OFFLINE), - timeout=10 * CHANGE_NETWORK_WAIT, - ) - + await self._change_network_state(NetworkState.OFFLINE) await asyncio.sleep(1) - - await self._api.change_network_state(NetworkState.CONNECTED) - await asyncio.wait_for( - self._wait_for_network_state(NetworkState.CONNECTED), - timeout=10 * CHANGE_NETWORK_WAIT, - ) - - await asyncio.sleep(1) - - await self._api.change_network_state(NetworkState.OFFLINE) - await asyncio.wait_for( - self._wait_for_network_state(NetworkState.OFFLINE), - timeout=10 * CHANGE_NETWORK_WAIT, - ) + await self._change_network_state(NetworkState.CONNECTED) async def load_network_info(self, *, load_devices=False): network_info = self.state.network_info From dc0cd978525b396281547adc16d7e8ddc722201a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 15 Dec 2021 22:19:49 -0500 Subject: [PATCH 11/27] Support more network state corner cases --- zigpy_deconz/zigbee/application.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 40ffa3a..2d5cb70 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -132,12 +132,6 @@ async def change_loop(): self._reset_watchdog_task = asyncio.create_task(self._reset_watchdog()) 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 self._change_network_state(NetworkState.CONNECTED) - - # TODO: this works maybe 1% of the time try: await self._api.write_parameter( NetworkParameter.nwk_frame_counter, network_info.network_key.tx_counter @@ -218,7 +212,6 @@ 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(1) await self._change_network_state(NetworkState.CONNECTED) async def load_network_info(self, *, load_devices=False): @@ -227,10 +220,6 @@ async def load_network_info(self, *, load_devices=False): (ieee,) = await self._api[NetworkParameter.mac_address] node_info.ieee = zigpy.types.EUI64(ieee) - - if node_info.ieee == zigpy.types.EUI64.convert("00:00:00:00:00:00:00:00"): - raise NetworkNotFormed(f"Node IEEE address is invalid: {node_info.ieee}") - (designed_coord,) = await self._api[NetworkParameter.aps_designed_coordinator] if designed_coord == 0x01: @@ -247,17 +236,23 @@ async def load_network_info(self, *, load_devices=False): (network_info.pan_id,) = await self._api[NetworkParameter.nwk_panid] (network_info.extended_pan_id,) = await self._api[ - NetworkParameter.nwk_extended_panid + NetworkParameter.aps_extended_panid ] + if network_info.extended_pan_id == zigpy.types.EUI64.convert( + "00:00:00:00:00:00:00:00" + ): + (network_info.extended_pan_id,) = await self._api[ + NetworkParameter.nwk_extended_panid + ] + (network_info.channel,) = await self._api[NetworkParameter.current_channel] (network_info.channel_mask,) = await self._api[NetworkParameter.channel_mask] (network_info.nwk_update_id,) = await self._api[NetworkParameter.nwk_update_id] - if not 11 <= network_info.channel <= 26: - raise NetworkNotFormed(f"Invalid network channel: {network_info.channel}") - - await self._api[NetworkParameter.aps_extended_panid] + if network_info.channel == 0: + network_info.channel = None + LOGGER.warning("Network channel is not set") network_info.network_key = zigpy.state.Key() ( @@ -286,7 +281,10 @@ async def load_network_info(self, *, load_devices=False): if security_mode == SecurityMode.NO_SECURITY: network_info.security_level = 0x00 + elif security_mode == SecurityMode.ONLY_TCLK: + network_info.security_level = 0x05 else: + LOGGER.warning("Unsupported security mode %r", security_mode) network_info.security_level = 0x05 async def force_remove(self, dev): From cf5cc8c272677b5ae373ab53f5c3ee978d5c1b89 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 26 Dec 2021 13:23:47 -0500 Subject: [PATCH 12/27] Do not disconnect when no serial connection was made --- zigpy_deconz/zigbee/application.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 2d5cb70..38c1c10 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -75,6 +75,9 @@ async def disconnect(self): if self._reset_watchdog_task is not None: self._reset_watchdog_task.cancel() + if self._api is None: + return + try: if self._api.protocol_version >= PROTO_VER_WATCHDOG: await self._api.write_parameter(NetworkParameter.watchdog_ttl, 0) From a13fe90bd3cf2f23461b5639152fb03890d0490c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 12 Mar 2022 14:54:59 -0500 Subject: [PATCH 13/27] Add unit tests --- setup.cfg | 3 + setup.py | 2 +- tests/test_api.py | 1 - tests/test_application.py | 198 ++++++++++---------- tests/test_network_state.py | 290 +++++++++++++++++++++++++++++ zigpy_deconz/api.py | 2 + zigpy_deconz/zigbee/application.py | 18 +- 7 files changed, 400 insertions(+), 114 deletions(-) create mode 100644 tests/test_network_state.py diff --git a/setup.cfg b/setup.cfg index 712ca4a..5550042 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,3 +20,6 @@ force_sort_within_sections = true known_first_party = zigpy_deconz,tests forced_separate = tests combine_as_imports = true + +[tool:pytest] +asyncio_mode = auto diff --git a/setup.py b/setup.py index f35e125..be063c2 100644 --- a/setup.py +++ b/setup.py @@ -22,5 +22,5 @@ license="GPL-3.0", packages=find_packages(exclude=["tests"]), install_requires=["pyserial-asyncio", "zigpy>=0.40.0"], - tests_require=["pytest", "pytest-asyncio", "asynctest"], + tests_require=["pytest", "pytest-asyncio>=0.17", "asynctest"], ) diff --git a/tests/test_api.py b/tests/test_api.py index 91c7d66..8e1c31f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -14,7 +14,6 @@ from .async_mock import AsyncMock, MagicMock, patch, sentinel -pytestmark = pytest.mark.asyncio DEVICE_CONFIG = {zigpy.config.CONF_DEVICE_PATH: "/dev/null"} diff --git a/tests/test_application.py b/tests/test_application.py index 84981cd..8840383 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -7,7 +7,7 @@ import zigpy.config import zigpy.device import zigpy.neighbor -from zigpy.types import EUI64, Channels +from zigpy.types import EUI64 import zigpy.zdo.types as zdo_t from zigpy_deconz import types as t @@ -17,7 +17,6 @@ from .async_mock import AsyncMock, MagicMock, patch, sentinel -pytestmark = pytest.mark.asyncio ZIGPY_NWK_CONFIG = { zigpy.config.CONF_NWK: { zigpy.config.CONF_NWK_PAN_ID: 0x4567, @@ -36,28 +35,44 @@ def device_path(): @pytest.fixture def api(): """Return API fixture.""" - api = MagicMock(spec_set=zigpy_deconz.api.Deconz) + api = MagicMock(spec_set=zigpy_deconz.api.Deconz(None, None)) api.device_state = AsyncMock( return_value=(deconz_api.DeviceState(deconz_api.NetworkState.CONNECTED), 0, 0) ) api.write_parameter = AsyncMock() - api.change_network_state = AsyncMock() + + # So the protocol version is effectively infinite + api._proto_ver.__ge__.return_value = True + api._proto_ver.__lt__.return_value = False + + api.protocol_version.__ge__.return_value = True + api.protocol_version.__lt__.return_value = False + return api @pytest.fixture -def app(device_path, api, database_file=None): +def app(device_path, api): config = application.ControllerApplication.SCHEMA( { **ZIGPY_NWK_CONFIG, zigpy.config.CONF_DEVICE: {zigpy.config.CONF_DEVICE_PATH: device_path}, - zigpy.config.CONF_DATABASE: database_file, } ) app = application.ControllerApplication(config) + + api.change_network_state = AsyncMock() + + device_state = MagicMock() + device_state.network_state.__eq__.return_value = True + api.device_state = AsyncMock(return_value=(device_state, 0, 0)) + + p1 = patch.object(app, "_api", api) p2 = patch.object(app, "_delayed_neighbour_scan") - with patch.object(app, "_api", api), p2: + p3 = patch.object(app, "_change_network_state", wraps=app._change_network_state) + + with p1, p2, p3: yield app @@ -205,106 +220,49 @@ def test_rx_unknown_device(app, addr_ieee, addr_nwk, caplog): assert app.handle_message.call_count == 0 -@patch.object(application, "CHANGE_NETWORK_WAIT", 0.001) -async def test_form_network(app, api): - """Test network forming.""" +@pytest.mark.parametrize( + "proto_ver, nwk_state, error", + [ + (0x0107, deconz_api.NetworkState.CONNECTED, None), + (0x0106, deconz_api.NetworkState.CONNECTED, None), + (0x0107, deconz_api.NetworkState.OFFLINE, None), + (0x0107, deconz_api.NetworkState.OFFLINE, asyncio.TimeoutError()), + ], +) +async def test_start_network(app, proto_ver, nwk_state, error): + app.load_network_info = AsyncMock() + app.restore_neighbours = AsyncMock() + app._change_network_state = AsyncMock(side_effect=error) - await app.form_network() - assert api.change_network_state.await_count == 2 - assert ( - api.change_network_state.call_args_list[0][0][0] - == deconz_api.NetworkState.OFFLINE - ) - assert ( - api.change_network_state.call_args_list[1][0][0] - == deconz_api.NetworkState.CONNECTED - ) - assert api.write_parameter.await_count >= 3 - assert ( - api.write_parameter.await_args_list[0][0][0] - == deconz_api.NetworkParameter.aps_designed_coordinator + app._api.device_state = AsyncMock( + return_value=(deconz_api.DeviceState(nwk_state), 0, 0) ) - assert api.write_parameter.await_args_list[0][0][1] == 1 + app._api._proto_ver = proto_ver + app._api.protocol_version = proto_ver - api.device_state.return_value = ( - deconz_api.DeviceState(deconz_api.NetworkState.JOINING), - 0, - 0, - ) - with pytest.raises(Exception): - await app.form_network() + if nwk_state != deconz_api.NetworkState.CONNECTED and error is not None: + with pytest.raises(zigpy.exceptions.FormationFailure): + await app.start_network() + return -@pytest.mark.parametrize( - "protocol_ver, watchdog_cc, nwk_state, designed_coord, form_count", - [ - (0x0107, False, deconz_api.NetworkState.CONNECTED, 1, 0), - (0x0108, True, deconz_api.NetworkState.CONNECTED, 1, 0), - (0x010B, True, deconz_api.NetworkState.CONNECTED, 1, 0), - (0x010B, True, deconz_api.NetworkState.CONNECTED, 0, 1), - (0x010B, True, deconz_api.NetworkState.OFFLINE, 1, 1), - (0x010B, True, deconz_api.NetworkState.OFFLINE, 0, 1), - ], -) -async def test_startup( - protocol_ver, watchdog_cc, app, nwk_state, designed_coord, form_count, version=0 -): - async def _version(): - app._api._proto_ver = protocol_ver - return [version] - - params = { - deconz_api.NetworkParameter.aps_designed_coordinator: [designed_coord], - deconz_api.NetworkParameter.nwk_address: [designed_coord], - deconz_api.NetworkParameter.protocol_version: [protocol_ver], - deconz_api.NetworkParameter.mac_address: [EUI64([0x01] * 8)], - deconz_api.NetworkParameter.nwk_address: [0x0000], - deconz_api.NetworkParameter.nwk_panid: [0x1234], - deconz_api.NetworkParameter.nwk_extended_panid: [EUI64([0x02] * 8)], - deconz_api.NetworkParameter.channel_mask: [Channels.CHANNEL_25], - deconz_api.NetworkParameter.aps_extended_panid: [EUI64([0x02] * 8)], - deconz_api.NetworkParameter.network_key: [0, t.Key([0x03] * 16)], - deconz_api.NetworkParameter.trust_center_address: [EUI64([0x04] * 8)], - deconz_api.NetworkParameter.link_key: [ - EUI64([0x04] * 8), - t.Key(b"ZigBeeAlliance09"), - ], - deconz_api.NetworkParameter.security_mode: [3], - deconz_api.NetworkParameter.current_channel: [25], - deconz_api.NetworkParameter.nwk_update_id: [0], - } + with patch.object(application.DeconzDevice, "initialize", AsyncMock()): + await app.start_network() + assert app.load_network_info.await_count == 1 - async def _read_param(param, *args): - try: - return params[param] - except KeyError: - raise zigpy_deconz.exception.CommandError( - deconz_api.Status.UNSUPPORTED, "Unsupported" + if nwk_state != deconz_api.NetworkState.CONNECTED: + assert app._change_network_state.await_count == 1 + assert ( + app._change_network_state.await_args_list[0][0][0] + == deconz_api.NetworkState.CONNECTED ) + else: + assert app._change_network_state.await_count == 0 - app._reset_watchdog = AsyncMock() - app.form_network = AsyncMock() - app._delayed_neighbour_scan = AsyncMock() - - app._api._command = AsyncMock() - api = deconz_api.Deconz(app, app._config[zigpy.config.CONF_DEVICE]) - api.connect = AsyncMock() - api._command = AsyncMock() - api.device_state = AsyncMock(return_value=(deconz_api.DeviceState(nwk_state), 0, 0)) - api.read_parameter = AsyncMock(side_effect=_read_param) - api.version = MagicMock(side_effect=_version) - api.write_parameter = AsyncMock() - - p2 = patch( - "zigpy_deconz.zigbee.application.DeconzDevice.new", - new=AsyncMock(return_value=zigpy.device.Device(app, sentinel.ieee, 0x0000)), - ) - with patch.object(application, "Deconz", return_value=api), p2: - await app.startup(auto_form=False) - assert app.form_network.call_count == 0 - assert app._reset_watchdog.call_count == watchdog_cc - await app.startup(auto_form=True) - assert app.form_network.call_count == form_count + if proto_ver >= application.PROTO_VER_NEIGBOURS: + assert app.restore_neighbours.await_count == 1 + else: + assert app.restore_neighbours.await_count == 0 async def test_permit(app, nwk): @@ -471,10 +429,48 @@ def _handle_reply(app, tsn): ) -async def test_shutdown(app): +async def test_connect(app): + def new_api(*args): + api = MagicMock() + api.connect = AsyncMock() + api.version = AsyncMock(return_value=sentinel.version) + + return api + + with patch.object(application, "Deconz", new=new_api): + app._api = None + await app.connect() + assert app._api is not None + + assert app._api.connect.await_count == 1 + assert app._api.version.await_count == 1 + assert app.version is sentinel.version + + +async def test_disconnect(app): + app._reset_watchdog_task = MagicMock() app._api.close = MagicMock() - await app.shutdown() + + await app.disconnect() assert app._api.close.call_count == 1 + assert app._reset_watchdog_task.cancel.call_count == 1 + + +async def test_disconnect_no_api(app): + app._api = None + await app.disconnect() + + +async def test_disconnect_close_error(app): + app._api.write_parameter = MagicMock( + side_effect=zigpy_deconz.exception.CommandError(1, "Error") + ) + 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") def test_rx_device_annce(app, addr_ieee, addr_nwk): diff --git a/tests/test_network_state.py b/tests/test_network_state.py new file mode 100644 index 0000000..9a368a9 --- /dev/null +++ b/tests/test_network_state.py @@ -0,0 +1,290 @@ +"""Test `load_network_info` and `write_network_info` methods.""" + +import pytest +from zigpy.exceptions import NetworkNotFormed +import zigpy.state as app_state +import zigpy.types as t +import zigpy.zdo.types as zdo_t + +import zigpy_deconz.api +import zigpy_deconz.exception +import zigpy_deconz.zigbee.application as application + +from tests.async_mock import AsyncMock, patch +from tests.test_application import api, app, device_path # noqa: F401 + + +def merge_objects(obj: object, update: dict) -> None: + for key, value in update.items(): + if "." not in key: + setattr(obj, key, value) + else: + subkey, rest = key.split(".", 1) + merge_objects(getattr(obj, subkey), {rest: value}) + + +@pytest.fixture +def node_info(): + return app_state.NodeInfo( + nwk=t.NWK(0x0000), + ieee=t.EUI64.convert("93:2C:A9:34:D9:D0:5D:12"), + logical_type=zdo_t.LogicalType.Coordinator, + ) + + +@pytest.fixture +def network_info(node_info): + return app_state.NetworkInfo( + extended_pan_id=t.ExtendedPanId.convert("0D:49:91:99:AE:CD:3C:35"), + pan_id=t.PanId(0x9BB0), + nwk_update_id=0x12, + nwk_manager_id=t.NWK(0x0000), + channel=t.uint8_t(15), + channel_mask=t.Channels.from_channel_list([15, 20, 25]), + security_level=t.uint8_t(5), + network_key=app_state.Key( + key=t.KeyData.convert("9A:79:D6:9A:DA:EC:45:C6:F2:EF:EB:AF:DA:A3:07:B6"), + seq=108, + tx_counter=39009277, + ), + tc_link_key=app_state.Key( + key=t.KeyData(b"ZigBeeAlliance09"), + partner_ieee=node_info.ieee, + tx_counter=8712428, + ), + key_table=[], + children=[], + nwk_addresses={}, + stack_specific={}, + ) + + +@patch.object(application, "CHANGE_NETWORK_WAIT", 0.001) +@pytest.mark.parametrize( + "channel_mask, channel, security_level, fw_supports_fc, logical_type", + [ + ( + t.Channels.from_channel_list([15]), + 15, + 0, + True, + zdo_t.LogicalType.Coordinator, + ), + ( + t.Channels.from_channel_list([15]), + 15, + 0, + False, + zdo_t.LogicalType.Coordinator, + ), + ( + t.Channels.from_channel_list([15, 20]), + 15, + 5, + True, + zdo_t.LogicalType.Coordinator, + ), + ( + t.Channels.from_channel_list([15, 20, 25]), + None, + 5, + True, + zdo_t.LogicalType.Router, + ), + (None, 15, 5, True, zdo_t.LogicalType.Coordinator), + ], +) +async def test_write_network_info( + app, # noqa: F811 + network_info, + node_info, + channel_mask, + channel, + security_level, + fw_supports_fc, + logical_type, +): + """Test that network info is correctly written.""" + + params = {} + + async def write_parameter(param, *args): + if ( + not fw_supports_fc + and param == zigpy_deconz.api.NetworkParameter.nwk_frame_counter + ): + raise zigpy_deconz.exception.CommandError( + status=zigpy_deconz.api.Status.UNSUPPORTED + ) + + params[param.name] = args + + app._api.write_parameter = AsyncMock(side_effect=write_parameter) + + network_info = network_info.replace( + channel=channel, + channel_mask=channel_mask, + security_level=security_level, + ) + + node_info = node_info.replace(logical_type=logical_type) + + await app.write_network_info( + network_info=network_info, + node_info=node_info, + ) + + params = { + call[0][0].name: call[0][1:] + for call in app._api.write_parameter.await_args_list + } + + assert params["nwk_frame_counter"] == (network_info.network_key.tx_counter,) + + if node_info.logical_type == zdo_t.LogicalType.Coordinator: + assert params["aps_designed_coordinator"] == (1,) + else: + assert params["aps_designed_coordinator"] == (0,) + + assert params["nwk_address"] == (node_info.nwk,) + assert params["mac_address"] == (node_info.ieee,) + + if channel is not None: + assert params["channel_mask"] == ( + t.Channels.from_channel_list([network_info.channel]), + ) + elif channel_mask is not None: + assert params["channel_mask"] == (network_info.channel_mask,) + else: + assert False + + assert params["use_predefined_nwk_panid"] == (True,) + assert params["nwk_panid"] == (network_info.pan_id,) + assert params["aps_extended_panid"] == (network_info.extended_pan_id,) + assert params["nwk_update_id"] == (network_info.nwk_update_id,) + assert params["network_key"] == (0, network_info.network_key.key) + assert params["trust_center_address"] == (node_info.ieee,) + assert params["link_key"] == (node_info.ieee, network_info.tc_link_key.key) + + if security_level == 0: + assert params["security_mode"] == (zigpy_deconz.api.SecurityMode.NO_SECURITY,) + else: + assert params["security_mode"] == (zigpy_deconz.api.SecurityMode.ONLY_TCLK,) + + +@patch.object(application, "CHANGE_NETWORK_WAIT", 0.001) +@pytest.mark.parametrize( + "error, param_overrides, nwk_state_changes, node_state_changes", + [ + (None, {}, {}, {}), + ( + None, + {("aps_designed_coordinator",): [0x00]}, + {}, + {"logical_type": zdo_t.LogicalType.Router}, + ), + ( + None, + { + ("aps_extended_panid",): [t.EUI64.convert("00:00:00:00:00:00:00:00")], + ("nwk_extended_panid",): [t.EUI64.convert("0D:49:91:99:AE:CD:3C:35")], + }, + {}, + {}, + ), + (NetworkNotFormed, {("current_channel",): [0]}, {}, {}), + ( + None, + { + ("nwk_frame_counter",): zigpy_deconz.exception.CommandError( + zigpy_deconz.api.Status.UNSUPPORTED + ) + }, + {"network_key.tx_counter": 0}, + {}, + ), + ( + None, + {("security_mode",): [zigpy_deconz.api.SecurityMode.NO_SECURITY]}, + {"security_level": 0}, + {}, + ), + ( + None, + { + ("security_mode",): [ + zigpy_deconz.api.SecurityMode.PRECONFIGURED_NETWORK_KEY + ] + }, + {"security_level": 5}, + {}, + ), + ], +) +async def test_load_network_info( + app, # noqa: F811 + network_info, + node_info, + error, + param_overrides, + nwk_state_changes, + node_state_changes, +): + """Test that network info is correctly read.""" + + params = { + ("nwk_frame_counter",): [network_info.network_key.tx_counter], + ("aps_designed_coordinator",): [1], + ("nwk_address",): [node_info.nwk], + ("mac_address",): [node_info.ieee], + ("current_channel",): [network_info.channel], + ("channel_mask",): [t.Channels.from_channel_list([network_info.channel])], + ("use_predefined_nwk_panid",): [True], + ("nwk_panid",): [network_info.pan_id], + ("aps_extended_panid",): [network_info.extended_pan_id], + ("nwk_update_id",): [network_info.nwk_update_id], + ("network_key", 0): [0, network_info.network_key.key], + ("trust_center_address",): [node_info.ieee], + ("link_key", node_info.ieee): [node_info.ieee, network_info.tc_link_key.key], + ("security_mode",): [zigpy_deconz.api.SecurityMode.ONLY_TCLK], + } + + params.update(param_overrides) + + async def read_param(param, *args): + try: + value = params[(param.name,) + args] + except KeyError: + raise zigpy_deconz.exception.CommandError( + zigpy_deconz.api.Status.UNSUPPORTED, f"Unsupported: {param!r} {args!r}" + ) + + if isinstance(value, Exception): + raise value + + return value + + app._api.__getitem__ = app._api.read_parameter = AsyncMock(side_effect=read_param) + + if error is not None: + with pytest.raises(error): + await app.load_network_info() + + return + + assert app.state.network_info != network_info + assert app.state.node_info != node_info + + await app.load_network_info() + + # Almost all of the info matches + network_info = network_info.replace( + channel_mask=t.Channels.from_channel_list([network_info.channel]), + network_key=network_info.network_key.replace(seq=0), + tc_link_key=network_info.tc_link_key.replace(tx_counter=0), + ) + merge_objects(network_info, nwk_state_changes) + + assert app.state.network_info == network_info + + assert app.state.node_info == node_info.replace(**node_state_changes) diff --git a/zigpy_deconz/api.py b/zigpy_deconz/api.py index 767528b..fbf263f 100644 --- a/zigpy_deconz/api.py +++ b/zigpy_deconz/api.py @@ -1,5 +1,7 @@ """deCONZ serial protocol API.""" +from __future__ import annotations + import asyncio import binascii import enum diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 2689bc2..08601b0 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -82,6 +82,8 @@ async def disconnect(self): try: if self._api.protocol_version >= PROTO_VER_WATCHDOG: await self._api.write_parameter(NetworkParameter.watchdog_ttl, 0) + except zigpy_deconz.exception.CommandError: + pass finally: self._api.close() @@ -158,13 +160,13 @@ async def write_network_info(self, *, network_info, node_info): await self._api.write_parameter(NetworkParameter.nwk_address, node_info.nwk) await self._api.write_parameter(NetworkParameter.mac_address, node_info.ieee) - # No way to specify both a mask and the logical channel + # There is no way to specify both a mask and the logical channel if network_info.channel is not None: channel_mask = zigpy.types.Channels.from_channel_list( [network_info.channel] ) - if channel_mask != network_info.channel_mask: + if network_info.channel_mask and channel_mask != network_info.channel_mask: LOGGER.warning( "Channel mask %s will be replaced with current logical channel %s", network_info.channel_mask, @@ -204,7 +206,7 @@ async def write_network_info(self, *, network_info, node_info): network_info.tc_link_key.key, ) - if self.state.network_info.security_level == 0x00: + if network_info.security_level == 0x00: await self._api.write_parameter( NetworkParameter.security_mode, SecurityMode.NO_SECURITY ) @@ -233,11 +235,6 @@ async def load_network_info(self, *, load_devices=False): (node_info.nwk,) = await self._api[NetworkParameter.nwk_address] - if designed_coord == 0x01 and node_info.nwk != 0x0000: - raise NetworkNotFormed( - f"Coordinator NWK is not 0x0000: 0x{node_info.nwk:04X}" - ) - (network_info.pan_id,) = await self._api[NetworkParameter.nwk_panid] (network_info.extended_pan_id,) = await self._api[ NetworkParameter.aps_extended_panid @@ -255,8 +252,7 @@ async def load_network_info(self, *, load_devices=False): (network_info.nwk_update_id,) = await self._api[NetworkParameter.nwk_update_id] if network_info.channel == 0: - network_info.channel = None - LOGGER.warning("Network channel is not set") + raise NetworkNotFormed("Network channel is zero") network_info.network_key = zigpy.state.Key() ( @@ -276,7 +272,7 @@ async def load_network_info(self, *, load_devices=False): NetworkParameter.trust_center_address ] - (_, network_info.tc_link_key.key,) = await self._api.read_parameter( + (_, network_info.tc_link_key.key) = await self._api.read_parameter( NetworkParameter.link_key, network_info.tc_link_key.partner_ieee, ) From 4b355d3395d0b6f00a1d4bc2fe2207e340586fc6 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 13 Mar 2022 17:12:02 -0400 Subject: [PATCH 14/27] Increase patch coverage to 100% --- tests/test_application.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/test_application.py b/tests/test_application.py index 8840383..58f9bd0 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -716,7 +716,7 @@ async def test_restore_neighbours(app): neighbours.neighbors.append(nei_5) coord.neighbors = neighbours - p2 = patch.object(app, "_api", spec_set=zigpy_deconz.api.Deconz) + p2 = patch.object(app, "_api", spec_set=zigpy_deconz.api.Deconz(None, None)) with patch.object(app, "get_device", return_value=coord), p2 as api_mock: api_mock.add_neighbour = AsyncMock() await app.restore_neighbours() @@ -742,3 +742,36 @@ async def test_delayed_scan(): with patch.object(app, "get_device", return_value=coord): await app._delayed_neighbour_scan() assert coord.neighbors.scan.await_count == 1 + + +@patch("zigpy_deconz.zigbee.application.CHANGE_NETWORK_WAIT", 0.001) +@pytest.mark.parametrize("support_watchdog", [False, True]) +async def test_change_network_state(app, support_watchdog): + app._reset_watchdog_task = MagicMock() + + app._api.device_state = AsyncMock( + side_effect=[ + (deconz_api.DeviceState(deconz_api.NetworkState.OFFLINE), 0, 0), + (deconz_api.DeviceState(deconz_api.NetworkState.JOINING), 0, 0), + (deconz_api.DeviceState(deconz_api.NetworkState.CONNECTED), 0, 0), + ] + ) + + if support_watchdog: + app._api._proto_ver = application.PROTO_VER_WATCHDOG + app._api.protocol_version = application.PROTO_VER_WATCHDOG + else: + app._api._proto_ver = 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() + + 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 From 9ac00de1ea7a46ad00a0c4f8cebf97734fb83621 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 15 Mar 2022 14:57:53 -0400 Subject: [PATCH 15/27] Allow the node IEEE address to not be written when forming a network --- zigpy_deconz/zigbee/application.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 08601b0..f175b3d 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -158,7 +158,13 @@ async def write_network_info(self, *, network_info, node_info): ) await self._api.write_parameter(NetworkParameter.nwk_address, node_info.nwk) - await self._api.write_parameter(NetworkParameter.mac_address, node_info.ieee) + + if node_info.ieee != zigpy.types.EUI64.UNKNOWN: + # TODO: is there a way to revert it back to the hardware default? Or is this + # information lost when the parameter is overwritten? + await self._api.write_parameter( + NetworkParameter.mac_address, node_info.ieee + ) # There is no way to specify both a mask and the logical channel if network_info.channel is not None: From 4b951107bb29799d3be3aa899fabc38960763c61 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 23 Mar 2022 10:37:03 -0400 Subject: [PATCH 16/27] Use consistent type annotations --- zigpy_deconz/api.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/zigpy_deconz/api.py b/zigpy_deconz/api.py index fbf263f..0907133 100644 --- a/zigpy_deconz/api.py +++ b/zigpy_deconz/api.py @@ -7,7 +7,7 @@ import enum import functools import logging -from typing import Any, Callable, Dict, Optional, Tuple +from typing import Any, Callable, Optional import serial from zigpy.config import CONF_DEVICE_PATH @@ -43,7 +43,7 @@ class DeviceState(enum.IntFlag): APSDE_DATA_REQUEST_SLOTS_AVAILABLE = 0x20 @classmethod - def deserialize(cls, data) -> Tuple["DeviceState", bytes]: + def deserialize(cls, data) -> tuple["DeviceState", bytes]: """Deserialize DevceState.""" state, data = t.uint8_t.deserialize(data) return cls(state), data @@ -227,7 +227,7 @@ class NetworkParameter(t.uint8_t, enum.Enum): class Deconz: """deCONZ API class.""" - def __init__(self, app: Callable, device_config: Dict[str, Any]): + def __init__(self, app: Callable, device_config: dict[str, Any]): """Init instance.""" self._app = app self._aps_data_ind_flags: int = 0x01 @@ -402,7 +402,7 @@ def _handle_change_network_state(self, data): LOGGER.debug("Change network state response: %s", NetworkState(data[0]).name) @classmethod - async def probe(cls, device_config: Dict[str, Any]) -> bool: + async def probe(cls, device_config: dict[str, Any]) -> bool: """Probe port for the device presence.""" api = cls(None, device_config) try: @@ -644,10 +644,10 @@ def _handle_device_state_value(self, state: DeviceState) -> None: LOGGER.debug("Data request queue full.") if DeviceState.APSDE_DATA_INDICATION in state and not self._data_indication: self._data_indication = True - asyncio.ensure_future(self._aps_data_indication()) + asyncio.create_task(self._aps_data_indication()) if DeviceState.APSDE_DATA_CONFIRM in state and not self._data_confirm: self._data_confirm = True - asyncio.ensure_future(self._aps_data_confirm()) + asyncio.create_task(self._aps_data_confirm()) def __getitem__(self, key): """Access parameters via getitem.""" @@ -655,4 +655,4 @@ def __getitem__(self, key): def __setitem__(self, key, value): """Set parameters via setitem.""" - return asyncio.ensure_future(self.write_parameter(key, value)) + return asyncio.create_task(self.write_parameter(key, value)) From 89f2d1a6117e34e4a81efd6310fb42ef69df5b1b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 28 Mar 2022 14:22:42 -0400 Subject: [PATCH 17/27] Replace `EUI64.UNKNOWN` with the device IEEE addr --- zigpy_deconz/zigbee/application.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index f175b3d..ef95bfd 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -165,6 +165,10 @@ async def write_network_info(self, *, network_info, node_info): await self._api.write_parameter( NetworkParameter.mac_address, node_info.ieee ) + node_ieee = node_info.ieee + else: + (ieee,) = await self._api[NetworkParameter.mac_address] + node_ieee = zigpy.types.EUI64(ieee) # There is no way to specify both a mask and the logical channel if network_info.channel is not None: @@ -201,16 +205,20 @@ async def write_network_info(self, *, network_info, node_info): network_info.network_key.seq, ) - if network_info.tc_link_key is not None: - await self._api.write_parameter( - NetworkParameter.trust_center_address, - network_info.tc_link_key.partner_ieee, - ) - await self._api.write_parameter( - NetworkParameter.link_key, - network_info.tc_link_key.partner_ieee, - network_info.tc_link_key.key, - ) + tc_link_key_partner_ieee = network_info.tc_link_key.partner_ieee + + if tc_link_key_partner_ieee == zigpy.types.EUI64.UNKNOWN: + tc_link_key_partner_ieee = node_ieee + + await self._api.write_parameter( + NetworkParameter.trust_center_address, + tc_link_key_partner_ieee, + ) + await self._api.write_parameter( + NetworkParameter.link_key, + tc_link_key_partner_ieee, + network_info.tc_link_key.key, + ) if network_info.security_level == 0x00: await self._api.write_parameter( From 99df53f0f1a6dff93b1e419e0552f07b926f87ce Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 19 Apr 2022 14:29:45 -0400 Subject: [PATCH 18/27] Implement `add_endpoint` --- tests/test_application.py | 76 ++++++++++++++++++++++++++++++ zigpy_deconz/api.py | 3 ++ zigpy_deconz/zigbee/application.py | 57 ++++++++++++++++++++++ 3 files changed, 136 insertions(+) diff --git a/tests/test_application.py b/tests/test_application.py index 52998dd..401b7fb 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -785,3 +785,79 @@ async def test_change_network_state(app, support_watchdog): else: assert cancel_mock.call_count == 0 assert app._reset_watchdog_task is old_watchdog_task + + +ENDPOINT = zdo_t.SimpleDescriptor( + endpoint=None, + profile=1, + device_type=2, + device_version=3, + input_clusters=[4], + output_clusters=[5], +) +INVALID_DESCRIPTOR = zdo_t.SimpleDescriptor( + endpoint=184, + profile=7329, + device_type=19200, + device_version=18, + input_clusters=[], + output_clusters=[], +) + + +@pytest.mark.parametrize( + "descriptor, slots, target_slot", + [ + # No defined endpoints + (ENDPOINT.replace(endpoint=1), {0: None, 1: None, 2: None}, 0), + # Target the first invalid endpoint ID + ( + ENDPOINT.replace(endpoint=184), + {0: None, 1: INVALID_DESCRIPTOR, 2: INVALID_DESCRIPTOR}, + 1, + ), + # Target the endpoint with the same ID + ( + ENDPOINT.replace(endpoint=1), + {0: None, 1: INVALID_DESCRIPTOR, 2: ENDPOINT.replace(endpoint=1)}, + 2, + ), + # No free endpoint slots, this is an error + ( + ENDPOINT.replace(endpoint=1), + { + 0: ENDPOINT.replace(endpoint=2), + 1: ENDPOINT.replace(endpoint=3), + 2: ENDPOINT.replace(endpoint=4), + }, + None, + ), + ], +) +async def test_add_endpoint(app, descriptor, slots, target_slot): + async def read_param(param_id, index): + assert param_id == deconz_api.NetworkParameter.configure_endpoint + assert 0 <= index <= 2 + + if slots[index] is None: + raise zigpy_deconz.exception.CommandError( + deconz_api.Status.UNSUPPORTED, "Unsupported" + ) + else: + return index, slots[index] + + app._api.read_parameter = AsyncMock(side_effect=read_param) + app._api.write_parameter = AsyncMock() + + if target_slot is None: + with pytest.raises(ValueError): + await app.add_endpoint(descriptor) + + app._api.write_parameter.assert_not_called() + + return + + await app.add_endpoint(descriptor) + app._api.write_parameter.assert_called_once_with( + deconz_api.NetworkParameter.configure_endpoint, target_slot, descriptor + ) diff --git a/zigpy_deconz/api.py b/zigpy_deconz/api.py index 0907133..acae611 100644 --- a/zigpy_deconz/api.py +++ b/zigpy_deconz/api.py @@ -13,6 +13,7 @@ from zigpy.config import CONF_DEVICE_PATH import zigpy.exceptions from zigpy.types import APSStatus, Bool, Channels +from zigpy.zdo.types import SimpleDescriptor from zigpy_deconz.exception import APIException, CommandError import zigpy_deconz.types as t @@ -189,6 +190,7 @@ class NetworkParameter(t.uint8_t, enum.Enum): aps_extended_panid = 0x0B trust_center_address = 0x0E security_mode = 0x10 + configure_endpoint = 0x13 use_predefined_nwk_panid = 0x15 network_key = 0x18 link_key = 0x19 @@ -216,6 +218,7 @@ class NetworkParameter(t.uint8_t, enum.Enum): NetworkParameter.link_key: (t.EUI64, t.Key), NetworkParameter.current_channel: (t.uint8_t,), NetworkParameter.permit_join: (t.uint8_t,), + NetworkParameter.configure_endpoint: (t.uint8_t, SimpleDescriptor), NetworkParameter.protocol_version: (t.uint16_t,), NetworkParameter.nwk_update_id: (t.uint8_t,), NetworkParameter.watchdog_ttl: (t.uint32_t,), diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 6bd7adb..25f5319 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -48,6 +48,7 @@ PROTO_VER_WATCHDOG = 0x0108 PROTO_VER_NEIGBOURS = 0x0107 WATCHDOG_TTL = 600 +MAX_NUM_ENDPOINTS = 3 # defined in firmware class ControllerApplication(zigpy.application.ControllerApplication): @@ -105,6 +106,7 @@ async def permit_with_key(self, node: t.EUI64, code: bytes, time_s=60): raise NotImplementedError() async def start_network(self): + await self.register_endpoints() await self.load_network_info(load_devices=False) device_state, _, _ = await self._api.device_state() @@ -319,6 +321,61 @@ async def force_remove(self, dev): """Forcibly remove device from NCP.""" pass + async def add_endpoint(self, descriptor: zdo_t.SimpleDescriptor) -> None: + """Register a new endpoint on the device, replacing any with conflicting IDs. + + Only three endpoints can be defined. + """ + + endpoints = {} + + # Read the current endpoints + for index in range(MAX_NUM_ENDPOINTS): + try: + _, current_descriptor = await self._api.read_parameter( + NetworkParameter.configure_endpoint, index + ) + except zigpy_deconz.exception.CommandError as ex: + assert ex.status == Status.UNSUPPORTED + current_descriptor = None + + endpoints[index] = current_descriptor + + LOGGER.debug("Got endpoint slots: %r", endpoints) + + # Keep track of the best endpoint descriptor to replace + target_index = None + + for index, current_descriptor in endpoints.items(): + if ( + current_descriptor is not None + and current_descriptor.endpoint == descriptor.endpoint + ): + # Prefer to replace the endpoint with the same ID + target_index = index + break + elif ( + # Otherwise, pick one with a missing simple descriptor + current_descriptor is None + or ( + # Or one with the "invalid" value + not current_descriptor.input_clusters + and not current_descriptor.output_clusters + and current_descriptor.device_type == 19200 + ) + ) and target_index is None: + # Pick the first free slot + target_index = index + + if target_index is None: + raise ValueError(f"No available endpoint slots exist: {endpoints!r}") + + LOGGER.debug("Writing %s to slot %r", descriptor, target_index) + + await self._api.write_parameter( + NetworkParameter.configure_endpoint, target_index, descriptor + ) + @contextlib.asynccontextmanager async def _limit_concurrency(self): """Async context manager to prevent devices from being overwhelmed by requests. From 030edfc44ace24916e0f0cac79e502dc4f15e705 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 20 Apr 2022 15:28:18 -0400 Subject: [PATCH 19/27] The invalid endpoint descriptor is only consistently missing cluster IDs --- tests/test_application.py | 14 +++++++++++--- zigpy_deconz/zigbee/application.py | 1 - 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/test_application.py b/tests/test_application.py index 401b7fb..83a103d 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -795,7 +795,7 @@ async def test_change_network_state(app, support_watchdog): input_clusters=[4], output_clusters=[5], ) -INVALID_DESCRIPTOR = zdo_t.SimpleDescriptor( +INVALID_DESCRIPTOR1 = zdo_t.SimpleDescriptor( endpoint=184, profile=7329, device_type=19200, @@ -803,6 +803,14 @@ async def test_change_network_state(app, support_watchdog): input_clusters=[], output_clusters=[], ) +INVALID_DESCRIPTOR2 = zdo_t.SimpleDescriptor( + endpoint=80, + profile=56832, + device_type=1, + device_version=1, + input_clusters=[], + output_clusters=[], +) @pytest.mark.parametrize( @@ -813,13 +821,13 @@ async def test_change_network_state(app, support_watchdog): # Target the first invalid endpoint ID ( ENDPOINT.replace(endpoint=184), - {0: None, 1: INVALID_DESCRIPTOR, 2: INVALID_DESCRIPTOR}, + {0: None, 1: INVALID_DESCRIPTOR1, 2: INVALID_DESCRIPTOR2}, 1, ), # Target the endpoint with the same ID ( ENDPOINT.replace(endpoint=1), - {0: None, 1: INVALID_DESCRIPTOR, 2: ENDPOINT.replace(endpoint=1)}, + {0: None, 1: INVALID_DESCRIPTOR1, 2: ENDPOINT.replace(endpoint=1)}, 2, ), # No free endpoint slots, this is an error diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 25f5319..3395047 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -361,7 +361,6 @@ async def add_endpoint(self, descriptor: zdo_t.SimpleDescriptor) -> None: # Or one with the "invalid" value not current_descriptor.input_clusters and not current_descriptor.output_clusters - and current_descriptor.device_type == 19200 ) ) and target_index is None: # Pick the first free slot From 6f7253df385e916aa42689d1a94e39664203767c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 22 Apr 2022 13:19:49 -0400 Subject: [PATCH 20/27] Only two endpoints seem to actually work --- tests/test_application.py | 83 ++++++++++++++++-------------- zigpy_deconz/zigbee/application.py | 32 +++++++----- 2 files changed, 64 insertions(+), 51 deletions(-) diff --git a/tests/test_application.py b/tests/test_application.py index 83a103d..9d863a7 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -795,57 +795,24 @@ async def test_change_network_state(app, support_watchdog): input_clusters=[4], output_clusters=[5], ) -INVALID_DESCRIPTOR1 = zdo_t.SimpleDescriptor( - endpoint=184, - profile=7329, - device_type=19200, - device_version=18, - input_clusters=[], - output_clusters=[], -) -INVALID_DESCRIPTOR2 = zdo_t.SimpleDescriptor( - endpoint=80, - profile=56832, - device_type=1, - device_version=1, - input_clusters=[], - output_clusters=[], -) @pytest.mark.parametrize( "descriptor, slots, target_slot", [ - # No defined endpoints - (ENDPOINT.replace(endpoint=1), {0: None, 1: None, 2: None}, 0), - # Target the first invalid endpoint ID - ( - ENDPOINT.replace(endpoint=184), - {0: None, 1: INVALID_DESCRIPTOR1, 2: INVALID_DESCRIPTOR2}, - 1, - ), - # Target the endpoint with the same ID + (ENDPOINT.replace(endpoint=1), {0: ENDPOINT.replace(endpoint=2), 1: None}, 1), + # Prefer the endpoint with the same ID ( ENDPOINT.replace(endpoint=1), - {0: None, 1: INVALID_DESCRIPTOR1, 2: ENDPOINT.replace(endpoint=1)}, - 2, - ), - # No free endpoint slots, this is an error - ( - ENDPOINT.replace(endpoint=1), - { - 0: ENDPOINT.replace(endpoint=2), - 1: ENDPOINT.replace(endpoint=3), - 2: ENDPOINT.replace(endpoint=4), - }, - None, + {0: ENDPOINT.replace(endpoint=1, profile=1234), 1: None}, + 0, ), ], ) async def test_add_endpoint(app, descriptor, slots, target_slot): async def read_param(param_id, index): assert param_id == deconz_api.NetworkParameter.configure_endpoint - assert 0 <= index <= 2 + assert index in (0x00, 0x01) if slots[index] is None: raise zigpy_deconz.exception.CommandError( @@ -869,3 +836,43 @@ async def read_param(param_id, index): app._api.write_parameter.assert_called_once_with( deconz_api.NetworkParameter.configure_endpoint, target_slot, descriptor ) + + +async def test_add_endpoint_no_free_space(app): + async def read_param(param_id, index): + assert param_id == deconz_api.NetworkParameter.configure_endpoint + assert index in (0x00, 0x01) + + raise zigpy_deconz.exception.CommandError( + deconz_api.Status.UNSUPPORTED, "Unsupported" + ) + + app._api.read_parameter = AsyncMock(side_effect=read_param) + app._api.write_parameter = AsyncMock() + app._written_endpoints.add(0x00) + app._written_endpoints.add(0x01) + + with pytest.raises(ValueError): + await app.add_endpoint(ENDPOINT.replace(endpoint=1)) + + app._api.write_parameter.assert_not_called() + + +async def test_add_endpoint_no_unnecessary_writes(app): + async def read_param(param_id, index): + assert param_id == deconz_api.NetworkParameter.configure_endpoint + assert index in (0x00, 0x01) + + return index, ENDPOINT.replace(endpoint=1) + + app._api.read_parameter = AsyncMock(side_effect=read_param) + app._api.write_parameter = AsyncMock() + + await app.add_endpoint(ENDPOINT.replace(endpoint=1)) + app._api.write_parameter.assert_not_called() + + # Writing another endpoint will cause a write + await app.add_endpoint(ENDPOINT.replace(endpoint=2)) + app._api.write_parameter.assert_called_once_with( + deconz_api.NetworkParameter.configure_endpoint, 1, ENDPOINT.replace(endpoint=2) + ) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 3395047..e39b1aa 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -48,7 +48,7 @@ PROTO_VER_WATCHDOG = 0x0108 PROTO_VER_NEIGBOURS = 0x0107 WATCHDOG_TTL = 600 -MAX_NUM_ENDPOINTS = 3 # defined in firmware +MAX_NUM_ENDPOINTS = 2 # defined in firmware class ControllerApplication(zigpy.application.ControllerApplication): @@ -71,6 +71,8 @@ def __init__(self, config: dict[str, Any]): self.version = 0 self._reset_watchdog_task = None + self._written_endpoints = set() + async def _reset_watchdog(self): while True: try: @@ -86,6 +88,7 @@ async def connect(self): await api.connect() self.version = await api.version() self._api = api + self._written_endpoints.clear() async def disconnect(self): if self._reset_watchdog_task is not None: @@ -343,28 +346,31 @@ async def add_endpoint(self, descriptor: zdo_t.SimpleDescriptor) -> None: LOGGER.debug("Got endpoint slots: %r", endpoints) + # Don't write endpoints unnecessarily + if descriptor in endpoints.values(): + LOGGER.debug("Endpoint already registered, skipping") + + # Pretend we wrote it + index = next(i for i, desc in endpoints.items() if desc == descriptor) + self._written_endpoints.add(index) + return + # Keep track of the best endpoint descriptor to replace target_index = None for index, current_descriptor in endpoints.items(): + # Ignore ones we've already written + if index in self._written_endpoints: + continue + + target_index = index + if ( current_descriptor is not None and current_descriptor.endpoint == descriptor.endpoint ): # Prefer to replace the endpoint with the same ID - target_index = index break - elif ( - # Otherwise, pick one with a missing simple descriptor - current_descriptor is None - or ( - # Or one with the "invalid" value - not current_descriptor.input_clusters - and not current_descriptor.output_clusters - ) - ) and target_index is None: - # Pick the first free slot - target_index = index if target_index is None: raise ValueError(f"No available endpoint slots exist: {endpoints!r}") From 448eb74772302513dc4ea8ec6b9703c3b33791a4 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 14 May 2022 20:21:28 -0400 Subject: [PATCH 21/27] Add types for `ZDPResponseHandling` --- zigpy_deconz/api.py | 7 ++++++- zigpy_deconz/types.py | 4 ++++ zigpy_deconz/uart.py | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/zigpy_deconz/api.py b/zigpy_deconz/api.py index acae611..462c0c4 100644 --- a/zigpy_deconz/api.py +++ b/zigpy_deconz/api.py @@ -73,6 +73,11 @@ class SecurityMode(t.uint8_t, enum.Enum): ONLY_TCLK = 0x03 +class ZDPResponseHandling(t.bitmap16): + NONE = 0x0000 + NodeDescRsp = 0x0001 + + class Command(t.uint8_t, enum.Enum): aps_data_confirm = 0x04 device_state = 0x07 @@ -223,7 +228,7 @@ class NetworkParameter(t.uint8_t, enum.Enum): NetworkParameter.nwk_update_id: (t.uint8_t,), NetworkParameter.watchdog_ttl: (t.uint32_t,), NetworkParameter.nwk_frame_counter: (t.uint32_t,), - NetworkParameter.app_zdp_response_handling: (t.uint16_t,), + NetworkParameter.app_zdp_response_handling: (ZDPResponseHandling,), } diff --git a/zigpy_deconz/types.py b/zigpy_deconz/types.py index 56feee6..0308d93 100644 --- a/zigpy_deconz/types.py +++ b/zigpy_deconz/types.py @@ -149,6 +149,10 @@ class bitmap8(bitmap_factory(uint8_t)): pass +class bitmap16(bitmap_factory(uint16_t)): + pass + + class DeconzSendDataFlags(bitmap8): NONE = 0x00 NODE_ID = 0x01 diff --git a/zigpy_deconz/uart.py b/zigpy_deconz/uart.py index d26dfdb..61ca6ec 100644 --- a/zigpy_deconz/uart.py +++ b/zigpy_deconz/uart.py @@ -91,7 +91,7 @@ def data_received(self, data): try: self._api.data_received(frame) except Exception as exc: - LOGGER.error("Unexpected error handling the frame: %s", exc) + LOGGER.error("Unexpected error handling the frame", exc_info=exc) def _unescape(self, data): ret = [] From 94363a0810229ef421223dcff85742e918f938b8 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 14 May 2022 20:26:06 -0400 Subject: [PATCH 22/27] Include more network info metadata --- zigpy_deconz/zigbee/application.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index e39b1aa..6f1e267 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -22,6 +22,7 @@ import zigpy.util import zigpy.zdo.types as zdo_t +import zigpy_deconz from zigpy_deconz import types as t from zigpy_deconz.api import ( Deconz, @@ -257,6 +258,13 @@ 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@{zigpy_deconz.__version__}" + network_info.metadata = { + "deconz": { + "version": self.version, + } + } + (ieee,) = await self._api[NetworkParameter.mac_address] node_info.ieee = zigpy.types.EUI64(ieee) (designed_coord,) = await self._api[NetworkParameter.aps_designed_coordinator] From e7e8f64c98dc201543bf3f84f855366de65e20f1 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 15 Jun 2022 16:56:13 -0400 Subject: [PATCH 23/27] Bump minimum required zigpy version to 0.47.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index be063c2..7cdb769 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,6 @@ author_email="schmidt.d@aon.at", license="GPL-3.0", packages=find_packages(exclude=["tests"]), - install_requires=["pyserial-asyncio", "zigpy>=0.40.0"], + install_requires=["pyserial-asyncio", "zigpy>=0.47.0"], tests_require=["pytest", "pytest-asyncio>=0.17", "asynctest"], ) From 08511f031f267d77913693e13640122957c008db Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 16 Jun 2022 14:46:58 -0400 Subject: [PATCH 24/27] Add new metadata to network state unit tests --- tests/test_network_state.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_network_state.py b/tests/test_network_state.py index 9a368a9..c4c2d6a 100644 --- a/tests/test_network_state.py +++ b/tests/test_network_state.py @@ -6,6 +6,7 @@ import zigpy.types as t import zigpy.zdo.types as zdo_t +import zigpy_deconz import zigpy_deconz.api import zigpy_deconz.exception import zigpy_deconz.zigbee.application as application @@ -56,6 +57,12 @@ def network_info(node_info): children=[], nwk_addresses={}, stack_specific={}, + source=f"zigpy-deconz@{zigpy_deconz.__version__}", + metadata={ + "deconz": { + "version": 0, + } + }, ) From 9f56abe2c60716a5a4dfad3f9d200c8aa36d9461 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 16 Jun 2022 14:47:13 -0400 Subject: [PATCH 25/27] Mock `add_endpoint` in startup unit test --- tests/test_application.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_application.py b/tests/test_application.py index 9d863a7..658d581 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -234,6 +234,7 @@ def test_rx_unknown_device(app, addr_ieee, addr_nwk, caplog): async def test_start_network(app, proto_ver, nwk_state, error): app.load_network_info = AsyncMock() app.restore_neighbours = AsyncMock() + app.add_endpoint = AsyncMock() app._change_network_state = AsyncMock(side_effect=error) app._api.device_state = AsyncMock( From ec3b56f23000f54a1481a7eeef11152c79a621f2 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 19 Jun 2022 16:51:30 -0400 Subject: [PATCH 26/27] Use `create_task` instead of `ensure_future` --- tests/test_application.py | 4 ++-- zigpy_deconz/api.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_application.py b/tests/test_application.py index 658d581..3129994 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -619,14 +619,14 @@ async def test_mrequest_send_aps_data_error(app): async def test_reset_watchdog(app): """Test watchdog.""" with patch.object(app._api, "write_parameter") as mock_api: - dog = asyncio.ensure_future(app._reset_watchdog()) + dog = asyncio.create_task(app._reset_watchdog()) await asyncio.sleep(0.3) dog.cancel() assert mock_api.call_count == 1 with patch.object(app._api, "write_parameter") as mock_api: mock_api.side_effect = zigpy_deconz.exception.CommandError - dog = asyncio.ensure_future(app._reset_watchdog()) + dog = asyncio.create_task(app._reset_watchdog()) await asyncio.sleep(0.3) dog.cancel() assert mock_api.call_count == 1 diff --git a/zigpy_deconz/api.py b/zigpy_deconz/api.py index 462c0c4..13cfbea 100644 --- a/zigpy_deconz/api.py +++ b/zigpy_deconz/api.py @@ -280,7 +280,7 @@ def connection_lost(self, exc: Exception) -> None: self._uart = None if self._conn_lost_task and not self._conn_lost_task.done(): self._conn_lost_task.cancel() - self._conn_lost_task = asyncio.ensure_future(self._connection_lost()) + self._conn_lost_task = asyncio.create_task(self._connection_lost()) async def _connection_lost(self) -> None: """Reconnect serial port.""" From 2945eac3babe41394c6d10b922ecbe6f3597588b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 19 Jun 2022 16:51:55 -0400 Subject: [PATCH 27/27] Increase `disconnect` watchdog TTL from 0s to 1s --- zigpy_deconz/zigbee/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 6f1e267..a8fbb5e 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -100,7 +100,7 @@ async def disconnect(self): try: if self._api.protocol_version >= PROTO_VER_WATCHDOG: - await self._api.write_parameter(NetworkParameter.watchdog_ttl, 0) + await self._api.write_parameter(NetworkParameter.watchdog_ttl, 1) except zigpy_deconz.exception.CommandError: pass finally: