Skip to content

Commit abc0949

Browse files
committed
Test and fix force_remove
1 parent 1fcf592 commit abc0949

File tree

4 files changed

+99
-37
lines changed

4 files changed

+99
-37
lines changed

tests/test_application.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ async def callback():
6969

7070
for response in responses:
7171
await asyncio.sleep(0.1)
72+
LOGGER.debug("Replying to %s with %s", request, response)
7273
self.send(response)
7374

7475
called_future.set_result(True)
@@ -84,6 +85,7 @@ async def callback():
8485

8586
for response in responses:
8687
await asyncio.sleep(0.1)
88+
LOGGER.debug("Replying to %s with %s", request, response)
8789
self.send(response)
8890

8991
callback.call_count = 0
@@ -822,3 +824,47 @@ async def test_update_network_bad_channel(mocker, caplog, application):
822824
await app.update_network(
823825
channel=t.uint8_t(12), channels=t.Channels.from_channel_list([11, 15, 20]),
824826
)
827+
828+
829+
@pytest_mark_asyncio_timeout(seconds=3)
830+
async def test_force_remove(application, mocker):
831+
app, znp_server = application
832+
833+
await app.startup(auto_form=False)
834+
835+
mocker.patch("zigpy_znp.zigbee.application.ZDO_REQUEST_TIMEOUT", new=0.3)
836+
837+
device = app.add_device(ieee=t.EUI64(range(8)), nwk=0xAABB)
838+
device.status = zigpy.device.Status.ENDPOINTS_INIT
839+
device.initializing = False
840+
841+
# Reply to zigpy's leave request
842+
bad_mgmt_leave_req = znp_server.reply_once_to(
843+
request=c.ZDOCommands.MgmtLeaveReq.Req(DstAddr=device.nwk, partial=True),
844+
responses=[c.ZDOCommands.MgmtLeaveReq.Rsp(Status=t.Status.Failure)],
845+
)
846+
847+
# Reply to our own leave request
848+
good_mgmt_leave_req = znp_server.reply_once_to(
849+
request=c.ZDOCommands.MgmtLeaveReq.Req(DstAddr=0x0000, partial=True),
850+
responses=[
851+
c.ZDOCommands.MgmtLeaveReq.Rsp(Status=t.Status.Success),
852+
c.ZDOCommands.LeaveInd.Callback(
853+
NWK=device.nwk,
854+
IEEE=device.ieee,
855+
Remove=False,
856+
Request=False,
857+
Rejoin=False,
858+
),
859+
],
860+
)
861+
862+
# Make sure the device exists
863+
assert app.get_device(nwk=device.nwk) is device
864+
865+
await app.remove(device.ieee)
866+
await asyncio.gather(bad_mgmt_leave_req, good_mgmt_leave_req)
867+
868+
# Make sure the device is gone once we remove it
869+
with pytest.raises(KeyError):
870+
app.get_device(nwk=device.nwk)

zigpy_znp/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ async def request(self, request, **response_params):
315315
# If the sync response we got is not what we wanted, this is an error
316316
if not partial_response.matches(response):
317317
raise InvalidCommandResponse(
318-
f"SRSP was not what we expected: {response} !~ {partial_response}"
318+
f"Expected SRSP response {partial_response}, got {response}", response
319319
)
320320

321321
return response

zigpy_znp/exceptions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ class CommandNotRecognized(CommandError):
1111

1212

1313
class InvalidCommandResponse(CommandError):
14-
pass
14+
def __init__(self, message, response):
15+
super().__init__(message)
16+
self.response = response

zigpy_znp/zigbee/application.py

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
from zigpy.types import ExtendedPanId, deserialize as list_deserialize
1818
from zigpy.zcl.clusters.security import IasZone
19+
from zigpy.exceptions import DeliveryError
1920

