1- from __future__ import annotations
2-
31import asyncio
42import json
53import logging
4+ from dataclasses import dataclass
65from pathlib import Path
76from typing import Any
87
1211from pyshark .packet .packet import Packet # type: ignore
1312
1413from roborock import RoborockException
15- from roborock .containers import DeviceData , HomeData , HomeDataProduct , LoginData
14+ from roborock .containers import DeviceData , HomeData , HomeDataProduct , LoginData , NetworkInfo , RoborockBase , UserData
1615from roborock .devices .device_manager import create_device_manager , create_home_data_api
1716from roborock .protocol import MessageParser
1817from roborock .util import run_sync
2322_LOGGER = logging .getLogger (__name__ )
2423
2524
25+ @dataclass
26+ class ConnectionCache (RoborockBase ):
27+ """Cache for Roborock data.
28+
29+ This is used to store data retrieved from the Roborock API, such as user
30+ data and home data to avoid repeated API calls.
31+
32+ This cache is superset of `LoginData` since we used to directly store that
33+ dataclass, but now we also store additional data.
34+ """
35+
36+ user_data : UserData
37+ email : str
38+ home_data : HomeData | None = None
39+ network_info : dict [str , NetworkInfo ] | None = None
40+
41+
2642class RoborockContext :
2743 roborock_file = Path ("~/.roborock" ).expanduser ()
28- _login_data : LoginData | None = None
44+ _cache_data : ConnectionCache | None = None
2945
3046 def __init__ (self ):
3147 self .reload ()
@@ -35,22 +51,22 @@ def reload(self):
3551 with open (self .roborock_file ) as f :
3652 data = json .load (f )
3753 if data :
38- self ._login_data = LoginData .from_dict (data )
54+ self ._cache_data = ConnectionCache .from_dict (data )
3955
40- def update (self , login_data : LoginData ):
41- data = json .dumps (login_data .as_dict (), default = vars )
56+ def update (self , cache_data : ConnectionCache ):
57+ data = json .dumps (cache_data .as_dict (), default = vars , indent = 4 )
4258 with open (self .roborock_file , "w" ) as f :
4359 f .write (data )
4460 self .reload ()
4561
4662 def validate (self ):
47- if self ._login_data is None :
63+ if self ._cache_data is None :
4864 raise RoborockException ("You must login first" )
4965
50- def login_data (self ) -> LoginData :
51- """Get the login data."""
66+ def cache_data (self ) -> ConnectionCache :
67+ """Get the cache data."""
5268 self .validate ()
53- return self ._login_data
69+ return self ._cache_data
5470
5571
5672@click .option ("-d" , "--debug" , default = False , count = True )
@@ -99,18 +115,18 @@ async def login(ctx, email, password):
99115@run_sync ()
100116async def session (ctx , duration : int ):
101117 context : RoborockContext = ctx .obj
102- login_data = context .login_data ()
118+ cache_data = context .cache_data ()
103119
104- home_data_api = create_home_data_api (login_data .email , login_data .user_data )
120+ home_data_api = create_home_data_api (cache_data .email , cache_data .user_data )
105121
106122 async def home_data_cache () -> HomeData :
107- if login_data .home_data is None :
108- login_data .home_data = await home_data_api ()
109- context .update (login_data )
110- return login_data .home_data
123+ if cache_data .home_data is None :
124+ cache_data .home_data = await home_data_api ()
125+ context .update (cache_data )
126+ return cache_data .home_data
111127
112128 # Create device manager
113- device_manager = await create_device_manager (login_data .user_data , home_data_cache )
129+ device_manager = await create_device_manager (cache_data .user_data , home_data_cache )
114130
115131 devices = await device_manager .get_devices ()
116132 click .echo (f"Discovered devices: { ', ' .join ([device .name for device in devices ])} " )
@@ -136,16 +152,26 @@ async def home_data_cache() -> HomeData:
136152
137153async def _discover (ctx ):
138154 context : RoborockContext = ctx .obj
139- login_data = context .login_data ()
140- if not login_data :
155+ cache_data = context .cache_data ()
156+ if not cache_data :
141157 raise Exception ("You need to login first" )
142- client = RoborockApiClient (login_data .email )
143- home_data = await client .get_home_data (login_data .user_data )
144- login_data .home_data = home_data
145- context .update (login_data )
158+ client = RoborockApiClient (cache_data .email )
159+ home_data = await client .get_home_data (cache_data .user_data )
160+ cache_data .home_data = home_data
161+ context .update (cache_data )
146162 click .echo (f"Discovered devices { ', ' .join ([device .name for device in home_data .get_all_devices ()])} " )
147163
148164
165+ async def _load_and_discover (ctx ) -> RoborockContext :
166+ """Discover devices if home data is not available."""
167+ context : RoborockContext = ctx .obj
168+ cache_data = context .cache_data ()
169+ if not cache_data .home_data :
170+ await _discover (ctx )
171+ cache_data = context .cache_data ()
172+ return context
173+
174+
149175@click .command ()
150176@click .pass_context
151177@run_sync ()
@@ -157,30 +183,22 @@ async def discover(ctx):
157183@click .pass_context
158184@run_sync ()
159185async def list_devices (ctx ):
160- context : RoborockContext = ctx .obj
161- login_data = context .login_data ()
162- if not login_data .home_data :
163- await _discover (ctx )
164- login_data = context .login_data ()
165- home_data = login_data .home_data
166- device_name_id = ", " .join (
167- [f"{ device .name } : { device .duid } " for device in home_data .devices + home_data .received_devices ]
168- )
169- click .echo (f"Known devices { device_name_id } " )
186+ context : RoborockContext = await _load_and_discover (ctx )
187+ cache_data = context .cache_data ()
188+ home_data = cache_data .home_data
189+ device_name_id = {device .name : device .duid for device in home_data .devices + home_data .received_devices }
190+ click .echo (json .dumps (device_name_id , indent = 4 ))
170191
171192
172193@click .command ()
173194@click .option ("--device_id" , required = True )
174195@click .pass_context
175196@run_sync ()
176197async def list_scenes (ctx , device_id ):
177- context : RoborockContext = ctx .obj
178- login_data = context .login_data ()
179- if not login_data .home_data :
180- await _discover (ctx )
181- login_data = context .login_data ()
182- client = RoborockApiClient (login_data .email )
183- scenes = await client .get_scenes (login_data .user_data , device_id )
198+ context : RoborockContext = await _load_and_discover (ctx )
199+ cache_data = context .cache_data ()
200+ client = RoborockApiClient (cache_data .email )
201+ scenes = await client .get_scenes (cache_data .user_data , device_id )
184202 output_list = []
185203 for scene in scenes :
186204 output_list .append (scene .as_dict ())
@@ -192,32 +210,34 @@ async def list_scenes(ctx, device_id):
192210@click .pass_context
193211@run_sync ()
194212async def execute_scene (ctx , scene_id ):
195- context : RoborockContext = ctx .obj
196- login_data = context .login_data ()
197- if not login_data .home_data :
198- await _discover (ctx )
199- login_data = context .login_data ()
200- client = RoborockApiClient (login_data .email )
201- await client .execute_scene (login_data .user_data , scene_id )
213+ context : RoborockContext = await _load_and_discover (ctx )
214+ cache_data = context .cache_data ()
215+ client = RoborockApiClient (cache_data .email )
216+ await client .execute_scene (cache_data .user_data , scene_id )
202217
203218
204219@click .command ()
205220@click .option ("--device_id" , required = True )
206221@click .pass_context
207222@run_sync ()
208223async def status (ctx , device_id ):
209- context : RoborockContext = ctx .obj
210- login_data = context .login_data ()
211- if not login_data .home_data :
212- await _discover (ctx )
213- login_data = context .login_data ()
214- home_data = login_data .home_data
224+ context : RoborockContext = await _load_and_discover (ctx )
225+ cache_data = context .cache_data ()
226+
227+ home_data = cache_data .home_data
215228 devices = home_data .devices + home_data .received_devices
216229 device = next (device for device in devices if device .duid == device_id )
217230 product_info : dict [str , HomeDataProduct ] = {product .id : product for product in home_data .products }
218231 device_data = DeviceData (device , product_info [device .product_id ].model )
219- mqtt_client = RoborockMqttClientV1 (login_data .user_data , device_data )
220- networking = await mqtt_client .get_networking ()
232+
233+ mqtt_client = RoborockMqttClientV1 (cache_data .user_data , device_data )
234+ if not (networking := cache_data .network_info .get (device .duid )):
235+ networking = await mqtt_client .get_networking ()
236+ cache_data .network_info [device .duid ] = networking
237+ context .update (cache_data )
238+ else :
239+ _LOGGER .debug ("Using cached networking info for device %s: %s" , device .duid , networking )
240+
221241 local_device_data = DeviceData (device , product_info [device .product_id ].model , networking .ip )
222242 local_client = RoborockLocalClientV1 (local_device_data )
223243 status = await local_client .get_status ()
@@ -231,12 +251,10 @@ async def status(ctx, device_id):
231251@click .pass_context
232252@run_sync ()
233253async def command (ctx , cmd , device_id , params ):
234- context : RoborockContext = ctx .obj
235- login_data = context .login_data ()
236- if not login_data .home_data :
237- await _discover (ctx )
238- login_data = context .login_data ()
239- home_data = login_data .home_data
254+ context : RoborockContext = await _load_and_discover (ctx )
255+ cache_data = context .cache_data ()
256+
257+ home_data = cache_data .home_data
240258 devices = home_data .devices + home_data .received_devices
241259 device = next (device for device in devices if device .duid == device_id )
242260 model = next (
@@ -246,7 +264,7 @@ async def command(ctx, cmd, device_id, params):
246264 if model is None :
247265 raise RoborockException (f"Could not find model for device { device .name } " )
248266 device_info = DeviceData (device = device , model = model )
249- mqtt_client = RoborockMqttClientV1 (login_data .user_data , device_info )
267+ mqtt_client = RoborockMqttClientV1 (cache_data .user_data , device_info )
250268 await mqtt_client .send_command (cmd , json .loads (params ) if params is not None else None )
251269 await mqtt_client .async_release ()
252270
0 commit comments