Skip to content

Conversation

@Lash-L
Copy link
Collaborator

@Lash-L Lash-L commented Jan 16, 2026

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

@codecov
Copy link

codecov bot commented Jan 16, 2026

Codecov Report

❌ Patch coverage is 94.33962% with 12 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
roborock/data/b01_q7/b01_q7_containers.py 82.45% 10 Missing ⚠️
roborock/devices/traits/b01/q7/clean_summary.py 94.87% 1 Missing and 1 partial ⚠️
Files with missing lines Coverage Δ
roborock/devices/traits/b01/q7/__init__.py 95.45% <100.00%> (+0.71%) ⬆️
tests/data/b01_q7/test_b01_q7_containers.py 100.00% <100.00%> (ø)
tests/devices/traits/b01/q7/__init__.py 100.00% <100.00%> (ø)
tests/devices/traits/b01/q7/conftest.py 100.00% <100.00%> (ø)
tests/devices/traits/b01/q7/test_clean_summary.py 100.00% <100.00%> (ø)
tests/devices/traits/b01/q7/test_init.py 100.00% <100.00%> (ø)
tests/mock_data.py 100.00% <100.00%> (ø)
roborock/devices/traits/b01/q7/clean_summary.py 94.87% <94.87%> (ø)
roborock/data/b01_q7/b01_q7_containers.py 85.78% <82.45%> (-0.55%) ⬇️

... and 13 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

Copilot AI left a 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 CleanSummaryTrait for B01 Q7 devices with methods to fetch and parse clean records
  • Integrates the new trait into Q7PropertiesApi as a clean_summary attribute

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.

Comment on lines 144 to 151
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
Copy link

Copilot AI Jan 16, 2026

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.

Copilot uses AI. Check for mistakes.
"""Represents an entry in the clean record list returned by `service.get_record_list`."""

url: str | None = None
detail: str | dict | None = None
Copy link
Contributor

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?

Copy link
Collaborator Author

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.

Copy link
Contributor

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)?

Copy link
Collaborator Author

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

Copy link
Contributor

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.

Copy link
Collaborator Author

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

Lash-L and others added 2 commits January 31, 2026 20:05
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")}
Copy link
Collaborator Author

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

@Lash-L Lash-L requested a review from allenporter February 1, 2026 03:42
allenporter
allenporter previously approved these changes Feb 1, 2026
],
}

details = await clean_summary_trait._get_clean_record_details(
Copy link
Contributor

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

Copy link
Collaborator Author

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.

Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

add a cached property

Copy link
Contributor

@allenporter allenporter left a 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.
Copy link
Contributor

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]:
Copy link
Contributor

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(
Copy link
Contributor

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.

@Lash-L Lash-L merged commit 329e52b into main Feb 1, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Roborock Q7B "Total cleaning time" property value in HA does not match total cleaning time reported by Roborock app

3 participants