Skip to content

Commit 38fe699

Browse files
jloehrAdminiuga
authored andcommitted
Add a command queue so there is always only one pending command (#112)
On RaspBee II modules (Firmware 26520700) sending multiple concurrent commands may result in missing command responses and duplicate device_state_changed commands. Sending commands one at a time with waiting for their response, seems to fix this issue.
1 parent eedf37a commit 38fe699

File tree

2 files changed

+39
-16
lines changed

2 files changed

+39
-16
lines changed

tests/test_api.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111

1212
@pytest.fixture
13-
def api():
13+
def api(event_loop):
1414
api = deconz_api.Deconz()
1515
api._uart = mock.MagicMock()
1616
return api
@@ -76,6 +76,26 @@ async def mock_fut():
7676
api._uart.send.reset_mock()
7777

7878

79+
@pytest.mark.asyncio
80+
async def test_command_queue(api, monkeypatch):
81+
def mock_api_frame(name, *args):
82+
return mock.sentinel.api_frame_data, api._seq
83+
84+
api._api_frame = mock.MagicMock(side_effect=mock_api_frame)
85+
api._uart.send = mock.MagicMock()
86+
87+
monkeypatch.setattr(deconz_api, "COMMAND_TIMEOUT", 0.1)
88+
89+
for cmd, cmd_opts in deconz_api.TX_COMMANDS.items():
90+
async with api._command_lock:
91+
with pytest.raises(asyncio.TimeoutError):
92+
await asyncio.wait_for(api._command(cmd, mock.sentinel.cmd_data), 0.1)
93+
assert api._api_frame.call_count == 0
94+
assert api._uart.send.call_count == 0
95+
api._api_frame.reset_mock()
96+
api._uart.send.reset_mock()
97+
98+
7999
@pytest.mark.asyncio
80100
async def test_command_timeout(api, monkeypatch):
81101
def mock_api_frame(name, *args):

zigpy_deconz/api.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,11 @@ def __init__(self):
193193
self._seq = 1
194194
self._awaiting = {}
195195
self._app = None
196-
self._cmd_mode_future = None
197-
self._conn_lost_task = None
196+
self._command_lock = asyncio.Lock()
197+
self._conn_lost_task: typing.Optional[asyncio.Task] = None
198+
self._data_indication: bool = False
199+
self._data_confirm: bool = False
198200
self._device_state = DeviceState(NetworkState.OFFLINE)
199-
self._data_indication = False
200-
self._data_confirm = False
201201
self._proto_ver = None
202202
self._aps_data_ind_flags = 0x01
203203

@@ -263,20 +263,23 @@ def close(self):
263263
self._uart = None
264264

265265
async def _command(self, cmd, *args):
266-
LOGGER.debug("Command %s %s", cmd, args)
267266
if self._uart is None:
268267
# connection was lost
269268
raise CommandError(Status.ERROR, "API is not running")
270-
data, seq = self._api_frame(cmd, *args)
271-
self._uart.send(data)
272-
fut = asyncio.Future()
273-
self._awaiting[seq] = fut
274-
try:
275-
return await asyncio.wait_for(fut, timeout=COMMAND_TIMEOUT)
276-
except asyncio.TimeoutError:
277-
LOGGER.warning("No response to '%s' command", cmd)
278-
self._awaiting.pop(seq)
279-
raise
269+
async with self._command_lock:
270+
LOGGER.debug("Command %s %s", cmd, args)
271+
data, seq = self._api_frame(cmd, *args)
272+
self._uart.send(data)
273+
fut = asyncio.Future()
274+
self._awaiting[seq] = fut
275+
try:
276+
return await asyncio.wait_for(fut, timeout=COMMAND_TIMEOUT)
277+
except asyncio.TimeoutError:
278+
LOGGER.warning(
279+
"No response to '%s' command with seq id '0x%02x'", cmd, seq
280+
)
281+
self._awaiting.pop(seq)
282+
raise
280283

281284
def _api_frame(self, cmd, *args):
282285
schema = TX_COMMANDS[cmd]

0 commit comments

Comments
 (0)