Skip to content

Commit 18b8efd

Browse files
authored
Merge branch 'main' into cli-session
2 parents c676b56 + 9e0ddf8 commit 18b8efd

File tree

10 files changed

+209
-161
lines changed

10 files changed

+209
-161
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ jobs:
7575
persist-credentials: false
7676
- name: Python Semantic Release
7777
id: release
78-
uses: python-semantic-release/python-semantic-release@v9.21.0
78+
uses: python-semantic-release/python-semantic-release@v10.1.0
7979
with:
8080
github_token: ${{ secrets.GH_TOKEN }}
8181

poetry.lock

Lines changed: 118 additions & 113 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "python-roborock"
3-
version = "2.19.0"
3+
version = "2.20.0"
44
description = "A package to control Roborock vacuums."
55
authors = ["humbertogontijo <humbertogontijo@users.noreply.github.com>"]
66
license = "GPL-3.0-only"

roborock/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def should_keepalive(self) -> bool:
8585

8686
async def validate_connection(self) -> None:
8787
if not self.should_keepalive():
88-
self._logger.info("Resetting Roborock connection due to kepalive timeout")
88+
self._logger.info("Resetting Roborock connection due to keepalive timeout")
8989
await self.async_disconnect()
9090
await self.async_connect()
9191

roborock/code_mappings.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,17 @@ class RoborockFanSpeedQRevoCurv(RoborockFanPowerCode):
267267
smart_mode = 110
268268

269269

270+
class RoborockFanSpeedQRevoMaxV(RoborockFanPowerCode):
271+
off = 105
272+
quiet = 101
273+
balanced = 102
274+
turbo = 103
275+
max = 104
276+
custom = 106
277+
max_plus = 108
278+
smart_mode = 110
279+
280+
270281
class RoborockFanSpeedP10(RoborockFanPowerCode):
271282
off = 105
272283
quiet = 101
@@ -275,6 +286,7 @@ class RoborockFanSpeedP10(RoborockFanPowerCode):
275286
max = 104
276287
custom = 106
277288
max_plus = 108
289+
smart_mode = 110
278290

279291

280292
class RoborockFanSpeedS8MaxVUltra(RoborockFanPowerCode):
@@ -316,6 +328,7 @@ class RoborockMopModeS8ProUltra(RoborockMopModeCode):
316328
deep_plus = 303
317329
fast = 304
318330
custom = 302
331+
smart_mode = 306
319332

320333

321334
class RoborockMopModeS8MaxVUltra(RoborockMopModeCode):
@@ -337,6 +350,15 @@ class RoborockMopModeQRevoMaster(RoborockMopModeCode):
337350
smart_mode = 306
338351

339352

353+
class RoborockMopModeQRevoMaxV(RoborockMopModeCode):
354+
standard = 300
355+
deep = 301
356+
custom = 302
357+
deep_plus = 303
358+
fast = 304
359+
smart_mode = 306
360+
361+
340362
class RoborockMopIntensityCode(RoborockEnum):
341363
"""Describes the mop intensity of the vacuum cleaner."""
342364

@@ -383,6 +405,16 @@ class RoborockMopIntensityQRevoCurv(RoborockMopIntensityCode):
383405
smart_mode = 209
384406

385407

408+
class RoborockMopIntensityQRevoMaxV(RoborockMopIntensityCode):
409+
off = 200
410+
low = 201
411+
medium = 202
412+
high = 203
413+
custom = 204
414+
custom_water_flow = 207
415+
smart_mode = 209
416+
417+
386418
class RoborockMopIntensityP10(RoborockMopIntensityCode):
387419
"""Describes the mop intensity of the vacuum cleaner."""
388420

@@ -392,6 +424,7 @@ class RoborockMopIntensityP10(RoborockMopIntensityCode):
392424
high = 203
393425
custom = 204
394426
custom_water_flow = 207
427+
smart_mode = 209
395428

396429

397430
class RoborockMopIntensityS8MaxVUltra(RoborockMopIntensityCode):

roborock/containers.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
RoborockFanSpeedQ7Max,
2323
RoborockFanSpeedQRevoCurv,
2424
RoborockFanSpeedQRevoMaster,
25+
RoborockFanSpeedQRevoMaxV,
2526
RoborockFanSpeedS6Pure,
2627
RoborockFanSpeedS7,
2728
RoborockFanSpeedS7MaxV,
@@ -33,13 +34,15 @@
3334
RoborockMopIntensityQ7Max,
3435
RoborockMopIntensityQRevoCurv,
3536
RoborockMopIntensityQRevoMaster,
37+
RoborockMopIntensityQRevoMaxV,
3638
RoborockMopIntensityS5Max,
3739
RoborockMopIntensityS6MaxV,
3840
RoborockMopIntensityS7,
3941
RoborockMopIntensityS8MaxVUltra,
4042
RoborockMopModeCode,
4143
RoborockMopModeQRevoCurv,
4244
RoborockMopModeQRevoMaster,
45+
RoborockMopModeQRevoMaxV,
4346
RoborockMopModeS7,
4447
RoborockMopModeS8MaxVUltra,
4548
RoborockMopModeS8ProUltra,
@@ -599,6 +602,13 @@ class QRevoCurvStatus(Status):
599602
mop_mode: RoborockMopModeQRevoCurv | None = None
600603

