From c843ab6c7c3af431360c83d71c96e5b36c38c609 Mon Sep 17 00:00:00 2001 From: Ciaran Gultnieks Date: Wed, 31 Dec 2025 16:24:08 +0000 Subject: [PATCH 1/2] docs: Fix example to clean up before cache flush The example was missing device_manager.close() before cache.flush(), causing it to hang indefinitely waiting for background tasks (MQTT connections, local reconnection loops, health managers) to complete. Added try/finally block to ensure cleanup always happens. --- examples/example.py | 49 +++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/examples/example.py b/examples/example.py index 72de5ca1..06c24e85 100644 --- a/examples/example.py +++ b/examples/example.py @@ -56,29 +56,34 @@ async def main(): # Create a device manager that can discover devices. device_manager = await create_device_manager(user_params, cache=cache) - devices = await device_manager.get_devices() - # Get all vacuum devices that support the v1 PropertiesApi - device_results = [] - for device in devices: - if not device.v1_properties: - continue - - # Refresh the current device status - status_trait = device.v1_properties.status - await status_trait.refresh() - - # Print the device status as JSON - device_results.append( - { - "device": device.name, - "status": remove_none_values(dataclasses.asdict(status_trait)), - } - ) - - print(json.dumps(device_results, indent=2)) - - await cache.flush() + try: + devices = await device_manager.get_devices() + + # Get all vacuum devices that support the v1 PropertiesApi + device_results = [] + for device in devices: + if not device.v1_properties: + continue + + # Refresh the current device status + status_trait = device.v1_properties.status + await status_trait.refresh() + + # Print the device status as JSON + device_results.append( + { + "device": device.name, + "status": remove_none_values(dataclasses.asdict(status_trait)), + } + ) + + print(json.dumps(device_results, indent=2)) + + finally: + # Close device manager to cancel background tasks before flushing cache + await device_manager.close() + await cache.flush() if __name__ == "__main__": From 3f349dc9f1e12b3c2ce460e1ddfe0a55dc67bb83 Mon Sep 17 00:00:00 2001 From: Ciaran Gultnieks Date: Wed, 31 Dec 2025 16:25:58 +0000 Subject: [PATCH 2/2] fix: Prevent caching unpicklable trait objects DeviceFeaturesTrait and NetworkInfoTrait were storing entire trait objects (including unpicklable _rpc_channel) in the cache, causing pickle errors during cache.flush(). Now creates pure data-only instances for caching, excluding runtime objects like _rpc_channel, _device_cache, and _nickname. Fixes: AttributeError when flushing cache - "Can't pickle local object 'V1Channel.rpc_channel..rpc_strategies_cb'" --- roborock/devices/traits/v1/device_features.py | 6 +++++- roborock/devices/traits/v1/network_info.py | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/roborock/devices/traits/v1/device_features.py b/roborock/devices/traits/v1/device_features.py index c4383d01..044f662d 100644 --- a/roborock/devices/traits/v1/device_features.py +++ b/roborock/devices/traits/v1/device_features.py @@ -34,7 +34,11 @@ async def refresh(self) -> None: return # Save cached device features await super().refresh() - cache_data.device_features = self + # Create a pure DeviceFeatures instance without runtime objects + device_features_data = DeviceFeatures() + for field in fields(DeviceFeatures): + setattr(device_features_data, field.name, getattr(self, field.name)) + cache_data.device_features = device_features_data await self._device_cache.set(cache_data) def _parse_response(self, response: common.V1ResponseData) -> DeviceFeatures: diff --git a/roborock/devices/traits/v1/network_info.py b/roborock/devices/traits/v1/network_info.py index a88394cd..57d0b89f 100644 --- a/roborock/devices/traits/v1/network_info.py +++ b/roborock/devices/traits/v1/network_info.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from dataclasses import fields from roborock.data import NetworkInfo from roborock.devices.cache import DeviceCache @@ -45,7 +46,11 @@ async def refresh(self) -> None: # Update the cache with the new network info device_cache_data = await self._device_cache.get() - device_cache_data.network_info = self + # Create a pure NetworkInfo instance without runtime objects + network_info_data = NetworkInfo(ip="") + for field in fields(NetworkInfo): + setattr(network_info_data, field.name, getattr(self, field.name)) + device_cache_data.network_info = network_info_data await self._device_cache.set(device_cache_data) def _parse_response(self, response: common.V1ResponseData) -> NetworkInfo: