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