Skip to content

Commit

Permalink
Sync up with zigpy 0.60.0 (#170)
Browse files Browse the repository at this point in the history
* Sync up with zigpy 0.60.0

* Fix unit tests

* Add remaining unit tests

* Fix watchdog unit test

* Re-add XBee config
  • Loading branch information
puddly authored Nov 20, 2023
1 parent 846130e commit 8314dcd
Show file tree
Hide file tree
Showing 8 changed files with 62 additions and 154 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ readme = "README.md"
license = {text = "GPL-3.0"}
requires-python = ">=3.8"
dependencies = [
"zigpy>=0.56.0",
"zigpy>=0.60.0",
]

[tool.setuptools.packages.find]
Expand Down
74 changes: 20 additions & 54 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
"""Tests for API."""

import asyncio
import logging

import pytest
import serial
import zigpy.config
import zigpy.exceptions
import zigpy.types as t

from zigpy_xbee import api as xbee_api, types as xbee_t, uart
import zigpy_xbee.config
from zigpy_xbee.exceptions import ATCommandError, ATCommandException, InvalidCommand
from zigpy_xbee.zigbee.application import ControllerApplication

import tests.async_mock as mock

DEVICE_CONFIG = zigpy_xbee.config.SCHEMA_DEVICE(
{zigpy_xbee.config.CONF_DEVICE_PATH: "/dev/null"}
DEVICE_CONFIG = zigpy.config.SCHEMA_DEVICE(
{
zigpy.config.CONF_DEVICE_PATH: "/dev/null",
zigpy.config.CONF_DEVICE_BAUDRATE: 57600,
}
)


Expand All @@ -38,13 +40,8 @@ async def test_connect(monkeypatch):
def test_close(api):
"""Test connection close."""
uart = api._uart
conn_lost_task = mock.MagicMock()
api._conn_lost_task = conn_lost_task

api.close()

assert api._conn_lost_task is None
assert conn_lost_task.cancel.call_count == 1
assert api._uart is None
assert uart.close.call_count == 1

Expand Down Expand Up @@ -602,51 +599,6 @@ def test_handle_many_to_one_rri(api):
api._handle_many_to_one_rri(ieee, nwk, 0)


async def test_reconnect_multiple_disconnects(monkeypatch, caplog):
"""Test reconnect with multiple disconnects."""
api = xbee_api.XBee(DEVICE_CONFIG)
connect_mock = mock.AsyncMock(return_value=True)
monkeypatch.setattr(uart, "connect", connect_mock)

await api.connect()

caplog.set_level(logging.DEBUG)
connect_mock.reset_mock()
connect_mock.side_effect = [OSError, mock.sentinel.uart_reconnect]
api.connection_lost("connection lost")
await asyncio.sleep(0.3)
api.connection_lost("connection lost 2")
await asyncio.sleep(0.3)

assert "Cancelling reconnection attempt" in caplog.messages
assert api._uart is mock.sentinel.uart_reconnect
assert connect_mock.call_count == 2


async def test_reconnect_multiple_attempts(monkeypatch, caplog):
"""Test reconnect with multiple attempts."""
api = xbee_api.XBee(DEVICE_CONFIG)
connect_mock = mock.AsyncMock(return_value=True)
monkeypatch.setattr(uart, "connect", connect_mock)

await api.connect()

caplog.set_level(logging.DEBUG)
connect_mock.reset_mock()
connect_mock.side_effect = [
asyncio.TimeoutError,
OSError,
mock.sentinel.uart_reconnect,
]

with mock.patch("asyncio.sleep"):
api.connection_lost("connection lost")
await api._conn_lost_task

assert api._uart is mock.sentinel.uart_reconnect
assert connect_mock.call_count == 3


@mock.patch.object(xbee_api.XBee, "_at_command", new_callable=mock.AsyncMock)
@mock.patch.object(uart, "connect", return_value=mock.MagicMock())
async def test_probe_success(mock_connect, mock_at_cmd):
Expand Down Expand Up @@ -727,3 +679,17 @@ async def test_xbee_new(conn_mck):
assert isinstance(api, xbee_api.XBee)
assert conn_mck.call_count == 1
assert conn_mck.await_count == 1


@mock.patch.object(xbee_api.XBee, "connect", return_value=mock.MagicMock())
async def test_connection_lost(conn_mck):
"""Test `connection_lost` propagataion."""
api = await xbee_api.XBee.new(mock.sentinel.application, DEVICE_CONFIG)
await api.connect()

app = api._app = mock.MagicMock()

err = RuntimeError()
api.connection_lost(err)

app.connection_lost.assert_called_once_with(err)
25 changes: 10 additions & 15 deletions tests/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import asyncio

import pytest
import zigpy.config as config
import zigpy.exceptions
import zigpy.state
import zigpy.types as t
import zigpy.zdo
import zigpy.zdo.types as zdo_t

from zigpy_xbee.api import XBee
import zigpy_xbee.config as config
from zigpy_xbee.exceptions import InvalidCommand
import zigpy_xbee.types as xbee_t
from zigpy_xbee.zigbee import application
Expand Down Expand Up @@ -363,6 +363,7 @@ def _at_command_mock(cmd, *args):
"SH": 0x08070605,
"SL": 0x04030201,
"ZS": zs,
"VR": 0x1234,
}.get(cmd, None)

def init_api_mode_mock():
Expand Down Expand Up @@ -441,20 +442,6 @@ async def test_permit(app):
assert app._api._at_command.call_args_list[0][0][1] == time_s


async def test_permit_with_key(app):
"""Test permit joins with join code."""
app._api._command = mock.AsyncMock(return_value=xbee_t.TXStatus.SUCCESS)
app._api._at_command = mock.AsyncMock(return_value="OK")
node = t.EUI64(b"\x01\x02\x03\x04\x05\x06\x07\x08")
code = b"\xC9\xA7\xD2\x44\x1A\x71\x16\x95\xCD\x62\x17\x0D\x33\x28\xEA\x2B\x42\x3D"
time_s = 500
await app.permit_with_key(node=node, code=code, time_s=time_s)
app._api._at_command.assert_called_once_with("KT", time_s)
app._api._command.assert_called_once_with(
"register_joining_device", node, 0xFFFE, 1, code
)


async def test_permit_with_link_key(app):
"""Test permit joins with link key."""
app._api._command = mock.AsyncMock(return_value=xbee_t.TXStatus.SUCCESS)
Expand Down Expand Up @@ -879,3 +866,11 @@ async def test_routes_updated(app, device):
assert router2.radio_details.call_count == 0

app._api._at_command.assert_awaited_once_with("DB")


async def test_watchdog(app):
"""Test watchdog feed method."""
app._api._at_command = mock.AsyncMock(return_value="OK")
await app._watchdog_feed()

assert app._api._at_command.mock_calls == [mock.call("VR")]
9 changes: 6 additions & 3 deletions tests/test_uart.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@

import pytest
import serial_asyncio
import zigpy.config

from zigpy_xbee import uart
import zigpy_xbee.config

DEVICE_CONFIG = zigpy_xbee.config.SCHEMA_DEVICE(
{zigpy_xbee.config.CONF_DEVICE_PATH: "/dev/null"}
DEVICE_CONFIG = zigpy.config.SCHEMA_DEVICE(
{
zigpy.config.CONF_DEVICE_PATH: "/dev/null",
zigpy.config.CONF_DEVICE_BAUDRATE: 57600,
}
)


Expand Down
58 changes: 3 additions & 55 deletions zigpy_xbee/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
from typing import Any, Dict, Optional

import serial
from zigpy.config import CONF_DEVICE_PATH, SCHEMA_DEVICE
from zigpy.exceptions import APIException, DeliveryError
import zigpy.types as t

import zigpy_xbee
from zigpy_xbee.config import CONF_DEVICE_BAUDRATE, CONF_DEVICE_PATH, SCHEMA_DEVICE
from zigpy_xbee.exceptions import (
ATCommandError,
ATCommandException,
Expand Down Expand Up @@ -287,7 +287,6 @@ def __init__(self, device_config: Dict[str, Any]) -> None:
self._awaiting = {}
self._app = None
self._cmd_mode_future: Optional[asyncio.Future] = None
self._conn_lost_task: Optional[asyncio.Task] = None
self._reset: asyncio.Event = asyncio.Event()
self._running: asyncio.Event = asyncio.Event()

Expand Down Expand Up @@ -323,64 +322,13 @@ async def connect(self) -> None:
assert self._uart is None
self._uart = await uart.connect(self._config, self)

def reconnect(self):
"""Reconnect using saved parameters."""
LOGGER.debug(
"Reconnecting '%s' serial port using %s",
self._config[CONF_DEVICE_PATH],
self._config[CONF_DEVICE_BAUDRATE],
)
return self.connect()

def connection_lost(self, exc: Exception) -> None:
"""Lost serial connection."""
LOGGER.warning(
"Serial '%s' connection lost unexpectedly: %s",
self._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.create_task(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")
raise

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[CONF_DEVICE_PATH],
wait,
str(exc),
)
await asyncio.sleep(wait)

LOGGER.debug(
"Reconnected '%s' serial port after %s attempts",
self._config[CONF_DEVICE_PATH],
attempt,
)
if self._app is not None:
self._app.connection_lost(exc)

def close(self):
"""Close the connection."""
if self._conn_lost_task:
self._conn_lost_task.cancel()
self._conn_lost_task = None

if self._uart:
self._uart.close()
self._uart = None
Expand Down
19 changes: 6 additions & 13 deletions zigpy_xbee/config.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
"""XBee module config."""

import voluptuous as vol
from zigpy.config import ( # noqa: F401 pylint: disable=unused-import
CONF_DATABASE,
CONF_DEVICE,
CONF_DEVICE_PATH,
CONFIG_SCHEMA,
SCHEMA_DEVICE,
cv_boolean,
)

CONF_DEVICE_BAUDRATE = "baudrate"
import zigpy.config

SCHEMA_DEVICE = SCHEMA_DEVICE.extend(
{vol.Optional(CONF_DEVICE_BAUDRATE, default=57600): int}
SCHEMA_DEVICE = zigpy.config.SCHEMA_DEVICE.extend(
{vol.Optional(zigpy.config.CONF_DEVICE_BAUDRATE, default=57600): int}
)

CONFIG_SCHEMA = CONFIG_SCHEMA.extend({vol.Required(CONF_DEVICE): SCHEMA_DEVICE})
CONFIG_SCHEMA = zigpy.config.CONFIG_SCHEMA.extend(
{vol.Required(zigpy.config.CONF_DEVICE): zigpy.config.SCHEMA_DEVICE}
)
7 changes: 3 additions & 4 deletions zigpy_xbee/uart.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
import logging
from typing import Any, Dict

import zigpy.config
import zigpy.serial

from zigpy_xbee.config import CONF_DEVICE_BAUDRATE, CONF_DEVICE_PATH

LOGGER = logging.getLogger(__name__)


Expand Down Expand Up @@ -178,8 +177,8 @@ async def connect(device_config: Dict[str, Any], api, loop=None) -> Gateway:
transport, protocol = await zigpy.serial.create_serial_connection(
loop,
lambda: protocol,
url=device_config[CONF_DEVICE_PATH],
baudrate=device_config[CONF_DEVICE_BAUDRATE],
url=device_config[zigpy.config.CONF_DEVICE_PATH],
baudrate=device_config[zigpy.config.CONF_DEVICE_BAUDRATE],
xonxoff=False,
)

Expand Down
22 changes: 13 additions & 9 deletions zigpy_xbee/zigbee/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import zigpy.application
import zigpy.config
from zigpy.config import CONF_DEVICE
import zigpy.device
import zigpy.exceptions
import zigpy.quirks
Expand All @@ -22,7 +23,7 @@

import zigpy_xbee
import zigpy_xbee.api
from zigpy_xbee.config import CONF_DEVICE, CONFIG_SCHEMA, SCHEMA_DEVICE
import zigpy_xbee.config
from zigpy_xbee.exceptions import InvalidCommand
from zigpy_xbee.types import EUI64, UNKNOWN_IEEE, UNKNOWN_NWK, TXOptions, TXStatus

Expand All @@ -42,17 +43,17 @@
class ControllerApplication(zigpy.application.ControllerApplication):
"""Implementation of Zigpy ControllerApplication for XBee devices."""

SCHEMA = CONFIG_SCHEMA
SCHEMA_DEVICE = SCHEMA_DEVICE

probe = zigpy_xbee.api.XBee.probe
CONFIG_SCHEMA = zigpy_xbee.config.CONFIG_SCHEMA

def __init__(self, config: dict[str, Any]):
"""Initialize instance."""
super().__init__(config=zigpy.config.ZIGPY_SCHEMA(config))
self._api: zigpy_xbee.api.XBee | None = None
self.topology.add_listener(self)

async def _watchdog_feed(self):
await self._api._at_command("VR")

async def disconnect(self):
"""Shutdown application."""
if self._api:
Expand Down Expand Up @@ -136,6 +137,13 @@ async def load_network_info(self, *, load_devices=False):
LOGGER.warning("CE command failed, assuming node is coordinator")
node_info.logical_type = zdo_t.LogicalType.Coordinator

# TODO: Feature detect the XBee's exact model
node_info.model = "XBee"
node_info.manufacturer = "Digi"

version = await self._api._at_command("VR")
node_info.version = f"{int(version):#06x}"

# Load network info
pan_id = await self._api._at_command("OI")
extended_pan_id = await self._api._at_command("ID")
Expand Down Expand Up @@ -350,10 +358,6 @@ async def permit_with_link_key(
# 1 = Install Code With CRC (I? command of the joining device)
await self._api.register_joining_device(node, reserved, key_type, link_key)

async def permit_with_key(self, node: EUI64, code: bytes, time_s=500):
"""Permits a new device to join with the given IEEE and Install Code."""
await self.permit_with_link_key(node, code, time_s, key_type=1)

def handle_modem_status(self, status):
"""Handle changed Modem Status of the device."""
LOGGER.info("Modem status update: %s (%s)", status.name, status.value)
Expand Down

0 comments on commit 8314dcd

Please sign in to comment.