Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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 roborock/devices/device_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat
trait = b01.q10.create(channel)
elif "sc" in model_part:
# Q7 devices start with 'sc' in their model naming.
trait = b01.q7.create(channel)
trait = b01.q7.create(product, device, channel)
else:
raise UnsupportedDeviceError(f"Device {device.name} has unsupported B01 model: {product.model}")
case _:
Expand Down
33 changes: 25 additions & 8 deletions roborock/devices/traits/b01/q7/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
"""Traits for Q7 B01 devices.
Potentially other devices may fall into this category in the future."""

Potentially other devices may fall into this category in the future.
"""

from __future__ import annotations

from typing import Any

from roborock import B01Props
from roborock.data import Q7MapList, Q7MapListEntry
from roborock.data import HomeDataDevice, HomeDataProduct, Q7MapList, Q7MapListEntry
from roborock.data.b01_q7.b01_q7_code_mappings import (
CleanPathPreferenceMapping,
CleanRepeatMapping,
Expand All @@ -23,30 +27,43 @@

from .clean_summary import CleanSummaryTrait
from .map import MapTrait
from .map_content import MapContentTrait

__all__ = [
"Q7PropertiesApi",
"CleanSummaryTrait",
"MapTrait",
"MapContentTrait",
"Q7MapList",
"Q7MapListEntry",
]


class Q7PropertiesApi(Trait):
"""API for interacting with B01 devices."""
"""API for interacting with B01 Q7 devices."""

clean_summary: CleanSummaryTrait
"""Trait for clean records / clean summary (Q7 `service.get_record_list`)."""

map: MapTrait
"""Trait for map list metadata + raw map payload retrieval."""

def __init__(self, channel: MqttChannel) -> None:
"""Initialize the B01Props API."""
map_content: MapContentTrait
"""Trait for fetching parsed current map content."""

def __init__(self, channel: MqttChannel, *, device: HomeDataDevice, product: HomeDataProduct) -> None:
"""Initialize the Q7 API."""
self._channel = channel
self._device = device
self._product = product

self.clean_summary = CleanSummaryTrait(channel)
self.map = MapTrait(channel)
self.map_content = MapContentTrait(
self.map,
serial=device.sn,
model=product.model,
)

async def query_values(self, props: list[RoborockB01Props]) -> B01Props | None:
"""Query the device for the values of the given Q7 properties."""
Expand Down Expand Up @@ -151,6 +168,6 @@ async def send(self, command: CommandType, params: ParamsType) -> Any:
)


def create(channel: MqttChannel) -> Q7PropertiesApi:
"""Create traits for B01 devices."""
return Q7PropertiesApi(channel)
def create(product: HomeDataProduct, device: HomeDataDevice, channel: MqttChannel) -> Q7PropertiesApi:
"""Create traits for B01 Q7 devices."""
return Q7PropertiesApi(channel, device=device, product=product)
104 changes: 104 additions & 0 deletions roborock/devices/traits/b01/q7/map_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Trait for fetching parsed map content from B01/Q7 devices.

This follows the same basic pattern as the v1 `MapContentTrait`:
- `refresh()` performs I/O and populates cached fields
- fields `image_content`, `map_data`, and `raw_api_response` are then readable

For B01/Q7 devices, the underlying raw map payload is retrieved via `MapTrait`.
"""

from __future__ import annotations
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.

Remove since this will mess with types and is no longer needed for modern code.


from dataclasses import dataclass

from vacuum_map_parser_base.map_data import MapData

from roborock.data import RoborockBase
from roborock.devices.traits import Trait
from roborock.exceptions import RoborockException
from roborock.map.b01_map_parser import B01MapParser, B01MapParserConfig

from .map import MapTrait

_TRUNCATE_LENGTH = 20


@dataclass
class MapContent(RoborockBase):
"""Dataclass representing map content."""

image_content: bytes | None = None
"""The rendered image of the map in PNG format."""

map_data: MapData | None = None
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This can hold the map image on it right? Or is it not easy to do that with this approach?

Still reading through code so I may answer my own question

"""Parsed map data (metadata for points on the map)."""

raw_api_response: bytes | None = None
"""Raw bytes of the map payload from the device.

This should be treated as an opaque blob used only internally by this
library to re-parse the map data when needed.
"""

def __repr__(self) -> str:
img = self.image_content
if img and len(img) > _TRUNCATE_LENGTH:
img = img[: _TRUNCATE_LENGTH - 3] + b"..."
return f"MapContent(image_content={img!r}, map_data={self.map_data!r})"


class MapContentTrait(MapContent, Trait):
"""Trait for fetching parsed map content for Q7 devices."""

def __init__(
self,
map_trait: MapTrait,
*,
serial: str | None,
model: str | None,
map_parser_config: B01MapParserConfig | None = None,
) -> None:
super().__init__()
self._map_trait = map_trait
self._serial = serial
self._model = model
self._map_parser = B01MapParser(map_parser_config)

async def refresh(self) -> None:
"""Fetch, decode, and parse the current map payload."""
raw_payload = await self._map_trait.get_current_map_payload()
parsed = self.parse_map_content(raw_payload)
self.image_content = parsed.image_content
self.map_data = parsed.map_data
self.raw_api_response = parsed.raw_api_response

def parse_map_content(self, response: bytes) -> MapContent:
"""Parse map content from raw bytes.

Exposed so callers can re-parse cached map payload bytes without
performing I/O.
"""
if not self._serial or not self._model:
raise RoborockException(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

So it seems like we always need self._serial and self.model.

Does it make sense to make them not optional in init?

"B01 map parsing requires device serial number and model metadata, but they were missing"
)

try:
parsed_data = self._map_parser.parse(
response,
serial=self._serial,
model=self._model,
)
except RoborockException:
raise
except Exception as ex:
Comment thread
arduano marked this conversation as resolved.
raise RoborockException("Failed to parse B01 map data") from ex

if parsed_data.image_content is None:
raise RoborockException("Failed to render B01 map image")

return MapContent(
image_content=parsed_data.image_content,
map_data=parsed_data.map_data,
raw_api_response=response,
)
Loading
Loading