Skip to content

Commit 5e14aef

Browse files
committed
Create ZDO handlers for commands that require using the internal API
1 parent cbecee4 commit 5e14aef

File tree

2 files changed

+177
-21
lines changed

2 files changed

+177
-21
lines changed

zigpy_znp/zigbee/application.py

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from zigpy_znp.utils import combine_concurrent_calls
3434
from zigpy_znp.exceptions import CommandNotRecognized, InvalidCommandResponse
3535
from zigpy_znp.types.nvids import OsalNvIds
36+
from zigpy_znp.zigbee.device import ZNPCoordinator
3637

3738
ZDO_ENDPOINT = 0
3839
ZHA_ENDPOINT = 1
@@ -74,27 +75,6 @@
7475
LOGGER = logging.getLogger(__name__)
7576

7677

77-
class ZNPCoordinator(zigpy.device.Device):
78-
"""
79-
Coordinator zigpy device that keeps track of our endpoints and clusters.
80-
"""
81-
82-
@property
83-
def manufacturer(self):
84-
return "Texas Instruments"
85-
86-
@property
87-
def model(self):
88-
if self.application._znp.version > 3.0:
89-
model = "CC1352/CC2652"
90-
version = "3.30+"
91-
else:
92-
model = "CC2538" if self.application._znp.nvram.align_structs else "CC2531"
93-
version = "Home 1.2" if self.application._znp.version == 1.2 else "3.0.x"
94-
95-
return f"{model}, Z-Stack {version} (build {self.application._zstack_build_id})"
96-
97-
9878
class ControllerApplication(zigpy.application.ControllerApplication):
9979
SCHEMA = conf.CONFIG_SCHEMA
10080
SCHEMA_DEVICE = conf.SCHEMA_DEVICE
@@ -289,6 +269,10 @@ async def _startup(self, auto_form=False, force_form=False, read_only=False):
289269

290270
# Receive a callback for every known ZDO command
291271
for cluster_id in zdo_t.ZDOCmd:
272+
# Ignore ZDO requests
273+
if cluster_id < 0x8000:
274+
continue
275+
292276
await self._znp.request(c.ZDO.MsgCallbackRegister.Req(ClusterId=cluster_id))
293277

294278
# Setup the coordinator as a zigpy device and initialize it to request node info
@@ -1262,6 +1246,33 @@ async def _send_request_raw(
12621246
RspStatus=t.Status.SUCCESS,
12631247
callback=c.ZDO.MgmtPermitJoinRsp.Callback(Src=0x0000, partial=True),
12641248
)
1249+
# Internally forward ZDO requests destined for the coordinator back to zigpy so
1250+
# we can send Z-Stack internal requests when necessary
1251+
elif dst_ep == ZDO_ENDPOINT and (
1252+
# Broadcast that will reach the device
1253+
(
1254+
dst_addr.mode == t.AddrMode.Broadcast
1255+
and dst_addr.address
1256+
in (
1257+
zigpy.types.BroadcastAddress.ALL_DEVICES,
1258+
zigpy.types.BroadcastAddress.RX_ON_WHEN_IDLE,
1259+
zigpy.types.BroadcastAddress.ALL_ROUTERS_AND_COORDINATOR,
1260+
)
1261+
)
1262+
# Or a direct unicast request
1263+
or (
1264+
dst_addr.mode == t.AddrMode.NWK
1265+
and dst_addr.address == self.zigpy_device.nwk
1266+
)
1267+
):
1268+
self.handle_message(
1269+
sender=self.zigpy_device,
1270+
profile=ZDO_PROFILE,
1271+
cluster=cluster,
1272+
src_ep=ZDO_ENDPOINT,
1273+
dst_ep=ZDO_ENDPOINT,
1274+
message=data,
1275+
)
12651276

12661277
if dst_addr.mode == t.AddrMode.Broadcast or dst_ep == ZDO_ENDPOINT:
12671278
# Broadcasts and ZDO requests will not receive a confirmation

