Skip to content

Commit 4edfb13

Browse files
committed
Add unit tests and properly handle unicast ZDO requests to coordinator
1 parent 9d9df3a commit 4edfb13

File tree

2 files changed

+173
-20
lines changed

2 files changed

+173
-20
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import asyncio
2+
3+
import pytest
4+
import zigpy.zdo
5+
import zigpy.types as zigpy_t
6+
import zigpy.zdo.types as zdo_t
7+
8+
import zigpy_znp.types as t
9+
import zigpy_znp.commands as c
10+
11+
from tests.conftest import FormedLaunchpadCC26X2R1
12+
13+
14+
@pytest.mark.parametrize(
15+
"broadcast,nwk_update_id,change_channel",
16+
[
17+
(False, 1, False),
18+
(False, 1, True),
19+
(True, 1, False),
20+
(False, 200, True),
21+
],
22+
)
23+
@pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1])
24+
async def test_mgmt_nwk_update_req(
25+
device, broadcast, nwk_update_id, change_channel, make_application, mocker
26+
):
27+
mocker.patch("zigpy_znp.zigbee.device.NWK_UPDATE_LOOP_DELAY", 0.1)
28+
29+
app, znp_server = make_application(server_cls=device)
30+
31+
if change_channel:
32+
new_channel = 11 + (26 - znp_server.nib.nwkLogicalChannel)
33+
else:
34+
new_channel = znp_server.nib.nwkLogicalChannel
35+
36+
async def update_channel(req):
37+
# Wait a bit before updating
38+
await asyncio.sleep(0.5)
39+
40+
znp_server.nib = znp_server.nib.replace(
41+
nwkUpdateId=znp_server.nib.nwkUpdateId + 1,
42+
nwkLogicalChannel=list(req.Channels)[0],
43+
channelList=req.Channels,
44+
)
45+
46+
yield
47+
48+
znp_server.reply_once_to(
49+
request=c.AF.DataRequestExt.Req(
50+
DstEndpoint=0,
51+
ClusterId=zdo_t.ZDOCmd.Mgmt_NWK_Update_req,
52+
partial=True,
53+
),
54+
responses=[c.AF.DataRequestExt.Rsp(Status=t.Status.SUCCESS)],
55+
)
56+
57+
nwk_update_req = znp_server.reply_once_to(
58+
request=c.ZDO.MgmtNWKUpdateReq.Req(
59+
Dst=0x0000,
60+
DstAddrMode=t.AddrMode.NWK,
61+
Channels=t.Channels.from_channel_list([new_channel]),
62+
ScanDuration=254,
63+
# Missing fields in the request cannot be `None` in the Z-Stack command
64+
ScanCount=0,
65+
NwkManagerAddr=0x0000,
66+
),
67+
responses=[
68+
c.ZDO.MgmtNWKUpdateReq.Rsp(Status=t.Status.SUCCESS),
69+
update_channel,
70+
],
71+
)
72+
73+
await app.startup(auto_form=False)
74+
75+
update = zdo_t.NwkUpdate(
76+
ScanChannels=t.Channels.from_channel_list([new_channel]),
77+
ScanDuration=zdo_t.NwkUpdate.CHANNEL_CHANGE_REQ,
78+
nwkUpdateId=nwk_update_id,
79+
)
80+
81+
if broadcast:
82+
await zigpy.zdo.broadcast(
83+
app,
84+
zdo_t.ZDOCmd.Mgmt_NWK_Update_req,
85+
0x0000, # group id (ignore)
86+
0, # radius
87+
update,
88+
broadcast_address=zigpy_t.BroadcastAddress.ALL_ROUTERS_AND_COORDINATOR,
89+
)
90+
else:
91+
await app.zigpy_device.zdo.Mgmt_NWK_Update_req(update)
92+
93+
if change_channel:
94+
await nwk_update_req
95+
else:
96+
assert not nwk_update_req.done()
97+
98+
assert znp_server.nib.nwkLogicalChannel == list(update.ScanChannels)[0]
99+
100+
await app.shutdown()

zigpy_znp/zigbee/device.py

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
from __future__ import annotations
2+
13
import asyncio
24
import logging
35

46
import zigpy.zdo
57
import zigpy.device
68
import zigpy.zdo.types as zdo_t
9+
import zigpy.application
710

811
import zigpy_znp.types as t
912
import zigpy_znp.commands as c
13+
import zigpy_znp.zigbee.application as znp_app
1014

1115
LOGGER = logging.getLogger(__name__)
1216

@@ -40,31 +44,61 @@ def model(self):
4044

4145
return f"{model}, Z-Stack {version} (build {self.application._zstack_build_id})"
4246

