Skip to content

Commit 9560899

Browse files
authored
Handle tx_status notification frames. (#40)
* Add TXStatus and DiscoveryStatus enums. * Use non keyword arguments for _handle_* methods. * Properly handle tx_status notifications. * Update test units.
1 parent cf63db3 commit 9560899

File tree

5 files changed

+253
-55
lines changed

5 files changed

+253
-55
lines changed

tests/test_api.py

Lines changed: 78 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -176,15 +176,27 @@ def test_api_frame(api):
176176
for cmd_name, cmd_opts in xbee_api.COMMAND_REQUESTS.items():
177177
cmd_id, schema, repl = cmd_opts
178178
if schema:
179-
args = [ieee if isinstance(a(), t.EUI64) else a() for a in schema]
179+
args = [ieee if issubclass(a, t.EUI64) else a() for a in schema]
180180
frame, repl = api._api_frame(cmd_name, *args)
181181
else:
182182
frame, repl = api._api_frame(cmd_name)
183183

184184

185185
def test_frame_received(api, monkeypatch):
186186
monkeypatch.setattr(t, 'deserialize', mock.MagicMock(
187-
return_value=(mock.sentinel.deserialize_data, b'')))
187+
return_value=(
188+
[
189+
mock.sentinel.arg_0,
190+
mock.sentinel.arg_1,
191+
mock.sentinel.arg_2,
192+
mock.sentinel.arg_3,
193+
mock.sentinel.arg_4,
194+
mock.sentinel.arg_5,
195+
mock.sentinel.arg_6,
196+
mock.sentinel.arg_7,
197+
mock.sentinel.arg_8,
198+
], b'')
199+
))
188200
my_handler = mock.MagicMock()
189201

190202
for cmd, cmd_opts in xbee_api.COMMAND_RESPONSES.items():
@@ -196,7 +208,10 @@ def test_frame_received(api, monkeypatch):
196208
assert t.deserialize.call_count == 1
197209
assert t.deserialize.call_args[0][0] == payload
198210
assert my_handler.call_count == 1
199-
assert my_handler.call_args[0][0] == mock.sentinel.deserialize_data
211+
assert my_handler.call_args[0][0] == mock.sentinel.arg_0
212+
assert my_handler.call_args[0][1] == mock.sentinel.arg_1
213+
assert my_handler.call_args[0][2] == mock.sentinel.arg_2
214+
assert my_handler.call_args[0][3] == mock.sentinel.arg_3
200215
t.deserialize.reset_mock()
201216
my_handler.reset_mock()
202217

@@ -224,7 +239,7 @@ def _handle_at_response(api, tsn, status, at_response=b''):
224239
data = (tsn, 'AI'.encode('ascii'), status, at_response)
225240
response = asyncio.Future()
226241
api._awaiting[tsn] = (response, )
227-
api._handle_at_response(data)
242+
api._handle_at_response(*data)
228243
return response
229244

230245

@@ -264,18 +279,18 @@ def test_handle_at_response_undef_error(api):
264279
def test_handle_remote_at_rsp(api):
265280
api._handle_at_response = mock.MagicMock()
266281
s = mock.sentinel
267-
api._handle_remote_at_response([s.frame_id, s.ieee, s.nwk, s.cmd,
268-
s.status, s.data])
282+
api._handle_remote_at_response(s.frame_id, s.ieee, s.nwk, s.cmd,
283+
s.status, s.data)
269284
assert api._handle_at_response.call_count == 1
270-
assert api._handle_at_response.call_args[0][0][0] == s.frame_id
271-
assert api._handle_at_response.call_args[0][0][1] == s.cmd
272-
assert api._handle_at_response.call_args[0][0][2] == s.status
273-
assert api._handle_at_response.call_args[0][0][3] == s.data
285+
assert api._handle_at_response.call_args[0][0] == s.frame_id
286+
assert api._handle_at_response.call_args[0][1] == s.cmd
287+
assert api._handle_at_response.call_args[0][2] == s.status
288+
assert api._handle_at_response.call_args[0][3] == s.data
274289

275290

276291
def _send_modem_event(api, event):
277292
api._app = mock.MagicMock(spec=ControllerApplication)
278-
api._handle_modem_status([event])
293+
api._handle_modem_status(event)
279294
assert api._app.handle_modem_status.call_count == 1
280295
assert api._app.handle_modem_status.call_args[0][0] == event
281296

@@ -301,15 +316,60 @@ def test_handle_modem_status(api):
301316

302317

303318
def test_handle_explicit_rx_indicator(api):
304-
data = b'\x00\x01\x02\x03\x04\x05\x06\x07'
319+
s = mock.sentinel
320+
data = [s.src_ieee, s.src_nwk, s.src_ep, s.dst_ep, s.cluster_id, s.profile,
321+
s.opts, b'abcdef']
305322
api._app = mock.MagicMock()
306323
api._app.handle_rx = mock.MagicMock()
307-
api._handle_explicit_rx_indicator(data)
324+
api._handle_explicit_rx_indicator(*data)
308325
assert api._app.handle_rx.call_count == 1
309326