zigpy_znp/zigbee/device.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import asyncio
2+
import logging
3+
4+
import zigpy.zdo
5+
import zigpy.device
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+
LOGGER = logging.getLogger(__name__)
12+
13+
14+
class ZNPCoordinator(zigpy.device.Device):
15+
"""
16+
Coordinator zigpy device that keeps track of our endpoints and clusters.
17+
"""
18+
19+
def __init__(self, *args, **kwargs):
20+
super().__init__(*args, **kwargs)
21+
22+
assert hasattr(self, "zdo")
23+
self.zdo = ZNPZDOEndpoint(self)
24+
self.endpoints[0] = self.zdo
25+
26+
@property
27+
def manufacturer(self):
28+
return "Texas Instruments"
29+
30+
@property
31+
def model(self):
32+
if self.application._znp.version > 3.0:
33+
model = "CC1352/CC2652"
34+
version = "3.30+"
35+
else:
36+
model = "CC2538" if self.application._znp.nvram.align_structs else "CC2531"
37+
version = "Home 1.2" if self.application._znp.version == 1.2 else "3.0.x"
38+
39+
return f"{model}, Z-Stack {version} (build {self.application._zstack_build_id})"
40+
41+
42+
class ZNPZDOEndpoint(zigpy.zdo.ZDO):
43+
@property
44+
def app(self):
45+
return self.device.application
46+
47+
def handle_mgmt_permit_joining_req(
48+
self,
49+
hdr: zdo_t.ZDOHeader,
50+
PermitDuration: t.uint8_t,
51+
TC_Significant: t.Bool,
52+
*,
53+
dst_addressing,
54+
):
55+
"""
56+
Handles ZDO `Mgmt_Permit_Joining_req` sent to the coordinator.
57+
"""
58+
59+
self.create_catching_task(
60+
self.async_handle_mgmt_permit_joining_req(
61+
hdr, PermitDuration, TC_Significant, dst_addressing=dst_addressing
62+
)
63+
)
64+
65+
async def async_handle_mgmt_permit_joining_req(
66+
self,
67+
hdr: zdo_t.ZDOHeader,
68+
PermitDuration: t.uint8_t,
69+
TC_Significant: t.Bool,
70+
*,
71+
dst_addressing,
72+
):
73+
# Joins *must* be sent via a ZDO command. Otherwise, Z-Stack will not actually
74+
# permit the coordinator to send the network key while routers will.
75+
await self.app._znp.request_callback_rsp(
76+
request=c.ZDO.MgmtPermitJoinReq.Req(
77+
AddrMode=t.AddrMode.NWK,
78+
Dst=0x0000,
79+
Duration=PermitDuration,
80+
TCSignificance=TC_Significant,
81+
),
82+
RspStatus=t.Status.SUCCESS,
83+
callback=c.ZDO.MgmtPermitJoinRsp.Callback(Src=0x0000, partial=True),
84+
)
85+
86+
def handle_mgmt_nwk_update_req(
87+
self, hdr: zdo_t.ZDOHeader, NwkUpdate: zdo_t.NwkUpdate, *, dst_addressing
88+
):
89+
"""
90+
Handles ZDO `Mgmt_NWK_Update_req` sent to the coordinator.
91+
"""
92+
93+
self.create_catching_task(
94+
self.async_handle_mgmt_nwk_update_req(
95+
hdr, NwkUpdate, dst_addressing=dst_addressing
96+
)
97+
)
98+
99+
async def async_handle_mgmt_nwk_update_req(
100+
self, hdr: zdo_t.ZDOHeader, NwkUpdate: zdo_t.NwkUpdate, *, dst_addressing
101+
):
102+
# Energy scans are handled properly by Z-Stack
103+
if NwkUpdate.ScanDuration not in (
104+
zdo_t.NwkUpdate.CHANNEL_CHANGE_REQ,
105+
zdo_t.NwkUpdate.CHANNEL_MASK_MANAGER_ADDR_CHANGE_REQ,
106+
):
107+
return
108+
109+
old_network_info = self.app.state.network_information
110+
111+
if (
112+
t.Channels.from_channel_list([old_network_info.channel])
113+
== NwkUpdate.ScanChannels
114+
):
115+
LOGGER.info("NWK update request is ignored when channel does not change")
116+
return
117+
118+
await self.app._znp.request(
119+
request=c.ZDO.MgmtNWKUpdateReq.Req(
120+
Dst=0x0000,
121+
DstAddrMode=t.AddrMode.NWK,
122+
Channels=NwkUpdate.ScanChannels,
123+
ScanDuration=NwkUpdate.ScanDuration,
124+
ScanCount=NwkUpdate.ScanCount or 0,
125+
NwkManagerAddr=NwkUpdate.nwkManagerAddr or 0x0000,
126+
),
127+
RspStatus=t.Status.SUCCESS,
128+
)
129+
130+
# Wait until the network info changes, it can take ~5s
131+
while (
132+
self.app.state.network_information.nwk_update_id
133+
== old_network_info.nwk_update_id
134+
):
135+
await self.app.load_network_info(load_devices=False)
136+
await asyncio.sleep(1)
137+
138+
# Z-Stack automatically increments the NWK update ID instead of setting it
139+
# TODO: Directly set it once radio settings API is finalized.
140+
if NwkUpdate.nwkUpdateId != self.app.state.network_information.nwk_update_id:
141+
LOGGER.warning(
142+
f"`nwkUpdateId` was incremented to"
143+
f" {self.app.state.network_information.nwk_update_id} instead of being"
144+
f" set to {NwkUpdate.nwkUpdateId}"
145+
)

0 commit comments

Comments
 (0)