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,954 @@
1
+ """Etekcity Outlets."""
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.outlet_base import VeSyncOutlet
11
+ from pyvesync.const import STATUS_OK, ConnectionStatus, DeviceStatus
12
+ from pyvesync.models.base_models import DefaultValues, RequestHeaders
13
+ from pyvesync.models.bypass_models import TimerModels
14
+ from pyvesync.models.outlet_models import (
15
+ Request15ADetails,
16
+ Request15ANightlight,
17
+ Request15AStatus,
18
+ RequestOutdoorStatus,
19
+ Response7AOutlet,
20
+ Response10ADetails,
21
+ Response15ADetails,
22
+ ResponseBSDGO1OutletResult,
23
+ ResponseOutdoorDetails,
24
+ ResultESW10Details,
25
+ Timer7AItem,
26
+ )
27
+ from pyvesync.utils.device_mixins import (
28
+ BypassV1Mixin,
29
+ BypassV2Mixin,
30
+ process_bypassv1_result,
31
+ process_bypassv2_result,
32
+ )
33
+ from pyvesync.utils.helpers import Helpers, Timer
34
+ from pyvesync.utils.logs import LibraryLogger
35
+
36
+ if TYPE_CHECKING:
37
+ from pyvesync import VeSync
38
+ from pyvesync.device_map import OutletMap
39
+ from pyvesync.models.vesync_models import ResponseDeviceDetailsModel
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ class VeSyncOutlet7A(VeSyncOutlet):
45
+ """Etekcity 7A Round Outlet Class.
46
+
47
+ Args:
48
+ details (ResponseDeviceDetailsModel): The device details.
49
+ manager (VeSync): The VeSync manager.
50
+ feature_map (OutletMap): The feature map for the device.
51
+
52
+ Attributes:
53
+ state (OutletState): The state of the outlet.
54
+ last_response (ResponseInfo): Last response from API call.
55
+ device_status (str): Device status.
56
+ connection_status (str): Connection status.
57
+ manager (VeSync): Manager object for API calls.
58
+ device_name (str): Name of device.
59
+ device_image (str): URL for device image.
60
+ cid (str): Device ID.
61
+ connection_type (str): Connection type of device.
62
+ device_type (str): Type of device.
63
+ type (str): Type of device.
64
+ uuid (str): UUID of device, not always present.
65
+ config_module (str): Configuration module of device.
66
+ mac_id (str): MAC ID of device.
67
+ current_firm_version (str): Current firmware version of device.
68
+ device_region (str): Region of device. (US, EU, etc.)
69
+ pid (str): Product ID of device, pulled by some devices on update.
70
+ sub_device_no (int): Sub-device number of device.
71
+ product_type (str): Product type of device.
72
+ features (dict): Features of device.
73
+ """
74
+
75
+ __slots__ = ()
76
+
77
+ def __init__(
78
+ self, details: ResponseDeviceDetailsModel, manager: VeSync, feature_map: OutletMap
79
+ ) -> None:
80
+ """Initialize Etekcity 7A round outlet class."""
81
+ super().__init__(details, manager, feature_map)
82
+
83
+ def _build_headers(self) -> dict:
84
+ """Build 7A Outlet Request Headers."""
85
+ headers = RequestHeaders.copy()
86
+ headers.update(
87
+ {
88
+ 'tz': self.manager.time_zone,
89
+ 'tk': self.manager.token,
90
+ 'accountid': self.manager.account_id,
91
+ }
92
+ )
93
+ return headers
94
+
95
+ async def get_details(self) -> None:
96
+ r_dict, _ = await self.manager.async_call_api(
97
+ '/v1/device/' + self.cid + '/detail',
98
+ 'get',
99
+ headers=self._build_headers(),
100
+ )
101
+
102
+ if not isinstance(r_dict, dict):
103
+ LibraryLogger.log_device_api_response_error(
104
+ logger,
105
+ self.device_name,
106
+ self.device_type,
107
+ 'get_details',
108
+ 'Response is not valid JSON',
109
+ )
110
+ return
111
+
112
+ if 'error' in r_dict:
113
+ _ = Helpers.process_dev_response(logger, 'get_details', self, r_dict)
114
+ return
115
+ self.state.update_ts()
116
+ resp_model = Response7AOutlet.from_dict(r_dict)
117
+ self.state.connection_status = ConnectionStatus.ONLINE
118
+ self.state.device_status = resp_model.deviceStatus
119
+ self.state.active_time = resp_model.activeTime
120
+ self.state.energy = resp_model.energy
121
+ self.state.power = self.parse_energy_detail(resp_model.power)
122
+ self.state.voltage = self.parse_energy_detail(resp_model.voltage)
123
+
124
+ @staticmethod
125
+ def parse_energy_detail(energy: str | float) -> float:
126
+ """Parse energy details to be compatible with new and old firmware."""
127
+ try:
128
+ if isinstance(energy, str) and ':' in energy:
129
+ power = round(float(Helpers.calculate_hex(energy)), 2)
130
+ else:
131
+ power = float(energy)
132
+ except ValueError:
133
+ logger.debug('Error parsing power response - %s', energy)
134
+ power = 0
135
+ return power
136
+
137
+ async def toggle_switch(self, toggle: bool | None = None) -> bool:
138
+ if toggle is None:
139
+ toggle = self.state.device_status != DeviceStatus.ON
140
+ toggle_str = DeviceStatus.ON if toggle else DeviceStatus.OFF
141
+ r_dict, status_code = await self.manager.async_call_api(
142
+ f'/v1/wifi-switch-1.3/{self.cid}/status/{toggle_str}',
143
+ 'put',
144
+ headers=Helpers.req_legacy_headers(self.manager),
145
+ )
146
+
147
+ if status_code != STATUS_OK:
148
+ LibraryLogger.log_device_api_response_error(
149
+ logger,
150
+ self.device_name,
151
+ self.device_type,
152
+ 'toggle_switch',
153
+ 'Response code is not 200',
154
+ )
155
+ return False
156
+
157
+ if isinstance(r_dict, dict) and 'error' in r_dict:
158
+ _ = Helpers.process_dev_response(logger, 'get_details', self, r_dict)
159
+ return False
160
+
161
+ self.state.update_ts()
162
+ self.state.device_status = toggle_str
163
+ self.state.connection_status = ConnectionStatus.ONLINE
164
+ return True
165
+
166
+ async def get_timer(self) -> None:
167
+ r_dict, status_code = await self.manager.async_call_api(
168
+ f'/v2/device/{self.cid}/timer',
169
+ 'get',
170
+ headers=Helpers.req_legacy_headers(self.manager),
171
+ )
172
+ if not r_dict or status_code != STATUS_OK:
173
+ logger.debug('No timer set.')
174
+ self.state.timer = None
175
+ return
176
+ if isinstance(r_dict, list) and len(r_dict) > 0:
177
+ timer_model = Helpers.model_maker(logger, Timer7AItem, 'get_timer', r_dict[0])
178
+ if timer_model is None:
179
+ self.state.timer = None
180
+ return
181
+ self.state.timer = Timer(
182
+ timer_duration=int(timer_model.counterTimer),
183
+ id=int(timer_model.timerID),
184
+ action=timer_model.action,
185
+ )
186
+ if timer_model.timerStatus == 'off':
187
+ self.state.timer.pause()
188
+ return
189
+ self.state.timer = None
190
+
191
+ async def set_timer(self, duration: int, action: str | None = None) -> bool:
192
+ if action is None:
193
+ action = (
194
+ DeviceStatus.ON
195
+ if self.state.device_status == DeviceStatus.OFF
196
+ else DeviceStatus.OFF
197
+ )
198
+ if not isinstance(action, str) or action not in [
199
+ DeviceStatus.ON,
200
+ DeviceStatus.OFF,
201
+ ]:
202
+ logger.error('Invalid action for timer - %s', action)
203
+ return False
204
+ update_dict = {
205
+ 'action': action,
206
+ 'counterTimer': duration,
207
+ 'timerStatus': 'start',
208
+ 'conflictAwayIds': [],
209
+ 'conflictScheduleIds': [],
210
+ 'conflictTimerIds': [],
211
+ }
212
+ r_dict, status_code = await self.manager.async_call_api(
213
+ f'/v2/device/{self.cid}/timer',
214
+ 'post',
215
+ headers=Helpers.req_legacy_headers(self.manager),
216
+ json_object=update_dict,
217
+ )
218
+ if status_code != STATUS_OK or not isinstance(r_dict, dict):
219
+ logger.debug('Failed to set timer.')
220
+ return False
221
+
222
+ if 'error' in r_dict:
223
+ logger.debug('Error in response: %s', r_dict['error'])
224
+ return False
225
+
226
+ result_model = Helpers.model_maker(
227
+ logger, TimerModels.ResultV1SetTimer, 'set_timer', r_dict, self
228
+ )
229
+ if result_model is None:
230
+ logger.debug('Failed to set timer.')
231
+ return False
232
+ if result_model.timerID == '':
233
+ logger.debug('Unable to set timer.')
234
+ if result_model.conflictTimerIds:
235
+ logger.debug('Conflicting timer IDs - %s', result_model.conflictTimerIds)
236
+ return False
237
+ self.state.timer = Timer(duration, action, int(result_model.timerID))
238
+ return True
239
+
240
+ async def clear_timer(self) -> bool:
241
+ if self.state.timer is None:
242
+ logger.debug('No timer set, nothing to clear, run get_timer().')
243
+ return False
244
+ _, status_code = await self.manager.async_call_api(
245
+ f'/v2/device/{self.cid}/timer/{self.state.timer.id}',
246
+ 'delete',
247
+ headers=Helpers.req_legacy_headers(self.manager),
248
+ )
249
+ if status_code != STATUS_OK:
250
+ return False
251
+ self.state.timer = None
252
+ return True
253
+
254
+
255
+ class VeSyncOutlet10A(VeSyncOutlet):
256
+ """Etekcity 10A Round Outlets.
257
+
258
+ Args:
259
+ details (ResponseDeviceDetailsModel): The device details.
260
+ manager (VeSync): The VeSync manager.
261
+ feature_map (OutletMap): The feature map for the device.
262
+
263
+ Attributes:
264
+ state (OutletState): The state of the outlet.
265
+ last_response (ResponseInfo): Last response from API call.
266
+ device_status (str): Device status.
267
+ connection_status (str): Connection status.
268
+ manager (VeSync): Manager object for API calls.
269
+ device_name (str): Name of device.
270
+ device_image (str): URL for device image.
271
+ cid (str): Device ID.
272
+ connection_type (str): Connection type of device.
273
+ device_type (str): Type of device.
274
+ type (str): Type of device.
275
+ uuid (str): UUID of device, not always present.
276
+ config_module (str): Configuration module of device.
277
+ mac_id (str): MAC ID of device.
278
+ current_firm_version (str): Current firmware version of device.
279
+ device_region (str): Region of device. (US, EU, etc.)
280
+ pid (str): Product ID of device, pulled by some devices on update.
281
+ sub_device_no (int): Sub-device number of device.
282
+ product_type (str): Product type of device.
283
+ features (dict): Features of device.
284
+ """
285
+
286
+ __slots__ = ()
287
+
288
+ def __init__(
289
+ self, details: ResponseDeviceDetailsModel, manager: VeSync, feature_map: OutletMap
290
+ ) -> None:
291
+ """Initialize 10A outlet class."""
292
+ super().__init__(details, manager, feature_map)
293
+ self.request_keys = [
294
+ 'acceptLanguage',
295
+ 'appVersion',
296
+ 'accountId',
297
+ 'mobileId',
298
+ 'phoneBrand',
299
+ 'phoneOS',
300
+ 'timeZone',
301
+ 'token',
302
+ 'traceId',
303
+ 'uuid',
304
+ ]
305
+
306
+ def _build_headers(self) -> dict:
307
+ """Build auth headers for 10A Outlet."""
308
+ headers = RequestHeaders.copy()
309
+ headers.update(
310
+ {
311
+ 'tz': self.manager.time_zone,
312
+ 'tk': self.manager.token,
313
+ 'accountid': self.manager.account_id,
314
+ }
315
+ )
316
+ return headers
317
+
318
+ def _build_detail_request(self, method: str) -> dict:
319
+ """Build 10A Outlet Request."""
320
+ body = Helpers.get_class_attributes(DefaultValues, self.request_keys)
321
+ body.update(Helpers.get_class_attributes(self.manager, self.request_keys))
322
+ body.update(Helpers.get_class_attributes(self, self.request_keys))
323
+ body['method'] = method
324
+ return body
325
+
326
+ def _build_status_request(self, status: str) -> dict:
327
+ """Build 10A Outlet Request to set status."""
328
+ status_keys = ['accountID', 'token', 'timeZone', 'uuid']
329
+ body = Helpers.get_class_attributes(self.manager, status_keys)
330
+ body.update(Helpers.get_class_attributes(self, status_keys))
331
+ body['status'] = status
332
+ return body
333
+
334
+ async def get_details(self) -> None:
335
+ body = self._build_detail_request('devicedetail')
336
+
337
+ r_dict, _ = await self.manager.async_call_api(
338
+ '/10a/v1/device/devicedetail',
339
+ 'post',
340
+ headers=Helpers.req_legacy_headers(self.manager),
341
+ json_object=body,
342
+ )
343
+ r = Helpers.process_dev_response(logger, 'get_details', self, r_dict)
344
+ if r is None:
345
+ return
346
+
347
+ resp_model = Response10ADetails.from_dict(r)
348
+
349
+ self.state.device_status = resp_model.deviceStatus or 'off'
350
+ self.state.connection_status = resp_model.connectionStatus or 'offline'
351
+ self.state.energy = resp_model.energy or 0
352
+ self.state.power = resp_model.power or 0
353
+ self.state.voltage = resp_model.voltage or 0
354
+
355
+ async def toggle_switch(self, toggle: bool | None = None) -> bool:
356
+ if toggle is None:
357
+ toggle = self.state.device_status != DeviceStatus.ON
358
+ toggle_str = DeviceStatus.ON if toggle else DeviceStatus.OFF
359
+ body = self._build_status_request(toggle_str)
360
+ headers = self._build_headers()
361
+
362
+ r_dict, _ = await self.manager.async_call_api(
363
+ '/10a/v1/device/devicestatus',
364
+ 'put',
365
+ headers=headers,
366
+ json_object=body,
367
+ )
368
+ response = Helpers.process_dev_response(logger, 'toggle_switch', self, r_dict)
369
+ if response is None:
370
+ return False
371
+
372
+ self.state.device_status = toggle_str
373
+ self.state.connection_status = ConnectionStatus.ONLINE
374
+ return True
375
+
376
+
377
+ class VeSyncOutlet15A(BypassV1Mixin, VeSyncOutlet):
378
+ """Class for Etekcity 15A Rectangular Outlets.
379
+
380
+ Args:
381
+ details (ResponseDeviceDetailsModel): The device details.
382
+ manager (VeSync): The VeSync manager.
383
+ feature_map (OutletMap): The feature map for the device.
384
+
385
+ Attributes:
386
+ state (OutletState): The state of the outlet.
387
+ last_response (ResponseInfo): Last response from API call.
388
+ device_status (str): Device status.
389
+ connection_status (str): Connection status.
390
+ manager (VeSync): Manager object for API calls.
391
+ device_name (str): Name of device.
392
+ device_image (str): URL for device image.
393
+ cid (str): Device ID.
394
+ connection_type (str): Connection type of device.
395
+ device_type (str): Type of device.
396
+ type (str): Type of device.
397
+ uuid (str): UUID of device, not always present.
398
+ config_module (str): Configuration module of device.
399
+ mac_id (str): MAC ID of device.
400
+ current_firm_version (str): Current firmware version of device.
401
+ device_region (str): Region of device. (US, EU, etc.)
402
+ pid (str): Product ID of device, pulled by some devices on update.
403
+ sub_device_no (int): Sub-device number of device.
404
+ product_type (str): Product type of device.
405
+ features (dict): Features of device.
406
+ """
407
+
408
+ __slots__ = ()
409
+
410
+ def __init__(
411
+ self, details: ResponseDeviceDetailsModel, manager: VeSync, feature_map: OutletMap
412
+ ) -> None:
413
+ """Initialize 15A rectangular outlets."""
414
+ super().__init__(details, manager, feature_map)
415
+
416
+ async def get_details(self) -> None:
417
+ r_dict = await self.call_bypassv1_api(
418
+ Request15ADetails, method='deviceDetail', endpoint='deviceDetail'
419
+ )
420
+
421
+ r = Helpers.process_dev_response(logger, 'get_details', self, r_dict)
422
+ if r is None:
423
+ return
424
+
425
+ resp_model = Response15ADetails.from_dict(r)
426
+ result = resp_model.result
427
+ self.state.device_status = result.deviceStatus
428
+ self.state.connection_status = result.connectionStatus
429
+ self.state.nightlight_status = result.nightLightStatus
430
+ self.state.nightlight_brightness = result.nightLightBrightness
431
+ self.state.nightlight_automode = result.nightLightAutoMode
432
+ self.state.active_time = result.activeTime
433
+ self.state.power = result.power or 0
434
+ self.state.voltage = result.voltage or 0
435
+ self.state.energy = result.energy or 0
436
+
437
+ async def toggle_switch(self, toggle: bool | None = None) -> bool:
438
+ if toggle is None:
439
+ toggle = self.state.device_status != DeviceStatus.ON
440
+ toggle_str = DeviceStatus.ON if toggle else DeviceStatus.OFF
441
+ r_dict = await self.call_bypassv1_api(
442
+ Request15AStatus,
443
+ update_dict={'status': toggle_str},
444
+ method='deviceStatus',
445
+ endpoint='deviceStatus',
446
+ )
447
+ response = Helpers.process_dev_response(logger, 'toggle_switch', self, r_dict)
448
+ if response is None:
449
+ return False
450
+
451
+ self.state.device_status = toggle_str
452
+ self.state.connection_status = ConnectionStatus.ONLINE
453
+ return True
454
+
455
+ async def set_nightlight_state(self, mode: str) -> bool:
456
+ """Set nightlight state for 15A Outlets."""
457
+ if mode.lower() not in self.nightlight_modes:
458
+ logger.error('Invalid nightlight mode - %s', mode)
459
+ return False
460
+ mode = mode.lower()
461
+ r_dict = await self.call_bypassv1_api(
462
+ Request15ANightlight,
463
+ update_dict={'mode': mode},
464
+ method='outletNightLightCtl',
465
+ endpoint='outletNightLightCtl',
466
+ )
467
+
468
+ response = Helpers.process_dev_response(
469
+ logger, 'set_nightlight_state', self, r_dict
470
+ )
471
+ if response is None:
472
+ return False
473
+
474
+ self.state.nightlight_status = mode
475
+ self.state.connection_status = ConnectionStatus.ONLINE
476
+ return True
477
+
478
+ async def get_timer(self) -> None:
479
+ method = 'getTimers'
480
+ endpoint = f'/timer/{method}'
481
+ r_dict = await self.call_bypassv1_api(
482
+ Request15ADetails, method=method, endpoint=endpoint
483
+ )
484
+ result_model = process_bypassv1_result(
485
+ self, logger, 'get_timer', r_dict, TimerModels.ResultV1GetTimer
486
+ )
487
+ if result_model is None:
488
+ return
489
+ timers = result_model.timers
490
+ if not isinstance(timers, list) or len(timers) == 0:
491
+ self.state.timer = None
492
+ return
493
+ timer = timers[0]
494
+ if not isinstance(timer, TimerModels.TimerItemV1):
495
+ logger.debug('Invalid timer model - %s', timer)
496
+ return
497
+ self.state.timer = Timer(
498
+ timer_duration=int(timer.counterTimer),
499
+ id=int(timer.timerID),
500
+ action=timer.action,
501
+ )
502
+
503
+ async def set_timer(self, duration: int, action: str | None = None) -> bool:
504
+ if action is None:
505
+ action = (
506
+ DeviceStatus.ON
507
+ if self.state.device_status == DeviceStatus.OFF
508
+ else DeviceStatus.OFF
509
+ )
510
+ if not isinstance(action, str) or action not in [
511
+ DeviceStatus.ON,
512
+ DeviceStatus.OFF,
513
+ ]:
514
+ logger.error('Invalid action for timer - %s', action)
515
+ return False
516
+ update_dict = {
517
+ 'action': action,
518
+ 'counterTime': str(duration),
519
+ }
520
+ r_dict = await self.call_bypassv1_api(
521
+ TimerModels.RequestV1SetTime,
522
+ update_dict=update_dict,
523
+ method='addTimer',
524
+ endpoint='timer/addTimer',
525
+ )
526
+ result_model = process_bypassv1_result(
527
+ self, logger, 'set_timer', r_dict, TimerModels.ResultV1SetTimer
528
+ )
529
+ if result_model is None:
530
+ return False
531
+ self.state.timer = Timer(duration, action, int(result_model.timerID))
532
+ return True
533
+
534
+ async def clear_timer(self) -> bool:
535
+ if self.state.timer is None:
536
+ logger.debug('No timer set, nothing to clear, run get_timer().')
537
+ return False
538
+ if self.state.timer.time_remaining == 0:
539
+ logger.debug('Timer already ended.')
540
+ self.state.timer = None
541
+ return True
542
+ r_dict = await self.call_bypassv1_api(
543
+ TimerModels.RequestV1ClearTimer,
544
+ {'timerId': str(self.state.timer.id)},
545
+ method='deleteTimer',
546
+ endpoint='timer/deleteTimer',
547
+ )
548
+ r_dict = Helpers.process_dev_response(logger, 'clear_timer', self, r_dict)
549
+ if r_dict is None:
550
+ if (
551
+ self.last_response is not None
552
+ and self.last_response.name == 'TIMER_NOT_EXISTS'
553
+ ):
554
+ self.state.timer = None
555
+ return False
556
+ self.state.timer = None
557
+ return True
558
+
559
+
560
+ class VeSyncOutdoorPlug(BypassV1Mixin, VeSyncOutlet):
561
+ """Class to hold Etekcity outdoor outlets.
562
+
563
+ Args:
564
+ details (ResponseDeviceDetailsModel): The device details.
565
+ manager (VeSync): The VeSync manager.
566
+ feature_map (OutletMap): The feature map for the device.
567
+
568
+ Attributes:
569
+ state (OutletState): The state of the outlet.
570
+ last_response (ResponseInfo): Last response from API call.
571
+ device_status (str): Device status.
572
+ connection_status (str): Connection status.
573
+ manager (VeSync): Manager object for API calls.
574
+ device_name (str): Name of device.
575
+ device_image (str): URL for device image.
576
+ cid (str): Device ID.
577
+ connection_type (str): Connection type of device.
578
+ device_type (str): Type of device.
579
+ type (str): Type of device.
580
+ uuid (str): UUID of device, not always present.
581
+ config_module (str): Configuration module of device.
582
+ mac_id (str): MAC ID of device.
583
+ current_firm_version (str): Current firmware version of device.
584
+ device_region (str): Region of device. (US, EU, etc.)
585
+ pid (str): Product ID of device, pulled by some devices on update.
586
+ sub_device_no (int): Sub-device number of device.
587
+ product_type (str): Product type of device.
588
+ features (dict): Features of device.
589
+ """
590
+
591
+ __slots__ = ()
592
+
593
+ def __init__(
594
+ self, details: ResponseDeviceDetailsModel, manager: VeSync, feature_map: OutletMap
595
+ ) -> None:
596
+ """Initialize Etekcity Outdoor Plug class."""
597
+ super().__init__(details, manager, feature_map)
598
+
599
+ async def get_details(self) -> None:
600
+ r_dict = await self.call_bypassv1_api(
601
+ Request15ADetails, method='deviceDetail', endpoint='deviceDetail'
602
+ )
603
+ r = Helpers.process_dev_response(logger, 'get_details', self, r_dict)
604
+ if r is None:
605
+ return
606
+
607
+ resp_model = ResponseOutdoorDetails.from_dict(r)
608
+ self.state.connection_status = resp_model.result.connectionStatus
609
+ self.state.energy = resp_model.result.energy
610
+ self.state.power = resp_model.result.power
611
+ self.state.voltage = resp_model.result.voltage
612
+ self.state.active_time = resp_model.result.activeTime
613
+ for outlet in resp_model.result.subDevices:
614
+ if not isinstance(self.sub_device_no, float):
615
+ continue
616
+ if int(self.sub_device_no) == int(outlet.subDeviceNo):
617
+ self.state.device_status = outlet.subDeviceStatus
618
+
619
+ @deprecated('Use toggle_switch(toggle: bool | None) instead')
620
+ async def toggle(self, status: str) -> bool:
621
+ """Deprecated - use toggle_switch()."""
622
+ toggle = status != DeviceStatus.ON
623
+ return await self.toggle_switch(toggle)
624
+
625
+ async def toggle_switch(self, toggle: bool | None = None) -> bool:
626
+ if toggle is None:
627
+ toggle = self.state.device_status != DeviceStatus.ON
628
+ status = DeviceStatus.ON if toggle else DeviceStatus.OFF
629
+
630
+ r_dict = await self.call_bypassv1_api(
631
+ RequestOutdoorStatus,
632
+ update_dict={'switchNo': self.sub_device_no, 'status': status},
633
+ method='deviceStatus',
634
+ endpoint='deviceStatus',
635
+ )
636
+
637
+ response = Helpers.process_dev_response(logger, 'toggle', self, r_dict)
638
+ if response is None:
639
+ return False
640
+
641
+ self.state.device_status = status
642
+ self.state.connection_status = ConnectionStatus.ONLINE
643
+ return True
644
+
645
+ async def get_timer(self) -> None:
646
+ method = 'getTimers'
647
+ endpoint = f'/timer/{method}'
648
+ r_dict = await self.call_bypassv1_api(
649
+ TimerModels.RequestV1GetTimer,
650
+ {'switchNo': self.sub_device_no},
651
+ method=method,
652
+ endpoint=endpoint,
653
+ )
654
+ result_model = process_bypassv1_result(
655
+ self, logger, 'get_timer', r_dict, TimerModels.ResultV1GetTimer
656
+ )
657
+ if result_model is None:
658
+ return
659
+ timers = result_model.timers
660
+ if not isinstance(timers, list) or len(timers) == 0:
661
+ self.state.timer = None
662
+ return
663
+ if len(timers) > 1:
664
+ logger.debug(
665
+ (
666
+ (
667
+ 'Multiple timers found - %s, this method '
668
+ 'will only return the most recent timer created.'
669
+ ),
670
+ ),
671
+ timers,
672
+ )
673
+ timer = timers[0]
674
+ if not isinstance(timer, TimerModels.TimerItemV1):
675
+ logger.debug('Invalid timer model - %s', timer)
676
+ return
677
+ self.state.timer = Timer(
678
+ timer_duration=int(timer.counterTimer),
679
+ id=int(timer.timerID),
680
+ action=timer.action,
681
+ )
682
+
683
+ async def set_timer(self, duration: int, action: str | None = None) -> bool:
684
+ if action is None:
685
+ action = (
686
+ DeviceStatus.ON
687
+ if self.state.device_status == DeviceStatus.OFF
688
+ else DeviceStatus.OFF
689
+ )
690
+ if not isinstance(action, str) or action not in [
691
+ DeviceStatus.ON,
692
+ DeviceStatus.OFF,
693
+ ]:
694
+ logger.error('Invalid action for timer - %s', action)
695
+ return False
696
+ update_dict = {
697
+ 'action': action,
698
+ 'counterTime': str(duration),
699
+ 'switchNo': self.sub_device_no,
700
+ }
701
+ r_dict = await self.call_bypassv1_api(
702
+ TimerModels.RequestV1SetTime,
703
+ update_dict=update_dict,
704
+ method='addTimer',
705
+ endpoint='timer/addTimer',
706
+ )
707
+ result_model = process_bypassv1_result(
708
+ self, logger, 'set_timer', r_dict, TimerModels.ResultV1SetTimer
709
+ )
710
+ if result_model is None:
711
+ return False
712
+ self.state.timer = Timer(duration, action, int(result_model.timerID))
713
+ return True
714
+
715
+ async def clear_timer(self) -> bool:
716
+ if self.state.timer is None:
717
+ logger.debug('No timer set, nothing to clear, run get_timer().')
718
+ return False
719
+ if self.state.timer.time_remaining == 0:
720
+ logger.debug('Timer already ended.')
721
+ self.state.timer = None
722
+ return True
723
+ r_dict = await self.call_bypassv1_api(
724
+ TimerModels.RequestV1ClearTimer,
725
+ {'timerId': str(self.state.timer.id)},
726
+ method='deleteTimer',
727
+ endpoint='timer/deleteTimer',
728
+ )
729
+ r_dict = Helpers.process_dev_response(logger, 'clear_timer', self, r_dict)
730
+ if r_dict is None:
731
+ return False
732
+ self.state.timer = None
733
+ return True
734
+
735
+
736
+ class VeSyncOutletBSDGO1(BypassV2Mixin, VeSyncOutlet):
737
+ """VeSync BSDGO1 smart plug.
738
+
739
+ Note that this device does not support energy monitoring.
740
+
741
+ Args:
742
+ details (ResponseDeviceDetailsModel): The device details.
743
+ manager (VeSync): The VeSync manager.
744
+ feature_map (OutletMap): The feature map for the device.
745
+
746
+ Attributes:
747
+ state (OutletState): The state of the outlet.
748
+ last_response (ResponseInfo): Last response from API call.
749
+ device_status (str): Device status.
750
+ connection_status (str): Connection status.
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
+ """
768
+
769
+ __slots__ = ()
770
+
771
+ def __init__(
772
+ self, details: ResponseDeviceDetailsModel, manager: VeSync, feature_map: OutletMap
773
+ ) -> None:
774
+ """Initialize BSDGO1 smart plug class."""
775
+ super().__init__(details, manager, feature_map)
776
+
777
+ async def get_details(self) -> None:
778
+ r_dict = await self.call_bypassv2_api('getProperty')
779
+
780
+ resp_model = process_bypassv2_result(
781
+ self, logger, 'get_details', r_dict, ResponseBSDGO1OutletResult
782
+ )
783
+ if resp_model is None:
784
+ return
785
+
786
+ device_state = resp_model.powerSwitch_1
787
+ str_status = DeviceStatus.ON if device_state == 1 else DeviceStatus.OFF
788
+ self.state.device_status = str_status
789
+ self.state.connection_status = resp_model.connectionStatus
790
+ self.state.active_time = resp_model.active_time
791
+
792
+ async def toggle_switch(self, toggle: bool | None = None) -> bool:
793
+ if toggle is None:
794
+ toggle = self.state.device_status != DeviceStatus.ON
795
+ toggle_int = 1 if toggle else 0
796
+ r_dict = await self.call_bypassv2_api(
797
+ 'setProperty', data={'powerSwitch_1': toggle_int}
798
+ )
799
+ r = Helpers.process_dev_response(logger, 'toggle_switch', self, r_dict)
800
+ if r is None:
801
+ return False
802
+
803
+ self.state.device_status = DeviceStatus.ON if toggle else DeviceStatus.OFF
804
+ self.state.connection_status = ConnectionStatus.ONLINE
805
+ return True
806
+
807
+ async def _set_power(self, power: bool) -> bool:
808
+ """Set power state of BSDGO1 outlet."""
809
+ return await self.toggle_switch(power)
810
+
811
+
812
+ class VeSyncESW10USA(BypassV2Mixin, VeSyncOutlet):
813
+ """VeSync ESW10 USA outlet.
814
+
815
+ Note that this device does not support energy monitoring.
816
+
817
+ Args:
818
+ details (ResponseDeviceDetailsModel): The device details.
819
+ manager (VeSync): The VeSync manager.
820
+ feature_map (OutletMap): The feature map for the device.
821
+
822
+ Attributes:
823
+ state (OutletState): The state of the outlet.
824
+ last_response (ResponseInfo): Last response from API call.
825
+ device_status (str): Device status.
826
+ connection_status (str): Connection status.
827
+ manager (VeSync): Manager object for API calls.
828
+ device_name (str): Name of device.
829
+ device_image (str): URL for device image.
830
+ cid (str): Device ID.
831
+ connection_type (str): Connection type of device.
832
+ device_type (str): Type of device.
833
+ type (str): Type of device.
834
+ uuid (str): UUID of device, not always present.
835
+ config_module (str): Configuration module of device.
836
+ mac_id (str): MAC ID of device.
837
+ current_firm_version (str): Current firmware version of device.
838
+ device_region (str): Region of device. (US, EU, etc.)
839
+ pid (str): Product ID of device, pulled by some devices on update.
840
+ sub_device_no (int): Sub-device number of device.
841
+ product_type (str): Product type of device.
842
+ features (dict): Features of device.
843
+ """
844
+
845
+ __slots__ = ()
846
+
847
+ def __init__(
848
+ self, details: ResponseDeviceDetailsModel, manager: VeSync, feature_map: OutletMap
849
+ ) -> None:
850
+ """Initialize ESW10 USA outlet."""
851
+ super().__init__(details, manager, feature_map)
852
+
853
+ async def get_details(self) -> None:
854
+ payload_data = {
855
+ 'id': 0,
856
+ }
857
+ payload_method = 'getSwitch'
858
+ r_dict = await self.call_bypassv2_api(payload_method, payload_data)
859
+ result = process_bypassv2_result(
860
+ self, logger, 'get_details', r_dict, ResultESW10Details
861
+ )
862
+ if not isinstance(result, dict) or not isinstance(result.get('enabled'), bool):
863
+ logger.debug('Error getting %s details', self.device_name)
864
+ self.state.connection_status = ConnectionStatus.OFFLINE
865
+ return
866
+ self.state.device_status = DeviceStatus.from_bool(result.enabled)
867
+ self.state.connection_status = ConnectionStatus.ONLINE
868
+
869
+ async def toggle_switch(self, toggle: bool | None = None) -> bool:
870
+ if toggle is None:
871
+ toggle = self.state.device_status != DeviceStatus.ON
872
+ payload_data = {
873
+ 'id': 0,
874
+ 'enabled': toggle,
875
+ }
876
+ payload_method = 'setSwitch'
877
+ r_dict = await self.call_bypassv2_api(payload_method, payload_data)
878
+ result = Helpers.process_dev_response(logger, 'toggle_switch', self, r_dict)
879
+ if not isinstance(result, dict):
880
+ logger.debug('Error toggling %s switch', self.device_name)
881
+ return False
882
+ self.state.device_status = DeviceStatus.from_bool(toggle)
883
+ self.state.connection_status = ConnectionStatus.ONLINE
884
+ return True
885
+
886
+ async def get_timer(self) -> Timer | None:
887
+ r_dict = await self.call_bypassv2_api('getTimer')
888
+ result_model = process_bypassv2_result(
889
+ self, logger, 'get_timer', r_dict, TimerModels.ResultV2GetTimer
890
+ )
891
+ if result_model is None:
892
+ return None
893
+ timers = result_model.timers
894
+ if not timers:
895
+ self.state.timer = None
896
+ return None
897
+ if len(timers) > 1:
898
+ logger.debug(
899
+ (
900
+ 'Multiple timers found - %s, this method '
901
+ 'will only return the most recent timer created.'
902
+ ),
903
+ timers,
904
+ )
905
+ timer = timers[0]
906
+ self.state.timer = Timer(
907
+ timer_duration=int(timer.remain),
908
+ id=int(timer.id),
909
+ action=timer.action,
910
+ )
911
+ return self.state.timer
912
+
913
+ async def set_timer(self, duration: int, action: str | None = None) -> bool:
914
+ if action is None:
915
+ action = (
916
+ DeviceStatus.ON
917
+ if self.state.device_status == DeviceStatus.OFF
918
+ else DeviceStatus.OFF
919
+ )
920
+ if action not in [DeviceStatus.ON, DeviceStatus.OFF]:
921
+ logger.error('Invalid action for timer - %s', action)
922
+ return False
923
+ payload_data = {'action': action, 'total': duration}
924
+ r_dict = await self.call_bypassv2_api(
925
+ payload_method='addTimer',
926
+ data=payload_data,
927
+ )
928
+ result_model = process_bypassv2_result(
929
+ self, logger, 'set_timer', r_dict, TimerModels.ResultV2SetTimer
930
+ )
931
+ if result_model is None:
932
+ return False
933
+ if result_model.id is None:
934
+ logger.debug('Unable to set timer.')
935
+ return False
936
+ self.state.timer = Timer(duration, action, int(result_model.id))
937
+ return True
938
+
939
+ async def clear_timer(self) -> bool:
940
+ if self.state.timer is None:
941
+ logger.debug('No timer set, nothing to clear, run get_timer().')
942
+ return False
943
+ if self.state.timer.time_remaining == 0:
944
+ logger.debug('Timer already ended.')
945
+ self.state.timer = None
946
+ return True
947
+ r_dict = await self.call_bypassv2_api(
948
+ payload_method='delTimer', data={'id': self.state.timer.id}
949
+ )
950
+ r_dict = Helpers.process_dev_response(logger, 'clear_timer', self, r_dict)
951
+ if r_dict is None:
952
+ return False
953
+ self.state.timer = None
954
+ return True