Skip to content

Commit 53d516c

Browse files
authored
Joining with link key (#150)
* joining with install code support * add test * fix lint * separate permit_with_key and permit_with_link_key methods
1 parent 098b19c commit 53d516c

File tree

4 files changed

+91
-3
lines changed

4 files changed

+91
-3
lines changed

tests/test_api.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,26 @@ def test_handle_tx_status_duplicate(api):
416416
assert send_fut.set_exception.call_count == 0
417417

418418

419+
def test_handle_registration_status(api):
420+
frame_id = 0x12
421+
status = xbee_api.RegistrationStatus.SUCCESS
422+
fut = asyncio.Future()
423+
api._awaiting[frame_id] = (fut,)
424+
api._handle_registration_status(frame_id, status)
425+
assert fut.done() is True
426+
assert fut.result() == xbee_api.RegistrationStatus.SUCCESS
427+
assert fut.exception() is None
428+
429+
frame_id = 0x13
430+
status = xbee_api.RegistrationStatus.KEY_TABLE_IS_FULL
431+
fut = asyncio.Future()
432+
api._awaiting[frame_id] = (fut,)
433+
api._handle_registration_status(frame_id, status)
434+
assert fut.done() is True
435+
with pytest.raises(RuntimeError, match="Registration Status: KEY_TABLE_IS_FULL"):
436+
fut.result()
437+
438+
419439
async def test_command_mode_at_cmd(api):
420440
command = "+++"
421441

tests/test_application.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,32 @@ async def test_permit(app):
413413
assert app._api._at_command.call_args_list[0][0][1] == time_s
414414

415415

416+
async def test_permit_with_key(app):
417+
app._api._command = mock.AsyncMock(return_value=xbee_t.TXStatus.SUCCESS)
418+
app._api._at_command = mock.AsyncMock(return_value="OK")
419+
node = t.EUI64(b"\x01\x02\x03\x04\x05\x06\x07\x08")
420+
code = b"\xC9\xA7\xD2\x44\x1A\x71\x16\x95\xCD\x62\x17\x0D\x33\x28\xEA\x2B\x42\x3D"
421+
time_s = 500
422+
await app.permit_with_key(node=node, code=code, time_s=time_s)
423+
app._api._at_command.assert_called_once_with("KT", time_s)
424+
app._api._command.assert_called_once_with(
425+
"register_joining_device", node, 0xFFFE, 1, code
426+
)
427+
428+
429+
async def test_permit_with_link_key(app):
430+
app._api._command = mock.AsyncMock(return_value=xbee_t.TXStatus.SUCCESS)
431+
app._api._at_command = mock.AsyncMock(return_value="OK")
432+
node = t.EUI64(b"\x01\x02\x03\x04\x05\x06\x07\x08")
433+
link_key = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"
434+
time_s = 500
435+
await app.permit_with_link_key(node=node, link_key=link_key, time_s=time_s)
436+
app._api._at_command.assert_called_once_with("KT", time_s)
437+
app._api._command.assert_called_once_with(
438+
"register_joining_device", node, 0xFFFE, 0, link_key
439+
)
440+
441+
416442
async def _test_request(
417443
app, expect_reply=True, send_success=True, send_timeout=False, **kwargs
418444
):

zigpy_xbee/api.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,20 @@ class ModemStatus(t.uint8_t, t.UndefinedEnum):
8989
_UNDEFINED = 0xFF
9090

9191

92+
class RegistrationStatus(t.uint8_t, t.UndefinedEnum):
93+
SUCCESS = 0x00
94+
KEY_TOO_LONG = 0x01
95+
TRANSIENT_KEY_TABLE_IS_FULL = 0x18
96+
ADDRESS_NOT_FOUND_IN_THE_KEY_TABLE = 0xB1
97+
KEY_IS_INVALID_OR_RESERVED = 0xB2
98+
INVALID_ADDRESS = 0xB3
99+
KEY_TABLE_IS_FULL = 0xB4
100+
SECURITY_DATA_IS_INVALID_INSTALL_CODE_CRC_FAILS = 0xBD
101+
102+
UNKNOWN_MODEM_STATUS = 0xFF
103+
_UNDEFINED = 0xFF
104+
105+
92106
# https://www.digi.com/resources/documentation/digidocs/PDFs/90000976.pdf
93107
COMMAND_REQUESTS = {
94108
"at": (0x08, (t.FrameId, t.ATCommand, t.Bytes), 0x88),
@@ -120,7 +134,11 @@ class ModemStatus(t.uint8_t, t.UndefinedEnum):
120134
(t.FrameId, t.EUI64, t.NWK, t.uint8_t, t.Relays),
121135
None,
122136
),
123-
"register_joining_device": (0x24, (), None),
137+
"register_joining_device": (
138+
0x24,
139+
(t.FrameId, t.EUI64, t.uint16_t, t.uint8_t, t.Bytes),
140+
0xA4,
141+
),
124142
}
125143
COMMAND_RESPONSES = {
126144
"at_response": (0x88, (t.FrameId, t.ATCommand, t.uint8_t, t.Bytes), None),
@@ -155,6 +173,7 @@ class ModemStatus(t.uint8_t, t.UndefinedEnum):
155173
"extended_status": (0x98, (), None),
156174
"route_record_indicator": (0xA1, (t.EUI64, t.NWK, t.uint8_t, t.Relays), None),
157175
"many_to_one_rri": (0xA3, (t.EUI64, t.NWK, t.uint8_t), None),
176+
"registration_status": (0xA4, (t.FrameId, RegistrationStatus), None),
158177
"node_id_indicator": (0x95, (), None),
159178
}
160179

