python-roborock 3.18.0__tar.gz → 3.19.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_roborock-3.18.0 → python_roborock-3.19.0}/PKG-INFO +1 -1
- {python_roborock-3.18.0 → python_roborock-3.19.0}/pyproject.toml +1 -1
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/v1/v1_containers.py +1 -1
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/device_manager.py +15 -2
- python_roborock-3.19.0/roborock/diagnostics.py +84 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/mqtt/roborock_session.py +37 -16
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/mqtt/session.py +10 -1
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/protocols/v1_protocol.py +2 -4
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/roborock_message.py +2 -4
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/util.py +10 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/.gitignore +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/LICENSE +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/README.md +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/api.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/broadcast_protocol.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/callbacks.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/cli.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/cloud_api.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/command_cache.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/const.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/b01_q10/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/b01_q7/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/code_mappings.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/containers.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/dyad/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/dyad/dyad_code_mappings.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/dyad/dyad_containers.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/v1/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/v1/v1_clean_modes.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/v1/v1_code_mappings.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/zeo/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/zeo/zeo_code_mappings.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/zeo/zeo_containers.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/device_features.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/README.md +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/a01_channel.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/b01_channel.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/cache.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/channel.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/device.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/file_cache.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/local_channel.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/mqtt_channel.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/a01/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/b01/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/b01/q10/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/b01/q7/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/traits_mixin.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/child_lock.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/command.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/common.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/consumeable.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/device_features.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/flow_led_status.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/home.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/led_status.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/map_content.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/maps.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/network_info.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/rooms.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/routines.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/status.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/volume.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/v1_channel.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/exceptions.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/map/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/map/map_parser.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/mqtt/health_manager.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/protocol.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/protocols/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/protocols/a01_protocol.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/protocols/b01_protocol.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/py.typed +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/roborock_future.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/version_1_apis/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/version_a01_apis/__init__.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
- {python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/web_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-roborock
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.19.0
|
|
4
4
|
Summary: A package to control Roborock vacuums.
|
|
5
5
|
Project-URL: Repository, https://github.com/humbertogontijo/python-roborock
|
|
6
6
|
Project-URL: Documentation, https://python-roborock.readthedocs.io/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-roborock"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.19.0"
|
|
4
4
|
description = "A package to control Roborock vacuums."
|
|
5
5
|
authors = [{ name = "humbertogontijo", email = "humbertogontijo@users.noreply.github.com" }, {name="Lash-L"}, {name="allenporter"}]
|
|
6
6
|
requires-python = ">=3.11, <4"
|
|
@@ -585,7 +585,7 @@ class AppInitStatus(RoborockBase):
|
|
|
585
585
|
local_info: AppInitStatusLocalInfo
|
|
586
586
|
feature_info: list[int]
|
|
587
587
|
new_feature_info: int
|
|
588
|
-
new_feature_info_str: str
|
|
588
|
+
new_feature_info_str: str = ""
|
|
589
589
|
new_feature_info_2: int | None = None
|
|
590
590
|
carriage_type: int | None = None
|
|
591
591
|
dsp_version: str | None = None
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import enum
|
|
5
5
|
import logging
|
|
6
|
-
from collections.abc import Callable
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
8
9
|
|
|
9
10
|
import aiohttp
|
|
10
11
|
|
|
@@ -15,6 +16,7 @@ from roborock.data import (
|
|
|
15
16
|
UserData,
|
|
16
17
|
)
|
|
17
18
|
from roborock.devices.device import DeviceReadyCallback, RoborockDevice
|
|
19
|
+
from roborock.diagnostics import Diagnostics
|
|
18
20
|
from roborock.exceptions import RoborockException
|
|
19
21
|
from roborock.map.map_parser import MapParserConfig
|
|
20
22
|
from roborock.mqtt.roborock_session import create_lazy_mqtt_session
|
|
@@ -58,6 +60,7 @@ class DeviceManager:
|
|
|
58
60
|
device_creator: DeviceCreator,
|
|
59
61
|
mqtt_session: MqttSession,
|
|
60
62
|
cache: Cache,
|
|
63
|
+
diagnostics: Diagnostics,
|
|
61
64
|
) -> None:
|
|
62
65
|
"""Initialize the DeviceManager with user data and optional cache storage.
|
|
63
66
|
|
|
@@ -68,12 +71,15 @@ class DeviceManager:
|
|
|
68
71
|
self._device_creator = device_creator
|
|
69
72
|
self._devices: dict[str, RoborockDevice] = {}
|
|
70
73
|
self._mqtt_session = mqtt_session
|
|
74
|
+
self._diagnostics = diagnostics
|
|
71
75
|
|
|
72
76
|
async def discover_devices(self, prefer_cache: bool = True) -> list[RoborockDevice]:
|
|
73
77
|
"""Discover all devices for the logged-in user."""
|
|
78
|
+
self._diagnostics.increment("discover_devices")
|
|
74
79
|
cache_data = await self._cache.get()
|
|
75
80
|
if not cache_data.home_data or not prefer_cache:
|
|
76
81
|
_LOGGER.debug("Fetching home data (prefer_cache=%s)", prefer_cache)
|
|
82
|
+
self._diagnostics.increment("fetch_home_data")
|
|
77
83
|
try:
|
|
78
84
|
cache_data.home_data = await self._web_api.get_home_data()
|
|
79
85
|
except RoborockException as ex:
|
|
@@ -116,6 +122,10 @@ class DeviceManager:
|
|
|
116
122
|
tasks.append(self._mqtt_session.close())
|
|
117
123
|
await asyncio.gather(*tasks)
|
|
118
124
|
|
|
125
|
+
def diagnostic_data(self) -> Mapping[str, Any]:
|
|
126
|
+
"""Return diagnostics information about the device manager."""
|
|
127
|
+
return self._diagnostics.as_dict()
|
|
128
|
+
|
|
119
129
|
|
|
120
130
|
@dataclass
|
|
121
131
|
class UserParams:
|
|
@@ -182,7 +192,10 @@ async def create_device_manager(
|
|
|
182
192
|
web_api = create_web_api_wrapper(user_params, session=session, cache=cache)
|
|
183
193
|
user_data = user_params.user_data
|
|
184
194
|
|
|
195
|
+
diagnostics = Diagnostics()
|
|
196
|
+
|
|
185
197
|
mqtt_params = create_mqtt_params(user_data.rriot)
|
|
198
|
+
mqtt_params.diagnostics = diagnostics.subkey("mqtt_session")
|
|
186
199
|
mqtt_session = await create_lazy_mqtt_session(mqtt_params)
|
|
187
200
|
|
|
188
201
|
def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDataProduct) -> RoborockDevice:
|
|
@@ -226,6 +239,6 @@ async def create_device_manager(
|
|
|
226
239
|
dev.add_ready_callback(ready_callback)
|
|
227
240
|
return dev
|
|
228
241
|
|
|
229
|
-
manager = DeviceManager(web_api, device_creator, mqtt_session=mqtt_session, cache=cache)
|
|
242
|
+
manager = DeviceManager(web_api, device_creator, mqtt_session=mqtt_session, cache=cache, diagnostics=diagnostics)
|
|
230
243
|
await manager.discover_devices()
|
|
231
244
|
return manager
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Diagnostics for debugging.
|
|
2
|
+
|
|
3
|
+
A Diagnostics object can be used to track counts and latencies of various
|
|
4
|
+
operations within a module. This can be useful for debugging performance issues
|
|
5
|
+
or understanding usage patterns.
|
|
6
|
+
|
|
7
|
+
This is an internal facing module and is not intended for public use. Diagnostics
|
|
8
|
+
data is collected and exposed to clients via higher level APIs like the
|
|
9
|
+
DeviceManager.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from collections import Counter
|
|
16
|
+
from collections.abc import Generator, Mapping
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Diagnostics:
|
|
22
|
+
"""A class that holds diagnostics information for a module.
|
|
23
|
+
|
|
24
|
+
You can use this class to hold counter or for recording timing information
|
|
25
|
+
that can be exported as a dictionary for debugging purposes.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
"""Initialize Diagnostics."""
|
|
30
|
+
self._counter: Counter = Counter()
|
|
31
|
+
self._subkeys: dict[str, Diagnostics] = {}
|
|
32
|
+
|
|
33
|
+
def increment(self, key: str, count: int = 1) -> None:
|
|
34
|
+
"""Increment a counter for the specified key/event."""
|
|
35
|
+
self._counter.update(Counter({key: count}))
|
|
36
|
+
|
|
37
|
+
def elapsed(self, key_prefix: str, elapsed_ms: int = 1) -> None:
|
|
38
|
+
"""Track a latency event for the specified key/event prefix."""
|
|
39
|
+
self.increment(f"{key_prefix}_count", 1)
|
|
40
|
+
self.increment(f"{key_prefix}_sum", elapsed_ms)
|
|
41
|
+
|
|
42
|
+
def as_dict(self) -> Mapping[str, Any]:
|
|
43
|
+
"""Return diagnostics as a debug dictionary."""
|
|
44
|
+
data: dict[str, Any] = {k: self._counter[k] for k in self._counter}
|
|
45
|
+
for k, d in self._subkeys.items():
|
|
46
|
+
v = d.as_dict()
|
|
47
|
+
if not v:
|
|
48
|
+
continue
|
|
49
|
+
data[k] = v
|
|
50
|
+
return data
|
|
51
|
+
|
|
52
|
+
def subkey(self, key: str) -> Diagnostics:
|
|
53
|
+
"""Return sub-Diagnostics object with the specified subkey.
|
|
54
|
+
|
|
55
|
+
This will create a new Diagnostics object if one does not already exist
|
|
56
|
+
for the specified subkey. Stats from the sub-Diagnostics will be included
|
|
57
|
+
in the parent Diagnostics when exported as a dictionary.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
key: The subkey for the diagnostics.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The Diagnostics object for the specified subkey.
|
|
64
|
+
"""
|
|
65
|
+
if key not in self._subkeys:
|
|
66
|
+
self._subkeys[key] = Diagnostics()
|
|
67
|
+
return self._subkeys[key]
|
|
68
|
+
|
|
69
|
+
@contextmanager
|
|
70
|
+
def timer(self, key_prefix: str) -> Generator[None, None, None]:
|
|
71
|
+
"""A context manager that records the timing of operations as a diagnostic."""
|
|
72
|
+
start = time.perf_counter()
|
|
73
|
+
try:
|
|
74
|
+
yield
|
|
75
|
+
finally:
|
|
76
|
+
end = time.perf_counter()
|
|
77
|
+
ms = int((end - start) * 1000)
|
|
78
|
+
self.elapsed(key_prefix, ms)
|
|
79
|
+
|
|
80
|
+
def reset(self) -> None:
|
|
81
|
+
"""Clear all diagnostics, for testing."""
|
|
82
|
+
self._counter = Counter()
|
|
83
|
+
for d in self._subkeys.values():
|
|
84
|
+
d.reset()
|
|
@@ -18,6 +18,7 @@ import aiomqtt
|
|
|
18
18
|
from aiomqtt import MqttCodeError, MqttError, TLSParameters
|
|
19
19
|
|
|
20
20
|
from roborock.callbacks import CallbackMap
|
|
21
|
+
from roborock.diagnostics import Diagnostics
|
|
21
22
|
|
|
22
23
|
from .health_manager import HealthManager
|
|
23
24
|
from .session import MqttParams, MqttSession, MqttSessionException, MqttSessionUnauthorized
|
|
@@ -76,6 +77,7 @@ class RoborockMqttSession(MqttSession):
|
|
|
76
77
|
self._connection_task: asyncio.Task[None] | None = None
|
|
77
78
|
self._topic_idle_timeout = topic_idle_timeout
|
|
78
79
|
self._idle_timers: dict[str, asyncio.Task[None]] = {}
|
|
80
|
+
self._diagnostics = params.diagnostics
|
|
79
81
|
self._health_manager = HealthManager(self.restart)
|
|
80
82
|
|
|
81
83
|
@property
|
|
@@ -96,24 +98,30 @@ class RoborockMqttSession(MqttSession):
|
|
|
96
98
|
handle the failure and retry if desired itself. Once connected,
|
|
97
99
|
the session will retry connecting in the background.
|
|
98
100
|
"""
|
|
101
|
+
self._diagnostics.increment("start_attempt")
|
|
99
102
|
start_future: asyncio.Future[None] = asyncio.Future()
|
|
100
103
|
loop = asyncio.get_event_loop()
|
|
101
104
|
self._reconnect_task = loop.create_task(self._run_reconnect_loop(start_future))
|
|
102
105
|
try:
|
|
103
106
|
await start_future
|
|
104
107
|
except MqttCodeError as err:
|
|
108
|
+
self._diagnostics.increment(f"start_failure:{err.rc}")
|
|
105
109
|
if err.rc == MqttReasonCode.RC_ERROR_UNAUTHORIZED:
|
|
106
110
|
raise MqttSessionUnauthorized(f"Authorization error starting MQTT session: {err}") from err
|
|
107
111
|
raise MqttSessionException(f"Error starting MQTT session: {err}") from err
|
|
108
112
|
except MqttError as err:
|
|
113
|
+
self._diagnostics.increment("start_failure:unknown")
|
|
109
114
|
raise MqttSessionException(f"Error starting MQTT session: {err}") from err
|
|
110
115
|
except Exception as err:
|
|
116
|
+
self._diagnostics.increment("start_failure:uncaught")
|
|
111
117
|
raise MqttSessionException(f"Unexpected error starting session: {err}") from err
|
|
112
118
|
else:
|
|
119
|
+
self._diagnostics.increment("start_success")
|
|
113
120
|
_LOGGER.debug("MQTT session started successfully")
|
|
114
121
|
|
|
115
122
|
async def close(self) -> None:
|
|
116
123
|
"""Cancels the MQTT loop and shutdown the client library."""
|
|
124
|
+
self._diagnostics.increment("close")
|
|
117
125
|
self._stop = True
|
|
118
126
|
tasks = [task for task in [self._connection_task, self._reconnect_task, *self._idle_timers.values()] if task]
|
|
119
127
|
self._connection_task = None
|
|
@@ -136,6 +144,7 @@ class RoborockMqttSession(MqttSession):
|
|
|
136
144
|
the reconnect loop. This is a no-op if there is no active connection.
|
|
137
145
|
"""
|
|
138
146
|
_LOGGER.info("Forcing MQTT session restart")
|
|
147
|
+
self._diagnostics.increment("restart")
|
|
139
148
|
if self._connection_task:
|
|
140
149
|
self._connection_task.cancel()
|
|
141
150
|
else:
|
|
@@ -144,6 +153,7 @@ class RoborockMqttSession(MqttSession):
|
|
|
144
153
|
async def _run_reconnect_loop(self, start_future: asyncio.Future[None] | None) -> None:
|
|
145
154
|
"""Run the MQTT loop."""
|
|
146
155
|
_LOGGER.info("Starting MQTT session")
|
|
156
|
+
self._diagnostics.increment("start_loop")
|
|
147
157
|
while True:
|
|
148
158
|
try:
|
|
149
159
|
self._connection_task = asyncio.create_task(self._run_connection(start_future))
|
|
@@ -164,6 +174,7 @@ class RoborockMqttSession(MqttSession):
|
|
|
164
174
|
_LOGGER.debug("MQTT session closed, stopping retry loop")
|
|
165
175
|
return
|
|
166
176
|
_LOGGER.info("MQTT session disconnected, retrying in %s seconds", self._backoff.total_seconds())
|
|
177
|
+
self._diagnostics.increment("reconnect_wait")
|
|
167
178
|
await asyncio.sleep(self._backoff.total_seconds())
|
|
168
179
|
self._backoff = min(self._backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_INTERVAL)
|
|
169
180
|
|
|
@@ -175,17 +186,19 @@ class RoborockMqttSession(MqttSession):
|
|
|
175
186
|
is lost, this method will exit.
|
|
176
187
|
"""
|
|
177
188
|
try:
|
|
178
|
-
|
|
179
|
-
self.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
start_future.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
with self._diagnostics.timer("connection"):
|
|
190
|
+
async with self._mqtt_client(self._params) as client:
|
|
191
|
+
self._backoff = MIN_BACKOFF_INTERVAL
|
|
192
|
+
self._healthy = True
|
|
193
|
+
_LOGGER.info("MQTT Session connected.")
|
|
194
|
+
if start_future and not start_future.done():
|
|
195
|
+
start_future.set_result(None)
|
|
196
|
+
|
|
197
|
+
_LOGGER.debug("Processing MQTT messages")
|
|
198
|
+
async for message in client.messages:
|
|
199
|
+
_LOGGER.debug("Received message: %s", message)
|
|
200
|
+
with self._diagnostics.timer("dispatch_message"):
|
|
201
|
+
self._listeners(message.topic.value, message.payload)
|
|
189
202
|
except MqttError as err:
|
|
190
203
|
if start_future and not start_future.done():
|
|
191
204
|
_LOGGER.info("MQTT error starting session: %s", err)
|
|
@@ -227,6 +240,7 @@ class RoborockMqttSession(MqttSession):
|
|
|
227
240
|
async with self._client_lock:
|
|
228
241
|
self._client = client
|
|
229
242
|
for topic in self._client_subscribed_topics:
|
|
243
|
+
self._diagnostics.increment("resubscribe")
|
|
230
244
|
_LOGGER.debug("Re-establishing subscription to topic %s", topic)
|
|
231
245
|
# TODO: If this fails it will break the whole connection. Make
|
|
232
246
|
# this retry again in the background with backoff.
|
|
@@ -251,6 +265,7 @@ class RoborockMqttSession(MqttSession):
|
|
|
251
265
|
|
|
252
266
|
# If there is an idle timer for this topic, cancel it (reuse subscription)
|
|
253
267
|
if idle_timer := self._idle_timers.pop(topic, None):
|
|
268
|
+
self._diagnostics.increment("unsubscribe_idle_cancel")
|
|
254
269
|
idle_timer.cancel()
|
|
255
270
|
_LOGGER.debug("Cancelled idle timer for topic %s (reused subscription)", topic)
|
|
256
271
|
|
|
@@ -262,13 +277,15 @@ class RoborockMqttSession(MqttSession):
|
|
|
262
277
|
if self._client:
|
|
263
278
|
_LOGGER.debug("Establishing subscription to topic %s", topic)
|
|
264
279
|
try:
|
|
265
|
-
|
|
280
|
+
with self._diagnostics.timer("subscribe"):
|
|
281
|
+
await self._client.subscribe(topic)
|
|
266
282
|
except MqttError as err:
|
|
267
283
|
# Clean up the callback if subscription fails
|
|
268
284
|
unsub()
|
|
269
285
|
self._client_subscribed_topics.discard(topic)
|
|
270
286
|
raise MqttSessionException(f"Error subscribing to topic: {err}") from err
|
|
271
287
|
else:
|
|
288
|
+
self._diagnostics.increment("subscribe_pending")
|
|
272
289
|
_LOGGER.debug("Client not connected, will establish subscription later")
|
|
273
290
|
|
|
274
291
|
def schedule_unsubscribe() -> None:
|
|
@@ -301,10 +318,11 @@ class RoborockMqttSession(MqttSession):
|
|
|
301
318
|
self._idle_timers[topic] = task
|
|
302
319
|
|
|
303
320
|
def delayed_unsub():
|
|
321
|
+
self._diagnostics.increment("unsubscribe")
|
|
304
322
|
unsub() # Remove the callback from CallbackMap
|
|
305
323
|
# If no more callbacks for this topic, start idle timer
|
|
306
324
|
if not self._listeners.get_callbacks(topic):
|
|
307
|
-
|
|
325
|
+
self._diagnostics.increment("unsubscribe_idle_start")
|
|
308
326
|
schedule_unsubscribe()
|
|
309
327
|
else:
|
|
310
328
|
_LOGGER.debug("Unsubscribing topic %s, still have active callbacks", topic)
|
|
@@ -320,7 +338,8 @@ class RoborockMqttSession(MqttSession):
|
|
|
320
338
|
raise MqttSessionException("Could not publish message, MQTT client not connected")
|
|
321
339
|
client = self._client
|
|
322
340
|
try:
|
|
323
|
-
|
|
341
|
+
with self._diagnostics.timer("publish"):
|
|
342
|
+
await client.publish(topic, message)
|
|
324
343
|
except MqttError as err:
|
|
325
344
|
raise MqttSessionException(f"Error publishing message: {err}") from err
|
|
326
345
|
|
|
@@ -333,11 +352,12 @@ class LazyMqttSession(MqttSession):
|
|
|
333
352
|
is made.
|
|
334
353
|
"""
|
|
335
354
|
|
|
336
|
-
def __init__(self, session: RoborockMqttSession) -> None:
|
|
355
|
+
def __init__(self, session: RoborockMqttSession, diagnostics: Diagnostics) -> None:
|
|
337
356
|
"""Initialize the lazy session with an existing session."""
|
|
338
357
|
self._lock = asyncio.Lock()
|
|
339
358
|
self._started = False
|
|
340
359
|
self._session = session
|
|
360
|
+
self._diagnostics = diagnostics
|
|
341
361
|
|
|
342
362
|
@property
|
|
343
363
|
def connected(self) -> bool:
|
|
@@ -353,6 +373,7 @@ class LazyMqttSession(MqttSession):
|
|
|
353
373
|
"""Start the MQTT session if not already started."""
|
|
354
374
|
async with self._lock:
|
|
355
375
|
if not self._started:
|
|
376
|
+
self._diagnostics.increment("start")
|
|
356
377
|
await self._session.start()
|
|
357
378
|
self._started = True
|
|
358
379
|
|
|
@@ -403,4 +424,4 @@ async def create_lazy_mqtt_session(params: MqttParams) -> MqttSession:
|
|
|
403
424
|
This function is a factory for creating an MQTT session that will
|
|
404
425
|
only connect when the first attempt to subscribe or publish is made.
|
|
405
426
|
"""
|
|
406
|
-
return LazyMqttSession(RoborockMqttSession(params))
|
|
427
|
+
return LazyMqttSession(RoborockMqttSession(params), diagnostics=params.diagnostics.subkey("lazy_mqtt"))
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from collections.abc import Callable
|
|
5
|
-
from dataclasses import dataclass
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
6
|
|
|
7
|
+
from roborock.diagnostics import Diagnostics
|
|
7
8
|
from roborock.exceptions import RoborockException
|
|
8
9
|
from roborock.mqtt.health_manager import HealthManager
|
|
9
10
|
|
|
@@ -32,6 +33,14 @@ class MqttParams:
|
|
|
32
33
|
timeout: float = DEFAULT_TIMEOUT
|
|
33
34
|
"""Timeout for communications with the broker in seconds."""
|
|
34
35
|
|
|
36
|
+
diagnostics: Diagnostics = field(default_factory=Diagnostics)
|
|
37
|
+
"""Diagnostics object for tracking MQTT session stats.
|
|
38
|
+
|
|
39
|
+
This defaults to a new Diagnostics object, but the common case is the
|
|
40
|
+
caller will provide their own (e.g., from a DeviceManager) so that the
|
|
41
|
+
shared MQTT session diagnostics are included in the overall diagnostics.
|
|
42
|
+
"""
|
|
43
|
+
|
|
35
44
|
|
|
36
45
|
class MqttSession(ABC):
|
|
37
46
|
"""An MQTT session for sending and receiving messages."""
|
|
@@ -5,10 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
import base64
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
|
-
import math
|
|
9
8
|
import secrets
|
|
10
9
|
import struct
|
|
11
|
-
import time
|
|
12
10
|
from collections.abc import Callable
|
|
13
11
|
from dataclasses import dataclass, field
|
|
14
12
|
from enum import StrEnum
|
|
@@ -19,7 +17,7 @@ from roborock.exceptions import RoborockException, RoborockUnsupportedFeature
|
|
|
19
17
|
from roborock.protocol import Utils
|
|
20
18
|
from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol
|
|
21
19
|
from roborock.roborock_typing import RoborockCommand
|
|
22
|
-
from roborock.util import get_next_int
|
|
20
|
+
from roborock.util import get_next_int, get_timestamp
|
|
23
21
|
|
|
24
22
|
_LOGGER = logging.getLogger(__name__)
|
|
25
23
|
|
|
@@ -70,7 +68,7 @@ class RequestMessage:
|
|
|
70
68
|
|
|
71
69
|
method: RoborockCommand | str
|
|
72
70
|
params: ParamsType
|
|
73
|
-
timestamp: int = field(default_factory=lambda:
|
|
71
|
+
timestamp: int = field(default_factory=lambda: get_timestamp())
|
|
74
72
|
request_id: int = field(default_factory=lambda: get_next_int(10000, 32767))
|
|
75
73
|
|
|
76
74
|
def encode_message(
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import math
|
|
4
|
-
import time
|
|
5
3
|
from dataclasses import dataclass, field
|
|
6
4
|
from enum import StrEnum
|
|
7
5
|
|
|
8
6
|
from roborock import RoborockEnum
|
|
9
|
-
from roborock.util import get_next_int
|
|
7
|
+
from roborock.util import get_next_int, get_timestamp
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
class RoborockMessageProtocol(RoborockEnum):
|
|
@@ -245,4 +243,4 @@ class RoborockMessage:
|
|
|
245
243
|
seq: int = field(default_factory=lambda: get_next_int(100000, 999999))
|
|
246
244
|
version: bytes = b"1.0"
|
|
247
245
|
random: int = field(default_factory=lambda: get_next_int(10000, 99999))
|
|
248
|
-
timestamp: int = field(default_factory=lambda:
|
|
246
|
+
timestamp: int = field(default_factory=lambda: get_timestamp())
|
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import datetime
|
|
5
5
|
import logging
|
|
6
|
+
import math
|
|
7
|
+
import time
|
|
6
8
|
from asyncio import TimerHandle
|
|
7
9
|
from collections.abc import Callable, Coroutine, MutableMapping
|
|
8
10
|
from typing import Any, TypeVar
|
|
@@ -97,3 +99,11 @@ def get_next_int(min_val: int, max_val: int) -> int:
|
|
|
97
99
|
counter_map[(min_val, max_val)] = min_val
|
|
98
100
|
counter_map[(min_val, max_val)] += 1
|
|
99
101
|
return counter_map[(min_val, max_val)] % max_val + min_val
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_timestamp() -> int:
|
|
105
|
+
"""Get the current timestamp in seconds since epoch.
|
|
106
|
+
|
|
107
|
+
This is separated out to allow for easier mocking in tests.
|
|
108
|
+
"""
|
|
109
|
+
return math.floor(time.time())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/b01_q10/b01_q10_code_mappings.py
RENAMED
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/b01_q10/b01_q10_containers.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/data/b01_q7/b01_q7_code_mappings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/b01/q10/__init__.py
RENAMED
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/b01/q7/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/clean_summary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/device_features.py
RENAMED
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/do_not_disturb.py
RENAMED
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/dust_collection_mode.py
RENAMED
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/flow_led_status.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/network_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/smart_wash_params.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/devices/traits/v1/wash_towel_mode.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/version_1_apis/roborock_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-3.18.0 → python_roborock-3.19.0}/roborock/version_a01_apis/roborock_client_a01.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|