601604

605+
@dataclass
606+
class QRevoMaxVStatus(Status):
607+
fan_power: RoborockFanSpeedQRevoMaxV | None = None
608+
water_box_mode: RoborockMopIntensityQRevoMaxV | None = None
609+
mop_mode: RoborockMopModeQRevoMaxV | None = None
610+
611+
602612
@dataclass
603613
class S6MaxVStatus(Status):
604614
fan_power: RoborockFanSpeedS7MaxV | None = None
@@ -672,7 +682,7 @@ class S8MaxvUltraStatus(Status):
672682
# but i am currently unable to do my typical reverse engineering/ get any data from users on this,
673683
# so this will be here in the mean time.
674684
ROBOROCK_QREVO_S: P10Status,
675-
ROBOROCK_QREVO_MAXV: P10Status,
685+
ROBOROCK_QREVO_MAXV: QRevoMaxVStatus,
676686
ROBOROCK_QREVO_PRO: P10Status,
677687
ROBOROCK_S8_MAXV_ULTRA: S8MaxvUltraStatus,
678688
}

roborock/mqtt/roborock_session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ async def _mqtt_client(self, params: MqttParams) -> aiomqtt.Client:
160160
async with self._client_lock:
161161
self._client = client
162162
for topic in self._listeners:
163-
_LOGGER.debug("Re-establising subscription to topic %s", topic)
163+
_LOGGER.debug("Re-establishing subscription to topic %s", topic)
164164
# TODO: If this fails it will break the whole connection. Make
165165
# this retry again in the background with backoff.
166166
await client.subscribe(topic)

roborock/web_api.py

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -90,39 +90,6 @@ def _get_header_client_id(self):
9090
md5.update(self._device_identifier.encode())
9191
return base64.b64encode(md5.digest()).decode()
9292

