pyvesync 3.0.0b0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. pyvesync/__init__.py +11 -0
  2. pyvesync/base_devices/__init__.py +35 -0
  3. pyvesync/base_devices/bulb_base.py +348 -0
  4. pyvesync/base_devices/fan_base.py +398 -0
  5. pyvesync/base_devices/fryer_base.py +69 -0
  6. pyvesync/base_devices/humidifier_base.py +460 -0
  7. pyvesync/base_devices/outlet_base.py +315 -0
  8. pyvesync/base_devices/purifier_base.py +518 -0
  9. pyvesync/base_devices/switch_base.py +270 -0
  10. pyvesync/base_devices/thermostat_base.py +277 -0
  11. pyvesync/base_devices/vesyncbasedevice.py +580 -0
  12. pyvesync/const.py +749 -0
  13. pyvesync/device_container.py +360 -0
  14. pyvesync/device_map.py +1090 -0
  15. pyvesync/devices/__init__.py +1 -0
  16. pyvesync/devices/vesyncbulb.py +1254 -0
  17. pyvesync/devices/vesyncfan.py +238 -0
  18. pyvesync/devices/vesynchumidifier.py +785 -0
  19. pyvesync/devices/vesynckitchen.py +664 -0
  20. pyvesync/devices/vesyncoutlet.py +962 -0
  21. pyvesync/devices/vesyncpurifier.py +951 -0
  22. pyvesync/devices/vesyncswitch.py +449 -0
  23. pyvesync/devices/vesyncthermostat.py +175 -0
  24. pyvesync/models/__init__.py +11 -0
  25. pyvesync/models/base_models.py +117 -0
  26. pyvesync/models/bulb_models.py +173 -0
  27. pyvesync/models/bypass_models.py +264 -0
  28. pyvesync/models/fan_models.py +46 -0
  29. pyvesync/models/fryer_models.py +28 -0
  30. pyvesync/models/home_models.py +158 -0
  31. pyvesync/models/humidifier_models.py +187 -0
  32. pyvesync/models/outlet_models.py +194 -0
  33. pyvesync/models/purifier_models.py +204 -0
  34. pyvesync/models/switch_models.py +89 -0
  35. pyvesync/models/thermostat_models.py +122 -0
  36. pyvesync/models/vesync_models.py +252 -0
  37. pyvesync/utils/__init__.py +1 -0
  38. pyvesync/utils/colors.py +248 -0
  39. pyvesync/utils/device_mixins.py +366 -0
  40. pyvesync/utils/enum_utils.py +25 -0
  41. pyvesync/utils/errors.py +859 -0
  42. pyvesync/utils/helpers.py +696 -0
  43. pyvesync/utils/logs.py +500 -0
  44. pyvesync/vesync.py +445 -0
  45. pyvesync-3.0.0b0.dist-info/METADATA +527 -0
  46. pyvesync-3.0.0b0.dist-info/RECORD +49 -0
  47. pyvesync-3.0.0b0.dist-info/WHEEL +5 -0
  48. pyvesync-3.0.0b0.dist-info/licenses/LICENSE +21 -0
  49. pyvesync-3.0.0b0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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