pyvesync 3.0.0b0__py3-none-any.whl

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