310327

311-
def test_handle_tx_status(api):
312-
api._handle_tx_status(b'\x01\x02\x03\x04')
328+
def _handle_tx_status(api, status, wrong_frame_id=False):
329+
status = t.TXStatus(status)
330+
frame_id = 0x12
331+
send_fut = mock.MagicMock(spec=asyncio.Future)
332+
api._awaiting[frame_id] = (send_fut, )
333+
s = mock.sentinel
334+
if wrong_frame_id:
335+
frame_id += 1
336+
api._handle_tx_status(frame_id, s.dst_nwk, s.retries, status,
337+
t.DiscoveryStatus())
338+
return send_fut
339+
340+
341+
def test_handle_tx_status_success(api):
342+
fut = _handle_tx_status(api, t.TXStatus.SUCCESS)
343+
assert len(api._awaiting) == 0
344+
assert fut.set_result.call_count == 1
345+
assert fut.set_exception.call_count == 0
346+
347+
348+
def test_handle_tx_status_except(api):
349+
fut = _handle_tx_status(api, t.TXStatus.ADDRESS_NOT_FOUND)
350+
assert len(api._awaiting) == 0
351+
assert fut.set_result.call_count == 0
352+
assert fut.set_exception.call_count == 1
353+
354+
355+
def test_handle_tx_status_unexpected(api):
356+
fut = _handle_tx_status(api, 1, wrong_frame_id=True)
357+
assert len(api._awaiting) == 1
358+
assert fut.set_result.call_count == 0
359+
assert fut.set_exception.call_count == 0
360+
361+
362+
def test_handle_tx_status_duplicate(api):
363+
status = t.TXStatus.SUCCESS
364+
frame_id = 0x12
365+
send_fut = mock.MagicMock(spec=asyncio.Future)
366+
send_fut.set_result.side_effect = asyncio.InvalidStateError
367+
api._awaiting[frame_id] = (send_fut, )
368+
s = mock.sentinel
369+
api._handle_tx_status(frame_id, s.dst_nwk, s.retries, status, s.disc)
370+
assert len(api._awaiting) == 0
371+
assert send_fut.set_result.call_count == 1
372+
assert send_fut.set_exception.call_count == 0
313373

314374

315375
@pytest.mark.asyncio
@@ -422,10 +482,11 @@ def test_set_application(api):
422482

423483

424484
def test_handle_route_record_indicator(api):
425-
api._handle_route_record_indicator(mock.sentinel.ri)
485+
s = mock.sentinel
486+
api._handle_route_record_indicator(s.ieee, s.src, s.rx_opts, s.hops)
426487

427488

428489
def test_handle_many_to_one_rri(api):
429490
ieee = t.EUI64([t.uint8_t(a) for a in range(0, 8)])
430491
nwk = 0x1234
431-
api._handle_many_to_one_rri([ieee, nwk, 0])
492+
api._handle_many_to_one_rri(ieee, nwk, 0)

tests/test_application.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33

44
import pytest
55

6+
from zigpy.exceptions import DeliveryError
67
from zigpy.types import EUI64, uint16_t
78
from zigpy_xbee.api import ModemStatus, XBee
8-
from zigpy_xbee.zigbee.application import ControllerApplication
9+
from zigpy_xbee.zigbee import application
910

1011

1112
@pytest.fixture
12-
def app(database_file=None):
13-
return ControllerApplication(XBee(), database_file=database_file)
13+
def app(monkeypatch, database_file=None):
14+
monkeypatch.setattr(application, 'TIMEOUT_TX_STATUS', 0.1)
15+
return application.ControllerApplication(XBee(),
16+
database_file=database_file)
1417

1518

1619
def test_modem_status(app):
@@ -293,7 +296,7 @@ async def mock_at_command(cmd, *args):
293296
app._api._queued_at = mock.MagicMock(spec=XBee._at_command,
294297
side_effect=mock_at_command)
295298
app._get_association_state = mock.MagicMock(
296-
spec=ControllerApplication._get_association_state,
299+
spec=application.ControllerApplication._get_association_state,
297300
side_effect=asyncio.coroutine(mock.MagicMock(return_value=0x00))
298301
)
299302

@@ -434,17 +437,26 @@ async def test_permit(app):
434437
assert app._api._at_command.call_args_list[0][0][1] == time_s
435438

436439

437-
async def _test_request(app, do_reply=True, expect_reply=True, **kwargs):
440+
async def _test_request(app, do_reply=True, expect_reply=True,
441+
send_success=True, send_timeout=False, **kwargs):
438442
seq = 123
439443
nwk = 0x2345
440444
ieee = EUI64(b'\x01\x02\x03\x04\x05\x06\x07\x08')
441445
app.add_device(ieee, nwk)
442446

443447
def _mock_command(cmdname, ieee, nwk, src_ep, dst_ep, cluster,
444448
profile, radius, options, data):
449+
send_fut = asyncio.Future()
450+
if not send_timeout:
451+
if send_success:
452+
send_fut.set_result(True)
453+
else:
454+
send_fut.set_exception(DeliveryError())
455+
445456
if expect_reply:
446457
if do_reply:
447458
app._pending[seq].set_result(mock.sentinel.reply_result)
459+
return send_fut
448460

449461
app._api._command = mock.MagicMock(side_effect=_mock_command)
450462
return await app.request(nwk, 0x0260, 1, 2, 3, seq, [4, 5, 6], expect_reply=expect_reply, **kwargs)
@@ -457,15 +469,27 @@ async def test_request_with_reply(app):
457469

458470
@pytest.mark.asyncio
459471
async def test_request_expect_no_reply(app):
460-
assert await _test_request(app, False, False, tries=2, timeout=0.1) is None
472+
assert await _test_request(app, False, False, tries=2, timeout=0.1) is True
461473

462474

463475
@pytest.mark.asyncio
464476
async def test_request_no_reply(app):
465-
with pytest.raises(asyncio.TimeoutError):
477+
with pytest.raises(DeliveryError):
466478
await _test_request(app, False, True, tries=2, timeout=0.1)
467479

468480

481+
@pytest.mark.asyncio
482+
async def test_request_send_timeout(app):
483+
with pytest.raises(DeliveryError):
484+
await _test_request(app, False, True, send_timeout=True, tries=2, timeout=0.1)
485+
486+
487+
@pytest.mark.asyncio
488+
async def test_request_send_fail(app):
489+
with pytest.raises(DeliveryError):
490+
await _test_request(app, False, True, send_success=False, tries=2, timeout=0.1)
491+
492+
469493
def _handle_reply(app, tsn):
470494
app.handle_message = mock.MagicMock()
471495
return app._handle_reply(

zigpy_xbee/api.py

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import functools
55
import logging
66

7+
from zigpy.exceptions import DeliveryError
78
from zigpy.types import LVList
89

910
from . import uart
@@ -35,14 +36,14 @@ class ModemStatus(t.uint8_t, t.UndefinedEnum):
3536
'queued_at': (0x09, (t.FrameId, t.ATCommand, t.Bytes), 0x88),
3637
'remote_at': (0x17, (t.FrameId, t.EUI64, t.NWK, t.uint8_t, t.ATCommand, t.Bytes), 0x97),
3738
'tx': (0x10, (), None),
38-
'tx_explicit': (0x11, (t.FrameId, t.EUI64, t.NWK, t.uint8_t, t.uint8_t, t.uint16_t, t.uint16_t, t.uint8_t, t.uint8_t, t.Bytes), None),
39+
'tx_explicit': (0x11, (t.FrameId, t.EUI64, t.NWK, t.uint8_t, t.uint8_t, t.uint16_t, t.uint16_t, t.uint8_t, t.uint8_t, t.Bytes), 0x8b),
3940
'create_source_route': (0x21, (t.FrameId, t.EUI64, t.NWK, t.uint8_t, LVList(t.NWK)), None),
4041
'register_joining_device': (0x24, (), None),
4142
}
4243
COMMAND_RESPONSES = {
4344
'at_response': (0x88, (t.FrameId, t.ATCommand, t.uint8_t, t.Bytes), None),
4445
'modem_status': (0x8A, (ModemStatus, ), None),
45-
'tx_status': (0x8B, (t.FrameId, t.NWK, t.uint8_t, t.uint8_t, t.uint8_t), None),
46+
'tx_status': (0x8B, (t.FrameId, t.NWK, t.uint8_t, t.TXStatus, t.DiscoveryStatus), None),
4647
'route_information': (0x8D, (), None),
4748
'rx': (0x90, (), None),
4849
'explicit_rx_indicator': (0x91, (t.EUI64, t.NWK, t.uint8_t, t.uint8_t, t.uint16_t, t.uint16_t, t.uint8_t, t.Bytes), None),
@@ -269,15 +270,15 @@ def frame_received(self, data):
269270
LOGGER.debug("Frame received: %s", command)
270271
data, rest = t.deserialize(data[1:], COMMAND_RESPONSES[command][1])
271272
try:
272-
getattr(self, '_handle_%s' % (command, ))(data)
273+
getattr(self, '_handle_%s' % (command, ))(*data)
273274
except AttributeError:
274275
LOGGER.error("No '%s' handler. Data: %s", command,
275276
binascii.hexlify(data))
276277

277-
def _handle_at_response(self, data):
278-
fut, = self._awaiting.pop(data[0])
278+
def _handle_at_response(self, frame_id, cmd, status, value):
279+
fut, = self._awaiting.pop(frame_id)
279280
try:
280-
status = ATCommandResult(data[2])
281+
status = ATCommandResult(status)
281282
except ValueError:
282283
status = ATCommandResult.ERROR
283284

@@ -286,25 +287,26 @@ def _handle_at_response(self, data):
286287
RuntimeError("AT Command response: {}".format(status.name)))
287288
return
288289

