-
Notifications
You must be signed in to change notification settings - Fork 48
feat: Add an explicit module for caching #432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,4 +3,5 @@ | |
__all__ = [ | ||
"device", | ||
"device_manager", | ||
"cache", | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
"""This module provides caching functionality for the Roborock device management system. | ||
|
||
This module defines a cache interface that you may use to cache device | ||
information to avoid unnecessary API calls. Callers may implement | ||
this interface to provide their own caching mechanism. | ||
""" | ||
|
||
from dataclasses import dataclass, field | ||
from typing import Protocol | ||
|
||
from roborock.containers import HomeData, NetworkInfo | ||
|
||
|
||
@dataclass | ||
class CacheData: | ||
"""Data structure for caching device information.""" | ||
|
||
home_data: HomeData | None = None | ||
"""Home data containing device and product information.""" | ||
|
||
network_info: dict[str, NetworkInfo] = field(default_factory=dict) | ||
"""Network information indexed by device DUID.""" | ||
|
||
|
||
class Cache(Protocol): | ||
"""Protocol for a cache that can store and retrieve values.""" | ||
|
||
async def get(self) -> CacheData: | ||
"""Get cached value.""" | ||
... | ||
|
||
async def set(self, value: CacheData) -> None: | ||
"""Set value in the cache.""" | ||
... | ||
|
||
|
||
class InMemoryCache(Cache): | ||
"""In-memory cache implementation.""" | ||
|
||
def __init__(self): | ||
self._data = CacheData() | ||
|
||
async def get(self) -> CacheData: | ||
return self._data | ||
|
||
async def set(self, value: CacheData) -> None: | ||
self._data = value | ||
|
||
|
||
class NoCache(Cache): | ||
"""No-op cache implementation.""" | ||
|
||
async def get(self) -> CacheData: | ||
return CacheData() | ||
|
||
async def set(self, value: CacheData) -> None: | ||
pass |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ | |
from roborock.protocol import create_mqtt_params | ||
from roborock.web_api import RoborockApiClient | ||
|
||
from .cache import Cache, NoCache | ||
from .channel import Channel | ||
from .mqtt_channel import create_mqtt_channel | ||
from .traits.dyad import DyadApi | ||
|
@@ -32,8 +33,6 @@ | |
"create_device_manager", | ||
"create_home_data_api", | ||
"DeviceManager", | ||
"HomeDataApi", | ||
"DeviceCreator", | ||
] | ||
|
||
|
||
|
@@ -57,19 +56,27 @@ def __init__( | |
home_data_api: HomeDataApi, | ||
device_creator: DeviceCreator, | ||
mqtt_session: MqttSession, | ||
cache: Cache, | ||
) -> None: | ||
"""Initialize the DeviceManager with user data and optional cache storage. | ||
|
||
This takes ownership of the MQTT session and will close it when the manager is closed. | ||
""" | ||
self._home_data_api = home_data_api | ||
self._cache = cache | ||
self._device_creator = device_creator | ||
self._devices: dict[str, RoborockDevice] = {} | ||
self._mqtt_session = mqtt_session | ||
|
||
async def discover_devices(self) -> list[RoborockDevice]: | ||
"""Discover all devices for the logged-in user.""" | ||
home_data = await self._home_data_api() | ||
cache_data = await self._cache.get() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should have a way to force an update There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would like to see if we can address this in the cache implementation e.g. caller can flush their own cache implementation if they don't want caching. If that doesn't work we can add this as an explicit flag here? The reason why i am thinking about this is because we also may need to flush network info as well sometimes, so it seems nice to invalidate the whole cache and try to refresh. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay sounds good! |
||
if not cache_data.home_data: | ||
_LOGGER.debug("No cached home data found, fetching from API") | ||
cache_data.home_data = await self._home_data_api() | ||
await self._cache.set(cache_data) | ||
home_data = cache_data.home_data | ||
|
||
device_products = home_data.device_products | ||
_LOGGER.debug("Discovered %d devices %s", len(device_products), home_data) | ||
|
||
|
@@ -118,13 +125,19 @@ async def home_data_api() -> HomeData: | |
return home_data_api | ||
|
||
|
||
async def create_device_manager(user_data: UserData, home_data_api: HomeDataApi) -> DeviceManager: | ||
async def create_device_manager( | ||
user_data: UserData, | ||
home_data_api: HomeDataApi, | ||
cache: Cache | None = None, | ||
) -> DeviceManager: | ||
"""Convenience function to create and initialize a DeviceManager. | ||
|
||
The Home Data is fetched using the provided home_data_api callable which | ||
is exposed this way to allow for swapping out other implementations to | ||
include caching or other optimizations. | ||
""" | ||
if cache is None: | ||
cache = NoCache() | ||
|
||
mqtt_params = create_mqtt_params(user_data.rriot) | ||
mqtt_session = await create_mqtt_session(mqtt_params) | ||
|
@@ -135,7 +148,7 @@ def device_creator(device: HomeDataDevice, product: HomeDataProduct) -> Roborock | |
# TODO: Define a registration mechanism/factory for v1 traits | ||
match device.pv: | ||
case DeviceVersion.V1: | ||
channel = create_v1_channel(user_data, mqtt_params, mqtt_session, device) | ||
channel = create_v1_channel(user_data, mqtt_params, mqtt_session, device, cache) | ||
traits.append(StatusTrait(product, channel.rpc_channel)) | ||
case DeviceVersion.A01: | ||
mqtt_channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device) | ||
|
@@ -150,6 +163,6 @@ def device_creator(device: HomeDataDevice, product: HomeDataProduct) -> Roborock | |
raise NotImplementedError(f"Device {device.name} has unsupported version {device.pv}") | ||
return RoborockDevice(device, channel, traits) | ||
|
||
manager = DeviceManager(home_data_api, device_creator, mqtt_session=mqtt_session) | ||
manager = DeviceManager(home_data_api, device_creator, mqtt_session=mqtt_session, cache=cache) | ||
await manager.discover_devices() | ||
return manager |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think adding local key would be very helpful here. That theoretically would then have everything you need for local usage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is in
home_data.devices.local_key
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes sorry - was conflating user data in my head - my bad