Skip to content

Commit cf63db3

Browse files
authored
Refactor api commands. (#39)
* Refactor api._command() method. * Allow calling of API commands through `__getattr__()`.
1 parent cb5cf5e commit cf63db3

File tree

5 files changed

+62
-57
lines changed

5 files changed

+62
-57
lines changed

tests/test_api.py

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ def test_close(api):
3333
def test_commands():
3434
import string
3535
anum = string.ascii_letters + string.digits + '_'
36+
commands = {**xbee_api.COMMAND_REQUESTS, **xbee_api.COMMAND_RESPONSES}
3637

37-
for cmd_name, cmd_opts in xbee_api.COMMANDS.items():
38+
for cmd_name, cmd_opts in commands.items():
3839
assert isinstance(cmd_name, str) is True
3940
assert all([c in anum for c in cmd_name]), cmd_name
4041
assert len(cmd_opts) == 3
@@ -47,12 +48,12 @@ def test_commands():
4748
@pytest.mark.asyncio
4849
async def test_command(api):
4950
def mock_api_frame(name, *args):
50-
c = xbee_api.COMMANDS[name]
51+
c = xbee_api.COMMAND_REQUESTS[name]
5152
return mock.sentinel.api_frame_data, c[2]
5253
api._api_frame = mock.MagicMock(side_effect=mock_api_frame)
5354
api._uart.send = mock.MagicMock()
5455

55-
for cmd_name, cmd_opts in xbee_api.COMMANDS.items():
56+
for cmd_name, cmd_opts in xbee_api.COMMAND_REQUESTS.items():
5657
cmd_id, schema, expect_reply = cmd_opts
5758
ret = api._command(cmd_name, mock.sentinel.cmd_data)
5859
if expect_reply:
@@ -62,28 +63,30 @@ def mock_api_frame(name, *args):
6263
assert ret is None
6364
assert api._api_frame.call_count == 1
6465
assert api._api_frame.call_args[0][0] == cmd_name
65-
assert api._api_frame.call_args[0][1] == mock.sentinel.cmd_data
66+
assert api._api_frame.call_args[0][1] == api._seq - 1
67+
assert api._api_frame.call_args[0][2] == mock.sentinel.cmd_data
6668
assert api._uart.send.call_count == 1
6769
assert api._uart.send.call_args[0][0] == mock.sentinel.api_frame_data
6870
api._api_frame.reset_mock()
6971
api._uart.send.reset_mock()
7072

71-
72-
def test_seq_command(api):
73-
api._command = mock.MagicMock()
74-
api._seq = mock.sentinel.seq
75-
api._seq_command(mock.sentinel.cmd_name, mock.sentinel.args)
76-
assert api._command.call_count == 1
77-
assert api._command.call_args[0][0] == mock.sentinel.cmd_name
78-
assert api._command.call_args[0][1] == mock.sentinel.seq
79-
assert api._command.call_args[0][2] == mock.sentinel.args
73+
ret = api._command(cmd_name, mock.sentinel.cmd_data, mask_frame_id=True)
74+
assert ret is None
75+
assert api._api_frame.call_count == 1
76+
assert api._api_frame.call_args[0][0] == cmd_name
77+
assert api._api_frame.call_args[0][1] == 0
78+
assert api._api_frame.call_args[0][2] == mock.sentinel.cmd_data
79+
assert api._uart.send.call_count == 1
80+
assert api._uart.send.call_args[0][0] == mock.sentinel.api_frame_data
81+
api._api_frame.reset_mock()
82+
api._uart.send.reset_mock()
8083

8184

8285
async def _test_at_or_queued_at_command(api, cmd, monkeypatch, do_reply=True):
8386
monkeypatch.setattr(t, 'serialize', mock.MagicMock(return_value=mock.sentinel.serialize))
8487

8588
def mock_command(name, *args):
86-
rsp = xbee_api.COMMANDS[name][2]
89+
rsp = xbee_api.COMMAND_REQUESTS[name][2]
8790
ret = None
8891
if rsp:
8992
ret = asyncio.Future()
@@ -99,9 +102,8 @@ def mock_command(name, *args):
99102
assert t.serialize.call_count == 1
100103
assert api._command.call_count == 1
101104
assert api._command.call_args[0][0] in ('at', 'queued_at')
102-
assert api._command.call_args[0][1] == mock.sentinel.seq
103-
assert api._command.call_args[0][2] == at_cmd.encode('ascii')
104-
assert api._command.call_args[0][3] == mock.sentinel.serialize
105+
assert api._command.call_args[0][1] == at_cmd.encode('ascii')
106+
assert api._command.call_args[0][2] == mock.sentinel.serialize
105107
assert res == mock.sentinel.at_result
106108
t.serialize.reset_mock()
107109
api._command.reset_mock()
@@ -129,7 +131,7 @@ async def _test_remote_at_command(api, monkeypatch, do_reply=True):
129131
monkeypatch.setattr(t, 'serialize', mock.MagicMock(return_value=mock.sentinel.serialize))
130132

131133
def mock_command(name, *args):
132-
rsp = xbee_api.COMMANDS[name][2]
134+
rsp = xbee_api.COMMAND_REQUESTS[name][2]
133135
ret = None
134136
if rsp:
135137
ret = asyncio.Future()
@@ -147,12 +149,11 @@ def mock_command(name, *args):
147149
assert t.serialize.call_count == 1
148150
assert api._command.call_count == 1
149151
assert api._command.call_args[0][0] == 'remote_at'
150-
assert api._command.call_args[0][1] == mock.sentinel.seq
151-
assert api._command.call_args[0][2] == mock.sentinel.ieee
152-
assert api._command.call_args[0][3] == mock.sentinel.nwk
153-
assert api._command.call_args[0][4] == mock.sentinel.opts
154-
assert api._command.call_args[0][5] == at_cmd.encode('ascii')
155-
assert api._command.call_args[0][6] == mock.sentinel.serialize
152+
assert api._command.call_args[0][1] == mock.sentinel.ieee
153+
assert api._command.call_args[0][2] == mock.sentinel.nwk
154+
assert api._command.call_args[0][3] == mock.sentinel.opts
155+
assert api._command.call_args[0][4] == at_cmd.encode('ascii')
156+
assert api._command.call_args[0][5] == mock.sentinel.serialize
156157
assert res == mock.sentinel.at_result
157158
t.serialize.reset_mock()
158159
api._command.reset_mock()
@@ -172,7 +173,7 @@ async def test_remote_at_cmd_no_rsp(api, monkeypatch):
172173

173174
def test_api_frame(api):
174175
ieee = t.EUI64([t.uint8_t(a) for a in range(0, 8)])
175-
for cmd_name, cmd_opts in xbee_api.COMMANDS.items():
176+
for cmd_name, cmd_opts in xbee_api.COMMAND_REQUESTS.items():
176177
cmd_id, schema, repl = cmd_opts
177178
if schema:
178179
args = [ieee if isinstance(a(), t.EUI64) else a() for a in schema]
@@ -186,7 +187,7 @@ def test_frame_received(api, monkeypatch):
186187
return_value=(mock.sentinel.deserialize_data, b'')))
187188
my_handler = mock.MagicMock()
188189

