Skip to content

Commit

Permalink
feat: add v1 api (#194)
Browse files Browse the repository at this point in the history
* feat: add v1 api

* fix: change some imports

* fix: bug and versioning

* chore: move location of v1

* fix: random exception
  • Loading branch information
Lash-L authored Mar 6, 2024
1 parent 4afbc98 commit 9fb124e
Show file tree
Hide file tree
Showing 12 changed files with 377 additions and 289 deletions.
42 changes: 21 additions & 21 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ construct = "^2.10.57"


[build-system]
requires = ["poetry-core==1.7.1"]
requires = ["poetry-core==1.8.0"]
build-backend = "poetry.core.masonry.api"

[tool.poetry.group.dev.dependencies]
Expand Down
202 changes: 3 additions & 199 deletions roborock/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,20 @@
import hashlib
import json
import logging
import math
import secrets
import struct
import time
from collections.abc import Callable, Coroutine
from random import randint
from typing import Any, TypeVar, final

from .code_mappings import RoborockDockTypeCode
from .command_cache import CacheableAttribute, CommandType, RoborockAttribute, find_cacheable_attribute, get_cache_map
from .containers import (
ChildLockStatus,
CleanRecord,
CleanSummary,
Consumable,
DeviceData,
DnDTimer,
DustCollectionMode,
FlowLedStatus,
ModelStatus,
MultiMapsList,
NetworkInfo,
RoborockBase,
RoomMapping,
S7MaxVStatus,
ServerTimer,
SmartWashParams,
Status,
ValleyElectricityTimer,
WashTowelMode,
)
from .exceptions import (
RoborockException,
Expand All @@ -54,21 +38,12 @@
RoborockMessage,
RoborockMessageProtocol,
)
from .roborock_typing import DeviceProp, DockSummary, RoborockCommand
from .util import RepeatableTask, RoborockLoggerAdapter, get_running_loop_or_create_one, unpack_list
from .roborock_typing import RoborockCommand
from .util import RepeatableTask, RoborockLoggerAdapter, get_running_loop_or_create_one

_LOGGER = logging.getLogger(__name__)
KEEPALIVE = 60
COMMANDS_SECURED = [
RoborockCommand.GET_MAP_V1,
RoborockCommand.GET_MULTI_MAP,
]
RT = TypeVar("RT", bound=RoborockBase)
WASH_N_FILL_DOCK = [
RoborockDockTypeCode.empty_wash_fill_dock,
RoborockDockTypeCode.s8_dock,
RoborockDockTypeCode.p10_dock,
]


def md5hex(message: str) -> str:
Expand Down Expand Up @@ -286,7 +261,7 @@ def on_message_received(self, messages: list[RoborockMessage]) -> None:
try:
decrypted = Utils.decrypt_cbc(data.payload[24:], self._nonce)
except ValueError as err:
raise RoborockException("Failed to decode %s for %s", data.payload, data.protocol) from err
raise RoborockException(f"Failed to decode {data.payload!r} for {data.protocol}") from err
decompressed = Utils.decompress(decrypted)
queue = self._waiting_queue.get(request_id)
if queue:
Expand Down Expand Up @@ -336,35 +311,6 @@ def _async_response(
self._waiting_queue[request_id] = queue
return self._wait_response(request_id, queue)

def _get_payload(
self,
method: RoborockCommand | str,
params: list | dict | int | None = None,
secured=False,
):
timestamp = math.floor(time.time())
request_id = randint(10000, 32767)
inner = {
"id": request_id,
"method": method,
"params": params or [],
}
if secured:
inner["security"] = {
"endpoint": self._endpoint,
"nonce": self._nonce.hex().lower(),
}
payload = bytes(
json.dumps(
{
"dps": {"101": json.dumps(inner, separators=(",", ":"))},
"t": timestamp,
},
separators=(",", ":"),
).encode()
)
return request_id, timestamp, payload

async def send_message(self, roborock_message: RoborockMessage):
raise NotImplementedError

Expand Down Expand Up @@ -402,148 +348,6 @@ async def send_command(
return return_type.from_dict(response)
return response

async def get_status(self) -> Status:
data = self._status_type.from_dict(await self.cache[CacheableAttribute.status].async_value())
if data is None:
return self._status_type()
return data

async def get_dnd_timer(self) -> DnDTimer | None:
return DnDTimer.from_dict(await self.cache[CacheableAttribute.dnd_timer].async_value())

async def get_valley_electricity_timer(self) -> ValleyElectricityTimer | None:
return ValleyElectricityTimer.from_dict(
await self.cache[CacheableAttribute.valley_electricity_timer].async_value()
)

async def get_clean_summary(self) -> CleanSummary | None:
clean_summary: dict | list | int = await self.send_command(RoborockCommand.GET_CLEAN_SUMMARY)
if isinstance(clean_summary, dict):
return CleanSummary.from_dict(clean_summary)
elif isinstance(clean_summary, list):
clean_time, clean_area, clean_count, records = unpack_list(clean_summary, 4)
return CleanSummary(
clean_time=clean_time,
clean_area=clean_area,
clean_count=clean_count,
records=records,
)
elif isinstance(clean_summary, int):
return CleanSummary(clean_time=clean_summary)
return None

async def get_clean_record(self, record_id: int) -> CleanRecord | None:
record: dict | list = await self.send_command(RoborockCommand.GET_CLEAN_RECORD, [record_id])
if isinstance(record, dict):
return CleanRecord.from_dict(record)
elif isinstance(record, list):
# There are still a few unknown variables in this.
begin, end, duration, area = unpack_list(record, 4)
return CleanRecord(begin=begin, end=end, duration=duration, area=area)
else:
_LOGGER.warning("Clean record was of a new type, please submit an issue request: %s", record)
return None

async def get_consumable(self) -> Consumable:
data = Consumable.from_dict(await self.cache[CacheableAttribute.consumable].async_value())
if data is None:
return Consumable()
return data

async def get_wash_towel_mode(self) -> WashTowelMode | None:
return WashTowelMode.from_dict(await self.cache[CacheableAttribute.wash_towel_mode].async_value())

async def get_dust_collection_mode(self) -> DustCollectionMode | None:
return DustCollectionMode.from_dict(await self.cache[CacheableAttribute.dust_collection_mode].async_value())

async def get_smart_wash_params(self) -> SmartWashParams | None:
return SmartWashParams.from_dict(await self.cache[CacheableAttribute.smart_wash_params].async_value())

async def get_dock_summary(self, dock_type: RoborockDockTypeCode) -> DockSummary:
"""Gets the status summary from the dock with the methods available for a given dock.
:param dock_type: RoborockDockTypeCode"""
commands: list[
Coroutine[
Any,
Any,
DustCollectionMode | WashTowelMode | SmartWashParams | None,
]
] = [self.get_dust_collection_mode()]
if dock_type in WASH_N_FILL_DOCK:
commands += [
self.get_wash_towel_mode(),
self.get_smart_wash_params(),
]
[dust_collection_mode, wash_towel_mode, smart_wash_params] = unpack_list(
list(await asyncio.gather(*commands)), 3
) # type: DustCollectionMode, WashTowelMode | None, SmartWashParams | None # type: ignore

return DockSummary(dust_collection_mode, wash_towel_mode, smart_wash_params)

async def get_prop(self) -> DeviceProp | None:
"""Gets device general properties."""
# Mypy thinks that each one of these is typed as a union of all the others. so we do type ignore.
status, clean_summary, consumable = await asyncio.gather(
*[
self.get_status(),
self.get_clean_summary(),
self.get_consumable(),
]
) # type: Status, CleanSummary, Consumable # type: ignore
last_clean_record = None
if clean_summary and clean_summary.records and len(clean_summary.records) > 0:
last_clean_record = await self.get_clean_record(clean_summary.records[0])
dock_summary = None
if status and status.dock_type is not None and status.dock_type != RoborockDockTypeCode.no_dock:
dock_summary = await self.get_dock_summary(status.dock_type)
if any([status, clean_summary, consumable]):
return DeviceProp(
status,
clean_summary,
consumable,
last_clean_record,
dock_summary,
)
return None

async def get_multi_maps_list(self) -> MultiMapsList | None:
return await self.send_command(RoborockCommand.GET_MULTI_MAPS_LIST, return_type=MultiMapsList)

async def get_networking(self) -> NetworkInfo | None:
return await self.send_command(RoborockCommand.GET_NETWORK_INFO, return_type=NetworkInfo)

async def get_room_mapping(self) -> list[RoomMapping] | None:
"""Gets the mapping from segment id -> iot id. Only works on local api."""
mapping: list = await self.send_command(RoborockCommand.GET_ROOM_MAPPING)
if isinstance(mapping, list):
return [
RoomMapping(segment_id=segment_id, iot_id=iot_id) # type: ignore
for segment_id, iot_id in [unpack_list(room, 2) for room in mapping if isinstance(room, list)]
]
return None

async def get_child_lock_status(self) -> ChildLockStatus:
"""Gets current child lock status."""
return ChildLockStatus.from_dict(await self.cache[CacheableAttribute.child_lock_status].async_value())

async def get_flow_led_status(self) -> FlowLedStatus:
"""Gets current flow led status."""
return FlowLedStatus.from_dict(await self.cache[CacheableAttribute.flow_led_status].async_value())

async def get_sound_volume(self) -> int | None:
"""Gets current volume level."""
return await self.cache[CacheableAttribute.sound_volume].async_value()

async def get_server_timer(self) -> list[ServerTimer]:
"""Gets current server timer."""
server_timers = await self.cache[CacheableAttribute.server_timer].async_value()
if server_timers:
if isinstance(server_timers[0], list):
return [ServerTimer(*server_timer) for server_timer in server_timers]
return [ServerTimer(*server_timers)]
return []

def add_listener(
self, protocol: RoborockDataProtocol, listener: Callable, cache: dict[CacheableAttribute, AttributeCache]
) -> None:
Expand Down
4 changes: 2 additions & 2 deletions roborock/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
from pyshark.packet.packet import Packet # type: ignore

from roborock import RoborockException
from roborock.cloud_api import RoborockMqttClient
from roborock.containers import DeviceData, LoginData
from roborock.protocol import MessageParser
from roborock.util import run_sync
from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1
from roborock.web_api import RoborockApiClient

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -135,7 +135,7 @@ async def command(ctx, cmd, device_id, params):
if model is None:
raise RoborockException(f"Could not find model for device {device.name}")
device_info = DeviceData(device=device, model=model)
mqtt_client = RoborockMqttClient(login_data.user_data, device_info)
mqtt_client = RoborockMqttClientV1(login_data.user_data, device_info)
await mqtt_client.send_command(cmd, json.loads(params) if params is not None else None)
mqtt_client.__del__()

Expand Down
Loading

0 comments on commit 9fb124e

Please sign in to comment.