Skip to content

Commit

Permalink
Implement zigpy 0.60.0 changes
Browse files Browse the repository at this point in the history
  • Loading branch information
puddly committed Nov 20, 2023
1 parent 697b649 commit 1bb23d4
Show file tree
Hide file tree
Showing 7 changed files with 42 additions and 203 deletions.
50 changes: 2 additions & 48 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -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"})
Expand Down Expand Up @@ -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):
Expand Down
10 changes: 4 additions & 6 deletions tests/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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"
Expand Down
6 changes: 2 additions & 4 deletions tests/test_uart.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down
105 changes: 10 additions & 95 deletions zigpy_zigate/api.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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()
7 changes: 0 additions & 7 deletions zigpy_zigate/config.py

This file was deleted.

7 changes: 3 additions & 4 deletions zigpy_zigate/uart.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()

Expand All @@ -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,
)

Expand Down
Loading

0 comments on commit 1bb23d4

Please sign in to comment.