Skip to content

Commit 8d16c95

Browse files
committed
feat: Add test coverage to device modules
1 parent e640e47 commit 8d16c95

File tree

5 files changed

+133
-13
lines changed

5 files changed

+133
-13
lines changed

roborock/devices/device.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import enum
88
import logging
9+
from collections.abc import Callable
910
from functools import cached_property
1011

1112
from roborock.containers import HomeDataDevice, HomeDataProduct, UserData
@@ -40,13 +41,16 @@ def __init__(
4041
) -> None:
4142
"""Initialize the RoborockDevice.
4243
43-
The device takes ownership of the MQTT channel for communication with the device
44-
and will close it when the device is closed.
44+
The device takes ownership of the MQTT channel for communication with the device.
45+
Use `connect()` to establish the connection, which will set up the MQTT channel
46+
for receiving messages from the device. Use `close()` to unsubscribe from the MQTT
47+
channel.
4548
"""
4649
self._user_data = user_data
4750
self._device_info = device_info
4851
self._product_info = product_info
4952
self._mqtt_channel = mqtt_channel
53+
self._unsub: Callable[[], None] | None = None
5054

5155
@property
5256
def duid(self) -> str:
@@ -82,14 +86,18 @@ async def connect(self) -> None:
8286
8387
This method will set up the MQTT channel for communication with the device.
8488
"""
85-
await self._mqtt_channel.subscribe(self._on_mqtt_message)
89+
if self._unsub:
90+
raise ValueError("Already connected to the device")
91+
self._unsub = await self._mqtt_channel.subscribe(self._on_mqtt_message)
8692

8793
async def close(self) -> None:
8894
"""Close the MQTT connection to the device.
8995
9096
This method will unsubscribe from the MQTT channel and clean up resources.
9197
"""
92-
await self._mqtt_channel.close()
98+
if self._unsub:
99+
self._unsub()
100+
self._unsub = None
93101

94102
def _on_mqtt_message(self, message: bytes) -> None:
95103
"""Handle incoming MQTT messages from the device.

roborock/devices/mqtt_channel.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ def _subscribe_topic(self) -> str:
3131
"""Topic to receive responses from the device."""
3232
return f"rr/m/o/{self._rriot.u}/{self._mqtt_params.username}/{self._duid}"
3333

34-
async def subscribe(self, callback: Callable[[bytes], None]) -> None:
35-
"""Subscribe to the device's response topic."""
36-
if self._unsub:
37-
raise ValueError("Already subscribed to the response topic")
38-
self._unsub = await self._mqtt_session.subscribe(self._subscribe_topic, callback)
34+
async def subscribe(self, callback: Callable[[bytes], None]) -> Callable[[], None]:
35+
"""Subscribe to the device's response topic.
3936
40-
async def close(self) -> None:
41-
"""Close the MQTT subscription."""
37+
The callback will be called with the message payload when a message is received.
38+
If already subscribed, raises ValueError.
39+
40+
Returns a callable that can be used to unsubscribe from the topic.
41+
"""
4242
if self._unsub:
43-
self._unsub()
44-
self._unsub = None
43+
raise ValueError("Already subscribed to the response topic")
44+
return await self._mqtt_session.subscribe(self._subscribe_topic, callback)

tests/devices/test_device.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Tests for the Device class."""
2+
3+
from unittest.mock import AsyncMock, Mock
4+
5+
from roborock.containers import HomeData, UserData
6+
from roborock.devices.device import DeviceVersion, RoborockDevice
7+
8+
from .. import mock_data
9+
10+
USER_DATA = UserData.from_dict(mock_data.USER_DATA)
11+
HOME_DATA = HomeData.from_dict(mock_data.HOME_DATA_RAW)
12+
13+
14+
async def test_device_connection() -> None:
15+
"""Test the Device connection setup."""
16+
17+
unsub = Mock()
18+
subscribe = AsyncMock()
19+
subscribe.return_value = unsub
20+
mqtt_channel = AsyncMock()
21+
mqtt_channel.subscribe = subscribe
22+
23+
device = RoborockDevice(
24+
USER_DATA,
25+
device_info=HOME_DATA.devices[0],
26+
product_info=HOME_DATA.products[0],
27+
mqtt_channel=mqtt_channel,
28+
)
29+
assert device.duid == "abc123"
30+
assert device.name == "Roborock S7 MaxV"
31+
assert device.device_version == DeviceVersion.V1
32+
33+
assert not subscribe.called
34+
35+
await device.connect()
36+
assert subscribe.called
37+
assert not unsub.called
38+
39+
await device.close()
40+
assert unsub.called

tests/devices/test_device_manager.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for the DeviceManager class."""
22

3+
from collections.abc import Generator
34
from unittest.mock import patch
45

56
import pytest
@@ -14,6 +15,13 @@
1415
USER_DATA = UserData.from_dict(mock_data.USER_DATA)
1516

1617

18+
@pytest.fixture(autouse=True)
19+
def setup_mqtt_session() -> Generator[None, None, None]:
20+
"""Fixture to set up the MQTT session for the tests."""
21+
with patch("roborock.devices.device_manager.create_mqtt_session"):
22+
yield
23+
24+
1725
async def home_home_data_no_devices() -> HomeData:
1826
"""Mock home data API that returns no devices."""
1927
return HomeData(

tests/devices/test_mqtt_channel.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Tests for the MqttChannel class."""
2+
3+
from collections.abc import Generator
4+
from unittest.mock import AsyncMock, Mock, patch
5+
6+
import pytest
7+
8+
from roborock.containers import HomeData, UserData
9+
from roborock.devices.mqtt_channel import MqttChannel
10+
from roborock.mqtt.session import MqttParams
11+
12+
from .. import mock_data
13+
14+
USER_DATA = UserData.from_dict(mock_data.USER_DATA)
15+
TEST_MQTT_PARAMS = MqttParams(
16+
host="localhost",
17+
port=1883,
18+
tls=False,
19+
username="username",
20+
password="password",
21+
timeout=10.0,
22+
)
23+
24+
25+
@pytest.fixture(autouse=True)
26+
def setup_mqtt_session() -> Generator[None, None, None]:
27+
"""Fixture to set up the MQTT session for the tests."""
28+
with patch("roborock.devices.device_manager.create_mqtt_session"):
29+
yield
30+
31+
32+
async def home_home_data_no_devices() -> HomeData:
33+
"""Mock home data API that returns no devices."""
34+
return HomeData(
35+
id=1,
36+
name="Test Home",
37+
devices=[],
38+
products=[],
39+
)
40+
41+
42+
async def mock_home_data() -> HomeData:
43+
"""Mock home data API that returns devices."""
44+
return HomeData.from_dict(mock_data.HOME_DATA_RAW)
45+
46+
47+
async def test_mqtt_channel() -> None:
48+
"""Test MQTT channel setup."""
49+
50+
mock_session = AsyncMock()
51+
52+
channel = MqttChannel(mock_session, duid="abc123", rriot=USER_DATA.rriot, mqtt_params=TEST_MQTT_PARAMS)
53+
54+
unsub = Mock()
55+
mock_session.subscribe.return_value = unsub
56+
57+
callback = Mock()
58+
result = await channel.subscribe(callback)
59+
60+
assert mock_session.subscribe.called
61+
assert mock_session.subscribe.call_args[0][0] == "rr/m/o/user123/username/abc123"
62+
assert mock_session.subscribe.call_args[0][1] == callback
63+
64+
assert result == unsub

0 commit comments

Comments
 (0)