python-roborock 2.28.0__tar.gz → 2.29.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-2.28.0 → python_roborock-2.29.0}/PKG-INFO +1 -1
- {python_roborock-2.28.0 → python_roborock-2.29.0}/pyproject.toml +1 -1
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/cli.py +4 -1
- python_roborock-2.29.0/roborock/devices/a01_channel.py +43 -0
- python_roborock-2.29.0/roborock/devices/channel.py +27 -0
- python_roborock-2.29.0/roborock/devices/device.py +88 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/devices/device_manager.py +36 -10
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/devices/local_channel.py +3 -1
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/devices/mqtt_channel.py +25 -3
- python_roborock-2.29.0/roborock/devices/traits/dyad.py +36 -0
- python_roborock-2.29.0/roborock/devices/traits/status.py +43 -0
- python_roborock-2.29.0/roborock/devices/traits/trait.py +10 -0
- python_roborock-2.29.0/roborock/devices/traits/zeo.py +36 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/devices/v1_channel.py +7 -1
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/protocols/a01_protocol.py +5 -1
- python_roborock-2.28.0/roborock/devices/device.py +0 -120
- {python_roborock-2.28.0 → python_roborock-2.29.0}/LICENSE +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/README.md +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/__init__.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/api.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/cloud_api.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/code_mappings.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/command_cache.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/const.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/containers.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/device_features.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/devices/README.md +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/devices/__init__.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/devices/v1_rpc_channel.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/exceptions.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/local_api.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/mqtt/__init__.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/mqtt/roborock_session.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/mqtt/session.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/protocol.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/protocols/v1_protocol.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/py.typed +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/roborock_future.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/roborock_message.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/roborock_typing.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/util.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/version_1_apis/__init__.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/version_a01_apis/__init__.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
- {python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/web_api.py +0 -0
|
@@ -117,8 +117,11 @@ async def session(ctx, duration: int):
|
|
|
117
117
|
|
|
118
118
|
click.echo("MQTT session started. Querying devices...")
|
|
119
119
|
for device in devices:
|
|
120
|
+
if not (status_trait := device.traits.get("status")):
|
|
121
|
+
click.echo(f"Device {device.name} does not have a status trait")
|
|
122
|
+
continue
|
|
120
123
|
try:
|
|
121
|
-
status = await
|
|
124
|
+
status = await status_trait.get_status()
|
|
122
125
|
except RoborockException as e:
|
|
123
126
|
click.echo(f"Failed to get status for {device.name}: {e}")
|
|
124
127
|
else:
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Thin wrapper around the MQTT channel for Roborock A01 devices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, overload
|
|
7
|
+
|
|
8
|
+
from roborock.protocols.a01_protocol import (
|
|
9
|
+
decode_rpc_response,
|
|
10
|
+
encode_mqtt_payload,
|
|
11
|
+
)
|
|
12
|
+
from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol
|
|
13
|
+
|
|
14
|
+
from .mqtt_channel import MqttChannel
|
|
15
|
+
|
|
16
|
+
_LOGGER = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@overload
|
|
20
|
+
async def send_decoded_command(
|
|
21
|
+
mqtt_channel: MqttChannel,
|
|
22
|
+
params: dict[RoborockDyadDataProtocol, Any],
|
|
23
|
+
) -> dict[RoborockDyadDataProtocol, Any]:
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@overload
|
|
28
|
+
async def send_decoded_command(
|
|
29
|
+
mqtt_channel: MqttChannel,
|
|
30
|
+
params: dict[RoborockZeoProtocol, Any],
|
|
31
|
+
) -> dict[RoborockZeoProtocol, Any]:
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def send_decoded_command(
|
|
36
|
+
mqtt_channel: MqttChannel,
|
|
37
|
+
params: dict[RoborockDyadDataProtocol, Any] | dict[RoborockZeoProtocol, Any],
|
|
38
|
+
) -> dict[RoborockDyadDataProtocol, Any] | dict[RoborockZeoProtocol, Any]:
|
|
39
|
+
"""Send a command on the MQTT channel and get a decoded response."""
|
|
40
|
+
_LOGGER.debug("Sending MQTT command: %s", params)
|
|
41
|
+
roborock_message = encode_mqtt_payload(params)
|
|
42
|
+
response = await mqtt_channel.send_message(roborock_message)
|
|
43
|
+
return decode_rpc_response(response) # type: ignore[return-value]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Low-level interface for connections to Roborock devices."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
from roborock.roborock_message import RoborockMessage
|
|
8
|
+
|
|
9
|
+
_LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Channel(Protocol):
|
|
13
|
+
"""A generic channel for establishing a connection with a Roborock device.
|
|
14
|
+
|
|
15
|
+
Individual channel implementations have their own methods for speaking to
|
|
16
|
+
the device that hide some of the protocol specific complexity, but they
|
|
17
|
+
are still specialized for the device type and protocol.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def is_connected(self) -> bool:
|
|
22
|
+
"""Return true if the channel is connected."""
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
async def subscribe(self, callback: Callable[[RoborockMessage], None]) -> Callable[[], None]:
|
|
26
|
+
"""Subscribe to messages from the device."""
|
|
27
|
+
...
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Module for Roborock devices.
|
|
2
|
+
|
|
3
|
+
This interface is experimental and subject to breaking changes without notice
|
|
4
|
+
until the API is stable.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from abc import ABC
|
|
9
|
+
from collections.abc import Callable, Mapping
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
|
|
12
|
+
from roborock.containers import HomeDataDevice
|
|
13
|
+
from roborock.roborock_message import RoborockMessage
|
|
14
|
+
|
|
15
|
+
from .channel import Channel
|
|
16
|
+
from .traits.trait import Trait
|
|
17
|
+
|
|
18
|
+
_LOGGER = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"RoborockDevice",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RoborockDevice(ABC):
|
|
26
|
+
"""A generic channel for establishing a connection with a Roborock device.
|
|
27
|
+
|
|
28
|
+
Individual channel implementations have their own methods for speaking to
|
|
29
|
+
the device that hide some of the protocol specific complexity, but they
|
|
30
|
+
are still specialized for the device type and protocol.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
device_info: HomeDataDevice,
|
|
36
|
+
channel: Channel,
|
|
37
|
+
traits: list[Trait],
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Initialize the RoborockDevice.
|
|
40
|
+
|
|
41
|
+
The device takes ownership of the channel for communication with the device.
|
|
42
|
+
Use `connect()` to establish the connection, which will set up the appropriate
|
|
43
|
+
protocol channel. Use `close()` to clean up all connections.
|
|
44
|
+
"""
|
|
45
|
+
self._duid = device_info.duid
|
|
46
|
+
self._name = device_info.name
|
|
47
|
+
self._channel = channel
|
|
48
|
+
self._unsub: Callable[[], None] | None = None
|
|
49
|
+
self._trait_map = {trait.name: trait for trait in traits}
|
|
50
|
+
if len(self._trait_map) != len(traits):
|
|
51
|
+
raise ValueError("Duplicate trait names found in traits list")
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def duid(self) -> str:
|
|
55
|
+
"""Return the device unique identifier (DUID)."""
|
|
56
|
+
return self._duid
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def name(self) -> str:
|
|
60
|
+
"""Return the device name."""
|
|
61
|
+
return self._name
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def is_connected(self) -> bool:
|
|
65
|
+
"""Return whether the device is connected."""
|
|
66
|
+
return self._channel.is_connected
|
|
67
|
+
|
|
68
|
+
async def connect(self) -> None:
|
|
69
|
+
"""Connect to the device using the appropriate protocol channel."""
|
|
70
|
+
if self._unsub:
|
|
71
|
+
raise ValueError("Already connected to the device")
|
|
72
|
+
self._unsub = await self._channel.subscribe(self._on_message)
|
|
73
|
+
_LOGGER.info("Connected to V1 device %s", self.name)
|
|
74
|
+
|
|
75
|
+
async def close(self) -> None:
|
|
76
|
+
"""Close all connections to the device."""
|
|
77
|
+
if self._unsub:
|
|
78
|
+
self._unsub()
|
|
79
|
+
self._unsub = None
|
|
80
|
+
|
|
81
|
+
def _on_message(self, message: RoborockMessage) -> None:
|
|
82
|
+
"""Handle incoming messages from the device."""
|
|
83
|
+
_LOGGER.debug("Received message from device: %s", message)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def traits(self) -> Mapping[str, Trait]:
|
|
87
|
+
"""Return the traits of the device."""
|
|
88
|
+
return MappingProxyType(self._trait_map)
|
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
"""Module for discovering Roborock devices."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import enum
|
|
4
5
|
import logging
|
|
5
6
|
from collections.abc import Awaitable, Callable
|
|
6
7
|
|
|
8
|
+
from roborock.code_mappings import RoborockCategory
|
|
7
9
|
from roborock.containers import (
|
|
8
10
|
HomeData,
|
|
9
11
|
HomeDataDevice,
|
|
10
12
|
HomeDataProduct,
|
|
11
13
|
UserData,
|
|
12
14
|
)
|
|
13
|
-
from roborock.devices.device import
|
|
15
|
+
from roborock.devices.device import RoborockDevice
|
|
14
16
|
from roborock.mqtt.roborock_session import create_mqtt_session
|
|
15
17
|
from roborock.mqtt.session import MqttSession
|
|
16
18
|
from roborock.protocol import create_mqtt_params
|
|
17
19
|
from roborock.web_api import RoborockApiClient
|
|
18
20
|
|
|
21
|
+
from .channel import Channel
|
|
22
|
+
from .mqtt_channel import create_mqtt_channel
|
|
23
|
+
from .traits.dyad import DyadApi
|
|
24
|
+
from .traits.status import StatusTrait
|
|
25
|
+
from .traits.trait import Trait
|
|
26
|
+
from .traits.zeo import ZeoApi
|
|
19
27
|
from .v1_channel import create_v1_channel
|
|
20
28
|
|
|
21
29
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -33,6 +41,14 @@ HomeDataApi = Callable[[], Awaitable[HomeData]]
|
|
|
33
41
|
DeviceCreator = Callable[[HomeDataDevice, HomeDataProduct], RoborockDevice]
|
|
34
42
|
|
|
35
43
|
|
|
44
|
+
class DeviceVersion(enum.StrEnum):
|
|
45
|
+
"""Enum for device versions."""
|
|
46
|
+
|
|
47
|
+
V1 = "1.0"
|
|
48
|
+
A01 = "A01"
|
|
49
|
+
UNKNOWN = "unknown"
|
|
50
|
+
|
|
51
|
+
|
|
36
52
|
class DeviceManager:
|
|
37
53
|
"""Central manager for Roborock device discovery and connections."""
|
|
38
54
|
|
|
@@ -114,15 +130,25 @@ async def create_device_manager(user_data: UserData, home_data_api: HomeDataApi)
|
|
|
114
130
|
mqtt_session = await create_mqtt_session(mqtt_params)
|
|
115
131
|
|
|
116
132
|
def device_creator(device: HomeDataDevice, product: HomeDataProduct) -> RoborockDevice:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
channel: Channel
|
|
134
|
+
traits: list[Trait] = []
|
|
135
|
+
# TODO: Define a registration mechanism/factory for v1 traits
|
|
136
|
+
match device.pv:
|
|
137
|
+
case DeviceVersion.V1:
|
|
138
|
+
channel = create_v1_channel(user_data, mqtt_params, mqtt_session, device)
|
|
139
|
+
traits.append(StatusTrait(product, channel.rpc_channel))
|
|
140
|
+
case DeviceVersion.A01:
|
|
141
|
+
mqtt_channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device)
|
|
142
|
+
match product.category:
|
|
143
|
+
case RoborockCategory.WET_DRY_VAC:
|
|
144
|
+
traits.append(DyadApi(mqtt_channel))
|
|
145
|
+
case RoborockCategory.WASHING_MACHINE:
|
|
146
|
+
traits.append(ZeoApi(mqtt_channel))
|
|
147
|
+
case _:
|
|
148
|
+
raise NotImplementedError(f"Device {device.name} has unsupported category {product.category}")
|
|
149
|
+
case _:
|
|
150
|
+
raise NotImplementedError(f"Device {device.name} has unsupported version {device.pv}")
|
|
151
|
+
return RoborockDevice(device, channel, traits)
|
|
126
152
|
|
|
127
153
|
manager = DeviceManager(home_data_api, device_creator, mqtt_session=mqtt_session)
|
|
128
154
|
await manager.discover_devices()
|
|
@@ -10,6 +10,8 @@ from roborock.exceptions import RoborockConnectionException, RoborockException
|
|
|
10
10
|
from roborock.protocol import Decoder, Encoder, create_local_decoder, create_local_encoder
|
|
11
11
|
from roborock.roborock_message import RoborockMessage
|
|
12
12
|
|
|
13
|
+
from .channel import Channel
|
|
14
|
+
|
|
13
15
|
_LOGGER = logging.getLogger(__name__)
|
|
14
16
|
_PORT = 58867
|
|
15
17
|
|
|
@@ -30,7 +32,7 @@ class _LocalProtocol(asyncio.Protocol):
|
|
|
30
32
|
self.connection_lost_cb(exc)
|
|
31
33
|
|
|
32
34
|
|
|
33
|
-
class LocalChannel:
|
|
35
|
+
class LocalChannel(Channel):
|
|
34
36
|
"""Simple RPC-style channel for communicating with a device over a local network.
|
|
35
37
|
|
|
36
38
|
Handles request/response correlation and timeouts, but leaves message
|
|
@@ -5,16 +5,18 @@ import logging
|
|
|
5
5
|
from collections.abc import Callable
|
|
6
6
|
from json import JSONDecodeError
|
|
7
7
|
|
|
8
|
-
from roborock.containers import RRiot
|
|
8
|
+
from roborock.containers import HomeDataDevice, RRiot, UserData
|
|
9
9
|
from roborock.exceptions import RoborockException
|
|
10
10
|
from roborock.mqtt.session import MqttParams, MqttSession
|
|
11
11
|
from roborock.protocol import create_mqtt_decoder, create_mqtt_encoder
|
|
12
12
|
from roborock.roborock_message import RoborockMessage
|
|
13
13
|
|
|
14
|
+
from .channel import Channel
|
|
15
|
+
|
|
14
16
|
_LOGGER = logging.getLogger(__name__)
|
|
15
17
|
|
|
16
18
|
|
|
17
|
-
class MqttChannel:
|
|
19
|
+
class MqttChannel(Channel):
|
|
18
20
|
"""Simple RPC-style channel for communicating with a device over MQTT.
|
|
19
21
|
|
|
20
22
|
Handles request/response correlation and timeouts, but leaves message
|
|
@@ -33,6 +35,12 @@ class MqttChannel:
|
|
|
33
35
|
self._decoder = create_mqtt_decoder(local_key)
|
|
34
36
|
self._encoder = create_mqtt_encoder(local_key)
|
|
35
37
|
self._queue_lock = asyncio.Lock()
|
|
38
|
+
self._mqtt_unsub: Callable[[], None] | None = None
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def is_connected(self) -> bool:
|
|
42
|
+
"""Return true if the channel is connected."""
|
|
43
|
+
return (self._mqtt_unsub is not None) and self._mqtt_session.connected
|
|
36
44
|
|
|
37
45
|
@property
|
|
38
46
|
def _publish_topic(self) -> str:
|
|
@@ -67,7 +75,14 @@ class MqttChannel:
|
|
|
67
75
|
except Exception as e:
|
|
68
76
|
_LOGGER.exception("Uncaught error in message handler callback: %s", e)
|
|
69
77
|
|
|
70
|
-
|
|
78
|
+
self._mqtt_unsub = await self._mqtt_session.subscribe(self._subscribe_topic, message_handler)
|
|
79
|
+
|
|
80
|
+
def unsub_wrapper() -> None:
|
|
81
|
+
if self._mqtt_unsub is not None:
|
|
82
|
+
self._mqtt_unsub()
|
|
83
|
+
self._mqtt_unsub = None
|
|
84
|
+
|
|
85
|
+
return unsub_wrapper
|
|
71
86
|
|
|
72
87
|
async def _resolve_future_with_lock(self, message: RoborockMessage) -> None:
|
|
73
88
|
"""Resolve waiting future with proper locking."""
|
|
@@ -113,3 +128,10 @@ class MqttChannel:
|
|
|
113
128
|
async with self._queue_lock:
|
|
114
129
|
self._waiting_queue.pop(request_id, None)
|
|
115
130
|
raise
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def create_mqtt_channel(
|
|
134
|
+
user_data: UserData, mqtt_params: MqttParams, mqtt_session: MqttSession, device: HomeDataDevice
|
|
135
|
+
) -> MqttChannel:
|
|
136
|
+
"""Create a V1Channel for the given device."""
|
|
137
|
+
return MqttChannel(mqtt_session, device.duid, device.local_key, user_data.rriot, mqtt_params)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from roborock.roborock_message import RoborockDyadDataProtocol
|
|
7
|
+
|
|
8
|
+
from ..a01_channel import send_decoded_command
|
|
9
|
+
from ..mqtt_channel import MqttChannel
|
|
10
|
+
from .trait import Trait
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"DyadApi",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DyadApi(Trait):
|
|
20
|
+
"""API for interacting with Dyad devices."""
|
|
21
|
+
|
|
22
|
+
name = "dyad"
|
|
23
|
+
|
|
24
|
+
def __init__(self, channel: MqttChannel) -> None:
|
|
25
|
+
"""Initialize the Dyad API."""
|
|
26
|
+
self._channel = channel
|
|
27
|
+
|
|
28
|
+
async def query_values(self, protocols: list[RoborockDyadDataProtocol]) -> dict[RoborockDyadDataProtocol, Any]:
|
|
29
|
+
"""Query the device for the values of the given Dyad protocols."""
|
|
30
|
+
params = {RoborockDyadDataProtocol.ID_QUERY: [int(p) for p in protocols]}
|
|
31
|
+
return await send_decoded_command(self._channel, params)
|
|
32
|
+
|
|
33
|
+
async def set_value(self, protocol: RoborockDyadDataProtocol, value: Any) -> dict[RoborockDyadDataProtocol, Any]:
|
|
34
|
+
"""Set a value for a specific protocol on the device."""
|
|
35
|
+
params = {protocol: value}
|
|
36
|
+
return await send_decoded_command(self._channel, params)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Module for Roborock V1 devices.
|
|
2
|
+
|
|
3
|
+
This interface is experimental and subject to breaking changes without notice
|
|
4
|
+
until the API is stable.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from roborock.containers import (
|
|
10
|
+
HomeDataProduct,
|
|
11
|
+
ModelStatus,
|
|
12
|
+
S7MaxVStatus,
|
|
13
|
+
Status,
|
|
14
|
+
)
|
|
15
|
+
from roborock.roborock_typing import RoborockCommand
|
|
16
|
+
|
|
17
|
+
from ..v1_rpc_channel import V1RpcChannel
|
|
18
|
+
from .trait import Trait
|
|
19
|
+
|
|
20
|
+
_LOGGER = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Status",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StatusTrait(Trait):
|
|
28
|
+
"""Unified Roborock device class with automatic connection setup."""
|
|
29
|
+
|
|
30
|
+
name = "status"
|
|
31
|
+
|
|
32
|
+
def __init__(self, product_info: HomeDataProduct, rpc_channel: V1RpcChannel) -> None:
|
|
33
|
+
"""Initialize the StatusTrait."""
|
|
34
|
+
self._product_info = product_info
|
|
35
|
+
self._rpc_channel = rpc_channel
|
|
36
|
+
|
|
37
|
+
async def get_status(self) -> Status:
|
|
38
|
+
"""Get the current status of the device.
|
|
39
|
+
|
|
40
|
+
This is a placeholder command and will likely be changed/moved in the future.
|
|
41
|
+
"""
|
|
42
|
+
status_type: type[Status] = ModelStatus.get(self._product_info.model, S7MaxVStatus)
|
|
43
|
+
return await self._rpc_channel.send_command(RoborockCommand.GET_STATUS, response_type=status_type)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from roborock.roborock_message import RoborockZeoProtocol
|
|
7
|
+
|
|
8
|
+
from ..a01_channel import send_decoded_command
|
|
9
|
+
from ..mqtt_channel import MqttChannel
|
|
10
|
+
from .trait import Trait
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ZeoApi",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ZeoApi(Trait):
|
|
20
|
+
"""API for interacting with Zeo devices."""
|
|
21
|
+
|
|
22
|
+
name = "zeo"
|
|
23
|
+
|
|
24
|
+
def __init__(self, channel: MqttChannel) -> None:
|
|
25
|
+
"""Initialize the Zeo API."""
|
|
26
|
+
self._channel = channel
|
|
27
|
+
|
|
28
|
+
async def query_values(self, protocols: list[RoborockZeoProtocol]) -> dict[RoborockZeoProtocol, Any]:
|
|
29
|
+
"""Query the device for the values of the given protocols."""
|
|
30
|
+
params = {RoborockZeoProtocol.ID_QUERY: [int(p) for p in protocols]}
|
|
31
|
+
return await send_decoded_command(self._channel, params)
|
|
32
|
+
|
|
33
|
+
async def set_value(self, protocol: RoborockZeoProtocol, value: Any) -> dict[RoborockZeoProtocol, Any]:
|
|
34
|
+
"""Set a value for a specific protocol on the device."""
|
|
35
|
+
params = {protocol: value}
|
|
36
|
+
return await send_decoded_command(self._channel, params)
|
|
@@ -18,6 +18,7 @@ from roborock.protocols.v1_protocol import (
|
|
|
18
18
|
from roborock.roborock_message import RoborockMessage
|
|
19
19
|
from roborock.roborock_typing import RoborockCommand
|
|
20
20
|
|
|
21
|
+
from .channel import Channel
|
|
21
22
|
from .local_channel import LocalChannel, LocalSession, create_local_session
|
|
22
23
|
from .mqtt_channel import MqttChannel
|
|
23
24
|
from .v1_rpc_channel import V1RpcChannel, create_combined_rpc_channel, create_mqtt_rpc_channel
|
|
@@ -31,7 +32,7 @@ __all__ = [
|
|
|
31
32
|
_T = TypeVar("_T", bound=RoborockBase)
|
|
32
33
|
|
|
33
34
|
|
|
34
|
-
class V1Channel:
|
|
35
|
+
class V1Channel(Channel):
|
|
35
36
|
"""Unified V1 protocol channel with automatic MQTT/local connection handling.
|
|
36
37
|
|
|
37
38
|
This channel abstracts away the complexity of choosing between MQTT and local
|
|
@@ -63,6 +64,11 @@ class V1Channel:
|
|
|
63
64
|
self._callback: Callable[[RoborockMessage], None] | None = None
|
|
64
65
|
self._networking_info: NetworkInfo | None = None
|
|
65
66
|
|
|
67
|
+
@property
|
|
68
|
+
def is_connected(self) -> bool:
|
|
69
|
+
"""Return whether any connection is available."""
|
|
70
|
+
return self.is_mqtt_connected or self.is_local_connected
|
|
71
|
+
|
|
66
72
|
@property
|
|
67
73
|
def is_local_connected(self) -> bool:
|
|
68
74
|
"""Return whether local connection is available."""
|
|
@@ -20,7 +20,11 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
20
20
|
A01_VERSION = b"A01"
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def encode_mqtt_payload(
|
|
23
|
+
def encode_mqtt_payload(
|
|
24
|
+
data: dict[RoborockDyadDataProtocol, Any]
|
|
25
|
+
| dict[RoborockZeoProtocol, Any]
|
|
26
|
+
| dict[RoborockDyadDataProtocol | RoborockZeoProtocol, Any],
|
|
27
|
+
) -> RoborockMessage:
|
|
24
28
|
"""Encode payload for A01 commands over MQTT."""
|
|
25
29
|
dps_data = {"dps": data}
|
|
26
30
|
payload = pad(json.dumps(dps_data).encode("utf-8"), AES.block_size)
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
"""Module for Roborock devices.
|
|
2
|
-
|
|
3
|
-
This interface is experimental and subject to breaking changes without notice
|
|
4
|
-
until the API is stable.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import enum
|
|
8
|
-
import logging
|
|
9
|
-
from collections.abc import Callable
|
|
10
|
-
from functools import cached_property
|
|
11
|
-
|
|
12
|
-
from roborock.containers import (
|
|
13
|
-
HomeDataDevice,
|
|
14
|
-
HomeDataProduct,
|
|
15
|
-
ModelStatus,
|
|
16
|
-
S7MaxVStatus,
|
|
17
|
-
Status,
|
|
18
|
-
UserData,
|
|
19
|
-
)
|
|
20
|
-
from roborock.roborock_message import RoborockMessage
|
|
21
|
-
from roborock.roborock_typing import RoborockCommand
|
|
22
|
-
|
|
23
|
-
from .v1_channel import V1Channel
|
|
24
|
-
|
|
25
|
-
_LOGGER = logging.getLogger(__name__)
|
|
26
|
-
|
|
27
|
-
__all__ = [
|
|
28
|
-
"RoborockDevice",
|
|
29
|
-
"DeviceVersion",
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class DeviceVersion(enum.StrEnum):
|
|
34
|
-
"""Enum for device versions."""
|
|
35
|
-
|
|
36
|
-
V1 = "1.0"
|
|
37
|
-
A01 = "A01"
|
|
38
|
-
UNKNOWN = "unknown"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class RoborockDevice:
|
|
42
|
-
"""Unified Roborock device class with automatic connection setup."""
|
|
43
|
-
|
|
44
|
-
def __init__(
|
|
45
|
-
self,
|
|
46
|
-
user_data: UserData,
|
|
47
|
-
device_info: HomeDataDevice,
|
|
48
|
-
product_info: HomeDataProduct,
|
|
49
|
-
v1_channel: V1Channel,
|
|
50
|
-
) -> None:
|
|
51
|
-
"""Initialize the RoborockDevice.
|
|
52
|
-
|
|
53
|
-
The device takes ownership of the V1 channel for communication with the device.
|
|
54
|
-
Use `connect()` to establish the connection, which will set up the appropriate
|
|
55
|
-
protocol channel. Use `close()` to clean up all connections.
|
|
56
|
-
"""
|
|
57
|
-
self._user_data = user_data
|
|
58
|
-
self._device_info = device_info
|
|
59
|
-
self._product_info = product_info
|
|
60
|
-
self._v1_channel = v1_channel
|
|
61
|
-
self._unsub: Callable[[], None] | None = None
|
|
62
|
-
|
|
63
|
-
@property
|
|
64
|
-
def duid(self) -> str:
|
|
65
|
-
"""Return the device unique identifier (DUID)."""
|
|
66
|
-
return self._device_info.duid
|
|
67
|
-
|
|
68
|
-
@property
|
|
69
|
-
def name(self) -> str:
|
|
70
|
-
"""Return the device name."""
|
|
71
|
-
return self._device_info.name
|
|
72
|
-
|
|
73
|
-
@cached_property
|
|
74
|
-
def device_version(self) -> str:
|
|
75
|
-
"""Return the device version.
|
|
76
|
-
|
|
77
|
-
At the moment this is a simple check against the product version (pv) of the device
|
|
78
|
-
and used as a placeholder for upcoming functionality for devices that will behave
|
|
79
|
-
differently based on the version and capabilities.
|
|
80
|
-
"""
|
|
81
|
-
if self._device_info.pv == DeviceVersion.V1.value:
|
|
82
|
-
return DeviceVersion.V1
|
|
83
|
-
elif self._device_info.pv == DeviceVersion.A01.value:
|
|
84
|
-
return DeviceVersion.A01
|
|
85
|
-
_LOGGER.warning(
|
|
86
|
-
"Unknown device version %s for device %s, using default UNKNOWN",
|
|
87
|
-
self._device_info.pv,
|
|
88
|
-
self._device_info.name,
|
|
89
|
-
)
|
|
90
|
-
return DeviceVersion.UNKNOWN
|
|
91
|
-
|
|
92
|
-
@property
|
|
93
|
-
def is_connected(self) -> bool:
|
|
94
|
-
"""Return whether the device is connected."""
|
|
95
|
-
return self._v1_channel.is_mqtt_connected or self._v1_channel.is_local_connected
|
|
96
|
-
|
|
97
|
-
async def connect(self) -> None:
|
|
98
|
-
"""Connect to the device using the appropriate protocol channel."""
|
|
99
|
-
if self._unsub:
|
|
100
|
-
raise ValueError("Already connected to the device")
|
|
101
|
-
self._unsub = await self._v1_channel.subscribe(self._on_message)
|
|
102
|
-
_LOGGER.info("Connected to V1 device %s", self.name)
|
|
103
|
-
|
|
104
|
-
async def close(self) -> None:
|
|
105
|
-
"""Close all connections to the device."""
|
|
106
|
-
if self._unsub:
|
|
107
|
-
self._unsub()
|
|
108
|
-
self._unsub = None
|
|
109
|
-
|
|
110
|
-
def _on_message(self, message: RoborockMessage) -> None:
|
|
111
|
-
"""Handle incoming messages from the device."""
|
|
112
|
-
_LOGGER.debug("Received message from device: %s", message)
|
|
113
|
-
|
|
114
|
-
async def get_status(self) -> Status:
|
|
115
|
-
"""Get the current status of the device.
|
|
116
|
-
|
|
117
|
-
This is a placeholder command and will likely be changed/moved in the future.
|
|
118
|
-
"""
|
|
119
|
-
status_type: type[Status] = ModelStatus.get(self._product_info.model, S7MaxVStatus)
|
|
120
|
-
return await self._v1_channel.rpc_channel.send_command(RoborockCommand.GET_STATUS, response_type=status_type)
|
|
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-2.28.0 → python_roborock-2.29.0}/roborock/version_1_apis/roborock_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_roborock-2.28.0 → python_roborock-2.29.0}/roborock/version_a01_apis/roborock_client_a01.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|