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,415 @@
|
|
|
1
|
+
"""Classes for VeSync Switch Devices.
|
|
2
|
+
|
|
3
|
+
This module provides classes for VeSync Switch Devices:
|
|
4
|
+
|
|
5
|
+
1. VeSyncSwitch: Abstract Base class for VeSync Switch Devices.
|
|
6
|
+
2. VeSyncWallSwitch: Class for VeSync Wall Switch Devices ESWL01 and ESWL03.
|
|
7
|
+
3. VeSyncDimmerSwitch: Class for VeSync Dimmer Switch Devices ESWD16.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
feature_dict (dict): Dictionary of switch models and their supported features.
|
|
12
|
+
Defines the class to use for each switch model and the list of features
|
|
13
|
+
switch_modules (dict): Dictionary of switch models as keys and their associated
|
|
14
|
+
classes as string values.
|
|
15
|
+
|
|
16
|
+
Note:
|
|
17
|
+
The switch device is built from the `feature_dict` dictionary and used by the
|
|
18
|
+
`vesync.object_factory` during initial call to pyvesync.vesync.update() and
|
|
19
|
+
determines the class to instantiate for each switch model. These classes should
|
|
20
|
+
not be instantiated manually.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from dataclasses import asdict
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
|
+
|
|
29
|
+
from typing_extensions import deprecated
|
|
30
|
+
|
|
31
|
+
from pyvesync.base_devices.switch_base import VeSyncSwitch
|
|
32
|
+
from pyvesync.const import ConnectionStatus, DeviceStatus
|
|
33
|
+
from pyvesync.models import switch_models
|
|
34
|
+
from pyvesync.models.bypass_models import RequestBypassV1, TimerModels
|
|
35
|
+
from pyvesync.utils.colors import Color
|
|
36
|
+
from pyvesync.utils.device_mixins import BypassV1Mixin, process_bypassv1_result
|
|
37
|
+
from pyvesync.utils.helpers import Helpers, Timer, Validators
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from pyvesync import VeSync
|
|
41
|
+
from pyvesync.device_map import SwitchMap
|
|
42
|
+
from pyvesync.models.vesync_models import ResponseDeviceDetailsModel
|
|
43
|
+
|
|
44
|
+
_LOGGER = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class VeSyncWallSwitch(BypassV1Mixin, VeSyncSwitch):
|
|
48
|
+
"""Etekcity standard wall switch.
|
|
49
|
+
|
|
50
|
+
Inherits from the BypassV1Mixin and VeSyncSwitch classes.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
__slots__ = ()
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self, details: ResponseDeviceDetailsModel, manager: VeSync, feature_map: SwitchMap
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Initialize Etekcity Wall Switch class.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
details (ResponseDeviceDetailsModel): The device details.
|
|
62
|
+
manager (VeSync): The VeSync manager.
|
|
63
|
+
feature_map (SwitchMap): The feature map for the device.
|
|
64
|
+
"""
|
|
65
|
+
super().__init__(details, manager, feature_map)
|
|
66
|
+
|
|
67
|
+
async def get_details(self) -> None:
|
|
68
|
+
r_dict = await self.call_bypassv1_api(
|
|
69
|
+
RequestBypassV1, method='deviceDetail', endpoint='deviceDetail'
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
r = Helpers.process_dev_response(_LOGGER, 'get_details', self, r_dict)
|
|
73
|
+
if r is None:
|
|
74
|
+
return
|
|
75
|
+
resp_model = Helpers.model_maker(
|
|
76
|
+
_LOGGER, switch_models.ResponseSwitchDetails, 'get_details', r, self
|
|
77
|
+
)
|
|
78
|
+
if resp_model is None:
|
|
79
|
+
return
|
|
80
|
+
result = resp_model.result
|
|
81
|
+
if not isinstance(result, switch_models.InternalSwitchResult):
|
|
82
|
+
_LOGGER.warning('Invalid response model for switch details')
|
|
83
|
+
return
|
|
84
|
+
self.state.device_status = result.deviceStatus
|
|
85
|
+
self.state.active_time = result.activeTime
|
|
86
|
+
self.state.connection_status = result.connectionStatus
|
|
87
|
+
|
|
88
|
+
async def toggle_switch(self, toggle: bool | None = None) -> bool:
|
|
89
|
+
"""Toggle switch device."""
|
|
90
|
+
if toggle is None:
|
|
91
|
+
toggle = self.state.device_status != DeviceStatus.ON
|
|
92
|
+
toggle_str = DeviceStatus.from_bool(toggle)
|
|
93
|
+
|
|
94
|
+
r_dict = await self.call_bypassv1_api(
|
|
95
|
+
switch_models.RequestSwitchStatus,
|
|
96
|
+
{'status': toggle_str, 'switchNo': 0},
|
|
97
|
+
'deviceStatus',
|
|
98
|
+
'deviceStatus',
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
r = Helpers.process_dev_response(_LOGGER, 'get_details', self, r_dict)
|
|
102
|
+
if r is None:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
self.state.device_status = DeviceStatus.from_bool(toggle)
|
|
106
|
+
self.state.connection_status = ConnectionStatus.ONLINE
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
async def get_timer(self) -> None:
|
|
110
|
+
r_dict = await self.call_bypassv1_api(
|
|
111
|
+
TimerModels.RequestV1GetTimer, method='getTimers', endpoint='timer/getTimers'
|
|
112
|
+
)
|
|
113
|
+
if r_dict is None:
|
|
114
|
+
return
|
|
115
|
+
result_model = process_bypassv1_result(
|
|
116
|
+
self, _LOGGER, 'get_timer', r_dict, TimerModels.ResultV1GetTimer
|
|
117
|
+
)
|
|
118
|
+
if result_model is None:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
timers = result_model.timers
|
|
122
|
+
if not isinstance(timers, list) or len(timers) == 0:
|
|
123
|
+
_LOGGER.debug('No timers found')
|
|
124
|
+
return
|
|
125
|
+
if len(timers) > 1:
|
|
126
|
+
_LOGGER.debug('More than one timer found, using first timer')
|
|
127
|
+
timer = timers[0]
|
|
128
|
+
if not isinstance(timer, TimerModels.TimerItemV1):
|
|
129
|
+
_LOGGER.warning('Invalid timer model')
|
|
130
|
+
return
|
|
131
|
+
self.state.timer = Timer(
|
|
132
|
+
int(timer.counterTimer),
|
|
133
|
+
action=timer.action,
|
|
134
|
+
id=int(timer.timerID),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
async def set_timer(self, duration: int, action: str | None = None) -> bool:
|
|
138
|
+
if action is None:
|
|
139
|
+
action = (
|
|
140
|
+
DeviceStatus.ON
|
|
141
|
+
if self.state.device_status == DeviceStatus.OFF
|
|
142
|
+
else DeviceStatus.OFF
|
|
143
|
+
)
|
|
144
|
+
if action not in [DeviceStatus.ON, DeviceStatus.OFF]:
|
|
145
|
+
_LOGGER.warning('Invalid action for timer - on/off')
|
|
146
|
+
return False
|
|
147
|
+
update_dict = {
|
|
148
|
+
'action': action,
|
|
149
|
+
'counterTime': str(duration),
|
|
150
|
+
}
|
|
151
|
+
r_dict = await self.call_bypassv1_api(
|
|
152
|
+
TimerModels.RequestV1SetTime,
|
|
153
|
+
update_dict,
|
|
154
|
+
method='addTimer',
|
|
155
|
+
endpoint='timer/addTimer',
|
|
156
|
+
)
|
|
157
|
+
if r_dict is None:
|
|
158
|
+
return False
|
|
159
|
+
result_model = process_bypassv1_result(
|
|
160
|
+
self, _LOGGER, 'set_timer', r_dict, TimerModels.ResultV1SetTimer
|
|
161
|
+
)
|
|
162
|
+
if result_model is None:
|
|
163
|
+
return False
|
|
164
|
+
self.state.timer = Timer(
|
|
165
|
+
int(duration),
|
|
166
|
+
action=action,
|
|
167
|
+
id=int(result_model.timerID),
|
|
168
|
+
)
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
async def clear_timer(self) -> bool:
|
|
172
|
+
if self.state.timer is None:
|
|
173
|
+
_LOGGER.warning('No timer set, run get_timer() first.')
|
|
174
|
+
return False
|
|
175
|
+
update_dict = {
|
|
176
|
+
'timerId': str(self.state.timer.id),
|
|
177
|
+
}
|
|
178
|
+
r_dict = await self.call_bypassv1_api(
|
|
179
|
+
TimerModels.RequestV1ClearTimer,
|
|
180
|
+
update_dict,
|
|
181
|
+
method='deleteTimer',
|
|
182
|
+
endpoint='timer/deleteTimer',
|
|
183
|
+
)
|
|
184
|
+
if r_dict is None:
|
|
185
|
+
return False
|
|
186
|
+
result = Helpers.process_dev_response(_LOGGER, 'clear_timer', self, r_dict)
|
|
187
|
+
if result is None:
|
|
188
|
+
return False
|
|
189
|
+
self.state.timer = None
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class VeSyncDimmerSwitch(BypassV1Mixin, VeSyncSwitch):
|
|
194
|
+
"""Vesync Dimmer Switch Class with RGB Faceplate.
|
|
195
|
+
|
|
196
|
+
Inherits from the BypassV1Mixin and VeSyncSwitch classes.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
__slots__ = ()
|
|
200
|
+
|
|
201
|
+
def __init__(
|
|
202
|
+
self, details: ResponseDeviceDetailsModel, manager: VeSync, feature_map: SwitchMap
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Initialize dimmer switch class.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
details (ResponseDeviceDetailsModel): The device details.
|
|
208
|
+
manager (VeSync): The VeSync manager.
|
|
209
|
+
feature_map (SwitchMap): The feature map for the device.
|
|
210
|
+
"""
|
|
211
|
+
super().__init__(details, manager, feature_map)
|
|
212
|
+
|
|
213
|
+
async def get_details(self) -> None:
|
|
214
|
+
r_bytes = await self.call_bypassv1_api(
|
|
215
|
+
switch_models.RequestDimmerDetails,
|
|
216
|
+
method='deviceDetail',
|
|
217
|
+
endpoint='deviceDetail',
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
r = Helpers.process_dev_response(_LOGGER, 'get_details', self, r_bytes)
|
|
221
|
+
if r is None:
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
resp_model = Helpers.model_maker(
|
|
225
|
+
_LOGGER, switch_models.ResponseSwitchDetails, 'set_timer', r, self
|
|
226
|
+
)
|
|
227
|
+
if resp_model is None:
|
|
228
|
+
return
|
|
229
|
+
result = resp_model.result
|
|
230
|
+
if not isinstance(result, switch_models.InternalDimmerDetailsResult):
|
|
231
|
+
_LOGGER.warning('Invalid response model for dimmer details')
|
|
232
|
+
return
|
|
233
|
+
self.state.active_time = result.activeTime
|
|
234
|
+
self.state.connection_status = result.connectionStatus
|
|
235
|
+
self.state.brightness = result.brightness
|
|
236
|
+
self.state.backlight_status = result.rgbStatus
|
|
237
|
+
new_color = result.rgbValue
|
|
238
|
+
if isinstance(new_color, switch_models.DimmerRGB):
|
|
239
|
+
self.state.backlight_color = Color.from_rgb(
|
|
240
|
+
new_color.red, new_color.green, new_color.blue
|
|
241
|
+
)
|
|
242
|
+
self.state.indicator_status = result.indicatorlightStatus
|
|
243
|
+
self.state.device_status = result.deviceStatus
|
|
244
|
+
|
|
245
|
+
@deprecated('switch_toggle() deprecated, use toggle_switch(toggle: bool | None)')
|
|
246
|
+
async def switch_toggle(self, status: str) -> bool:
|
|
247
|
+
"""Toggle switch status."""
|
|
248
|
+
return await self.toggle_switch(status == DeviceStatus.ON)
|
|
249
|
+
|
|
250
|
+
async def toggle_switch(self, toggle: bool | None = None) -> bool:
|
|
251
|
+
if toggle is None:
|
|
252
|
+
toggle = self.state.device_status == 'off'
|
|
253
|
+
toggle_status = DeviceStatus.from_bool(toggle)
|
|
254
|
+
|
|
255
|
+
r_bytes = await self.call_bypassv1_api(
|
|
256
|
+
switch_models.RequestDimmerStatus,
|
|
257
|
+
{'status': toggle_status},
|
|
258
|
+
'dimmerPowerSwitchCtl',
|
|
259
|
+
'dimmerPowerSwitchCtl',
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
r = Helpers.process_dev_response(_LOGGER, 'toggle_switch', self, r_bytes)
|
|
263
|
+
if r is None:
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
self.state.device_status = toggle_status
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
async def toggle_indicator_light(self, toggle: bool | None = None) -> bool:
|
|
270
|
+
"""Toggle indicator light."""
|
|
271
|
+
if toggle is None:
|
|
272
|
+
toggle = self.state.indicator_status == 'off'
|
|
273
|
+
toggle_status = DeviceStatus.from_bool(toggle)
|
|
274
|
+
|
|
275
|
+
r_bytes = await self.call_bypassv1_api(
|
|
276
|
+
switch_models.RequestDimmerStatus,
|
|
277
|
+
{'status': toggle_status},
|
|
278
|
+
'dimmerIndicatorLightCtl',
|
|
279
|
+
'dimmerIndicatorLightCtl',
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
r = Helpers.process_dev_response(_LOGGER, 'toggle_indicator_light', self, r_bytes)
|
|
283
|
+
if r is None:
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
self.state.indicator_status = toggle_status
|
|
287
|
+
return True
|
|
288
|
+
|
|
289
|
+
async def set_backlight_status(
|
|
290
|
+
self,
|
|
291
|
+
status: bool,
|
|
292
|
+
red: int | None = None,
|
|
293
|
+
green: int | None = None,
|
|
294
|
+
blue: int | None = None,
|
|
295
|
+
) -> bool:
|
|
296
|
+
if red is not None and blue is not None and green is not None:
|
|
297
|
+
new_color = Color.from_rgb(red, green, blue)
|
|
298
|
+
else:
|
|
299
|
+
new_color = None
|
|
300
|
+
status_str = DeviceStatus.from_bool(status)
|
|
301
|
+
|
|
302
|
+
update_dict: dict[str, str | dict] = {'status': status_str.value}
|
|
303
|
+
if new_color is not None:
|
|
304
|
+
update_dict['rgbValue'] = asdict(new_color.rgb)
|
|
305
|
+
r_bytes = await self.call_bypassv1_api(
|
|
306
|
+
switch_models.RequestDimmerStatus,
|
|
307
|
+
update_dict,
|
|
308
|
+
'dimmerRgbValueCtl',
|
|
309
|
+
'dimmerRgbValueCtl',
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
r = Helpers.process_dev_response(_LOGGER, 'set_rgb_backlight', self, r_bytes)
|
|
313
|
+
if r is None:
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
self.state.backlight_status = status_str
|
|
317
|
+
if new_color is not None:
|
|
318
|
+
self.state.backlight_color = new_color
|
|
319
|
+
self.state.backlight_status = status_str
|
|
320
|
+
self.state.device_status = DeviceStatus.ON
|
|
321
|
+
self.state.connection_status = ConnectionStatus.ONLINE
|
|
322
|
+
return True
|
|
323
|
+
|
|
324
|
+
async def set_brightness(self, brightness: int) -> bool:
|
|
325
|
+
"""Set brightness of dimmer - 1 - 100."""
|
|
326
|
+
if not Validators.validate_zero_to_hundred(brightness):
|
|
327
|
+
_LOGGER.warning('Invalid brightness - must be between 0 and 100')
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
r_bytes = await self.call_bypassv1_api(
|
|
331
|
+
switch_models.RequestDimmerBrightness,
|
|
332
|
+
{'brightness': brightness},
|
|
333
|
+
'dimmerBrightnessCtl',
|
|
334
|
+
'dimmerBrightnessCtl',
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
r = Helpers.process_dev_response(_LOGGER, 'get_details', self, r_bytes)
|
|
338
|
+
if r is None:
|
|
339
|
+
return False
|
|
340
|
+
|
|
341
|
+
self.state.brightness = brightness
|
|
342
|
+
self.state.device_status = DeviceStatus.ON
|
|
343
|
+
self.state.connection_status = ConnectionStatus.ONLINE
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
async def get_timer(self) -> None:
|
|
347
|
+
r_dict = await self.call_bypassv1_api(
|
|
348
|
+
TimerModels.RequestV1GetTimer, method='getTimers', endpoint='timer/getTimers'
|
|
349
|
+
)
|
|
350
|
+
result_model = process_bypassv1_result(
|
|
351
|
+
self, _LOGGER, 'get_timer', r_dict, TimerModels.ResultV1GetTimer
|
|
352
|
+
)
|
|
353
|
+
if result_model is None:
|
|
354
|
+
return
|
|
355
|
+
timers = result_model.timers
|
|
356
|
+
if not isinstance(timers, list) or len(timers) == 0:
|
|
357
|
+
_LOGGER.info('No timers found')
|
|
358
|
+
return
|
|
359
|
+
if len(timers) > 1:
|
|
360
|
+
_LOGGER.debug('More than one timer found, using first timer')
|
|
361
|
+
timer = timers[0]
|
|
362
|
+
if not isinstance(timer, TimerModels.TimeItemV1):
|
|
363
|
+
_LOGGER.warning('Invalid timer model')
|
|
364
|
+
return
|
|
365
|
+
self.state.timer = Timer(
|
|
366
|
+
int(timer.counterTime),
|
|
367
|
+
action=timer.action,
|
|
368
|
+
id=int(timer.timerID),
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
async def set_timer(self, duration: int, action: str | None = None) -> bool:
|
|
372
|
+
if action is None:
|
|
373
|
+
action = (
|
|
374
|
+
DeviceStatus.ON
|
|
375
|
+
if self.state.device_status == DeviceStatus.OFF
|
|
376
|
+
else DeviceStatus.OFF
|
|
377
|
+
)
|
|
378
|
+
if action not in [DeviceStatus.ON, DeviceStatus.OFF]:
|
|
379
|
+
_LOGGER.warning('Invalid action for timer - on/off')
|
|
380
|
+
return False
|
|
381
|
+
update_dict = {'action': action, 'counterTime': str(duration), 'status': '1'}
|
|
382
|
+
r_dict = await self.call_bypassv1_api(
|
|
383
|
+
TimerModels.RequestV1SetTime,
|
|
384
|
+
update_dict,
|
|
385
|
+
method='addTimer',
|
|
386
|
+
endpoint='timer/addTimer',
|
|
387
|
+
)
|
|
388
|
+
result_model = process_bypassv1_result(
|
|
389
|
+
self, _LOGGER, 'set_timer', r_dict, TimerModels.ResultV1SetTimer
|
|
390
|
+
)
|
|
391
|
+
if result_model is None:
|
|
392
|
+
return False
|
|
393
|
+
self.state.timer = Timer(
|
|
394
|
+
int(duration),
|
|
395
|
+
action=action,
|
|
396
|
+
id=int(result_model.timerID),
|
|
397
|
+
)
|
|
398
|
+
return True
|
|
399
|
+
|
|
400
|
+
async def clear_timer(self) -> bool:
|
|
401
|
+
if self.state.timer is None:
|
|
402
|
+
_LOGGER.debug('No timer set, run get_timer() first.')
|
|
403
|
+
return False
|
|
404
|
+
update_dict = {'timerId': str(self.state.timer.id), 'status': '1'}
|
|
405
|
+
r_dict = await self.call_bypassv1_api(
|
|
406
|
+
TimerModels.RequestV1ClearTimer,
|
|
407
|
+
update_dict,
|
|
408
|
+
method='deleteTimer',
|
|
409
|
+
endpoint='timer/deleteTimer',
|
|
410
|
+
)
|
|
411
|
+
result = Helpers.process_dev_response(_LOGGER, 'clear_timer', self, r_dict)
|
|
412
|
+
if result is None:
|
|
413
|
+
return False
|
|
414
|
+
self.state.timer = None
|
|
415
|
+
return True
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Thermostat device classes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from pyvesync.base_devices.thermostat_base import VeSyncThermostat
|
|
9
|
+
from pyvesync.const import (
|
|
10
|
+
ThermostatConst,
|
|
11
|
+
ThermostatEcoTypes,
|
|
12
|
+
ThermostatFanModes,
|
|
13
|
+
ThermostatHoldStatus,
|
|
14
|
+
ThermostatWorkModes,
|
|
15
|
+
)
|
|
16
|
+
from pyvesync.models.thermostat_models import (
|
|
17
|
+
ResultThermostatDetails,
|
|
18
|
+
ThermostatMinorDetails,
|
|
19
|
+
)
|
|
20
|
+
from pyvesync.utils.device_mixins import BypassV2Mixin, process_bypassv2_result
|
|
21
|
+
from pyvesync.utils.helpers import Helpers, Validators
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from pyvesync import VeSync
|
|
25
|
+
from pyvesync.device_map import ThermostatMap
|
|
26
|
+
from pyvesync.models.vesync_models import ResponseDeviceDetailsModel
|
|
27
|
+
|
|
28
|
+
_LOGGER = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class VeSyncAuraThermostat(BypassV2Mixin, VeSyncThermostat):
|
|
32
|
+
"""VeSync Aura Thermostat."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
details: ResponseDeviceDetailsModel,
|
|
37
|
+
manager: VeSync,
|
|
38
|
+
feature_map: ThermostatMap,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Initialize VeSync Aura Thermostat."""
|
|
41
|
+
super().__init__(details, manager, feature_map)
|
|
42
|
+
|
|
43
|
+
def _process_details(self, details: ResultThermostatDetails) -> None:
|
|
44
|
+
"""Internal method to process thermostat details."""
|
|
45
|
+
if ResultThermostatDetails.supportMode is not None:
|
|
46
|
+
self.supported_work_modes = [
|
|
47
|
+
ThermostatWorkModes(mode) for mode in ResultThermostatDetails.supportMode
|
|
48
|
+
]
|
|
49
|
+
self.state.work_mode = ThermostatConst.WorkMode(details.workMode)
|
|
50
|
+
self.state.work_status = ThermostatConst.WorkStatus(details.workStatus)
|
|
51
|
+
self.state.fan_status = ThermostatConst.FanStatus(details.fanStatus)
|
|
52
|
+
self.state.fan_mode = ThermostatConst.FanMode(details.fanMode)
|
|
53
|
+
self.state.temperature_unit = details.tempUnit
|
|
54
|
+
self.state.temperature = details.temperature
|
|
55
|
+
self.state.humidity = details.humidity
|
|
56
|
+
self.state.heat_to_temp = details.heatToTemp
|
|
57
|
+
self.state.cool_to_temp = details.coolToTemp
|
|
58
|
+
self.state.deadband = details.deadband
|
|
59
|
+
self.state.lock_status = details.lockStatus
|
|
60
|
+
self.state.schedule_or_hold = ThermostatConst.ScheduleOrHoldOption(
|
|
61
|
+
details.scheduleOrHold
|
|
62
|
+
)
|
|
63
|
+
self.state.hold_end_time = details.holdEndTime
|
|
64
|
+
self.state.hold_option = ThermostatConst.HoldOption(details.holdOption)
|
|
65
|
+
self.state.eco_type = ThermostatConst.EcoType(details.ecoType)
|
|
66
|
+
self.state.alert_status = details.alertStatus
|
|
67
|
+
self.state.routine_running_id = details.routineRunningId
|
|
68
|
+
self.state.routines = details.routines
|
|
69
|
+
|
|
70
|
+
async def get_details(self) -> None:
|
|
71
|
+
"""Get the details of the thermostat."""
|
|
72
|
+
r_dict = await self.call_bypassv2_api('getTsStatus')
|
|
73
|
+
r_model = process_bypassv2_result(
|
|
74
|
+
self, _LOGGER, 'get_details', r_dict, ResultThermostatDetails
|
|
75
|
+
)
|
|
76
|
+
if r_model is None:
|
|
77
|
+
return
|
|
78
|
+
self._process_details(r_model)
|
|
79
|
+
|
|
80
|
+
async def get_configuration(self) -> None:
|
|
81
|
+
"""Get configuration or 'minor details'."""
|
|
82
|
+
r_dict = await self.call_bypassv2_api('getTsMinorInfo')
|
|
83
|
+
result = process_bypassv2_result(
|
|
84
|
+
self, _LOGGER, 'get_configuration', r_dict, ThermostatMinorDetails
|
|
85
|
+
)
|
|
86
|
+
if result is None:
|
|
87
|
+
return
|
|
88
|
+
self.state.configuration = result
|
|
89
|
+
|
|
90
|
+
async def _set_hold_status_api(
|
|
91
|
+
self,
|
|
92
|
+
key: str | None = None,
|
|
93
|
+
temp: float | None = None,
|
|
94
|
+
hold_status: ThermostatHoldStatus | None = None,
|
|
95
|
+
) -> bool:
|
|
96
|
+
"""Internal function to call the 'setHoldStatus' API."""
|
|
97
|
+
if key is None and temp is None and hold_status is None:
|
|
98
|
+
_LOGGER.debug('At least one of key, temp, or hold_status must be provided.')
|
|
99
|
+
return False
|
|
100
|
+
payload_data: dict[str, str | float] = {}
|
|
101
|
+
if hold_status is not None and hold_status.value == ThermostatHoldStatus.CANCEL:
|
|
102
|
+
payload_data = {'holdStatus': ThermostatHoldStatus.CANCEL}
|
|
103
|
+
else:
|
|
104
|
+
if key is None or temp is None:
|
|
105
|
+
_LOGGER.debug('Either key or temp must be provided.')
|
|
106
|
+
return False
|
|
107
|
+
payload_data = {key: temp, 'holdStatus': ThermostatHoldStatus.SET.value}
|
|
108
|
+
r_dict = await self.call_bypassv2_api('setHoldStatus', data=payload_data)
|
|
109
|
+
result = Helpers.process_dev_response(_LOGGER, 'setHoldStatus', self, r_dict)
|
|
110
|
+
return bool(result)
|
|
111
|
+
|
|
112
|
+
async def set_temp_point(self, temperature: float) -> bool:
|
|
113
|
+
return await self._set_hold_status_api('setTempPoint', temperature)
|
|
114
|
+
|
|
115
|
+
async def cancel_hold(self) -> bool:
|
|
116
|
+
"""Cancel hold."""
|
|
117
|
+
return await self._set_hold_status_api(hold_status=ThermostatHoldStatus.CANCEL)
|
|
118
|
+
|
|
119
|
+
async def set_cool_to_temp(self, temperature: float) -> bool:
|
|
120
|
+
"""Set cool to temperature."""
|
|
121
|
+
return await self._set_hold_status_api('coolToTemp', temperature)
|
|
122
|
+
|
|
123
|
+
async def set_heat_to_temp(self, temperature: float) -> bool:
|
|
124
|
+
"""Set heat to temperature."""
|
|
125
|
+
return await self._set_hold_status_api('heatToTemp', temperature)
|
|
126
|
+
|
|
127
|
+
async def set_mode(self, mode: ThermostatWorkModes) -> bool:
|
|
128
|
+
"""Set thermostat mode."""
|
|
129
|
+
if mode not in self.supported_work_modes:
|
|
130
|
+
_LOGGER.debug('Invalid mode: %s', mode)
|
|
131
|
+
return False
|
|
132
|
+
payload_data = {'tsMode': mode}
|
|
133
|
+
r_dict = await self.call_bypassv2_api('setTsMode', data=payload_data)
|
|
134
|
+
result = Helpers.process_dev_response(_LOGGER, 'setTsMode', self, r_dict)
|
|
135
|
+
return bool(result)
|
|
136
|
+
|
|
137
|
+
async def set_fan_mode(self, mode: ThermostatFanModes) -> bool:
|
|
138
|
+
"""Set thermostat fan mode."""
|
|
139
|
+
if mode not in self.fan_modes:
|
|
140
|
+
_LOGGER.debug('Invalid fan mode: %s', mode)
|
|
141
|
+
return False
|
|
142
|
+
payload_data = {'fanMode': mode.value}
|
|
143
|
+
r_dict = await self.call_bypassv2_api('setFanMode', data=payload_data)
|
|
144
|
+
result = Helpers.process_dev_response(_LOGGER, 'setFanMode', self, r_dict)
|
|
145
|
+
return bool(result)
|
|
146
|
+
|
|
147
|
+
async def toggle_lock(self, toggle: bool, pin: int | str | None = None) -> bool:
|
|
148
|
+
"""Toggle thermostat lock status."""
|
|
149
|
+
if toggle is True and pin is None:
|
|
150
|
+
_LOGGER.debug('PIN required to lock the thermostat.')
|
|
151
|
+
return False
|
|
152
|
+
payload_data: dict[str, bool | str] = {'lockStatus': toggle}
|
|
153
|
+
if toggle is True:
|
|
154
|
+
pin_int = int(pin) if isinstance(pin, str) else pin
|
|
155
|
+
if not Validators.validate_range(pin_int, 0, 9999):
|
|
156
|
+
_LOGGER.debug('PIN must be between 0 and 9999.')
|
|
157
|
+
return False
|
|
158
|
+
payload_data['lockPinCode'] = f'{pin_int:0>4}'
|
|
159
|
+
r_dict = await self.call_bypassv2_api('setLockStatus', data=payload_data)
|
|
160
|
+
result = Helpers.process_dev_response(_LOGGER, 'toggle_lock_status', self, r_dict)
|
|
161
|
+
return bool(result)
|
|
162
|
+
|
|
163
|
+
async def set_eco_type(self, eco_type: ThermostatEcoTypes) -> bool:
|
|
164
|
+
"""Set thermostat eco type."""
|
|
165
|
+
if eco_type not in self.eco_types:
|
|
166
|
+
_LOGGER.debug('Invalid eco type: %s', eco_type)
|
|
167
|
+
return False
|
|
168
|
+
payload_data = {'ecoType': eco_type.value}
|
|
169
|
+
r_dict = await self.call_bypassv2_api('setECOType', data=payload_data)
|
|
170
|
+
result = Helpers.process_dev_response(_LOGGER, 'setEcoType', self, r_dict)
|
|
171
|
+
return bool(result)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Data models for VeSync API requests and responses.
|
|
2
|
+
|
|
3
|
+
Models should follow the naming convention of Request/Response + API Name.
|
|
4
|
+
Internal models can have any descriptive name.
|
|
5
|
+
|
|
6
|
+
The `base_models` module contains the base classes for the VeSync API models, while the
|
|
7
|
+
models for each device type and other API calls are stored in their respective modules.
|
|
8
|
+
|
|
9
|
+
The [bypassv2_models][pyvesync.models.bypass_models] module contains the models for
|
|
10
|
+
the common bypassV2 API calls.
|
|
11
|
+
"""
|