Skip to content

Commit e3bf1b8

Browse files
authored
Refactor Deconz._command() method. (#61)
1 parent 5121895 commit e3bf1b8

File tree

2 files changed

+54
-34
lines changed

2 files changed

+54
-34
lines changed

tests/test_api.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@
77
import zigpy_deconz.exception
88

99

10-
COMMANDS = [*deconz_api.TX_COMMANDS.items(), *deconz_api.RX_COMMANDS.items()]
11-
12-
1310
@pytest.fixture
1411
def api():
1512
api = deconz_api.Deconz()
@@ -41,7 +38,7 @@ def test_close(api):
4138
def test_commands():
4239
import string
4340
anum = string.ascii_letters + string.digits + '_'
44-
for cmd_name, cmd_opts in COMMANDS:
41+
for cmd_name, cmd_opts in deconz_api.RX_COMMANDS.items():
4542
assert isinstance(cmd_name, str) is True
4643
assert all([c in anum for c in cmd_name]), cmd_name
4744
assert len(cmd_opts) == 3
@@ -50,12 +47,19 @@ def test_commands():
5047
assert isinstance(schema, tuple) is True
5148
assert reply is None or isinstance(reply, bool)
5249

50+
for cmd_name, cmd_opts in deconz_api.TX_COMMANDS.items():
51+
assert isinstance(cmd_name, str) is True
52+
assert all([c in anum for c in cmd_name]), cmd_name
53+
assert len(cmd_opts) == 2
54+
cmd_id, schema = cmd_opts
55+
assert isinstance(cmd_id, int) is True
56+
assert isinstance(schema, tuple) is True
57+
5358

5459
@pytest.mark.asyncio
5560
async def test_command(api, monkeypatch):
5661
def mock_api_frame(name, *args):
57-
c = deconz_api.TX_COMMANDS[name]
58-
return mock.sentinel.api_frame_data, c[2]
62+
return mock.sentinel.api_frame_data
5963
api._api_frame = mock.MagicMock(side_effect=mock_api_frame)
6064
api._uart.send = mock.MagicMock()
6165

@@ -64,12 +68,29 @@ async def mock_fut():
6468
monkeypatch.setattr(asyncio, 'Future', mock_fut)
6569

6670
for cmd_name, cmd_opts in deconz_api.TX_COMMANDS.items():
67-
_, _, expect_reply = cmd_opts
6871
ret = await api._command(cmd_name, mock.sentinel.cmd_data)
69-
if expect_reply:
70-
assert ret is mock.sentinel.cmd_result
71-
else:
72-
assert ret is None
72+
assert ret is mock.sentinel.cmd_result
73+
assert api._api_frame.call_count == 1
74+
assert api._api_frame.call_args[0][0] == cmd_name
75+
assert api._api_frame.call_args[0][1] == mock.sentinel.cmd_data
76+
assert api._uart.send.call_count == 1
77+
assert api._uart.send.call_args[0][0] == mock.sentinel.api_frame_data
78+
api._api_frame.reset_mock()
79+
api._uart.send.reset_mock()
80+
81+
82+
@pytest.mark.asyncio
83+
async def test_command_timeout(api, monkeypatch):
84+
def mock_api_frame(name, *args):
85+
return mock.sentinel.api_frame_data
86+
api._api_frame = mock.MagicMock(side_effect=mock_api_frame)
87+
api._uart.send = mock.MagicMock()
88+
89+
monkeypatch.setattr(deconz_api, 'COMMAND_TIMEOUT', 0.1)
90+
91+
for cmd_name, cmd_opts in deconz_api.TX_COMMANDS.items():
92+
with pytest.raises(asyncio.TimeoutError):
93+
await api._command(cmd_name, mock.sentinel.cmd_data)
7394
assert api._api_frame.call_count == 1
7495
assert api._api_frame.call_args[0][0] == cmd_name
7596
assert api._api_frame.call_args[0][1] == mock.sentinel.cmd_data
@@ -85,7 +106,7 @@ def test_api_frame(api):
85106
addr.address = t.uint8_t(0)
86107
addr.endpoint = t.uint8_t(0)
87108
for cmd_name, cmd_opts in deconz_api.TX_COMMANDS.items():
88-
_, schema, _ = cmd_opts
109+
_, schema = cmd_opts
89110
if schema:
90111
args = [addr if isinstance(a(), t.DeconzAddressEndpoint) else a() for a in schema]
91112
api._api_frame(cmd_name, *args)
@@ -103,7 +124,7 @@ def test_data_received(api, monkeypatch):
103124
payload = b'\x01\x02\x03\x04'
104125
data = cmd_id.to_bytes(1, 'big') + b'\x00\x00\x00\x00' + payload
105126
setattr(api, '_handle_{}'.format(cmd), my_handler)
106-
api._awaiting[0] = (mock.MagicMock(), )
127+
api._awaiting[0] = mock.MagicMock()
107128
api.data_received(data)
108129
assert t.deserialize.call_count == 1
109130
assert t.deserialize.call_args[0][0] == payload
@@ -125,7 +146,7 @@ def test_data_received_unk_status(api, monkeypatch):
125146
data = cmd_id.to_bytes(1, 'big') + b'\x00' + \
126147
status + b'\x00\x00' + payload
127148
setattr(api, '_handle_{}'.format(cmd), my_handler)
128-
api._awaiting[0] = (mock.MagicMock(), )
149+
api._awaiting[0] = mock.MagicMock()
129150
api.data_received(data)
130151
assert t.deserialize.call_count == 1
131152
assert t.deserialize.call_args[0][0] == payload

zigpy_deconz/api.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,19 @@
1313
DECONZ_BAUDRATE = 38400
1414

1515
TX_COMMANDS = {
16-
'device_state': (0x07, (t.uint8_t, t.uint8_t, t.uint8_t), True),
17-
'change_network_state': (0x08, (t.uint8_t, ), True),
18-
'read_parameter': (0x0A, (t.uint16_t, t.uint8_t), True),
19-
'write_parameter': (0x0B, (t.uint16_t, t.uint8_t, t.Bytes), True),
20-
'version': (0x0D, (), True),
21-
'aps_data_indication': (0x17, (t.uint16_t, t.uint8_t), True),
16+
'device_state': (0x07, (t.uint8_t, t.uint8_t, t.uint8_t), ),
17+
'change_network_state': (0x08, (t.uint8_t, ), ),
18+
'read_parameter': (0x0A, (t.uint16_t, t.uint8_t), ),
19+
'write_parameter': (0x0B, (t.uint16_t, t.uint8_t, t.Bytes), ),
20+
'version': (0x0D, (), ),
21+
'aps_data_indication': (0x17, (t.uint16_t, t.uint8_t), ),
2222
'aps_data_request': (
2323
0x12,
2424
(t.uint16_t, t.uint8_t, t.uint8_t, t.DeconzAddressEndpoint,
2525
t.uint16_t, t.uint16_t, t.uint8_t, t.LVBytes, t.uint8_t,
2626
t.uint8_t),
27-
True),
28-
'aps_data_confirm': (0x04, (t.uint16_t, ), True),
27+
),
28+
'aps_data_confirm': (0x04, (t.uint16_t, ), ),
2929
}
3030

