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