47+
def request(
48+
self,
49+
profile,
50+
cluster,
51+
src_ep,
52+
dst_ep,
53+
sequence,
54+
data,
55+
expect_reply=True,
56+
# Extend the default timeout
57+
timeout=2 * zigpy.device.APS_REPLY_TIMEOUT,
58+
use_ieee=False,
59+
):
60+
"""
61+
Normal `zigpy.device.Device:request` except its default timeout is longer.
62+
"""
63+
64+
return super().request(
65+
profile,
66+
cluster,
67+
src_ep,
68+
dst_ep,
69+
sequence,
70+
data,
71+
expect_reply=expect_reply,
72+
timeout=timeout,
73+
use_ieee=use_ieee,
74+
)
75+
4376

4477
class ZNPZDOEndpoint(zigpy.zdo.ZDO):
4578
@property
46-
def app(self):
79+
def app(self) -> zigpy.application.ControllerApplication:
4780
return self.device.application
4881

49-
async def async_handle_mgmt_permit_joining_req(
50-
self,
51-
hdr: zdo_t.ZDOHeader,
52-
PermitDuration: t.uint8_t,
53-
TC_Significant: t.Bool,
54-
*,
55-
dst_addressing,
82+
def _send_loopback_reply(
83+
self, command_id: zdo_t.ZDOCmd, *, tsn: t.uint8_t, **kwargs
5684
):
57-
# Joins *must* be sent via a ZDO command. Otherwise, Z-Stack will not actually
58-
# permit the coordinator to send the network key while routers will.
59-
await self.app._znp.request_callback_rsp(
60-
request=c.ZDO.MgmtPermitJoinReq.Req(
61-
AddrMode=t.AddrMode.NWK,
62-
Dst=0x0000,
63-
Duration=PermitDuration,
64-
TCSignificance=TC_Significant,
65-
),
66-
RspStatus=t.Status.SUCCESS,
67-
callback=c.ZDO.MgmtPermitJoinRsp.Callback(Src=0x0000, partial=True),
85+
"""
86+
Constructs and sends back a loopback ZDO response.
87+
"""
88+
89+
message = t.uint8_t(tsn).serialize() + self._serialize(
90+
command_id, *kwargs.values()
91+
)
92+
93+
LOGGER.debug("Sending loopback reply %s (%s), tsn=%s", command_id, kwargs, tsn)
94+
95+
self.app.handle_message(
96+
sender=self.app.zigpy_device,
97+
profile=znp_app.ZDO_PROFILE,
98+
cluster=command_id,
99+
src_ep=znp_app.ZDO_ENDPOINT,
100+
dst_ep=znp_app.ZDO_ENDPOINT,
101+
message=message,
68102
)
69103

70104
def handle_mgmt_nwk_update_req(
@@ -83,7 +117,7 @@ def handle_mgmt_nwk_update_req(
83117
async def async_handle_mgmt_nwk_update_req(
84118
self, hdr: zdo_t.ZDOHeader, NwkUpdate: zdo_t.NwkUpdate, *, dst_addressing
85119
):
86-
# Energy scans are handled properly by Z-Stack
120+
# Energy scans are handled properly by Z-Stack, no need to do anything
87121
if NwkUpdate.ScanDuration not in (
88122
zdo_t.NwkUpdate.CHANNEL_CHANGE_REQ,
89123
zdo_t.NwkUpdate.CHANNEL_MASK_MANAGER_ADDR_CHANGE_REQ,
@@ -97,6 +131,15 @@ async def async_handle_mgmt_nwk_update_req(
97131
== NwkUpdate.ScanChannels
98132
):
99133
LOGGER.warning("NWK update request is ignored when channel does not change")
134+
self._send_loopback_reply(
135+
zdo_t.ZDOCmd.Mgmt_NWK_Update_rsp,
136+
Status=zdo_t.Status.SUCCESS,
137+
ScannedChannels=t.Channels.NO_CHANNELS,
138+
TotalTransmissions=0,
139+
TransmissionFailures=0,
140+
EnergyValues=[],
141+
tsn=hdr.tsn,
142+
)
100143
return
101144

102145
await self.app._znp.request(
@@ -128,3 +171,13 @@ async def async_handle_mgmt_nwk_update_req(
128171
f" {self.app.state.network_information.nwk_update_id} instead of being"
129172
f" set to {NwkUpdate.nwkUpdateId}"
130173
)
174+
175+
self._send_loopback_reply(
176+
zdo_t.ZDOCmd.Mgmt_NWK_Update_rsp,
177+
Status=zdo_t.Status.SUCCESS,
178+
ScannedChannels=t.Channels.NO_CHANNELS,
179+
TotalTransmissions=0,
180+
TransmissionFailures=0,
181+
EnergyValues=[],
182+
tsn=hdr.tsn,
183+
)

0 commit comments

Comments
 (0)