From e3f97063df1dde660a1c43c9c7cb92988c80bba1 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 5 Sep 2020 19:33:14 -0400 Subject: [PATCH 01/63] 0.6.3.dev0 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 97ab9b6..0df6337 100644 --- a/zigpy_zigate/__init__.py +++ b/zigpy_zigate/__init__.py @@ -1,5 +1,5 @@ MAJOR_VERSION = 0 MINOR_VERSION = 6 -PATCH_VERSION = '1' +PATCH_VERSION = '3.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 87d2edb7dd86d54dcf02145dec387412bcd7aee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sat, 17 Oct 2020 21:43:52 +0200 Subject: [PATCH 02/63] Add data ack support --- zigpy_zigate/__init__.py | 4 ++-- zigpy_zigate/api.py | 1 + zigpy_zigate/zigbee/application.py | 20 +++++++++----------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/zigpy_zigate/__init__.py b/zigpy_zigate/__init__.py index 0df6337..a051fcc 100644 --- a/zigpy_zigate/__init__.py +++ b/zigpy_zigate/__init__.py @@ -1,5 +1,5 @@ MAJOR_VERSION = 0 -MINOR_VERSION = 6 -PATCH_VERSION = '3.dev0' +MINOR_VERSION = 7 +PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index e092100..def7a60 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -23,6 +23,7 @@ t.Address, t.Address, t.Bytes), 0x8009: (t.NWK, t.EUI64, t.uint16_t, t.uint64_t, t.uint8_t), 0x8010: (t.uint16_t, t.uint16_t), + 0x8011: (t.uint8_t, t.NWK, t.uint8_t, t.uint16_t, t.uint8_t), 0x8024: (t.uint8_t, t.NWK, t.EUI64, t.uint8_t), 0x8048: (t.EUI64, t.uint8_t), 0x8701: (t.uint8_t, t.uint8_t), diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index 09fed65..e27d2e0 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -117,6 +117,8 @@ def zigate_callback_handler(self, msg, response, lqi): self.handle_message(device, response[1], response[2], response[3], response[4], response[-1]) + elif msg == 0x8011: # ACK Data + self._handle_frame_failure(response[4], response[0]) elif msg == 0x8702: # APS Data confirm Fail self._handle_frame_failure(response[4], response[0]) @@ -147,17 +149,13 @@ async def request(self, device, profile, cluster, src_ep, dst_ep, sequence, data self._pending.pop(req_id) return v[0], "Message send failure {}".format(v[0]) - # Commented out for now - # Currently (Firmware 3.1a) only send APS Data confirm in case of failure - # https://github.com/fairecasoimeme/ZiGate/issues/239 -# 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) -# return v, "Message sent" - return 0, "Message sent" + 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) + return v, "Message sent" async def permit_ncp(self, time_s=60): assert 0 <= time_s <= 254 From a0529d70458244ff52ffd0b7d132b1f9b057b4a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sun, 18 Oct 2020 15:05:39 +0200 Subject: [PATCH 03/63] add support for 0x8035 and disable ack --- zigpy_zigate/api.py | 3 +- zigpy_zigate/zigbee/application.py | 55 ++++++++++++++++++++++++------ 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index def7a60..bf0b36a 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -25,6 +25,7 @@ 0x8010: (t.uint16_t, t.uint16_t), 0x8011: (t.uint8_t, t.NWK, t.uint8_t, t.uint16_t, t.uint8_t), 0x8024: (t.uint8_t, t.NWK, t.EUI64, t.uint8_t), + 0x8035: (t.uint8_t, t.uint32_t), 0x8048: (t.EUI64, t.uint8_t), 0x8701: (t.uint8_t, t.uint8_t), 0x8702: (t.uint8_t, t.uint8_t, t.uint8_t, t.Address, t.uint8_t), @@ -77,7 +78,7 @@ def data_received(self, cmd, data, lqi): LOGGER.debug("data received %s %s LQI:%s", hex(cmd), binascii.hexlify(data), lqi) if cmd not in RESPONSES: - LOGGER.error('Received unhandled response %s', hex(cmd)) + LOGGER.error('Received unhandled response 0x{:04x}'.format(cmd)) return data, rest = t.deserialize(data, RESPONSES[cmd]) if cmd == 0x8000: diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index e27d2e0..486e79d 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -1,6 +1,7 @@ import asyncio import logging from typing import Any, Dict, Optional +import enum import zigpy.application import zigpy.config @@ -15,6 +16,30 @@ LOGGER = logging.getLogger(__name__) +class AutoEnum(enum.IntEnum): + def _generate_next_value_(name, start, count, last_values): + return count + + +class PDM_EVENT(AutoEnum): + E_PDM_SYSTEM_EVENT_WEAR_COUNT_TRIGGER_VALUE_REACHED = enum.auto() + E_PDM_SYSTEM_EVENT_DESCRIPTOR_SAVE_FAILED = enum.auto() + E_PDM_SYSTEM_EVENT_PDM_NOT_ENOUGH_SPACE = enum.auto() + E_PDM_SYSTEM_EVENT_LARGEST_RECORD_FULL_SAVE_NO_LONGER_POSSIBLE = enum.auto() + E_PDM_SYSTEM_EVENT_SEGMENT_DATA_CHECKSUM_FAIL = enum.auto() + E_PDM_SYSTEM_EVENT_SEGMENT_SAVE_OK = enum.auto() + E_PDM_SYSTEM_EVENT_EEPROM_SEGMENT_HEADER_REPAIRED = enum.auto() + E_PDM_SYSTEM_EVENT_SYSTEM_INTERNAL_BUFFER_WEAR_COUNT_SWAP = enum.auto() + E_PDM_SYSTEM_EVENT_SYSTEM_DUPLICATE_FILE_SEGMENT_DETECTED = enum.auto() + E_PDM_SYSTEM_EVENT_SYSTEM_ERROR = enum.auto() + E_PDM_SYSTEM_EVENT_SEGMENT_PREWRITE = enum.auto() + E_PDM_SYSTEM_EVENT_SEGMENT_POSTWRITE = enum.auto() + E_PDM_SYSTEM_EVENT_SEQUENCE_DUPLICATE_DETECTED = enum.auto() + E_PDM_SYSTEM_EVENT_SEQUENCE_VERIFY_FAIL = enum.auto() + E_PDM_SYSTEM_EVENT_PDM_SMART_SAVE = enum.auto() + E_PDM_SYSTEM_EVENT_PDM_FULL_SAVE = enum.auto() + + class ControllerApplication(zigpy.application.ControllerApplication): SCHEMA = CONFIG_SCHEMA SCHEMA_DEVICE = SCHEMA_DEVICE @@ -118,8 +143,16 @@ def zigate_callback_handler(self, msg, response, lqi): response[2], response[3], response[4], response[-1]) elif msg == 0x8011: # ACK Data + LOGGER.debug('ACK Data received %s %s', response[4], response[0]) self._handle_frame_failure(response[4], response[0]) + elif msg == 0x8035: # PDM Event + try: + event = PDM_EVENT(response[0]).name + except: + event = 'Unknown event' + LOGGER.debug('PDM Event %s %s, record %s', response[0], event, response[1]) elif msg == 0x8702: # APS Data confirm Fail + LOGGER.debug('APS Data confirm Fail %s %s', response[4], response[0]) self._handle_frame_failure(response[4], response[0]) def _handle_frame_failure(self, message_tag, status): @@ -137,25 +170,27 @@ async def request(self, device, profile, cluster, src_ep, dst_ep, sequence, data src_ep = 1 if dst_ep else 0 # ZiGate only support endpoint 1 LOGGER.debug('request %s', (device.nwk, profile, cluster, src_ep, dst_ep, sequence, data, expect_reply, use_ieee)) - req_id = self.get_sequence() - send_fut = asyncio.Future() - self._pending[req_id] = send_fut try: v, lqi = await self._api.raw_aps_data_request(device.nwk, src_ep, dst_ep, profile, cluster, data) except NoResponseError: return 1, "ZiGate doesn't answer to command" + req_id = v[1] + send_fut = asyncio.Future() + self._pending[req_id] = send_fut if v[0] != 0: self._pending.pop(req_id) return v[0], "Message send failure {}".format(v[0]) - 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) - return v, "Message sent" + # 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) + # return v, "Message sent" + return 0, "Message sent" async def permit_ncp(self, time_s=60): assert 0 <= time_s <= 254 From fb34d00c06b54fecee8dad05298a871238d724e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sun, 18 Oct 2020 20:47:55 +0200 Subject: [PATCH 04/63] move PDM_EVENT to api --- zigpy_zigate/api.py | 24 ++++++++++++++++++++++++ zigpy_zigate/zigbee/application.py | 28 ++-------------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index bf0b36a..822bd89 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -2,6 +2,7 @@ import binascii import functools import logging +import enum from typing import Any, Dict import serial @@ -40,6 +41,29 @@ 0x0530: (t.uint8_t, t.NWK, t.uint8_t, t.uint8_t, t.uint16_t, t.uint16_t, t.uint8_t, t.uint8_t, t.LBytes), } +class AutoEnum(enum.IntEnum): + def _generate_next_value_(name, start, count, last_values): + return count + + +class PDM_EVENT(AutoEnum): + E_PDM_SYSTEM_EVENT_WEAR_COUNT_TRIGGER_VALUE_REACHED = enum.auto() + E_PDM_SYSTEM_EVENT_DESCRIPTOR_SAVE_FAILED = enum.auto() + E_PDM_SYSTEM_EVENT_PDM_NOT_ENOUGH_SPACE = enum.auto() + E_PDM_SYSTEM_EVENT_LARGEST_RECORD_FULL_SAVE_NO_LONGER_POSSIBLE = enum.auto() + E_PDM_SYSTEM_EVENT_SEGMENT_DATA_CHECKSUM_FAIL = enum.auto() + E_PDM_SYSTEM_EVENT_SEGMENT_SAVE_OK = enum.auto() + E_PDM_SYSTEM_EVENT_EEPROM_SEGMENT_HEADER_REPAIRED = enum.auto() + E_PDM_SYSTEM_EVENT_SYSTEM_INTERNAL_BUFFER_WEAR_COUNT_SWAP = enum.auto() + E_PDM_SYSTEM_EVENT_SYSTEM_DUPLICATE_FILE_SEGMENT_DETECTED = enum.auto() + E_PDM_SYSTEM_EVENT_SYSTEM_ERROR = enum.auto() + E_PDM_SYSTEM_EVENT_SEGMENT_PREWRITE = enum.auto() + E_PDM_SYSTEM_EVENT_SEGMENT_POSTWRITE = enum.auto() + E_PDM_SYSTEM_EVENT_SEQUENCE_DUPLICATE_DETECTED = enum.auto() + E_PDM_SYSTEM_EVENT_SEQUENCE_VERIFY_FAIL = enum.auto() + E_PDM_SYSTEM_EVENT_PDM_SMART_SAVE = enum.auto() + E_PDM_SYSTEM_EVENT_PDM_FULL_SAVE = enum.auto() + class NoResponseError(zigpy.exceptions.APIException): pass diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index 486e79d..6501b53 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -10,36 +10,12 @@ import zigpy.util from zigpy_zigate import types as t -from zigpy_zigate.api import NoResponseError, ZiGate +from zigpy_zigate.api import NoResponseError, ZiGate, PDM_EVENT from zigpy_zigate.config import CONF_DEVICE, CONFIG_SCHEMA, SCHEMA_DEVICE LOGGER = logging.getLogger(__name__) -class AutoEnum(enum.IntEnum): - def _generate_next_value_(name, start, count, last_values): - return count - - -class PDM_EVENT(AutoEnum): - E_PDM_SYSTEM_EVENT_WEAR_COUNT_TRIGGER_VALUE_REACHED = enum.auto() - E_PDM_SYSTEM_EVENT_DESCRIPTOR_SAVE_FAILED = enum.auto() - E_PDM_SYSTEM_EVENT_PDM_NOT_ENOUGH_SPACE = enum.auto() - E_PDM_SYSTEM_EVENT_LARGEST_RECORD_FULL_SAVE_NO_LONGER_POSSIBLE = enum.auto() - E_PDM_SYSTEM_EVENT_SEGMENT_DATA_CHECKSUM_FAIL = enum.auto() - E_PDM_SYSTEM_EVENT_SEGMENT_SAVE_OK = enum.auto() - E_PDM_SYSTEM_EVENT_EEPROM_SEGMENT_HEADER_REPAIRED = enum.auto() - E_PDM_SYSTEM_EVENT_SYSTEM_INTERNAL_BUFFER_WEAR_COUNT_SWAP = enum.auto() - E_PDM_SYSTEM_EVENT_SYSTEM_DUPLICATE_FILE_SEGMENT_DETECTED = enum.auto() - E_PDM_SYSTEM_EVENT_SYSTEM_ERROR = enum.auto() - E_PDM_SYSTEM_EVENT_SEGMENT_PREWRITE = enum.auto() - E_PDM_SYSTEM_EVENT_SEGMENT_POSTWRITE = enum.auto() - E_PDM_SYSTEM_EVENT_SEQUENCE_DUPLICATE_DETECTED = enum.auto() - E_PDM_SYSTEM_EVENT_SEQUENCE_VERIFY_FAIL = enum.auto() - E_PDM_SYSTEM_EVENT_PDM_SMART_SAVE = enum.auto() - E_PDM_SYSTEM_EVENT_PDM_FULL_SAVE = enum.auto() - - class ControllerApplication(zigpy.application.ControllerApplication): SCHEMA = CONFIG_SCHEMA SCHEMA_DEVICE = SCHEMA_DEVICE @@ -148,7 +124,7 @@ def zigate_callback_handler(self, msg, response, lqi): elif msg == 0x8035: # PDM Event try: event = PDM_EVENT(response[0]).name - except: + except ValueError: event = 'Unknown event' LOGGER.debug('PDM Event %s %s, record %s', response[0], event, response[1]) elif msg == 0x8702: # APS Data confirm Fail From 42145fac6acb61b2e70bda390de83792548b8606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sun, 18 Oct 2020 20:59:44 +0200 Subject: [PATCH 05/63] fix lint --- zigpy_zigate/api.py | 1 + zigpy_zigate/zigbee/application.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index 822bd89..4adcec4 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -41,6 +41,7 @@ 0x0530: (t.uint8_t, t.NWK, t.uint8_t, t.uint8_t, t.uint16_t, t.uint16_t, t.uint8_t, t.uint8_t, t.LBytes), } + class AutoEnum(enum.IntEnum): def _generate_next_value_(name, start, count, last_values): return count diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index 6501b53..f5fbb64 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -1,7 +1,6 @@ import asyncio import logging from typing import Any, Dict, Optional -import enum import zigpy.application import zigpy.config From 4ed9f12f0704b7bf5463bad2f5ef9b1aa96c9592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 2 Nov 2020 21:13:32 +0100 Subject: [PATCH 06/63] add zigate din support --- setup.py | 1 + zigpy_zigate/uart.py | 78 ++++++++++++++++++++++++++++++ zigpy_zigate/zigbee/application.py | 10 +++- 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 1ece6a4..844d1a9 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ def is_raspberry_pi(raise_on_errors=False): requires = [ 'pyserial-asyncio', + 'pyusb', 'zigpy>=0.22.2', ] diff --git a/zigpy_zigate/uart.py b/zigpy_zigate/uart.py index 5ee9970..5c47da8 100644 --- a/zigpy_zigate/uart.py +++ b/zigpy_zigate/uart.py @@ -2,11 +2,13 @@ import binascii import logging import struct +import re from typing import Any, Dict import serial # noqa import serial.tools.list_ports import serial_asyncio +import usb from zigpy_zigate.config import CONF_DEVICE_PATH @@ -137,6 +139,14 @@ async def connect(device_config: Dict[str, Any], api, loop=None): LOGGER.error('Unable to find ZiGate using auto mode') raise serial.SerialException("Unable to find Zigate using auto mode") + if re.match(r"/dev/(tty(S|AMA)|serial)\d+", port): + # Suppose pizigate on /dev/ttyAMAx or /dev/serialx + await set_pizigate_running_mode() + if re.match(r"/dev/ttyUSB\d+", port): + device = next(serial.tools.list_ports.grep(port)) + if device.manufacturer == 'FTDI': # Suppose zigate din /dev/ttyUSBx + await set_zigatedin_running_mode() + _, protocol = await serial_asyncio.create_serial_connection( loop, lambda: protocol, @@ -155,6 +165,7 @@ async def connect(device_config: Dict[str, Any], api, loop=None): async def set_pizigate_running_mode(): try: import RPi.GPIO as GPIO + LOGGER.info('Put PiZiGate in running mode') GPIO.setmode(GPIO.BCM) GPIO.setup(27, GPIO.OUT) # GPIO2 GPIO.output(27, GPIO.HIGH) # GPIO2 @@ -165,3 +176,70 @@ async def set_pizigate_running_mode(): except Exception as e: LOGGER.error('Unable to set PiZiGate GPIO, please check configuration') LOGGER.error(str(e)) + + +async def set_pizigate_flashing_mode(): + try: + import RPi.GPIO as GPIO + LOGGER.info('Put PiZiGate in flashing mode') + GPIO.setmode(GPIO.BCM) + GPIO.setup(27, GPIO.OUT) # GPIO2 + GPIO.output(27, GPIO.LOW) # GPIO2 + GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # GPIO0 + await asyncio.sleep(0.5) + GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP) # GPIO0 + await asyncio.sleep(0.5) + except Exception as e: + LOGGER.error('Unable to set PiZiGate GPIO, please check configuration') + LOGGER.error(str(e)) + + +def ftdi_set_bitmode(dev, bitmask): + ''' + Set mode for ZiGate DIN module + ''' + BITMODE_CBUS = 0x20 + SIO_SET_BITMODE_REQUEST = 0x0b + bmRequestType = usb.util.build_request_type(usb.util.CTRL_OUT, + usb.util.CTRL_TYPE_VENDOR, + usb.util.CTRL_RECIPIENT_DEVICE) + wValue = bitmask | (BITMODE_CBUS << BITMODE_CBUS) + dev.ctrl_transfer(bmRequestType, SIO_SET_BITMODE_REQUEST, wValue) + + +async def set_zigatedin_running_mode(): + try: + dev = usb.core.find(idVendor=0x0403, idProduct=0x6001) + if not dev: + LOGGER.error('ZiGate DIN not found.') + return + LOGGER.info('Put ZiGate DIN in running mode') + ftdi_set_bitmode(dev, 0xC8) + await asyncio.sleep(0.5) + ftdi_set_bitmode(dev, 0xCC) + await asyncio.sleep(0.5) + except Exception as e: + LOGGER.error('Unable to set FTDI bitmode, please check configuration') + LOGGER.error(str(e)) + + +async def set_zigatedin_flashing_mode(): + try: + dev = usb.core.find(idVendor=0x0403, idProduct=0x6001) + if not dev: + LOGGER.error('ZiGate DIN not found.') + return + LOGGER.info('Put ZiGate DIN in flashing mode') + ftdi_set_bitmode(dev, 0x00) + await asyncio.sleep(0.5) + ftdi_set_bitmode(dev, 0xCC) + await asyncio.sleep(0.5) + ftdi_set_bitmode(dev, 0xC0) + await asyncio.sleep(0.5) + ftdi_set_bitmode(dev, 0xC4) + await asyncio.sleep(0.5) + ftdi_set_bitmode(dev, 0xCC) + await asyncio.sleep(0.5) + except Exception as e: + LOGGER.error('Unable to set FTDI bitmode, please check configuration') + LOGGER.error(str(e)) diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index f5fbb64..13335d0 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -50,7 +50,7 @@ async def startup(self, auto_form=False): self._nwk = network_state[0] self._ieee = zigpy.types.EUI64(network_state[1]) - dev = ZiGateDevice(self, self._ieee, self._nwk) + dev = ZiGateDevice(self, self._ieee, self._nwk, self.version) self.devices[dev.ieee] = dev async def shutdown(self): @@ -179,10 +179,16 @@ async def broadcast(self, profile, cluster, src_ep, dst_ep, grpid, radius, class ZiGateDevice(zigpy.device.Device): + def __init__(self, application, ieee, nwk, version): + """Initialize instance.""" + + super().__init__(application, ieee, nwk) + self._model = 'ZiGate {}'.format(version) + @property def manufacturer(self): return "ZiGate" @property def model(self): - return 'ZiGate' + return self._model From 0309e92040f538c7fce132c7f34612f694569a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 2 Nov 2020 22:21:29 +0100 Subject: [PATCH 07/63] use realpath --- zigpy_zigate/uart.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zigpy_zigate/uart.py b/zigpy_zigate/uart.py index 5c47da8..f781e39 100644 --- a/zigpy_zigate/uart.py +++ b/zigpy_zigate/uart.py @@ -3,6 +3,7 @@ import logging import struct import re +import os.path from typing import Any, Dict import serial # noqa @@ -139,6 +140,7 @@ async def connect(device_config: Dict[str, Any], api, loop=None): LOGGER.error('Unable to find ZiGate using auto mode') raise serial.SerialException("Unable to find Zigate using auto mode") + port = os.path.realpath(port) if re.match(r"/dev/(tty(S|AMA)|serial)\d+", port): # Suppose pizigate on /dev/ttyAMAx or /dev/serialx await set_pizigate_running_mode() From 618fb5a51b46e1a05faa086e28ce5ef291b872c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 2 Nov 2020 22:22:02 +0100 Subject: [PATCH 08/63] handle two stage join --- zigpy_zigate/api.py | 2 +- zigpy_zigate/zigbee/application.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index 4adcec4..bc1cb3e 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -18,7 +18,7 @@ COMMAND_TIMEOUT = 1.5 RESPONSES = { - 0x004D: (t.NWK, t.EUI64, t.uint8_t), + 0x004D: (t.NWK, t.EUI64, t.uint8_t, t.uint8_t), 0x8000: (t.uint8_t, t.uint8_t, t.uint16_t, t.Bytes), 0x8002: (t.uint8_t, t.uint16_t, t.uint16_t, t.uint8_t, t.uint8_t, t.Address, t.Address, t.Bytes), diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index 13335d0..592cd51 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -26,6 +26,7 @@ def __init__(self, config: Dict[str, Any]): self._api: Optional[ZiGate] = None self._pending = {} + self._pending_join = [] self._nwk = 0 self._ieee = 0 @@ -99,7 +100,15 @@ def zigate_callback_handler(self, msg, response, lqi): nwk = response[0] ieee = zigpy.types.EUI64(response[1]) parent_nwk = 0 - self.handle_join(nwk, ieee, parent_nwk) + rejoin = response[3] + if nwk in self._pending_join or rejoin: + LOGGER.debug('Finish pairing {} (2nd device announce)'.format(nwk)) + if nwk in self._pending_join: + del self._pending_join.index(nwk) + self.handle_join(nwk, ieee, parent_nwk) + else: + LOGGER.debug('Start pairing {} (1st device announce)'.format(nwk)) + self._pending_join.append(nwk) elif msg == 0x8002: try: if response[5].address_mode == t.ADDRESS_MODE.NWK: From 64c51f01b4a7bdccca8fc4cc3d8132a64503ca5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 2 Nov 2020 22:23:19 +0100 Subject: [PATCH 09/63] fix join --- zigpy_zigate/zigbee/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index 592cd51..c2e65fa 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -104,7 +104,7 @@ def zigate_callback_handler(self, msg, response, lqi): if nwk in self._pending_join or rejoin: LOGGER.debug('Finish pairing {} (2nd device announce)'.format(nwk)) if nwk in self._pending_join: - del self._pending_join.index(nwk) + self._pending_join.remove(nwk) self.handle_join(nwk, ieee, parent_nwk) else: LOGGER.debug('Start pairing {} (1st device announce)'.format(nwk)) From b01a282011eefb47a1f5415a9f342a37db87d75b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 2 Nov 2020 22:51:37 +0100 Subject: [PATCH 10/63] improve zigate wifi support --- zigpy_zigate/uart.py | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/zigpy_zigate/uart.py b/zigpy_zigate/uart.py index f781e39..b3e2374 100644 --- a/zigpy_zigate/uart.py +++ b/zigpy_zigate/uart.py @@ -140,24 +140,31 @@ async def connect(device_config: Dict[str, Any], api, loop=None): LOGGER.error('Unable to find ZiGate using auto mode') raise serial.SerialException("Unable to find Zigate using auto mode") - port = os.path.realpath(port) - if re.match(r"/dev/(tty(S|AMA)|serial)\d+", port): - # Suppose pizigate on /dev/ttyAMAx or /dev/serialx - await set_pizigate_running_mode() - if re.match(r"/dev/ttyUSB\d+", port): - device = next(serial.tools.list_ports.grep(port)) - if device.manufacturer == 'FTDI': # Suppose zigate din /dev/ttyUSBx - await 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, - ) + if port.startswith('socket://'): + host, port = port.split(':', 1) # 192.168.x.y:9999 + port = int(port) + _, protocol = await loop.create_connection( + lambda: protocol, + host, port) + else: + port = os.path.realpath(port) + if re.match(r"/dev/(tty(S|AMA)|serial)\d+", port): + # Suppose pizigate on /dev/ttyAMAx or /dev/serialx + await set_pizigate_running_mode() + if re.match(r"/dev/ttyUSB\d+", port): + device = next(serial.tools.list_ports.grep(port)) + if device.manufacturer == 'FTDI': # Suppose zigate din /dev/ttyUSBx + await 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, + ) await connected_future From 8db135cadaffe940624ce4e29cc2d941ef8001cb Mon Sep 17 00:00:00 2001 From: Hedda Date: Tue, 3 Nov 2020 13:39:03 +0100 Subject: [PATCH 11/63] Update README.md https://github.com/Neonox31/zigate and https://github.com/nouknouk/node-zigate libraries by @Neonox31 and @nouknouk respectively can help developers in understanding the protocol and implementation of ZiGate adapters. Neonox31's zigate library converts ZiGate frames into human readable messages and vice versa, and the node-zigate project by nouknouk aims to provide low-level and high-level APIs for managing the Zigate USB TTL adapter in node.js --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0177472..9a53b48 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,8 @@ Tagged versions are also released via PyPI Documents that layout the serial protocol used for ZiGate serial interface communication can be found here: - https://github.com/fairecasoimeme/ZiGate/tree/master/Protocol +- https://github.com/Neonox31/zigate +- https://github.com/nouknouk/node-zigate ## How to contribute From 3094cb05434dd63aa15a8922e90d113a2cc562ce Mon Sep 17 00:00:00 2001 From: Hedda Date: Tue, 3 Nov 2020 15:31:47 +0100 Subject: [PATCH 12/63] Update README.md to mention Open Lumi Gateway with ZiGate firmware Update README.md to mention Open Lumi Gateway with ZiGate firmware --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0177472..ba3cd81 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Note! ZiGate open source ZigBee adapter hardware requires ZiGate firmware 3.1a o ### Experimental Zigbee radio modules - [ZiGate Pack WiFi](https://zigate.fr/produit/zigate-pack-wifi-v1-3/) (work in progress) +- [Open Lumi Gateway](https://github.com/openlumi) - [DIY ZiGate WiFi bridge hacked from an Xiaomi Lumi Gateway with modded OpenWRT firmware](https://github.com/zigpy/zigpy-zigate/issues/59) ## Port configuration From 9481d96ce42c7e5f3db3d98e8b6d22e5e92b9ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 3 Nov 2020 19:43:25 +0100 Subject: [PATCH 13/63] Fix ZiGate Wifi --- zigpy_zigate/uart.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/zigpy_zigate/uart.py b/zigpy_zigate/uart.py index b3e2374..f9ed755 100644 --- a/zigpy_zigate/uart.py +++ b/zigpy_zigate/uart.py @@ -123,10 +123,7 @@ async def connect(device_config: Dict[str, Any], api, loop=None): protocol = Gateway(api, connected_future) port = device_config[CONF_DEVICE_PATH] - if port.startswith('pizigate:'): - await set_pizigate_running_mode() - port = port.split(':', 1)[1] - elif port == 'auto': + if port == 'auto': devices = list(serial.tools.list_ports.grep('ZiGate')) if devices: port = devices[0].device @@ -141,8 +138,13 @@ async def connect(device_config: Dict[str, Any], api, loop=None): raise serial.SerialException("Unable to find Zigate using auto mode") if port.startswith('socket://'): - host, port = port.split(':', 1) # 192.168.x.y:9999 - port = int(port) + 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) From a129e74f3d8caa2d86c4431f3a8a267b0db4c164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 3 Nov 2020 19:46:11 +0100 Subject: [PATCH 14/63] Disable two stages pairing --- zigpy_zigate/zigbee/application.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index c2e65fa..62c05a7 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -100,15 +100,17 @@ def zigate_callback_handler(self, msg, response, lqi): nwk = response[0] ieee = zigpy.types.EUI64(response[1]) parent_nwk = 0 - rejoin = response[3] - if nwk in self._pending_join or rejoin: - LOGGER.debug('Finish pairing {} (2nd device announce)'.format(nwk)) - if nwk in self._pending_join: - self._pending_join.remove(nwk) - self.handle_join(nwk, ieee, parent_nwk) - else: - LOGGER.debug('Start pairing {} (1st device announce)'.format(nwk)) - self._pending_join.append(nwk) + self.handle_join(nwk, ieee, parent_nwk) + # Temporary disable two stages pairing due to firmware bug + # rejoin = response[3] + # if nwk in self._pending_join or rejoin: + # LOGGER.debug('Finish pairing {} (2nd device announce)'.format(nwk)) + # if nwk in self._pending_join: + # self._pending_join.remove(nwk) + # self.handle_join(nwk, ieee, parent_nwk) + # else: + # LOGGER.debug('Start pairing {} (1st device announce)'.format(nwk)) + # self._pending_join.append(nwk) elif msg == 0x8002: try: if response[5].address_mode == t.ADDRESS_MODE.NWK: From 5a87f3547ca6c0e37cf554f075f30404b2e706eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 3 Nov 2020 19:47:45 +0100 Subject: [PATCH 15/63] add firmware version warning --- zigpy_zigate/zigbee/application.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index 62c05a7..d7214fc 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -40,6 +40,8 @@ async def startup(self, auto_form=False): version = '{:x}'.format(version[1]) version = '{}.{}'.format(version[0], version[1:]) self.version = version + if version < '3.1d': + LOGGER.warning('Old ZiGate firmware detected, you should upgrade to 3.1d or newer') network_state, lqi = await self._api.get_network_state() should_form = not network_state or network_state[0] == 0xffff or network_state[3] == 0 From 26d577f964bd04d3e354a6b07733b4f77c4b08c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 3 Nov 2020 20:12:52 +0100 Subject: [PATCH 16/63] update Readme --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ba3cd81..af0504a 100644 --- a/README.md +++ b/README.md @@ -24,19 +24,23 @@ Note! ZiGate open source ZigBee adapter hardware requires ZiGate firmware 3.1a o - [ZiGate USB-TTL](https://zigate.fr/produit/zigate-ttl/) - [ZiGate USB-DIN](https://zigate.fr/produit/zigate-usb-din/) - [PiZiGate (ZiGate module for Raspberry Pi GPIO)](https://zigate.fr/produit/pizigate-v1-0/) +- [ZiGate Pack WiFi](https://zigate.fr/produit/zigate-pack-wifi-v1-3/) (work in progress) ### Experimental Zigbee radio modules -- [ZiGate Pack WiFi](https://zigate.fr/produit/zigate-pack-wifi-v1-3/) (work in progress) - [Open Lumi Gateway](https://github.com/openlumi) - [DIY ZiGate WiFi bridge hacked from an Xiaomi Lumi Gateway with modded OpenWRT firmware](https://github.com/zigpy/zigpy-zigate/issues/59) ## Port configuration -- To configure __usb__ ZiGate port, just specify the port, example : `/dev/ttyUSB0` - - Alternatively you could set port to `auto` to enable automatic usb port discovery -- To configure __pizigate__ port, prefix the port with `pizigate:`, example : `pizigate:/dev/serial0` -- To configure __wifi__ ZiGate, specify IP address and port, example : `socket://192.168.1.10:9999` +- To configure __usb__ ZiGate (USB TTL or DIN) port, just specify the port, example : `/dev/ttyUSB0` + - Alternatively you could manually set port to `auto` to enable automatic usb port discovery +- To configure __pizigate__ port, specify the port, example : `/dev/serial0` or `/dev/ttyAMA0` +- To configure __wifi__ ZiGate, manually specify IP address and port, example : `socket://192.168.1.10:9999` + +__pizigate__ may requiert some adjustements on Rpi3 or Rpi4: +- [Rpi3](https://zigate.fr/documentation/compatibilite-raspberry-pi-3-et-zero-w/) +- [Rpi4](https://zigate.fr/documentation/compatibilite-raspberry-pi-4-b/) -Note! Requires ZiGate firmware 3.1a and later +Note! Requires ZiGate firmware 3.1d and later - https://zigate.fr/tag/firmware/ ## Testing new releases From 95f0800e79258e0f73297888a2a335dde5af6704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 3 Nov 2020 20:14:06 +0100 Subject: [PATCH 17/63] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index af0504a..9fd2339 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Note! ZiGate open source ZigBee adapter hardware requires ZiGate firmware 3.1a o - [ZiGate USB-TTL](https://zigate.fr/produit/zigate-ttl/) - [ZiGate USB-DIN](https://zigate.fr/produit/zigate-usb-din/) - [PiZiGate (ZiGate module for Raspberry Pi GPIO)](https://zigate.fr/produit/pizigate-v1-0/) -- [ZiGate Pack WiFi](https://zigate.fr/produit/zigate-pack-wifi-v1-3/) (work in progress) +- [ZiGate Pack WiFi](https://zigate.fr/produit/zigate-pack-wifi-v1-3/) ### Experimental Zigbee radio modules - [Open Lumi Gateway](https://github.com/openlumi) - [DIY ZiGate WiFi bridge hacked from an Xiaomi Lumi Gateway with modded OpenWRT firmware](https://github.com/zigpy/zigpy-zigate/issues/59) From ecb3bfeaffac026618d6774281a723182403d1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Wed, 4 Nov 2020 21:12:53 +0100 Subject: [PATCH 18/63] add firmware download tool --- zigpy_zigate/tools/__init__.py | 0 zigpy_zigate/tools/firmware.py | 61 ++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 zigpy_zigate/tools/__init__.py create mode 100644 zigpy_zigate/tools/firmware.py diff --git a/zigpy_zigate/tools/__init__.py b/zigpy_zigate/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zigpy_zigate/tools/firmware.py b/zigpy_zigate/tools/firmware.py new file mode 100644 index 0000000..30a197b --- /dev/null +++ b/zigpy_zigate/tools/firmware.py @@ -0,0 +1,61 @@ +import urllib.request +import logging +import os +import argparse +import json +from collections import OrderedDict + +URL = 'https://api.github.com/repos/fairecasoimeme/ZiGate/releases' +LOGGER = logging.getLogger("ZiGate Firmware") + + +def get_releases(): + LOGGER.info('Searching for ZiGate firmware') + releases = OrderedDict() + r = urllib.request.urlopen(URL) + if r.status == 200: + for release in json.loads(r.read()): + for asset in release['assets']: + if asset['name'].endswith('.bin'): + LOGGER.info('Found %s', asset['name']) + releases[asset['name']] = asset['browser_download_url'] + return releases + + +def download(url, dest='/tmp'): + filename = url.rsplit('/', 1)[1] + LOGGER.info('Downloading %s to %s', url, dest) + r = requests.get(url, allow_redirects=True) + filename = os.path.join(dest, filename) + with open(filename, 'wb') as fp: + fp.write(r.content) + LOGGER.info('Done') + return filename + + +def download_latest(dest='/tmp'): + LOGGER.info('Download latest firmware') + releases = get_releases() + if releases: + latest = list(releases.keys())[0] + LOGGER.info('Latest is %s', latest) + return download(releases[latest], dest) + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + parser = argparse.ArgumentParser() + parser.add_argument("command", help="Command to start", choices=["list", "download", "download_latest"]) + parser.add_argument('--url', help="Download URL") + parser.add_argument('--dest', help="Download folder, default to /tmp", default='/tmp') + args = parser.parse_args() + if args.command == 'list': + for k, v in get_releases().items(): + print(k, v) + elif args.command == 'download': + if not args.url: + LOGGER.error('You have to give a URL to download using --url') + else: + download(args.url, args.dest) + elif args.command == 'download_latest': + download_latest(args.dest) From f14aa826b1cc3b9382164adcac4df4368b484581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Wed, 4 Nov 2020 21:15:11 +0100 Subject: [PATCH 19/63] Add firmware download tool --- zigpy_zigate/tools/firmware.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/zigpy_zigate/tools/firmware.py b/zigpy_zigate/tools/firmware.py index 30a197b..947760d 100644 --- a/zigpy_zigate/tools/firmware.py +++ b/zigpy_zigate/tools/firmware.py @@ -25,12 +25,15 @@ def get_releases(): def download(url, dest='/tmp'): filename = url.rsplit('/', 1)[1] LOGGER.info('Downloading %s to %s', url, dest) - r = requests.get(url, allow_redirects=True) - filename = os.path.join(dest, filename) - with open(filename, 'wb') as fp: - fp.write(r.content) - LOGGER.info('Done') - return filename + r = urllib.request.urlopen(url) + if r.status == 200: + filename = os.path.join(dest, filename) + with open(filename, 'wb') as fp: + fp.write(r.read()) + LOGGER.info('Done') + return filename + else: + LOGGER.error('Error downloading %s %s',r.status, r.reason) def download_latest(dest='/tmp'): From 532e76fa633624d2a0c0fe1635eeb3e4262a5659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Thu, 5 Nov 2020 22:37:42 +0100 Subject: [PATCH 20/63] add github CI action --- .github/workflows/tests.yml | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..2d741bf --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,45 @@ +name: Build & Tests + +on: + push: + branches: [ master, dev ] + pull_request: + branches: [ master, dev ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install -e ".[dev]" + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest + + markdownlint: + + runs-on: ubuntu-latest + name: Test Markdown + steps: + - name: Run mdl + uses: actionshub/markdownlint@master From 88670e86dd970563aec1abef61415171c3659867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Thu, 5 Nov 2020 22:41:05 +0100 Subject: [PATCH 21/63] add github CI action --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2d741bf..ef8edf9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,9 +23,9 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest + pip install flake8 pytest asynctest coveralls pytest-cov pytest-asyncio if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - pip install -e ".[dev]" + pip install -e "." - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names From bf2e75021881c2260bd568cac7e7bdb58d1dd87d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sun, 8 Nov 2020 07:55:51 +0100 Subject: [PATCH 22/63] disable data ack --- zigpy_zigate/zigbee/application.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index d7214fc..cc7cef2 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -132,7 +132,8 @@ def zigate_callback_handler(self, msg, response, lqi): response[3], response[4], response[-1]) elif msg == 0x8011: # ACK Data LOGGER.debug('ACK Data received %s %s', response[4], response[0]) - self._handle_frame_failure(response[4], response[0]) + # disabled because of https://github.com/fairecasoimeme/ZiGate/issues/324 + # self._handle_frame_failure(response[4], response[0]) elif msg == 0x8035: # PDM Event try: event = PDM_EVENT(response[0]).name From 88c8aed4ab72e54bfa827d2c3c6963c373e3ea91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sun, 8 Nov 2020 08:01:20 +0100 Subject: [PATCH 23/63] add py39 --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ef8edf9..3dd1ff1 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] + python-version: [3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 @@ -31,7 +31,7 @@ jobs: # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + flake8 . --count --exit-zero --max-complexity=16 --max-line-length=127 --statistics - name: Test with pytest run: | pytest From aa1c1bcd468c38880fd8c26a018e40ed627198ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sun, 8 Nov 2020 08:02:10 +0100 Subject: [PATCH 24/63] remove whitespace --- zigpy_zigate/tools/firmware.py | 2 +- zigpy_zigate/tools/flasher.py | 530 +++++++++++++++++++++++++++++++++ zigpy_zigate/uart.py | 34 ++- 3 files changed, 558 insertions(+), 8 deletions(-) create mode 100644 zigpy_zigate/tools/flasher.py diff --git a/zigpy_zigate/tools/firmware.py b/zigpy_zigate/tools/firmware.py index 947760d..e03d445 100644 --- a/zigpy_zigate/tools/firmware.py +++ b/zigpy_zigate/tools/firmware.py @@ -33,7 +33,7 @@ def download(url, dest='/tmp'): LOGGER.info('Done') return filename else: - LOGGER.error('Error downloading %s %s',r.status, r.reason) + LOGGER.error('Error downloading %s %s', r.status, r.reason) def download_latest(dest='/tmp'): diff --git a/zigpy_zigate/tools/flasher.py b/zigpy_zigate/tools/flasher.py new file mode 100644 index 0000000..abbdce3 --- /dev/null +++ b/zigpy_zigate/tools/flasher.py @@ -0,0 +1,530 @@ +#!/usr/bin/python3 +# +# Copyright (c) 2018 Sébastien RAMAGE +# +# For the full copyright and license information, please view the LICENSE +# file that was distributed with this source code. +# +# Thanks to Sander Hoentjen (tjikkun) we now have a flasher ! +# https://github.com/tjikkun/zigate-flasher + +import argparse +import atexit +import functools +import itertools +import logging +import struct +from operator import xor +import datetime +from .firmware import download_latest +from .transport import discover_port +import time +import serial +from serial.tools.list_ports import comports +try: + import RPi.GPIO as GPIO +except Exception: + # Fake GPIO + class GPIO: + def fake(self, *args, **kwargs): + pass + + def __getattr__(self, *args, **kwargs): + return self.fake + GPIO = GPIO() +import usb + + +logger = logging.getLogger('ZiGate Flasher') +_responses = {} + +ZIGATE_CHIP_ID = 0x10408686 +ZIGATE_BINARY_VERSION = bytes.fromhex('07030008') +ZIGATE_FLASH_START = 0x00000000 +ZIGATE_FLASH_END = 0x00040000 + + +class Command: + + def __init__(self, type_, fmt=None, raw=False): + assert not (raw and fmt), 'Raw commands cannot use built-in struct formatting' + logger.debug('Command {} {} {}'.format(type_, fmt, raw)) + self.type = type_ + self.raw = raw + if fmt: + self.struct = struct.Struct(fmt) + else: + self.struct = None + + def __call__(self, func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + rv = func(*args, **kwargs) + + if self.struct: + try: + data = self.struct.pack(*rv) + except TypeError: + data = self.struct.pack(rv) + elif self.raw: + data = rv + else: + data = bytearray() + + return prepare(self.type, data) + + return wrapper + + +class Response: + + def __init__(self, type_, data, chksum): + logger.debug('Response {} {} {}'.format(type_, data, chksum)) + self.type = type_ + self.data = data[1:] + self.chksum = chksum + self.status = data[0] + + @property + def ok(self): + return self.status == 0 + + def __str__(self): + return 'Response(type=0x%02x, data=0x%s, checksum=0x%02x)' % (self.type, + self.data.hex(), + self.chksum) + + +def register(type_): + assert type_ not in _responses, 'Duplicate response type 0x%02x' % type_ + + def decorator(func): + _responses[type_] = func + return func + + return decorator + + +def prepare(type_, data): + length = len(data) + 2 + + checksum = functools.reduce(xor, + itertools.chain(type_.to_bytes(2, 'big'), + length.to_bytes(2, 'big'), + data), 0) + + message = struct.pack('!BB%dsB' % len(data), length, type_, data, checksum) + # print('Prepared command 0x%s' % message.hex()) + return message + + +def read_response(ser): + length = ser.read() + length = int.from_bytes(length, 'big') + logger.debug('read_response length {}'.format(length)) + answer = ser.read(length) + logger.debug('read_response answer {}'.format(answer)) + return _unpack_raw_message(length, answer) + # type_, data, chksum = struct.unpack('!B%dsB' % (length - 2), answer) + # return {'type': type_, 'data': data, 'chksum': chksum} + + +def _unpack_raw_message(length, decoded): + logger.debug('unpack raw message {} {}'.format(length, decoded)) + if len(decoded) != length or length < 2: + logger.exception("Unpack failed, length: %d, msg %s" % (length, decoded.hex())) + return + type_, data, chksum = \ + struct.unpack('!B%dsB' % (length - 2), decoded) + return _responses.get(type_, Response)(type_, data, chksum) + + +@Command(0x07) +def req_flash_erase(): + pass + + +@Command(0x09, raw=True) +def req_flash_write(addr, data): + msg = struct.pack(' flash_end: + read_bytes = flash_end - cur + ser.write(req_flash_read(cur, read_bytes)) + res = read_response(ser) + if not res or not res.ok: + print('Reading flash failed') + raise SystemExit(1) + if cur == 0: + (flash_end,) = struct.unpack('>L', res.data[0x20:0x24]) + fd.write(res.data) + printProgressBar(cur, flash_end, 'Reading') + cur += read_bytes + printProgressBar(flash_end, flash_end, 'Reading') + logger.info('Backup firmware done') + + +def write_file_to_flash(ser, filename): + logger.info('Writing new firmware from %s', filename) + with open(filename, 'rb') as fd: + ser.write(req_flash_erase()) + res = read_response(ser) + if not res or not res.ok: + print('Erasing flash failed') + raise SystemExit(1) + + # flash_start = cur = ZIGATE_FLASH_START + cur = ZIGATE_FLASH_START + flash_end = ZIGATE_FLASH_END + + bin_ver = fd.read(4) + if bin_ver != ZIGATE_BINARY_VERSION: + print('Not a valid image for Zigate') + raise SystemExit(1) + read_bytes = 128 + while cur < flash_end: + data = fd.read(read_bytes) + if not data: + break + ser.write(req_flash_write(cur, data)) + res = read_response(ser) + if not res.ok: + print('writing failed at 0x%08x, status: 0x%x, data: %s' % (cur, res.status, data.hex())) + raise SystemExit(1) + printProgressBar(cur, flash_end, 'Writing') + cur += read_bytes + printProgressBar(flash_end, flash_end, 'Writing') + logger.info('Writing new firmware done') + + +def erase_EEPROM(ser, pdm_only=False): + ser.timeout = 10 # increase timeout because official NXP programmer do it + ser.write(req_eeprom_erase(pdm_only)) + res = read_response(ser) + if not res or not res.ok: + print('Erasing EEPROM failed') + raise SystemExit(1) + + +def flash(serialport='auto', write=None, save=None, erase=False, pdm_only=False): + """ + Read or write firmware + """ + serialport = discover_port(serialport) + try: + ser = serial.Serial(serialport, 38400, timeout=5) + except serial.SerialException: + logger.exception("Could not open serial device %s", serialport) + return + + change_baudrate(ser, 115200) + check_chip_id(ser) + flash_type = get_flash_type(ser) + mac_address = get_mac(ser) + logger.info('Found MAC-address: %s', mac_address) + if write or save or erase: + select_flash(ser, flash_type) + + if save: + write_flash_to_file(ser, save) + + if write: + write_file_to_flash(ser, write) + + if erase: + erase_EEPROM(ser, pdm_only) + change_baudrate(ser, 38400) + ser.close() + + +def upgrade_firmware(port): + backup_filename = 'zigate_backup_{:%Y%m%d%H%M%S}.bin'.format(datetime.datetime.now()) + flash(port, save=backup_filename) + print('ZiGate backup created {}'.format(backup_filename)) + firmware_path = download_latest() + print('Firmware downloaded', firmware_path) + flash(port, write=firmware_path) + print('ZiGate flashed with {}'.format(firmware_path)) + + +def ftdi_set_bitmode(dev, bitmask): + ''' + Set mode for ZiGate DIN module + ''' + BITMODE_CBUS = 0x20 + SIO_SET_BITMODE_REQUEST = 0x0b + bmRequestType = usb.util.build_request_type(usb.util.CTRL_OUT, + usb.util.CTRL_TYPE_VENDOR, + usb.util.CTRL_RECIPIENT_DEVICE) + wValue = bitmask | (BITMODE_CBUS << BITMODE_CBUS) + dev.ctrl_transfer(bmRequestType, SIO_SET_BITMODE_REQUEST, wValue) + + +def main(): + ports_available = [port for (port, _, _) in sorted(comports())] + parser = argparse.ArgumentParser() + parser.add_argument('-p', '--serialport', choices=ports_available, + help='Serial port, e.g. /dev/ttyUSB0', required=True) + parser.add_argument('-w', '--write', help='Firmware bin to flash onto the chip') + parser.add_argument('-s', '--save', help='File to save the currently loaded firmware to') + parser.add_argument('-u', '--upgrade', help='Download and flash the lastest available firmware', + action='store_true', default=False) +# parser.add_argument('-e', '--erase', help='Erase EEPROM', action='store_true') +# parser.add_argument('--pdm-only', help='Erase PDM only, use it with --erase', action='store_true') + parser.add_argument('-d', '--debug', help='Set log level to DEBUG', action='store_true') + parser.add_argument('--gpio', help='Configure GPIO for PiZiGate flash', action='store_true', default=False) + parser.add_argument('--din', help='Configure USB for ZiGate DIN flash', action='store_true', default=False) + args = parser.parse_args() + logger.setLevel(logging.INFO) + if args.debug: + logger.setLevel(logging.DEBUG) + + if args.gpio: + logger.info('Put PiZiGate in flash mode') + GPIO.setmode(GPIO.BCM) + GPIO.setup(27, GPIO.OUT) # GPIO2 + GPIO.output(27, GPIO.LOW) # GPIO2 + GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # GPIO0 + time.sleep(0.5) + GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP) # GPIO0 + time.sleep(0.5) + elif args.din: + logger.info('Put ZiGate DIN in flash mode') + dev = usb.core.find(idVendor=0x0403, idProduct=0x6001) + if not dev: + logger.error('ZiGate DIN not found.') + return + ftdi_set_bitmode(dev, 0x00) + time.sleep(0.5) + # Set CBUS2/3 high... + ftdi_set_bitmode(dev, 0xCC) + time.sleep(0.5) + # Set CBUS2/3 low... + ftdi_set_bitmode(dev, 0xC0) + time.sleep(0.5) + ftdi_set_bitmode(dev, 0xC4) + time.sleep(0.5) + # Set CBUS2/3 back to tristate + ftdi_set_bitmode(dev, 0xCC) + time.sleep(0.5) + + if args.upgrade: + upgrade_firmware(args.serialport) + + else: + try: + ser = serial.Serial(args.serialport, 38400, timeout=5) + except serial.SerialException: + logger.exception("Could not open serial device %s", args.serialport) + raise SystemExit(1) + + # atexit.register(change_baudrate, ser, 38400) + + change_baudrate(ser, 115200) + check_chip_id(ser) + flash_type = get_flash_type(ser) + mac_address = get_mac(ser) + logger.info('Found MAC-address: %s', mac_address) + if args.write or args.save: # or args.erase: + select_flash(ser, flash_type) + + if args.save: + write_flash_to_file(ser, args.save) + + if args.write: + write_file_to_flash(ser, args.write) + +# if args.erase: +# erase_EEPROM(ser, args.pdm_only) + + + if args.gpio: + logger.info('Put PiZiGate in running mode') + GPIO.output(27, GPIO.HIGH) # GPIO2 + GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # GPIO0 + time.sleep(0.5) + GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP) # GPIO0 + time.sleep(0.5) + elif args.din: + logger.info('Put ZiGate DIN in running mode') + ftdi_set_bitmode(dev, 0xC8) + time.sleep(0.5) + ftdi_set_bitmode(dev, 0xCC) + time.sleep(0.5) + + +if __name__ == "__main__": + logging.basicConfig() + main() diff --git a/zigpy_zigate/uart.py b/zigpy_zigate/uart.py index f9ed755..9925891 100644 --- a/zigpy_zigate/uart.py +++ b/zigpy_zigate/uart.py @@ -137,7 +137,7 @@ async def connect(device_config: Dict[str, Any], api, loop=None): LOGGER.error('Unable to find ZiGate using auto mode') raise serial.SerialException("Unable to find Zigate using auto mode") - if port.startswith('socket://'): + if is_zigate_wifi(port): port = port.split('socket://', 1)[1] if ':' in port: host, port = port.split(':', 1) # 192.168.x.y:9999 @@ -150,13 +150,10 @@ async def connect(device_config: Dict[str, Any], api, loop=None): host, port) else: port = os.path.realpath(port) - if re.match(r"/dev/(tty(S|AMA)|serial)\d+", port): - # Suppose pizigate on /dev/ttyAMAx or /dev/serialx + if is_pizigate(port): await set_pizigate_running_mode() - if re.match(r"/dev/ttyUSB\d+", port): - device = next(serial.tools.list_ports.grep(port)) - if device.manufacturer == 'FTDI': # Suppose zigate din /dev/ttyUSBx - await set_zigatedin_running_mode() + elif is_zigate_din: + await set_zigatedin_running_mode() _, protocol = await serial_asyncio.create_serial_connection( loop, @@ -173,6 +170,29 @@ async def connect(device_config: Dict[str, Any], api, loop=None): return protocol +def is_pizigate(port): + """ detect pizigate """ + # Suppose pizigate on /dev/ttyAMAx or /dev/serialx + return re.match(r"/dev/(tty(S|AMA)|serial)\d+", port) is not None + + +def is_zigate_din(port): + """ detect zigate din """ + if re.match(r"/dev/ttyUSB\d+", port): + try: + device = next(serial.tools.list_ports.grep(port)) + # Suppose zigate din /dev/ttyUSBx + return device.description == 'ZiGate' and device.manufacturer == 'FTDI' + except StopIteration: + pass + return False + + +def is_zigate_wifi(port): + """ detect zigate din """ + return port.startswith('socket://') + + async def set_pizigate_running_mode(): try: import RPi.GPIO as GPIO From 5534ff9163abacbeba43e30d3d59a496d5d9fba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sun, 8 Nov 2020 08:05:31 +0100 Subject: [PATCH 25/63] add debug log --- zigpy_zigate/uart.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zigpy_zigate/uart.py b/zigpy_zigate/uart.py index 9925891..82b0162 100644 --- a/zigpy_zigate/uart.py +++ b/zigpy_zigate/uart.py @@ -138,6 +138,7 @@ async def connect(device_config: Dict[str, Any], api, loop=None): raise serial.SerialException("Unable to find Zigate using auto mode") if 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 @@ -151,8 +152,10 @@ async def connect(device_config: Dict[str, Any], api, loop=None): else: port = os.path.realpath(port) if is_pizigate(port): + LOGGER.debug('PiZiGate detected') await set_pizigate_running_mode() elif is_zigate_din: + LOGGER.debug('ZiGate USB DIN detected') await set_zigatedin_running_mode() _, protocol = await serial_asyncio.create_serial_connection( From 215aaeda31e6f77d48422659b68ffd1dcb6aade3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sun, 8 Nov 2020 08:09:11 +0100 Subject: [PATCH 26/63] fix lint --- zigpy_zigate/tools/flasher.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/zigpy_zigate/tools/flasher.py b/zigpy_zigate/tools/flasher.py index abbdce3..2dde00b 100644 --- a/zigpy_zigate/tools/flasher.py +++ b/zigpy_zigate/tools/flasher.py @@ -9,7 +9,6 @@ # https://github.com/tjikkun/zigate-flasher import argparse -import atexit import functools import itertools import logging @@ -288,7 +287,7 @@ def select_flash(ser, flash_type): raise SystemExit(1) -def printProgressBar (iteration, total, prefix = '', suffix = '', decimals = 1, length = 100, fill = '█', printEnd = "\r"): +def printProgressBar(iteration, total, prefix='', suffix='', decimals=1, length=100, fill='█', printEnd="\r"): """ Call in a loop to create terminal progress bar @params: @@ -304,9 +303,9 @@ def printProgressBar (iteration, total, prefix = '', suffix = '', decimals = 1, percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total))) filledLength = int(length * iteration // total) bar = fill * filledLength + '-' * (length - filledLength) - print('\r{0} |{1}| {2}% {3}'.format(prefix, bar, percent, suffix), end = printEnd) + print('\r{0} |{1}| {2}% {3}'.format(prefix, bar, percent, suffix), end=printEnd) # Print New Line on Complete - if iteration == total: + if iteration == total: print() @@ -490,8 +489,6 @@ def main(): logger.exception("Could not open serial device %s", args.serialport) raise SystemExit(1) - # atexit.register(change_baudrate, ser, 38400) - change_baudrate(ser, 115200) check_chip_id(ser) flash_type = get_flash_type(ser) @@ -509,7 +506,6 @@ def main(): # if args.erase: # erase_EEPROM(ser, args.pdm_only) - if args.gpio: logger.info('Put PiZiGate in running mode') GPIO.output(27, GPIO.HIGH) # GPIO2 From 504dcd4aebbfc7bcd00d62158b3fad08ad1e67e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sun, 8 Nov 2020 09:45:46 +0100 Subject: [PATCH 27/63] move to common --- zigpy_zigate/common.py | 113 ++++++++++++++++++++++++++++++++++++++ zigpy_zigate/uart.py | 121 +++-------------------------------------- 2 files changed, 120 insertions(+), 114 deletions(-) create mode 100644 zigpy_zigate/common.py diff --git a/zigpy_zigate/common.py b/zigpy_zigate/common.py new file mode 100644 index 0000000..856068e --- /dev/null +++ b/zigpy_zigate/common.py @@ -0,0 +1,113 @@ +import re +import serial.tools.list_ports +import usb +import logging +import asyncio + +LOGGER = logging.getLogger(__name__) + + +def is_pizigate(port): + """ detect pizigate """ + # Suppose pizigate on /dev/ttyAMAx or /dev/serialx + return re.match(r"/dev/(tty(S|AMA)|serial)\d+", port) is not None + + +def is_zigate_din(port): + """ detect zigate din """ + if re.match(r"/dev/ttyUSB\d+", port): + try: + device = next(serial.tools.list_ports.grep(port)) + # Suppose zigate din /dev/ttyUSBx + return device.description == 'ZiGate' and device.manufacturer == 'FTDI' + except StopIteration: + pass + return False + + +def is_zigate_wifi(port): + """ detect zigate din """ + return port.startswith('socket://') + + +async def set_pizigate_running_mode(): + try: + import RPi.GPIO as GPIO + LOGGER.info('Put PiZiGate in running mode') + GPIO.setmode(GPIO.BCM) + GPIO.setup(27, GPIO.OUT) # GPIO2 + GPIO.output(27, GPIO.HIGH) # GPIO2 + GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # GPIO0 + await asyncio.sleep(0.5) + GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP) # GPIO0 + await asyncio.sleep(0.5) + except Exception as e: + LOGGER.error('Unable to set PiZiGate GPIO, please check configuration') + LOGGER.error(str(e)) + + +async def set_pizigate_flashing_mode(): + try: + import RPi.GPIO as GPIO + LOGGER.info('Put PiZiGate in flashing mode') + GPIO.setmode(GPIO.BCM) + GPIO.setup(27, GPIO.OUT) # GPIO2 + GPIO.output(27, GPIO.LOW) # GPIO2 + GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # GPIO0 + await asyncio.sleep(0.5) + GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP) # GPIO0 + await asyncio.sleep(0.5) + except Exception as e: + LOGGER.error('Unable to set PiZiGate GPIO, please check configuration') + LOGGER.error(str(e)) + + +def ftdi_set_bitmode(dev, bitmask): + ''' + Set mode for ZiGate DIN module + ''' + BITMODE_CBUS = 0x20 + SIO_SET_BITMODE_REQUEST = 0x0b + bmRequestType = usb.util.build_request_type(usb.util.CTRL_OUT, + usb.util.CTRL_TYPE_VENDOR, + usb.util.CTRL_RECIPIENT_DEVICE) + wValue = bitmask | (BITMODE_CBUS << BITMODE_CBUS) + dev.ctrl_transfer(bmRequestType, SIO_SET_BITMODE_REQUEST, wValue) + + +async def set_zigatedin_running_mode(): + try: + dev = usb.core.find(idVendor=0x0403, idProduct=0x6001) + if not dev: + LOGGER.error('ZiGate DIN not found.') + return + LOGGER.info('Put ZiGate DIN in running mode') + ftdi_set_bitmode(dev, 0xC8) + await asyncio.sleep(0.5) + ftdi_set_bitmode(dev, 0xCC) + await asyncio.sleep(0.5) + except Exception as e: + LOGGER.error('Unable to set FTDI bitmode, please check configuration') + LOGGER.error(str(e)) + + +async def set_zigatedin_flashing_mode(): + try: + dev = usb.core.find(idVendor=0x0403, idProduct=0x6001) + if not dev: + LOGGER.error('ZiGate DIN not found.') + return + LOGGER.info('Put ZiGate DIN in flashing mode') + ftdi_set_bitmode(dev, 0x00) + await asyncio.sleep(0.5) + ftdi_set_bitmode(dev, 0xCC) + await asyncio.sleep(0.5) + ftdi_set_bitmode(dev, 0xC0) + await asyncio.sleep(0.5) + ftdi_set_bitmode(dev, 0xC4) + await asyncio.sleep(0.5) + ftdi_set_bitmode(dev, 0xCC) + await asyncio.sleep(0.5) + except Exception as e: + LOGGER.error('Unable to set FTDI bitmode, please check configuration') + LOGGER.error(str(e)) diff --git a/zigpy_zigate/uart.py b/zigpy_zigate/uart.py index 82b0162..19464dd 100644 --- a/zigpy_zigate/uart.py +++ b/zigpy_zigate/uart.py @@ -2,16 +2,15 @@ import binascii import logging import struct -import re import os.path from typing import Any, Dict import serial # noqa import serial.tools.list_ports import serial_asyncio -import usb -from zigpy_zigate.config import CONF_DEVICE_PATH +from .config import CONF_DEVICE_PATH +from . import common as c LOGGER = logging.getLogger(__name__) ZIGATE_BAUDRATE = 115200 @@ -137,7 +136,7 @@ async def connect(device_config: Dict[str, Any], api, loop=None): LOGGER.error('Unable to find ZiGate using auto mode') raise serial.SerialException("Unable to find Zigate using auto mode") - if is_zigate_wifi(port): + if c.is_zigate_wifi(port): LOGGER.debug('ZiGate WiFi detected') port = port.split('socket://', 1)[1] if ':' in port: @@ -151,12 +150,12 @@ async def connect(device_config: Dict[str, Any], api, loop=None): host, port) else: port = os.path.realpath(port) - if is_pizigate(port): + if c.is_pizigate(port): LOGGER.debug('PiZiGate detected') - await set_pizigate_running_mode() - elif is_zigate_din: + await c.set_pizigate_running_mode() + elif c.is_zigate_din: LOGGER.debug('ZiGate USB DIN detected') - await set_zigatedin_running_mode() + await c.set_zigatedin_running_mode() _, protocol = await serial_asyncio.create_serial_connection( loop, @@ -171,109 +170,3 @@ async def connect(device_config: Dict[str, Any], api, loop=None): await connected_future return protocol - - -def is_pizigate(port): - """ detect pizigate """ - # Suppose pizigate on /dev/ttyAMAx or /dev/serialx - return re.match(r"/dev/(tty(S|AMA)|serial)\d+", port) is not None - - -def is_zigate_din(port): - """ detect zigate din """ - if re.match(r"/dev/ttyUSB\d+", port): - try: - device = next(serial.tools.list_ports.grep(port)) - # Suppose zigate din /dev/ttyUSBx - return device.description == 'ZiGate' and device.manufacturer == 'FTDI' - except StopIteration: - pass - return False - - -def is_zigate_wifi(port): - """ detect zigate din """ - return port.startswith('socket://') - - -async def set_pizigate_running_mode(): - try: - import RPi.GPIO as GPIO - LOGGER.info('Put PiZiGate in running mode') - GPIO.setmode(GPIO.BCM) - GPIO.setup(27, GPIO.OUT) # GPIO2 - GPIO.output(27, GPIO.HIGH) # GPIO2 - GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # GPIO0 - await asyncio.sleep(0.5) - GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP) # GPIO0 - await asyncio.sleep(0.5) - except Exception as e: - LOGGER.error('Unable to set PiZiGate GPIO, please check configuration') - LOGGER.error(str(e)) - - -async def set_pizigate_flashing_mode(): - try: - import RPi.GPIO as GPIO - LOGGER.info('Put PiZiGate in flashing mode') - GPIO.setmode(GPIO.BCM) - GPIO.setup(27, GPIO.OUT) # GPIO2 - GPIO.output(27, GPIO.LOW) # GPIO2 - GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # GPIO0 - await asyncio.sleep(0.5) - GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP) # GPIO0 - await asyncio.sleep(0.5) - except Exception as e: - LOGGER.error('Unable to set PiZiGate GPIO, please check configuration') - LOGGER.error(str(e)) - - -def ftdi_set_bitmode(dev, bitmask): - ''' - Set mode for ZiGate DIN module - ''' - BITMODE_CBUS = 0x20 - SIO_SET_BITMODE_REQUEST = 0x0b - bmRequestType = usb.util.build_request_type(usb.util.CTRL_OUT, - usb.util.CTRL_TYPE_VENDOR, - usb.util.CTRL_RECIPIENT_DEVICE) - wValue = bitmask | (BITMODE_CBUS << BITMODE_CBUS) - dev.ctrl_transfer(bmRequestType, SIO_SET_BITMODE_REQUEST, wValue) - - -async def set_zigatedin_running_mode(): - try: - dev = usb.core.find(idVendor=0x0403, idProduct=0x6001) - if not dev: - LOGGER.error('ZiGate DIN not found.') - return - LOGGER.info('Put ZiGate DIN in running mode') - ftdi_set_bitmode(dev, 0xC8) - await asyncio.sleep(0.5) - ftdi_set_bitmode(dev, 0xCC) - await asyncio.sleep(0.5) - except Exception as e: - LOGGER.error('Unable to set FTDI bitmode, please check configuration') - LOGGER.error(str(e)) - - -async def set_zigatedin_flashing_mode(): - try: - dev = usb.core.find(idVendor=0x0403, idProduct=0x6001) - if not dev: - LOGGER.error('ZiGate DIN not found.') - return - LOGGER.info('Put ZiGate DIN in flashing mode') - ftdi_set_bitmode(dev, 0x00) - await asyncio.sleep(0.5) - ftdi_set_bitmode(dev, 0xCC) - await asyncio.sleep(0.5) - ftdi_set_bitmode(dev, 0xC0) - await asyncio.sleep(0.5) - ftdi_set_bitmode(dev, 0xC4) - await asyncio.sleep(0.5) - ftdi_set_bitmode(dev, 0xCC) - await asyncio.sleep(0.5) - except Exception as e: - LOGGER.error('Unable to set FTDI bitmode, please check configuration') - LOGGER.error(str(e)) From ceabcc8e57dc6e6e3d2ecf2e7e840023977a2cc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sun, 8 Nov 2020 18:34:20 +0100 Subject: [PATCH 28/63] fix initialization --- zigpy_zigate/uart.py | 2 ++ zigpy_zigate/zigbee/application.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/zigpy_zigate/uart.py b/zigpy_zigate/uart.py index 19464dd..f5184da 100644 --- a/zigpy_zigate/uart.py +++ b/zigpy_zigate/uart.py @@ -63,12 +63,14 @@ def data_received(self, data): length, len(frame) - 6) self._buffer = self._buffer[endpos + 1:] + endpos = self._buffer.find(self.END) continue if self._checksum(frame[:4], lqi, f_data) != checksum: LOGGER.warning("Invalid checksum: %s, data: 0x%s", checksum, binascii.hexlify(frame).decode()) self._buffer = self._buffer[endpos + 1:] + endpos = self._buffer.find(self.END) continue LOGGER.debug("Frame received: %s", binascii.hexlify(frame).decode()) self._api.data_received(cmd, f_data, lqi) diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index cc7cef2..350b067 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -35,7 +35,11 @@ def __init__(self, config: Dict[str, Any]): async def startup(self, auto_form=False): """Perform a complete application startup""" self._api = await ZiGate.new(self._config[CONF_DEVICE], self) - await self._api.set_raw_mode() + try: + await self._api.set_raw_mode() + except NoResponseError: + # retry, sometimes after reboot it fails for some reasons + await self._api.set_raw_mode() version, lqi = await self._api.version() version = '{:x}'.format(version[1]) version = '{}.{}'.format(version[0], version[1:]) From 27cf586d3aa44b72864e3f10950ba07805164744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sun, 8 Nov 2020 18:37:02 +0100 Subject: [PATCH 29/63] fix pizigate gpio command --- zigpy_zigate/common.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/zigpy_zigate/common.py b/zigpy_zigate/common.py index 856068e..bd85d69 100644 --- a/zigpy_zigate/common.py +++ b/zigpy_zigate/common.py @@ -35,11 +35,13 @@ async def set_pizigate_running_mode(): import RPi.GPIO as GPIO LOGGER.info('Put PiZiGate in running mode') GPIO.setmode(GPIO.BCM) + GPIO.setup(17, GPIO.OUT) # GPIO0 GPIO.setup(27, GPIO.OUT) # GPIO2 - GPIO.output(27, GPIO.HIGH) # GPIO2 - GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # GPIO0 + GPIO.output(27, GPIO.HIGH) await asyncio.sleep(0.5) - GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP) # GPIO0 + GPIO.output(17, GPIO.LOW) + await asyncio.sleep(0.5) + GPIO.output(17, GPIO.HIGH) await asyncio.sleep(0.5) except Exception as e: LOGGER.error('Unable to set PiZiGate GPIO, please check configuration') @@ -51,11 +53,13 @@ async def set_pizigate_flashing_mode(): import RPi.GPIO as GPIO LOGGER.info('Put PiZiGate in flashing mode') GPIO.setmode(GPIO.BCM) + GPIO.setup(17, GPIO.OUT) # GPIO0 GPIO.setup(27, GPIO.OUT) # GPIO2 - GPIO.output(27, GPIO.LOW) # GPIO2 - GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # GPIO0 + GPIO.output(27, GPIO.LOW) + await asyncio.sleep(0.5) + GPIO.output(17, GPIO.LOW) await asyncio.sleep(0.5) - GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP) # GPIO0 + GPIO.output(17, GPIO.HIGH) await asyncio.sleep(0.5) except Exception as e: LOGGER.error('Unable to set PiZiGate GPIO, please check configuration') From 4be3b57d3c91b57510353d22296fc4f92ebc860e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 12:53:43 +0100 Subject: [PATCH 30/63] move discover_port to common --- zigpy_zigate/common.py | 17 +++++++++++++++++ zigpy_zigate/uart.py | 13 +------------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/zigpy_zigate/common.py b/zigpy_zigate/common.py index bd85d69..11ebfb2 100644 --- a/zigpy_zigate/common.py +++ b/zigpy_zigate/common.py @@ -7,6 +7,23 @@ LOGGER = logging.getLogger(__name__) +def discover_port(): + """ discover zigate port """ + devices = list(serial.tools.list_ports.grep('ZiGate')) + if devices: + port = devices[0].device + LOGGER.info('ZiGate found at %s', port) + else: + devices = list(serial.tools.list_ports.grep('067b:2303|CP2102')) + if devices: + port = devices[0].device + LOGGER.info('ZiGate probably found at %s', port) + else: + LOGGER.error('Unable to find ZiGate using auto mode') + raise serial.SerialException("Unable to find Zigate using auto mode") + return port + + def is_pizigate(port): """ detect pizigate """ # Suppose pizigate on /dev/ttyAMAx or /dev/serialx diff --git a/zigpy_zigate/uart.py b/zigpy_zigate/uart.py index f5184da..f40d54c 100644 --- a/zigpy_zigate/uart.py +++ b/zigpy_zigate/uart.py @@ -125,18 +125,7 @@ async def connect(device_config: Dict[str, Any], api, loop=None): port = device_config[CONF_DEVICE_PATH] if port == 'auto': - devices = list(serial.tools.list_ports.grep('ZiGate')) - if devices: - port = devices[0].device - LOGGER.info('ZiGate found at %s', port) - else: - devices = list(serial.tools.list_ports.grep('067b:2303|CP2102')) - if devices: - port = devices[0].device - LOGGER.info('ZiGate probably found at %s', port) - else: - LOGGER.error('Unable to find ZiGate using auto mode') - raise serial.SerialException("Unable to find Zigate using auto mode") + port = c.discover_port() if c.is_zigate_wifi(port): LOGGER.debug('ZiGate WiFi detected') From 298774a70aa6dfd0c1394c039548c436972ae7bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 12:59:44 +0100 Subject: [PATCH 31/63] fix flasher --- zigpy_zigate/common.py | 1 + zigpy_zigate/tools/flasher.py | 55 ++++++++++++++++++----------------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/zigpy_zigate/common.py b/zigpy_zigate/common.py index 11ebfb2..30c3703 100644 --- a/zigpy_zigate/common.py +++ b/zigpy_zigate/common.py @@ -1,5 +1,6 @@ import re import serial.tools.list_ports +import serial import usb import logging import asyncio diff --git a/zigpy_zigate/tools/flasher.py b/zigpy_zigate/tools/flasher.py index 2dde00b..1569fe1 100644 --- a/zigpy_zigate/tools/flasher.py +++ b/zigpy_zigate/tools/flasher.py @@ -16,7 +16,7 @@ from operator import xor import datetime from .firmware import download_latest -from .transport import discover_port +from zigpy_zigate import common as c import time import serial from serial.tools.list_ports import comports @@ -34,7 +34,7 @@ def __getattr__(self, *args, **kwargs): import usb -logger = logging.getLogger('ZiGate Flasher') +LOGGER = logging.getLogger(__name__) _responses = {} ZIGATE_CHIP_ID = 0x10408686 @@ -47,7 +47,7 @@ class Command: def __init__(self, type_, fmt=None, raw=False): assert not (raw and fmt), 'Raw commands cannot use built-in struct formatting' - logger.debug('Command {} {} {}'.format(type_, fmt, raw)) + LOGGER.debug('Command {} {} {}'.format(type_, fmt, raw)) self.type = type_ self.raw = raw if fmt: @@ -78,7 +78,7 @@ def wrapper(*args, **kwargs): class Response: def __init__(self, type_, data, chksum): - logger.debug('Response {} {} {}'.format(type_, data, chksum)) + LOGGER.debug('Response {} {} {}'.format(type_, data, chksum)) self.type = type_ self.data = data[1:] self.chksum = chksum @@ -120,18 +120,18 @@ def prepare(type_, data): def read_response(ser): length = ser.read() length = int.from_bytes(length, 'big') - logger.debug('read_response length {}'.format(length)) + LOGGER.debug('read_response length {}'.format(length)) answer = ser.read(length) - logger.debug('read_response answer {}'.format(answer)) + LOGGER.debug('read_response answer {}'.format(answer)) return _unpack_raw_message(length, answer) # type_, data, chksum = struct.unpack('!B%dsB' % (length - 2), answer) # return {'type': type_, 'data': data, 'chksum': chksum} def _unpack_raw_message(length, decoded): - logger.debug('unpack raw message {} {}'.format(length, decoded)) + LOGGER.debug('unpack raw message {} {}'.format(length, decoded)) if len(decoded) != length or length < 2: - logger.exception("Unpack failed, length: %d, msg %s" % (length, decoded.hex())) + LOGGER.exception("Unpack failed, length: %d, msg %s" % (length, decoded.hex())) return type_, data, chksum = \ struct.unpack('!B%dsB' % (length - 2), decoded) @@ -238,7 +238,7 @@ def change_baudrate(ser, baudrate): res = read_response(ser) if not res or not res.ok: - logger.exception('Change baudrate failed') + LOGGER.exception('Change baudrate failed') raise SystemExit(1) ser.baudrate = baudrate @@ -248,10 +248,10 @@ def check_chip_id(ser): ser.write(req_chip_id()) res = read_response(ser) if not res or not res.ok: - logger.exception('Getting Chip ID failed') + LOGGER.exception('Getting Chip ID failed') raise SystemExit(1) if res.chip_id != ZIGATE_CHIP_ID: - logger.exception('This is not a supported chip, patches welcome') + LOGGER.exception('This is not a supported chip, patches welcome') raise SystemExit(1) @@ -314,7 +314,7 @@ def write_flash_to_file(ser, filename): cur = ZIGATE_FLASH_START flash_end = ZIGATE_FLASH_END - logger.info('Backup firmware to %s', filename) + LOGGER.info('Backup firmware to %s', filename) with open(filename, 'wb') as fd: fd.write(ZIGATE_BINARY_VERSION) read_bytes = 128 @@ -332,11 +332,11 @@ def write_flash_to_file(ser, filename): printProgressBar(cur, flash_end, 'Reading') cur += read_bytes printProgressBar(flash_end, flash_end, 'Reading') - logger.info('Backup firmware done') + LOGGER.info('Backup firmware done') def write_file_to_flash(ser, filename): - logger.info('Writing new firmware from %s', filename) + LOGGER.info('Writing new firmware from %s', filename) with open(filename, 'rb') as fd: ser.write(req_flash_erase()) res = read_response(ser) @@ -365,7 +365,7 @@ def write_file_to_flash(ser, filename): printProgressBar(cur, flash_end, 'Writing') cur += read_bytes printProgressBar(flash_end, flash_end, 'Writing') - logger.info('Writing new firmware done') + LOGGER.info('Writing new firmware done') def erase_EEPROM(ser, pdm_only=False): @@ -381,18 +381,19 @@ def flash(serialport='auto', write=None, save=None, erase=False, pdm_only=False) """ Read or write firmware """ - serialport = discover_port(serialport) + if serialport == 'auto': + serialport = c.discover_port() try: ser = serial.Serial(serialport, 38400, timeout=5) except serial.SerialException: - logger.exception("Could not open serial device %s", serialport) + LOGGER.exception("Could not open serial device %s", serialport) return change_baudrate(ser, 115200) check_chip_id(ser) flash_type = get_flash_type(ser) mac_address = get_mac(ser) - logger.info('Found MAC-address: %s', mac_address) + LOGGER.info('Found MAC-address: %s', mac_address) if write or save or erase: select_flash(ser, flash_type) @@ -446,12 +447,12 @@ def main(): parser.add_argument('--gpio', help='Configure GPIO for PiZiGate flash', action='store_true', default=False) parser.add_argument('--din', help='Configure USB for ZiGate DIN flash', action='store_true', default=False) args = parser.parse_args() - logger.setLevel(logging.INFO) + LOGGER.setLevel(logging.INFO) if args.debug: - logger.setLevel(logging.DEBUG) + LOGGER.setLevel(logging.DEBUG) if args.gpio: - logger.info('Put PiZiGate in flash mode') + LOGGER.info('Put PiZiGate in flash mode') GPIO.setmode(GPIO.BCM) GPIO.setup(27, GPIO.OUT) # GPIO2 GPIO.output(27, GPIO.LOW) # GPIO2 @@ -460,10 +461,10 @@ def main(): GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP) # GPIO0 time.sleep(0.5) elif args.din: - logger.info('Put ZiGate DIN in flash mode') + LOGGER.info('Put ZiGate DIN in flash mode') dev = usb.core.find(idVendor=0x0403, idProduct=0x6001) if not dev: - logger.error('ZiGate DIN not found.') + LOGGER.error('ZiGate DIN not found.') return ftdi_set_bitmode(dev, 0x00) time.sleep(0.5) @@ -486,14 +487,14 @@ def main(): try: ser = serial.Serial(args.serialport, 38400, timeout=5) except serial.SerialException: - logger.exception("Could not open serial device %s", args.serialport) + LOGGER.exception("Could not open serial device %s", args.serialport) raise SystemExit(1) change_baudrate(ser, 115200) check_chip_id(ser) flash_type = get_flash_type(ser) mac_address = get_mac(ser) - logger.info('Found MAC-address: %s', mac_address) + LOGGER.info('Found MAC-address: %s', mac_address) if args.write or args.save: # or args.erase: select_flash(ser, flash_type) @@ -507,14 +508,14 @@ def main(): # erase_EEPROM(ser, args.pdm_only) if args.gpio: - logger.info('Put PiZiGate in running mode') + LOGGER.info('Put PiZiGate in running mode') GPIO.output(27, GPIO.HIGH) # GPIO2 GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # GPIO0 time.sleep(0.5) GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP) # GPIO0 time.sleep(0.5) elif args.din: - logger.info('Put ZiGate DIN in running mode') + LOGGER.info('Put ZiGate DIN in running mode') ftdi_set_bitmode(dev, 0xC8) time.sleep(0.5) ftdi_set_bitmode(dev, 0xCC) From c3bcd0080e623dabca3ba517cfa5401373aa9efb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 13:07:08 +0100 Subject: [PATCH 32/63] move to AsyncMock --- .github/workflows/tests.yml | 2 +- tests/async_mock.py | 9 +++++++++ tests/test_api.py | 12 ++++++------ 3 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 tests/async_mock.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3dd1ff1..6436a37 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest asynctest coveralls pytest-cov pytest-asyncio + pip install flake8 pytest pytest-asyncio if [ -f requirements.txt ]; then pip install -r requirements.txt; fi pip install -e "." - name: Lint with flake8 diff --git a/tests/async_mock.py b/tests/async_mock.py new file mode 100644 index 0000000..8257ddd --- /dev/null +++ b/tests/async_mock.py @@ -0,0 +1,9 @@ +"""Mock utilities that are async aware.""" +import sys + +if sys.version_info[:2] < (3, 8): + from asynctest.mock import * # noqa + + AsyncMock = CoroutineMock # noqa: F405 +else: + from unittest.mock import * # noqa diff --git a/tests/test_api.py b/tests/test_api.py index 7cdeec6..6e960a3 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -3,7 +3,7 @@ import pytest import serial import serial_asyncio -from asynctest import CoroutineMock, mock +from .async_mock import AsyncMock, MagicMock, patch, sentinel import zigpy_zigate.config as config import zigpy_zigate.uart @@ -15,13 +15,13 @@ @pytest.fixture def api(): api = zigate_api.ZiGate(DEVICE_CONFIG) - api._uart = mock.MagicMock() + api._uart = MagicMock() return api def test_set_application(api): - api.set_application(mock.sentinel.app) - assert api._app == mock.sentinel.app + api.set_application(sentinel.app) + assert api._app == sentinel.app @pytest.mark.asyncio @@ -39,7 +39,7 @@ async def mock_conn(loop, protocol_factory, **kwargs): def test_close(api): - api._uart.close = mock.MagicMock() + api._uart.close = MagicMock() uart = api._uart api.close() assert uart.close.call_count == 1 @@ -47,7 +47,7 @@ def test_close(api): @pytest.mark.asyncio -@mock.patch.object(zigpy_zigate.uart, "connect") +@patch.object(zigpy_zigate.uart, "connect") async def test_api_new(conn_mck): """Test new class method.""" api = await zigate_api.ZiGate.new(DEVICE_CONFIG, mock.sentinel.application) From 688ead3b586aa25d5e6937114997439f76c25928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 13:09:10 +0100 Subject: [PATCH 33/63] move to AsyncMock --- tests/test_api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 6e960a3..8b7f6c8 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -50,15 +50,15 @@ def test_close(api): @patch.object(zigpy_zigate.uart, "connect") async def test_api_new(conn_mck): """Test new class method.""" - api = await zigate_api.ZiGate.new(DEVICE_CONFIG, mock.sentinel.application) + api = await zigate_api.ZiGate.new(DEVICE_CONFIG, sentinel.application) assert isinstance(api, zigate_api.ZiGate) assert conn_mck.call_count == 1 assert conn_mck.await_count == 1 @pytest.mark.asyncio -@mock.patch.object(zigate_api.ZiGate, "set_raw_mode", new_callable=CoroutineMock) -@mock.patch.object(zigpy_zigate.uart, "connect") +@patch.object(zigate_api.ZiGate, "set_raw_mode", new_callable=CoroutineMock) +@patch.object(zigpy_zigate.uart, "connect") async def test_probe_success(mock_connect, mock_raw_mode): """Test device probing.""" @@ -72,8 +72,8 @@ async def test_probe_success(mock_connect, mock_raw_mode): @pytest.mark.asyncio -@mock.patch.object(zigate_api.ZiGate, "set_raw_mode", side_effect=asyncio.TimeoutError) -@mock.patch.object(zigpy_zigate.uart, "connect") +@patch.object(zigate_api.ZiGate, "set_raw_mode", side_effect=asyncio.TimeoutError) +@patch.object(zigpy_zigate.uart, "connect") @pytest.mark.parametrize( "exception", (asyncio.TimeoutError, serial.SerialException, zigate_api.NoResponseError), From db816cd27517583732745a9933f38e38953f0dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 13:15:15 +0100 Subject: [PATCH 34/63] move to AsyncMock --- .github/workflows/tests.yml | 2 +- setup.py | 4 +++- tests/test_api.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6436a37..2a7b6cd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest pytest-asyncio + pip install flake8 pytest pytest-asyncio asynctest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi pip install -e "." - name: Lint with flake8 diff --git a/setup.py b/setup.py index 844d1a9..78c53b0 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ def is_raspberry_pi(raise_on_errors=False): description="A library which communicates with ZiGate radios for zigpy", long_description=long_description, long_description_content_type="text/markdown", - url="http://github.com/doudz/zigpy-zigate", + url="http://github.com/zigpy/zigpy-zigate", author="Sébastien RAMAGE", author_email="sebatien.ramage@gmail.com", license="GPL-3.0", @@ -75,5 +75,7 @@ def is_raspberry_pi(raise_on_errors=False): install_requires=requires, tests_require=[ 'pytest', + 'pytest-asyncio', + 'asynctest' ], ) diff --git a/tests/test_api.py b/tests/test_api.py index 8b7f6c8..10cc1ac 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -57,7 +57,7 @@ async def test_api_new(conn_mck): @pytest.mark.asyncio -@patch.object(zigate_api.ZiGate, "set_raw_mode", new_callable=CoroutineMock) +@patch.object(zigate_api.ZiGate, "set_raw_mode", new_callable=AsyncMock) @patch.object(zigpy_zigate.uart, "connect") async def test_probe_success(mock_connect, mock_raw_mode): """Test device probing.""" From a6c21b6529a6fc5b85440bba685e1d6b98807f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 13:19:15 +0100 Subject: [PATCH 35/63] fix test --- tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 826da071b1e4e6b45330841a53ad9191289f5986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 13:22:23 +0100 Subject: [PATCH 36/63] remove trailing space --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 78c53b0..9a8faa2 100644 --- a/setup.py +++ b/setup.py @@ -75,7 +75,7 @@ def is_raspberry_pi(raise_on_errors=False): install_requires=requires, tests_require=[ 'pytest', - 'pytest-asyncio', + 'pytest-asyncio', 'asynctest' ], ) From 47d47e20dd0386cce13d29d08c527b986fa69b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 13:48:49 +0100 Subject: [PATCH 37/63] update readme --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 9fd2339..59f0910 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,32 @@ __pizigate__ may requiert some adjustements on Rpi3 or Rpi4: Note! Requires ZiGate firmware 3.1d and later - https://zigate.fr/tag/firmware/ +## Flasher + +Python tool to flash your Zigate (Jennic JN5168) + +Thanks to Sander Hoentjen (tjikkun) we now have a flasher ! +[Original repo](https://github.com/tjikkun/zigate-flasher) + +### Flasher Usage + +```bash +usage: python3 -m zigate.flasher [-h] -p {/dev/ttyUSB0} [-w WRITE] [-s SAVE] [-u] [-d] [--gpio] [--din] + +optional arguments: + -h, --help show this help message and exit + -p {/dev/ttyUSB0}, --serialport {/dev/ttyUSB0} + Serial port, e.g. /dev/ttyUSB0 + -w WRITE, --write WRITE + Firmware bin to flash onto the chip + -s SAVE, --save SAVE File to save the currently loaded firmware to + -u, --upgrade Download and flash the lastest available firmware + -d, --debug Set log level to DEBUG + --gpio Configure GPIO for PiZiGate flash + --din Configure USB for ZiGate DIN flash + +``` + ## Testing new releases Testing a new release of the zigpy-zigate library before it is released in Home Assistant. From bd12909ccd3c5eddd776da8acf2bd6764094ae25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 13:49:23 +0100 Subject: [PATCH 38/63] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 59f0910..4a8cf23 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Thanks to Sander Hoentjen (tjikkun) we now have a flasher ! ### Flasher Usage ```bash -usage: python3 -m zigate.flasher [-h] -p {/dev/ttyUSB0} [-w WRITE] [-s SAVE] [-u] [-d] [--gpio] [--din] +usage: python3 -m zigpy_zigate.tools.flasher [-h] -p {/dev/ttyUSB0} [-w WRITE] [-s SAVE] [-u] [-d] [--gpio] [--din] optional arguments: -h, --help show this help message and exit From 2d3b39b12972873d47b808bbf6ffe4185cac0a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 13:53:00 +0100 Subject: [PATCH 39/63] add release drafter --- .github/release-drafter.yml | 22 ++++++++++++++++++++++ .github/workflows/release-management.yml | 16 ++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/release-management.yml diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..004002a --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,22 @@ +name-template: '$NEXT_PATCH_VERSION Release.' +tag-template: '$NEXT_PATCH_VERSION' +categories: + - title: 'Breaking changes' + labels: + - 'breaking' + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '🧰 Maintenance' + label: 'chore' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +template: | + ## Changes + + $CHANGES diff --git a/.github/workflows/release-management.yml b/.github/workflows/release-management.yml new file mode 100644 index 0000000..40e9702 --- /dev/null +++ b/.github/workflows/release-management.yml @@ -0,0 +1,16 @@ +name: Release Management + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + +jobs: + update_draft_release: + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 6f43b7143bd83ada358d822b73adad82d56f4ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 14:10:02 +0100 Subject: [PATCH 40/63] fix asyncmock --- .github/workflows/tests.yml | 2 +- setup.py | 2 +- tests/async_mock.py | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2a7b6cd..57ef587 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest pytest-asyncio asynctest + pip install flake8 pytest pytest-asyncio mock if [ -f requirements.txt ]; then pip install -r requirements.txt; fi pip install -e "." - name: Lint with flake8 diff --git a/setup.py b/setup.py index 9a8faa2..5b426c2 100644 --- a/setup.py +++ b/setup.py @@ -76,6 +76,6 @@ def is_raspberry_pi(raise_on_errors=False): tests_require=[ 'pytest', 'pytest-asyncio', - 'asynctest' + 'mock' ], ) diff --git a/tests/async_mock.py b/tests/async_mock.py index 8257ddd..7d6b494 100644 --- a/tests/async_mock.py +++ b/tests/async_mock.py @@ -2,8 +2,6 @@ import sys if sys.version_info[:2] < (3, 8): - from asynctest.mock import * # noqa - - AsyncMock = CoroutineMock # noqa: F405 + from mock import * # noqa else: from unittest.mock import * # noqa From e93b8c1563326c0aa7a75474e72e56a4c113f683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 14:22:55 +0100 Subject: [PATCH 41/63] remove travis --- .travis.yml | 13 ------------- README.md | 2 +- tox.ini | 28 ---------------------------- 3 files changed, 1 insertion(+), 42 deletions(-) delete mode 100644 .travis.yml delete mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 333d47a..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: python -matrix: - fast_finish: true - include: - - python: "3.7" - env: TOXENV=lint - - python: "3.7" - env: TOXENV=py37 - - python: "3.8" - env: TOXENV=py38 -install: pip install -U setuptools tox coveralls -script: tox -after_success: coveralls diff --git a/README.md b/README.md index 4a8cf23..49daf3a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # zigpy-zigate -[![Build Status](https://travis-ci.com/zigpy/zigpy-zigate.svg?branch=master)](https://travis-ci.com/zigpy/zigpy-zigate) +![Build & Tests](https://github.com/zigpy/zigpy-zigate/workflows/Build%20&%20Tests/badge.svg?branch=master) [![Coverage](https://coveralls.io/repos/github/zigpy/zigpy-zigate/badge.svg?branch=master)](https://coveralls.io/github/zigpy/zigpy-zigate?branch=master) **WARNING: EXPERIMENTAL! This project is under development as WIP (work in progress). Developer’s work provided “AS IS”.** diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 8c390df..0000000 --- a/tox.ini +++ /dev/null @@ -1,28 +0,0 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, "pip install tox" -# and then run "tox" from this directory. - -[tox] -envlist = py37, py38, lint -skip_missing_interpreters = True - -[testenv] -setenv = PYTHONPATH = {toxinidir} -install_command = pip install {opts} {packages} -commands = py.test --cov --cov-report= -deps = - asynctest - coveralls - pytest - pytest-cov - pytest-asyncio - -[testenv:lint] -basepython = python3 -deps = flake8 -commands = flake8 - -[flake8] -ignore = E501 -max-complexity = 16 From 5082134b5ef01c79a6607f862ca02e80f5cb26c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 21:58:13 +0100 Subject: [PATCH 42/63] improve probing --- zigpy_zigate/api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index bc1cb3e..6f8cd99 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -199,6 +199,7 @@ async def probe(cls, device_config: Dict[str, Any]) -> bool: await asyncio.wait_for(api._probe(), timeout=COMMAND_TIMEOUT) return True except ( + StopIteration, asyncio.TimeoutError, serial.SerialException, zigpy.exceptions.ZigbeeException, @@ -215,5 +216,8 @@ async def probe(cls, device_config: Dict[str, Any]) -> bool: async def _probe(self) -> None: """Open port and try sending a command""" + device = next(serial.tools.list_ports.grep(self._config[zigpy_zigate.config.CONF_DEVICE_PATH])) + if device.description == 'ZiGate': + return await self.connect() await self.set_raw_mode() From b931837b9373cdca3f14ebbe86ad3e8ec467a060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 22:05:37 +0100 Subject: [PATCH 43/63] Improve probing --- zigpy_zigate/api.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index 6f8cd99..3f22f53 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -199,7 +199,6 @@ async def probe(cls, device_config: Dict[str, Any]) -> bool: await asyncio.wait_for(api._probe(), timeout=COMMAND_TIMEOUT) return True except ( - StopIteration, asyncio.TimeoutError, serial.SerialException, zigpy.exceptions.ZigbeeException, @@ -216,8 +215,11 @@ async def probe(cls, device_config: Dict[str, Any]) -> bool: async def _probe(self) -> None: """Open port and try sending a command""" - device = next(serial.tools.list_ports.grep(self._config[zigpy_zigate.config.CONF_DEVICE_PATH])) - if device.description == 'ZiGate': - return + try: + device = next(serial.tools.list_ports.grep(self._config[zigpy_zigate.config.CONF_DEVICE_PATH])) + if device.description == 'ZiGate': + return + except StopIteration: + pass await self.connect() await self.set_raw_mode() From 9706ba796ea320a0cfcbc67dc44a1ef2d25f0ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 22:07:33 +0100 Subject: [PATCH 44/63] add erase persistent command --- zigpy_zigate/api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index 3f22f53..d755f27 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -149,6 +149,9 @@ async def set_raw_mode(self, enable=True): async def reset(self): self._command(0x0011, wait_status=False) + async def erase_persistent_data(self): + self._command(0x0012, wait_status=False) + async def set_channel(self, channels=None): channels = channels or [11, 14, 15, 19, 20, 24, 25, 26] if not isinstance(channels, list): From affa15639edd5a3371f00a992d6cf6895ddb7ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 22:53:09 +0100 Subject: [PATCH 45/63] add many api command and simple cli tool --- zigpy_zigate/api.py | 28 +++++++++++++++++ zigpy_zigate/tools/cli.py | 49 ++++++++++++++++++++++++++++++ zigpy_zigate/zigbee/application.py | 1 + 3 files changed, 78 insertions(+) create mode 100644 zigpy_zigate/tools/cli.py diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index d755f27..037f5dc 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -3,6 +3,7 @@ import functools import logging import enum +import datetime from typing import Any, Dict import serial @@ -25,6 +26,7 @@ 0x8009: (t.NWK, t.EUI64, t.uint16_t, t.uint64_t, t.uint8_t), 0x8010: (t.uint16_t, t.uint16_t), 0x8011: (t.uint8_t, t.NWK, t.uint8_t, t.uint16_t, t.uint8_t), + 0x8017: (t.uint32_t,), 0x8024: (t.uint8_t, t.NWK, t.EUI64, t.uint8_t), 0x8035: (t.uint8_t, t.uint32_t), 0x8048: (t.EUI64, t.uint8_t), @@ -34,6 +36,8 @@ COMMANDS = { 0x0002: (t.uint8_t,), + 0x0016: (t.uint32_t,), + 0x0018: (t.uint8_t,), 0x0020: (t.uint64_t,), 0x0021: (t.uint32_t,), 0x0026: (t.EUI64, t.EUI64), @@ -139,6 +143,12 @@ def _command(self, cmd, data=b'', wait_response=None, wait_status=True): async def version(self): return await self.command(0x0010, wait_response=0x8010) + async def version_str(self): + version, lqi = await self.version() + version = '{:x}'.format(version[1]) + version = '{}.{}'.format(version[0], version[1:]) + return version + async def get_network_state(self): return await self.command(0x0009, wait_response=0x8009) @@ -151,6 +161,24 @@ async def reset(self): async def erase_persistent_data(self): self._command(0x0012, wait_status=False) + + async def set_time(self, dt=None): + """ set internal time + if timestamp is None, now is used + """ + dt = dt or datetime.datetime.now() + timestamp = int((dt - datetime.datetime(2000, 1, 1)).total_seconds()) + data = t.serialize([timestamp], COMMANDS[0x0016]) + self._command(0x0016, data) + + async def get_time_server(self): + timestamp, lqi = await self._command(0x0017, wait_response=0x8017) + dt = datetime.datetime(2000, 1, 1) + datetime.timedelta(seconds=timestamp[0]) + return dt + + async def set_led(self, enable=True): + data = t.serialize([enable], COMMANDS[0x0018]) + await self.command(0x0018, data) async def set_channel(self, channels=None): channels = channels or [11, 14, 15, 19, 20, 24, 25, 26] diff --git a/zigpy_zigate/tools/cli.py b/zigpy_zigate/tools/cli.py new file mode 100644 index 0000000..4febccc --- /dev/null +++ b/zigpy_zigate/tools/cli.py @@ -0,0 +1,49 @@ +""" + Simple CLI ZiGate tool +""" + +import argparse +import asyncio +import logging +import time +from zigpy_zigate.api import ZiGate +import zigpy_zigate.config + + +async def main(): + logging.basicConfig(level=logging.INFO) + parser = argparse.ArgumentParser() + parser.add_argument("command", help="Command to start", + choices=["version", "reset", "erase_persistent", + "set_time", "get_time", "set_led"]) + parser.add_argument("-p", "--port", help="Port", default='auto') + parser.add_argument("-d", "--debug", help="Debug log", action='store_true') + parser.add_argument("-v", "--value", help="Set command's value") + args = parser.parse_args() + print('Port set to', args.port) + if args.debug: + logging.root.setLevel(logging.DEBUG) + device_config = {zigpy_zigate.config.CONF_DEVICE_PATH: args.port} + api = ZiGate(device_config) + await api.connect() + if args.command == 'version': + print('Firmware version', await api.version_str()) + elif args.command == 'reset': + await api.reset() + print('ZiGate reseted') + elif args.command == 'erase_persistent': + await api.erase_persistent_data() + print('ZiGate pesistent date erased') + elif args.command == 'set_time': + await api.set_time() + print('ZiGate internal time server set to current time') + elif args.command == 'get_time': + print('ZiGate internal time server is', await api.get_time_server()) + elif args.command == 'set_led': + enable = int(args.value or 1) + print('Set ZiGate led to', enable) + await api.set_led(enable) + api.close() + +if __name__ == '__main__': + asyncio.run(main()) \ No newline at end of file diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index 350b067..8014049 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -40,6 +40,7 @@ async def startup(self, auto_form=False): except NoResponseError: # retry, sometimes after reboot it fails for some reasons await self._api.set_raw_mode() + await self._api.set_time() version, lqi = await self._api.version() version = '{:x}'.format(version[1]) version = '{}.{}'.format(version[0], version[1:]) From 2c636425f251acb88ee8d98eb050c093c386f59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 23:08:13 +0100 Subject: [PATCH 46/63] add certification and tx power commands --- zigpy_zigate/api.py | 16 ++++++++++++++++ zigpy_zigate/tools/cli.py | 10 +++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index 037f5dc..efeb03c 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -32,17 +32,20 @@ 0x8048: (t.EUI64, t.uint8_t), 0x8701: (t.uint8_t, t.uint8_t), 0x8702: (t.uint8_t, t.uint8_t, t.uint8_t, t.Address, t.uint8_t), + 0x8806: (t.uint8_t,), } COMMANDS = { 0x0002: (t.uint8_t,), 0x0016: (t.uint32_t,), 0x0018: (t.uint8_t,), + 0x0019: (t.uint8_t,), 0x0020: (t.uint64_t,), 0x0021: (t.uint32_t,), 0x0026: (t.EUI64, t.EUI64), 0x0049: (t.NWK, t.uint8_t, t.uint8_t), 0x0530: (t.uint8_t, t.NWK, t.uint8_t, t.uint8_t, t.uint16_t, t.uint16_t, t.uint8_t, t.uint8_t, t.LBytes), + 0x0806: (t.uint8_t,), } @@ -180,6 +183,19 @@ async def set_led(self, enable=True): data = t.serialize([enable], COMMANDS[0x0018]) await self.command(0x0018, data) + async def set_certification(self, typ='CE'): + cert = {'CE': 1, 'FCC': 2}[typ] + data = t.serialize([cert], COMMANDS[0x0019]) + await self.command(0x0019, data) + + async def set_tx_power(self, power=63): + if power > 63: + power = 63 + if power < 0: + power = 0 + data = t.serialize([power], COMMANDS[0x0806]) + return await self.command(0x0806, data, wait_response=0x8806) + async def set_channel(self, channels=None): channels = channels or [11, 14, 15, 19, 20, 24, 25, 26] if not isinstance(channels, list): diff --git a/zigpy_zigate/tools/cli.py b/zigpy_zigate/tools/cli.py index 4febccc..3c27dfc 100644 --- a/zigpy_zigate/tools/cli.py +++ b/zigpy_zigate/tools/cli.py @@ -15,7 +15,7 @@ async def main(): parser = argparse.ArgumentParser() parser.add_argument("command", help="Command to start", choices=["version", "reset", "erase_persistent", - "set_time", "get_time", "set_led"]) + "set_time", "get_time", "set_led", "set_certification", "set_tx_power"]) parser.add_argument("-p", "--port", help="Port", default='auto') parser.add_argument("-d", "--debug", help="Debug log", action='store_true') parser.add_argument("-v", "--value", help="Set command's value") @@ -43,6 +43,14 @@ async def main(): enable = int(args.value or 1) print('Set ZiGate led to', enable) await api.set_led(enable) + elif args.command == 'set_certification': + _type = args.value or 'CE' + print('Set ZiGate Certification to', _type) + await api.set_certification(_type) + elif args.command == 'set_tx_power': + power = int(args.value or 63) + print('Set ZiGate TX Power to', power) + print(await api.set_tx_power(power)) api.close() if __name__ == '__main__': From 7511d68c8dd63df289e53a87fc893b3a3c2bcf30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 9 Nov 2020 23:09:22 +0100 Subject: [PATCH 47/63] add tx power command --- zigpy_zigate/api.py | 3 ++- zigpy_zigate/tools/cli.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index efeb03c..610a0cf 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -194,7 +194,8 @@ async def set_tx_power(self, power=63): if power < 0: power = 0 data = t.serialize([power], COMMANDS[0x0806]) - return await self.command(0x0806, data, wait_response=0x8806) + power, lqi = await self.command(0x0806, data, wait_response=0x8806) + return power[0] async def set_channel(self, channels=None): channels = channels or [11, 14, 15, 19, 20, 24, 25, 26] diff --git a/zigpy_zigate/tools/cli.py b/zigpy_zigate/tools/cli.py index 3c27dfc..451639a 100644 --- a/zigpy_zigate/tools/cli.py +++ b/zigpy_zigate/tools/cli.py @@ -50,7 +50,7 @@ async def main(): elif args.command == 'set_tx_power': power = int(args.value or 63) print('Set ZiGate TX Power to', power) - print(await api.set_tx_power(power)) + print('Tx power set to', await api.set_tx_power(power)) api.close() if __name__ == '__main__': From d8c8b9aed01d93bcab06af677815f521d610a06e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 10 Nov 2020 09:20:58 +0100 Subject: [PATCH 48/63] retry command --- tests/test_api.py | 11 +++++++++++ zigpy_zigate/api.py | 22 ++++++++++++++-------- zigpy_zigate/zigbee/application.py | 6 +----- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 10cc1ac..9b65be5 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -91,3 +91,14 @@ async def test_probe_fail(mock_connect, mock_raw_mode, exception): assert mock_connect.call_args[0][0] == DEVICE_CONFIG assert mock_raw_mode.call_count == 1 assert mock_connect.return_value.close.call_count == 1 + + +@pytest.mark.asyncio +@patch.object(zigate_api.ZiGate, "_command", side_effect=asyncio.TimeoutError) +async def test_api_command(mock_command, api): + """Test command method.""" + try: + await api.set_raw_mode() + except zigate_api.NoResponseError: + pass + assert mock_command.call_count == 2 diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index 610a0cf..4a789df 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -123,14 +123,20 @@ def data_received(self, cmd, data, lqi): self.handle_callback(cmd, data, lqi) async def command(self, cmd, data=b'', wait_response=None, wait_status=True): - try: - return await asyncio.wait_for( - self._command(cmd, data, wait_response, wait_status), - timeout=COMMAND_TIMEOUT - ) - except asyncio.TimeoutError: - LOGGER.warning("No response to command 0x{:04x}".format(cmd)) - raise NoResponseError + tries = 2 + while tries > 0: + tries -= 1 + try: + return await asyncio.wait_for( + self._command(cmd, data, wait_response, wait_status), + timeout=COMMAND_TIMEOUT + ) + except asyncio.TimeoutError: + LOGGER.warning("No response to command 0x{:04x}".format(cmd)) + if tries > 0: + LOGGER.warning("Retry command 0x{:04x}".format(cmd)) + else: + raise NoResponseError def _command(self, cmd, data=b'', wait_response=None, wait_status=True): self._uart.send(cmd, data) diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index 8014049..1fabe58 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -35,11 +35,7 @@ def __init__(self, config: Dict[str, Any]): async def startup(self, auto_form=False): """Perform a complete application startup""" self._api = await ZiGate.new(self._config[CONF_DEVICE], self) - try: - await self._api.set_raw_mode() - except NoResponseError: - # retry, sometimes after reboot it fails for some reasons - await self._api.set_raw_mode() + await self._api.set_raw_mode() await self._api.set_time() version, lqi = await self._api.version() version = '{:x}'.format(version[1]) From 3fb1b8c62bddf6dce8ff70773fa0a10ece59097e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 10 Nov 2020 09:35:21 +0100 Subject: [PATCH 49/63] fix log format --- zigpy_zigate/api.py | 6 +++--- zigpy_zigate/uart.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index 4a789df..1ca2b3b 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -110,7 +110,7 @@ def data_received(self, cmd, data, lqi): LOGGER.debug("data received %s %s LQI:%s", hex(cmd), binascii.hexlify(data), lqi) if cmd not in RESPONSES: - LOGGER.error('Received unhandled response 0x{:04x}'.format(cmd)) + LOGGER.error('Received unhandled response 0x%04x', cmd) return data, rest = t.deserialize(data, RESPONSES[cmd]) if cmd == 0x8000: @@ -132,9 +132,9 @@ async def command(self, cmd, data=b'', wait_response=None, wait_status=True): timeout=COMMAND_TIMEOUT ) except asyncio.TimeoutError: - LOGGER.warning("No response to command 0x{:04x}".format(cmd)) + LOGGER.warning("No response to command 0x%04x", cmd) if tries > 0: - LOGGER.warning("Retry command 0x{:04x}".format(cmd)) + LOGGER.warning("Retry command 0x%04x", cmd) else: raise NoResponseError diff --git a/zigpy_zigate/uart.py b/zigpy_zigate/uart.py index f40d54c..f2e58dc 100644 --- a/zigpy_zigate/uart.py +++ b/zigpy_zigate/uart.py @@ -37,7 +37,7 @@ def close(self): def send(self, cmd, data=b''): """Send data, taking care of escaping and framing""" - LOGGER.debug("Send: %s %s", hex(cmd), binascii.hexlify(data)) + LOGGER.debug("Send: 0x%04x %s", cmd, binascii.hexlify(data)) length = len(data) byte_head = struct.pack('!HH', cmd, length) checksum = self._checksum(byte_head, data) From ec0d6a715232664a7f283a2cf6c2d8faa27b073f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 10 Nov 2020 09:46:08 +0100 Subject: [PATCH 50/63] add command lock --- zigpy_zigate/api.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index 1ca2b3b..765aff8 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -84,9 +84,11 @@ def __init__(self, device_config: Dict[str, Any]): self._uart = None self._awaiting = {} self._status_awaiting = {} + self._lock = asyncio.Lock() self.network_state = None + @classmethod async def new(cls, config: Dict[str, Any], application=None) -> "ZiGate": api = cls(config) @@ -126,6 +128,8 @@ async def command(self, cmd, data=b'', wait_response=None, wait_status=True): tries = 2 while tries > 0: tries -= 1 + + await self._lock.acquire() try: return await asyncio.wait_for( self._command(cmd, data, wait_response, wait_status), @@ -137,11 +141,13 @@ async def command(self, cmd, data=b'', wait_response=None, wait_status=True): LOGGER.warning("Retry command 0x%04x", cmd) else: raise NoResponseError + finally: + self._lock.release() def _command(self, cmd, data=b'', wait_response=None, wait_status=True): self._uart.send(cmd, data) - fut = asyncio.Future() if wait_status: + fut = asyncio.Future() self._status_awaiting[cmd] = fut if wait_response: fut = asyncio.Future() From cbd703514d3a383a46d045c0d1ed5229499ad5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 10 Nov 2020 10:03:21 +0100 Subject: [PATCH 51/63] fix lint --- zigpy_zigate/api.py | 6 ++---- zigpy_zigate/tools/cli.py | 7 +++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index 765aff8..c2e04ce 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -88,7 +88,6 @@ def __init__(self, device_config: Dict[str, Any]): self.network_state = None - @classmethod async def new(cls, config: Dict[str, Any], application=None) -> "ZiGate": api = cls(config) @@ -128,7 +127,6 @@ async def command(self, cmd, data=b'', wait_response=None, wait_status=True): tries = 2 while tries > 0: tries -= 1 - await self._lock.acquire() try: return await asyncio.wait_for( @@ -176,7 +174,7 @@ async def reset(self): async def erase_persistent_data(self): self._command(0x0012, wait_status=False) - + async def set_time(self, dt=None): """ set internal time if timestamp is None, now is used @@ -190,7 +188,7 @@ async def get_time_server(self): timestamp, lqi = await self._command(0x0017, wait_response=0x8017) dt = datetime.datetime(2000, 1, 1) + datetime.timedelta(seconds=timestamp[0]) return dt - + async def set_led(self, enable=True): data = t.serialize([enable], COMMANDS[0x0018]) await self.command(0x0018, data) diff --git a/zigpy_zigate/tools/cli.py b/zigpy_zigate/tools/cli.py index 451639a..d1ec18a 100644 --- a/zigpy_zigate/tools/cli.py +++ b/zigpy_zigate/tools/cli.py @@ -5,7 +5,6 @@ import argparse import asyncio import logging -import time from zigpy_zigate.api import ZiGate import zigpy_zigate.config @@ -14,8 +13,8 @@ async def main(): logging.basicConfig(level=logging.INFO) parser = argparse.ArgumentParser() parser.add_argument("command", help="Command to start", - choices=["version", "reset", "erase_persistent", - "set_time", "get_time", "set_led", "set_certification", "set_tx_power"]) + choices=["version", "reset", "erase_persistent", + "set_time", "get_time", "set_led", "set_certification", "set_tx_power"]) parser.add_argument("-p", "--port", help="Port", default='auto') parser.add_argument("-d", "--debug", help="Debug log", action='store_true') parser.add_argument("-v", "--value", help="Set command's value") @@ -54,4 +53,4 @@ async def main(): api.close() if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file + asyncio.run(main()) From f7faefe8f8155bfe99174efaddd6c8cfee19d6a4 Mon Sep 17 00:00:00 2001 From: Hedda Date: Tue, 10 Nov 2020 10:34:02 +0100 Subject: [PATCH 52/63] Update README.md with developer reference Adding https://github.com/doudz/zigate as developer reference as suggested in https://github.com/zigpy/zigpy-zigate/pull/58 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ff778e2..6c10e2a 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ Tagged versions are also released via PyPI Documents that layout the serial protocol used for ZiGate serial interface communication can be found here: - https://github.com/fairecasoimeme/ZiGate/tree/master/Protocol +- https://github.com/doudz/zigate - https://github.com/Neonox31/zigate - https://github.com/nouknouk/node-zigate From de86e409d54213fb6ff7c754cb7c37b61a9ce515 Mon Sep 17 00:00:00 2001 From: Hedda Date: Tue, 10 Nov 2020 11:01:36 +0100 Subject: [PATCH 53/63] Update README.md with extended ZiGate Flasher Firmware Tool description Update README.md with extended ZiGate Flasher Firmware Tool description --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ff778e2..aed16a9 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,12 @@ __pizigate__ may requiert some adjustements on Rpi3 or Rpi4: Note! Requires ZiGate firmware 3.1d and later - https://zigate.fr/tag/firmware/ -## Flasher +## Flasher (ZiGate Firmware Tool) -Python tool to flash your Zigate (Jennic JN5168) +zigpy-zigate has an integrated Python "flasher" tool to flash firmware updates on your ZiGate (NXP Jennic JN5168). -Thanks to Sander Hoentjen (tjikkun) we now have a flasher ! -[Original repo](https://github.com/tjikkun/zigate-flasher) +Thanks to Sander Hoentjen (tjikkun) zigpy-zigate now has an integrated firmware flasher tool! +- [tjikkun original zigate-flasher repo](https://github.com/tjikkun/zigate-flasher) ### Flasher Usage From 2edf03cef18cb940125925070e1c2ac6fd803058 Mon Sep 17 00:00:00 2001 From: Hedda Date: Tue, 10 Nov 2020 11:15:20 +0100 Subject: [PATCH 54/63] Updated related projects in README.md Updated related projects in README.md as copied with https://github.com/zigpy/zigpy/blob/dev/README.md with the exception of Zigpy Deconz Parser as that is not related to zigate. --- README.md | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ff778e2..1b2ae91 100644 --- a/README.md +++ b/README.md @@ -115,17 +115,32 @@ Some developers might also be interested in receiving donations in the form of h ## Related projects -### Zigpy -[Zigpy](https://github.com/zigpy/zigpy)** is **[Zigbee protocol stack](https://en.wikipedia.org/wiki/Zigbee)** integration project to implement the **[Zigbee Home Automation](https://www.zigbee.org/)** standard as a Python 3 library. Zigbee Home Automation integration with zigpy allows you to connect one of many off-the-shelf Zigbee adapters using one of the available Zigbee radio library modules compatible with zigpy to control Zigbee based devices. There is currently support for controlling Zigbee device types such as binary sensors (e.g., motion and door sensors), sensors (e.g., temperature sensors), lightbulbs, switches, and fans. A working implementation of zigbe exist in **[Home Assistant](https://www.home-assistant.io)** (Python based open source home automation software) as part of its **[ZHA component](https://www.home-assistant.io/components/zha/)** +#### Zigpy +[Zigpy](https://github.com/zigpy/zigpy) is [Zigbee protocol stack](https://en.wikipedia.org/wiki/Zigbee) integration project to implement the [Zigbee Home Automation](https://www.zigbee.org/) standard as a Python 3 library. Zigbee Home Automation integration with zigpy allows you to connect one of many off-the-shelf Zigbee adapters using one of the available Zigbee radio library modules compatible with zigpy to control Zigbee based devices. There is currently support for controlling Zigbee device types such as binary sensors (e.g., motion and door sensors), sensors (e.g., temperature sensors), lightbulbs, switches, and fans. A working implementation of zigbe exist in [Home Assistant](https://www.home-assistant.io) (Python based open source home automation software) as part of its [ZHA component](https://www.home-assistant.io/components/zha/) -### ZHA Device Handlers -ZHA deviation handling in Home Assistant relies on on the third-party [ZHA Device Handlers](https://github.com/dmulcahey/zha-device-handlers) project. Zigbee devices that deviate from or do not fully conform to the standard specifications set by the [Zigbee Alliance](https://www.zigbee.org) may require the development of custom [ZHA Device Handlers](https://github.com/dmulcahey/zha-device-handlers) (ZHA custom quirks handler implementation) to for all their functions to work properly with the ZHA component in Home Assistant. These ZHA Device Handlers for Home Assistant can thus be used to parse custom messages to and from non-compliant Zigbee devices. The custom quirks implementations for zigpy implemented as ZHA Device Handlers for Home Assistant are a similar concept to that of [Hub-connected Device Handlers for the SmartThings Classics platform](https://docs.smartthings.com/en/latest/device-type-developers-guide/) as well as that of [Zigbee-Shepherd Converters as used by Zigbee2mqtt](https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html), meaning they are each virtual representations of a physical device that expose additional functionality that is not provided out-of-the-box by the existing integration between these platforms. +#### ZHA Device Handlers +ZHA deviation handling in Home Assistant relies on the third-party [ZHA Device Handlers](https://github.com/zigpy/zha-device-handlers) project. Zigbee devices that deviate from or do not fully conform to the standard specifications set by the [Zigbee Alliance](https://www.zigbee.org) may require the development of custom [ZHA Device Handlers](https://github.com/zigpy/zha-device-handlers) (ZHA custom quirks handler implementation) to for all their functions to work properly with the ZHA component in Home Assistant. These ZHA Device Handlers for Home Assistant can thus be used to parse custom messages to and from non-compliant Zigbee devices. The custom quirks implementations for zigpy implemented as ZHA Device Handlers for Home Assistant are a similar concept to that of [Hub-connected Device Handlers for the SmartThings platform](https://docs.smartthings.com/en/latest/device-type-developers-guide/) as well as that of [zigbee-herdsman converters as used by Zigbee2mqtt](https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html), meaning they are each virtual representations of a physical device that expose additional functionality that is not provided out-of-the-box by the existing integration between these platforms. -### ZHA Map -Home Assistant can build ZHA network topology map using the [zha-map](https://github.com/zha-ng/zha-map) project. +#### ZHA integration component for Home Assistant +[ZHA integration component for Home Assistant](https://www.home-assistant.io/integrations/zha/) is a reference implementation of the zigpy library as integrated into the core of [Home Assistant](https://www.home-assistant.io) (a Python based open source home automation software). There are also other GUI and non-GUI projects for Home Assistant's ZHA components which builds on or depends on its features and functions to enhance or improve its user-experience, some of those are listed and linked below. -### zha-network-visualization-card -[zha-network-visualization-card](https://github.com/dmulcahey/zha-network-visualization-card) is a custom Lovelace element for visualizing the ZHA Zigbee network in Home Assistant. +#### ZHA Custom Radios +[zha-custom-radios](https://github.com/zha-ng/zha-custom-radios) adds support for custom radio modules for zigpy to [[Home Assistant's ZHA (Zigbee Home Automation) integration component]](https://www.home-assistant.io/integrations/zha/). This custom component for Home Assistant allows users to test out new modules for zigpy in Home Assistant's ZHA integration component before they are integrated into zigpy ZHA and also helps developers new zigpy radio modules without having to modify the Home Assistant's source code. -### ZHA Network Card -[zha-network-card](https://github.com/dmulcahey/zha-network-card) is a custom Lovelace card that displays ZHA network and device information in Home Assistant +#### ZHA Custom +[zha_custom](https://github.com/Adminiuga/zha_custom) is a custom component package for Home Assistant (with its ZHA component for zigpy integration) that acts as zigpy commands service wrapper, when installed it allows you to enter custom commands via to zigy to example change advanced configuration and settings that are not available in the UI. + +#### ZHA Map +[zha-map](https://github.com/zha-ng/zha-map) for Home Assistant's ZHA component can build a Zigbee network topology map. + +#### ZHA Network Visualization Card +[zha-network-visualization-card](https://github.com/dmulcahey/zha-network-visualization-card) is a custom Lovelace element for Home Assistant which visualize the Zigbee network for the ZHA component. + +#### ZHA Network Card +[zha-network-card](https://github.com/dmulcahey/zha-network-card) is a custom Lovelace card for Home Assistant that displays ZHA component Zigbee network and device information in Home Assistant + +#### Zigzag +[Zigzag](https://github.com/Samantha-uk/zigzag) is an custom card/panel for [Home Assistant](https://www.home-assistant.io/) that displays a graphical layout of Zigbee devices and the connections between them. Zigzag can be installed as a panel or a custom card and relies on the data provided by the [zha-map](https://github.com/zha-ng/zha-map) integration commponent. + +#### ZHA Device Exporter +[zha-device-exporter](https://github.com/dmulcahey/zha-device-exporter) is a custom component for Home Assistant to allow the ZHA component to export lists of Zigbee devices. From c8af77c44ce42144439c249bcb1fc01fdca0a41e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 10 Nov 2020 11:44:38 +0100 Subject: [PATCH 55/63] improve model detection --- zigpy_zigate/zigbee/application.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index 1fabe58..3a4853f 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -9,8 +9,9 @@ import zigpy.util from zigpy_zigate import types as t +from zigpy_zigate import common as c from zigpy_zigate.api import NoResponseError, ZiGate, PDM_EVENT -from zigpy_zigate.config import CONF_DEVICE, CONFIG_SCHEMA, SCHEMA_DEVICE +from zigpy_zigate.config import CONF_DEVICE, CONF_DEVICE_PATH, CONFIG_SCHEMA, SCHEMA_DEVICE LOGGER = logging.getLogger(__name__) @@ -54,7 +55,7 @@ async def startup(self, auto_form=False): self._nwk = network_state[0] self._ieee = zigpy.types.EUI64(network_state[1]) - dev = ZiGateDevice(self, self._ieee, self._nwk, self.version) + dev = ZiGateDevice(self, self._ieee, self._nwk) self.devices[dev.ieee] = dev async def shutdown(self): @@ -194,11 +195,19 @@ async def broadcast(self, profile, cluster, src_ep, dst_ep, grpid, radius, class ZiGateDevice(zigpy.device.Device): - def __init__(self, application, ieee, nwk, version): + def __init__(self, application, ieee, nwk): """Initialize instance.""" super().__init__(application, ieee, nwk) - self._model = 'ZiGate {}'.format(version) + port = application._config['CONF_DEVICE_PATH'] + model = 'ZiGate USB-TTL' + if c.is_zigate_wifi(port): + model = 'ZiGate WiFi' + elif c.is_pizigate(port): + model = 'PiZiGate' + elif c.is_zigate_din(): + model = 'ZiGate USB-DIN' + self._model = '{} {}'.format(model, application.version) @property def manufacturer(self): From 1fde4182578e400da3da3daddc44467027705e4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 10 Nov 2020 11:48:13 +0100 Subject: [PATCH 56/63] improve model detection --- zigpy_zigate/zigbee/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index 3a4853f..a1a4e6a 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -199,7 +199,7 @@ def __init__(self, application, ieee, nwk): """Initialize instance.""" super().__init__(application, ieee, nwk) - port = application._config['CONF_DEVICE_PATH'] + port = application._config[CONF_DEVICE_PATH] model = 'ZiGate USB-TTL' if c.is_zigate_wifi(port): model = 'ZiGate WiFi' From 93366af24a26b4aa6eab1d422b8664275688cda9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 10 Nov 2020 11:51:42 +0100 Subject: [PATCH 57/63] improve model detection --- zigpy_zigate/zigbee/application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index a1a4e6a..ca84bda 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -11,7 +11,7 @@ from zigpy_zigate import types as t from zigpy_zigate import common as c from zigpy_zigate.api import NoResponseError, ZiGate, PDM_EVENT -from zigpy_zigate.config import CONF_DEVICE, CONF_DEVICE_PATH, CONFIG_SCHEMA, SCHEMA_DEVICE +from zigpy_zigate.config import CONF_DEVICE, CONFIG_SCHEMA, SCHEMA_DEVICE LOGGER = logging.getLogger(__name__) @@ -199,7 +199,7 @@ def __init__(self, application, ieee, nwk): """Initialize instance.""" super().__init__(application, ieee, nwk) - port = application._config[CONF_DEVICE_PATH] + port = application._config[CONF_DEVICE] model = 'ZiGate USB-TTL' if c.is_zigate_wifi(port): model = 'ZiGate WiFi' From 3944052e4a89085ec8936172f2d2ff39b7dc72bd Mon Sep 17 00:00:00 2001 From: Hedda Date: Tue, 10 Nov 2020 11:53:42 +0100 Subject: [PATCH 58/63] Update README.md with 3.1d firmware recommendations or requirements Update README.md with 3.1d firmware recommendations or requirements --- README.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index bfd7fc2..e00a498 100644 --- a/README.md +++ b/README.md @@ -14,16 +14,16 @@ ZiGate is a open source ZigBee adapter hardware that was initially launched on K - https://www.zigate.fr - https://www.kickstarter.com/projects/1361563794/zigate-universal-zigbee-gateway-for-smarthome -## Compatible hardware +## Hardware and firmware compatibility The ZiGate USB adapter communicates via a PL-2303HX USB to Serial Bridge Controller module by Prolific. There's also a Wi-Fi adapter to communicate with ZiGate over network. -Note! ZiGate open source ZigBee adapter hardware requires ZiGate firmware 3.1a or later to work with this zigpy-zigate module. +Note! ZiGate open source ZigBee USB and GPIO adapter hardware requires ZiGate 3.1a firmware or later to work with this zigpy-zigate module, however ZiGate 3.1d firmware or later is recommended is it contains a a specific bug-fix related to zigpy. ### Known working Zigbee radio modules - [ZiGate USB-TTL](https://zigate.fr/produit/zigate-ttl/) - [ZiGate USB-DIN](https://zigate.fr/produit/zigate-usb-din/) -- [PiZiGate (ZiGate module for Raspberry Pi GPIO)](https://zigate.fr/produit/pizigate-v1-0/) +- [PiZiGate (ZiGate module for Raspberry Pi GPIO)](https://zigate.fr/produit/pizigate-v1-0/) - Requires ZiGate 3.1d firmware or later. - [ZiGate Pack WiFi](https://zigate.fr/produit/zigate-pack-wifi-v1-3/) ### Experimental Zigbee radio modules @@ -36,12 +36,9 @@ Note! ZiGate open source ZigBee adapter hardware requires ZiGate firmware 3.1a o - To configure __pizigate__ port, specify the port, example : `/dev/serial0` or `/dev/ttyAMA0` - To configure __wifi__ ZiGate, manually specify IP address and port, example : `socket://192.168.1.10:9999` -__pizigate__ may requiert some adjustements on Rpi3 or Rpi4: -- [Rpi3](https://zigate.fr/documentation/compatibilite-raspberry-pi-3-et-zero-w/) -- [Rpi4](https://zigate.fr/documentation/compatibilite-raspberry-pi-4-b/) - -Note! Requires ZiGate firmware 3.1d and later -- https://zigate.fr/tag/firmware/ +__pizigate__ does require ZiGate 3.1d firmware or later as well as some additional adjustements on Raspberry Pi 3/Zero, and 4: +- [Raspberry Pi 3 and Raspberry Pi Zero configuration adjustements](https://zigate.fr/documentation/compatibilite-raspberry-pi-3-et-zero-w/) +- [Raspberry Pi 4 configuration adjustements](https://zigate.fr/documentation/compatibilite-raspberry-pi-4-b/) ## Flasher (ZiGate Firmware Tool) From 0fbda1daca8185c1c6bb11d237a1e7a6102f160e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 10 Nov 2020 12:00:55 +0100 Subject: [PATCH 59/63] fix model detection --- tests/test_application.py | 10 ++++++++-- zigpy_zigate/zigbee/application.py | 6 +++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/test_application.py b/tests/test_application.py index 90ae337..848e540 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -13,11 +13,13 @@ config.CONF_DATABASE: None, } ) - +FAKE_FIRMWARE_VERSION = '3.1z' @pytest.fixture def app(): - return zigpy_zigate.zigbee.application.ControllerApplication(APP_CONFIG) + a = zigpy_zigate.zigbee.application.ControllerApplication(APP_CONFIG) + a.version = FAKE_FIRMWARE_VERSION + return a def test_zigpy_ieee(app): @@ -30,3 +32,7 @@ def test_zigpy_ieee(app): dst_addr = app.get_dst_address(cluster) assert dst_addr.serialize() == b"\x03" + data[::-1] + b"\x01" + +def test_model_detection(app): + device = zigpy_zigate.zigbee.application.ZiGateDevice(app, 0, 0) + assert device.model == 'ZiGate USB-TTL {}'.format(FAKE_FIRMWARE_VERSION) diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index ca84bda..5a61398 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -11,7 +11,7 @@ from zigpy_zigate import types as t from zigpy_zigate import common as c from zigpy_zigate.api import NoResponseError, ZiGate, PDM_EVENT -from zigpy_zigate.config import CONF_DEVICE, CONFIG_SCHEMA, SCHEMA_DEVICE +from zigpy_zigate.config import CONF_DEVICE, CONF_DEVICE_PATH, CONFIG_SCHEMA, SCHEMA_DEVICE LOGGER = logging.getLogger(__name__) @@ -199,13 +199,13 @@ def __init__(self, application, ieee, nwk): """Initialize instance.""" super().__init__(application, ieee, nwk) - port = application._config[CONF_DEVICE] + port = application._config[CONF_DEVICE][CONF_DEVICE_PATH] model = 'ZiGate USB-TTL' if c.is_zigate_wifi(port): model = 'ZiGate WiFi' elif c.is_pizigate(port): model = 'PiZiGate' - elif c.is_zigate_din(): + elif c.is_zigate_din(port): model = 'ZiGate USB-DIN' self._model = '{} {}'.format(model, application.version) From b2d886b9df17fc3b21dc02aeee6f11e0fd049fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 10 Nov 2020 12:21:41 +0100 Subject: [PATCH 60/63] remove whitespace --- tests/test_application.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_application.py b/tests/test_application.py index 848e540..bfb8c11 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -33,6 +33,7 @@ def test_zigpy_ieee(app): dst_addr = app.get_dst_address(cluster) assert dst_addr.serialize() == b"\x03" + data[::-1] + b"\x01" + def test_model_detection(app): device = zigpy_zigate.zigbee.application.ZiGateDevice(app, 0, 0) assert device.model == 'ZiGate USB-TTL {}'.format(FAKE_FIRMWARE_VERSION) From 8f63be19267b7457090fe0b2bafbab09e53244a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 10 Nov 2020 12:23:28 +0100 Subject: [PATCH 61/63] bump to 0.7.0 --- 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 a051fcc..509ed44 100644 --- a/zigpy_zigate/__init__.py +++ b/zigpy_zigate/__init__.py @@ -1,5 +1,5 @@ MAJOR_VERSION = 0 MINOR_VERSION = 7 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 098395731c7792e383b5001f9ce01983a02879aa Mon Sep 17 00:00:00 2001 From: Hedda Date: Tue, 10 Nov 2020 14:00:14 +0100 Subject: [PATCH 62/63] Recommend ZiGate 3.1a firmware or later Recommend ZiGate 3.1a firmware or later --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e00a498..d9267d9 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,12 @@ ZiGate is a open source ZigBee adapter hardware that was initially launched on K The ZiGate USB adapter communicates via a PL-2303HX USB to Serial Bridge Controller module by Prolific. There's also a Wi-Fi adapter to communicate with ZiGate over network. -Note! ZiGate open source ZigBee USB and GPIO adapter hardware requires ZiGate 3.1a firmware or later to work with this zigpy-zigate module, however ZiGate 3.1d firmware or later is recommended is it contains a a specific bug-fix related to zigpy. +Note! ZiGate open source ZigBee USB and GPIO adapter hardware requires ZiGate 3.1a firmware or later to work with this zigpy-zigate module, however ZiGate 3.1d firmware or later is recommended as it contains a specific bug-fix related to zigpy. ### Known working Zigbee radio modules - [ZiGate USB-TTL](https://zigate.fr/produit/zigate-ttl/) - [ZiGate USB-DIN](https://zigate.fr/produit/zigate-usb-din/) -- [PiZiGate (ZiGate module for Raspberry Pi GPIO)](https://zigate.fr/produit/pizigate-v1-0/) - Requires ZiGate 3.1d firmware or later. +- [PiZiGate (ZiGate module for Raspberry Pi GPIO)](https://zigate.fr/produit/pizigate-v1-0/) - [ZiGate Pack WiFi](https://zigate.fr/produit/zigate-pack-wifi-v1-3/) ### Experimental Zigbee radio modules From a378a831ee22b86c15ceec2256059903c06f3a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Tue, 10 Nov 2020 14:18:33 +0100 Subject: [PATCH 63/63] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d9267d9..a4d5b71 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Note! ZiGate open source ZigBee USB and GPIO adapter hardware requires ZiGate 3. - To configure __pizigate__ port, specify the port, example : `/dev/serial0` or `/dev/ttyAMA0` - To configure __wifi__ ZiGate, manually specify IP address and port, example : `socket://192.168.1.10:9999` -__pizigate__ does require ZiGate 3.1d firmware or later as well as some additional adjustements on Raspberry Pi 3/Zero, and 4: +__pizigate__ does require some additional adjustements on Raspberry Pi 3/Zero, and 4: - [Raspberry Pi 3 and Raspberry Pi Zero configuration adjustements](https://zigate.fr/documentation/compatibilite-raspberry-pi-3-et-zero-w/) - [Raspberry Pi 4 configuration adjustements](https://zigate.fr/documentation/compatibilite-raspberry-pi-4-b/)