Skip to content

Commit 646b07a

Browse files
authored
Implement the zigpy channel changing API (#212)
* Implement the zigpy channel changing API * Bump minimum zigpy version to 0.55.0
1 parent 57f303c commit 646b07a

File tree

4 files changed

+27
-149
lines changed

4 files changed

+27
-149
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ readme = "README.md"
1414
license = {text = "GPL-3.0"}
1515
requires-python = ">=3.8"
1616
dependencies = [
17-
"zigpy>=0.52.0",
17+
"zigpy>=0.55.0",
1818
"async_timeout",
1919
"voluptuous",
2020
"coloredlogs",

tests/application/test_zdo_requests.py

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import asyncio
22

33
import pytest
4-
import zigpy.zdo
5-
import zigpy.types as zigpy_t
64
import zigpy.zdo.types as zdo_t
75

86
import zigpy_znp.types as t
@@ -12,19 +10,19 @@
1210

1311

1412
@pytest.mark.parametrize(
15-
"broadcast,nwk_update_id,change_channel",
13+
"nwk_update_id,change_channel",
1614
[
17-
(False, 1, False),
18-
(False, 1, True),
19-
(True, 1, False),
20-
(False, 200, True),
15+
(1, False),
16+
(1, True),
17+
(1, False),
18+
(200, True),
2119
],
2220
)
2321
@pytest.mark.parametrize("device", [FormedLaunchpadCC26X2R1])
2422
async def test_mgmt_nwk_update_req(
25-
device, broadcast, nwk_update_id, change_channel, make_application, mocker
23+
device, nwk_update_id, change_channel, make_application, mocker
2624
):
27-
mocker.patch("zigpy_znp.zigbee.device.NWK_UPDATE_LOOP_DELAY", 0.1)
25+
mocker.patch("zigpy.application.CHANNEL_CHANGE_SETTINGS_RELOAD_DELAY_S", 0.1)
2826

2927
app, znp_server = make_application(server_cls=device)
3028

@@ -72,29 +70,13 @@ async def update_channel(req):
7270

7371
await app.startup(auto_form=False)
7472

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._device.zdo.Mgmt_NWK_Update_req(update)
73+
await app.move_network_to_channel(new_channel=new_channel)
9274

9375
if change_channel:
9476
await nwk_update_req
9577
else:
9678
assert not nwk_update_req.done()
9779

98-
assert znp_server.nib.nwkLogicalChannel == list(update.ScanChannels)[0]
80+
assert znp_server.nib.nwkLogicalChannel == new_channel
9981

10082
await app.shutdown()

zigpy_znp/zigbee/application.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,23 @@ async def permit_with_key(self, node: t.EUI64, code: bytes, time_s=60):
376376
RspStatus=t.Status.SUCCESS,
377377
)
378378

379+
async def _move_network_to_channel(
380+
self, new_channel: int, new_nwk_update_id: int
381+
) -> None:
382+
"""Moves device to a new channel."""
383+
await self._znp.request(
384+
request=c.ZDO.MgmtNWKUpdateReq.Req(
385+
Dst=0x0000,
386+
DstAddrMode=t.AddrMode.NWK,
387+
Channels=t.Channels.from_channel_list([new_channel]),
388+
ScanDuration=zdo_t.NwkUpdate.CHANNEL_CHANGE_REQ,
389+
ScanCount=0,
390+
NwkManagerAddr=0x0000,
391+
# `new_nwk_update_id` is ignored
392+
),
393+
RspStatus=t.Status.SUCCESS,
394+
)
395+
379396
def connection_lost(self, exc):
380397
"""
381398
Propagated up from UART through ZNP when the connection is lost.

zigpy_znp/zigbee/device.py

Lines changed: 0 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
from __future__ import annotations
22

3-
import asyncio
43
import logging
54

65
import zigpy.zdo
76
import zigpy.device
8-
import zigpy.zdo.types as zdo_t
97
import zigpy.application
108

11-
import zigpy_znp.types as t
12-
import zigpy_znp.commands as c
13-
import zigpy_znp.zigbee.application as znp_app
14-
159
LOGGER = logging.getLogger(__name__)
1610

1711
NWK_UPDATE_LOOP_DELAY = 1
@@ -22,13 +16,6 @@ class ZNPCoordinator(zigpy.device.Device):
2216
Coordinator zigpy device that keeps track of our endpoints and clusters.
2317
"""
2418

25-
def __init__(self, *args, **kwargs):
26-
super().__init__(*args, **kwargs)
27-
28-
assert hasattr(self, "zdo")
29-
self.zdo = ZNPZDOEndpoint(self)
30-
self.endpoints[0] = self.zdo
31-
3219
@property
3320
def manufacturer(self):
3421
return "Texas Instruments"
@@ -72,111 +59,3 @@ def request(
7259
timeout=timeout,
7360
use_ieee=use_ieee,
7461
)
75-
76-
77-
class ZNPZDOEndpoint(zigpy.zdo.ZDO):
78-
@property
79-
def app(self) -> zigpy.application.ControllerApplication:
80-
return self.device.application
81-
82-
def _send_loopback_reply(
83-
self, command_id: zdo_t.ZDOCmd, *, tsn: t.uint8_t, **kwargs
84-
):
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._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,
102-
)
103-
104-
def handle_mgmt_nwk_update_req(
105-
self, hdr: zdo_t.ZDOHeader, NwkUpdate: zdo_t.NwkUpdate, *, dst_addressing
106-
):
107-
"""
108-
Handles ZDO `Mgmt_NWK_Update_req` sent to the coordinator.
109-
"""
110-
111-
self.create_catching_task(
112-
self.async_handle_mgmt_nwk_update_req(
113-
hdr, NwkUpdate, dst_addressing=dst_addressing
114-
)
115-
)
116-
117-
async def async_handle_mgmt_nwk_update_req(
118-
self, hdr: zdo_t.ZDOHeader, NwkUpdate: zdo_t.NwkUpdate, *, dst_addressing
119-
):
120-
# Energy scans are handled properly by Z-Stack, no need to do anything
121-
if NwkUpdate.ScanDuration not in (
122-
zdo_t.NwkUpdate.CHANNEL_CHANGE_REQ,
123-
zdo_t.NwkUpdate.CHANNEL_MASK_MANAGER_ADDR_CHANGE_REQ,
124-
):
125-
return
126-
127-
old_network_info = self.app.state.network_info
128-
129-
if (
130-
t.Channels.from_channel_list([old_network_info.channel])
131-
== NwkUpdate.ScanChannels
132-
):
133-
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-
)
143-
return
144-
145-
await self.app._znp.request(
146-
request=c.ZDO.MgmtNWKUpdateReq.Req(
147-
Dst=0x0000,
148-
DstAddrMode=t.AddrMode.NWK,
149-
Channels=NwkUpdate.ScanChannels,
150-
ScanDuration=NwkUpdate.ScanDuration,
151-
# Missing fields in the request cannot be `None` in the Z-Stack command
152-
ScanCount=NwkUpdate.ScanCount or 0,
153-
NwkManagerAddr=NwkUpdate.nwkManagerAddr or 0x0000,
154-
),
155-
RspStatus=t.Status.SUCCESS,
156-
)
157-
158-
# Wait until the network info changes, it can take ~5s
159-
while (
160-
self.app.state.network_info.nwk_update_id == old_network_info.nwk_update_id
161-
):
162-
await self.app.load_network_info(load_devices=False)
163-
await asyncio.sleep(NWK_UPDATE_LOOP_DELAY)
164-
165-
# Z-Stack automatically increments the NWK update ID instead of setting it
166-
# TODO: Directly set it once radio settings API is finalized.
167-
if NwkUpdate.nwkUpdateId != self.app.state.network_info.nwk_update_id:
168-
LOGGER.warning(
169-
f"`nwkUpdateId` was incremented to"
170-
f" {self.app.state.network_info.nwk_update_id} instead of being"
171-
f" set to {NwkUpdate.nwkUpdateId}"
172-
)
173-
174-
self._send_loopback_reply(
175-
zdo_t.ZDOCmd.Mgmt_NWK_Update_rsp,
176-
Status=zdo_t.Status.SUCCESS,
177-
ScannedChannels=t.Channels.NO_CHANNELS,
178-
TotalTransmissions=0,
179-
TransmissionFailures=0,
180-
EnergyValues=[],
181-
tsn=hdr.tsn,
182-
)

0 commit comments

Comments
 (0)