Skip to content

Commit cb5cf5e

Browse files
authored
Implement 'remote_at' XBee command (#37)
* FrameId class for commands schema. * Remote AT command schema. * Remote AT command api support. * Remote_AT ControllerApplication support. * Update test units. * Use partialmethod for at/queued_at commands.
1 parent 29d3c06 commit cb5cf5e

File tree

5 files changed

+122
-21
lines changed

5 files changed

+122
-21
lines changed

tests/test_api.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,51 @@ async def test_queued_at_command(api, monkeypatch):
125125
await _test_at_or_queued_at_command(api, api._queued_at, monkeypatch)
126126

127127

128+
async def _test_remote_at_command(api, monkeypatch, do_reply=True):
129+
monkeypatch.setattr(t, 'serialize', mock.MagicMock(return_value=mock.sentinel.serialize))
130+
131+
def mock_command(name, *args):
132+
rsp = xbee_api.COMMANDS[name][2]
133+
ret = None
134+
if rsp:
135+
ret = asyncio.Future()
136+
if do_reply:
137+
ret.set_result(mock.sentinel.at_result)
138+
return ret
139+
140+
api._command = mock.MagicMock(side_effect=mock_command)
141+
api._seq = mock.sentinel.seq
142+
143+
for at_cmd in xbee_api.AT_COMMANDS:
144+
res = await api._remote_at_command(
145+
mock.sentinel.ieee, mock.sentinel.nwk, mock.sentinel.opts, at_cmd,
146+
mock.sentinel.args)
147+
assert t.serialize.call_count == 1
148+
assert api._command.call_count == 1
149+
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
156+
assert res == mock.sentinel.at_result
157+
t.serialize.reset_mock()
158+
api._command.reset_mock()
159+
160+
161+
@pytest.mark.asyncio
162+
async def test_remote_at_cmd(api, monkeypatch):
163+
await _test_remote_at_command(api, monkeypatch)
164+
165+
166+
@pytest.mark.asyncio
167+
async def test_remote_at_cmd_no_rsp(api, monkeypatch):
168+
monkeypatch.setattr(xbee_api, 'REMOTE_AT_COMMAND_TIMEOUT', 0.1)
169+
with pytest.raises(asyncio.TimeoutError):
170+
await _test_remote_at_command(api, monkeypatch, do_reply=False)
171+
172+
128173
def test_api_frame(api):
129174
ieee = t.EUI64([t.uint8_t(a) for a in range(0, 8)])
130175
for cmd_name, cmd_opts in xbee_api.COMMANDS.items():
@@ -215,6 +260,18 @@ def test_handle_at_response_undef_error(api):
215260
assert fut.exception() is not None
216261

217262

263+
def test_handle_remote_at_rsp(api):
264+
api._handle_at_response = mock.MagicMock()
265+
s = mock.sentinel
266+
api._handle_remote_at_response([s.frame_id, s.ieee, s.nwk, s.cmd,
267+
s.status, s.data])
268+
assert api._handle_at_response.call_count == 1
269+
assert api._handle_at_response.call_args[0][0][0] == s.frame_id
270+
assert api._handle_at_response.call_args[0][0][1] == s.cmd
271+
assert api._handle_at_response.call_args[0][0][2] == s.status
272+
assert api._handle_at_response.call_args[0][0][3] == s.data
273+
274+
218275
def _send_modem_event(api, event):
219276
api._app = mock.MagicMock(spec=ControllerApplication)
220277
api._handle_modem_status([event])

tests/test_application.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,3 +524,18 @@ async def test_shutdown(app):
524524
app._api.close = mock.MagicMock()
525525
await app.shutdown()
526526
assert app._api.close.call_count == 1
527+
528+
529+
def test_remote_at_cmd(app, device):
530+
dev = device()
531+
app.get_device = mock.MagicMock(return_value=dev)
532+
app._api = mock.MagicMock(spec=XBee)
533+
s = mock.sentinel
534+
app.remote_at_command(s.nwk, s.cmd, s.data,
535+
apply_changes=True, encryption=True)
536+
assert app._api._remote_at_command.call_count == 1
537+
assert app._api._remote_at_command.call_args[0][0] is dev.ieee
538+
assert app._api._remote_at_command.call_args[0][1] == s.nwk
539+
assert app._api._remote_at_command.call_args[0][2] == 0x22
540+
assert app._api._remote_at_command.call_args[0][3] == s.cmd
541+
assert app._api._remote_at_command.call_args[0][4] == s.data

zigpy_xbee/api.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import binascii
33
import enum
4+
import functools
45
import logging
56

67
from zigpy.types import LVList
@@ -11,6 +12,7 @@
1112
LOGGER = logging.getLogger(__name__)
1213

1314
AT_COMMAND_TIMEOUT = 2
15+
REMOTE_AT_COMMAND_TIMEOUT = 30
1416

1517

1618
class ModemStatus(t.uint8_t, t.UndefinedEnum):
@@ -29,22 +31,22 @@ class ModemStatus(t.uint8_t, t.UndefinedEnum):
2931

3032
# https://www.digi.com/resources/documentation/digidocs/PDFs/90000976.pdf
3133
COMMANDS = {
32-
'at': (0x08, (t.uint8_t, t.ATCommand, t.Bytes), 0x88),
33-
'queued_at': (0x09, (t.uint8_t, t.ATCommand, t.Bytes), 0x88),
34-
'remote_at': (0x17, (), None),
34+
'at': (0x08, (t.FrameId, t.ATCommand, t.Bytes), 0x88),
35+
'queued_at': (0x09, (t.FrameId, t.ATCommand, t.Bytes), 0x88),
36+
'remote_at': (0x17, (t.FrameId, t.EUI64, t.NWK, t.uint8_t, t.ATCommand, t.Bytes), 0x97),
3537
'tx': (0x10, (), None),
36-
'tx_explicit': (0x11, (t.uint8_t, 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),
37-
'create_source_route': (0x21, (t.uint8_t, t.EUI64, t.NWK, t.uint8_t, LVList(t.NWK)), 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+
'create_source_route': (0x21, (t.FrameId, t.EUI64, t.NWK, t.uint8_t, LVList(t.NWK)), None),
3840
'register_joining_device': (0x24, (), None),
3941

40-
'at_response': (0x88, (t.uint8_t, t.ATCommand, t.uint8_t, t.Bytes), None),
42+
'at_response': (0x88, (t.FrameId, t.ATCommand, t.uint8_t, t.Bytes), None),
4143
'modem_status': (0x8A, (ModemStatus, ), None),
42-
'tx_status': (0x8B, (t.uint8_t, t.NWK, t.uint8_t, t.uint8_t, t.uint8_t), None),
44+
'tx_status': (0x8B, (t.FrameId, t.NWK, t.uint8_t, t.uint8_t, t.uint8_t), None),
4345
'route_information': (0x8D, (), None),
4446
'rx': (0x90, (), None),
4547
'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),
4648
'rx_io_data_long_addr': (0x92, (), None),
47-
'remote_at_response': (0x97, (), None),
49+
'remote_at_response': (0x97, (t.FrameId, t.EUI64, t.NWK, t.ATCommand, t.uint8_t, t.Bytes), None),
4850
'extended_status': (0x98, (), None),
4951
'route_record_indicator': (0xA1, (t.EUI64, t.NWK, t.uint8_t, LVList(t.NWK)), None),
5052
'many_to_one_rri': (0xA3, (t.EUI64, t.NWK, t.uint8_t), None),
@@ -183,6 +185,7 @@ class ATCommandResult(enum.IntEnum):
183185
ERROR = 1
184186
INVALID_COMMAND = 2
185187
INVALID_PARAMETER = 3
188+
TX_FAILURE = 4
186189

187190

188191
class XBee:
@@ -233,27 +236,32 @@ def _seq_command(self, name, *args):
233236
LOGGER.debug("Sequenced command: %s %s", name, args)
234237
return self._command(name, self._seq, *args)
235238

236-
def _queued_at(self, name, *args):
237-
LOGGER.debug("Queue AT command: %s %s", name, args)
239+
async def _remote_at_command(self, ieee, nwk, options, name, *args):
240+
LOGGER.debug("Remote AT command: %s %s", name, args)
238241
data = t.serialize(args, (AT_COMMANDS[name], ))
239-
return self._command(
240-
'queued_at',
241-
self._seq,
242-
name.encode('ascii'),
243-
data,
244-
)
245-
246-
async def _at_command(self, name, *args):
247-
LOGGER.debug("AT command: %s %s", name, args)
242+
try:
243+
return await asyncio.wait_for(
244+
self._command('remote_at', self._seq, ieee, nwk, options,
245+
name.encode('ascii'), data,),
246+
timeout=REMOTE_AT_COMMAND_TIMEOUT)
247+
except asyncio.TimeoutError:
248+
LOGGER.warning("No response to %s command", name)
249+
raise
250+
251+
async def _at_partial(self, cmd_type, name, *args):
252+
LOGGER.debug("%s command: %s %s", cmd_type, name, args)
248253
data = t.serialize(args, (AT_COMMANDS[name], ))
249254
try:
250255
return await asyncio.wait_for(
251-
self._command('at', self._seq, name.encode('ascii'), data,),
256+
self._command(cmd_type, self._seq, name.encode('ascii'), data),
252257
timeout=AT_COMMAND_TIMEOUT)
253258
except asyncio.TimeoutError:
254-
LOGGER.warning("No response to %s command", name)
259+
LOGGER.warning("%s: No response to %s command", cmd_type, name)
255260
raise
256261

262+
_at_command = functools.partialmethod(_at_partial, 'at')
263+
_queued_at = functools.partialmethod(_at_partial, 'queued_at')
264+
257265
def _api_frame(self, name, *args):
258266
c = COMMANDS[name]
259267
return (bytes([c[0]]) + t.serialize(args, c[1])), c[2]
@@ -288,6 +296,11 @@ def _handle_at_response(self, data):
288296
response, remains = response_type.deserialize(data[3])
289297
fut.set_result(response)
290298

299+
def _handle_remote_at_response(self, data):
300+
"""Remote AT command response."""
301+
LOGGER.debug("Remote AT command response: %s", data)
302+
return self._handle_at_response((data[0], data[3], data[4], data[5]))
303+
291304
def _handle_many_to_one_rri(self, data):
292305
LOGGER.debug("_handle_many_to_one_rri: %s", data)
293306

zigpy_xbee/types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ class UndefinedEnum(enum.Enum, metaclass=UndefinedEnumMeta):
149149
pass
150150

151151

152+
class FrameId(uint8_t):
153+
pass
154+
155+
152156
class NWK(uint16_t):
153157
def __repr__(self):
154158
return '0x{:04x}'.format(self)

zigpy_xbee/zigbee/application.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,18 @@ async def request(self, nwk, profile, cluster, src_ep, dst_ep, sequence, data, e
147147
self._pending.pop(sequence, None)
148148
raise
149149

150+
@zigpy.util.retryable_request
151+
def remote_at_command(self, nwk, cmd_name, *args, apply_changes=True,
152+
encryption=True):
153+
LOGGER.debug("Remote AT%s command: %s", cmd_name, args)
154+
options = zigpy.types.uint8_t(0)
155+
if apply_changes:
156+
options |= 0x02
157+
if encryption:
158+
options |= 0x20
159+
dev = self.get_device(nwk=nwk)
160+
return self._api._remote_at_command(dev.ieee, nwk, options, cmd_name, *args)
161+
150162
async def permit_ncp(self, time_s=60):
151163
assert 0 <= time_s <= 254
152164
await self._api._at_command('NJ', time_s)

0 commit comments

Comments
 (0)