diff --git a/bellows/ezsp/__init__.py b/bellows/ezsp/__init__.py index 57abc2dc..311b0afd 100644 --- a/bellows/ezsp/__init__.py +++ b/bellows/ezsp/__init__.py @@ -778,3 +778,21 @@ async def xncp_get_flow_control_type(self) -> FlowControlType: """Get flow control type.""" rsp = await self.send_xncp_frame(xncp.GetFlowControlTypeReq()) return rsp.flow_control_type + + async def xncp_get_chip_info(self) -> xncp.GetChipInfoRsp: + """Get the part number.""" + return await self.send_xncp_frame(xncp.GetChipInfoReq()) + + async def get_default_adapter_concurrency(self) -> int: + """Get the recommended concurrency based on chip information.""" + if FirmwareFeatures.CHIP_INFO not in self._xncp_features: + return 8 + + chip_info = await self.xncp_get_chip_info() + + # Usually 98304 bytes for MG21 + if chip_info.ram_size < 100000: + return 8 + + # Usually 262144 bytes for MG24 + return 32 diff --git a/bellows/ezsp/xncp.py b/bellows/ezsp/xncp.py index ee5761f1..61cf6a80 100644 --- a/bellows/ezsp/xncp.py +++ b/bellows/ezsp/xncp.py @@ -39,12 +39,14 @@ class XncpCommandId(t.enum16): GET_MFG_TOKEN_OVERRIDE_REQ = 0x0002 GET_BUILD_STRING_REQ = 0x0003 GET_FLOW_CONTROL_TYPE_REQ = 0x0004 + GET_CHIP_INFO_REQ = 0x0005 GET_SUPPORTED_FEATURES_RSP = GET_SUPPORTED_FEATURES_REQ | 0x8000 SET_SOURCE_ROUTE_RSP = SET_SOURCE_ROUTE_REQ | 0x8000 GET_MFG_TOKEN_OVERRIDE_RSP = GET_MFG_TOKEN_OVERRIDE_REQ | 0x8000 GET_BUILD_STRING_RSP = GET_BUILD_STRING_REQ | 0x8000 GET_FLOW_CONTROL_TYPE_RSP = GET_FLOW_CONTROL_TYPE_REQ | 0x8000 + GET_CHIP_INFO_RSP = GET_CHIP_INFO_REQ | 0x8000 UNKNOWN = 0xFFFF @@ -106,6 +108,9 @@ class FirmwareFeatures(t.bitmap32): # The flow control type (software or hardware) can be queried FLOW_CONTROL_TYPE = 1 << 4 + # Chip info (e.g. name, RAM size) can be read + CHIP_INFO = 1 << 5 + class XncpCommandPayload(t.Struct): pass @@ -167,6 +172,17 @@ class GetFlowControlTypeRsp(XncpCommandPayload): flow_control_type: FlowControlType +@register_command(XncpCommandId.GET_CHIP_INFO_REQ) +class GetChipInfoReq(XncpCommandPayload): + pass + + +@register_command(XncpCommandId.GET_CHIP_INFO_RSP) +class GetChipInfoRsp(XncpCommandPayload): + ram_size: t.uint32_t + part_number: t.CharacterString + + @register_command(XncpCommandId.UNKNOWN) class Unknown(XncpCommandPayload): pass diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index 28e9bf62..ba5dfd91 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -252,6 +252,19 @@ async def start_network(self): self._multicast = bellows.multicast.Multicast(ezsp) await self._multicast.startup(ezsp_device) + if self._config[zigpy.config.CONF_MAX_CONCURRENT_REQUESTS] in ( + None, + zigpy.config.defaults.CONF_MAX_CONCURRENT_REQUESTS_DEFAULT, + ): + max_concurrent_requests = await self._ezsp.get_default_adapter_concurrency() + else: + max_concurrent_requests = self._config[ + zigpy.config.CONF_MAX_CONCURRENT_REQUESTS + ] + + LOGGER.debug("Setting adapter concurrency to %d", max_concurrent_requests) + self._concurrent_requests_semaphore.max_concurrency = max_concurrent_requests + async def load_network_info(self, *, load_devices=False) -> None: ezsp = self._ezsp @@ -310,6 +323,11 @@ async def load_network_info(self, *, load_devices=False) -> None: else: flow_control = None + if FirmwareFeatures.CHIP_INFO in ezsp._xncp_features: + chip_info = await ezsp.xncp_get_chip_info() + else: + chip_info = None + self.state.network_info = zigpy.state.NetworkInfo( source=f"bellows@{LIB_VERSION}", extended_pan_id=zigpy.types.ExtendedPanId(nwk_params.extendedPanId), @@ -327,13 +345,18 @@ async def load_network_info(self, *, load_devices=False) -> None: stack_specific=stack_specific, metadata={ "ezsp": { + "chip_info": ( + chip_info.as_dict(recursive=True) + if chip_info is not None + else None + ), "stack_version": ezsp.ezsp_version, "can_burn_userdata_custom_eui64": can_burn_userdata_custom_eui64, "can_rewrite_custom_eui64": can_rewrite_custom_eui64, "flow_control": ( flow_control.name.lower() if flow_control is not None else None ), - } + }, }, ) diff --git a/tests/test_application.py b/tests/test_application.py index c1e050eb..76cdb910 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -17,7 +17,7 @@ from bellows.exception import ControllerError, EzspError import bellows.ezsp as ezsp from bellows.ezsp.v9.commands import GetTokenDataRsp -from bellows.ezsp.xncp import FirmwareFeatures, FlowControlType +from bellows.ezsp.xncp import FirmwareFeatures, FlowControlType, GetChipInfoRsp import bellows.types import bellows.types as t import bellows.types.struct @@ -1786,6 +1786,36 @@ async def test_startup_new_coordinator_no_groups_joined(app, ieee): assert app._ezsp._protocol.setMulticastTableEntry.mock_calls == [] +@pytest.mark.parametrize( + ("concurrency_config", "chip_concurrency", "expected_concurrency"), + [ + (None, 32, 32), # Default config (None) uses chip + (8, 16, 16), # Default fallback (8) uses chip + (16, 32, 16), # Explicit config overrides chip + (12, 32, 12), # Low explicit config overrides chip + ], +) +async def test_startup_concurrency_setting( + app, ieee, concurrency_config, chip_concurrency, expected_concurrency +): + """Test that adapter concurrency is set correctly based on configuration.""" + app._config[zigpy.config.CONF_MAX_CONCURRENT_REQUESTS] = concurrency_config + + with mock_for_startup(app, ieee) as ezsp: + ezsp._xncp_features |= FirmwareFeatures.CHIP_INFO + ezsp.get_default_adapter_concurrency = AsyncMock(return_value=chip_concurrency) + ezsp.xncp_get_chip_info = AsyncMock( + return_value=GetChipInfoRsp(ram_size=0, part_number="") + ) + + await app.connect() + await app.start_network() + + assert ( + app._concurrent_requests_semaphore.max_concurrency == expected_concurrency + ) + + @pytest.mark.parametrize( "scan_results", [ @@ -1964,6 +1994,7 @@ def zigpy_backup() -> zigpy.backups.NetworkBackup: "flow_control": "hardware", "can_burn_userdata_custom_eui64": True, "can_rewrite_custom_eui64": True, + "chip_info": None, } }, ), @@ -1999,6 +2030,26 @@ async def test_load_network_info_xncp_flow_control( assert app.state.network_info == zigpy_backup.network_info +async def test_load_network_info_chip_info( + app: ControllerApplication, + ieee: zigpy_t.EUI64, +) -> None: + """Test that chip info is included in network metadata when available.""" + app._ezsp._xncp_features |= FirmwareFeatures.CHIP_INFO + expected_chip_info = GetChipInfoRsp( + ram_size=262144, part_number="EFR32MG24A020F1536IM48" + ) + app._ezsp.xncp_get_chip_info = AsyncMock(return_value=expected_chip_info) + + await app.load_network_info(load_devices=True) + + # Check that chip info is included in the metadata + assert app.state.network_info.metadata["ezsp"]["chip_info"] == { + "ram_size": 262144, + "part_number": "EFR32MG24A020F1536IM48", + } + + async def test_write_network_info( app: ControllerApplication, ieee: zigpy_t.EUI64, diff --git a/tests/test_ezsp.py b/tests/test_ezsp.py index 3a725d04..5747c769 100644 --- a/tests/test_ezsp.py +++ b/tests/test_ezsp.py @@ -972,3 +972,49 @@ def test_frame_parsing_error_doesnt_disconnect(ezsp_f, caplog): ezsp_f.frame_received(b"test") assert "Failed to parse frame" in caplog.text + + +async def test_xncp_get_chip_info(ezsp_f): + """Test getting chip info via XNCP.""" + ezsp_f._xncp_features = xncp.FirmwareFeatures.CHIP_INFO + + # Mock the XNCP response + expected_response = xncp.GetChipInfoRsp( + ram_size=262144, part_number="EFR32MG24A020F1536IM48" + ) + + with patch.object( + ezsp_f, "send_xncp_frame", new=AsyncMock(return_value=expected_response) + ) as mock_send: + result = await ezsp_f.xncp_get_chip_info() + + assert result == expected_response + assert mock_send.mock_calls == [call(xncp.GetChipInfoReq())] + + +@pytest.mark.parametrize( + "chip_info_available,ram_size,part_number,expected_concurrency", + [ + (False, None, None, 8), # No chip info feature + (True, 98304, "EFR32MG21A020F1024IM32", 8), # MG21 (low RAM) + (True, 262144, "EFR32MG24A020F1536IM48", 32), # MG24 (high RAM) + ], +) +async def test_get_default_adapter_concurrency( + ezsp_f, + chip_info_available: bool, + ram_size: int, + part_number: str, + expected_concurrency: int, +) -> None: + """Test default concurrency based on chip info availability and RAM size.""" + if chip_info_available: + ezsp_f._xncp_features = xncp.FirmwareFeatures.CHIP_INFO + chip_info = xncp.GetChipInfoRsp(ram_size=ram_size, part_number=part_number) + with patch.object(ezsp_f, "xncp_get_chip_info", return_value=chip_info): + result = await ezsp_f.get_default_adapter_concurrency() + else: + ezsp_f._xncp_features = xncp.FirmwareFeatures(0) # No CHIP_INFO feature + result = await ezsp_f.get_default_adapter_concurrency() + + assert result == expected_concurrency