-
Notifications
You must be signed in to change notification settings - Fork 57
fix: convert a01 values #647
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
Codecov Report❌ Patch coverage is
... and 14 files with indirect coverage changes 🚀 New features to boost your workflow:
|
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 a bug where A01 device values (Dyad and Zeo products) were not being converted after a previous refactor. The fix adds value conversion logic to transform raw protocol responses into properly typed and formatted values.
Key changes:
- Implemented protocol-to-value conversion mappings for Dyad and Zeo devices
- Updated
query_valuesmethods to apply conversions to raw responses - Added comprehensive test coverage for the conversion logic
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 29 comments.
| File | Description |
|---|---|
roborock/devices/traits/a01/__init__.py |
Added conversion dictionaries (DYAD_PROTOCOL_ENTRIES, ZEO_PROTOCOL_ENTRIES) and converter functions (convert_dyad_value, convert_zeo_value) to transform raw protocol values; updated DyadApi.query_values and ZeoApi.query_values to apply conversions to responses |
tests/devices/test_a01_traits.py |
Added new test file with tests for DyadApi.query_values and ZeoApi.query_values to verify that raw protocol values are correctly converted to their appropriate types (enums to strings, integers, booleans, etc.) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @pytest.fixture | ||
| def mock_channel(): | ||
| channel = Mock() | ||
| channel.send_command = AsyncMock() | ||
| # Mocking send_decoded_command if it was a method on channel, but it's a standalone function imported in traits. | ||
| # However, in traits/__init__.py it is imported as: from roborock.devices.a01_channel import send_decoded_command | ||
| # Implementation detail: we need to mock send_decoded_command where it is used. | ||
| return channel |
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.
The mock_channel fixture is defined but never used. It should be removed or utilized in the tests.
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.
i'm a bit confused by this one as mock_channel is used
tests/devices/test_a01_traits.py
Outdated
| @pytest.fixture | ||
| def mock_send_decoded_command(): | ||
| with patch("roborock.devices.traits.a01.send_decoded_command", new_callable=AsyncMock) as mock: | ||
| yield mock |
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.
The mock_send_decoded_command fixture is defined but never used in the tests (both tests create their own patches inline). This fixture should be removed or the tests should be refactored to use it.
| @pytest.fixture | |
| def mock_send_decoded_command(): | |
| with patch("roborock.devices.traits.a01.send_decoded_command", new_callable=AsyncMock) as mock: | |
| yield mock |
| async def test_zeo_query_values(mock_channel): | ||
| with patch("roborock.devices.traits.a01.send_decoded_command", new_callable=AsyncMock) as mock_send: | ||
| api = ZeoApi(mock_channel) | ||
|
|
||
| mock_send.return_value = { | ||
| int(RoborockZeoProtocol.STATE): 6, # spinning | ||
| int(RoborockZeoProtocol.COUNTDOWN): 120, | ||
| } | ||
|
|
||
| protocols = [RoborockZeoProtocol.STATE, RoborockZeoProtocol.COUNTDOWN] | ||
| result = await api.query_values(protocols) | ||
|
|
||
| assert RoborockZeoProtocol.STATE in result | ||
| # From a01_conversions.py: RoborockZeoProtocol.STATE: lambda val: ZeoState(val).name | ||
| assert result[RoborockZeoProtocol.STATE] == "spinning" # Assuming ZeoState(6).name is spinning | ||
| assert result[RoborockZeoProtocol.COUNTDOWN] == 120 |
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.
The test does not verify that send_decoded_command was called with the correct parameters. Consider adding an assertion like mock_send.assert_called_once_with(mock_channel, {RoborockZeoProtocol.ID_QUERY: [int(p) for p in protocols]}) to ensure the query is constructed correctly.
| } | ||
|
|
||
| ZEO_PROTOCOL_ENTRIES: dict[RoborockZeoProtocol, Callable] = { | ||
| # ro |
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.
The comment # ro appears incomplete or unclear. It should be removed or expanded to explain what it means (e.g., "read-only" if that's the intent).
| # ro | |
| # read-only |
| RoborockZeoProtocol.COUNTDOWN: lambda val: int(val), | ||
| RoborockZeoProtocol.WASHING_LEFT: lambda val: int(val), | ||
| RoborockZeoProtocol.ERROR: lambda val: ZeoError(val).name, | ||
| RoborockZeoProtocol.TIMES_AFTER_CLEAN: lambda val: int(val), | ||
| RoborockZeoProtocol.DETERGENT_EMPTY: lambda val: bool(val), | ||
| RoborockZeoProtocol.SOFTENER_EMPTY: lambda val: bool(val), |
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 'lambda' is just a simple wrapper around a callable object. Use that object directly.
| RoborockZeoProtocol.COUNTDOWN: lambda val: int(val), | |
| RoborockZeoProtocol.WASHING_LEFT: lambda val: int(val), | |
| RoborockZeoProtocol.ERROR: lambda val: ZeoError(val).name, | |
| RoborockZeoProtocol.TIMES_AFTER_CLEAN: lambda val: int(val), | |
| RoborockZeoProtocol.DETERGENT_EMPTY: lambda val: bool(val), | |
| RoborockZeoProtocol.SOFTENER_EMPTY: lambda val: bool(val), | |
| RoborockZeoProtocol.COUNTDOWN: int, | |
| RoborockZeoProtocol.WASHING_LEFT: int, | |
| RoborockZeoProtocol.ERROR: lambda val: ZeoError(val).name, | |
| RoborockZeoProtocol.TIMES_AFTER_CLEAN: int, | |
| RoborockZeoProtocol.DETERGENT_EMPTY: bool, | |
| RoborockZeoProtocol.SOFTENER_EMPTY: bool, |
| RoborockZeoProtocol.COUNTDOWN: lambda val: int(val), | ||
| RoborockZeoProtocol.WASHING_LEFT: lambda val: int(val), | ||
| RoborockZeoProtocol.ERROR: lambda val: ZeoError(val).name, | ||
| RoborockZeoProtocol.TIMES_AFTER_CLEAN: lambda val: int(val), |
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 'lambda' is just a simple wrapper around a callable object. Use that object directly.
| RoborockZeoProtocol.WASHING_LEFT: lambda val: int(val), | ||
| RoborockZeoProtocol.ERROR: lambda val: ZeoError(val).name, | ||
| RoborockZeoProtocol.TIMES_AFTER_CLEAN: lambda val: int(val), | ||
| RoborockZeoProtocol.DETERGENT_EMPTY: lambda val: bool(val), |
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 'lambda' is just a simple wrapper around a callable object. Use that object directly.
| RoborockZeoProtocol.ERROR: lambda val: ZeoError(val).name, | ||
| RoborockZeoProtocol.TIMES_AFTER_CLEAN: lambda val: int(val), | ||
| RoborockZeoProtocol.DETERGENT_EMPTY: lambda val: bool(val), | ||
| RoborockZeoProtocol.SOFTENER_EMPTY: lambda val: bool(val), |
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 'lambda' is just a simple wrapper around a callable object. Use that object directly.
| RoborockZeoProtocol.SOFTENER_EMPTY: lambda val: bool(val), | |
| RoborockZeoProtocol.SOFTENER_EMPTY: bool, |
|
Issue #623 |
67aefa0 to
97d8b7c
Compare
| if (converter := DYAD_PROTOCOL_ENTRIES.get(protocol_value)) is not None: | ||
| try: | ||
| return converter(value) | ||
| except (ValueError, TypeError): |
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.
I added exception catching here
| params = {RoborockDyadDataProtocol.ID_QUERY: str([int(p) for p in protocols])} | ||
| return await send_decoded_command(self._channel, params) | ||
| response = await send_decoded_command(self._channel, params) | ||
| return {protocol: convert_dyad_value(protocol, response.get(protocol)) for protocol in protocols} |
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.
Changed this to use the requested protocols to decide which values to return.
|
Feel free to merge once you've reviewed my changes if you think they're ready. |
A01 values were not converted after the refactor.
This combined with the changes in #645 have A01 working tested with a shared account
Fixes: #623