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