pyvesync 3.0.0b0__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 (49) hide show
  1. pyvesync/__init__.py +11 -0
  2. pyvesync/base_devices/__init__.py +35 -0
  3. pyvesync/base_devices/bulb_base.py +348 -0
  4. pyvesync/base_devices/fan_base.py +398 -0
  5. pyvesync/base_devices/fryer_base.py +69 -0
  6. pyvesync/base_devices/humidifier_base.py +460 -0
  7. pyvesync/base_devices/outlet_base.py +315 -0
  8. pyvesync/base_devices/purifier_base.py +518 -0
  9. pyvesync/base_devices/switch_base.py +270 -0
  10. pyvesync/base_devices/thermostat_base.py +277 -0
  11. pyvesync/base_devices/vesyncbasedevice.py +580 -0
  12. pyvesync/const.py +749 -0
  13. pyvesync/device_container.py +360 -0
  14. pyvesync/device_map.py +1090 -0
  15. pyvesync/devices/__init__.py +1 -0
  16. pyvesync/devices/vesyncbulb.py +1254 -0
  17. pyvesync/devices/vesyncfan.py +238 -0
  18. pyvesync/devices/vesynchumidifier.py +785 -0
  19. pyvesync/devices/vesynckitchen.py +664 -0
  20. pyvesync/devices/vesyncoutlet.py +962 -0
  21. pyvesync/devices/vesyncpurifier.py +951 -0
  22. pyvesync/devices/vesyncswitch.py +449 -0
  23. pyvesync/devices/vesyncthermostat.py +175 -0
  24. pyvesync/models/__init__.py +11 -0
  25. pyvesync/models/base_models.py +117 -0
  26. pyvesync/models/bulb_models.py +173 -0
  27. pyvesync/models/bypass_models.py +264 -0
  28. pyvesync/models/fan_models.py +46 -0
  29. pyvesync/models/fryer_models.py +28 -0
  30. pyvesync/models/home_models.py +158 -0
  31. pyvesync/models/humidifier_models.py +187 -0
  32. pyvesync/models/outlet_models.py +194 -0
  33. pyvesync/models/purifier_models.py +204 -0
  34. pyvesync/models/switch_models.py +89 -0
  35. pyvesync/models/thermostat_models.py +122 -0
  36. pyvesync/models/vesync_models.py +252 -0
  37. pyvesync/utils/__init__.py +1 -0
  38. pyvesync/utils/colors.py +248 -0
  39. pyvesync/utils/device_mixins.py +366 -0
  40. pyvesync/utils/enum_utils.py +25 -0
  41. pyvesync/utils/errors.py +859 -0
  42. pyvesync/utils/helpers.py +696 -0
  43. pyvesync/utils/logs.py +500 -0
  44. pyvesync/vesync.py +445 -0
  45. pyvesync-3.0.0b0.dist-info/METADATA +527 -0
  46. pyvesync-3.0.0b0.dist-info/RECORD +49 -0
  47. pyvesync-3.0.0b0.dist-info/WHEEL +5 -0
  48. pyvesync-3.0.0b0.dist-info/licenses/LICENSE +21 -0
  49. pyvesync-3.0.0b0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,951 @@