@@ -201,6 +220,7 @@ class ModemStatus(t.uint8_t, t.UndefinedEnum):
201220
"EO": t.uint8_t,
202221
"NK": t.Bytes, # 128-bit value
203222
"KY": t.Bytes, # 128-bit value
223+
"KT": t.uint16_t, # 0x1E - 0xFFFF
204224
# RF interfacing commands
205225
"PL": t.uint8_t, # 0 - 4 (basically an Enum)
206226
"PM": t.Bool,
@@ -549,6 +569,15 @@ def _handle_tx_status(self, frame_id, nwk, tries, tx_status, dsc_status):
549569
except asyncio.InvalidStateError as ex:
550570
LOGGER.debug("duplicate tx_status for %s nwk? State: %s", nwk, ex)
551571

572+
def _handle_registration_status(self, frame_id, status):
573+
(fut,) = self._awaiting.pop(frame_id)
574+
if status:
575+
fut.set_exception(RuntimeError(f"Registration Status: {status.name}"))
576+
return
577+
LOGGER.debug(f"Registration Status: {status.name}")
578+
579+
fut.set_result(status)
580+
552581
def set_application(self, app):
553582
self._app = app
554583

zigpy_xbee/zigbee/application.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,21 @@ async def permit_ncp(self, time_s=60):
319319
await self._api._at_command("NJ", time_s)
320320
await self._api._at_command("AC")
321321

322-
async def permit_with_key(self, node, code, time_s=60):
323-
raise NotImplementedError("XBee does not support install codes")
322+
async def permit_with_link_key(
323+
self, node: EUI64, link_key: zigpy.types.KeyData, time_s: int = 500, key_type=0
324+
):
325+
"""Permits a new device to join with the given IEEE and link key."""
326+
assert 0x1E <= time_s <= 0xFFFF
327+
await self._api._at_command("KT", time_s)
328+
reserved = 0xFFFE
329+
# Key type:
330+
# 0 = Pre-configured Link Key (KY command of the joining device)
331+
# 1 = Install Code With CRC (I? command of the joining device)
332+
await self._api.register_joining_device(node, reserved, key_type, link_key)
333+
334+
async def permit_with_key(self, node: EUI64, code: bytes, time_s=500):
335+
"""Permits a new device to join with the given IEEE and Install Code."""
336+
await self.permit_with_link_key(node, code, time_s, key_type=1)
324337

325338
def handle_modem_status(self, status):
326339
LOGGER.info("Modem status update: %s (%s)", status.name, status.value)

0 commit comments

Comments
 (0)