pyvesync 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. pyvesync/__init__.py +5 -0
  2. pyvesync/base_devices/__init__.py +35 -0
  3. pyvesync/base_devices/bulb_base.py +330 -0
  4. pyvesync/base_devices/fan_base.py +392 -0
  5. pyvesync/base_devices/fryer_base.py +68 -0
  6. pyvesync/base_devices/humidifier_base.py +462 -0
  7. pyvesync/base_devices/outlet_base.py +327 -0
  8. pyvesync/base_devices/purifier_base.py +525 -0
  9. pyvesync/base_devices/switch_base.py +271 -0
  10. pyvesync/base_devices/thermostat_base.py +283 -0
  11. pyvesync/base_devices/vesyncbasedevice.py +572 -0
  12. pyvesync/const.py +825 -0
  13. pyvesync/device_container.py +373 -0
  14. pyvesync/device_map.py +1172 -0
  15. pyvesync/devices/__init__.py +1 -0
  16. pyvesync/devices/vesyncbulb.py +1126 -0
  17. pyvesync/devices/vesyncfan.py +233 -0
  18. pyvesync/devices/vesynchumidifier.py +808 -0
  19. pyvesync/devices/vesynckitchen.py +651 -0
  20. pyvesync/devices/vesyncoutlet.py +954 -0
  21. pyvesync/devices/vesyncpurifier.py +990 -0
  22. pyvesync/devices/vesyncswitch.py +415 -0
  23. pyvesync/devices/vesyncthermostat.py +171 -0
  24. pyvesync/models/__init__.py +11 -0
  25. pyvesync/models/base_models.py +129 -0
  26. pyvesync/models/bulb_models.py +197 -0
  27. pyvesync/models/bypass_models.py +289 -0
  28. pyvesync/models/fan_models.py +47 -0
  29. pyvesync/models/fryer_models.py +34 -0
  30. pyvesync/models/home_models.py +169 -0
  31. pyvesync/models/humidifier_models.py +202 -0
  32. pyvesync/models/outlet_models.py +220 -0
  33. pyvesync/models/purifier_models.py +261 -0
  34. pyvesync/models/switch_models.py +101 -0
  35. pyvesync/models/thermostat_models.py +134 -0
  36. pyvesync/models/vesync_models.py +439 -0
  37. pyvesync/utils/__init__.py +1 -0
  38. pyvesync/utils/colors.py +250 -0
  39. pyvesync/utils/device_mixins.py +362 -0
  40. pyvesync/utils/enum_utils.py +26 -0
  41. pyvesync/utils/errors.py +867 -0
  42. pyvesync/utils/helpers.py +700 -0
  43. pyvesync/utils/logs.py +525 -0
  44. pyvesync/vesync.py +638 -0
  45. pyvesync/vesynchome.py +152 -0
  46. pyvesync-3.0.0.dist-info/METADATA +567 -0
  47. pyvesync-3.0.0.dist-info/RECORD +50 -0
  48. pyvesync-3.0.0.dist-info/WHEEL +5 -0
  49. pyvesync-3.0.0.dist-info/licenses/LICENSE +21 -0
  50. pyvesync-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,651 @@
