Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/test_quirks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copilot AI Feb 10, 2026

Copy link

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 id 0x12, 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’s command_id. Prefer returning a Default Response whose command_id is derived from the command_id kwarg of the request (e.g., via an AsyncMock side_effect).

Suggested change
request_mock.return_value = (0x12, zcl.foundation.Status.SUCCESS)
async def _request_side_effect(*args, **kwargs):
return kwargs["command_id"], zcl.foundation.Status.SUCCESS
request_mock.side_effect = _request_side_effect

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 😅

@puddly puddly Feb 10, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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.

@TheJulianJES TheJulianJES Feb 10, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like only three...

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

But I'm sure custom quirks might exist that rely on this behavior.

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,
)
Expand Down
79 changes: 70 additions & 9 deletions tests/test_zcl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?
On the other hand, the logic for the default response currently only adds the status to records_group, so this is basically already covered by the test above.



@pytest.mark.parametrize(
("attributes", "result"),
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion zigpy/zcl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1255,7 +1255,7 @@ async def write_attributes(
)
else:
# Default response: apply status to all attributes in this group
status = result[0]
status = result[1]
records_group.extend(
foundation.WriteAttributesStatusRecord(
status=status, attrid=zcl_attr.attrid
Expand Down
Loading