From 308b64e47376dcb53363a7f6b21c4f3c3bdfafa6 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 3 Oct 2022 18:21:32 -0400 Subject: [PATCH 1/2] Implement new zigpy packet API (#131) * Use zigpy for making serial connections * Implement new zigpy packet API * Bump minimum required zigpy version * Implement `reset_network_info` * Drop 3.7 from CI Drop 3.7 from CI --- .github/workflows/publish-to-pypi.yml | 4 +- .github/workflows/tests.yml | 2 +- setup.py | 2 +- zigpy_zigate/api.py | 7 +- zigpy_zigate/types.py | 55 ++++++++++-- zigpy_zigate/uart.py | 53 +++++------- zigpy_zigate/zigbee/application.py | 117 ++++++++++++++------------ 7 files changed, 138 insertions(+), 102 deletions(-) diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index c37162d..49a989d 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -10,10 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@master - - name: Set up Python 3.7 + - name: Set up Python 3.8 uses: actions/setup-python@v1 with: - version: 3.7 + version: 3.8 - name: Install wheel run: >- pip install wheel diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fb07389..3ef8906 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 diff --git a/setup.py b/setup.py index 81a31e8..7018239 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ 'pyserial-asyncio>=0.5; platform_system!="Windows"', 'pyserial-asyncio!=0.5; platform_system=="Windows"', # 0.5 broke writes 'pyusb>=1.1.0', - 'zigpy>=0.47.0', + 'zigpy>=0.51.0', 'gpiozero', ], tests_require=[ diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index 7a70b2a..3f0aacc 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -67,6 +67,10 @@ class ResponseId(enum.IntEnum): EXTENDED_ERROR = 0x9999 +class SendSecurity(t.uint8_t, enum.Enum): + NETWORK = 0x00 + APPLINK = 0x01 + TEMP_APPLINK = 0x02 class NonFactoryNewRestartStatus(t.uint8_t, enum.Enum): @@ -482,11 +486,10 @@ async def remove_device(self, zigate_ieee, ieee): return await self.command(CommandId.NETWORK_REMOVE_DEVICE, data) async def raw_aps_data_request(self, addr, src_ep, dst_ep, profile, - cluster, payload, addr_mode=2, security=0): + cluster, payload, addr_mode=t.AddressMode.NWK, security=SendSecurity.NETWORK, radius=0): ''' Send raw APS Data request ''' - radius = 0 data = t.serialize([addr_mode, addr, src_ep, dst_ep, cluster, profile, security, radius, payload], COMMANDS[CommandId.SEND_RAW_APS_DATA_PACKET]) diff --git a/zigpy_zigate/types.py b/zigpy_zigate/types.py index 5f49502..2a94f2f 100644 --- a/zigpy_zigate/types.py +++ b/zigpy_zigate/types.py @@ -144,9 +144,20 @@ def __str__(self): class AddressMode(uint8_t, enum.Enum): # Address modes used in zigate protocol + BOUND = 0x00 GROUP = 0x01 NWK = 0x02 IEEE = 0x03 + BROADCAST = 0x04 + + NO_TRANSMIT = 0x05 + + BOUND_NO_ACK = 0x06 + NWK_NO_ACK = 0x07 + IEEE_NO_ACK = 0x08 + + BOUND_NON_BLOCKING = 0x09 + BOUND_NON_BLOCKING_NO_ACK = 0x0A class Status(uint8_t, enum.Enum): @@ -259,6 +270,26 @@ def __repr__(self): return r +ZIGPY_TO_ZIGATE_ADDR_MODE = { + # With ACKs + (zigpy.types.AddrMode.NWK, True): AddressMode.NWK, + (zigpy.types.AddrMode.IEEE, True): AddressMode.IEEE, + (zigpy.types.AddrMode.Broadcast, True): AddressMode.BROADCAST, + (zigpy.types.AddrMode.Group, True): AddressMode.GROUP, + + # Without ACKs + (zigpy.types.AddrMode.NWK, False): AddressMode.NWK_NO_ACK, + (zigpy.types.AddrMode.IEEE, False): AddressMode.IEEE_NO_ACK, + (zigpy.types.AddrMode.Broadcast, False): AddressMode.BROADCAST, + (zigpy.types.AddrMode.Group, False): AddressMode.GROUP, +} + +ZIGATE_TO_ZIGPY_ADDR_MODE = { + zigate_addr: (zigpy_addr, ack) + for (zigpy_addr, ack), zigate_addr in ZIGPY_TO_ZIGATE_ADDR_MODE.items() +} + + class Address(Struct): _fields = [ ('address_mode', AddressMode), @@ -271,17 +302,23 @@ def __eq__(self, other): @classmethod def deserialize(cls, data): r = cls() - field_name, field_type = cls._fields[0] - mode, data = field_type.deserialize(data) - setattr(r, field_name, mode) - v = None - if mode in [AddressMode.GROUP, AddressMode.NWK]: - v, data = NWK.deserialize(data) - elif mode == AddressMode.IEEE: - v, data = EUI64.deserialize(data) - setattr(r, cls._fields[1][0], v) + r.address_mode, data = AddressMode.deserialize(data) + + if r.address_mode in (AddressMode.IEEE, AddressMode.IEEE_NO_ACK): + r.address, data = EUI64.deserialize(data) + else: + r.address, data = NWK.deserialize(data) + return r, data + def to_zigpy_type(self): + zigpy_addr_mode, ack = ZIGATE_TO_ZIGPY_ADDR_MODE[self.address_mode] + + return ( + zigpy.types.AddrModeAddress(addr_mode=zigpy_addr_mode, address=self.address), + ack, + ) + class DeviceEntry(Struct): _fields = [ diff --git a/zigpy_zigate/uart.py b/zigpy_zigate/uart.py index cc69936..0bcd2f1 100644 --- a/zigpy_zigate/uart.py +++ b/zigpy_zigate/uart.py @@ -4,9 +4,7 @@ import struct from typing import Any, Dict -import serial # noqa -import serial.tools.list_ports -import serial_asyncio +import zigpy.serial from .config import CONF_DEVICE_PATH from . import common as c @@ -141,38 +139,25 @@ async def connect(device_config: Dict[str, Any], api, loop=None): if port == 'auto': port = c.discover_port() - if c.is_zigate_wifi(port): + if c.is_pizigate(port): + LOGGER.debug('PiZiGate detected') + await c.async_set_pizigate_running_mode() + # in case of pizigate:/dev/ttyAMA0 syntax + if port.startswith('pizigate:'): + port = port.replace('pizigate:', '', 1) + elif c.is_zigate_din(port): + LOGGER.debug('ZiGate USB DIN detected') + await c.async_set_zigatedin_running_mode() + elif c.is_zigate_wifi(port): LOGGER.debug('ZiGate WiFi detected') - port = port.split('socket://', 1)[1] - if ':' in port: - host, port = port.split(':', 1) # 192.168.x.y:9999 - port = int(port) - else: - host = port - port = 9999 - _, protocol = await loop.create_connection( - lambda: protocol, - host, port) - else: - if c.is_pizigate(port): - LOGGER.debug('PiZiGate detected') - await c.async_set_pizigate_running_mode() - # in case of pizigate:/dev/ttyAMA0 syntax - if port.startswith('pizigate:'): - port = port[9:] - elif c.is_zigate_din(port): - LOGGER.debug('ZiGate USB DIN detected') - await c.async_set_zigatedin_running_mode() - - _, protocol = await serial_asyncio.create_serial_connection( - loop, - lambda: protocol, - url=port, - baudrate=ZIGATE_BAUDRATE, - parity=serial.PARITY_NONE, - stopbits=serial.STOPBITS_ONE, - xonxoff=False, - ) + + _, protocol = await zigpy.serial.create_serial_connection( + loop, + lambda: protocol, + url=port, + baudrate=ZIGATE_BAUDRATE, + xonxoff=False, + ) await connected_future diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index b0249c6..c19c477 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -115,11 +115,13 @@ async def load_network_info(self, *, load_devices: bool = False): self.state.network_info.children.append(ieee) self.state.network_info.nwk_addresses[ieee] = zigpy.types.NWK(device.short_addr) + async def reset_network_info(self): + await self._api.erase_persistent_data() + async def write_network_info(self, *, network_info, node_info): LOGGER.warning('Setting the pan_id is not supported by ZiGate') - await self._api.erase_persistent_data() - + await self.reset_network_info() await self._api.set_channel(network_info.channel) epid, _ = zigpy.types.uint64_t.deserialize(network_info.extended_pan_id.serialize()) @@ -179,28 +181,30 @@ def zigate_callback_handler(self, msg, response, lqi): # LOGGER.debug('Start pairing {} (1st device announce)'.format(nwk)) # self._pending_join.append(nwk) elif msg == ResponseId.DATA_INDICATION: - if response[1] == 0x0 and response[2] == 0x13: - nwk = zigpy.types.NWK(response[5].address) - ieee = zigpy.types.EUI64(response[7][3:11]) - parent_nwk = 0 - self.handle_join(nwk, ieee, parent_nwk) - return - try: - if response[5].address_mode == t.AddressMode.NWK: - device = self.get_device(nwk = zigpy.types.NWK(response[5].address)) - elif response[5].address_mode == t.AddressMode.IEEE: - device = self.get_device(ieee=zigpy.types.EUI64(response[5].address)) - else: - LOGGER.error("No such device %s", response[5].address) - return - except KeyError: - LOGGER.debug("No such device %s", response[5].address) - return - rssi = 0 - device.radio_details(lqi, rssi) - self.handle_message(device, response[1], - response[2], - response[3], response[4], response[-1]) + ( + status, + profile_id, + cluster_id, + src_ep, + dst_ep, + src, + dst, + payload, + ) = response + + packet = zigpy.types.ZigbeePacket( + src=src.to_zigpy_type()[0], + src_ep=src_ep, + dst=dst.to_zigpy_type()[0], + dst_ep=dst_ep, + profile_id=profile_id, + cluster_id=cluster_id, + data=zigpy.types.SerializableBytes(payload), + lqi=lqi, + rssi=None, + ) + + self.packet_received(packet) elif msg == ResponseId.ACK_DATA: LOGGER.debug('ACK Data received %s %s', response[4], response[0]) # disabled because of https://github.com/fairecasoimeme/ZiGate/issues/324 @@ -228,32 +232,44 @@ def _handle_frame_failure(self, message_tag, status): except asyncio.futures.InvalidStateError as exc: LOGGER.debug("Invalid state on future - probably duplicate response: %s", exc) - @zigpy.util.retryable_request - async def request(self, device, profile, cluster, src_ep, dst_ep, sequence, data, - expect_reply=True, use_ieee=False): - return await self._request(device.nwk, profile, cluster, src_ep, dst_ep, sequence, data, - expect_reply, use_ieee) - - async def mrequest(self, group_id, profile, cluster, src_ep, sequence, data, *, hops=0, non_member_radius=3): - src_ep = 1 - return await self._request(group_id, profile, cluster, src_ep, src_ep, sequence, data, addr_mode=1) - - async def _request(self, nwk, profile, cluster, src_ep, dst_ep, sequence, data, - expect_reply=True, use_ieee=False, addr_mode=2): - src_ep = 1 if dst_ep else 0 # ZiGate only support endpoint 1 - LOGGER.debug('request %s', - (nwk, profile, cluster, src_ep, dst_ep, sequence, data, expect_reply, use_ieee)) + async def send_packet(self, packet): + LOGGER.debug("Sending packet %r", packet) + + if packet.dst.addr_mode == zigpy.types.AddrMode.IEEE: + LOGGER.warning("IEEE addressing is not supported, falling back to NWK") + + try: + device = self.get_device_with_address(packet.dst) + except (KeyError, ValueError): + raise ValueError(f"Cannot find device with IEEE {packet.dst.address}") + + packet = packet.replace( + dst=zigpy.types.AddrModeAddress( + addr_mode=zigpy.types.AddrMode.NWK, address=device.nwk + ) + ) + + ack = (zigpy.types.TransmitOptions.ACK in packet.tx_options) + try: - v, lqi = await self._api.raw_aps_data_request(nwk, src_ep, dst_ep, profile, cluster, data, addr_mode) + (status, tsn, packet_type, _), _ = await self._api.raw_aps_data_request( + addr=packet.dst.address, + src_ep=(1 if packet.dst_ep > 0 else 0), # ZiGate only support endpoint 1 + dst_ep=packet.dst_ep, + profile=packet.profile_id, + cluster=packet.cluster_id, + payload=packet.data.serialize(), + addr_mode=t.ZIGPY_TO_ZIGATE_ADDR_MODE[packet.dst.addr_mode, ack], + radius=packet.radius, + ) except NoResponseError: - return 1, "ZiGate doesn't answer to command" - req_id = v[1] - send_fut = asyncio.Future() - self._pending[req_id] = send_fut + raise zigpy.exceptions.DeliveryError("ZiGate did not respond to command") - if v[0] != t.Status.Success: - self._pending.pop(req_id) - return v[0], "Message send failure {}".format(v[0]) + if status != t.Status.Success: + self._pending.pop(tsn) + raise zigpy.exceptions.DeliveryError(f"Failed to deliver packet: {status}", status=status) + + self._pending[tsn] = asyncio.get_running_loop().create_future() # disabled because of https://github.com/fairecasoimeme/ZiGate/issues/324 # try: @@ -261,9 +277,8 @@ async def _request(self, nwk, profile, cluster, src_ep, dst_ep, sequence, data, # except asyncio.TimeoutError: # return 1, "timeout waiting for message %s send ACK" % (sequence, ) # finally: - # self._pending.pop(req_id) + # self._pending.pop(tsn) # return v, "Message sent" - return 0, "Message sent" async def permit_ncp(self, time_s=60): assert 0 <= time_s <= 254 @@ -271,10 +286,6 @@ async def permit_ncp(self, time_s=60): if status[0] != t.Status.Success: await self._api.reset() - async def broadcast(self, profile, cluster, src_ep, dst_ep, grpid, radius, - sequence, data, broadcast_address): - LOGGER.debug("Broadcast not implemented.") - class ZiGateDevice(zigpy.device.Device): def __init__(self, application, ieee, nwk): From cbf895079ff192410c53a49745b9e6b04aff08e0 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 3 Oct 2022 18:23:05 -0400 Subject: [PATCH 2/2] 0.10.0 version bump --- zigpy_zigate/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zigpy_zigate/__init__.py b/zigpy_zigate/__init__.py index 12cae53..5131efb 100644 --- a/zigpy_zigate/__init__.py +++ b/zigpy_zigate/__init__.py @@ -1,5 +1,5 @@ MAJOR_VERSION = 0 -MINOR_VERSION = 9 +MINOR_VERSION = 10 PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)