3131
RX_COMMANDS = {
@@ -126,28 +126,27 @@ def close(self):
126126

127127
async def _command(self, name, *args):
128128
LOGGER.debug("Command %s %s", name, args)
129-
data, needs_response = self._api_frame(name, *args)
130-
self._uart.send(data)
131-
fut = None
132-
if needs_response:
133-
fut = asyncio.Future()
134-
self._awaiting[self._seq] = (fut, )
135129
self._seq = (self._seq % 255) + 1
130+
data = self._api_frame(name, *args)
131+
self._uart.send(data)
132+
fut = asyncio.Future()
133+
self._awaiting[self._seq] = fut
136134
try:
137135
return await asyncio.wait_for(fut, timeout=COMMAND_TIMEOUT)
138136
except asyncio.TimeoutError:
139137
LOGGER.warning("No response to '%s' command", name)
138+
self._awaiting.pop(self._seq)
140139
raise
141140

142141
def _api_frame(self, name, *args):
143-
c = TX_COMMANDS[name]
144-
d = t.serialize(args, c[1])
145-
data = t.uint8_t(c[0]).serialize()
142+
cmd_id, schema = TX_COMMANDS[name]
143+
d = t.serialize(args, schema)
144+
data = t.uint8_t(cmd_id).serialize()
146145
data += t.uint8_t(self._seq).serialize()
147146
data += t.uint8_t(0).serialize()
148147
data += t.uint16_t(len(d) + 5).serialize()
149148
data += d
150-
return data, c[2]
149+
return data
151150

152151
def data_received(self, data):
153152
if data[0] not in self._commands_by_id:
@@ -163,8 +162,8 @@ def data_received(self, data):
163162
data, _ = t.deserialize(data[5:], RX_COMMANDS[command][1])
164163
except Exception:
165164
LOGGER.warning("Failed to deserialize frame: %s", binascii.hexlify(data))
166-
if RX_COMMANDS[command][2]:
167-
fut, = self._awaiting.pop(seq)
165+
if RX_COMMANDS[command][2] and seq in self._awaiting:
166+
fut = self._awaiting.pop(seq)
168167
if status != STATUS.SUCCESS:
169168
fut.set_exception(
170169
CommandError(status, '%s, status: %s' % (command,

0 commit comments

Comments
 (0)