Skip to content

Commit 2ec05d0

Browse files
authored
Set adapter concurrency using chip info XNCP command (#681)
* XNCP command to read chip info * Set the adapter concurrency using chip info * Fix unit tests * Add some more tests * Clean up for new zigpy APIs * Adjust unit test to use a compatible concurrency setting
1 parent 07d7cf0 commit 2ec05d0

File tree

5 files changed

+156
-2
lines changed

5 files changed

+156
-2
lines changed

bellows/ezsp/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,3 +778,21 @@ async def xncp_get_flow_control_type(self) -> FlowControlType:
778778
"""Get flow control type."""
779779
rsp = await self.send_xncp_frame(xncp.GetFlowControlTypeReq())
780780
return rsp.flow_control_type
781+
782+
async def xncp_get_chip_info(self) -> xncp.GetChipInfoRsp:
783+
"""Get the part number."""
784+
return await self.send_xncp_frame(xncp.GetChipInfoReq())
785+
786+
async def get_default_adapter_concurrency(self) -> int:
787+
"""Get the recommended concurrency based on chip information."""
788+
if FirmwareFeatures.CHIP_INFO not in self._xncp_features:
789+
return 8
790+
791+
chip_info = await self.xncp_get_chip_info()
792+
793+
# Usually 98304 bytes for MG21
794+
if chip_info.ram_size < 100000:
795+
return 8
796+
797+
# Usually 262144 bytes for MG24
798+
return 32

bellows/ezsp/xncp.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,14 @@ class XncpCommandId(t.enum16):
3939
GET_MFG_TOKEN_OVERRIDE_REQ = 0x0002
4040
GET_BUILD_STRING_REQ = 0x0003
4141
GET_FLOW_CONTROL_TYPE_REQ = 0x0004
42+
GET_CHIP_INFO_REQ = 0x0005
4243

4344
GET_SUPPORTED_FEATURES_RSP = GET_SUPPORTED_FEATURES_REQ | 0x8000
4445
SET_SOURCE_ROUTE_RSP = SET_SOURCE_ROUTE_REQ | 0x8000
4546
GET_MFG_TOKEN_OVERRIDE_RSP = GET_MFG_TOKEN_OVERRIDE_REQ | 0x8000
4647
GET_BUILD_STRING_RSP = GET_BUILD_STRING_REQ | 0x8000
4748
GET_FLOW_CONTROL_TYPE_RSP = GET_FLOW_CONTROL_TYPE_REQ | 0x8000
49+
GET_CHIP_INFO_RSP = GET_CHIP_INFO_REQ | 0x8000
4850

4951
UNKNOWN = 0xFFFF
5052

@@ -106,6 +108,9 @@ class FirmwareFeatures(t.bitmap32):
106108
# The flow control type (software or hardware) can be queried
107109
FLOW_CONTROL_TYPE = 1 << 4
108110

111+
# Chip info (e.g. name, RAM size) can be read
112+
CHIP_INFO = 1 << 5
113+
109114

110115
class XncpCommandPayload(t.Struct):
111116
pass
@@ -167,6 +172,17 @@ class GetFlowControlTypeRsp(XncpCommandPayload):
167172
flow_control_type: FlowControlType
168173

169174

175+
@register_command(XncpCommandId.GET_CHIP_INFO_REQ)
176+
class GetChipInfoReq(XncpCommandPayload):
177+
pass
178+
179+
180+
@register_command(XncpCommandId.GET_CHIP_INFO_RSP)
181+
class GetChipInfoRsp(XncpCommandPayload):
182+
ram_size: t.uint32_t
183+
part_number: t.CharacterString
184+
185+
170186
@register_command(XncpCommandId.UNKNOWN)
171187
class Unknown(XncpCommandPayload):
172188
pass

bellows/zigbee/application.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,19 @@ async def start_network(self):
252252
self._multicast = bellows.multicast.Multicast(ezsp)
253253
await self._multicast.startup(ezsp_device)
254254

255+
if self._config[zigpy.config.CONF_MAX_CONCURRENT_REQUESTS] in (
256+
None,
257+
zigpy.config.defaults.CONF_MAX_CONCURRENT_REQUESTS_DEFAULT,
258+
):
259+
max_concurrent_requests = await self._ezsp.get_default_adapter_concurrency()
260+
else:
261+
max_concurrent_requests = self._config[
262+
zigpy.config.CONF_MAX_CONCURRENT_REQUESTS
263+
]
264+
265+
LOGGER.debug("Setting adapter concurrency to %d", max_concurrent_requests)
266+
self._concurrent_requests_semaphore.max_concurrency = max_concurrent_requests
267+
255268
async def load_network_info(self, *, load_devices=False) -> None:
256269
ezsp = self._ezsp
257270

@@ -310,6 +323,11 @@ async def load_network_info(self, *, load_devices=False) -> None:
310323
else:
311324
flow_control = None
312325

326+
if FirmwareFeatures.CHIP_INFO in ezsp._xncp_features:
327+
chip_info = await ezsp.xncp_get_chip_info()
328+
else:
329+
chip_info = None
330+
313331
self.state.network_info = zigpy.state.NetworkInfo(
314332
source=f"bellows@{LIB_VERSION}",
315333
extended_pan_id=zigpy.types.ExtendedPanId(nwk_params.extendedPanId),
@@ -327,13 +345,18 @@ async def load_network_info(self, *, load_devices=False) -> None:
327345
stack_specific=stack_specific,
328346
metadata={
329347
"ezsp": {
348+
"chip_info": (
349+
chip_info.as_dict(recursive=True)
350+
if chip_info is not None
351+
else None
352+
),
330353
"stack_version": ezsp.ezsp_version,
331354
"can_burn_userdata_custom_eui64": can_burn_userdata_custom_eui64,
332355
"can_rewrite_custom_eui64": can_rewrite_custom_eui64,
333356
"flow_control": (
334357
flow_control.name.lower() if flow_control is not None else None
335358
),
336-
}
359+
},
337360
},
338361
)
339362

