Skip to content

Commit

Permalink
Implement zigpy 0.60.0 changes (#150)
Browse files Browse the repository at this point in the history
* Implement zigpy 0.60.0 changes

* Implement command priority
  • Loading branch information
puddly authored Jul 13, 2024
1 parent 8b00d74 commit 7a88a69
Show file tree
Hide file tree
Showing 8 changed files with 54 additions and 196 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ license = {text = "GPL-3.0"}
requires-python = ">=3.8"
dependencies = [
"voluptuous",
"zigpy>=0.60.0",
"zigpy>=0.60.2",
"pyusb>=1.1.0",
"gpiozero",
'async-timeout; python_version<"3.11"',
Expand Down
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
108 changes: 21 additions & 87 deletions zigpy_zigate/api.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import asyncio
import binascii
import datetime
from datetime import datetime, timezone
import enum
import functools
import logging
from typing import Any, Dict

import serial
from zigpy.datastructures import PriorityLock
import zigpy.exceptions
import zigpy.types

import zigpy_zigate.config
import zigpy_zigate.uart

from . import types as t
Expand Down Expand Up @@ -227,8 +226,7 @@ def __init__(self, device_config: Dict[str, Any]):
self._uart = None
self._awaiting = {}
self._status_awaiting = {}
self._lock = asyncio.Lock()
self._conn_lost_task = None
self._lock = PriorityLock()

self.network_state = None

Expand All @@ -245,59 +243,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 @@ -351,6 +304,14 @@ async def wait_for_response(self, wait_response):
self._awaiting[wait_response].cancel()
del self._awaiting[wait_response]

def _get_command_priority(self, cmd):
return {
# Watchdog command is prioritized
CommandId.SET_TIMESERVER: 9999,
# APS command is deprioritized
CommandId.SEND_RAW_APS_DATA_PACKET: -1,
}.get(cmd, 0)

async def command(
self,
cmd,
Expand All @@ -359,7 +320,7 @@ async def command(
wait_status=True,
timeout=COMMAND_TIMEOUT,
):
async with self._lock:
async with self._lock(priority=self._get_command_priority(cmd)):
tries = 3

tasks = []
Expand Down Expand Up @@ -454,13 +415,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 @@ -585,30 +546,3 @@ async def get_network_key(self):
raise CommandNotSupportedError()

return rsp[0]

@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"""
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 = await loop.run_in_executor(None, 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 7a88a69

Please sign in to comment.