pyvesync 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyvesync/__init__.py +5 -0
- pyvesync/base_devices/__init__.py +35 -0
- pyvesync/base_devices/bulb_base.py +330 -0
- pyvesync/base_devices/fan_base.py +392 -0
- pyvesync/base_devices/fryer_base.py +68 -0
- pyvesync/base_devices/humidifier_base.py +462 -0
- pyvesync/base_devices/outlet_base.py +327 -0
- pyvesync/base_devices/purifier_base.py +525 -0
- pyvesync/base_devices/switch_base.py +271 -0
- pyvesync/base_devices/thermostat_base.py +283 -0
- pyvesync/base_devices/vesyncbasedevice.py +572 -0
- pyvesync/const.py +825 -0
- pyvesync/device_container.py +373 -0
- pyvesync/device_map.py +1172 -0
- pyvesync/devices/__init__.py +1 -0
- pyvesync/devices/vesyncbulb.py +1126 -0
- pyvesync/devices/vesyncfan.py +233 -0
- pyvesync/devices/vesynchumidifier.py +808 -0
- pyvesync/devices/vesynckitchen.py +651 -0
- pyvesync/devices/vesyncoutlet.py +954 -0
- pyvesync/devices/vesyncpurifier.py +990 -0
- pyvesync/devices/vesyncswitch.py +415 -0
- pyvesync/devices/vesyncthermostat.py +171 -0
- pyvesync/models/__init__.py +11 -0
- pyvesync/models/base_models.py +129 -0
- pyvesync/models/bulb_models.py +197 -0
- pyvesync/models/bypass_models.py +289 -0
- pyvesync/models/fan_models.py +47 -0
- pyvesync/models/fryer_models.py +34 -0
- pyvesync/models/home_models.py +169 -0
- pyvesync/models/humidifier_models.py +202 -0
- pyvesync/models/outlet_models.py +220 -0
- pyvesync/models/purifier_models.py +261 -0
- pyvesync/models/switch_models.py +101 -0
- pyvesync/models/thermostat_models.py +134 -0
- pyvesync/models/vesync_models.py +439 -0
- pyvesync/utils/__init__.py +1 -0
- pyvesync/utils/colors.py +250 -0
- pyvesync/utils/device_mixins.py +362 -0
- pyvesync/utils/enum_utils.py +26 -0
- pyvesync/utils/errors.py +867 -0
- pyvesync/utils/helpers.py +700 -0
- pyvesync/utils/logs.py +525 -0
- pyvesync/vesync.py +638 -0
- pyvesync/vesynchome.py +152 -0
- pyvesync-3.0.0.dist-info/METADATA +567 -0
- pyvesync-3.0.0.dist-info/RECORD +50 -0
- pyvesync-3.0.0.dist-info/WHEEL +5 -0
- pyvesync-3.0.0.dist-info/licenses/LICENSE +21 -0
- pyvesync-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|