diff --git a/bellows/__init__.py b/bellows/__init__.py index 0315dc82..0386d076 100644 --- a/bellows/__init__.py +++ b/bellows/__init__.py @@ -1,5 +1,5 @@ MAJOR_VERSION = 0 MINOR_VERSION = 35 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" diff --git a/bellows/ezsp/config.py b/bellows/ezsp/config.py new file mode 100644 index 00000000..992f0ee3 --- /dev/null +++ b/bellows/ezsp/config.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import dataclasses + +import bellows.ezsp.v4.types as types_v4 +import bellows.ezsp.v6.types as types_v6 +import bellows.types as t + + +@dataclasses.dataclass(frozen=True) +class RuntimeConfig: + config_id: t.enum8 + value: int + minimum: bool = False + + +DEFAULT_CONFIG = [ + RuntimeConfig( + config_id=types_v4.EzspConfigId.CONFIG_SOURCE_ROUTE_TABLE_SIZE, + value=200, + minimum=True, + ), + RuntimeConfig( + config_id=types_v4.EzspConfigId.CONFIG_END_DEVICE_POLL_TIMEOUT, + value=8, + ), + RuntimeConfig( + config_id=types_v4.EzspConfigId.CONFIG_INDIRECT_TRANSMISSION_TIMEOUT, + value=7680, + ), + RuntimeConfig( + config_id=types_v4.EzspConfigId.CONFIG_STACK_PROFILE, + value=2, + ), + RuntimeConfig( + config_id=types_v4.EzspConfigId.CONFIG_SUPPORTED_NETWORKS, + value=1, + minimum=True, + ), + RuntimeConfig( + config_id=types_v4.EzspConfigId.CONFIG_MULTICAST_TABLE_SIZE, + value=16, + minimum=True, + ), + RuntimeConfig( + config_id=types_v4.EzspConfigId.CONFIG_TRUST_CENTER_ADDRESS_CACHE_SIZE, + value=2, + minimum=True, + ), + RuntimeConfig( + config_id=types_v4.EzspConfigId.CONFIG_SECURITY_LEVEL, + value=5, + ), + RuntimeConfig( + config_id=types_v4.EzspConfigId.CONFIG_ADDRESS_TABLE_SIZE, + value=16, + minimum=True, + ), + RuntimeConfig( + config_id=types_v6.EzspConfigId.CONFIG_TC_REJOINS_USING_WELL_KNOWN_KEY_TIMEOUT_S, + value=90, + ), + RuntimeConfig( + config_id=types_v4.EzspConfigId.CONFIG_PAN_ID_CONFLICT_REPORT_THRESHOLD, + value=2, + ), + RuntimeConfig( + config_id=types_v4.EzspConfigId.CONFIG_APPLICATION_ZDO_FLAGS, + value=( + t.EmberZdoConfigurationFlags.APP_RECEIVES_SUPPORTED_ZDO_REQUESTS + | t.EmberZdoConfigurationFlags.APP_HANDLES_UNSUPPORTED_ZDO_REQUESTS + ), + ), + # Must be set last + RuntimeConfig(types_v4.EzspConfigId.CONFIG_PACKET_BUFFER_COUNT, value=0xFF), +] diff --git a/bellows/ezsp/protocol.py b/bellows/ezsp/protocol.py index dcab7b61..24644f6e 100644 --- a/bellows/ezsp/protocol.py +++ b/bellows/ezsp/protocol.py @@ -1,6 +1,7 @@ import abc import asyncio import binascii +import dataclasses import functools import logging import sys @@ -16,7 +17,6 @@ from bellows.typing import GatewayType LOGGER = logging.getLogger(__name__) - EZSP_CMD_TIMEOUT = 5 @@ -38,9 +38,6 @@ def __init__(self, cb_handler: Callable, gateway: GatewayType) -> None: self.tc_policy = 0 async def _cfg(self, config_id: int, value: Any) -> None: - if value is None: - return - (status,) = await self.setConfigurationValue(config_id, value) if status != self.types.EmberStatus.SUCCESS: LOGGER.warning( @@ -68,45 +65,69 @@ async def pre_permit(self, time_s: int) -> None: async def initialize(self, zigpy_config: Dict) -> None: """Initialize EmberZNet Stack.""" - buffers = await self.get_free_buffers() - _, conf_buffers = await self.getConfigurationValue( - self.types.EzspConfigId.CONFIG_PACKET_BUFFER_COUNT - ) - LOGGER.debug( - "Free/configured buffers before any configurations: %s/%s", - buffers, - conf_buffers, - ) - ezsp_config = self.SCHEMAS[CONF_EZSP_CONFIG](zigpy_config[CONF_EZSP_CONFIG]) - for config, value in ezsp_config.items(): - if config in (self.types.EzspConfigId.CONFIG_PACKET_BUFFER_COUNT.name,): - # we want to set these last + # Prevent circular import + from bellows.ezsp.config import DEFAULT_CONFIG, RuntimeConfig + + # Not all config will be present in every EZSP version so only use valid keys + ezsp_config = {} + + for cfg in DEFAULT_CONFIG: + try: + config_id = self.types.EzspConfigId[cfg.config_id.name] + except KeyError: + pass + else: + ezsp_config[cfg.config_id.name] = dataclasses.replace( + cfg, config_id=config_id + ) + + # Override the defaults with user-specified values (or `None` for deletions) + for name, value in self.SCHEMAS[CONF_EZSP_CONFIG]( + zigpy_config[CONF_EZSP_CONFIG] + ).items(): + if value is None: + ezsp_config.pop(name) continue - await self._cfg(self.types.EzspConfigId[config], value) - - buffers = await self.get_free_buffers() - _, conf_buffers = await self.getConfigurationValue( - self.types.EzspConfigId.CONFIG_PACKET_BUFFER_COUNT - ) - LOGGER.debug( - "Free/configured buffers before all memory allocation: %s/%s", - buffers, - conf_buffers, - ) - c = self.types.EzspConfigId - await self._cfg( - self.types.EzspConfigId.CONFIG_PACKET_BUFFER_COUNT, - ezsp_config[c.CONFIG_PACKET_BUFFER_COUNT.name], - ) - buffers = await self.get_free_buffers() - _, conf_buffers = await self.getConfigurationValue( - self.types.EzspConfigId.CONFIG_PACKET_BUFFER_COUNT - ) - LOGGER.debug( - "Free/configured buffers after all memory allocation: %s/%s", - buffers, - conf_buffers, - ) + + ezsp_config[name] = RuntimeConfig( + config_id=self.types.EzspConfigId[name], + value=value, + ) + + # Make sure CONFIG_PACKET_BUFFER_COUNT is always set last + if self.types.EzspConfigId.CONFIG_PACKET_BUFFER_COUNT.name in ezsp_config: + ezsp_config = { + **ezsp_config, + self.types.EzspConfigId.CONFIG_PACKET_BUFFER_COUNT.name: ezsp_config[ + self.types.EzspConfigId.CONFIG_PACKET_BUFFER_COUNT.name + ], + } + + # Finally, set the config + for cfg in ezsp_config.values(): + (status, current_value) = await self.getConfigurationValue(cfg.config_id) + + # Only grow some config entries, all others should be set + if ( + status == self.types.EmberStatus.SUCCESS + and cfg.minimum + and current_value >= cfg.value + ): + LOGGER.debug( + "Current config %s = %s exceeds the default of %s, skipping", + cfg.config_id.name, + current_value, + cfg.value, + ) + continue + + LOGGER.debug( + "Setting config %s = %s (old value %s)", + cfg.config_id.name, + cfg.value, + current_value, + ) + await self._cfg(cfg.config_id, cfg.value) async def get_free_buffers(self) -> Optional[int]: status, value = await self.getValue(self.types.EzspValueId.VALUE_FREE_BUFFERS) diff --git a/bellows/ezsp/v4/config.py b/bellows/ezsp/v4/config.py index 5b4ba8b5..35774af8 100644 --- a/bellows/ezsp/v4/config.py +++ b/bellows/ezsp/v4/config.py @@ -1,14 +1,8 @@ import voluptuous as vol from bellows.config import cv_optional_int, cv_uint16 -import bellows.multicast -from .types import ( - EmberZdoConfigurationFlags as zdo_flags, - EzspConfigId, - EzspDecisionId, - EzspPolicyId, -) +from .types import EzspConfigId, EzspDecisionId, EzspPolicyId EZSP_SCHEMA = { # @@ -16,9 +10,9 @@ # value 0xFF, the NCP will allocate all remaining configuration RAM towards # packet buffers, such that the resulting count will be the largest whole number # of packet buffers that can fit into the available memory - vol.Optional( - EzspConfigId.CONFIG_PACKET_BUFFER_COUNT.name, default=0xFF - ): cv_optional_int(min=1, max=255), + vol.Optional(EzspConfigId.CONFIG_PACKET_BUFFER_COUNT.name): cv_optional_int( + min=1, max=255 + ), # The maximum number of router neighbors the stack can keep track of. A neighbor # is a node within radio range vol.Optional(EzspConfigId.CONFIG_NEIGHBOR_TABLE_SIZE.name): cv_optional_int( @@ -40,15 +34,12 @@ # maintain for the application. (Note: The total number of such address # associations maintained by the NCP is the sum of the value of this setting and # the value of EZSP_CONFIG_TRUST_CENTER_ADDRESS_CA CHE_SIZE) - vol.Optional( - EzspConfigId.CONFIG_ADDRESS_TABLE_SIZE.name, default=16 - ): cv_optional_int(min=0), + vol.Optional(EzspConfigId.CONFIG_ADDRESS_TABLE_SIZE.name): cv_optional_int(min=0), # # The maximum number of multicast groups that the device may be a member of - vol.Optional( - EzspConfigId.CONFIG_MULTICAST_TABLE_SIZE.name, - default=bellows.multicast.Multicast.TABLE_SIZE, - ): cv_optional_int(min=0, max=255), + vol.Optional(EzspConfigId.CONFIG_MULTICAST_TABLE_SIZE.name): cv_optional_int( + min=0, max=255 + ), # # The maximum number of destinations to which a node can route messages. This # includes both messages originating at this node and those relayed for others @@ -72,14 +63,14 @@ ), # # Specifies the stack profile - vol.Optional(EzspConfigId.CONFIG_STACK_PROFILE.name, default=2): cv_optional_int( + vol.Optional(EzspConfigId.CONFIG_STACK_PROFILE.name): cv_optional_int( min=0, max=255 ), # # The security level used for security at the MAC and network layers. The # supported values are 0 (no security) and 5 (payload is encrypted and a # four-byte MIC is used for authentication) - vol.Optional(EzspConfigId.CONFIG_SECURITY_LEVEL.name, default=5): cv_optional_int( + vol.Optional(EzspConfigId.CONFIG_SECURITY_LEVEL.name): cv_optional_int( min=0, max=5 ), # @@ -87,14 +78,14 @@ vol.Optional(EzspConfigId.CONFIG_MAX_HOPS.name): cv_optional_int(min=0, max=30), # # The maximum number of end device children that a router will support - vol.Optional( - EzspConfigId.CONFIG_MAX_END_DEVICE_CHILDREN.name, default=32 - ): cv_optional_int(min=0, max=32), + vol.Optional(EzspConfigId.CONFIG_MAX_END_DEVICE_CHILDREN.name): cv_optional_int( + min=0, max=32 + ), # # The maximum amount of time that the MAC will hold a message for indirect # transmission to a child vol.Optional( - EzspConfigId.CONFIG_INDIRECT_TRANSMISSION_TIMEOUT.name, default=7680 + EzspConfigId.CONFIG_INDIRECT_TRANSMISSION_TIMEOUT.name ): cv_optional_int(min=0, max=30000), # # The maximum amount of time that an end device child can wait between polls. @@ -102,9 +93,9 @@ # device from its tables. The timeout corresponding to a value of zero is 10 # seconds. The timeout corresponding to a nonzero value N is 2^N minutes, # ranging from 2^1 = 2 minutes to 2^14 = 16384 minutes - vol.Optional( - EzspConfigId.CONFIG_END_DEVICE_POLL_TIMEOUT.name, default=60 - ): cv_optional_int(min=0, max=255), + vol.Optional(EzspConfigId.CONFIG_END_DEVICE_POLL_TIMEOUT.name): cv_optional_int( + min=0, max=255 + ), # # The maximum amount of time that a mobile node can wait between polls. If no # poll is heard within this timeout, then the parent removes the mobile node @@ -135,16 +126,16 @@ # by the NCP is the sum of the value of this setting and the value of # EZSP_CONFIG_ADDRESS_TABLE_SIZE) vol.Optional( - EzspConfigId.CONFIG_TRUST_CENTER_ADDRESS_CACHE_SIZE.name, default=2 + EzspConfigId.CONFIG_TRUST_CENTER_ADDRESS_CACHE_SIZE.name ): cv_optional_int(min=0), # # The size of the source route table - vol.Optional( - EzspConfigId.CONFIG_SOURCE_ROUTE_TABLE_SIZE.name, default=16 - ): cv_optional_int(min=0), + vol.Optional(EzspConfigId.CONFIG_SOURCE_ROUTE_TABLE_SIZE.name): cv_optional_int( + min=0 + ), # The units used for timing out end devices on their parent vol.Optional( - EzspConfigId.CONFIG_END_DEVICE_POLL_TIMEOUT_SHIFT.name, default=8 + EzspConfigId.CONFIG_END_DEVICE_POLL_TIMEOUT_SHIFT.name ): cv_optional_int(min=0, max=10), # # The number of blocks of a fragmented message that can be sent in a single @@ -159,9 +150,7 @@ # # The size of the Key Table used for storing individual link keys (if the device # is a Trust Center) or Application Link Keys (if the device is a normal node) - vol.Optional(EzspConfigId.CONFIG_KEY_TABLE_SIZE.name, default=4): cv_optional_int( - min=0 - ), + vol.Optional(EzspConfigId.CONFIG_KEY_TABLE_SIZE.name): cv_optional_int(min=0), # # The APS ACK timeout value (ms). The stack waits this amount of time between # resends of APS retried messages @@ -182,7 +171,7 @@ # The number of PAN id conflict reports that must be received by the network # manager within one minute to trigger a PAN id change vol.Optional( - EzspConfigId.CONFIG_PAN_ID_CONFLICT_REPORT_THRESHOLD.name, default=2 + EzspConfigId.CONFIG_PAN_ID_CONFLICT_REPORT_THRESHOLD.name ): cv_optional_int(min=1, max=63), # # The timeout value in minutes for how long the Trust Center or a normal node @@ -210,11 +199,9 @@ # reply to an incoming message, the application must check the APS options # bitfield within the incomingMessageHandler callback to see if the # EMBER_APS_OPTION_ZDO_RESPONSE_REQUIRED flag is set - vol.Optional( - EzspConfigId.CONFIG_APPLICATION_ZDO_FLAGS.name, - default=zdo_flags.APP_RECEIVES_SUPPORTED_ZDO_REQUESTS - | zdo_flags.APP_HANDLES_UNSUPPORTED_ZDO_REQUESTS, - ): cv_optional_int(min=0, max=255), + vol.Optional(EzspConfigId.CONFIG_APPLICATION_ZDO_FLAGS.name): cv_optional_int( + min=0, max=255 + ), # # The maximum number of broadcasts during a single broadcast timeout period vol.Optional(EzspConfigId.CONFIG_BROADCAST_TABLE_SIZE.name): cv_optional_int( @@ -227,9 +214,9 @@ ), # # The number of supported networks - vol.Optional( - EzspConfigId.CONFIG_SUPPORTED_NETWORKS.name, default=1 - ): cv_optional_int(min=1, max=2), + vol.Optional(EzspConfigId.CONFIG_SUPPORTED_NETWORKS.name): cv_optional_int( + min=1, max=2 + ), # # Whether multicasts are sent to the RxOnWhenIdle=true address (0xFFFD) or the # sleepy broadcast address (0xFFFF). The RxOnWhenIdle=true address is the ZigBee diff --git a/bellows/ezsp/v8/config.py b/bellows/ezsp/v8/config.py index cf1aadb0..1b297034 100644 --- a/bellows/ezsp/v8/config.py +++ b/bellows/ezsp/v8/config.py @@ -20,8 +20,7 @@ ): cv_optional_int(min=0, max=14), vol.Optional(EzspConfigId.CONFIG_KEY_TABLE_SIZE.name): cv_optional_int(min=0), vol.Optional( - EzspConfigId.CONFIG_TC_REJOINS_USING_WELL_KNOWN_KEY_TIMEOUT_S.name, - default=90, + EzspConfigId.CONFIG_TC_REJOINS_USING_WELL_KNOWN_KEY_TIMEOUT_S.name ): cv_optional_int(min=0), }, ) diff --git a/bellows/multicast.py b/bellows/multicast.py index ad5f522f..db0ba3cf 100644 --- a/bellows/multicast.py +++ b/bellows/multicast.py @@ -8,8 +8,6 @@ class Multicast: """Multicast table controller for EZSP.""" - TABLE_SIZE = 16 - def __init__(self, ezsp): self._ezsp = ezsp self._multicast = {} diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 580e2cad..007b8798 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -32,7 +32,7 @@ from bellows.ezsp.v8.types.named import EmberDeviceUpdate import bellows.multicast import bellows.types as t -from bellows.zigbee.device import EZSPCoordinator, EZSPEndpoint +from bellows.zigbee.device import EZSPEndpoint import bellows.zigbee.util as util APS_ACK_TIMEOUT = 120 @@ -181,7 +181,7 @@ async def start_network(self): except KeyError: db_device = None - ezsp_device = EZSPCoordinator( + ezsp_device = zigpy.device.Device( application=self, ieee=self.state.node_info.ieee, nwk=self.state.node_info.nwk, diff --git a/bellows/zigbee/device.py b/bellows/zigbee/device.py index 1adbc307..fcfff160 100644 --- a/bellows/zigbee/device.py +++ b/bellows/zigbee/device.py @@ -7,7 +7,6 @@ import zigpy.endpoint import zigpy.util import zigpy.zdo -import zigpy.zdo.types as zdo_t import bellows.types as t @@ -54,24 +53,3 @@ async def remove_from_group(self, grp_id: int) -> t.EmberStatus: app.groups[grp_id].remove_member(self) return status - - -class EZSPZDOEndpoint(zigpy.zdo.ZDO): - @property - def app(self) -> zigpy.application.ControllerApplication: - return self.device.application - - def make_zdo_reply(self, cmd: zdo_t.ZDOCmd, **kwargs): - """Provides a way to create ZDO commands with schemas. Currently does nothing.""" - return list(kwargs.values()) - - -class EZSPCoordinator(zigpy.device.Device): - """Zigpy Device representing Coordinator.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - assert hasattr(self, "zdo") - self.zdo = EZSPZDOEndpoint(self) - self.endpoints[0] = self.zdo diff --git a/tests/test_application.py b/tests/test_application.py index 2a23d7d4..5ccd17f6 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -3,6 +3,7 @@ import pytest import zigpy.config +import zigpy.device import zigpy.exceptions import zigpy.types as zigpy_t import zigpy.zdo.types as zdo_t @@ -1264,7 +1265,7 @@ async def test_shutdown(app): @pytest.fixture def coordinator(app, ieee): - dev = bellows.zigbee.device.EZSPCoordinator(app, ieee, 0x0000) + dev = zigpy.device.Device(app, ieee, 0x0000) dev.endpoints[1] = bellows.zigbee.device.EZSPEndpoint(dev, 1) dev.model = dev.endpoints[1].model dev.manufacturer = dev.endpoints[1].manufacturer diff --git a/tests/test_application_device.py b/tests/test_application_device.py deleted file mode 100644 index 37f6e3d6..00000000 --- a/tests/test_application_device.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest -import zigpy.endpoint -import zigpy.types as t -import zigpy.zdo.types as zdo_t - -from bellows.zigbee.device import EZSPCoordinator, EZSPEndpoint - -from tests.async_mock import AsyncMock -from tests.test_application import app, ezsp_mock, make_app - - -@pytest.fixture -def app_and_coordinator(app): - app.state.node_info.ieee = t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:11") - app.state.node_info.nwk = t.NWK(0x0000) - - coordinator = EZSPCoordinator( - application=app, - ieee=app.state.node_info.ieee, - nwk=app.state.node_info.nwk, - ) - coordinator.node_desc = zdo_t.NodeDescriptor() - - app.devices[app.state.node_info.ieee] = coordinator - - # The coordinator device does not respond to attribute reads - coordinator.endpoints[1] = EZSPEndpoint(coordinator, 1) - coordinator.endpoints[1].status = zigpy.endpoint.Status.ZDO_INIT - - return app, coordinator diff --git a/tests/test_ezsp_protocol.py b/tests/test_ezsp_protocol.py index 591388ad..6535324e 100644 --- a/tests/test_ezsp_protocol.py +++ b/tests/test_ezsp_protocol.py @@ -118,6 +118,16 @@ async def test_cfg_initialize_skip(prot_hndl): t.EzspConfigId.CONFIG_END_DEVICE_POLL_TIMEOUT, ANY ) + with p1, p2, p3: + await prot_hndl.initialize( + {"ezsp_config": {"CONFIG_MULTICAST_TABLE_SIZE": 123}} + ) + + # Config is overridden + prot_hndl.setConfigurationValue.assert_any_call( + t.EzspConfigId.CONFIG_MULTICAST_TABLE_SIZE, 123 + ) + with p1, p2, p3: await prot_hndl.initialize({"ezsp_config": {}})