From a4903302808f8d414f1ac47c7861c7d3d949aa80 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 23 Dec 2025 18:40:44 -0800 Subject: [PATCH 1/4] fix: Allow startup with unsupported devices Raise and catch when finding an unsupported device. We will allow the device manager to continue with the devices that are supported, ignoring the ones that are not supoprted. We test with an unsupported protocol version but it also applies to unsupported q10 devices. --- roborock/devices/device_manager.py | 20 ++++++++--- tests/devices/test_device_manager.py | 52 ++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/roborock/devices/device_manager.py b/roborock/devices/device_manager.py index 1717e94c..128e42b9 100644 --- a/roborock/devices/device_manager.py +++ b/roborock/devices/device_manager.py @@ -51,6 +51,10 @@ class DeviceVersion(enum.StrEnum): UNKNOWN = "unknown" +class UnsupportedDeviceError(RoborockException): + """Exception raised when a device is unsupported.""" + + class DeviceManager: """Central manager for Roborock device discovery and connections.""" @@ -95,11 +99,19 @@ async def discover_devices(self, prefer_cache: bool = True) -> list[RoborockDevi # These are connected serially to avoid overwhelming the MQTT broker new_devices = {} start_tasks = [] + supported_devices_counter = self._diagnostics.subkey("supported_devices") + unsupported_devices_counter = self._diagnostics.subkey("unsupported_devices") for duid, (device, product) in device_products.items(): _LOGGER.debug("[%s] Discovered device %s %s", duid, product.summary_info(), device.summary_info()) if duid in self._devices: continue - new_device = self._device_creator(home_data, device, product) + try: + new_device = self._device_creator(home_data, device, product) + except UnsupportedDeviceError: + _LOGGER.info("Skipping unsupported device %s %s", product.summary_info(), device.summary_info()) + unsupported_devices_counter.increment(device.pv) + continue + supported_devices_counter.increment(device.pv) start_tasks.append(new_device.start_connect()) new_devices[duid] = new_device @@ -228,16 +240,16 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device) model_part = product.model.split(".")[-1] if "ss" in model_part: - raise NotImplementedError( + raise UnsupportedDeviceError( f"Device {device.name} has unsupported version B01_{product.model.strip('.')[-1]}" ) elif "sc" in model_part: # Q7 devices start with 'sc' in their model naming. trait = b01.q7.create(channel) else: - raise NotImplementedError(f"Device {device.name} has unsupported B01 model: {product.model}") + raise UnsupportedDeviceError(f"Device {device.name} has unsupported B01 model: {product.model}") case _: - raise NotImplementedError(f"Device {device.name} has unsupported version {device.pv}") + raise UnsupportedDeviceError(f"Device {device.name} has unsupported version {device.pv}") dev = RoborockDevice(device, product, channel, trait) if ready_callback: diff --git a/tests/devices/test_device_manager.py b/tests/devices/test_device_manager.py index f5ff75f7..d5b256c1 100644 --- a/tests/devices/test_device_manager.py +++ b/tests/devices/test_device_manager.py @@ -9,6 +9,7 @@ import pytest from roborock.data import HomeData, UserData +from roborock.data.containers import HomeDataDevice from roborock.devices.cache import InMemoryCache from roborock.devices.device import RoborockDevice from roborock.devices.device_manager import UserParams, create_device_manager, create_web_api_wrapper @@ -362,3 +363,54 @@ async def test_diagnostics_collection(home_data: HomeData) -> None: assert diagnostics.get("fetch_home_data") == 1 await device_manager.close() + + +async def test_unsupported_protocol_versio() -> None: + """Test the DeviceManager with some supported and unsupported product IDs.""" + with patch("roborock.devices.device_manager.UserWebApiClient.get_home_data") as mock_home_data: + home_data = HomeData.from_dict({ + "id": 1, + "name": "Test Home", + "devices": [ + { + "duid": "device-uid-1", + "name": "Device 1", + "pv": "1.0", + "productId": "product-id-1", + "localKey": mock_data.LOCAL_KEY, + }, + { + "duid": "device-uid-2", + "name": "Device 2", + "pv": "unknown-pv", # Fake new protocol version we've never seen + "productId": "product-id-2", + "localKey": mock_data.LOCAL_KEY, + }, + ], + "products": [ + { + "id": "product-id-1", + "name": "Roborock S7 MaxV", + "model": "roborock.vacuum.a27", + "category": "robot.vacuum.cleaner", + }, + { + "id": "product-id-2", + "name": "New Roborock Model", + "model": "roborock.vacuum.newmodel", + "category": "robot.vacuum.cleaner", + }, + ], + }) + mock_home_data.return_value = home_data + + device_manager = await create_device_manager(USER_PARAMS) + + # Only the supported device should be created. The other device is ignored + devices = await device_manager.get_devices() + assert [device.duid for device in devices] == ["device-uid-1"] + + # Verify diagnostics + data = device_manager.diagnostic_data() + assert data.get("supported_devices") == {"1.0": 1} + assert data.get("unsupported_devices") == {"unknown-pv": 1} From e3cdd87f4615ca9a9cb2d2b3351fc6127d36e2fb Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 23 Dec 2025 19:29:03 -0800 Subject: [PATCH 2/4] chore: Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- roborock/devices/device_manager.py | 2 +- tests/devices/test_device_manager.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/roborock/devices/device_manager.py b/roborock/devices/device_manager.py index 128e42b9..212aa3bc 100644 --- a/roborock/devices/device_manager.py +++ b/roborock/devices/device_manager.py @@ -241,7 +241,7 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat model_part = product.model.split(".")[-1] if "ss" in model_part: raise UnsupportedDeviceError( - f"Device {device.name} has unsupported version B01_{product.model.strip('.')[-1]}" + f"Device {device.name} has unsupported version B01_{model_part}" ) elif "sc" in model_part: # Q7 devices start with 'sc' in their model naming. diff --git a/tests/devices/test_device_manager.py b/tests/devices/test_device_manager.py index d5b256c1..ae52527a 100644 --- a/tests/devices/test_device_manager.py +++ b/tests/devices/test_device_manager.py @@ -9,7 +9,6 @@ import pytest from roborock.data import HomeData, UserData -from roborock.data.containers import HomeDataDevice from roborock.devices.cache import InMemoryCache from roborock.devices.device import RoborockDevice from roborock.devices.device_manager import UserParams, create_device_manager, create_web_api_wrapper @@ -365,7 +364,7 @@ async def test_diagnostics_collection(home_data: HomeData) -> None: await device_manager.close() -async def test_unsupported_protocol_versio() -> None: +async def test_unsupported_protocol_version() -> None: """Test the DeviceManager with some supported and unsupported product IDs.""" with patch("roborock.devices.device_manager.UserWebApiClient.get_home_data") as mock_home_data: home_data = HomeData.from_dict({ @@ -405,7 +404,6 @@ async def test_unsupported_protocol_versio() -> None: mock_home_data.return_value = home_data device_manager = await create_device_manager(USER_PARAMS) - # Only the supported device should be created. The other device is ignored devices = await device_manager.get_devices() assert [device.duid for device in devices] == ["device-uid-1"] From dde57b9b4253a5d8a73b0ecc7e98d6c921e5f1ca Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 23 Dec 2025 19:30:25 -0800 Subject: [PATCH 3/4] chore: fix lint formatting --- tests/devices/test_device_manager.py | 70 ++++++++++++++-------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/tests/devices/test_device_manager.py b/tests/devices/test_device_manager.py index ae52527a..a788dd0e 100644 --- a/tests/devices/test_device_manager.py +++ b/tests/devices/test_device_manager.py @@ -367,40 +367,42 @@ async def test_diagnostics_collection(home_data: HomeData) -> None: async def test_unsupported_protocol_version() -> None: """Test the DeviceManager with some supported and unsupported product IDs.""" with patch("roborock.devices.device_manager.UserWebApiClient.get_home_data") as mock_home_data: - home_data = HomeData.from_dict({ - "id": 1, - "name": "Test Home", - "devices": [ - { - "duid": "device-uid-1", - "name": "Device 1", - "pv": "1.0", - "productId": "product-id-1", - "localKey": mock_data.LOCAL_KEY, - }, - { - "duid": "device-uid-2", - "name": "Device 2", - "pv": "unknown-pv", # Fake new protocol version we've never seen - "productId": "product-id-2", - "localKey": mock_data.LOCAL_KEY, - }, - ], - "products": [ - { - "id": "product-id-1", - "name": "Roborock S7 MaxV", - "model": "roborock.vacuum.a27", - "category": "robot.vacuum.cleaner", - }, - { - "id": "product-id-2", - "name": "New Roborock Model", - "model": "roborock.vacuum.newmodel", - "category": "robot.vacuum.cleaner", - }, - ], - }) + home_data = HomeData.from_dict( + { + "id": 1, + "name": "Test Home", + "devices": [ + { + "duid": "device-uid-1", + "name": "Device 1", + "pv": "1.0", + "productId": "product-id-1", + "localKey": mock_data.LOCAL_KEY, + }, + { + "duid": "device-uid-2", + "name": "Device 2", + "pv": "unknown-pv", # Fake new protocol version we've never seen + "productId": "product-id-2", + "localKey": mock_data.LOCAL_KEY, + }, + ], + "products": [ + { + "id": "product-id-1", + "name": "Roborock S7 MaxV", + "model": "roborock.vacuum.a27", + "category": "robot.vacuum.cleaner", + }, + { + "id": "product-id-2", + "name": "New Roborock Model", + "model": "roborock.vacuum.newmodel", + "category": "robot.vacuum.cleaner", + }, + ], + } + ) mock_home_data.return_value = home_data device_manager = await create_device_manager(USER_PARAMS) From 384b3147f6281f7a5ae82feff7991ecc84f1f9b1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 23 Dec 2025 19:33:26 -0800 Subject: [PATCH 4/4] chore: update diagnostics counters --- roborock/devices/device_manager.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/roborock/devices/device_manager.py b/roborock/devices/device_manager.py index 212aa3bc..2ff01085 100644 --- a/roborock/devices/device_manager.py +++ b/roborock/devices/device_manager.py @@ -109,9 +109,9 @@ async def discover_devices(self, prefer_cache: bool = True) -> list[RoborockDevi new_device = self._device_creator(home_data, device, product) except UnsupportedDeviceError: _LOGGER.info("Skipping unsupported device %s %s", product.summary_info(), device.summary_info()) - unsupported_devices_counter.increment(device.pv) + unsupported_devices_counter.increment(device.pv or "unknown") continue - supported_devices_counter.increment(device.pv) + supported_devices_counter.increment(device.pv or "unknown") start_tasks.append(new_device.start_connect()) new_devices[duid] = new_device @@ -241,7 +241,7 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat model_part = product.model.split(".")[-1] if "ss" in model_part: raise UnsupportedDeviceError( - f"Device {device.name} has unsupported version B01_{model_part}" + f"Device {device.name} has unsupported version B01 product model {product.model}" ) elif "sc" in model_part: # Q7 devices start with 'sc' in their model naming. @@ -249,7 +249,9 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat else: raise UnsupportedDeviceError(f"Device {device.name} has unsupported B01 model: {product.model}") case _: - raise UnsupportedDeviceError(f"Device {device.name} has unsupported version {device.pv}") + raise UnsupportedDeviceError( + f"Device {device.name} has unsupported version {device.pv} {product.model}" + ) dev = RoborockDevice(device, product, channel, trait) if ready_callback: