Skip to content

Commit 16bd2d1

Browse files
authored
chore: fix mypy errors (#34)
* chore: fix mypy errors * fix: run mypy through pre-commit * fix: spacing for ci * fix: tests changes * fix: cli exclusion * fix: add typing for roborockenum * fix: ignore warnings with mqtt.client * fix: more mypy changes * fix: limit cli mypy * fix: ignore type for containers * fix: add pre-commit information to dev poetry dependencies
1 parent 6df4511 commit 16bd2d1

File tree

14 files changed

+187
-127
lines changed

14 files changed

+187
-127
lines changed

.github/workflows/ci.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ jobs:
2121
with:
2222
fetch-depth: 0
2323
- uses: wagoid/commitlint-github-action@v5.3.0
24+
lint:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v3
28+
- uses: actions/setup-python@v4
29+
with:
30+
python-version: "3.10"
31+
- uses: pre-commit/action@v3.0.0
2432

2533
test:
2634
strategy:
@@ -57,8 +65,8 @@ jobs:
5765
steps:
5866
- uses: actions/checkout@v3
5967
with:
60-
fetch-depth: 0
61-
persist-credentials: false
68+
fetch-depth: 0
69+
persist-credentials: false
6270

6371
# Run semantic release:
6472
# - Update CHANGELOG.md

.pre-commit-config.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# See https://pre-commit.com for more information
2+
# See https://pre-commit.com/hooks.html for more hooks
3+
default_stages: [ commit ]
4+
5+
6+
repos:
7+
- repo: https://github.com/python-poetry/poetry
8+
rev: 1.3.2
9+
hooks:
10+
- id: poetry-check
11+
- repo: https://github.com/pre-commit/mirrors-mypy
12+
rev: v0.931
13+
hooks:
14+
- id: mypy
15+
exclude: cli.py
16+
additional_dependencies: [ "types-paho-mqtt" ]

mypy.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[mypy]
2+
check_untyped_defs = True
3+
exclude = cli.py

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ build-backend = "poetry.core.masonry.api"
3636
[tool.poetry.dev-dependencies]
3737
pytest-asyncio = "*"
3838
pytest = "*"
39+
pre-commit = "*"
40+
mypy = "*"
3941

4042
[tool.semantic_release]
4143
branch = "main"

roborock/api.py

Lines changed: 76 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import struct
1515
import time
1616
from random import randint
17-
from typing import Any, Callable
17+
from typing import Optional, Any, Callable, Coroutine, Mapping
1818

1919
import aiohttp
2020
from Crypto.Cipher import AES
@@ -23,7 +23,7 @@
2323
from roborock.exceptions import (
2424
RoborockException, RoborockTimeout, VacuumError,
2525
)
26-
from .code_mappings import RoborockDockTypeCode
26+
from .code_mappings import RoborockDockTypeCode, RoborockEnum
2727
from .containers import (
2828
UserData,
2929
Status,
@@ -63,29 +63,29 @@ def md5hex(message: str) -> str:
6363

6464

6565
class PreparedRequest:
66-
def __init__(self, base_url: str, base_headers: dict = None) -> None:
66+
def __init__(self, base_url: str, base_headers: Optional[dict] = None) -> None:
6767
self.base_url = base_url
6868
self.base_headers = base_headers or {}
6969

7070
async def request(
71-
self, method: str, url: str, params=None, data=None, headers=None
72-
) -> dict | list:
71+
self, method: str, url: str, params=None, data=None, headers=None
72+
) -> dict:
7373
_url = "/".join(s.strip("/") for s in [self.base_url, url])
7474
_headers = {**self.base_headers, **(headers or {})}
7575
async with aiohttp.ClientSession() as session:
7676
async with session.request(
77-
method,
78-
_url,
79-
params=params,
80-
data=data,
81-
headers=_headers,
77+
method,
78+
_url,
79+
params=params,
80+
data=data,
81+
headers=_headers,
8282
) as resp:
8383
return await resp.json()
8484

8585

8686
class RoborockClient:
8787

88-
def __init__(self, endpoint: str, devices_info: dict[str, RoborockDeviceInfo]) -> None:
88+
def __init__(self, endpoint: str, devices_info: Mapping[str, RoborockDeviceInfo]) -> None:
8989
self.devices_info = devices_info
9090
self._endpoint = endpoint
9191
self._nonce = secrets.token_bytes(16)
@@ -161,7 +161,7 @@ async def _async_response(self, request_id: int, protocol_id: int = 0) -> tuple[
161161
del self._waiting_queue[request_id]
162162

163163
def _get_payload(
164-
self, method: RoborockCommand, params: list = None, secured=False
164+
self, method: RoborockCommand, params: Optional[list] = None, secured=False
165165
):
166166
timestamp = math.floor(time.time())
167167
request_id = randint(10000, 99999)
@@ -187,24 +187,26 @@ def _get_payload(
187187
return request_id, timestamp, payload
188188

189189
async def send_command(
190-
self, device_id: str, method: RoborockCommand, params: list = None
190+
self, device_id: str, method: RoborockCommand, params: Optional[list] = None
191191
):
192192
raise NotImplementedError
193193

194-
async def get_status(self, device_id: str) -> Status:
194+
async def get_status(self, device_id: str) -> Status | None:
195195
status = await self.send_command(device_id, RoborockCommand.GET_STATUS)
196196
if isinstance(status, dict):
197197
return Status.from_dict(status)
198+
return None
198199

199-
async def get_dnd_timer(self, device_id: str) -> DNDTimer:
200+
async def get_dnd_timer(self, device_id: str) -> DNDTimer | None:
200201
try:
201202
dnd_timer = await self.send_command(device_id, RoborockCommand.GET_DND_TIMER)
202203
if isinstance(dnd_timer, dict):
203204
return DNDTimer.from_dict(dnd_timer)
204205
except RoborockTimeout as e:
205206
_LOGGER.error(e)
207+
return None
206208

207-
async def get_clean_summary(self, device_id: str) -> CleanSummary:
209+
async def get_clean_summary(self, device_id: str) -> CleanSummary | None:
208210
try:
209211
clean_summary = await self.send_command(
210212
device_id, RoborockCommand.GET_CLEAN_SUMMARY
@@ -215,8 +217,9 @@ async def get_clean_summary(self, device_id: str) -> CleanSummary:
215217
return CleanSummary(clean_time=int.from_bytes(clean_summary, 'big'))
216218
except RoborockTimeout as e:
217219
_LOGGER.error(e)
220+
return None
218221

219-
async def get_clean_record(self, device_id: str, record_id: int) -> CleanRecord:
222+
async def get_clean_record(self, device_id: str, record_id: int) -> CleanRecord | None:
220223
try:
221224
clean_record = await self.send_command(
222225
device_id, RoborockCommand.GET_CLEAN_RECORD, [record_id]
@@ -225,56 +228,68 @@ async def get_clean_record(self, device_id: str, record_id: int) -> CleanRecord:
225228
return CleanRecord.from_dict(clean_record)
226229
except RoborockTimeout as e:
227230
_LOGGER.error(e)
231+
return None
228232

229-
async def get_consumable(self, device_id: str) -> Consumable:
233+
async def get_consumable(self, device_id: str) -> Consumable | None:
230234
try:
231235
consumable = await self.send_command(device_id, RoborockCommand.GET_CONSUMABLE)
232236
if isinstance(consumable, dict):
233237
return Consumable.from_dict(consumable)
234238
except RoborockTimeout as e:
235239
_LOGGER.error(e)
240+
return None
236241

237-
async def get_wash_towel_mode(self, device_id: str) -> WashTowelMode:
242+
async def get_wash_towel_mode(self, device_id: str) -> WashTowelMode | None:
238243
try:
239244
washing_mode = await self.send_command(device_id, RoborockCommand.GET_WASH_TOWEL_MODE)
240245
if isinstance(washing_mode, dict):
241246
return WashTowelMode.from_dict(washing_mode)
242247
except RoborockTimeout as e:
243248
_LOGGER.error(e)
249+
return None
244250

245-
async def get_dust_collection_mode(self, device_id: str) -> DustCollectionMode:
251+
async def get_dust_collection_mode(self, device_id: str) -> DustCollectionMode | None:
246252
try:
247253
dust_collection = await self.send_command(device_id, RoborockCommand.GET_DUST_COLLECTION_MODE)
248254
if isinstance(dust_collection, dict):
249255
return DustCollectionMode.from_dict(dust_collection)
250256
except RoborockTimeout as e:
251257
_LOGGER.error(e)
258+
return None
252259

253-
async def get_smart_wash_params(self, device_id: str) -> SmartWashParams:
260+
async def get_smart_wash_params(self, device_id: str) -> SmartWashParams | None:
254261
try:
255262
mop_wash_mode = await self.send_command(device_id, RoborockCommand.GET_SMART_WASH_PARAMS)
256263
if isinstance(mop_wash_mode, dict):
257264
return SmartWashParams.from_dict(mop_wash_mode)
258265
except RoborockTimeout as e:
259266
_LOGGER.error(e)
267+
return None
260268

261-
async def get_dock_summary(self, device_id: str, dock_type: RoborockDockTypeCode) -> RoborockDockSummary:
269+
async def get_dock_summary(self, device_id: str, dock_type: RoborockEnum) -> RoborockDockSummary | None:
270+
"""Gets the status summary from the dock with the methods available for a given dock.
271+
272+
:param dock_type: RoborockDockTypeCode"""
273+
if RoborockDockTypeCode.name != "RoborockDockTypeCode":
274+
raise RoborockException("Invalid enum given for dock type")
262275
try:
263-
commands = [self.get_dust_collection_mode(device_id)]
276+
commands: list[Coroutine[Any, Any, DustCollectionMode | WashTowelMode | SmartWashParams | None]] = [
277+
self.get_dust_collection_mode(device_id)]
264278
if dock_type == RoborockDockTypeCode['3']:
265279
commands += [self.get_wash_towel_mode(device_id), self.get_smart_wash_params(device_id)]
266280
[
267281
dust_collection_mode,
268282
wash_towel_mode,
269283
smart_wash_params
270284
] = (
271-
list(await asyncio.gather(*commands))
272-
+ [None, None]
285+
list(await asyncio.gather(*commands))
286+
+ [None, None]
273287
)[:3]
274288

275289
return RoborockDockSummary(dust_collection_mode, wash_towel_mode, smart_wash_params)
276290
except RoborockTimeout as e:
277291
_LOGGER.error(e)
292+
return None
278293

279294
async def get_prop(self, device_id: str) -> RoborockDeviceProp | None:
280295
[status, dnd_timer, clean_summary, consumable] = await asyncio.gather(
@@ -299,7 +314,7 @@ async def get_prop(self, device_id: str) -> RoborockDeviceProp | None:
299314
)
300315
return None
301316

302-
async def get_multi_maps_list(self, device_id) -> MultiMapsList:
317+
async def get_multi_maps_list(self, device_id) -> MultiMapsList | None:
303318
try:
304319
multi_maps_list = await self.send_command(
305320
device_id, RoborockCommand.GET_MULTI_MAPS_LIST
@@ -308,14 +323,16 @@ async def get_multi_maps_list(self, device_id) -> MultiMapsList:
308323
return MultiMapsList.from_dict(multi_maps_list)
309324
except RoborockTimeout as e:
310325
_LOGGER.error(e)
326+
return None
311327

312-
async def get_networking(self, device_id) -> NetworkInfo:
328+
async def get_networking(self, device_id) -> NetworkInfo | None:
313329
try:
314330
networking_info = await self.send_command(device_id, RoborockCommand.GET_NETWORK_INFO)
315331
if isinstance(networking_info, dict):
316332
return NetworkInfo.from_dict(networking_info)
317333
except RoborockTimeout as e:
318334
_LOGGER.error(e)
335+
return None
319336

320337

321338
class RoborockApiClient:
@@ -334,9 +351,14 @@ async def _get_base_url(self) -> str:
334351
"/api/v1/getUrlByEmail",
335352
params={"email": self._username, "needtwostepauth": "false"},
336353
)
354+
if response is None:
355+
raise RoborockException("get url by email returned None")
337356
if response.get("code") != 200:
338357
raise RoborockException(response.get("error"))
339-
self.base_url = response.get("data").get("url")
358+
response_data = response.get("data")
359+
if response_data is None:
360+
raise RoborockException("response does not have 'data'")
361+
self.base_url = response_data.get("url")
340362
return self.base_url
341363

342364
def _get_header_client_id(self):
@@ -358,7 +380,8 @@ async def request_code(self) -> None:
358380
"type": "auth",
359381
},
360382
)
361-
383+
if code_response is None:
384+
raise RoborockException("Failed to get a response from send email code")
362385
if code_response.get("code") != 200:
363386
raise RoborockException(code_response.get("msg"))
364387

@@ -376,10 +399,14 @@ async def pass_login(self, password: str) -> UserData:
376399
"needtwostepauth": "false",
377400
},
378401
)
379-
402+
if login_response is None:
403+
raise RoborockException("Login response is none")
380404
if login_response.get("code") != 200:
381405
raise RoborockException(login_response.get("msg"))
382-
return UserData.from_dict(login_response.get("data"))
406+
user_data = login_response.get("data")
407+
if not isinstance(user_data, dict):
408+
raise RoborockException("Got unexpected data type for user_data")
409+
return UserData.from_dict(user_data)
383410

384411
async def code_login(self, code) -> UserData:
385412
base_url = await self._get_base_url()
@@ -395,15 +422,21 @@ async def code_login(self, code) -> UserData:
395422
"verifycodetype": "AUTH_EMAIL_CODE",
396423
},
397424
)
398-
425+
if login_response is None:
426+
raise RoborockException("Login request response is None")
399427
if login_response.get("code") != 200:
400428
raise RoborockException(login_response.get("msg"))
401-
return UserData.from_dict(login_response.get("data"))
429+
user_data = login_response.get("data")
430+
if not isinstance(user_data, dict):
431+
raise RoborockException("Got unexpected data type for user_data")
432+
return UserData.from_dict(user_data)
402433

403434
async def get_home_data(self, user_data: UserData) -> HomeData:
404435
base_url = await self._get_base_url()
405436
header_clientid = self._get_header_client_id()
406437
rriot = user_data.rriot
438+
if rriot is None:
439+
raise RoborockException("rriot is none")
407440
home_id_request = PreparedRequest(
408441
base_url, {"header_clientid": header_clientid}
409442
)
@@ -412,9 +445,12 @@ async def get_home_data(self, user_data: UserData) -> HomeData:
412445
"/api/v1/getHomeDetail",
413446
headers={"Authorization": user_data.token},
414447
)
448+
if home_id_response is None:
449+
raise RoborockException("home_id_response is None")
415450
if home_id_response.get("code") != 200:
416451
raise RoborockException(home_id_response.get("msg"))
417-
home_id = home_id_response.get("data").get("rrHomeId")
452+
453+
home_id = home_id_response['data'].get("rrHomeId")
418454
timestamp = math.floor(time.time())
419455
nonce = secrets.token_urlsafe(6)
420456
prestr = ":".join(
@@ -431,6 +467,8 @@ async def get_home_data(self, user_data: UserData) -> HomeData:
431467
mac = base64.b64encode(
432468
hmac.new(rriot.h.encode(), prestr.encode(), hashlib.sha256).digest()
433469
).decode()
470+
if rriot.r.a is None:
471+
raise RoborockException("Missing field 'a' in rriot reference")
434472
home_request = PreparedRequest(
435473
rriot.r.a,
436474
{
@@ -442,4 +480,7 @@ async def get_home_data(self, user_data: UserData) -> HomeData:
442480
if not home_response.get("success"):
443481
raise RoborockException(home_response)
444482
home_data = home_response.get("result")
445-
return HomeData.from_dict(home_data)
483+
if isinstance(home_data, dict):
484+
return HomeData.from_dict(home_data)
485+
else:
486+
raise RoborockException("home_response result was an unexpected type")

roborock/cli.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import json
24
import logging
35
from pathlib import Path
@@ -16,7 +18,7 @@
1618

1719
class RoborockContext:
1820
roborock_file = Path("~/.roborock").expanduser()
19-
_login_data: LoginData = None
21+
_login_data: LoginData | None = None
2022

2123
def __init__(self):
2224
self.reload()

0 commit comments

Comments
 (0)