Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Implement zigpy 0.60.0 changes #150

Merged
merged 4 commits into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading