Skip to content

Commit 765447b

Browse files
committed
fix: update DeviceCache interface
1 parent d64f3ed commit 765447b

File tree

13 files changed

+220
-186
lines changed

13 files changed

+220
-186
lines changed

roborock/cli.py

Lines changed: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from pyshark.packet.packet import Packet # type: ignore
4343

4444
from roborock import SHORT_MODEL_TO_ENUM, RoborockCommand
45-
from roborock.data import CombinedMapInfo, DeviceData, HomeData, NetworkInfo, RoborockBase, UserData
45+
from roborock.data import DeviceData, RoborockBase, UserData
4646
from roborock.device_features import DeviceFeatures
4747
from roborock.devices.cache import Cache, CacheData
4848
from roborock.devices.device import RoborockDevice
@@ -116,10 +116,8 @@ class ConnectionCache(RoborockBase):
116116

117117
user_data: UserData
118118
email: str
119-
home_data: HomeData | None = None
120-
network_info: dict[str, NetworkInfo] | None = None
121-
home_map_info: dict[int, CombinedMapInfo] | None = None
122-
trait_data: dict[str, Any] | None = None
119+
# TODO: Used new APIs for cache file storage
120+
cache_data: CacheData | None = None
123121

124122

125123
class DeviceConnectionManager:
@@ -134,10 +132,10 @@ def __init__(self, context: "RoborockContext", loop: asyncio.AbstractEventLoop |
134132
async def ensure_device_manager(self) -> DeviceManager:
135133
"""Ensure device manager is initialized."""
136134
if self.device_manager is None:
137-
cache_data = self.context.cache_data()
135+
connection_cache = self.context.connection_cache()
138136
user_params = UserParams(
139-
username=cache_data.email,
140-
user_data=cache_data.user_data,
137+
username=connection_cache.email,
138+
user_data=connection_cache.user_data,
141139
)
142140
self.device_manager = await create_device_manager(user_params, cache=self.context)
143141
# Cache devices for quick lookup
@@ -164,7 +162,8 @@ class RoborockContext(Cache):
164162
"""Context that handles both CLI and session modes internally."""
165163

166164
roborock_file = Path("~/.roborock").expanduser()
167-
_cache_data: ConnectionCache | None = None
165+
roborock_cache_file = Path("~/.roborock.cache").expanduser()
166+
_connection_cache: ConnectionCache | None = None
168167

169168
def __init__(self):
170169
self.reload()
@@ -177,22 +176,22 @@ def reload(self):
177176
with open(self.roborock_file) as f:
178177
data = json.load(f)
179178
if data:
180-
self._cache_data = ConnectionCache.from_dict(data)
179+
self._connection_cache = ConnectionCache.from_dict(data)
181180

182-
def update(self, cache_data: ConnectionCache):
183-
data = json.dumps(cache_data.as_dict(), default=vars, indent=4)
181+
def update(self, connection_cache: ConnectionCache):
182+
data = json.dumps(connection_cache.as_dict(), default=vars, indent=4)
184183
with open(self.roborock_file, "w") as f:
185184
f.write(data)
186185
self.reload()
187186

188187
def validate(self):
189-
if self._cache_data is None:
188+
if self._connection_cache is None:
190189
raise RoborockException("You must login first")
191190

192-
def cache_data(self) -> ConnectionCache:
191+
def connection_cache(self) -> ConnectionCache:
193192
"""Get the cache data."""
194193
self.validate()
195-
return cast(ConnectionCache, self._cache_data)
194+
return cast(ConnectionCache, self._connection_cache)
196195

197196
def start_session_mode(self):
198197
"""Start session mode with a background event loop."""
@@ -229,19 +228,21 @@ async def get_device_manager(self) -> DeviceConnectionManager:
229228

230229
async def refresh_devices(self) -> ConnectionCache:
231230
"""Refresh device data from server (always fetches fresh data)."""
232-
cache_data = self.cache_data()
233-
client = RoborockApiClient(cache_data.email)
234-
home_data = await client.get_home_data_v3(cache_data.user_data)
235-
cache_data.home_data = home_data
236-
self.update(cache_data)
237-
return cache_data
231+
connection_cache = self.connection_cache()
232+
client = RoborockApiClient(connection_cache.email)
233+
home_data = await client.get_home_data_v3(connection_cache.user_data)
234+
if connection_cache.cache_data is None:
235+
connection_cache.cache_data = CacheData()
236+
connection_cache.cache_data.home_data = home_data
237+
self.update(connection_cache)
238+
return connection_cache
238239

239240
async def get_devices(self) -> ConnectionCache:
240241
"""Get device data (uses cache if available, fetches if needed)."""
241-
cache_data = self.cache_data()
242-
if not cache_data.home_data:
243-
cache_data = await self.refresh_devices()
244-
return cache_data
242+
connection_cache = self.connection_cache()
243+
if (connection_cache.cache_data is None) or (connection_cache.cache_data.home_data is None):
244+
connection_cache = await self.refresh_devices()
245+
return connection_cache
245246

246247
async def cleanup(self):
247248
"""Clean up resources (mainly for session mode)."""
@@ -266,22 +267,16 @@ def finish_session(self) -> None:
266267
async def get(self) -> CacheData:
267268
"""Get cached value."""
268269
_LOGGER.debug("Getting cache data")
269-
connection_cache = self.cache_data()
270-
return CacheData(
271-
home_data=connection_cache.home_data,
272-
network_info=connection_cache.network_info or {},
273-
home_map_info=connection_cache.home_map_info,
274-
trait_data=connection_cache.trait_data or {},
275-
)
270+
connection_cache = self.connection_cache()
271+
if connection_cache.cache_data is not None:
272+
return connection_cache.cache_data
273+
return CacheData()
276274

277275
async def set(self, value: CacheData) -> None:
278276
"""Set value in the cache."""
279277
_LOGGER.debug("Setting cache data")
280-
connection_cache = self.cache_data()
281-
connection_cache.home_data = value.home_data
282-
connection_cache.network_info = value.network_info
283-
connection_cache.home_map_info = value.home_map_info
284-
connection_cache.trait_data = value.trait_data
278+
connection_cache = self.connection_cache()
279+
connection_cache.cache_data = value
285280
self.update(connection_cache)
286281

287282

@@ -367,9 +362,9 @@ async def discover(ctx):
367362
@async_command
368363
async def list_devices(ctx):
369364
context: RoborockContext = ctx.obj
370-
cache_data = await context.get_devices()
365+
connection_cache = await context.get_devices()
371366

372-
home_data = cache_data.home_data
367+
home_data = connection_cache.cache_data.home_data
373368

374369
device_name_id = {device.name: device.duid for device in home_data.get_all_devices()}
375370
click.echo(json.dumps(device_name_id, indent=4))

roborock/devices/cache.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ class DeviceCacheData(RoborockBase):
2020
network_info: NetworkInfo | None = None
2121
"""Network information for the device"""
2222

23-
home_map_info: CombinedMapInfo | None = None
24-
"""Home map information for the device."""
23+
home_map_info: dict[int, CombinedMapInfo] | None = None
24+
"""Home map information for the device by map_flag."""
2525

26-
home_map_content_base64: str | None = None
27-
"""Home cache content for the device (encoded base64)."""
26+
home_map_content_base64: dict[int, str] | None = None
27+
"""Home cache content for the device (encoded base64) by map_flag."""
2828

2929
device_features: DeviceFeatures | None = None
3030
"""Device features information."""
@@ -91,19 +91,33 @@ async def set(self, value: CacheData) -> None:
9191
"""Set value in the cache."""
9292
...
9393

94-
async def get_device_info(self, duid: str) -> DeviceCacheData:
94+
95+
@dataclass
96+
class DeviceCache(RoborockBase):
97+
"""Provides a cache interface for a specific device.
98+
99+
This is a convenience wrapper around a general Cache implementation to
100+
provide device-specific caching functionality.
101+
"""
102+
103+
def __init__(self, duid: str, cache: Cache) -> None:
104+
"""Initialize the device cache with the given cache implementation."""
105+
self._duid = duid
106+
self._cache = cache
107+
108+
async def get(self) -> DeviceCacheData:
95109
"""Get cached device-specific information."""
96-
cache_data = await self.get()
97-
if duid not in cache_data.device_info:
98-
cache_data.device_info[duid] = DeviceCacheData()
99-
await self.set(cache_data)
100-
return cache_data.device_info[duid]
110+
cache_data = await self._cache.get()
111+
if self._duid not in cache_data.device_info:
112+
cache_data.device_info[self._duid] = DeviceCacheData()
113+
await self._cache.set(cache_data)
114+
return cache_data.device_info[self._duid]
101115

102-
async def set_device_info(self, duid: str, device_cache_data: DeviceCacheData) -> None:
116+
async def set(self, device_cache_data: DeviceCacheData) -> None:
103117
"""Set cached device-specific information."""
104-
cache_data = await self.get()
105-
cache_data.device_info[duid] = device_cache_data
106-
await self.set(cache_data)
118+
cache_data = await self._cache.get()
119+
cache_data.device_info[self._duid] = device_cache_data
120+
await self._cache.set(cache_data)
107121

108122

109123
class InMemoryCache(Cache):

roborock/devices/device_manager.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from roborock.protocol import create_mqtt_params
2222
from roborock.web_api import RoborockApiClient, UserWebApiClient
2323

24-
from .cache import Cache, NoCache
24+
from .cache import Cache, DeviceCache, NoCache
2525
from .channel import Channel
2626
from .mqtt_channel import create_mqtt_channel
2727
from .traits import Trait, a01, b01, v1
@@ -177,9 +177,10 @@ async def create_device_manager(
177177
def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDataProduct) -> RoborockDevice:
178178
channel: Channel
179179
trait: Trait
180+
device_cache: DeviceCache = DeviceCache(device.duid, cache)
180181
match device.pv:
181182
case DeviceVersion.V1:
182-
channel = create_v1_channel(user_data, mqtt_params, mqtt_session, device, cache)
183+
channel = create_v1_channel(user_data, mqtt_params, mqtt_session, device, device_cache)
183184
trait = v1.create(
184185
device.duid,
185186
product,
@@ -188,7 +189,7 @@ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDat
188189
channel.mqtt_rpc_channel,
189190
channel.map_rpc_channel,
190191
web_api,
191-
cache,
192+
device_cache=device_cache,
192193
map_parser_config=map_parser_config,
193194
)
194195
case DeviceVersion.A01:

roborock/devices/traits/v1/__init__.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@
3232

3333
import logging
3434
from dataclasses import dataclass, field, fields
35+
from functools import cache
3536
from typing import Any, get_args
3637

3738
from roborock.data.containers import HomeData, HomeDataProduct, RoborockBase
3839
from roborock.data.v1.v1_code_mappings import RoborockDockTypeCode
39-
from roborock.devices.cache import Cache
40+
from roborock.devices.cache import DeviceCache
4041
from roborock.devices.traits import Trait
4142
from roborock.map.map_parser import MapParserConfig
4243
from roborock.protocols.v1_protocol import V1RpcChannel
@@ -131,7 +132,7 @@ def __init__(
131132
mqtt_rpc_channel: V1RpcChannel,
132133
map_rpc_channel: V1RpcChannel,
133134
web_api: UserWebApiClient,
134-
cache: Cache,
135+
device_cache: DeviceCache,
135136
map_parser_config: MapParserConfig | None = None,
136137
) -> None:
137138
"""Initialize the V1TraitProps."""
@@ -140,16 +141,16 @@ def __init__(
140141
self._mqtt_rpc_channel = mqtt_rpc_channel
141142
self._map_rpc_channel = map_rpc_channel
142143
self._web_api = web_api
143-
self._cache = cache
144+
self._device_cache = device_cache
144145

145146
self.status = StatusTrait(product)
146147
self.consumables = ConsumableTrait()
147148
self.rooms = RoomsTrait(home_data)
148149
self.maps = MapsTrait(self.status)
149150
self.map_content = MapContentTrait(map_parser_config)
150-
self.home = HomeTrait(self.status, self.maps, self.map_content, self.rooms, cache)
151-
self.device_features = DeviceFeaturesTrait(product.product_nickname, cache)
152-
self.network_info = NetworkInfoTrait(device_uid, cache)
151+
self.home = HomeTrait(self.status, self.maps, self.map_content, self.rooms, self._device_cache)
152+
self.device_features = DeviceFeaturesTrait(product.product_nickname, self._device_cache)
153+
self.network_info = NetworkInfoTrait(device_uid, self._device_cache)
153154
self.routines = RoutinesTrait(device_uid, web_api)
154155

155156
# Dynamically create any traits that need to be populated
@@ -239,20 +240,20 @@ async def _dock_type(self) -> RoborockDockTypeCode:
239240

240241
async def _get_cached_trait_data(self, name: str) -> Any:
241242
"""Get the dock type from the status trait or cache."""
242-
cache_data = await self._cache.get()
243+
cache_data = await self._device_cache.get()
243244
if cache_data.trait_data is None:
244245
cache_data.trait_data = {}
245246
_LOGGER.debug("Cached trait data: %s", cache_data.trait_data)
246247
return cache_data.trait_data.get(name)
247248

248249
async def _set_cached_trait_data(self, name: str, value: Any) -> None:
249250
"""Set trait-specific cached data."""
250-
cache_data = await self._cache.get()
251+
cache_data = await self._device_cache.get()
251252
if cache_data.trait_data is None:
252253
cache_data.trait_data = {}
253254
cache_data.trait_data[name] = value
254255
_LOGGER.debug("Updating cached trait data: %s", cache_data.trait_data)
255-
await self._cache.set(cache_data)
256+
await self._device_cache.set(cache_data)
256257

257258
def as_dict(self) -> dict[str, Any]:
258259
"""Return the trait data as a dictionary."""
@@ -275,7 +276,7 @@ def create(
275276
mqtt_rpc_channel: V1RpcChannel,
276277
map_rpc_channel: V1RpcChannel,
277278
web_api: UserWebApiClient,
278-
cache: Cache,
279+
device_cache: DeviceCache,
279280
map_parser_config: MapParserConfig | None = None,
280281
) -> PropertiesApi:
281282
"""Create traits for V1 devices."""
@@ -287,6 +288,6 @@ def create(
287288
mqtt_rpc_channel,
288289
map_rpc_channel,
289290
web_api,
290-
cache,
291+
device_cache,
291292
map_parser_config,
292293
)

roborock/devices/traits/v1/device_features.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from roborock.data import AppInitStatus, RoborockProductNickname
44
from roborock.device_features import DeviceFeatures
5-
from roborock.devices.cache import Cache
5+
from roborock.devices.cache import DeviceCache
66
from roborock.devices.traits.v1 import common
77
from roborock.roborock_typing import RoborockCommand
88

@@ -12,10 +12,10 @@ class DeviceFeaturesTrait(DeviceFeatures, common.V1TraitMixin):
1212

1313
command = RoborockCommand.APP_GET_INIT_STATUS
1414

15-
def __init__(self, product_nickname: RoborockProductNickname, cache: Cache) -> None: # pylint: disable=super-init-not-called
15+
def __init__(self, product_nickname: RoborockProductNickname, device_cache: DeviceCache) -> None: # pylint: disable=super-init-not-called
1616
"""Initialize MapContentTrait."""
1717
self._nickname = product_nickname
18-
self._cache = cache
18+
self._device_cache = device_cache
1919
# All fields of DeviceFeatures are required. Initialize them to False
2020
# so we have some known state.
2121
for field in fields(self):
@@ -28,14 +28,14 @@ async def refresh(self) -> None:
2828
change often and this avoids unnecessary RPC calls. This would only
2929
ever change with a firmware update, so caching is appropriate.
3030
"""
31-
cache_data = await self._cache.get()
31+
cache_data = await self._device_cache.get()
3232
if cache_data.device_features is not None:
3333
self._update_trait_values(cache_data.device_features)
3434
return
3535
# Save cached device features
3636
await super().refresh()
3737
cache_data.device_features = self
38-
await self._cache.set(cache_data)
38+
await self._device_cache.set(cache_data)
3939

4040
def _parse_response(self, response: common.V1ResponseData) -> DeviceFeatures:
4141
"""Parse the response from the device into a MapContentTrait instance."""

0 commit comments

Comments
 (0)