289-
response_type = AT_COMMANDS[data[1].decode('ascii')]
290-
if response_type is None or len(data[3]) == 0:
290+
response_type = AT_COMMANDS[cmd.decode('ascii')]
291+
if response_type is None or len(value) == 0:
291292
fut.set_result(None)
292293
return
293294

294-
response, remains = response_type.deserialize(data[3])
295+
response, remains = response_type.deserialize(value)
295296
fut.set_result(response)
296297

297-
def _handle_remote_at_response(self, data):
298+
def _handle_remote_at_response(self, frame_id, ieee, nwk, cmd, status, value):
298299
"""Remote AT command response."""
299-
LOGGER.debug("Remote AT command response: %s", data)
300-
return self._handle_at_response((data[0], data[3], data[4], data[5]))
300+
LOGGER.debug("Remote AT command response from: %s",
301+
(frame_id, ieee, nwk, cmd, status, value))
302+
return self._handle_at_response(frame_id, cmd, status, value)
301303

302-
def _handle_many_to_one_rri(self, data):
303-
LOGGER.debug("_handle_many_to_one_rri: %s", data)
304+
def _handle_many_to_one_rri(self, ieee, nwk, reserved):
305+
LOGGER.debug("_handle_many_to_one_rri: %s", (ieee, nwk, reserved))
304306

305-
def _handle_modem_status(self, data):
306-
LOGGER.debug("Handle modem status frame: %s", data)
307-
status = data[0]
307+
def _handle_modem_status(self, status):
308+
LOGGER.debug("Handle modem status frame: %s", status)
309+
status = status
308310
if status == ModemStatus.COORDINATOR_STARTED:
309311
self.coordinator_started_event.set()
310312
elif status in (ModemStatus.HARDWARE_RESET, ModemStatus.WATCHDOG_TIMER_RESET):
@@ -316,16 +318,38 @@ def _handle_modem_status(self, data):
316318
if self._app:
317319
self._app.handle_modem_status(status)
318320

319-
def _handle_explicit_rx_indicator(self, data):
320-
LOGGER.debug("_handle_explicit_rx: opts=%s", data[6])
321-
self._app.handle_rx(*data)
321+
def _handle_explicit_rx_indicator(self, ieee, nwk, src_ep,
322+
dst_ep, cluster, profile, rx_opts, data):
323+
LOGGER.debug("_handle_explicit_rx: %s",
324+
(ieee, nwk, dst_ep, cluster, rx_opts,
325+
binascii.hexlify(data)))
326+
self._app.handle_rx(ieee, nwk, src_ep, dst_ep, cluster,
327+
profile, rx_opts, data)
322328

323-
def _handle_route_record_indicator(self, data):
329+
def _handle_route_record_indicator(self, ieee, src, rx_opts, hops):
324330
"""Handle Route Record indicator from a device."""
325-
LOGGER.debug("_handle_route_record_indicator: %s", data)
331+
LOGGER.debug("_handle_route_record_indicator: %s",
332+
(ieee, src, rx_opts, hops))
326333

327-
def _handle_tx_status(self, data):
328-
LOGGER.debug("tx_status: %s", data)
334+
def _handle_tx_status(self, frame_id, nwk, tries, tx_status, dsc_status):
335+
LOGGER.debug(
336+
("tx_explicit to 0x%04x: %s after %i tries. Discovery Status: %s,"
337+
" Frame #%i"), nwk, tx_status, tries, dsc_status, frame_id)
338+
try:
339+
fut, = self._awaiting.pop(frame_id)
340+
except KeyError:
341+
LOGGER.debug("unexpected tx_status report received")
342+
return
343+
344+
try:
345+
if tx_status in (t.TXStatus.SUCCESS,
346+
t.TXStatus.BROADCAST_APS_TX_ATTEMPT):
347+
fut.set_result(tx_status)
348+
else:
349+
fut.set_exception(
350+
DeliveryError('%s' % (tx_status, )))
351+
except asyncio.InvalidStateError as ex:
352+
LOGGER.debug("duplicate tx_status for %s nwk? State: %s", nwk, ex)
329353

330354
def set_application(self, app):
331355
self._app = app

0 commit comments

Comments
 (0)