Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

0.10.0 Release #132

Merged
merged 3 commits into from
Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=[
Expand Down
4 changes: 2 additions & 2 deletions zigpy_zigate/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
MAJOR_VERSION = 0
MINOR_VERSION = 9
PATCH_VERSION = '2'
MINOR_VERSION = 10
PATCH_VERSION = '0'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
7 changes: 5 additions & 2 deletions zigpy_zigate/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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])
Expand Down
55 changes: 46 additions & 9 deletions zigpy_zigate/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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),
Expand All @@ -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 = [
Expand Down
53 changes: 19 additions & 34 deletions zigpy_zigate/uart.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
117 changes: 64 additions & 53 deletions zigpy_zigate/zigbee/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -228,53 +232,60 @@ 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:
# v = await asyncio.wait_for(send_fut, 120)
# 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
status, lqi = await self._api.permit_join(time_s)
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):
Expand Down