Skip to content

Commit c763b6b

Browse files
authored
EZSP v14 (#631)
* WIP * Fix `exportLinkKeyByIndex` * Fix `setExtendedTimeout` * Fix `sendUnicast` and `sendBroadcast * Update `sl_Status` enum and remove all use of `EmberStatus` * Compatibility with EZSP v9 * Compatibility with EZSP v4 * Handle `EmberStatus.ERR_FATAL` * Handle `EmberStatus.MAC_INDIRECT_TIMEOUT` * Add a few more network status codes * Fix `launchStandaloneBootloader` * Log `Unknown status` warning one frame earlier * Fix unit tests * Fix stack status unit tests * Migrate version-specific logic into `EZSP` subclasses * Move network and TCLK key reading as well * Move NWK and APS frame counter writing * Move child table writing and APS link key writing * Move network initialization * Move version-specific unit tests into EZSP test files * Mark abstract methods * Annotations for old Python * More annotations * Last one :) * Rename `write_child_table` to `write_child_data` * WIP: tests * Finish unit tests for EZSP protocol handlers * Drop `async_mock` * Move `tokenFactoryReset` to EZSPv13, it's not in v8 * Reorganize incoming frame handling to make mapping more explicit * Abstract away `send_unicast`, `send_multicast`, and `send_broadcast` * Oops, forgot to commit `test_ezsp_v14.py` * Fix application unit tests * Test `load_network_info` * Test `write_network_info`
1 parent 3657cf3 commit c763b6b

40 files changed

+2585
-4328
lines changed

bellows/cli/backup.py

+9-9
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ async def backup(ctx):
8585
async def _backup(ezsp):
8686
(status,) = await ezsp.networkInit()
8787
LOGGER.debug("Network init status: %s", status)
88-
assert status == t.EmberStatus.SUCCESS
88+
assert t.sl_Status.from_ember_status(status) == t.sl_Status.OK
8989

9090
(status, node_type, network) = await ezsp.getNetworkParameters()
91-
assert status == t.EmberStatus.SUCCESS
91+
assert t.sl_Status.from_ember_status(status) == t.sl_Status.OK
9292
assert node_type == ezsp.types.EmberNodeType.COORDINATOR
9393
LOGGER.debug("Network params: %s", network)
9494

@@ -112,7 +112,7 @@ async def _backup(ezsp):
112112
(ATTR_KEY_NWK, ezsp.types.EmberKeyType.CURRENT_NETWORK_KEY),
113113
):
114114
(status, key) = await ezsp.getKey(key_type)
115-
assert status == t.EmberStatus.SUCCESS
115+
assert t.sl_Status.from_ember_status(status) == t.sl_Status.OK
116116
LOGGER.debug("%s key: %s", key_name, key)
117117
result[key_name] = key.as_dict()
118118
result[key_name][ATTR_KEY_PARTNER] = str(key.partnerEUI64)
@@ -248,7 +248,7 @@ async def _restore(
248248

249249
(status,) = await ezsp.setInitialSecurityState(init_sec_state)
250250
LOGGER.debug("Set initial security state: %s", status)
251-
assert status == t.EmberStatus.SUCCESS
251+
assert t.sl_Status.from_ember_status(status) == t.sl_Status.OK
252252

253253
if backup_data[ATTR_KEY_TABLE]:
254254
await _restore_keys(ezsp, backup_data[ATTR_KEY_TABLE])
@@ -259,15 +259,15 @@ async def _restore(
259259
t.uint32_t(network_key[ATTR_KEY_FRAME_COUNTER_OUT]).serialize(),
260260
)
261261
LOGGER.debug("Set network frame counter: %s", status)
262-
assert status == t.EmberStatus.SUCCESS
262+
assert t.sl_Status.from_ember_status(status) == t.sl_Status.OK
263263

264264
tc_key = backup_data[ATTR_KEY_GLOBAL]
265265
(status,) = await ezsp.setValue(
266266
ezsp.types.EzspValueId.VALUE_APS_FRAME_COUNTER,
267267
t.uint32_t(tc_key[ATTR_KEY_FRAME_COUNTER_OUT]).serialize(),
268268
)
269269
LOGGER.debug("Set network frame counter: %s", status)
270-
assert status == t.EmberStatus.SUCCESS
270+
assert t.sl_Status.from_ember_status(status) == t.sl_Status.OK
271271

272272
await _form_network(ezsp, backup_data)
273273
await asyncio.sleep(2)
@@ -279,7 +279,7 @@ async def _restore_keys(ezsp, key_table):
279279
(status,) = await ezsp.setConfigurationValue(
280280
ezsp.types.EzspConfigId.CONFIG_KEY_TABLE_SIZE, len(key_table)
281281
)
282-
assert status == t.EmberStatus.SUCCESS
282+
assert t.sl_Status.from_ember_status(status) == t.sl_Status.OK
283283

284284
for key in key_table:
285285
is_link_key = key[ATTR_KEY_TYPE] in (
@@ -312,7 +312,7 @@ async def _form_network(ezsp, backup_data):
312312

313313
(status,) = await ezsp.setValue(ezsp.types.EzspValueId.VALUE_STACK_TOKEN_WRITING, 1)
314314
LOGGER.debug("Set token writing: %s", status)
315-
assert status == t.EmberStatus.SUCCESS
315+
assert t.sl_Status.from_ember_status(status) == t.sl_Status.OK
316316

317317

318318
async def _update_nwk_id(ezsp, nwk_update_id):
@@ -338,7 +338,7 @@ async def _update_nwk_id(ezsp, nwk_update_id):
338338
0x01,
339339
payload,
340340
)
341-
assert status == t.EmberStatus.SUCCESS
341+
assert t.sl_Status.from_ember_status(status) == t.sl_Status.OK
342342
await asyncio.sleep(1)
343343

344344

bellows/ezsp/__init__.py

+22-21
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727
import bellows.types as t
2828
import bellows.uart
2929

30-
from . import v4, v5, v6, v7, v8, v9, v10, v11, v12, v13
30+
from . import v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14
3131

32-
EZSP_LATEST = v13.EZSPv13.VERSION
32+
EZSP_LATEST = v14.EZSPv14.VERSION
3333
LOGGER = logging.getLogger(__name__)
3434
MTOR_MIN_INTERVAL = 60
3535
MTOR_MAX_INTERVAL = 3600
@@ -56,6 +56,7 @@ class EZSP:
5656
v11.EZSPv11.VERSION: v11.EZSPv11,
5757
v12.EZSPv12.VERSION: v12.EZSPv12,
5858
v13.EZSPv13.VERSION: v13.EZSPv13,
59+
v14.EZSPv14.VERSION: v14.EZSPv14,
5960
}
6061

6162
def __init__(self, device_config: dict):
@@ -68,7 +69,7 @@ def __init__(self, device_config: dict):
6869
self._send_sem = PriorityDynamicBoundedSemaphore(value=MAX_COMMAND_CONCURRENCY)
6970

7071
self._stack_status_listeners: collections.defaultdict[
71-
t.EmberStatus, list[asyncio.Future]
72+
t.sl_Status, list[asyncio.Future]
7273
] = collections.defaultdict(list)
7374

7475
self.add_callback(self.stack_status_callback)
@@ -78,13 +79,13 @@ def stack_status_callback(self, frame_name: str, args: list[Any]) -> None:
7879
if frame_name != "stackStatusHandler":
7980
return
8081

81-
status = args[0]
82+
status = t.sl_Status.from_ember_status(args[0])
8283

8384
for listener in self._stack_status_listeners[status]:
8485
listener.set_result(status)
8586

8687
@contextlib.contextmanager
87-
def wait_for_stack_status(self, status: t.EmberStatus) -> Generator[asyncio.Future]:
88+
def wait_for_stack_status(self, status: t.sl_Status) -> Generator[asyncio.Future]:
8889
"""Waits for a `stackStatusHandler` to come in with the provided status."""
8990
listeners = self._stack_status_listeners[status]
9091

@@ -228,10 +229,10 @@ def cb(frame_name, response):
228229
cbid = self.add_callback(cb)
229230
try:
230231
v = await self._command(name, *args)
231-
if v[0] != t.EmberStatus.SUCCESS:
232+
if t.sl_Status.from_ember_status(v[0]) != t.sl_Status.OK:
232233
raise Exception(v)
233234
v = await fut
234-
if v[spos] != t.EmberStatus.SUCCESS:
235+
if t.sl_Status.from_ember_status(v[spos]) != t.sl_Status.OK:
235236
raise Exception(v)
236237
finally:
237238
self.remove_callback(cbid)
@@ -267,9 +268,9 @@ async def leaveNetwork(self, timeout: float | int = NETWORK_OPS_TIMEOUT) -> None
267268
"""Send leaveNetwork command and wait for stackStatusHandler frame."""
268269
stack_status = asyncio.Future()
269270

270-
with self.wait_for_stack_status(t.EmberStatus.NETWORK_DOWN) as stack_status:
271+
with self.wait_for_stack_status(t.sl_Status.NETWORK_DOWN) as stack_status:
271272
(status,) = await self._command("leaveNetwork")
272-
if status != t.EmberStatus.SUCCESS:
273+
if status != t.sl_Status.OK:
273274
raise EzspError(f"failed to leave network: {status.name}")
274275

275276
async with asyncio_timeout(timeout):
@@ -302,10 +303,10 @@ def __getattr__(self, name: str) -> Callable:
302303
return functools.partial(self._command, name)
303304

304305
async def formNetwork(self, parameters: t.EmberNetworkParameters) -> None:
305-
with self.wait_for_stack_status(t.EmberStatus.NETWORK_UP) as stack_status:
306+
with self.wait_for_stack_status(t.sl_Status.NETWORK_UP) as stack_status:
306307
v = await self._command("formNetwork", parameters)
307308

308-
if v[0] != self.types.EmberStatus.SUCCESS:
309+
if t.sl_Status.from_ember_status(v[0]) != t.sl_Status.OK:
309310
raise zigpy.exceptions.FormationFailure(f"Failure forming network: {v}")
310311

311312
async with asyncio_timeout(NETWORK_OPS_TIMEOUT):
@@ -361,7 +362,7 @@ async def get_board_info(
361362
)
362363
version = None
363364

364-
if status == t.EmberStatus.SUCCESS:
365+
if t.sl_Status.from_ember_status(status) == t.sl_Status.OK:
365366
build, ver_info_bytes = t.uint16_t.deserialize(ver_info_bytes)
366367
major, ver_info_bytes = t.uint8_t.deserialize(ver_info_bytes)
367368
minor, ver_info_bytes = t.uint8_t.deserialize(ver_info_bytes)
@@ -388,7 +389,7 @@ async def _get_nv3_restored_eui64_key(self) -> t.NV3KeyId | None:
388389
# is not implemented in the firmware
389390
return None
390391

391-
if rsp.status == t.EmberStatus.SUCCESS:
392+
if t.sl_Status.from_ember_status(rsp.status) == t.sl_Status.OK:
392393
nv3_restored_eui64, _ = t.EUI64.deserialize(rsp.value)
393394
LOGGER.debug("NV3 restored EUI64: %s=%s", key, nv3_restored_eui64)
394395

@@ -434,7 +435,7 @@ async def reset_custom_eui64(self) -> None:
434435
0,
435436
t.LVBytes32(t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF").serialize()),
436437
)
437-
assert status == t.EmberStatus.SUCCESS
438+
assert t.sl_Status.from_ember_status(status) == t.sl_Status.OK
438439

439440
async def write_custom_eui64(
440441
self, ieee: t.EUI64, *, burn_into_userdata: bool = False
@@ -460,12 +461,12 @@ async def write_custom_eui64(
460461
0,
461462
t.LVBytes32(ieee.serialize()),
462463
)
463-
assert status == t.EmberStatus.SUCCESS
464+
assert t.sl_Status.from_ember_status(status) == t.sl_Status.OK
464465
elif mfg_custom_eui64 is None and burn_into_userdata:
465466
(status,) = await self.setMfgToken(
466467
t.EzspMfgTokenId.MFG_CUSTOM_EUI_64, ieee.serialize()
467468
)
468-
assert status == t.EmberStatus.SUCCESS
469+
assert t.sl_Status.from_ember_status(status) == t.sl_Status.OK
469470
elif mfg_custom_eui64 is None and not burn_into_userdata:
470471
raise EzspError(
471472
f"Firmware does not support NV3 tokens. Custom IEEE {ieee} will not be"
@@ -507,7 +508,7 @@ async def set_source_routing(self) -> None:
507508
0,
508509
)
509510
LOGGER.debug("Set concentrator type: %s", res)
510-
if res[0] != self.types.EmberStatus.SUCCESS:
511+
if t.sl_Status.from_ember_status(res[0]) != t.sl_Status.OK:
511512
LOGGER.warning("Couldn't set concentrator type %s: %s", True, res)
512513

513514
if self._ezsp_version >= 8:
@@ -579,7 +580,7 @@ async def write_config(self, config: dict) -> None:
579580
# XXX: A read failure does not mean the value is not writeable!
580581
status, current_value = await self.getValue(cfg.value_id)
581582

582-
if status == self.types.EmberStatus.SUCCESS:
583+
if t.sl_Status.from_ember_status(status) == t.sl_Status.OK:
583584
current_value, _ = type(cfg.value).deserialize(current_value)
584585
else:
585586
current_value = None
@@ -593,7 +594,7 @@ async def write_config(self, config: dict) -> None:
593594

594595
(status,) = await self.setValue(cfg.value_id, cfg.value.serialize())
595596

596-
if status != self.types.EmberStatus.SUCCESS:
597+
if t.sl_Status.from_ember_status(status) != t.sl_Status.OK:
597598
LOGGER.debug(
598599
"Could not set value %s = %s: %s",
599600
cfg.value_id.name,
@@ -608,7 +609,7 @@ async def write_config(self, config: dict) -> None:
608609

609610
# Only grow some config entries, all others should be set
610611
if (
611-
status == self.types.EmberStatus.SUCCESS
612+
t.sl_Status.from_ember_status(status) == t.sl_Status.OK
612613
and cfg.minimum
613614
and current_value >= cfg.value
614615
):
@@ -628,7 +629,7 @@ async def write_config(self, config: dict) -> None:
628629
)
629630

630631
(status,) = await self.setConfigurationValue(cfg.config_id, cfg.value)
631-
if status != self.types.EmberStatus.SUCCESS:
632+
if t.sl_Status.from_ember_status(status) != t.sl_Status.OK:
632633
LOGGER.debug(
633634
"Could not set config %s = %s: %s",
634635
cfg.config_id,

bellows/ezsp/config.py

+1
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,5 @@ class ValueConfig:
131131
11: DEFAULT_CONFIG_NEW,
132132
12: DEFAULT_CONFIG_NEW,
133133
13: DEFAULT_CONFIG_NEW,
134+
14: DEFAULT_CONFIG_NEW,
134135
}

bellows/ezsp/protocol.py

+93-10
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import functools
77
import logging
88
import sys
9-
from typing import TYPE_CHECKING, Any, Callable
9+
from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable, Iterable
10+
11+
import zigpy.state
1012

1113
if sys.version_info[:2] < (3, 11):
1214
from async_timeout import timeout as asyncio_timeout # pragma: no cover
@@ -56,14 +58,6 @@ def _ezsp_frame_rx(self, data: bytes) -> tuple[int, int, bytes]:
5658
def _ezsp_frame_tx(self, name: str) -> bytes:
5759
"""Serialize the named frame."""
5860

59-
async def pre_permit(self, time_s: int) -> None:
60-
"""Schedule task before allowing new joins."""
61-
62-
async def add_transient_link_key(
63-
self, ieee: t.EUI64, key: t.KeyData
64-
) -> t.EmberStatus:
65-
"""Add a transient link key."""
66-
6761
async def command(self, name, *args) -> Any:
6862
"""Serialize command and send it."""
6963
LOGGER.debug("Sending command %s: %s", name, args)
@@ -85,7 +79,9 @@ async def update_policies(self, policy_config: dict) -> None:
8579

8680
for policy, value in policies.items():
8781
(status,) = await self.setPolicy(self.types.EzspPolicyId[policy], value)
88-
assert status == self.types.EmberStatus.SUCCESS # TODO: Better check
82+
assert (
83+
t.sl_Status.from_ember_status(status) == t.sl_Status.OK
84+
) # TODO: Better check
8985

9086
def __call__(self, data: bytes) -> None:
9187
"""Handler for received data frame."""
@@ -148,3 +144,90 @@ def __getattr__(self, name: str) -> Callable:
148144
raise AttributeError(f"{name} not found in COMMANDS")
149145

150146
return functools.partial(self.command, name)
147+
148+
async def pre_permit(self, time_s: int) -> None:
149+
"""Schedule task before allowing new joins."""
150+
151+
async def add_transient_link_key(
152+
self, ieee: t.EUI64, key: t.KeyData
153+
) -> t.sl_Status:
154+
"""Add a transient link key."""
155+
156+
@abc.abstractmethod
157+
async def read_child_data(
158+
self,
159+
) -> AsyncGenerator[tuple[t.NWK, t.EUI64, t.EmberNodeType], None]:
160+
raise NotImplementedError
161+
162+
@abc.abstractmethod
163+
async def read_link_keys(self) -> AsyncGenerator[zigpy.state.Key, None]:
164+
raise NotImplementedError
165+
166+
@abc.abstractmethod
167+
async def read_address_table(self) -> AsyncGenerator[tuple[t.NWK, t.EUI64], None]:
168+
raise NotImplementedError
169+
170+
@abc.abstractmethod
171+
async def get_network_key(self) -> zigpy.state.Key:
172+
raise NotImplementedError
173+
174+
@abc.abstractmethod
175+
async def get_tc_link_key(self) -> zigpy.state.Key:
176+
raise NotImplementedError
177+
178+
@abc.abstractmethod
179+
async def write_nwk_frame_counter(self, frame_counter: t.uint32_t) -> None:
180+
raise NotImplementedError
181+
182+
@abc.abstractmethod
183+
async def write_aps_frame_counter(self, frame_counter: t.uint32_t) -> None:
184+
raise NotImplementedError
185+
186+
@abc.abstractmethod
187+
async def write_link_keys(self, keys: Iterable[zigpy.state.Key]) -> None:
188+
raise NotImplementedError
189+
190+
@abc.abstractmethod
191+
async def write_child_data(self, children: dict[t.EUI64, t.NWK]) -> None:
192+
raise NotImplementedError
193+
194+
@abc.abstractmethod
195+
async def initialize_network(self) -> t.sl_Status:
196+
raise NotImplementedError
197+
198+
@abc.abstractmethod
199+
async def factory_reset(self) -> None:
200+
raise NotImplementedError
201+
202+
@abc.abstractmethod
203+
async def send_unicast(
204+
self,
205+
nwk: t.NWK,
206+
aps_frame: t.EmberApsFrame,
207+
message_tag: t.uint8_t,
208+
data: bytes,
209+
) -> tuple[t.sl_Status, t.uint8_t]:
210+
raise NotImplementedError
211+
212+
@abc.abstractmethod
213+
async def send_multicast(
214+
self,
215+
aps_frame: t.EmberApsFrame,
216+
radius: t.uint8_t,
217+
non_member_radius: t.uint8_t,
218+
message_tag: t.uint8_t,
219+
data: bytes,
220+
) -> tuple[t.sl_Status, t.uint8_t]:
221+
raise NotImplementedError
222+
223+
@abc.abstractmethod
224+
async def send_broadcast(
225+
self,
226+
address: t.BroadcastAddress,
227+
aps_frame: t.EmberApsFrame,
228+
radius: t.uint8_t,
229+
message_tag: t.uint8_t,
230+
aps_sequence: t.uint8_t,
231+
data: bytes,
232+
) -> tuple[t.sl_Status, t.uint8_t]:
233+
raise NotImplementedError

0 commit comments

Comments
 (0)