From 2bcfa9a9f0eabe9b503a8931821761dabdb7c2dd Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 12 Dec 2021 17:30:18 -0500 Subject: [PATCH 01/49] [WIP] Implement new zigpy network state --- bellows/types/named.py | 22 ++++++ bellows/types/struct.py | 15 ---- bellows/zigbee/application.py | 142 +++++++++++++++++++++++++++------- bellows/zigbee/util.py | 20 +++++ 4 files changed, 154 insertions(+), 45 deletions(-) diff --git a/bellows/types/named.py b/bellows/types/named.py index 30ea9a9e..c08f46dc 100644 --- a/bellows/types/named.py +++ b/bellows/types/named.py @@ -603,6 +603,9 @@ class EmberStatus(basic.enum8): # An index was passed into the function that was larger than the valid # range. INDEX_OUT_OF_RANGE = 0xB1 + # The passed key data is not valid. A key of all zeros or all F's are reserved + # values and cannot be used. + KEY_INVALID = 0xB2 # There are no empty entries left in the table. TABLE_FULL = 0xB4 # The requested table entry has been erased and contains no valid data. @@ -1182,3 +1185,22 @@ class EmberSignature283k1Data(basic.fixed_list(72, basic.uint8_t)): class EmberMessageDigest(basic.fixed_list(16, basic.uint8_t)): """The calculated digest of a message""" + + +class EmberDistinguishedNodeId(basic.enum16): + """A distinguished network ID that will never be assigned to any node""" + + # This value is used when getting the remote node ID from the address or binding + # tables. It indicates that the address or binding table entry is currently in use + # and network address discovery is underway. + DISCOVERY_ACTIVE = 0xFFFC + + # This value is used when getting the remote node ID from the address or binding + # tables. It indicates that the address or binding table entry is currently in use + # but the node ID corresponding to the EUI64 in the table is currently unknown. + UNKNOWN = 0xFFFD + + # This value is used when setting or getting the remote node ID in the address table + # or getting the remote node ID from the binding table. It indicates that the + # address or binding table entry is not in use. + TABLE_ENTRY_UNUSED = 0xFFFF diff --git a/bellows/types/struct.py b/bellows/types/struct.py index d2251683..2f95d9e4 100644 --- a/bellows/types/struct.py +++ b/bellows/types/struct.py @@ -2,8 +2,6 @@ import inspect import typing -import zigpy.state as app_state - from . import basic, named NoneType = type(None) @@ -282,19 +280,6 @@ class EmberNetworkParameters(EzspStruct): # method. channels: named.Channels - @property - def zigpy_network_information(self) -> app_state.NetworkInformation: - """Convert to NetworkInformation.""" - r = app_state.NetworkInformation( - self.extendedPanId, - app_state.t.PanId(self.panId), - self.nwkUpdateId, - app_state.t.NWK(self.nwkManagerId), - self.radioChannel, - channel_mask=self.channels, - ) - return r - class EmberZigbeeNetwork(EzspStruct): # The parameters of a ZigBee network. diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 3e5f119d..86d593e1 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -9,9 +9,9 @@ import zigpy.application import zigpy.config import zigpy.device +from zigpy.exceptions import NetworkNotFormed from zigpy.quirks import CustomDevice, CustomEndpoint -import zigpy.state as app_state -from zigpy.types import Addressing, BroadcastAddress +from zigpy.types import Addressing, BroadcastAddress, KeyData import zigpy.util import zigpy.zdo.types as zdo_t @@ -127,13 +127,10 @@ async def cleanup_tc_link_key(self, ieee: t.EmberEUI64) -> None: status = await self._ezsp.eraseKeyTableEntry(index) LOGGER.debug("Cleaned up TC link key for %s device: %s", ieee, status) - async def startup(self, auto_form=False): - """Perform a complete application startup""" + async def connect(self): self._ezsp = await bellows.ezsp.EZSP.initialize(self.config) ezsp = self._ezsp - self._multicast = bellows.multicast.Multicast(ezsp) - status, count = await ezsp.getConfigurationValue( ezsp.types.EzspConfigId.CONFIG_APS_UNICAST_MESSAGE_COUNT ) @@ -153,35 +150,20 @@ async def startup(self, auto_form=False): except EzspError as exc: LOGGER.info("EZSP Radio does not support getMfgToken command: %s", str(exc)) + async def start_network(self): + ezsp = self._ezsp + v = await ezsp.networkInit() if v[0] != t.EmberStatus.SUCCESS: - if not auto_form: - raise ControllerError("Could not initialize network") - await self.form_network() + raise NetworkNotFormed() status, node_type, nwk_params = await ezsp.getNetworkParameters() assert status == t.EmberStatus.SUCCESS # TODO: Better check if node_type != t.EmberNodeType.COORDINATOR: - if not auto_form: - raise ControllerError("Network not configured as coordinator") - - LOGGER.info( - "Leaving current network as %s and forming new network", node_type.name - ) - (status,) = await self._ezsp.leaveNetwork() - assert status == t.EmberStatus.NETWORK_DOWN - await self.form_network() - status, node_type, nwk_params = await ezsp.getNetworkParameters() - assert status == t.EmberStatus.SUCCESS + raise NetworkNotFormed("Network not configured as coordinator") - LOGGER.info("Node type: %s, Network parameters: %s", node_type, nwk_params) - await ezsp.update_policies(self.config) - (nwk,) = await ezsp.getNodeId() - (ieee,) = await ezsp.getEui64() + await self.load_network_info(load_devices=False) - node_info = app_state.NodeInfo(nwk, ieee, node_type.zdo_logical_type) - self.state.node_information = node_info - self.state.network_information = nwk_params.zigpy_network_information for cnt_group in self.state.counters: cnt_group.reset() @@ -198,9 +180,109 @@ async def startup(self, auto_form=False): await self.multicast.startup(self.get_device(self.ieee)) - async def shutdown(self): - """Shutdown and cleanup ControllerApplication.""" - LOGGER.info("Shutting down ControllerApplication") + async def load_network_info(self, *, load_devices=False) -> None: + ezsp = self._ezsp + + (status,) = await ezsp.networkInit() + LOGGER.debug("Network init status: %s", status) + assert status == t.EmberStatus.SUCCESS + + status, node_type, nwk_params = await ezsp.getNetworkParameters() + assert status == t.EmberStatus.SUCCESS + + node_info = self.state.node_info + (node_info.nwk,) = await ezsp.getNodeId() + (node_info.ieee,) = await ezsp.getEui64() + node_info.logical_type = node_type.zdo_logical_type + + network_info = self.state.network_info + + network_info.extended_pan_id = nwk_params.extendedPanId + network_info.pan_id = nwk_params.panId + network_info.nwk_update_id = nwk_params.nwkUpdateId + network_info.nwk_manager_id = nwk_params.nwkManagerId + network_info.channel = nwk_params.radioChannel + network_info.channel_mask = nwk_params.channels + + (status, security_level) = await ezsp.getConfigurationValue( + ezsp.types.EzspConfigId.CONFIG_SECURITY_LEVEL + ) + assert status == t.EmberStatus.SUCCESS + network_info.security_level = security_level + + # Network key + (status, key) = await ezsp.getKey(ezsp.types.EmberKeyType.CURRENT_NETWORK_KEY) + assert status == t.EmberStatus.SUCCESS + network_info.network_key = bellows.zigbee.util.ezsp_key_struct_to_zigpy_key( + key, ezsp=ezsp + ) + + # Security state + (status, state) = await ezsp.getCurrentSecurityState() + assert status == t.EmberStatus.SUCCESS + + # TCLK + (status, key) = await ezsp.getKey(ezsp.types.EmberKeyType.TRUST_CENTER_LINK_KEY) + assert status == t.EmberStatus.SUCCESS + + network_info.tc_link_key = bellows.zigbee.util.ezsp_key_struct_to_zigpy_key( + key, ezsp=ezsp + ) + + if ( + state.bitmask + & ezsp.types.EmberCurrentSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY + ): + network_info.stack_specific = { + "ezsp": {"hashed_tclk": network_info.tc_link_key.key.serialize().hex()} + } + network_info.tc_link_key.key = KeyData(b"ZigBeeAlliance09") + + if not load_devices: + return + + network_info.key_table = [] + + for idx in range(0, 192): + (status, key) = await ezsp.getKeyTableEntry(idx) + + if status == t.EmberStatus.INDEX_OUT_OF_RANGE: + break + elif status == t.EmberStatus.TABLE_ENTRY_ERASED: + continue + + assert status == t.EmberStatus.SUCCESS + + network_info.key_table.append( + bellows.zigbee.util.ezsp_key_struct_to_zigpy_key(key, ezsp=ezsp) + ) + + network_info.children = [] + network_info.nwk_addresses = {} + + for idx in range(0, 255 + 1): + (status, nwk, eui64, node_type) = await ezsp.getChildData(idx) + + if status == t.EmberStatus.NOT_JOINED: + continue + + network_info.children.append(eui64) + network_info.nwk_addresses[eui64] = nwk + + for idx in range(0, 255 + 1): + (nwk,) = await ezsp.getAddressTableRemoteNodeId(idx) + (eui64,) = await ezsp.getAddressTableRemoteEui64(idx) + + # Ignore invalid NWK entries + if nwk in t.EmberDistinguishedNodeId.__members__.values(): + continue + + network_info.nwk_addresses[eui64] = nwk + + async def write_network_info(*, network_info, node_info): + pass + + async def disconnect(self): self.controller_event.clear() if self._watchdog_task and not self._watchdog_task.done(): LOGGER.debug("Cancelling watchdog") diff --git a/bellows/zigbee/util.py b/bellows/zigbee/util.py index ac0b9c41..818c50c7 100644 --- a/bellows/zigbee/util.py +++ b/bellows/zigbee/util.py @@ -2,6 +2,7 @@ from typing import Any, Dict import zigpy.config +import zigpy.state import bellows.types as t @@ -37,3 +38,22 @@ def zha_security( t.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY ) return isc + + +def ezsp_key_struct_to_zigpy_key(key, *, ezsp): + zigpy_key = zigpy.state.Key() + zigpy_key.key = key.key + + if key.bitmask & ezsp.types.EmberKeyStructBitmask.KEY_HAS_SEQUENCE_NUMBER: + zigpy_key.seq = key.sequenceNumber + + if key.bitmask & ezsp.types.EmberKeyStructBitmask.KEY_HAS_OUTGOING_FRAME_COUNTER: + zigpy_key.tx_counter = key.outgoingFrameCounter + + if key.bitmask & ezsp.types.EmberKeyStructBitmask.KEY_HAS_INCOMING_FRAME_COUNTER: + zigpy_key.rx_counter = key.outgoingFrameCounter + + if key.bitmask & ezsp.types.EmberKeyStructBitmask.KEY_HAS_PARTNER_EUI64: + zigpy_key.partner_ieee = key.partnerEUI64 + + return zigpy_key From d2c112972df0572af05f0325dac5ce4ba13339c9 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 15 Dec 2021 21:49:31 -0500 Subject: [PATCH 02/49] [WIP] Implement writing network settings --- bellows/zigbee/application.py | 130 +++++++++++++++++++++++----------- bellows/zigbee/util.py | 63 +++++++++++----- 2 files changed, 131 insertions(+), 62 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 86d593e1..b6739ac4 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -9,7 +9,7 @@ import zigpy.application import zigpy.config import zigpy.device -from zigpy.exceptions import NetworkNotFormed +from zigpy.exceptions import FormationFailure, NetworkNotFormed from zigpy.quirks import CustomDevice, CustomEndpoint from zigpy.types import Addressing, BroadcastAddress, KeyData import zigpy.util @@ -184,7 +184,6 @@ async def load_network_info(self, *, load_devices=False) -> None: ezsp = self._ezsp (status,) = await ezsp.networkInit() - LOGGER.debug("Network init status: %s", status) assert status == t.EmberStatus.SUCCESS status, node_type, nwk_params = await ezsp.getNetworkParameters() @@ -213,9 +212,7 @@ async def load_network_info(self, *, load_devices=False) -> None: # Network key (status, key) = await ezsp.getKey(ezsp.types.EmberKeyType.CURRENT_NETWORK_KEY) assert status == t.EmberStatus.SUCCESS - network_info.network_key = bellows.zigbee.util.ezsp_key_struct_to_zigpy_key( - key, ezsp=ezsp - ) + network_info.network_key = bellows.zigbee.util.ezsp_key_to_zigpy_key(key, ezsp) # Security state (status, state) = await ezsp.getCurrentSecurityState() @@ -224,10 +221,7 @@ async def load_network_info(self, *, load_devices=False) -> None: # TCLK (status, key) = await ezsp.getKey(ezsp.types.EmberKeyType.TRUST_CENTER_LINK_KEY) assert status == t.EmberStatus.SUCCESS - - network_info.tc_link_key = bellows.zigbee.util.ezsp_key_struct_to_zigpy_key( - key, ezsp=ezsp - ) + network_info.tc_link_key = bellows.zigbee.util.ezsp_key_to_zigpy_key(key, ezsp) if ( state.bitmask @@ -254,7 +248,7 @@ async def load_network_info(self, *, load_devices=False) -> None: assert status == t.EmberStatus.SUCCESS network_info.key_table.append( - bellows.zigbee.util.ezsp_key_struct_to_zigpy_key(key, ezsp=ezsp) + bellows.zigbee.util.ezsp_key_to_zigpy_key(key, ezsp) ) network_info.children = [] @@ -279,50 +273,100 @@ async def load_network_info(self, *, load_devices=False) -> None: network_info.nwk_addresses[eui64] = nwk - async def write_network_info(*, network_info, node_info): - pass + async def write_network_info( + self, *, network_info: zigpy.state.NetworkInfo, node_info: zigpy.state.NodeInfo + ) -> None: + ezsp = self._ezsp - async def disconnect(self): - self.controller_event.clear() - if self._watchdog_task and not self._watchdog_task.done(): - LOGGER.debug("Cancelling watchdog") - self._watchdog_task.cancel() - if self._reset_task and not self._reset_task.done(): - self._reset_task.cancel() - if self._ezsp is not None: - self._ezsp.close() + (status,) = await ezsp.networkInit() + assert status in (t.EmberStatus.SUCCESS, t.EmberStatus.NOT_JOINED) - async def form_network(self): - nwk = self.config[zigpy.config.CONF_NWK] + if status == t.EmberStatus.SUCCESS: + (status,) = await ezsp.leaveNetwork() + if status != t.EmberStatus.NETWORK_DOWN: + raise FormationFailure("Couldn't leave network") - pan_id = nwk[zigpy.config.CONF_NWK_PAN_ID] - if pan_id is None: - pan_id = int.from_bytes(os.urandom(2), byteorder="little") + # Is this necessary? + (status,) = await ezsp.clearKeyTable() + assert status == t.EmberStatus.SUCCESS - extended_pan_id = nwk[zigpy.config.CONF_NWK_EXTENDED_PAN_ID] - if extended_pan_id is None: - extended_pan_id = t.EmberEUI64([t.uint8_t(0)] * 8) + stack_specific = network_info.stack_specific.get("ezsp", {}) + (current_eui64,) = await ezsp.getEui64() + should_update_eui64 = stack_specific.get( + "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" + ) + + if current_eui64 != node_info.ieee: + if not should_update_eui64: + LOGGER.warning("Not updating node EUI64 to %s", node_info.ieee) + else: + new_ncp_eui64 = t.EmberEUI64(node_info.ieee) + (status,) = await ezsp.setMfgToken( + t.EzspMfgTokenId.MFG_CUSTOM_EUI_64, new_ncp_eui64.serialize() + ) + assert status[0] == t.EmberStatus.SUCCESS - hashed_tclk = self._ezsp.ezsp_version > 4 + hashed_tclk = ezsp.ezsp_version > 4 + if hashed_tclk and not stack_specific.get("hashed_tclk"): + network_info.stack_specific.setdefault("ezsp", {})[ + "hashed_tclk" + ] = os.urandom(8).hex() + + # TODO: support router mode initial_security_state = bellows.zigbee.util.zha_security( - nwk, controller=True, hashed_tclk=hashed_tclk + network_info, controller=True, hashed_tclk=hashed_tclk ) - v = await self._ezsp.setInitialSecurityState(initial_security_state) - assert v[0] == t.EmberStatus.SUCCESS # TODO: Better check + status = await ezsp.setInitialSecurityState(initial_security_state) + + # Write keys + key_table = [ + ( + bellows.zigbee.util.zigpy_key_to_ezsp_key( + network_info.tc_link_key, ezsp + ), + True, + ) + ] + [ + (bellows.zigbee.util.zigpy_key_to_ezsp_key(key, ezsp), False) + for key in network_info.key_table + ] + + (status,) = await ezsp.setConfigurationValue( + ezsp.types.EzspConfigId.CONFIG_KEY_TABLE_SIZE, len(key_table) + ) + assert status == t.EmberStatus.SUCCESS + + for index, (key, is_link_key) in enumerate(key_table): + # XXX: is there no way to set the outgoing frame counter per key? + (status,) = await ezsp.addOrUpdateKeyTableEntry( + key.partnerEUI64, is_link_key, key.key + ) + if status != t.EmberStatus.SUCCESS: + LOGGER.warning("Couldn't add %s key: %s", key, status) + await asyncio.sleep(0.2) + parameters = t.EmberNetworkParameters() - parameters.panId = t.EmberPanId(pan_id) - parameters.extendedPanId = extended_pan_id + parameters.panId = t.EmberPanId(network_info.pan_id) + parameters.extendedPanId = t.EmberEUI64(network_info.extended_pan_id) parameters.radioTxPower = t.uint8_t(8) - parameters.radioChannel = t.uint8_t(nwk[zigpy.config.CONF_NWK_CHANNEL]) + parameters.radioChannel = t.uint8_t(network_info.channel) parameters.joinMethod = t.EmberJoinMethod.USE_MAC_ASSOCIATION - parameters.nwkManagerId = t.EmberNodeId(0) - parameters.nwkUpdateId = t.uint8_t(nwk[zigpy.config.CONF_NWK_UPDATE_ID]) - parameters.channels = nwk[zigpy.config.CONF_NWK_CHANNELS] + parameters.nwkManagerId = t.EmberNodeId(network_info.nwk_manager_id) + parameters.nwkUpdateId = t.uint8_t(network_info.nwk_update_id) + parameters.channels = t.Channels(network_info.channel_mask) - await self._ezsp.formNetwork(parameters) - await self._ezsp.setValue( - self._ezsp.types.EzspValueId.VALUE_STACK_TOKEN_WRITING, 1 - ) + await ezsp.formNetwork(parameters) + await ezsp.setValue(ezsp.types.EzspValueId.VALUE_STACK_TOKEN_WRITING, 1) + + async def disconnect(self): + self.controller_event.clear() + if self._watchdog_task and not self._watchdog_task.done(): + LOGGER.debug("Cancelling watchdog") + self._watchdog_task.cancel() + if self._reset_task and not self._reset_task.done(): + self._reset_task.cancel() + if self._ezsp is not None: + self._ezsp.close() async def force_remove(self, dev): # This should probably be delivered to the parent device instead diff --git a/bellows/zigbee/util.py b/bellows/zigbee/util.py index 818c50c7..fbfd848c 100644 --- a/bellows/zigbee/util.py +++ b/bellows/zigbee/util.py @@ -1,31 +1,30 @@ -import os -from typing import Any, Dict - -import zigpy.config import zigpy.state import bellows.types as t def zha_security( - config: Dict[str, Any], controller: bool = False, hashed_tclk: bool = True -) -> None: + network_info: zigpy.state.NetworkInfo, + controller: bool = False, + hashed_tclk: bool = True, +) -> t.EmberInitialSecurityState: isc = t.EmberInitialSecurityState() isc.bitmask = ( t.EmberInitialSecurityBitmask.HAVE_PRECONFIGURED_KEY | t.EmberInitialSecurityBitmask.REQUIRE_ENCRYPTED_KEY ) - isc.preconfiguredKey = t.EmberKeyData(config[zigpy.config.CONF_NWK_TC_LINK_KEY]) - nwk_key = config[zigpy.config.CONF_NWK_KEY] - if nwk_key is None: - nwk_key = os.urandom(16) - isc.networkKey = t.EmberKeyData(nwk_key) - isc.networkKeySequenceNumber = t.uint8_t(config[zigpy.config.CONF_NWK_KEY_SEQ]) - tc_addr = config[zigpy.config.CONF_NWK_TC_ADDRESS] - if tc_addr is None: - tc_addr = [0x00] * 8 - isc.preconfiguredTrustCenterEui64 = t.EmberEUI64(tc_addr) + isc.preconfiguredKey = t.EmberKeyData(network_info.tc_link_key.key) + isc.networkKey = t.EmberKeyData(network_info.network_key.key) + isc.networkKeySequenceNumber = t.uint8_t(network_info.network_key.seq) + + if network_info.tc_link_key.partner_ieee: + isc.bitmask |= t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 + isc.preconfiguredTrustCenterEui64 = t.EmberEUI64( + network_info.tc_link_key.partner_ieee + ) + else: + isc.preconfiguredTrustCenterEui64 = t.EmberEUI64([0x00] * 8) if controller: isc.bitmask |= ( @@ -33,14 +32,16 @@ def zha_security( | t.EmberInitialSecurityBitmask.HAVE_NETWORK_KEY ) if hashed_tclk: - isc.preconfiguredKey = t.EmberKeyData(os.urandom(16)) + isc.preconfiguredKey, _ = t.EmberKeyData.deserialize( + bytes.fromhex(network_info.stack_specific["ezsp"]["hashed_tclk"]) + ) isc.bitmask |= ( t.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY ) return isc -def ezsp_key_struct_to_zigpy_key(key, *, ezsp): +def ezsp_key_to_zigpy_key(key, ezsp): zigpy_key = zigpy.state.Key() zigpy_key.key = key.key @@ -51,9 +52,33 @@ def ezsp_key_struct_to_zigpy_key(key, *, ezsp): zigpy_key.tx_counter = key.outgoingFrameCounter if key.bitmask & ezsp.types.EmberKeyStructBitmask.KEY_HAS_INCOMING_FRAME_COUNTER: - zigpy_key.rx_counter = key.outgoingFrameCounter + zigpy_key.rx_counter = key.incomingFrameCounter if key.bitmask & ezsp.types.EmberKeyStructBitmask.KEY_HAS_PARTNER_EUI64: zigpy_key.partner_ieee = key.partnerEUI64 return zigpy_key + + +def zigpy_key_to_ezsp_key(zigpy_key, ezsp): + key = ezsp.types.EmberKeyStruct() + key.key = zigpy_key.key + key.bitmask = ezsp.types.EmberKeyStructBitmask(0) + + if zigpy_key.seq is not None: + key.seq = zigpy_key.seq + key.bitmask |= ezsp.types.EmberKeyStructBitmask.KEY_HAS_SEQUENCE_NUMBER + + if zigpy_key.tx_counter is not None: + key.outgoingFrameCounter = zigpy_key.tx_counter + key.bitmask |= ezsp.types.EmberKeyStructBitmask.KEY_HAS_OUTGOING_FRAME_COUNTER + + if zigpy_key.rx_counter is not None: + key.outgoingFrameCounter = zigpy_key.rx_counter + key.bitmask |= ezsp.types.EmberKeyStructBitmask.KEY_HAS_INCOMING_FRAME_COUNTER + + if zigpy_key.partner_ieee is not None: + key.partnerEUI64 = zigpy_key.partner_ieee + key.bitmask |= ezsp.types.EmberKeyStructBitmask.KEY_HAS_PARTNER_EUI64 + + return key From b16049036d7da417b52ec83b84393da6c784c736 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 20 Dec 2021 16:55:04 -0500 Subject: [PATCH 03/49] Set NWK and APS frame counters --- bellows/zigbee/application.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index b6739ac4..5482844d 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -345,6 +345,21 @@ async def write_network_info( LOGGER.warning("Couldn't add %s key: %s", key, status) await asyncio.sleep(0.2) + # Set NWK frame counter + (status,) = await ezsp.setValue( + ezsp.types.EzspValueId.VALUE_NWK_FRAME_COUNTER, + t.uint32_t(network_info.network_key.tx_counter).serialize(), + ) + assert status == t.EmberStatus.SUCCESS + + # Set APS frame counter + (status,) = await ezsp.setValue( + ezsp.types.EzspValueId.VALUE_APS_FRAME_COUNTER, + t.uint32_t(network_info.tc_link_key.tx_counter).serialize(), + ) + LOGGER.debug("Set network frame counter: %s", status) + assert status == t.EmberStatus.SUCCESS + parameters = t.EmberNetworkParameters() parameters.panId = t.EmberPanId(network_info.pan_id) parameters.extendedPanId = t.EmberEUI64(network_info.extended_pan_id) From 8b8b3136c429df1c57f629bb73f0b0055e405fd7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 12 Jan 2022 23:17:11 -0500 Subject: [PATCH 04/49] Change key table size only if the new keys don't fit --- bellows/zigbee/application.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 5482844d..2b6bcd6c 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -331,11 +331,17 @@ async def write_network_info( for key in network_info.key_table ] - (status,) = await ezsp.setConfigurationValue( - ezsp.types.EzspConfigId.CONFIG_KEY_TABLE_SIZE, len(key_table) + (status, key_table_size) = await ezsp.getConfigurationValue( + ezsp.types.EzspConfigId.CONFIG_KEY_TABLE_SIZE ) assert status == t.EmberStatus.SUCCESS + if key_table_size < len(key_table): + (status,) = await ezsp.setConfigurationValue( + ezsp.types.EzspConfigId.CONFIG_KEY_TABLE_SIZE, len(key_table) + ) + assert status == t.EmberStatus.SUCCESS + for index, (key, is_link_key) in enumerate(key_table): # XXX: is there no way to set the outgoing frame counter per key? (status,) = await ezsp.addOrUpdateKeyTableEntry( From 0d486e46fdb78fc2897a0c1a94e9524514d0864c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 12 Jan 2022 23:17:27 -0500 Subject: [PATCH 05/49] Style cleanup --- bellows/zigbee/application.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 2b6bcd6c..fa806644 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -195,7 +195,6 @@ async def load_network_info(self, *, load_devices=False) -> None: node_info.logical_type = node_type.zdo_logical_type network_info = self.state.network_info - network_info.extended_pan_id = nwk_params.extendedPanId network_info.pan_id = nwk_params.panId network_info.nwk_update_id = nwk_params.nwkUpdateId @@ -298,7 +297,11 @@ async def write_network_info( if current_eui64 != node_info.ieee: if not should_update_eui64: - LOGGER.warning("Not updating node EUI64 to %s", node_info.ieee) + LOGGER.warning( + "Current node's IEEE address (%s) does not match the backup's (%s)", + current_eui64, + node_info.ieee, + ) else: new_ncp_eui64 = t.EmberEUI64(node_info.ieee) (status,) = await ezsp.setMfgToken( @@ -308,9 +311,7 @@ async def write_network_info( hashed_tclk = ezsp.ezsp_version > 4 if hashed_tclk and not stack_specific.get("hashed_tclk"): - network_info.stack_specific.setdefault("ezsp", {})[ - "hashed_tclk" - ] = os.urandom(8).hex() + stack_specific.setdefault("ezsp", {})["hashed_tclk"] = os.urandom(8).hex() # TODO: support router mode initial_security_state = bellows.zigbee.util.zha_security( @@ -349,7 +350,6 @@ async def write_network_info( ) if status != t.EmberStatus.SUCCESS: LOGGER.warning("Couldn't add %s key: %s", key, status) - await asyncio.sleep(0.2) # Set NWK frame counter (status,) = await ezsp.setValue( @@ -366,6 +366,7 @@ async def write_network_info( LOGGER.debug("Set network frame counter: %s", status) assert status == t.EmberStatus.SUCCESS + # Set the network settings parameters = t.EmberNetworkParameters() parameters.panId = t.EmberPanId(network_info.pan_id) parameters.extendedPanId = t.EmberEUI64(network_info.extended_pan_id) From f51152b191d1929f96136ea7fe564789d4397455 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 7 Mar 2022 13:56:49 -0500 Subject: [PATCH 06/49] Directly create `EZSPCoordinator` instead of using a quirk --- bellows/zigbee/application.py | 37 +++++++++++++---------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index fa806644..89742d25 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -9,8 +9,8 @@ import zigpy.application import zigpy.config import zigpy.device +import zigpy.endpoint from zigpy.exceptions import FormationFailure, NetworkNotFormed -from zigpy.quirks import CustomDevice, CustomEndpoint from zigpy.types import Addressing, BroadcastAddress, KeyData import zigpy.util import zigpy.zdo.types as zdo_t @@ -175,10 +175,18 @@ async def start_network(self): self.controller_event.set() self._watchdog_task = asyncio.create_task(self._watchdog()) - self.handle_join(self.nwk, self.ieee, 0) - LOGGER.debug("EZSP nwk=0x%04x, IEEE=%s", self._nwk, str(self._ieee)) + ezsp_device = EZSPCoordinator( + application=self, + ieee=self.state.node_info.ieee, + nwk=self.state.node_info.nwk, + ) + self.devices[self.state.node_info.ieee] = ezsp_device + await ezsp_device.initialize() + ezsp_device.endpoints[1] = EZSPCoordinator.EZSPEndpoint(ezsp_device, 1) + ezsp_device.model = ezsp_device.endpoints[1].model + ezsp_device.manufacturer = ezsp_device.endpoints[1].manufacturer - await self.multicast.startup(self.get_device(self.ieee)) + await self.multicast.startup(ezsp_device) async def load_network_info(self, *, load_devices=False) -> None: ezsp = self._ezsp @@ -944,10 +952,10 @@ def handle_route_error(self, status: t.EmberStatus, nwk: t.EmberNodeId) -> None: dev.relays = None -class EZSPCoordinator(CustomDevice): +class EZSPCoordinator(zigpy.device.Device): """Zigpy Device representing Coordinator.""" - class EZSPEndpoint(CustomEndpoint): + class EZSPEndpoint(zigpy.endpoint.Endpoint): @property def manufacturer(self) -> str: """Manufacturer.""" @@ -984,20 +992,3 @@ async def remove_from_group(self, grp_id: int) -> t.EmberStatus: app.groups[grp_id].remove_member(self) return status - - signature = { - "endpoints": { - 1: { - "profile_id": 0x0104, - "device_type": 0xBEEF, - "input_clusters": [], - "output_clusters": [zigpy.zcl.clusters.security.IasZone.cluster_id], - } - } - } - - replacement = { - "endpoints": {1: (EZSPEndpoint, {})}, - "manufacturer": "Silicon Labs", - "model": "EZSP", - } From dfa5791dd7ac3665a96e71748f2f185c25a68d56 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 7 Mar 2022 13:57:47 -0500 Subject: [PATCH 07/49] Use zigpy types when creating network state objects --- bellows/zigbee/application.py | 87 ++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 89742d25..5278bac6 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -11,7 +11,8 @@ import zigpy.device import zigpy.endpoint from zigpy.exceptions import FormationFailure, NetworkNotFormed -from zigpy.types import Addressing, BroadcastAddress, KeyData +import zigpy.state +import zigpy.types import zigpy.util import zigpy.zdo.types as zdo_t @@ -197,53 +198,62 @@ async def load_network_info(self, *, load_devices=False) -> None: status, node_type, nwk_params = await ezsp.getNetworkParameters() assert status == t.EmberStatus.SUCCESS - node_info = self.state.node_info - (node_info.nwk,) = await ezsp.getNodeId() - (node_info.ieee,) = await ezsp.getEui64() - node_info.logical_type = node_type.zdo_logical_type + (nwk,) = await ezsp.getNodeId() + (ieee,) = await ezsp.getEui64() - network_info = self.state.network_info - network_info.extended_pan_id = nwk_params.extendedPanId - network_info.pan_id = nwk_params.panId - network_info.nwk_update_id = nwk_params.nwkUpdateId - network_info.nwk_manager_id = nwk_params.nwkManagerId - network_info.channel = nwk_params.radioChannel - network_info.channel_mask = nwk_params.channels + self.state.node_info = zigpy.state.NodeInfo( + nwk=zigpy.types.NWK(nwk), + ieee=zigpy.types.EUI64(ieee), + logical_type=node_type.zdo_logical_type, + ) (status, security_level) = await ezsp.getConfigurationValue( ezsp.types.EzspConfigId.CONFIG_SECURITY_LEVEL ) assert status == t.EmberStatus.SUCCESS - network_info.security_level = security_level + security_level = zigpy.types.uint8_t(security_level) - # Network key - (status, key) = await ezsp.getKey(ezsp.types.EmberKeyType.CURRENT_NETWORK_KEY) + (status, network_key) = await ezsp.getKey( + ezsp.types.EmberKeyType.CURRENT_NETWORK_KEY + ) assert status == t.EmberStatus.SUCCESS - network_info.network_key = bellows.zigbee.util.ezsp_key_to_zigpy_key(key, ezsp) - # Security state - (status, state) = await ezsp.getCurrentSecurityState() + (status, tc_link_key) = await ezsp.getKey( + ezsp.types.EmberKeyType.TRUST_CENTER_LINK_KEY + ) assert status == t.EmberStatus.SUCCESS - # TCLK - (status, key) = await ezsp.getKey(ezsp.types.EmberKeyType.TRUST_CENTER_LINK_KEY) + (status, state) = await ezsp.getCurrentSecurityState() assert status == t.EmberStatus.SUCCESS - network_info.tc_link_key = bellows.zigbee.util.ezsp_key_to_zigpy_key(key, ezsp) + + stack_specific = {} if ( state.bitmask & ezsp.types.EmberCurrentSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY ): - network_info.stack_specific = { - "ezsp": {"hashed_tclk": network_info.tc_link_key.key.serialize().hex()} - } - network_info.tc_link_key.key = KeyData(b"ZigBeeAlliance09") + stack_specific["ezsp"] = {"hashed_tclk": tc_link_key.key.serialize().hex()} + tc_link_key.key = zigpy.types.KeyData(b"ZigBeeAlliance09") + + self.state.network_info = zigpy.state.NetworkInfo( + extended_pan_id=zigpy.types.ExtendedPanId(nwk_params.extendedPanId), + pan_id=zigpy.types.PanId(nwk_params.panId), + nwk_update_id=zigpy.types.uint8_t(nwk_params.nwkUpdateId), + nwk_manager_id=zigpy.types.NWK(nwk_params.nwkManagerId), + channel=zigpy.types.uint8_t(nwk_params.radioChannel), + channel_mask=zigpy.types.Channels(nwk_params.channels), + security_level=zigpy.types.uint8_t(security_level), + network_key=bellows.zigbee.util.ezsp_key_to_zigpy_key(network_key, ezsp), + tc_link_key=bellows.zigbee.util.ezsp_key_to_zigpy_key(tc_link_key, ezsp), + key_table=[], + children=[], + nwk_addresses={}, + stack_specific=stack_specific, + ) if not load_devices: return - network_info.key_table = [] - for idx in range(0, 192): (status, key) = await ezsp.getKeyTableEntry(idx) @@ -254,21 +264,18 @@ async def load_network_info(self, *, load_devices=False) -> None: assert status == t.EmberStatus.SUCCESS - network_info.key_table.append( + self.state.network_info.key_table.append( bellows.zigbee.util.ezsp_key_to_zigpy_key(key, ezsp) ) - network_info.children = [] - network_info.nwk_addresses = {} - for idx in range(0, 255 + 1): (status, nwk, eui64, node_type) = await ezsp.getChildData(idx) if status == t.EmberStatus.NOT_JOINED: continue - network_info.children.append(eui64) - network_info.nwk_addresses[eui64] = nwk + self.state.network_info.children.append(eui64) + self.state.network_info.nwk_addresses[eui64] = nwk for idx in range(0, 255 + 1): (nwk,) = await ezsp.getAddressTableRemoteNodeId(idx) @@ -278,7 +285,7 @@ async def load_network_info(self, *, load_devices=False) -> None: if nwk in t.EmberDistinguishedNodeId.__members__.values(): continue - network_info.nwk_addresses[eui64] = nwk + self.state.network_info.nwk_addresses[eui64] = nwk async def write_network_info( self, *, network_info: zigpy.state.NetworkInfo, node_info: zigpy.state.NodeInfo @@ -433,13 +440,17 @@ def _handle_frame( ) -> None: if message_type == t.EmberIncomingMessageType.INCOMING_BROADCAST: self.state.counters[COUNTERS_CTRL][COUNTER_RX_BCAST].increment() - dst_addressing = Addressing.nwk(0xFFFE, aps_frame.destinationEndpoint) + dst_addressing = zigpy.types.Addressing.nwk( + 0xFFFE, aps_frame.destinationEndpoint + ) elif message_type == t.EmberIncomingMessageType.INCOMING_MULTICAST: self.state.counters[COUNTERS_CTRL][COUNTER_RX_MCAST].increment() - dst_addressing = Addressing.group(aps_frame.groupId) + dst_addressing = zigpy.types.Addressing.group(aps_frame.groupId) elif message_type == t.EmberIncomingMessageType.INCOMING_UNICAST: self.state.counters[COUNTERS_CTRL][COUNTER_RX_UNICAST].increment() - dst_addressing = Addressing.nwk(self.nwk, aps_frame.destinationEndpoint) + dst_addressing = zigpy.types.Addressing.nwk( + self.state.node_info.nwk, aps_frame.destinationEndpoint + ) else: dst_addressing = None @@ -809,7 +820,7 @@ async def broadcast( radius, sequence, data, - broadcast_address=BroadcastAddress.RX_ON_WHEN_IDLE, + broadcast_address=zigpy.types.BroadcastAddress.RX_ON_WHEN_IDLE, ): """Submit and send data out as an unicast transmission. From 539f2b65ad9782af37195e6ed0a4ba8135eb3b21 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 7 Mar 2022 14:21:54 -0500 Subject: [PATCH 08/49] Allow `load_network_info` to be called before/after `start_network` --- bellows/zigbee/application.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 5278bac6..e8c0867a 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -151,12 +151,26 @@ async def connect(self): except EzspError as exc: LOGGER.info("EZSP Radio does not support getMfgToken command: %s", str(exc)) + async def _ensure_network_running(self) -> bool: + """ + Ensures the network is currently running and returns whether or not the network + was started. + """ + (state,) = await self._ezsp.networkState() + + if state != self._ezsp.types.EmberNetworkStatus.NO_NETWORK: + return False + + (init_status,) = await self._ezsp.networkInit() + if init_status != t.EmberStatus.SUCCESS: + raise NetworkNotFormed(f"Failed to init network: {init_status!r}") + + return True + async def start_network(self): ezsp = self._ezsp - v = await ezsp.networkInit() - if v[0] != t.EmberStatus.SUCCESS: - raise NetworkNotFormed() + await self._ensure_network_running() status, node_type, nwk_params = await ezsp.getNetworkParameters() assert status == t.EmberStatus.SUCCESS # TODO: Better check @@ -192,8 +206,7 @@ async def start_network(self): async def load_network_info(self, *, load_devices=False) -> None: ezsp = self._ezsp - (status,) = await ezsp.networkInit() - assert status == t.EmberStatus.SUCCESS + await self._ensure_network_running() status, node_type, nwk_params = await ezsp.getNetworkParameters() assert status == t.EmberStatus.SUCCESS From c6223c4ece2bd1d56477e3dc237f26e7df4d77be Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 15 Mar 2022 14:53:26 -0400 Subject: [PATCH 09/49] Allow `node_info.ieee` to be `None` --- bellows/zigbee/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index e8c0867a..ce92656d 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -323,7 +323,7 @@ async def write_network_info( "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" ) - if current_eui64 != node_info.ieee: + if node_info.ieee is not None and node_info.ieee != current_eui64: if not should_update_eui64: LOGGER.warning( "Current node's IEEE address (%s) does not match the backup's (%s)", From 71aab0f1d4c6ba33967952233cb66de8eff06a25 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 15 Mar 2022 15:02:54 -0400 Subject: [PATCH 10/49] Use `t.EUI64.UNKNOWN` instead of `None` to represent an unset IEEE addr --- bellows/zigbee/application.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index ce92656d..af2e11b1 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -323,7 +323,10 @@ async def write_network_info( "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" ) - if node_info.ieee is not None and node_info.ieee != current_eui64: + if ( + node_info.ieee != zigpy.types.EUI64.UNKNOWN + and node_info.ieee != current_eui64 + ): if not should_update_eui64: LOGGER.warning( "Current node's IEEE address (%s) does not match the backup's (%s)", From bbd71fd8af9cb77fcfc62e41e2b855a808223937 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sun, 20 Mar 2022 23:31:23 -0400 Subject: [PATCH 11/49] Do not leak EZSP types into zigpy --- bellows/zigbee/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bellows/zigbee/util.py b/bellows/zigbee/util.py index fbfd848c..d2100852 100644 --- a/bellows/zigbee/util.py +++ b/bellows/zigbee/util.py @@ -1,4 +1,5 @@ import zigpy.state +import zigpy.types as zigpy_t import bellows.types as t @@ -43,7 +44,7 @@ def zha_security( def ezsp_key_to_zigpy_key(key, ezsp): zigpy_key = zigpy.state.Key() - zigpy_key.key = key.key + zigpy_key.key = zigpy_t.KeyData(key.key) if key.bitmask & ezsp.types.EmberKeyStructBitmask.KEY_HAS_SEQUENCE_NUMBER: zigpy_key.seq = key.sequenceNumber From 1bd1c8e2d8b03bca7997394c808573e6b1e32a6d Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 23 Mar 2022 02:22:20 -0400 Subject: [PATCH 12/49] Fix network formation when a hashed TCLK is generated --- bellows/zigbee/application.py | 4 +++- bellows/zigbee/util.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index af2e11b1..9f778d77 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -342,7 +342,9 @@ async def write_network_info( hashed_tclk = ezsp.ezsp_version > 4 if hashed_tclk and not stack_specific.get("hashed_tclk"): - stack_specific.setdefault("ezsp", {})["hashed_tclk"] = os.urandom(8).hex() + network_info.stack_specific.setdefault("ezsp", {})[ + "hashed_tclk" + ] = os.urandom(8).hex() # TODO: support router mode initial_security_state = bellows.zigbee.util.zha_security( diff --git a/bellows/zigbee/util.py b/bellows/zigbee/util.py index d2100852..9db4c1c8 100644 --- a/bellows/zigbee/util.py +++ b/bellows/zigbee/util.py @@ -19,7 +19,7 @@ def zha_security( isc.networkKey = t.EmberKeyData(network_info.network_key.key) isc.networkKeySequenceNumber = t.uint8_t(network_info.network_key.seq) - if network_info.tc_link_key.partner_ieee: + if network_info.tc_link_key.partner_ieee != zigpy_t.EUI64.UNKNOWN: isc.bitmask |= t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 isc.preconfiguredTrustCenterEui64 = t.EmberEUI64( network_info.tc_link_key.partner_ieee From febed7a29f271d2dd01118ce3a0f218f7e10944b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 23 Mar 2022 02:23:44 -0400 Subject: [PATCH 13/49] Joins were not being properly permitting after fcf0b49bc141fd81 --- bellows/zigbee/application.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 9f778d77..4fe9ef96 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -131,7 +131,9 @@ async def cleanup_tc_link_key(self, ieee: t.EmberEUI64) -> None: async def connect(self): self._ezsp = await bellows.ezsp.EZSP.initialize(self.config) ezsp = self._ezsp + self._multicast = bellows.multicast.Multicast(ezsp) + status, count = await ezsp.getConfigurationValue( ezsp.types.EzspConfigId.CONFIG_APS_UNICAST_MESSAGE_COUNT ) @@ -177,6 +179,8 @@ async def start_network(self): if node_type != t.EmberNodeType.COORDINATOR: raise NetworkNotFormed("Network not configured as coordinator") + await ezsp.update_policies(self.config) + await self.load_network_info(load_devices=False) for cnt_group in self.state.counters: @@ -414,6 +418,7 @@ async def write_network_info( await ezsp.setValue(ezsp.types.EzspValueId.VALUE_STACK_TOKEN_WRITING, 1) async def disconnect(self): + # TODO: how do you shut down the stack? self.controller_event.clear() if self._watchdog_task and not self._watchdog_task.done(): LOGGER.debug("Cancelling watchdog") From 1d267a454289a6bb3835f72da3dbbda9620a4a1f Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 19 Apr 2022 14:34:24 -0400 Subject: [PATCH 14/49] Use the new zigpy `add_endpoint` method --- bellows/zigbee/application.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 4fe9ef96..ccac4b98 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -98,25 +98,17 @@ def multicast(self): """Return EZSP MulticastController.""" return self._multicast - async def add_endpoint( - self, - endpoint=1, - profile_id=zigpy.profiles.zha.PROFILE_ID, - device_id=0xBEEF, - app_flags=0x00, - input_clusters=[], - output_clusters=[], - ): + async def add_endpoint(self, descriptor: zdo_t.SimpleDescriptor) -> None: """Add endpoint.""" res = await self._ezsp.addEndpoint( - endpoint, - profile_id, - device_id, - app_flags, - len(input_clusters), - len(output_clusters), - input_clusters, - output_clusters, + descriptor.endpoint, + descriptor.profile, + descriptor.device_type, + descriptor.device_version, + len(descriptor.input_clusters), + len(descriptor.output_clusters), + descriptor.input_clusters, + descriptor.output_clusters, ) LOGGER.debug("Ezsp adding endpoint: %s", res) From 26e297f810d52c1e75c992433f8446418ba10128 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 19 Apr 2022 15:11:28 -0400 Subject: [PATCH 15/49] Call `register_endpoints` when connecting --- bellows/zigbee/application.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index ccac4b98..682766a7 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -133,9 +133,7 @@ async def connect(self): self._in_flight_msg = asyncio.Semaphore(count) LOGGER.debug("APS_UNICAST_MESSAGE_COUNT is set to %s", count) - await self.add_endpoint( - output_clusters=[zigpy.zcl.clusters.security.IasZone.cluster_id] - ) + await self.register_endpoints() try: brd_manuf, brd_name, version = await self._ezsp.get_board_info() From d56d8c603cf347b82300076532e6db59b894e502 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 19 Apr 2022 15:13:45 -0400 Subject: [PATCH 16/49] Always leave the current network during formation --- bellows/zigbee/application.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 682766a7..35df5186 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -150,7 +150,7 @@ async def _ensure_network_running(self) -> bool: """ (state,) = await self._ezsp.networkState() - if state != self._ezsp.types.EmberNetworkStatus.NO_NETWORK: + if state == self._ezsp.types.EmberNetworkStatus.JOINED_NETWORK: return False (init_status,) = await self._ezsp.networkInit() @@ -299,11 +299,11 @@ async def write_network_info( ) -> None: ezsp = self._ezsp - (status,) = await ezsp.networkInit() - assert status in (t.EmberStatus.SUCCESS, t.EmberStatus.NOT_JOINED) - - if status == t.EmberStatus.SUCCESS: + try: (status,) = await ezsp.leaveNetwork() + except bellows.exception.EzspError: + pass + else: if status != t.EmberStatus.NETWORK_DOWN: raise FormationFailure("Couldn't leave network") From ae46b547a7c8af1eca50a2433e310ad0b0102051 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 2 May 2022 14:07:34 -0400 Subject: [PATCH 17/49] Get some unit tests passing --- bellows/zigbee/application.py | 9 ++--- tests/test_application.py | 74 +++++++++++++++++++++++++---------- tests/test_types.py | 24 ------------ 3 files changed, 57 insertions(+), 50 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 35df5186..56649272 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -164,13 +164,7 @@ async def start_network(self): await self._ensure_network_running() - status, node_type, nwk_params = await ezsp.getNetworkParameters() - assert status == t.EmberStatus.SUCCESS # TODO: Better check - if node_type != t.EmberNodeType.COORDINATOR: - raise NetworkNotFormed("Network not configured as coordinator") - await ezsp.update_policies(self.config) - await self.load_network_info(load_devices=False) for cnt_group in self.state.counters: @@ -205,6 +199,9 @@ async def load_network_info(self, *, load_devices=False) -> None: status, node_type, nwk_params = await ezsp.getNetworkParameters() assert status == t.EmberStatus.SUCCESS + if node_type != t.EmberNodeType.COORDINATOR: + raise NetworkNotFormed("Device not configured as coordinator") + (nwk,) = await ezsp.getNodeId() (ieee,) = await ezsp.getEui64() diff --git a/tests/test_application.py b/tests/test_application.py index 59c71bbb..e0a46ee4 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -3,8 +3,7 @@ import pytest import zigpy.config -from zigpy.device import Device -from zigpy.zcl.clusters import security +import zigpy.exceptions import zigpy.zdo.types as zdo_t import bellows.config as config @@ -135,11 +134,49 @@ async def mock_leave(*args, **kwargs): ezsp_mock.getEui64 = AsyncMock(return_value=[ieee]) ezsp_mock.getValue = AsyncMock(return_value=(0, b"\x01" * 6)) ezsp_mock.leaveNetwork = AsyncMock(side_effect=mock_leave) - app.form_network = AsyncMock() ezsp_mock.reset = AsyncMock() ezsp_mock.version = AsyncMock() ezsp_mock.getConfigurationValue = AsyncMock(return_value=(0, 1)) ezsp_mock.update_policies = AsyncMock() + ezsp_mock.networkState = AsyncMock( + return_value=[ezsp_mock.types.EmberNetworkStatus.JOINED_NETWORK] + ) + ezsp_mock.getKey = AsyncMock( + return_value=[ + t.EmberStatus.SUCCESS, + t.EmberKeyStruct( + bitmask=t.EmberKeyStructBitmask.KEY_HAS_SEQUENCE_NUMBER + | t.EmberKeyStructBitmask.KEY_HAS_OUTGOING_FRAME_COUNTER, + type=t.EmberKeyType.CURRENT_NETWORK_KEY, + key=t.EmberKeyData(b"ActualNetworkKey"), + outgoingFrameCounter=t.uint32_t(0x12345678), + incomingFrameCounter=t.uint32_t(0x00000000), + sequenceNumber=t.uint8_t(1), + partnerEUI64=t.EmberEUI64.convert("ff:ff:ff:ff:ff:ff:ff:ff"), + ), + ] + ) + + ezsp_mock.getCurrentSecurityState = AsyncMock( + return_value=[ + t.EmberStatus.SUCCESS, + t.EmberCurrentSecurityState( + bitmask=t.EmberCurrentSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY, + trustCenterLongAddress=t.EmberEUI64.convert("ff:ff:ff:ff:ff:ff:ff:ff"), + ), + ] + ) + ezsp_mock.pre_permit = AsyncMock() + app.permit = AsyncMock() + + def form_network(): + ezsp_mock.getNetworkParameters.return_value = [ + 0, + t.EmberNodeType.COORDINATOR, + nwk_params, + ] + + app.form_network = AsyncMock(side_effect=form_network) p1 = patch.object(bellows.ezsp, "EZSP", new=ezsp_mock) p2 = patch.object(bellows.multicast.Multicast, "startup") @@ -156,8 +193,8 @@ async def test_startup(app, ieee): async def test_startup_nwk_params(app, ieee): assert app.pan_id == 0xFFFE assert app.extended_pan_id == t.ExtendedPanId.convert("ff:ff:ff:ff:ff:ff:ff:ff") - assert app.channel is None - assert app.channels is None + assert app.channel == 0 + assert app.channels == t.Channels.NO_CHANNELS assert app.nwk_update_id == 0 await _test_startup(app, t.EmberNodeType.COORDINATOR, ieee) @@ -189,7 +226,7 @@ async def test_startup_ezsp_ver8(app, ieee): async def test_startup_no_status(app, ieee): """Test when NCP is not a coordinator and not auto forming.""" - with pytest.raises(ControllerError): + with pytest.raises(zigpy.exceptions.NetworkNotFormed): await _test_startup( app, t.EmberNodeType.UNKNOWN_DEVICE, ieee, auto_form=False, init=1 ) @@ -204,7 +241,7 @@ async def test_startup_no_status_form(app, ieee): async def test_startup_end(app, ieee): """Test when NCP is a End Device and not auto forming.""" - with pytest.raises(ControllerError): + with pytest.raises(zigpy.exceptions.NetworkNotFormed): await _test_startup( app, t.EmberNodeType.SLEEPY_END_DEVICE, ieee, auto_form=False ) @@ -215,14 +252,11 @@ async def test_startup_end_form(app, ieee): await _test_startup(app, t.EmberNodeType.SLEEPY_END_DEVICE, ieee, auto_form=True) -async def test_form_network(app): - f = asyncio.Future() - f.set_result([0]) - app._ezsp.setInitialSecurityState.side_effect = [f] - app._ezsp.formNetwork = AsyncMock() - app._ezsp.setValue = AsyncMock() +async def test_write_network_info_failed_leave(app): + app._ezsp.leaveNetwork = AsyncMock(return_value=[t.EmberStatus.BAD_ARGUMENT]) - await app.form_network() + with pytest.raises(zigpy.exceptions.FormationFailure): + await app.form_network() def _frame_handler( @@ -1048,12 +1082,12 @@ async def test_shutdown(app): @pytest.fixture def coordinator(app, ieee): - dev = Device(app, ieee, 0x0000) - ep = dev.add_endpoint(1) - ep.profile_id = 0x0104 - ep.device_type = 0xBEEF - ep.add_output_cluster(security.IasZone.cluster_id) - return bellows.zigbee.application.EZSPCoordinator(app, ieee, 0x0000, dev) + dev = bellows.zigbee.application.EZSPCoordinator(app, ieee, 0x0000) + dev.endpoints[1] = bellows.zigbee.application.EZSPCoordinator.EZSPEndpoint(dev, 1) + dev.model = dev.endpoints[1].model + dev.manufacturer = dev.endpoints[1].manufacturer + + return dev async def test_ezsp_add_to_group(coordinator): diff --git a/tests/test_types.py b/tests/test_types.py index e94785ab..ac2533af 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,5 +1,4 @@ import pytest -import zigpy.types as zt import zigpy.zdo.types as zdo_t import bellows.types as t @@ -222,26 +221,3 @@ def test_ember_node_type_to_zdo_logical_type(node_type, logical_type): node_type = t.EmberNodeType(node_type) assert node_type.zdo_logical_type == zdo_t.LogicalType(logical_type) - - -def test_ember_network_params_to_network_info(): - """Coverage for Ember Network param to zigpy network information converter.""" - - network_params = t.EmberNetworkParameters( - t.ExtendedPanId.convert("ff:7b:aa:bb:cc:dd:ee:ff"), - panId=0xD539, - radioTxPower=8, - radioChannel=20, - joinMethod=t.EmberJoinMethod.USE_MAC_ASSOCIATION, - nwkManagerId=0x1234, - nwkUpdateId=22, - channels=t.Channels.ALL_CHANNELS, - ) - network_info = network_params.zigpy_network_information - assert network_info.extended_pan_id == t.ExtendedPanId.convert( - "ff:7b:aa:bb:cc:dd:ee:ff" - ) - assert network_info.pan_id == zt.PanId(0xD539) - assert network_info.nwk_update_id == 22 - assert network_info.nwk_manager_id == zt.NWK(0x1234) - assert network_info.channel == 20 From 68e492f23d52cd64cdb7d3f7115fe4d9fef5407d Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 3 May 2022 13:13:14 -0400 Subject: [PATCH 18/49] Add unit tests for `bellows.zigbee.util` --- bellows/zigbee/application.py | 30 ++++---- bellows/zigbee/util.py | 50 +++++++------- tests/test_util.py | 126 ++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 39 deletions(-) create mode 100644 tests/test_util.py diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 56649272..869889e2 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -27,7 +27,7 @@ from bellows.ezsp.v8.types.named import EmberDeviceUpdate import bellows.multicast import bellows.types as t -import bellows.zigbee.util +import bellows.zigbee.util as util APS_ACK_TIMEOUT = 120 COUNTER_EZSP_BUFFERS = "EZSP_FREE_BUFFERS" @@ -247,8 +247,8 @@ async def load_network_info(self, *, load_devices=False) -> None: channel=zigpy.types.uint8_t(nwk_params.radioChannel), channel_mask=zigpy.types.Channels(nwk_params.channels), security_level=zigpy.types.uint8_t(security_level), - network_key=bellows.zigbee.util.ezsp_key_to_zigpy_key(network_key, ezsp), - tc_link_key=bellows.zigbee.util.ezsp_key_to_zigpy_key(tc_link_key, ezsp), + network_key=util.ezsp_key_to_zigpy_key(network_key, ezsp), + tc_link_key=util.ezsp_key_to_zigpy_key(tc_link_key, ezsp), key_table=[], children=[], nwk_addresses={}, @@ -269,7 +269,7 @@ async def load_network_info(self, *, load_devices=False) -> None: assert status == t.EmberStatus.SUCCESS self.state.network_info.key_table.append( - bellows.zigbee.util.ezsp_key_to_zigpy_key(key, ezsp) + util.ezsp_key_to_zigpy_key(key, ezsp) ) for idx in range(0, 255 + 1): @@ -331,28 +331,26 @@ async def write_network_info( ) assert status[0] == t.EmberStatus.SUCCESS - hashed_tclk = ezsp.ezsp_version > 4 - if hashed_tclk and not stack_specific.get("hashed_tclk"): + use_hashed_tclk = ezsp.ezsp_version > 4 + + if use_hashed_tclk and not stack_specific.get("hashed_tclk"): + # Generate a random default network_info.stack_specific.setdefault("ezsp", {})[ "hashed_tclk" ] = os.urandom(8).hex() - # TODO: support router mode - initial_security_state = bellows.zigbee.util.zha_security( - network_info, controller=True, hashed_tclk=hashed_tclk + initial_security_state = util.zha_security( + network_info=network_info, + node_info=node_info, + use_hashed_tclk=use_hashed_tclk, ) status = await ezsp.setInitialSecurityState(initial_security_state) # Write keys key_table = [ - ( - bellows.zigbee.util.zigpy_key_to_ezsp_key( - network_info.tc_link_key, ezsp - ), - True, - ) + (util.zigpy_key_to_ezsp_key(network_info.tc_link_key, ezsp), True) ] + [ - (bellows.zigbee.util.zigpy_key_to_ezsp_key(key, ezsp), False) + (util.zigpy_key_to_ezsp_key(key, ezsp), False) for key in network_info.key_table ] diff --git a/bellows/zigbee/util.py b/bellows/zigbee/util.py index 9db4c1c8..3b11da99 100644 --- a/bellows/zigbee/util.py +++ b/bellows/zigbee/util.py @@ -1,25 +1,31 @@ import zigpy.state import zigpy.types as zigpy_t +import zigpy.zdo.types as zdo_t import bellows.types as t def zha_security( + *, network_info: zigpy.state.NetworkInfo, - controller: bool = False, - hashed_tclk: bool = True, + node_info: zigpy.state.NodeInfo, + use_hashed_tclk: bool = True, ) -> t.EmberInitialSecurityState: - + """Construct an `EmberInitialSecurityState` out of zigpy network state.""" isc = t.EmberInitialSecurityState() isc.bitmask = ( t.EmberInitialSecurityBitmask.HAVE_PRECONFIGURED_KEY | t.EmberInitialSecurityBitmask.REQUIRE_ENCRYPTED_KEY + | t.EmberInitialSecurityBitmask.TRUST_CENTER_GLOBAL_LINK_KEY + | t.EmberInitialSecurityBitmask.HAVE_NETWORK_KEY ) - isc.preconfiguredKey = t.EmberKeyData(network_info.tc_link_key.key) isc.networkKey = t.EmberKeyData(network_info.network_key.key) isc.networkKeySequenceNumber = t.uint8_t(network_info.network_key.seq) - if network_info.tc_link_key.partner_ieee != zigpy_t.EUI64.UNKNOWN: + # This field must be set when using commissioning mode + if node_info.logical_type == zdo_t.LogicalType.Coordinator: + assert network_info.tc_link_key.partner_ieee != zigpy_t.EUI64.UNKNOWN + isc.bitmask |= t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 isc.preconfiguredTrustCenterEui64 = t.EmberEUI64( network_info.tc_link_key.partner_ieee @@ -27,22 +33,19 @@ def zha_security( else: isc.preconfiguredTrustCenterEui64 = t.EmberEUI64([0x00] * 8) - if controller: - isc.bitmask |= ( - t.EmberInitialSecurityBitmask.TRUST_CENTER_GLOBAL_LINK_KEY - | t.EmberInitialSecurityBitmask.HAVE_NETWORK_KEY + if use_hashed_tclk: + isc.bitmask |= t.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY + isc.preconfiguredKey, _ = t.EmberKeyData.deserialize( + bytes.fromhex(network_info.stack_specific["ezsp"]["hashed_tclk"]) ) - if hashed_tclk: - isc.preconfiguredKey, _ = t.EmberKeyData.deserialize( - bytes.fromhex(network_info.stack_specific["ezsp"]["hashed_tclk"]) - ) - isc.bitmask |= ( - t.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY - ) + else: + isc.preconfiguredKey = t.EmberKeyData(network_info.tc_link_key.key) + return isc -def ezsp_key_to_zigpy_key(key, ezsp): +def ezsp_key_to_zigpy_key(key, ezsp) -> zigpy.state.Key: + """Convert an `EmberKeyStruct` into a zigpy `Key`.""" zigpy_key = zigpy.state.Key() zigpy_key.key = zigpy_t.KeyData(key.key) @@ -61,25 +64,26 @@ def ezsp_key_to_zigpy_key(key, ezsp): return zigpy_key -def zigpy_key_to_ezsp_key(zigpy_key, ezsp): +def zigpy_key_to_ezsp_key(zigpy_key: zigpy.state.Key, ezsp): + """Convert a zigpy `Key` into a `EmberKeyStruct`.""" key = ezsp.types.EmberKeyStruct() - key.key = zigpy_key.key + key.key = ezsp.types.EmberKeyData(zigpy_key.key) key.bitmask = ezsp.types.EmberKeyStructBitmask(0) if zigpy_key.seq is not None: - key.seq = zigpy_key.seq + key.sequenceNumber = t.uint8_t(zigpy_key.seq) key.bitmask |= ezsp.types.EmberKeyStructBitmask.KEY_HAS_SEQUENCE_NUMBER if zigpy_key.tx_counter is not None: - key.outgoingFrameCounter = zigpy_key.tx_counter + key.outgoingFrameCounter = t.uint32_t(zigpy_key.tx_counter) key.bitmask |= ezsp.types.EmberKeyStructBitmask.KEY_HAS_OUTGOING_FRAME_COUNTER if zigpy_key.rx_counter is not None: - key.outgoingFrameCounter = zigpy_key.rx_counter + key.outgoingFrameCounter = t.uint32_t(zigpy_key.rx_counter) key.bitmask |= ezsp.types.EmberKeyStructBitmask.KEY_HAS_INCOMING_FRAME_COUNTER if zigpy_key.partner_ieee is not None: - key.partnerEUI64 = zigpy_key.partner_ieee + key.partnerEUI64 = t.EmberEUI64(zigpy_key.partner_ieee) key.bitmask |= ezsp.types.EmberKeyStructBitmask.KEY_HAS_PARTNER_EUI64 return key diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 00000000..4550e8c7 --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,126 @@ +import pytest +import zigpy.state +import zigpy.types as t +import zigpy.zdo.types as zdo_t + +import bellows.types as bellows_t +import bellows.zigbee.util as util + +from tests.test_application import ezsp_mock # noqa: F401 + + +@pytest.fixture +def node_info(): + return zigpy.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 zigpy.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=zigpy.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=zigpy.state.Key( + key=t.KeyData(b"ZigBeeAlliance09"), + partner_ieee=node_info.ieee, + tx_counter=8712428, + ), + key_table=[], + children=[], + nwk_addresses={}, + stack_specific={"ezsp": {"hashed_tclk": "71e31105bb92a2d15747a0d0a042dbfd"}}, + ) + + +@pytest.fixture +def zigpy_key(): + return zigpy.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, + rx_counter=1234567, + partner_ieee=t.EUI64.convert("0D:49:91:99:AE:CD:3C:35"), + ) + + +@pytest.fixture +def ezsp_key(ezsp_mock): # noqa: F811 + ezsp = ezsp_mock + return ezsp.types.EmberKeyStruct( + bitmask=( + ezsp.types.EmberKeyStructBitmask.KEY_HAS_SEQUENCE_NUMBER + | ezsp.types.EmberKeyStructBitmask.KEY_HAS_OUTGOING_FRAME_COUNTER + | ezsp.types.EmberKeyStructBitmask.KEY_HAS_INCOMING_FRAME_COUNTER + | ezsp.types.EmberKeyStructBitmask.KEY_HAS_PARTNER_EUI64 + ), + key=ezsp.types.EmberKeyData(bytes.fromhex("9A79D69ADAEC45C6F2EFEBAFDAA307B6")), + sequenceNumber=108, + outgoingFrameCounter=39009277, + incomingFrameCounter=1234567, + partnerEUI64=bellows_t.EmberEUI64.convert("0D:49:91:99:AE:CD:3C:35"), + ) + + +def test_zha_security_normal(network_info, node_info): + security = util.zha_security( + network_info=network_info, node_info=node_info, use_hashed_tclk=True + ) + + assert ( + security.preconfiguredTrustCenterEui64 == network_info.tc_link_key.partner_ieee + ) + assert ( + security.bitmask & bellows_t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 + == bellows_t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 + ) + + assert ( + security.preconfiguredKey.serialize().hex() + == network_info.stack_specific["ezsp"]["hashed_tclk"] + ) + assert ( + security.bitmask + & bellows_t.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY + == bellows_t.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY + ) + + +def test_zha_security_router(network_info, node_info): + router_node_info = node_info.replace(logical_type=zdo_t.LogicalType.Router) + security = util.zha_security( + network_info=network_info, node_info=router_node_info, use_hashed_tclk=False + ) + + assert security.preconfiguredTrustCenterEui64 == bellows_t.EmberEUI64([0x00] * 8) + assert ( + security.bitmask & bellows_t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 + != bellows_t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 + ) + + assert security.preconfiguredKey == network_info.tc_link_key.key + assert ( + security.bitmask + & bellows_t.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY + != bellows_t.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY + ) + + +def test_ezsp_key_to_zigpy_key(zigpy_key, ezsp_key, ezsp_mock): # noqa: F811 + return util.ezsp_key_to_zigpy_key(ezsp_key, ezsp_mock) == zigpy_key + + +def test_zigpy_key_to_ezsp_key(zigpy_key, ezsp_key, ezsp_mock): # noqa: F811 + return util.zigpy_key_to_ezsp_key(zigpy_key, ezsp_mock) == ezsp_key From 8c84e628564d2fd15047071d26e1b6a14faa85a4 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 3 May 2022 21:24:29 -0400 Subject: [PATCH 19/49] Increase `bellows.uart` test coverage to 100% --- tests/test_uart.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_uart.py b/tests/test_uart.py index 25d0c639..5f193b27 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -12,7 +12,8 @@ pytestmark = pytest.mark.asyncio -async def test_connect(monkeypatch): +@pytest.mark.parametrize("flow_control", [conf.CONF_FLOW_CONTROL_DEFAULT, "hardware"]) +async def test_connect(flow_control, monkeypatch): appmock = MagicMock() transport = MagicMock() @@ -24,7 +25,11 @@ async def mockconnect(loop, protocol_factory, **kwargs): monkeypatch.setattr(serial_asyncio, "create_serial_connection", mockconnect) gw = await uart.connect( conf.SCHEMA_DEVICE( - {conf.CONF_DEVICE_PATH: "/dev/serial", conf.CONF_DEVICE_BAUDRATE: 115200} + { + conf.CONF_DEVICE_PATH: "/dev/serial", + conf.CONF_DEVICE_BAUDRATE: 115200, + conf.CONF_FLOW_CONTROL: flow_control, + } ), appmock, use_thread=False, From de1e1e3aff12e536160771791a1f308c20d76256 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 3 May 2022 21:25:16 -0400 Subject: [PATCH 20/49] Ignore `.DS_Store` files --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index faf75845..15f03246 100644 --- a/.gitignore +++ b/.gitignore @@ -70,4 +70,8 @@ ENV/ .*.swp # Visual Studio Code -.vscode \ No newline at end of file +.vscode + + +# macOS +.DS_Store \ No newline at end of file From 3579f0f436b8e41a6b8af5cefad527d6b59fd6ee Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 3 May 2022 21:34:27 -0400 Subject: [PATCH 21/49] Use new `pytest-asyncio` behavior --- requirements_test.txt | 2 +- setup.cfg | 3 +++ tests/test_application.py | 2 -- tests/test_ezsp.py | 2 -- tests/test_ezsp_protocol.py | 6 ------ tests/test_ezsp_v5.py | 2 -- tests/test_ezsp_v8.py | 4 ---- tests/test_multicast.py | 11 ----------- tests/test_thread.py | 2 -- tests/test_uart.py | 2 -- 10 files changed, 4 insertions(+), 32 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index beb7dcb7..6b9647b0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,6 +4,6 @@ asynctest coveralls==2.1.2 pre-commit pytest -pytest-asyncio +pytest-asyncio>=0.17 pytest-cov pytest-timeout diff --git a/setup.cfg b/setup.cfg index 3f801541..04f06efc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,3 +19,6 @@ force_sort_within_sections = true known_first_party = bellows,tests forced_separate = tests combine_as_imports = true + +[tool:pytest] +asyncio_mode = auto \ No newline at end of file diff --git a/tests/test_application.py b/tests/test_application.py index e0a46ee4..702e5fc5 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -20,8 +20,6 @@ from .async_mock import AsyncMock, MagicMock, PropertyMock, patch, sentinel -pytestmark = pytest.mark.asyncio - APP_CONFIG = { config.CONF_DEVICE: { config.CONF_DEVICE_PATH: "/dev/null", diff --git a/tests/test_ezsp.py b/tests/test_ezsp.py index 23ac5b36..95581048 100644 --- a/tests/test_ezsp.py +++ b/tests/test_ezsp.py @@ -15,8 +15,6 @@ config.CONF_DEVICE_BAUDRATE: 115200, } -pytestmark = pytest.mark.asyncio - @pytest.fixture async def ezsp_f(): diff --git a/tests/test_ezsp_protocol.py b/tests/test_ezsp_protocol.py index 754ad2ba..94595378 100644 --- a/tests/test_ezsp_protocol.py +++ b/tests/test_ezsp_protocol.py @@ -8,8 +8,6 @@ from .async_mock import AsyncMock, MagicMock, patch -pytestmark = pytest.mark.asyncio - class _DummyProtocolHandler(bellows.ezsp.v4.EZSPv4): """Protocol handler mock.""" @@ -29,7 +27,6 @@ def prot_hndl(): return _DummyProtocolHandler(MagicMock(), MagicMock()) -@pytest.mark.asyncio async def test_command(prot_hndl): coro = prot_hndl.command("nop") prot_hndl._awaiting[prot_hndl._seq - 1][2].set_result(True) @@ -73,7 +70,6 @@ def test_receive_reply_invalid_command(prot_hndl): assert prot_hndl.cb_mock.call_count == 0 -@pytest.mark.asyncio async def test_cfg_initialize(prot_hndl, caplog): """Test initialization.""" @@ -94,7 +90,6 @@ async def test_cfg_initialize(prot_hndl, caplog): assert "Couldn't set" in caplog.text -@pytest.mark.asyncio async def test_update_policies(prot_hndl): """Test update_policies.""" @@ -107,7 +102,6 @@ async def test_update_policies(prot_hndl): await prot_hndl.update_policies({"ezsp_policies": {}}) -@pytest.mark.asyncio @pytest.mark.parametrize( "status, raw, expected_value", ( diff --git a/tests/test_ezsp_v5.py b/tests/test_ezsp_v5.py index 41a78396..714bc25d 100644 --- a/tests/test_ezsp_v5.py +++ b/tests/test_ezsp_v5.py @@ -4,8 +4,6 @@ from .async_mock import AsyncMock, MagicMock, patch -pytestmark = pytest.mark.asyncio - @pytest.fixture def ezsp_f(): diff --git a/tests/test_ezsp_v8.py b/tests/test_ezsp_v8.py index 6ea14672..a39f9384 100644 --- a/tests/test_ezsp_v8.py +++ b/tests/test_ezsp_v8.py @@ -4,8 +4,6 @@ from .async_mock import AsyncMock, MagicMock, patch -pytestmark = pytest.mark.asyncio - @pytest.fixture def ezsp_f(): @@ -27,7 +25,6 @@ def test_ezsp_frame_rx(ezsp_f): assert ezsp_f._handle_callback.call_args[0][1] == [0x01, 0x02, 0x1234] -@pytest.mark.asyncio async def test_set_source_routing(ezsp_f): """Test setting source routing.""" with patch.object( @@ -37,7 +34,6 @@ async def test_set_source_routing(ezsp_f): assert src_mock.await_count == 1 -@pytest.mark.asyncio async def test_pre_permit(ezsp_f): """Test pre permit.""" p1 = patch.object(ezsp_f, "setPolicy", new=AsyncMock()) diff --git a/tests/test_multicast.py b/tests/test_multicast.py index 5ae884c1..47d5a6ee 100644 --- a/tests/test_multicast.py +++ b/tests/test_multicast.py @@ -7,8 +7,6 @@ from .async_mock import AsyncMock, MagicMock, sentinel -pytestmark = pytest.mark.asyncio - CUSTOM_SIZE = 12 @@ -25,7 +23,6 @@ def multicast(ezsp_f): return m -@pytest.mark.asyncio async def test_initialize(multicast): group_id = 0x0200 mct = active_multicasts = 4 @@ -49,7 +46,6 @@ async def mock_get(*args): assert len(multicast._available) == CUSTOM_SIZE - active_multicasts -@pytest.mark.asyncio async def test_initialize_fail_configured_size(multicast): multicast._ezsp.getConfigurationValue.return_value = t.EmberStatus.ERR_FATAL, 16 @@ -59,7 +55,6 @@ async def test_initialize_fail_configured_size(multicast): assert len(multicast._available) == 0 -@pytest.mark.asyncio async def test_initialize_fail(multicast): group_id = 0x0200 @@ -79,7 +74,6 @@ async def mock_get(*args): assert len(multicast._available) == 0 -@pytest.mark.asyncio async def test_startup(multicast): coordinator = MagicMock() @@ -107,7 +101,6 @@ async def mock_set(*args): return multicast.subscribe(group_id) -@pytest.mark.asyncio async def test_subscribe(multicast): grp_id = 0x0200 multicast._available.add(1) @@ -128,7 +121,6 @@ async def test_subscribe(multicast): assert grp_id in multicast._multicast -@pytest.mark.asyncio async def test_subscribe_fail(multicast): grp_id = 0x0200 multicast._available.add(1) @@ -142,7 +134,6 @@ async def test_subscribe_fail(multicast): assert len(multicast._available) == 1 -@pytest.mark.asyncio async def test_subscribe_no_avail(multicast): grp_id = 0x0200 @@ -161,7 +152,6 @@ async def mock_set(*args): return multicast.unsubscribe(group_id) -@pytest.mark.asyncio async def test_unsubscribe(multicast): grp_id = 0x0200 multicast._available.add(1) @@ -185,7 +175,6 @@ async def test_unsubscribe(multicast): assert len(multicast._available) == 1 -@pytest.mark.asyncio async def test_unsubscribe_fail(multicast): grp_id = 0x0200 multicast._available.add(1) diff --git a/tests/test_thread.py b/tests/test_thread.py index fa51cfee..561c7cc1 100644 --- a/tests/test_thread.py +++ b/tests/test_thread.py @@ -7,8 +7,6 @@ from bellows.thread import EventLoopThread, ThreadsafeProxy -pytestmark = pytest.mark.asyncio - async def test_thread_start(monkeypatch): current_loop = asyncio.get_event_loop() diff --git a/tests/test_uart.py b/tests/test_uart.py index 5f193b27..7d27ccf4 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -9,8 +9,6 @@ from .async_mock import AsyncMock, MagicMock, sentinel -pytestmark = pytest.mark.asyncio - @pytest.mark.parametrize("flow_control", [conf.CONF_FLOW_CONTROL_DEFAULT, "hardware"]) async def test_connect(flow_control, monkeypatch): From 47fa17478d3eccfb32c0d38bd492d6dfd50da42c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 4 May 2022 21:40:47 -0400 Subject: [PATCH 22/49] Newer versions of EmberZNet use a different response for `getChildData` --- bellows/ezsp/v7/commands.py | 2 +- bellows/ezsp/v7/types/struct.py | 22 ++++++++++++++++++++++ bellows/ezsp/v8/commands.py | 2 +- bellows/ezsp/v8/types/struct.py | 22 ++++++++++++++++++++++ bellows/zigbee/application.py | 9 ++++++++- 5 files changed, 54 insertions(+), 3 deletions(-) diff --git a/bellows/ezsp/v7/commands.py b/bellows/ezsp/v7/commands.py index 3da7314b..4c0d89bd 100644 --- a/bellows/ezsp/v7/commands.py +++ b/bellows/ezsp/v7/commands.py @@ -137,7 +137,7 @@ "getChildData": ( 0x4A, (t.uint8_t,), - (t.EmberStatus, t.EmberNodeId, t.EmberEUI64, t.EmberNodeType), + (t.EmberStatus, t.EmberChildData), ), "getSourceRouteTableTotalSize": (0xC3, (), (t.uint8_t,)), "getSourceRouteTableFilledSize": (0xC2, (), (t.uint8_t,)), diff --git a/bellows/ezsp/v7/types/struct.py b/bellows/ezsp/v7/types/struct.py index 7a454846..be0259dc 100644 --- a/bellows/ezsp/v7/types/struct.py +++ b/bellows/ezsp/v7/types/struct.py @@ -189,3 +189,25 @@ class EmberPerDeviceDutyCycle(EzspStruct): nodeId: named.EmberNodeId # Amount of overall duty cycle consumed (up to suspend limit). dutyCycleConsumed: named.EmberDutyCycleHectoPct + + +class EmberChildData(EzspStruct): + """A structure containing a child node's data.""" + + # The EUI64 of the child + eui64: named.EmberEUI64 + # The node type of the child + type: named.EmberNodeType + # The short address of the child + id: named.EmberNodeId + # The phy of the child + phy: basic.uint8_t + # The power of the child + power: basic.uint8_t + # The timeout of the child + timeout: basic.uint8_t + + # The GPD's EUI64. + # gpdIeeeAddress: named.EmberEUI64 + # The GPD's source ID. + # sourceId: basic.uint32_t diff --git a/bellows/ezsp/v8/commands.py b/bellows/ezsp/v8/commands.py index 0c6a5925..ea2b07cc 100644 --- a/bellows/ezsp/v8/commands.py +++ b/bellows/ezsp/v8/commands.py @@ -134,7 +134,7 @@ "getChildData": ( 0x004A, (t.uint8_t,), - (t.EmberStatus, t.EmberNodeId, t.EmberEUI64, t.EmberNodeType), + (t.EmberStatus, t.EmberChildData), ), "getSourceRouteTableTotalSize": (0x00C3, (), (t.uint8_t,)), "getSourceRouteTableFilledSize": (0x00C2, (), (t.uint8_t,)), diff --git a/bellows/ezsp/v8/types/struct.py b/bellows/ezsp/v8/types/struct.py index 8a0fd328..f1a56628 100644 --- a/bellows/ezsp/v8/types/struct.py +++ b/bellows/ezsp/v8/types/struct.py @@ -202,3 +202,25 @@ class EmberTransientKeyData(EzspStruct): # The number of seconds remaining before the key is automatically timed out of the # transient key table. remainingTimeSeconds: basic.uint16_t + + +class EmberChildData(EzspStruct): + """A structure containing a child node's data.""" + + # The EUI64 of the child + eui64: named.EmberEUI64 + # The node type of the child + type: named.EmberNodeType + # The short address of the child + id: named.EmberNodeId + # The phy of the child + phy: basic.uint8_t + # The power of the child + power: basic.uint8_t + # The timeout of the child + timeout: basic.uint8_t + + # The GPD's EUI64. + # gpdIeeeAddress: named.EmberEUI64 + # The GPD's source ID. + # sourceId: basic.uint32_t diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 869889e2..67a5678d 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -273,11 +273,18 @@ async def load_network_info(self, *, load_devices=False) -> None: ) for idx in range(0, 255 + 1): - (status, nwk, eui64, node_type) = await ezsp.getChildData(idx) + (status, *rsp) = await ezsp.getChildData(idx) if status == t.EmberStatus.NOT_JOINED: continue + if ezsp.ezsp_version >= 7: + nwk = rsp[0].id + eui64 = rsp[0].eui64 + node_type = rsp[0].type + else: + nwk, eui64, node_type = rsp + self.state.network_info.children.append(eui64) self.state.network_info.nwk_addresses[eui64] = nwk From 4db1ee4dd43972f482429b6616296af8fba4df53 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 4 May 2022 21:41:43 -0400 Subject: [PATCH 23/49] Use zigpy data types when populating `state` --- bellows/zigbee/application.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 67a5678d..93f5651a 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -296,7 +296,9 @@ async def load_network_info(self, *, load_devices=False) -> None: if nwk in t.EmberDistinguishedNodeId.__members__.values(): continue - self.state.network_info.nwk_addresses[eui64] = nwk + self.state.network_info.nwk_addresses[ + zigpy.types.EUI64(eui64) + ] = zigpy.types.NWK(nwk) async def write_network_info( self, *, network_info: zigpy.state.NetworkInfo, node_info: zigpy.state.NodeInfo From 4b5f4eaf9de3592a8118c4e67b072d74a22e3b59 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 4 May 2022 21:42:22 -0400 Subject: [PATCH 24/49] Correct the TCLK's `partner_ieee` to use the node's IEEE address --- bellows/zigbee/application.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 93f5651a..3db0d571 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -222,10 +222,11 @@ async def load_network_info(self, *, load_devices=False) -> None: ) assert status == t.EmberStatus.SUCCESS - (status, tc_link_key) = await ezsp.getKey( + (status, ezsp_tc_link_key) = await ezsp.getKey( ezsp.types.EmberKeyType.TRUST_CENTER_LINK_KEY ) assert status == t.EmberStatus.SUCCESS + tc_link_key = util.ezsp_key_to_zigpy_key(ezsp_tc_link_key, ezsp) (status, state) = await ezsp.getCurrentSecurityState() assert status == t.EmberStatus.SUCCESS @@ -233,12 +234,16 @@ async def load_network_info(self, *, load_devices=False) -> None: stack_specific = {} if ( - state.bitmask - & ezsp.types.EmberCurrentSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY + ezsp.types.EmberCurrentSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY + in state.bitmask ): stack_specific["ezsp"] = {"hashed_tclk": tc_link_key.key.serialize().hex()} tc_link_key.key = zigpy.types.KeyData(b"ZigBeeAlliance09") + # The TCLK IEEE address is returned as `FF:FF:FF:FF:FF:FF:FF:FF` + if self.state.node_info.logical_type == zdo_t.LogicalType.Coordinator: + tc_link_key.partner_ieee = self.state.node_info.ieee + self.state.network_info = zigpy.state.NetworkInfo( extended_pan_id=zigpy.types.ExtendedPanId(nwk_params.extendedPanId), pan_id=zigpy.types.PanId(nwk_params.panId), @@ -248,7 +253,7 @@ async def load_network_info(self, *, load_devices=False) -> None: channel_mask=zigpy.types.Channels(nwk_params.channels), security_level=zigpy.types.uint8_t(security_level), network_key=util.ezsp_key_to_zigpy_key(network_key, ezsp), - tc_link_key=util.ezsp_key_to_zigpy_key(tc_link_key, ezsp), + tc_link_key=tc_link_key, key_table=[], children=[], nwk_addresses={}, From 08187457528374a48b94935ff4940ead820bd371 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 4 May 2022 21:45:03 -0400 Subject: [PATCH 25/49] Unit test `load_network_info` Foo --- bellows/zigbee/util.py | 8 +- tests/test_application.py | 1 + tests/test_application_network_state.py | 237 ++++++++++++++++++++++++ 3 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 tests/test_application_network_state.py diff --git a/bellows/zigbee/util.py b/bellows/zigbee/util.py index 3b11da99..078b7827 100644 --- a/bellows/zigbee/util.py +++ b/bellows/zigbee/util.py @@ -49,16 +49,16 @@ def ezsp_key_to_zigpy_key(key, ezsp) -> zigpy.state.Key: zigpy_key = zigpy.state.Key() zigpy_key.key = zigpy_t.KeyData(key.key) - if key.bitmask & ezsp.types.EmberKeyStructBitmask.KEY_HAS_SEQUENCE_NUMBER: + if ezsp.types.EmberKeyStructBitmask.KEY_HAS_SEQUENCE_NUMBER in key.bitmask: zigpy_key.seq = key.sequenceNumber - if key.bitmask & ezsp.types.EmberKeyStructBitmask.KEY_HAS_OUTGOING_FRAME_COUNTER: + if ezsp.types.EmberKeyStructBitmask.KEY_HAS_OUTGOING_FRAME_COUNTER in key.bitmask: zigpy_key.tx_counter = key.outgoingFrameCounter - if key.bitmask & ezsp.types.EmberKeyStructBitmask.KEY_HAS_INCOMING_FRAME_COUNTER: + if ezsp.types.EmberKeyStructBitmask.KEY_HAS_INCOMING_FRAME_COUNTER in key.bitmask: zigpy_key.rx_counter = key.incomingFrameCounter - if key.bitmask & ezsp.types.EmberKeyStructBitmask.KEY_HAS_PARTNER_EUI64: + if ezsp.types.EmberKeyStructBitmask.KEY_HAS_PARTNER_EUI64 in key.bitmask: zigpy_key.partner_ieee = key.partnerEUI64 return zigpy_key diff --git a/tests/test_application.py b/tests/test_application.py index 702e5fc5..b77cc8f5 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -114,6 +114,7 @@ async def mock_leave(*args, **kwargs): app._in_flight_msg = None ezsp_mock = MagicMock() + ezsp_mock.types = ezsp_t7 type(ezsp_mock).ezsp_version = PropertyMock(return_value=ezsp_version) ezsp_mock.initialize = AsyncMock(return_value=ezsp_mock) ezsp_mock.connect = AsyncMock() diff --git a/tests/test_application_network_state.py b/tests/test_application_network_state.py new file mode 100644 index 00000000..4eda0285 --- /dev/null +++ b/tests/test_application_network_state.py @@ -0,0 +1,237 @@ +import pytest +import zigpy.state +import zigpy.types as zigpy_t +import zigpy.zdo.types as zdo_t + +import bellows.types as t + +from tests.async_mock import AsyncMock +from tests.test_application import app, ezsp_mock # noqa: F401 + + +@pytest.fixture +def node_info(): + return zigpy.state.NodeInfo( + nwk=zigpy_t.NWK(0x0000), + ieee=zigpy_t.EUI64.convert("00:12:4b:00:1c:a1:b8:46"), + logical_type=zdo_t.LogicalType.Coordinator, + ) + + +@pytest.fixture +def network_info(node_info): + return zigpy.state.NetworkInfo( + extended_pan_id=zigpy_t.ExtendedPanId.convert("bd:27:0b:38:37:95:dc:87"), + pan_id=zigpy_t.PanId(0x9BB0), + nwk_update_id=18, + nwk_manager_id=zigpy_t.NWK(0x0000), + channel=zigpy_t.uint8_t(15), + channel_mask=zigpy_t.Channels.ALL_CHANNELS, + security_level=zigpy_t.uint8_t(5), + network_key=zigpy.state.Key( + key=zigpy_t.KeyData.convert("2ccade06b3090c310315b3d574d3c85a"), + seq=108, + tx_counter=118785, + ), + tc_link_key=zigpy.state.Key( + key=zigpy_t.KeyData(b"ZigBeeAlliance09"), + partner_ieee=node_info.ieee, + tx_counter=8712428, + ), + key_table=[ + zigpy.state.Key( + key=zigpy_t.KeyData.convert( + "85:7C:05:00:3E:76:1A:F9:68:9A:49:41:6A:60:5C:76" + ), + tx_counter=3792973670, + rx_counter=1083290572, + seq=147, + partner_ieee=zigpy_t.EUI64.convert("CC:CC:CC:FF:FE:E6:8E:CA"), + ), + zigpy.state.Key( + key=zigpy_t.KeyData.convert( + "CA:02:E8:BB:75:7C:94:F8:93:39:D3:9C:B3:CD:A7:BE" + ), + tx_counter=2597245184, + rx_counter=824424412, + seq=19, + partner_ieee=zigpy_t.EUI64.convert("EC:1B:BD:FF:FE:2F:41:A4"), + ), + ], + children=[zigpy_t.EUI64.convert("00:0B:57:FF:FE:2B:D4:57")], + # If exposed by the stack, NWK addresses of other connected devices on the network + nwk_addresses={ + # Two routers + zigpy_t.EUI64.convert("CC:CC:CC:FF:FE:E6:8E:CA"): zigpy_t.NWK(0x44CB), + zigpy_t.EUI64.convert("EC:1B:BD:FF:FE:2F:41:A4"): zigpy_t.NWK(0x0702), + # Child device + zigpy_t.EUI64.convert("00:0b:57:ff:fe:2b:d4:57"): zigpy_t.NWK(0xC06B), + }, + stack_specific={"ezsp": {"hashed_tclk": "abcdabcdabcdabcdabcdabcdabcdabcd"}}, + ) + + +def _mock_app_for_load(app): # noqa: F811 + ezsp = app._ezsp + + app._ensure_network_running = AsyncMock() + ezsp.getNetworkParameters = AsyncMock( + return_value=[ + t.EmberStatus.SUCCESS, + t.EmberNodeType.COORDINATOR, + t.EmberNetworkParameters( + extendedPanId=t.ExtendedPanId.convert("bd:27:0b:38:37:95:dc:87"), + panId=t.EmberPanId(0x9BB0), + radioTxPower=8, + radioChannel=15, + joinMethod=t.EmberJoinMethod.USE_MAC_ASSOCIATION, + nwkManagerId=t.EmberNodeId(0x0000), + nwkUpdateId=18, + channels=t.Channels.ALL_CHANNELS, + ), + ] + ) + + ezsp.getNodeId = AsyncMock(return_value=[t.EmberNodeId(0x0000)]) + ezsp.getEui64 = AsyncMock( + return_value=[t.EmberEUI64.convert("00:12:4b:00:1c:a1:b8:46")] + ) + ezsp.getConfigurationValue = AsyncMock(return_value=[t.EmberStatus.SUCCESS, 5]) + + def get_key(key_type): + key = { + ezsp.types.EmberKeyType.CURRENT_NETWORK_KEY: ezsp.types.EmberKeyStruct( + bitmask=( + t.EmberKeyStructBitmask.KEY_HAS_OUTGOING_FRAME_COUNTER + | t.EmberKeyStructBitmask.KEY_HAS_SEQUENCE_NUMBER + ), + type=ezsp.types.EmberKeyType.CURRENT_NETWORK_KEY, + key=t.EmberKeyData(bytes.fromhex("2ccade06b3090c310315b3d574d3c85a")), + outgoingFrameCounter=118785, + incomingFrameCounter=0, + sequenceNumber=108, + partnerEUI64=t.EmberEUI64.convert("00:00:00:00:00:00:00:00"), + ), + ezsp.types.EmberKeyType.TRUST_CENTER_LINK_KEY: ezsp.types.EmberKeyStruct( + bitmask=( + t.EmberKeyStructBitmask.KEY_IS_AUTHORIZED + | t.EmberKeyStructBitmask.KEY_HAS_PARTNER_EUI64 + | t.EmberKeyStructBitmask.KEY_HAS_OUTGOING_FRAME_COUNTER + ), + type=ezsp.types.EmberKeyType.TRUST_CENTER_LINK_KEY, + key=t.EmberKeyData(bytes.fromhex("abcdabcdabcdabcdabcdabcdabcdabcd")), + outgoingFrameCounter=8712428, + incomingFrameCounter=0, + sequenceNumber=0, + partnerEUI64=t.EmberEUI64.convert("00:12:4b:00:1c:a1:b8:46"), + ), + }[key_type] + + return (t.EmberStatus.SUCCESS, key) + + ezsp.getKey = AsyncMock(side_effect=get_key) + ezsp.getCurrentSecurityState = AsyncMock( + return_value=( + t.EmberStatus.SUCCESS, + ezsp.types.EmberCurrentSecurityState( + bitmask=( + t.EmberCurrentSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY + | 64 + | 32 + | t.EmberCurrentSecurityBitmask.HAVE_TRUST_CENTER_LINK_KEY + | t.EmberCurrentSecurityBitmask.GLOBAL_LINK_KEY + ), + trustCenterLongAddress=t.EmberEUI64.convert("00:12:4b:00:1c:a1:b8:46"), + ), + ) + ) + + +async def test_load_network_info_no_devices(app, network_info, node_info): # noqa: F811 + _mock_app_for_load(app) + + await app.load_network_info(load_devices=False) + + assert app.state.node_info == node_info + assert app.state.network_info == network_info.replace( + key_table=[], children=[], nwk_addresses={} + ) + + +async def test_load_network_info_with_devices( + app, network_info, node_info # noqa: F811 +): + _mock_app_for_load(app) + + def get_child_data(index): + if index == 0: + status = t.EmberStatus.SUCCESS + else: + status = t.EmberStatus.NOT_JOINED + + return ( + status, + app._ezsp.types.EmberChildData( + eui64=t.EmberEUI64.convert("00:0b:57:ff:fe:2b:d4:57"), + type=t.EmberNodeType.SLEEPY_END_DEVICE, + id=t.EmberNodeId(0xC06B), + phy=0, + power=128, + timeout=3, + ), + ) + + app._ezsp.getChildData = AsyncMock(side_effect=get_child_data) + + def get_key_table_entry(index): + if index < 14: + status = t.EmberStatus.TABLE_ENTRY_ERASED + else: + status = t.EmberStatus.INDEX_OUT_OF_RANGE + + return ( + status, + app._ezsp.types.EmberKeyStruct( + bitmask=t.EmberKeyStructBitmask(244), + type=app._ezsp.types.EmberKeyType(0x46), + key=t.EmberKeyData(bytes.fromhex("b8a11c004b1200cdabcdabcdabcdabcd")), + outgoingFrameCounter=8192, + incomingFrameCounter=0, + sequenceNumber=0, + partnerEUI64=t.EmberEUI64.convert("00:12:4b:00:1c:a1:b8:46"), + ), + ) + + app._ezsp.getKeyTableEntry = AsyncMock(side_effect=get_key_table_entry) + + def get_addr_table_node_id(index): + return ( + { + 16: t.EmberNodeId(0x44CB), + 17: t.EmberNodeId(0x0702), + }.get(index, t.EmberNodeId(0xFFFF)), + ) + + app._ezsp.getAddressTableRemoteNodeId = AsyncMock( + side_effect=get_addr_table_node_id + ) + + def get_addr_table_eui64(index): + if index < 16: + return (t.EmberEUI64.convert("ff:ff:ff:ff:ff:ff:ff:ff"),) + elif 16 <= index <= 17: + return ( + { + 16: t.EmberEUI64.convert("cc:cc:cc:ff:fe:e6:8e:ca"), + 17: t.EmberEUI64.convert("ec:1b:bd:ff:fe:2f:41:a4"), + }[index], + ) + else: + return (t.EmberEUI64.convert("00:00:00:00:00:00:00:00"),) + + app._ezsp.getAddressTableRemoteEui64 = AsyncMock(side_effect=get_addr_table_eui64) + + await app.load_network_info(load_devices=True) + + assert app.state.node_info == node_info + assert app.state.network_info == network_info.replace(key_table=[]) From d0178603f9072a77581bfad4749be105b22c26a8 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 4 May 2022 21:51:39 -0400 Subject: [PATCH 26/49] Assume the TCLK is always the well-known key --- bellows/zigbee/application.py | 9 +++------ bellows/zigbee/util.py | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 3db0d571..a9e2a5f7 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -362,10 +362,7 @@ async def write_network_info( # Write keys key_table = [ - (util.zigpy_key_to_ezsp_key(network_info.tc_link_key, ezsp), True) - ] + [ - (util.zigpy_key_to_ezsp_key(key, ezsp), False) - for key in network_info.key_table + util.zigpy_key_to_ezsp_key(key, ezsp) for key in network_info.key_table ] (status, key_table_size) = await ezsp.getConfigurationValue( @@ -379,10 +376,10 @@ async def write_network_info( ) assert status == t.EmberStatus.SUCCESS - for index, (key, is_link_key) in enumerate(key_table): + for key in key_table: # XXX: is there no way to set the outgoing frame counter per key? (status,) = await ezsp.addOrUpdateKeyTableEntry( - key.partnerEUI64, is_link_key, key.key + key.partnerEUI64, True, key.key ) if status != t.EmberStatus.SUCCESS: LOGGER.warning("Couldn't add %s key: %s", key, status) diff --git a/bellows/zigbee/util.py b/bellows/zigbee/util.py index 078b7827..f298f877 100644 --- a/bellows/zigbee/util.py +++ b/bellows/zigbee/util.py @@ -1,15 +1,19 @@ +import logging + import zigpy.state import zigpy.types as zigpy_t import zigpy.zdo.types as zdo_t import bellows.types as t +LOGGER = logging.getLogger(__name__) + def zha_security( *, network_info: zigpy.state.NetworkInfo, node_info: zigpy.state.NodeInfo, - use_hashed_tclk: bool = True, + use_hashed_tclk: bool, ) -> t.EmberInitialSecurityState: """Construct an `EmberInitialSecurityState` out of zigpy network state.""" isc = t.EmberInitialSecurityState() @@ -24,16 +28,20 @@ def zha_security( # This field must be set when using commissioning mode if node_info.logical_type == zdo_t.LogicalType.Coordinator: - assert network_info.tc_link_key.partner_ieee != zigpy_t.EUI64.UNKNOWN + if network_info.tc_link_key.partner_ieee == zigpy_t.EUI64.UNKNOWN: + partner_ieee = node_info.ieee + else: + partner_ieee = network_info.tc_link_key.partner_ieee isc.bitmask |= t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 - isc.preconfiguredTrustCenterEui64 = t.EmberEUI64( - network_info.tc_link_key.partner_ieee - ) + isc.preconfiguredTrustCenterEui64 = t.EmberEUI64(partner_ieee) else: isc.preconfiguredTrustCenterEui64 = t.EmberEUI64([0x00] * 8) if use_hashed_tclk: + if network_info.tc_link_key.key != zigpy_t.KeyData(b"ZigBeeAlliance09"): + LOGGER.warning("Only the well-known TC Link Key is supported") + isc.bitmask |= t.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY isc.preconfiguredKey, _ = t.EmberKeyData.deserialize( bytes.fromhex(network_info.stack_specific["ezsp"]["hashed_tclk"]) From 1ce47e3a6801951b0b040e2f22ef2e9c91ce9d64 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 4 May 2022 22:37:54 -0400 Subject: [PATCH 27/49] Add a few more unknown bitfields --- bellows/types/named.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bellows/types/named.py b/bellows/types/named.py index c08f46dc..42182e18 100644 --- a/bellows/types/named.py +++ b/bellows/types/named.py @@ -966,6 +966,8 @@ class EmberCurrentSecurityBitmask(basic.bitmap16): GLOBAL_LINK_KEY = 0x0004 # This denotes that the node has a Trust Center Link Key. HAVE_TRUST_CENTER_LINK_KEY = 0x0010 + # TODO: 0x0020 is unknown + # TODO: 0x0040 is unknown # This denotes that the Trust Center is using a Hashed Link Key. TRUST_CENTER_USES_HASHED_LINK_KEY = 0x0084 @@ -990,6 +992,12 @@ class EmberKeyStructBitmask(basic.bitmap16): # hears a device announce from the partner indicating it is not # an 'RX on when idle' device. KEY_PARTNER_IS_SLEEPY = 0x0020 + # This indicates that the transient key which is being added is unconfirmed. This + # bit is set when we add a transient key while the EmberTcLinkKeyRequestPolicy is + # EMBER_ALLOW_TC_LINK_KEY_REQUEST_AND_GENERATE_NEW_KEY + UNCONFIRMED_TRANSIENT_KEY = 0x0040 + + # TODO: 0x0080 is unknown class EmberKeyStatus(basic.enum8): From 07f21b75870a585263bf962c1b2efc5462b7a071 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 4 May 2022 22:50:54 -0400 Subject: [PATCH 28/49] Refactor code dealing with keys and add some comments --- bellows/zigbee/application.py | 44 +++++++++++++------------ tests/test_application_network_state.py | 3 ++ 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index a9e2a5f7..2643b045 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -312,38 +312,34 @@ async def write_network_info( try: (status,) = await ezsp.leaveNetwork() - except bellows.exception.EzspError: - pass - else: if status != t.EmberStatus.NETWORK_DOWN: raise FormationFailure("Couldn't leave network") - - # Is this necessary? - (status,) = await ezsp.clearKeyTable() - assert status == t.EmberStatus.SUCCESS + except bellows.exception.EzspError: + pass stack_specific = network_info.stack_specific.get("ezsp", {}) (current_eui64,) = await ezsp.getEui64() - should_update_eui64 = stack_specific.get( - "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" - ) if ( node_info.ieee != zigpy.types.EUI64.UNKNOWN and node_info.ieee != current_eui64 ): - if not should_update_eui64: - LOGGER.warning( - "Current node's IEEE address (%s) does not match the backup's (%s)", - current_eui64, - node_info.ieee, - ) - else: + should_update_eui64 = stack_specific.get( + "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" + ) + + if should_update_eui64: new_ncp_eui64 = t.EmberEUI64(node_info.ieee) (status,) = await ezsp.setMfgToken( t.EzspMfgTokenId.MFG_CUSTOM_EUI_64, new_ncp_eui64.serialize() ) assert status[0] == t.EmberStatus.SUCCESS + else: + LOGGER.warning( + "Current node's IEEE address (%s) does not match the backup's (%s)", + current_eui64, + node_info.ieee, + ) use_hashed_tclk = ezsp.ezsp_version > 4 @@ -359,23 +355,29 @@ async def write_network_info( use_hashed_tclk=use_hashed_tclk, ) status = await ezsp.setInitialSecurityState(initial_security_state) + assert status == t.EmberStatus.SUCCESS - # Write keys - key_table = [ - util.zigpy_key_to_ezsp_key(key, ezsp) for key in network_info.key_table - ] + # Clear the key table (is this necessary?) + (status,) = await ezsp.clearKeyTable() + assert status == t.EmberStatus.SUCCESS + # Grow the key table if necessary (status, key_table_size) = await ezsp.getConfigurationValue( ezsp.types.EzspConfigId.CONFIG_KEY_TABLE_SIZE ) assert status == t.EmberStatus.SUCCESS + key_table = [ + util.zigpy_key_to_ezsp_key(key, ezsp) for key in network_info.key_table + ] + if key_table_size < len(key_table): (status,) = await ezsp.setConfigurationValue( ezsp.types.EzspConfigId.CONFIG_KEY_TABLE_SIZE, len(key_table) ) assert status == t.EmberStatus.SUCCESS + # Write APS link keys for key in key_table: # XXX: is there no way to set the outgoing frame counter per key? (status,) = await ezsp.addOrUpdateKeyTableEntry( diff --git a/tests/test_application_network_state.py b/tests/test_application_network_state.py index 4eda0285..36dfbd0e 100644 --- a/tests/test_application_network_state.py +++ b/tests/test_application_network_state.py @@ -72,6 +72,7 @@ def network_info(node_info): def _mock_app_for_load(app): # noqa: F811 + """Mock methods on the application and EZSP objects to run network state code.""" ezsp = app._ezsp app._ensure_network_running = AsyncMock() @@ -148,6 +149,7 @@ def get_key(key_type): async def test_load_network_info_no_devices(app, network_info, node_info): # noqa: F811 + """Test `load_network_info(load_devices=False)`""" _mock_app_for_load(app) await app.load_network_info(load_devices=False) @@ -161,6 +163,7 @@ async def test_load_network_info_no_devices(app, network_info, node_info): # no async def test_load_network_info_with_devices( app, network_info, node_info # noqa: F811 ): + """Test `load_network_info(load_devices=True)`""" _mock_app_for_load(app) def get_child_data(index): From 31da303a50d97875f4673f7efd08f65e0373aaeb Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 5 May 2022 11:48:41 -0400 Subject: [PATCH 29/49] Write initial unit tests for `write_network_info` --- bellows/zigbee/application.py | 6 +- setup.cfg | 5 +- tests/test_application.py | 7 -- tests/test_application_network_state.py | 96 +++++++++++++++++++++++++ tests/test_util.py | 8 +-- 5 files changed, 107 insertions(+), 15 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 2643b045..06ff479e 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -333,7 +333,7 @@ async def write_network_info( (status,) = await ezsp.setMfgToken( t.EzspMfgTokenId.MFG_CUSTOM_EUI_64, new_ncp_eui64.serialize() ) - assert status[0] == t.EmberStatus.SUCCESS + assert status == t.EmberStatus.SUCCESS else: LOGGER.warning( "Current node's IEEE address (%s) does not match the backup's (%s)", @@ -347,14 +347,14 @@ async def write_network_info( # Generate a random default network_info.stack_specific.setdefault("ezsp", {})[ "hashed_tclk" - ] = os.urandom(8).hex() + ] = os.urandom(16).hex() initial_security_state = util.zha_security( network_info=network_info, node_info=node_info, use_hashed_tclk=use_hashed_tclk, ) - status = await ezsp.setInitialSecurityState(initial_security_state) + (status,) = await ezsp.setInitialSecurityState(initial_security_state) assert status == t.EmberStatus.SUCCESS # Clear the key table (is this necessary?) diff --git a/setup.cfg b/setup.cfg index 04f06efc..51ea8f48 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,6 +12,9 @@ ignore = E501, D202 +per-file-ignores = + tests/*:F811,F401,F403 + [isort] profile = black # will group `import x` and `from x import` of the same module. @@ -21,4 +24,4 @@ forced_separate = tests combine_as_imports = true [tool:pytest] -asyncio_mode = auto \ No newline at end of file +asyncio_mode = auto diff --git a/tests/test_application.py b/tests/test_application.py index b77cc8f5..af692022 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -251,13 +251,6 @@ async def test_startup_end_form(app, ieee): await _test_startup(app, t.EmberNodeType.SLEEPY_END_DEVICE, ieee, auto_form=True) -async def test_write_network_info_failed_leave(app): - app._ezsp.leaveNetwork = AsyncMock(return_value=[t.EmberStatus.BAD_ARGUMENT]) - - with pytest.raises(zigpy.exceptions.FormationFailure): - await app.form_network() - - def _frame_handler( app, aps, diff --git a/tests/test_application_network_state.py b/tests/test_application_network_state.py index 36dfbd0e..2256850a 100644 --- a/tests/test_application_network_state.py +++ b/tests/test_application_network_state.py @@ -238,3 +238,99 @@ def get_addr_table_eui64(index): assert app.state.node_info == node_info assert app.state.network_info == network_info.replace(key_table=[]) + + +def _mock_app_for_write(app, network_info, node_info): + ezsp = app._ezsp + + ezsp.leaveNetwork = AsyncMock(return_value=[t.EmberStatus.NETWORK_DOWN]) + ezsp.getEui64 = AsyncMock( + return_value=[t.EmberEUI64.convert("00:12:4b:00:1c:a1:b8:46")] + ) + + ezsp.setInitialSecurityState = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) + ezsp.clearKeyTable = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) + ezsp.getConfigurationValue = AsyncMock( + return_value=[t.EmberStatus.SUCCESS, t.uint8_t(200)] + ) + ezsp.addOrUpdateKeyTableEntry = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) + ezsp.setValue = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) + ezsp.formNetwork = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) + ezsp.setValue = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) + ezsp.setMfgToken = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) + + +async def test_write_network_info_failed_leave(app, network_info, node_info): + _mock_app_for_write(app, network_info, node_info) + + app._ezsp.leaveNetwork.return_value = [t.EmberStatus.BAD_ARGUMENT] + + with pytest.raises(zigpy.exceptions.FormationFailure): + await app.write_network_info(network_info=network_info, node_info=node_info) + + +async def test_write_network_info(app, network_info, node_info): + _mock_app_for_write(app, network_info, node_info) + + await app.write_network_info(network_info=network_info, node_info=node_info) + + +async def test_write_network_info_write_new_eui64(app, network_info, node_info): + _mock_app_for_write(app, network_info, node_info) + + # Differs from what is in `node_info` + app._ezsp.getEui64.return_value = [t.EmberEUI64.convert("AA:AA:AA:AA:AA:AA:AA:AA")] + + await app.write_network_info( + network_info=network_info.replace( + stack_specific={ + "ezsp": { + "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it": True, + **network_info.stack_specific["ezsp"], + } + } + ), + node_info=node_info, + ) + + # The EUI64 is written + expected_eui64 = t.EmberEUI64(node_info.ieee).serialize() + app._ezsp.setMfgToken.assert_called_once_with( + t.EzspMfgTokenId.MFG_CUSTOM_EUI_64, expected_eui64 + ) + + +async def test_write_network_info_dont_write_new_eui64(app, network_info, node_info): + _mock_app_for_write(app, network_info, node_info) + + # Differs from what is in `node_info` + app._ezsp.getEui64.return_value = [t.EmberEUI64.convert("AA:AA:AA:AA:AA:AA:AA:AA")] + + await app.write_network_info( + # We don't provide the magic key so nothing is written + network_info=network_info, + node_info=node_info, + ) + + # The EUI64 is *not* written + app._ezsp.setMfgToken.assert_not_called() + + +async def test_write_network_info_generate_hashed_tclk(app, network_info, node_info): + _mock_app_for_write(app, network_info, node_info) + + seen_keys = set() + + for i in range(10): + app._ezsp.setInitialSecurityState.reset_mock() + + await app.write_network_info( + network_info=network_info.replace(stack_specific={}), + node_info=node_info, + ) + + call = app._ezsp.setInitialSecurityState.mock_calls[0] + seen_keys.add(tuple(call.args[0].preconfiguredKey)) + + # A new hashed key is randomly generated each time if none is provided + assert len(seen_keys) == 10 diff --git a/tests/test_util.py b/tests/test_util.py index 4550e8c7..cd3d11d2 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -6,7 +6,7 @@ import bellows.types as bellows_t import bellows.zigbee.util as util -from tests.test_application import ezsp_mock # noqa: F401 +from tests.test_application import ezsp_mock @pytest.fixture @@ -57,7 +57,7 @@ def zigpy_key(): @pytest.fixture -def ezsp_key(ezsp_mock): # noqa: F811 +def ezsp_key(ezsp_mock): ezsp = ezsp_mock return ezsp.types.EmberKeyStruct( bitmask=( @@ -118,9 +118,9 @@ def test_zha_security_router(network_info, node_info): ) -def test_ezsp_key_to_zigpy_key(zigpy_key, ezsp_key, ezsp_mock): # noqa: F811 +def test_ezsp_key_to_zigpy_key(zigpy_key, ezsp_key, ezsp_mock): return util.ezsp_key_to_zigpy_key(ezsp_key, ezsp_mock) == zigpy_key -def test_zigpy_key_to_ezsp_key(zigpy_key, ezsp_key, ezsp_mock): # noqa: F811 +def test_zigpy_key_to_ezsp_key(zigpy_key, ezsp_key, ezsp_mock): return util.zigpy_key_to_ezsp_key(zigpy_key, ezsp_mock) == ezsp_key From 82924fcf310995908c387f31bf29fc89d97bbf57 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 5 May 2022 11:52:19 -0400 Subject: [PATCH 30/49] Remove ignored flake8 errors from unit tests --- tests/test_application_network_state.py | 10 ++++------ tests/test_cli.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/test_application_network_state.py b/tests/test_application_network_state.py index 2256850a..59a23822 100644 --- a/tests/test_application_network_state.py +++ b/tests/test_application_network_state.py @@ -6,7 +6,7 @@ import bellows.types as t from tests.async_mock import AsyncMock -from tests.test_application import app, ezsp_mock # noqa: F401 +from tests.test_application import app, ezsp_mock @pytest.fixture @@ -71,7 +71,7 @@ def network_info(node_info): ) -def _mock_app_for_load(app): # noqa: F811 +def _mock_app_for_load(app): """Mock methods on the application and EZSP objects to run network state code.""" ezsp = app._ezsp @@ -148,7 +148,7 @@ def get_key(key_type): ) -async def test_load_network_info_no_devices(app, network_info, node_info): # noqa: F811 +async def test_load_network_info_no_devices(app, network_info, node_info): """Test `load_network_info(load_devices=False)`""" _mock_app_for_load(app) @@ -160,9 +160,7 @@ async def test_load_network_info_no_devices(app, network_info, node_info): # no ) -async def test_load_network_info_with_devices( - app, network_info, node_info # noqa: F811 -): +async def test_load_network_info_with_devices(app, network_info, node_info): """Test `load_network_info(load_devices=True)`""" _mock_app_for_load(app) diff --git a/tests/test_cli.py b/tests/test_cli.py index ecf8d4f2..d7aca4c1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,3 @@ -import bellows.cli # noqa: F401 +import bellows.cli # Just being able to import is a small test... From 846545401376abf96c9862c0b7ae7ce184e298c3 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 5 May 2022 12:06:27 -0400 Subject: [PATCH 31/49] Only use one pair of `network_info` and `node_info` fixtures --- tests/test_util.py | 76 +++++++++++----------------------------------- 1 file changed, 18 insertions(+), 58 deletions(-) diff --git a/tests/test_util.py b/tests/test_util.py index cd3d11d2..6ba350a3 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -7,57 +7,19 @@ import bellows.zigbee.util as util from tests.test_application import ezsp_mock +from tests.test_application_network_state import network_info, node_info @pytest.fixture -def node_info(): - return zigpy.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 zigpy.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=zigpy.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=zigpy.state.Key( - key=t.KeyData(b"ZigBeeAlliance09"), - partner_ieee=node_info.ieee, - tx_counter=8712428, - ), - key_table=[], - children=[], - nwk_addresses={}, - stack_specific={"ezsp": {"hashed_tclk": "71e31105bb92a2d15747a0d0a042dbfd"}}, - ) - - -@pytest.fixture -def zigpy_key(): - return zigpy.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, +def zigpy_key(network_info, node_info): + return network_info.network_key.replace( rx_counter=1234567, - partner_ieee=t.EUI64.convert("0D:49:91:99:AE:CD:3C:35"), + partner_ieee=node_info.ieee, ) @pytest.fixture -def ezsp_key(ezsp_mock): +def ezsp_key(ezsp_mock, network_info, node_info, zigpy_key): ezsp = ezsp_mock return ezsp.types.EmberKeyStruct( bitmask=( @@ -66,11 +28,11 @@ def ezsp_key(ezsp_mock): | ezsp.types.EmberKeyStructBitmask.KEY_HAS_INCOMING_FRAME_COUNTER | ezsp.types.EmberKeyStructBitmask.KEY_HAS_PARTNER_EUI64 ), - key=ezsp.types.EmberKeyData(bytes.fromhex("9A79D69ADAEC45C6F2EFEBAFDAA307B6")), - sequenceNumber=108, - outgoingFrameCounter=39009277, - incomingFrameCounter=1234567, - partnerEUI64=bellows_t.EmberEUI64.convert("0D:49:91:99:AE:CD:3C:35"), + key=ezsp.types.EmberKeyData(network_info.network_key.key), + sequenceNumber=zigpy_key.seq, + outgoingFrameCounter=zigpy_key.tx_counter, + incomingFrameCounter=zigpy_key.rx_counter, + partnerEUI64=bellows_t.EmberEUI64(node_info.ieee), ) @@ -83,8 +45,8 @@ def test_zha_security_normal(network_info, node_info): security.preconfiguredTrustCenterEui64 == network_info.tc_link_key.partner_ieee ) assert ( - security.bitmask & bellows_t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 - == bellows_t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 + bellows_t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 + in security.bitmask ) assert ( @@ -92,9 +54,8 @@ def test_zha_security_normal(network_info, node_info): == network_info.stack_specific["ezsp"]["hashed_tclk"] ) assert ( - security.bitmask - & bellows_t.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY - == bellows_t.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY + bellows_t.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY + in security.bitmask ) @@ -106,15 +67,14 @@ def test_zha_security_router(network_info, node_info): assert security.preconfiguredTrustCenterEui64 == bellows_t.EmberEUI64([0x00] * 8) assert ( - security.bitmask & bellows_t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 - != bellows_t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 + bellows_t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 + not in security.bitmask ) assert security.preconfiguredKey == network_info.tc_link_key.key assert ( - security.bitmask - & bellows_t.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY - != bellows_t.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY + bellows_t.EmberInitialSecurityBitmask.TRUST_CENTER_USES_HASHED_LINK_KEY + not in security.bitmask ) From defc081bf04d3e6bc0094351d75f63c2847f6d1b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 5 May 2022 12:16:25 -0400 Subject: [PATCH 32/49] Test new code in `bellows.zigbee.util` --- tests/test_util.py | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/tests/test_util.py b/tests/test_util.py index 6ba350a3..d7eceeca 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,3 +1,5 @@ +import logging + import pytest import zigpy.state import zigpy.types as t @@ -60,9 +62,10 @@ def test_zha_security_normal(network_info, node_info): def test_zha_security_router(network_info, node_info): - router_node_info = node_info.replace(logical_type=zdo_t.LogicalType.Router) security = util.zha_security( - network_info=network_info, node_info=router_node_info, use_hashed_tclk=False + network_info=network_info, + node_info=node_info.replace(logical_type=zdo_t.LogicalType.Router), + use_hashed_tclk=False, ) assert security.preconfiguredTrustCenterEui64 == bellows_t.EmberEUI64([0x00] * 8) @@ -78,6 +81,45 @@ def test_zha_security_router(network_info, node_info): ) +def test_zha_security_replace_missing_tc_partner_addr(network_info, node_info): + security = util.zha_security( + network_info=network_info.replace( + tc_link_key=network_info.tc_link_key.replace(partner_ieee=t.EUI64.UNKNOWN) + ), + node_info=node_info, + use_hashed_tclk=True, + ) + + assert node_info.ieee != t.EUI64.UNKNOWN + assert security.preconfiguredTrustCenterEui64 == node_info.ieee + + +def test_zha_security_hashed_nonstandard_tclk_warning(network_info, node_info, caplog): + # Nothing should be logged normally + with caplog.at_level(logging.WARNING): + util.zha_security( + network_info=network_info, + node_info=node_info, + use_hashed_tclk=True, + ) + + assert "Only the well-known TC Link Key is supported" not in caplog.text + + # But it will be when a non-standard TCLK is used along with TCLK hashing + with caplog.at_level(logging.WARNING): + util.zha_security( + network_info=network_info.replace( + tc_link_key=network_info.tc_link_key.replace( + key=t.KeyData(b"ANonstandardTCLK") + ) + ), + node_info=node_info, + use_hashed_tclk=True, + ) + + assert "Only the well-known TC Link Key is supported" in caplog.text + + def test_ezsp_key_to_zigpy_key(zigpy_key, ezsp_key, ezsp_mock): return util.ezsp_key_to_zigpy_key(ezsp_key, ezsp_mock) == zigpy_key From eadf4ab62c25cb7a8d2d38be39d66b01db5445c6 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 5 May 2022 13:28:20 -0400 Subject: [PATCH 33/49] Add unit test for when `getMfgToken` is missing --- bellows/zigbee/application.py | 5 +++-- tests/test_application.py | 25 ++++++++++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 06ff479e..e1b72a40 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -137,11 +137,12 @@ async def connect(self): try: brd_manuf, brd_name, version = await self._ezsp.get_board_info() + except EzspError as exc: + LOGGER.info("EZSP Radio does not support getMfgToken command: %r", exc) + else: LOGGER.info("EZSP Radio manufacturer: %s", brd_manuf) LOGGER.info("EZSP Radio board name: %s", brd_name) LOGGER.info("EmberZNet version: %s", version) - except EzspError as exc: - LOGGER.info("EZSP Radio does not support getMfgToken command: %s", str(exc)) async def _ensure_network_running(self) -> bool: """ diff --git a/tests/test_application.py b/tests/test_application.py index af692022..6e411b03 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -96,7 +96,9 @@ def ieee(init=0): @patch("zigpy.device.Device._initialize", new=AsyncMock()) @patch("bellows.zigbee.application.ControllerApplication._watchdog", new=AsyncMock()) -async def _test_startup(app, nwk_type, ieee, auto_form=False, init=0, ezsp_version=4): +async def _test_startup( + app, nwk_type, ieee, auto_form=False, init=0, ezsp_version=4, board_info=True +): nwk_params = bellows.types.struct.EmberNetworkParameters( extendedPanId=t.ExtendedPanId.convert("aa:bb:cc:dd:ee:ff:aa:bb"), panId=t.EmberPanId(0x55AA), @@ -124,9 +126,14 @@ async def mock_leave(*args, **kwargs): ezsp_mock.setConfigurationValue = AsyncMock(return_value=t.EmberStatus.SUCCESS) ezsp_mock.networkInit = AsyncMock(return_value=[init]) ezsp_mock.getNetworkParameters = AsyncMock(return_value=[0, nwk_type, nwk_params]) - ezsp_mock.get_board_info = AsyncMock( - return_value=("Mock Manufacturer", "Mock board", "Mock version") - ) + + if board_info: + ezsp_mock.get_board_info = AsyncMock( + return_value=("Mock Manufacturer", "Mock board", "Mock version") + ) + else: + ezsp_mock.get_board_info = AsyncMock(side_effect=EzspError("Not supported")) + ezsp_mock.setPolicy = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) ezsp_mock.getMfgToken = AsyncMock(return_value=(b"Some token\xff",)) ezsp_mock.getNodeId = AsyncMock(return_value=[t.EmberNodeId(0x0000)]) @@ -251,6 +258,14 @@ async def test_startup_end_form(app, ieee): await _test_startup(app, t.EmberNodeType.SLEEPY_END_DEVICE, ieee, auto_form=True) +async def test_startup_no_board_info(app, ieee, caplog): + """Test when NCP does not support `get_board_info`.""" + with caplog.at_level(logging.INFO): + await _test_startup(app, t.EmberNodeType.COORDINATOR, ieee, board_info=False) + + assert "EZSP Radio does not support getMfgToken command" in caplog.text + + def _frame_handler( app, aps, @@ -1473,7 +1488,7 @@ async def test_set_mfg_id(ieee, expected_mfg_id, app, ezsp_mock): sentinel.parent, ], ) - await asyncio.sleep(0.03) + await asyncio.sleep(0.20) if expected_mfg_id is not None: assert ezsp_mock.setManufacturerCode.await_count == 2 assert ezsp_mock.setManufacturerCode.await_args_list[0][0][0] == expected_mfg_id From 76831bb576570816ebe86f30f1706c9269e9428e Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 5 May 2022 13:51:45 -0400 Subject: [PATCH 34/49] Unit test `_ensure_network_running` --- tests/test_application.py | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/test_application.py b/tests/test_application.py index 6e411b03..677f371d 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1498,3 +1498,44 @@ async def test_set_mfg_id(ieee, expected_mfg_id, app, ezsp_mock): ) else: assert ezsp_mock.setManufacturerCode.await_count == 0 + + +async def test_ensure_network_running_joined(app): + ezsp = app._ezsp + ezsp.networkState = AsyncMock( + return_value=[ezsp.types.EmberNetworkStatus.JOINED_NETWORK] + ) + + rsp = await app._ensure_network_running() + + assert not rsp + + ezsp.networkInit.assert_not_called() + + +async def test_ensure_network_running_not_joined_failure(app): + ezsp = app._ezsp + ezsp.networkState = AsyncMock( + return_value=[ezsp.types.EmberNetworkStatus.NO_NETWORK] + ) + ezsp.networkInit = AsyncMock(return_value=[ezsp.types.EmberStatus.INVALID_CALL]) + + with pytest.raises(zigpy.exceptions.NetworkNotFormed): + await app._ensure_network_running() + + ezsp.networkState.assert_called_once() + ezsp.networkInit.assert_called_once() + + +async def test_ensure_network_running_not_joined_success(app): + ezsp = app._ezsp + ezsp.networkState = AsyncMock( + return_value=[ezsp.types.EmberNetworkStatus.NO_NETWORK] + ) + ezsp.networkInit = AsyncMock(return_value=[ezsp.types.EmberStatus.SUCCESS]) + + rsp = await app._ensure_network_running() + assert rsp + + ezsp.networkState.assert_called_once() + ezsp.networkInit.assert_called_once() From cb013f10f1be20af132e55f0b074f082c691ed02 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 5 May 2022 13:52:08 -0400 Subject: [PATCH 35/49] Fix startup delay caused by slow coordinator initialization --- bellows/zigbee/application.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index e1b72a40..f21d04b2 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -184,11 +184,14 @@ async def start_network(self): ieee=self.state.node_info.ieee, nwk=self.state.node_info.nwk, ) - self.devices[self.state.node_info.ieee] = ezsp_device - await ezsp_device.initialize() + + # The coordinator device does not respond to attribute reads ezsp_device.endpoints[1] = EZSPCoordinator.EZSPEndpoint(ezsp_device, 1) ezsp_device.model = ezsp_device.endpoints[1].model ezsp_device.manufacturer = ezsp_device.endpoints[1].manufacturer + await ezsp_device.schedule_initialize() + + self.devices[self.state.node_info.ieee] = ezsp_device await self.multicast.startup(ezsp_device) From f897655c28ca8ec966a96688678f24ac7ed3a856 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 5 May 2022 14:40:41 -0400 Subject: [PATCH 36/49] Key table size can't be adjusted --- bellows/zigbee/application.py | 26 +++------- tests/test_application_network_state.py | 63 +++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 25 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index f21d04b2..51aa0c4b 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -361,31 +361,17 @@ async def write_network_info( (status,) = await ezsp.setInitialSecurityState(initial_security_state) assert status == t.EmberStatus.SUCCESS - # Clear the key table (is this necessary?) + # Clear the key table (status,) = await ezsp.clearKeyTable() assert status == t.EmberStatus.SUCCESS - # Grow the key table if necessary - (status, key_table_size) = await ezsp.getConfigurationValue( - ezsp.types.EzspConfigId.CONFIG_KEY_TABLE_SIZE - ) - assert status == t.EmberStatus.SUCCESS - - key_table = [ - util.zigpy_key_to_ezsp_key(key, ezsp) for key in network_info.key_table - ] - - if key_table_size < len(key_table): - (status,) = await ezsp.setConfigurationValue( - ezsp.types.EzspConfigId.CONFIG_KEY_TABLE_SIZE, len(key_table) - ) - assert status == t.EmberStatus.SUCCESS - # Write APS link keys - for key in key_table: - # XXX: is there no way to set the outgoing frame counter per key? + for key in network_info.key_table: + ember_key = util.zigpy_key_to_ezsp_key(key, ezsp) + + # XXX: is there no way to set the outgoing frame counter or seq? (status,) = await ezsp.addOrUpdateKeyTableEntry( - key.partnerEUI64, True, key.key + ember_key.partnerEUI64, True, ember_key.key ) if status != t.EmberStatus.SUCCESS: LOGGER.warning("Couldn't add %s key: %s", key, status) diff --git a/tests/test_application_network_state.py b/tests/test_application_network_state.py index 59a23822..7990ec0e 100644 --- a/tests/test_application_network_state.py +++ b/tests/test_application_network_state.py @@ -185,10 +185,50 @@ def get_child_data(index): app._ezsp.getChildData = AsyncMock(side_effect=get_child_data) def get_key_table_entry(index): - if index < 14: - status = t.EmberStatus.TABLE_ENTRY_ERASED - else: + if index == 0: + return ( + t.EmberStatus.SUCCESS, + app._ezsp.types.EmberKeyStruct( + bitmask=( + t.EmberKeyStructBitmask.KEY_IS_AUTHORIZED + | t.EmberKeyStructBitmask.KEY_HAS_PARTNER_EUI64 + | t.EmberKeyStructBitmask.KEY_HAS_INCOMING_FRAME_COUNTER + | t.EmberKeyStructBitmask.KEY_HAS_OUTGOING_FRAME_COUNTER + ), + type=app._ezsp.types.EmberKeyType.APPLICATION_LINK_KEY, + key=t.EmberKeyData( + bytes.fromhex("857C05003E761AF9689A49416A605C76") + ), + outgoingFrameCounter=3792973670, + incomingFrameCounter=1083290572, + sequenceNumber=147, + partnerEUI64=t.EmberEUI64.convert("CC:CC:CC:FF:FE:E6:8E:CA"), + ), + ) + elif index == 1: + return ( + t.EmberStatus.SUCCESS, + app._ezsp.types.EmberKeyStruct( + bitmask=( + t.EmberKeyStructBitmask.KEY_IS_AUTHORIZED + | t.EmberKeyStructBitmask.KEY_HAS_PARTNER_EUI64 + | t.EmberKeyStructBitmask.KEY_HAS_INCOMING_FRAME_COUNTER + | t.EmberKeyStructBitmask.KEY_HAS_OUTGOING_FRAME_COUNTER + ), + type=app._ezsp.types.EmberKeyType.APPLICATION_LINK_KEY, + key=t.EmberKeyData( + bytes.fromhex("CA02E8BB757C94F89339D39CB3CDA7BE") + ), + outgoingFrameCounter=2597245184, + incomingFrameCounter=824424412, + sequenceNumber=19, + partnerEUI64=t.EmberEUI64.convert("EC:1B:BD:FF:FE:2F:41:A4"), + ), + ) + elif index >= 12: status = t.EmberStatus.INDEX_OUT_OF_RANGE + else: + status = t.EmberStatus.TABLE_ENTRY_ERASED return ( status, @@ -234,8 +274,11 @@ def get_addr_table_eui64(index): await app.load_network_info(load_devices=True) + # EZSP doesn't provide a command to set the key sequence number + assert app.state.network_info == network_info.replace( + key_table=[key.replace(seq=0) for key in network_info.key_table] + ) assert app.state.node_info == node_info - assert app.state.network_info == network_info.replace(key_table=[]) def _mock_app_for_write(app, network_info, node_info): @@ -251,7 +294,17 @@ def _mock_app_for_write(app, network_info, node_info): ezsp.getConfigurationValue = AsyncMock( return_value=[t.EmberStatus.SUCCESS, t.uint8_t(200)] ) - ezsp.addOrUpdateKeyTableEntry = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) + ezsp.addOrUpdateKeyTableEntry = AsyncMock( + side_effect=[ + # Only the first one succeeds + (t.EmberStatus.SUCCESS,), + ] + + [ + # The rest will fail + (t.EmberStatus.TABLE_FULL,), + ] + * 20 + ) ezsp.setValue = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) ezsp.formNetwork = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) ezsp.setValue = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) From 90a6b4816140d2113670b8db1034bca42e807578 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 5 May 2022 14:51:14 -0400 Subject: [PATCH 37/49] Unit test `getChildData` for EZSP v6 --- tests/test_application_network_state.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/tests/test_application_network_state.py b/tests/test_application_network_state.py index 7990ec0e..14a79a2e 100644 --- a/tests/test_application_network_state.py +++ b/tests/test_application_network_state.py @@ -160,11 +160,25 @@ async def test_load_network_info_no_devices(app, network_info, node_info): ) -async def test_load_network_info_with_devices(app, network_info, node_info): +@pytest.mark.parametrize("ezsp_ver", [6, 7]) +async def test_load_network_info_with_devices(app, network_info, node_info, ezsp_ver): """Test `load_network_info(load_devices=True)`""" _mock_app_for_load(app) - def get_child_data(index): + def get_child_data_v6(index): + if index == 0: + status = t.EmberStatus.SUCCESS + else: + status = t.EmberStatus.NOT_JOINED + + return ( + status, + t.EmberNodeId(0xC06B), + t.EmberEUI64.convert("00:0b:57:ff:fe:2b:d4:57"), + t.EmberNodeType.SLEEPY_END_DEVICE, + ) + + def get_child_data_v7(index): if index == 0: status = t.EmberStatus.SUCCESS else: @@ -182,7 +196,10 @@ def get_child_data(index): ), ) - app._ezsp.getChildData = AsyncMock(side_effect=get_child_data) + app._ezsp.ezsp_version = ezsp_ver + app._ezsp.getChildData = AsyncMock( + side_effect={7: get_child_data_v7, 6: get_child_data_v6}[ezsp_ver] + ) def get_key_table_entry(index): if index == 0: From 2925708907b0d5ac74b714daf2265dae45f68aa9 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 5 May 2022 15:00:23 -0400 Subject: [PATCH 38/49] Unit test `leaveNetwork` failing with `INVALID_CALL` --- tests/test_application_network_state.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_application_network_state.py b/tests/test_application_network_state.py index 14a79a2e..6d94ca17 100644 --- a/tests/test_application_network_state.py +++ b/tests/test_application_network_state.py @@ -3,6 +3,7 @@ import zigpy.types as zigpy_t import zigpy.zdo.types as zdo_t +from bellows.exception import EzspError import bellows.types as t from tests.async_mock import AsyncMock @@ -328,7 +329,7 @@ def _mock_app_for_write(app, network_info, node_info): ezsp.setMfgToken = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) -async def test_write_network_info_failed_leave(app, network_info, node_info): +async def test_write_network_info_failed_leave1(app, network_info, node_info): _mock_app_for_write(app, network_info, node_info) app._ezsp.leaveNetwork.return_value = [t.EmberStatus.BAD_ARGUMENT] @@ -337,6 +338,14 @@ async def test_write_network_info_failed_leave(app, network_info, node_info): await app.write_network_info(network_info=network_info, node_info=node_info) +async def test_write_network_info_failed_leave2(app, network_info, node_info): + _mock_app_for_write(app, network_info, node_info) + + app._ezsp.leaveNetwork.side_effect = EzspError("failed to leave network") + + await app.write_network_info(network_info=network_info, node_info=node_info) + + async def test_write_network_info(app, network_info, node_info): _mock_app_for_write(app, network_info, node_info) From 10e7ef4de46c6691bdac9e225859e9b1a7f50823 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 5 May 2022 15:46:44 -0400 Subject: [PATCH 39/49] Add the coordinator to the zigpy device dictionary before initialization --- bellows/zigbee/application.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 51aa0c4b..61329565 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -184,6 +184,7 @@ async def start_network(self): ieee=self.state.node_info.ieee, nwk=self.state.node_info.nwk, ) + self.devices[self.state.node_info.ieee] = ezsp_device # The coordinator device does not respond to attribute reads ezsp_device.endpoints[1] = EZSPCoordinator.EZSPEndpoint(ezsp_device, 1) @@ -191,8 +192,6 @@ async def start_network(self): ezsp_device.manufacturer = ezsp_device.endpoints[1].manufacturer await ezsp_device.schedule_initialize() - self.devices[self.state.node_info.ieee] = ezsp_device - await self.multicast.startup(ezsp_device) async def load_network_info(self, *, load_devices=False) -> None: From 46cfa92e6d5e8ed817c3a6b63a160008760bf55f Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 5 May 2022 16:01:59 -0400 Subject: [PATCH 40/49] Do not back up the address tables on EZSPv4, it is unstable --- bellows/zigbee/application.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 61329565..c60634af 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -296,6 +296,11 @@ async def load_network_info(self, *, load_devices=False) -> None: self.state.network_info.children.append(eui64) self.state.network_info.nwk_addresses[eui64] = nwk + # v4 cran crash when getAddressTableRemoteNodeId(32) is received + # Error code: undefined_0x8a + if ezsp.ezsp_version == 4: + return + for idx in range(0, 255 + 1): (nwk,) = await ezsp.getAddressTableRemoteNodeId(idx) (eui64,) = await ezsp.getAddressTableRemoteEui64(idx) From e049ddc5f0d77baf905bf27412b5b4a3d50fa5ca Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 5 May 2022 16:08:28 -0400 Subject: [PATCH 41/49] Do not set frame counters when using EZSPv4 --- bellows/zigbee/application.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index c60634af..4789bf11 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -380,20 +380,20 @@ async def write_network_info( if status != t.EmberStatus.SUCCESS: LOGGER.warning("Couldn't add %s key: %s", key, status) - # Set NWK frame counter - (status,) = await ezsp.setValue( - ezsp.types.EzspValueId.VALUE_NWK_FRAME_COUNTER, - t.uint32_t(network_info.network_key.tx_counter).serialize(), - ) - assert status == t.EmberStatus.SUCCESS + if ezsp.ezsp_version > 4: + # Set NWK frame counter + (status,) = await ezsp.setValue( + ezsp.types.EzspValueId.VALUE_NWK_FRAME_COUNTER, + t.uint32_t(network_info.network_key.tx_counter).serialize(), + ) + assert status == t.EmberStatus.SUCCESS - # Set APS frame counter - (status,) = await ezsp.setValue( - ezsp.types.EzspValueId.VALUE_APS_FRAME_COUNTER, - t.uint32_t(network_info.tc_link_key.tx_counter).serialize(), - ) - LOGGER.debug("Set network frame counter: %s", status) - assert status == t.EmberStatus.SUCCESS + # Set APS frame counter + (status,) = await ezsp.setValue( + ezsp.types.EzspValueId.VALUE_APS_FRAME_COUNTER, + t.uint32_t(network_info.tc_link_key.tx_counter).serialize(), + ) + assert status == t.EmberStatus.SUCCESS # Set the network settings parameters = t.EmberNetworkParameters() From b2ee3d4c72d98840ba1e24e758403d29336e86bf Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 10 May 2022 18:43:22 +0200 Subject: [PATCH 42/49] Fix UART port close on RSTACK message during startup When a RSTACK message is processed right after the UART has been opened, it causes EZSP.enter_failed_state() getting called at a point where the application callbacks are not registered yet. In that case the UART will get closed and it won't get opened again. Bellows is stuck with a closed transport. Avoid this issue by not closing the port in case there is no application callback registered yet. Typically, it is unlikely that a RSTACK message arrives right when the port gets opened (the race window is very narrow). However, with hardware flow control opening the port leads to RTS signal to get asserted which causes the radio to send pending messages, e.g. resets caused by EmberZNet watchdog. Note: With hardware flow control this is only the case if the tty "hupcl" option is set. The option is set by default, but cleared by tools like GNU screen. This option makes sure that the RTS signal is deasserted while the port is closed. Pyserial/bellows does not change the state of that option. --- bellows/ezsp/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bellows/ezsp/__init__.py b/bellows/ezsp/__init__.py index 14b32e5c..58459605 100644 --- a/bellows/ezsp/__init__.py +++ b/bellows/ezsp/__init__.py @@ -219,9 +219,12 @@ def connection_lost(self, exc): def enter_failed_state(self, error): """UART received error frame.""" - LOGGER.error("NCP entered failed state. Requesting APP controller restart") - self.close() - self.handle_callback("_reset_controller_application", (error,)) + if self._callbacks: + LOGGER.error("NCP entered failed state. Requesting APP controller restart") + self.close() + self.handle_callback("_reset_controller_application", (error,)) + else: + LOGGER.info("NCP entered failed state. No application handler registered, ignoring...") def __getattr__(self, name: str) -> Callable: if name not in self._protocol.COMMANDS: From 74165f2c48c2a3ccc93c5c3ba4ced003c65ea7d1 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 10 May 2022 14:02:13 -0400 Subject: [PATCH 43/49] Unit test EZSP v4 changes --- bellows/zigbee/application.py | 2 +- tests/test_application_network_state.py | 48 ++++++++++++++++++++----- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 4789bf11..76ba94b8 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -296,7 +296,7 @@ async def load_network_info(self, *, load_devices=False) -> None: self.state.network_info.children.append(eui64) self.state.network_info.nwk_addresses[eui64] = nwk - # v4 cran crash when getAddressTableRemoteNodeId(32) is received + # v4 can crash when getAddressTableRemoteNodeId(32) is received # Error code: undefined_0x8a if ezsp.ezsp_version == 4: return diff --git a/tests/test_application_network_state.py b/tests/test_application_network_state.py index 6d94ca17..cb281e62 100644 --- a/tests/test_application_network_state.py +++ b/tests/test_application_network_state.py @@ -6,7 +6,7 @@ from bellows.exception import EzspError import bellows.types as t -from tests.async_mock import AsyncMock +from tests.async_mock import AsyncMock, PropertyMock from tests.test_application import app, ezsp_mock @@ -161,7 +161,7 @@ async def test_load_network_info_no_devices(app, network_info, node_info): ) -@pytest.mark.parametrize("ezsp_ver", [6, 7]) +@pytest.mark.parametrize("ezsp_ver", [4, 6, 7]) async def test_load_network_info_with_devices(app, network_info, node_info, ezsp_ver): """Test `load_network_info(load_devices=True)`""" _mock_app_for_load(app) @@ -199,7 +199,11 @@ def get_child_data_v7(index): app._ezsp.ezsp_version = ezsp_ver app._ezsp.getChildData = AsyncMock( - side_effect={7: get_child_data_v7, 6: get_child_data_v6}[ezsp_ver] + side_effect={ + 7: get_child_data_v7, + 6: get_child_data_v6, + 4: get_child_data_v6, + }[ezsp_ver] ) def get_key_table_entry(index): @@ -292,14 +296,33 @@ def get_addr_table_eui64(index): await app.load_network_info(load_devices=True) + nwk_addresses = network_info.nwk_addresses + + # v4 doesn't support address table reads so the only known addresses are from the + # `getChildData` call + if ezsp_ver == 4: + nwk_addresses = { + ieee: addr + for ieee, addr in network_info.nwk_addresses.items() + if ieee in network_info.children + } + else: + nwk_addresses = network_info.nwk_addresses + # EZSP doesn't provide a command to set the key sequence number assert app.state.network_info == network_info.replace( - key_table=[key.replace(seq=0) for key in network_info.key_table] + key_table=[key.replace(seq=0) for key in network_info.key_table], + nwk_addresses=nwk_addresses, ) assert app.state.node_info == node_info + # No crash-prone calls were made + if ezsp_ver == 4: + app._ezsp.getAddressTableRemoteNodeId.assert_not_called() + app._ezsp.getAddressTableRemoteEui64.assert_not_called() + -def _mock_app_for_write(app, network_info, node_info): +def _mock_app_for_write(app, network_info, node_info, ezsp_ver=None): ezsp = app._ezsp ezsp.leaveNetwork = AsyncMock(return_value=[t.EmberStatus.NETWORK_DOWN]) @@ -323,7 +346,15 @@ def _mock_app_for_write(app, network_info, node_info): ] * 20 ) - ezsp.setValue = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) + + if ezsp_ver is not None: + ezsp.ezsp_version = ezsp_ver + + if ezsp_ver == 4: + ezsp.setValue = AsyncMock(return_value=[t.EmberStatus.BAD_ARGUMENT]) + else: + ezsp.setValue = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) + ezsp.formNetwork = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) ezsp.setValue = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) ezsp.setMfgToken = AsyncMock(return_value=[t.EmberStatus.SUCCESS]) @@ -346,8 +377,9 @@ async def test_write_network_info_failed_leave2(app, network_info, node_info): await app.write_network_info(network_info=network_info, node_info=node_info) -async def test_write_network_info(app, network_info, node_info): - _mock_app_for_write(app, network_info, node_info) +@pytest.mark.parametrize("ezsp_ver", [4, 7]) +async def test_write_network_info(app, network_info, node_info, ezsp_ver): + _mock_app_for_write(app, network_info, node_info, ezsp_ver) await app.write_network_info(network_info=network_info, node_info=node_info) From c59b92ff3e54a1254aac4fe5d791312090d4e2ef Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 10 May 2022 20:16:12 +0200 Subject: [PATCH 44/49] Fix tests --- tests/test_ezsp.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_ezsp.py b/tests/test_ezsp.py index 23ac5b36..4d9fb3d6 100644 --- a/tests/test_ezsp.py +++ b/tests/test_ezsp.py @@ -241,12 +241,13 @@ def test_connection_lost(ezsp_f): async def test_enter_failed_state(ezsp_f): ezsp_f.stop_ezsp = MagicMock(spec_set=ezsp_f.stop_ezsp) - ezsp_f.handle_callback = MagicMock(spec_set=ezsp_f.handle_callback) + cb = MagicMock(spec_set=ezsp_f.handle_callback) + ezsp_f.add_callback(cb) ezsp_f.enter_failed_state(sentinel.error) await asyncio.sleep(0) assert ezsp_f.stop_ezsp.call_count == 1 - assert ezsp_f.handle_callback.call_count == 1 - assert ezsp_f.handle_callback.call_args[0][1][0] == sentinel.error + assert cb.call_count == 1 + assert cb.call_args[0][1][0] == sentinel.error @patch.object(ezsp.EZSP, "reset", new_callable=AsyncMock) From c4c8d3bd6d5915936dc975f7ea2549c366058adc Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 14 May 2022 20:29:50 -0400 Subject: [PATCH 45/49] Include radio library metadata in network info --- bellows/zigbee/application.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 76ba94b8..384fcffe 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -16,6 +16,7 @@ import zigpy.util import zigpy.zdo.types as zdo_t +import bellows from bellows.config import ( CONF_PARAM_SRC_RTG, CONF_PARAM_UNK_DEV, @@ -120,6 +121,15 @@ async def cleanup_tc_link_key(self, ieee: t.EmberEUI64) -> None: status = await self._ezsp.eraseKeyTableEntry(index) LOGGER.debug("Cleaned up TC link key for %s device: %s", ieee, status) + async def _get_board_info(self) -> tuple[str, str, str] | tuple[None, None, None]: + """Get the board info, handling errors when `getMfgToken` is not supported.""" + try: + return await self._ezsp.get_board_info() + except EzspError as exc: + LOGGER.info("EZSP Radio does not support getMfgToken command: %r", exc) + + return None, None, None + async def connect(self): self._ezsp = await bellows.ezsp.EZSP.initialize(self.config) ezsp = self._ezsp @@ -135,14 +145,10 @@ async def connect(self): await self.register_endpoints() - try: - brd_manuf, brd_name, version = await self._ezsp.get_board_info() - except EzspError as exc: - LOGGER.info("EZSP Radio does not support getMfgToken command: %r", exc) - else: - LOGGER.info("EZSP Radio manufacturer: %s", brd_manuf) - LOGGER.info("EZSP Radio board name: %s", brd_name) - LOGGER.info("EmberZNet version: %s", version) + brd_manuf, brd_name, version = await self._get_board_info() + LOGGER.info("EZSP Radio manufacturer: %s", brd_manuf) + LOGGER.info("EZSP Radio board name: %s", brd_name) + LOGGER.info("EmberZNet version: %s", version) async def _ensure_network_running(self) -> bool: """ @@ -247,7 +253,10 @@ async def load_network_info(self, *, load_devices=False) -> None: if self.state.node_info.logical_type == zdo_t.LogicalType.Coordinator: tc_link_key.partner_ieee = self.state.node_info.ieee + brd_manuf, brd_name, version = await self._get_board_info() + self.state.network_info = zigpy.state.NetworkInfo( + source=f"bellows@{bellows.__version__}", extended_pan_id=zigpy.types.ExtendedPanId(nwk_params.extendedPanId), pan_id=zigpy.types.PanId(nwk_params.panId), nwk_update_id=zigpy.types.uint8_t(nwk_params.nwkUpdateId), @@ -261,6 +270,14 @@ async def load_network_info(self, *, load_devices=False) -> None: children=[], nwk_addresses={}, stack_specific=stack_specific, + metadata={ + "ezsp": { + "manufacturer": brd_manuf, + "board": brd_name, + "version": version, + "stack_version": ezsp.ezsp_version, + } + }, ) if not load_devices: From 8741573fb55143545ce7f72aeea5f67575cce62b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 14 May 2022 21:04:13 -0400 Subject: [PATCH 46/49] Fix unit tests broken by new `metadata` key --- tests/test_application_network_state.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_application_network_state.py b/tests/test_application_network_state.py index cb281e62..d5b5434e 100644 --- a/tests/test_application_network_state.py +++ b/tests/test_application_network_state.py @@ -3,6 +3,7 @@ import zigpy.types as zigpy_t import zigpy.zdo.types as zdo_t +import bellows from bellows.exception import EzspError import bellows.types as t @@ -69,6 +70,7 @@ def network_info(node_info): zigpy_t.EUI64.convert("00:0b:57:ff:fe:2b:d4:57"): zigpy_t.NWK(0xC06B), }, stack_specific={"ezsp": {"hashed_tclk": "abcdabcdabcdabcdabcdabcdabcdabcd"}}, + source=f"bellows@{bellows.__version__}", ) @@ -157,7 +159,10 @@ async def test_load_network_info_no_devices(app, network_info, node_info): assert app.state.node_info == node_info assert app.state.network_info == network_info.replace( - key_table=[], children=[], nwk_addresses={} + key_table=[], + children=[], + nwk_addresses={}, + metadata=app.state.network_info.metadata, ) @@ -313,6 +318,7 @@ def get_addr_table_eui64(index): assert app.state.network_info == network_info.replace( key_table=[key.replace(seq=0) for key in network_info.key_table], nwk_addresses=nwk_addresses, + metadata=app.state.network_info.metadata, ) assert app.state.node_info == node_info From 549632ac0b7488ed59e2e2c13f3e33b1d0a0acc7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 28 May 2022 18:45:39 -0400 Subject: [PATCH 47/49] Do not set the `HAVE_TRUST_CENTER_EUI64` bit when forming a network --- bellows/zigbee/util.py | 15 +++++++-------- tests/test_util.py | 31 ++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/bellows/zigbee/util.py b/bellows/zigbee/util.py index f298f877..1c615418 100644 --- a/bellows/zigbee/util.py +++ b/bellows/zigbee/util.py @@ -26,15 +26,14 @@ def zha_security( isc.networkKey = t.EmberKeyData(network_info.network_key.key) isc.networkKeySequenceNumber = t.uint8_t(network_info.network_key.seq) - # This field must be set when using commissioning mode - if node_info.logical_type == zdo_t.LogicalType.Coordinator: - if network_info.tc_link_key.partner_ieee == zigpy_t.EUI64.UNKNOWN: - partner_ieee = node_info.ieee - else: - partner_ieee = network_info.tc_link_key.partner_ieee - + if ( + node_info.logical_type != zdo_t.LogicalType.Coordinator + and network_info.tc_link_key.partner_ieee != zigpy_t.EUI64.UNKNOWN + ): isc.bitmask |= t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 - isc.preconfiguredTrustCenterEui64 = t.EmberEUI64(partner_ieee) + isc.preconfiguredTrustCenterEui64 = t.EmberEUI64( + network_info.tc_link_key.partner_ieee + ) else: isc.preconfiguredTrustCenterEui64 = t.EmberEUI64([0x00] * 8) diff --git a/tests/test_util.py b/tests/test_util.py index d7eceeca..725b505f 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -43,12 +43,10 @@ def test_zha_security_normal(network_info, node_info): network_info=network_info, node_info=node_info, use_hashed_tclk=True ) - assert ( - security.preconfiguredTrustCenterEui64 == network_info.tc_link_key.partner_ieee - ) + assert security.preconfiguredTrustCenterEui64 == bellows_t.EmberEUI64([0x00] * 8) assert ( bellows_t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 - in security.bitmask + not in security.bitmask ) assert ( @@ -68,10 +66,12 @@ def test_zha_security_router(network_info, node_info): use_hashed_tclk=False, ) - assert security.preconfiguredTrustCenterEui64 == bellows_t.EmberEUI64([0x00] * 8) + assert security.preconfiguredTrustCenterEui64 == bellows_t.EmberEUI64( + network_info.tc_link_key.partner_ieee + ) assert ( bellows_t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 - not in security.bitmask + in security.bitmask ) assert security.preconfiguredKey == network_info.tc_link_key.key @@ -81,6 +81,23 @@ def test_zha_security_router(network_info, node_info): ) +def test_zha_security_router_unknown_tclk_partner_ieee(network_info, node_info): + security = util.zha_security( + network_info=network_info.replace( + tc_link_key=network_info.tc_link_key.replace(partner_ieee=t.EUI64.UNKNOWN) + ), + node_info=node_info.replace(logical_type=zdo_t.LogicalType.Router), + use_hashed_tclk=False, + ) + + # Not set, since we don't know it + assert security.preconfiguredTrustCenterEui64 == bellows_t.EmberEUI64([0x00] * 8) + assert ( + bellows_t.EmberInitialSecurityBitmask.HAVE_TRUST_CENTER_EUI64 + not in security.bitmask + ) + + def test_zha_security_replace_missing_tc_partner_addr(network_info, node_info): security = util.zha_security( network_info=network_info.replace( @@ -91,7 +108,7 @@ def test_zha_security_replace_missing_tc_partner_addr(network_info, node_info): ) assert node_info.ieee != t.EUI64.UNKNOWN - assert security.preconfiguredTrustCenterEui64 == node_info.ieee + assert security.preconfiguredTrustCenterEui64 == bellows_t.EmberEUI64([0x00] * 8) def test_zha_security_hashed_nonstandard_tclk_warning(network_info, node_info, caplog): From 080e7b43c5733ecbb4b599b3ed028f480a367ab8 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 15 Jun 2022 16:56:17 -0400 Subject: [PATCH 48/49] 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 72a87189..38eaaea0 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ "pyserial", "pyserial-asyncio", "voluptuous", - "zigpy>=0.37.0", + "zigpy>=0.47.0", ], dependency_links=[ "https://codeload.github.com/rcloran/pure-pcapy-3/zip/master", From aa865b3ae2b9b3ff47e85c8f345316fd0eb04943 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 16 Jun 2022 14:19:05 -0400 Subject: [PATCH 49/49] Fix unit tests for Python 3.7 --- tests/test_application_network_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_application_network_state.py b/tests/test_application_network_state.py index d5b5434e..74d1fff7 100644 --- a/tests/test_application_network_state.py +++ b/tests/test_application_network_state.py @@ -445,7 +445,7 @@ async def test_write_network_info_generate_hashed_tclk(app, network_info, node_i ) call = app._ezsp.setInitialSecurityState.mock_calls[0] - seen_keys.add(tuple(call.args[0].preconfiguredKey)) + seen_keys.add(tuple(call[1][0].preconfiguredKey)) # A new hashed key is randomly generated each time if none is provided assert len(seen_keys) == 10