189-
for cmd, cmd_opts in xbee_api.COMMANDS.items():
190+
for cmd, cmd_opts in xbee_api.COMMAND_RESPONSES.items():
190191
cmd_id = cmd_opts[0]
191192
payload = b'\x01\x02\x03\x04'
192193
data = cmd_id.to_bytes(1, 'big') + payload
@@ -206,10 +207,10 @@ def test_frame_received_no_handler(api, monkeypatch):
206207
my_handler = mock.MagicMock()
207208
cmd = 'no_handler'
208209
cmd_id = 0x00
209-
xbee_api.COMMANDS[cmd] = (cmd_id, (), None)
210+
xbee_api.COMMAND_RESPONSES[cmd] = (cmd_id, (), None)
210211
api._commands_by_id[cmd_id] = cmd
211212

212-
cmd_opts = xbee_api.COMMANDS[cmd]
213+
cmd_opts = xbee_api.COMMAND_RESPONSES[cmd]
213214
cmd_id = cmd_opts[0]
214215
payload = b'\x01\x02\x03\x04'
215216
data = cmd_id.to_bytes(1, 'big') + payload

tests/test_application.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -250,17 +250,17 @@ async def test_broadcast(app):
250250
0x260, 1, 2, 3, 0x0100, 0x06, 210, b'\x02\x01\x00'
251251
)
252252

