pyvesync 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. pyvesync/__init__.py +5 -0
  2. pyvesync/base_devices/__init__.py +35 -0
  3. pyvesync/base_devices/bulb_base.py +330 -0
  4. pyvesync/base_devices/fan_base.py +392 -0
  5. pyvesync/base_devices/fryer_base.py +68 -0
  6. pyvesync/base_devices/humidifier_base.py +462 -0
  7. pyvesync/base_devices/outlet_base.py +327 -0
  8. pyvesync/base_devices/purifier_base.py +525 -0
  9. pyvesync/base_devices/switch_base.py +271 -0
  10. pyvesync/base_devices/thermostat_base.py +283 -0
  11. pyvesync/base_devices/vesyncbasedevice.py +572 -0
  12. pyvesync/const.py +825 -0
  13. pyvesync/device_container.py +373 -0
  14. pyvesync/device_map.py +1172 -0
  15. pyvesync/devices/__init__.py +1 -0
  16. pyvesync/devices/vesyncbulb.py +1126 -0
  17. pyvesync/devices/vesyncfan.py +233 -0
  18. pyvesync/devices/vesynchumidifier.py +808 -0
  19. pyvesync/devices/vesynckitchen.py +651 -0
  20. pyvesync/devices/vesyncoutlet.py +954 -0
  21. pyvesync/devices/vesyncpurifier.py +990 -0
  22. pyvesync/devices/vesyncswitch.py +415 -0
  23. pyvesync/devices/vesyncthermostat.py +171 -0
  24. pyvesync/models/__init__.py +11 -0
  25. pyvesync/models/base_models.py +129 -0
  26. pyvesync/models/bulb_models.py +197 -0
  27. pyvesync/models/bypass_models.py +289 -0
  28. pyvesync/models/fan_models.py +47 -0
  29. pyvesync/models/fryer_models.py +34 -0
  30. pyvesync/models/home_models.py +169 -0
  31. pyvesync/models/humidifier_models.py +202 -0
  32. pyvesync/models/outlet_models.py +220 -0
  33. pyvesync/models/purifier_models.py +261 -0
  34. pyvesync/models/switch_models.py +101 -0
  35. pyvesync/models/thermostat_models.py +134 -0
  36. pyvesync/models/vesync_models.py +439 -0
  37. pyvesync/utils/__init__.py +1 -0
  38. pyvesync/utils/colors.py +250 -0
  39. pyvesync/utils/device_mixins.py +362 -0
  40. pyvesync/utils/enum_utils.py +26 -0
  41. pyvesync/utils/errors.py +867 -0
  42. pyvesync/utils/helpers.py +700 -0
  43. pyvesync/utils/logs.py +525 -0
  44. pyvesync/vesync.py +638 -0
  45. pyvesync/vesynchome.py +152 -0
  46. pyvesync-3.0.0.dist-info/METADATA +567 -0
  47. pyvesync-3.0.0.dist-info/RECORD +50 -0
  48. pyvesync-3.0.0.dist-info/WHEEL +5 -0
  49. pyvesync-3.0.0.dist-info/licenses/LICENSE +21 -0
  50. pyvesync-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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