Skip to content

Commit 452fc19

Browse files
authored
Refactor device join. (#31)
Use ZDO device_annce message to trigger new device join.
1 parent dd79fda commit 452fc19

File tree

2 files changed

+160
-17
lines changed

2 files changed

+160
-17
lines changed

tests/test_application.py

+126-10
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pytest
55

6-
from zigpy.types import EUI64
6+
from zigpy.types import EUI64, uint16_t
77
from zigpy_xbee.api import ModemStatus, XBee
88
from zigpy_xbee.zigbee.application import ControllerApplication
99

@@ -20,19 +20,22 @@ def test_modem_status(app):
2020
app.handle_modem_status(ModemStatus(0xEE))
2121

2222

23-
def _test_rx(app, device, deserialized):
23+
def _test_rx(app, device, nwk, deserialized,
24+
dst_ep=mock.sentinel.dst_ep,
25+
cluster_id=mock.sentinel.cluster_id,
26+
data=b''):
2427
app.get_device = mock.MagicMock(return_value=device)
2528
app.deserialize = mock.MagicMock(return_value=deserialized)
2629

2730
app.handle_rx(
2831
b'\x01\x02\x03\x04\x05\x06\x07\x08',
29-
mock.sentinel.src_nwk,
32+
nwk,
3033
mock.sentinel.src_ep,
31-
mock.sentinel.dst_ep,
32-
mock.sentinel.cluster_id,
34+
dst_ep,
35+
cluster_id,
3336
mock.sentinel.profile_id,
3437
mock.sentinel.rxopts,
35-
b'',
38+
data,
3639
)
3740

3841
assert app.deserialize.call_count == 1
@@ -41,7 +44,7 @@ def _test_rx(app, device, deserialized):
4144
def test_rx(app):
4245
device = mock.MagicMock()
4346
app.handle_message = mock.MagicMock()
44-
_test_rx(app, device, (1, 2, False, []))
47+
_test_rx(app, device, mock.sentinel.src_nwk, (1, 2, False, []))
4548
assert app.handle_message.call_count == 1
4649
assert app.handle_message.call_args == ((
4750
device,
@@ -73,16 +76,43 @@ def test_rx_nwk_0000(app):
7376
assert app._handle_reply.call_count == 0
7477

7578

79+
def test_rx_unknown_device(app):
80+
app._handle_reply = mock.MagicMock()
81+
app.handle_message = mock.MagicMock()
82+
app.handle_join = mock.MagicMock()
83+
app.get_device = mock.MagicMock(side_effect=KeyError)
84+
app.handle_rx(
85+
b'\x01\x02\x03\x04\x05\x06\x07\x08',
86+
0x1234,
87+
mock.sentinel.src_ep,
88+
mock.sentinel.dst_ep,
89+
mock.sentinel.cluster_id,
90+
mock.sentinel.profile_id,
91+
mock.sentinel.rxopts,
92+
b''
93+
)
94+
assert app.handle_join.call_count == 0
95+
assert app.get_device.call_count == 1
96+
assert app.handle_message.call_count == 0
97+
assert app._handle_reply.call_count == 0
98+
99+
76100
def test_rx_reply(app):
77101
app._handle_reply = mock.MagicMock()
78-
_test_rx(app, mock.MagicMock(), (1, 2, True, []))
102+
_test_rx(app, mock.MagicMock(), mock.sentinel.src_nwk, (1, 2, True, []))
79103
assert app._handle_reply.call_count == 1
80104

81105

82106
def test_rx_failed_deserialize(app, caplog):
107+
from zigpy.device import Status as DeviceStatus
108+
83109
app._handle_reply = mock.MagicMock()
84110
app.handle_message = mock.MagicMock()
85-
app.get_device = mock.MagicMock(return_value=mock.sentinel.device)
111+
nwk = 0x1234
112+
ieee, _ = EUI64.deserialize(b'\x01\x02\x03\x04\x05\x06\x07\x08')
113+
device = app.add_device(ieee, nwk)
114+
device.status = DeviceStatus.ENDPOINTS_INIT
115+
app.get_device = mock.MagicMock(return_value=device)
86116
app.deserialize = mock.MagicMock(side_effect=ValueError)
87117

88118
app.handle_rx(
@@ -102,6 +132,91 @@ def test_rx_failed_deserialize(app, caplog):
102132
assert app.handle_message.call_count == 0
103133

104134

135+
@pytest.fixture
136+
def device(app):
137+
def _device(new=False, zdo_init=False, nwk=uint16_t(0x1234)):
138+
from zigpy.device import Device, Status as DeviceStatus
139+
140+
ieee, _ = EUI64.deserialize(b'\x08\x07\x06\x05\x04\x03\x02\x01')
141+
dev = Device(app, ieee, nwk)
142+
if new:
143+
dev.status = DeviceStatus.NEW
144+
elif zdo_init:
145+
dev.status = DeviceStatus.ZDO_INIT
146+
else:
147+
dev.status = DeviceStatus.ENDPOINTS_INIT
148+
return dev
149+
return _device
150+
151+
152+
def _device_join(app, dev, data):
153+
app.handle_message = mock.MagicMock()
154+
app.handle_join = mock.MagicMock()
155+
156+
deserialized = (1, 2, False, [])
157+
dst_ep = 0
158+
cluster_id = 0x0013
159+
160+
_test_rx(app, dev, dev.nwk, deserialized, dst_ep, cluster_id, data)
161+
assert app.handle_join.call_count == 1
162+
assert app.handle_message.call_count == 1
163+
164+
165+
def test_device_join_new(app, device):
166+
dev = device()
167+
data = b'\xee' + dev.nwk.serialize() + dev.ieee.serialize()
168+
169+
_device_join(app, dev, data)
170+
171+
172+
def test_device_join_inconsistent_nwk(app, device):
173+
dev = device()
174+
data = b'\xee' + b'\x01\x02' + dev.ieee.serialize()
175+
176+
_device_join(app, dev, data)
177+
178+
179+
def test_device_join_inconsistent_ieee(app, device):
180+
dev = device()
181+
data = b'\xee' + dev.nwk.serialize() + b'\x01\x02\x03\x04\x05\x06\x07\x08'
182+
183+
_device_join(app, dev, data)
184+
185+
186+
def _device_status_new(app, dev):
187+
app.handle_join = mock.MagicMock()
188+
app.handle_message = mock.MagicMock()
189+
app.handle_reply = mock.MagicMock()
190+
app.get_device = mock.MagicMock(return_value=dev)
191+
app.deserialize = mock.MagicMock()
192+
193+
app.handle_rx(
194+
b'\x01\x02\x03\x04\x05\x06\x07\x08',
195+
dev.nwk,
196+
mock.sentinel.src_ep,
197+
mock.sentinel.dst_ep,
198+
mock.sentinel.cluster_id,
199+
mock.sentinel.profile_id,
200+
mock.sentinel.rxopts,
201+
b'',
202+
)
203+
204+
assert app.deserialize.call_count == 0
205+
assert app.handle_join.call_count == 0
206+
assert app.handle_message.call_count == 0
207+
assert app.handle_reply.call_count == 0
208+
209+
210+
def test_handle_rx_device_status_new(app, device):
211+
dev = device(new=True)
212+
_device_status_new(app, dev)
213+
214+
215+
def test_handle_rx_device_status_zdo(app, device):
216+
dev = device(zdo_init=True)
217+
_device_status_new(app, dev)
218+
219+
105220
@pytest.mark.asyncio
106221
async def test_broadcast(app):
107222
(profile, cluster, src_ep, dst_ep, grpid, radius, tsn, data) = (
@@ -295,7 +410,8 @@ async def test_permit(app):
295410
async def _test_request(app, do_reply=True, expect_reply=True, **kwargs):
296411
seq = 123
297412
nwk = 0x2345
298-
app._devices_by_nwk[nwk] = 0x22334455
413+
ieee = EUI64(b'\x01\x02\x03\x04\x05\x06\x07\x08')
414+
app.add_device(ieee, nwk)
299415

300416
def _mock_seq_command(cmdname, ieee, nwk, src_ep, dst_ep, cluster,
301417
profile, radius, options, data):

zigpy_xbee/zigbee/application.py

+34-7
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ def __init__(self, api, database_file=None):
2222
api.set_application(self)
2323

2424
self._pending = {}
25-
self._devices_by_nwk = {d.nwk: d.ieee for a, d in self.devices.items()}
26-
2725
self._nwk = 0
2826

2927
async def startup(self, auto_form=False):
@@ -116,9 +114,11 @@ async def request(self, nwk, profile, cluster, src_ep, dst_ep, sequence, data, e
116114
if expect_reply:
117115
reply_fut = asyncio.Future()
118116
self._pending[sequence] = reply_fut
117+
118+
dev = self.get_device(nwk=nwk)
119119
self._api._seq_command(
120120
'tx_explicit',
121-
self._devices_by_nwk[nwk],
121+
dev.ieee,
122122
nwk,
123123
src_ep,
124124
dst_ep,
@@ -153,10 +153,37 @@ def handle_rx(self, src_ieee, src_nwk, src_ep, dst_ep, cluster_id, profile_id, r
153153
return
154154

155155
ember_ieee = zigpy.types.EUI64(src_ieee)
156-
if ember_ieee not in self.devices:
157-
self.handle_join(src_nwk, ember_ieee, 0) # TODO: Parent nwk
158-
self._devices_by_nwk[src_nwk] = src_ieee
159-
device = self.get_device(ember_ieee)
156+
if dst_ep == 0 and cluster_id == 0x13:
157+
# ZDO Device announce request
158+
nwk, data = zigpy.types.uint16_t.deserialize(data[1:])
159+
ieee, data = zigpy.types.EUI64.deserialize(data)
160+
LOGGER.info("New device joined: NWK 0x%04x, IEEE %s", nwk, ieee)
161+
if ember_ieee != ieee:
162+
LOGGER.warning(
163+
"Announced IEEE %s is different from originator %s",
164+
str(ieee), str(ember_ieee))
165+
if src_nwk != nwk:
166+
LOGGER.warning(
167+
"Announced 0x%04x NWK is different from originator 0x%04x",
168+
nwk, src_nwk
169+
)
170+
self.handle_join(nwk, ieee, 0)
171+
172+
try:
173+
device = self.get_device(ieee=ember_ieee)
174+
except KeyError:
175+
LOGGER.debug("Received frame from unknown device: 0x%04x/%s",
176+
src_nwk, str(ember_ieee))
177+
return
178+
179+
if device.status == zigpy.device.Status.NEW and dst_ep != 0:
180+
# only allow ZDO responses while initializing device
181+
LOGGER.debug("Received frame on uninitialized device %s (%s) for endpoint: %s", device.ieee, device.status, dst_ep)
182+
return
183+
elif device.status == zigpy.device.Status.ZDO_INIT and dst_ep != 0 and cluster_id != 0:
184+
# only allow access to basic cluster while initializing endpoints
185+
LOGGER.debug("Received frame on uninitialized device %s endpoint %s for cluster: %s", device.ieee, dst_ep, cluster_id)
186+
return
160187

161188
try:
162189
tsn, command_id, is_reply, args = self.deserialize(device, src_ep, cluster_id, data)

0 commit comments

Comments
 (0)