253-
app._api._seq_command = mock.MagicMock(
253+
app._api._command = mock.MagicMock(
254254
side_effect=asyncio.coroutine(mock.MagicMock())
255255
)
256256

257257
await app.broadcast(
258258
profile, cluster, src_ep, dst_ep, grpid, radius, tsn, data)
259-
assert app._api._seq_command.call_count == 1
260-
assert app._api._seq_command.call_args[0][0] == 'tx_explicit'
261-
assert app._api._seq_command.call_args[0][3] == src_ep
262-
assert app._api._seq_command.call_args[0][4] == dst_ep
263-
assert app._api._seq_command.call_args[0][9] == data
259+
assert app._api._command.call_count == 1
260+
assert app._api._command.call_args[0][0] == 'tx_explicit'
261+
assert app._api._command.call_args[0][3] == src_ep
262+
assert app._api._command.call_args[0][4] == dst_ep
263+
assert app._api._command.call_args[0][9] == data
264264

265265

266266
@pytest.mark.asyncio
@@ -440,13 +440,13 @@ async def _test_request(app, do_reply=True, expect_reply=True, **kwargs):
440440
ieee = EUI64(b'\x01\x02\x03\x04\x05\x06\x07\x08')
441441
app.add_device(ieee, nwk)
442442

443-
def _mock_seq_command(cmdname, ieee, nwk, src_ep, dst_ep, cluster,
444-
profile, radius, options, data):
443+
def _mock_command(cmdname, ieee, nwk, src_ep, dst_ep, cluster,
444+
profile, radius, options, data):
445445
if expect_reply:
446446
if do_reply:
447447
app._pending[seq].set_result(mock.sentinel.reply_result)
448448

449-
app._api._seq_command = mock.MagicMock(side_effect=_mock_seq_command)
449+
app._api._command = mock.MagicMock(side_effect=_mock_command)
450450
return await app.request(nwk, 0x0260, 1, 2, 3, seq, [4, 5, 6], expect_reply=expect_reply, **kwargs)
451451

452452

tests/test_types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ class undEnum(t.uint8_t, t.UndefinedEnum):
6262
i = undEnum(0xEE)
6363
assert i.name == 'UNDEFINED_VALUE'
6464

65+
i = undEnum()
66+
assert i is undEnum.OK
67+
6568

6669
def test_undefined_enum_undefinede():
6770
class undEnum(t.uint8_t, t.UndefinedEnum):

zigpy_xbee/api.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,16 @@ class ModemStatus(t.uint8_t, t.UndefinedEnum):
3030

3131

3232
# https://www.digi.com/resources/documentation/digidocs/PDFs/90000976.pdf
33-
COMMANDS = {
33+
COMMAND_REQUESTS = {
3434
'at': (0x08, (t.FrameId, t.ATCommand, t.Bytes), 0x88),
3535
'queued_at': (0x09, (t.FrameId, t.ATCommand, t.Bytes), 0x88),
3636
'remote_at': (0x17, (t.FrameId, t.EUI64, t.NWK, t.uint8_t, t.ATCommand, t.Bytes), 0x97),
3737
'tx': (0x10, (), None),
3838
'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),
3939
'create_source_route': (0x21, (t.FrameId, t.EUI64, t.NWK, t.uint8_t, LVList(t.NWK)), None),
4040
'register_joining_device': (0x24, (), None),
41-
41+
}
42+
COMMAND_RESPONSES = {
4243
'at_response': (0x88, (t.FrameId, t.ATCommand, t.uint8_t, t.Bytes), None),
4344
'modem_status': (0x8A, (ModemStatus, ), None),
4445
'tx_status': (0x8B, (t.FrameId, t.NWK, t.uint8_t, t.uint8_t, t.uint8_t), None),
@@ -192,7 +193,7 @@ class XBee:
192193
def __init__(self):
193194
self._uart = None
194195
self._seq = 1
195-
self._commands_by_id = {v[0]: k for k, v in COMMANDS.items()}
196+
self._commands_by_id = {v[0]: k for k, v in COMMAND_RESPONSES.items()}
196197
self._awaiting = {}
197198
self._app = None
198199
self._cmd_mode_future = None
@@ -221,27 +222,24 @@ async def connect(self, device, baudrate=115200):
221222
def close(self):
222223
return self._uart.close()
223224

224-
def _command(self, name, *args):
225+
def _command(self, name, *args, mask_frame_id=False):
225226
LOGGER.debug("Command %s %s", name, args)
226-
data, needs_response = self._api_frame(name, *args)
227+
frame_id = 0 if mask_frame_id else self._seq
228+
data, needs_response = self._api_frame(name, frame_id, *args)
227229
self._uart.send(data)
228230
future = None
229-
if needs_response:
231+
if needs_response and frame_id:
230232
future = asyncio.Future()
231-
self._awaiting[self._seq] = (future, )
233+
self._awaiting[frame_id] = (future, )
232234
self._seq = (self._seq % 255) + 1
233235
return future
234236

235-
def _seq_command(self, name, *args):
236-
LOGGER.debug("Sequenced command: %s %s", name, args)
237-
return self._command(name, self._seq, *args)
238-
239237
async def _remote_at_command(self, ieee, nwk, options, name, *args):
240238
LOGGER.debug("Remote AT command: %s %s", name, args)
241239
data = t.serialize(args, (AT_COMMANDS[name], ))
242240
try:
243241
return await asyncio.wait_for(
244-
self._command('remote_at', self._seq, ieee, nwk, options,
242+
self._command('remote_at', ieee, nwk, options,
245243
name.encode('ascii'), data,),
246244
timeout=REMOTE_AT_COMMAND_TIMEOUT)
247245
except asyncio.TimeoutError:
@@ -253,7 +251,7 @@ async def _at_partial(self, cmd_type, name, *args):
253251
data = t.serialize(args, (AT_COMMANDS[name], ))
254252
try:
255253
return await asyncio.wait_for(
256-
self._command(cmd_type, self._seq, name.encode('ascii'), data),
254+
self._command(cmd_type, name.encode('ascii'), data),
257255
timeout=AT_COMMAND_TIMEOUT)
258256
except asyncio.TimeoutError:
259257
LOGGER.warning("%s: No response to %s command", cmd_type, name)
@@ -263,13 +261,13 @@ async def _at_partial(self, cmd_type, name, *args):
263261
_queued_at = functools.partialmethod(_at_partial, 'queued_at')
264262

265263
def _api_frame(self, name, *args):
266-
c = COMMANDS[name]
264+
c = COMMAND_REQUESTS[name]
267265
return (bytes([c[0]]) + t.serialize(args, c[1])), c[2]
268266

269267
def frame_received(self, data):
270268
command = self._commands_by_id[data[0]]
271269
LOGGER.debug("Frame received: %s", command)
272-
data, rest = t.deserialize(data[1:], COMMANDS[command][1])
270+
data, rest = t.deserialize(data[1:], COMMAND_RESPONSES[command][1])
273271
try:
274272
getattr(self, '_handle_%s' % (command, ))(data)
275273
except AttributeError:
@@ -401,3 +399,8 @@ async def init_api_mode(self):
401399
LOGGER.debug(("Couldn't enter AT command mode at any known baudrate."
402400
"Configure XBee manually for escaped API mode ATAP2"))
403401
return False
402+
403+
def __getattr__(self, item):
404+
if item in COMMAND_REQUESTS:
405+
return functools.partial(self._command, item)
406+
raise AttributeError("Unknown command {}".format(item))

zigpy_xbee/zigbee/application.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,7 @@ async def request(self, nwk, profile, cluster, src_ep, dst_ep, sequence, data, e
126126
self._pending[sequence] = reply_fut
127127

128128
dev = self.get_device(nwk=nwk)
129-
self._api._seq_command(
130-
'tx_explicit',
129+
self._api.tx_explicit(
131130
dev.ieee,
132131
nwk,
133132
src_ep,
@@ -214,7 +213,7 @@ def handle_rx(self, src_ieee, src_nwk, src_ep, dst_ep, cluster_id, profile_id, r
214213
try:
215214
tsn, command_id, is_reply, args = self.deserialize(device, src_ep, cluster_id, data)
216215
except ValueError as e:
217-
LOGGER.error("Failed to parse message (%s) on cluster %d, because %s", binascii.hexlify(data), cluster_id, e)
216+
LOGGER.error("Failed to parse message (%s) on cluster %s, because %s", binascii.hexlify(data), cluster_id, e)
218217
return
219218

220219
if is_reply:
@@ -246,8 +245,7 @@ async def broadcast(self, profile, cluster, src_ep, dst_ep, grpid, radius,
246245
broadcast_as_bytes = [
247246
zigpy.types.uint8_t(b) for b in broadcast_address.to_bytes(8, 'big')
248247
]
249-
self._api._seq_command(
250-
'tx_explicit',
248+
self._api.tx_explicit(
251249
zigpy.types.EUI64(broadcast_as_bytes),
252250
broadcast_address,
253251
src_ep,

0 commit comments

Comments
 (0)