pyvesync 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. pyvesync/__init__.py +5 -0
  2. pyvesync/base_devices/__init__.py +35 -0
  3. pyvesync/base_devices/bulb_base.py +330 -0
  4. pyvesync/base_devices/fan_base.py +392 -0
  5. pyvesync/base_devices/fryer_base.py +68 -0
  6. pyvesync/base_devices/humidifier_base.py +462 -0
  7. pyvesync/base_devices/outlet_base.py +327 -0
  8. pyvesync/base_devices/purifier_base.py +525 -0
  9. pyvesync/base_devices/switch_base.py +271 -0
  10. pyvesync/base_devices/thermostat_base.py +283 -0
  11. pyvesync/base_devices/vesyncbasedevice.py +572 -0
  12. pyvesync/const.py +825 -0
  13. pyvesync/device_container.py +373 -0
  14. pyvesync/device_map.py +1172 -0
  15. pyvesync/devices/__init__.py +1 -0
  16. pyvesync/devices/vesyncbulb.py +1126 -0
  17. pyvesync/devices/vesyncfan.py +233 -0
  18. pyvesync/devices/vesynchumidifier.py +808 -0
  19. pyvesync/devices/vesynckitchen.py +651 -0
  20. pyvesync/devices/vesyncoutlet.py +954 -0
  21. pyvesync/devices/vesyncpurifier.py +990 -0
  22. pyvesync/devices/vesyncswitch.py +415 -0
  23. pyvesync/devices/vesyncthermostat.py +171 -0
  24. pyvesync/models/__init__.py +11 -0
  25. pyvesync/models/base_models.py +129 -0
  26. pyvesync/models/bulb_models.py +197 -0
  27. pyvesync/models/bypass_models.py +289 -0
  28. pyvesync/models/fan_models.py +47 -0
  29. pyvesync/models/fryer_models.py +34 -0
  30. pyvesync/models/home_models.py +169 -0
  31. pyvesync/models/humidifier_models.py +202 -0
  32. pyvesync/models/outlet_models.py +220 -0
  33. pyvesync/models/purifier_models.py +261 -0
  34. pyvesync/models/switch_models.py +101 -0
  35. pyvesync/models/thermostat_models.py +134 -0
  36. pyvesync/models/vesync_models.py +439 -0
  37. pyvesync/utils/__init__.py +1 -0
  38. pyvesync/utils/colors.py +250 -0
  39. pyvesync/utils/device_mixins.py +362 -0
  40. pyvesync/utils/enum_utils.py +26 -0
  41. pyvesync/utils/errors.py +867 -0
  42. pyvesync/utils/helpers.py +700 -0
  43. pyvesync/utils/logs.py +525 -0
  44. pyvesync/vesync.py +638 -0
  45. pyvesync/vesynchome.py +152 -0
  46. pyvesync-3.0.0.dist-info/METADATA +567 -0
  47. pyvesync-3.0.0.dist-info/RECORD +50 -0
  48. pyvesync-3.0.0.dist-info/WHEEL +5 -0
  49. pyvesync-3.0.0.dist-info/licenses/LICENSE +21 -0
  50. pyvesync-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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
+ """