From f345d443b2b45806775d52d1955248b56a342313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ndor=20Bal=C3=A1zs?= Date: Sun, 19 Jul 2020 17:13:30 +0200 Subject: [PATCH 1/4] pull updates from zigbee-herdsman fix zigbee groups fixes #58 --- setup.py | 4 +- test.py | 9 ++- tests/test_api.py | 4 +- tests/test_application.py | 1 + tests/test_buffalo.py | 10 ++- tests/test_types.py | 8 +- version.py | 2 +- zigpy_cc/api.py | 139 +++++++++++++++++++-------------- zigpy_cc/buffalo.py | 9 ++- zigpy_cc/types.py | 36 ++++++--- zigpy_cc/uart.py | 28 ++++--- zigpy_cc/zigbee/application.py | 69 +++++++++++----- zigpy_cc/zigbee/common.py | 2 +- zigpy_cc/zigbee/nv_items.py | 11 +-- zigpy_cc/zigbee/start_znp.py | 44 ++++++----- zigpy_cc/zigbee/utils.py | 17 ---- zigpy_cc/zpi_object.py | 57 +++++++++++--- 17 files changed, 286 insertions(+), 164 deletions(-) delete mode 100644 zigpy_cc/zigbee/utils.py diff --git a/setup.py b/setup.py index ef47ae8..c20895a 100644 --- a/setup.py +++ b/setup.py @@ -17,8 +17,8 @@ "Texas Instruments CC2531 radios for zigpy", long_description=long_description, long_description_content_type="text/markdown", - url="http://github.com/sanyatuning/zigpy-cc", - author="Balázs Sándor", + url="http://github.com/zigpy/zigpy-cc", + author="Balazs Sandor", author_email="sanyatuning@gmail.com", license="GPL-3.0", packages=find_packages(exclude=["*.tests"]), diff --git a/test.py b/test.py index 0196d3e..864a348 100644 --- a/test.py +++ b/test.py @@ -3,15 +3,20 @@ import os import coloredlogs as coloredlogs -from zigpy_cc import config +import zigpy.config from zigpy.device import Device +from zigpy_cc import config from zigpy_cc.zigbee import application fmt = "%(name)s %(levelname)s %(message)s" coloredlogs.install(level="DEBUG", fmt=fmt) APP_CONFIG = { + zigpy.config.CONF_NWK: { + zigpy.config.CONF_NWK_PAN_ID: 0x2A61, + zigpy.config.CONF_NWK_EXTENDED_PAN_ID: "A0:B0:C0:D0:10:20:30:40", + }, config.CONF_DEVICE: { config.CONF_DEVICE_PATH: "auto", config.CONF_DEVICE_BAUDRATE: 115200, @@ -80,7 +85,7 @@ async def main(): await app.startup(auto_form=False) await app.form_network() - # await app.permit_ncp() + await app.permit_ncp() loop.run_until_complete(main()) diff --git a/tests/test_api.py b/tests/test_api.py index a954505..457dc99 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -81,7 +81,7 @@ async def mock_fut(): for subsystem, commands in Definition.items(): for cmd in commands: - ret = await api._command(subsystem, cmd["name"], mock.sentinel.cmd_data) + ret = await api.request(subsystem, cmd["name"], mock.sentinel.cmd_data) assert ret is mock.sentinel.cmd_result # assert api._api_frame.call_count == 1 # assert api._api_frame.call_args[0][0] == cmd @@ -111,7 +111,7 @@ def mock_obj(subsystem, command, payload): for subsystem, commands in Definition.items(): for cmd in commands: with pytest.raises(asyncio.TimeoutError): - await api._command(subsystem, cmd["name"], mock.sentinel.cmd_data) + await api.request(subsystem, cmd["name"], mock.sentinel.cmd_data) # assert api._api_frame.call_count == 1 # assert api._api_frame.call_args[0][0] == cmd # assert api._api_frame.call_args[0][1] == mock.sentinel.cmd_data diff --git a/tests/test_application.py b/tests/test_application.py index 4557e7e..9537fa0 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -26,6 +26,7 @@ def app(): app = application.ControllerApplication(APP_CONFIG) app._api = API(APP_CONFIG[config.CONF_DEVICE]) app._api.set_application(app) + app._semaphore = asyncio.Semaphore() return app diff --git a/tests/test_buffalo.py b/tests/test_buffalo.py index de2f1bd..7e01b1d 100644 --- a/tests/test_buffalo.py +++ b/tests/test_buffalo.py @@ -1,6 +1,6 @@ -import zigpy_cc.types as t -from zigpy.types import EUI64, NWK +from zigpy.types import EUI64, Group, NWK +import zigpy_cc.types as t from zigpy_cc.buffalo import Buffalo, BuffaloOptions ieeeAddr1 = { @@ -26,6 +26,12 @@ def test_write_ieee2(): assert ieeeAddr2["hex"] == data_out.buffer +def test_write_ieee_group(): + data_out = Buffalo(b"") + data_out.write_parameter(t.ParameterType.IEEEADDR, Group(2), {}) + assert b"\x02\x00\x00\x00\x00\x00\x00\x00" == data_out.buffer + + def test_read_ieee(): data_in = Buffalo(ieeeAddr1["hex"]) actual = data_in.read_parameter(t.ParameterType.IEEEADDR, {}) diff --git a/tests/test_types.py b/tests/test_types.py index 4ee11e5..62263c6 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -305,7 +305,7 @@ def test_ieee_addr(): def test_from_cluster_id(): profile = 0 obj = ZpiObject.from_cluster( - NWK(53322), profile, ZDOCmd.Node_Desc_req, 0, 0, 3, b"\x03\x4a\xd0", 32 + NWK(53322), profile, ZDOCmd.Node_Desc_req, 0, 0, 3, b"\x03\x4a\xd0" ) assert ( @@ -327,11 +327,11 @@ def test_from_cluster_id(): def test_from_cluster_id_ZCL(): profile = 260 obj = ZpiObject.from_cluster( - NWK(53322), profile, 0, 1, 1, 1, b"\x00\x0b\x00\x04\x00\x05\x00", 123 + NWK(53322), profile, 0, 1, 1, 123, b"\x00\x0b\x00\x04\x00\x05\x00" ) assert ( - "SREQ AF dataRequest tsn: 1 {'dstaddr': 53322, 'destendpoint': 1, " + "SREQ AF dataRequest tsn: 123 {'dstaddr': 53322, 'destendpoint': 1, " "'srcendpoint': 1, 'clusterid': 0, 'transid': 123, 'options': 0, 'radius': 30, " "'len': 7, 'data': b'\\x00\\x0b\\x00\\x04\\x00\\x05\\x00'}" == str(obj) ) @@ -376,7 +376,7 @@ def test_bind_req(): data = b"\x02 None: def match(self, obj: ZpiObject): matcher = self.matcher if ( - matcher.type != obj.type + matcher.command_type != obj.command_type or matcher.subsystem != obj.subsystem or matcher.command != obj.command ): @@ -64,13 +72,16 @@ def match(self, obj: ZpiObject): class API: + _uart: Optional[Gateway] + def __init__(self, device_config: Dict[str, Any]): - self._uart = None self._config = device_config - self._seq = 1 - self._waiters: List[Waiter] = [] + self._lock = asyncio.Lock() + self._waiter_id = 0 + self._waiters: Dict[int, Waiter] = {} self._app = None self._proto_ver = None + self._uart = None @property def protocol_version(self): @@ -99,23 +110,24 @@ def close(self): def connection_lost(self): self._app.connection_lost() - async def _command(self, subsystem, command, payload) -> ZpiObject: - return await self.request(subsystem, command, payload) - - async def request(self, subsystem, command, payload, expected_status=None): + async def request( + self, subsystem, command, payload, waiter_id=None, expected_status=None + ): obj = ZpiObject.from_command(subsystem, command, payload) - return await self.request_raw(obj, expected_status) + return await self.request_raw(obj, waiter_id, expected_status) + + async def request_raw(self, obj: ZpiObject, waiter_id=None, expected_status=None): + async with self._lock: + return await self._request_raw(obj, waiter_id, expected_status) - async def request_raw(self, obj: ZpiObject, expected_status=None): + async def _request_raw(self, obj: ZpiObject, waiter_id=None, expected_status=None): if expected_status is None: expected_status = [0] - """ - TODO add queue - """ + LOGGER.debug("--> %s", obj) frame = obj.to_unpi_frame() - if obj.type == CommandType.SREQ: + if obj.command_type == CommandType.SREQ: timeout = ( 20000 if obj.command == "bdbStartCommissioning" @@ -132,6 +144,9 @@ async def request_raw(self, obj: ZpiObject, expected_status=None): and "status" in result.payload and result.payload["status"] not in expected_status ): + if waiter_id is not None: + self._waiters.pop(waiter_id).set_result(result) + raise CommandError( result.payload["status"], "SREQ '{}' failed with status '{}' (expected '{}')".format( @@ -139,42 +154,38 @@ async def request_raw(self, obj: ZpiObject, expected_status=None): ), ) else: - if obj.type == CommandType.SREQ and obj.command == "dataRequest": - payload = { - "endpoint": obj.payload["destendpoint"], - "transid": obj.payload["transid"], - } - waiter = self.wait_for( - CommandType.AREQ, Subsystem.AF, "dataConfirm", payload - ) - result = await waiter.wait() - return result - elif obj.type == CommandType.AREQ and obj.is_reset_command(): + elif obj.command_type == CommandType.AREQ and obj.is_reset_command(): waiter = self.wait_for( CommandType.AREQ, Subsystem.SYS, "resetInd", {}, Timeouts.reset ) - # TODO clear queue + # TODO clear queue, requests waiting for lock self._uart.send(frame) return await waiter.wait() else: - if obj.type == CommandType.AREQ: + if obj.command_type == CommandType.AREQ: self._uart.send(frame) return None else: - LOGGER.warning("Unknown type '%s'", obj.type) - raise Exception("Unknown type '{}'".format(obj.type)) + LOGGER.warning("Unknown type '%s'", obj.command_type) + raise Exception("Unknown type '{}'".format(obj.command_type)) def create_response_waiter(self, obj: ZpiObject, sequence=None): waiter = self.get_response_waiter(obj, sequence) if waiter: LOGGER.debug("waiting for %d %s", sequence, obj.command) + return waiter def get_response_waiter(self, obj: ZpiObject, sequence=None): - if obj.type == CommandType.SREQ and obj.command == "dataRequest": - return None + if obj.command_type == CommandType.SREQ and obj.command.startswith( + "dataRequest" + ): + payload = { + "transid": obj.payload["transid"], + } + return self.wait_for(CommandType.AREQ, Subsystem.AF, "dataConfirm", payload) - if obj.type == CommandType.SREQ and obj.command.endswith("Req"): + if obj.command_type == CommandType.SREQ and obj.command.endswith("Req"): rsp = obj.command.replace("Req", "Rsp") for cmd in Definition[obj.subsystem]: if rsp == cmd["name"]: @@ -188,20 +199,38 @@ def get_response_waiter(self, obj: ZpiObject, sequence=None): def wait_for( self, - type, - subsystem, - command, + command_type: CommandType, + subsystem: Subsystem, + command: str, payload=None, timeout=Timeouts.default, sequence=None, ): - waiter = Waiter(type, subsystem, command, payload, timeout, sequence) - self._waiters.append(waiter) + waiter = Waiter( + self._waiter_id, + command_type, + subsystem, + command, + payload, + timeout, + sequence, + ) + self._waiters[waiter.id] = waiter + self._waiter_id += 1 def callback(): if not waiter.future.done() or waiter.future.cancelled(): - LOGGER.warning("Waiter timeout: %s", waiter) - self._waiters.remove(waiter) + LOGGER.warning( + "No response for: %s %s %s %s", + command_type.name, + subsystem.name, + command, + payload, + ) + try: + self._waiters.pop(waiter.id) + except KeyError: + LOGGER.warning("Waiter not found: %s", waiter) asyncio.get_event_loop().call_later(timeout / 1000 + 0.1, callback) @@ -214,11 +243,10 @@ def data_received(self, frame): LOGGER.error("Error while parsing frame: %s", frame) raise e - to_remove = [] - for waiter in self._waiters: + for waiter_id in list(self._waiters): + waiter = self._waiters.get(waiter_id) if waiter.match(obj): - # LOGGER.debug("MATCH FOUND %s", waiter) - to_remove.append(waiter) + self._waiters.pop(waiter_id) waiter.set_result(obj) if waiter.sequence: obj.sequence = waiter.sequence @@ -226,9 +254,6 @@ def data_received(self, frame): LOGGER.debug("<-- %s", obj) - for waiter in to_remove: - self._waiters.remove(waiter) - if self._app is not None: self._app.handle_znp(obj) @@ -238,7 +263,7 @@ def data_received(self, frame): pass async def version(self): - version = await self._command(Subsystem.SYS, "version", {}) + version = await self.request(Subsystem.SYS, "version", {}) # todo check version self._proto_ver = version.payload return version.payload diff --git a/zigpy_cc/buffalo.py b/zigpy_cc/buffalo.py index b80770f..a5c15b5 100644 --- a/zigpy_cc/buffalo.py +++ b/zigpy_cc/buffalo.py @@ -1,3 +1,5 @@ +from collections.abc import Iterable + import zigpy.types from zigpy_cc.exception import TODO from zigpy_cc.types import ParameterType @@ -27,8 +29,11 @@ def write_parameter(self, type, value, options): elif type == ParameterType.UINT32: self.write(value, 4) elif type == ParameterType.IEEEADDR: - for i in value: - self.write(i) + if isinstance(value, Iterable): + for i in value: + self.write(i) + else: + self.write(value, 8) elif type == ParameterType.BUFFER: self.buffer += value elif type == ParameterType.LIST_UINT8: diff --git a/zigpy_cc/types.py b/zigpy_cc/types.py index 910ef5c..5e04e1b 100644 --- a/zigpy_cc/types.py +++ b/zigpy_cc/types.py @@ -1,6 +1,7 @@ import enum -from zigpy.types import uint8_t +import zigpy.config +import zigpy.types as t class Repr: @@ -17,7 +18,7 @@ class Timeouts: default = 10000 -class LedMode(uint8_t, enum.Enum): +class LedMode(t.uint8_t, enum.Enum): Off = 0 On = 1 Blink = 2 @@ -25,20 +26,20 @@ class LedMode(uint8_t, enum.Enum): Toggle = 4 -class ZnpVersion(uint8_t, enum.Enum): +class ZnpVersion(t.uint8_t, enum.Enum): zStack12 = 0 zStack3x0 = 1 zStack30x = 2 -class CommandType(uint8_t, enum.Enum): +class CommandType(t.uint8_t, enum.Enum): POLL = 0 SREQ = 1 AREQ = 2 SRSP = 3 -class Subsystem(uint8_t, enum.Enum): +class Subsystem(t.uint8_t, enum.Enum): RESERVED = 0 SYS = 1 MAC = 2 @@ -53,7 +54,7 @@ class Subsystem(uint8_t, enum.Enum): GREENPOWER = 21 -class ParameterType(uint8_t, enum.Enum): +class ParameterType(t.uint8_t, enum.Enum): UINT8 = 0 UINT16 = 1 UINT32 = 2 @@ -89,10 +90,21 @@ def is_buffer(type): ) -class NetworkOptions: - def __init__(self) -> None: +class NetworkOptions(Repr): + networkKey: t.KeyData + panID: t.PanId + extendedPanID: t.ExtendedPanId + channelList: t.Channels + + def __init__(self, config: zigpy.config.SCHEMA_NETWORK) -> None: self.networkKeyDistribute = False - self.networkKey = [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13] - self.panID = 0x1A62 - self.extendedPanID = [0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD] - self.channelList = [11] + self.networkKey = config[zigpy.config.CONF_NWK_KEY] or ( + zigpy.config.cv_key([1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13]) + ) + self.panID = config[zigpy.config.CONF_NWK_PAN_ID] or t.PanId(0x1A62) + self.extendedPanID = config[zigpy.config.CONF_NWK_EXTENDED_PAN_ID] or ( + t.ExtendedPanId([0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD]) + ) + self.channelList = ( + config[zigpy.config.CONF_NWK_CHANNELS] or t.Channels.CHANNEL_11 + ) diff --git a/zigpy_cc/uart.py b/zigpy_cc/uart.py index 141c77d..da15a3a 100644 --- a/zigpy_cc/uart.py +++ b/zigpy_cc/uart.py @@ -5,6 +5,7 @@ import serial import serial.tools.list_ports import serial_asyncio +from serial.tools.list_ports_common import ListPortInfo from zigpy_cc.config import CONF_DEVICE_BAUDRATE, CONF_DEVICE_PATH, CONF_FLOW_CONTROL import zigpy_cc.types as t @@ -66,7 +67,7 @@ def __init__( length=None, fcs=None, ): - self.type = t.CommandType(command_type) + self.command_type = t.CommandType(command_type) self.subsystem = t.Subsystem(subsystem) self.command_id = command_id self.data = data @@ -99,7 +100,7 @@ def calculate_checksum(values): def to_buffer(self): length = len(self.data) - cmd0 = ((self.type << 5) & 0xE0) | (self.subsystem & 0x1F) + cmd0 = ((self.command_type << 5) & 0xE0) | (self.subsystem & 0x1F) res = bytes([SOF, length, cmd0, self.command_id]) res += self.data @@ -159,6 +160,19 @@ def connection_lost(self, exc): self._api.connection_lost() +def detect_port() -> ListPortInfo: + devices = list(serial.tools.list_ports.grep(usb_regexp)) + if len(devices) < 1: + raise serial.SerialException("Unable to find TI CC device using auto mode") + if len(devices) > 1: + raise serial.SerialException( + "Unable to select TI CC device, multiple devices found: {}".format( + ", ".join(map(lambda d: str(d), devices)) + ) + ) + return devices[0] + + async def connect(config: Dict[str, Any], api, loop=None) -> Gateway: if loop is None: loop = asyncio.get_event_loop() @@ -168,13 +182,9 @@ async def connect(config: Dict[str, Any], api, loop=None) -> Gateway: port, baudrate = config[CONF_DEVICE_PATH], config[CONF_DEVICE_BAUDRATE] if port == "auto": - devices = list(serial.tools.list_ports.grep(usb_regexp)) - if devices: - port = devices[0].device - LOGGER.info("%s found at %s", devices[0].product, port) - else: - LOGGER.error("Unable to find TI CC device using auto mode") - raise serial.SerialException("Unable to find TI CC device using auto mode") + device = detect_port() + LOGGER.info("Auto select TI CC device: %s", device) + port = device.device xonxoff, rtscts = False, False if config[CONF_FLOW_CONTROL] == "hardware": diff --git a/zigpy_cc/zigbee/application.py b/zigpy_cc/zigbee/application.py index c1c480d..2b7a063 100644 --- a/zigpy_cc/zigbee/application.py +++ b/zigpy_cc/zigbee/application.py @@ -1,5 +1,6 @@ import asyncio import logging +from asyncio.locks import Semaphore from typing import Any, Dict, Optional import zigpy.application @@ -40,11 +41,9 @@ } IGNORED = ( - # "activeEpRsp", "bdbComissioningNotifcation", "dataConfirm", "leaveInd", - # "mgmtPermitJoinRsp", "srcRtgInd", "stateChangeInd", "tcDeviceInd", @@ -52,6 +51,7 @@ class ControllerApplication(zigpy.application.ControllerApplication): + _semaphore: Semaphore _api: Optional[API] SCHEMA = CONFIG_SCHEMA SCHEMA_DEVICE = SCHEMA_DEVICE @@ -89,7 +89,19 @@ async def startup(self, auto_form=False): LOGGER.info("Starting zigpy-cc version: %s", __version__) self._api = await API.new(self, self._config[CONF_DEVICE]) + + try: + await self._api.request(Subsystem.SYS, "ping", {"capabilities": 1}) + except CommandError as e: + raise Exception("Failed to connect to the adapter(%s)", e) + self.version = await self._api.version() + + concurrent = 16 if self.version["product"] == ZnpVersion.zStack3x0 else 2 + LOGGER.debug("Adapter concurrent: %d", concurrent) + + self._semaphore = asyncio.Semaphore(concurrent) + ver = ZnpVersion(self.version["product"]).name LOGGER.info("Detected znp version '%s' (%s)", ver, self.version) @@ -112,12 +124,14 @@ async def force_remove(self, dev: zigpy.device.Device): async def form_network(self, channel=15, pan_id=None, extended_pan_id=None): LOGGER.info("Forming network") - options = NetworkOptions() + LOGGER.debug("Config: %s", self.config) + options = NetworkOptions(self.config[zigpy.config.CONF_NWK]) + LOGGER.debug("NetworkOptions: %s", options) backupPath = "" status = await start_znp( self._api, self.version["product"], options, 0x0B84, backupPath ) - LOGGER.debug("ZNP started, status: %s", status) + LOGGER.info("ZNP started, status: %s", status) self.set_led(LedMode.Off) @@ -149,7 +163,6 @@ async def mrequest( :returns: return a tuple of a status and an error_message. Original requestor has more context to provide a more meaningful error message """ - req_id = self.get_sequence() LOGGER.debug( "multicast %s", ( @@ -165,10 +178,21 @@ async def mrequest( ) try: obj = ZpiObject.from_cluster( - group_id, profile, cluster, src_ep, src_ep, sequence, data, req_id + 0, profile, cluster, src_ep, 0xFF, sequence, data, group=group_id ) - - await self._api.request_raw(obj) + waiter_id = None + waiter = self._api.create_response_waiter(obj, sequence) + if waiter: + waiter_id = waiter.id + + async with self._semaphore: + await self._api.request_raw(obj, waiter_id) + """ + As a group command is not confirmed and thus immediately returns + (contrary to network address requests) we will give the + command some time to 'settle' in the network. + """ + await asyncio.sleep(0.2) except CommandError as ex: return ex.status, "Couldn't enqueue send data multicast: {}".format(ex) @@ -188,7 +212,6 @@ async def request( expect_reply=True, use_ieee=False, ): - req_id = self.get_sequence() LOGGER.debug( "request %s", ( @@ -206,12 +229,16 @@ async def request( try: obj = ZpiObject.from_cluster( - device.nwk, profile, cluster, src_ep, dst_ep, sequence, data, req_id + device.nwk, profile, cluster, src_ep, dst_ep, sequence, data ) + waiter_id = None if expect_reply: - self._api.create_response_waiter(obj, sequence) + waiter = self._api.create_response_waiter(obj, sequence) + if waiter: + waiter_id = waiter.id - await self._api.request_raw(obj) + async with self._semaphore: + await self._api.request_raw(obj, waiter_id) except CommandError as ex: return ex.status, "Couldn't enqueue send data request: {}".format(ex) @@ -230,7 +257,6 @@ async def broadcast( data, broadcast_address=zigpy.types.BroadcastAddress.RX_ON_WHEN_IDLE, ): - req_id = self.get_sequence() LOGGER.debug( "broadcast %s", ( @@ -254,11 +280,11 @@ async def broadcast( dst_ep, sequence, data, - req_id, radius=radius, ) - await self._api.request_raw(obj) + async with self._semaphore: + await self._api.request_raw(obj) except CommandError as ex: return ( @@ -276,10 +302,11 @@ async def permit_ncp(self, time_s=60): "duration": time_s, "tcsignificance": 0, } - await self._api.request(Subsystem.ZDO, "mgmtPermitJoinReq", payload) + async with self._semaphore: + await self._api.request(Subsystem.ZDO, "mgmtPermitJoinReq", payload) def handle_znp(self, obj: ZpiObject): - if obj.type != t.CommandType.AREQ: + if obj.command_type != t.CommandType.AREQ: return frame = obj.to_unpi_frame() @@ -299,7 +326,6 @@ def handle_znp(self, obj: ZpiObject): obj.sequence = 0 if obj.command in IGNORED: - LOGGER.debug("message ignored: %s", obj.command) return if obj.subsystem == t.Subsystem.ZDO and obj.command == "mgmtPermitJoinRsp": @@ -330,7 +356,7 @@ def handle_znp(self, obj: ZpiObject): else: LOGGER.warning( "Unhandled message: %s %s %s", - t.CommandType(obj.type), + t.CommandType(obj.command_type), t.Subsystem(obj.subsystem), obj.command, ) @@ -366,6 +392,11 @@ def set_led(self, mode: LedMode): class Coordinator(zigpy.device.Device): + """ + todo add endpoints? + @see zStackAdapter.ts - getCoordinator + """ + @property def manufacturer(self): return "Texas Instruments" diff --git a/zigpy_cc/zigbee/common.py b/zigpy_cc/zigbee/common.py index f04935a..ec6e516 100644 --- a/zigpy_cc/zigbee/common.py +++ b/zigpy_cc/zigbee/common.py @@ -1,6 +1,6 @@ import enum -from zigpy_cc.types import uint8_t +from zigpy.types import uint8_t class NvItemsIds(uint8_t, enum.Enum): diff --git a/zigpy_cc/zigbee/nv_items.py b/zigpy_cc/zigbee/nv_items.py index 48f1caa..b47095a 100644 --- a/zigpy_cc/zigbee/nv_items.py +++ b/zigpy_cc/zigbee/nv_items.py @@ -1,6 +1,7 @@ +from zigpy.types import ExtendedPanId, Channels + from zigpy_cc.types import ZnpVersion from zigpy_cc.zigbee.common import NvItemsIds -from zigpy_cc.zigbee.utils import getChannelMask class Items: @@ -36,21 +37,21 @@ def panID(panID): } @staticmethod - def extendedPanID(extendedPanID): + def extendedPanID(extendedPanID: ExtendedPanId): return { "id": NvItemsIds.EXTENDED_PAN_ID, "len": 0x08, "offset": 0x00, - "value": bytes(extendedPanID), + "value": extendedPanID.serialize(), } @staticmethod - def channelList(channelList): + def channelList(channelList: Channels): return { "id": NvItemsIds.CHANLIST, "len": 0x04, "offset": 0x00, - "value": bytes(getChannelMask(channelList)), + "value": channelList.serialize(), } @staticmethod diff --git a/zigpy_cc/zigbee/start_znp.py b/zigpy_cc/zigbee/start_znp.py index a56d102..d0d7114 100644 --- a/zigpy_cc/zigbee/start_znp.py +++ b/zigpy_cc/zigbee/start_znp.py @@ -1,8 +1,9 @@ +import asyncio import logging import os from zigpy.zcl.clusters.general import Ota -from zigpy.zcl.clusters.security import IasZone +from zigpy.zcl.clusters.security import IasZone, IasWd from zigpy_cc.api import API from zigpy_cc.const import Constants @@ -10,7 +11,6 @@ from zigpy_cc.exception import CommandError from zigpy_cc.zigbee.backup import Restore from zigpy_cc.zigbee.common import Common -from zigpy_cc.zigbee.utils import getChannelMask from .nv_items import Items LOGGER = logging.getLogger(__name__) @@ -42,8 +42,8 @@ def __init__(self, **kwargs) -> None: endpoint=11, appprofid=0x0104, appdeviceid=0x0400, - appnumoutclusters=1, - appoutclusterlist=[IasZone.cluster_id], + appnumoutclusters=2, + appoutclusterlist=[IasZone.cluster_id, IasWd.cluster_id], ), # TERNCY: https://github.com/Koenkk/zigbee-herdsman/issues/82 Endpoint(endpoint=0x6E, appprofid=0x0104), @@ -70,7 +70,7 @@ async def validate_item( command="osalNvRead", expected_status=None, ): - result = await znp.request(subsystem, command, item, expected_status) + result = await znp.request(subsystem, command, item, None, expected_status) if result.payload["value"] != item["value"]: msg = "Item '{}' is invalid, got '{}', expected '{}'".format( message, result.payload["value"], item["value"] @@ -138,7 +138,9 @@ async def boot(znp: API): started = znp.wait_for( CommandType.AREQ, Subsystem.ZDO, "stateChangeInd", {"state": 9}, 60000 ) - await znp.request(Subsystem.ZDO, "startupFromApp", {"startdelay": 100}, [0, 1]) + await znp.request( + Subsystem.ZDO, "startupFromApp", {"startdelay": 100}, None, [0, 1] + ) await started.wait() LOGGER.info("ZNP started as coordinator") else: @@ -148,17 +150,16 @@ async def boot(znp: API): async def registerEndpoints(znp: API): LOGGER.debug("Register endpoints...") - activeEpResponse = znp.wait_for(CommandType.AREQ, Subsystem.ZDO, "activeEpRsp") - try: - await znp.request( + active_ep_response = znp.wait_for(CommandType.AREQ, Subsystem.ZDO, "activeEpRsp") + asyncio.create_task( + znp.request( Subsystem.ZDO, "activeEpReq", {"dstaddr": 0, "nwkaddrofinterest": 0} ) - except Exception as e: - LOGGER.debug(e) - activeEp = await activeEpResponse.wait() + ) + active_ep = await active_ep_response.wait() for endpoint in Endpoints: - if endpoint.endpoint in activeEp.payload["activeeplist"]: + if endpoint.endpoint in active_ep.payload["activeeplist"]: LOGGER.debug("Endpoint '%s' already registered", endpoint.endpoint) else: LOGGER.debug("Registering endpoint '%s'", endpoint.endpoint) @@ -194,13 +195,10 @@ async def initialise(znp: API, version, options: NetworkOptions): ) # Default link key is already OK for Z-Stack 3 ('ZigBeeAlliance09') - channelMask = int.from_bytes( - bytes(getChannelMask(options.channelList)), "little" - ) await znp.request( Subsystem.APP_CNF, "bdbSetChannel", - {"isPrimary": 0x1, "channel": channelMask}, + {"isPrimary": 0x1, "channel": options.channelList}, ) await znp.request( Subsystem.APP_CNF, "bdbSetChannel", {"isPrimary": 0x0, "channel": 0x0} @@ -227,14 +225,22 @@ async def initialise(znp: API, version, options: NetworkOptions): # expect status code 9 (= item created and initialized) await znp.request( - Subsystem.SYS, "osalNvItemInit", Items.znpHasConfiguredInit(version), [0, 9] + Subsystem.SYS, + "osalNvItemInit", + Items.znpHasConfiguredInit(version), + None, + [0, 9], ) await znp.request(Subsystem.SYS, "osalNvWrite", Items.znpHasConfigured(version)) async def addToGroup(znp: API, endpoint: int, group: int): result = await znp.request( - Subsystem.ZDO, "extFindGroup", {"endpoint": endpoint, "groupid": group}, [0, 1] + Subsystem.ZDO, + "extFindGroup", + {"endpoint": endpoint, "groupid": group}, + None, + [0, 1], ) if result.payload["status"] == 1: await znp.request( diff --git a/zigpy_cc/zigbee/utils.py b/zigpy_cc/zigbee/utils.py deleted file mode 100644 index 3556418..0000000 --- a/zigpy_cc/zigbee/utils.py +++ /dev/null @@ -1,17 +0,0 @@ -from zigpy_cc.zigbee.common import Common - - -def getChannelMask(channels): - value = 0 - - for channel in channels: - for key, logicalChannel in Common.logicalChannels.items(): - if logicalChannel == channel: - value = value | Common.channelMask[key] - - return [ - value & 0xFF, - (value >> 8) & 0xFF, - (value >> 16) & 0xFF, - (value >> 24) & 0xFF, - ] diff --git a/zigpy_cc/zpi_object.py b/zigpy_cc/zpi_object.py index ddcee31..f375a03 100644 --- a/zigpy_cc/zpi_object.py +++ b/zigpy_cc/zpi_object.py @@ -27,7 +27,7 @@ class ZpiObject: def __init__( self, - type, + command_type, subsystem, command: str, commandId, @@ -35,8 +35,8 @@ def __init__( parameters, sequence=None, ): - self.type = type - self.subsystem = subsystem + self.command_type = CommandType(command_type) + self.subsystem = Subsystem(subsystem) self.command = command self.command_id = commandId self.payload = payload @@ -55,7 +55,9 @@ def to_unpi_frame(self): value = self.payload[p["name"]] data.write_parameter(p["parameterType"], value, {}) - return uart.UnpiFrame(self.type, self.subsystem, self.command_id, data.buffer) + return uart.UnpiFrame( + self.command_type, self.subsystem, self.command_id, data.buffer + ) @classmethod def from_command(cls, subsystem, command, payload): @@ -72,21 +74,41 @@ def from_unpi_frame(cls, frame): c for c in Definition[frame.subsystem] if c["ID"] == frame.command_id ) parameters = ( - cmd["response"] if frame.type == CommandType.SRSP else cmd["request"] + cmd["response"] + if frame.command_type == CommandType.SRSP + else cmd["request"] ) payload = cls.read_parameters(frame.data, parameters) return cls( - frame.type, frame.subsystem, cmd["name"], cmd["ID"], payload, parameters + frame.command_type, + frame.subsystem, + cmd["name"], + cmd["ID"], + payload, + parameters, ) @classmethod def from_cluster( - cls, nwk, profile, cluster, src_ep, dst_ep, sequence, data, req_id, *, radius=30 + cls, + nwk, + profile, + cluster, + src_ep, + dst_ep, + sequence, + data, + *, + radius=30, + group=None ): if profile == zha.PROFILE_ID: subsystem = Subsystem.AF - cmd = next(c for c in Definition[subsystem] if c["ID"] == 1) + if group is None: + cmd = next(c for c in Definition[subsystem] if c["ID"] == 1) + else: + cmd = next(c for c in Definition[subsystem] if c["ID"] == 2) else: subsystem = Subsystem.ZDO cmd = next(c for c in Definition[subsystem] if c["ID"] == cluster) @@ -101,7 +123,22 @@ def from_cluster( "destendpoint": dst_ep, "srcendpoint": src_ep, "clusterid": cluster, - "transid": req_id, + "transid": sequence, + "options": 0, + "radius": radius, + "len": len(data), + "data": data, + } + elif name == "dataRequestExt": + payload = { + # 1: group + "dstaddrmode": 1, + "dstaddr": group, + "destendpoint": 0xFF, + "dstpanid": 0, + "srcendpoint": src_ep or 1, + "clusterid": cluster, + "transid": sequence, "options": 0, "radius": radius, "len": len(data), @@ -161,7 +198,7 @@ def read_parameters(cls, data: bytes, parameters): return res def __repr__(self) -> str: - command_type = CommandType(self.type).name + command_type = CommandType(self.command_type).name subsystem = Subsystem(self.subsystem).name return "{} {} {} tsn: {} {}".format( command_type, subsystem, self.command, self.sequence, self.payload From b5e671c2e2bdddd132ea5392caa38f9d75ceb867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ndor=20Bal=C3=A1zs?= Date: Sun, 19 Jul 2020 17:37:50 +0200 Subject: [PATCH 2/4] add test --- tests/test_application.py | 62 +++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/tests/test_application.py b/tests/test_application.py index 9537fa0..f5bbb5f 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -3,8 +3,9 @@ from unittest import mock import pytest -from zigpy.types import EUI64 +from zigpy.types import EUI64, Group import zigpy.zdo.types as zdo_t +from zigpy.zcl.clusters.general import Groups from zigpy_cc import types as t from zigpy_cc.api import API @@ -65,39 +66,6 @@ def addr_nwk_and_ieee(nwk, ieee): return addr -""" -DEBUG:zigpy_cc.api:--> AREQ ZDO leaveInd {'srcaddr': 406, 'extaddr': '0x07a3c302008d1500', 'request': 0, 'removechildren': 0, 'rejoin': 0} -DEBUG:zigpy_cc.api:--> AREQ ZDO tcDeviceInd {'nwkaddr': 11938, 'extaddr': '0x07a3c302008d1500', 'parentaddr': 0} -DEBUG:zigpy_cc.api:--> AREQ ZDO endDeviceAnnceInd {'srcaddr': 11938, 'nwkaddr': 11938, 'ieeeaddr': '0x07a3c302008d1500', 'capabilities': 128} -DEBUG:zigpy_cc.api:--> AREQ AF incomingMsg {'groupid': 0, 'clusterid': 0, 'srcaddr': 11938, 'srcendpoint': 1, 'dstendpoint': 1, 'wasbroadcast': 0, 'linkquality': 123, 'securityuse': 0, 'timestamp': 1000027, 'transseqnumber': 0, 'len': 25, 'data': bytearray(b'\x18\x00\n\x05\x00B\x12lumi.sensor_switch')} -DEBUG:zigpy_cc.api:--> AREQ AF incomingMsg {'groupid': 0, 'clusterid': 0, 'srcaddr': 11938, 'srcendpoint': 1, 'dstendpoint': 1, 'wasbroadcast': 0, 'linkquality': 123, 'securityuse': 0, 'timestamp': 1000039, 'transseqnumber': 0, 'len': 7, 'data': bytearray(b'\x18\x01\n\x01\x00 \n')} -DEBUG:zigpy_cc.api:--> AREQ AF incomingMsg {'groupid': 0, 'clusterid': 0, 'srcaddr': 11938, 'srcendpoint': 1, 'dstendpoint': 1, 'wasbroadcast': 0, 'linkquality': 126, 'securityuse': 0, 'timestamp': 1000050, 'transseqnumber': 0, 'len': 29, 'data': bytearray(b'\x1c4\x12\x02\n\x02\xffL\x06\x00\x10\x01!\xd8\x0b!\xa8\x01$\x00\x00\x00\x00\x00!\xbdJ Y')} -DEBUG:zigpy_cc.api:--> AREQ ZDO srcRtgInd {'dstaddr': 6595, 'relaycount': 1, 'relaylist': [30485]} -DEBUG:zigpy_cc.api:--> AREQ AF incomingMsg {'groupid': 0, 'clusterid': 10, 'srcaddr': 6595, 'srcendpoint': 1, 'dstendpoint': 1, 'wasbroadcast': 0, 'linkquality': 60, 'securityuse': 0, 'timestamp': 1069473, 'transseqnumber': 0, 'len': 5, 'data': bytearray(b'\x10\x94\x00\x00\x00')} -DEBUG:zigpy_cc.api:--> AREQ ZDO srcRtgInd {'dstaddr': 49164, 'relaycount': 2, 'relaylist': [6595, 30485]} -DEBUG:zigpy_cc.api:--> AREQ AF incomingMsg {'groupid': 0, 'clusterid': 10, 'srcaddr': 49164, 'srcendpoint': 1, 'dstendpoint': 1, 'wasbroadcast': 0, 'linkquality': 60, 'securityuse': 0, 'timestamp': 1117084, 'transseqnumber': 0, 'len': 5, 'data': bytearray(b'\x10\x13\x00\x00\x00')} -DEBUG:zigpy_cc.api:--> AREQ ZDO srcRtgInd {'dstaddr': 51918, 'relaycount': 0, 'relaylist': []} -DEBUG:zigpy_cc.api:--> AREQ ZDO srcRtgInd {'dstaddr': 51918, 'relaycount': 0, 'relaylist': []} -DEBUG:zigpy_cc.api:--> AREQ ZDO endDeviceAnnceInd {'srcaddr': 53322, 'nwkaddr': 53322, 'ieeeaddr': '0x41e54b02008d1500', 'capabilities': 132} -DEBUG:zigpy_cc.api:--> AREQ ZDO tcDeviceInd {'nwkaddr': 53322, 'extaddr': '0x41e54b02008d1500', 'parentaddr': 51918} -DEBUG:zigpy_cc.api:--> AREQ ZDO srcRtgInd {'dstaddr': 53322, 'relaycount': 1, 'relaylist': [51918]} -DEBUG:zigpy_cc.api:--> AREQ AF incomingMsg {'groupid': 0, 'clusterid': 10, 'srcaddr': 53322, 'srcendpoint': 1, 'dstendpoint': 1, 'wasbroadcast': 0, 'linkquality': 39, 'securityuse': 0, 'timestamp': 1137608, 'transseqnumber': 0, 'len': 5, 'data': bytearray(b'\x10\x1d\x00\x00\x00')} -DEBUG:zigpy_cc.api:--> AREQ ZDO srcRtgInd {'dstaddr': 44052, 'relaycount': 2, 'relaylist': [6595, 30485]} -DEBUG:zigpy_cc.api:--> AREQ AF incomingMsg {'groupid': 0, 'clusterid': 18, 'srcaddr': 44052, 'srcendpoint': 2, 'dstendpoint': 1, 'wasbroadcast': 0, 'linkquality': 60, 'securityuse': 0, 'timestamp': 1167545, 'transseqnumber': 0, 'len': 8, 'data': bytearray(b'\x18E\nU\x00!\x02\x00')} -DEBUG:zigpy_cc.api:--> AREQ ZDO srcRtgInd {'dstaddr': 6595, 'relaycount': 1, 'relaylist': [30485]} -DEBUG:zigpy_cc.api:--> AREQ AF incomingMsg {'groupid': 0, 'clusterid': 10, 'srcaddr': 6595, 'srcendpoint': 1, 'dstendpoint': 1, 'wasbroadcast': 0, 'linkquality': 57, 'securityuse': 0, 'timestamp': 1256953, 'transseqnumber': 0, 'len': 5, 'data': bytearray(b'\x10\xd1\x00\x00\x00')} -DEBUG:zigpy_cc.api:--> AREQ ZDO srcRtgInd {'dstaddr': 49164, 'relaycount': 2, 'relaylist': [6595, 30485]} -DEBUG:zigpy_cc.api:--> AREQ AF incomingMsg {'groupid': 0, 'clusterid': 10, 'srcaddr': 49164, 'srcendpoint': 1, 'dstendpoint': 1, 'wasbroadcast': 0, 'linkquality': 57, 'securityuse': 0, 'timestamp': 1310524, 'transseqnumber': 0, 'len': 5, 'data': bytearray(b'\x10\x14\x00\x00\x00')} -DEBUG:zigpy_cc.api:--> AREQ ZDO srcRtgInd {'dstaddr': 53322, 'relaycount': 1, 'relaylist': [51918]} -DEBUG:zigpy_cc.api:--> AREQ AF incomingMsg {'groupid': 0, 'clusterid': 10, 'srcaddr': 53322, 'srcendpoint': 1, 'dstendpoint': 1, 'wasbroadcast': 0, 'linkquality': 39, 'securityuse': 0, 'timestamp': 1331211, 'transseqnumber': 0, 'len': 5, 'data': bytearray(b'\x10\x1e\x00\x00\x00')} -DEBUG:zigpy_cc.api:--> AREQ ZDO srcRtgInd {'dstaddr': 6595, 'relaycount': 1, 'relaylist': [30485]} -DEBUG:zigpy_cc.api:--> AREQ AF incomingMsg {'groupid': 0, 'clusterid': 10, 'srcaddr': 6595, 'srcendpoint': 1, 'dstendpoint': 1, 'wasbroadcast': 0, 'linkquality': 60, 'securityuse': 0, 'timestamp': 1444466, 'transseqnumber': 0, 'len': 5, 'data': bytearray(b'\x10\x0e\x00\x00\x00')} -DEBUG:zigpy_cc.api:--> AREQ ZDO srcRtgInd {'dstaddr': 49164, 'relaycount': 2, 'relaylist': [6595, 30485]} -DEBUG:zigpy_cc.api:--> AREQ AF incomingMsg {'groupid': 0, 'clusterid': 10, 'srcaddr': 49164, 'srcendpoint': 1, 'dstendpoint': 1, 'wasbroadcast': 0, 'linkquality': 57, 'securityuse': 0, 'timestamp': 1504140, 'transseqnumber': 0, 'len': 5, 'data': bytearray(b'\x10\x15\x00\x00\x00')} -DEBUG:zigpy_cc.api:--> AREQ ZDO tcDeviceInd {'nwkaddr': 49164, 'extaddr': '0x7ceb2303008d1500', 'parentaddr': 6595} -""" - - def test_join(app): payload = {"nwkaddr": 27441, "extaddr": "0x07a3c302008d1500", "parentaddr": 0} obj = ZpiObject(2, 5, "tcDeviceInd", 202, payload, []) @@ -200,13 +168,37 @@ async def test_request(app: application.ControllerApplication): ) assert len(app._api._waiters) == 1 + assert ( + "SREQ ZDO nodeDescReq tsn: 1 {'dstaddr': 0xd04a, 'nwkaddrofinterest': 0x2ea2}" + == str(app._api.request_raw.call_args[0][0]) + ) assert res == (0, "message send success") +@pytest.mark.asyncio +async def test_mrequest(app: application.ControllerApplication): + fut = asyncio.Future() + fut.set_result(None) + app._api.request_raw = mock.MagicMock(return_value=fut) + + # multicast (0x0002, 260, 6, 1, 39, b"\x01'\x00", 0, 3) + res = await app.mrequest(Group(2), 260, Groups.cluster_id, 1, 39, b"\x01'\x00") + + assert 1 == len(app._api._waiters) + assert ( + "SREQ AF dataRequestExt tsn: 39 {'dstaddrmode': 1, 'dstaddr': 0x0002, 'destendpoint': 255, 'dstpanid': 0, " + "'srcendpoint': 1, 'clusterid': 4, 'transid': 39, 'options': 0, 'radius': 30, 'len': 3, " + "'data': b\"\\x01'\\x00\"}" == str(app._api.request_raw.call_args[0][0]) + ) + assert (0, "message send success") == res + + """ zigpy_cc.api DEBUG <-- SREQ ZDO nodeDescReq {'dstaddr': 53322, 'nwkaddrofinterest': 0} zigpy_cc.api DEBUG --> SRSP ZDO nodeDescReq {'status': 0} -zigpy_cc.api DEBUG --> AREQ ZDO nodeDescRsp {'srcaddr': 53322, 'status': 128, 'nwkaddr': 0, 'logicaltype_cmplxdescavai_userdescavai': 0, 'apsflags_freqband': 0, 'maccapflags': 0, 'manufacturercode': 0, 'maxbuffersize': 0, 'maxintransfersize': 0, 'servermask': 0, 'maxouttransfersize': 0, 'descriptorcap': 0} +zigpy_cc.api DEBUG --> AREQ ZDO nodeDescRsp {'srcaddr': 53322, 'status': 128, 'nwkaddr': 0, + 'logicaltype_cmplxdescavai_userdescavai': 0, 'apsflags_freqband': 0, 'maccapflags': 0, 'manufacturercode': 0, + 'maxbuffersize': 0, 'maxintransfersize': 0, 'servermask': 0, 'maxouttransfersize': 0, 'descriptorcap': 0} """ From feac7bd309b1af3b6b15f316aaf4bdfa7d5454cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ndor=20Bal=C3=A1zs?= Date: Sun, 19 Jul 2020 18:52:42 +0200 Subject: [PATCH 3/4] update broadcast based on zigbee-herdsman --- tests/test_application.py | 25 ++++++++++++++++-- tests/test_buffalo.py | 6 ++--- tests/test_types.py | 9 ++++--- zigpy_cc/buffalo.py | 14 +++++++--- zigpy_cc/types.py | 8 ++++++ zigpy_cc/zigbee/application.py | 20 +++++++++++--- zigpy_cc/zpi_object.py | 48 ++++++++++++++++------------------ 7 files changed, 88 insertions(+), 42 deletions(-) diff --git a/tests/test_application.py b/tests/test_application.py index f5bbb5f..4d06e5d 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -3,7 +3,7 @@ from unittest import mock import pytest -from zigpy.types import EUI64, Group +from zigpy.types import EUI64, Group, BroadcastAddress import zigpy.zdo.types as zdo_t from zigpy.zcl.clusters.general import Groups @@ -186,13 +186,34 @@ async def test_mrequest(app: application.ControllerApplication): assert 1 == len(app._api._waiters) assert ( - "SREQ AF dataRequestExt tsn: 39 {'dstaddrmode': 1, 'dstaddr': 0x0002, 'destendpoint': 255, 'dstpanid': 0, " + "SREQ AF dataRequestExt tsn: 39 {'dstaddrmode': , " + "'dstaddr': 0x0002, 'destendpoint': 255, 'dstpanid': 0, " "'srcendpoint': 1, 'clusterid': 4, 'transid': 39, 'options': 0, 'radius': 30, 'len': 3, " "'data': b\"\\x01'\\x00\"}" == str(app._api.request_raw.call_args[0][0]) ) assert (0, "message send success") == res +@pytest.mark.asyncio +async def test_broadcast(app: application.ControllerApplication): + fut = asyncio.Future() + fut.set_result(None) + app._api.request_raw = mock.MagicMock(return_value=fut) + + # broadcast (0, 54, 0, 0, 0, 0, 45, b'-<\x00', ) + res = await app.broadcast( + 0, 54, 0, 0, 0, 0, 45, b"-<\x00", BroadcastAddress.ALL_ROUTERS_AND_COORDINATOR + ) + + assert 0 == len(app._api._waiters) + assert ( + "SREQ ZDO mgmtPermitJoinReq tsn: 45 {'addrmode': , " + "'dstaddr': 0xfffc, 'duration': 60, 'tcsignificance': 0}" + == str(app._api.request_raw.call_args[0][0]) + ) + assert (0, "broadcast send success") == res + + """ zigpy_cc.api DEBUG <-- SREQ ZDO nodeDescReq {'dstaddr': 53322, 'nwkaddrofinterest': 0} zigpy_cc.api DEBUG --> SRSP ZDO nodeDescReq {'status': 0} diff --git a/tests/test_buffalo.py b/tests/test_buffalo.py index 7e01b1d..0e7bd56 100644 --- a/tests/test_buffalo.py +++ b/tests/test_buffalo.py @@ -34,13 +34,13 @@ def test_write_ieee_group(): def test_read_ieee(): data_in = Buffalo(ieeeAddr1["hex"]) - actual = data_in.read_parameter(t.ParameterType.IEEEADDR, {}) + actual = data_in.read_parameter("test", t.ParameterType.IEEEADDR, {}) assert ieeeAddr1["string"] == actual def test_read_ieee2(): data_in = Buffalo(ieeeAddr2["hex"]) - actual = data_in.read_parameter(t.ParameterType.IEEEADDR, {}) + actual = data_in.read_parameter("test", t.ParameterType.IEEEADDR, {}) assert ieeeAddr2["string"] == actual @@ -68,5 +68,5 @@ def test_list_nighbor_lqi(): data_in = Buffalo(data_out.buffer) options = BuffaloOptions() options.length = len(value) - act = data_in.read_parameter(t.ParameterType.LIST_NEIGHBOR_LQI, options) + act = data_in.read_parameter("test", t.ParameterType.LIST_NEIGHBOR_LQI, options) assert value == act diff --git a/tests/test_types.py b/tests/test_types.py index 62263c6..10888b1 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -368,7 +368,8 @@ def test_bind_req(): b"\x01 SREQ ZDO bindReq tsn: 1 { - 'dstaddr': 0xbd8b, 'srcaddr': 00:0b:57:ff:fe:27:78:3c, 'srcendpoint': 1, 'clusterid': 8, 'dstaddrmode': 3, 'dstaddress': 00:12:4b:00:18:ed:25:0c, 'dstendpoint': 1} + 'dstaddr': 0xbd8b, 'srcaddr': 00:0b:57:ff:fe:27:78:3c, 'srcendpoint': 1, 'clusterid': 8, + 'dstaddrmode': 3, 'dstaddress': 00:12:4b:00:18:ed:25:0c, 'dstendpoint': 1} zigpy_cc.uart DEBUG Send: b"\xfe\x17%!\x8b\xbd, " "'dstaddress': 00:12:4b:00:18:ed:25:0c, " "'dstendpoint': 1}" == str(obj) ) @@ -431,7 +432,7 @@ def test_bind_req_serialize(): "srcaddr": EUI64(reversed(b"\x00\x0b\x57\xff\xfe\x27\x78\x3c")), "srcendpoint": 1, "clusterid": 8, - "dstaddrmode": 3, + "dstaddrmode": t.AddressMode.ADDR_64BIT, "dstaddress": EUI64(reversed(b"\x00\x12\x4b\x00\x18\xed\x25\x0c")), "dstendpoint": 1, } @@ -442,7 +443,7 @@ def test_bind_req_serialize(): "'srcaddr': 00:0b:57:ff:fe:27:78:3c, " "'srcendpoint': 1, " "'clusterid': 8, " - "'dstaddrmode': 3, " + "'dstaddrmode': , " "'dstaddress': 00:12:4b:00:18:ed:25:0c, " "'dstendpoint': 1}" == str(obj) ) diff --git a/zigpy_cc/buffalo.py b/zigpy_cc/buffalo.py index a5c15b5..23399ea 100644 --- a/zigpy_cc/buffalo.py +++ b/zigpy_cc/buffalo.py @@ -2,14 +2,13 @@ import zigpy.types from zigpy_cc.exception import TODO -from zigpy_cc.types import ParameterType +from zigpy_cc.types import AddressMode, ParameterType class BuffaloOptions: def __init__(self) -> None: self.startIndex = None self.length = None - self.is_address = False class Buffalo: @@ -68,12 +67,19 @@ def write_neighbor_lqi(self, value): self.write(value["depth"]) self.write(value["lqi"]) - def read_parameter(self, type, options): + def read_parameter(self, name, type, options): + if type == ParameterType.UINT8: res = self.read_int() + if name.endswith("addrmode"): + res = AddressMode(res) elif type == ParameterType.UINT16: res = self.read_int(2) - if options.is_address: + if ( + name.endswith("addr") + or name.endswith("address") + or name.endswith("addrofinterest") + ): res = zigpy.types.NWK(res) elif type == ParameterType.UINT32: res = self.read_int(4) diff --git a/zigpy_cc/types.py b/zigpy_cc/types.py index 5e04e1b..e3c43fd 100644 --- a/zigpy_cc/types.py +++ b/zigpy_cc/types.py @@ -18,6 +18,14 @@ class Timeouts: default = 10000 +class AddressMode(t.uint8_t, enum.Enum): + ADDR_NOT_PRESENT = 0 + ADDR_GROUP = 1 + ADDR_16BIT = 2 + ADDR_64BIT = 3 + ADDR_BROADCAST = 15 + + class LedMode(t.uint8_t, enum.Enum): Off = 0 On = 1 diff --git a/zigpy_cc/zigbee/application.py b/zigpy_cc/zigbee/application.py index 2b7a063..a47b821 100644 --- a/zigpy_cc/zigbee/application.py +++ b/zigpy_cc/zigbee/application.py @@ -16,7 +16,7 @@ from zigpy_cc.api import API from zigpy_cc.config import CONF_DEVICE, CONFIG_SCHEMA, SCHEMA_DEVICE from zigpy_cc.exception import TODO, CommandError -from zigpy_cc.types import NetworkOptions, Subsystem, ZnpVersion, LedMode +from zigpy_cc.types import NetworkOptions, Subsystem, ZnpVersion, LedMode, AddressMode from zigpy_cc.zigbee.start_znp import start_znp from zigpy_cc.zpi_object import ZpiObject @@ -178,7 +178,14 @@ async def mrequest( ) try: obj = ZpiObject.from_cluster( - 0, profile, cluster, src_ep, 0xFF, sequence, data, group=group_id + group_id, + profile, + cluster, + src_ep or 1, + 0xFF, + sequence, + data, + addr_mode=AddressMode.ADDR_GROUP, ) waiter_id = None waiter = self._api.create_response_waiter(obj, sequence) @@ -281,10 +288,17 @@ async def broadcast( sequence, data, radius=radius, + addr_mode=AddressMode.ADDR_16BIT, ) async with self._semaphore: await self._api.request_raw(obj) + """ + As a broadcast command is not confirmed and thus immediately returns + (contrary to network address requests) we will give the + command some time to 'settle' in the network. + """ + await asyncio.sleep(0.2) except CommandError as ex: return ( @@ -297,7 +311,7 @@ async def broadcast( async def permit_ncp(self, time_s=60): assert 0 <= time_s <= 254 payload = { - "addrmode": 0x0F, + "addrmode": AddressMode.ADDR_BROADCAST, "dstaddr": BroadcastAddress.ALL_ROUTERS_AND_COORDINATOR, "duration": time_s, "tcsignificance": 0, diff --git a/zigpy_cc/zpi_object.py b/zigpy_cc/zpi_object.py index f375a03..d69c6a6 100644 --- a/zigpy_cc/zpi_object.py +++ b/zigpy_cc/zpi_object.py @@ -4,7 +4,7 @@ from zigpy_cc import uart from zigpy_cc.buffalo import Buffalo, BuffaloOptions from zigpy_cc.definition import Definition -from zigpy_cc.types import CommandType, ParameterType, Subsystem +from zigpy_cc.types import CommandType, ParameterType, Subsystem, AddressMode BufferAndListTypes = [ ParameterType.BUFFER, @@ -30,7 +30,7 @@ def __init__( command_type, subsystem, command: str, - commandId, + command_id, payload, parameters, sequence=None, @@ -38,7 +38,7 @@ def __init__( self.command_type = CommandType(command_type) self.subsystem = Subsystem(subsystem) self.command = command - self.command_id = commandId + self.command_id = command_id self.payload = payload self.parameters = parameters self.sequence = sequence @@ -101,11 +101,11 @@ def from_cluster( data, *, radius=30, - group=None + addr_mode=None ): if profile == zha.PROFILE_ID: subsystem = Subsystem.AF - if group is None: + if addr_mode is None: cmd = next(c for c in Definition[subsystem] if c["ID"] == 1) else: cmd = next(c for c in Definition[subsystem] if c["ID"] == 2) @@ -131,12 +131,11 @@ def from_cluster( } elif name == "dataRequestExt": payload = { - # 1: group - "dstaddrmode": 1, - "dstaddr": group, - "destendpoint": 0xFF, + "dstaddrmode": addr_mode, + "dstaddr": nwk, + "destendpoint": dst_ep, "dstpanid": 0, - "srcendpoint": src_ep or 1, + "srcendpoint": src_ep, "clusterid": cluster, "transid": sequence, "options": 0, @@ -146,7 +145,9 @@ def from_cluster( } elif name == "mgmtPermitJoinReq": addrmode = ( - 0x0F if nwk == BroadcastAddress.ALL_ROUTERS_AND_COORDINATOR else 0x02 + AddressMode.ADDR_BROADCAST + if nwk == BroadcastAddress.ALL_ROUTERS_AND_COORDINATOR + else AddressMode.ADDR_16BIT ) payload = cls.read_parameters( bytes([addrmode]) + nwk.to_bytes(2, "little") + data[1:], parameters @@ -168,29 +169,24 @@ def read_parameters(cls, data: bytes, parameters): buffalo = Buffalo(data) res = {} length = None - startIndex = None + start_index = None for p in parameters: options = BuffaloOptions() name = p["name"] - if ( - name.endswith("addr") - or name.endswith("address") - or name.endswith("addrofinterest") - ): - options.is_address = True - type = p["parameterType"] - if type in BufferAndListTypes: + param_type = p["parameterType"] + if param_type in BufferAndListTypes: if isinstance(length, int): options.length = length - if type == ParameterType.LIST_ASSOC_DEV: - if isinstance(startIndex, int): - options.startIndex = startIndex + if param_type == ParameterType.LIST_ASSOC_DEV: + if isinstance(start_index, int): + options.startIndex = start_index + + res[name] = buffalo.read_parameter(name, param_type, options) - res[name] = buffalo.read_parameter(type, options) - # For LIST_ASSOC_DEV, we need to grab the startindex which is + # For LIST_ASSOC_DEV, we need to grab the start_index which is # right before the length - startIndex = length + start_index = length # When reading a buffer, assume that the previous parsed parameter # contains the length of the buffer length = res[name] From 60b10803bde60733fed9c90e6a3e7e17bd492cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A1ndor=20Bal=C3=A1zs?= Date: Thu, 13 Aug 2020 20:39:16 +0200 Subject: [PATCH 4/4] ignore resetInd, do not log waiting for... --- zigpy_cc/api.py | 6 ------ zigpy_cc/types.py | 2 +- zigpy_cc/zigbee/application.py | 1 + 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/zigpy_cc/api.py b/zigpy_cc/api.py index 857cf47..e0944e3 100644 --- a/zigpy_cc/api.py +++ b/zigpy_cc/api.py @@ -171,12 +171,6 @@ async def _request_raw(self, obj: ZpiObject, waiter_id=None, expected_status=Non raise Exception("Unknown type '{}'".format(obj.command_type)) def create_response_waiter(self, obj: ZpiObject, sequence=None): - waiter = self.get_response_waiter(obj, sequence) - if waiter: - LOGGER.debug("waiting for %d %s", sequence, obj.command) - return waiter - - def get_response_waiter(self, obj: ZpiObject, sequence=None): if obj.command_type == CommandType.SREQ and obj.command.startswith( "dataRequest" ): diff --git a/zigpy_cc/types.py b/zigpy_cc/types.py index e3c43fd..c3b56c8 100644 --- a/zigpy_cc/types.py +++ b/zigpy_cc/types.py @@ -114,5 +114,5 @@ def __init__(self, config: zigpy.config.SCHEMA_NETWORK) -> None: t.ExtendedPanId([0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD]) ) self.channelList = ( - config[zigpy.config.CONF_NWK_CHANNELS] or t.Channels.CHANNEL_11 + config[zigpy.config.CONF_NWK_CHANNELS] or t.Channels.from_channel_list([11]) ) diff --git a/zigpy_cc/zigbee/application.py b/zigpy_cc/zigbee/application.py index a47b821..d001efe 100644 --- a/zigpy_cc/zigbee/application.py +++ b/zigpy_cc/zigbee/application.py @@ -44,6 +44,7 @@ "bdbComissioningNotifcation", "dataConfirm", "leaveInd", + "resetInd", "srcRtgInd", "stateChangeInd", "tcDeviceInd",