1
- from __future__ import annotations
2
-
3
1
import asyncio
4
2
import json
5
3
import logging
6
4
from pathlib import Path
7
5
from typing import Any
6
+ from dataclasses import dataclass , field
8
7
9
8
import click
10
9
from pyshark import FileCapture # type: ignore
11
10
from pyshark .capture .live_capture import LiveCapture , UnknownInterfaceException # type: ignore
12
11
from pyshark .packet .packet import Packet # type: ignore
13
12
14
13
from roborock import RoborockException
15
- from roborock .containers import DeviceData , HomeData , HomeDataProduct , LoginData
14
+ from roborock .containers import DeviceData , HomeData , HomeDataProduct , LoginData , NetworkInfo , UserData , RoborockBase
16
15
from roborock .devices .device_manager import create_device_manager , create_home_data_api
17
16
from roborock .protocol import MessageParser
18
17
from roborock .util import run_sync
23
22
_LOGGER = logging .getLogger (__name__ )
24
23
25
24
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
+
26
42
class RoborockContext :
27
43
roborock_file = Path ("~/.roborock" ).expanduser ()
28
- _login_data : LoginData | None = None
44
+ _cache_data : ConnectionCache | None = None
29
45
30
46
def __init__ (self ):
31
47
self .reload ()
@@ -35,22 +51,22 @@ def reload(self):
35
51
with open (self .roborock_file ) as f :
36
52
data = json .load (f )
37
53
if data :
38
- self ._login_data = LoginData .from_dict (data )
54
+ self ._cache_data = ConnectionCache .from_dict (data )
39
55
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 )
42
58
with open (self .roborock_file , "w" ) as f :
43
59
f .write (data )
44
60
self .reload ()
45
61
46
62
def validate (self ):
47
- if self ._login_data is None :
63
+ if self ._cache_data is None :
48
64
raise RoborockException ("You must login first" )
49
65
50
- def login_data (self ) -> LoginData :
51
- """Get the login data."""
66
+ def cache_data (self ) -> ConnectionCache :
67
+ """Get the cache data."""
52
68
self .validate ()
53
- return self ._login_data
69
+ return self ._cache_data
54
70
55
71
56
72
@click .option ("-d" , "--debug" , default = False , count = True )
@@ -99,18 +115,18 @@ async def login(ctx, email, password):
99
115
@run_sync ()
100
116
async def session (ctx , duration : int ):
101
117
context : RoborockContext = ctx .obj
102
- login_data = context .login_data ()
118
+ cache_data = context .cache_data ()
103
119
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 )
105
121
106
122
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
111
127
112
128
# 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 )
114
130
115
131
devices = await device_manager .get_devices ()
116
132
click .echo (f"Discovered devices: { ', ' .join ([device .name for device in devices ])} " )
@@ -136,16 +152,27 @@ async def home_data_cache() -> HomeData:
136
152
137
153
async def _discover (ctx ):
138
154
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 :
141
157
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 )
146
162
click .echo (f"Discovered devices { ', ' .join ([device .name for device in home_data .get_all_devices ()])} " )
147
163
148
164
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
+
175
+
149
176
@click .command ()
150
177
@click .pass_context
151
178
@run_sync ()
@@ -157,30 +184,25 @@ async def discover(ctx):
157
184
@click .pass_context
158
185
@run_sync ()
159
186
async 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 } " )
187
+ context : RoborockContext = await _load_and_discover (ctx )
188
+ cache_data = context .cache_data ()
189
+ home_data = cache_data .home_data
190
+ device_name_id = {
191
+ device .name : device .duid
192
+ for device in home_data .devices + home_data .received_devices
193
+ }
194
+ click .echo (json .dumps (device_name_id , indent = 4 ))
170
195
171
196
172
197
@click .command ()
173
198
@click .option ("--device_id" , required = True )
174
199
@click .pass_context
175
200
@run_sync ()
176
201
async 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 )
202
+ context : RoborockContext = await _load_and_discover (ctx )
203
+ cache_data = context .cache_data ()
204
+ client = RoborockApiClient (cache_data .email )
205
+ scenes = await client .get_scenes (cache_data .user_data , device_id )
184
206
output_list = []
185
207
for scene in scenes :
186
208
output_list .append (scene .as_dict ())
@@ -192,32 +214,34 @@ async def list_scenes(ctx, device_id):
192
214
@click .pass_context
193
215
@run_sync ()
194
216
async 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 )
217
+ context : RoborockContext = await _load_and_discover (ctx )
218
+ cache_data = context .cache_data ()
219
+ client = RoborockApiClient (cache_data .email )
220
+ await client .execute_scene (cache_data .user_data , scene_id )
202
221
203
222
204
223
@click .command ()
205
224
@click .option ("--device_id" , required = True )
206
225
@click .pass_context
207
226
@run_sync ()
208
227
async 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
228
+ context : RoborockContext = await _load_and_discover (ctx )
229
+ cache_data = context .cache_data ()
230
+
231
+ home_data = cache_data .home_data
215
232
devices = home_data .devices + home_data .received_devices
216
233
device = next (device for device in devices if device .duid == device_id )
217
234
product_info : dict [str , HomeDataProduct ] = {product .id : product for product in home_data .products }
218
235
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 ()
236
+
237
+ mqtt_client = RoborockMqttClientV1 (cache_data .user_data , device_data )
238
+ if not (networking := cache_data .network_info .get (device .duid )):
239
+ networking = await mqtt_client .get_networking ()
240
+ cache_data .network_info [device .duid ] = networking
241
+ context .update (cache_data )
242
+ else :
243
+ _LOGGER .debug ("Using cached networking info for device %s: %s" , device .duid , networking )
244
+
221
245
local_device_data = DeviceData (device , product_info [device .product_id ].model , networking .ip )
222
246
local_client = RoborockLocalClientV1 (local_device_data )
223
247
status = await local_client .get_status ()
@@ -231,12 +255,10 @@ async def status(ctx, device_id):
231
255
@click .pass_context
232
256
@run_sync ()
233
257
async 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
258
+ context : RoborockContext = await _load_and_discover (ctx )
259
+ cache_data = context .cache_data ()
260
+
261
+ home_data = cache_data .home_data
240
262
devices = home_data .devices + home_data .received_devices
241
263
device = next (device for device in devices if device .duid == device_id )
242
264
model = next (
@@ -246,7 +268,7 @@ async def command(ctx, cmd, device_id, params):
246
268
if model is None :
247
269
raise RoborockException (f"Could not find model for device { device .name } " )
248
270
device_info = DeviceData (device = device , model = model )
249
- mqtt_client = RoborockMqttClientV1 (login_data .user_data , device_info )
271
+ mqtt_client = RoborockMqttClientV1 (cache_data .user_data , device_info )
250
272
await mqtt_client .send_command (cmd , json .loads (params ) if params is not None else None )
251
273
await mqtt_client .async_release ()
252
274
0 commit comments