From 53b26ce40a710b44ecf36206f321b7d2b403783f Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 11 Jan 2024 20:05:34 +0100 Subject: [PATCH 1/5] expose room name in position object added room_id to Position obj current_room_id on device fix add room_id on PositionEvent removed current_room_id semplified get and set device rooms added room_name to position fix map property don't used yet expose room name in position object --- deebot_client/command.py | 13 ++++++----- deebot_client/commands/json/pos.py | 35 +++++++++++++++++++++++------- deebot_client/device.py | 22 +++++++++++++++++-- deebot_client/events/map.py | 1 + deebot_client/map.py | 6 ++++- deebot_client/models.py | 3 +++ requirements.txt | 1 + tests/test_map.py | 2 +- 8 files changed, 66 insertions(+), 17 deletions(-) diff --git a/deebot_client/command.py b/deebot_client/command.py index c02ea7b45..300bb0e65 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/pos.py b/deebot_client/commands/json/pos.py index d96e1f500..a852fb421 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 6a6e15b5a..fc757579d 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 345ac452b..7d7346faf 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 51520bf47..6df45e4b5 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,10 @@ 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) + decompressed_coords = _decompress_7z_base64_data(event.coordinates).decode('utf-8') + coordinates = [tuple(map(float, pair.split(','))) for pair in decompressed_coords.split(';')] + room = Room(event.name, event.id, decompressed_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 674c7b43a..1c92a0ec0 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 22ee78dd9..ad91049aa 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 f1b4fc801..a9ed80489 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, -1)) map_data.rooms[x] = Room("test", x, "1,2") assert map_data.changed is True From b28f1921b16ce79087b7d93e5a2a455a3db217f1 Mon Sep 17 00:00:00 2001 From: Francesco Date: Thu, 18 Jan 2024 20:01:31 +0100 Subject: [PATCH 2/5] fix test_map --- tests/test_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_map.py b/tests/test_map.py index a9ed80489..f131baeee 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, -1)) + 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 From f04d926f18aebe7ae0059fdf0b359332b799f584 Mon Sep 17 00:00:00 2001 From: Francesco Date: Sat, 20 Jan 2024 17:57:35 +0100 Subject: [PATCH 3/5] fix parameter room in Cean.py --- deebot_client/commands/json/clean.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deebot_client/commands/json/clean.py b/deebot_client/commands/json/clean.py index 0da9e6aab..4ebffd064 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]: From 72be904b6f2085a6e2d0ff6eeadd862ccb33f398 Mon Sep 17 00:00:00 2001 From: Francesco Date: Tue, 23 Jan 2024 00:24:22 +0100 Subject: [PATCH 4/5] fix shapely dipendence --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ad91049aa..6dd738d29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ defusedxml numpy>=1.23.2,<2.0 Pillow>=10.0.1,<11.0 svg.py>=1.4.2 -shapely~=2.0.2 +shapely>=2.0.2 From 37a76deeebec608d8305981c8df09b91eeb7b937 Mon Sep 17 00:00:00 2001 From: Francesco Date: Tue, 23 Jan 2024 00:50:30 +0100 Subject: [PATCH 5/5] handle plain and compressed coords --- deebot_client/map.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/deebot_client/map.py b/deebot_client/map.py index 6df45e4b5..63c12d6ad 100644 --- a/deebot_client/map.py +++ b/deebot_client/map.py @@ -408,9 +408,14 @@ async def on_map_set(event: MapSetEvent) -> None: async def on_map_subset(event: MapSubsetEvent) -> None: if event.type == MapSetType.ROOMS and event.name: - decompressed_coords = _decompress_7z_base64_data(event.coordinates).decode('utf-8') - coordinates = [tuple(map(float, pair.split(','))) for pair in decompressed_coords.split(';')] - room = Room(event.name, event.id, decompressed_coords, shapely.geometry.Polygon(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