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,362 @@
|
|
|
1
|
+
"""Mixins for Devices that have similar API requests.
|
|
2
|
+
|
|
3
|
+
The `BypassV2Mixin` and `BypassV1Mixin` classes are used to send API requests to the
|
|
4
|
+
Bypass V2 and Bypass V1 endpoints, respectively. The `BypassV2Mixin` class is used for
|
|
5
|
+
devices that use the `/cloud/v2/deviceManaged/bypassV2` endpoint, while the
|
|
6
|
+
`BypassV1Mixin` class is used for devices that use the
|
|
7
|
+
`/cloud/v1/deviceManaged/{endpoint}` path.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from logging import Logger
|
|
13
|
+
from typing import TYPE_CHECKING, ClassVar, TypeVar
|
|
14
|
+
|
|
15
|
+
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
16
|
+
|
|
17
|
+
from pyvesync.const import ConnectionStatus
|
|
18
|
+
from pyvesync.models.base_models import DefaultValues
|
|
19
|
+
from pyvesync.models.bypass_models import (
|
|
20
|
+
RequestBypassV1,
|
|
21
|
+
RequestBypassV2,
|
|
22
|
+
)
|
|
23
|
+
from pyvesync.utils.errors import ErrorCodes, ErrorTypes, raise_api_errors
|
|
24
|
+
from pyvesync.utils.helpers import Helpers
|
|
25
|
+
from pyvesync.utils.logs import LibraryLogger
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from pyvesync import VeSync
|
|
29
|
+
from pyvesync.base_devices import VeSyncBaseDevice
|
|
30
|
+
from pyvesync.utils.errors import ResponseInfo
|
|
31
|
+
|
|
32
|
+
T_MODEL = TypeVar('T_MODEL', bound=DataClassORJSONMixin)
|
|
33
|
+
|
|
34
|
+
BYPASS_V1_PATH = '/cloud/v1/deviceManaged/'
|
|
35
|
+
BYPASS_V2_BASE = '/cloud/v2/deviceManaged/'
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def process_bypassv1_result(
|
|
39
|
+
device: VeSyncBaseDevice,
|
|
40
|
+
logger: Logger,
|
|
41
|
+
method: str,
|
|
42
|
+
resp_dict: dict | None,
|
|
43
|
+
model: type[T_MODEL],
|
|
44
|
+
) -> T_MODEL | None:
|
|
45
|
+
"""Process the Bypass V1 API response.
|
|
46
|
+
|
|
47
|
+
This will gracefully handle errors in the response and error codes,
|
|
48
|
+
logging them as needed. The return value is the built model.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
device (VeSyncBaseDevice): The device object.
|
|
52
|
+
logger (Logger): The logger to use for logging.
|
|
53
|
+
method (str): The method used in the payload.
|
|
54
|
+
resp_dict (dict | str): The api response.
|
|
55
|
+
model (type[T_MODEL]): The model to use for the response.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
dict: The response data
|
|
59
|
+
"""
|
|
60
|
+
if not isinstance(resp_dict, dict) or 'code' not in resp_dict:
|
|
61
|
+
LibraryLogger.log_device_api_response_error(
|
|
62
|
+
logger,
|
|
63
|
+
device.device_name,
|
|
64
|
+
device.device_type,
|
|
65
|
+
method,
|
|
66
|
+
'Error decoding JSON response',
|
|
67
|
+
)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
error_info = ErrorCodes.get_error_info(resp_dict['code'])
|
|
71
|
+
device.last_response = error_info
|
|
72
|
+
device.last_response.response_data = resp_dict
|
|
73
|
+
if error_info.error_type != ErrorTypes.SUCCESS:
|
|
74
|
+
_handle_bypass_error(logger, device, method, error_info, resp_dict['code'])
|
|
75
|
+
return None
|
|
76
|
+
result = resp_dict.get('result')
|
|
77
|
+
if not isinstance(result, dict):
|
|
78
|
+
return None
|
|
79
|
+
return Helpers.model_maker(logger, model, method, result, device)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _handle_bypass_error(
|
|
83
|
+
logger: Logger,
|
|
84
|
+
device: VeSyncBaseDevice,
|
|
85
|
+
method: str,
|
|
86
|
+
error_info: ResponseInfo,
|
|
87
|
+
code: int,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Process the outer result error code.
|
|
90
|
+
|
|
91
|
+
Internal method for handling the error code in the response field
|
|
92
|
+
used by the `process_bypassv1_result` and `process_bypassv2_result` method.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
logger (Logger): The logger to use for logging.
|
|
96
|
+
device (VeSyncBaseDevice): The device object.
|
|
97
|
+
method (str): The method used in the payload.
|
|
98
|
+
error_info (ResponseInfo): The error info object.
|
|
99
|
+
code (int): The error code.
|
|
100
|
+
|
|
101
|
+
Note:
|
|
102
|
+
This will raise the appropriate exception based on the error code. See
|
|
103
|
+
`pyvesync.utils.errors.ErrorCodes` for more information
|
|
104
|
+
about the error codes and their meanings.
|
|
105
|
+
"""
|
|
106
|
+
raise_api_errors(error_info)
|
|
107
|
+
LibraryLogger.log_device_return_code(
|
|
108
|
+
logger,
|
|
109
|
+
method,
|
|
110
|
+
device.device_name,
|
|
111
|
+
device.product_type,
|
|
112
|
+
code,
|
|
113
|
+
error_info.message,
|
|
114
|
+
)
|
|
115
|
+
device.state.connection_status = ConnectionStatus.from_bool(error_info.device_online)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _get_inner_result(
|
|
119
|
+
device: VeSyncBaseDevice,
|
|
120
|
+
logger: Logger,
|
|
121
|
+
method: str,
|
|
122
|
+
resp_dict: dict,
|
|
123
|
+
) -> dict | None:
|
|
124
|
+
"""Process the code in the result field of Bypass V2."""
|
|
125
|
+
try:
|
|
126
|
+
outer_result = resp_dict['result']
|
|
127
|
+
inner_result = outer_result['result']
|
|
128
|
+
code = int(outer_result['code'])
|
|
129
|
+
except (ValueError, TypeError, KeyError):
|
|
130
|
+
LibraryLogger.log_device_api_response_error(
|
|
131
|
+
logger,
|
|
132
|
+
device.device_name,
|
|
133
|
+
device.device_type,
|
|
134
|
+
method,
|
|
135
|
+
'Error processing bypass V2 API response result.',
|
|
136
|
+
)
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
if code != 0:
|
|
140
|
+
error_info = ErrorCodes.get_error_info(code)
|
|
141
|
+
error_msg = f'{error_info.message}'
|
|
142
|
+
if inner_result.get('msg') is not None:
|
|
143
|
+
error_info.message = f'{error_info.message} - {inner_result["msg"]}'
|
|
144
|
+
LibraryLogger.log_device_return_code(
|
|
145
|
+
logger,
|
|
146
|
+
method,
|
|
147
|
+
device.device_name,
|
|
148
|
+
device.product_type,
|
|
149
|
+
code,
|
|
150
|
+
error_msg,
|
|
151
|
+
)
|
|
152
|
+
device.last_response = error_info
|
|
153
|
+
return None
|
|
154
|
+
return inner_result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def process_bypassv2_result(
|
|
158
|
+
device: VeSyncBaseDevice,
|
|
159
|
+
logger: Logger,
|
|
160
|
+
method: str,
|
|
161
|
+
resp_dict: dict | None,
|
|
162
|
+
model: type[T_MODEL],
|
|
163
|
+
) -> T_MODEL | None:
|
|
164
|
+
"""Process the Bypass V1 API response.
|
|
165
|
+
|
|
166
|
+
This will gracefully handle errors in the response and error codes,
|
|
167
|
+
logging them as needed. The return dictionary is the **inner** result value of
|
|
168
|
+
the API response.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
device (VeSyncBaseDevice): The device object.
|
|
172
|
+
logger (Logger): The logger to use for logging.
|
|
173
|
+
method (str): The method used in the payload.
|
|
174
|
+
resp_dict (dict | str): The api response.
|
|
175
|
+
model (type[T_MODEL]): The model to use for the response.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
T_MODEL: An instance of the inner result model.
|
|
179
|
+
"""
|
|
180
|
+
if not isinstance(resp_dict, dict) or 'code' not in resp_dict:
|
|
181
|
+
LibraryLogger.log_device_api_response_error(
|
|
182
|
+
logger,
|
|
183
|
+
device.device_name,
|
|
184
|
+
device.device_type,
|
|
185
|
+
method,
|
|
186
|
+
'Error decoding JSON response',
|
|
187
|
+
)
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
error_info = ErrorCodes.get_error_info(resp_dict['code'])
|
|
191
|
+
device.last_response = error_info
|
|
192
|
+
device.last_response.response_data = resp_dict
|
|
193
|
+
if error_info.error_type != ErrorTypes.SUCCESS:
|
|
194
|
+
_handle_bypass_error(logger, device, method, error_info, resp_dict['code'])
|
|
195
|
+
return None
|
|
196
|
+
result = _get_inner_result(device, logger, method, resp_dict)
|
|
197
|
+
if not isinstance(result, dict):
|
|
198
|
+
return None
|
|
199
|
+
return Helpers.model_maker(logger, model, method, result, device)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class BypassV2Mixin:
|
|
203
|
+
"""Mixin for bypass V2 API.
|
|
204
|
+
|
|
205
|
+
Overrides the `_build_request` method and `request_keys` attribute for devices
|
|
206
|
+
that use the Bypass V2 API- /cloud/v2/deviceManaged/bypassV2.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
if TYPE_CHECKING:
|
|
210
|
+
manager: VeSync
|
|
211
|
+
|
|
212
|
+
__slots__ = ()
|
|
213
|
+
request_keys: ClassVar[list[str]] = [
|
|
214
|
+
'acceptLanguage',
|
|
215
|
+
'appVersion',
|
|
216
|
+
'phoneBrand',
|
|
217
|
+
'phoneOS',
|
|
218
|
+
'accountID',
|
|
219
|
+
'cid',
|
|
220
|
+
'configModule',
|
|
221
|
+
'debugMode',
|
|
222
|
+
'traceId',
|
|
223
|
+
'timeZone',
|
|
224
|
+
'token',
|
|
225
|
+
'userCountryCode',
|
|
226
|
+
'configModel',
|
|
227
|
+
'deviceId',
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
def _build_request(
|
|
231
|
+
self,
|
|
232
|
+
payload_method: str,
|
|
233
|
+
data: dict | None = None,
|
|
234
|
+
method: str = 'bypassV2',
|
|
235
|
+
) -> RequestBypassV2:
|
|
236
|
+
"""Build API request body Bypass V2 endpoint.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
payload_method (str): The method to use in the payload dict.
|
|
240
|
+
data (dict | None): The data dict inside the payload value.
|
|
241
|
+
method (str): The method to use in the outer body, defaults to bypassV2.
|
|
242
|
+
"""
|
|
243
|
+
body = Helpers.get_class_attributes(DefaultValues, self.request_keys)
|
|
244
|
+
body.update(Helpers.get_class_attributes(self.manager, self.request_keys))
|
|
245
|
+
body.update(Helpers.get_class_attributes(self, self.request_keys))
|
|
246
|
+
body['method'] = method
|
|
247
|
+
body['payload'] = {'method': payload_method, 'source': 'APP', 'data': data or {}}
|
|
248
|
+
return RequestBypassV2.from_dict(body)
|
|
249
|
+
|
|
250
|
+
async def call_bypassv2_api(
|
|
251
|
+
self,
|
|
252
|
+
payload_method: str,
|
|
253
|
+
data: dict | None = None,
|
|
254
|
+
method: str = 'bypassV2',
|
|
255
|
+
endpoint: str = 'bypassV2',
|
|
256
|
+
) -> dict | None:
|
|
257
|
+
"""Send Bypass V2 API request.
|
|
258
|
+
|
|
259
|
+
This uses the `_build_request` method to send API requests to the Bypass V2 API.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
payload_method (str): The method to use in the payload dict.
|
|
263
|
+
data (dict | None): The data to send in the request.
|
|
264
|
+
method (str): The method to use in the outer body.
|
|
265
|
+
endpoint (str | None): The last part of the API url, defaults to
|
|
266
|
+
`bypassV2`, e.g. `/cloud/v2/deviceManaged/bypassV2`.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
bytes: The response from the API request.
|
|
270
|
+
"""
|
|
271
|
+
request = self._build_request(payload_method, data, method)
|
|
272
|
+
endpoint = BYPASS_V2_BASE + endpoint
|
|
273
|
+
resp_dict, _ = await self.manager.async_call_api(
|
|
274
|
+
endpoint, 'post', request, Helpers.req_header_bypass()
|
|
275
|
+
)
|
|
276
|
+
return resp_dict
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class BypassV1Mixin:
|
|
280
|
+
"""Mixin for bypass V1 API.
|
|
281
|
+
|
|
282
|
+
Overrides the `_build_request` method and `request_keys` attribute for devices
|
|
283
|
+
that use the Bypass V1 API- /cloud/v1/deviceManaged/bypass. The primary method to
|
|
284
|
+
call is `call_bypassv1_api`, which is a wrapper for setting up the request body and
|
|
285
|
+
calling the API. The `bypass` endpoint can also be overridden for specific API calls.
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
if TYPE_CHECKING:
|
|
289
|
+
manager: VeSync
|
|
290
|
+
|
|
291
|
+
__slots__ = ()
|
|
292
|
+
request_keys: ClassVar[list[str]] = [
|
|
293
|
+
'acceptLanguage',
|
|
294
|
+
'appVersion',
|
|
295
|
+
'phoneBrand',
|
|
296
|
+
'phoneOS',
|
|
297
|
+
'accountID',
|
|
298
|
+
'cid',
|
|
299
|
+
'configModule',
|
|
300
|
+
'debugMode',
|
|
301
|
+
'traceId',
|
|
302
|
+
'timeZone',
|
|
303
|
+
'token',
|
|
304
|
+
'userCountryCode',
|
|
305
|
+
'uuid',
|
|
306
|
+
'configModel',
|
|
307
|
+
'deviceId',
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
def _build_request(
|
|
311
|
+
self,
|
|
312
|
+
request_model: type[RequestBypassV1],
|
|
313
|
+
update_dict: dict | None = None,
|
|
314
|
+
method: str = 'bypass',
|
|
315
|
+
) -> RequestBypassV1:
|
|
316
|
+
"""Build API request body for the Bypass V1 endpoint.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
request_model (type[RequestBypassV1]): The request model to use.
|
|
320
|
+
update_dict (dict | None): Additional keys to add on.
|
|
321
|
+
method (str): The method to use in the outer body, defaults to bypass.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
RequestBypassV1: The request body for the Bypass V1 endpoint, the correct
|
|
325
|
+
model is determined from the RequestBypassV1 discriminator.
|
|
326
|
+
"""
|
|
327
|
+
body = Helpers.get_class_attributes(DefaultValues, self.request_keys)
|
|
328
|
+
body.update(Helpers.get_class_attributes(self.manager, self.request_keys))
|
|
329
|
+
body.update(Helpers.get_class_attributes(self, self.request_keys))
|
|
330
|
+
body['method'] = method
|
|
331
|
+
body.update(update_dict or {})
|
|
332
|
+
return request_model.from_dict(body)
|
|
333
|
+
|
|
334
|
+
async def call_bypassv1_api(
|
|
335
|
+
self,
|
|
336
|
+
request_model: type[RequestBypassV1],
|
|
337
|
+
update_dict: dict | None = None,
|
|
338
|
+
method: str = 'bypass',
|
|
339
|
+
endpoint: str = 'bypass',
|
|
340
|
+
) -> dict | None:
|
|
341
|
+
"""Send ByPass V1 API request.
|
|
342
|
+
|
|
343
|
+
This uses the `_build_request` method to send API requests to the Bypass V1 API.
|
|
344
|
+
The endpoint can be overridden with the `endpoint` argument.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
request_model (type[RequestBypassV1]): The request model to use.
|
|
348
|
+
update_dict (dict): Additional keys to add on.
|
|
349
|
+
method (str): The method to use in the outer body.
|
|
350
|
+
endpoint (str | None): The last part of the url path, defaults to
|
|
351
|
+
`bypass`, e.g. `/cloud/v1/deviceManaged/bypass`.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
bytes: The response from the API request.
|
|
355
|
+
"""
|
|
356
|
+
request = self._build_request(request_model, update_dict, method)
|
|
357
|
+
url_path = BYPASS_V1_PATH + endpoint
|
|
358
|
+
resp_dict, _ = await self.manager.async_call_api(
|
|
359
|
+
url_path, 'post', request, Helpers.req_header_bypass()
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
return resp_dict
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Utilities to handle Enums."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IntEnumMixin(enum.IntEnum):
|
|
9
|
+
"""Mixin class to handle missing enum values.
|
|
10
|
+
|
|
11
|
+
Adds __missing__ method using the `extend_enum` function to
|
|
12
|
+
return a new enum member with the name "UNKNOWN" and the
|
|
13
|
+
missing value.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def _missing_(cls: type[enum.IntEnum], value: object) -> enum.IntEnum:
|
|
18
|
+
"""Handle missing enum values by returning member with UNKNOWN name."""
|
|
19
|
+
for member in cls:
|
|
20
|
+
if member.value == value:
|
|
21
|
+
return member
|
|
22
|
+
unknown_enum_val = int.__new__(cls, value) # type: ignore[call-overload]
|
|
23
|
+
unknown_enum_val._name_ = 'UNKNOWN'
|
|
24
|
+
unknown_enum_val._value_ = value # type: ignore[assignment]
|
|
25
|
+
unknown_enum_val.__objclass__ = cls.__class__ # type: ignore[assignment]
|
|
26
|
+
return unknown_enum_val
|