Skip to content

Commit aa497a8

Browse files
authored
Implement radio probe (#90)
* Probe method. * Relax command mode AT response decoding. * Update tests.
1 parent 58f7292 commit aa497a8

File tree

4 files changed

+109
-14
lines changed

4 files changed

+109
-14
lines changed

tests/test_api.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from asynctest import CoroutineMock, mock
55
import pytest
6+
import serial
67
import zigpy.exceptions
78

89
from zigpy_xbee import api as xbee_api, types as t, uart
@@ -588,3 +589,80 @@ async def test_reconnect_multiple_attempts(monkeypatch, caplog):
588589

589590
assert api._uart is mock.sentinel.uart_reconnect
590591
assert connect_mock.call_count == 3
592+
593+
594+
@pytest.mark.asyncio
595+
@mock.patch.object(xbee_api.XBee, "_at_command", new_callable=CoroutineMock)
596+
@mock.patch.object(uart, "connect")
597+
async def test_probe_success(mock_connect, mock_at_cmd):
598+
"""Test device probing."""
599+
600+
res = await xbee_api.XBee.probe(mock.sentinel.uart, mock.sentinel.baud)
601+
assert res is True
602+
assert mock_connect.call_count == 1
603+
assert mock_connect.await_count == 1
604+
assert mock_connect.call_args[0][0] is mock.sentinel.uart
605+
assert mock_at_cmd.call_count == 1
606+
assert mock_connect.return_value.close.call_count == 1
607+
608+
609+
@pytest.mark.asyncio
610+
@mock.patch.object(xbee_api.XBee, "init_api_mode", return_value=True)
611+
@mock.patch.object(xbee_api.XBee, "_at_command", side_effect=asyncio.TimeoutError)
612+
@mock.patch.object(uart, "connect")
613+
async def test_probe_success_api_mode(mock_connect, mock_at_cmd, mock_api_mode):
614+
"""Test device probing."""
615+
616+
res = await xbee_api.XBee.probe(mock.sentinel.uart, mock.sentinel.baud)
617+
assert res is True
618+
assert mock_connect.call_count == 1
619+
assert mock_connect.await_count == 1
620+
assert mock_connect.call_args[0][0] is mock.sentinel.uart
621+
assert mock_at_cmd.call_count == 1
622+
assert mock_api_mode.call_count == 1
623+
assert mock_connect.return_value.close.call_count == 1
624+
625+
626+
@pytest.mark.asyncio
627+
@mock.patch.object(xbee_api.XBee, "init_api_mode")
628+
@mock.patch.object(xbee_api.XBee, "_at_command", side_effect=asyncio.TimeoutError)
629+
@mock.patch.object(uart, "connect")
630+
@pytest.mark.parametrize(
631+
"exception",
632+
(asyncio.TimeoutError, serial.SerialException, zigpy.exceptions.APIException),
633+
)
634+
async def test_probe_fail(mock_connect, mock_at_cmd, mock_api_mode, exception):
635+
"""Test device probing fails."""
636+
637+
mock_api_mode.side_effect = exception
638+
mock_api_mode.reset_mock()
639+
mock_at_cmd.reset_mock()
640+
mock_connect.reset_mock()
641+
res = await xbee_api.XBee.probe(mock.sentinel.uart, mock.sentinel.baud)
642+
assert res is False
643+
assert mock_connect.call_count == 1
644+
assert mock_connect.await_count == 1
645+
assert mock_connect.call_args[0][0] is mock.sentinel.uart
646+
assert mock_at_cmd.call_count == 1
647+
assert mock_api_mode.call_count == 1
648+
assert mock_connect.return_value.close.call_count == 1
649+
650+
651+
@pytest.mark.asyncio
652+
@mock.patch.object(xbee_api.XBee, "init_api_mode", return_value=False)
653+
@mock.patch.object(xbee_api.XBee, "_at_command", side_effect=asyncio.TimeoutError)
654+
@mock.patch.object(uart, "connect")
655+
async def test_probe_fail_api_mode(mock_connect, mock_at_cmd, mock_api_mode):
656+
"""Test device probing fails."""
657+
658+
mock_api_mode.reset_mock()
659+
mock_at_cmd.reset_mock()
660+
mock_connect.reset_mock()
661+
res = await xbee_api.XBee.probe(mock.sentinel.uart, mock.sentinel.baud)
662+
assert res is False
663+
assert mock_connect.call_count == 1
664+
assert mock_connect.await_count == 1
665+
assert mock_connect.call_args[0][0] is mock.sentinel.uart
666+
assert mock_at_cmd.call_count == 1
667+
assert mock_api_mode.call_count == 1
668+
assert mock_connect.return_value.close.call_count == 1

tests/test_uart.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,6 @@ def test_command_mode_rsp(gw):
4848
assert gw._api.handle_command_mode_rsp.call_args[0][0] == "OK"
4949

5050

51-
def test_command_mode_rsp_decode_exc(gw):
52-
data = b"OK\x81"
53-
with pytest.raises(UnicodeDecodeError):
54-
gw.command_mode_rsp(data)
55-
assert gw._api.handle_command_mode_rsp.call_count == 0
56-
57-
5851
def test_command_mode_send(gw):
5952
data = b"ATAP2\x0D"
6053
gw.command_mode_send(data)

zigpy_xbee/api.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import functools
55
import logging
66

7+
import serial
78
from zigpy.exceptions import APIException, DeliveryError
89
from zigpy.types import LVList
910

@@ -13,6 +14,7 @@
1314

1415
AT_COMMAND_TIMEOUT = 2
1516
REMOTE_AT_COMMAND_TIMEOUT = 30
17+
PROBE_TIMEOUT = 45
1618

1719

1820
class ModemStatus(t.uint8_t, t.UndefinedEnum):
@@ -480,9 +482,9 @@ def handle_command_mode_rsp(self, data):
480482
fut = self._cmd_mode_future
481483
if fut is None or fut.done():
482484
return
483-
if data == "OK":
485+
if "OK" in data:
484486
fut.set_result(True)
485-
elif data == "ERROR":
487+
elif "ERROR" in data:
486488
fut.set_result(False)
487489
else:
488490
fut.set_result(data)
@@ -548,6 +550,32 @@ async def init_api_mode(self):
548550
)
549551
return False
550552

