diff --git a/docs/DEVICES.md b/docs/DEVICES.md index 9d31df1b..39ad775e 100644 --- a/docs/DEVICES.md +++ b/docs/DEVICES.md @@ -564,30 +564,40 @@ The new design: ``` roborock/ ├── devices/ # Device management and channels -│ ├── device_manager.py # High-level device lifecycle -│ ├── channel.py # Base Channel interface -│ ├── mqtt_channel.py # MQTT channel implementation -│ ├── local_channel.py # Local TCP channel implementation -│ ├── v1_channel.py # V1 protocol channel with RPC strategies -│ ├── a01_channel.py # A01 protocol helpers -│ ├── b01_q7_channel.py # B01 Q7 protocol helpers -│ └── traits/ # Device-specific command traits -│ └── v1/ # V1 device traits -│ ├── __init__.py # Trait initialization -│ ├── clean.py # Cleaning commands -│ ├── map.py # Map management +│ ├── device_manager.py # High-level device lifecycle +│ ├── transport/ # Module for network connections to devices +│ | ├── channel.py # Base Channel interface +│ | ├── mqtt_channel.py # MQTT channel implementation +│ | ├── local_channel.py # Local TCP channel implementation +│ | └── ... +│ ├── rpc/ # Application-level protocol/device-specific glue +│ | ├── v1_channel.py # V1 protocol channel with RPC strategies +│ | ├── a01_channel.py # A01 protocol helpers +│ | ├── b01_q7_channel.py # B01 Q7 protocol helpers +│ | ├── b01_q10_channel.py # B01 Q10 protocol helpers +│ | └── ... +│ └── traits/ # High-level device-specific command traits +│ └── v1/ # V1 device traits +│ ├── __init__.py # Trait initialization +│ ├── clean.py # Cleaning commands +│ ├── map.py # Map management │ └── ... -├── mqtt/ # MQTT session management +├── mqtt/ # MQTT session management │ ├── session.py # Base session interface │ └── roborock_session.py # MQTT session with idle timeout -├── protocols/ # Protocol encoders/decoders +├── protocols/ # Low level protocol encoders/decoders │ ├── v1_protocol.py # V1 JSON RPC protocol │ ├── a01_protocol.py # A01 protocol │ ├── b01_q7_protocol.py # B01 Q7 protocol │ └── ... -└── data/ # Data containers and mappings +└── data/ # Data containers and mappings ├── containers.py # Status, HomeData, etc. - └── v1/ # V1-specific data structures + ├── v1/ # V1-specific data structures + ├── dyad/ # Dyad-specific data structures + ├── zeo/ # Zeo-specific data structures + ├── b01_q7/ # B01 Q7-specific data structures + ├── b01_q10/ # B01 Q10-specific data structures + └── ... ``` ### Threading Model diff --git a/roborock/devices/device.py b/roborock/devices/device.py index d7908c74..9026c4a7 100644 --- a/roborock/devices/device.py +++ b/roborock/devices/device.py @@ -18,9 +18,9 @@ from roborock.roborock_message import RoborockMessage from roborock.util import RoborockLoggerAdapter -from .channel import Channel from .traits import Trait from .traits.traits_mixin import TraitsMixin +from .transport.channel import Channel _LOGGER = logging.getLogger(__name__) diff --git a/roborock/devices/device_manager.py b/roborock/devices/device_manager.py index 9773923d..945daa24 100644 --- a/roborock/devices/device_manager.py +++ b/roborock/devices/device_manager.py @@ -25,10 +25,10 @@ from roborock.web_api import RoborockApiClient, UserWebApiClient from .cache import Cache, DeviceCache, NoCache -from .channel import Channel -from .mqtt_channel import create_mqtt_channel +from .rpc.v1_channel import create_v1_channel from .traits import Trait, a01, b01, v1 -from .v1_channel import create_v1_channel +from .transport.channel import Channel +from .transport.mqtt_channel import create_mqtt_channel _LOGGER = logging.getLogger(__name__) diff --git a/roborock/devices/rpc/__init__.py b/roborock/devices/rpc/__init__.py new file mode 100644 index 00000000..97de9d3e --- /dev/null +++ b/roborock/devices/rpc/__init__.py @@ -0,0 +1,14 @@ +"""Module for sending device specific commands to Roborock devices. + +This module provides a application-level interface for sending commands to Roborock +devices. These modules can be used by traits (higher level APIs) to send commands. + +Each module may contain details that are common across all traits, and may depend +on the transport level modules (e.g. MQTT, Local device) for issuing the +commands. + +The lowest level protocol encoding is handled in `roborock.protocols` which +have no dependencies on the transport level modules. +""" + +__all__: list[str] = [] diff --git a/roborock/devices/a01_channel.py b/roborock/devices/rpc/a01_channel.py similarity index 98% rename from roborock/devices/a01_channel.py rename to roborock/devices/rpc/a01_channel.py index f698bb6e..2e2f9cea 100644 --- a/roborock/devices/a01_channel.py +++ b/roborock/devices/rpc/a01_channel.py @@ -5,6 +5,7 @@ from collections.abc import Callable from typing import Any, overload +from roborock.devices.transport.mqtt_channel import MqttChannel from roborock.exceptions import RoborockException from roborock.protocols.a01_protocol import ( decode_rpc_response, @@ -16,8 +17,6 @@ RoborockZeoProtocol, ) -from .mqtt_channel import MqttChannel - _LOGGER = logging.getLogger(__name__) _TIMEOUT = 10.0 diff --git a/roborock/devices/b01_q10_channel.py b/roborock/devices/rpc/b01_q10_channel.py similarity index 94% rename from roborock/devices/b01_q10_channel.py rename to roborock/devices/rpc/b01_q10_channel.py index 1e3043a7..a482e109 100644 --- a/roborock/devices/b01_q10_channel.py +++ b/roborock/devices/rpc/b01_q10_channel.py @@ -5,14 +5,13 @@ import logging from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP +from roborock.devices.transport.mqtt_channel import MqttChannel from roborock.exceptions import RoborockException from roborock.protocols.b01_q10_protocol import ( ParamsType, encode_mqtt_payload, ) -from .mqtt_channel import MqttChannel - _LOGGER = logging.getLogger(__name__) diff --git a/roborock/devices/b01_q7_channel.py b/roborock/devices/rpc/b01_q7_channel.py similarity index 98% rename from roborock/devices/b01_q7_channel.py rename to roborock/devices/rpc/b01_q7_channel.py index 41c9085c..add5bc97 100644 --- a/roborock/devices/b01_q7_channel.py +++ b/roborock/devices/rpc/b01_q7_channel.py @@ -7,6 +7,7 @@ import logging from typing import Any +from roborock.devices.transport.mqtt_channel import MqttChannel from roborock.exceptions import RoborockException from roborock.protocols.b01_q7_protocol import ( Q7RequestMessage, @@ -15,8 +16,6 @@ ) from roborock.roborock_message import RoborockMessage -from .mqtt_channel import MqttChannel - _LOGGER = logging.getLogger(__name__) _TIMEOUT = 10.0 diff --git a/roborock/devices/v1_channel.py b/roborock/devices/rpc/v1_channel.py similarity index 98% rename from roborock/devices/v1_channel.py rename to roborock/devices/rpc/v1_channel.py index 68b96d25..d1b4ee24 100644 --- a/roborock/devices/v1_channel.py +++ b/roborock/devices/rpc/v1_channel.py @@ -12,6 +12,10 @@ from typing import Any, TypeVar from roborock.data import HomeDataDevice, NetworkInfo, RoborockBase, UserData +from roborock.devices.cache import DeviceCache +from roborock.devices.transport.channel import Channel +from roborock.devices.transport.local_channel import LocalChannel, LocalSession, create_local_session +from roborock.devices.transport.mqtt_channel import MqttChannel from roborock.exceptions import RoborockException from roborock.mqtt.health_manager import HealthManager from roborock.mqtt.session import MqttParams, MqttSession @@ -32,11 +36,6 @@ from roborock.roborock_typing import RoborockCommand from roborock.util import RoborockLoggerAdapter -from .cache import DeviceCache -from .channel import Channel -from .local_channel import LocalChannel, LocalSession, create_local_session -from .mqtt_channel import MqttChannel - _LOGGER = logging.getLogger(__name__) __all__ = [ diff --git a/roborock/devices/traits/a01/__init__.py b/roborock/devices/traits/a01/__init__.py index 4c4b4cb7..537c8437 100644 --- a/roborock/devices/traits/a01/__init__.py +++ b/roborock/devices/traits/a01/__init__.py @@ -48,9 +48,9 @@ ZeoState, ZeoTemperature, ) -from roborock.devices.a01_channel import send_decoded_command -from roborock.devices.mqtt_channel import MqttChannel +from roborock.devices.rpc.a01_channel import send_decoded_command from roborock.devices.traits import Trait +from roborock.devices.transport.mqtt_channel import MqttChannel from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol __init__ = [ diff --git a/roborock/devices/traits/b01/q10/__init__.py b/roborock/devices/traits/b01/q10/__init__.py index ea6862b3..9073bf60 100644 --- a/roborock/devices/traits/b01/q10/__init__.py +++ b/roborock/devices/traits/b01/q10/__init__.py @@ -2,9 +2,9 @@ from typing import Any -from roborock.devices.b01_q7_channel import send_decoded_command -from roborock.devices.mqtt_channel import MqttChannel +from roborock.devices.rpc.b01_q7_channel import send_decoded_command from roborock.devices.traits import Trait +from roborock.devices.transport.mqtt_channel import MqttChannel from .command import CommandTrait diff --git a/roborock/devices/traits/b01/q10/command.py b/roborock/devices/traits/b01/q10/command.py index ca41644c..4aa3a593 100644 --- a/roborock/devices/traits/b01/q10/command.py +++ b/roborock/devices/traits/b01/q10/command.py @@ -1,8 +1,8 @@ from typing import Any from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP -from roborock.devices.b01_q10_channel import send_command -from roborock.devices.mqtt_channel import MqttChannel +from roborock.devices.rpc.b01_q10_channel import send_command +from roborock.devices.transport.mqtt_channel import MqttChannel from roborock.protocols.b01_q10_protocol import ParamsType diff --git a/roborock/devices/traits/b01/q7/__init__.py b/roborock/devices/traits/b01/q7/__init__.py index 47a4c6c2..246fc87d 100644 --- a/roborock/devices/traits/b01/q7/__init__.py +++ b/roborock/devices/traits/b01/q7/__init__.py @@ -10,9 +10,9 @@ SCWindMapping, WaterLevelMapping, ) -from roborock.devices.b01_q7_channel import send_decoded_command -from roborock.devices.mqtt_channel import MqttChannel +from roborock.devices.rpc.b01_q7_channel import send_decoded_command from roborock.devices.traits import Trait +from roborock.devices.transport.mqtt_channel import MqttChannel from roborock.protocols.b01_q7_protocol import CommandType, ParamsType, Q7RequestMessage from roborock.roborock_message import RoborockB01Props from roborock.roborock_typing import RoborockB01Q7Methods diff --git a/roborock/devices/transport/__init__.py b/roborock/devices/transport/__init__.py new file mode 100644 index 00000000..d6dee79c --- /dev/null +++ b/roborock/devices/transport/__init__.py @@ -0,0 +1,8 @@ +"""Module for handling network connections to Roborock devices. + +This is used internally by the device manager for creating connections to +Roborock devices. These modules contain common code, not specific to a +particular device or application level protocol. +""" + +__all__: list[str] = [] diff --git a/roborock/devices/channel.py b/roborock/devices/transport/channel.py similarity index 100% rename from roborock/devices/channel.py rename to roborock/devices/transport/channel.py diff --git a/roborock/devices/local_channel.py b/roborock/devices/transport/local_channel.py similarity index 98% rename from roborock/devices/local_channel.py rename to roborock/devices/transport/local_channel.py index 0a7abd18..25d1380a 100644 --- a/roborock/devices/local_channel.py +++ b/roborock/devices/transport/local_channel.py @@ -8,10 +8,10 @@ from roborock.callbacks import CallbackList, decoder_callback from roborock.exceptions import RoborockConnectionException, RoborockException from roborock.protocol import create_local_decoder, create_local_encoder +from roborock.protocols.v1_protocol import LocalProtocolVersion from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol +from roborock.util import RoborockLoggerAdapter, get_next_int -from ..protocols.v1_protocol import LocalProtocolVersion -from ..util import RoborockLoggerAdapter, get_next_int from .channel import Channel _LOGGER = logging.getLogger(__name__) diff --git a/roborock/devices/mqtt_channel.py b/roborock/devices/transport/mqtt_channel.py similarity index 100% rename from roborock/devices/mqtt_channel.py rename to roborock/devices/transport/mqtt_channel.py diff --git a/tests/devices/test_a01_channel.py b/tests/devices/rpc/test_a01_channel.py similarity index 96% rename from tests/devices/test_a01_channel.py rename to tests/devices/rpc/test_a01_channel.py index befa8951..e98cf228 100644 --- a/tests/devices/test_a01_channel.py +++ b/tests/devices/rpc/test_a01_channel.py @@ -4,7 +4,7 @@ import pytest -from roborock.devices.a01_channel import send_decoded_command +from roborock.devices.rpc.a01_channel import send_decoded_command from roborock.protocols.a01_protocol import encode_mqtt_payload from roborock.roborock_message import ( RoborockDyadDataProtocol, diff --git a/tests/devices/test_v1_channel.py b/tests/devices/rpc/test_v1_channel.py similarity index 98% rename from tests/devices/test_v1_channel.py rename to tests/devices/rpc/test_v1_channel.py index c9c40cad..293eb260 100644 --- a/tests/devices/test_v1_channel.py +++ b/tests/devices/rpc/test_v1_channel.py @@ -13,8 +13,8 @@ from roborock.data import NetworkInfo, RoborockStateCode, S5MaxStatus, UserData from roborock.devices.cache import DeviceCache, DeviceCacheData, InMemoryCache -from roborock.devices.local_channel import LocalSession -from roborock.devices.v1_channel import V1Channel +from roborock.devices.rpc.v1_channel import V1Channel +from roborock.devices.transport.local_channel import LocalSession from roborock.exceptions import RoborockException from roborock.protocol import ( create_local_decoder, @@ -107,7 +107,7 @@ def fake_next_int(*args) -> int: @pytest.fixture(name="mock_create_map_response_decoder") def setup_mock_map_decoder() -> Iterator[Mock]: """Mock the map response decoder to control its behavior in tests.""" - with patch("roborock.devices.v1_channel.create_map_response_decoder") as mock_create_decoder: + with patch("roborock.devices.rpc.v1_channel.create_map_response_decoder") as mock_create_decoder: yield mock_create_decoder diff --git a/tests/devices/test_a01_traits.py b/tests/devices/test_a01_traits.py index be400f0a..83a5b5cb 100644 --- a/tests/devices/test_a01_traits.py +++ b/tests/devices/test_a01_traits.py @@ -10,8 +10,6 @@ def mock_channel(): channel = Mock() channel.send_command = AsyncMock() - # Mocking send_decoded_command if it was a method on channel, but it's a standalone function imported in traits. - # However, in traits/__init__.py it is imported as: from roborock.devices.a01_channel import send_decoded_command return channel diff --git a/tests/devices/traits/b01/q7/test_init.py b/tests/devices/traits/b01/q7/test_init.py index 50096a5a..bc141ebf 100644 --- a/tests/devices/traits/b01/q7/test_init.py +++ b/tests/devices/traits/b01/q7/test_init.py @@ -16,7 +16,7 @@ WaterLevelMapping, WorkStatusMapping, ) -from roborock.devices.b01_q7_channel import send_decoded_command +from roborock.devices.rpc.b01_q7_channel import send_decoded_command from roborock.devices.traits.b01.q7 import Q7PropertiesApi from roborock.exceptions import RoborockException from roborock.protocols.b01_q7_protocol import B01_VERSION, Q7RequestMessage diff --git a/tests/devices/test_local_channel.py b/tests/devices/transport/test_local_channel.py similarity index 98% rename from tests/devices/test_local_channel.py rename to tests/devices/transport/test_local_channel.py index 1cc6f842..959a021d 100644 --- a/tests/devices/test_local_channel.py +++ b/tests/devices/transport/test_local_channel.py @@ -7,7 +7,7 @@ import pytest -from roborock.devices.local_channel import LocalChannel, LocalChannelParams +from roborock.devices.transport.local_channel import LocalChannel, LocalChannelParams from roborock.exceptions import RoborockConnectionException, RoborockException from roborock.protocol import create_local_decoder, create_local_encoder from roborock.protocols.v1_protocol import LocalProtocolVersion @@ -52,7 +52,7 @@ def setup_mock_loop(mock_transport: Mock) -> Generator[Mock, None, None]: loop = Mock() loop.create_connection = AsyncMock(return_value=(mock_transport, Mock())) - with patch("roborock.devices.local_channel.get_running_loop", return_value=loop): + with patch("roborock.devices.transport.local_channel.get_running_loop", return_value=loop): yield loop @@ -427,7 +427,7 @@ async def test_keep_alive_ping_exceptions_handled_gracefully( local_channel: LocalChannel, mock_loop: Mock, caplog: pytest.LogCaptureFixture ) -> None: """Test that exceptions in the ping loop are handled gracefully without stopping the loop.""" - from roborock.devices.local_channel import _PING_INTERVAL + from roborock.devices.transport.local_channel import _PING_INTERVAL # Set log level to capture DEBUG messages caplog.set_level("DEBUG") diff --git a/tests/devices/test_mqtt_channel.py b/tests/devices/transport/test_mqtt_channel.py similarity index 99% rename from tests/devices/test_mqtt_channel.py rename to tests/devices/transport/test_mqtt_channel.py index 3d71173d..55e7b8bb 100644 --- a/tests/devices/test_mqtt_channel.py +++ b/tests/devices/transport/test_mqtt_channel.py @@ -9,7 +9,7 @@ import pytest from roborock.data import HomeData, UserData -from roborock.devices.mqtt_channel import MqttChannel +from roborock.devices.transport.mqtt_channel import MqttChannel from roborock.mqtt.session import MqttParams from roborock.protocol import create_mqtt_decoder, create_mqtt_encoder from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol diff --git a/tests/e2e/test_local_session.py b/tests/e2e/test_local_session.py index 6d8458b1..813a3518 100644 --- a/tests/e2e/test_local_session.py +++ b/tests/e2e/test_local_session.py @@ -6,7 +6,7 @@ import pytest import syrupy -from roborock.devices.local_channel import LocalChannel +from roborock.devices.transport.local_channel import LocalChannel from roborock.protocol import MessageParser, create_local_decoder from roborock.protocols.v1_protocol import LocalProtocolVersion from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol diff --git a/tests/fixtures/local_async_fixtures.py b/tests/fixtures/local_async_fixtures.py index e328d663..1edef1e1 100644 --- a/tests/fixtures/local_async_fixtures.py +++ b/tests/fixtures/local_async_fixtures.py @@ -79,7 +79,7 @@ def start_handle_write(data: bytes) -> None: return (mock_transport, protocol) - with patch("roborock.devices.local_channel.get_running_loop") as mock_loop: + with patch("roborock.devices.transport.local_channel.get_running_loop") as mock_loop: mock_loop.return_value.create_connection.side_effect = create_connection yield diff --git a/tests/fixtures/logging_fixtures.py b/tests/fixtures/logging_fixtures.py index 542ec164..95355cfe 100644 --- a/tests/fixtures/logging_fixtures.py +++ b/tests/fixtures/logging_fixtures.py @@ -51,7 +51,7 @@ def get_token_bytes(n: int) -> bytes: return result with ( - patch("roborock.devices.local_channel.get_next_int", side_effect=get_next_int), + patch("roborock.devices.transport.local_channel.get_next_int", side_effect=get_next_int), patch("roborock.protocols.v1_protocol.get_next_int", side_effect=get_next_int), patch("roborock.protocols.v1_protocol.get_timestamp", side_effect=get_timestamp), patch("roborock.protocols.v1_protocol.secrets.token_bytes", side_effect=get_token_bytes),