-
Notifications
You must be signed in to change notification settings - Fork 57
fix: Fix exception when sending dyad/zeo requests #651
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
Conversation
The bug was introduced in Python-roborock#645. Add tests that exercise the actual request encoding. This changes the ID QUERY value encoding by passing in a function, which is another variation on the first version of Python-roborock#645 where the json encoding happened inside the decode function.
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.
Pull request overview
This PR fixes an exception when sending dyad/zeo requests by changing how ID_QUERY values are encoded. Instead of manually converting protocol lists to strings, the fix introduces a value_encoder parameter to encode_mqtt_payload() that allows passing a function (like json.dumps) to handle value encoding consistently.
Key Changes
- Added
value_encoderparameter toencode_mqtt_payload()andsend_decoded_command()functions to enable flexible value encoding - Updated
DyadApi.query_values()andZeoApi.query_values()to passvalue_encoder=json.dumpsfor proper list-to-string conversion - Added comprehensive tests for encoding behavior, including edge cases with lists and complex data structures
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| roborock/protocols/a01_protocol.py | Adds value_encoder parameter to encode_mqtt_payload() with default _no_encode() function |
| roborock/devices/a01_channel.py | Updates send_decoded_command() overloads to accept and pass through value_encoder parameter |
| roborock/devices/traits/a01/init.py | Updates query_values() methods to use value_encoder=json.dumps for ID_QUERY encoding; adds explicit identity encoder to ZeoApi.set_value() |
| tests/protocols/test_a01_protocol.py | Adds unit tests for encode_mqtt_payload() with various data types, though tests have critical issue with missing value_encoder parameter |
| tests/protocols/common.py | Adds build_a01_message() helper function for creating test response messages |
| tests/devices/traits/a01/test_init.py | Converts from mocked tests to integration-style tests using FakeChannel to verify actual encoding behavior |
| tests/test_a01_api.py | Refactors to use build_a01_message() helper and removes duplicate message construction code |
| tests/devices/test_a01_channel.py | Adds value_encoder parameter to test call to match new function signature |
Comments suppressed due to low confidence (1)
tests/protocols/test_a01_protocol.py:105
- The tests
test_encode_mqtt_payload_basic,test_encode_mqtt_payload_list_conversion, andtest_encode_mqtt_payload_complex_datadon't pass thevalue_encoderparameter toencode_mqtt_payload(), but their assertions expect values to be JSON-encoded as strings.
Without value_encoder=json.dumps, the default _no_encode function is used, which would result in:
- Line 39:
{200: {"test": "data", "number": 42}}instead of{200: '{"test": "data", "number": 42}'} - Line 68:
{"dps": {"10000": [101, 102]}}instead of{"dps": {"10000": "[101, 102]"}} - Lines 94-104: Raw dict/string values instead of JSON-encoded strings
These tests should pass value_encoder=json.dumps to match production usage and verify the fix correctly.
def test_encode_mqtt_payload_basic():
"""Test basic MQTT payload encoding."""
# Test data with proper protocol keys
data: dict[RoborockDyadDataProtocol | RoborockZeoProtocol, Any] = {
RoborockDyadDataProtocol.START: {"test": "data", "number": 42}
}
result = encode_mqtt_payload(data)
# Verify result is a RoborockMessage
assert isinstance(result, RoborockMessage)
assert result.protocol == RoborockMessageProtocol.RPC_REQUEST
assert result.version == b"A01"
assert result.payload is not None
assert isinstance(result.payload, bytes)
assert len(result.payload) % 16 == 0 # Should be padded to AES block size
# Decode the payload to verify structure
decoded_data = decode_rpc_response(result)
assert decoded_data == {200: {"test": "data", "number": 42}}
def test_encode_mqtt_payload_empty_data():
"""Test encoding with empty data."""
data: dict[RoborockDyadDataProtocol | RoborockZeoProtocol, Any] = {}
result = encode_mqtt_payload(data)
assert isinstance(result, RoborockMessage)
assert result.protocol == RoborockMessageProtocol.RPC_REQUEST
assert result.payload is not None
# Decode the payload to verify structure
decoded_data = decode_rpc_response(result)
assert decoded_data == {}
def test_value_encoder():
"""Test that value_encoder is applied to all values."""
data: dict[RoborockDyadDataProtocol | RoborockZeoProtocol, Any] = {RoborockDyadDataProtocol.ID_QUERY: [101, 102]}
result = encode_mqtt_payload(data, value_encoder=json.dumps)
# Decode manually to check the raw JSON structure
decoded_json = json.loads(unpad(result.payload, AES.block_size).decode())
# ID_QUERY (10000) should be a string "[101, 102]", not a list [101, 102]
assert decoded_json["dps"]["10000"] == "[101, 102]"
assert isinstance(decoded_json["dps"]["10000"], str)
def test_encode_mqtt_payload_complex_data():
"""Test encoding with complex nested data."""
data: dict[RoborockDyadDataProtocol | RoborockZeoProtocol, Any] = {
RoborockDyadDataProtocol.STATUS: {
"nested": {"deep": {"value": 123}},
"list": [1, 2, 3, "test"],
"boolean": True,
"null": None,
},
RoborockZeoProtocol.MODE: "simple_value",
}
result = encode_mqtt_payload(data)
assert isinstance(result, RoborockMessage)
assert result.protocol == RoborockMessageProtocol.RPC_REQUEST
assert result.payload is not None
assert isinstance(result.payload, bytes)
# Decode the payload to verify structure
decoded_data = decode_rpc_response(result)
assert decoded_data == {
201: {
"nested": {"deep": {"value": 123}},
# Note: The list inside the dictionary is NOT converted because
# our fix only targets top-level list values in the dps map
"list": [1, 2, 3, "test"],
"boolean": True,
"null": None,
},
204: "simple_value",
}
def test_decode_rpc_response_valid_message():
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Note: The list inside the dictionary is NOT converted because | ||
| # our fix only targets top-level list values in the dps map |
Copilot
AI
Dec 7, 2025
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.
This comment states that lists inside dictionaries are not converted because "our fix only targets top-level list values in the dps map". However, this is misleading. The fix doesn't specifically target lists - it applies the value_encoder function to ALL values in the dps map. When value_encoder=json.dumps, ALL values (not just lists) are JSON-encoded as strings.
The reason lists inside nested dictionaries aren't double-encoded is because json.dumps serializes the entire value (including nested structures) as a single JSON string. Consider updating the comment to:
# Note: The list inside the dictionary is preserved within the JSON string
# because json.dumps serializes the entire dictionary structure, not individual nested elements| # Note: The list inside the dictionary is NOT converted because | |
| # our fix only targets top-level list values in the dps map | |
| # Note: The list inside the dictionary is preserved within the JSON string | |
| # because json.dumps serializes the entire dictionary structure, not individual nested elements |
The bug was introduced in #645.
Add tests that exercise the actual request encoding. This changes the ID QUERY value encoding by passing in a function, which is another variation on the first version of #645 where the json encoding happened inside the decode function.
Fixes #623