Repository navigation
Correctly parse the ZCL default response in write_attributes
#1767
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
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -880,7 +880,7 @@ class TestCluster(ManufacturerSpecificCluster): | |
| # The default behavior for a manufacturer-specific cluster, command, or attribute is | ||
| # to include the manufacturer ID in the request | ||
| with patch.object(ep, "request", AsyncMock()) as request_mock: | ||
| request_mock.return_value = (zcl.foundation.Status.SUCCESS, "done") | ||
| request_mock.return_value = (0x12, zcl.foundation.Status.SUCCESS) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we'll need to update quite a few quirks tests as well then 😅
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like only three... FAILED tests/test_develco.py::test_frient_emi - ValueError: Failed to convert status='done' from type <class 'str'> to <enum 'Status'>
FAILED tests/test_tuya.py::test_fan_switch_writes_attributes[TS0501FanSwitch] - ValueError: Failed to convert status='done' from type <class 'str'> to <enum 'Status'>
FAILED tests/test_xiaomi.py::test_xiaomi_eu_plug_binding[PlugMAEU01] - ValueError: Failed to convert status='done' from type <class 'str'> to <enum 'Status'>But I'm sure custom quirks might exist that rely on this behavior.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Interesting, so I guess we don't verify the output a lot..? https://github.com/search?q=repo%3Azigpy%2Fzha-device-handlers+Status.SUCCESS%2C+%22done%22&type=code
Really? For these, it's just the tests which do something stupid. I wouldn't expect any actual quirks to somehow return a default response with the fields in a different order. |
||
| await ep.just_a_cluster.command( | ||
| ep.just_a_cluster.commands_by_name["server_cmd0"].id, | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -528,16 +528,65 @@ async def test_write_attribute_types( | |
| "status", [foundation.Status.SUCCESS, foundation.Status.UNSUPPORTED_ATTRIBUTE] | ||
| ) | ||
| async def test_write_attributes_cache_default_response(cluster, status): | ||
| write_mock = AsyncMock( | ||
| return_value=[foundation.GeneralCommand.Write_Attributes, status] | ||
| _, default_rsp = cluster._create_request( | ||
| general=True, | ||
| command_id=foundation.GeneralCommand.Default_Response, | ||
| schema=foundation.GENERAL_COMMANDS[ | ||
| foundation.GeneralCommand.Default_Response | ||
| ].schema, | ||
| disable_default_response=True, | ||
| direction=foundation.Direction.Server_to_Client, | ||
| args=(), | ||
| kwargs={ | ||
| "command_id": foundation.GeneralCommand.Write_Attributes, | ||
| "status": status, | ||
| }, | ||
| ) | ||
| with patch.object(cluster, "_write_attributes", write_mock): | ||
| attributes = {4: "manufacturer", 5: "model", 12: 12} | ||
| await cluster.write_attributes(attributes) | ||
| assert cluster._write_attributes.call_count == 1 | ||
| cluster.endpoint.request.return_value = default_rsp | ||
|
|
||
| attributes = { | ||
| Basic.AttributeDefs.manufacturer: "manufacturer", | ||
| Basic.AttributeDefs.model: "model", | ||
| Basic.AttributeDefs.manufacturer_version_details: 12, | ||
| } | ||
| results = await cluster.write_attributes(attributes) | ||
| assert cluster.endpoint.request.call_count == 1 | ||
|
|
||
| if status == foundation.Status.SUCCESS: | ||
| for attr_id in attributes: | ||
| assert attr_id in cluster._attr_cache | ||
| else: | ||
| for attr_id in attributes: | ||
| assert attr_id not in cluster._attr_cache | ||
|
|
||
| assert len(results[0]) == 3 | ||
| assert all(r.status == status for r in results[0]) | ||
|
|
||
|
|
||
| async def test_write_attributes_default_response_status(cluster): | ||
| _, default_rsp = cluster._create_request( | ||
| general=True, | ||
| command_id=foundation.GeneralCommand.Default_Response, | ||
| schema=foundation.GENERAL_COMMANDS[ | ||
| foundation.GeneralCommand.Default_Response | ||
| ].schema, | ||
| disable_default_response=True, | ||
| direction=foundation.Direction.Server_to_Client, | ||
| args=(), | ||
| kwargs={ | ||
| "command_id": foundation.GeneralCommand.Write_Attributes, | ||
| "status": foundation.Status.FAILURE, | ||
| }, | ||
| ) | ||
| cluster.endpoint.request.return_value = default_rsp | ||
|
|
||
| results = await cluster.write_attributes( | ||
| {Basic.AttributeDefs.manufacturer: "manufacturer"} | ||
| ) | ||
|
|
||
| # It should correctly return FAILURE (0x01), not Write_Attributes (0x02) | ||
| assert results[0][0].status == foundation.Status.FAILURE | ||
|
Comment on lines
+583
to
+588
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: While we're at it, should we also assert this didn't make it into the attribute cache? |
||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ("attributes", "result"), | ||
|
|
@@ -870,10 +919,22 @@ async def test_configure_reporting_multiple(cluster): | |
|
|
||
| async def test_configure_reporting_multiple_def_rsp(cluster): | ||
| """Configure reporting returned a default response. May happen.""" | ||
| cluster.endpoint.request.return_value = ( | ||
| zcl.foundation.GeneralCommand.Configure_Reporting, | ||
| zcl.foundation.Status.UNSUP_GENERAL_COMMAND, | ||
| _, default_rsp = cluster._create_request( | ||
| general=True, | ||
| command_id=foundation.GeneralCommand.Default_Response, | ||
| schema=foundation.GENERAL_COMMANDS[ | ||
| foundation.GeneralCommand.Default_Response | ||
| ].schema, | ||
| disable_default_response=True, | ||
| direction=foundation.Direction.Server_to_Client, | ||
| args=(), | ||
| kwargs={ | ||
| "command_id": foundation.GeneralCommand.Configure_Reporting, | ||
| "status": foundation.Status.UNSUP_GENERAL_COMMAND, | ||
| }, | ||
| ) | ||
| cluster.endpoint.request.return_value = default_rsp | ||
|
|
||
| results = await cluster.configure_reporting_multiple( | ||
| { | ||
| Basic.AttributeDefs.hw_version: ReportingConfig( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mocked
ep.request()response uses a hardcoded command id0x12, which doesn’t correspond to any of the three commands invoked in this test (cluster command, read_attributes, write_attributes). This makes the test brittle if the code later validates the Default Response’scommand_id. Prefer returning a Default Response whosecommand_idis derived from thecommand_idkwarg of the request (e.g., via an AsyncMockside_effect).