Skip to content

Commit 8314dcd

Browse files
authored
Sync up with zigpy 0.60.0 (#170)
* Sync up with zigpy 0.60.0 * Fix unit tests * Add remaining unit tests * Fix watchdog unit test * Re-add XBee config
1 parent 846130e commit 8314dcd

File tree

8 files changed

+62
-154
lines changed

8 files changed

+62
-154
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ readme = "README.md"
1414
license = {text = "GPL-3.0"}
1515
requires-python = ">=3.8"
1616
dependencies = [
17-
"zigpy>=0.56.0",
17+
"zigpy>=0.60.0",
1818
]
1919

2020
[tool.setuptools.packages.find]

tests/test_api.py

Lines changed: 20 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
"""Tests for API."""
22

33
import asyncio
4-
import logging
54

65
import pytest
76
import serial
7+
import zigpy.config
88
import zigpy.exceptions
99
import zigpy.types as t
1010

1111
from zigpy_xbee import api as xbee_api, types as xbee_t, uart
12-
import zigpy_xbee.config
1312
from zigpy_xbee.exceptions import ATCommandError, ATCommandException, InvalidCommand
1413
from zigpy_xbee.zigbee.application import ControllerApplication
1514

1615
import tests.async_mock as mock
1716

18-
DEVICE_CONFIG = zigpy_xbee.config.SCHEMA_DEVICE(
19-
{zigpy_xbee.config.CONF_DEVICE_PATH: "/dev/null"}
17+
DEVICE_CONFIG = zigpy.config.SCHEMA_DEVICE(
18+
{
19+
zigpy.config.CONF_DEVICE_PATH: "/dev/null",
20+
zigpy.config.CONF_DEVICE_BAUDRATE: 57600,
21+
}
2022
)
2123

2224

@@ -38,13 +40,8 @@ async def test_connect(monkeypatch):
3840
def test_close(api):
3941
"""Test connection close."""
4042
uart = api._uart
41-
conn_lost_task = mock.MagicMock()
42-
api._conn_lost_task = conn_lost_task
43-
4443
api.close()
4544

46-
assert api._conn_lost_task is None
47-
assert conn_lost_task.cancel.call_count == 1
4845
assert api._uart is None
4946
assert uart.close.call_count == 1
5047

@@ -602,51 +599,6 @@ def test_handle_many_to_one_rri(api):
602599
api._handle_many_to_one_rri(ieee, nwk, 0)
603600

604601

605-
async def test_reconnect_multiple_disconnects(monkeypatch, caplog):
606-
"""Test reconnect with multiple disconnects."""
607-
api = xbee_api.XBee(DEVICE_CONFIG)
608-
connect_mock = mock.AsyncMock(return_value=True)
609-
monkeypatch.setattr(uart, "connect", connect_mock)
610-
611-
await api.connect()
612-
613-
caplog.set_level(logging.DEBUG)
614-
connect_mock.reset_mock()
615-
connect_mock.side_effect = [OSError, mock.sentinel.uart_reconnect]
616-
api.connection_lost("connection lost")
617-
await asyncio.sleep(0.3)
618-
api.connection_lost("connection lost 2")
619-
await asyncio.sleep(0.3)
620-
621-
assert "Cancelling reconnection attempt" in caplog.messages
622-
assert api._uart is mock.sentinel.uart_reconnect
623-
assert connect_mock.call_count == 2
624-
625-
626-
async def test_reconnect_multiple_attempts(monkeypatch, caplog):
627-
"""Test reconnect with multiple attempts."""
628-
api = xbee_api.XBee(DEVICE_CONFIG)
629-
connect_mock = mock.AsyncMock(return_value=True)
630-
monkeypatch.setattr(uart, "connect", connect_mock)
631-
632-
await api.connect()
633-
634-
caplog.set_level(logging.DEBUG)
635-
connect_mock.reset_mock()
636-
connect_mock.side_effect = [
637-
asyncio.TimeoutError,
638-
OSError,
639-
mock.sentinel.uart_reconnect,
640-
]
641-
642-
with mock.patch("asyncio.sleep"):
643-
api.connection_lost("connection lost")
644-
await api._conn_lost_task
645-
646-
assert api._uart is mock.sentinel.uart_reconnect
647-
assert connect_mock.call_count == 3
648-
649-
650602
@mock.patch.object(xbee_api.XBee, "_at_command", new_callable=mock.AsyncMock)
651603
@mock.patch.object(uart, "connect", return_value=mock.MagicMock())
652604
async def test_probe_success(mock_connect, mock_at_cmd):
@@ -727,3 +679,17 @@ async def test_xbee_new(conn_mck):
727679
assert isinstance(api, xbee_api.XBee)
728680
assert conn_mck.call_count == 1
729681
assert conn_mck.await_count == 1
682+
683+
684+
@mock.patch.object(xbee_api.XBee, "connect", return_value=mock.MagicMock())
685+
async def test_connection_lost(conn_mck):
686+
"""Test `connection_lost` propagataion."""
687+
api = await xbee_api.XBee.new(mock.sentinel.application, DEVICE_CONFIG)
688+
await api.connect()
689+
690+
app = api._app = mock.MagicMock()
691+
692+
err = RuntimeError()
693+
api.connection_lost(err)
694+
695+
app.connection_lost.assert_called_once_with(err)

tests/test_application.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
import asyncio
44

55
import pytest
6+
import zigpy.config as config
67
import zigpy.exceptions
78
import zigpy.state
89
import zigpy.types as t
910
import zigpy.zdo
1011
import zigpy.zdo.types as zdo_t
1112

1213
from zigpy_xbee.api import XBee
13-
import zigpy_xbee.config as config
1414
from zigpy_xbee.exceptions import InvalidCommand
1515
import zigpy_xbee.types as xbee_t
1616
from zigpy_xbee.zigbee import application
@@ -363,6 +363,7 @@ def _at_command_mock(cmd, *args):
363363
"SH": 0x08070605,
364364
"SL": 0x04030201,
365365
"ZS": zs,
366+
"VR": 0x1234,
366367
}.get(cmd, None)
367368

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

443444

444-
async def test_permit_with_key(app):
445-
"""Test permit joins with join code."""
446-
app._api._command = mock.AsyncMock(return_value=xbee_t.TXStatus.SUCCESS)
447-
app._api._at_command = mock.AsyncMock(return_value="OK")
448-
node = t.EUI64(b"\x01\x02\x03\x04\x05\x06\x07\x08")
449-
code = b"\xC9\xA7\xD2\x44\x1A\x71\x16\x95\xCD\x62\x17\x0D\x33\x28\xEA\x2B\x42\x3D"
450-
time_s = 500
451-
await app.permit_with_key(node=node, code=code, time_s=time_s)
452-
app._api._at_command.assert_called_once_with("KT", time_s)
453-
app._api._command.assert_called_once_with(
454-
"register_joining_device", node, 0xFFFE, 1, code
455-
)
456-
457-
458445
async def test_permit_with_link_key(app):
459446
"""Test permit joins with link key."""
460447
app._api._command = mock.AsyncMock(return_value=xbee_t.TXStatus.SUCCESS)
@@ -879,3 +866,11 @@ async def test_routes_updated(app, device):
879866
assert router2.radio_details.call_count == 0
880867

881868
app._api._at_command.assert_awaited_once_with("DB")
869+
870+
871+
async def test_watchdog(app):
872+
"""Test watchdog feed method."""
873+
app._api._at_command = mock.AsyncMock(return_value="OK")
874+
await app._watchdog_feed()
875+
876+
assert app._api._at_command.mock_calls == [mock.call("VR")]

tests/test_uart.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55

66
import pytest
77
import serial_asyncio
8+
import zigpy.config
89

910
from zigpy_xbee import uart
10-
import zigpy_xbee.config
1111

12-
DEVICE_CONFIG = zigpy_xbee.config.SCHEMA_DEVICE(
13-
{zigpy_xbee.config.CONF_DEVICE_PATH: "/dev/null"}
12+
DEVICE_CONFIG = zigpy.config.SCHEMA_DEVICE(
13+
{
14+
zigpy.config.CONF_DEVICE_PATH: "/dev/null",
15+
zigpy.config.CONF_DEVICE_BAUDRATE: 57600,
16+
}
1417
)
1518

1619

zigpy_xbee/api.py

Lines changed: 3 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
from typing import Any, Dict, Optional
88

99
import serial
10+
from zigpy.config import CONF_DEVICE_PATH, SCHEMA_DEVICE
1011
from zigpy.exceptions import APIException, DeliveryError
1112
import zigpy.types as t
1213

1314
import zigpy_xbee
14-
from zigpy_xbee.config import CONF_DEVICE_BAUDRATE, CONF_DEVICE_PATH, SCHEMA_DEVICE
1515
from zigpy_xbee.exceptions import (
1616
ATCommandError,
1717
ATCommandException,
@@ -287,7 +287,6 @@ def __init__(self, device_config: Dict[str, Any]) -> None:
287287
self._awaiting = {}
288288
self._app = None
289289
self._cmd_mode_future: Optional[asyncio.Future] = None
290-
self._conn_lost_task: Optional[asyncio.Task] = None
291290
self._reset: asyncio.Event = asyncio.Event()
292291
self._running: asyncio.Event = asyncio.Event()
293292

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

326-
def reconnect(self):
327-
"""Reconnect using saved parameters."""
328-
LOGGER.debug(
329-
"Reconnecting '%s' serial port using %s",
330-
self._config[CONF_DEVICE_PATH],
331-
self._config[CONF_DEVICE_BAUDRATE],
332-
)
333-
return self.connect()
334-
335325
def connection_lost(self, exc: Exception) -> None:
336326
"""Lost serial connection."""
337-
LOGGER.warning(
338-
"Serial '%s' connection lost unexpectedly: %s",
339-
self._config[CONF_DEVICE_PATH],
340-
exc,
341-
)
342-
self._uart = None
343-
if self._conn_lost_task and not self._conn_lost_task.done():
344-
self._conn_lost_task.cancel()
345-
self._conn_lost_task = asyncio.create_task(self._connection_lost())
346-
347-
async def _connection_lost(self) -> None:
348-
"""Reconnect serial port."""
349-
try:
350-
await self._reconnect_till_done()
351-
except asyncio.CancelledError:
352-
LOGGER.debug("Cancelling reconnection attempt")
353-
raise
354-
355-
async def _reconnect_till_done(self) -> None:
356-
attempt = 1
357-
while True:
358-
try:
359-
await asyncio.wait_for(self.reconnect(), timeout=10)
360-
break
361-
except (asyncio.TimeoutError, OSError) as exc:
362-
wait = 2 ** min(attempt, 5)
363-
attempt += 1
364-
LOGGER.debug(
365-
"Couldn't re-open '%s' serial port, retrying in %ss: %s",
366-
self._config[CONF_DEVICE_PATH],
367-
wait,
368-
str(exc),
369-
)
370-
await asyncio.sleep(wait)
371-
372-
LOGGER.debug(
373-
"Reconnected '%s' serial port after %s attempts",
374-
self._config[CONF_DEVICE_PATH],
375-
attempt,
376-
)
327+
if self._app is not None:
328+
self._app.connection_lost(exc)
377329

378330
def close(self):
379331
"""Close the connection."""
380-
if self._conn_lost_task:
381-
self._conn_lost_task.cancel()
382-
self._conn_lost_task = None
383-
384332
if self._uart:
385333
self._uart.close()
386334
self._uart = None

zigpy_xbee/config.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
11
"""XBee module config."""
22

33
import voluptuous as vol
4-
from zigpy.config import ( # noqa: F401 pylint: disable=unused-import
5-
CONF_DATABASE,
6-
CONF_DEVICE,
7-
CONF_DEVICE_PATH,
8-
CONFIG_SCHEMA,
9-
SCHEMA_DEVICE,
10-
cv_boolean,
11-
)
12-
13-
CONF_DEVICE_BAUDRATE = "baudrate"
4+
import zigpy.config
145

15-
SCHEMA_DEVICE = SCHEMA_DEVICE.extend(
16-
{vol.Optional(CONF_DEVICE_BAUDRATE, default=57600): int}
6+
SCHEMA_DEVICE = zigpy.config.SCHEMA_DEVICE.extend(
7+
{vol.Optional(zigpy.config.CONF_DEVICE_BAUDRATE, default=57600): int}
178
)
189

19-
CONFIG_SCHEMA = CONFIG_SCHEMA.extend({vol.Required(CONF_DEVICE): SCHEMA_DEVICE})
10+
CONFIG_SCHEMA = zigpy.config.CONFIG_SCHEMA.extend(
11+
{vol.Required(zigpy.config.CONF_DEVICE): zigpy.config.SCHEMA_DEVICE}
12+
)

zigpy_xbee/uart.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
import logging
55
from typing import Any, Dict
66

7+
import zigpy.config
78
import zigpy.serial
89

9-
from zigpy_xbee.config import CONF_DEVICE_BAUDRATE, CONF_DEVICE_PATH
10-
1110
LOGGER = logging.getLogger(__name__)
1211

1312

@@ -178,8 +177,8 @@ async def connect(device_config: Dict[str, Any], api, loop=None) -> Gateway:
178177
transport, protocol = await zigpy.serial.create_serial_connection(
179178
loop,
180179
lambda: protocol,
181-
url=device_config[CONF_DEVICE_PATH],
182-
baudrate=device_config[CONF_DEVICE_BAUDRATE],
180+
url=device_config[zigpy.config.CONF_DEVICE_PATH],
181+
baudrate=device_config[zigpy.config.CONF_DEVICE_BAUDRATE],
183182
xonxoff=False,
184183
)
185184

zigpy_xbee/zigbee/application.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import zigpy.application
1212
import zigpy.config
13+
from zigpy.config import CONF_DEVICE
1314
import zigpy.device
1415
import zigpy.exceptions
1516
import zigpy.quirks
@@ -22,7 +23,7 @@
2223

2324
import zigpy_xbee
2425
import zigpy_xbee.api
25-
from zigpy_xbee.config import CONF_DEVICE, CONFIG_SCHEMA, SCHEMA_DEVICE
26+
import zigpy_xbee.config
2627
from zigpy_xbee.exceptions import InvalidCommand
2728
from zigpy_xbee.types import EUI64, UNKNOWN_IEEE, UNKNOWN_NWK, TXOptions, TXStatus
2829

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

45-
SCHEMA = CONFIG_SCHEMA
46-
SCHEMA_DEVICE = SCHEMA_DEVICE
47-
48-
probe = zigpy_xbee.api.XBee.probe
46+
CONFIG_SCHEMA = zigpy_xbee.config.CONFIG_SCHEMA
4947

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

54+
async def _watchdog_feed(self):
55+
await self._api._at_command("VR")
56+
5657
async def disconnect(self):
5758
"""Shutdown application."""
5859
if self._api:
@@ -136,6 +137,13 @@ async def load_network_info(self, *, load_devices=False):
136137
LOGGER.warning("CE command failed, assuming node is coordinator")
137138
node_info.logical_type = zdo_t.LogicalType.Coordinator
138139

140+
# TODO: Feature detect the XBee's exact model
141+
node_info.model = "XBee"
142+
node_info.manufacturer = "Digi"
143+
144+
version = await self._api._at_command("VR")
145+
node_info.version = f"{int(version):#06x}"
146+
139147
# Load network info
140148
pan_id = await self._api._at_command("OI")
141149
extended_pan_id = await self._api._at_command("ID")
@@ -350,10 +358,6 @@ async def permit_with_link_key(
350358
# 1 = Install Code With CRC (I? command of the joining device)
351359
await self._api.register_joining_device(node, reserved, key_type, link_key)
352360

353-
async def permit_with_key(self, node: EUI64, code: bytes, time_s=500):
354-
"""Permits a new device to join with the given IEEE and Install Code."""
355-
await self.permit_with_link_key(node, code, time_s, key_type=1)
356-
357361
def handle_modem_status(self, status):
358362
"""Handle changed Modem Status of the device."""
359363
LOGGER.info("Modem status update: %s (%s)", status.name, status.value)

0 commit comments

Comments
 (0)