From 9456baad18bff41f4571b06c2955fb0c6fba7fa7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:07:11 -0400 Subject: [PATCH 01/22] Fix data type of `CommandId` --- zigpy_espzb/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index 4e09a62..0e31d49 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -88,7 +88,7 @@ class FormNetwork(t.Struct): nwk_cfg1: t.uint32_t -class CommandId(t.uint16_t): +class CommandId(t.enum16): networkinit = 0x0000 start = 0x0001 device_state = 0x0002 From 86ec87f208d136cdc7c3139813396dbc4facfe92 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:07:44 -0400 Subject: [PATCH 02/22] Import `deserialize_dict` from the correct module --- zigpy_espzb/api.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index 0e31d49..b2ddfbf 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -19,7 +19,13 @@ from zigpy.zdo.types import SimpleDescriptor from zigpy_espzb.exception import APIException, CommandError, MismatchedResponseError -from zigpy_espzb.types import Bytes, DeviceAddrMode, ZnspTransmitOptions, list_replace +from zigpy_espzb.types import ( + Bytes, + DeviceAddrMode, + ZnspTransmitOptions, + deserialize_dict, + list_replace, +) import zigpy_espzb.uart LOGGER = logging.getLogger(__name__) @@ -731,7 +737,7 @@ def data_received(self, data: bytes) -> None: break try: - params, rest = t.deserialize_dict(command.payload, rx_schema) + params, rest = deserialize_dict(command.payload, rx_schema) except Exception: LOGGER.warning("Failed to parse command %s", command, exc_info=True) From 7231da1ad407896c5ca050f0e82b1441a5ec3a75 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:09:03 -0400 Subject: [PATCH 03/22] Use command ID names from ESP Zigbee SDK --- zigpy_espzb/api.py | 60 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index b2ddfbf..eabf8c5 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -95,30 +95,41 @@ class FormNetwork(t.Struct): class CommandId(t.enum16): - networkinit = 0x0000 + network_init = 0x0000 start = 0x0001 device_state = 0x0002 change_network_state = 0x0003 form_network = 0x0004 permit_joining = 0x0005 + join_network = 0x0006 + leave_network = 0x0007 + start_scan = 0x0008 + scan_complete_handler = 0x0009 + stop_scan = 0x000A panid_get = 0x000B panid_set = 0x000C extpanid_get = 0x000D extpanid_set = 0x000E - channel_mask_get = 0x000F - channel_mask_set = 0x0010 + primary_channel_mask_get = 0x000F + primary_channel_mask_set = 0x0010 + secondary_channel_mask_get = 0x0011 + secondary_channel_mask_set = 0x0012 current_channel_get = 0x0013 current_channel_set = 0x0014 + tx_power_get = 0x0015 + tx_power_set = 0x0016 network_key_get = 0x0017 network_key_set = 0x0018 nwk_frame_counter_get = 0x0019 nwk_frame_counter_set = 0x001A - aps_designed_coordinator_get = 0x001B - aps_designed_coordinator_set = 0x001C + network_role_get = 0x001B + network_role_set = 0x001C short_addr_get = 0x001D short_addr_set = 0x001E long_addr_get = 0x001F long_addr_set = 0x0020 + channel_masks_get = 0x0021 + channel_masks_set = 0x0022 nwk_update_id_get = 0x0023 nwk_update_id_set = 0x0024 trust_center_address_get = 0x0025 @@ -128,7 +139,20 @@ class CommandId(t.enum16): security_mode_get = 0x0029 security_mode_set = 0x002A use_predefined_nwk_panid_set = 0x002B - addendpoint = 0x0100 + short_to_ieee = 0x002C + ieee_to_short = 0x002D + add_endpoint = 0x0100 + remove_endpoint = 0x0101 + attribute_read = 0x0102 + attribute_write = 0x0103 + attribute_report = 0x0104 + attribute_discover = 0x0105 + aps_read = 0x0106 + aps_write = 0x0107 + report_config = 0x0108 + bind_set = 0x0200 + unbind_set = 0x0201 + find_match = 0x0202 aps_data_request = 0x0300 aps_data_indication = 0x0301 aps_data_confirm = 0x0302 @@ -173,7 +197,7 @@ class Command(t.Struct): COMMAND_SCHEMAS = { - CommandId.networkinit: ( + CommandId.network_init: ( { "payload_length": PAYLOAD_LENGTH, }, @@ -299,14 +323,14 @@ class Command(t.Struct): }, {}, ), - CommandId.channel_mask_get: ( + CommandId.primary_channel_mask_get: ( { "payload_length": PAYLOAD_LENGTH, }, {"payload_length": t.uint16_t, "channel_mask": t.uint32_t}, {}, ), - CommandId.channel_mask_set: ( + CommandId.primary_channel_mask_set: ( {"payload_length": PAYLOAD_LENGTH, "channel_mask": t.Channels}, { "payload_length": t.uint16_t, @@ -314,7 +338,7 @@ class Command(t.Struct): }, {}, ), - CommandId.addendpoint: ( + CommandId.add_endpoint: ( { "payload_length": PAYLOAD_LENGTH, "endpoint": t.uint8_t, @@ -477,14 +501,14 @@ class Command(t.Struct): }, {}, ), - CommandId.aps_designed_coordinator_get: ( + CommandId.network_role_get: ( { "payload_length": PAYLOAD_LENGTH, }, {"payload_length": t.uint16_t, "role": t.uint8_t}, {}, ), - CommandId.aps_designed_coordinator_set: ( + CommandId.network_role_set: ( {"payload_length": PAYLOAD_LENGTH, "role": t.uint8_t}, { "payload_length": t.uint16_t, @@ -877,7 +901,7 @@ def _handle_device_state( self._handle_device_state_changed(status=status, device_state=device_state) async def network_init(self): - await self.send_command(CommandId.networkinit) + await self.send_command(CommandId.network_init) await self.form_network( FormNetwork( role=DeviceType.COORDINATOR, policy=False, nwk_cfg0=0x14, nwk_cfg1=0 @@ -889,7 +913,7 @@ async def network_init(self): async def channel_mask(self): rssult = [] - rsp = await self.send_command(CommandId.channel_mask_get) + rsp = await self.send_command(CommandId.primary_channel_mask_get) for index in range(32): if (rsp["channel_mask"] & (1 << index)) != 0: @@ -899,7 +923,7 @@ async def channel_mask(self): async def set_channel_mask(self, parameter: t.Channels): rsp = await self.send_command( - CommandId.channel_mask_set, channel_mask=parameter + CommandId.primary_channel_mask_set, channel_mask=parameter ) return rsp["status"] @@ -1049,7 +1073,7 @@ async def add_endpoint( return Status.SUCCESS rsp = await self.send_command( - CommandId.addendpoint, + CommandId.add_endpoint, endpoint=endpoint, profileId=profile, deviceId=device_type, @@ -1088,7 +1112,7 @@ async def set_watchdog_ttl(self, parameter: t.uint16_t): async def aps_designed_coordinator(self): rsp = await self.send_command( - CommandId.aps_designed_coordinator_get, + CommandId.network_role_get, reserved=0, ) @@ -1096,7 +1120,7 @@ async def aps_designed_coordinator(self): async def set_aps_designed_coordinator(self, parameter: t.uint8_t): rsp = await self.send_command( - CommandId.aps_designed_coordinator_set, + CommandId.network_role_set, role=parameter, ) From f1dcf72e7325c92048c296e8a5b29937cda03718 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:24:56 -0400 Subject: [PATCH 04/22] Rename `device_state` to `network_state` --- zigpy_espzb/api.py | 82 ++++++++++++++++++++-------------------------- 1 file changed, 36 insertions(+), 46 deletions(-) diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index eabf8c5..d0660f5 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -67,12 +67,8 @@ class NetworkState(t.enum8): JOINING = 1 CONNECTED = 2 LEAVING = 3 - CONFIRM = (4,) - INDICATION = (5,) - - -class DeviceState(t.Struct): - network_state: NetworkState + CONFIRM = 4 + INDICATION = 5 class SecurityMode(t.enum8): @@ -97,7 +93,7 @@ class FormNetwork(t.Struct): class CommandId(t.enum16): network_init = 0x0000 start = 0x0001 - device_state = 0x0002 + network_state = 0x0002 change_network_state = 0x0003 form_network = 0x0004 permit_joining = 0x0005 @@ -356,13 +352,13 @@ class Command(t.Struct): }, {}, ), - CommandId.device_state: ( + CommandId.network_state: ( { "payload_length": PAYLOAD_LENGTH, }, { "payload_length": t.uint16_t, - "device_state": DeviceState, + "network_state": NetworkState, }, {}, ), @@ -406,7 +402,7 @@ class Command(t.Struct): }, { "payload_length": t.uint16_t, - "device_state": DeviceState, + "network_state": NetworkState, "dst_addr_mode": t.uint8_t, "dst_addr": t.EUI64, "dst_ep": t.uint8_t, @@ -424,7 +420,7 @@ class Command(t.Struct): }, { "payload_length": t.uint16_t, - "device_state": DeviceState, + "network_state": NetworkState, "dst_addr_mode": t.uint8_t, "dst_addr": t.EUI64, "dst_ep": t.uint8_t, @@ -447,7 +443,7 @@ class Command(t.Struct): }, { "payload_length": t.uint16_t, - "device_state": DeviceState, + "network_state": NetworkState, "dst_addr_mode": t.uint8_t, "dst_addr": t.EUI64, "dst_ep": t.uint8_t, @@ -460,7 +456,7 @@ class Command(t.Struct): }, { "payload_length": t.uint16_t, - "device_state": DeviceState, + "network_state": NetworkState, "dst_addr_mode": t.uint8_t, "dst_addr": t.EUI64, "dst_ep": t.uint8_t, @@ -598,9 +594,7 @@ def __init__(self, app: Callable, device_config: dict[str, Any]): self._awaiting = collections.defaultdict(lambda: collections.defaultdict(list)) self._command_lock = asyncio.Lock() self._config = device_config - self._device_state = DeviceState( - network_state=NetworkState.OFFLINE, - ) + self._network_state = NetworkState.OFFLINE self._data_poller_event = asyncio.Event() self._data_poller_event.set() @@ -619,17 +613,14 @@ def firmware_version(self) -> FirmwareVersion: @property def network_state(self) -> NetworkState: """Return current network state.""" - return self._device_state.network_state + return self._network_state async def connect(self) -> None: assert self._uart is None self._uart = await zigpy_espzb.uart.connect(self._config, self) - await self.network_init() - - device_state_rsp = await self.send_command(CommandId.device_state) - self._device_state = device_state_rsp["device_state"] - + # TODO: implement a firmware version command + self._network_state = await self.get_network_state() self._data_poller_task = asyncio.create_task(self._data_poller()) def connection_lost(self, exc: Exception) -> None: @@ -838,16 +829,16 @@ async def _data_poller(self): await self._data_poller_event.wait() self._data_poller_event.clear() - if self._device_state.network_state == NetworkState.OFFLINE: + if self._network_state == NetworkState.OFFLINE: continue # Poll data indication rsp = await self.send_command(CommandId.aps_data_indication) - self._handle_device_state_changed( - Status.SUCCESS, device_state=rsp["device_state"] + self._handle_network_state_changed( + Status.SUCCESS, network_state=rsp["network_state"] ) - if rsp["device_state"] == NetworkState.INDICATION: + if rsp["network_state"] == NetworkState.INDICATION: self._app.packet_received( t.ZigbeePacket( src=t.AddrModeAddress( @@ -871,34 +862,33 @@ async def _data_poller(self): # Poll data confirm rsp = await self.send_command(CommandId.aps_data_confirm) - self._handle_device_state_changed( - Status.SUCCESS, device_state=rsp["device_state"] + self._handle_network_state_changed( + Status.SUCCESS, network_state=rsp["network_state"] ) - def _handle_device_state_changed( + def _handle_network_state_changed( self, status: t.Status, - device_state: DeviceState, - reserved: t.uint8_t = 0, + network_state: NetworkState, ) -> None: - if device_state.network_state != self.network_state: + if network_state.network_state != self.network_state: LOGGER.debug( - "Network device_state transition: %s -> %s", + "Network network_state transition: %s -> %s", self.network_state.name, - device_state.network_state.name, + network_state.name, ) - self._device_state = device_state + self._network_state = network_state self._data_poller_event.set() - def _handle_device_state( + def _handle_network_state( self, status: t.Status, - device_state: DeviceState, + network_state: NetworkState, reserved1: t.uint8_t, reserved2: t.uint8_t, ) -> None: - self._handle_device_state_changed(status=status, device_state=device_state) + self._handle_network_state_changed(status=status, network_state=network_state) async def network_init(self): await self.send_command(CommandId.network_init) @@ -907,7 +897,7 @@ async def network_init(self): role=DeviceType.COORDINATOR, policy=False, nwk_cfg0=0x14, nwk_cfg1=0 ) ) - await self.start(False) + await self.start(autostart=False) return Status.SUCCESS @@ -936,8 +926,8 @@ async def form_network(self, parameter: FormNetwork): return rsp["status"] - async def start(self, parameter: t.uint8_t): - rsp = await self.send_command(CommandId.start, autostart=parameter) + async def start(self, autostart: bool) -> Status: + rsp = await self.send_command(CommandId.start, autostart=t.uint8_t(autostart)) return rsp["status"] @@ -1178,16 +1168,16 @@ async def aps_data_request( LOGGER.debug("retrying 'aps_data_request' in %ss", delay) await asyncio.sleep(delay) else: - self._handle_device_state_changed( + self._handle_network_state_changed( status=rsp["status"], - device_state=DeviceState(network_state=NetworkState.CONNECTED), + network_state=NetworkState(network_state=NetworkState.CONNECTED), ) return - async def get_device_state(self) -> DeviceState: - rsp = await self.send_command(CommandId.device_state) + async def get_network_state(self) -> NetworkState: + rsp = await self.send_command(CommandId.network_state) - return rsp["device_state"] + return rsp["network_state"] async def change_network_state(self, new_state: NetworkState) -> None: await self.send_command(CommandId.change_network_state, network_state=new_state) From 2cc8df361316cf428281d3bf22e9cb3ab8eaed22 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 10 Apr 2024 17:04:45 -0400 Subject: [PATCH 05/22] WIP: Clean up deconz-specific code and begin implementing full serial API --- zigpy_espzb/api.py | 644 ++++++++++++------------------ zigpy_espzb/types.py | 70 +++- zigpy_espzb/zigbee/application.py | 323 +++------------ 3 files changed, 387 insertions(+), 650 deletions(-) diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index d0660f5..1d782f4 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -4,7 +4,6 @@ import asyncio import collections -import itertools import logging import sys from typing import Any, Callable @@ -16,15 +15,15 @@ from zigpy.config import CONF_DEVICE_PATH import zigpy.types as t -from zigpy.zdo.types import SimpleDescriptor -from zigpy_espzb.exception import APIException, CommandError, MismatchedResponseError +from zigpy_espzb.exception import APIException, CommandError from zigpy_espzb.types import ( Bytes, - DeviceAddrMode, + ExtendedAddrMode, + ShiftedChannels, ZnspTransmitOptions, + addr_mode_with_eui64_to_addr_mode_address, deserialize_dict, - list_replace, ) import zigpy_espzb.uart @@ -34,14 +33,12 @@ PROBE_TIMEOUT = 2 REQUEST_RETRY_DELAYS = (0.5, 1.0, 1.5, None) -FRAME_LENGTH = object() -PAYLOAD_LENGTH = object() - class DeviceType(t.enum8): - COORDINATOR = 0 - ROUTER = 1 - ED = 2 + COORDINATOR = 0x00 + ROUTER = 0x01 + END_DEVICE = 0x02 + NONE = 0x03 class Status(t.enum8): @@ -74,8 +71,6 @@ class NetworkState(t.enum8): class SecurityMode(t.enum8): NO_SECURITY = 0x00 PRECONFIGURED_NETWORK_KEY = 0x01 - NETWORK_KEY_FROM_TC = 0x02 - ONLY_TCLK = 0x03 class ZDPResponseHandling(t.bitmap16): @@ -85,16 +80,27 @@ class ZDPResponseHandling(t.bitmap16): class FormNetwork(t.Struct): role: DeviceType - policy: t.Bool - nwk_cfg0: t.uint8_t - nwk_cfg1: t.uint32_t + install_code_policy: t.Bool + + # For coordinators/routers + max_children: t.uint8_t = t.StructField( + requires=lambda f: f.role in (DeviceType.ROUTER, DeviceType.COORDINATOR) + ) + + # For end devices + ed_timeout: t.uint8_t = t.StructField( + requires=lambda f: f.role == DeviceType.END_DEVICE + ) + keep_alive: t.uint32_t = t.StructField( + requires=lambda f: f.role == DeviceType.END_DEVICE + ) class CommandId(t.enum16): network_init = 0x0000 start = 0x0001 network_state = 0x0002 - change_network_state = 0x0003 + stack_status_handler = 0x0003 form_network = 0x0004 permit_joining = 0x0005 join_network = 0x0006 @@ -166,65 +172,48 @@ def _missing_(cls, value): return status -class IndexedKey(t.Struct): - index: t.uint8_t - key: t.KeyData - - -class LinkKey(t.Struct): - ieee: t.EUI64 - key: t.KeyData - - -class IndexedEndpoint(t.Struct): - index: t.uint8_t - descriptor: SimpleDescriptor - - -class UpdateNeighborAction(t.enum8): - ADD = 0x01 +class FrameType(t.enum4): + Request = 0 + Response = 1 + Indicate = 2 class Command(t.Struct): - flags: t.uint16_t + version: t.uint4_t + frame_type: FrameType + reserved: t.uint8_t + command_id: CommandId seq: t.uint8_t + length: t.uint16_t payload: Bytes COMMAND_SCHEMAS = { CommandId.network_init: ( + {}, { - "payload_length": PAYLOAD_LENGTH, - }, - { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.start: ( { - "payload_length": PAYLOAD_LENGTH, "autostart": t.Bool, }, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.form_network: ( { - "payload_length": PAYLOAD_LENGTH, - "form_mwk": FormNetwork, + "form_nwk": FormNetwork, }, { - "payload_length": t.uint16_t, "status": Status, }, { - "payload_length": t.uint16_t, "extended_panid": t.EUI64, "panid": t.uint16_t, "channel": t.uint8_t, @@ -232,150 +221,132 @@ class Command(t.Struct): ), CommandId.permit_joining: ( { - "payload_length": PAYLOAD_LENGTH, - "form_mwk": FormNetwork, + "duration": t.uint8_t, }, { - "payload_length": t.uint16_t, - "permit": t.uint8_t, + "status": Status, }, { - "payload_length": t.uint16_t, - "permit": t.uint8_t, + "duration": t.uint8_t, }, ), - CommandId.extpanid_get: ( + CommandId.leave_network: ( + {}, + { + "status": Status, + }, { - "payload_length": PAYLOAD_LENGTH, + "short_addr": t.NWK, + "device_addr": t.EUI64, + "rejoin": t.Bool, }, - {"payload_length": t.uint16_t, "ieee": t.EUI64}, + ), + CommandId.extpanid_get: ( + {}, + {"ieee": t.EUI64}, {}, ), CommandId.extpanid_set: ( - {"payload_length": PAYLOAD_LENGTH, "ieee": t.EUI64}, + {"ieee": t.EUI64}, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.panid_get: ( - { - "payload_length": PAYLOAD_LENGTH, - }, - {"payload_length": t.uint16_t, "panid": t.uint16_t}, + {}, + {"panid": t.uint16_t}, {}, ), CommandId.panid_set: ( - {"payload_length": PAYLOAD_LENGTH, "panid": t.PanId}, + {"panid": t.PanId}, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.short_addr_get: ( - { - "payload_length": PAYLOAD_LENGTH, - }, - {"payload_length": t.uint16_t, "short_addr": t.uint16_t}, + {}, + {"short_addr": t.NWK}, {}, ), CommandId.short_addr_set: ( - {"payload_length": PAYLOAD_LENGTH, "short_addr": t.uint16_t}, + {"short_addr": t.NWK}, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.long_addr_get: ( - { - "payload_length": PAYLOAD_LENGTH, - }, - {"payload_length": t.uint16_t, "ieee": t.EUI64}, + {}, + {"ieee": t.EUI64}, {}, ), CommandId.long_addr_set: ( - {"payload_length": PAYLOAD_LENGTH, "ieee": t.EUI64}, + {"ieee": t.EUI64}, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.current_channel_get: ( - { - "payload_length": PAYLOAD_LENGTH, - }, - {"payload_length": t.uint16_t, "channel": t.uint8_t}, + {}, + {"channel": t.uint8_t}, {}, ), CommandId.current_channel_set: ( - {"payload_length": PAYLOAD_LENGTH, "channel": t.uint8_t}, + {"channel": t.uint8_t}, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.primary_channel_mask_get: ( - { - "payload_length": PAYLOAD_LENGTH, - }, - {"payload_length": t.uint16_t, "channel_mask": t.uint32_t}, + {}, + {"channel_mask": ShiftedChannels}, {}, ), CommandId.primary_channel_mask_set: ( - {"payload_length": PAYLOAD_LENGTH, "channel_mask": t.Channels}, + {"channel_mask": ShiftedChannels}, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.add_endpoint: ( { - "payload_length": PAYLOAD_LENGTH, "endpoint": t.uint8_t, - "profileId": t.uint16_t, - "deviceId": t.uint16_t, - "appFlags": t.uint8_t, - "inputClusterCount": t.uint8_t, - "outputClusterCount": t.uint8_t, - "inputClusterList": t.List[t.uint8_t], - "outputClusterList": t.List[t.uint8_t], + "profile_id": t.uint16_t, + "device_id": t.uint16_t, + "app_flags": t.uint8_t, + "input_cluster_count": t.uint8_t, + "output_cluster_count": t.uint8_t, + "input_cluster_list": t.List[t.uint16_t], + "output_cluster_list": t.List[t.uint16_t], }, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.network_state: ( + {}, { - "payload_length": PAYLOAD_LENGTH, - }, - { - "payload_length": t.uint16_t, "network_state": NetworkState, }, {}, ), - CommandId.change_network_state: ( + CommandId.stack_status_handler: ( + {}, { - "payload_length": PAYLOAD_LENGTH, "network_state": t.uint8_t, }, { - "payload_length": t.uint16_t, "network_state": t.uint8_t, }, - {}, ), CommandId.aps_data_request: ( { - "payload_length": PAYLOAD_LENGTH, "dst_addr": t.EUI64, "dst_endpoint": t.uint8_t, "src_endpoint": t.uint8_t, @@ -388,25 +359,21 @@ class Command(t.Struct): "sequence": t.uint8_t, "radius": t.uint8_t, "asdu_length": t.uint32_t, - "asdu": t.List[t.uint8_t], + "asdu": Bytes, }, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.aps_data_indication: ( + {}, { - "payload_length": PAYLOAD_LENGTH, - }, - { - "payload_length": t.uint16_t, "network_state": NetworkState, - "dst_addr_mode": t.uint8_t, + "dst_addr_mode": ExtendedAddrMode, "dst_addr": t.EUI64, "dst_ep": t.uint8_t, - "src_addr_mode": t.uint8_t, + "src_addr_mode": ExtendedAddrMode, "src_addr": t.EUI64, "src_ep": t.uint8_t, "profile_id": t.uint16_t, @@ -416,15 +383,14 @@ class Command(t.Struct): "lqi": t.uint8_t, "rx_time": t.uint32_t, "asdu_length": t.uint32_t, - "asdu": t.List[t.uint8_t], + "asdu": Bytes, }, { - "payload_length": t.uint16_t, "network_state": NetworkState, - "dst_addr_mode": t.uint8_t, + "dst_addr_mode": ExtendedAddrMode, "dst_addr": t.EUI64, "dst_ep": t.uint8_t, - "src_addr_mode": t.uint8_t, + "src_addr_mode": ExtendedAddrMode, "src_addr": t.EUI64, "src_ep": t.uint8_t, "profile_id": t.uint16_t, @@ -434,17 +400,14 @@ class Command(t.Struct): "lqi": t.uint8_t, "rx_time": t.uint32_t, "asdu_length": t.uint32_t, - "asdu": t.List[t.uint8_t], + "asdu": Bytes, }, ), CommandId.aps_data_confirm: ( + {}, { - "payload_length": PAYLOAD_LENGTH, - }, - { - "payload_length": t.uint16_t, "network_state": NetworkState, - "dst_addr_mode": t.uint8_t, + "dst_addr_mode": ExtendedAddrMode, "dst_addr": t.EUI64, "dst_ep": t.uint8_t, "src_ep": t.uint8_t, @@ -452,130 +415,107 @@ class Command(t.Struct): "request_id": t.uint8_t, "confirm_status": TXStatus, "asdu_length": t.uint32_t, - "asdu": t.List[t.uint8_t], + "asdu": Bytes, }, { - "payload_length": t.uint16_t, "network_state": NetworkState, - "dst_addr_mode": t.uint8_t, + "dst_addr_mode": ExtendedAddrMode, "dst_addr": t.EUI64, "dst_ep": t.uint8_t, "src_ep": t.uint8_t, "tx_time": t.uint32_t, "confirm_status": TXStatus, "asdu_length": t.uint32_t, - "asdu": t.List[t.uint8_t], + "asdu": Bytes, }, ), CommandId.network_key_get: ( - { - "payload_length": PAYLOAD_LENGTH, - }, - {"payload_length": t.uint16_t, "nwk_key": t.KeyData}, + {}, + {"nwk_key": t.KeyData}, {}, ), CommandId.network_key_set: ( - {"payload_length": PAYLOAD_LENGTH, "nwk_key": t.KeyData}, + {"nwk_key": t.KeyData}, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.nwk_frame_counter_get: ( - { - "payload_length": PAYLOAD_LENGTH, - }, - {"payload_length": t.uint16_t, "nwk_frame_counter": t.uint32_t}, + {}, + {"nwk_frame_counter": t.uint32_t}, {}, ), CommandId.nwk_frame_counter_set: ( - {"payload_length": PAYLOAD_LENGTH, "nwk_frame_counter": t.uint32_t}, + {"nwk_frame_counter": t.uint32_t}, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.network_role_get: ( - { - "payload_length": PAYLOAD_LENGTH, - }, - {"payload_length": t.uint16_t, "role": t.uint8_t}, + {}, + {"role": DeviceType}, {}, ), CommandId.network_role_set: ( - {"payload_length": PAYLOAD_LENGTH, "role": t.uint8_t}, + {"role": DeviceType}, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.use_predefined_nwk_panid_set: ( - {"payload_length": PAYLOAD_LENGTH, "predefined": t.Bool}, + {"predefined": t.Bool}, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.nwk_update_id_get: ( - { - "payload_length": PAYLOAD_LENGTH, - }, - {"payload_length": t.uint16_t, "nwk_update_id": t.uint8_t}, + {}, + {"nwk_update_id": t.uint8_t}, {}, ), CommandId.nwk_update_id_set: ( - {"payload_length": PAYLOAD_LENGTH, "nwk_update_id": t.uint8_t}, + {"nwk_update_id": t.uint8_t}, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.trust_center_address_get: ( - { - "payload_length": PAYLOAD_LENGTH, - }, - {"payload_length": t.uint16_t, "trust_center_addr": t.EUI64}, + {}, + {"trust_center_addr": t.EUI64}, {}, ), CommandId.trust_center_address_set: ( - {"payload_length": PAYLOAD_LENGTH, "trust_center_addr": t.EUI64}, + {"trust_center_addr": t.EUI64}, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.link_key_get: ( - { - "payload_length": PAYLOAD_LENGTH, - }, - {"payload_length": t.uint16_t, "link_key": LinkKey}, + {}, + {"key": t.KeyData}, {}, ), CommandId.link_key_set: ( - {"payload_length": PAYLOAD_LENGTH, "link_key": t.KeyData}, + {"key": t.KeyData}, { - "payload_length": t.uint16_t, "status": Status, }, {}, ), CommandId.security_mode_get: ( - { - "payload_length": PAYLOAD_LENGTH, - }, - {"payload_length": t.uint16_t, "security_mode": SecurityMode}, + {}, + {"security_mode": SecurityMode}, {}, ), CommandId.security_mode_set: ( - {"payload_length": PAYLOAD_LENGTH, "security_mode": SecurityMode}, + {"security_mode": SecurityMode}, { - "payload_length": t.uint16_t, "status": Status, }, {}, @@ -621,7 +561,6 @@ async def connect(self) -> None: # TODO: implement a firmware version command self._network_state = await self.get_network_state() - self._data_poller_task = asyncio.create_task(self._data_poller()) def connection_lost(self, exc: Exception) -> None: """Lost serial connection.""" @@ -645,14 +584,7 @@ def close(self): self._uart.close() self._uart = None - async def send_command(self, cmd, **kwargs) -> Any: - while True: - try: - return await self._command(cmd, **kwargs) - except MismatchedResponseError as exc: - LOGGER.debug("Firmware responded incorrectly (%s), retrying", exc) - - async def _command(self, cmd, **kwargs): + async def send_command(self, cmd, **kwargs): payload = [] tx_schema, _, _ = COMMAND_SCHEMAS[cmd] trailing_optional = False @@ -664,8 +596,6 @@ async def _command(self, cmd, **kwargs): value = param_type.serialize() else: value = type(param_type)(kwargs[name]).serialize() - elif name in ("frame_length", "payload_length"): - value = param_type elif kwargs.get(name) is None: trailing_optional = True value = None @@ -685,20 +615,15 @@ async def _command(self, cmd, **kwargs): payload.append(value) - if PAYLOAD_LENGTH in payload: - payload = list_replace( - lst=payload, - old=PAYLOAD_LENGTH, - new=t.uint16_t( - sum(len(p) for p in payload[payload.index(PAYLOAD_LENGTH) + 1 :]) - ).serialize(), - ) - + serialized_payload = b"".join(payload) command = Command( - flags=0x0000, + version=0b0000, + frame_type=FrameType.Request, + reserved=0x00, command_id=cmd, seq=None, - payload=b"".join(payload), + length=len(serialized_payload), + payload=serialized_payload, ) if self._uart is None: @@ -732,27 +657,32 @@ def data_received(self, data: bytes) -> None: LOGGER.warning("Unknown command received: %s", command) return - if command.flags == 0x0010: - _, rx_schema, _ = COMMAND_SCHEMAS[command.command_id] - elif command.flags == 0x0020: - _, _, rx_schema = COMMAND_SCHEMAS[command.command_id] + tx_schema, rx_schema, ind_schema = COMMAND_SCHEMAS[command.command_id] + + if command.frame_type == FrameType.Request: + schema = tx_schema + elif command.frame_type == FrameType.Response: + schema = rx_schema + elif command.frame_type == FrameType.Indicate: + schema = ind_schema + else: + raise ValueError(f"Unknown frame type: {command}") + + # We won't implement requests for now + assert command.frame_type != FrameType.Request fut = None - wrong_fut_cmd_id = None - try: - fut = self._awaiting[command.seq][command.command_id][0] - except IndexError: - # XXX: The firmware can sometimes respond with the wrong response. Find the - # future associated with it so we can throw an appropriate error. - for cmd_id, futs in self._awaiting[command.seq].items(): - if futs: - fut = futs[0] - wrong_fut_cmd_id = cmd_id - break + if command.frame_type == FrameType.Response: + try: + fut = self._awaiting[command.seq][command.command_id][0] + except IndexError: + LOGGER.warning( + "Received unexpected response %s%s", command.command_id, command + ) try: - params, rest = deserialize_dict(command.payload, rx_schema) + params, rest = deserialize_dict(command.payload, schema) except Exception: LOGGER.warning("Failed to parse command %s", command, exc_info=True) @@ -766,17 +696,6 @@ def data_received(self, data: bytes) -> None: if rest: LOGGER.debug("Unparsed data remains after frame: %s, %s", command, rest) - if "payload_length" in params: - running_length = itertools.accumulate( - len(v.serialize()) if v is not None else 0 for v in params.values() - ) - length_at_param = dict(zip(params.keys(), running_length)) - - assert ( - len(data) - length_at_param["payload_length"] - 5 - == params["payload_length"] - ) - LOGGER.debug( "Received command %s%s (seq %d)", command.command_id, params, command.seq ) @@ -787,16 +706,7 @@ def data_received(self, data: bytes) -> None: exc = None - if wrong_fut_cmd_id is not None: - exc = MismatchedResponseError( - command.command_id, - params, - ( - f"Response is mismatched! Sent {wrong_fut_cmd_id}," - f" received {command.command_id}" - ), - ) - elif status != Status.SUCCESS: + if status != Status.SUCCESS: exc = CommandError(status, f"{command.command_id}, status: {status}") if fut is not None: @@ -814,64 +724,50 @@ def data_received(self, data: bytes) -> None: if exc is not None: return - if handler := getattr(self, f"_handle_{command.command_id}", None): - handler_params = { - k: v - for k, v in params.items() - if k not in ("frame_length", "payload_length") - } - + if handler := getattr(self, f"_handle_{command.command_id.name}", None): # Queue up the callback within the event loop - asyncio.get_running_loop().call_soon(lambda: handler(**handler_params)) - - async def _data_poller(self): - while True: - await self._data_poller_event.wait() - self._data_poller_event.clear() - - if self._network_state == NetworkState.OFFLINE: - continue + asyncio.get_running_loop().call_soon(lambda: handler(**params)) - # Poll data indication - rsp = await self.send_command(CommandId.aps_data_indication) - self._handle_network_state_changed( - Status.SUCCESS, network_state=rsp["network_state"] - ) - - if rsp["network_state"] == NetworkState.INDICATION: - self._app.packet_received( - t.ZigbeePacket( - src=t.AddrModeAddress( - addr_mode=rsp["src_addr_mode"], - address=rsp["src_addr"], - ), - src_ep=rsp["src_ep"], - dst=t.AddrModeAddress( - addr_mode=rsp["dst_addr_mode"], - address=rsp["dst_addr"], - ), - dst_ep=rsp["dst_ep"], - tsn=None, - profile_id=rsp["profile_id"], - cluster_id=rsp["cluster_id"], - data=t.SerializableBytes(rsp["asdu"]), - lqi=rsp["lqi"], - rssi=rsp["rssi"], - ) + def _handle_aps_data_indication( + self, + network_state: NetworkState, + dst_addr_mode: ExtendedAddrMode, + dst_addr: t.EUI64, + dst_ep: t.uint8_t, + src_addr_mode: ExtendedAddrMode, + src_addr: t.EUI64, + src_ep: t.uint8_t, + profile_id: t.uint16_t, + cluster_id: t.uint16_t, + indication_status: TXStatus, + security_status: t.uint8_t, + lqi: t.uint8_t, + rx_time: t.uint32_t, + asdu_length: t.uint32_t, + asdu: Bytes, + ): + if network_state == NetworkState.INDICATION: + self._app.packet_received( + t.ZigbeePacket( + src=addr_mode_with_eui64_to_addr_mode_address( + src_addr_mode, src_addr + ), + src_ep=src_ep, + dst=addr_mode_with_eui64_to_addr_mode_address( + dst_addr_mode, dst_addr + ), + dst_ep=dst_ep, + tsn=None, + profile_id=profile_id, + cluster_id=cluster_id, + data=t.SerializableBytes(asdu), + lqi=lqi, + rssi=None, ) - - # Poll data confirm - rsp = await self.send_command(CommandId.aps_data_confirm) - self._handle_network_state_changed( - Status.SUCCESS, network_state=rsp["network_state"] ) - def _handle_network_state_changed( - self, - status: t.Status, - network_state: NetworkState, - ) -> None: - if network_state.network_state != self.network_state: + def _handle_network_state_changed(self, network_state: NetworkState) -> None: + if network_state != self.network_state: LOGGER.debug( "Network network_state transition: %s -> %s", self.network_state.name, @@ -881,57 +777,57 @@ def _handle_network_state_changed( self._network_state = network_state self._data_poller_event.set() - def _handle_network_state( - self, - status: t.Status, - network_state: NetworkState, - reserved1: t.uint8_t, - reserved2: t.uint8_t, - ) -> None: - self._handle_network_state_changed(status=status, network_state=network_state) + def _handle_network_state(self, network_state: NetworkState) -> None: + self._handle_network_state_changed(network_state=network_state) - async def network_init(self): + async def network_init(self) -> None: await self.send_command(CommandId.network_init) - await self.form_network( - FormNetwork( - role=DeviceType.COORDINATOR, policy=False, nwk_cfg0=0x14, nwk_cfg1=0 - ) - ) - await self.start(autostart=False) - return Status.SUCCESS - - async def channel_mask(self): - rssult = [] + async def get_channel_mask(self) -> t.Channels: rsp = await self.send_command(CommandId.primary_channel_mask_get) + return t.Channels.from_channel_list(tuple(rsp["channel_mask"])) - for index in range(32): - if (rsp["channel_mask"] & (1 << index)) != 0: - rssult.append(index) - - return rssult - - async def set_channel_mask(self, parameter: t.Channels): - rsp = await self.send_command( - CommandId.primary_channel_mask_set, channel_mask=parameter + async def set_channel_mask(self, channels: t.Channels) -> None: + await self.send_command( + CommandId.primary_channel_mask_set, + channel_mask=ShiftedChannels.from_channel_list(channels), ) - return rsp["status"] + async def set_channel(self, channel: int) -> None: + await self.send_command(CommandId.current_channel_set, channel=channel) - async def form_network(self, parameter: FormNetwork): + async def form_network( + self, + role: DeviceType = DeviceType.COORDINATOR, + install_code_policy: bool = False, + # For coordinators/routers + max_children: t.uint8_t = 20, + # For end devices + ed_timeout: t.uint8_t = 0, + keep_alive: t.uint32_t = 0, + ) -> None: rsp = await self.send_command( CommandId.form_network, - form_mwk=parameter, + form_nwk=FormNetwork( + role=role, + install_code_policy=install_code_policy, + max_children=max_children, + ed_timeout=ed_timeout, + keep_alive=keep_alive, + ), ) return rsp["status"] + async def leave_network(self) -> None: + await self.send_command(CommandId.leave_network) + async def start(self, autostart: bool) -> Status: rsp = await self.send_command(CommandId.start, autostart=t.uint8_t(autostart)) return rsp["status"] - async def mac_address(self): + async def get_mac_address(self): rsp = await self.send_command(CommandId.long_addr_get) return rsp["ieee"] @@ -941,7 +837,7 @@ async def set_mac_address(self, parameter: t.EUI64): return rsp["status"] - async def nwk_address(self): + async def get_nwk_address(self): rsp = await self.send_command(CommandId.short_addr_get) return rsp["short_addr"] @@ -951,7 +847,7 @@ async def set_nwk_address(self, parameter: t.uint16_t): return rsp["status"] - async def nwk_panid(self): + async def get_nwk_panid(self): rsp = await self.send_command(CommandId.panid_get) return rsp["panid"] @@ -961,7 +857,7 @@ async def set_nwk_panid(self, parameter: t.PanId): return rsp["status"] - async def nwk_extended_panid(self): + async def get_nwk_extended_panid(self): rsp = await self.send_command(CommandId.extpanid_get) return rsp["ieee"] @@ -971,12 +867,12 @@ async def set_nwk_extended_panid(self, parameter: t.ExtendedPanId): return rsp["status"] - async def current_channel(self): + async def get_current_channel(self) -> int: rsp = await self.send_command(CommandId.current_channel_get) return rsp["channel"] - async def nwk_update_id(self): + async def get_nwk_update_id(self): rsp = await self.send_command(CommandId.nwk_update_id_get) return rsp["nwk_update_id"] @@ -988,19 +884,15 @@ async def set_nwk_update_id(self, parameter: t.uint8_t): return rsp["status"] - async def network_key(self): + async def get_network_key(self): rsp = await self.send_command(CommandId.network_key_get) - indexed_key = IndexedKey(index=0, key=rsp["nwk_key"]) + return rsp["nwk_key"] - return indexed_key + async def set_network_key(self, key: t.KeyData): + await self.send_command(CommandId.network_key_set, nwk_key=key) - async def set_network_key(self, parameter: IndexedKey): - rsp = await self.send_command(CommandId.network_key_set, nwk_key=parameter.key) - - return rsp["status"] - - async def nwk_frame_counter(self): + async def get_nwk_frame_counter(self): rsp = await self.send_command(CommandId.nwk_frame_counter_get) return rsp["nwk_frame_counter"] @@ -1013,7 +905,7 @@ async def set_nwk_frame_counter(self, parameter: t.uint32_t): return rsp["status"] - async def trust_center_address(self): + async def get_trust_center_address(self): rsp = await self.send_command(CommandId.trust_center_address_get) return rsp["trust_center_addr"] @@ -1025,17 +917,15 @@ async def set_trust_center_address(self, parameter: t.EUI64): return rsp["status"] - async def link_key(self, parameter: Any = None) -> Any: + async def get_link_key(self) -> Any: rsp = await self.send_command(CommandId.link_key_get) - return rsp["link_key"] + return rsp["key"] - async def set_link_key(self, parameter: LinkKey): - rsp = await self.send_command(CommandId.link_key_set, link_key=parameter.key) - - return rsp["status"] + async def set_link_key(self, key: t.KeyData): + await self.send_command(CommandId.link_key_set, key=key) - async def security_mode(self): + async def get_security_mode(self): rsp = await self.send_command(CommandId.security_mode_get) return rsp["security_mode"] @@ -1053,25 +943,22 @@ async def add_endpoint( profile: t.uint16_t, device_type: t.uint16_t, device_version: t.uint8_t, - input_clusters: t.LVList[t.uint16_t], - output_clusters: t.LVList[t.uint16_t], + input_clusters: list[t.ClusterId], + output_clusters: list[t.ClusterId], ): - inputClusterList = t.LVList[t.uint16_t].serialize(input_clusters) - outputClusterList = t.LVList[t.uint16_t].serialize(output_clusters) - if profile == 0xC05E: return Status.SUCCESS rsp = await self.send_command( CommandId.add_endpoint, endpoint=endpoint, - profileId=profile, - deviceId=device_type, - appFlags=device_version, - inputClusterCount=len(input_clusters), - outputClusterCount=len(output_clusters), - inputClusterList=t.List(inputClusterList[1:]), - outputClusterList=t.List(outputClusterList[1:]), + profile_id=profile, + device_id=device_type, + app_flags=device_version, + input_cluster_count=len(input_clusters), + output_cluster_count=len(output_clusters), + input_cluster_list=input_clusters, + output_cluster_list=output_clusters, ) return rsp["status"] @@ -1100,32 +987,18 @@ async def set_watchdog_ttl(self, parameter: t.uint16_t): return rsp["status"] - async def aps_designed_coordinator(self): - rsp = await self.send_command( - CommandId.network_role_get, - reserved=0, - ) - + async def get_network_role(self) -> DeviceType: + rsp = await self.send_command(CommandId.network_role_get) return rsp["role"] - async def set_aps_designed_coordinator(self, parameter: t.uint8_t): + async def set_network_role(self, role: DeviceType) -> None: rsp = await self.send_command( CommandId.network_role_set, - role=parameter, + role=role, ) return rsp["status"] - async def aps_extended_panid(self): - rsp = await self.send_command(CommandId.extpanid_get) - - return rsp["ieee"] - - async def set_aps_extended_panid(self, parameter: t.ExtendedPanId): - rsp = await self.send_command(CommandId.extpanid_set, ieee=parameter) - - return rsp["status"] - async def aps_data_request( self, dst_addr: t.EUI64, @@ -1133,7 +1006,7 @@ async def aps_data_request( src_addr: t.EUI64, src_ep: t.uint8_t, profile: t.uint16_t, - addr_mode: DeviceAddrMode, + addr_mode: t.AddrMode, cluster: t.uint16_t, sequence: t.uint16_t, options: ZnspTransmitOptions, @@ -1144,7 +1017,7 @@ async def aps_data_request( ): for delay in REQUEST_RETRY_DELAYS: try: - rsp = await self.send_command( + await self.send_command( CommandId.aps_data_request, dst_addr=dst_addr, dst_endpoint=dst_ep, @@ -1168,10 +1041,6 @@ async def aps_data_request( LOGGER.debug("retrying 'aps_data_request' in %ss", delay) await asyncio.sleep(delay) else: - self._handle_network_state_changed( - status=rsp["status"], - network_state=NetworkState(network_state=NetworkState.CONNECTED), - ) return async def get_network_state(self) -> NetworkState: @@ -1179,16 +1048,23 @@ async def get_network_state(self) -> NetworkState: return rsp["network_state"] - async def change_network_state(self, new_state: NetworkState) -> None: - await self.send_command(CommandId.change_network_state, network_state=new_state) + async def reset(self) -> None: + # TODO: There is no reset command but we can trigger a crash if we form the + # network twice - async def add_neighbour( - self, nwk: t.NWK, ieee: t.EUI64, mac_capability_flags: t.uint8_t - ) -> None: - await self.send_command( - CommandId.update_neighbor, - action=UpdateNeighborAction.ADD, - nwk=nwk, - ieee=ieee, - mac_capability_flags=mac_capability_flags, - ) + LOGGER.debug("Resetting via crash...") + + for attempt in range(5): + try: + await self.form_network() + except asyncio.TimeoutError: + break + else: + raise RuntimeError("Failed to trigger a reset/crash") + + await asyncio.sleep(0.5) + + LOGGER.debug("Reset complete") + + # TODO: is this required to load any network settings? + await self.network_init() diff --git a/zigpy_espzb/types.py b/zigpy_espzb/types.py index 5a3f601..947cc6a 100644 --- a/zigpy_espzb/types.py +++ b/zigpy_espzb/types.py @@ -1,6 +1,8 @@ """Data types module.""" -from zigpy.types import bitmap8 +from __future__ import annotations + +import zigpy.types as t def serialize_dict(data, schema): @@ -46,12 +48,70 @@ def deserialize(cls, data): return cls(data), b"" -class ZnspTransmitOptions(bitmap8): +class ZnspTransmitOptions(t.bitmap8): NONE = 0x00 ACK_ENABLED = 0x01 SECURITY_ENABLED = 0x02 -class DeviceAddrMode: - # TODO: implement this class - pass +class ExtendedAddrMode(t.enum8): + Unknown = 0x00 + IEEE = 0x01 + NWK = 0x02 + Group = 0x03 + Broadcast = 0x0F + + +def addr_mode_with_eui64_to_addr_mode_address( + addr_mode: ExtendedAddrMode, address: t.EUI64 +) -> t.AddrModeAddress: + """Convert an address mode and an EUI64 address to an AddrModeAddress.""" + address_short, _ = t.uint16_t.deserialize(address.serialize()[:2]) + + if addr_mode == ExtendedAddrMode.IEEE: + address = address + elif addr_mode == ExtendedAddrMode.NWK: + address = t.NWK(address_short) + elif addr_mode == ExtendedAddrMode.Group: + address = t.Group(address_short) + elif addr_mode == ExtendedAddrMode.Broadcast: + address = t.BroadcastAddress(address_short) + elif addr_mode == ExtendedAddrMode.Unknown: + # TODO: Is this correct? It seems to be used only for loopback + address = address_short + addr_mode = t.AddrMode.NWK + else: + raise ValueError(f"Unknown address mode: {addr_mode}") + + return t.AddrModeAddress(addr_mode=t.AddrMode(addr_mode), address=address) + + +class ShiftedChannels(t.bitmap32): + """Zigbee Channels.""" + + CHANNEL_11 = 0b00000000000000000000010000000000 + CHANNEL_12 = 0b00000000000000000000100000000000 + CHANNEL_13 = 0b00000000000000000001000000000000 + CHANNEL_14 = 0b00000000000000000010000000000000 + CHANNEL_15 = 0b00000000000000000100000000000000 + CHANNEL_16 = 0b00000000000000001000000000000000 + CHANNEL_17 = 0b00000000000000010000000000000000 + CHANNEL_18 = 0b00000000000000100000000000000000 + CHANNEL_19 = 0b00000000000001000000000000000000 + CHANNEL_20 = 0b00000000000010000000000000000000 + CHANNEL_21 = 0b00000000000100000000000000000000 + CHANNEL_22 = 0b00000000001000000000000000000000 + CHANNEL_23 = 0b00000000010000000000000000000000 + CHANNEL_24 = 0b00000000100000000000000000000000 + CHANNEL_25 = 0b00000001000000000000000000000000 + CHANNEL_26 = 0b00000010000000000000000000000000 + ALL_CHANNELS = 0b00000011111111111111110000000000 + NO_CHANNELS = 0b00000000000000000000000000000000 + + __iter__ = t.Channels.__iter__ + from_channel_list = classmethod(t.Channels.from_channel_list.__func__) + + @classmethod + def from_zigpy_channels(cls, channels: t.Channels) -> ShiftedChannels: + """Convert a Zigpy Channels to a ShiftedChannels.""" + return cls.from_channel_list(tuple(channels)) diff --git a/zigpy_espzb/zigbee/application.py b/zigpy_espzb/zigbee/application.py index 6fe04aa..c3b6c74 100644 --- a/zigpy_espzb/zigbee/application.py +++ b/zigpy_espzb/zigbee/application.py @@ -21,20 +21,12 @@ from zigpy.exceptions import FormationFailure, NetworkNotFormed import zigpy.state import zigpy.types +import zigpy.types as t import zigpy.util import zigpy.zdo.types as zdo_t -import zigpy_espzb -from zigpy_espzb import types as t -from zigpy_espzb.api import ( - IndexedKey, - LinkKey, - NetworkState, - SecurityMode, - Status, - Znsp, -) -import zigpy_espzb.exception +from zigpy_espzb.api import DeviceType, NetworkState, SecurityMode, Znsp +import zigpy_espzb.types as espzb_t LOGGER = logging.getLogger(__name__) @@ -64,12 +56,8 @@ def __init__(self, config: dict[str, Any]): self._api = None self._pending = zigpy.util.Requests() - - self._delayed_neighbor_scan_task = None self._reconnect_task = None - self._written_endpoints = set() - async def _watchdog_feed(self): await self._api.set_watchdog_ttl(int(self._watchdog_period / 0.75)) @@ -82,41 +70,36 @@ async def connect(self): api.close() raise + await api.reset() + + # TODO: Most commands fail if the network is not formed. Why? + await api.form_network(role=DeviceType.COORDINATOR) + self._api = api - self._written_endpoints.clear() async def disconnect(self): - if self._delayed_neighbor_scan_task is not None: - self._delayed_neighbor_scan_task.cancel() - self._delayed_neighbor_scan_task = None - if self._api is not None: self._api.close() self._api = None async def permit_with_link_key(self, node: t.EUI64, link_key: t.KeyData, time_s=60): - await self._api.set_link_key( - LinkKey(ieee=node, key=link_key), - ) - await self.permit(time_s) + raise NotImplementedError() async def start_network(self): + await self._api.start(autostart=True) + await self.register_endpoints() await self.load_network_info(load_devices=False) - await self._change_network_state(NetworkState.CONNECTED) - coordinator = await ZnspDevice.new( - self, - self.state.node_info.ieee, - self.state.node_info.nwk, - self.state.node_info.model, + # Create the coordinator device + coordinator = zigpy.device.Device( + application=self, + ieee=self.state.node_info.ieee, + nwk=self.state.node_info.nwk, ) - self.devices[self.state.node_info.ieee] = coordinator - self._delayed_neighbor_scan_task = asyncio.create_task( - self._delayed_neighbour_scan() - ) + await coordinator.schedule_initialize() async def _change_network_state( self, @@ -127,11 +110,11 @@ async def _change_network_state( async def change_loop(): while True: try: - device_state = await self._api.get_device_state() + network_state = await self._api.get_network_state() except asyncio.TimeoutError: LOGGER.debug("Failed to poll device state") else: - if NetworkState(device_state.network_state) == target_state: + if network_state == target_state: break await asyncio.sleep(CHANGE_NETWORK_POLL_TIME) @@ -148,22 +131,17 @@ async def change_loop(): raise FormationFailure("Network formation refused.") async def reset_network_info(self): - await self.form_network() + await self._api.leave_network() async def write_network_info(self, *, network_info, node_info): - try: - await self._api.set_nwk_frame_counter(network_info.network_key.tx_counter) - except zigpy_espzb.exception.CommandError as ex: - assert ex.status == Status.UNSUPPORTED - LOGGER.warning( - "Doesn't support writing the network frame counter with this firmware" - ) + await self._api.reset() - if node_info.logical_type == zdo_t.LogicalType.Coordinator: - await self._api.set_aps_designed_coordinator(1) - else: - await self._api.set_aps_designed_coordinator(0) + role = { + zdo_t.LogicalType.Coordinator: DeviceType.COORDINATOR, + zdo_t.LogicalType.Router: DeviceType.ROUTER, + }[node_info.logical_type] + await self._api.set_network_role(role) await self._api.set_nwk_address(node_info.nwk) if node_info.ieee != zigpy.types.EUI64.UNKNOWN: @@ -173,29 +151,13 @@ async def write_network_info(self, *, network_info, node_info): ieee = await self._api.mac_address() node_ieee = zigpy.types.EUI64(ieee) - if network_info.channel is not None: - channel_mask = zigpy.types.Channels.from_channel_list( - [network_info.channel] - ) - - if network_info.channel_mask and channel_mask != network_info.channel_mask: - LOGGER.warning( - "Channel mask %s will be replaced with current logical channel %s", - network_info.channel_mask, - channel_mask, - ) - else: - channel_mask = network_info.channel_mask - - await self._api.set_channel_mask(channel_mask) + # TODO: Why does setting the PAN ID or extended PAN ID trigger a crash? await self._api.set_use_predefined_nwk_panid(True) await self._api.set_nwk_panid(network_info.pan_id) - await self._api.set_aps_extended_panid(network_info.extended_pan_id) + await self._api.set_nwk_extended_panid(network_info.extended_pan_id) await self._api.set_nwk_update_id(network_info.nwk_update_id) - - await self._api.set_network_key( - IndexedKey(index=0, key=network_info.network_key.key), - ) + await self._api.set_network_key(network_info.network_key.key) + await self._api.set_nwk_frame_counter(network_info.network_key.tx_counter) if network_info.network_key.seq != 0: LOGGER.warning( @@ -208,91 +170,66 @@ async def write_network_info(self, *, network_info, node_info): if tc_link_key_partner_ieee == zigpy.types.EUI64.UNKNOWN: tc_link_key_partner_ieee = node_ieee - await self._api.set_trust_center_address( - tc_link_key_partner_ieee, - ) - await self._api.set_link_key( - LinkKey( - ieee=tc_link_key_partner_ieee, - key=network_info.tc_link_key.key, - ), - ) + await self._api.set_trust_center_address(tc_link_key_partner_ieee) + await self._api.set_link_key(network_info.tc_link_key.key) if network_info.security_level == 0x00: await self._api.set_security_mode(SecurityMode.NO_SECURITY) else: - await self._api.set_security_mode(SecurityMode.ONLY_TCLK) + await self._api.set_security_mode(SecurityMode.PRECONFIGURED_NETWORK_KEY) - await self._change_network_state(NetworkState.OFFLINE) - await asyncio.sleep(CHANGE_NETWORK_STATE_DELAY) - await self._change_network_state(NetworkState.CONNECTED) + await self._api.set_channel(network_info.channel) + await self._api.form_network(role=role) + + await asyncio.sleep(1) async def load_network_info(self, *, load_devices=False): network_info = self.state.network_info node_info = self.state.node_info - ieee = await self._api.mac_address() - node_info.ieee = zigpy.types.EUI64(ieee) - designed_coord = await self._api.aps_designed_coordinator() + role = await self._api.get_network_role() - if designed_coord == 0x01: + if role == DeviceType.COORDINATOR: node_info.logical_type = zdo_t.LogicalType.Coordinator else: node_info.logical_type = zdo_t.LogicalType.Router - node_info.nwk = await self._api.nwk_address() + node_info.nwk = await self._api.get_nwk_address() + node_info.ieee = await self._api.get_mac_address() + # TODO: implement firmware commands to read the board name, manufacturer node_info.manufacturer = "Espressif Systems" - node_info.model = "ESP32H2" + # TODO: implement firmware command to read out the firmware version and build ID node_info.version = f"{int(self._api.firmware_version):#010x}" network_info.source = f"zigpy-espzb@{importlib.metadata.version('zigpy-espzb')}" - network_info.metadata = { - "espzb": { - "version": node_info.version, - } - } - - network_info.pan_id = await self._api.nwk_panid() - network_info.extended_pan_id = await self._api.aps_extended_panid() + network_info.metadata = {} - if network_info.extended_pan_id == zigpy.types.EUI64.convert( - "00:00:00:00:00:00:00:00" - ): - network_info.extended_pan_id = await self._api.nwk_extended_panid() + network_info.pan_id = await self._api.get_nwk_panid() + network_info.extended_pan_id = await self._api.get_nwk_extended_panid() + network_info.channel = await self._api.get_current_channel() + network_info.channel_mask = await self._api.get_channel_mask() + network_info.nwk_update_id = await self._api.get_nwk_update_id() - network_info.channel = await self._api.current_channel() - network_info.channel_mask = await self._api.channel_mask() - network_info.nwk_update_id = await self._api.nwk_update_id() + if network_info.channel in (0, 255): + raise NetworkNotFormed(f"Channel is invalid: {network_info.channel}") - if network_info.channel == 0: - raise NetworkNotFormed("Network channel is zero") - - indexed_key = await self._api.network_key() - - network_info.network_key = zigpy.state.Key() - network_info.network_key.key = indexed_key.key - - try: - network_info.network_key.tx_counter = await self._api.nwk_frame_counter() - except zigpy_espzb.exception.CommandError as ex: - assert ex.status == Status.UNSUPPORTED + network_info.network_key.key = await self._api.get_network_key() + network_info.network_key.tx_counter = await self._api.get_nwk_frame_counter() network_info.tc_link_key = zigpy.state.Key() - network_info.tc_link_key.partner_ieee = await self._api.trust_center_address() - - link_key = await self._api.link_key( - network_info.tc_link_key.partner_ieee, + network_info.tc_link_key.key = await self._api.get_link_key() + network_info.tc_link_key.partner_ieee = ( + await self._api.get_trust_center_address() ) - network_info.tc_link_key.key = link_key.key - security_mode = await self._api.security_mode() + security_mode = await self._api.get_security_mode() if security_mode == SecurityMode.NO_SECURITY: network_info.security_level = 0x00 - elif security_mode == SecurityMode.ONLY_TCLK: + elif security_mode == SecurityMode.PRECONFIGURED_NETWORK_KEY: network_info.security_level = 0x05 else: LOGGER.warning("Unsupported security mode %r", security_mode) @@ -301,45 +238,11 @@ async def load_network_info(self, *, load_devices=False): async def force_remove(self, dev): """Forcibly remove device from NCP.""" - async def energy_scan( - self, channels: t.Channels.ALL_CHANNELS, duration_exp: int, count: int - ) -> dict[int, float]: - results = await super().energy_scan( - channels=channels, duration_exp=duration_exp, count=count - ) - - return {c: v * 3 for c, v in results.items()} - - for i in range(ENERGY_SCAN_ATTEMPTS): - try: - rsp = await self._device.zdo.Mgmt_NWK_Update_req( - zigpy.zdo.types.NwkUpdate( - ScanChannels=channels, - ScanDuration=duration_exp, - ScanCount=count, - ) - ) - break - except (asyncio.TimeoutError, zigpy.exceptions.DeliveryError): - if i == ENERGY_SCAN_ATTEMPTS - 1: - raise - - continue - - _, scanned_channels, _, _, energy_values = rsp - return dict(zip(scanned_channels, energy_values)) - async def _move_network_to_channel( self, new_channel: int, new_nwk_update_id: int ) -> None: """Move device to a new channel.""" - channel_mask = zigpy.types.Channels.from_channel_list([new_channel]) - await self._api.set_channel_mask(channel_mask) - await self._api.set_nwk_update_id(new_nwk_update_id) - - await self._change_network_state(NetworkState.OFFLINE) - await asyncio.sleep(CHANGE_NETWORK_STATE_DELAY) - await self._change_network_state(NetworkState.CONNECTED) + raise NotImplementedError() async def add_endpoint(self, descriptor: zdo_t.SimpleDescriptor) -> None: """Register a new endpoint on the device.""" @@ -393,13 +296,13 @@ async def send_packet(self, packet): if packet.source_route is not None: force_relays = packet.source_route - tx_options = t.ZnspTransmitOptions.NONE + tx_options = espzb_t.ZnspTransmitOptions.NONE if zigpy.types.TransmitOptions.ACK in packet.tx_options: - tx_options |= t.ZnspTransmitOptions.ACK_ENABLED + tx_options |= espzb_t.ZnspTransmitOptions.ACK_ENABLED if zigpy.types.TransmitOptions.APS_Encryption in packet.tx_options: - tx_options |= t.ZnspTransmitOptions.SECURITY_ENABLED + tx_options |= espzb_t.ZnspTransmitOptions.SECURITY_ENABLED async with self._limit_concurrency(): await self._api.aps_data_request( @@ -421,105 +324,3 @@ async def send_packet(self, packet): async def permit_ncp(self, time_s=60): assert 0 <= time_s <= 254 await self._api.set_permit_join(time_s) - - async def restore_neighbours(self) -> None: - """Restore children.""" - coord = self.get_device(ieee=self.state.node_info.ieee) - - for neighbor in self.topology.neighbors[coord.ieee]: - try: - device = self.get_device(ieee=neighbor.ieee) - except KeyError: - continue - - descr = device.node_desc - LOGGER.debug( - "device: 0x%04x - %s %s, FFD=%s, Rx_on_when_idle=%s", - device.nwk, - device.manufacturer, - device.model, - descr.is_full_function_device if descr is not None else None, - descr.is_receiver_on_when_idle if descr is not None else None, - ) - if ( - descr is None - or descr.is_full_function_device - or descr.is_receiver_on_when_idle - ): - continue - - LOGGER.debug("Restoring %s as direct child", device) - - try: - await self._api.add_neighbour( - nwk=device.nwk, - ieee=device.ieee, - mac_capability_flags=descr.mac_capability_flags, - ) - except zigpy_espzb.exception.CommandError as ex: - assert ex.status == Status.FAILURE - LOGGER.debug("Failed to add device to neighbor table: %s", ex) - - async def _delayed_neighbour_scan(self) -> None: - """Scan coordinator's neighbours.""" - await asyncio.sleep(DELAY_NEIGHBOUR_SCAN_S) - coord = self.get_device(ieee=self.state.node_info.ieee) - await self.topology.scan(devices=[coord]) - - -class ZnspDevice(zigpy.device.Device): - """Zigpy Device representing Coordinator.""" - - def __init__(self, model: str, *args): - """Initialize instance.""" - - super().__init__(*args) - self._model = model - - async def add_to_group(self, grp_id: int, name: str = None) -> None: - group = self.application.groups.add_group(grp_id, name) - - for epid in self.endpoints: - if not epid: - continue # skip ZDO - group.add_member(self.endpoints[epid]) - return [0] - - async def remove_from_group(self, grp_id: int) -> None: - for epid in self.endpoints: - if not epid: - continue # skip ZDO - self.application.groups[grp_id].remove_member(self.endpoints[epid]) - return [0] - - @property - def manufacturer(self): - return "Espressif Systems" - - @property - def model(self): - return self._model - - @classmethod - async def new(cls, application, ieee, nwk, model: str): - """Create or replace zigpy device.""" - dev = cls(model, application, ieee, nwk) - - if ieee in application.devices: - from_dev = application.get_device(ieee=ieee) - dev.status = from_dev.status - dev.node_desc = from_dev.node_desc - for ep_id, from_ep in from_dev.endpoints.items(): - if not ep_id: - continue # Skip ZDO - ep = dev.add_endpoint(ep_id) - ep.profile_id = from_ep.profile_id - ep.device_type = from_ep.device_type - ep.status = from_ep.status - ep.in_clusters = from_ep.in_clusters - ep.out_clusters = from_ep.out_clusters - else: - application.devices[ieee] = dev - await dev.initialize() - - return dev From 13470cdb4bee065dfab1e05dfe6152e9457ba3fb Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 10 Apr 2024 17:56:22 -0400 Subject: [PATCH 06/22] WIP: Get energy scanning, network formation, and backup somewhat working --- zigpy_espzb/api.py | 17 ++++----- zigpy_espzb/zigbee/application.py | 63 +++++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index 1d782f4..51b13b0 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -215,7 +215,7 @@ class Command(t.Struct): }, { "extended_panid": t.EUI64, - "panid": t.uint16_t, + "panid": t.PanId, "channel": t.uint8_t, }, ), @@ -255,7 +255,7 @@ class Command(t.Struct): ), CommandId.panid_get: ( {}, - {"panid": t.uint16_t}, + {"panid": t.PanId}, {}, ), CommandId.panid_set: ( @@ -498,7 +498,7 @@ class Command(t.Struct): ), CommandId.link_key_get: ( {}, - {"key": t.KeyData}, + {"ieee": t.EUI64, "key": t.KeyData}, {}, ), CommandId.link_key_set: ( @@ -971,10 +971,10 @@ async def set_use_predefined_nwk_panid(self, parameter: t.Bool): return rsp["status"] - async def set_permit_join(self, parameter: t.uint8_t): + async def set_permit_join(self, duration: t.uint8_t): rsp = await self.send_command( - CommandId.permit_join_set, - role=parameter, + CommandId.permit_joining, + duration=duration, ) return rsp["status"] @@ -1062,9 +1062,6 @@ async def reset(self) -> None: else: raise RuntimeError("Failed to trigger a reset/crash") - await asyncio.sleep(0.5) + await asyncio.sleep(2) LOGGER.debug("Reset complete") - - # TODO: is this required to load any network settings? - await self.network_init() diff --git a/zigpy_espzb/zigbee/application.py b/zigpy_espzb/zigbee/application.py index c3b6c74..8f5bd31 100644 --- a/zigpy_espzb/zigbee/application.py +++ b/zigpy_espzb/zigbee/application.py @@ -32,13 +32,8 @@ CHANGE_NETWORK_POLL_TIME = 1 CHANGE_NETWORK_STATE_DELAY = 2 -DELAY_NEIGHBOUR_SCAN_S = 1500 SEND_CONFIRM_TIMEOUT = 60 -PROTO_VER_MANUAL_SOURCE_ROUTE = 0x010C -PROTO_VER_WATCHDOG = 0x0108 -PROTO_VER_NEIGBOURS = 0x0107 - ENERGY_SCAN_ATTEMPTS = 5 @@ -59,7 +54,8 @@ def __init__(self, config: dict[str, Any]): self._reconnect_task = None async def _watchdog_feed(self): - await self._api.set_watchdog_ttl(int(self._watchdog_period / 0.75)) + # TODO: implement a proper software-driven watchdog + await self._api.get_network_state() async def connect(self): api = Znsp(self, self._config[zigpy.config.CONF_DEVICE]) @@ -73,7 +69,9 @@ async def connect(self): await api.reset() # TODO: Most commands fail if the network is not formed. Why? + await api.network_init() await api.form_network(role=DeviceType.COORDINATOR) + await api.start(autostart=False) self._api = api @@ -88,8 +86,8 @@ async def permit_with_link_key(self, node: t.EUI64, link_key: t.KeyData, time_s= async def start_network(self): await self._api.start(autostart=True) - await self.register_endpoints() await self.load_network_info(load_devices=False) + await self.register_endpoints() # Create the coordinator device coordinator = zigpy.device.Device( @@ -99,8 +97,15 @@ async def start_network(self): ) self.devices[self.state.node_info.ieee] = coordinator + # TODO: why does the coordinator respond to the loopback ZDO Active_EP_req with + # [242, 242]? It should include endpoints 1 and 2, we registered them. await coordinator.schedule_initialize() + # TODO: add our registered endpoints manually so things don't crash. These + # should be discovered automatically. + coordinator.add_endpoint(1) + coordinator.add_endpoint(2) + async def _change_network_state( self, target_state: NetworkState, @@ -135,6 +140,9 @@ async def reset_network_info(self): async def write_network_info(self, *, network_info, node_info): await self._api.reset() + await self._api.network_init() + await self._api.form_network(role=DeviceType.COORDINATOR) + await self._api.start(autostart=False) role = { zdo_t.LogicalType.Coordinator: DeviceType.COORDINATOR, @@ -148,13 +156,23 @@ async def write_network_info(self, *, network_info, node_info): await self._api.set_mac_address(node_info.ieee) node_ieee = node_info.ieee else: - ieee = await self._api.mac_address() - node_ieee = zigpy.types.EUI64(ieee) + node_ieee = await self._api.get_mac_address() - # TODO: Why does setting the PAN ID or extended PAN ID trigger a crash? await self._api.set_use_predefined_nwk_panid(True) await self._api.set_nwk_panid(network_info.pan_id) - await self._api.set_nwk_extended_panid(network_info.extended_pan_id) + + # TODO: Why does setting the extended PAN ID trigger a crash? + # Unknown command received: Command( + # version=0, + # frame_type=, + # reserved=0, + # command_id=, + # seq=123, + # length=1, + # payload=b'\x02' + # ) + + # await self._api.set_nwk_extended_panid(network_info.extended_pan_id) await self._api.set_nwk_update_id(network_info.nwk_update_id) await self._api.set_network_key(network_info.network_key.key) await self._api.set_nwk_frame_counter(network_info.network_key.tx_counter) @@ -179,9 +197,12 @@ async def write_network_info(self, *, network_info, node_info): await self._api.set_security_mode(SecurityMode.PRECONFIGURED_NETWORK_KEY) await self._api.set_channel(network_info.channel) - await self._api.form_network(role=role) - await asyncio.sleep(1) + # TODO: Network settings do not persist. How do you write them? + await self._api.reset() + await self._api.network_init() + await self._api.form_network(role=DeviceType.COORDINATOR) + await self._api.start(autostart=True) async def load_network_info(self, *, load_devices=False): network_info = self.state.network_info @@ -310,7 +331,7 @@ async def send_packet(self, packet): dst_ep=packet.dst_ep, src_addr=src_addr, src_ep=packet.src_ep, - profile=packet.profile_id, + profile=packet.profile_id or 0, addr_mode=addr_mode, cluster=packet.cluster_id, sequence=packet.tsn, @@ -323,4 +344,16 @@ async def send_packet(self, packet): async def permit_ncp(self, time_s=60): assert 0 <= time_s <= 254 - await self._api.set_permit_join(time_s) + + # TODO: this does not work, the NCP responds again with: + # Unknown command received: Command( + # version=0, + # frame_type=, + # reserved=0, + # command_id=, + # seq=144, + # length=1, + # payload=b'\x02' + # ) + + # await self._api.set_permit_join(time_s) From fb91c5236a917214f164b9c39a2fdd5612aa598b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 16 Apr 2024 12:01:35 -0400 Subject: [PATCH 07/22] WIP: do not allow optional, missing, or mis-named command parameters --- zigpy_espzb/api.py | 30 ++++++++++-------------------- zigpy_espzb/zigbee/application.py | 14 +------------- 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index 51b13b0..148a5f7 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -585,34 +585,24 @@ def close(self): self._uart = None async def send_command(self, cmd, **kwargs): - payload = [] tx_schema, _, _ = COMMAND_SCHEMAS[cmd] - trailing_optional = False + + if tx_schema.keys() != kwargs.keys(): + raise TypeError( + f"Command {cmd} with kwargs {kwargs} does not match schema:" + f" {list(tx_schema.keys())}" + ) + + payload = [] for name, param_type in tx_schema.items(): if isinstance(param_type, int): - if name not in kwargs: - # Default value - value = param_type.serialize() - else: - value = type(param_type)(kwargs[name]).serialize() - elif kwargs.get(name) is None: - trailing_optional = True - value = None + value = type(param_type)(kwargs[name]).serialize() elif not isinstance(kwargs[name], param_type): value = param_type(kwargs[name]).serialize() else: value = kwargs[name].serialize() - if value is None: - continue - - if trailing_optional: - raise ValueError( - f"Command {cmd} with kwargs {kwargs}" - f" has non-trailing optional argument" - ) - payload.append(value) serialized_payload = b"".join(payload) @@ -863,7 +853,7 @@ async def get_nwk_extended_panid(self): return rsp["ieee"] async def set_nwk_extended_panid(self, parameter: t.ExtendedPanId): - rsp = await self.send_command(CommandId.extpanid_set, panid=parameter) + rsp = await self.send_command(CommandId.extpanid_set, ieee=parameter) return rsp["status"] diff --git a/zigpy_espzb/zigbee/application.py b/zigpy_espzb/zigbee/application.py index 8f5bd31..b45abdc 100644 --- a/zigpy_espzb/zigbee/application.py +++ b/zigpy_espzb/zigbee/application.py @@ -160,19 +160,7 @@ async def write_network_info(self, *, network_info, node_info): await self._api.set_use_predefined_nwk_panid(True) await self._api.set_nwk_panid(network_info.pan_id) - - # TODO: Why does setting the extended PAN ID trigger a crash? - # Unknown command received: Command( - # version=0, - # frame_type=, - # reserved=0, - # command_id=, - # seq=123, - # length=1, - # payload=b'\x02' - # ) - - # await self._api.set_nwk_extended_panid(network_info.extended_pan_id) + await self._api.set_nwk_extended_panid(network_info.extended_pan_id) await self._api.set_nwk_update_id(network_info.nwk_update_id) await self._api.set_network_key(network_info.network_key.key) await self._api.set_nwk_frame_counter(network_info.network_key.tx_counter) From ab7bd1589b866adf8620f5007038e5488b186e34 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 16 Apr 2024 12:53:22 -0400 Subject: [PATCH 08/22] Log the type of received command --- zigpy_espzb/api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index 148a5f7..ea4a479 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -687,7 +687,11 @@ def data_received(self, data: bytes) -> None: LOGGER.debug("Unparsed data remains after frame: %s, %s", command, rest) LOGGER.debug( - "Received command %s%s (seq %d)", command.command_id, params, command.seq + "Received %s %s%s (seq %d)", + ("indication" if command.frame_type == FrameType.Indicate else "response"), + command.command_id, + params, + command.seq, ) status = Status.SUCCESS From 33b57977c9c5bdce8d8677ad207327242d150b1c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 16 Apr 2024 12:53:45 -0400 Subject: [PATCH 09/22] WIP: Add some sleeps and try to get network formation working (it doesn't) --- zigpy_espzb/api.py | 6 ++++++ zigpy_espzb/zigbee/application.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index ea4a479..00feb57 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -811,6 +811,9 @@ async def form_network( ), ) + # TODO: wait for the `form_network` indication as well? + await asyncio.sleep(2) + return rsp["status"] async def leave_network(self) -> None: @@ -819,6 +822,9 @@ async def leave_network(self) -> None: async def start(self, autostart: bool) -> Status: rsp = await self.send_command(CommandId.start, autostart=t.uint8_t(autostart)) + # TODO: wait for the `form_network` indication as well? + await asyncio.sleep(2) + return rsp["status"] async def get_mac_address(self): diff --git a/zigpy_espzb/zigbee/application.py b/zigpy_espzb/zigbee/application.py index b45abdc..5cf4d7e 100644 --- a/zigpy_espzb/zigbee/application.py +++ b/zigpy_espzb/zigbee/application.py @@ -187,6 +187,8 @@ async def write_network_info(self, *, network_info, node_info): await self._api.set_channel(network_info.channel) # TODO: Network settings do not persist. How do you write them? + await self._api.start(autostart=True) + await self._api.reset() await self._api.network_init() await self._api.form_network(role=DeviceType.COORDINATOR) From 640bb5445719996de1fbb8dfc4819995ad059e15 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 16 Apr 2024 13:28:18 -0400 Subject: [PATCH 10/22] Remove all command serialization code and migrate everything to structs --- zigpy_espzb/api.py | 599 ++------------------- zigpy_espzb/commands.py | 832 ++++++++++++++++++++++++++++++ zigpy_espzb/types.py | 56 ++ zigpy_espzb/zigbee/application.py | 15 +- 4 files changed, 951 insertions(+), 551 deletions(-) create mode 100644 zigpy_espzb/commands.py diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index 00feb57..d48efe8 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -16,14 +16,26 @@ from zigpy.config import CONF_DEVICE_PATH import zigpy.types as t +from zigpy_espzb.commands import ( + COMMAND_SCHEMAS, + Command, + CommandId, + FormNetwork, + FrameType, +) from zigpy_espzb.exception import APIException, CommandError from zigpy_espzb.types import ( Bytes, + DeviceType, ExtendedAddrMode, + FirmwareVersion, + NetworkState, + SecurityMode, ShiftedChannels, + Status, + TXStatus, ZnspTransmitOptions, addr_mode_with_eui64_to_addr_mode_address, - deserialize_dict, ) import zigpy_espzb.uart @@ -34,495 +46,6 @@ REQUEST_RETRY_DELAYS = (0.5, 1.0, 1.5, None) -class DeviceType(t.enum8): - COORDINATOR = 0x00 - ROUTER = 0x01 - END_DEVICE = 0x02 - NONE = 0x03 - - -class Status(t.enum8): - SUCCESS = 0 - FAILURE = 1 - INVALID_VALUE = 2 - TIMEOUT = 3 - UNSUPPORTED = 4 - ERROR = 5 - NO_NETWORK = 6 - BUSY = 7 - - -class FirmwareVersion(t.Struct, t.uint32_t): - reserved: t.uint8_t - patch: t.uint8_t - minor: t.uint8_t - major: t.uint8_t - - -class NetworkState(t.enum8): - OFFLINE = 0 - JOINING = 1 - CONNECTED = 2 - LEAVING = 3 - CONFIRM = 4 - INDICATION = 5 - - -class SecurityMode(t.enum8): - NO_SECURITY = 0x00 - PRECONFIGURED_NETWORK_KEY = 0x01 - - -class ZDPResponseHandling(t.bitmap16): - NONE = 0x0000 - NodeDescRsp = 0x0001 - - -class FormNetwork(t.Struct): - role: DeviceType - install_code_policy: t.Bool - - # For coordinators/routers - max_children: t.uint8_t = t.StructField( - requires=lambda f: f.role in (DeviceType.ROUTER, DeviceType.COORDINATOR) - ) - - # For end devices - ed_timeout: t.uint8_t = t.StructField( - requires=lambda f: f.role == DeviceType.END_DEVICE - ) - keep_alive: t.uint32_t = t.StructField( - requires=lambda f: f.role == DeviceType.END_DEVICE - ) - - -class CommandId(t.enum16): - network_init = 0x0000 - start = 0x0001 - network_state = 0x0002 - stack_status_handler = 0x0003 - form_network = 0x0004 - permit_joining = 0x0005 - join_network = 0x0006 - leave_network = 0x0007 - start_scan = 0x0008 - scan_complete_handler = 0x0009 - stop_scan = 0x000A - panid_get = 0x000B - panid_set = 0x000C - extpanid_get = 0x000D - extpanid_set = 0x000E - primary_channel_mask_get = 0x000F - primary_channel_mask_set = 0x0010 - secondary_channel_mask_get = 0x0011 - secondary_channel_mask_set = 0x0012 - current_channel_get = 0x0013 - current_channel_set = 0x0014 - tx_power_get = 0x0015 - tx_power_set = 0x0016 - network_key_get = 0x0017 - network_key_set = 0x0018 - nwk_frame_counter_get = 0x0019 - nwk_frame_counter_set = 0x001A - network_role_get = 0x001B - network_role_set = 0x001C - short_addr_get = 0x001D - short_addr_set = 0x001E - long_addr_get = 0x001F - long_addr_set = 0x0020 - channel_masks_get = 0x0021 - channel_masks_set = 0x0022 - nwk_update_id_get = 0x0023 - nwk_update_id_set = 0x0024 - trust_center_address_get = 0x0025 - trust_center_address_set = 0x0026 - link_key_get = 0x0027 - link_key_set = 0x0028 - security_mode_get = 0x0029 - security_mode_set = 0x002A - use_predefined_nwk_panid_set = 0x002B - short_to_ieee = 0x002C - ieee_to_short = 0x002D - add_endpoint = 0x0100 - remove_endpoint = 0x0101 - attribute_read = 0x0102 - attribute_write = 0x0103 - attribute_report = 0x0104 - attribute_discover = 0x0105 - aps_read = 0x0106 - aps_write = 0x0107 - report_config = 0x0108 - bind_set = 0x0200 - unbind_set = 0x0201 - find_match = 0x0202 - aps_data_request = 0x0300 - aps_data_indication = 0x0301 - aps_data_confirm = 0x0302 - - -class TXStatus(t.enum8): - SUCCESS = 0x00 - - @classmethod - def _missing_(cls, value): - chained = t.APSStatus(value) - status = t.uint8_t.__new__(cls, chained.value) - status._name_ = chained.name - status._value_ = value - return status - - -class FrameType(t.enum4): - Request = 0 - Response = 1 - Indicate = 2 - - -class Command(t.Struct): - version: t.uint4_t - frame_type: FrameType - reserved: t.uint8_t - - command_id: CommandId - seq: t.uint8_t - length: t.uint16_t - payload: Bytes - - -COMMAND_SCHEMAS = { - CommandId.network_init: ( - {}, - { - "status": Status, - }, - {}, - ), - CommandId.start: ( - { - "autostart": t.Bool, - }, - { - "status": Status, - }, - {}, - ), - CommandId.form_network: ( - { - "form_nwk": FormNetwork, - }, - { - "status": Status, - }, - { - "extended_panid": t.EUI64, - "panid": t.PanId, - "channel": t.uint8_t, - }, - ), - CommandId.permit_joining: ( - { - "duration": t.uint8_t, - }, - { - "status": Status, - }, - { - "duration": t.uint8_t, - }, - ), - CommandId.leave_network: ( - {}, - { - "status": Status, - }, - { - "short_addr": t.NWK, - "device_addr": t.EUI64, - "rejoin": t.Bool, - }, - ), - CommandId.extpanid_get: ( - {}, - {"ieee": t.EUI64}, - {}, - ), - CommandId.extpanid_set: ( - {"ieee": t.EUI64}, - { - "status": Status, - }, - {}, - ), - CommandId.panid_get: ( - {}, - {"panid": t.PanId}, - {}, - ), - CommandId.panid_set: ( - {"panid": t.PanId}, - { - "status": Status, - }, - {}, - ), - CommandId.short_addr_get: ( - {}, - {"short_addr": t.NWK}, - {}, - ), - CommandId.short_addr_set: ( - {"short_addr": t.NWK}, - { - "status": Status, - }, - {}, - ), - CommandId.long_addr_get: ( - {}, - {"ieee": t.EUI64}, - {}, - ), - CommandId.long_addr_set: ( - {"ieee": t.EUI64}, - { - "status": Status, - }, - {}, - ), - CommandId.current_channel_get: ( - {}, - {"channel": t.uint8_t}, - {}, - ), - CommandId.current_channel_set: ( - {"channel": t.uint8_t}, - { - "status": Status, - }, - {}, - ), - CommandId.primary_channel_mask_get: ( - {}, - {"channel_mask": ShiftedChannels}, - {}, - ), - CommandId.primary_channel_mask_set: ( - {"channel_mask": ShiftedChannels}, - { - "status": Status, - }, - {}, - ), - CommandId.add_endpoint: ( - { - "endpoint": t.uint8_t, - "profile_id": t.uint16_t, - "device_id": t.uint16_t, - "app_flags": t.uint8_t, - "input_cluster_count": t.uint8_t, - "output_cluster_count": t.uint8_t, - "input_cluster_list": t.List[t.uint16_t], - "output_cluster_list": t.List[t.uint16_t], - }, - { - "status": Status, - }, - {}, - ), - CommandId.network_state: ( - {}, - { - "network_state": NetworkState, - }, - {}, - ), - CommandId.stack_status_handler: ( - {}, - { - "network_state": t.uint8_t, - }, - { - "network_state": t.uint8_t, - }, - ), - CommandId.aps_data_request: ( - { - "dst_addr": t.EUI64, - "dst_endpoint": t.uint8_t, - "src_endpoint": t.uint8_t, - "address_mode": t.uint8_t, - "profile_id": t.uint16_t, - "cluster_id": t.uint16_t, - "tx_options": t.uint8_t, - "use_alias": t.Bool, - "src_addr": t.EUI64, - "sequence": t.uint8_t, - "radius": t.uint8_t, - "asdu_length": t.uint32_t, - "asdu": Bytes, - }, - { - "status": Status, - }, - {}, - ), - CommandId.aps_data_indication: ( - {}, - { - "network_state": NetworkState, - "dst_addr_mode": ExtendedAddrMode, - "dst_addr": t.EUI64, - "dst_ep": t.uint8_t, - "src_addr_mode": ExtendedAddrMode, - "src_addr": t.EUI64, - "src_ep": t.uint8_t, - "profile_id": t.uint16_t, - "cluster_id": t.uint16_t, - "indication_status": TXStatus, - "security_status": t.uint8_t, - "lqi": t.uint8_t, - "rx_time": t.uint32_t, - "asdu_length": t.uint32_t, - "asdu": Bytes, - }, - { - "network_state": NetworkState, - "dst_addr_mode": ExtendedAddrMode, - "dst_addr": t.EUI64, - "dst_ep": t.uint8_t, - "src_addr_mode": ExtendedAddrMode, - "src_addr": t.EUI64, - "src_ep": t.uint8_t, - "profile_id": t.uint16_t, - "cluster_id": t.uint16_t, - "indication_status": TXStatus, - "security_status": t.uint8_t, - "lqi": t.uint8_t, - "rx_time": t.uint32_t, - "asdu_length": t.uint32_t, - "asdu": Bytes, - }, - ), - CommandId.aps_data_confirm: ( - {}, - { - "network_state": NetworkState, - "dst_addr_mode": ExtendedAddrMode, - "dst_addr": t.EUI64, - "dst_ep": t.uint8_t, - "src_ep": t.uint8_t, - "tx_time": t.uint32_t, - "request_id": t.uint8_t, - "confirm_status": TXStatus, - "asdu_length": t.uint32_t, - "asdu": Bytes, - }, - { - "network_state": NetworkState, - "dst_addr_mode": ExtendedAddrMode, - "dst_addr": t.EUI64, - "dst_ep": t.uint8_t, - "src_ep": t.uint8_t, - "tx_time": t.uint32_t, - "confirm_status": TXStatus, - "asdu_length": t.uint32_t, - "asdu": Bytes, - }, - ), - CommandId.network_key_get: ( - {}, - {"nwk_key": t.KeyData}, - {}, - ), - CommandId.network_key_set: ( - {"nwk_key": t.KeyData}, - { - "status": Status, - }, - {}, - ), - CommandId.nwk_frame_counter_get: ( - {}, - {"nwk_frame_counter": t.uint32_t}, - {}, - ), - CommandId.nwk_frame_counter_set: ( - {"nwk_frame_counter": t.uint32_t}, - { - "status": Status, - }, - {}, - ), - CommandId.network_role_get: ( - {}, - {"role": DeviceType}, - {}, - ), - CommandId.network_role_set: ( - {"role": DeviceType}, - { - "status": Status, - }, - {}, - ), - CommandId.use_predefined_nwk_panid_set: ( - {"predefined": t.Bool}, - { - "status": Status, - }, - {}, - ), - CommandId.nwk_update_id_get: ( - {}, - {"nwk_update_id": t.uint8_t}, - {}, - ), - CommandId.nwk_update_id_set: ( - {"nwk_update_id": t.uint8_t}, - { - "status": Status, - }, - {}, - ), - CommandId.trust_center_address_get: ( - {}, - {"trust_center_addr": t.EUI64}, - {}, - ), - CommandId.trust_center_address_set: ( - {"trust_center_addr": t.EUI64}, - { - "status": Status, - }, - {}, - ), - CommandId.link_key_get: ( - {}, - {"ieee": t.EUI64, "key": t.KeyData}, - {}, - ), - CommandId.link_key_set: ( - {"key": t.KeyData}, - { - "status": Status, - }, - {}, - ), - CommandId.security_mode_get: ( - {}, - {"security_mode": SecurityMode}, - {}, - ), - CommandId.security_mode_set: ( - {"security_mode": SecurityMode}, - { - "status": Status, - }, - {}, - ), -} - - class Znsp: """Espressif ZNSP API class.""" @@ -587,25 +110,9 @@ def close(self): async def send_command(self, cmd, **kwargs): tx_schema, _, _ = COMMAND_SCHEMAS[cmd] - if tx_schema.keys() != kwargs.keys(): - raise TypeError( - f"Command {cmd} with kwargs {kwargs} does not match schema:" - f" {list(tx_schema.keys())}" - ) - - payload = [] + params = tx_schema(**kwargs) + serialized_payload = params.serialize() - for name, param_type in tx_schema.items(): - if isinstance(param_type, int): - value = type(param_type)(kwargs[name]).serialize() - elif not isinstance(kwargs[name], param_type): - value = param_type(kwargs[name]).serialize() - else: - value = kwargs[name].serialize() - - payload.append(value) - - serialized_payload = b"".join(payload) command = Command( version=0b0000, frame_type=FrameType.Request, @@ -623,7 +130,7 @@ async def send_command(self, cmd, **kwargs): async with self._command_lock: seq = self._seq - LOGGER.debug("Sending %s%s (seq=%s)", cmd, kwargs, seq) + LOGGER.debug("Sending %s (seq=%s)", params, seq) self._uart.send(command.replace(seq=seq).serialize()) self._seq = (self._seq % 255) + 1 @@ -672,7 +179,7 @@ def data_received(self, data: bytes) -> None: ) try: - params, rest = deserialize_dict(command.payload, schema) + params, rest = schema.deserialize(command.payload) except Exception: LOGGER.warning("Failed to parse command %s", command, exc_info=True) @@ -687,20 +194,20 @@ def data_received(self, data: bytes) -> None: LOGGER.debug("Unparsed data remains after frame: %s, %s", command, rest) LOGGER.debug( - "Received %s %s%s (seq %d)", + "Received %s %s (seq %d)", ("indication" if command.frame_type == FrameType.Indicate else "response"), - command.command_id, params, command.seq, ) - status = Status.SUCCESS - if "status" in params: - status = params["status"] + status = None + + if hasattr(params, "status"): + status = params.status exc = None - if status != Status.SUCCESS: + if status is not None and status != Status.SUCCESS: exc = CommandError(status, f"{command.command_id}, status: {status}") if fut is not None: @@ -720,7 +227,7 @@ def data_received(self, data: bytes) -> None: if handler := getattr(self, f"_handle_{command.command_id.name}", None): # Queue up the callback within the event loop - asyncio.get_running_loop().call_soon(lambda: handler(**params)) + asyncio.get_running_loop().call_soon(lambda: handler(**params.as_dict())) def _handle_aps_data_indication( self, @@ -779,7 +286,7 @@ async def network_init(self) -> None: async def get_channel_mask(self) -> t.Channels: rsp = await self.send_command(CommandId.primary_channel_mask_get) - return t.Channels.from_channel_list(tuple(rsp["channel_mask"])) + return t.Channels.from_channel_list(tuple(rsp.channel_mask)) async def set_channel_mask(self, channels: t.Channels) -> None: await self.send_command( @@ -814,7 +321,7 @@ async def form_network( # TODO: wait for the `form_network` indication as well? await asyncio.sleep(2) - return rsp["status"] + return rsp.status async def leave_network(self) -> None: await self.send_command(CommandId.leave_network) @@ -825,69 +332,69 @@ async def start(self, autostart: bool) -> Status: # TODO: wait for the `form_network` indication as well? await asyncio.sleep(2) - return rsp["status"] + return rsp.status async def get_mac_address(self): rsp = await self.send_command(CommandId.long_addr_get) - return rsp["ieee"] + return rsp.ieee async def set_mac_address(self, parameter: t.EUI64): rsp = await self.send_command(CommandId.long_addr_set, ieee=parameter) - return rsp["status"] + return rsp.status async def get_nwk_address(self): rsp = await self.send_command(CommandId.short_addr_get) - return rsp["short_addr"] + return rsp.short_addr async def set_nwk_address(self, parameter: t.uint16_t): rsp = await self.send_command(CommandId.short_addr_set, short_addr=parameter) - return rsp["status"] + return rsp.status async def get_nwk_panid(self): rsp = await self.send_command(CommandId.panid_get) - return rsp["panid"] + return rsp.panid async def set_nwk_panid(self, parameter: t.PanId): rsp = await self.send_command(CommandId.panid_set, panid=parameter) - return rsp["status"] + return rsp.status async def get_nwk_extended_panid(self): rsp = await self.send_command(CommandId.extpanid_get) - return rsp["ieee"] + return rsp.ieee async def set_nwk_extended_panid(self, parameter: t.ExtendedPanId): rsp = await self.send_command(CommandId.extpanid_set, ieee=parameter) - return rsp["status"] + return rsp.status async def get_current_channel(self) -> int: rsp = await self.send_command(CommandId.current_channel_get) - return rsp["channel"] + return rsp.channel async def get_nwk_update_id(self): rsp = await self.send_command(CommandId.nwk_update_id_get) - return rsp["nwk_update_id"] + return rsp.nwk_update_id async def set_nwk_update_id(self, parameter: t.uint8_t): rsp = await self.send_command( CommandId.nwk_update_id_set, nwk_update_id=parameter ) - return rsp["status"] + return rsp.status async def get_network_key(self): rsp = await self.send_command(CommandId.network_key_get) - return rsp["nwk_key"] + return rsp.nwk_key async def set_network_key(self, key: t.KeyData): await self.send_command(CommandId.network_key_set, nwk_key=key) @@ -895,7 +402,7 @@ async def set_network_key(self, key: t.KeyData): async def get_nwk_frame_counter(self): rsp = await self.send_command(CommandId.nwk_frame_counter_get) - return rsp["nwk_frame_counter"] + return rsp.nwk_frame_counter async def set_nwk_frame_counter(self, parameter: t.uint32_t): rsp = await self.send_command( @@ -903,24 +410,24 @@ async def set_nwk_frame_counter(self, parameter: t.uint32_t): nwk_frame_counter=parameter, ) - return rsp["status"] + return rsp.status async def get_trust_center_address(self): rsp = await self.send_command(CommandId.trust_center_address_get) - return rsp["trust_center_addr"] + return rsp.trust_center_addr async def set_trust_center_address(self, parameter: t.EUI64): rsp = await self.send_command( CommandId.trust_center_address_set, trust_center_addr=parameter ) - return rsp["status"] + return rsp.status async def get_link_key(self) -> Any: rsp = await self.send_command(CommandId.link_key_get) - return rsp["key"] + return rsp.key async def set_link_key(self, key: t.KeyData): await self.send_command(CommandId.link_key_set, key=key) @@ -928,14 +435,14 @@ async def set_link_key(self, key: t.KeyData): async def get_security_mode(self): rsp = await self.send_command(CommandId.security_mode_get) - return rsp["security_mode"] + return rsp.security_mode async def set_security_mode(self, parameter: SecurityMode): rsp = await self.send_command( CommandId.security_mode_set, security_mode=parameter ) - return rsp["status"] + return rsp.status async def add_endpoint( self, @@ -961,7 +468,7 @@ async def add_endpoint( output_cluster_list=output_clusters, ) - return rsp["status"] + return rsp.status async def set_use_predefined_nwk_panid(self, parameter: t.Bool): rsp = await self.send_command( @@ -969,7 +476,7 @@ async def set_use_predefined_nwk_panid(self, parameter: t.Bool): predefined=parameter, ) - return rsp["status"] + return rsp.status async def set_permit_join(self, duration: t.uint8_t): rsp = await self.send_command( @@ -977,7 +484,7 @@ async def set_permit_join(self, duration: t.uint8_t): duration=duration, ) - return rsp["status"] + return rsp.status async def set_watchdog_ttl(self, parameter: t.uint16_t): rsp = await self.send_command( @@ -985,11 +492,11 @@ async def set_watchdog_ttl(self, parameter: t.uint16_t): role=parameter, ) - return rsp["status"] + return rsp.status async def get_network_role(self) -> DeviceType: rsp = await self.send_command(CommandId.network_role_get) - return rsp["role"] + return rsp.role async def set_network_role(self, role: DeviceType) -> None: rsp = await self.send_command( @@ -997,7 +504,7 @@ async def set_network_role(self, role: DeviceType) -> None: role=role, ) - return rsp["status"] + return rsp.status async def aps_data_request( self, @@ -1046,7 +553,7 @@ async def aps_data_request( async def get_network_state(self) -> NetworkState: rsp = await self.send_command(CommandId.network_state) - return rsp["network_state"] + return rsp.network_state async def reset(self) -> None: # TODO: There is no reset command but we can trigger a crash if we form the diff --git a/zigpy_espzb/commands.py b/zigpy_espzb/commands.py new file mode 100644 index 0000000..b679b37 --- /dev/null +++ b/zigpy_espzb/commands.py @@ -0,0 +1,832 @@ +"""Serial command schemas.""" + +import zigpy.types as t + +from zigpy_espzb.types import ( + Bytes, + DeviceType, + ExtendedAddrMode, + NetworkState, + SecurityMode, + ShiftedChannels, + Status, + TXStatus, +) + + +class CommandId(t.enum16): + network_init = 0x0000 + start = 0x0001 + network_state = 0x0002 + stack_status_handler = 0x0003 + form_network = 0x0004 + permit_joining = 0x0005 + join_network = 0x0006 + leave_network = 0x0007 + start_scan = 0x0008 + scan_complete_handler = 0x0009 + stop_scan = 0x000A + panid_get = 0x000B + panid_set = 0x000C + extpanid_get = 0x000D + extpanid_set = 0x000E + primary_channel_mask_get = 0x000F + primary_channel_mask_set = 0x0010 + secondary_channel_mask_get = 0x0011 + secondary_channel_mask_set = 0x0012 + current_channel_get = 0x0013 + current_channel_set = 0x0014 + tx_power_get = 0x0015 + tx_power_set = 0x0016 + network_key_get = 0x0017 + network_key_set = 0x0018 + nwk_frame_counter_get = 0x0019 + nwk_frame_counter_set = 0x001A + network_role_get = 0x001B + network_role_set = 0x001C + short_addr_get = 0x001D + short_addr_set = 0x001E + long_addr_get = 0x001F + long_addr_set = 0x0020 + channel_masks_get = 0x0021 + channel_masks_set = 0x0022 + nwk_update_id_get = 0x0023 + nwk_update_id_set = 0x0024 + trust_center_address_get = 0x0025 + trust_center_address_set = 0x0026 + link_key_get = 0x0027 + link_key_set = 0x0028 + security_mode_get = 0x0029 + security_mode_set = 0x002A + use_predefined_nwk_panid_set = 0x002B + short_to_ieee = 0x002C + ieee_to_short = 0x002D + add_endpoint = 0x0100 + remove_endpoint = 0x0101 + attribute_read = 0x0102 + attribute_write = 0x0103 + attribute_report = 0x0104 + attribute_discover = 0x0105 + aps_read = 0x0106 + aps_write = 0x0107 + report_config = 0x0108 + bind_set = 0x0200 + unbind_set = 0x0201 + find_match = 0x0202 + aps_data_request = 0x0300 + aps_data_indication = 0x0301 + aps_data_confirm = 0x0302 + + +class FrameType(t.enum4): + Request = 0 + Response = 1 + Indicate = 2 + + +class Command(t.Struct): + version: t.uint4_t + frame_type: FrameType + reserved: t.uint8_t + + command_id: CommandId + seq: t.uint8_t + length: t.uint16_t + payload: Bytes + + +class FormNetwork(t.Struct): + role: DeviceType + install_code_policy: t.Bool + + # For coordinators/routers + max_children: t.uint8_t = t.StructField( + requires=lambda f: f.role in (DeviceType.ROUTER, DeviceType.COORDINATOR) + ) + + # For end devices + ed_timeout: t.uint8_t = t.StructField( + requires=lambda f: f.role == DeviceType.END_DEVICE + ) + keep_alive: t.uint32_t = t.StructField( + requires=lambda f: f.role == DeviceType.END_DEVICE + ) + + +class NetworkInitReq(t.Struct): + pass + + +class NetworkInitRsp(t.Struct): + status: Status + + +class NetworkInitInd(t.Struct): + pass + + +class StartReq(t.Struct): + autostart: t.Bool + + +class StartRsp(t.Struct): + status: Status + + +class StartInd(t.Struct): + pass + + +class FormNetworkReq(t.Struct): + form_nwk: FormNetwork + + +class FormNetworkRsp(t.Struct): + status: Status + + +class FormNetworkInd(t.Struct): + extended_panid: t.EUI64 + panid: t.PanId + channel: t.uint8_t + + +class PermitJoiningReq(t.Struct): + duration: t.uint8_t + + +class PermitJoiningRsp(t.Struct): + status: Status + + +class PermitJoiningInd(t.Struct): + duration: t.uint8_t + + +class LeaveNetworkReq(t.Struct): + pass + + +class LeaveNetworkRsp(t.Struct): + status: Status + + +class LeaveNetworkInd(t.Struct): + short_addr: t.NWK + device_addr: t.EUI64 + rejoin: t.Bool + + +class ExtpanidGetReq(t.Struct): + pass + + +class ExtpanidGetRsp(t.Struct): + ieee: t.EUI64 + + +class ExtpanidGetInd(t.Struct): + pass + + +class ExtpanidSetReq(t.Struct): + ieee: t.EUI64 + + +class ExtpanidSetRsp(t.Struct): + status: Status + + +class ExtpanidSetInd(t.Struct): + pass + + +class PanidGetReq(t.Struct): + pass + + +class PanidGetRsp(t.Struct): + panid: t.PanId + + +class PanidGetInd(t.Struct): + pass + + +class PanidSetReq(t.Struct): + panid: t.PanId + + +class PanidSetRsp(t.Struct): + status: Status + + +class PanidSetInd(t.Struct): + pass + + +class ShortAddrGetReq(t.Struct): + pass + + +class ShortAddrGetRsp(t.Struct): + short_addr: t.NWK + + +class ShortAddrGetInd(t.Struct): + pass + + +class ShortAddrSetReq(t.Struct): + short_addr: t.NWK + + +class ShortAddrSetRsp(t.Struct): + status: Status + + +class ShortAddrSetInd(t.Struct): + pass + + +class LongAddrGetReq(t.Struct): + pass + + +class LongAddrGetRsp(t.Struct): + ieee: t.EUI64 + + +class LongAddrGetInd(t.Struct): + pass + + +class LongAddrSetReq(t.Struct): + ieee: t.EUI64 + + +class LongAddrSetRsp(t.Struct): + status: Status + + +class LongAddrSetInd(t.Struct): + pass + + +class CurrentChannelGetReq(t.Struct): + pass + + +class CurrentChannelGetRsp(t.Struct): + channel: t.uint8_t + + +class CurrentChannelGetInd(t.Struct): + pass + + +class CurrentChannelSetReq(t.Struct): + channel: t.uint8_t + + +class CurrentChannelSetRsp(t.Struct): + status: Status + + +class CurrentChannelSetInd(t.Struct): + pass + + +class PrimaryChannelMaskGetReq(t.Struct): + pass + + +class PrimaryChannelMaskGetRsp(t.Struct): + channel_mask: ShiftedChannels + + +class PrimaryChannelMaskGetInd(t.Struct): + pass + + +class PrimaryChannelMaskSetReq(t.Struct): + channel_mask: ShiftedChannels + + +class PrimaryChannelMaskSetRsp(t.Struct): + status: Status + + +class PrimaryChannelMaskSetInd(t.Struct): + pass + + +class AddEndpointReq(t.Struct): + endpoint: t.uint8_t + profile_id: t.uint16_t + device_id: t.uint16_t + app_flags: t.uint8_t + input_cluster_count: t.uint8_t + output_cluster_count: t.uint8_t + input_cluster_list: t.List[t.uint16_t] + output_cluster_list: t.List[t.uint16_t] + + +class AddEndpointRsp(t.Struct): + status: Status + + +class AddEndpointInd(t.Struct): + pass + + +class NetworkStateReq(t.Struct): + pass + + +class NetworkStateRsp(t.Struct): + network_state: NetworkState + + +class NetworkStateInd(t.Struct): + pass + + +class StackStatusHandlerReq(t.Struct): + pass + + +class StackStatusHandlerRsp(t.Struct): + network_state: t.uint8_t + + +class StackStatusHandlerInd(t.Struct): + network_state: t.uint8_t + + +class ApsDataRequestReq(t.Struct): + dst_addr: t.EUI64 + dst_endpoint: t.uint8_t + src_endpoint: t.uint8_t + address_mode: t.uint8_t + profile_id: t.uint16_t + cluster_id: t.uint16_t + tx_options: t.uint8_t + use_alias: t.Bool + src_addr: t.EUI64 + sequence: t.uint8_t + radius: t.uint8_t + asdu_length: t.uint32_t + asdu: Bytes + + +class ApsDataRequestRsp(t.Struct): + status: Status + + +class ApsDataRequestInd(t.Struct): + pass + + +class ApsDataIndicationReq(t.Struct): + pass + + +class ApsDataIndicationRsp(t.Struct): + network_state: NetworkState + dst_addr_mode: ExtendedAddrMode + dst_addr: t.EUI64 + dst_ep: t.uint8_t + src_addr_mode: ExtendedAddrMode + src_addr: t.EUI64 + src_ep: t.uint8_t + profile_id: t.uint16_t + cluster_id: t.uint16_t + indication_status: TXStatus + security_status: t.uint8_t + lqi: t.uint8_t + rx_time: t.uint32_t + asdu_length: t.uint32_t + asdu: Bytes + + +class ApsDataIndicationInd(t.Struct): + network_state: NetworkState + dst_addr_mode: ExtendedAddrMode + dst_addr: t.EUI64 + dst_ep: t.uint8_t + src_addr_mode: ExtendedAddrMode + src_addr: t.EUI64 + src_ep: t.uint8_t + profile_id: t.uint16_t + cluster_id: t.uint16_t + indication_status: TXStatus + security_status: t.uint8_t + lqi: t.uint8_t + rx_time: t.uint32_t + asdu_length: t.uint32_t + asdu: Bytes + + +class ApsDataConfirmReq(t.Struct): + pass + + +class ApsDataConfirmRsp(t.Struct): + network_state: NetworkState + dst_addr_mode: ExtendedAddrMode + dst_addr: t.EUI64 + dst_ep: t.uint8_t + src_ep: t.uint8_t + tx_time: t.uint32_t + request_id: t.uint8_t + confirm_status: TXStatus + asdu_length: t.uint32_t + asdu: Bytes + + +class ApsDataConfirmInd(t.Struct): + network_state: NetworkState + dst_addr_mode: ExtendedAddrMode + dst_addr: t.EUI64 + dst_ep: t.uint8_t + src_ep: t.uint8_t + tx_time: t.uint32_t + confirm_status: TXStatus + asdu_length: t.uint32_t + asdu: Bytes + + +class NetworkKeyGetReq(t.Struct): + pass + + +class NetworkKeyGetRsp(t.Struct): + nwk_key: t.KeyData + + +class NetworkKeyGetInd(t.Struct): + pass + + +class NetworkKeySetReq(t.Struct): + nwk_key: t.KeyData + + +class NetworkKeySetRsp(t.Struct): + status: Status + + +class NetworkKeySetInd(t.Struct): + pass + + +class NwkFrameCounterGetReq(t.Struct): + pass + + +class NwkFrameCounterGetRsp(t.Struct): + nwk_frame_counter: t.uint32_t + + +class NwkFrameCounterGetInd(t.Struct): + pass + + +class NwkFrameCounterSetReq(t.Struct): + nwk_frame_counter: t.uint32_t + + +class NwkFrameCounterSetRsp(t.Struct): + status: Status + + +class NwkFrameCounterSetInd(t.Struct): + pass + + +class NetworkRoleGetReq(t.Struct): + pass + + +class NetworkRoleGetRsp(t.Struct): + role: DeviceType + + +class NetworkRoleGetInd(t.Struct): + pass + + +class NetworkRoleSetReq(t.Struct): + role: DeviceType + + +class NetworkRoleSetRsp(t.Struct): + status: Status + + +class NetworkRoleSetInd(t.Struct): + pass + + +class UsePredefinedNwkPanidSetReq(t.Struct): + predefined: t.Bool + + +class UsePredefinedNwkPanidSetRsp(t.Struct): + status: Status + + +class UsePredefinedNwkPanidSetInd(t.Struct): + pass + + +class NwkUpdateIdGetReq(t.Struct): + pass + + +class NwkUpdateIdGetRsp(t.Struct): + nwk_update_id: t.uint8_t + + +class NwkUpdateIdGetInd(t.Struct): + pass + + +class NwkUpdateIdSetReq(t.Struct): + nwk_update_id: t.uint8_t + + +class NwkUpdateIdSetRsp(t.Struct): + status: Status + + +class NwkUpdateIdSetInd(t.Struct): + pass + + +class TrustCenterAddressGetReq(t.Struct): + pass + + +class TrustCenterAddressGetRsp(t.Struct): + trust_center_addr: t.EUI64 + + +class TrustCenterAddressGetInd(t.Struct): + pass + + +class TrustCenterAddressSetReq(t.Struct): + trust_center_addr: t.EUI64 + + +class TrustCenterAddressSetRsp(t.Struct): + status: Status + + +class TrustCenterAddressSetInd(t.Struct): + pass + + +class LinkKeyGetReq(t.Struct): + pass + + +class LinkKeyGetRsp(t.Struct): + ieee: t.EUI64 + key: t.KeyData + + +class LinkKeyGetInd(t.Struct): + pass + + +class LinkKeySetReq(t.Struct): + key: t.KeyData + + +class LinkKeySetRsp(t.Struct): + status: Status + + +class LinkKeySetInd(t.Struct): + pass + + +class SecurityModeGetReq(t.Struct): + pass + + +class SecurityModeGetRsp(t.Struct): + security_mode: SecurityMode + + +class SecurityModeGetInd(t.Struct): + pass + + +class SecurityModeSetReq(t.Struct): + security_mode: SecurityMode + + +class SecurityModeSetRsp(t.Struct): + status: Status + + +class SecurityModeSetInd(t.Struct): + pass + + +COMMAND_SCHEMAS = { + CommandId.network_init: ( + NetworkInitReq, + NetworkInitRsp, + NetworkInitInd, + ), + CommandId.start: ( + StartReq, + StartRsp, + StartInd, + ), + CommandId.form_network: ( + FormNetworkReq, + FormNetworkRsp, + FormNetworkInd, + ), + CommandId.permit_joining: ( + PermitJoiningReq, + PermitJoiningRsp, + PermitJoiningInd, + ), + CommandId.leave_network: ( + LeaveNetworkReq, + LeaveNetworkRsp, + LeaveNetworkInd, + ), + CommandId.extpanid_get: ( + ExtpanidGetReq, + ExtpanidGetRsp, + ExtpanidGetInd, + ), + CommandId.extpanid_set: ( + ExtpanidSetReq, + ExtpanidSetRsp, + ExtpanidSetInd, + ), + CommandId.panid_get: ( + PanidGetReq, + PanidGetRsp, + PanidGetInd, + ), + CommandId.panid_set: ( + PanidSetReq, + PanidSetRsp, + PanidSetInd, + ), + CommandId.short_addr_get: ( + ShortAddrGetReq, + ShortAddrGetRsp, + ShortAddrGetInd, + ), + CommandId.short_addr_set: ( + ShortAddrSetReq, + ShortAddrSetRsp, + ShortAddrSetInd, + ), + CommandId.long_addr_get: ( + LongAddrGetReq, + LongAddrGetRsp, + LongAddrGetInd, + ), + CommandId.long_addr_set: ( + LongAddrSetReq, + LongAddrSetRsp, + LongAddrSetInd, + ), + CommandId.current_channel_get: ( + CurrentChannelGetReq, + CurrentChannelGetRsp, + CurrentChannelGetInd, + ), + CommandId.current_channel_set: ( + CurrentChannelSetReq, + CurrentChannelSetRsp, + CurrentChannelSetInd, + ), + CommandId.primary_channel_mask_get: ( + PrimaryChannelMaskGetReq, + PrimaryChannelMaskGetRsp, + PrimaryChannelMaskGetInd, + ), + CommandId.primary_channel_mask_set: ( + PrimaryChannelMaskSetReq, + PrimaryChannelMaskSetRsp, + PrimaryChannelMaskSetInd, + ), + CommandId.add_endpoint: ( + AddEndpointReq, + AddEndpointRsp, + AddEndpointInd, + ), + CommandId.network_state: ( + NetworkStateReq, + NetworkStateRsp, + NetworkStateInd, + ), + CommandId.stack_status_handler: ( + StackStatusHandlerReq, + StackStatusHandlerRsp, + StackStatusHandlerInd, + ), + CommandId.aps_data_request: ( + ApsDataRequestReq, + ApsDataRequestRsp, + ApsDataRequestInd, + ), + CommandId.aps_data_indication: ( + ApsDataIndicationReq, + ApsDataIndicationRsp, + ApsDataIndicationInd, + ), + CommandId.aps_data_confirm: ( + ApsDataConfirmReq, + ApsDataConfirmRsp, + ApsDataConfirmInd, + ), + CommandId.network_key_get: ( + NetworkKeyGetReq, + NetworkKeyGetRsp, + NetworkKeyGetInd, + ), + CommandId.network_key_set: ( + NetworkKeySetReq, + NetworkKeySetRsp, + NetworkKeySetInd, + ), + CommandId.nwk_frame_counter_get: ( + NwkFrameCounterGetReq, + NwkFrameCounterGetRsp, + NwkFrameCounterGetInd, + ), + CommandId.nwk_frame_counter_set: ( + NwkFrameCounterSetReq, + NwkFrameCounterSetRsp, + NwkFrameCounterSetInd, + ), + CommandId.network_role_get: ( + NetworkRoleGetReq, + NetworkRoleGetRsp, + NetworkRoleGetInd, + ), + CommandId.network_role_set: ( + NetworkRoleSetReq, + NetworkRoleSetRsp, + NetworkRoleSetInd, + ), + CommandId.use_predefined_nwk_panid_set: ( + UsePredefinedNwkPanidSetReq, + UsePredefinedNwkPanidSetRsp, + UsePredefinedNwkPanidSetInd, + ), + CommandId.nwk_update_id_get: ( + NwkUpdateIdGetReq, + NwkUpdateIdGetRsp, + NwkUpdateIdGetInd, + ), + CommandId.nwk_update_id_set: ( + NwkUpdateIdSetReq, + NwkUpdateIdSetRsp, + NwkUpdateIdSetInd, + ), + CommandId.trust_center_address_get: ( + TrustCenterAddressGetReq, + TrustCenterAddressGetRsp, + TrustCenterAddressGetInd, + ), + CommandId.trust_center_address_set: ( + TrustCenterAddressSetReq, + TrustCenterAddressSetRsp, + TrustCenterAddressSetInd, + ), + CommandId.link_key_get: ( + LinkKeyGetReq, + LinkKeyGetRsp, + LinkKeyGetInd, + ), + CommandId.link_key_set: ( + LinkKeySetReq, + LinkKeySetRsp, + LinkKeySetInd, + ), + CommandId.security_mode_get: ( + SecurityModeGetReq, + SecurityModeGetRsp, + SecurityModeGetInd, + ), + CommandId.security_mode_set: ( + SecurityModeSetReq, + SecurityModeSetRsp, + SecurityModeSetInd, + ), +} diff --git a/zigpy_espzb/types.py b/zigpy_espzb/types.py index 947cc6a..d2f02ca 100644 --- a/zigpy_espzb/types.py +++ b/zigpy_espzb/types.py @@ -115,3 +115,59 @@ class ShiftedChannels(t.bitmap32): def from_zigpy_channels(cls, channels: t.Channels) -> ShiftedChannels: """Convert a Zigpy Channels to a ShiftedChannels.""" return cls.from_channel_list(tuple(channels)) + + +class DeviceType(t.enum8): + COORDINATOR = 0x00 + ROUTER = 0x01 + END_DEVICE = 0x02 + NONE = 0x03 + + +class Status(t.enum8): + SUCCESS = 0 + FAILURE = 1 + INVALID_VALUE = 2 + TIMEOUT = 3 + UNSUPPORTED = 4 + ERROR = 5 + NO_NETWORK = 6 + BUSY = 7 + + +class FirmwareVersion(t.Struct, t.uint32_t): + reserved: t.uint8_t + patch: t.uint8_t + minor: t.uint8_t + major: t.uint8_t + + +class NetworkState(t.enum8): + OFFLINE = 0 + JOINING = 1 + CONNECTED = 2 + LEAVING = 3 + CONFIRM = 4 + INDICATION = 5 + + +class SecurityMode(t.enum8): + NO_SECURITY = 0x00 + PRECONFIGURED_NETWORK_KEY = 0x01 + + +class ZDPResponseHandling(t.bitmap16): + NONE = 0x0000 + NodeDescRsp = 0x0001 + + +class TXStatus(t.enum8): + SUCCESS = 0x00 + + @classmethod + def _missing_(cls, value): + chained = t.APSStatus(value) + status = t.uint8_t.__new__(cls, chained.value) + status._name_ = chained.name + status._value_ = value + return status diff --git a/zigpy_espzb/zigbee/application.py b/zigpy_espzb/zigbee/application.py index 5cf4d7e..2db6a00 100644 --- a/zigpy_espzb/zigbee/application.py +++ b/zigpy_espzb/zigbee/application.py @@ -25,8 +25,13 @@ import zigpy.util import zigpy.zdo.types as zdo_t -from zigpy_espzb.api import DeviceType, NetworkState, SecurityMode, Znsp -import zigpy_espzb.types as espzb_t +from zigpy_espzb.api import Znsp +from zigpy_espzb.types import ( + DeviceType, + NetworkState, + SecurityMode, + ZnspTransmitOptions, +) LOGGER = logging.getLogger(__name__) @@ -307,13 +312,13 @@ async def send_packet(self, packet): if packet.source_route is not None: force_relays = packet.source_route - tx_options = espzb_t.ZnspTransmitOptions.NONE + tx_options = ZnspTransmitOptions.NONE if zigpy.types.TransmitOptions.ACK in packet.tx_options: - tx_options |= espzb_t.ZnspTransmitOptions.ACK_ENABLED + tx_options |= ZnspTransmitOptions.ACK_ENABLED if zigpy.types.TransmitOptions.APS_Encryption in packet.tx_options: - tx_options |= espzb_t.ZnspTransmitOptions.SECURITY_ENABLED + tx_options |= ZnspTransmitOptions.SECURITY_ENABLED async with self._limit_concurrency(): await self._api.aps_data_request( From c00bfc6994f3a8136e730f6e616ad6c8c919c6bc Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 16 Apr 2024 13:29:19 -0400 Subject: [PATCH 11/22] Merge `FormNetwork` with `FormNetworkReq` --- zigpy_espzb/api.py | 20 ++++++-------------- zigpy_espzb/commands.py | 34 +++++++++++++++------------------- 2 files changed, 21 insertions(+), 33 deletions(-) diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index d48efe8..a7604ce 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -16,13 +16,7 @@ from zigpy.config import CONF_DEVICE_PATH import zigpy.types as t -from zigpy_espzb.commands import ( - COMMAND_SCHEMAS, - Command, - CommandId, - FormNetwork, - FrameType, -) +from zigpy_espzb.commands import COMMAND_SCHEMAS, Command, CommandId, FrameType from zigpy_espzb.exception import APIException, CommandError from zigpy_espzb.types import ( Bytes, @@ -309,13 +303,11 @@ async def form_network( ) -> None: rsp = await self.send_command( CommandId.form_network, - form_nwk=FormNetwork( - role=role, - install_code_policy=install_code_policy, - max_children=max_children, - ed_timeout=ed_timeout, - keep_alive=keep_alive, - ), + role=role, + install_code_policy=install_code_policy, + max_children=max_children, + ed_timeout=ed_timeout, + keep_alive=keep_alive, ) # TODO: wait for the `form_network` indication as well? diff --git a/zigpy_espzb/commands.py b/zigpy_espzb/commands.py index b679b37..bf65741 100644 --- a/zigpy_espzb/commands.py +++ b/zigpy_espzb/commands.py @@ -95,24 +95,6 @@ class Command(t.Struct): payload: Bytes -class FormNetwork(t.Struct): - role: DeviceType - install_code_policy: t.Bool - - # For coordinators/routers - max_children: t.uint8_t = t.StructField( - requires=lambda f: f.role in (DeviceType.ROUTER, DeviceType.COORDINATOR) - ) - - # For end devices - ed_timeout: t.uint8_t = t.StructField( - requires=lambda f: f.role == DeviceType.END_DEVICE - ) - keep_alive: t.uint32_t = t.StructField( - requires=lambda f: f.role == DeviceType.END_DEVICE - ) - - class NetworkInitReq(t.Struct): pass @@ -138,7 +120,21 @@ class StartInd(t.Struct): class FormNetworkReq(t.Struct): - form_nwk: FormNetwork + role: DeviceType + install_code_policy: t.Bool + + # For coordinators/routers + max_children: t.uint8_t = t.StructField( + requires=lambda f: f.role in (DeviceType.ROUTER, DeviceType.COORDINATOR) + ) + + # For end devices + ed_timeout: t.uint8_t = t.StructField( + requires=lambda f: f.role == DeviceType.END_DEVICE + ) + keep_alive: t.uint32_t = t.StructField( + requires=lambda f: f.role == DeviceType.END_DEVICE + ) class FormNetworkRsp(t.Struct): From c182c74a2abd74614b999b6cf19666e87128b60d Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 16 Apr 2024 13:46:16 -0400 Subject: [PATCH 12/22] Directly send command objects, don't pass kwargs --- zigpy_espzb/api.py | 234 +++++++++++++++++---------------------- zigpy_espzb/commands.py | 238 +++++++++++++++++++++------------------- 2 files changed, 223 insertions(+), 249 deletions(-) diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index a7604ce..0bbc02a 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -16,7 +16,13 @@ from zigpy.config import CONF_DEVICE_PATH import zigpy.types as t -from zigpy_espzb.commands import COMMAND_SCHEMAS, Command, CommandId, FrameType +from zigpy_espzb import commands +from zigpy_espzb.commands import ( + COMMAND_SCHEMA_TO_COMMAND_ID, + COMMAND_SCHEMAS, + CommandFrame, + FrameType, +) from zigpy_espzb.exception import APIException, CommandError from zigpy_espzb.types import ( Bytes, @@ -101,17 +107,15 @@ def close(self): self._uart.close() self._uart = None - async def send_command(self, cmd, **kwargs): - tx_schema, _, _ = COMMAND_SCHEMAS[cmd] - - params = tx_schema(**kwargs) - serialized_payload = params.serialize() + async def send_command(self, command: t.Struct): + command_id = COMMAND_SCHEMA_TO_COMMAND_ID[type(command)] + serialized_payload = command.serialize() - command = Command( + command_frame = CommandFrame( version=0b0000, frame_type=FrameType.Request, reserved=0x00, - command_id=cmd, + command_id=command_id, seq=None, length=len(serialized_payload), payload=serialized_payload, @@ -124,25 +128,25 @@ async def send_command(self, cmd, **kwargs): async with self._command_lock: seq = self._seq - LOGGER.debug("Sending %s (seq=%s)", params, seq) - self._uart.send(command.replace(seq=seq).serialize()) + LOGGER.debug("Sending %s (seq=%s)", command, seq) + self._uart.send(command_frame.replace(seq=seq).serialize()) self._seq = (self._seq % 255) + 1 fut = asyncio.Future() - self._awaiting[seq][cmd].append(fut) + self._awaiting[seq][command_id].append(fut) try: async with asyncio_timeout(COMMAND_TIMEOUT): return await fut except asyncio.TimeoutError: - LOGGER.debug("No response to '%s' command with seq %d", cmd, seq) + LOGGER.debug("No response to '%s' command with seq %d", command, seq) raise finally: - self._awaiting[seq][cmd].remove(fut) + self._awaiting[seq][command_id].remove(fut) def data_received(self, data: bytes) -> None: - command, _ = Command.deserialize(data) + command, _ = CommandFrame.deserialize(data) if command.command_id not in COMMAND_SCHEMAS: LOGGER.warning("Unknown command received: %s", command) @@ -276,20 +280,21 @@ def _handle_network_state(self, network_state: NetworkState) -> None: self._handle_network_state_changed(network_state=network_state) async def network_init(self) -> None: - await self.send_command(CommandId.network_init) + await self.send_command(commands.NetworkInitReq()) async def get_channel_mask(self) -> t.Channels: - rsp = await self.send_command(CommandId.primary_channel_mask_get) + rsp = await self.send_command(commands.PrimaryChannelMaskGetReq()) return t.Channels.from_channel_list(tuple(rsp.channel_mask)) async def set_channel_mask(self, channels: t.Channels) -> None: await self.send_command( - CommandId.primary_channel_mask_set, - channel_mask=ShiftedChannels.from_channel_list(channels), + commands.PrimaryChannelMaskSetReq( + channel_mask=ShiftedChannels.from_channel_list(channels) + ) ) async def set_channel(self, channel: int) -> None: - await self.send_command(CommandId.current_channel_set, channel=channel) + await self.send_command(commands.CurrentChannelSetReq(channel=channel)) async def form_network( self, @@ -301,140 +306,116 @@ async def form_network( ed_timeout: t.uint8_t = 0, keep_alive: t.uint32_t = 0, ) -> None: - rsp = await self.send_command( - CommandId.form_network, - role=role, - install_code_policy=install_code_policy, - max_children=max_children, - ed_timeout=ed_timeout, - keep_alive=keep_alive, + await self.send_command( + commands.FormNetworkReq( + role=role, + install_code_policy=install_code_policy, + max_children=max_children, + ed_timeout=ed_timeout, + keep_alive=keep_alive, + ) ) # TODO: wait for the `form_network` indication as well? await asyncio.sleep(2) - return rsp.status - async def leave_network(self) -> None: - await self.send_command(CommandId.leave_network) + await self.send_command(commands.LeaveNetworkReq) async def start(self, autostart: bool) -> Status: - rsp = await self.send_command(CommandId.start, autostart=t.uint8_t(autostart)) + await self.send_command(commands.StartReq(autostart=autostart)) # TODO: wait for the `form_network` indication as well? await asyncio.sleep(2) - return rsp.status - async def get_mac_address(self): - rsp = await self.send_command(CommandId.long_addr_get) + rsp = await self.send_command(commands.LongAddrGetReq()) return rsp.ieee async def set_mac_address(self, parameter: t.EUI64): - rsp = await self.send_command(CommandId.long_addr_set, ieee=parameter) - - return rsp.status + await self.send_command(commands.LongAddrSetReq(ieee=parameter)) async def get_nwk_address(self): - rsp = await self.send_command(CommandId.short_addr_get) + rsp = await self.send_command(commands.ShortAddrGetReq()) return rsp.short_addr async def set_nwk_address(self, parameter: t.uint16_t): - rsp = await self.send_command(CommandId.short_addr_set, short_addr=parameter) - - return rsp.status + await self.send_command(commands.ShortAddrSetReq(short_addr=parameter)) async def get_nwk_panid(self): - rsp = await self.send_command(CommandId.panid_get) + rsp = await self.send_command(commands.PanidGetReq()) return rsp.panid async def set_nwk_panid(self, parameter: t.PanId): - rsp = await self.send_command(CommandId.panid_set, panid=parameter) - - return rsp.status + await self.send_command(commands.PanidSetReq(panid=parameter)) async def get_nwk_extended_panid(self): - rsp = await self.send_command(CommandId.extpanid_get) + rsp = await self.send_command(commands.ExtpanidGetReq()) return rsp.ieee async def set_nwk_extended_panid(self, parameter: t.ExtendedPanId): - rsp = await self.send_command(CommandId.extpanid_set, ieee=parameter) - - return rsp.status + await self.send_command(commands.ExtpanidSetReq(ieee=parameter)) async def get_current_channel(self) -> int: - rsp = await self.send_command(CommandId.current_channel_get) + rsp = await self.send_command(commands.CurrentChannelGetReq()) return rsp.channel async def get_nwk_update_id(self): - rsp = await self.send_command(CommandId.nwk_update_id_get) + rsp = await self.send_command(commands.NwkUpdateIdGetReq()) return rsp.nwk_update_id async def set_nwk_update_id(self, parameter: t.uint8_t): - rsp = await self.send_command( - CommandId.nwk_update_id_set, nwk_update_id=parameter - ) - - return rsp.status + await self.send_command(commands.NwkUpdateIdSetReq(nwk_update_id=parameter)) async def get_network_key(self): - rsp = await self.send_command(CommandId.network_key_get) + rsp = await self.send_command(commands.NetworkKeyGetReq()) return rsp.nwk_key async def set_network_key(self, key: t.KeyData): - await self.send_command(CommandId.network_key_set, nwk_key=key) + await self.send_command(commands.NetworkKeySetReq(nwk_key=key)) async def get_nwk_frame_counter(self): - rsp = await self.send_command(CommandId.nwk_frame_counter_get) + rsp = await self.send_command(commands.NwkFrameCounterGetReq()) return rsp.nwk_frame_counter - async def set_nwk_frame_counter(self, parameter: t.uint32_t): - rsp = await self.send_command( - CommandId.nwk_frame_counter_set, - nwk_frame_counter=parameter, + async def set_nwk_frame_counter(self, counter: t.uint32_t): + await self.send_command( + commands.NwkFrameCounterSetReq(nwk_frame_counter=counter) ) - return rsp.status - async def get_trust_center_address(self): - rsp = await self.send_command(CommandId.trust_center_address_get) + rsp = await self.send_command(commands.TrustCenterAddressGetReq()) return rsp.trust_center_addr - async def set_trust_center_address(self, parameter: t.EUI64): - rsp = await self.send_command( - CommandId.trust_center_address_set, trust_center_addr=parameter + async def set_trust_center_address(self, addr: t.EUI64) -> None: + await self.send_command( + commands.TrustCenterAddressSetReq(trust_center_addr=addr) ) - return rsp.status - async def get_link_key(self) -> Any: - rsp = await self.send_command(CommandId.link_key_get) + rsp = await self.send_command(commands.LinkKeyGetReq()) return rsp.key async def set_link_key(self, key: t.KeyData): - await self.send_command(CommandId.link_key_set, key=key) + await self.send_command(commands.LinkKeySetReq(key=key)) async def get_security_mode(self): - rsp = await self.send_command(CommandId.security_mode_get) + rsp = await self.send_command(commands.SecurityModeGetReq()) return rsp.security_mode - async def set_security_mode(self, parameter: SecurityMode): - rsp = await self.send_command( - CommandId.security_mode_set, security_mode=parameter - ) - - return rsp.status + async def set_security_mode(self, mode: SecurityMode): + await self.send_command(commands.SecurityModeSetReq(security_mode=mode)) async def add_endpoint( self, @@ -446,57 +427,41 @@ async def add_endpoint( output_clusters: list[t.ClusterId], ): if profile == 0xC05E: - return Status.SUCCESS - - rsp = await self.send_command( - CommandId.add_endpoint, - endpoint=endpoint, - profile_id=profile, - device_id=device_type, - app_flags=device_version, - input_cluster_count=len(input_clusters), - output_cluster_count=len(output_clusters), - input_cluster_list=input_clusters, - output_cluster_list=output_clusters, - ) - - return rsp.status + return - async def set_use_predefined_nwk_panid(self, parameter: t.Bool): - rsp = await self.send_command( - CommandId.use_predefined_nwk_panid_set, - predefined=parameter, + await self.send_command( + commands.AddEndpointReq( + endpoint=endpoint, + profile_id=profile, + device_id=device_type, + app_flags=device_version, + input_cluster_count=len(input_clusters), + output_cluster_count=len(output_clusters), + input_cluster_list=input_clusters, + output_cluster_list=output_clusters, + ) ) - return rsp.status - - async def set_permit_join(self, duration: t.uint8_t): - rsp = await self.send_command( - CommandId.permit_joining, - duration=duration, + async def set_use_predefined_nwk_panid(self, use_predefined: t.Bool): + await self.send_command( + commands.UsePredefinedNwkPanidSetReq( + predefined=use_predefined, + ) ) - return rsp.status - - async def set_watchdog_ttl(self, parameter: t.uint16_t): - rsp = await self.send_command( - CommandId.watchdog_ttl_set, - role=parameter, + async def set_permit_join(self, duration: t.uint8_t): + await self.send_command( + commands.PermitJoiningReq( + duration=duration, + ) ) - return rsp.status - async def get_network_role(self) -> DeviceType: - rsp = await self.send_command(CommandId.network_role_get) + rsp = await self.send_command(commands.NetworkRoleGetReq()) return rsp.role async def set_network_role(self, role: DeviceType) -> None: - rsp = await self.send_command( - CommandId.network_role_set, - role=role, - ) - - return rsp.status + await self.send_command(commands.NetworkRoleSetReq(role=role)) async def aps_data_request( self, @@ -517,20 +482,21 @@ async def aps_data_request( for delay in REQUEST_RETRY_DELAYS: try: await self.send_command( - CommandId.aps_data_request, - dst_addr=dst_addr, - dst_endpoint=dst_ep, - src_endpoint=src_ep, - address_mode=addr_mode, - profile_id=profile, - cluster_id=cluster, - tx_options=options, - use_alias=False, - src_addr=src_addr, - sequence=sequence, - radius=radius, - asdu_length=len(data), - asdu=t.List(data), + commands.ApsDataRequestReq( + dst_addr=dst_addr, + dst_endpoint=dst_ep, + src_endpoint=src_ep, + address_mode=addr_mode, + profile_id=profile, + cluster_id=cluster, + tx_options=options, + use_alias=False, + src_addr=src_addr, + sequence=sequence, + radius=radius, + asdu_length=len(data), + asdu=t.List(data), + ) ) except CommandError as ex: LOGGER.debug("'aps_data_request' failure: %s", ex) @@ -543,7 +509,7 @@ async def aps_data_request( return async def get_network_state(self) -> NetworkState: - rsp = await self.send_command(CommandId.network_state) + rsp = await self.send_command(commands.NetworkStateReq()) return rsp.network_state diff --git a/zigpy_espzb/commands.py b/zigpy_espzb/commands.py index bf65741..136ffe2 100644 --- a/zigpy_espzb/commands.py +++ b/zigpy_espzb/commands.py @@ -84,7 +84,7 @@ class FrameType(t.enum4): Indicate = 2 -class Command(t.Struct): +class CommandFrame(t.Struct): version: t.uint4_t frame_type: FrameType reserved: t.uint8_t @@ -95,31 +95,35 @@ class Command(t.Struct): payload: Bytes -class NetworkInitReq(t.Struct): +class BaseCommand(t.Struct): pass -class NetworkInitRsp(t.Struct): +class NetworkInitReq(BaseCommand): + pass + + +class NetworkInitRsp(BaseCommand): status: Status -class NetworkInitInd(t.Struct): +class NetworkInitInd(BaseCommand): pass -class StartReq(t.Struct): +class StartReq(BaseCommand): autostart: t.Bool -class StartRsp(t.Struct): +class StartRsp(BaseCommand): status: Status -class StartInd(t.Struct): +class StartInd(BaseCommand): pass -class FormNetworkReq(t.Struct): +class FormNetworkReq(BaseCommand): role: DeviceType install_code_policy: t.Bool @@ -137,187 +141,187 @@ class FormNetworkReq(t.Struct): ) -class FormNetworkRsp(t.Struct): +class FormNetworkRsp(BaseCommand): status: Status -class FormNetworkInd(t.Struct): +class FormNetworkInd(BaseCommand): extended_panid: t.EUI64 panid: t.PanId channel: t.uint8_t -class PermitJoiningReq(t.Struct): +class PermitJoiningReq(BaseCommand): duration: t.uint8_t -class PermitJoiningRsp(t.Struct): +class PermitJoiningRsp(BaseCommand): status: Status -class PermitJoiningInd(t.Struct): +class PermitJoiningInd(BaseCommand): duration: t.uint8_t -class LeaveNetworkReq(t.Struct): +class LeaveNetworkReq(BaseCommand): pass -class LeaveNetworkRsp(t.Struct): +class LeaveNetworkRsp(BaseCommand): status: Status -class LeaveNetworkInd(t.Struct): +class LeaveNetworkInd(BaseCommand): short_addr: t.NWK device_addr: t.EUI64 rejoin: t.Bool -class ExtpanidGetReq(t.Struct): +class ExtpanidGetReq(BaseCommand): pass -class ExtpanidGetRsp(t.Struct): +class ExtpanidGetRsp(BaseCommand): ieee: t.EUI64 -class ExtpanidGetInd(t.Struct): +class ExtpanidGetInd(BaseCommand): pass -class ExtpanidSetReq(t.Struct): +class ExtpanidSetReq(BaseCommand): ieee: t.EUI64 -class ExtpanidSetRsp(t.Struct): +class ExtpanidSetRsp(BaseCommand): status: Status -class ExtpanidSetInd(t.Struct): +class ExtpanidSetInd(BaseCommand): pass -class PanidGetReq(t.Struct): +class PanidGetReq(BaseCommand): pass -class PanidGetRsp(t.Struct): +class PanidGetRsp(BaseCommand): panid: t.PanId -class PanidGetInd(t.Struct): +class PanidGetInd(BaseCommand): pass -class PanidSetReq(t.Struct): +class PanidSetReq(BaseCommand): panid: t.PanId -class PanidSetRsp(t.Struct): +class PanidSetRsp(BaseCommand): status: Status -class PanidSetInd(t.Struct): +class PanidSetInd(BaseCommand): pass -class ShortAddrGetReq(t.Struct): +class ShortAddrGetReq(BaseCommand): pass -class ShortAddrGetRsp(t.Struct): +class ShortAddrGetRsp(BaseCommand): short_addr: t.NWK -class ShortAddrGetInd(t.Struct): +class ShortAddrGetInd(BaseCommand): pass -class ShortAddrSetReq(t.Struct): +class ShortAddrSetReq(BaseCommand): short_addr: t.NWK -class ShortAddrSetRsp(t.Struct): +class ShortAddrSetRsp(BaseCommand): status: Status -class ShortAddrSetInd(t.Struct): +class ShortAddrSetInd(BaseCommand): pass -class LongAddrGetReq(t.Struct): +class LongAddrGetReq(BaseCommand): pass -class LongAddrGetRsp(t.Struct): +class LongAddrGetRsp(BaseCommand): ieee: t.EUI64 -class LongAddrGetInd(t.Struct): +class LongAddrGetInd(BaseCommand): pass -class LongAddrSetReq(t.Struct): +class LongAddrSetReq(BaseCommand): ieee: t.EUI64 -class LongAddrSetRsp(t.Struct): +class LongAddrSetRsp(BaseCommand): status: Status -class LongAddrSetInd(t.Struct): +class LongAddrSetInd(BaseCommand): pass -class CurrentChannelGetReq(t.Struct): +class CurrentChannelGetReq(BaseCommand): pass -class CurrentChannelGetRsp(t.Struct): +class CurrentChannelGetRsp(BaseCommand): channel: t.uint8_t -class CurrentChannelGetInd(t.Struct): +class CurrentChannelGetInd(BaseCommand): pass -class CurrentChannelSetReq(t.Struct): +class CurrentChannelSetReq(BaseCommand): channel: t.uint8_t -class CurrentChannelSetRsp(t.Struct): +class CurrentChannelSetRsp(BaseCommand): status: Status -class CurrentChannelSetInd(t.Struct): +class CurrentChannelSetInd(BaseCommand): pass -class PrimaryChannelMaskGetReq(t.Struct): +class PrimaryChannelMaskGetReq(BaseCommand): pass -class PrimaryChannelMaskGetRsp(t.Struct): +class PrimaryChannelMaskGetRsp(BaseCommand): channel_mask: ShiftedChannels -class PrimaryChannelMaskGetInd(t.Struct): +class PrimaryChannelMaskGetInd(BaseCommand): pass -class PrimaryChannelMaskSetReq(t.Struct): +class PrimaryChannelMaskSetReq(BaseCommand): channel_mask: ShiftedChannels -class PrimaryChannelMaskSetRsp(t.Struct): +class PrimaryChannelMaskSetRsp(BaseCommand): status: Status -class PrimaryChannelMaskSetInd(t.Struct): +class PrimaryChannelMaskSetInd(BaseCommand): pass -class AddEndpointReq(t.Struct): +class AddEndpointReq(BaseCommand): endpoint: t.uint8_t profile_id: t.uint16_t device_id: t.uint16_t @@ -328,39 +332,39 @@ class AddEndpointReq(t.Struct): output_cluster_list: t.List[t.uint16_t] -class AddEndpointRsp(t.Struct): +class AddEndpointRsp(BaseCommand): status: Status -class AddEndpointInd(t.Struct): +class AddEndpointInd(BaseCommand): pass -class NetworkStateReq(t.Struct): +class NetworkStateReq(BaseCommand): pass -class NetworkStateRsp(t.Struct): +class NetworkStateRsp(BaseCommand): network_state: NetworkState -class NetworkStateInd(t.Struct): +class NetworkStateInd(BaseCommand): pass -class StackStatusHandlerReq(t.Struct): +class StackStatusHandlerReq(BaseCommand): pass -class StackStatusHandlerRsp(t.Struct): +class StackStatusHandlerRsp(BaseCommand): network_state: t.uint8_t -class StackStatusHandlerInd(t.Struct): +class StackStatusHandlerInd(BaseCommand): network_state: t.uint8_t -class ApsDataRequestReq(t.Struct): +class ApsDataRequestReq(BaseCommand): dst_addr: t.EUI64 dst_endpoint: t.uint8_t src_endpoint: t.uint8_t @@ -376,19 +380,19 @@ class ApsDataRequestReq(t.Struct): asdu: Bytes -class ApsDataRequestRsp(t.Struct): +class ApsDataRequestRsp(BaseCommand): status: Status -class ApsDataRequestInd(t.Struct): +class ApsDataRequestInd(BaseCommand): pass -class ApsDataIndicationReq(t.Struct): +class ApsDataIndicationReq(BaseCommand): pass -class ApsDataIndicationRsp(t.Struct): +class ApsDataIndicationRsp(BaseCommand): network_state: NetworkState dst_addr_mode: ExtendedAddrMode dst_addr: t.EUI64 @@ -406,7 +410,7 @@ class ApsDataIndicationRsp(t.Struct): asdu: Bytes -class ApsDataIndicationInd(t.Struct): +class ApsDataIndicationInd(BaseCommand): network_state: NetworkState dst_addr_mode: ExtendedAddrMode dst_addr: t.EUI64 @@ -424,11 +428,11 @@ class ApsDataIndicationInd(t.Struct): asdu: Bytes -class ApsDataConfirmReq(t.Struct): +class ApsDataConfirmReq(BaseCommand): pass -class ApsDataConfirmRsp(t.Struct): +class ApsDataConfirmRsp(BaseCommand): network_state: NetworkState dst_addr_mode: ExtendedAddrMode dst_addr: t.EUI64 @@ -441,7 +445,7 @@ class ApsDataConfirmRsp(t.Struct): asdu: Bytes -class ApsDataConfirmInd(t.Struct): +class ApsDataConfirmInd(BaseCommand): network_state: NetworkState dst_addr_mode: ExtendedAddrMode dst_addr: t.EUI64 @@ -453,184 +457,184 @@ class ApsDataConfirmInd(t.Struct): asdu: Bytes -class NetworkKeyGetReq(t.Struct): +class NetworkKeyGetReq(BaseCommand): pass -class NetworkKeyGetRsp(t.Struct): +class NetworkKeyGetRsp(BaseCommand): nwk_key: t.KeyData -class NetworkKeyGetInd(t.Struct): +class NetworkKeyGetInd(BaseCommand): pass -class NetworkKeySetReq(t.Struct): +class NetworkKeySetReq(BaseCommand): nwk_key: t.KeyData -class NetworkKeySetRsp(t.Struct): +class NetworkKeySetRsp(BaseCommand): status: Status -class NetworkKeySetInd(t.Struct): +class NetworkKeySetInd(BaseCommand): pass -class NwkFrameCounterGetReq(t.Struct): +class NwkFrameCounterGetReq(BaseCommand): pass -class NwkFrameCounterGetRsp(t.Struct): +class NwkFrameCounterGetRsp(BaseCommand): nwk_frame_counter: t.uint32_t -class NwkFrameCounterGetInd(t.Struct): +class NwkFrameCounterGetInd(BaseCommand): pass -class NwkFrameCounterSetReq(t.Struct): +class NwkFrameCounterSetReq(BaseCommand): nwk_frame_counter: t.uint32_t -class NwkFrameCounterSetRsp(t.Struct): +class NwkFrameCounterSetRsp(BaseCommand): status: Status -class NwkFrameCounterSetInd(t.Struct): +class NwkFrameCounterSetInd(BaseCommand): pass -class NetworkRoleGetReq(t.Struct): +class NetworkRoleGetReq(BaseCommand): pass -class NetworkRoleGetRsp(t.Struct): +class NetworkRoleGetRsp(BaseCommand): role: DeviceType -class NetworkRoleGetInd(t.Struct): +class NetworkRoleGetInd(BaseCommand): pass -class NetworkRoleSetReq(t.Struct): +class NetworkRoleSetReq(BaseCommand): role: DeviceType -class NetworkRoleSetRsp(t.Struct): +class NetworkRoleSetRsp(BaseCommand): status: Status -class NetworkRoleSetInd(t.Struct): +class NetworkRoleSetInd(BaseCommand): pass -class UsePredefinedNwkPanidSetReq(t.Struct): +class UsePredefinedNwkPanidSetReq(BaseCommand): predefined: t.Bool -class UsePredefinedNwkPanidSetRsp(t.Struct): +class UsePredefinedNwkPanidSetRsp(BaseCommand): status: Status -class UsePredefinedNwkPanidSetInd(t.Struct): +class UsePredefinedNwkPanidSetInd(BaseCommand): pass -class NwkUpdateIdGetReq(t.Struct): +class NwkUpdateIdGetReq(BaseCommand): pass -class NwkUpdateIdGetRsp(t.Struct): +class NwkUpdateIdGetRsp(BaseCommand): nwk_update_id: t.uint8_t -class NwkUpdateIdGetInd(t.Struct): +class NwkUpdateIdGetInd(BaseCommand): pass -class NwkUpdateIdSetReq(t.Struct): +class NwkUpdateIdSetReq(BaseCommand): nwk_update_id: t.uint8_t -class NwkUpdateIdSetRsp(t.Struct): +class NwkUpdateIdSetRsp(BaseCommand): status: Status -class NwkUpdateIdSetInd(t.Struct): +class NwkUpdateIdSetInd(BaseCommand): pass -class TrustCenterAddressGetReq(t.Struct): +class TrustCenterAddressGetReq(BaseCommand): pass -class TrustCenterAddressGetRsp(t.Struct): +class TrustCenterAddressGetRsp(BaseCommand): trust_center_addr: t.EUI64 -class TrustCenterAddressGetInd(t.Struct): +class TrustCenterAddressGetInd(BaseCommand): pass -class TrustCenterAddressSetReq(t.Struct): +class TrustCenterAddressSetReq(BaseCommand): trust_center_addr: t.EUI64 -class TrustCenterAddressSetRsp(t.Struct): +class TrustCenterAddressSetRsp(BaseCommand): status: Status -class TrustCenterAddressSetInd(t.Struct): +class TrustCenterAddressSetInd(BaseCommand): pass -class LinkKeyGetReq(t.Struct): +class LinkKeyGetReq(BaseCommand): pass -class LinkKeyGetRsp(t.Struct): +class LinkKeyGetRsp(BaseCommand): ieee: t.EUI64 key: t.KeyData -class LinkKeyGetInd(t.Struct): +class LinkKeyGetInd(BaseCommand): pass -class LinkKeySetReq(t.Struct): +class LinkKeySetReq(BaseCommand): key: t.KeyData -class LinkKeySetRsp(t.Struct): +class LinkKeySetRsp(BaseCommand): status: Status -class LinkKeySetInd(t.Struct): +class LinkKeySetInd(BaseCommand): pass -class SecurityModeGetReq(t.Struct): +class SecurityModeGetReq(BaseCommand): pass -class SecurityModeGetRsp(t.Struct): +class SecurityModeGetRsp(BaseCommand): security_mode: SecurityMode -class SecurityModeGetInd(t.Struct): +class SecurityModeGetInd(BaseCommand): pass -class SecurityModeSetReq(t.Struct): +class SecurityModeSetReq(BaseCommand): security_mode: SecurityMode -class SecurityModeSetRsp(t.Struct): +class SecurityModeSetRsp(BaseCommand): status: Status -class SecurityModeSetInd(t.Struct): +class SecurityModeSetInd(BaseCommand): pass @@ -826,3 +830,7 @@ class SecurityModeSetInd(t.Struct): SecurityModeSetInd, ), } + +COMMAND_SCHEMA_TO_COMMAND_ID = { + req: command_id for command_id, (req, _, _) in COMMAND_SCHEMAS.items() +} From 66c8a0100eead98b47a398ba3cd1246c8d436fe7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 16 Apr 2024 13:47:41 -0400 Subject: [PATCH 13/22] Remove unnecessary indication frames for commands that don't have them --- zigpy_espzb/commands.py | 198 +++++++--------------------------------- 1 file changed, 33 insertions(+), 165 deletions(-) diff --git a/zigpy_espzb/commands.py b/zigpy_espzb/commands.py index 136ffe2..a6d3c49 100644 --- a/zigpy_espzb/commands.py +++ b/zigpy_espzb/commands.py @@ -107,10 +107,6 @@ class NetworkInitRsp(BaseCommand): status: Status -class NetworkInitInd(BaseCommand): - pass - - class StartReq(BaseCommand): autostart: t.Bool @@ -119,10 +115,6 @@ class StartRsp(BaseCommand): status: Status -class StartInd(BaseCommand): - pass - - class FormNetworkReq(BaseCommand): role: DeviceType install_code_policy: t.Bool @@ -185,10 +177,6 @@ class ExtpanidGetRsp(BaseCommand): ieee: t.EUI64 -class ExtpanidGetInd(BaseCommand): - pass - - class ExtpanidSetReq(BaseCommand): ieee: t.EUI64 @@ -197,10 +185,6 @@ class ExtpanidSetRsp(BaseCommand): status: Status -class ExtpanidSetInd(BaseCommand): - pass - - class PanidGetReq(BaseCommand): pass @@ -209,10 +193,6 @@ class PanidGetRsp(BaseCommand): panid: t.PanId -class PanidGetInd(BaseCommand): - pass - - class PanidSetReq(BaseCommand): panid: t.PanId @@ -221,10 +201,6 @@ class PanidSetRsp(BaseCommand): status: Status -class PanidSetInd(BaseCommand): - pass - - class ShortAddrGetReq(BaseCommand): pass @@ -233,10 +209,6 @@ class ShortAddrGetRsp(BaseCommand): short_addr: t.NWK -class ShortAddrGetInd(BaseCommand): - pass - - class ShortAddrSetReq(BaseCommand): short_addr: t.NWK @@ -245,10 +217,6 @@ class ShortAddrSetRsp(BaseCommand): status: Status -class ShortAddrSetInd(BaseCommand): - pass - - class LongAddrGetReq(BaseCommand): pass @@ -257,10 +225,6 @@ class LongAddrGetRsp(BaseCommand): ieee: t.EUI64 -class LongAddrGetInd(BaseCommand): - pass - - class LongAddrSetReq(BaseCommand): ieee: t.EUI64 @@ -269,10 +233,6 @@ class LongAddrSetRsp(BaseCommand): status: Status -class LongAddrSetInd(BaseCommand): - pass - - class CurrentChannelGetReq(BaseCommand): pass @@ -281,10 +241,6 @@ class CurrentChannelGetRsp(BaseCommand): channel: t.uint8_t -class CurrentChannelGetInd(BaseCommand): - pass - - class CurrentChannelSetReq(BaseCommand): channel: t.uint8_t @@ -293,10 +249,6 @@ class CurrentChannelSetRsp(BaseCommand): status: Status -class CurrentChannelSetInd(BaseCommand): - pass - - class PrimaryChannelMaskGetReq(BaseCommand): pass @@ -305,10 +257,6 @@ class PrimaryChannelMaskGetRsp(BaseCommand): channel_mask: ShiftedChannels -class PrimaryChannelMaskGetInd(BaseCommand): - pass - - class PrimaryChannelMaskSetReq(BaseCommand): channel_mask: ShiftedChannels @@ -317,10 +265,6 @@ class PrimaryChannelMaskSetRsp(BaseCommand): status: Status -class PrimaryChannelMaskSetInd(BaseCommand): - pass - - class AddEndpointReq(BaseCommand): endpoint: t.uint8_t profile_id: t.uint16_t @@ -336,10 +280,6 @@ class AddEndpointRsp(BaseCommand): status: Status -class AddEndpointInd(BaseCommand): - pass - - class NetworkStateReq(BaseCommand): pass @@ -348,10 +288,6 @@ class NetworkStateRsp(BaseCommand): network_state: NetworkState -class NetworkStateInd(BaseCommand): - pass - - class StackStatusHandlerReq(BaseCommand): pass @@ -384,14 +320,6 @@ class ApsDataRequestRsp(BaseCommand): status: Status -class ApsDataRequestInd(BaseCommand): - pass - - -class ApsDataIndicationReq(BaseCommand): - pass - - class ApsDataIndicationRsp(BaseCommand): network_state: NetworkState dst_addr_mode: ExtendedAddrMode @@ -465,10 +393,6 @@ class NetworkKeyGetRsp(BaseCommand): nwk_key: t.KeyData -class NetworkKeyGetInd(BaseCommand): - pass - - class NetworkKeySetReq(BaseCommand): nwk_key: t.KeyData @@ -477,10 +401,6 @@ class NetworkKeySetRsp(BaseCommand): status: Status -class NetworkKeySetInd(BaseCommand): - pass - - class NwkFrameCounterGetReq(BaseCommand): pass @@ -489,10 +409,6 @@ class NwkFrameCounterGetRsp(BaseCommand): nwk_frame_counter: t.uint32_t -class NwkFrameCounterGetInd(BaseCommand): - pass - - class NwkFrameCounterSetReq(BaseCommand): nwk_frame_counter: t.uint32_t @@ -501,10 +417,6 @@ class NwkFrameCounterSetRsp(BaseCommand): status: Status -class NwkFrameCounterSetInd(BaseCommand): - pass - - class NetworkRoleGetReq(BaseCommand): pass @@ -513,10 +425,6 @@ class NetworkRoleGetRsp(BaseCommand): role: DeviceType -class NetworkRoleGetInd(BaseCommand): - pass - - class NetworkRoleSetReq(BaseCommand): role: DeviceType @@ -525,10 +433,6 @@ class NetworkRoleSetRsp(BaseCommand): status: Status -class NetworkRoleSetInd(BaseCommand): - pass - - class UsePredefinedNwkPanidSetReq(BaseCommand): predefined: t.Bool @@ -537,10 +441,6 @@ class UsePredefinedNwkPanidSetRsp(BaseCommand): status: Status -class UsePredefinedNwkPanidSetInd(BaseCommand): - pass - - class NwkUpdateIdGetReq(BaseCommand): pass @@ -549,10 +449,6 @@ class NwkUpdateIdGetRsp(BaseCommand): nwk_update_id: t.uint8_t -class NwkUpdateIdGetInd(BaseCommand): - pass - - class NwkUpdateIdSetReq(BaseCommand): nwk_update_id: t.uint8_t @@ -561,10 +457,6 @@ class NwkUpdateIdSetRsp(BaseCommand): status: Status -class NwkUpdateIdSetInd(BaseCommand): - pass - - class TrustCenterAddressGetReq(BaseCommand): pass @@ -573,10 +465,6 @@ class TrustCenterAddressGetRsp(BaseCommand): trust_center_addr: t.EUI64 -class TrustCenterAddressGetInd(BaseCommand): - pass - - class TrustCenterAddressSetReq(BaseCommand): trust_center_addr: t.EUI64 @@ -585,10 +473,6 @@ class TrustCenterAddressSetRsp(BaseCommand): status: Status -class TrustCenterAddressSetInd(BaseCommand): - pass - - class LinkKeyGetReq(BaseCommand): pass @@ -598,10 +482,6 @@ class LinkKeyGetRsp(BaseCommand): key: t.KeyData -class LinkKeyGetInd(BaseCommand): - pass - - class LinkKeySetReq(BaseCommand): key: t.KeyData @@ -610,10 +490,6 @@ class LinkKeySetRsp(BaseCommand): status: Status -class LinkKeySetInd(BaseCommand): - pass - - class SecurityModeGetReq(BaseCommand): pass @@ -622,10 +498,6 @@ class SecurityModeGetRsp(BaseCommand): security_mode: SecurityMode -class SecurityModeGetInd(BaseCommand): - pass - - class SecurityModeSetReq(BaseCommand): security_mode: SecurityMode @@ -634,20 +506,16 @@ class SecurityModeSetRsp(BaseCommand): status: Status -class SecurityModeSetInd(BaseCommand): - pass - - COMMAND_SCHEMAS = { CommandId.network_init: ( NetworkInitReq, NetworkInitRsp, - NetworkInitInd, + None, ), CommandId.start: ( StartReq, StartRsp, - StartInd, + None, ), CommandId.form_network: ( FormNetworkReq, @@ -667,72 +535,72 @@ class SecurityModeSetInd(BaseCommand): CommandId.extpanid_get: ( ExtpanidGetReq, ExtpanidGetRsp, - ExtpanidGetInd, + None, ), CommandId.extpanid_set: ( ExtpanidSetReq, ExtpanidSetRsp, - ExtpanidSetInd, + None, ), CommandId.panid_get: ( PanidGetReq, PanidGetRsp, - PanidGetInd, + None, ), CommandId.panid_set: ( PanidSetReq, PanidSetRsp, - PanidSetInd, + None, ), CommandId.short_addr_get: ( ShortAddrGetReq, ShortAddrGetRsp, - ShortAddrGetInd, + None, ), CommandId.short_addr_set: ( ShortAddrSetReq, ShortAddrSetRsp, - ShortAddrSetInd, + None, ), CommandId.long_addr_get: ( LongAddrGetReq, LongAddrGetRsp, - LongAddrGetInd, + None, ), CommandId.long_addr_set: ( LongAddrSetReq, LongAddrSetRsp, - LongAddrSetInd, + None, ), CommandId.current_channel_get: ( CurrentChannelGetReq, CurrentChannelGetRsp, - CurrentChannelGetInd, + None, ), CommandId.current_channel_set: ( CurrentChannelSetReq, CurrentChannelSetRsp, - CurrentChannelSetInd, + None, ), CommandId.primary_channel_mask_get: ( PrimaryChannelMaskGetReq, PrimaryChannelMaskGetRsp, - PrimaryChannelMaskGetInd, + None, ), CommandId.primary_channel_mask_set: ( PrimaryChannelMaskSetReq, PrimaryChannelMaskSetRsp, - PrimaryChannelMaskSetInd, + None, ), CommandId.add_endpoint: ( AddEndpointReq, AddEndpointRsp, - AddEndpointInd, + None, ), CommandId.network_state: ( NetworkStateReq, NetworkStateRsp, - NetworkStateInd, + None, ), CommandId.stack_status_handler: ( StackStatusHandlerReq, @@ -742,10 +610,10 @@ class SecurityModeSetInd(BaseCommand): CommandId.aps_data_request: ( ApsDataRequestReq, ApsDataRequestRsp, - ApsDataRequestInd, + None, ), CommandId.aps_data_indication: ( - ApsDataIndicationReq, + None, ApsDataIndicationRsp, ApsDataIndicationInd, ), @@ -757,77 +625,77 @@ class SecurityModeSetInd(BaseCommand): CommandId.network_key_get: ( NetworkKeyGetReq, NetworkKeyGetRsp, - NetworkKeyGetInd, + None, ), CommandId.network_key_set: ( NetworkKeySetReq, NetworkKeySetRsp, - NetworkKeySetInd, + None, ), CommandId.nwk_frame_counter_get: ( NwkFrameCounterGetReq, NwkFrameCounterGetRsp, - NwkFrameCounterGetInd, + None, ), CommandId.nwk_frame_counter_set: ( NwkFrameCounterSetReq, NwkFrameCounterSetRsp, - NwkFrameCounterSetInd, + None, ), CommandId.network_role_get: ( NetworkRoleGetReq, NetworkRoleGetRsp, - NetworkRoleGetInd, + None, ), CommandId.network_role_set: ( NetworkRoleSetReq, NetworkRoleSetRsp, - NetworkRoleSetInd, + None, ), CommandId.use_predefined_nwk_panid_set: ( UsePredefinedNwkPanidSetReq, UsePredefinedNwkPanidSetRsp, - UsePredefinedNwkPanidSetInd, + None, ), CommandId.nwk_update_id_get: ( NwkUpdateIdGetReq, NwkUpdateIdGetRsp, - NwkUpdateIdGetInd, + None, ), CommandId.nwk_update_id_set: ( NwkUpdateIdSetReq, NwkUpdateIdSetRsp, - NwkUpdateIdSetInd, + None, ), CommandId.trust_center_address_get: ( TrustCenterAddressGetReq, TrustCenterAddressGetRsp, - TrustCenterAddressGetInd, + None, ), CommandId.trust_center_address_set: ( TrustCenterAddressSetReq, TrustCenterAddressSetRsp, - TrustCenterAddressSetInd, + None, ), CommandId.link_key_get: ( LinkKeyGetReq, LinkKeyGetRsp, - LinkKeyGetInd, + None, ), CommandId.link_key_set: ( LinkKeySetReq, LinkKeySetRsp, - LinkKeySetInd, + None, ), CommandId.security_mode_get: ( SecurityModeGetReq, SecurityModeGetRsp, - SecurityModeGetInd, + None, ), CommandId.security_mode_set: ( SecurityModeSetReq, SecurityModeSetRsp, - SecurityModeSetInd, + None, ), } From b6e9dd5057015cdc4659a82b5a7174699c191041 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:06:47 -0400 Subject: [PATCH 14/22] Fix address modes for transmit --- zigpy_espzb/api.py | 22 ++--- zigpy_espzb/commands.py | 9 +- zigpy_espzb/types.py | 139 +++++++++++++++--------------- zigpy_espzb/zigbee/application.py | 112 +++++++++++++----------- 4 files changed, 145 insertions(+), 137 deletions(-) diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index 0bbc02a..e8932b4 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -33,8 +33,8 @@ SecurityMode, ShiftedChannels, Status, + TransmitOptions, TXStatus, - ZnspTransmitOptions, addr_mode_with_eui64_to_addr_mode_address, ) import zigpy_espzb.uart @@ -198,12 +198,8 @@ def data_received(self, data: bytes) -> None: command.seq, ) - status = None - - if hasattr(params, "status"): - status = params.status - exc = None + status = getattr(params, "status", None) if status is not None and status != Status.SUCCESS: exc = CommandError(status, f"{command.command_id}, status: {status}") @@ -216,7 +212,7 @@ def data_received(self, data: bytes) -> None: fut.set_exception(exc) except asyncio.InvalidStateError: LOGGER.warning( - "Duplicate or delayed response for 0x:%02x sequence", + "Duplicate or delayed response for 0x%02x sequence", command.seq, ) @@ -294,7 +290,7 @@ async def set_channel_mask(self, channels: t.Channels) -> None: ) async def set_channel(self, channel: int) -> None: - await self.send_command(commands.CurrentChannelSetReq(channel=channel)) + await self.set_channel_mask(channels=t.Channels.from_channel_list([channel])) async def form_network( self, @@ -473,11 +469,9 @@ async def aps_data_request( addr_mode: t.AddrMode, cluster: t.uint16_t, sequence: t.uint16_t, - options: ZnspTransmitOptions, + options: TransmitOptions, radius: t.uint16_t, data: bytes, - relays: list[int] | None = None, - extended_timeout: bool = False, ): for delay in REQUEST_RETRY_DELAYS: try: @@ -491,11 +485,11 @@ async def aps_data_request( cluster_id=cluster, tx_options=options, use_alias=False, - src_addr=src_addr, - sequence=sequence, + alias_src_addr=src_addr, + alias_seq_num=sequence, radius=radius, asdu_length=len(data), - asdu=t.List(data), + asdu=data, ) ) except CommandError as ex: diff --git a/zigpy_espzb/commands.py b/zigpy_espzb/commands.py index a6d3c49..3651ef4 100644 --- a/zigpy_espzb/commands.py +++ b/zigpy_espzb/commands.py @@ -10,6 +10,7 @@ SecurityMode, ShiftedChannels, Status, + TransmitOptions, TXStatus, ) @@ -304,13 +305,13 @@ class ApsDataRequestReq(BaseCommand): dst_addr: t.EUI64 dst_endpoint: t.uint8_t src_endpoint: t.uint8_t - address_mode: t.uint8_t + address_mode: ExtendedAddrMode profile_id: t.uint16_t cluster_id: t.uint16_t - tx_options: t.uint8_t + tx_options: TransmitOptions use_alias: t.Bool - src_addr: t.EUI64 - sequence: t.uint8_t + alias_src_addr: t.EUI64 + alias_seq_num: t.uint8_t radius: t.uint8_t asdu_length: t.uint32_t asdu: Bytes diff --git a/zigpy_espzb/types.py b/zigpy_espzb/types.py index d2f02ca..9294211 100644 --- a/zigpy_espzb/types.py +++ b/zigpy_espzb/types.py @@ -5,40 +5,6 @@ import zigpy.types as t -def serialize_dict(data, schema): - chunks = [] - - for key in schema: - value = data[key] - if value is None: - break - - if not isinstance(value, schema[key]): - value = schema[key](value) - - chunks.append(value.serialize()) - - return b"".join(chunks) - - -def deserialize_dict(data, schema): - result = {} - for name, type_ in schema.items(): - try: - result[name], data = type_.deserialize(data) - except ValueError: - if data: - raise - - result[name] = None - return result, data - - -def list_replace(lst: list, old: object, new: object) -> list: - """Replace all occurrences of `old` with `new` in `lst`.""" - return [new if x == old else x for x in lst] - - class Bytes(bytes): def serialize(self): return self @@ -48,18 +14,52 @@ def deserialize(cls, data): return cls(data), b"" -class ZnspTransmitOptions(t.bitmap8): +class TransmitOptions(t.bitmap8): NONE = 0x00 - ACK_ENABLED = 0x01 - SECURITY_ENABLED = 0x02 + + # Security enabled transmission + SECURITY_ENABLED = 0x01 + # Use NWK key (obsolete) + USE_NWK_KEY_R21OBSOLETE = 0x02 + # Extension: do not include long src/dst addresses into NWK hdr + NO_LONG_ADDR = 0x02 + # Acknowledged transmission + ACK_TX = 0x04 + # Fragmentation permitted + FRAG_PERMITTED = 0x08 + # Include extended nonce in APS security frame + INC_EXT_NONCE = 0x10 class ExtendedAddrMode(t.enum8): - Unknown = 0x00 - IEEE = 0x01 - NWK = 0x02 - Group = 0x03 - Broadcast = 0x0F + # DstAddress and DstEndpoint not present + MODE_DST_ADDR_ENDP_NOT_PRESENT = 0x00 + # 16-bit group address for DstAddress; DstEndpoint not present + MODE_16_GROUP_ENDP_NOT_PRESENT = 0x01 + # 16-bit address for DstAddress and DstEndpoint present + MODE_16_ENDP_PRESENT = 0x02 + # 64-bit extended address for DstAddress and DstEndpoint present + MODE_64_ENDP_PRESENT = 0x03 + + @classmethod + def from_zigpy_addr_mode(cls, addr_mode: t.AddrMode) -> ExtendedAddrMode: + """Convert a Zigpy AddrMode to an ExtendedAddrMode.""" + return { + t.AddrMode.IEEE: cls.MODE_64_ENDP_PRESENT, + t.AddrMode.NWK: cls.MODE_16_ENDP_PRESENT, + t.AddrMode.Group: cls.MODE_16_GROUP_ENDP_NOT_PRESENT, + t.AddrMode.Broadcast: cls.MODE_16_GROUP_ENDP_NOT_PRESENT, + }[addr_mode] + + def to_zigpy_addr_mode(self) -> t.AddrMode: + """Convert a Zigpy AddrMode to an ExtendedAddrMode.""" + return { + self.MODE_64_ENDP_PRESENT: t.AddrMode.IEEE, + self.MODE_16_ENDP_PRESENT: t.AddrMode.NWK, + self.MODE_DST_ADDR_ENDP_NOT_PRESENT: t.AddrMode.NWK, + self.MODE_16_GROUP_ENDP_NOT_PRESENT: t.AddrMode.Group, + self.MODE_16_GROUP_ENDP_NOT_PRESENT: t.AddrMode.Broadcast, + }[self] def addr_mode_with_eui64_to_addr_mode_address( @@ -67,46 +67,45 @@ def addr_mode_with_eui64_to_addr_mode_address( ) -> t.AddrModeAddress: """Convert an address mode and an EUI64 address to an AddrModeAddress.""" address_short, _ = t.uint16_t.deserialize(address.serialize()[:2]) + zigpy_addr_mode = addr_mode.to_zigpy_addr_mode() - if addr_mode == ExtendedAddrMode.IEEE: + if zigpy_addr_mode == t.AddrMode.IEEE: address = address - elif addr_mode == ExtendedAddrMode.NWK: + elif zigpy_addr_mode == t.AddrMode.NWK: address = t.NWK(address_short) - elif addr_mode == ExtendedAddrMode.Group: + elif zigpy_addr_mode == t.AddrMode.Group: address = t.Group(address_short) - elif addr_mode == ExtendedAddrMode.Broadcast: + elif zigpy_addr_mode == t.AddrMode.Broadcast: address = t.BroadcastAddress(address_short) - elif addr_mode == ExtendedAddrMode.Unknown: - # TODO: Is this correct? It seems to be used only for loopback - address = address_short - addr_mode = t.AddrMode.NWK else: - raise ValueError(f"Unknown address mode: {addr_mode}") + raise ValueError(f"Unknown address mode: {zigpy_addr_mode}") - return t.AddrModeAddress(addr_mode=t.AddrMode(addr_mode), address=address) + return t.AddrModeAddress(addr_mode=zigpy_addr_mode, address=address) class ShiftedChannels(t.bitmap32): """Zigbee Channels.""" - CHANNEL_11 = 0b00000000000000000000010000000000 - CHANNEL_12 = 0b00000000000000000000100000000000 - CHANNEL_13 = 0b00000000000000000001000000000000 - CHANNEL_14 = 0b00000000000000000010000000000000 - CHANNEL_15 = 0b00000000000000000100000000000000 - CHANNEL_16 = 0b00000000000000001000000000000000 - CHANNEL_17 = 0b00000000000000010000000000000000 - CHANNEL_18 = 0b00000000000000100000000000000000 - CHANNEL_19 = 0b00000000000001000000000000000000 - CHANNEL_20 = 0b00000000000010000000000000000000 - CHANNEL_21 = 0b00000000000100000000000000000000 - CHANNEL_22 = 0b00000000001000000000000000000000 - CHANNEL_23 = 0b00000000010000000000000000000000 - CHANNEL_24 = 0b00000000100000000000000000000000 - CHANNEL_25 = 0b00000001000000000000000000000000 - CHANNEL_26 = 0b00000010000000000000000000000000 - ALL_CHANNELS = 0b00000011111111111111110000000000 - NO_CHANNELS = 0b00000000000000000000000000000000 + # fmt: off + CHANNEL_11 = 0b00000000000000000000100000000000 + CHANNEL_12 = 0b00000000000000000001000000000000 + CHANNEL_13 = 0b00000000000000000010000000000000 + CHANNEL_14 = 0b00000000000000000100000000000000 + CHANNEL_15 = 0b00000000000000001000000000000000 + CHANNEL_16 = 0b00000000000000010000000000000000 + CHANNEL_17 = 0b00000000000000100000000000000000 + CHANNEL_18 = 0b00000000000001000000000000000000 + CHANNEL_19 = 0b00000000000010000000000000000000 + CHANNEL_20 = 0b00000000000100000000000000000000 + CHANNEL_21 = 0b00000000001000000000000000000000 + CHANNEL_22 = 0b00000000010000000000000000000000 + CHANNEL_23 = 0b00000000100000000000000000000000 + CHANNEL_24 = 0b00000001000000000000000000000000 + CHANNEL_25 = 0b00000010000000000000000000000000 + CHANNEL_26 = 0b00000100000000000000000000000000 + ALL_CHANNELS = 0b00000111111111111111100000000000 + NO_CHANNELS = 0b00000000000000000000000000000000 + # fmt: on __iter__ = t.Channels.__iter__ from_channel_list = classmethod(t.Channels.from_channel_list.__func__) diff --git a/zigpy_espzb/zigbee/application.py b/zigpy_espzb/zigbee/application.py index 2db6a00..e46d148 100644 --- a/zigpy_espzb/zigbee/application.py +++ b/zigpy_espzb/zigbee/application.py @@ -20,7 +20,6 @@ import zigpy.exceptions from zigpy.exceptions import FormationFailure, NetworkNotFormed import zigpy.state -import zigpy.types import zigpy.types as t import zigpy.util import zigpy.zdo.types as zdo_t @@ -28,9 +27,10 @@ from zigpy_espzb.api import Znsp from zigpy_espzb.types import ( DeviceType, + ExtendedAddrMode, NetworkState, SecurityMode, - ZnspTransmitOptions, + TransmitOptions, ) LOGGER = logging.getLogger(__name__) @@ -108,8 +108,10 @@ async def start_network(self): # TODO: add our registered endpoints manually so things don't crash. These # should be discovered automatically. - coordinator.add_endpoint(1) - coordinator.add_endpoint(2) + ep1 = coordinator.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep2 = coordinator.add_endpoint(2) + ep2.status = zigpy.endpoint.Status.ZDO_INIT async def _change_network_state( self, @@ -147,7 +149,7 @@ async def write_network_info(self, *, network_info, node_info): await self._api.reset() await self._api.network_init() await self._api.form_network(role=DeviceType.COORDINATOR) - await self._api.start(autostart=False) + # await self._api.start(autostart=False) role = { zdo_t.LogicalType.Coordinator: DeviceType.COORDINATOR, @@ -157,7 +159,7 @@ async def write_network_info(self, *, network_info, node_info): await self._api.set_network_role(role) await self._api.set_nwk_address(node_info.nwk) - if node_info.ieee != zigpy.types.EUI64.UNKNOWN: + if node_info.ieee != t.EUI64.UNKNOWN: await self._api.set_mac_address(node_info.ieee) node_ieee = node_info.ieee else: @@ -178,7 +180,7 @@ async def write_network_info(self, *, network_info, node_info): tc_link_key_partner_ieee = network_info.tc_link_key.partner_ieee - if tc_link_key_partner_ieee == zigpy.types.EUI64.UNKNOWN: + if tc_link_key_partner_ieee == t.EUI64.UNKNOWN: tc_link_key_partner_ieee = node_ieee await self._api.set_trust_center_address(tc_link_key_partner_ieee) @@ -275,50 +277,62 @@ async def add_endpoint(self, descriptor: zdo_t.SimpleDescriptor) -> None: async def send_packet(self, packet): LOGGER.debug("Sending packet: %r", packet) - force_relays = None - - dst_addr = packet.dst.address - addr_mode = packet.dst.addr_mode - if packet.dst.addr_mode != zigpy.types.AddrMode.IEEE: - dst_addr = t.EUI64( - [ - packet.dst.address % 0x100, - packet.dst.address >> 8, - 0, - 0, - 0, - 0, - 0, - 0, - ] - ) - if packet.dst.addr_mode == zigpy.types.AddrMode.Broadcast: - addr_mode = zigpy.types.AddrMode.Group - - if packet.dst.addr_mode != zigpy.types.AddrMode.IEEE: - src_addr = t.EUI64( - [ - packet.dst.address % 0x100, - packet.dst.address >> 8, - 0, - 0, - 0, - 0, - 0, - 0, - ] + try: + device = self.get_device_with_address(packet.dst) + except (KeyError, ValueError): + device = None + + if packet.dst.addr_mode == t.AddrMode.IEEE: + LOGGER.warning("IEEE addressing is not supported, falling back to NWK") + + if device is None: + raise ValueError(f"Cannot find device with IEEE {packet.dst.address}") + + packet = packet.replace( + dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=device.nwk) ) - if packet.source_route is not None: - force_relays = packet.source_route + assert packet.src.addr_mode == t.AddrMode.NWK + src_addr = t.EUI64( + [ + packet.src.address % 0x100, + packet.src.address >> 8, + 0, + 0, + 0, + 0, + 0, + 0, + ] + ) + + dst_addr_mode = { + t.AddrMode.NWK: ExtendedAddrMode.MODE_16_ENDP_PRESENT, + t.AddrMode.IEEE: ExtendedAddrMode.MODE_64_ENDP_PRESENT, + t.AddrMode.Group: ExtendedAddrMode.MODE_16_GROUP_ENDP_NOT_PRESENT, + t.AddrMode.Broadcast: ExtendedAddrMode.MODE_16_GROUP_ENDP_NOT_PRESENT, + }[packet.dst.addr_mode] + + dst_addr = t.EUI64( + [ + packet.dst.address % 0x100, + packet.dst.address >> 8, + 0, + 0, + 0, + 0, + 0, + 0, + ] + ) - tx_options = ZnspTransmitOptions.NONE + tx_options = TransmitOptions.NONE - if zigpy.types.TransmitOptions.ACK in packet.tx_options: - tx_options |= ZnspTransmitOptions.ACK_ENABLED + if t.TransmitOptions.ACK in packet.tx_options: + tx_options |= TransmitOptions.ACK_TX - if zigpy.types.TransmitOptions.APS_Encryption in packet.tx_options: - tx_options |= ZnspTransmitOptions.SECURITY_ENABLED + if t.TransmitOptions.APS_Encryption in packet.tx_options: + tx_options |= TransmitOptions.SECURITY_ENABLED async with self._limit_concurrency(): await self._api.aps_data_request( @@ -327,19 +341,19 @@ async def send_packet(self, packet): src_addr=src_addr, src_ep=packet.src_ep, profile=packet.profile_id or 0, - addr_mode=addr_mode, + addr_mode=dst_addr_mode, cluster=packet.cluster_id, sequence=packet.tsn, options=tx_options, radius=packet.radius or 0, data=packet.data.serialize(), - relays=force_relays, - extended_timeout=packet.extended_timeout, ) async def permit_ncp(self, time_s=60): assert 0 <= time_s <= 254 + await self._device.zdo.permit(time_s) + # TODO: this does not work, the NCP responds again with: # Unknown command received: Command( # version=0, From 203f6da79a2f4addaeb7afacb5757c53e1915928 Mon Sep 17 00:00:00 2001 From: liuhan Date: Thu, 25 Apr 2024 19:25:37 +0800 Subject: [PATCH 15/22] Add system command --- zigpy_espzb/api.py | 37 ++++++++++++++++++- zigpy_espzb/commands.py | 60 +++++++++++++++++++++++++++++++ zigpy_espzb/zigbee/application.py | 6 ++-- 3 files changed, 99 insertions(+), 4 deletions(-) diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index e8932b4..455841b 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -83,6 +83,7 @@ async def connect(self) -> None: self._uart = await zigpy_espzb.uart.connect(self._config, self) # TODO: implement a firmware version command + self._firmware_version = await self.system_firmware() self._network_state = await self.get_network_state() def connection_lost(self, exc: Exception) -> None: @@ -166,6 +167,9 @@ def data_received(self, data: bytes) -> None: # We won't implement requests for now assert command.frame_type != FrameType.Request + if schema is None: + return + fut = None if command.frame_type == FrameType.Response: @@ -515,7 +519,7 @@ async def reset(self) -> None: for attempt in range(5): try: - await self.form_network() + await self.send_command(commands.SystemResetReq()) except asyncio.TimeoutError: break else: @@ -524,3 +528,34 @@ async def reset(self) -> None: await asyncio.sleep(2) LOGGER.debug("Reset complete") + + async def system_factory(self): + + LOGGER.debug("Factory...") + + for attempt in range(5): + try: + await self.send_command(commands.SystemFactoryReq()) + except asyncio.TimeoutError: + break + else: + raise RuntimeError("Failed to trigger a factory/crash") + + await asyncio.sleep(2) + + LOGGER.debug("Factory complete") + + async def system_firmware(self): + rsp = await self.send_command(commands.SystemFirmwareReq()) + + return rsp.firmware_version + + async def system_model(self): + rsp = await self.send_command(commands.SystemModelReq()) + + return rsp.payload + + async def system_manufacturer(self): + rsp = await self.send_command(commands.SystemManufacturerReq()) + + return rsp.payload diff --git a/zigpy_espzb/commands.py b/zigpy_espzb/commands.py index 3651ef4..de7c729 100644 --- a/zigpy_espzb/commands.py +++ b/zigpy_espzb/commands.py @@ -6,6 +6,7 @@ Bytes, DeviceType, ExtendedAddrMode, + FirmwareVersion, NetworkState, SecurityMode, ShiftedChannels, @@ -77,6 +78,11 @@ class CommandId(t.enum16): aps_data_request = 0x0300 aps_data_indication = 0x0301 aps_data_confirm = 0x0302 + system_reset = 0x0400 + system_factory = 0x0401 + system_firmware = 0x0402 + system_model = 0x0403 + system_manufacturer = 0x0404 class FrameType(t.enum4): @@ -506,6 +512,35 @@ class SecurityModeSetReq(BaseCommand): class SecurityModeSetRsp(BaseCommand): status: Status +class SystemResetReq(BaseCommand): + pass + +class SystemResetRsp(BaseCommand): + status: Status + +class SystemFactoryReq(BaseCommand): + pass + +class SystemFactoryRsp(BaseCommand): + status: Status + +class SystemFirmwareReq(BaseCommand): + pass + +class SystemFirmwareRsp(BaseCommand): + firmware_version: FirmwareVersion + +class SystemModelReq(BaseCommand): + pass + +class SystemModelRsp(BaseCommand): + payload: t.CharacterString + +class SystemManufacturerReq(BaseCommand): + pass + +class SystemManufacturerRsp(BaseCommand): + payload: t.CharacterString COMMAND_SCHEMAS = { CommandId.network_init: ( @@ -698,6 +733,31 @@ class SecurityModeSetRsp(BaseCommand): SecurityModeSetRsp, None, ), + CommandId.system_reset: ( + SystemResetReq, + SystemResetRsp, + None, + ), + CommandId.system_factory: ( + SystemFactoryReq, + SystemFactoryRsp, + None, + ), + CommandId.system_firmware: ( + SystemFirmwareReq, + SystemFirmwareRsp, + None, + ), + CommandId.system_model: ( + SystemModelReq, + SystemModelRsp, + None, + ), + CommandId.system_manufacturer: ( + SystemManufacturerReq, + SystemManufacturerRsp, + None, + ), } COMMAND_SCHEMA_TO_COMMAND_ID = { diff --git a/zigpy_espzb/zigbee/application.py b/zigpy_espzb/zigbee/application.py index e46d148..9f5ca54 100644 --- a/zigpy_espzb/zigbee/application.py +++ b/zigpy_espzb/zigbee/application.py @@ -146,7 +146,7 @@ async def reset_network_info(self): await self._api.leave_network() async def write_network_info(self, *, network_info, node_info): - await self._api.reset() + await self._api.system_factory() await self._api.network_init() await self._api.form_network(role=DeviceType.COORDINATOR) # await self._api.start(autostart=False) @@ -216,8 +216,8 @@ async def load_network_info(self, *, load_devices=False): node_info.ieee = await self._api.get_mac_address() # TODO: implement firmware commands to read the board name, manufacturer - node_info.manufacturer = "Espressif Systems" - node_info.model = "ESP32H2" + node_info.manufacturer = await self._api.system_manufacturer() + node_info.model = await self._api.system_model() # TODO: implement firmware command to read out the firmware version and build ID node_info.version = f"{int(self._api.firmware_version):#010x}" From a6b6bcbd8e0da3893ebf46a9863900a119f7f036 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:46:41 -0400 Subject: [PATCH 16/22] Poll the stack after reset to make sure it is functional --- zigpy_espzb/api.py | 51 +++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index 455841b..33c033a 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -41,6 +41,7 @@ LOGGER = logging.getLogger(__name__) +POLL_UNTIL_RUNNING_TIMEOUT = 10 COMMAND_TIMEOUT = 1.8 PROBE_TIMEOUT = 2 REQUEST_RETRY_DELAYS = (0.5, 1.0, 1.5, None) @@ -108,7 +109,7 @@ def close(self): self._uart.close() self._uart = None - async def send_command(self, command: t.Struct): + async def send_command(self, command: t.Struct, *, wait_for_response: bool = True): command_id = COMMAND_SCHEMA_TO_COMMAND_ID[type(command)] serialized_payload = command.serialize() @@ -134,6 +135,10 @@ async def send_command(self, command: t.Struct): self._seq = (self._seq % 255) + 1 + if not wait_for_response: + LOGGER.debug("Not waiting for a response") + return + fut = asyncio.Future() self._awaiting[seq][command_id].append(fut) @@ -511,39 +516,25 @@ async def get_network_state(self) -> NetworkState: return rsp.network_state - async def reset(self) -> None: - # TODO: There is no reset command but we can trigger a crash if we form the - # network twice + async def _poll_until_running(self): + async with asyncio_timeout(POLL_UNTIL_RUNNING_TIMEOUT): + while True: + await asyncio.sleep(0.5) - LOGGER.debug("Resetting via crash...") - - for attempt in range(5): - try: - await self.send_command(commands.SystemResetReq()) - except asyncio.TimeoutError: - break - else: - raise RuntimeError("Failed to trigger a reset/crash") - - await asyncio.sleep(2) + try: + LOGGER.debug("Polling firmware to see if it is running") + await self.system_firmware() + break + except asyncio.TimeoutError: + pass - LOGGER.debug("Reset complete") + async def reset(self) -> None: + await self.send_command(commands.SystemResetReq(), wait_for_response=False) + await self._poll_until_running() async def system_factory(self): - - LOGGER.debug("Factory...") - - for attempt in range(5): - try: - await self.send_command(commands.SystemFactoryReq()) - except asyncio.TimeoutError: - break - else: - raise RuntimeError("Failed to trigger a factory/crash") - - await asyncio.sleep(2) - - LOGGER.debug("Factory complete") + await self.send_command(commands.SystemFactoryReq(), wait_for_response=False) + await self._poll_until_running() async def system_firmware(self): rsp = await self.send_command(commands.SystemFirmwareReq()) From 6d9348d4de98fd2b2b16914469990f48d2682630 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:54:13 -0400 Subject: [PATCH 17/22] Clean up factory reset --- zigpy_espzb/api.py | 5 +---- zigpy_espzb/zigbee/application.py | 9 +++++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/zigpy_espzb/api.py b/zigpy_espzb/api.py index 33c033a..c629ae0 100644 --- a/zigpy_espzb/api.py +++ b/zigpy_espzb/api.py @@ -324,9 +324,6 @@ async def form_network( # TODO: wait for the `form_network` indication as well? await asyncio.sleep(2) - async def leave_network(self) -> None: - await self.send_command(commands.LeaveNetworkReq) - async def start(self, autostart: bool) -> Status: await self.send_command(commands.StartReq(autostart=autostart)) @@ -532,7 +529,7 @@ async def reset(self) -> None: await self.send_command(commands.SystemResetReq(), wait_for_response=False) await self._poll_until_running() - async def system_factory(self): + async def factory_reset(self): await self.send_command(commands.SystemFactoryReq(), wait_for_response=False) await self._poll_until_running() diff --git a/zigpy_espzb/zigbee/application.py b/zigpy_espzb/zigbee/application.py index 9f5ca54..2c4d0c4 100644 --- a/zigpy_espzb/zigbee/application.py +++ b/zigpy_espzb/zigbee/application.py @@ -143,10 +143,10 @@ async def change_loop(): raise FormationFailure("Network formation refused.") async def reset_network_info(self): - await self._api.leave_network() + await self._api.factory_reset() async def write_network_info(self, *, network_info, node_info): - await self._api.system_factory() + await self._api.factory_reset() await self._api.network_init() await self._api.form_network(role=DeviceType.COORDINATOR) # await self._api.start(autostart=False) @@ -202,6 +202,11 @@ async def write_network_info(self, *, network_info, node_info): await self._api.start(autostart=True) async def load_network_info(self, *, load_devices=False): + channel = await self._api.get_current_channel() + + if not 11 <= channel <= 26: + raise NetworkNotFormed(f"Channel is invalid: {channel}") + network_info = self.state.network_info node_info = self.state.node_info From 08800a011ce9201f001e88ea585630860494e405 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:58:45 -0400 Subject: [PATCH 18/22] Fix formatting in `commands` --- zigpy_espzb/commands.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/zigpy_espzb/commands.py b/zigpy_espzb/commands.py index de7c729..28bd0f8 100644 --- a/zigpy_espzb/commands.py +++ b/zigpy_espzb/commands.py @@ -512,36 +512,47 @@ class SecurityModeSetReq(BaseCommand): class SecurityModeSetRsp(BaseCommand): status: Status + class SystemResetReq(BaseCommand): pass + class SystemResetRsp(BaseCommand): status: Status + class SystemFactoryReq(BaseCommand): pass + class SystemFactoryRsp(BaseCommand): status: Status + class SystemFirmwareReq(BaseCommand): pass + class SystemFirmwareRsp(BaseCommand): firmware_version: FirmwareVersion + class SystemModelReq(BaseCommand): pass + class SystemModelRsp(BaseCommand): payload: t.CharacterString + class SystemManufacturerReq(BaseCommand): pass + class SystemManufacturerRsp(BaseCommand): payload: t.CharacterString + COMMAND_SCHEMAS = { CommandId.network_init: ( NetworkInitReq, From 7497f5e08c946dac3c104a6f4f756ddce63672a7 Mon Sep 17 00:00:00 2001 From: liuhan Date: Wed, 17 Jul 2024 14:54:35 +0800 Subject: [PATCH 19/22] changes startup sequence and support factory and reset indication --- zigpy_espzb/commands.py | 8 ++++++-- zigpy_espzb/zigbee/application.py | 16 +++------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/zigpy_espzb/commands.py b/zigpy_espzb/commands.py index 28bd0f8..b6cc609 100644 --- a/zigpy_espzb/commands.py +++ b/zigpy_espzb/commands.py @@ -520,6 +520,8 @@ class SystemResetReq(BaseCommand): class SystemResetRsp(BaseCommand): status: Status +class SystemResetInd(BaseCommand): + error: t.uint32_t class SystemFactoryReq(BaseCommand): pass @@ -528,6 +530,8 @@ class SystemFactoryReq(BaseCommand): class SystemFactoryRsp(BaseCommand): status: Status +class SystemFactoryInd(BaseCommand): + error: t.uint32_t class SystemFirmwareReq(BaseCommand): pass @@ -747,12 +751,12 @@ class SystemManufacturerRsp(BaseCommand): CommandId.system_reset: ( SystemResetReq, SystemResetRsp, - None, + SystemResetInd, ), CommandId.system_factory: ( SystemFactoryReq, SystemFactoryRsp, - None, + SystemFactoryInd, ), CommandId.system_firmware: ( SystemFirmwareReq, diff --git a/zigpy_espzb/zigbee/application.py b/zigpy_espzb/zigbee/application.py index 2c4d0c4..65928a6 100644 --- a/zigpy_espzb/zigbee/application.py +++ b/zigpy_espzb/zigbee/application.py @@ -75,7 +75,6 @@ async def connect(self): # TODO: Most commands fail if the network is not formed. Why? await api.network_init() - await api.form_network(role=DeviceType.COORDINATOR) await api.start(autostart=False) self._api = api @@ -89,8 +88,6 @@ async def permit_with_link_key(self, node: t.EUI64, link_key: t.KeyData, time_s= raise NotImplementedError() async def start_network(self): - await self._api.start(autostart=True) - await self.load_network_info(load_devices=False) await self.register_endpoints() @@ -113,6 +110,8 @@ async def start_network(self): ep2 = coordinator.add_endpoint(2) ep2.status = zigpy.endpoint.Status.ZDO_INIT + await self._api.form_network(role=DeviceType.COORDINATOR) + async def _change_network_state( self, target_state: NetworkState, @@ -148,8 +147,7 @@ async def reset_network_info(self): async def write_network_info(self, *, network_info, node_info): await self._api.factory_reset() await self._api.network_init() - await self._api.form_network(role=DeviceType.COORDINATOR) - # await self._api.start(autostart=False) + await self._api.start(autostart=False) role = { zdo_t.LogicalType.Coordinator: DeviceType.COORDINATOR, @@ -193,14 +191,6 @@ async def write_network_info(self, *, network_info, node_info): await self._api.set_channel(network_info.channel) - # TODO: Network settings do not persist. How do you write them? - await self._api.start(autostart=True) - - await self._api.reset() - await self._api.network_init() - await self._api.form_network(role=DeviceType.COORDINATOR) - await self._api.start(autostart=True) - async def load_network_info(self, *, load_devices=False): channel = await self._api.get_current_channel() From d6ff41d82d68a53c01ad151970f5895baf1fa102 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 16 Oct 2024 11:20:52 -0400 Subject: [PATCH 20/22] Do not double convert the zigpy config schema --- zigpy_espzb/zigbee/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zigpy_espzb/zigbee/application.py b/zigpy_espzb/zigbee/application.py index 65928a6..1998107 100644 --- a/zigpy_espzb/zigbee/application.py +++ b/zigpy_espzb/zigbee/application.py @@ -52,7 +52,7 @@ class ControllerApplication(zigpy.application.ControllerApplication): def __init__(self, config: dict[str, Any]): """Initialize instance.""" - super().__init__(config=zigpy.config.ZIGPY_SCHEMA(config)) + super().__init__(config=config) self._api = None self._pending = zigpy.util.Requests() From ac5063bb5391d6b2ada152fed95b5b47ef79cdaa Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 16 Oct 2024 11:21:35 -0400 Subject: [PATCH 21/22] Bump zigpy dependency to support packet priority API --- pyproject.toml | 2 +- zigpy_espzb/zigbee/application.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 26a7b59..9497e3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ license = {text = "GPL-3.0"} requires-python = ">=3.8" dependencies = [ "voluptuous", - "zigpy>=0.60.2", + "zigpy>=0.68.1", 'async-timeout; python_version<"3.11"', ] diff --git a/zigpy_espzb/zigbee/application.py b/zigpy_espzb/zigbee/application.py index 1998107..352d896 100644 --- a/zigpy_espzb/zigbee/application.py +++ b/zigpy_espzb/zigbee/application.py @@ -329,7 +329,7 @@ async def send_packet(self, packet): if t.TransmitOptions.APS_Encryption in packet.tx_options: tx_options |= TransmitOptions.SECURITY_ENABLED - async with self._limit_concurrency(): + async with self._limit_concurrency(priority=packet.priority): await self._api.aps_data_request( dst_addr=dst_addr, dst_ep=packet.dst_ep, From 32245f43a14f943f9fbeda41145e9e7569f1dbe2 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:20:02 -0400 Subject: [PATCH 22/22] Handle unknown `0xFF` address mode --- zigpy_espzb/types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zigpy_espzb/types.py b/zigpy_espzb/types.py index 9294211..dc6b229 100644 --- a/zigpy_espzb/types.py +++ b/zigpy_espzb/types.py @@ -59,6 +59,8 @@ def to_zigpy_addr_mode(self) -> t.AddrMode: self.MODE_DST_ADDR_ENDP_NOT_PRESENT: t.AddrMode.NWK, self.MODE_16_GROUP_ENDP_NOT_PRESENT: t.AddrMode.Group, self.MODE_16_GROUP_ENDP_NOT_PRESENT: t.AddrMode.Broadcast, + # TODO: why is this necessary? + 0xFF: t.AddrMode.NWK, }[self]