Skip to content

Commit 8d99373

Browse files
authored
Handle CB3 not sending the correct add_neighbor response (#247)
* Handle CB3 not sending the correct response * Fix broken unit test * Fix remaining broken unit tests
1 parent adf2b39 commit 8d99373

File tree

7 files changed

+132
-30
lines changed

7 files changed

+132
-30
lines changed

tests/test_api.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,7 @@ async def test_bad_command_parsing(api, caplog):
755755

756756
assert 0xFF not in deconz_api.COMMAND_SCHEMAS
757757

758-
with caplog.at_level(logging.WARNING):
758+
with caplog.at_level(logging.DEBUG):
759759
api.data_received(
760760
bytes.fromhex(
761761
"172c002f0028002e02000000020000000000"
@@ -961,6 +961,65 @@ async def test_add_neighbour(api, mock_command_rsp):
961961
]
962962

963963

964+
async def test_add_neighbour_conbee3_success(api):
965+
api._command = AsyncMock(wraps=api._command)
966+
api._uart = AsyncMock()
967+
968+
# Simulate a good but invalid response from the Conbee III
969+
asyncio.get_running_loop().call_later(
970+
0.001,
971+
lambda: api.data_received(
972+
b"\x1d" + bytes([api._seq - 1]) + b"\x00\x06\x00\x01"
973+
),
974+
)
975+
976+
await api.add_neighbour(
977+
nwk=0x1234,
978+
ieee=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"),
979+
mac_capability_flags=0x12,
980+
)
981+
982+
assert api._command.mock_calls == [
983+
call(
984+
deconz_api.CommandId.update_neighbor,
985+
action=deconz_api.UpdateNeighborAction.ADD,
986+
nwk=0x1234,
987+
ieee=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"),
988+
mac_capability_flags=0x12,
989+
)
990+
]
991+
992+
993+
async def test_add_neighbour_conbee3_failure(api):
994+
api._command = AsyncMock(wraps=api._command)
995+
api._uart = AsyncMock()
996+
997+
# Simulate a bad response from the Conbee III
998+
asyncio.get_running_loop().call_later(
999+
0.001,
1000+
lambda: api.data_received(
1001+
b"\x1d" + bytes([api._seq - 1]) + b"\x01\x06\x00\x01"
1002+
),
1003+
)
1004+
1005+
with pytest.raises(deconz_api.CommandError):
1006+
await api.add_neighbour(
1007+
nwk=0x1234,
1008+
ieee=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"),
1009+
mac_capability_flags=0x12,
1010+
)
1011+
1012+
assert api._command.mock_calls == [
1013+
call(
1014+
deconz_api.CommandId.update_neighbor,
1015+
action=deconz_api.UpdateNeighborAction.ADD,
1016+
nwk=0x1234,
1017+
ieee=t.EUI64.convert("aa:bb:cc:dd:11:22:33:44"),
1018+
mac_capability_flags=0x12,
1019+
)
1020+
]
1021+
1022+
9641023
async def test_cb3_device_state_callback_bug(api, mock_command_rsp):
9651024
mock_command_rsp(
9661025
command_id=deconz_api.CommandId.version,

tests/test_application.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ async def test_disconnect_no_api(app):
216216

217217
async def test_disconnect_close_error(app):
218218
app._api.write_parameter = MagicMock(
219-
side_effect=zigpy_deconz.exception.CommandError(1, "Error")
219+
side_effect=zigpy_deconz.exception.CommandError("Error", status=1, command=None)
220220
)
221221
await app.disconnect()
222222

@@ -399,13 +399,17 @@ def mock_add_neighbour(nwk, ieee, mac_capability_flags):
399399

400400
if max_neighbors < 0:
401401
raise zigpy_deconz.exception.CommandError(
402-
deconz_api.Status.FAILURE, "Failure"
402+
"Failure",
403+
status=deconz_api.Status.FAILURE,
404+
command=None,
403405
)
404406

405407
p = patch.object(app, "_api", spec_set=zigpy_deconz.api.Deconz(None, None))
406408

407409
with p as api_mock:
408-
err = zigpy_deconz.exception.CommandError(deconz_api.Status.FAILURE, "Failure")
410+
err = zigpy_deconz.exception.CommandError(
411+
"Failure", status=deconz_api.Status.FAILURE, command=None
412+
)
409413
api_mock.add_neighbour = AsyncMock(side_effect=[None, err, err, err])
410414

411415
with caplog.at_level(logging.DEBUG):
@@ -483,7 +487,9 @@ async def read_param(param_id, index):
483487

484488
if index not in slots:
485489
raise zigpy_deconz.exception.CommandError(
486-
deconz_api.Status.UNSUPPORTED, "Unsupported"
490+
"Unsupported",
491+
status=deconz_api.Status.UNSUPPORTED,
492+
command=None,
487493
)
488494
else:
489495
return deconz_api.IndexedEndpoint(index=index, descriptor=slots[index])
@@ -504,7 +510,9 @@ async def read_param(param_id, index):
504510
assert index in (0x00, 0x01)
505511

506512
raise zigpy_deconz.exception.CommandError(
507-
deconz_api.Status.UNSUPPORTED, "Unsupported"
513+
"Unsupported",
514+
status=deconz_api.Status.UNSUPPORTED,
515+
command=None,
508516
)
509517

510518
app._api.read_parameter = AsyncMock(side_effect=read_param)
@@ -524,7 +532,9 @@ async def read_param(param_id, index):
524532

525533
if index > 0x01:
526534
raise zigpy_deconz.exception.CommandError(
527-
deconz_api.Status.UNSUPPORTED, "Unsupported"
535+
"Unsupported",
536+
status=deconz_api.Status.UNSUPPORTED,
537+
command=None,
528538
)
529539

530540
return deconz_api.IndexedEndpoint(

tests/test_exception.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
def test_command_error():
99
ex = zigpy_deconz.exception.CommandError(
10-
mock.sentinel.status, mock.sentinel.message
10+
mock.sentinel.message,
11+
status=mock.sentinel.status,
12+
command=mock.sentinel.command,
1113
)
1214
assert ex.status is mock.sentinel.status
15+
assert ex.command is mock.sentinel.command

tests/test_network_state.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ async def write_parameter(param, *args):
123123
and param == zigpy_deconz.api.NetworkParameter.nwk_frame_counter
124124
):
125125
raise zigpy_deconz.exception.CommandError(
126-
status=zigpy_deconz.api.Status.UNSUPPORTED
126+
"Command is unsupported",
127+
status=zigpy_deconz.api.Status.UNSUPPORTED,
128+
command=None,
127129
)
128130

129131
params[param.name] = args
@@ -212,7 +214,9 @@ async def write_parameter(param, *args):
212214
None,
213215
{
214216
("nwk_frame_counter",): zigpy_deconz.exception.CommandError(
215-
zigpy_deconz.api.Status.UNSUPPORTED
217+
"Some error",
218+
status=zigpy_deconz.api.Status.UNSUPPORTED,
219+
command=None,
216220
)
217221
},
218222
{"network_key.tx_counter": 0},

tests/test_send_receive.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import zigpy.exceptions
99
import zigpy.types as zigpy_t
1010

11-
from zigpy_deconz.api import TXStatus
11+
from zigpy_deconz.api import Status, TXStatus
1212
import zigpy_deconz.exception
1313
import zigpy_deconz.types as t
1414

@@ -23,7 +23,9 @@ async def mock_send(req_id, *args, **kwargs):
2323
await asyncio.sleep(0)
2424

2525
if fail_enqueue:
26-
raise zigpy_deconz.exception.CommandError("Error")
26+
raise zigpy_deconz.exception.CommandError(
27+
"Error", status=Status.FAILURE, command=None
28+
)
2729

2830
if fail_deliver:
2931
app.handle_tx_confirm(

zigpy_deconz/api.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
)
2828
from zigpy.zdo.types import SimpleDescriptor
2929

30-
from zigpy_deconz.exception import APIException, CommandError, MismatchedResponseError
30+
from zigpy_deconz.exception import CommandError, MismatchedResponseError, ParsingError
3131
import zigpy_deconz.types as t
3232
import zigpy_deconz.uart
3333
from zigpy_deconz.utils import restart_forever
@@ -568,7 +568,11 @@ async def _command(self, cmd, **kwargs):
568568

569569
if self._uart is None:
570570
# connection was lost
571-
raise CommandError(Status.ERROR, "API is not running")
571+
raise CommandError(
572+
"API is not running",
573+
status=Status.ERROR,
574+
command=command,
575+
)
572576

573577
async with self._command_lock(priority=self._get_command_priority(command)):
574578
seq = self._seq
@@ -616,11 +620,15 @@ def data_received(self, data: bytes) -> None:
616620
try:
617621
params, rest = t.deserialize_dict(command.payload, rx_schema)
618622
except Exception:
619-
LOGGER.warning("Failed to parse command %s", command, exc_info=True)
623+
LOGGER.debug("Failed to parse command %s", command, exc_info=True)
620624

621625
if fut is not None and not fut.done():
622626
fut.set_exception(
623-
APIException(f"Failed to deserialize command: {command}")
627+
ParsingError(
628+
f"Failed to parse command: {command}",
629+
status=Status.ERROR,
630+
command=command,
631+
)
624632
)
625633

626634
return
@@ -677,7 +685,11 @@ def data_received(self, data: bytes) -> None:
677685
# Make sure we do not resolve the future
678686
fut = None
679687
elif status != Status.SUCCESS:
680-
exc = CommandError(status, f"{command.command_id}, status: {status}")
688+
exc = CommandError(
689+
f"{command.command_id}, status: {status}",
690+
status=status,
691+
command=command,
692+
)
681693

682694
if fut is not None:
683695
try:
@@ -905,10 +917,21 @@ async def change_network_state(self, new_state: NetworkState) -> None:
905917
async def add_neighbour(
906918
self, nwk: t.NWK, ieee: t.EUI64, mac_capability_flags: t.uint8_t
907919
) -> None:
908-
await self.send_command(
909-
CommandId.update_neighbor,
910-
action=UpdateNeighborAction.ADD,
911-
nwk=nwk,
912-
ieee=ieee,
913-
mac_capability_flags=mac_capability_flags,
914-
)
920+
try:
921+
await self.send_command(
922+
CommandId.update_neighbor,
923+
action=UpdateNeighborAction.ADD,
924+
nwk=nwk,
925+
ieee=ieee,
926+
mac_capability_flags=mac_capability_flags,
927+
)
928+
except ParsingError as exc:
929+
# Older Conbee III firmwares send back an invalid response
930+
status = Status(exc.command.payload[0])
931+
932+
if status != Status.SUCCESS:
933+
raise CommandError(
934+
f"{exc.command.command_id}, status: {status}",
935+
status=status,
936+
command=exc.command,
937+
) from exc

zigpy_deconz/exception.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@
77
from zigpy.exceptions import APIException
88

99
if typing.TYPE_CHECKING:
10-
from zigpy_deconz.api import CommandId
10+
from zigpy_deconz.api import Command, CommandId, Status
1111

1212

1313
class CommandError(APIException):
14-
def __init__(self, status=1, *args, **kwargs):
14+
def __init__(self, *args, status: Status, command: Command, **kwargs):
1515
"""Initialize instance."""
16-
self._status = status
1716
super().__init__(*args, **kwargs)
17+
self.command = command
18+
self.status = status
1819

19-
@property
20-
def status(self):
21-
return self._status
20+
21+
class ParsingError(CommandError):
22+
pass
2223

2324

2425
class MismatchedResponseError(APIException):

0 commit comments

Comments
 (0)