Skip to content

Commit 7a88a69

Browse files
authored
Implement zigpy 0.60.0 changes (#150)
* Implement zigpy 0.60.0 changes * Implement command priority
1 parent 8b00d74 commit 7a88a69

File tree

8 files changed

+54
-196
lines changed

8 files changed

+54
-196
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ license = {text = "GPL-3.0"}
1515
requires-python = ">=3.8"
1616
dependencies = [
1717
"voluptuous",
18-
"zigpy>=0.60.0",
18+
"zigpy>=0.60.2",
1919
"pyusb>=1.1.0",
2020
"gpiozero",
2121
'async-timeout; python_version<"3.11"',

tests/test_api.py

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import asyncio
2-
from unittest.mock import AsyncMock, MagicMock, patch, sentinel
2+
from unittest.mock import MagicMock, patch, sentinel
33

44
import pytest
5-
import serial
65
import serial_asyncio
6+
import zigpy.config as config
77

88
from zigpy_zigate import api as zigate_api
9-
import zigpy_zigate.config as config
109
import zigpy_zigate.uart
1110

1211
DEVICE_CONFIG = config.SCHEMA_DEVICE({config.CONF_DEVICE_PATH: "/dev/null"})
@@ -55,51 +54,6 @@ async def test_api_new(conn_mck):
5554
assert conn_mck.await_count == 1
5655

5756

58-
@pytest.mark.asyncio
59-
@patch.object(zigate_api.ZiGate, "set_raw_mode", new_callable=AsyncMock)
60-
@pytest.mark.parametrize(
61-
"port",
62-
("/dev/null", "pizigate:/dev/ttyAMA0"),
63-
)
64-
async def test_probe_success(mock_raw_mode, port, monkeypatch):
65-
"""Test device probing."""
66-
67-
async def mock_conn(loop, protocol_factory, **kwargs):
68-
protocol = protocol_factory()
69-
loop.call_soon(protocol.connection_made, None)
70-
return None, protocol
71-
72-
monkeypatch.setattr(serial_asyncio, "create_serial_connection", mock_conn)
73-
DEVICE_CONFIG = zigpy_zigate.config.SCHEMA_DEVICE(
74-
{zigpy_zigate.config.CONF_DEVICE_PATH: port}
75-
)
76-
res = await zigate_api.ZiGate.probe(DEVICE_CONFIG)
77-
assert res is True
78-
assert mock_raw_mode.call_count == 1
79-
80-
81-
@pytest.mark.asyncio
82-
@patch.object(zigate_api.ZiGate, "set_raw_mode", side_effect=asyncio.TimeoutError)
83-
@patch.object(zigpy_zigate.uart, "connect")
84-
@pytest.mark.parametrize(
85-
"exception",
86-
(asyncio.TimeoutError, serial.SerialException, zigate_api.NoResponseError),
87-
)
88-
async def test_probe_fail(mock_connect, mock_raw_mode, exception):
89-
"""Test device probing fails."""
90-
91-
mock_raw_mode.side_effect = exception
92-
mock_connect.reset_mock()
93-
mock_raw_mode.reset_mock()
94-
res = await zigate_api.ZiGate.probe(DEVICE_CONFIG)
95-
assert res is False
96-
assert mock_connect.call_count == 1
97-
assert mock_connect.await_count == 1
98-
assert mock_connect.call_args[0][0] == DEVICE_CONFIG
99-
assert mock_raw_mode.call_count == 1
100-
assert mock_connect.return_value.close.call_count == 1
101-
102-
10357
@pytest.mark.asyncio
10458
@patch.object(asyncio, "wait", return_value=([], []))
10559
async def test_api_command(mock_command, api):

tests/test_application.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
from unittest.mock import AsyncMock, MagicMock, call, patch
33

44
import pytest
5+
import zigpy.config as config
56
import zigpy.exceptions
67
import zigpy.types as zigpy_t
78

89
import zigpy_zigate.api
9-
import zigpy_zigate.config as config
1010
import zigpy_zigate.types as t
1111
import zigpy_zigate.zigbee.application
1212

@@ -39,11 +39,6 @@ def test_zigpy_ieee(app):
3939
assert dst_addr.serialize() == b"\x03" + data[::-1] + b"\x01"
4040

4141

42-
def test_model_detection(app):
43-
device = zigpy_zigate.zigbee.application.ZiGateDevice(app, 0, 0)
44-
assert device.model == "ZiGate USB-TTL {}".format(FAKE_FIRMWARE_VERSION)
45-
46-
4742
@pytest.mark.asyncio
4843
async def test_form_network_success(app):
4944
app._api.erase_persistent_data = AsyncMock()
@@ -76,6 +71,9 @@ async def mock_get_network_state():
7671
assert app.state.node_info.ieee == zigpy.types.EUI64.convert(
7772
"01:23:45:67:89:ab:cd:ef"
7873
)
74+
assert app.state.node_info.version == "3.1z"
75+
assert app.state.node_info.model == "ZiGate USB-TTL"
76+
assert app.state.node_info.manufacturer == "ZiGate"
7977
assert app.state.network_info.pan_id == 0x1234
8078
assert app.state.network_info.extended_pan_id == zigpy.types.ExtendedPanId.convert(
8179
"12:34:ab:cd:ef:01:23:45"

tests/test_uart.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
import pytest
55
import serial.tools.list_ports
66
import serial_asyncio
7+
import zigpy.config
78

89
from zigpy_zigate import common, uart
9-
import zigpy_zigate.config
1010

1111

1212
@pytest.fixture
@@ -32,9 +32,7 @@ async def mock_conn(loop, protocol_factory, url, **kwargs):
3232

3333
monkeypatch.setattr(serial_asyncio, "create_serial_connection", mock_conn)
3434
monkeypatch.setattr(common, "set_pizigate_running_mode", AsyncMock())
35-
DEVICE_CONFIG = zigpy_zigate.config.SCHEMA_DEVICE(
36-
{zigpy_zigate.config.CONF_DEVICE_PATH: port}
37-
)
35+
DEVICE_CONFIG = zigpy.config.SCHEMA_DEVICE({zigpy.config.CONF_DEVICE_PATH: port})
3836

3937
await uart.connect(DEVICE_CONFIG, api)
4038

zigpy_zigate/api.py

Lines changed: 21 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import asyncio
22
import binascii
3-
import datetime
3+
from datetime import datetime, timezone
44
import enum
55
import functools
66
import logging
77
from typing import Any, Dict
88

9-
import serial
9+
from zigpy.datastructures import PriorityLock
1010
import zigpy.exceptions
1111
import zigpy.types
1212

13-
import zigpy_zigate.config
1413
import zigpy_zigate.uart
1514

1615
from . import types as t
@@ -227,8 +226,7 @@ def __init__(self, device_config: Dict[str, Any]):
227226
self._uart = None
228227
self._awaiting = {}
229228
self._status_awaiting = {}
230-
self._lock = asyncio.Lock()
231-
self._conn_lost_task = None
229+
self._lock = PriorityLock()
232230

233231
self.network_state = None
234232

@@ -245,59 +243,14 @@ async def connect(self):
245243

246244
def connection_lost(self, exc: Exception) -> None:
247245
"""Lost serial connection."""
248-
LOGGER.warning(
249-
"Serial '%s' connection lost unexpectedly: %s",
250-
self._config[zigpy_zigate.config.CONF_DEVICE_PATH],
251-
exc,
252-
)
253-
self._uart = None
254-
if self._conn_lost_task and not self._conn_lost_task.done():
255-
self._conn_lost_task.cancel()
256-
self._conn_lost_task = asyncio.ensure_future(self._connection_lost())
257-
258-
async def _connection_lost(self) -> None:
259-
"""Reconnect serial port."""
260-
try:
261-
await self._reconnect_till_done()
262-
except asyncio.CancelledError:
263-
LOGGER.debug("Cancelling reconnection attempt")
264-
265-
async def _reconnect_till_done(self) -> None:
266-
attempt = 1
267-
while True:
268-
try:
269-
await asyncio.wait_for(self.reconnect(), timeout=10)
270-
break
271-
except (asyncio.TimeoutError, OSError) as exc:
272-
wait = 2 ** min(attempt, 5)
273-
attempt += 1
274-
LOGGER.debug(
275-
"Couldn't re-open '%s' serial port, retrying in %ss: %s",
276-
self._config[zigpy_zigate.config.CONF_DEVICE_PATH],
277-
wait,
278-
str(exc),
279-
)
280-
await asyncio.sleep(wait)
281-
282-
LOGGER.debug(
283-
"Reconnected '%s' serial port after %s attempts",
284-
self._config[zigpy_zigate.config.CONF_DEVICE_PATH],
285-
attempt,
286-
)
246+
if self._app is not None:
247+
self._app.connection_lost(exc)
287248

288249
def close(self):
289250
if self._uart:
290251
self._uart.close()
291252
self._uart = None
292253

293-
def reconnect(self):
294-
"""Reconnect using saved parameters."""
295-
LOGGER.debug(
296-
"Reconnecting '%s' serial port",
297-
self._config[zigpy_zigate.config.CONF_DEVICE_PATH],
298-
)
299-
return self.connect()
300-
301254
def set_application(self, app):
302255
self._app = app
303256

@@ -351,6 +304,14 @@ async def wait_for_response(self, wait_response):
351304
self._awaiting[wait_response].cancel()
352305
del self._awaiting[wait_response]
353306

307+
def _get_command_priority(self, cmd):
308+
return {
309+
# Watchdog command is prioritized
310+
CommandId.SET_TIMESERVER: 9999,
311+
# APS command is deprioritized
312+
CommandId.SEND_RAW_APS_DATA_PACKET: -1,
313+
}.get(cmd, 0)
314+
354315
async def command(
355316
self,
356317
cmd,
@@ -359,7 +320,7 @@ async def command(
359320
wait_status=True,
360321
timeout=COMMAND_TIMEOUT,
361322
):
362-
async with self._lock:
323+
async with self._lock(priority=self._get_command_priority(cmd)):
363324
tries = 3
364325

365326
tasks = []
@@ -454,13 +415,13 @@ async def erase_persistent_data(self):
454415
CommandId.RESET, wait_response=ResponseId.NODE_FACTORY_NEW_RESTART
455416
)
456417

457-
async def set_time(self, dt=None):
458-
"""set internal time
459-
if timestamp is None, now is used
460-
"""
461-
dt = dt or datetime.datetime.now()
462-
timestamp = int((dt - datetime.datetime(2000, 1, 1)).total_seconds())
463-
data = t.serialize([timestamp], COMMANDS[CommandId.SET_TIMESERVER])
418+
async def set_time(self):
419+
"""set internal time"""
420+
timestamp = (
421+
datetime.now(timezone.utc) - datetime(2000, 1, 1, tzinfo=timezone.utc)
422+
).total_seconds()
423+
424+
data = t.serialize([int(timestamp)], COMMANDS[CommandId.SET_TIMESERVER])
464425
await self.command(CommandId.SET_TIMESERVER, data)
465426

466427
async def get_time_server(self):
@@ -585,30 +546,3 @@ async def get_network_key(self):
585546
raise CommandNotSupportedError()
586547

587548
return rsp[0]
588-
589-
@classmethod
590-
async def probe(cls, device_config: Dict[str, Any]) -> bool:
591-
"""Probe port for the device presence."""
592-
api = cls(zigpy_zigate.config.SCHEMA_DEVICE(device_config))
593-
try:
594-
await asyncio.wait_for(api._probe(), timeout=PROBE_TIMEOUT)
595-
return True
596-
except (
597-
asyncio.TimeoutError,
598-
serial.SerialException,
599-
zigpy.exceptions.ZigbeeException,
600-
) as exc:
601-
LOGGER.debug(
602-
"Unsuccessful radio probe of '%s' port",
603-
device_config[zigpy_zigate.config.CONF_DEVICE_PATH],
604-
exc_info=exc,
605-
)
606-
finally:
607-
api.close()
608-
609-
return False
610-
611-
async def _probe(self) -> None:
612-
"""Open port and try sending a command"""
613-
await self.connect()
614-
await self.set_raw_mode()

zigpy_zigate/config.py

Lines changed: 0 additions & 7 deletions
This file was deleted.

zigpy_zigate/uart.py

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

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

910
from . import common as c
10-
from .config import CONF_DEVICE_PATH
1111

1212
LOGGER = logging.getLogger(__name__)
13-
ZIGATE_BAUDRATE = 115200
1413

1514

1615
class Gateway(asyncio.Protocol):
@@ -139,7 +138,7 @@ async def connect(device_config: Dict[str, Any], api, loop=None):
139138
connected_future = asyncio.Future()
140139
protocol = Gateway(api, connected_future)
141140

142-
port = device_config[CONF_DEVICE_PATH]
141+
port = device_config[zigpy.config.CONF_DEVICE_PATH]
143142
if port == "auto":
144143
port = await loop.run_in_executor(None, c.discover_port)
145144

@@ -159,7 +158,7 @@ async def connect(device_config: Dict[str, Any], api, loop=None):
159158
loop,
160159
lambda: protocol,
161160
url=port,
162-
baudrate=ZIGATE_BAUDRATE,
161+
baudrate=device_config[zigpy.config.CONF_DEVICE_BAUDRATE],
163162
xonxoff=False,
164163
)
165164

0 commit comments

Comments
 (0)