From b993b2f1e59f660271cba448a9ff85c473570708 Mon Sep 17 00:00:00 2001 From: zigpy-review-bot <286747149+zigpy-review-bot@users.noreply.github.com> Date: Sun, 13 Sep 2026 18:52:52 +0200 Subject: [PATCH] Fix stale EZSP v14+ command and callback schemas The v14 command table copies every v13 command it doesn't override and only swaps `EmberStatus`/`EzspStatus` for `sl_Status`. Commands whose layout changed in any other way at v14 (or later) kept their old schemas, and v16-v19 inherit them. None of these are called by bellows, so nothing broke visibly, but they would misparse if used, and callbacks are parsed whenever the NCP sends them. Checked against the Silicon Labs EZSP host encoders/decoders (GSDK 4.3.0 and 4.4.6, SiSDK 2024.6.0, 2024.12.0, 2025.6.0, 2025.6.2, 2025.12.3, 2026.6.x) and the NCP handlers where available: - v14: status moved first or a context was added in the key export/import responses (`exportLinkKeyByEui`, `exportTransientKeyByIndex`/`ByEui`, `getApsKeyInfo`, `importKey`), `findAndRejoinNetwork` gained `reason` and `nodeType`, and `setAddressTableRemoteEui64` became `setAddressTableInfo`. - v14: `setPowerDescriptor` and `clearStoredBeacons` gained an `sl_Status` response, while `sendPanIdUpdate`, `readAttribute`, and `writeAttribute` keep their one-byte response that the global replacement had widened. - v14: callbacks that replaced `lastHopLqi`/`lastHopRssi` with `sl_zigbee_rx_packet_info_t`, and `rawTransmitCompleteHandler`, which now carries the message. - v14: drop commands the SDK removed. Several of their frame IDs were reused (0x0004, 0x0037, 0x003A right away; 0x003D, 0x005D, 0x005F, 0x0062 in later versions, which collided with commands bellows added for v17 and v18). - v18: `getTokenCount` returns a `uint32_t`, and `macFilterMatchMessageHandler` also carries `sl_zigbee_rx_packet_info_t`. - v19: `getTokenInfo`'s token size is a `uint32_t` (widened in SiSDK 2025.12 without an EZSP version bump, so v18 is left as-is). - v12/v13: `checkKeyContext` was defined with `setPassiveAckConfig`'s arguments; it takes a security manager context and returns `sl_Status`. --- bellows/ezsp/v12/commands.py | 5 +- bellows/ezsp/v13/commands.py | 9 + bellows/ezsp/v14/commands.py | 214 ++++++++++++++++++++++++ bellows/ezsp/v18/commands.py | 13 +- bellows/ezsp/v19/commands.py | 11 ++ bellows/types/struct.py | 14 ++ tests/test_ezsp_v12.py | 23 +++ tests/test_ezsp_v13.py | 22 +++ tests/test_ezsp_v14.py | 311 +++++++++++++++++++++++++++++++++++ tests/test_ezsp_v18.py | 32 ++++ tests/test_ezsp_v19.py | 19 +++ 11 files changed, 668 insertions(+), 5 deletions(-) diff --git a/bellows/ezsp/v12/commands.py b/bellows/ezsp/v12/commands.py index 8ddbf5fb2..2512f4a72 100644 --- a/bellows/ezsp/v12/commands.py +++ b/bellows/ezsp/v12/commands.py @@ -50,11 +50,10 @@ "checkKeyContext": ( 0x0110, { - "config": t.uint8_t, - "minAcksNeeded": t.uint8_t, + "context": t.SecurityManagerContextV12, }, { - "status": t.EmberStatus, + "status": t.sl_Status, }, ), "childId": ( diff --git a/bellows/ezsp/v13/commands.py b/bellows/ezsp/v13/commands.py index f87664aec..06c522cc9 100644 --- a/bellows/ezsp/v13/commands.py +++ b/bellows/ezsp/v13/commands.py @@ -35,6 +35,15 @@ }, ), # The following commands are redefined because `SecurityManagerContext` changed + "checkKeyContext": ( + 0x0110, + { + "context": t.SecurityManagerContextV13, + }, + { + "status": t.sl_Status, + }, + ), "exportKey": ( 0x0114, { diff --git a/bellows/ezsp/v14/commands.py b/bellows/ezsp/v14/commands.py index 0994e3411..cacdc4a84 100644 --- a/bellows/ezsp/v14/commands.py +++ b/bellows/ezsp/v14/commands.py @@ -66,6 +66,201 @@ class GetTokenDataRsp(Struct): "context": t.SecurityManagerContextV13, }, ), + "exportLinkKeyByEui": ( + 0x010D, + { + "eui64": t.EUI64, + }, + { + "status": t.sl_Status, + "context": t.SecurityManagerContextV13, + "plaintext_key": t.KeyData, + "key_data": t.SecurityManagerAPSKeyMetadata, + }, + ), + "exportTransientKeyByIndex": ( + 0x0112, + { + "index": t.uint8_t, + }, + { + "status": t.sl_Status, + "context": t.SecurityManagerContextV13, + "plaintext_key": t.KeyData, + "key_data": t.SecurityManagerAPSKeyMetadata, + }, + ), + "exportTransientKeyByEui": ( + 0x0113, + { + "eui64": t.EUI64, + }, + { + "status": t.sl_Status, + "context": t.SecurityManagerContextV13, + "plaintext_key": t.KeyData, + "key_data": t.SecurityManagerAPSKeyMetadata, + }, + ), + "getApsKeyInfo": ( + 0x010C, + { + "context_in": t.SecurityManagerContextV13, + }, + { + "status": t.sl_Status, + "key_data": t.SecurityManagerAPSKeyMetadata, + "context": t.SecurityManagerContextV13, + }, + ), + "importKey": ( + 0x0115, + { + "context": t.SecurityManagerContextV13, + "key": t.KeyData, + }, + { + "status": t.sl_Status, + "context": t.SecurityManagerContextV13, + }, + ), + "findAndRejoinNetwork": ( + 0x0021, + { + "haveCurrentNetworkKey": t.Bool, + "channelMask": t.uint32_t, + "reason": t.uint8_t, + "nodeType": t.EmberNodeType, + }, + { + "status": t.sl_Status, + }, + ), + # Replaces `setAddressTableRemoteEui64` and `setAddressTableRemoteNodeId` + "setAddressTableInfo": ( + 0x005C, + { + "index": t.uint8_t, + "eui64": EUI64, + "nwk": NWK, + }, + { + "status": t.sl_Status, + }, + ), + "setPowerDescriptor": ( + 0x0016, + { + "descriptor": t.uint16_t, + }, + { + "status": t.sl_Status, + }, + ), + "clearStoredBeacons": ( + 0x003C, + {}, + { + "status": t.sl_Status, + }, + ), + # These three still respond with a single byte, not an `sl_Status` + "sendPanIdUpdate": ( + 0x0057, + { + "newPan": t.EmberPanId, + }, + { + "status": t.Bool, + }, + ), + "readAttribute": ( + 0x0108, + { + "endpoint": t.uint8_t, + "cluster": t.uint16_t, + "attributeId": t.uint16_t, + "mask": t.uint8_t, + "manufacturerCode": t.uint16_t, + }, + { + "status": t.EmberStatus, + "dataType": t.uint8_t, + "data": t.LVBytes, + }, + ), + "writeAttribute": ( + 0x0109, + { + "endpoint": t.uint8_t, + "cluster": t.uint16_t, + "attributeId": t.uint16_t, + "mask": t.uint8_t, + "manufacturerCode": t.uint16_t, + "overrideReadOnlyAndDataType": t.Bool, + "justTest": t.Bool, + "dataType": t.uint8_t, + "data": t.LVBytes, + }, + { + "status": t.EmberStatus, + }, + ), + # `lastHopLqi` and `lastHopRssi` were replaced with `packetInfo` + "macPassthroughMessageHandler": ( + 0x0097, + {}, + { + "messageType": t.EmberMacPassthroughType, + "packetInfo": t.SlRxPacketInfo, + "messageContents": t.LVBytes, + }, + ), + "macFilterMatchMessageHandler": ( + 0x0046, + {}, + { + "filterIndexMatch": t.uint8_t, + "legacyPassthroughType": t.EmberMacPassthroughType, + "packetInfo": t.SlRxPacketInfo, + "messageContents": t.LVBytes, + }, + ), + "incomingBootloadMessageHandler": ( + 0x0092, + {}, + { + "longId": t.EUI64, + "packetInfo": t.SlRxPacketInfo, + "messageContents": t.LVBytes, + }, + ), + "zllNetworkFoundHandler": ( + 0x00B6, + {}, + { + "networkInfo": t.EmberZllNetwork, + "isDeviceInfoNull": t.Bool, + "deviceInfo": t.EmberZllDeviceInfoRecord, + "packetInfo": t.SlRxPacketInfo, + }, + ), + "zllAddressAssignmentHandler": ( + 0x00B8, + {}, + { + "addressInfo": t.EmberZllAddressAssignment, + "packetInfo": t.SlRxPacketInfo, + }, + ), + "rawTransmitCompleteHandler": ( + 0x0098, + {}, + { + "messageContents": t.LVBytes, + "status": t.sl_Status, + }, + ), "getAddressTableInfo": ( 0x005E, { @@ -177,3 +372,22 @@ class GetTokenDataRsp(Struct): COMMANDS[name] = (command_id, tx_schema, rx_schema) del COMMANDS["getAddressTableRemoteEui64"] +del COMMANDS["setAddressTableRemoteEui64"] + +# Removed in v14. Their frame IDs were reused, either right away or in later versions +del COMMANDS["getNextBeacon"] # 0x0004 is now `getStoredBeacon` +del COMMANDS["proxyBroadcast"] # 0x0037 is now `setNumBeaconsToStore` +del COMMANDS["sendMulticastWithAlias"] # 0x003A is now `setupDelayedJoin` +del COMMANDS["getFirstBeacon"] # 0x003D +del COMMANDS["setAddressTableRemoteNodeId"] # 0x005D +del COMMANDS["getAddressTableRemoteNodeId"] # 0x005F +del COMMANDS["incomingSenderEui64Handler"] # 0x0062 + +# Removed in v14 +del COMMANDS["sendRawMessage"] # Now the name of 0x0051, `sendRawMessageExtended` +del COMMANDS["setLongUpTime"] +del COMMANDS["setHubConnectivity"] +del COMMANDS["isUpTimeLong"] +del COMMANDS["isHubConnected"] +del COMMANDS["setParentClassificationEnabled"] +del COMMANDS["getParentClassificationEnabled"] diff --git a/bellows/ezsp/v18/commands.py b/bellows/ezsp/v18/commands.py index 63812bd88..52ef6cd39 100644 --- a/bellows/ezsp/v18/commands.py +++ b/bellows/ezsp/v18/commands.py @@ -10,11 +10,20 @@ { "filterValueMatch": t.uint16_t, # Was `filterIndexMatch: uint8_t` "legacyPassthroughType": t.EmberMacPassthroughType, - "lastHopLqi": t.uint8_t, - "lastHopRssi": t.int8s, + "packetInfo": t.SlRxPacketInfo, "messageContents": t.LVBytes, }, ), + "getTokenCount": ( + 0x0100, + {}, + { + "count": t.uint32_t, # Was `uint8_t` + }, + ), + # NOTE: `getTokenInfo` (0x0101) is left as-is. Simplicity SDK 2025.12 widened the + # `size` field of its token info to `uint32_t` without an EZSP version bump, so + # firmware built from SiSDK 2025.6.x (also EZSP v18) still sends a single byte. # Added in Simplicity SDK 2025.12.0 "setPendingNetworkUpdateChannel": ( 0x003D, diff --git a/bellows/ezsp/v19/commands.py b/bellows/ezsp/v19/commands.py index 739c610bb..4ede5e53a 100644 --- a/bellows/ezsp/v19/commands.py +++ b/bellows/ezsp/v19/commands.py @@ -12,4 +12,15 @@ }, {}, ), + # The token info `size` field is a `uint32_t` since Simplicity SDK 2025.12 + "getTokenInfo": ( + 0x0101, + { + "index": t.uint8_t, + }, + { + "status": t.sl_Status, + "token_info": t.SlZigbeeTokenInfo, + }, + ), } diff --git a/bellows/types/struct.py b/bellows/types/struct.py index a95c09e16..1253f1d5a 100644 --- a/bellows/types/struct.py +++ b/bellows/types/struct.py @@ -326,6 +326,20 @@ class EmberTokenInfo(EzspStruct): arraySize: basic.uint8_t +class SlZigbeeTokenInfo(EzspStruct): + # Information of a token in the token table + # NVM3 key of the token + nvm3Key: named.NV3KeyId + # Token is a counter type + isCnt: named.Bool + # Token is an indexed token + isIdx: named.Bool + # Size of the token, widened from `uint8_t` in Simplicity SDK 2025.12 + size: basic.uint32_t + # Array size of the token + arraySize: basic.uint8_t + + class EmberTokTypeStackZllData(EzspStruct): # Public API for ZLL stack data token. # Token bitmask. diff --git a/tests/test_ezsp_v12.py b/tests/test_ezsp_v12.py index cdfcbb83a..ebbbaa8b2 100644 --- a/tests/test_ezsp_v12.py +++ b/tests/test_ezsp_v12.py @@ -3,6 +3,7 @@ import pytest import bellows.ezsp.v12 +import bellows.types as t from tests.common import mock_ezsp_commands @@ -28,3 +29,25 @@ def test_ezsp_frame_rx(ezsp_f): assert ezsp_f._handle_callback.call_count == 1 assert ezsp_f._handle_callback.call_args[0][0] == "version" assert ezsp_f._handle_callback.call_args[0][1] == [0x01, 0x02, 0x1234] + + +def test_check_key_context(ezsp_f) -> None: + """`checkKeyContext` takes a security manager context and returns an `sl_status_t`.""" + context = t.SecurityManagerContextV12( + core_key_type=t.SecurityManagerKeyType.APP_LINK, + key_index=0, + derived_type=t.SecurityManagerDerivedKeyTypeV12.NONE, + eui64=t.EUI64.convert("01:02:03:04:05:06:07:08"), + multi_network_index=0, + flags=t.SecurityManagerContextFlags.EUI_IS_VALID, + psa_key_alg_permission=0, + ) + _, tx_schema, rx_schema = ezsp_f.COMMANDS["checkKeyContext"] + + assert t.serialize_dict((), {"context": context}, tx_schema) == bytes.fromhex( + "04" "00" "00" "0807060504030201" "00" "02" "00000000" + ) + assert t.deserialize_dict(b"\x00\x00\x00\x00", rx_schema) == ( + {"status": t.sl_Status.OK}, + b"", + ) diff --git a/tests/test_ezsp_v13.py b/tests/test_ezsp_v13.py index 69c5909ff..59ae03d6c 100644 --- a/tests/test_ezsp_v13.py +++ b/tests/test_ezsp_v13.py @@ -227,3 +227,25 @@ async def test_factory_reset(ezsp_f) -> None: assert ezsp_f.tokenFactoryReset.mock_calls == [ call(excludeOutgoingFC=False, excludeBootCounter=False) ] + + +def test_check_key_context(ezsp_f) -> None: + """`checkKeyContext` takes a security manager context and returns an `sl_status_t`.""" + context = t.SecurityManagerContextV13( + core_key_type=t.SecurityManagerKeyType.APP_LINK, + key_index=0, + derived_type=t.SecurityManagerDerivedKeyTypeV13.NONE, + eui64=t.EUI64.convert("01:02:03:04:05:06:07:08"), + multi_network_index=0, + flags=t.SecurityManagerContextFlags.EUI_IS_VALID, + psa_key_alg_permission=0, + ) + _, tx_schema, rx_schema = ezsp_f.COMMANDS["checkKeyContext"] + + assert t.serialize_dict((), {"context": context}, tx_schema) == bytes.fromhex( + "04" "00" "0000" "0807060504030201" "00" "02" "00000000" + ) + assert t.deserialize_dict(b"\x00\x00\x00\x00", rx_schema) == ( + {"status": t.sl_Status.OK}, + b"", + ) diff --git a/tests/test_ezsp_v14.py b/tests/test_ezsp_v14.py index 49bf152b4..9bc3e0c98 100644 --- a/tests/test_ezsp_v14.py +++ b/tests/test_ezsp_v14.py @@ -4,6 +4,7 @@ import zigpy.exceptions import zigpy.state +import bellows.ezsp import bellows.ezsp.v14 import bellows.types as t @@ -155,6 +156,316 @@ async def test_get_network_key_without_network(ezsp_f): await ezsp_f.get_network_key() +V14_AND_LATER = [v for v in bellows.ezsp.EZSP._BY_VERSION if v >= 14] + +EUI64 = t.EUI64.convert("01:02:03:04:05:06:07:08") +EUI64_BYTES = bytes.fromhex("0807060504030201") + +# `sl_zigbee_rx_packet_info_t` +PACKET_INFO_BYTES = bytes.fromhex( + "3412" "0807060504030201" "ff" "03" "aa" "c4" "04030201" +) +PACKET_INFO = t.SlRxPacketInfo( + sender_short_id=0x1234, + sender_long_id=EUI64, + binding_index=0xFF, + address_index=0x03, + last_hop_lqi=0xAA, + last_hop_rssi=-60, + last_hop_timestamp=0x01020304, +) + +# `sl_zigbee_sec_man_context_t` +CONTEXT_BYTES = bytes.fromhex("04" "00" "0000" "0807060504030201" "00" "02" "00000000") +CONTEXT = t.SecurityManagerContextV13( + core_key_type=t.SecurityManagerKeyType.APP_LINK, + key_index=0, + derived_type=t.SecurityManagerDerivedKeyTypeV13.NONE, + eui64=EUI64, + multi_network_index=0, + flags=t.SecurityManagerContextFlags.EUI_IS_VALID, + psa_key_alg_permission=0, +) + +KEY_BYTES = bytes.fromhex("000102030405060708090a0b0c0d0e0f") +KEY = t.KeyData.convert("000102030405060708090a0b0c0d0e0f") + +# `sl_zigbee_sec_man_aps_key_metadata_t` +KEY_METADATA_BYTES = bytes.fromhex("1200" "01000000" "02000000" "b400") +KEY_METADATA = t.SecurityManagerAPSKeyMetadata( + bitmask=( + t.EmberKeyStructBitmask.KEY_HAS_OUTGOING_FRAME_COUNTER + | t.EmberKeyStructBitmask.KEY_IS_AUTHORIZED + ), + outgoing_frame_counter=1, + incoming_frame_counter=2, + ttl_in_seconds=180, +) + +STATUS_OK_BYTES = bytes.fromhex("00000000") + + +def zll_network_found_handler_data() -> tuple[bytes, dict]: + data = ( + # `sl_zigbee_zll_network_t` + bytes.fromhex("0b" "3412" "0807060504030201" "01" "02" "00") + + bytes.fromhex("78563412" "21436587" "0100") + + bytes.fromhex("0807060504030201" "cdab" "0000" "02" "01" "00" "00") + # `isDeviceInfoNull` + + bytes.fromhex("00") + # `sl_zigbee_zll_device_info_record_t` + + bytes.fromhex("0807060504030201" "01" "5ec0" "0001" "02" "00") + + PACKET_INFO_BYTES + ) + + expected = { + "networkInfo": t.EmberZllNetwork( + zigbeeNetwork=t.EmberZigbeeNetwork( + channel=11, + panId=0x1234, + extendedPanId=t.ExtendedPanId.convert("01:02:03:04:05:06:07:08"), + allowingJoin=t.Bool.true, + stackProfile=2, + nwkUpdateId=0, + ), + securityAlgorithm=t.EmberZllSecurityAlgorithmData( + transactionId=0x12345678, + responseId=0x87654321, + bitmask=0x0001, + ), + eui64=EUI64, + nodeId=0xABCD, + state=t.EmberZllState(0x0000), + nodeType=t.EmberNodeType.ROUTER, + numberSubDevices=1, + totalGroupIdentifiers=0, + rssiCorrection=0, + ), + "isDeviceInfoNull": t.Bool.false, + "deviceInfo": t.EmberZllDeviceInfoRecord( + ieeeAddress=EUI64, + endpointId=1, + profileId=0xC05E, + deviceId=0x0100, + version=2, + groupIdCount=0, + ), + "packetInfo": PACKET_INFO, + } + + return data, expected + + +RESPONSE_LAYOUTS = [ + ( + "exportLinkKeyByEui", + STATUS_OK_BYTES + CONTEXT_BYTES + KEY_BYTES + KEY_METADATA_BYTES, + { + "status": t.sl_Status.OK, + "context": CONTEXT, + "plaintext_key": KEY, + "key_data": KEY_METADATA, + }, + ), + ( + "exportTransientKeyByIndex", + STATUS_OK_BYTES + CONTEXT_BYTES + KEY_BYTES + KEY_METADATA_BYTES, + { + "status": t.sl_Status.OK, + "context": CONTEXT, + "plaintext_key": KEY, + "key_data": KEY_METADATA, + }, + ), + ( + "exportTransientKeyByEui", + STATUS_OK_BYTES + CONTEXT_BYTES + KEY_BYTES + KEY_METADATA_BYTES, + { + "status": t.sl_Status.OK, + "context": CONTEXT, + "plaintext_key": KEY, + "key_data": KEY_METADATA, + }, + ), + ( + "getApsKeyInfo", + STATUS_OK_BYTES + KEY_METADATA_BYTES + CONTEXT_BYTES, + { + "status": t.sl_Status.OK, + "key_data": KEY_METADATA, + "context": CONTEXT, + }, + ), + ( + "importKey", + STATUS_OK_BYTES + CONTEXT_BYTES, + {"status": t.sl_Status.OK, "context": CONTEXT}, + ), + ("checkKeyContext", STATUS_OK_BYTES, {"status": t.sl_Status.OK}), + ("findAndRejoinNetwork", STATUS_OK_BYTES, {"status": t.sl_Status.OK}), + ("setAddressTableInfo", STATUS_OK_BYTES, {"status": t.sl_Status.OK}), + ("setPowerDescriptor", STATUS_OK_BYTES, {"status": t.sl_Status.OK}), + ("clearStoredBeacons", STATUS_OK_BYTES, {"status": t.sl_Status.OK}), + ("sendPanIdUpdate", b"\x01", {"status": t.Bool.true}), + ( + "readAttribute", + b"\x00" + b"\x21" + b"\x02\x34\x12", + {"status": t.EmberStatus.SUCCESS, "dataType": 0x21, "data": b"\x34\x12"}, + ), + ("writeAttribute", b"\x00", {"status": t.EmberStatus.SUCCESS}), + ( + "macPassthroughMessageHandler", + b"\x01" + PACKET_INFO_BYTES + b"\x03abc", + { + "messageType": t.EmberMacPassthroughType.MAC_PASSTHROUGH_SE_INTERPAN, + "packetInfo": PACKET_INFO, + "messageContents": b"abc", + }, + ), + ( + "incomingBootloadMessageHandler", + EUI64_BYTES + PACKET_INFO_BYTES + b"\x03abc", + {"longId": EUI64, "packetInfo": PACKET_INFO, "messageContents": b"abc"}, + ), + ( + "zllNetworkFoundHandler", + *zll_network_found_handler_data(), + ), + ( + "zllAddressAssignmentHandler", + bytes.fromhex("0100" "0200" "fff7" "0100" "ff00" "0001" "fffe") + + PACKET_INFO_BYTES, + { + "addressInfo": t.EmberZllAddressAssignment( + nodeId=0x0001, + freeNodeIdMin=0x0002, + freeNodeIdMax=0xF7FF, + groupIdMin=0x0001, + groupIdMax=0x00FF, + freeGroupIdMin=0x0100, + freeGroupIdMax=0xFEFF, + ), + "packetInfo": PACKET_INFO, + }, + ), + ( + "rawTransmitCompleteHandler", + b"\x03abc" + bytes.fromhex("01000000"), + {"messageContents": b"abc", "status": t.sl_Status.FAIL}, + ), +] + + +@pytest.mark.parametrize("version", V14_AND_LATER) +@pytest.mark.parametrize( + ("name", "data", "expected"), + RESPONSE_LAYOUTS, + ids=[name for name, _, _ in RESPONSE_LAYOUTS], +) +def test_v14_response_layouts( + version: int, name: str, data: bytes, expected: dict +) -> None: + """Responses and callbacks whose layout changed in v14 parse SDK-layout bytes.""" + _, _, rx_schema = bellows.ezsp.EZSP._BY_VERSION[version].COMMANDS[name] + result, rest = t.deserialize_dict(data, rx_schema) + + assert rest == b"" + assert result == expected + + +@pytest.mark.parametrize("version", [v for v in V14_AND_LATER if v < 18]) +def test_v14_mac_filter_match_message_handler(version: int) -> None: + _, _, rx_schema = bellows.ezsp.EZSP._BY_VERSION[version].COMMANDS[ + "macFilterMatchMessageHandler" + ] + result, rest = t.deserialize_dict( + b"\x02" + b"\x01" + PACKET_INFO_BYTES + b"\x03abc", rx_schema + ) + + assert rest == b"" + assert result == { + "filterIndexMatch": 2, + "legacyPassthroughType": t.EmberMacPassthroughType.MAC_PASSTHROUGH_SE_INTERPAN, + "packetInfo": PACKET_INFO, + "messageContents": b"abc", + } + + +REQUEST_LAYOUTS = [ + ( + "findAndRejoinNetwork", + { + "haveCurrentNetworkKey": True, + "channelMask": 0x07FFF800, + "reason": 0x03, + "nodeType": t.EmberNodeType.ROUTER, + }, + bytes.fromhex("01" "00f8ff07" "03" "02"), + ), + ( + "setAddressTableInfo", + {"index": 5, "eui64": EUI64, "nwk": 0xABCD}, + b"\x05" + EUI64_BYTES + b"\xcd\xab", + ), + ("checkKeyContext", {"context": CONTEXT}, CONTEXT_BYTES), + ("getApsKeyInfo", {"context_in": CONTEXT}, CONTEXT_BYTES), + ("importKey", {"context": CONTEXT, "key": KEY}, CONTEXT_BYTES + KEY_BYTES), + ("setPowerDescriptor", {"descriptor": 0x0010}, b"\x10\x00"), +] + + +@pytest.mark.parametrize("version", V14_AND_LATER) +@pytest.mark.parametrize( + ("name", "kwargs", "data"), + REQUEST_LAYOUTS, + ids=[name for name, _, _ in REQUEST_LAYOUTS], +) +def test_v14_request_layouts( + version: int, name: str, kwargs: dict, data: bytes +) -> None: + """Requests whose layout changed in v14 serialize to the SDK layout.""" + _, tx_schema, _ = bellows.ezsp.EZSP._BY_VERSION[version].COMMANDS[name] + + assert t.serialize_dict((), kwargs, tx_schema) == data + + +@pytest.mark.parametrize("version", V14_AND_LATER) +@pytest.mark.parametrize( + "name", + [ + "setAddressTableRemoteEui64", + "setAddressTableRemoteNodeId", + "getAddressTableRemoteNodeId", + "incomingSenderEui64Handler", + "getFirstBeacon", + "getNextBeacon", + "proxyBroadcast", + "sendMulticastWithAlias", + "sendRawMessage", + "setLongUpTime", + "setHubConnectivity", + "isUpTimeLong", + "isHubConnected", + "setParentClassificationEnabled", + "getParentClassificationEnabled", + ], +) +def test_v14_removed_commands(version: int, name: str) -> None: + """Commands removed from the SDK in v14 are not defined.""" + assert name not in bellows.ezsp.EZSP._BY_VERSION[version].COMMANDS + + +@pytest.mark.parametrize("version", V14_AND_LATER) +def test_no_duplicate_frame_ids(version: int) -> None: + """Every frame ID maps to a single command.""" + names_by_id: dict[int, list[str]] = {} + + for name, (cmd_id, _, _) in bellows.ezsp.EZSP._BY_VERSION[version].COMMANDS.items(): + names_by_id.setdefault(cmd_id, []).append(name) + + assert {k: v for k, v in names_by_id.items() if len(v) > 1} == {} + + async def test_send_unicast(ezsp_f) -> None: ezsp_f.sendUnicast.return_value = (t.sl_Status.OK, 0x0042) status, message_tag = await ezsp_f.send_unicast( diff --git a/tests/test_ezsp_v18.py b/tests/test_ezsp_v18.py index b0c8c90e7..39ee5fae8 100644 --- a/tests/test_ezsp_v18.py +++ b/tests/test_ezsp_v18.py @@ -2,10 +2,12 @@ import pytest +import bellows.ezsp import bellows.ezsp.v18 import bellows.types as t from tests.common import mock_ezsp_commands +from tests.test_ezsp_v14 import PACKET_INFO, PACKET_INFO_BYTES @pytest.fixture @@ -29,3 +31,33 @@ def test_ezsp_frame_rx(ezsp_f): assert ezsp_f._handle_callback.call_count == 1 assert ezsp_f._handle_callback.call_args[0][0] == "version" assert ezsp_f._handle_callback.call_args[0][1] == [0x01, 0x02, 0x1234] + + +V18_AND_LATER = [v for v in bellows.ezsp.EZSP._BY_VERSION if v >= 18] + + +@pytest.mark.parametrize("version", V18_AND_LATER) +def test_mac_filter_match_message_handler(version: int) -> None: + _, _, rx_schema = bellows.ezsp.EZSP._BY_VERSION[version].COMMANDS[ + "macFilterMatchMessageHandler" + ] + result, rest = t.deserialize_dict( + b"\x34\x12" + b"\x01" + PACKET_INFO_BYTES + b"\x03abc", rx_schema + ) + + assert rest == b"" + assert result == { + "filterValueMatch": 0x1234, + "legacyPassthroughType": t.EmberMacPassthroughType.MAC_PASSTHROUGH_SE_INTERPAN, + "packetInfo": PACKET_INFO, + "messageContents": b"abc", + } + + +@pytest.mark.parametrize("version", V18_AND_LATER) +def test_get_token_count(version: int) -> None: + _, _, rx_schema = bellows.ezsp.EZSP._BY_VERSION[version].COMMANDS["getTokenCount"] + result, rest = t.deserialize_dict(b"\x2a\x00\x00\x00", rx_schema) + + assert rest == b"" + assert result == {"count": 42} diff --git a/tests/test_ezsp_v19.py b/tests/test_ezsp_v19.py index e4229ae96..5c716750b 100644 --- a/tests/test_ezsp_v19.py +++ b/tests/test_ezsp_v19.py @@ -2,6 +2,7 @@ import pytest +import bellows.ezsp import bellows.ezsp.v19 import bellows.types as t @@ -29,3 +30,21 @@ def test_ezsp_frame_rx(ezsp_f): assert ezsp_f._handle_callback.call_count == 1 assert ezsp_f._handle_callback.call_args[0][0] == "version" assert ezsp_f._handle_callback.call_args[0][1] == [0x01, 0x02, 0x1234] + + +@pytest.mark.parametrize( + "version", [v for v in bellows.ezsp.EZSP._BY_VERSION if v >= 19] +) +def test_get_token_info(version: int) -> None: + _, _, rx_schema = bellows.ezsp.EZSP._BY_VERSION[version].COMMANDS["getTokenInfo"] + result, rest = t.deserialize_dict( + bytes.fromhex("00000000" "01000000" "00" "01" "2c010000" "05"), rx_schema + ) + + assert rest == b"" + assert result["status"] == t.sl_Status.OK + assert result["token_info"].nvm3Key == 0x00000001 + assert result["token_info"].isCnt == t.Bool.false + assert result["token_info"].isIdx == t.Bool.true + assert result["token_info"].size == 300 + assert result["token_info"].arraySize == 5