Skip to content

Formally support EZSP v16 and v17 #678

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions bellows/ezsp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions bellows/ezsp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
20 changes: 20 additions & 0 deletions bellows/ezsp/v16/__init__.py
Original file line number Diff line number Diff line change
@@ -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),
}
5 changes: 5 additions & 0 deletions bellows/ezsp/v16/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from ..v14.commands import COMMANDS as COMMANDS_v14

COMMANDS = {
**COMMANDS_v14,
}
16 changes: 16 additions & 0 deletions bellows/ezsp/v16/config.py
Original file line number Diff line number Diff line change
@@ -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},
}
20 changes: 20 additions & 0 deletions bellows/ezsp/v17/__init__.py
Original file line number Diff line number Diff line change
@@ -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),
}
47 changes: 47 additions & 0 deletions bellows/ezsp/v17/commands.py
Original file line number Diff line number Diff line change
@@ -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.SlRxPacketInfo,
},
),
}
16 changes: 16 additions & 0 deletions bellows/ezsp/v17/config.py
Original file line number Diff line number Diff line change
@@ -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},
}
28 changes: 28 additions & 0 deletions bellows/types/named.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"""

Expand Down Expand Up @@ -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):
Expand Down
34 changes: 34 additions & 0 deletions bellows/types/struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,3 +715,37 @@ class NV3StackNetworkManagementToken(EzspStruct):
manager_node_id: named.NWK
update_id: basic.uint8_t
padding: basic.uint8_t


class SlRxPacketInfo(EzspStruct):
"""Received packet information.

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
3 changes: 3 additions & 0 deletions tests/test_ezsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions tests/test_ezsp_v16.py
Original file line number Diff line number Diff line change
@@ -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.EZSPv16(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]
33 changes: 33 additions & 0 deletions tests/test_ezsp_v17.py
Original file line number Diff line number Diff line change
@@ -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.EZSPv17(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]
2 changes: 1 addition & 1 deletion tests/test_uart.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading