Skip to content

Commit bad6be2

Browse files
authored
Add method to send multicast requests. (#81)
* Fix DeconzAddressEndpoint group mode serialization. * Send multicast requests method. * Fix test typo. * Update test coverage.
1 parent b7be1a4 commit bad6be2

File tree

5 files changed

+162
-5
lines changed

5 files changed

+162
-5
lines changed

tests/test_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def mock_api_frame(name, *args):
9393

9494

9595
def test_api_frame(api):
96-
addr = t.DeconzAddress()
96+
addr = t.DeconzAddressEndpoint()
9797
addr.address_mode = t.ADDRESS_MODE.NWK
9898
addr.address = t.uint8_t(0)
9999
addr.endpoint = t.uint8_t(0)

tests/test_application.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,3 +469,42 @@ def test_tx_confirm_unexpcted(app, caplog):
469469
app.handle_tx_confirm(123, 0x00)
470470
assert any(r.levelname == 'WARNING' for r in caplog.records)
471471
assert "Unexpected transmit confirm for request id" in caplog.text
472+
473+
474+
async def _test_mrequest(app, send_success=True, aps_data_error=False,
475+
**kwargs):
476+
seq = 123
477+
req_id = mock.sentinel.req_id
478+
app.get_sequence = mock.MagicMock(return_value=req_id)
479+
480+
async def req_mock(req_id, dst_addr_ep, profile, cluster, src_ep, data):
481+
if aps_data_error:
482+
raise zigpy_deconz.exception.CommandError(1, "Command Error")
483+
if send_success:
484+
app._pending[req_id].result.set_result(0)
485+
else:
486+
app._pending[req_id].result.set_result(1)
487+
488+
app._api.aps_data_request = mock.MagicMock(side_effect=req_mock)
489+
device = zigpy.device.Device(app, mock.sentinel.ieee, 0x1122)
490+
app.get_device = mock.MagicMock(return_value=device)
491+
492+
return await app.mrequest(0x55aa, 0x0260, 1, 2, seq, b'\x01\x02\x03', **kwargs)
493+
494+
495+
@pytest.mark.asyncio
496+
async def test_mrequest_send_success(app):
497+
r = await _test_mrequest(app, True)
498+
assert r[0] == 0
499+
500+
501+
@pytest.mark.asyncio
502+
async def test_mrequest_send_fail(app):
503+
r = await _test_mrequest(app, False)
504+
assert r[0] != 0
505+
506+
507+
@pytest.mark.asyncio
508+
async def test_mrequest_send_aps_data_error(app):
509+
r = await _test_mrequest(app, False, aps_data_error=True)
510+
assert r[0] != 0

tests/test_types.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,48 @@ def test_addr_ep_ieee():
230230
assert r.address_mode == t.ADDRESS_MODE.IEEE
231231
assert repr(r.address) == "31:32:33:34:35:36:37:38"
232232
assert r.endpoint == 0xcc
233+
234+
235+
def test_deconz_addr_ep():
236+
data = b'\x01\xaa\x55'
237+
extra = b'the rest of the owl'
238+
239+
r, rest = t.DeconzAddressEndpoint.deserialize(data + extra)
240+
assert rest == extra
241+
assert r.address_mode == t.ADDRESS_MODE.GROUP
242+
assert r.address == 0x55aa
243+
assert r.serialize() == data
244+
a = t.DeconzAddressEndpoint()
245+
a.address_mode = 1
246+
a.address = 0x55aa
247+
assert a.serialize() == data
248+
249+
data = b'\x02\xaa\x55\xcc'
250+
r, rest = t.DeconzAddressEndpoint.deserialize(data + extra)
251+
assert rest == extra
252+
assert r.address_mode == t.ADDRESS_MODE.NWK
253+
assert r.address == 0x55aa
254+
assert r.endpoint == 0xcc
255+
assert r.serialize() == data
256+
a = t.DeconzAddressEndpoint()
257+
a.address_mode = 2
258+
a.address = 0x55aa
259+
with pytest.raises(AttributeError):
260+
a.serialize()
261+
a.endpoint = 0xcc
262+
assert a.serialize() == data
263+
264+
data = b'\x03\x31\x32\x33\x34\x35\x36\x37\x38\xcc'
265+
r, rest = t.DeconzAddressEndpoint.deserialize(data + extra)
266+
assert rest == extra
267+
assert r.address_mode == t.ADDRESS_MODE.IEEE
268+
assert r.address == [0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38]
269+
assert r.endpoint == 0xcc
270+
assert r.serialize() == data
271+
a = t.DeconzAddressEndpoint()
272+
a.address_mode = 3
273+
a.address = [0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38]
274+
with pytest.raises(AttributeError):
275+
a.serialize()
276+
a.endpoint = 0xcc
277+
assert a.serialize() == data

zigpy_deconz/types.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ def __str__(self):
209209
return ('0x{:0' + str(self._size * 2) + 'x}').format(self)
210210

211211

212+
class GroupId(HexRepr, uint16_t):
213+
pass
214+
215+
212216
class NWK(HexRepr, uint16_t):
213217
pass
214218

@@ -225,7 +229,7 @@ class DeconzAddress(Struct):
225229
_fields = [
226230
# The address format (AddressMode)
227231
('address_mode', ADDRESS_MODE),
228-
('address', uint64_t),
232+
('address', EUI64),
229233
]
230234

231235
@classmethod
@@ -254,7 +258,7 @@ class DeconzAddressEndpoint(Struct):
254258
_fields = [
255259
# The address format (AddressMode)
256260
('address_mode', ADDRESS_MODE),
257-
('address', uint64_t),
261+
('address', EUI64),
258262
('endpoint', uint8_t)
259263
]
260264

@@ -264,7 +268,9 @@ def deserialize(cls, data):
264268
mode, data = ADDRESS_MODE.deserialize(data)
265269
r.address_mode = mode
266270
a = e = None
267-
if mode in [ADDRESS_MODE.GROUP, ADDRESS_MODE.NWK]:
271+
if mode == ADDRESS_MODE.GROUP:
272+
a, data = GroupId.deserialize(data)
273+
elif mode == ADDRESS_MODE.NWK:
268274
a, data = NWK.deserialize(data)
269275
elif mode == ADDRESS_MODE.IEEE:
270276
a, data = EUI64.deserialize(data)
@@ -274,6 +280,18 @@ def deserialize(cls, data):
274280
setattr(r, cls._fields[2][0], e)
275281
return r, data
276282

283+
def serialize(self):
284+
r = uint8_t(self.address_mode).serialize()
285+
if self.address_mode == ADDRESS_MODE.NWK:
286+
r += NWK(self.address).serialize()
287+
elif self.address_mode == ADDRESS_MODE.GROUP:
288+
r += GroupId(self.address).serialize()
289+
elif self.address_mode == ADDRESS_MODE.IEEE:
290+
r += EUI64(self.address).serialize()
291+
if self.address_mode in (ADDRESS_MODE.NWK, ADDRESS_MODE.IEEE, ):
292+
r += uint8_t(self.endpoint).serialize()
293+
return r
294+
277295

278296
class Key(FixedList):
279297
_itemtype = uint8_t

zigpy_deconz/zigbee/application.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import zigpy.util
1111
import zigpy_deconz.exception
1212
from zigpy_deconz import types as t
13-
from zigpy_deconz.api import NetworkParameter, NetworkState
13+
from zigpy_deconz.api import NetworkParameter, NetworkState, Status
1414

1515
LOGGER = logging.getLogger(__name__)
1616

@@ -83,6 +83,61 @@ async def form_network(self, channel=15, pan_id=None, extended_pan_id=None):
8383
await asyncio.sleep(CHANGE_NETWORK_WAIT)
8484
raise Exception("Could not form network.")
8585

86+
async def mrequest(
87+
self,
88+
group_id,
89+
profile,
90+
cluster,
91+
src_ep,
92+
sequence,
93+
data,
94+
*,
95+
hops=0,
96+
non_member_radius=3
97+
):
98+
"""Submit and send data out as a multicast transmission.
99+
100+
:param group_id: destination multicast address
101+
:param profile: Zigbee Profile ID to use for outgoing message
102+
:param cluster: cluster id where the message is being sent
103+
:param src_ep: source endpoint id
104+
:param sequence: transaction sequence number of the message
105+
:param data: Zigbee message payload
106+
:param hops: the message will be delivered to all nodes within this number of
107+
hops of the sender. A value of zero is converted to MAX_HOPS
108+
:param non_member_radius: the number of hops that the message will be forwarded
109+
by devices that are not members of the group. A value
110+
of 7 or greater is treated as infinite
111+
:returns: return a tuple of a status and an error_message. Original requestor
112+
has more context to provide a more meaningful error message
113+
"""
114+
req_id = self.get_sequence()
115+
LOGGER.debug("Sending Zigbee multicast with tsn %s under %s request id, data: %s",
116+
sequence, req_id, binascii.hexlify(data))
117+
dst_addr_ep = t.DeconzAddressEndpoint()
118+
dst_addr_ep.address_mode = t.ADDRESS_MODE.GROUP
119+
dst_addr_ep.address = group_id
120+
121+
with self._pending.new(req_id) as req:
122+
try:
123+
await self._api.aps_data_request(
124+
req_id,
125+
dst_addr_ep,
126+
profile,
127+
cluster,
128+
min(1, src_ep),
129+
data
130+
)
131+
except zigpy_deconz.exception.CommandError as ex:
132+
return ex.status, "Couldn't enqueue send data request: {}".format(ex)
133+
134+
r = await asyncio.wait_for(req.result, SEND_CONFIRM_TIMEOUT)
135+
if r:
136+
LOGGER.warning("Error while sending %s req id frame: 0x%02x", req_id, r)
137+
return r, "message send failure"
138+
139+
return Status.SUCCESS, "message send success"
140+
86141
@zigpy.util.retryable_request
87142
async def request(self, device, profile, cluster, src_ep, dst_ep, sequence, data,
88143
expect_reply=True, use_ieee=False):

0 commit comments

Comments
 (0)