From 1bb23d44e44e8ebfd459e0084c90591dcbf30f8c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 20 Nov 2023 12:58:48 -0500 Subject: [PATCH] Implement zigpy 0.60.0 changes --- tests/test_api.py | 50 +------------- tests/test_application.py | 10 ++- tests/test_uart.py | 6 +- zigpy_zigate/api.py | 105 +++-------------------------- zigpy_zigate/config.py | 7 -- zigpy_zigate/uart.py | 7 +- zigpy_zigate/zigbee/application.py | 60 ++++++----------- 7 files changed, 42 insertions(+), 203 deletions(-) delete mode 100644 zigpy_zigate/config.py diff --git a/tests/test_api.py b/tests/test_api.py index 488a4ea..0abb37c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,12 +1,11 @@ import asyncio -from unittest.mock import AsyncMock, MagicMock, patch, sentinel +from unittest.mock import MagicMock, patch, sentinel import pytest -import serial import serial_asyncio +import zigpy.config as config from zigpy_zigate import api as zigate_api -import zigpy_zigate.config as config import zigpy_zigate.uart DEVICE_CONFIG = config.SCHEMA_DEVICE({config.CONF_DEVICE_PATH: "/dev/null"}) @@ -55,51 +54,6 @@ async def test_api_new(conn_mck): assert conn_mck.await_count == 1 -@pytest.mark.asyncio -@patch.object(zigate_api.ZiGate, "set_raw_mode", new_callable=AsyncMock) -@pytest.mark.parametrize( - "port", - ("/dev/null", "pizigate:/dev/ttyAMA0"), -) -async def test_probe_success(mock_raw_mode, port, monkeypatch): - """Test device probing.""" - - async def mock_conn(loop, protocol_factory, **kwargs): - protocol = protocol_factory() - loop.call_soon(protocol.connection_made, None) - return None, protocol - - monkeypatch.setattr(serial_asyncio, "create_serial_connection", mock_conn) - DEVICE_CONFIG = zigpy_zigate.config.SCHEMA_DEVICE( - {zigpy_zigate.config.CONF_DEVICE_PATH: port} - ) - res = await zigate_api.ZiGate.probe(DEVICE_CONFIG) - assert res is True - assert mock_raw_mode.call_count == 1 - - -@pytest.mark.asyncio -@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), -) -async def test_probe_fail(mock_connect, mock_raw_mode, exception): - """Test device probing fails.""" - - mock_raw_mode.side_effect = exception - mock_connect.reset_mock() - mock_raw_mode.reset_mock() - res = await zigate_api.ZiGate.probe(DEVICE_CONFIG) - assert res is False - assert mock_connect.call_count == 1 - assert mock_connect.await_count == 1 - 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(asyncio, "wait", return_value=([], [])) async def test_api_command(mock_command, api): diff --git a/tests/test_application.py b/tests/test_application.py index bcabb2e..2fd66c8 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -2,11 +2,11 @@ from unittest.mock import AsyncMock, MagicMock, call, patch import pytest +import zigpy.config as config import zigpy.exceptions import zigpy.types as zigpy_t import zigpy_zigate.api -import zigpy_zigate.config as config import zigpy_zigate.types as t import zigpy_zigate.zigbee.application @@ -39,11 +39,6 @@ def test_zigpy_ieee(app): 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) - - @pytest.mark.asyncio async def test_form_network_success(app): app._api.erase_persistent_data = AsyncMock() @@ -76,6 +71,9 @@ async def mock_get_network_state(): assert app.state.node_info.ieee == zigpy.types.EUI64.convert( "01:23:45:67:89:ab:cd:ef" ) + assert app.state.node_info.version == "3.1z" + assert app.state.node_info.model == "ZiGate USB-TTL" + assert app.state.node_info.manufacturer == "ZiGate" assert app.state.network_info.pan_id == 0x1234 assert app.state.network_info.extended_pan_id == zigpy.types.ExtendedPanId.convert( "12:34:ab:cd:ef:01:23:45" diff --git a/tests/test_uart.py b/tests/test_uart.py index 12f994b..824a2db 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -4,9 +4,9 @@ import pytest import serial.tools.list_ports import serial_asyncio +import zigpy.config from zigpy_zigate import common, uart -import zigpy_zigate.config @pytest.fixture @@ -32,9 +32,7 @@ async def mock_conn(loop, protocol_factory, url, **kwargs): monkeypatch.setattr(serial_asyncio, "create_serial_connection", mock_conn) monkeypatch.setattr(common, "set_pizigate_running_mode", AsyncMock()) - DEVICE_CONFIG = zigpy_zigate.config.SCHEMA_DEVICE( - {zigpy_zigate.config.CONF_DEVICE_PATH: port} - ) + DEVICE_CONFIG = zigpy.config.SCHEMA_DEVICE({zigpy.config.CONF_DEVICE_PATH: port}) await uart.connect(DEVICE_CONFIG, api) diff --git a/zigpy_zigate/api.py b/zigpy_zigate/api.py index 6f4ab3b..4556579 100644 --- a/zigpy_zigate/api.py +++ b/zigpy_zigate/api.py @@ -1,15 +1,13 @@ import asyncio import binascii -import datetime +from datetime import datetime, timezone import enum import functools import logging from typing import Any, Dict -import serial import zigpy.exceptions -import zigpy_zigate.config import zigpy_zigate.uart from . import types as t @@ -220,7 +218,6 @@ def __init__(self, device_config: Dict[str, Any]): self._awaiting = {} self._status_awaiting = {} self._lock = asyncio.Lock() - self._conn_lost_task = None self.network_state = None @@ -237,59 +234,14 @@ async def connect(self): def connection_lost(self, exc: Exception) -> None: """Lost serial connection.""" - LOGGER.warning( - "Serial '%s' connection lost unexpectedly: %s", - self._config[zigpy_zigate.config.CONF_DEVICE_PATH], - exc, - ) - self._uart = None - if self._conn_lost_task and not self._conn_lost_task.done(): - self._conn_lost_task.cancel() - self._conn_lost_task = asyncio.ensure_future(self._connection_lost()) - - async def _connection_lost(self) -> None: - """Reconnect serial port.""" - try: - await self._reconnect_till_done() - except asyncio.CancelledError: - LOGGER.debug("Cancelling reconnection attempt") - - async def _reconnect_till_done(self) -> None: - attempt = 1 - while True: - try: - await asyncio.wait_for(self.reconnect(), timeout=10) - break - except (asyncio.TimeoutError, OSError) as exc: - wait = 2 ** min(attempt, 5) - attempt += 1 - LOGGER.debug( - "Couldn't re-open '%s' serial port, retrying in %ss: %s", - self._config[zigpy_zigate.config.CONF_DEVICE_PATH], - wait, - str(exc), - ) - await asyncio.sleep(wait) - - LOGGER.debug( - "Reconnected '%s' serial port after %s attempts", - self._config[zigpy_zigate.config.CONF_DEVICE_PATH], - attempt, - ) + if self._app is not None: + self._app.connection_lost(exc) def close(self): if self._uart: self._uart.close() self._uart = None - def reconnect(self): - """Reconnect using saved parameters.""" - LOGGER.debug( - "Reconnecting '%s' serial port", - self._config[zigpy_zigate.config.CONF_DEVICE_PATH], - ) - return self.connect() - def set_application(self, app): self._app = app @@ -446,13 +398,13 @@ async def erase_persistent_data(self): CommandId.RESET, wait_response=ResponseId.NODE_FACTORY_NEW_RESTART ) - 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[CommandId.SET_TIMESERVER]) + async def set_time(self): + """set internal time""" + timestamp = ( + datetime.now(timezone.utc) - datetime(2000, 1, 1, tzinfo=timezone.utc) + ).total_seconds() + + data = t.serialize([int(timestamp)], COMMANDS[CommandId.SET_TIMESERVER]) await self.command(CommandId.SET_TIMESERVER, data) async def get_time_server(self): @@ -567,40 +519,3 @@ def handle_callback(self, *args): self._app.zigate_callback_handler(*args) except Exception as e: LOGGER.exception("Exception running handler", exc_info=e) - - @classmethod - async def probe(cls, device_config: Dict[str, Any]) -> bool: - """Probe port for the device presence.""" - api = cls(zigpy_zigate.config.SCHEMA_DEVICE(device_config)) - try: - await asyncio.wait_for(api._probe(), timeout=PROBE_TIMEOUT) - return True - except ( - asyncio.TimeoutError, - serial.SerialException, - zigpy.exceptions.ZigbeeException, - ) as exc: - LOGGER.debug( - "Unsuccessful radio probe of '%s' port", - device_config[zigpy_zigate.config.CONF_DEVICE_PATH], - exc_info=exc, - ) - finally: - api.close() - - return False - - async def _probe(self) -> None: - """Open port and try sending a command""" - 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() diff --git a/zigpy_zigate/config.py b/zigpy_zigate/config.py deleted file mode 100644 index 4c530fe..0000000 --- a/zigpy_zigate/config.py +++ /dev/null @@ -1,7 +0,0 @@ -from zigpy.config import ( # noqa: F401 pylint: disable=unused-import - CONF_DATABASE, - CONF_DEVICE, - CONF_DEVICE_PATH, - CONFIG_SCHEMA, - SCHEMA_DEVICE, -) diff --git a/zigpy_zigate/uart.py b/zigpy_zigate/uart.py index d0ca340..0bbe9a9 100644 --- a/zigpy_zigate/uart.py +++ b/zigpy_zigate/uart.py @@ -4,13 +4,12 @@ import struct from typing import Any, Dict +import zigpy.config import zigpy.serial from . import common as c -from .config import CONF_DEVICE_PATH LOGGER = logging.getLogger(__name__) -ZIGATE_BAUDRATE = 115200 class Gateway(asyncio.Protocol): @@ -139,7 +138,7 @@ async def connect(device_config: Dict[str, Any], api, loop=None): connected_future = asyncio.Future() protocol = Gateway(api, connected_future) - port = device_config[CONF_DEVICE_PATH] + port = device_config[zigpy.config.CONF_DEVICE_PATH] if port == "auto": port = c.discover_port() @@ -159,7 +158,7 @@ async def connect(device_config: Dict[str, Any], api, loop=None): loop, lambda: protocol, url=port, - baudrate=ZIGATE_BAUDRATE, + baudrate=device_config[zigpy.config.CONF_DEVICE_BAUDRATE], xonxoff=False, ) diff --git a/zigpy_zigate/zigbee/application.py b/zigpy_zigate/zigbee/application.py index b54e5b8..47c3f29 100644 --- a/zigpy_zigate/zigbee/application.py +++ b/zigpy_zigate/zigbee/application.py @@ -15,22 +15,11 @@ from zigpy_zigate import common as c, types as t from zigpy_zigate.api import PDM_EVENT, NoResponseError, ResponseId, ZiGate -from zigpy_zigate.config import ( - CONF_DEVICE, - CONF_DEVICE_PATH, - CONFIG_SCHEMA, - SCHEMA_DEVICE, -) LOGGER = logging.getLogger(__name__) class ControllerApplication(zigpy.application.ControllerApplication): - SCHEMA = CONFIG_SCHEMA - SCHEMA_DEVICE = SCHEMA_DEVICE - - probe = ZiGate.probe - def __init__(self, config: dict[str, Any]): super().__init__(zigpy.config.ZIGPY_SCHEMA(config)) self._api: ZiGate | None = None @@ -40,8 +29,11 @@ def __init__(self, config: dict[str, Any]): self.version: str = "" + async def _watchdog_feed(self): + await self._api.set_time() + async def connect(self): - api = await ZiGate.new(self._config[CONF_DEVICE], self) + api = await ZiGate.new(self._config[zigpy.config.CONF_DEVICE], self) await api.set_raw_mode() await api.set_time() @@ -58,7 +50,6 @@ async def connect(self): async def disconnect(self): # TODO: how do you stop the network? Is it possible? - if self._api is not None: try: await self._api.reset(wait=False) @@ -70,8 +61,9 @@ async def disconnect(self): async def start_network(self): # TODO: how do you start the network? Is it always automatically started? - dev = ZiGateDevice(self, self.state.node_info.ieee, self.state.node_info.nwk) - self.devices[dev.ieee] = dev + dev = self.add_device( + ieee=self.state.node_info.ieee, nwk=self.state.node_info.nwk + ) await dev.schedule_initialize() async def load_network_info(self, *, load_devices: bool = False): @@ -80,10 +72,24 @@ async def load_network_info(self, *, load_devices: bool = False): if not network_state or network_state[3] == 0 or network_state[0] == 0xFFFF: raise zigpy.exceptions.NetworkNotFormed() + port = self._config[zigpy.config.CONF_DEVICE][zigpy.config.CONF_DEVICE_PATH] + + if c.is_zigate_wifi(port): + model = "ZiGate WiFi" + elif c.is_pizigate(port): + model = "PiZiGate" + elif c.is_zigate_din(port): + model = "ZiGate USB-DIN" + else: + model = "ZiGate USB-TTL" + self.state.node_info = zigpy.state.NodeInfo( nwk=zigpy.types.NWK(network_state[0]), ieee=zigpy.types.EUI64(network_state[1]), logical_type=zigpy.zdo.types.LogicalType.Coordinator, + model=model, + manufacturer="ZiGate", + version=self.version, ) epid, _ = zigpy.types.ExtendedPanId.deserialize( @@ -320,27 +326,3 @@ async def permit_ncp(self, time_s=60): status, lqi = await self._api.permit_join(time_s) if status[0] != t.Status.Success: await self._api.reset() - - -class ZiGateDevice(zigpy.device.Device): - def __init__(self, application, ieee, nwk): - """Initialize instance.""" - - super().__init__(application, ieee, nwk) - 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(port): - model = "ZiGate USB-DIN" - self._model = f"{model} {application.version}" - - @property - def manufacturer(self): - return "ZiGate" - - @property - def model(self): - return self._model