Skip to content

Commit 63f727e

Browse files
authored
WIP Retry aps_data_request on STATUS.BUSY failures. (#51)
* Status enum as int. * Add zigpy_deconz exceptions. * Failback and retry aps_data_request command. Retry aps_data_request() if ConBee is "busy" with other requests. * Update tests.
1 parent 883dbc4 commit 63f727e

File tree

4 files changed

+163
-14
lines changed

4 files changed

+163
-14
lines changed

tests/test_api.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pytest
55

66
from zigpy_deconz import api as deconz_api, types as t, uart
7+
import zigpy_deconz.exception
78

89

910
COMMANDS = [*deconz_api.TX_COMMANDS.items(), *deconz_api.RX_COMMANDS.items()]
@@ -109,6 +110,47 @@ def test_data_received(api, monkeypatch):
109110
my_handler.reset_mock()
110111

111112

113+
def test_data_received_unk_status(api, monkeypatch):
114+
monkeypatch.setattr(t, 'deserialize', mock.MagicMock(
115+
return_value=(mock.sentinel.deserialize_data, b'')))
116+
my_handler = mock.MagicMock()
117+
118+
for cmd, cmd_opts in deconz_api.RX_COMMANDS.items():
119+
cmd_id, unsolicited = cmd_opts[0], cmd_opts[2]
120+
payload = b'\x01\x02\x03\x04'
121+
status = t.uint8_t(0xfe).serialize()
122+
data = cmd_id.to_bytes(1, 'big') + b'\x00' + \
123+
status + b'\x00\x00' + payload
124+
setattr(api, '_handle_{}'.format(cmd), my_handler)
125+
api._awaiting[0] = (mock.MagicMock(), )
126+
api.data_received(data)
127+
assert t.deserialize.call_count == 1
128+
assert t.deserialize.call_args[0][0] == payload
129+
if unsolicited:
130+
assert my_handler.call_count == 0
131+
else:
132+
assert my_handler.call_count == 1
133+
t.deserialize.reset_mock()
134+
my_handler.reset_mock()
135+
136+
137+
def test_data_received_unk_cmd(api, monkeypatch):
138+
monkeypatch.setattr(t, 'deserialize', mock.MagicMock(
139+
return_value=(mock.sentinel.deserialize_data, b'')))
140+
141+
for cmd_id in range(0, 255):
142+
if cmd_id in api._commands_by_id:
143+
continue
144+
payload = b'\x01\x02\x03\x04'
145+
status = t.uint8_t(0x00).serialize()
146+
data = cmd_id.to_bytes(1, 'big') + b'\x00' + \
147+
status + b'\x00\x00' + payload
148+
api._awaiting[0] = (mock.MagicMock(), )
149+
api.data_received(data)
150+
assert t.deserialize.call_count == 0
151+
t.deserialize.reset_mock()
152+
153+
112154
def test_simplified_beacon(api):
113155
api._handle_simplified_beacon(
114156
(0x0007, 0x1234, 0x5678, 0x19, 0x00, 0x01)
@@ -166,3 +208,71 @@ def mock_cmd(*args, **kwargs):
166208
res = await api._aps_data_indication()
167209
assert res is None
168210
assert api._data_indication is False
211+
212+
213+
@pytest.mark.asyncio
214+
async def test_aps_data_request(api):
215+
params = [
216+
0x00, # req id
217+
t.DeconzAddressEndpoint.deserialize(
218+
b'\x02\xaa\x55\x01')[0], # dst + ep
219+
0x0104, # profile id
220+
0x0007, # cluster id
221+
0x01, # src ep
222+
b'aps payload'
223+
]
224+
225+
mock_cmd = mock.MagicMock(
226+
side_effect=asyncio.coroutine(mock.MagicMock()))
227+
api._command = mock_cmd
228+
229+
await api.aps_data_request(*params)
230+
assert mock_cmd.call_count == 1
231+
232+
233+
@pytest.mark.asyncio
234+
async def test_aps_data_request_timeout(api, monkeypatch):
235+
params = [
236+
0x00, # req id
237+
t.DeconzAddressEndpoint.deserialize(
238+
b'\x02\xaa\x55\x01')[0], # dst + ep
239+
0x0104, # profile id
240+
0x0007, # cluster id
241+
0x01, # src ep
242+
b'aps payload'
243+
]
244+
245+
mock_cmd = mock.MagicMock(return_value=asyncio.Future())
246+
api._command = mock_cmd
247+
monkeypatch.setattr(deconz_api, 'COMMAND_TIMEOUT', .1)
248+
249+
with pytest.raises(asyncio.TimeoutError):
250+
await api.aps_data_request(*params)
251+
assert mock_cmd.call_count == 1
252+
253+
254+
@pytest.mark.asyncio
255+
async def test_aps_data_request_busy(api, monkeypatch):
256+
params = [
257+
0x00, # req id
258+
t.DeconzAddressEndpoint.deserialize(
259+
b'\x02\xaa\x55\x01')[0], # dst + ep
260+
0x0104, # profile id
261+
0x0007, # cluster id
262+
0x01, # src ep
263+
b'aps payload'
264+
]
265+
266+
res = asyncio.Future()
267+
exc = zigpy_deconz.exception.CommandError(deconz_api.STATUS.BUSY, 'busy')
268+
res.set_exception(exc)
269+
mock_cmd = mock.MagicMock(return_value=res)
270+
271+
api._command = mock_cmd
272+
monkeypatch.setattr(deconz_api, 'COMMAND_TIMEOUT', .1)
273+
sleep = mock.MagicMock(side_effect=asyncio.coroutine(mock.MagicMock()))
274+
monkeypatch.setattr(asyncio, 'sleep', sleep)
275+
276+
with pytest.raises(zigpy_deconz.exception.CommandError):
277+
await api.aps_data_request(*params)
278+
assert mock_cmd.call_count == 4

tests/test_exception.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from unittest import mock
2+
3+
import zigpy_deconz.exception
4+
5+
6+
def test_command_error():
7+
ex = zigpy_deconz.exception.CommandError(mock.sentinel.status,
8+
mock.sentinel.message)
9+
assert ex.status is mock.sentinel.status

zigpy_deconz/api.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from . import uart
77
from . import types as t
8+
from zigpy_deconz.exception import CommandError
89

910
LOGGER = logging.getLogger(__name__)
1011

@@ -76,7 +77,7 @@
7677
NETWORK_PARAMETER_BY_ID = {v[0]: (k, v[1]) for k, v in NETWORK_PARAMETER.items()}
7778

7879

79-
class STATUS(enum.Enum):
80+
class STATUS(t.uint8_t, enum.Enum):
8081
SUCCESS = 0
8182
FAILURE = 1
8283
BUSY = 2
@@ -150,15 +151,20 @@ def data_received(self, data):
150151
return
151152
command = self._commands_by_id[data[0]]
152153
seq = data[1]
153-
status = data[2]
154+
try:
155+
status = STATUS(data[2])
156+
except ValueError:
157+
status = data[2]
154158
try:
155159
data, _ = t.deserialize(data[5:], RX_COMMANDS[command][1])
156160
except Exception:
157161
LOGGER.warning("Failed to deserialize frame: %s", binascii.hexlify(data))
158162
if RX_COMMANDS[command][2]:
159163
fut, = self._awaiting.pop(seq)
160-
if status is not STATUS.SUCCESS.value:
161-
fut.set_exception(Exception('%s, status: %s' % (command, status, )))
164+
if status != STATUS.SUCCESS:
165+
fut.set_exception(
166+
CommandError(status, '%s, status: %s' % (command,
167+
status, )))
162168
return
163169
fut.set_result(data)
164170
getattr(self, '_handle_%s' % (command, ))(data)
@@ -265,16 +271,25 @@ def _handle_aps_data_indication(self, data):
265271
async def aps_data_request(self, req_id, dst_addr_ep, profile, cluster, src_ep, aps_payload):
266272
dst = dst_addr_ep.serialize()
267273
length = len(dst) + len(aps_payload) + 11
268-
try:
269-
return await asyncio.wait_for(
270-
self._command('aps_data_request', length, req_id, 0,
271-
dst_addr_ep, profile, cluster, src_ep,
272-
aps_payload, 0, 0),
273-
timeout=COMMAND_TIMEOUT
274-
)
275-
except asyncio.TimeoutError:
276-
LOGGER.warning("No response to aps_data_request command")
277-
raise
274+
delays = (0.5, 1.0, 1.5, None)
275+
for delay in delays:
276+
try:
277+
return await asyncio.wait_for(
278+
self._command('aps_data_request', length, req_id, 0,
279+
dst_addr_ep, profile, cluster, src_ep,
280+
aps_payload, 2, 0),
281+
timeout=COMMAND_TIMEOUT
282+
)
283+
except asyncio.TimeoutError:
284+
LOGGER.warning("No response to aps_data_request command")
285+
raise
286+
except CommandError as ex:
287+
LOGGER.debug("'aps_data_request' failure: %s", ex)
288+
if delay is not None and ex.status == STATUS.BUSY:
289+
LOGGER.debug("retrying 'aps_data_request' in %ss", delay)
290+
await asyncio.sleep(delay)
291+
continue
292+
raise
278293

279294
def _handle_aps_data_request(self, data):
280295
LOGGER.debug("APS data request response: %s", data)

zigpy_deconz/exception.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from zigpy.exceptions import ZigbeeException
2+
3+
4+
class DeconException(ZigbeeException):
5+
pass
6+
7+
8+
class CommandError(DeconException):
9+
def __init__(self, status, *args, **kwargs):
10+
self._status = status
11+
super().__init__(*args, **kwargs)
12+
13+
@property
14+
def status(self):
15+
return self._status

0 commit comments

Comments
 (0)