-
Notifications
You must be signed in to change notification settings - Fork 62
feat: add clean record for Q7 #745
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
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 adds clean record functionality for B01 Q7 devices by implementing a new trait that retrieves and parses clean record data from the device's service.get_record_list API.
Changes:
- Introduces four new data container classes (
CleanRecordDetail,CleanRecordListItem,CleanRecordList,CleanRecordSummary) to represent clean record data structures - Implements
CleanSummaryTraitfor B01 Q7 devices with methods to fetch and parse clean records - Integrates the new trait into
Q7PropertiesApias aclean_summaryattribute
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| roborock/data/b01_q7/b01_q7_containers.py | Adds data container classes for clean record information including detail fields, list items, and summary totals |
| roborock/devices/traits/b01/q7/clean_summary.py | Implements the CleanSummaryTrait with methods to fetch record lists, parse JSON detail fields, and retrieve clean record details |
| roborock/devices/traits/b01/q7/init.py | Integrates CleanSummaryTrait into Q7PropertiesApi and exports it in all |
| tests/data/b01_q7/test_b01_q7_containers.py | Adds test to verify CleanRecordList parsing and detail field extraction |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| detail = CleanSummaryTrait._parse_record_detail(parsed.record_list[0].detail) | ||
| assert isinstance(detail, CleanRecordDetail) | ||
| assert detail.method == 0 | ||
| assert detail.clean_count == 1 | ||
| assert detail.record_clean_way == 0 | ||
| assert detail.record_faultcode == 0 | ||
| assert detail.record_dust_num == 0 | ||
| assert detail.clean_current_map == 0 |
Copilot
AI
Jan 16, 2026
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 appears to be missing assertions for some of the parsed detail fields. The test should verify record_start_time, record_use_time, record_clean_area, record_clean_mode, record_task_status, and record_map_url to ensure comprehensive coverage of the CleanRecordDetail parsing functionality.
| """Represents an entry in the clean record list returned by `service.get_record_list`.""" | ||
|
|
||
| url: str | None = None | ||
| detail: str | dict | None = None |
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 really can be either type?
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 originally thought i saw data where it was not a str in the app code, but I looked again and I think it is always a json str.
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.
So it's always a json dict? So then this should be dict (post json parsing)?
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.
it is a dict after we parse from json - but not at the time of accessing/loading from result of api.
it is a dumped json dict within the main json dict, so it is not parsed.
We have to manually parse it before access. We don't update the value either after we parse - i guess we theoretically could?
CleanRecordList.from_dict(result) has detail as a str
then we access .detail -> parse it to a dict -> convert it to CleanRecordDetail
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 don't think we should use the same field for the string or dict value and expose it in the API. Maybe there could be a manual step to "clean" the data first, converting it from json to dict, then parse it? or something else.
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 have a local change i'm trying to finish up - but i changed the typing to be just str. You can let me know if that's what you were thinking
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
|
||
| TESTDATA = pathlib.Path("tests/testdata") | ||
|
|
||
| PRODUCTS = {file.name: json.load(file.open()) for file in TESTDATA.glob("home_data_product_*.json")} |
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.
unrelated change but windows seems to require this or tests crash.
I have just been doing it and not including it in any PRs
| ], | ||
| } | ||
|
|
||
| details = await clean_summary_trait._get_clean_record_details( |
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.
Not sure about testing private methods like this
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 alternative is I do a whole wrapping around the refresh() call, but this was a bit simplier mock wise. If you want me to change it i will.
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 guess i assume this can be tested with either the response builder for a more end to end test, or with putting all the parsing inside the dataclass object and testing it there. I would probably just make last_record_detail also cacheable and test it entirely inside the dataclass, so everything is encapsulated.
|
|
||
| url: str | None = None | ||
| detail: str | dict | None = None | ||
| detail: str | None = None |
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.
Can you add a comment here explaining what's up with this field? It looks like its not really usable and folks are not expected to use this directly and should only access the summary for the last detail.
As an alternative you could put the json parsing here as something like detail_parsed in a @cachedproperty if you want to make it accessible and shared with the
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.
add a cached property
allenporter
left a comment
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.
Fine to fix in follow up PRs since its just style
|
|
||
| parsed = item.detail_parsed | ||
| except RoborockException as ex: | ||
| # Rather than failing if something goes wrong here, we should fail and log to tell the user. |
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.
Given there is already an exception catch in detail_parsed i'd just push this in there? What I was trying to get at by suggesting moving the parsing into the dataclas is that this whole function feels like it can be a one-liner:
return reverse([
item.detail_parsed
for item in record_list.record_list
if item.detail_parsed is not None
])
While typically i think it is good to not swallow exceptions down deep, i'm just thinking since in practice you're ignoring parse errors then just make it less code.
| raise TypeError(f"Unexpected response type for GET_RECORD_LIST: {type(result).__name__}: {result!r}") | ||
| return CleanRecordList.from_dict(result) | ||
|
|
||
| async def _get_clean_record_details(self, *, record_list: CleanRecordList) -> list[CleanRecordDetail]: |
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 don't think this needs to be async
| ], | ||
| } | ||
|
|
||
| details = await clean_summary_trait._get_clean_record_details( |
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 guess i assume this can be tested with either the response builder for a more end to end test, or with putting all the parsing inside the dataclass object and testing it there. I would probably just make last_record_detail also cacheable and test it entirely inside the dataclass, so everything is encapsulated.
Would appreciate you being pretty in depth on this one as it is the second trait for Q7 so I want to identify anything now rather than later.
fixes #735