93-
def _process_extra_hawk_values(self, values: dict | None) -> str:
94-
if values is None:
95-
return ""
96-
else:
97-
sorted_keys = sorted(values.keys())
98-
result = []
99-
for key in sorted_keys:
100-
value = values.get(key)
101-
result.append(f"{key}={value}")
102-
return hashlib.md5("&".join(result).encode()).hexdigest()
103-
104-
def _get_hawk_authentication(
105-
self, rriot: RRiot, url: str, formdata: dict | None = None, params: dict | None = None
106-
) -> str:
107-
timestamp = math.floor(time.time())
108-
nonce = secrets.token_urlsafe(6)
109-
formdata_str = self._process_extra_hawk_values(formdata)
110-
params_str = self._process_extra_hawk_values(params)
111-
112-
prestr = ":".join(
113-
[
114-
rriot.u,
115-
rriot.s,
116-
nonce,
117-
str(timestamp),
118-
hashlib.md5(url.encode()).hexdigest(),
119-
params_str,
120-
formdata_str,
121-
]
122-
)
123-
mac = base64.b64encode(hmac.new(rriot.h.encode(), prestr.encode(), hashlib.sha256).digest()).decode()
124-
return f'Hawk id="{rriot.u}",s="{rriot.s}",ts="{timestamp}",nonce="{nonce}",mac="{mac}"'
125-
12693
async def nc_prepare(self, user_data: UserData, timezone: str) -> dict:
12794
"""This gets a few critical parameters for adding a device to your account."""
12895
if (
@@ -144,7 +111,7 @@ async def nc_prepare(self, user_data: UserData, timezone: str) -> dict:
144111
"post",
145112
"/nc/prepare",
146113
headers={
147-
"Authorization": self._get_hawk_authentication(
114+
"Authorization": _get_hawk_authentication(
148115
user_data.rriot, "/nc/prepare", {"hid": hid, "tzid": timezone}
149116
),
150117
},
@@ -177,7 +144,7 @@ async def add_device(self, user_data: UserData, s: str, t: str) -> dict:
177144
"GET",
178145
"/user/devices/newadd",
179146
headers={
180-
"Authorization": self._get_hawk_authentication(
147+
"Authorization": _get_hawk_authentication(
181148
user_data.rriot, "/user/devices/newadd", params={"s": s, "t": t}
182149
),
183150
},
@@ -337,7 +304,7 @@ async def get_home_data(self, user_data: UserData) -> HomeData:
337304
rriot.r.a,
338305
self.session,
339306
{
340-
"Authorization": self._get_hawk_authentication(rriot, f"/user/homes/{str(home_id)}"),
307+
"Authorization": _get_hawk_authentication(rriot, f"/user/homes/{str(home_id)}"),
341308
},
342309
)
343310
home_response = await home_request.request("get", "/user/homes/" + str(home_id))
@@ -366,7 +333,7 @@ async def get_home_data_v2(self, user_data: UserData) -> HomeData:
366333
rriot.r.a,
367334
self.session,
368335
{
369-
"Authorization": self._get_hawk_authentication(rriot, "/v2/user/homes/" + str(home_id)),
336+
"Authorization": _get_hawk_authentication(rriot, "/v2/user/homes/" + str(home_id)),
370337
},
371338
)
372339
home_response = await home_request.request("get", "/v2/user/homes/" + str(home_id))
@@ -393,7 +360,7 @@ async def get_home_data_v3(self, user_data: UserData) -> HomeData:
393360
rriot.r.a,
394361
self.session,
395362
{
396-
"Authorization": self._get_hawk_authentication(rriot, "/v3/user/homes/" + str(home_id)),
363+
"Authorization": _get_hawk_authentication(rriot, "/v3/user/homes/" + str(home_id)),
397364
},
398365
)
399366
home_response = await home_request.request("get", "/v3/user/homes/" + str(home_id))
@@ -416,7 +383,7 @@ async def get_rooms(self, user_data: UserData, home_id: int | None = None) -> li
416383
rriot.r.a,
417384
self.session,
418385
{
419-
"Authorization": self._get_hawk_authentication(rriot, "/v2/user/homes/" + str(home_id)),
386+
"Authorization": _get_hawk_authentication(rriot, "/v2/user/homes/" + str(home_id)),
420387
},
421388
)
422389
room_response = await room_request.request("get", f"/user/homes/{str(home_id)}/rooms" + str(home_id))
@@ -441,7 +408,7 @@ async def get_scenes(self, user_data: UserData, device_id: str) -> list[HomeData
441408
rriot.r.a,
442409
self.session,
443410
{
444-
"Authorization": self._get_hawk_authentication(rriot, f"/user/scene/device/{str(device_id)}"),
411+
"Authorization": _get_hawk_authentication(rriot, f"/user/scene/device/{str(device_id)}"),
445412
},
446413
)
447414
scenes_response = await scenes_request.request("get", f"/user/scene/device/{str(device_id)}")
@@ -463,7 +430,7 @@ async def execute_scene(self, user_data: UserData, scene_id: int) -> None:
463430
rriot.r.a,
464431
self.session,
465432
{
466-
"Authorization": self._get_hawk_authentication(rriot, f"/user/scene/{str(scene_id)}/execute"),
433+
"Authorization": _get_hawk_authentication(rriot, f"/user/scene/{str(scene_id)}/execute"),
467434
},
468435
)
469436
execute_scene_response = await execute_scene_request.request("POST", f"/user/scene/{str(scene_id)}/execute")
@@ -546,3 +513,36 @@ async def request(self, method: str, url: str, params=None, data=None, headers=N
546513
finally:
547514
if close_session:
548515
await session.close()
516+
517+
518+
def _process_extra_hawk_values(values: dict | None) -> str:
519+
if values is None:
520+
return ""
521+
else:
522+
sorted_keys = sorted(values.keys())
523+
result = []
524+
for key in sorted_keys:
525+
value = values.get(key)
526+
result.append(f"{key}={value}")
527+
return hashlib.md5("&".join(result).encode()).hexdigest()
528+
529+
530+
def _get_hawk_authentication(rriot: RRiot, url: str, formdata: dict | None = None, params: dict | None = None) -> str:
531+
timestamp = math.floor(time.time())
532+
nonce = secrets.token_urlsafe(6)
533+
formdata_str = _process_extra_hawk_values(formdata)
534+
params_str = _process_extra_hawk_values(params)
535+
536+
prestr = ":".join(
537+
[
538+
rriot.u,
539+
rriot.s,
540+
nonce,
541+
str(timestamp),
542+
hashlib.md5(url.encode()).hexdigest(),
543+
params_str,
544+
formdata_str,
545+
]
546+
)
547+
mac = base64.b64encode(hmac.new(rriot.h.encode(), prestr.encode(), hashlib.sha256).digest()).decode()
548+
return f'Hawk id="{rriot.u}",s="{rriot.s}",ts="{timestamp}",nonce="{nonce}",mac="{mac}"'

tests/mqtt_packet.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def gen_publish(
111111
properties = prop_finalise(properties)
112112
rl += len(properties)
113113
# This will break if len(properties) > 127
114-
pack_format = pack_format + "%ds" % (len(properties))
114+
pack_format = f"{pack_format}{len(properties)}s"
115115

116116
if payload is not None:
117117
# payload = payload.encode("utf-8")

tests/test_util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def test_start_date_lower_than_now_lower_than_end_date():
2121

2222

2323
# start_date > now > end_date
24-
def test_start_date_greater_than_now_greater_tat_end_date():
24+
def test_start_date_greater_than_now_greater_than_end_date():
2525
start, end = parse_time_to_datetime(
2626
(datetime.datetime.now() + datetime.timedelta(hours=1)).time(),
2727
(datetime.datetime.now() + datetime.timedelta(hours=2)).time(),

0 commit comments

Comments
 (0)