2021
import zigpy_znp.config as conf
2122
import zigpy_znp.types as t
2223
import zigpy_znp.commands as c
24+
from zigpy_znp.exceptions import InvalidCommandResponse
2325

2426
from zigpy_znp.api import ZNP
2527
from zigpy_znp.types.nvids import NwkNvIds
@@ -33,46 +35,45 @@
3335

3436
ZDO_CONVERTERS = {
3537
ZDOCmd.Node_Desc_req: (
36-
ZDOCmd.Node_Desc_rsp,
3738
(
38-
lambda addr, NWKAddrOfInterest: c.ZDOCommands.NodeDescReq.Req(
39+
lambda addr, device, NWKAddrOfInterest: c.ZDOCommands.NodeDescReq.Req(
3940
DstAddr=addr, NWKAddrOfInterest=NWKAddrOfInterest
4041
)
4142
),
42-
(
43-
lambda addr: c.ZDOCommands.NodeDescRsp.Callback(
44-
partial=True, Src=addr, Status=t.ZDOStatus.SUCCESS
45-
)
46-
),
47-
(lambda rsp, dev: [rsp.NodeDescriptor]),
43+
(lambda addr: c.ZDOCommands.NodeDescRsp.Callback(partial=True, Src=addr)),
44+
(lambda rsp: (ZDOCmd.Node_Desc_rsp, [rsp.NodeDescriptor])),
4845
),
4946
ZDOCmd.Active_EP_req: (
50-
ZDOCmd.Active_EP_rsp,
5147
(
52-
lambda addr, NWKAddrOfInterest: c.ZDOCommands.ActiveEpReq.Req(
48+
lambda addr, device, NWKAddrOfInterest: c.ZDOCommands.ActiveEpReq.Req(
5349
DstAddr=addr, NWKAddrOfInterest=NWKAddrOfInterest
5450
)
5551
),
56-
(
57-
lambda addr: c.ZDOCommands.ActiveEpRsp.Callback(
58-
partial=True, Src=addr, Status=t.ZDOStatus.SUCCESS
59-
)
60-
),
61-
(lambda rsp, dev: [rsp.ActiveEndpoints]),
52+
(lambda addr: c.ZDOCommands.ActiveEpRsp.Callback(partial=True, Src=addr)),
53+
(lambda rsp: (ZDOCmd.Active_EP_rsp, [rsp.ActiveEndpoints])),
6254
),
6355
ZDOCmd.Simple_Desc_req: (
64-
ZDOCmd.Simple_Desc_rsp,
6556
(
66-
lambda addr, NWKAddrOfInterest, EndPoint: c.ZDOCommands.SimpleDescReq.Req(
57+
# fmt: off
58+
lambda addr, device, NWKAddrOfInterest, EndPoint: \
59+
c.ZDOCommands.SimpleDescReq.Req(
6760
DstAddr=addr, NWKAddrOfInterest=NWKAddrOfInterest, Endpoint=EndPoint
6861
)
62+
# fmt: on
6963
),
64+
(lambda addr: c.ZDOCommands.SimpleDescRsp.Callback(partial=True, Src=addr)),
65+
(lambda rsp: (ZDOCmd.Simple_Desc_rsp, [rsp.SimpleDescriptor])),
66+
),
67+
ZDOCmd.Mgmt_Leave_req: (
7068
(
71-
lambda addr: c.ZDOCommands.SimpleDescRsp.Callback(
72-
partial=True, Src=addr, Status=t.ZDOStatus.SUCCESS
69+
lambda addr, device, DeviceAddress, Options: c.ZDOCommands.MgmtLeaveReq.Req(
70+
DstAddr=addr,
71+
IEEE=device.ieee,
72+
RemoveChildren_Rejoin=c.zdo.LeaveOptions(Options),
7373
)
7474
),
75-
(lambda rsp, dev: [rsp.SimpleDescriptor]),
75+
(lambda addr: c.ZDOCommands.MgmtLeaveRsp.Callback(partial=True, Src=addr)),
76+
(lambda rsp: (ZDOCmd.Mgmt_Leave_rsp, [rsp.Status])),
7677
),
7778
}
7879

@@ -480,34 +481,38 @@ async def _send_zdo_request(
480481
zdo_args, _ = list_deserialize(data, field_types)
481482
zdo_kwargs = dict(zip(field_names, zdo_args))
482483

484+
device = self.get_device(nwk=dst_addr.address)
485+
483486
# Call the converter with the ZDO request's kwargs
484-
rsp_cluster, req_factory, callback_factory, converter = ZDO_CONVERTERS[cluster]
485-
request = req_factory(dst_addr.address, **zdo_kwargs)
486-
callback = callback_factory(dst_addr.address)
487+
req_factory, rsp_factory, zdo_rsp_factory = ZDO_CONVERTERS[cluster]
488+
request = req_factory(dst_addr.address, device, **zdo_kwargs)
489+
callback = rsp_factory(dst_addr.address)
487490

488491
LOGGER.debug(
489-
"Intercepted AP ZDO request and replaced with %s - %s", request, callback
492+
"Intercepted AP ZDO request and replaced with %s/%s", request, callback
490493
)
491494

492-
async with async_timeout.timeout(ZDO_REQUEST_TIMEOUT):
493-
response = await self._znp.request_callback_rsp(
494-
request=request, RspStatus=t.Status.Success, callback=callback
495-
)
495+
try:
496+
async with async_timeout.timeout(ZDO_REQUEST_TIMEOUT):
497+
response = await self._znp.request_callback_rsp(
498+
request=request, RspStatus=t.Status.Success, callback=callback
499+
)
500+
except InvalidCommandResponse as e:
501+
raise DeliveryError(f"Could not send command: {e.response.Status}") from e
496502

497-
device = self.get_device(nwk=dst_addr.address)
503+
zdo_rsp_cluster, zdo_response_args = zdo_rsp_factory(response)
498504

499505
# Build up a ZDO response
500506
message = t.serialize_list(
501-
[t.uint8_t(sequence), response.Status, response.NWK]
502-
+ converter(response, device)
507+
[t.uint8_t(sequence), response.Status, response.NWK] + zdo_response_args
503508
)
504509
LOGGER.trace("Pretending we received a ZDO message: %s", message)
505510

506511
# We do not get any LQI info here
507512
self.handle_message(
508513
sender=device,
509514
profile=zigpy.profiles.zha.PROFILE_ID,
510-
cluster=rsp_cluster,
515+
cluster=zdo_rsp_cluster,
511516
src_ep=dst_ep,
512517
dst_ep=src_ep,
513518
message=message,
@@ -631,13 +636,22 @@ async def force_remove(self, device) -> None:
631636
"""Forcibly remove device from NCP."""
632637
await self._znp.request(
633638
c.ZDOCommands.MgmtLeaveReq.Req(
634-
DstAddr=device.nwk,
639+
DstAddr=0x0000, # We handle it
635640
IEEE=device.ieee,
636-
LeaveOptions=c.zdo.LeaveOptions.NONE,
641+
RemoveChildren_Rejoin=c.zdo.LeaveOptions.NONE,
637642
),
638643
RspStatus=t.Status.Success,
639644
)
640645

646+
# TODO: see what happens when we forcibly remove a device that isn't our child
647+
648+
# Just wait for the response, removing the device will be handled upstream
649+
await self._znp.wait_for_response(
650+
c.ZDOCommands.LeaveInd.Callback(
651+
NWK=device.nwk, IEEE=device.ieee, partial=True
652+
)
653+
)
654+
641655
async def permit_ncp(self, time_s: int) -> None:
642656
response = await self._znp.request_callback_rsp(
643657
request=c.ZDOCommands.MgmtPermitJoinReq.Req(

0 commit comments

Comments
 (0)