1
+ """VeSync Kitchen Devices.
2
+
3
+ The Cosori 3.7 and 5.8 Quart Air Fryer has several methods and properties that can be
4
+ used to monitor and control the device.
5
+
6
+ To maintain consistency of state, the update() method is called after each of the methods
7
+ that change the state of the device.
8
+
9
+ There is also an instance attribute that can be set `VeSyncAirFryer158.refresh_interval`
10
+ that will set the interval in seconds that the state of the air fryer should be updated
11
+ before a method that changes state is called. This is an additional API call but is
12
+ necessary to maintain state, especially when trying to `pause` or `resume` the device.
13
+ Defaults to 60 seconds but can be set via:
14
+
15
+ ```python
16
+ # Change to 120 seconds before status is updated between calls
17
+ VeSyncAirFryer158.refresh_interval = 120
18
+
19
+ # Set status update before every call
20
+ VeSyncAirFryer158.refresh_interval = 0
21
+
22
+ # Disable status update before every call
23
+ VeSyncAirFryer158.refresh_interval = -1
24
+ ```
25
+
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import logging
31
+ import time
32
+ from typing import TYPE_CHECKING, TypeVar
33
+
34
+ from typing_extensions import deprecated
35
+
36
+ from pyvesync.base_devices import FryerState, VeSyncFryer
37
+ from pyvesync.const import AIRFRYER_PID_MAP, ConnectionStatus, DeviceStatus
38
+ from pyvesync.models.base_models import DefaultValues
39
+ from pyvesync.utils.errors import VeSyncError
40
+ from pyvesync.utils.helpers import Helpers
41
+ from pyvesync.utils.logs import LibraryLogger
42
+
43
+ if TYPE_CHECKING:
44
+ from pyvesync import VeSync
45
+ from pyvesync.device_map import AirFryerMap
46
+ from pyvesync.models.vesync_models import ResponseDeviceDetailsModel
47
+
48
+ T = TypeVar('T')
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+
53
+ # Status refresh interval in seconds
54
+ # API calls outside of interval are automatically refreshed
55
+ # Set VeSyncAirFryer158.refresh_interval to 0 to refresh every call
56
+ # Set to None or -1 to disable auto-refresh
57
+ REFRESH_INTERVAL = 60
58
+
59
+ RECIPE_ID = 1
60
+ RECIPE_TYPE = 3
61
+ CUSTOM_RECIPE = 'Manual Cook'
62
+ COOK_MODE = 'custom'
63
+
64
+
65
+ class AirFryer158138State(FryerState):
66
+ """Dataclass for air fryer status.
67
+
68
+ Attributes:
69
+ active_time (int): Active time of device, defaults to None.
70
+ connection_status (str): Connection status of device.
71
+ device (VeSyncBaseDevice): Device object.
72
+ device_status (str): Device status.
73
+ features (dict): Features of device.
74
+ last_update_ts (int): Last update timestamp of device, defaults to None.
75
+ ready_start (bool): Ready start status of device, defaults to False.
76
+ preheat (bool): Preheat status of device, defaults to False.
77
+ cook_status (str): Cooking status of device, defaults to None.
78
+ current_temp (int): Current temperature of device, defaults to None.
79
+ cook_set_temp (int): Cooking set temperature of device, defaults to None.
80
+ last_timestamp (int): Last timestamp of device, defaults to None.
81
+ preheat_set_time (int): Preheat set time of device, defaults to None.
82
+ preheat_last_time (int): Preheat last time of device, defaults to None.
83
+ _temp_unit (str): Temperature unit of device, defaults to None.
84
+ """
85
+
86
+ __slots__ = (
87
+ '_temp_unit',
88
+ 'cook_last_time',
89
+ 'cook_set_temp',
90
+ 'cook_set_time',
91
+ 'cook_status',
92
+ 'current_temp',
93
+ 'last_timestamp',
94
+ 'max_temp_c',
95
+ 'max_temp_f',
96
+ 'min_temp_c',
97
+ 'min_temp_f',
98
+ 'preheat',
99
+ 'preheat_last_time',
100
+ 'preheat_set_time',
101
+ 'ready_start',
102
+ )
103
+
104
+ def __init__(
105
+ self,
106
+ device: VeSyncAirFryer158,
107
+ details: ResponseDeviceDetailsModel,
108
+ feature_map: AirFryerMap,
109
+ ) -> None:
110
+ """Init the Air Fryer 158 class."""
111
+ super().__init__(device, details, feature_map)
112
+ self.device: VeSyncFryer = device
113
+ self.features: list[str] = feature_map.features
114
+ self.min_temp_f: int = feature_map.temperature_range_f[0]
115
+ self.max_temp_f: int = feature_map.temperature_range_f[1]
116
+ self.min_temp_c: int = feature_map.temperature_range_c[0]
117
+ self.max_temp_c: int = feature_map.temperature_range_c[1]
118
+ self.ready_start: bool = False
119
+ self.preheat: bool = False
120
+ self.cook_status: str | None = None
121
+ self.current_temp: int | None = None
122
+ self.cook_set_temp: int | None = None
123
+ self.cook_set_time: int | None = None
124
+ self.cook_last_time: int | None = None
125
+ self.last_timestamp: int | None = None
126
+ self.preheat_set_time: int | None = None
127
+ self.preheat_last_time: int | None = None
128
+ self._temp_unit: str | None = None
129
+
130
+ @property
131
+ def is_resumable(self) -> bool:
132
+ """Return if cook is resumable."""
133
+ if self.cook_status in ['cookStop', 'preheatStop']:
134
+ if self.cook_set_time is not None:
135
+ return self.cook_set_time > 0
136
+ if self.preheat_set_time is not None:
137
+ return self.preheat_set_time > 0
138
+ return False
139
+
140
+ @property
141
+ def temp_unit(self) -> str | None:
142
+ """Return temperature unit."""
143
+ return self._temp_unit
144
+
145
+ @temp_unit.setter
146
+ def temp_unit(self, temp_unit: str) -> None:
147
+ """Set temperature unit."""
148
+ if temp_unit.lower() in ['f', 'fahrenheit', 'fahrenheight']: # API TYPO
149
+ self._temp_unit = 'fahrenheit'
150
+ elif temp_unit.lower() in ['c', 'celsius']:
151
+ self._temp_unit = 'celsius'
152
+ else:
153
+ msg = f'Invalid temperature unit - {temp_unit}'
154
+ raise ValueError(msg)
155
+
156
+ @property
157
+ def preheat_time_remaining(self) -> int:
158
+ """Return preheat time remaining."""
159
+ if self.preheat is False or self.cook_status == 'preheatEnd':
160
+ return 0
161
+ if self.cook_status in ['pullOut', 'preheatStop']:
162
+ if self.preheat_last_time is None:
163
+ return 0
164
+ return int(self.preheat_last_time // 60)
165
+ if self.preheat_last_time is not None and self.last_timestamp is not None:
166
+ return int(
167
+ max(
168
+ (self.preheat_last_time - (int(time.time()) - self.last_timestamp))
169
+ // 60,
170
+ 0,
171
+ )
172
+ )
173
+ return 0
174
+
175
+ @property
176
+ def cook_time_remaining(self) -> int:
177
+ """Returns the amount of time remaining if cooking."""
178
+ if self.preheat is True or self.cook_status == 'cookEnd':
179
+ return 0
180
+ if self.cook_status in ['pullOut', 'cookStop']:
181
+ if self.cook_last_time is None:
182
+ return 0
183
+ return int(max(self.cook_last_time // 60, 0))
184
+ if self.cook_last_time is not None and self.last_timestamp is not None:
185
+ return int(
186
+ max(
187
+ (self.cook_last_time - (int(time.time()) - self.last_timestamp))
188
+ // 60,
189
+ 0,
190
+ )
191
+ )
192
+ return 0
193
+
194
+ @property
195
+ def remaining_time(self) -> int:
196
+ """Return minutes remaining if cooking/heating."""
197
+ if self.preheat is True:
198
+ return self.preheat_time_remaining
199
+ return self.cook_time_remaining
200
+
201
+ @property
202
+ def is_running(self) -> bool:
203
+ """Return if cooking or heating."""
204
+ return bool(self.cook_status in ['cooking', 'heating']) and bool(
205
+ self.remaining_time > 0
206
+ )
207
+
208
+ @property
209
+ def is_cooking(self) -> bool:
210
+ """Return if cooking."""
211
+ return self.cook_status == 'cooking' and self.remaining_time > 0
212
+
213
+ @property
214
+ def is_heating(self) -> bool:
215
+ """Return if heating."""
216
+ return self.cook_status == 'heating' and self.remaining_time > 0
217
+
218
+ def status_request(self, json_cmd: dict) -> None: # noqa: C901
219
+ """Set status from jsonCmd of API call."""
220
+ self.last_timestamp = None
221
+ if not isinstance(json_cmd, dict):
222
+ return
223
+ self.preheat = False
224
+ preheat = json_cmd.get('preheat')
225
+ cook = json_cmd.get('cookMode')
226
+ if isinstance(preheat, dict):
227
+ self.preheat = True
228
+ if preheat.get('preheatStatus') == 'stop':
229
+ self.cook_status = 'preheatStop'
230
+ elif preheat.get('preheatStatus') == 'heating':
231
+ self.cook_status = 'heating'
232
+ self.last_timestamp = int(time.time())
233
+ self.preheat_set_time = preheat.get(
234
+ 'preheatSetTime', self.preheat_set_time
235
+ )
236
+ if preheat.get('preheatSetTime') is not None:
237
+ self.preheat_last_time = preheat.get('preheatSetTime')
238
+ self.cook_set_temp = preheat.get('targetTemp', self.cook_set_temp)
239
+ self.cook_set_time = preheat.get('cookSetTime', self.cook_set_time)
240
+ self.cook_last_time = None
241
+ elif preheat.get('preheatStatus') == 'end':
242
+ self.cook_status = 'preheatEnd'
243
+ self.preheat_last_time = 0
244
+ elif isinstance(cook, dict):
245
+ self.clear_preheat()
246
+ if cook.get('cookStatus') == 'stop':
247
+ self.cook_status = 'cookStop'
248
+ elif cook.get('cookStatus') == 'cooking':
249
+ self.cook_status = 'cooking'
250
+ self.last_timestamp = int(time.time())
251
+ self.cook_set_time = cook.get('cookSetTime', self.cook_set_time)
252
+ self.cook_set_temp = cook.get('cookSetTemp', self.cook_set_temp)
253
+ self.current_temp = cook.get('currentTemp', self.current_temp)
254
+ self.temp_unit = cook.get(
255
+ 'tempUnit', self.temp_unit
256
+ ) # type: ignore[assignment]
257
+ elif cook.get('cookStatus') == 'end':
258
+ self.set_standby()
259
+ self.cook_status = 'cookEnd'
260
+
261
+ def clear_preheat(self) -> None:
262
+ """Clear preheat status."""
263
+ self.preheat = False
264
+ self.preheat_set_time = None
265
+ self.preheat_last_time = None
266
+
267
+ def set_standby(self) -> None:
268
+ """Clear cooking status."""
269
+ self.cook_status = 'standby'
270
+ self.clear_preheat()
271
+ self.cook_last_time = None
272
+ self.current_temp = None
273
+ self.cook_set_time = None
274
+ self.cook_set_temp = None
275
+ self.last_timestamp = None
276
+
277
+ def status_response(self, return_status: dict) -> None:
278
+ """Set status of Air Fryer Based on API Response."""
279
+ self.last_timestamp = None
280
+ self.preheat = False
281
+ self.cook_status = return_status.get('cookStatus')
282
+ if self.cook_status == 'standby':
283
+ self.set_standby()
284
+ return
285
+
286
+ # If drawer is pulled out, set standby if resp does not contain other details
287
+ if self.cook_status == 'pullOut':
288
+ self.last_timestamp = None
289
+ if 'currentTemp' not in return_status or 'tempUnit' not in return_status:
290
+ self.set_standby()
291
+ self.cook_status = 'pullOut'
292
+ return
293
+ if return_status.get('preheatLastTime') is not None or self.cook_status in [
294
+ 'heating',
295
+ 'preheatStop',
296
+ 'preheatEnd',
297
+ ]:
298
+ self.preheat = True
299
+
300
+ self.cook_set_time = return_status.get('cookSetTime', self.cook_set_time)
301
+ self.cook_last_time = return_status.get('cookLastTime')
302
+ self.current_temp = return_status.get('curentTemp')
303
+ self.cook_set_temp = return_status.get(
304
+ 'targetTemp', return_status.get('cookSetTemp')
305
+ )
306
+ self.temp_unit = return_status.get(
307
+ 'tempUnit', self.temp_unit
308
+ ) # type: ignore[assignment]
309
+ self.preheat_set_time = return_status.get('preheatSetTime')
310
+ self.preheat_last_time = return_status.get('preheatLastTime')
311
+
312
+ # Set last_time timestamp if cooking
313
+ if self.cook_status in ['cooking', 'heating']:
314
+ self.last_timestamp = int(time.time())
315
+
316
+ if self.cook_status == 'preheatEnd':
317
+ self.preheat_last_time = 0
318
+ self.cook_last_time = None
319
+ if self.cook_status == 'cookEnd':
320
+ self.cook_last_time = 0
321
+
322
+ # If Cooking, clear preheat status
323
+ if self.cook_status in ['cooking', 'cookStop', 'cookEnd']:
324
+ self.clear_preheat()
325
+
326
+
327
+ class VeSyncAirFryer158(VeSyncFryer):
328
+ """Cosori Air Fryer Class.
329
+
330
+ Args:
331
+ details (ResponseDeviceDetailsModel): Device details.
332
+ manager (VeSync): Manager class.
333
+ feature_map (DeviceMapTemplate): Device feature map.
334
+
335
+ Attributes:
336
+ features (list[str]): List of features.
337
+ state (AirFryer158138State): Air fryer state.
338
+ last_update (int): Last update timestamp.
339
+ refresh_interval (int): Refresh interval in seconds.
340
+ cook_temps (dict[str, list[int]] | None): Cook temperatures.
341
+ pid (str): PID for the device.
342
+ last_response (ResponseInfo): Last response from API call.
343
+ manager (VeSync): Manager object for API calls.
344
+ device_name (str): Name of device.
345
+ device_image (str): URL for device image.
346
+ cid (str): Device ID.
347
+ connection_type (str): Connection type of device.
348
+ device_type (str): Type of device.
349
+ type (str): Type of device.
350
+ uuid (str): UUID of device, not always present.
351
+ config_module (str): Configuration module of device.
352
+ mac_id (str): MAC ID of device.
353
+ current_firm_version (str): Current firmware version of device.
354
+ device_region (str): Region of device. (US, EU, etc.)
355
+ sub_device_no (int): Sub-device number of device.
356
+ product_type (str): Product type of device.
357
+ """
358
+
359
+ __slots__ = (
360
+ 'cook_temps',
361
+ 'fryer_status',
362
+ 'last_update',
363
+ 'ready_start',
364
+ 'refresh_interval',
365
+ )
366
+
367
+ def __init__(
368
+ self,
369
+ details: ResponseDeviceDetailsModel,
370
+ manager: VeSync,
371
+ feature_map: AirFryerMap,
372
+ ) -> None:
373
+ """Init the VeSync Air Fryer 158 class."""
374
+ super().__init__(details, manager, feature_map)
375
+ self.features: list[str] = feature_map.features
376
+ self.state: AirFryer158138State = AirFryer158138State(self, details, feature_map)
377
+ self.last_update: int = int(time.time())
378
+ self.refresh_interval = 0
379
+ self.ready_start = False
380
+ self.cook_temps: dict[str, list[int]] | None = None
381
+ if self.config_module not in AIRFRYER_PID_MAP:
382
+ msg = (
383
+ 'Report this error as an issue - '
384
+ f'{self.config_module} not found in PID map for {self}'
385
+ )
386
+ raise VeSyncError(msg)
387
+ self.pid = AIRFRYER_PID_MAP[self.config_module]
388
+ self.request_keys = [
389
+ 'acceptLanguage',
390
+ 'accountID',
391
+ 'appVersion',
392
+ 'cid',
393
+ 'configModule',
394
+ 'deviceRegion',
395
+ 'phoneBrand',
396
+ 'phoneOS',
397
+ 'timeZone',
398
+ 'token',
399
+ 'traceId',
400
+ 'userCountryCode',
401
+ 'method',
402
+ 'debugMode',
403
+ 'uuid',
404
+ 'pid',
405
+ ]
406
+
407
+ @deprecated('There is no on/off function for Air Fryers.')
408
+ async def toggle_switch(self, toggle: bool | None = None) -> bool:
409
+ """Turn on or off the air fryer."""
410
+ return toggle if toggle is not None else not self.is_on
411
+
412
+ def _build_request(
413
+ self,
414
+ json_cmd: dict | None = None,
415
+ method: str | None = None,
416
+ ) -> dict:
417
+ """Return body of api calls."""
418
+ req_dict = Helpers.get_class_attributes(DefaultValues, self.request_keys)
419
+ req_dict.update(Helpers.get_class_attributes(self.manager, self.request_keys))
420
+ req_dict.update(Helpers.get_class_attributes(self, self.request_keys))
421
+ req_dict['method'] = method or 'bypass'
422
+ req_dict['jsonCmd'] = json_cmd or {}
423
+ return req_dict
424
+
425
+ def _build_status_body(self, cmd_dict: dict) -> dict:
426
+ """Return body of api calls."""
427
+ body = self._build_request()
428
+ body.update(
429
+ {
430
+ 'uuid': self.uuid,
431
+ 'configModule': self.config_module,
432
+ 'jsonCmd': cmd_dict,
433
+ 'pid': self.pid,
434
+ 'accountID': self.manager.account_id,
435
+ }
436
+ )
437
+ return body
438
+
439
+ @property
440
+ def temp_unit(self) -> str | None:
441
+ """Return temp unit."""
442
+ return self.state.temp_unit
443
+
444
+ async def get_details(self) -> None:
445
+ """Get Air Fryer Status and Details."""
446
+ cmd = {'getStatus': 'status'}
447
+ req_body = self._build_request(json_cmd=cmd)
448
+ url = '/cloud/v1/deviceManaged/bypass'
449
+ r_dict, _ = await self.manager.async_call_api(url, 'post', json_object=req_body)
450
+ resp = Helpers.process_dev_response(logger, 'get_details', self, r_dict)
451
+ if resp is None:
452
+ self.state.device_status = DeviceStatus.OFF
453
+ self.state.connection_status = ConnectionStatus.OFFLINE
454
+ return
455
+
456
+ return_status = resp.get('result', {}).get('returnStatus')
457
+ if return_status is None:
458
+ LibraryLogger.log_device_api_response_error(
459
+ logger,
460
+ self.device_name,
461
+ self.device_type,
462
+ 'get_details',
463
+ msg='Return status not found in response',
464
+ )
465
+ return
466
+ self.state.status_response(return_status)
467
+
468
+ async def check_status(self) -> None:
469
+ """Update status if REFRESH_INTERVAL has passed."""
470
+ seconds_elapsed = int(time.time()) - self.last_update
471
+ logger.debug('Seconds elapsed between updates: %s', seconds_elapsed)
472
+ refresh = False
473
+ if self.refresh_interval is None:
474
+ refresh = bool(seconds_elapsed > REFRESH_INTERVAL)
475
+ elif self.refresh_interval == 0:
476
+ refresh = True
477
+ elif self.refresh_interval > 0:
478
+ refresh = bool(seconds_elapsed > self.refresh_interval)
479
+ if refresh is True:
480
+ logger.debug('Updating status, %s seconds elapsed', seconds_elapsed)
481
+ await self.update()
482
+
483
+ async def end(self) -> bool:
484
+ """End the cooking process."""
485
+ await self.check_status()
486
+ if self.state.preheat is False and self.state.cook_status in [
487
+ 'cookStop',
488
+ 'cooking',
489
+ ]:
490
+ cmd = {'cookMode': {'cookStatus': 'end'}}
491
+ elif self.state.preheat is True and self.state.cook_status in [
492
+ 'preheatStop',
493
+ 'heating',
494
+ ]:
495
+ cmd = {'preheat': {'cookStatus': 'end'}}
496
+ else:
497
+ logger.debug(
498
+ 'Cannot end %s as it is not cooking or preheating', self.device_name
499
+ )
500
+ return False
501
+
502
+ status_api = await self._status_api(cmd)
503
+ if status_api is False:
504
+ return False
505
+ self.state.set_standby()
506
+ return True
507
+
508
+ async def pause(self) -> bool:
509
+ """Pause the cooking process."""
510
+ await self.check_status()
511
+ if self.state.cook_status not in ['cooking', 'heating']:
512
+ logger.debug(
513
+ 'Cannot pause %s as it is not cooking or preheating', self.device_name
514
+ )
515
+ return False
516
+ if self.state.preheat is True:
517
+ cmd = {'preheat': {'preheatStatus': 'stop'}}
518
+ else:
519
+ cmd = {'cookMode': {'cookStatus': 'stop'}}
520
+ status_api = await self._status_api(cmd)
521
+ if status_api is True:
522
+ if self.state.preheat is True:
523
+ self.state.cook_status = 'preheatStop'
524
+ else:
525
+ self.state.cook_status = 'cookStop'
526
+ return True
527
+ return False
528
+
529
+ def _validate_temp(self, set_temp: int) -> bool:
530
+ """Temperature validation."""
531
+ if self.state.temp_unit == 'fahrenheit' and (
532
+ set_temp < self.state.min_temp_f or set_temp > self.state.max_temp_f
533
+ ):
534
+ logger.debug('Invalid temperature %s for %s', set_temp, self.device_name)
535
+ return False
536
+ if self.state.temp_unit == 'celsius' and (
537
+ set_temp < self.state.min_temp_c or set_temp > self.state.max_temp_c
538
+ ):
539
+ logger.debug('Invalid temperature %s for %s', set_temp, self.device_name)
540
+ return False
541
+ return True
542
+
543
+ async def cook(self, set_temp: int, set_time: int) -> bool:
544
+ """Set cook time and temperature in Minutes."""
545
+ await self.check_status()
546
+ if self._validate_temp(set_temp) is False:
547
+ return False
548
+ return await self._set_cook(set_temp, set_time)
549
+
550
+ async def resume(self) -> bool:
551
+ """Resume paused preheat or cook."""
552
+ await self.check_status()
553
+ if self.state.cook_status not in ['preheatStop', 'cookStop']:
554
+ logger.debug('Cannot resume %s as it is not paused', self.device_name)
555
+ return False
556
+ if self.state.preheat is True:
557
+ cmd = {'preheat': {'preheatStatus': 'heating'}}
558
+ else:
559
+ cmd = {'cookMode': {'cookStatus': 'cooking'}}
560
+ status_api = await self._status_api(cmd)
561
+ if status_api is True:
562
+ if self.state.preheat is True:
563
+ self.state.cook_status = 'heating'
564
+ else:
565
+ self.state.cook_status = 'cooking'
566
+ return True
567
+ return False
568
+
569
+ async def set_preheat(self, target_temp: int, cook_time: int) -> bool:
570
+ """Set preheat mode with cooking time."""
571
+ await self.check_status()
572
+ if self.state.cook_status not in ['standby', 'cookEnd', 'preheatEnd']:
573
+ logger.debug(
574
+ 'Cannot set preheat for %s as it is not in standby', self.device_name
575
+ )
576
+ return False
577
+ if self._validate_temp(target_temp) is False:
578
+ return False
579
+ cmd = self._cmd_api_dict
580
+ cmd['preheatSetTime'] = 5
581
+ cmd['preheatStatus'] = 'heating'
582
+ cmd['targetTemp'] = target_temp
583
+ cmd['cookSetTime'] = cook_time
584
+ json_cmd = {'preheat': cmd}
585
+ return await self._status_api(json_cmd)
586
+
587
+ async def cook_from_preheat(self) -> bool:
588
+ """Start Cook when preheat has ended."""
589
+ await self.check_status()
590
+ if self.state.preheat is False or self.state.cook_status != 'preheatEnd':
591
+ logger.debug('Cannot start cook from preheat for %s', self.device_name)
592
+ return False
593
+ return await self._set_cook(status='cooking')
594
+
595
+ async def update(self) -> None:
596
+ """Update the device details."""
597
+ await self.get_details()
598
+
599
+ @property
600
+ def _cmd_api_base(self) -> dict:
601
+ """Return Base api dictionary for setting status."""
602
+ return {
603
+ 'mode': COOK_MODE,
604
+ 'accountId': self.manager.account_id,
605
+ }
606
+
607
+ @property
608
+ def _cmd_api_dict(self) -> dict:
609
+ """Return API dictionary for setting status."""
610
+ cmd = self._cmd_api_base
611
+ cmd.update(
612
+ {
613
+ 'appointmentTs': 0,
614
+ 'recipeId': RECIPE_ID,
615
+ 'readyStart': self.ready_start,
616
+ 'recipeType': RECIPE_TYPE,
617
+ 'customRecipe': CUSTOM_RECIPE,
618
+ }
619
+ )
620
+ return cmd
621
+
622
+ async def _set_cook(
623
+ self,
624
+ set_temp: int | None = None,
625
+ set_time: int | None = None,
626
+ status: str = 'cooking',
627
+ ) -> bool:
628
+ if set_temp is not None and set_time is not None:
629
+ set_cmd = self._cmd_api_dict
630
+
631
+ set_cmd['cookSetTime'] = set_time
632
+ set_cmd['cookSetTemp'] = set_temp
633
+ else:
634
+ set_cmd = self._cmd_api_base
635
+ set_cmd['cookStatus'] = status
636
+ cmd = {'cookMode': set_cmd}
637
+ return await self._status_api(cmd)
638
+
639
+ async def _status_api(self, json_cmd: dict) -> bool:
640
+ """Set API status with jsonCmd."""
641
+ body = self._build_status_body(json_cmd)
642
+ url = '/cloud/v1/deviceManaged/bypass'
643
+ r_dict, _ = await self.manager.async_call_api(url, 'post', json_object=body)
644
+ resp = Helpers.process_dev_response(logger, 'set_status', self, r_dict)
645
+ if resp is None:
646
+ return False
647
+
648
+ self.last_update = int(time.time())
649
+ self.state.status_request(json_cmd)
650
+ await self.update()
651
+ return True