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