From 30554134cd4764d4877ad1dd7cb35a57a884c194 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 15 Jul 2025 14:19:59 -0400 Subject: [PATCH 1/6] Support EZSP v16 and v17 --- bellows/ezsp/__init__.py | 6 +++-- bellows/ezsp/v16/__init__.py | 20 +++++++++++++++ bellows/ezsp/v16/commands.py | 5 ++++ bellows/ezsp/v16/config.py | 16 ++++++++++++ bellows/ezsp/v17/__init__.py | 20 +++++++++++++++ bellows/ezsp/v17/commands.py | 47 ++++++++++++++++++++++++++++++++++++ bellows/ezsp/v17/config.py | 16 ++++++++++++ bellows/types/named.py | 28 +++++++++++++++++++++ 8 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 bellows/ezsp/v16/__init__.py create mode 100644 bellows/ezsp/v16/commands.py create mode 100644 bellows/ezsp/v16/config.py create mode 100644 bellows/ezsp/v17/__init__.py create mode 100644 bellows/ezsp/v17/commands.py create mode 100644 bellows/ezsp/v17/config.py diff --git a/bellows/ezsp/__init__.py b/bellows/ezsp/__init__.py index 510077ad..57abc2dc 100644 --- a/bellows/ezsp/__init__.py +++ b/bellows/ezsp/__init__.py @@ -29,9 +29,9 @@ import bellows.types as t import bellows.uart -from . import v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14 +from . import v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v16, v17 -EZSP_LATEST = v14.EZSPv14.VERSION +EZSP_LATEST = v17.EZSPv17.VERSION LOGGER = logging.getLogger(__name__) MTOR_MIN_INTERVAL = 60 MTOR_MAX_INTERVAL = 3600 @@ -57,6 +57,8 @@ class EZSP: v12.EZSPv12.VERSION: v12.EZSPv12, v13.EZSPv13.VERSION: v13.EZSPv13, v14.EZSPv14.VERSION: v14.EZSPv14, + v16.EZSPv16.VERSION: v16.EZSPv16, + v17.EZSPv17.VERSION: v17.EZSPv17, } def __init__(self, device_config: dict, application: Any | None = None): diff --git a/bellows/ezsp/v16/__init__.py b/bellows/ezsp/v16/__init__.py new file mode 100644 index 00000000..5ddbf58c --- /dev/null +++ b/bellows/ezsp/v16/__init__.py @@ -0,0 +1,20 @@ +""""EZSP Protocol version 16 protocol handler.""" +from __future__ import annotations + +import voluptuous as vol + +import bellows.config + +from . import commands, config +from ..v14 import EZSPv14 + + +class EZSPv16(EZSPv14): + """EZSP Version 16 Protocol version handler.""" + + VERSION = 16 + COMMANDS = commands.COMMANDS + SCHEMAS = { + bellows.config.CONF_EZSP_CONFIG: vol.Schema(config.EZSP_SCHEMA), + bellows.config.CONF_EZSP_POLICIES: vol.Schema(config.EZSP_POLICIES_SCH), + } diff --git a/bellows/ezsp/v16/commands.py b/bellows/ezsp/v16/commands.py new file mode 100644 index 00000000..51dc4248 --- /dev/null +++ b/bellows/ezsp/v16/commands.py @@ -0,0 +1,5 @@ +from ..v14.commands import COMMANDS as COMMANDS_v14 + +COMMANDS = { + **COMMANDS_v14, +} diff --git a/bellows/ezsp/v16/config.py b/bellows/ezsp/v16/config.py new file mode 100644 index 00000000..d502f1ec --- /dev/null +++ b/bellows/ezsp/v16/config.py @@ -0,0 +1,16 @@ +import voluptuous as vol + +from bellows.config import cv_uint16 +from bellows.types import EzspPolicyId + +from ..v4.config import EZSP_POLICIES_SHARED +from ..v14 import config as v14_config + +EZSP_SCHEMA = { + **v14_config.EZSP_SCHEMA, +} + +EZSP_POLICIES_SCH = { + **EZSP_POLICIES_SHARED, + **{vol.Optional(policy.name): cv_uint16 for policy in EzspPolicyId}, +} diff --git a/bellows/ezsp/v17/__init__.py b/bellows/ezsp/v17/__init__.py new file mode 100644 index 00000000..1f6d9832 --- /dev/null +++ b/bellows/ezsp/v17/__init__.py @@ -0,0 +1,20 @@ +""""EZSP Protocol version 17 protocol handler.""" +from __future__ import annotations + +import voluptuous as vol + +import bellows.config + +from . import commands, config +from ..v16 import EZSPv16 + + +class EZSPv17(EZSPv16): + """EZSP Version 17 Protocol version handler.""" + + VERSION = 17 + COMMANDS = commands.COMMANDS + SCHEMAS = { + bellows.config.CONF_EZSP_CONFIG: vol.Schema(config.EZSP_SCHEMA), + bellows.config.CONF_EZSP_POLICIES: vol.Schema(config.EZSP_POLICIES_SCH), + } diff --git a/bellows/ezsp/v17/commands.py b/bellows/ezsp/v17/commands.py new file mode 100644 index 00000000..cba1b26b --- /dev/null +++ b/bellows/ezsp/v17/commands.py @@ -0,0 +1,47 @@ +import bellows.types as t + +from ..v16.commands import COMMANDS as COMMANDS_v16 + +COMMANDS = { + **COMMANDS_v16, + "gpProxyTableRemoveEntry": ( + 0x005D, + { + "proxyIndex": t.uint8_t, + }, + {}, + ), + "gpClearProxyTable": ( + 0x005F, + {}, + {}, + ), + "muxInvalidRxHandler": ( + 0x0062, + {}, + { + "newRxChannel": t.uint8_t, + "oldRxChannel": t.uint8_t, + }, + ), + "gpepIncomingMessageHandler": ( + 0x00C5, + {}, + { + "status": t.sl_GpStatus, + "gpdLink": t.uint8_t, + "sequenceNumber": t.uint8_t, + "addr": t.EmberGpAddress, + "gpdfSecurityLevel": t.EmberGpSecurityLevel, + "gpdfSecurityKeyType": t.EmberGpKeyType, + "autoCommissioning": t.Bool, + "bidirectionalInfo": t.uint8_t, + "gpdSecurityFrameCounter": t.uint32_t, + "gpdCommandId": t.uint8_t, + "mic": t.uint32_t, + "proxyTableIndex": t.uint8_t, + "gpdCommandPayload": t.LVBytes, + "packetInfo": t.EmberRxPacketInfo, + }, + ), +} diff --git a/bellows/ezsp/v17/config.py b/bellows/ezsp/v17/config.py new file mode 100644 index 00000000..45bf59e5 --- /dev/null +++ b/bellows/ezsp/v17/config.py @@ -0,0 +1,16 @@ +import voluptuous as vol + +from bellows.config import cv_uint16 +from bellows.types import EzspPolicyId + +from ..v4.config import EZSP_POLICIES_SHARED +from ..v16 import config as v16_config + +EZSP_SCHEMA = { + **v16_config.EZSP_SCHEMA, +} + +EZSP_POLICIES_SCH = { + **EZSP_POLICIES_SHARED, + **{vol.Optional(policy.name): cv_uint16 for policy in EzspPolicyId}, +} diff --git a/bellows/types/named.py b/bellows/types/named.py index f2a7e897..84328074 100644 --- a/bellows/types/named.py +++ b/bellows/types/named.py @@ -80,6 +80,9 @@ class EzspExtendedValueId(basic.enum8): # This number of bytes of overhead required in the network frame for source routing # to a particular destination. EXTENDED_VALUE_GET_SOURCE_ROUTE_OVERHEAD = 0x02 + # These values are current or boot-time metrics gathered by the memory + # manager/buffer manager. + EXTENDED_VALUE_MEMORY_USAGE_DATA = 0x03 class EzspEndpointFlags(basic.enum16): @@ -1583,6 +1586,25 @@ def from_ember_status( # fmt: on +class sl_GpStatus(basic.enum8): + # Success Status + OK = 0x00 + # Match Frame + MATCH = 0x01 + # Drop Frame + DROP_FRAME = 0x02 + # Frame Unprocessed + UNPROCESSED = 0x03 + # Frame Pass Unprocessed + PASS_UNPROCESSED = 0x04 + # Frame TX Then Drop + TX_THEN_DROP = 0x05 + # No Security + NO_SECURITY = 0x06 + # Security Failure + AUTH_FAILURE = 0x07 + + class EmberDistinguishedNodeId(basic.enum16): """A distinguished network ID that will never be assigned to any node""" @@ -2169,6 +2191,12 @@ class EzspValueId(basic.enum8): # Return activation state about TC Delayed Join on an NCP. A return value of # 0 indicates that the feature is not activated. VALUE_DELAYED_JOIN_ACTIVATION = 0x45 + # The maximum number of NWK retries that will be attempted. + VALUE_MAX_NWK_RETRIES = 0x46 + # Policies for allowing/disallowing rejoins. + VALUE_REJOIN_MODE = 0x47 + # Controls whether devices must use an install code when joining. + VALUE_JOIN_USE_INSTALL_CODE_ENABLE = 0x48 class EmberRf4ceTxOption(basic.uint8_t): From 8ba1e7192e0f3e999dbc495546f2eeaf6ab97ba3 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 15 Jul 2025 14:22:19 -0400 Subject: [PATCH 2/6] Add unit tests --- tests/test_ezsp_v16.py | 33 +++++++++++++++++++++++++++++++++ tests/test_ezsp_v17.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 tests/test_ezsp_v16.py create mode 100644 tests/test_ezsp_v17.py diff --git a/tests/test_ezsp_v16.py b/tests/test_ezsp_v16.py new file mode 100644 index 00000000..7df734d6 --- /dev/null +++ b/tests/test_ezsp_v16.py @@ -0,0 +1,33 @@ +from unittest.mock import MagicMock + +import pytest +import zigpy.exceptions +import zigpy.state + +import bellows.ezsp.v16 +import bellows.types as t + +from tests.common import mock_ezsp_commands + + +@pytest.fixture +def ezsp_f(): + """EZSP v16 protocol handler.""" + ezsp = bellows.ezsp.v16.EZSPv14(MagicMock(), MagicMock()) + mock_ezsp_commands(ezsp) + + return ezsp + + +def test_ezsp_frame(ezsp_f): + ezsp_f._seq = 0x22 + data = ezsp_f._ezsp_frame("version", 16) + assert data == b"\x22\x00\x01\x00\x00\x10" + + +def test_ezsp_frame_rx(ezsp_f): + """Test receiving a version frame.""" + ezsp_f(b"\x01\x01\x80\x00\x00\x01\x02\x34\x12") + 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] diff --git a/tests/test_ezsp_v17.py b/tests/test_ezsp_v17.py new file mode 100644 index 00000000..978b0ea3 --- /dev/null +++ b/tests/test_ezsp_v17.py @@ -0,0 +1,33 @@ +from unittest.mock import MagicMock + +import pytest +import zigpy.exceptions +import zigpy.state + +import bellows.ezsp.v17 +import bellows.types as t + +from tests.common import mock_ezsp_commands + + +@pytest.fixture +def ezsp_f(): + """EZSP v17 protocol handler.""" + ezsp = bellows.ezsp.v17.EZSPv14(MagicMock(), MagicMock()) + mock_ezsp_commands(ezsp) + + return ezsp + + +def test_ezsp_frame(ezsp_f): + ezsp_f._seq = 0x22 + data = ezsp_f._ezsp_frame("version", 17) + assert data == b"\x22\x00\x01\x00\x00\x11" + + +def test_ezsp_frame_rx(ezsp_f): + """Test receiving a version frame.""" + ezsp_f(b"\x01\x01\x80\x00\x00\x01\x02\x34\x12") + 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] From 805975c2b4e5bddb18016d79ff305568f4cec8a7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 15 Jul 2025 14:31:03 -0400 Subject: [PATCH 3/6] Add missing type --- bellows/ezsp/v17/commands.py | 2 +- bellows/types/struct.py | 37 ++++++++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/bellows/ezsp/v17/commands.py b/bellows/ezsp/v17/commands.py index cba1b26b..47cdc2c5 100644 --- a/bellows/ezsp/v17/commands.py +++ b/bellows/ezsp/v17/commands.py @@ -41,7 +41,7 @@ "mic": t.uint32_t, "proxyTableIndex": t.uint8_t, "gpdCommandPayload": t.LVBytes, - "packetInfo": t.EmberRxPacketInfo, + "packetInfo": t.SlRxPacketInfo, }, ), } diff --git a/bellows/types/struct.py b/bellows/types/struct.py index a975d459..5867fedb 100644 --- a/bellows/types/struct.py +++ b/bellows/types/struct.py @@ -708,10 +708,35 @@ class EmberMultiPhyRadioParameters(EzspStruct): radioChannel: basic.uint8_t -class NV3StackNetworkManagementToken(EzspStruct): - """NV3 stack network management token value.""" +class SlRxPacketInfo(EzspStruct): + """Received packet information. - active_channels: named.Channels - manager_node_id: named.NWK - update_id: basic.uint8_t - padding: basic.uint8_t + Contains information about the incoming packet. + """ + + # Short ID of the sender of the message + sender_short_id: named.NWK + # EUI64 of the sender of the message if the sender chose to include this + # information in the message. The SL_ZIGBEE_APS_OPTION_SOURCE_EUI64 bit in + # the options field of the APS frame of the incoming message indicates that + # the EUI64 is present in the message. Also, when not set, the sender long ID + # is set to all zeros + sender_long_id: named.EUI64 + # The index of the entry in the binding table that matches the sender of + # the message or 0xFF if there is no matching entry. A binding matches the message if: + # - The binding's source endpoint is the same as the message's destination endpoint + # - The binding's destination endpoint is the same as the message's source endpoint + # - The source of the message has been previously identified as the binding's remote + # node by a successful address discovery or by the application via a call to either + # sl_zigbee_set_reply_binding() or sl_zigbee_note_senders_binding() + binding_index: basic.uint8_t + # The index of the entry in the address table that matches the sender of + # the message or 0xFF if there is no matching entry + address_index: basic.uint8_t + # Link quality of the node that last relayed the current message + last_hop_lqi: basic.uint8_t + # Received signal strength indicator (RSSI) of the node that last + # relayed the message + last_hop_rssi: basic.int8s + # Timestamp of the moment when Start Frame Delimiter (SFD) was received + last_hop_timestamp: basic.uint32_t From cd8a3df23076c34ae3bb0e5955059ffe379a7e26 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 15 Jul 2025 14:33:05 -0400 Subject: [PATCH 4/6] Fix failing unit tests --- bellows/ezsp/config.py | 2 ++ tests/test_ezsp.py | 3 +++ tests/test_ezsp_v16.py | 2 +- tests/test_ezsp_v17.py | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bellows/ezsp/config.py b/bellows/ezsp/config.py index 58f83ced..1045da0f 100644 --- a/bellows/ezsp/config.py +++ b/bellows/ezsp/config.py @@ -127,4 +127,6 @@ class ValueConfig: 12: DEFAULT_CONFIG_NEW, 13: DEFAULT_CONFIG_NEW, 14: DEFAULT_CONFIG_NEW, + 16: DEFAULT_CONFIG_NEW, + 17: DEFAULT_CONFIG_NEW, } diff --git a/tests/test_ezsp.py b/tests/test_ezsp.py index 1bfea386..3a725d04 100644 --- a/tests/test_ezsp.py +++ b/tests/test_ezsp.py @@ -799,6 +799,9 @@ async def test_wait_for_stack_status(ezsp_f): def test_ezsp_versions(ezsp_f): for version in range(4, EZSP_LATEST + 1): + # Version 15 was never released, so skip it + if version == 15: + continue assert version in ezsp_f._BY_VERSION assert ezsp_f._BY_VERSION[version].__name__ == f"EZSPv{version}" assert ezsp_f._BY_VERSION[version].VERSION == version diff --git a/tests/test_ezsp_v16.py b/tests/test_ezsp_v16.py index 7df734d6..ee75c22b 100644 --- a/tests/test_ezsp_v16.py +++ b/tests/test_ezsp_v16.py @@ -13,7 +13,7 @@ @pytest.fixture def ezsp_f(): """EZSP v16 protocol handler.""" - ezsp = bellows.ezsp.v16.EZSPv14(MagicMock(), MagicMock()) + ezsp = bellows.ezsp.v16.EZSPv16(MagicMock(), MagicMock()) mock_ezsp_commands(ezsp) return ezsp diff --git a/tests/test_ezsp_v17.py b/tests/test_ezsp_v17.py index 978b0ea3..8c775868 100644 --- a/tests/test_ezsp_v17.py +++ b/tests/test_ezsp_v17.py @@ -13,7 +13,7 @@ @pytest.fixture def ezsp_f(): """EZSP v17 protocol handler.""" - ezsp = bellows.ezsp.v17.EZSPv14(MagicMock(), MagicMock()) + ezsp = bellows.ezsp.v17.EZSPv17(MagicMock(), MagicMock()) mock_ezsp_commands(ezsp) return ezsp From 8762eeca24ba44719791d5c2949175ee1a9db6d1 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:22:19 -0400 Subject: [PATCH 5/6] Oops --- bellows/types/struct.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bellows/types/struct.py b/bellows/types/struct.py index 5867fedb..d0dd38ee 100644 --- a/bellows/types/struct.py +++ b/bellows/types/struct.py @@ -708,6 +708,15 @@ class EmberMultiPhyRadioParameters(EzspStruct): radioChannel: basic.uint8_t +class NV3StackNetworkManagementToken(EzspStruct): + """NV3 stack network management token value.""" + + active_channels: named.Channels + manager_node_id: named.NWK + update_id: basic.uint8_t + padding: basic.uint8_t + + class SlRxPacketInfo(EzspStruct): """Received packet information. From 6c38e7bc142e8dadcbec2664c25d0fcd957fe33b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:26:16 -0400 Subject: [PATCH 6/6] Fix tests for 3.9 --- tests/test_uart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_uart.py b/tests/test_uart.py index 1fdc05f3..26ccb9ef 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -139,7 +139,7 @@ async def mock_connect(loop, protocol_factory, *args, **kwargs): @pytest.fixture -def gw(): +async def gw(): gw = uart.Gateway(MagicMock()) gw._transport = MagicMock() return gw