Skip to content

Commit 1f9f6a2

Browse files
authored
Implement radio probe (#101)
1 parent f4088a7 commit 1f9f6a2

File tree

3 files changed

+84
-10
lines changed

3 files changed

+84
-10
lines changed

tests/test_api.py

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import asyncio
22
import logging
3-
from unittest import mock
43

5-
import asynctest
4+
from asynctest import CoroutineMock, mock
65
import pytest
6+
import serial
77

88
from zigpy_deconz import api as deconz_api, types as t, uart
99
import zigpy_deconz.exception
@@ -32,9 +32,10 @@ async def test_connect(monkeypatch):
3232

3333

3434
def test_close(api):
35-
api._uart.close = mock.MagicMock()
35+
uart = api._uart
3636
api.close()
37-
assert api._uart.close.call_count == 1
37+
assert api._uart is None
38+
assert uart.close.call_count == 1
3839

3940

4041
def test_commands():
@@ -438,7 +439,7 @@ def test_device_state_network_state(data, network_state):
438439
async def test_reconnect_multiple_disconnects(monkeypatch, caplog):
439440
api = deconz_api.Deconz()
440441
dev = mock.sentinel.uart
441-
connect_mock = asynctest.CoroutineMock()
442+
connect_mock = CoroutineMock()
442443
connect_mock.return_value = asyncio.Future()
443444
connect_mock.return_value.set_result(True)
444445
monkeypatch.setattr(uart, "connect", connect_mock)
@@ -464,7 +465,7 @@ async def test_reconnect_multiple_disconnects(monkeypatch, caplog):
464465
async def test_reconnect_multiple_attempts(monkeypatch, caplog):
465466
api = deconz_api.Deconz()
466467
dev = mock.sentinel.uart
467-
connect_mock = asynctest.CoroutineMock()
468+
connect_mock = CoroutineMock()
468469
connect_mock.return_value = asyncio.Future()
469470
connect_mock.return_value.set_result(True)
470471
monkeypatch.setattr(uart, "connect", connect_mock)
@@ -477,9 +478,57 @@ async def test_reconnect_multiple_attempts(monkeypatch, caplog):
477478
connect_mock.reset_mock()
478479
connect_mock.side_effect = [asyncio.TimeoutError, OSError, connected]
479480

480-
with asynctest.mock.patch("asyncio.sleep"):
481+
with mock.patch("asyncio.sleep"):
481482
api.connection_lost("connection lost")
482483
await api._conn_lost_task
483484

484485
assert api._uart is mock.sentinel.uart_reconnect
485486
assert connect_mock.call_count == 3
487+
488+
489+
@pytest.mark.asyncio
490+
@mock.patch.object(deconz_api.Deconz, "device_state", new_callable=CoroutineMock)
491+
@mock.patch.object(uart, "connect")
492+
async def test_probe_success(mock_connect, mock_device_state):
493+
"""Test device probing."""
494+
495+
res = await deconz_api.Deconz.probe(mock.sentinel.uart, mock.sentinel.baud)
496+
assert res is True
497+
assert mock_connect.call_count == 1
498+
assert mock_connect.await_count == 1
499+
assert mock_connect.call_args[0][0] is mock.sentinel.uart
500+
assert mock_device_state.call_count == 1
501+
assert mock_connect.return_value.close.call_count == 1
502+
503+
mock_connect.reset_mock()
504+
mock_device_state.reset_mock()
505+
mock_connect.reset_mock()
506+
res = await deconz_api.Deconz.probe(mock.sentinel.uart, mock.sentinel.baud)
507+
assert res is True
508+
assert mock_connect.call_count == 1
509+
assert mock_connect.await_count == 1
510+
assert mock_connect.call_args[0][0] is mock.sentinel.uart
511+
assert mock_device_state.call_count == 1
512+
assert mock_connect.return_value.close.call_count == 1
513+
514+
515+
@pytest.mark.asyncio
516+
@mock.patch.object(deconz_api.Deconz, "device_state", new_callable=CoroutineMock)
517+
@mock.patch.object(uart, "connect")
518+
@pytest.mark.parametrize(
519+
"exception",
520+
(asyncio.TimeoutError, serial.SerialException, zigpy_deconz.exception.CommandError),
521+
)
522+
async def test_probe_fail(mock_connect, mock_device_state, exception):
523+
"""Test device probing fails."""
524+
525+
mock_device_state.side_effect = exception
526+
mock_device_state.reset_mock()
527+
mock_connect.reset_mock()
528+
res = await deconz_api.Deconz.probe(mock.sentinel.uart, mock.sentinel.baud)
529+
assert res is False
530+
assert mock_connect.call_count == 1
531+
assert mock_connect.await_count == 1
532+
assert mock_connect.call_args[0][0] is mock.sentinel.uart
533+
assert mock_device_state.call_count == 1
534+
assert mock_connect.return_value.close.call_count == 1

zigpy_deconz/api.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
import logging
55
import typing
66

7-
from zigpy_deconz.exception import CommandError
7+
import serial
8+
9+
from zigpy_deconz.exception import APIException, CommandError
810

911
from . import types as t, uart
1012

1113
LOGGER = logging.getLogger(__name__)
1214

1315
COMMAND_TIMEOUT = 2
1416
DECONZ_BAUDRATE = 38400
17+
PROBE_TIMEOUT = 3
1518
MIN_PROTO_VERSION = 0x010B
1619

1720

@@ -255,7 +258,9 @@ async def _reconnect_till_done(self) -> None:
255258
)
256259

257260
def close(self):
258-
return self._uart.close()
261+
if self._uart:
262+
self._uart.close()
263+
self._uart = None
259264

260265
async def _command(self, cmd, *args):
261266
LOGGER.debug("Command %s %s", cmd, args)
@@ -327,6 +332,26 @@ def change_network_state(self, state):
327332
def _handle_change_network_state(self, data):
328333
LOGGER.debug("Change network state response: %s", NetworkState(data[0]).name)
329334

335+
@classmethod
336+
async def probe(cls, device: str, baudrate: int = DECONZ_BAUDRATE) -> bool:
337+
"""Probe port for the device presence."""
338+
api = cls()
339+
try:
340+
await asyncio.wait_for(api._probe(device, baudrate), timeout=PROBE_TIMEOUT)
341+
return True
342+
except (asyncio.TimeoutError, serial.SerialException, APIException) as exc:
343+
LOGGER.debug("Unsuccessful radio probe of '%s' port", exc_info=exc)
344+
finally:
345+
api.close()
346+
347+
return False
348+
349+
async def _probe(self, device: str, baudrate: int = DECONZ_BAUDRATE) -> None:
350+
"""Open port and try sending a command"""
351+
await self.connect(device, baudrate)
352+
await self.device_state()
353+
self.close()
354+
330355
async def read_parameter(self, id_, *args):
331356
try:
332357
if isinstance(id_, str):

zigpy_deconz/exception.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
class CommandError(APIException):
5-
def __init__(self, status, *args, **kwargs):
5+
def __init__(self, status=1, *args, **kwargs):
66
self._status = status
77
super().__init__(*args, **kwargs)
88

0 commit comments

Comments
 (0)