tests/test_application.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from bellows.exception import ControllerError, EzspError
1818
import bellows.ezsp as ezsp
1919
from bellows.ezsp.v9.commands import GetTokenDataRsp
20-
from bellows.ezsp.xncp import FirmwareFeatures, FlowControlType
20+
from bellows.ezsp.xncp import FirmwareFeatures, FlowControlType, GetChipInfoRsp
2121
import bellows.types
2222
import bellows.types as t
2323
import bellows.types.struct
@@ -1786,6 +1786,36 @@ async def test_startup_new_coordinator_no_groups_joined(app, ieee):
17861786
assert app._ezsp._protocol.setMulticastTableEntry.mock_calls == []
17871787

17881788

1789+
@pytest.mark.parametrize(
1790+
("concurrency_config", "chip_concurrency", "expected_concurrency"),
1791+
[
1792+
(None, 32, 32), # Default config (None) uses chip
1793+
(8, 16, 16), # Default fallback (8) uses chip
1794+
(16, 32, 16), # Explicit config overrides chip
1795+
(12, 32, 12), # Low explicit config overrides chip
1796+
],
1797+
)
1798+
async def test_startup_concurrency_setting(
1799+
app, ieee, concurrency_config, chip_concurrency, expected_concurrency
1800+
):
1801+
"""Test that adapter concurrency is set correctly based on configuration."""
1802+
app._config[zigpy.config.CONF_MAX_CONCURRENT_REQUESTS] = concurrency_config
1803+
1804+
with mock_for_startup(app, ieee) as ezsp:
1805+
ezsp._xncp_features |= FirmwareFeatures.CHIP_INFO
1806+
ezsp.get_default_adapter_concurrency = AsyncMock(return_value=chip_concurrency)
1807+
ezsp.xncp_get_chip_info = AsyncMock(
1808+
return_value=GetChipInfoRsp(ram_size=0, part_number="")
1809+
)
1810+
1811+
await app.connect()
1812+
await app.start_network()
1813+
1814+
assert (
1815+
app._concurrent_requests_semaphore.max_concurrency == expected_concurrency
1816+
)
1817+
1818+
17891819
@pytest.mark.parametrize(
17901820
"scan_results",
17911821
[
@@ -1964,6 +1994,7 @@ def zigpy_backup() -> zigpy.backups.NetworkBackup:
19641994
"flow_control": "hardware",
19651995
"can_burn_userdata_custom_eui64": True,
19661996
"can_rewrite_custom_eui64": True,
1997+
"chip_info": None,
19671998
}
19681999
},
19692000
),
@@ -1999,6 +2030,26 @@ async def test_load_network_info_xncp_flow_control(
19992030
assert app.state.network_info == zigpy_backup.network_info
20002031

20012032

2033+
async def test_load_network_info_chip_info(
2034+
app: ControllerApplication,
2035+
ieee: zigpy_t.EUI64,
2036+
) -> None:
2037+
"""Test that chip info is included in network metadata when available."""
2038+
app._ezsp._xncp_features |= FirmwareFeatures.CHIP_INFO
2039+
expected_chip_info = GetChipInfoRsp(
2040+
ram_size=262144, part_number="EFR32MG24A020F1536IM48"
2041+
)
2042+
app._ezsp.xncp_get_chip_info = AsyncMock(return_value=expected_chip_info)
2043+
2044+
await app.load_network_info(load_devices=True)
2045+
2046+
# Check that chip info is included in the metadata
2047+
assert app.state.network_info.metadata["ezsp"]["chip_info"] == {
2048+
"ram_size": 262144,
2049+
"part_number": "EFR32MG24A020F1536IM48",
2050+
}
2051+
2052+
20022053
async def test_write_network_info(
20032054
app: ControllerApplication,
20042055
ieee: zigpy_t.EUI64,

tests/test_ezsp.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,3 +972,49 @@ def test_frame_parsing_error_doesnt_disconnect(ezsp_f, caplog):
972972
ezsp_f.frame_received(b"test")
973973

974974
assert "Failed to parse frame" in caplog.text
975+
976+
977+
async def test_xncp_get_chip_info(ezsp_f):
978+
"""Test getting chip info via XNCP."""
979+
ezsp_f._xncp_features = xncp.FirmwareFeatures.CHIP_INFO
980+
981+
# Mock the XNCP response
982+
expected_response = xncp.GetChipInfoRsp(
983+
ram_size=262144, part_number="EFR32MG24A020F1536IM48"
984+
)
985+
986+
with patch.object(
987+
ezsp_f, "send_xncp_frame", new=AsyncMock(return_value=expected_response)
988+
) as mock_send:
989+
result = await ezsp_f.xncp_get_chip_info()
990+
991+
assert result == expected_response
992+
assert mock_send.mock_calls == [call(xncp.GetChipInfoReq())]
993+
994+
995+
@pytest.mark.parametrize(
996+
"chip_info_available,ram_size,part_number,expected_concurrency",
997+
[
998+
(False, None, None, 8), # No chip info feature
999+
(True, 98304, "EFR32MG21A020F1024IM32", 8), # MG21 (low RAM)
1000+
(True, 262144, "EFR32MG24A020F1536IM48", 32), # MG24 (high RAM)
1001+
],
1002+
)
1003+
async def test_get_default_adapter_concurrency(
1004+
ezsp_f,
1005+
chip_info_available: bool,
1006+
ram_size: int,
1007+
part_number: str,
1008+
expected_concurrency: int,
1009+
) -> None:
1010+
"""Test default concurrency based on chip info availability and RAM size."""
1011+
if chip_info_available:
1012+
ezsp_f._xncp_features = xncp.FirmwareFeatures.CHIP_INFO
1013+
chip_info = xncp.GetChipInfoRsp(ram_size=ram_size, part_number=part_number)
1014+
with patch.object(ezsp_f, "xncp_get_chip_info", return_value=chip_info):
1015+
result = await ezsp_f.get_default_adapter_concurrency()
1016+
else:
1017+
ezsp_f._xncp_features = xncp.FirmwareFeatures(0) # No CHIP_INFO feature
1018+
result = await ezsp_f.get_default_adapter_concurrency()
1019+
1020+
assert result == expected_concurrency

0 commit comments

Comments
 (0)