1
+ """VeSync API for controlling air purifiers."""
2
+ from __future__ import annotations
3
+ import logging
4
+ from typing import TYPE_CHECKING
5
+
6
+ from typing_extensions import deprecated
7
+
8
+ from pyvesync.base_devices.purifier_base import VeSyncPurifier
9
+ from pyvesync.utils.device_mixins import (
10
+ BypassV1Mixin,
11
+ BypassV2Mixin,
12
+ process_bypassv2_result
13
+ )
14
+ from pyvesync.utils.helpers import Helpers, Timer
15
+ # from pyvesync.models.base_models import DefaultValues
16
+ from pyvesync.const import (
17
+ IntFlag,
18
+ StrFlag,
19
+ PurifierAutoPreference,
20
+ DeviceStatus,
21
+ ConnectionStatus,
22
+ PurifierModes
23
+ )
24
+ from pyvesync.models.bypass_models import (
25
+ ResultV2GetTimer,
26
+ ResultV2SetTimer,
27
+ )
28
+ from pyvesync.models.purifier_models import (
29
+ PurifierCoreDetailsResult,
30
+ PurifierV2DetailsResult,
31
+ PurifierV2EventTiming,
32
+ PurifierV2TimerActionItems,
33
+ PurifierV2TimerPayloadData,
34
+ RequestPurifier131,
35
+ RequestPurifier131Mode,
36
+ RequestPurifier131Level,
37
+ Purifier131Result,
38
+ )
39
+
40
+ if TYPE_CHECKING:
41
+ from pyvesync import VeSync
42
+ from pyvesync.models.vesync_models import ResponseDeviceDetailsModel
43
+ from pyvesync.device_map import PurifierMap
44
+
45
+
46
+ _LOGGER = logging.getLogger(__name__)
47
+
48
+
49
+ class VeSyncAirBypass(BypassV2Mixin, VeSyncPurifier):
50
+ """Initialize air purifier devices.
51
+
52
+ Instantiated by VeSync manager object. Inherits from
53
+ VeSyncBaseDevice class. This is the primary class for most
54
+ air purifiers, using the Bypass V2 API, except the original LV-PUR131S.
55
+
56
+ Parameters:
57
+ details (dict): Dictionary of device details
58
+ manager (VeSync): Instantiated VeSync object used to make API calls
59
+ feature_map (PurifierMap): Device map template
60
+
61
+ Attributes:
62
+ state (PurifierState): State of the device.
63
+ last_response (ResponseInfo): Last response from API call.
64
+ manager (VeSync): Manager object for API calls.
65
+ device_name (str): Name of device.
66
+ device_image (str): URL for device image.
67
+ cid (str): Device ID.
68
+ connection_type (str): Connection type of device.
69
+ device_type (str): Type of device.
70
+ type (str): Type of device.
71
+ uuid (str): UUID of device, not always present.
72
+ config_module (str): Configuration module of device.
73
+ mac_id (str): MAC ID of device.
74
+ current_firm_version (str): Current firmware version of device.
75
+ device_region (str): Region of device. (US, EU, etc.)
76
+ pid (str): Product ID of device, pulled by some devices on update.
77
+ sub_device_no (int): Sub-device number of device.
78
+ product_type (str): Product type of device.
79
+ features (dict): Features of device.
80
+ modes (list[str]): List of modes supported by the device.
81
+ fan_levels (list[int]): List of fan levels supported by the device.
82
+ nightlight_modes (list[str]): List of nightlight modes supported by the device.
83
+ auto_preferences (list[str]): List of auto preferences supported by the device.
84
+
85
+ Notes:
86
+ The `details` attribute holds device information that is updated when
87
+ the `update()` method is called. An example of the `details` attribute:
88
+ ```python
89
+ json.dumps(self.details, indent=4)
90
+ {
91
+ 'filter_life': 0,
92
+ 'mode': 'manual',
93
+ 'level': 0,
94
+ 'display': False,
95
+ 'child_lock': False,
96
+ 'night_light': 'off',
97
+ 'air_quality': 0 # air quality level
98
+ 'air_quality_value': 0, # PM2.5 value from device,
99
+ 'display_forever': False
100
+ }
101
+ ```
102
+ """
103
+
104
+ __slots__ = ()
105
+
106
+ def __init__(self, details: ResponseDeviceDetailsModel,
107
+ manager: VeSync, feature_map: PurifierMap) -> None:
108
+ """Initialize VeSync Air Purifier Bypass Base Class."""
109
+ super().__init__(details, manager, feature_map)
110
+
111
+ def _set_purifier_state(self, result: PurifierCoreDetailsResult) -> None:
112
+ """Populate PurifierState with details from API response.
113
+
114
+ Populates `self.state` and instance variables with device details.
115
+
116
+ Args:
117
+ result (InnerPurifierResult): Data model for inner result in purifier
118
+ details response.
119
+ """
120
+ self.state.device_status = DeviceStatus.ON if result.enabled else DeviceStatus.OFF
121
+ self.state.filter_life = result.filter_life or 0
122
+ self.state.mode = result.mode
123
+ self.state.fan_level = result.level or 0
124
+ self.state.display_status = DeviceStatus.ON if result.display \
125
+ else DeviceStatus.OFF
126
+ self.state.child_lock = result.child_lock or False
127
+ config = result.configuration
128
+ if config is not None:
129
+ self.state.display_set_state = DeviceStatus.ON if config.display \
130
+ else DeviceStatus.OFF
131
+ self.state.display_forever = config.display_forever
132
+ if config.auto_preference is not None:
133
+ self.state.auto_preference_type = config.auto_preference.type
134
+ self.state.auto_room_size = config.auto_preference.room_size
135
+ if self.supports_air_quality is True:
136
+ self.state.pm25 = result.air_quality_value
137
+ self.state.set_air_quality_level(result.air_quality)
138
+ if result.night_light != StrFlag.NOT_SUPPORTED:
139
+ self.state.nightlight_status = DeviceStatus.ON if result.night_light \
140
+ else DeviceStatus.OFF
141
+
142
+ async def get_details(self) -> None:
143
+ r_dict = await self.call_bypassv2_api('getPurifierStatus')
144
+ resp_model = process_bypassv2_result(
145
+ self, _LOGGER, "get_details", r_dict, PurifierCoreDetailsResult
146
+ )
147
+ if resp_model is None:
148
+ return
149
+ self._set_purifier_state(resp_model)
150
+
151
+ async def get_timer(self) -> Timer | None:
152
+ r_bytes = await self.call_bypassv2_api('getTimer')
153
+ resp_model = process_bypassv2_result(
154
+ self, _LOGGER, "get_timer", r_bytes, ResultV2GetTimer
155
+ )
156
+ if resp_model is None:
157
+ return None
158
+ timers = resp_model.timers
159
+ if not timers:
160
+ _LOGGER.debug('No timers found')
161
+ self.state.timer = None
162
+ return None
163
+ timer = timers[0]
164
+ self.state.timer = Timer(
165
+ timer_duration=timer.total,
166
+ action=timer.action,
167
+ id=timer.id,
168
+ remaining=timer.remain,
169
+ )
170
+ self.state.device_status = DeviceStatus.ON
171
+ self.state.connection_status = ConnectionStatus.ONLINE
172
+ _LOGGER.debug("Timer found: %s", str(self.state.timer))
173
+ return self.state.timer
174
+
175
+ async def set_timer(self, duration: int, action: str | None = None) -> bool:
176
+ action = DeviceStatus.OFF # No other actions available for this device
177
+ if self.state.device_status != DeviceStatus.ON:
178
+ _LOGGER.debug("Can't set timer when device is off")
179
+ # head, body = self.build_api_dict('addTimer')
180
+ payload_data = {
181
+ "action": str(action),
182
+ "total": duration
183
+ }
184
+ r_dict = await self.call_bypassv2_api('addTimer', payload_data)
185
+ resp_model = process_bypassv2_result(
186
+ self, _LOGGER, "set_timer", r_dict, ResultV2SetTimer
187
+ )
188
+ if resp_model is None:
189
+ return False
190
+ self.state.timer = Timer(
191
+ timer_duration=duration, action="off", id=resp_model.id
192
+ )
193
+ self.state.device_status = DeviceStatus.ON
194
+ self.state.connection_status = ConnectionStatus.ONLINE
195
+ return True
196
+
197
+ async def clear_timer(self) -> bool:
198
+ if self.state.timer is None:
199
+ _LOGGER.debug('No timer to clear, run `get_timer()` to get active timer')
200
+ return False
201
+ payload_data = {
202
+ "id": self.state.timer.id
203
+ }
204
+
205
+ r_dict = await self.call_bypassv2_api('delTimer', payload_data)
206
+ r = Helpers.process_dev_response(_LOGGER, "clear_timer", self, r_dict)
207
+ if r is None:
208
+ return False
209
+
210
+ _LOGGER.debug("Timer cleared")
211
+ self.state.timer = None
212
+ self.state.device_status = DeviceStatus.ON
213
+ self.state.connection_status = ConnectionStatus.ONLINE
214
+ return True
215
+
216
+ async def set_auto_preference(self, preference: str, room_size: int = 800) -> bool:
217
+ if self.state.device_status != DeviceStatus.ON:
218
+ _LOGGER.debug("Can't set auto preference when device is off")
219
+ payload_data = {
220
+ "type": preference,
221
+ "room_size": room_size
222
+ }
223
+ r_dict = await self.call_bypassv2_api('setAutoPreference', payload_data)
224
+ r = Helpers.process_dev_response(_LOGGER, "set_auto_preference", self, r_dict)
225
+ if r is None:
226
+ return False
227
+ self.state.connection_status = ConnectionStatus.ONLINE
228
+ self.state.auto_preference_type = preference
229
+ self.state.auto_room_size = room_size
230
+ return True
231
+
232
+ async def set_fan_speed(self, speed: None | int = None) -> bool:
233
+ speeds: list = self.fan_levels
234
+ current_speed = self.state.fan_level
235
+
236
+ if speed is not None:
237
+ if speed not in speeds:
238
+ _LOGGER.debug(
239
+ "%s is invalid speed - valid speeds are %s", speed, str(speeds))
240
+ return False
241
+ new_speed = speed
242
+ else:
243
+ new_speed = Helpers.bump_level(current_speed, self.fan_levels)
244
+
245
+ data = {
246
+ "id": 0,
247
+ "level": new_speed,
248
+ "type": "wind",
249
+ }
250
+ r_dict = await self.call_bypassv2_api('setLevel', data)
251
+ r = Helpers.process_dev_response(_LOGGER, "set_fan_speed", self, r_dict)
252
+ if r is None:
253
+ return False
254
+
255
+ self.state.fan_level = new_speed
256
+ self.state.mode = PurifierModes.MANUAL # Set mode to manual to set fan speed
257
+ self.state.device_status = DeviceStatus.ON
258
+ self.state.connection_status = ConnectionStatus.ONLINE
259
+ return True
260
+
261
+ async def toggle_child_lock(self, toggle: bool | None = None) -> bool:
262
+ """Toggle child lock.
263
+
264
+ Set child lock to on or off. Internal method used by `turn_on_child_lock` and
265
+ `turn_off_child_lock`.
266
+
267
+ Args:
268
+ toggle (bool): True to turn child lock on, False to turn off
269
+
270
+ Returns:
271
+ bool : True if child lock is set, False if not
272
+ """
273
+ if toggle is None:
274
+ toggle = self.state.child_lock is False
275
+ data = {"child_lock": toggle}
276
+
277
+ r_dict = await self.call_bypassv2_api('setChildLock', data)
278
+ r = Helpers.process_dev_response(_LOGGER, "toggle_child_lock", self, r_dict)
279
+ if r is None:
280
+ return False
281
+
282
+ self.state.child_lock = toggle
283
+ self.state.connection_status = ConnectionStatus.ONLINE
284
+ return True
285
+
286
+ async def reset_filter(self) -> bool:
287
+ """Reset filter to 100%.
288
+
289
+ Returns:
290
+ bool : True if filter is reset, False if not
291
+ """
292
+ r_dict = await self.call_bypassv2_api('resetFilter')
293
+ r = Helpers.process_dev_response(_LOGGER, "reset_filter", self, r_dict)
294
+ return bool(r)
295
+
296
+ @deprecated("Use set_mode(mode: str) instead.")
297
+ async def mode_toggle(self, mode: str) -> bool:
298
+ """Deprecated method for setting purifier mode."""
299
+ return await self.set_mode(mode)
300
+
301
+ async def set_mode(self, mode: str) -> bool:
302
+ if mode.lower() not in self.modes:
303
+ _LOGGER.debug("Invalid purifier mode used - %s", mode)
304
+ return False
305
+
306
+ if mode.lower() == PurifierModes.MANUAL:
307
+ return await self.set_fan_speed(self.state.fan_level or 1)
308
+
309
+ data = {
310
+ "mode": mode,
311
+ }
312
+ r_dict = await self.call_bypassv2_api('setPurifierMode', data)
313
+
314
+ r = Helpers.process_dev_response(_LOGGER, "set_mode", self, r_dict)
315
+ if r is None:
316
+ return False
317
+
318
+ self.state.mode = mode
319
+ self.state.device_status = DeviceStatus.ON
320
+ self.state.connection_status = ConnectionStatus.ONLINE
321
+ return True
322
+
323
+ async def toggle_switch(self, toggle: bool | None = None) -> bool:
324
+ if toggle is None:
325
+ toggle = self.state.device_status != DeviceStatus.ON
326
+ if not isinstance(toggle, bool):
327
+ _LOGGER.debug('Invalid toggle value for purifier switch')
328
+ return False
329
+
330
+ data = {
331
+ "enabled": toggle,
332
+ "id": 0
333
+ }
334
+ r_dict = await self.call_bypassv2_api('setSwitch', data)
335
+ r = Helpers.process_dev_response(_LOGGER, "toggle_switch", self, r_dict)
336
+ if r is None:
337
+ return False
338
+
339
+ self.state.device_status = DeviceStatus.ON if toggle else DeviceStatus.OFF
340
+ self.state.connection_status = ConnectionStatus.ONLINE
341
+ return True
342
+
343
+ async def toggle_display(self, mode: bool) -> bool:
344
+ if not isinstance(mode, bool):
345
+ _LOGGER.debug("Mode must be True or False")
346
+ return False
347
+
348
+ data = {
349
+ "state": mode
350
+ }
351
+ r_dict = await self.call_bypassv2_api('setDisplay', data)
352
+ r = Helpers.process_dev_response(_LOGGER, "set_display", self, r_dict)
353
+ if r is None:
354
+ return False
355
+ self.state.connection_status = ConnectionStatus.ONLINE
356
+ self.state.display_set_state = DeviceStatus.from_bool(mode)
357
+ return True
358
+
359
+ async def set_nightlight_mode(self, mode: str) -> bool:
360
+ if not self.supports_nightlight:
361
+ _LOGGER.debug("Device does not support night light")
362
+ return False
363
+ if mode.lower() not in self.nightlight_modes:
364
+ _LOGGER.warning("Invalid nightlight mode used (on, off or dim)- %s", mode)
365
+ return False
366
+
367
+ r_dict = await self.call_bypassv2_api(
368
+ 'setNightLight', {'night_light': mode.lower()}
369
+ )
370
+ r = Helpers.process_dev_response(_LOGGER, "set_night_light", self, r_dict)
371
+ if r is None:
372
+ return False
373
+
374
+ self.state.nightlight_status = mode
375
+ return True
376
+
377
+ @property
378
+ @deprecated("Use self.state.air_quality instead.")
379
+ def air_quality(self) -> int | None:
380
+ """Get air quality value PM2.5 (ug/m3)."""
381
+ return self.state.pm25
382
+
383
+ @property
384
+ @deprecated("Use self.state.fan_level instead.")
385
+ def fan_level(self) -> int | None:
386
+ """Get current fan level."""
387
+ return self.state.fan_level
388
+
389
+ @property
390
+ @deprecated("Use self.state.filter_life instead.")
391
+ def filter_life(self) -> int | None:
392
+ """Get percentage of filter life remaining."""
393
+ return self.state.filter_life
394
+
395
+ @property
396
+ @deprecated("Use self.state.display_status instead.")
397
+ def display_state(self) -> bool:
398
+ """Get display state.
399
+
400
+ See [pyvesync.VeSyncAirBypass.display_status][`self.display_status`]
401
+ """
402
+ return self.state.display_status == DeviceStatus.ON
403
+
404
+ @property
405
+ @deprecated("Use self.state.display_status instead.")
406
+ def screen_status(self) -> bool:
407
+ """Get display status.
408
+
409
+ Returns:
410
+ bool : True if display is on, False if off
411
+ """
412
+ return self.state.display_status == DeviceStatus.ON
413
+
414
+
415
+ class VeSyncAirBaseV2(VeSyncAirBypass):
416
+ """Levoit V2 Air Purifier Class.
417
+
418
+ Inherits from VeSyncAirBypass and VeSyncBaseDevice class. For
419
+ newer devices that use camel-case API calls.
420
+
421
+ Args:
422
+ details (dict): Dictionary of device details
423
+ manager (VeSync): Instantiated VeSync object
424
+ feature_map (PurifierMap): Device map template
425
+
426
+ Attributes:
427
+ state (PurifierState): State of the device.
428
+ last_response (ResponseInfo): Last response from API call.
429
+ manager (VeSync): Manager object for API calls.
430
+ device_name (str): Name of device.
431
+ device_image (str): URL for device image.
432
+ cid (str): Device ID.
433
+ connection_type (str): Connection type of device.
434
+ device_type (str): Type of device.
435
+ type (str): Type of device.
436
+ uuid (str): UUID of device, not always present.
437
+ config_module (str): Configuration module of device.
438
+ mac_id (str): MAC ID of device.
439
+ current_firm_version (str): Current firmware version of device.
440
+ device_region (str): Region of device. (US, EU, etc.)
441
+ pid (str): Product ID of device, pulled by some devices on update.
442
+ sub_device_no (int): Sub-device number of device.
443
+ product_type (str): Product type of device.
444
+ features (dict): Features of device.
445
+ modes (list[str]): List of modes supported by the device.
446
+ fan_levels (list[int]): List of fan levels supported by the device.
447
+ nightlight_modes (list[str]): List of nightlight modes supported by the device.
448
+ auto_preferences (list[str]): List of auto preferences supported by the device.
449
+ """
450
+
451
+ __slots__ = ()
452
+
453
+ def __init__(
454
+ self,
455
+ details: ResponseDeviceDetailsModel,
456
+ manager: VeSync,
457
+ feature_map: PurifierMap,
458
+ ) -> None:
459
+ """Initialize the VeSync Base API V2 Air Purifier Class."""
460
+ super().__init__(details, manager, feature_map)
461
+
462
+ def _set_state(self, details: PurifierV2DetailsResult) -> None:
463
+ """Set Purifier state from details response."""
464
+ self.state.connection_status = ConnectionStatus.ONLINE
465
+ self.state.device_status = DeviceStatus.from_int(details.powerSwitch)
466
+ self.state.mode = details.workMode
467
+ self.state.filter_life = details.filterLifePercent
468
+ if details.fanSpeedLevel == 255:
469
+ self.state.fan_level = 0
470
+ else:
471
+ self.state.fan_level = details.fanSpeedLevel
472
+ self.state.fan_set_level = details.manualSpeedLevel
473
+ self.state.child_lock = bool(details.childLockSwitch)
474
+ self.state.air_quality_level = details.AQLevel
475
+ self.state.pm25 = details.PM25
476
+ self.state.light_detection_switch = DeviceStatus.from_int(
477
+ details.lightDetectionSwitch
478
+ )
479
+ self.state.light_detection_status = DeviceStatus.from_int(
480
+ details.environmentLightState
481
+ )
482
+ self.state.display_set_state = DeviceStatus.from_int(details.screenSwitch)
483
+ self.state.display_status = DeviceStatus.from_int(details.screenState)
484
+ auto_pref = details.autoPreference
485
+ if auto_pref is not None:
486
+ self.state.auto_preference_type = auto_pref.autoPreferenceType
487
+ self.state.auto_room_size = auto_pref.roomSize
488
+
489
+ self.state.pm1 = details.PM1
490
+ self.state.pm10 = details.PM10
491
+ self.state.aq_percent = details.AQPercent
492
+ self.state.fan_rotate_angle = details.fanRotateAngle
493
+ if details.filterOpenState != IntFlag.NOT_SUPPORTED:
494
+ self.state.filter_open_state = bool(details.filterOpenState)
495
+ if details.timerRemain > 0:
496
+ self.state.timer = Timer(details.timerRemain, 'off')
497
+
498
+ @property
499
+ @deprecated("Use self.state.fan_set_level instead.")
500
+ def set_speed_level(self) -> int | None:
501
+ """Get set speed level."""
502
+ return self.state.fan_set_level
503
+
504
+ @property
505
+ @deprecated("Use self.state.light_detection_switch, this returns 'on' or 'off")
506
+ def light_detection(self) -> bool:
507
+ """Return true if light detection feature is enabled."""
508
+ return self.state.light_detection_switch == DeviceStatus.ON
509
+
510
+ @property
511
+ @deprecated("Use self.state.light_detection_status, this returns 'on' or 'off'")
512
+ def light_detection_state(self) -> bool:
513
+ """Return true if light is detected."""
514
+ return self.state.light_detection_status == DeviceStatus.ON
515
+
516
+ async def get_details(self) -> None:
517
+ """Build API V2 Purifier details dictionary."""
518
+ r_dict = await self.call_bypassv2_api('getPurifierStatus')
519
+ r_model = process_bypassv2_result(
520
+ self, _LOGGER, "get_details", r_dict, PurifierV2DetailsResult
521
+ )
522
+ if r_model is None:
523
+ return
524
+
525
+ self._set_state(r_model)
526
+
527
+ @deprecated("Use toggle_light_detection(toggle) instead.")
528
+ async def set_light_detection(self, toggle: bool) -> bool:
529
+ """Set light detection feature."""
530
+ return await self.toggle_light_detection(toggle)
531
+
532
+ async def toggle_light_detection(self, toggle: bool | None = None) -> bool:
533
+ """Enable/Disable Light Detection Feature."""
534
+ if toggle is None:
535
+ toggle = not bool(self.state.light_detection_status)
536
+ payload_data = {"lightDetectionSwitch": int(toggle)}
537
+ r_dict = await self.call_bypassv2_api('setLightDetection', payload_data)
538
+ r = Helpers.process_dev_response(_LOGGER, "set_light_detection", self, r_dict)
539
+ if r is None:
540
+ return False
541
+
542
+ self.state.light_detection_switch = DeviceStatus.ON if toggle \
543
+ else DeviceStatus.OFF
544
+ self.state.connection_status = ConnectionStatus.ONLINE
545
+ return True
546
+
547
+ async def toggle_switch(self, toggle: bool | None = None) -> bool:
548
+ if toggle is None:
549
+ toggle = not bool(self.state.device_status)
550
+ if not isinstance(toggle, bool):
551
+ _LOGGER.debug('Invalid toggle value for purifier switch')
552
+ return False
553
+ if toggle == bool(self.state.device_status):
554
+ _LOGGER.debug('Purifier is already %s', self.state.device_status)
555
+ return True
556
+
557
+ payload_data = {
558
+ 'powerSwitch': int(toggle),
559
+ 'switchIdx': 0
560
+ }
561
+ r_dict = await self.call_bypassv2_api('setSwitch', payload_data)
562
+ r = Helpers.process_dev_response(_LOGGER, "toggle_switch", self, r_dict)
563
+ if r is None:
564
+ return False
565
+
566
+ self.state.device_status = DeviceStatus.from_bool(toggle)
567
+ self.state.connection_status = ConnectionStatus.ONLINE
568
+ return True
569
+
570
+ async def toggle_child_lock(self, toggle: bool | None = None) -> bool:
571
+ """Levoit 100S/200S set Child Lock.
572
+
573
+ Parameters:
574
+ toggle (bool): True to turn child lock on, False to turn off
575
+
576
+ Returns:
577
+ bool : True if successful, False if not
578
+ """
579
+ if toggle is None:
580
+ toggle = not bool(self.state.child_lock)
581
+ payload_data = {'childLockSwitch': int(toggle)}
582
+ r_dict = await self.call_bypassv2_api('setChildLock', payload_data)
583
+
584
+ r = Helpers.process_dev_response(_LOGGER, "toggle_child_lock", self, r_dict)
585
+ if r is None:
586
+ return False
587
+
588
+ self.state.child_lock = toggle
589
+ self.state.connection_status = ConnectionStatus.ONLINE
590
+ return True
591
+
592
+ async def toggle_display(self, mode: bool) -> bool:
593
+ if bool(self.state.display_set_state) == mode:
594
+ _LOGGER.debug('Display is already %s', mode)
595
+ return True
596
+
597
+ payload_data = {
598
+ 'screenSwitch': int(mode)
599
+ }
600
+ r_dict = await self.call_bypassv2_api('setDisplay', payload_data)
601
+
602
+ r = Helpers.process_dev_response(_LOGGER, "set_display", self, r_dict)
603
+ if r is None:
604
+ return False
605
+
606
+ self.state.display_set_state = DeviceStatus.from_bool(mode)
607
+ self.state.connection_status = ConnectionStatus.ONLINE
608
+ return True
609
+
610
+ async def set_timer(
611
+ self, duration: int, action: str | None = None
612
+ ) -> bool:
613
+ action = DeviceStatus.OFF # No other actions available for this device
614
+ if action not in [DeviceStatus.ON, DeviceStatus.OFF]:
615
+ _LOGGER.debug('Invalid action for timer')
616
+ return False
617
+
618
+ method = 'powerSwitch'
619
+ action_int = 1 if action == DeviceStatus.ON else 0
620
+ action_item = PurifierV2TimerActionItems(type=method, act=action_int)
621
+ timing = PurifierV2EventTiming(clkSec=duration)
622
+ payload_data = PurifierV2TimerPayloadData(
623
+ enabled=True, startAct=[action_item], tmgEvt=timing,
624
+ )
625
+
626
+ r_dict = await self.call_bypassv2_api('addTimerV2', payload_data.to_dict())
627
+ r = Helpers.process_dev_response(_LOGGER, "set_timer", self, r_dict)
628
+ if r is None:
629
+ return False
630
+
631
+ r_model = ResultV2SetTimer.from_dict(r)
632
+
633
+ self.state.timer = Timer(duration, action=action, id=r_model.id)
634
+ self.state.connection_status = ConnectionStatus.ONLINE
635
+ return True
636
+
637
+ async def clear_timer(self) -> bool:
638
+ if self.state.timer is None:
639
+ _LOGGER.warning("No timer found, run get_timer() to retrieve timer.")
640
+ return False
641
+
642
+ payload_data = {
643
+ "id": self.state.timer.id,
644
+ "subDeviceNo": 0
645
+ }
646
+ r_dict = await self.call_bypassv2_api('delTimerV2', payload_data)
647
+ r = Helpers.process_dev_response(_LOGGER, "clear_timer", self, r_dict)
648
+ if r is None:
649
+ return False
650
+
651
+ self.state.timer = None
652
+ self.state.connection_status = ConnectionStatus.ONLINE
653
+ return True
654
+
655
+ async def set_auto_preference(self, preference: str = PurifierAutoPreference.DEFAULT,
656
+ room_size: int = 600) -> bool:
657
+ """Set Levoit Vital 100S/200S auto mode.
658
+
659
+ Parameters:
660
+ preference (str | None | PurifierAutoPreference):
661
+ Preference for auto mode, default 'default' (default, efficient, quiet)
662
+ room_size (int | None):
663
+ Room size in square feet, by default 600
664
+ """
665
+ if preference not in self.auto_preferences:
666
+ _LOGGER.debug(
667
+ "%s is invalid preference -"
668
+ " valid preferences are default, efficient, quiet",
669
+ preference,
670
+ )
671
+ return False
672
+ payload_data = {
673
+ "autoPreference": preference,
674
+ "roomSize": room_size
675
+ }
676
+ r_dict = await self.call_bypassv2_api('setAutoPreference', payload_data)
677
+ r = Helpers.process_dev_response(_LOGGER, "set_auto_preference", self, r_dict)
678
+ if r is None:
679
+ return False
680
+
681
+ self.state.auto_preference_type = preference
682
+ self.state.auto_room_size = room_size
683
+ self.state.connection_status = ConnectionStatus.ONLINE
684
+ return True
685
+
686
+ async def set_fan_speed(self, speed: None | int = None) -> bool:
687
+ if speed is not None:
688
+ if speed not in self.fan_levels:
689
+ _LOGGER.debug(
690
+ "%s is invalid speed - valid speeds are %s",
691
+ speed,
692
+ str(self.fan_levels),
693
+ )
694
+ return False
695
+ new_speed = speed
696
+ elif self.state.fan_level is None:
697
+ new_speed = self.fan_levels[0]
698
+ else:
699
+ new_speed = Helpers.bump_level(self.state.fan_level, self.fan_levels)
700
+
701
+ payload_data = {
702
+ "levelIdx": 0,
703
+ "manualSpeedLevel": new_speed,
704
+ "levelType": "wind"
705
+ }
706
+ r_dict = await self.call_bypassv2_api('setLevel', payload_data)
707
+ r = Helpers.process_dev_response(_LOGGER, "set_fan_speed", self, r_dict)
708
+ if r is None:
709
+ return False
710
+
711
+ self.state.fan_set_level = new_speed
712
+ self.state.mode = PurifierModes.MANUAL
713
+ self.state.device_status = DeviceStatus.ON
714
+ self.state.connection_status = ConnectionStatus.ONLINE
715
+ return True
716
+
717
+ async def set_mode(self, mode: str) -> bool:
718
+ if mode.lower() not in self.modes:
719
+ _LOGGER.debug("Invalid purifier mode used - %s", mode)
720
+ return False
721
+
722
+ # Call change_fan_speed if mode is set to manual
723
+ if mode == PurifierModes.MANUAL:
724
+ if self.state.fan_set_level is None or self.state.fan_level == 0:
725
+ return await self.set_fan_speed(1)
726
+ return await self.set_fan_speed(self.state.fan_set_level)
727
+
728
+ payload_data = {
729
+ "workMode": mode
730
+ }
731
+ r_dict = await self.call_bypassv2_api('setPurifierMode', payload_data)
732
+ r = Helpers.process_dev_response(_LOGGER, "mode_toggle", self, r_dict)
733
+ if r is None:
734
+ return False
735
+
736
+ self.state.mode = mode
737
+ self.state.connection_status = ConnectionStatus.ONLINE
738
+ self.state.device_status = DeviceStatus.ON
739
+ return True
740
+
741
+
742
+ class VeSyncAir131(BypassV1Mixin, VeSyncPurifier):
743
+ """Levoit Air Purifier Class.
744
+
745
+ Class for LV-PUR131S, using BypassV1 API.
746
+
747
+ Attributes:
748
+ state (PurifierState): State of the device.
749
+ last_response (ResponseInfo): Last response from API call.
750
+ manager (VeSync): Manager object for API calls.
751
+ device_name (str): Name of device.
752
+ device_image (str): URL for device image.
753
+ cid (str): Device ID.
754
+ connection_type (str): Connection type of device.
755
+ device_type (str): Type of device.
756
+ type (str): Type of device.
757
+ uuid (str): UUID of device, not always present.
758
+ config_module (str): Configuration module of device.
759
+ mac_id (str): MAC ID of device.
760
+ current_firm_version (str): Current firmware version of device.
761
+ device_region (str): Region of device. (US, EU, etc.)
762
+ pid (str): Product ID of device, pulled by some devices on update.
763
+ sub_device_no (int): Sub-device number of device.
764
+ product_type (str): Product type of device.
765
+ features (dict): Features of device.
766
+ modes (list[str]): List of modes supported by the device.
767
+ fan_levels (list[int]): List of fan levels supported by the device.
768
+ nightlight_modes (list[str]): List of nightlight modes supported by the device.
769
+ auto_preferences (list[str]): List of auto preferences supported by the device.
770
+ """
771
+
772
+ __slots__ = ()
773
+
774
+ def __init__(self, details: ResponseDeviceDetailsModel,
775
+ manager: VeSync, feature_map: PurifierMap) -> None:
776
+ """Initialize air purifier class."""
777
+ super().__init__(details, manager, feature_map)
778
+ # self.request_keys = [
779
+ # "acceptLanguage",
780
+ # "appVersion",
781
+ # "phoneBrand",
782
+ # "phoneOS",
783
+ # "accountID",
784
+ # "debugMode"
785
+ # "traceId",
786
+ # "timeZone",
787
+ # "token",
788
+ # "userCountryCode",
789
+ # "uuid"
790
+ # ]
791
+
792
+ # def _build_request(
793
+ # self, method: str, update_dict: dict | None = None
794
+ # ) -> RequestPurifier131:
795
+ # """Build API request body for air purifier timer."""
796
+ # body = Helpers.get_class_attributes(DefaultValues, self.request_keys)
797
+ # body.update(Helpers.get_class_attributes(self.manager, self.request_keys))
798
+ # body.update(Helpers.get_class_attributes(self, self.request_keys))
799
+ # body["method"] = method
800
+ # if update_dict is not None:
801
+ # body.update(update_dict)
802
+ # return RequestPurifier131.from_dict(body)
803
+
804
+ def _set_state(self, details: Purifier131Result) -> None:
805
+ """Set state from purifier API get_details() response."""
806
+ self.state.device_status = details.deviceStatus
807
+ self.state.connection_status = details.connectionStatus
808
+ self.state.active_time = details.activeTime
809
+ self.state.filter_life = details.filterLife.percent
810
+ self.state.display_status = details.screenStatus
811
+ self.state.display_set_state = details.screenStatus
812
+ self.state.child_lock = bool(DeviceStatus(details.childLock))
813
+ self.state.mode = details.mode
814
+ self.state.fan_level = details.level or 0
815
+ self.state.fan_set_level = details.levelNew
816
+ self.state.set_air_quality_level(details.airQuality)
817
+
818
+ async def get_details(self) -> None:
819
+ # body = self._build_request('deviceDetail')
820
+ # headers = Helpers.req_header_bypass()
821
+
822
+ # r_dict, _ = await self.manager.async_call_api(
823
+ # '/cloud/v1/deviceManaged/deviceDetail',
824
+ # method='post',
825
+ # headers=headers,
826
+ # json_object=body.to_dict(),
827
+ # )
828
+ r_dict = await self.call_bypassv1_api(
829
+ RequestPurifier131, method="deviceDetail", endpoint="deviceDetail"
830
+ )
831
+ r = Helpers.process_dev_response(_LOGGER, "get_details", self, r_dict)
832
+ if r is None:
833
+ return
834
+
835
+ r_model = Purifier131Result.from_dict(r.get('result', {}))
836
+ self._set_state(r_model)
837
+
838
+ async def toggle_display(self, mode: bool) -> bool:
839
+ update_dict = {
840
+ "status": "on" if mode else "off"
841
+ }
842
+ # body = self._build_request('airPurifierScreenCtl', update_dict=update_dict)
843
+ # headers = Helpers.req_header_bypass()
844
+
845
+ # r_dict, _ = await self.manager.async_call_api(
846
+ # '/cloud/v1/deviceManaged/airPurifierScreenCtl', 'post',
847
+ # json_object=body.to_dict(), headers=headers
848
+ # )
849
+ r_dict = await self.call_bypassv1_api(
850
+ RequestPurifier131, method="airPurifierScreenCtl",
851
+ endpoint="airPurifierScreenCtl", update_dict=update_dict
852
+ )
853
+ r = Helpers.process_dev_response(_LOGGER, "toggle_display", self, r_dict)
854
+ if r is None:
855
+ return False
856
+
857
+ self.state.display_set_state = DeviceStatus.from_bool(mode)
858
+ self.state.display_status = DeviceStatus.from_bool(mode)
859
+ self.state.connection_status = ConnectionStatus.ONLINE
860
+ return True
861
+
862
+ async def toggle_switch(self, toggle: bool | None = None) -> bool:
863
+ if toggle is None:
864
+ toggle = self.state.device_status != DeviceStatus.ON
865
+
866
+ update_dict = {
867
+ "status": DeviceStatus.from_bool(toggle).value
868
+ }
869
+ # body = self._build_request('airPurifierPowerSwitchCtl', update_dict=update_dict)
870
+ # headers = Helpers.req_header_bypass()
871
+ # r_dict, _ = await self.manager.async_call_api(
872
+ # '/cloud/v1/deviceManaged/airPurifierPowerSwitchCtl', 'post',
873
+ # json_object=body.to_dict(), headers=headers
874
+ # )
875
+ r_dict = await self.call_bypassv1_api(
876
+ RequestPurifier131, method="airPurifierPowerSwitchCtl",
877
+ endpoint="airPurifierPowerSwitchCtl", update_dict=update_dict
878
+ )
879
+ r = Helpers.process_dev_response(_LOGGER, "toggle_switch", self, r_dict)
880
+ if r is None:
881
+ return False
882
+
883
+ self.state.device_status = DeviceStatus.from_bool(toggle)
884
+ self.state.connection_status = ConnectionStatus.ONLINE
885
+ return True
886
+
887
+ async def set_fan_speed(self, speed: int | None = None) -> bool:
888
+ current_speed = self.state.fan_set_level or 0
889
+
890
+ if speed is not None:
891
+ if speed not in self.fan_levels:
892
+ _LOGGER.debug(
893
+ "%s is invalid speed - valid speeds are %s",
894
+ speed,
895
+ str(self.fan_levels),
896
+ )
897
+ return False
898
+ new_speed = speed
899
+ else:
900
+ new_speed = Helpers.bump_level(current_speed, self.fan_levels)
901
+
902
+ update_dict = {
903
+ "level": new_speed
904
+ }
905
+ # body = self._build_request('airPurifierSpeedCtl', update_dict=update_dict)
906
+ # headers = Helpers.req_header_bypass()
907
+
908
+ # r_dict, _ = await self.manager.async_call_api(
909
+ # '/cloud/v1/deviceManaged/airPurifierSpeedCtl', 'post',
910
+ # json_object=body, headers=headers
911
+ # )
912
+ r_dict = await self.call_bypassv1_api(
913
+ RequestPurifier131Level, method="airPurifierSpeedCtl",
914
+ endpoint="airPurifierSpeedCtl", update_dict=update_dict
915
+ )
916
+ r = Helpers.process_dev_response(_LOGGER, "set_fan_speed", self, r_dict)
917
+ if r is None:
918
+ return False
919
+
920
+ self.state.fan_level = new_speed
921
+ self.state.fan_set_level = new_speed
922
+ self.state.connection_status = 'online'
923
+ self.state.mode = PurifierModes.MANUAL
924
+ return True
925
+
926
+ async def set_mode(self, mode: str) -> bool:
927
+ if mode not in self.modes:
928
+ _LOGGER.debug('Invalid purifier mode used - %s', mode)
929
+ return False
930
+
931
+ update_dict = {
932
+ "mode": mode
933
+ }
934
+ # body = self._build_request('airPurifierModeCtl', update_dict=update_dict)
935
+ # headers = Helpers.req_header_bypass()
936
+
937
+ # r_dict, _ = await self.manager.async_call_api(
938
+ # '/cloud/v1/deviceManaged/airPurifierRunModeCtl', 'post',
939
+ # json_object=body.to_dict(), headers=headers
940
+ # )
941
+ r_dict = await self.call_bypassv1_api(
942
+ RequestPurifier131Mode, method="airPurifierRunModeCtl",
943
+ endpoint="airPurifierRunModeCtl", update_dict=update_dict
944
+ )
945
+ r = Helpers.process_dev_response(_LOGGER, "mode_toggle", self, r_dict)
946
+ if r is None:
947
+ return False
948
+
949
+ self.state.mode = mode
950
+ self.state.connection_status = ConnectionStatus.ONLINE
951
+ return True