553+
@classmethod
554+
async def probe(cls, device: str, baudrate: int) -> bool:
555+
"""Probe port for the device presence."""
556+
api = cls()
557+
try:
558+
await asyncio.wait_for(api._probe(device, baudrate), timeout=PROBE_TIMEOUT)
559+
return True
560+
except (asyncio.TimeoutError, serial.SerialException, APIException) as exc:
561+
LOGGER.debug("Unsuccessful radio probe of '%s' port", exc_info=exc)
562+
finally:
563+
api.close()
564+
565+
return False
566+
567+
async def _probe(self, device: str, baudrate: int) -> None:
568+
"""Open port and try sending a command"""
569+
await self.connect(device, baudrate)
570+
try:
571+
# Ensure we have escaped commands
572+
await self._at_command("AP", 2)
573+
except asyncio.TimeoutError:
574+
if not await self.init_api_mode():
575+
raise APIException("Failed to configure XBee for API mode")
576+
finally:
577+
self.close()
578+
551579
def __getattr__(self, item):
552580
if item in COMMAND_REQUESTS:
553581
return functools.partial(self._command, item)

zigpy_xbee/uart.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,7 @@ def connection_made(self, transport):
6969

7070
def command_mode_rsp(self, data):
7171
"""Handles AT command mode response."""
72-
try:
73-
data = data.decode("ascii")
74-
except UnicodeDecodeError as ex:
75-
LOGGER.debug("Couldn't ascii decode AT command mode response: %s", ex)
76-
raise
72+
data = data.decode("ascii", "ignore")
7773
LOGGER.debug("Handling AT command mode response: %s", data)
7874
self._api.handle_command_mode_rsp(data)
7975

0 commit comments

Comments
 (0)