zigpy 1.4.1__tar.gz → 1.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {zigpy-1.4.1/zigpy.egg-info → zigpy-1.5.0}/PKG-INFO +1 -1
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_application.py +2 -52
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_device.py +257 -9
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_endpoint.py +6 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_topology.py +2 -2
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_zcl.py +198 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/application.py +20 -37
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/config/__init__.py +2 -13
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/config/defaults.py +0 -7
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/device.py +76 -41
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/endpoint.py +8 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/ota/__init__.py +0 -10
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/ota/json_schemas.py +0 -61
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/ota/providers.py +0 -63
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/topology.py +1 -4
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/__init__.py +208 -151
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zdo/__init__.py +8 -0
- {zigpy-1.4.1 → zigpy-1.5.0/zigpy.egg-info}/PKG-INFO +1 -1
- {zigpy-1.4.1 → zigpy-1.5.0}/COPYING +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/LICENSE +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/README.md +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/pyproject.toml +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/setup.cfg +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/setup.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_app_state.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_appdb.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_appdb_migration.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_backups.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_config.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_datastructures.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_datastructures_cpython.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_event.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_group.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_listeners.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_packet_callbacks.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_quirks.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_quirks_registry.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_quirks_v2.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_serial.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_struct.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_types.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_zcl_clusters.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_zcl_foundation.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_zcl_helpers.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_zdo.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_zdo_types.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tests/test_zigbee_util.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tools/__init__.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/tools/regenerate_mypy_ignores.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/__init__.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/__init__.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v0.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v1.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v10.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v11.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v12.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v13.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v14.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v15.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v2.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v3.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v4.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v5.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v6.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v7.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v8.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/appdb_schemas/schema_v9.sql +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/backports/__init__.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/backups.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/config/validators.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/const.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/datastructures.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/event/__init__.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/event/event_base.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/exceptions.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/group.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/listeners.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/ota/image.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/ota/manager.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/ota/validators.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/profiles/__init__.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/profiles/zgp.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/profiles/zha.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/profiles/zll.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/py.typed +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/quirks/__init__.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/quirks/registry.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/quirks/v2/__init__.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/quirks/v2/homeassistant/__init__.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/quirks/v2/homeassistant/binary_sensor.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/quirks/v2/homeassistant/number.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/quirks/v2/homeassistant/sensor.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/serial.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/state.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/types/__init__.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/types/basic.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/types/named.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/types/struct.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/typing.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/util.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/clusters/__init__.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/clusters/closures.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/clusters/general.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/clusters/general_const.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/clusters/greenpower.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/clusters/homeautomation.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/clusters/hvac.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/clusters/lighting.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/clusters/lightlink.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/clusters/manufacturer_specific.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/clusters/measurement.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/clusters/protocol.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/clusters/security.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/clusters/smartenergy.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/clusters/wwah.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/foundation.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zcl/helpers.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zdo/types.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zgp/__init__.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy/zgp/types.py +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy.egg-info/SOURCES.txt +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy.egg-info/dependency_links.txt +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy.egg-info/requires.txt +0 -0
- {zigpy-1.4.1 → zigpy-1.5.0}/zigpy.egg-info/top_level.txt +0 -0
|
@@ -947,13 +947,7 @@ async def test_request(app, device, packet):
|
|
|
947
947
|
app.send_packet.reset_mock()
|
|
948
948
|
|
|
949
949
|
|
|
950
|
-
async def
|
|
951
|
-
app.send_packet.side_effect = [
|
|
952
|
-
DeliveryError("Failure"),
|
|
953
|
-
DeliveryError("Failure"),
|
|
954
|
-
None,
|
|
955
|
-
]
|
|
956
|
-
|
|
950
|
+
async def test_force_route_discovery(app, device, packet) -> None:
|
|
957
951
|
await app.request(
|
|
958
952
|
device=device,
|
|
959
953
|
profile=0x1234,
|
|
@@ -965,54 +959,10 @@ async def test_request_retrying_success(app, device, packet) -> None:
|
|
|
965
959
|
expect_reply=True,
|
|
966
960
|
use_ieee=False,
|
|
967
961
|
extended_timeout=False,
|
|
962
|
+
force_route_discovery=True,
|
|
968
963
|
)
|
|
969
964
|
|
|
970
965
|
assert app.send_packet.mock_calls == [
|
|
971
|
-
call(packet.replace(priority=t.PacketPriority.NORMAL)),
|
|
972
|
-
call(
|
|
973
|
-
packet.replace(
|
|
974
|
-
tx_options=packet.tx_options | t.TransmitOptions.FORCE_ROUTE_DISCOVERY,
|
|
975
|
-
priority=t.PacketPriority.NORMAL,
|
|
976
|
-
)
|
|
977
|
-
),
|
|
978
|
-
call(
|
|
979
|
-
packet.replace(
|
|
980
|
-
tx_options=packet.tx_options | t.TransmitOptions.FORCE_ROUTE_DISCOVERY,
|
|
981
|
-
priority=t.PacketPriority.NORMAL,
|
|
982
|
-
)
|
|
983
|
-
),
|
|
984
|
-
]
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
async def test_request_retrying_failure(app, device, packet) -> None:
|
|
988
|
-
app.send_packet.side_effect = [
|
|
989
|
-
DeliveryError("Failure"),
|
|
990
|
-
DeliveryError("Failure"),
|
|
991
|
-
DeliveryError("Failure"),
|
|
992
|
-
]
|
|
993
|
-
|
|
994
|
-
with pytest.raises(DeliveryError):
|
|
995
|
-
await app.request(
|
|
996
|
-
device=device,
|
|
997
|
-
profile=0x1234,
|
|
998
|
-
cluster=0x0006,
|
|
999
|
-
src_ep=0x9A,
|
|
1000
|
-
dst_ep=0xBC,
|
|
1001
|
-
sequence=0xDE,
|
|
1002
|
-
data=b"test data",
|
|
1003
|
-
expect_reply=True,
|
|
1004
|
-
use_ieee=False,
|
|
1005
|
-
extended_timeout=False,
|
|
1006
|
-
)
|
|
1007
|
-
|
|
1008
|
-
assert app.send_packet.mock_calls == [
|
|
1009
|
-
call(packet.replace(priority=t.PacketPriority.NORMAL)),
|
|
1010
|
-
call(
|
|
1011
|
-
packet.replace(
|
|
1012
|
-
tx_options=packet.tx_options | t.TransmitOptions.FORCE_ROUTE_DISCOVERY,
|
|
1013
|
-
priority=t.PacketPriority.NORMAL,
|
|
1014
|
-
)
|
|
1015
|
-
),
|
|
1016
966
|
call(
|
|
1017
967
|
packet.replace(
|
|
1018
968
|
tx_options=packet.tx_options | t.TransmitOptions.FORCE_ROUTE_DISCOVERY,
|
|
@@ -523,10 +523,6 @@ async def test_update_device_firmware_already_in_progress(dev, caplog):
|
|
|
523
523
|
|
|
524
524
|
@patch("zigpy.ota.manager.MAX_TIME_WITHOUT_PROGRESS", 0.1)
|
|
525
525
|
@patch("zigpy.device.AFTER_OTA_ATTR_READ_DELAY", 0.01)
|
|
526
|
-
@patch(
|
|
527
|
-
"zigpy.device.OTA_RETRY_DECORATOR",
|
|
528
|
-
zigpy.util.retryable_request(tries=1, delay=0.01),
|
|
529
|
-
)
|
|
530
526
|
async def test_update_device_firmware(monkeypatch, dev, caplog):
|
|
531
527
|
"""Test that device firmware updates execute the expected calls."""
|
|
532
528
|
ep = dev.add_endpoint(1)
|
|
@@ -947,10 +943,6 @@ async def test_update_device_firmware(monkeypatch, dev, caplog):
|
|
|
947
943
|
|
|
948
944
|
@patch("zigpy.ota.manager.MAX_TIME_WITHOUT_PROGRESS", 0.1)
|
|
949
945
|
@patch("zigpy.device.AFTER_OTA_ATTR_READ_DELAY", 0.01)
|
|
950
|
-
@patch(
|
|
951
|
-
"zigpy.device.OTA_RETRY_DECORATOR",
|
|
952
|
-
zigpy.util.retryable_request(tries=1, delay=0.01),
|
|
953
|
-
)
|
|
954
946
|
async def test_update_legrand_device_firmware(monkeypatch, dev, caplog):
|
|
955
947
|
"""Legrand device (manufacturer_code == 4129) firmware update expects the "image_block" command "maximum_data_size" to be complied with."""
|
|
956
948
|
ep = dev.add_endpoint(1)
|
|
@@ -1359,7 +1351,7 @@ async def test_request_exception_propagation(dev):
|
|
|
1359
1351
|
data=t.SerializableBytes(
|
|
1360
1352
|
foundation.ZCLHeader(
|
|
1361
1353
|
frame_control=foundation.FrameControl(
|
|
1362
|
-
frame_type=foundation.FrameType.
|
|
1354
|
+
frame_type=foundation.FrameType.GLOBAL_COMMAND,
|
|
1363
1355
|
is_manufacturer_specific=False,
|
|
1364
1356
|
direction=foundation.Direction.Server_to_Client,
|
|
1365
1357
|
disable_default_response=True,
|
|
@@ -2227,3 +2219,259 @@ async def test_update_firmware_triggers_reinterview(monkeypatch, dev):
|
|
|
2227
2219
|
|
|
2228
2220
|
assert result == foundation.Status.SUCCESS
|
|
2229
2221
|
dev.reinterview.assert_awaited_once()
|
|
2222
|
+
|
|
2223
|
+
|
|
2224
|
+
async def test_request_retry_success(app) -> None:
|
|
2225
|
+
"""Test retry logic succeeding after a few attempts."""
|
|
2226
|
+
tsn = 0x12
|
|
2227
|
+
|
|
2228
|
+
dev = app.add_device(nwk=0x1234, ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:11"))
|
|
2229
|
+
dev.node_desc = make_node_desc()
|
|
2230
|
+
|
|
2231
|
+
ep = dev.add_endpoint(1)
|
|
2232
|
+
ep.status = endpoint.Status.ZDO_INIT
|
|
2233
|
+
ep.add_input_cluster(Basic.cluster_id)
|
|
2234
|
+
|
|
2235
|
+
attempt = 0
|
|
2236
|
+
|
|
2237
|
+
def send_packet(*args, **kwargs) -> None:
|
|
2238
|
+
nonlocal attempt
|
|
2239
|
+
attempt += 1
|
|
2240
|
+
|
|
2241
|
+
if attempt < 3:
|
|
2242
|
+
raise zigpy.exceptions.DeliveryError("Failure")
|
|
2243
|
+
|
|
2244
|
+
asyncio.get_running_loop().call_soon(
|
|
2245
|
+
dev.packet_received,
|
|
2246
|
+
t.ZigbeePacket(
|
|
2247
|
+
profile_id=260,
|
|
2248
|
+
cluster_id=Basic.cluster_id,
|
|
2249
|
+
src_ep=1,
|
|
2250
|
+
dst_ep=1,
|
|
2251
|
+
data=t.SerializableBytes(
|
|
2252
|
+
foundation.ZCLHeader(
|
|
2253
|
+
frame_control=foundation.FrameControl(
|
|
2254
|
+
frame_type=foundation.FrameType.GLOBAL_COMMAND,
|
|
2255
|
+
is_manufacturer_specific=False,
|
|
2256
|
+
direction=foundation.Direction.Server_to_Client,
|
|
2257
|
+
disable_default_response=True,
|
|
2258
|
+
reserved=0,
|
|
2259
|
+
),
|
|
2260
|
+
tsn=tsn,
|
|
2261
|
+
command_id=foundation.GeneralCommand.Default_Response,
|
|
2262
|
+
manufacturer=None,
|
|
2263
|
+
).serialize()
|
|
2264
|
+
+ (
|
|
2265
|
+
foundation.GENERAL_COMMANDS[
|
|
2266
|
+
foundation.GeneralCommand.Default_Response
|
|
2267
|
+
]
|
|
2268
|
+
.schema(
|
|
2269
|
+
command_id=Basic.ServerCommandDefs.reset_fact_default.id,
|
|
2270
|
+
status=foundation.Status.SUCCESS,
|
|
2271
|
+
)
|
|
2272
|
+
.serialize()
|
|
2273
|
+
)
|
|
2274
|
+
),
|
|
2275
|
+
src=t.AddrModeAddress(
|
|
2276
|
+
addr_mode=t.AddrMode.NWK,
|
|
2277
|
+
address=dev.nwk,
|
|
2278
|
+
),
|
|
2279
|
+
dst=t.AddrModeAddress(
|
|
2280
|
+
addr_mode=t.AddrMode.NWK,
|
|
2281
|
+
address=0x0000,
|
|
2282
|
+
),
|
|
2283
|
+
),
|
|
2284
|
+
)
|
|
2285
|
+
|
|
2286
|
+
app.send_packet.side_effect = send_packet
|
|
2287
|
+
dev.get_sequence = MagicMock(return_value=tsn)
|
|
2288
|
+
|
|
2289
|
+
rsp = await dev.endpoints[1].basic.reset_fact_default()
|
|
2290
|
+
assert rsp == foundation.DefaultResponse(
|
|
2291
|
+
command_id=Basic.ServerCommandDefs.reset_fact_default.id,
|
|
2292
|
+
status=foundation.Status.SUCCESS,
|
|
2293
|
+
)
|
|
2294
|
+
|
|
2295
|
+
assert len(app.send_packet.mock_calls) == 3
|
|
2296
|
+
|
|
2297
|
+
|
|
2298
|
+
async def test_request_retry_failure(app) -> None:
|
|
2299
|
+
"""Test retry logic when all attempts fail."""
|
|
2300
|
+
dev = app.add_device(nwk=0x1234, ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:11"))
|
|
2301
|
+
dev.node_desc = make_node_desc()
|
|
2302
|
+
|
|
2303
|
+
ep = dev.add_endpoint(1)
|
|
2304
|
+
ep.status = endpoint.Status.ZDO_INIT
|
|
2305
|
+
ep.add_input_cluster(Basic.cluster_id)
|
|
2306
|
+
|
|
2307
|
+
app.send_packet.side_effect = [
|
|
2308
|
+
zigpy.exceptions.DeliveryError("Failure"),
|
|
2309
|
+
zigpy.exceptions.DeliveryError("Failure"),
|
|
2310
|
+
zigpy.exceptions.DeliveryError("Failure"),
|
|
2311
|
+
zigpy.exceptions.DeliveryError("Failure"),
|
|
2312
|
+
]
|
|
2313
|
+
|
|
2314
|
+
with pytest.raises(zigpy.exceptions.DeliveryError):
|
|
2315
|
+
await dev.request(
|
|
2316
|
+
profile=0x1234,
|
|
2317
|
+
cluster=0x0006,
|
|
2318
|
+
src_ep=1,
|
|
2319
|
+
dst_ep=1,
|
|
2320
|
+
sequence=0xDE,
|
|
2321
|
+
data=b"test data",
|
|
2322
|
+
expect_reply=True,
|
|
2323
|
+
use_ieee=False,
|
|
2324
|
+
retries=3,
|
|
2325
|
+
)
|
|
2326
|
+
|
|
2327
|
+
packet = t.ZigbeePacket(
|
|
2328
|
+
priority=None,
|
|
2329
|
+
src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000),
|
|
2330
|
+
src_ep=1,
|
|
2331
|
+
dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=dev.nwk),
|
|
2332
|
+
dst_ep=1,
|
|
2333
|
+
source_route=None,
|
|
2334
|
+
extended_timeout=False,
|
|
2335
|
+
tsn=222,
|
|
2336
|
+
profile_id=4660,
|
|
2337
|
+
cluster_id=6,
|
|
2338
|
+
data=t.SerializableBytes(b"test data"),
|
|
2339
|
+
tx_options=t.TransmitOptions.NONE,
|
|
2340
|
+
radius=0,
|
|
2341
|
+
non_member_radius=0,
|
|
2342
|
+
)
|
|
2343
|
+
|
|
2344
|
+
assert app.send_packet.mock_calls == [
|
|
2345
|
+
call(packet),
|
|
2346
|
+
call(
|
|
2347
|
+
packet.replace(
|
|
2348
|
+
tx_options=packet.tx_options | t.TransmitOptions.FORCE_ROUTE_DISCOVERY
|
|
2349
|
+
)
|
|
2350
|
+
),
|
|
2351
|
+
call(
|
|
2352
|
+
packet.replace(
|
|
2353
|
+
tx_options=packet.tx_options | t.TransmitOptions.FORCE_ROUTE_DISCOVERY
|
|
2354
|
+
)
|
|
2355
|
+
),
|
|
2356
|
+
call(
|
|
2357
|
+
packet.replace(
|
|
2358
|
+
tx_options=packet.tx_options | t.TransmitOptions.FORCE_ROUTE_DISCOVERY
|
|
2359
|
+
)
|
|
2360
|
+
),
|
|
2361
|
+
]
|
|
2362
|
+
|
|
2363
|
+
|
|
2364
|
+
async def test_request_retry_reply_timeout(app) -> None:
|
|
2365
|
+
"""Test retry logic when a request is enqueued but no reply arrives, then a later attempt succeeds."""
|
|
2366
|
+
tsn = 0x12
|
|
2367
|
+
|
|
2368
|
+
dev = app.add_device(nwk=0x1234, ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:11"))
|
|
2369
|
+
dev.node_desc = make_node_desc()
|
|
2370
|
+
|
|
2371
|
+
ep = dev.add_endpoint(1)
|
|
2372
|
+
ep.status = endpoint.Status.ZDO_INIT
|
|
2373
|
+
ep.add_input_cluster(Basic.cluster_id)
|
|
2374
|
+
|
|
2375
|
+
attempt = 0
|
|
2376
|
+
|
|
2377
|
+
def send_packet(*args, **kwargs) -> None:
|
|
2378
|
+
nonlocal attempt
|
|
2379
|
+
attempt += 1
|
|
2380
|
+
|
|
2381
|
+
# The first two attempts are enqueued successfully but no reply ever arrives
|
|
2382
|
+
if attempt < 3:
|
|
2383
|
+
return
|
|
2384
|
+
|
|
2385
|
+
# The third attempt receives a reply
|
|
2386
|
+
asyncio.get_running_loop().call_soon(
|
|
2387
|
+
dev.packet_received,
|
|
2388
|
+
t.ZigbeePacket(
|
|
2389
|
+
profile_id=260,
|
|
2390
|
+
cluster_id=Basic.cluster_id,
|
|
2391
|
+
src_ep=1,
|
|
2392
|
+
dst_ep=1,
|
|
2393
|
+
data=t.SerializableBytes(
|
|
2394
|
+
foundation.ZCLHeader(
|
|
2395
|
+
frame_control=foundation.FrameControl(
|
|
2396
|
+
frame_type=foundation.FrameType.GLOBAL_COMMAND,
|
|
2397
|
+
is_manufacturer_specific=False,
|
|
2398
|
+
direction=foundation.Direction.Server_to_Client,
|
|
2399
|
+
disable_default_response=True,
|
|
2400
|
+
reserved=0,
|
|
2401
|
+
),
|
|
2402
|
+
tsn=tsn,
|
|
2403
|
+
command_id=foundation.GeneralCommand.Default_Response,
|
|
2404
|
+
manufacturer=None,
|
|
2405
|
+
).serialize()
|
|
2406
|
+
+ (
|
|
2407
|
+
foundation.GENERAL_COMMANDS[
|
|
2408
|
+
foundation.GeneralCommand.Default_Response
|
|
2409
|
+
]
|
|
2410
|
+
.schema(
|
|
2411
|
+
command_id=Basic.ServerCommandDefs.reset_fact_default.id,
|
|
2412
|
+
status=foundation.Status.SUCCESS,
|
|
2413
|
+
)
|
|
2414
|
+
.serialize()
|
|
2415
|
+
)
|
|
2416
|
+
),
|
|
2417
|
+
src=t.AddrModeAddress(
|
|
2418
|
+
addr_mode=t.AddrMode.NWK,
|
|
2419
|
+
address=dev.nwk,
|
|
2420
|
+
),
|
|
2421
|
+
dst=t.AddrModeAddress(
|
|
2422
|
+
addr_mode=t.AddrMode.NWK,
|
|
2423
|
+
address=0x0000,
|
|
2424
|
+
),
|
|
2425
|
+
),
|
|
2426
|
+
)
|
|
2427
|
+
|
|
2428
|
+
app.send_packet.side_effect = send_packet
|
|
2429
|
+
dev.get_sequence = MagicMock(return_value=tsn)
|
|
2430
|
+
|
|
2431
|
+
rsp = await dev.endpoints[1].basic.reset_fact_default(timeout=0.01, retry_delay=0)
|
|
2432
|
+
assert rsp == foundation.DefaultResponse(
|
|
2433
|
+
command_id=Basic.ServerCommandDefs.reset_fact_default.id,
|
|
2434
|
+
status=foundation.Status.SUCCESS,
|
|
2435
|
+
)
|
|
2436
|
+
|
|
2437
|
+
assert len(app.send_packet.mock_calls) == 3
|
|
2438
|
+
|
|
2439
|
+
|
|
2440
|
+
async def test_request_retry_delay_releases_concurrency(app) -> None:
|
|
2441
|
+
"""A request awaiting its post-retry delay must not hold a concurrency slot."""
|
|
2442
|
+
dev = app.add_device(nwk=0x1234, ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:11"))
|
|
2443
|
+
|
|
2444
|
+
# Every send fails, so each request enters its (long) post-retry delay
|
|
2445
|
+
app.send_packet.side_effect = zigpy.exceptions.DeliveryError("Failure")
|
|
2446
|
+
|
|
2447
|
+
async def make_request(sequence: int) -> None:
|
|
2448
|
+
await dev.request(
|
|
2449
|
+
profile=0x1234,
|
|
2450
|
+
cluster=0x0006,
|
|
2451
|
+
src_ep=1,
|
|
2452
|
+
dst_ep=1,
|
|
2453
|
+
sequence=sequence,
|
|
2454
|
+
data=b"",
|
|
2455
|
+
expect_reply=False,
|
|
2456
|
+
retries=1,
|
|
2457
|
+
retry_delay=10,
|
|
2458
|
+
)
|
|
2459
|
+
|
|
2460
|
+
async with asyncio.TaskGroup() as tg:
|
|
2461
|
+
tasks = [
|
|
2462
|
+
tg.create_task(make_request(seq))
|
|
2463
|
+
for seq in range(2 * device.MAX_DEVICE_CONCURRENCY)
|
|
2464
|
+
]
|
|
2465
|
+
|
|
2466
|
+
# Each request is now parked in a 10s post-retry delay. Even so, every one
|
|
2467
|
+
# should still get its first attempt sent: the concurrency slot is released
|
|
2468
|
+
# *before* the delay, not held during it.
|
|
2469
|
+
async with asyncio.timeout(1):
|
|
2470
|
+
while len(app.send_packet.mock_calls) < len(tasks):
|
|
2471
|
+
await asyncio.sleep(0)
|
|
2472
|
+
|
|
2473
|
+
assert len(app.send_packet.mock_calls) == len(tasks)
|
|
2474
|
+
|
|
2475
|
+
# Tear down the still-pending requests so the task group can exit
|
|
2476
|
+
for task in tasks:
|
|
2477
|
+
task.cancel()
|
|
@@ -175,6 +175,8 @@ async def test_reply_change_profile_id(ep):
|
|
|
175
175
|
use_ieee=False,
|
|
176
176
|
ask_for_ack=None,
|
|
177
177
|
priority=None,
|
|
178
|
+
retries=None,
|
|
179
|
+
retry_delay=None,
|
|
178
180
|
)
|
|
179
181
|
]
|
|
180
182
|
|
|
@@ -193,6 +195,8 @@ async def test_reply_change_profile_id(ep):
|
|
|
193
195
|
use_ieee=False,
|
|
194
196
|
ask_for_ack=None,
|
|
195
197
|
priority=None,
|
|
198
|
+
retries=None,
|
|
199
|
+
retry_delay=None,
|
|
196
200
|
)
|
|
197
201
|
]
|
|
198
202
|
|
|
@@ -212,6 +216,8 @@ async def test_reply_change_profile_id(ep):
|
|
|
212
216
|
use_ieee=False,
|
|
213
217
|
ask_for_ack=None,
|
|
214
218
|
priority=None,
|
|
219
|
+
retries=None,
|
|
220
|
+
retry_delay=None,
|
|
215
221
|
)
|
|
216
222
|
]
|
|
217
223
|
|
|
@@ -47,7 +47,7 @@ def patch_device_tables(
|
|
|
47
47
|
neighbors: list | BaseException | zdo_t.Status,
|
|
48
48
|
routes: list | BaseException | zdo_t.Status,
|
|
49
49
|
):
|
|
50
|
-
def mgmt_lqi_req(StartIndex: t.uint8_t):
|
|
50
|
+
def mgmt_lqi_req(StartIndex: t.uint8_t, **kwargs):
|
|
51
51
|
status = zdo_t.Status.SUCCESS
|
|
52
52
|
entries = 0
|
|
53
53
|
start_index = 0
|
|
@@ -73,7 +73,7 @@ def patch_device_tables(
|
|
|
73
73
|
}.values()
|
|
74
74
|
)
|
|
75
75
|
|
|
76
|
-
def mgmt_rtg_req(StartIndex: t.uint8_t):
|
|
76
|
+
def mgmt_rtg_req(StartIndex: t.uint8_t, **kwargs):
|
|
77
77
|
status = zdo_t.Status.SUCCESS
|
|
78
78
|
entries = 0
|
|
79
79
|
start_index = 0
|
|
@@ -21,10 +21,12 @@ import zigpy.endpoint
|
|
|
21
21
|
import zigpy.profiles.zha
|
|
22
22
|
import zigpy.types as t
|
|
23
23
|
from zigpy.zcl import (
|
|
24
|
+
MAX_ATTRIBUTE_RECORDS_BYTES,
|
|
24
25
|
AttributeReadEvent,
|
|
25
26
|
AttributeReportedEvent,
|
|
26
27
|
AttributeUpdatedEvent,
|
|
27
28
|
AttributeWrittenEvent,
|
|
29
|
+
_chunk_records_by_size,
|
|
28
30
|
foundation,
|
|
29
31
|
)
|
|
30
32
|
from zigpy.zcl.clusters.general import Basic, OnOff, Ota
|
|
@@ -1612,6 +1614,8 @@ async def test_received_onoff_toggle_generates_default_response():
|
|
|
1612
1614
|
use_ieee=False,
|
|
1613
1615
|
ask_for_ack=None,
|
|
1614
1616
|
priority=t.PacketPriority.LOW,
|
|
1617
|
+
retries=None,
|
|
1618
|
+
retry_delay=None,
|
|
1615
1619
|
)
|
|
1616
1620
|
]
|
|
1617
1621
|
|
|
@@ -2401,6 +2405,200 @@ async def test_configure_reporting_multiple_manufacturer_groups(app_mock) -> Non
|
|
|
2401
2405
|
assert manuf_call.args[0][0].reportable_change == 5
|
|
2402
2406
|
|
|
2403
2407
|
|
|
2408
|
+
async def test_configure_reporting_multiple_chunked_by_size(app_mock) -> None:
|
|
2409
|
+
"""Configure_reporting_multiple splits requests so no single one exceeds
|
|
2410
|
+
MAX_ATTRIBUTE_RECORDS_BYTES of serialized records.
|
|
2411
|
+
"""
|
|
2412
|
+
|
|
2413
|
+
class TestCluster(Basic):
|
|
2414
|
+
_skip_registry = True
|
|
2415
|
+
|
|
2416
|
+
class AttributeDefs(Basic.AttributeDefs):
|
|
2417
|
+
# 6 uint8 attrs, each serializing to 9 bytes as a SendReports config
|
|
2418
|
+
# (1 dir + 2 attrid + 1 type + 2 min + 2 max + 1 reportable_change).
|
|
2419
|
+
# 6 * 9 = 54 bytes > 50, so the request must split into 2 chunks.
|
|
2420
|
+
attr_a = foundation.ZCLAttributeDef(id=0xFF00, type=t.uint8_t)
|
|
2421
|
+
attr_b = foundation.ZCLAttributeDef(id=0xFF01, type=t.uint8_t)
|
|
2422
|
+
attr_c = foundation.ZCLAttributeDef(id=0xFF02, type=t.uint8_t)
|
|
2423
|
+
attr_d = foundation.ZCLAttributeDef(id=0xFF03, type=t.uint8_t)
|
|
2424
|
+
attr_e = foundation.ZCLAttributeDef(id=0xFF04, type=t.uint8_t)
|
|
2425
|
+
attr_f = foundation.ZCLAttributeDef(id=0xFF05, type=t.uint8_t)
|
|
2426
|
+
|
|
2427
|
+
dev = add_initialized_device(app_mock, nwk=0x1234, ieee=make_ieee(1))
|
|
2428
|
+
cluster = TestCluster(dev.endpoints[1])
|
|
2429
|
+
dev.endpoints[1].add_input_cluster(TestCluster.cluster_id, cluster)
|
|
2430
|
+
|
|
2431
|
+
cfg_success = zcl.foundation.ConfigureReportingResponse(
|
|
2432
|
+
[zcl.foundation.ConfigureReportingResponseRecord(zcl.foundation.Status.SUCCESS)]
|
|
2433
|
+
)
|
|
2434
|
+
|
|
2435
|
+
cfg = ReportingConfig(min_interval=1, max_interval=2, reportable_change=3)
|
|
2436
|
+
attrs = [
|
|
2437
|
+
TestCluster.AttributeDefs.attr_a,
|
|
2438
|
+
TestCluster.AttributeDefs.attr_b,
|
|
2439
|
+
TestCluster.AttributeDefs.attr_c,
|
|
2440
|
+
TestCluster.AttributeDefs.attr_d,
|
|
2441
|
+
TestCluster.AttributeDefs.attr_e,
|
|
2442
|
+
TestCluster.AttributeDefs.attr_f,
|
|
2443
|
+
]
|
|
2444
|
+
|
|
2445
|
+
with patch.object(
|
|
2446
|
+
cluster,
|
|
2447
|
+
"_configure_reporting",
|
|
2448
|
+
new_callable=AsyncMock,
|
|
2449
|
+
side_effect=[[cfg_success], [cfg_success]],
|
|
2450
|
+
) as mock_configure:
|
|
2451
|
+
results = await cluster.configure_reporting_multiple(dict.fromkeys(attrs, cfg))
|
|
2452
|
+
|
|
2453
|
+
assert mock_configure.await_count == 2
|
|
2454
|
+
|
|
2455
|
+
sent_attrids = []
|
|
2456
|
+
for call_obj in mock_configure.call_args_list:
|
|
2457
|
+
chunk_configs = call_obj.args[0]
|
|
2458
|
+
chunk_size = sum(len(c.serialize()) for c in chunk_configs)
|
|
2459
|
+
assert chunk_size <= MAX_ATTRIBUTE_RECORDS_BYTES
|
|
2460
|
+
sent_attrids.extend(c.attrid for c in chunk_configs)
|
|
2461
|
+
|
|
2462
|
+
assert sent_attrids == [a.id for a in attrs]
|
|
2463
|
+
|
|
2464
|
+
assert len(results) == 6
|
|
2465
|
+
assert all(s == zcl.foundation.Status.SUCCESS for s in results.values())
|
|
2466
|
+
|
|
2467
|
+
|
|
2468
|
+
async def test_read_attributes_chunked_by_count(app_mock) -> None:
|
|
2469
|
+
"""Read_attributes splits requests into chunks of at most five."""
|
|
2470
|
+
|
|
2471
|
+
class TestCluster(Basic):
|
|
2472
|
+
_skip_registry = True
|
|
2473
|
+
|
|
2474
|
+
class AttributeDefs(Basic.AttributeDefs):
|
|
2475
|
+
attr_0 = foundation.ZCLAttributeDef(id=0xFF00, type=t.uint8_t)
|
|
2476
|
+
attr_1 = foundation.ZCLAttributeDef(id=0xFF01, type=t.uint8_t)
|
|
2477
|
+
attr_2 = foundation.ZCLAttributeDef(id=0xFF02, type=t.uint8_t)
|
|
2478
|
+
attr_3 = foundation.ZCLAttributeDef(id=0xFF03, type=t.uint8_t)
|
|
2479
|
+
attr_4 = foundation.ZCLAttributeDef(id=0xFF04, type=t.uint8_t)
|
|
2480
|
+
attr_5 = foundation.ZCLAttributeDef(id=0xFF05, type=t.uint8_t)
|
|
2481
|
+
attr_6 = foundation.ZCLAttributeDef(id=0xFF06, type=t.uint8_t)
|
|
2482
|
+
attr_7 = foundation.ZCLAttributeDef(id=0xFF07, type=t.uint8_t)
|
|
2483
|
+
attr_8 = foundation.ZCLAttributeDef(id=0xFF08, type=t.uint8_t)
|
|
2484
|
+
attr_9 = foundation.ZCLAttributeDef(id=0xFF09, type=t.uint8_t)
|
|
2485
|
+
attr_10 = foundation.ZCLAttributeDef(id=0xFF0A, type=t.uint8_t)
|
|
2486
|
+
|
|
2487
|
+
dev = add_initialized_device(app_mock, nwk=0x1234, ieee=make_ieee(1))
|
|
2488
|
+
cluster = TestCluster(dev.endpoints[1])
|
|
2489
|
+
dev.endpoints[1].add_input_cluster(TestCluster.cluster_id, cluster)
|
|
2490
|
+
|
|
2491
|
+
attrs = [getattr(TestCluster.AttributeDefs, f"attr_{i}") for i in range(11)]
|
|
2492
|
+
|
|
2493
|
+
# Exclude 2 and 7 so the mock returns UNSUPPORTED
|
|
2494
|
+
supported = {attr: i for i, attr in enumerate(attrs) if i not in (2, 7)}
|
|
2495
|
+
with mock_attribute_reads(cluster, supported) as (mock_read, _):
|
|
2496
|
+
success, failure = await cluster.read_attributes(attrs)
|
|
2497
|
+
|
|
2498
|
+
# 11 attributes, at most MAX_READ_ATTRIBUTES_PER_REQ (5) per request -> 5 + 5 + 1
|
|
2499
|
+
chunks = [call_obj.args[0] for call_obj in mock_read.call_args_list]
|
|
2500
|
+
assert [len(chunk) for chunk in chunks] == [5, 5, 1]
|
|
2501
|
+
|
|
2502
|
+
# Every attribute was requested exactly once, in order, across the chunks
|
|
2503
|
+
requested_ids = []
|
|
2504
|
+
for chunk in chunks:
|
|
2505
|
+
requested_ids.extend(chunk)
|
|
2506
|
+
assert requested_ids == [attr.id for attr in attrs]
|
|
2507
|
+
|
|
2508
|
+
# Supported attributes succeed; the two omitted ones fail
|
|
2509
|
+
assert success == supported
|
|
2510
|
+
assert failure == {
|
|
2511
|
+
attrs[2]: foundation.Status.UNSUPPORTED_ATTRIBUTE,
|
|
2512
|
+
attrs[7]: foundation.Status.UNSUPPORTED_ATTRIBUTE,
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
|
|
2516
|
+
async def test_write_attributes_chunked_by_size(app_mock) -> None:
|
|
2517
|
+
"""Write_attributes splits requests if a single one would exceed the limit."""
|
|
2518
|
+
|
|
2519
|
+
class TestCluster(Basic):
|
|
2520
|
+
_skip_registry = True
|
|
2521
|
+
|
|
2522
|
+
class AttributeDefs(Basic.AttributeDefs):
|
|
2523
|
+
attr_a = foundation.ZCLAttributeDef(id=0xFF00, type=t.uint64_t)
|
|
2524
|
+
attr_b = foundation.ZCLAttributeDef(id=0xFF01, type=t.uint64_t)
|
|
2525
|
+
attr_c = foundation.ZCLAttributeDef(id=0xFF02, type=t.uint64_t)
|
|
2526
|
+
attr_d = foundation.ZCLAttributeDef(id=0xFF03, type=t.uint64_t)
|
|
2527
|
+
attr_e = foundation.ZCLAttributeDef(id=0xFF04, type=t.uint64_t)
|
|
2528
|
+
attr_f = foundation.ZCLAttributeDef(id=0xFF05, type=t.uint64_t)
|
|
2529
|
+
|
|
2530
|
+
dev = add_initialized_device(app_mock, nwk=0x1234, ieee=make_ieee(1))
|
|
2531
|
+
cluster = TestCluster(dev.endpoints[1])
|
|
2532
|
+
dev.endpoints[1].add_input_cluster(TestCluster.cluster_id, cluster)
|
|
2533
|
+
|
|
2534
|
+
attrs = [
|
|
2535
|
+
TestCluster.AttributeDefs.attr_a,
|
|
2536
|
+
TestCluster.AttributeDefs.attr_b,
|
|
2537
|
+
TestCluster.AttributeDefs.attr_c,
|
|
2538
|
+
TestCluster.AttributeDefs.attr_d,
|
|
2539
|
+
TestCluster.AttributeDefs.attr_e,
|
|
2540
|
+
TestCluster.AttributeDefs.attr_f,
|
|
2541
|
+
]
|
|
2542
|
+
|
|
2543
|
+
with mock_attribute_writes(
|
|
2544
|
+
cluster, dict.fromkeys(attrs, foundation.Status.SUCCESS)
|
|
2545
|
+
) as (mock_write, _):
|
|
2546
|
+
[results] = await cluster.write_attributes(dict.fromkeys(attrs, 1))
|
|
2547
|
+
|
|
2548
|
+
# 6 records of 11 bytes each split into 2 chunks: 44 bytes + 22 bytes
|
|
2549
|
+
assert mock_write.call_count == 2
|
|
2550
|
+
|
|
2551
|
+
sent_attrids = []
|
|
2552
|
+
for call_obj in mock_write.call_args_list:
|
|
2553
|
+
chunk_attrs = call_obj.args[0]
|
|
2554
|
+
chunk_size = sum(len(a.serialize()) for a in chunk_attrs)
|
|
2555
|
+
assert chunk_size <= MAX_ATTRIBUTE_RECORDS_BYTES
|
|
2556
|
+
sent_attrids.extend(a.attrid for a in chunk_attrs)
|
|
2557
|
+
|
|
2558
|
+
assert sent_attrids == [attr.id for attr in attrs]
|
|
2559
|
+
|
|
2560
|
+
assert len(results) == 6
|
|
2561
|
+
assert all(r.status == foundation.Status.SUCCESS for r in results)
|
|
2562
|
+
|
|
2563
|
+
|
|
2564
|
+
@pytest.mark.parametrize(
|
|
2565
|
+
("sizes", "max_bytes", "expected"),
|
|
2566
|
+
[
|
|
2567
|
+
# No records produces no chunks
|
|
2568
|
+
([], 10, []),
|
|
2569
|
+
# Records that all fit stay in a single chunk
|
|
2570
|
+
([3, 3, 3], 10, [[3, 3, 3]]),
|
|
2571
|
+
# Filling exactly to the limit does not start a new chunk
|
|
2572
|
+
([5, 5], 10, [[5, 5]]),
|
|
2573
|
+
# A single record exactly at the limit is allowed
|
|
2574
|
+
([10], 10, [[10]]),
|
|
2575
|
+
# Exceeding the limit rolls over into a new chunk
|
|
2576
|
+
([5, 5, 1], 10, [[5, 5], [1]]),
|
|
2577
|
+
],
|
|
2578
|
+
)
|
|
2579
|
+
def test_chunk_records_by_size(sizes, max_bytes, expected) -> None:
|
|
2580
|
+
"""The chunker packs records into chunks that never exceed max_bytes."""
|
|
2581
|
+
chunks = _chunk_records_by_size(sizes, lambda size: size, max_bytes=max_bytes)
|
|
2582
|
+
assert chunks == expected
|
|
2583
|
+
|
|
2584
|
+
|
|
2585
|
+
@pytest.mark.parametrize(
|
|
2586
|
+
("sizes", "max_bytes"),
|
|
2587
|
+
[
|
|
2588
|
+
# A single record larger than the limit, on its own
|
|
2589
|
+
([15], 10),
|
|
2590
|
+
# An oversized record surrounded by ones that would otherwise fit
|
|
2591
|
+
([3, 15, 4], 10),
|
|
2592
|
+
],
|
|
2593
|
+
)
|
|
2594
|
+
def test_chunk_records_by_size_oversized_record(sizes, max_bytes) -> None:
|
|
2595
|
+
"""A record that on its own exceeds max_bytes can never be sent, so the chunker
|
|
2596
|
+
fails loudly instead of emitting an oversized chunk the transport would reject.
|
|
2597
|
+
"""
|
|
2598
|
+
with pytest.raises(ValueError, match="too large to fit in a single request"):
|
|
2599
|
+
_chunk_records_by_size(sizes, lambda size: size, max_bytes=max_bytes)
|
|
2600
|
+
|
|
2601
|
+
|
|
2404
2602
|
def test_manufacturer_id_override_manuf_specific_cluster(app_mock) -> None:
|
|
2405
2603
|
"""Test class-level `manufacturer_id_override` for custom clusters."""
|
|
2406
2604
|
|