diff --git a/deebot_client/command.py b/deebot_client/command.py index c02ea7b4..300bb0e6 100644 --- a/deebot_client/command.py +++ b/deebot_client/command.py @@ -13,7 +13,7 @@ from .event_bus import EventBus from .logging_filter import get_logger from .message import HandlingResult, HandlingState, Message -from .models import DeviceInfo +from .models import DeviceInfo, Room _LOGGER = get_logger(__name__) @@ -63,7 +63,7 @@ def _get_payload(self) -> dict[str, Any] | list[Any] | str: @final async def execute( - self, authenticator: Authenticator, device_info: DeviceInfo, event_bus: EventBus + self, authenticator: Authenticator, device_info: DeviceInfo, event_bus: EventBus, rooms: list[Room] = [] ) -> bool: """Execute command. @@ -73,14 +73,14 @@ async def execute( This value is not indicating if the command was executed successfully. """ try: - result = await self._execute(authenticator, device_info, event_bus) + result = await self._execute(authenticator, device_info, event_bus, rooms) if result.state == HandlingState.SUCCESS: # Execute command which are requested by the handler async with asyncio.TaskGroup() as tg: for requested_command in result.requested_commands: tg.create_task( requested_command.execute( - authenticator, device_info, event_bus + authenticator, device_info, event_bus, rooms ) ) @@ -94,11 +94,14 @@ async def execute( return False async def _execute( - self, authenticator: Authenticator, device_info: DeviceInfo, event_bus: EventBus + self, authenticator: Authenticator, device_info: DeviceInfo, event_bus: EventBus, rooms: list[Room] = [] ) -> CommandResult: """Execute command.""" response = await self._execute_api_request(authenticator, device_info) + if self.name == "getPos": + response["resp"]["body"]["data"]["rooms"] = rooms + result = self.__handle_response(event_bus, response) if result.state == HandlingState.ANALYSE: _LOGGER.debug( diff --git a/deebot_client/commands/json/clean.py b/deebot_client/commands/json/clean.py index 0da9e6aa..4ebffd06 100644 --- a/deebot_client/commands/json/clean.py +++ b/deebot_client/commands/json/clean.py @@ -7,7 +7,7 @@ from deebot_client.events import StateEvent from deebot_client.logging_filter import get_logger from deebot_client.message import HandlingResult, MessageBodyDataDict -from deebot_client.models import CleanAction, CleanMode, DeviceInfo, State +from deebot_client.models import CleanAction, CleanMode, DeviceInfo, State, Room from .common import ExecuteCommand, JsonCommandWithMessageHandling @@ -23,7 +23,7 @@ def __init__(self, action: CleanAction) -> None: super().__init__(self.__get_args(action)) async def _execute( - self, authenticator: Authenticator, device_info: DeviceInfo, event_bus: EventBus + self, authenticator: Authenticator, device_info: DeviceInfo, event_bus: EventBus, rooms: list[Room] = [] ) -> CommandResult: """Execute command.""" state = event_bus.get_last_event(StateEvent) @@ -39,7 +39,7 @@ async def _execute( ): self._args = self.__get_args(CleanAction.RESUME) - return await super()._execute(authenticator, device_info, event_bus) + return await super()._execute(authenticator, device_info, event_bus, rooms) @staticmethod def __get_args(action: CleanAction) -> dict[str, Any]: diff --git a/deebot_client/commands/json/pos.py b/deebot_client/commands/json/pos.py index d96e1f50..a852fb42 100644 --- a/deebot_client/commands/json/pos.py +++ b/deebot_client/commands/json/pos.py @@ -8,6 +8,7 @@ from .common import JsonCommandWithMessageHandling +from shapely.geometry import Point class GetPos(JsonCommandWithMessageHandling, MessageBodyDataDict): """Get volume command.""" @@ -27,26 +28,44 @@ def _handle_body_data_dict( """ positions = [] + rooms = data.get("rooms", []) + for type_str in ["deebotPos", "chargePos"]: + + room_name = None data_positions = data.get(type_str, []) if isinstance(data_positions, dict): + point = Point(data_positions.get("x"), data_positions.get("y")) + for room in rooms: + if room.polygon is not None and room.polygon.contains(point): + room_name = room.name + break + positions.append( Position( type=PositionType(type_str), - x=data_positions["x"], - y=data_positions["y"], + x=data_positions.get("x"), + y=data_positions.get("y"), + room=room_name ) ) else: - positions.extend( - [ + for entry in data_positions: + point = Point(entry.get("x"), entry.get("y")) + for room in rooms: + if room.polygon is not None and room.polygon.contains(point): + room_name = room.name + break + + positions.append( Position( - type=PositionType(type_str), x=entry["x"], y=entry["y"] + type=PositionType(type_str), + x=entry.get("x"), + y=entry.get("y"), + room=room_name ) - for entry in data_positions - ] - ) + ) if positions: event_bus.notify(PositionsEvent(positions=positions)) diff --git a/deebot_client/device.py b/deebot_client/device.py index 6a6e15b5..fc757579 100644 --- a/deebot_client/device.py +++ b/deebot_client/device.py @@ -22,12 +22,15 @@ StateEvent, StatsEvent, TotalStatsEvent, + RoomsEvent, Position ) from .logging_filter import get_logger -from .map import Map +from .map import Map, Room from .messages import get_message from .models import DeviceInfo, State +from shapely.geometry import Point + if TYPE_CHECKING: from collections.abc import Callable @@ -53,6 +56,8 @@ def __init__( self._available_task: asyncio.Task[Any] | None = None self._unsubscribe: Callable[[], None] | None = None + self.rooms: list[Room] = [] + self.fw_version: str | None = None self.mac: str | None = None self.events: Final[EventBus] = EventBus( @@ -80,6 +85,12 @@ async def on_pos(event: PositionsEvent) -> None: self.events.subscribe(PositionsEvent, on_pos) + async def on_rooms(event: RoomsEvent) -> None: + self.rooms = event.rooms + self.events.request_refresh(PositionsEvent) + + self.events.subscribe(RoomsEvent, on_rooms) + async def on_state(event: StateEvent) -> None: if event.state == State.DOCKED: self.events.request_refresh(CleanLogEvent) @@ -155,7 +166,7 @@ async def _execute_command(self, command: Command) -> bool: """Execute given command.""" async with self._semaphore: if await command.execute( - self._authenticator, self.device_info, self.events + self._authenticator, self.device_info, self.events, self.rooms ): self._set_available(available=True) return True @@ -184,10 +195,17 @@ def _handle_message( _LOGGER.debug("Try to handle message %s: %s", message_name, message_data) if message := get_message(message_name, self.device_info.data_type): + + rooms = self.rooms + if isinstance(message_data, dict): data = message_data + if message_name == "onPos": + data["rooms"] = rooms else: data = json.loads(message_data) + if message_name == "onPos": + data["body"]["data"]["rooms"] = rooms fw_version = data.get("header", {}).get("fwVer", None) if fw_version: diff --git a/deebot_client/events/map.py b/deebot_client/events/map.py index 345ac452..7d7346fa 100644 --- a/deebot_client/events/map.py +++ b/deebot_client/events/map.py @@ -22,6 +22,7 @@ class Position: type: PositionType x: int y: int + room: str | None @dataclass(frozen=True) diff --git a/deebot_client/map.py b/deebot_client/map.py index 51520bf4..63c12d6a 100644 --- a/deebot_client/map.py +++ b/deebot_client/map.py @@ -12,6 +12,7 @@ import struct from typing import Any, Final import zlib +import shapely.geometry from PIL import Image, ImageColor, ImageOps, ImagePalette import svg @@ -407,7 +408,15 @@ async def on_map_set(event: MapSetEvent) -> None: async def on_map_subset(event: MapSubsetEvent) -> None: if event.type == MapSetType.ROOMS and event.name: - room = Room(event.name, event.id, event.coordinates) + + if event.coordinates.endswith("="): + coords = _decompress_7z_base64_data(event.coordinates).decode('utf-8') + else: + coords = event.coordinates + + coordinates = [tuple(map(float, pair.split(','))) for pair in coords.split(';')] + room = Room(event.name, event.id, coords, shapely.geometry.Polygon(coordinates)) + if self._map_data.rooms.get(event.id, None) != room: self._map_data.rooms[room.id] = room diff --git a/deebot_client/models.py b/deebot_client/models.py index 674c7b43..1c92a0ec 100644 --- a/deebot_client/models.py +++ b/deebot_client/models.py @@ -9,6 +9,8 @@ from deebot_client.const import DataType from deebot_client.util.continents import get_continent +from shapely.geometry import Polygon + if TYPE_CHECKING: from deebot_client.capabilities import Capabilities @@ -109,6 +111,7 @@ class Room: name: str id: int coordinates: str + polygon: Polygon = None @unique diff --git a/requirements.txt b/requirements.txt index 22ee78dd..6dd738d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ defusedxml numpy>=1.23.2,<2.0 Pillow>=10.0.1,<11.0 svg.py>=1.4.2 +shapely>=2.0.2 diff --git a/tests/test_map.py b/tests/test_map.py index f1b4fc80..f131baee 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -96,7 +96,7 @@ async def test_MapData(event_bus: EventBus) -> None: async def test_cycle() -> None: for x in range(10000): - map_data.positions.append(Position(PositionType.DEEBOT, x, x)) + map_data.positions.append(Position(PositionType.DEEBOT, x, x, "test")) map_data.rooms[x] = Room("test", x, "1,2") assert map_data.changed is True