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,439 @@
1
+ """Models for general VeSync API requests and responses.
2
+
3
+ Dataclasses should follow the naming convention of Request/Response + <API Name> + Model.
4
+ Internal models should be named starting with IntResp/IntReq<API Name>Model.
5
+
6
+ Note:
7
+ All models should inherit `ResponseBaseModel` or `RequestBaseModel`. Use
8
+ `pyvesync.models.base_models.DefaultValues` to set default values. There
9
+ should be no repeating keys set in the child models.
10
+
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ from dataclasses import dataclass, field
17
+ from typing import Any
18
+
19
+ from pyvesync.models.base_models import (
20
+ DefaultValues,
21
+ RequestBaseModel,
22
+ ResponseBaseModel,
23
+ ResponseCodeModel,
24
+ )
25
+
26
+
27
+ @dataclass
28
+ class RequestGetTokenModel(RequestBaseModel):
29
+ """Request model for requesting auth token (used for login)."""
30
+
31
+ # Arguments to set
32
+ email: str
33
+ method: str
34
+ password: str
35
+ # default values
36
+ acceptLanguage: str = DefaultValues.acceptLanguage
37
+ accountID: str = ''
38
+ authProtocolType: str = 'generic'
39
+ clientInfo: str = DefaultValues.phoneBrand
40
+ clientType: str = DefaultValues.clientType
41
+ clientVersion: str = f'VeSync {DefaultValues.appVersion}'
42
+ debugMode: bool = False
43
+ osInfo: str = DefaultValues.phoneOS
44
+ terminalId: str = DefaultValues.terminalId
45
+ timeZone: str = DefaultValues.timeZone
46
+ token: str = ''
47
+ userCountryCode: str = DefaultValues.userCountryCode
48
+ appID: str = DefaultValues.appId
49
+ sourceAppID: str = DefaultValues.appId
50
+ traceId: str = field(default_factory=DefaultValues.newTraceId)
51
+
52
+ def __post_init__(self) -> None:
53
+ """Hash the password field."""
54
+ self.password = self.hash_password(self.password)
55
+
56
+ @staticmethod
57
+ def hash_password(string: str) -> str:
58
+ """Encode password."""
59
+ return hashlib.md5(string.encode('utf-8')).hexdigest() # noqa: S324
60
+
61
+
62
+ @dataclass
63
+ class RespGetTokenResultModel(ResponseBaseModel):
64
+ """Model for the 'result' field in auth response with authorizeCode and account ID.
65
+
66
+ This class is referenced by the `ResponseAuthModel` class.
67
+ """
68
+
69
+ accountID: str
70
+ authorizeCode: str
71
+
72
+
73
+ @dataclass
74
+ class RequestLoginTokenModel(RequestBaseModel):
75
+ """Request model for login."""
76
+
77
+ # Arguments to set
78
+ method: str
79
+ authorizeCode: str | None
80
+ # default values
81
+ acceptLanguage: str = DefaultValues.acceptLanguage
82
+ accountID: str = ''
83
+ clientInfo: str = DefaultValues.phoneBrand
84
+ clientType: str = DefaultValues.clientType
85
+ clientVersion: str = f'VeSync {DefaultValues.appVersion}'
86
+ debugMode: bool = False
87
+ emailSubscriptions: bool = False
88
+ osInfo: str = DefaultValues.phoneOS
89
+ terminalId: str = DefaultValues.terminalId
90
+ timeZone: str = DefaultValues.timeZone
91
+ token: str = ''
92
+ bizToken: str | None = None
93
+ regionChange: str | None = None
94
+ userCountryCode: str = DefaultValues.userCountryCode
95
+ traceId: str = field(default_factory=DefaultValues.newTraceId)
96
+
97
+ def __post_serialize__(self, d: dict[Any, Any]) -> dict[Any, Any]:
98
+ """Remove null keys."""
99
+ if d['regionChange'] is None:
100
+ d.pop('regionChange')
101
+ if d['authorizeCode'] is None:
102
+ d.pop('authorizeCode')
103
+ if d['bizToken'] is None:
104
+ d.pop('bizToken')
105
+ return d
106
+
107
+
108
+ @dataclass
109
+ class RespLoginTokenResultModel(ResponseBaseModel):
110
+ """Model for the 'result' field in login response containing token and account ID.
111
+
112
+ This class is referenced by the `ResponseLoginModel` class.
113
+ """
114
+
115
+ accountID: str
116
+ acceptLanguage: str
117
+ countryCode: str
118
+ token: str
119
+ bizToken: str = ''
120
+ currentRegion: str = ''
121
+
122
+
123
+ @dataclass
124
+ class ResponseLoginModel(ResponseCodeModel):
125
+ """Model for the login response.
126
+
127
+ Inherits from `BaseResultModel`. The `BaseResultModel` class provides the
128
+ defaults "code" and "msg" fields for the response.
129
+
130
+ Attributes:
131
+ result: ResponseLoginResultModel
132
+ The inner model for the 'result' field in the login response.
133
+
134
+ Examples:
135
+ ```python
136
+ a = {
137
+ "code": 0,
138
+ "msg": "success",
139
+ "stacktrace": null,
140
+ "module": null,
141
+ "traceId": "123456",
142
+ "result": {
143
+ "accountID": "123456",
144
+ "acceptLanguage": "en",
145
+ "countryCode": "US",
146
+ "token": "abcdef1234567890"
147
+ }
148
+ }
149
+ b = ResponseLoginModel.from_dict(a)
150
+ account_id = b.result.accountId
151
+ token = b.result.token
152
+ ```
153
+ """
154
+
155
+ result: RespLoginTokenResultModel | RespGetTokenResultModel
156
+
157
+
158
+ @dataclass
159
+ class RequestDeviceListModel(RequestBaseModel):
160
+ """Model for the device list request."""
161
+
162
+ token: str
163
+ accountID: str
164
+ timeZone: str = DefaultValues.timeZone
165
+ method: str = 'devices'
166
+ pageNo: int = 1
167
+ pageSize: int = 100
168
+ appVersion: str = DefaultValues.appVersion
169
+ phoneBrand: str = DefaultValues.phoneBrand
170
+ phoneOS: str = DefaultValues.phoneOS
171
+ acceptLanguage: str = DefaultValues.acceptLanguage
172
+ traceId: str = str(DefaultValues.traceId())
173
+
174
+
175
+ def _flatten_device_prop(d: dict[str, Any]) -> dict[str, Any]:
176
+ """Flatten deviceProp field in device list response."""
177
+ if isinstance(d.get('deviceProp'), dict):
178
+ device_prop = d['deviceProp']
179
+ if device_prop.get('powerSwitch') is not None:
180
+ d['deviceStatus'] = 'on' if device_prop['powerSwitch'] == 1 else 'off'
181
+ if device_prop.get('connectionStatus') is not None:
182
+ d['connectionStatus'] = device_prop['connectionStatus']
183
+ if device_prop.get('wifiMac') is not None:
184
+ d['macID'] = device_prop['wifiMac']
185
+ d.pop('deviceProp', None)
186
+ return d
187
+
188
+
189
+ @dataclass
190
+ class ResponseDeviceDetailsModel(ResponseBaseModel):
191
+ """Internal response model for each device in device list response.
192
+
193
+ Populates the 'list' field in the `InternalDeviceListResult`.
194
+
195
+ Certain Devices have device status information in the `deviceProp` or `extension`
196
+ fields. This model flattens those fields into the `deviceStatus` and
197
+ `connectionStatus` fields before deserialization.
198
+ """
199
+
200
+ deviceRegion: str
201
+ isOwner: bool
202
+ deviceName: str
203
+ cid: str
204
+ connectionType: str
205
+ deviceType: str
206
+ type: str
207
+ configModule: str
208
+ uuid: str | None = None
209
+ macID: str = ''
210
+ mode: str = ''
211
+ deviceImg: str = ''
212
+ speed: str | None = None
213
+ currentFirmVersion: str | None = None
214
+ subDeviceType: str | None = None
215
+ subDeviceList: str | None = None
216
+ extension: InternalDeviceListExtension | None = None
217
+ subDeviceNo: int | None = None
218
+ deviceStatus: str = 'off'
219
+ connectionStatus: str = 'offline'
220
+ productType: str | None = None
221
+
222
+ @classmethod
223
+ def __pre_deserialize__(cls, d: dict[Any, Any]) -> dict[Any, Any]:
224
+ """Perform device_list pre-deserialization processes.
225
+
226
+ This performs the following:
227
+ - Flattens the `deviceProp` field into `deviceStatus`, `connectionStatus`
228
+ and `macID`
229
+ - Sets `cid` to `uuid` or `macID` if null
230
+ """
231
+ super().__pre_deserialize__(d)
232
+ d = _flatten_device_prop(d)
233
+ if d.get('cid') is None:
234
+ d['cid'] = d.get('uuid') if d.get('uuid') is not None else d.get('macID')
235
+ return d
236
+
237
+
238
+ @dataclass
239
+ class InternalDeviceListResult(ResponseBaseModel):
240
+ """Internal model for the 'result' field in device list response.
241
+
242
+ Notes:
243
+ Used by the `ResponseDeviceListModel` class to populate result field.
244
+ """
245
+
246
+ total: int
247
+ pageSize: int
248
+ pageNo: int
249
+ list: list[ResponseDeviceDetailsModel]
250
+
251
+
252
+ @dataclass
253
+ class ResponseDeviceListModel(ResponseCodeModel):
254
+ """Device list response model.
255
+
256
+ Inherits from `BaseResultModel`. The `BaseResultModel` class provides the
257
+ defaults "code" and "msg" fields for the response.
258
+
259
+ Attributes:
260
+ result: InternalDeviceListResult
261
+ The inner model for the 'result' field in the device list response.
262
+ module: str | None
263
+ stacktrace: str | None
264
+
265
+ Notes:
266
+ See the `DeviceListResultModel` and `DeviceListDeviceModel` classes for
267
+ the inner model of the 'result' field.
268
+ """
269
+
270
+ result: InternalDeviceListResult
271
+
272
+
273
+ @dataclass
274
+ class InternalDeviceListExtension(ResponseBaseModel):
275
+ """Internal Optional 'extension' field in device list response.
276
+
277
+ Used by the `InnerRespDeviceListDevModel` class to populate
278
+ the extension field in the device list response.
279
+ """
280
+
281
+ airQuality: None | int
282
+ airQualityLevel: None | int
283
+ mode: None | str
284
+ fanSpeedLevel: None | str
285
+
286
+
287
+ @dataclass
288
+ class RequestPID(RequestBaseModel):
289
+ """Model for the PID request."""
290
+
291
+ method: str
292
+ appVersion: str
293
+ phoneBrand: str
294
+ phoneOS: str
295
+ traceId: str
296
+ token: str
297
+ accountID: str
298
+ mobileID: str
299
+ configModule: str
300
+ region: str
301
+
302
+
303
+ @dataclass
304
+ class RequestFirmwareModel(RequestBaseModel):
305
+ """Model for the firmware request."""
306
+
307
+ accountID: str
308
+ timeZone: str
309
+ token: str
310
+ userCountryCode: str
311
+ cidList: list[str]
312
+ acceptLanguage: str = DefaultValues.acceptLanguage
313
+ traceId: str = field(default_factory=DefaultValues.traceId)
314
+ appVersion: str = DefaultValues.appVersion
315
+ phoneBrand: str = DefaultValues.phoneBrand
316
+ phoneOS: str = DefaultValues.phoneOS
317
+ method: str = 'getFirmwareUpdateInfoList'
318
+ debugMode: bool = False
319
+
320
+
321
+ @dataclass
322
+ class ResponseFirmwareModel(ResponseCodeModel):
323
+ """Model for the firmware response."""
324
+
325
+ result: FirmwareResultModel
326
+
327
+
328
+ @dataclass
329
+ class FirmwareUpdateInfoModel(ResponseBaseModel):
330
+ """Firmware update information model."""
331
+
332
+ currentVersion: str
333
+ latestVersion: str
334
+ releaseNotes: str
335
+ pluginName: str
336
+ isMainFw: bool
337
+
338
+
339
+ @dataclass
340
+ class FirmwareDeviceItemModel(ResponseBaseModel):
341
+ """Model for the firmware device item in the firmware response."""
342
+
343
+ deviceCid: str
344
+ deviceName: str
345
+ code: int
346
+ msg: str | None
347
+ firmUpdateInfos: list[FirmwareUpdateInfoModel]
348
+
349
+
350
+ @dataclass
351
+ class FirmwareResultModel(ResponseBaseModel):
352
+ """Model for the firmware response result."""
353
+
354
+ cidFwInfoList: list[FirmwareDeviceItemModel]
355
+
356
+
357
+ @dataclass
358
+ class RequestDeviceConfiguration(RequestBaseModel):
359
+ """Model for the device configuration request."""
360
+
361
+ accountID: str
362
+ token: str
363
+ acceptLanguage: str = DefaultValues.acceptLanguage
364
+ appVersion: str = f'VeSync {DefaultValues.appVersion}'
365
+ debugMode: bool = False
366
+ method: str = 'getAppConfigurationV2'
367
+ phoneBrand: str = DefaultValues.phoneBrand
368
+ phoneOS: str = DefaultValues.phoneOS
369
+ timeZone: str = DefaultValues.timeZone
370
+ traceId: str = field(default_factory=DefaultValues.traceId)
371
+ userCountryCode: str = DefaultValues.userCountryCode
372
+ categories: list[dict[str, str | bool]] = field(
373
+ default_factory=lambda: [
374
+ {
375
+ 'category': 'SupportedModelsV3',
376
+ 'language': 'en',
377
+ 'testMode': False,
378
+ 'version': '',
379
+ }
380
+ ]
381
+ )
382
+ recall: bool = False
383
+
384
+
385
+ @dataclass
386
+ class ResponseDeviceConfiguration(ResponseCodeModel):
387
+ """Model for the device configuration response.
388
+
389
+ Inherits from `BaseResultModel`. The `BaseResultModel` class provides the
390
+ defaults "code" and "msg" fields for the response.
391
+
392
+ Attributes:
393
+ result: dict
394
+ The inner model for the 'result' field in the device configuration response.
395
+ """
396
+
397
+ result: dict[str, Any]
398
+
399
+
400
+ @dataclass
401
+ class ResultDeviceConfiguration(ResponseBaseModel):
402
+ """Model for the device configuration result field.
403
+
404
+ This class is referenced by the `ResponseDeviceConfiguration` class.
405
+ """
406
+
407
+ configList: list[DeviceConfigurationConfigListItem]
408
+
409
+
410
+ @dataclass
411
+ class DeviceConfigurationConfigListItem(ResponseBaseModel):
412
+ """Model for each item in the configList field of the device configuration result.
413
+
414
+ This class is referenced by the `ResultDeviceConfiguration` class.
415
+ """
416
+
417
+ category: str
418
+ items: list[dict[str, Any]]
419
+
420
+
421
+ @dataclass
422
+ class DeviceConfigItem(ResponseBaseModel):
423
+ """Model for each item in the configList field of the device configuration result.
424
+
425
+ This class is referenced by the `DeviceConfigurationConfigListItem` class.
426
+ """
427
+
428
+ itemKey: str
429
+ itemValue: list[dict[str, Any]]
430
+
431
+
432
+ @dataclass
433
+ class DeviceConfigItemValue(ResponseBaseModel):
434
+ """Model for each item in the configList field of the device configuration result.
435
+
436
+ This class is referenced by the `DeviceConfigItem` class.
437
+ """
438
+
439
+ productLineList: list
@@ -0,0 +1 @@
1
+ """Helper utilities and classes for the VeSync API."""
@@ -0,0 +1,250 @@
1
+ """Data structures for handling colors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import colorsys
6
+ import logging
7
+ from dataclasses import InitVar, dataclass
8
+
9
+ from pyvesync.utils.helpers import Validators
10
+
11
+ _LOGGER = logging.getLogger(__name__)
12
+
13
+
14
+ @dataclass
15
+ class RGB:
16
+ """RGB color space dataclass, for internal use in `utils.colors.Color` dataclass.
17
+
18
+ Does not perform any validation, it should not be used directly. Used as an
19
+ attribute in the :obj:`pyvesync.helpers.Color` dataclass. This should only be
20
+ used through the :obj:`Color` dataclass with the Color.from_rgb(red, green, blue)
21
+ classmethod.
22
+
23
+ Attributes:
24
+ red (float): The red component of the RGB color.
25
+ green (float): The green component of the RGB color.
26
+ blue (float): The blue component of the RGB color.
27
+ """
28
+
29
+ red: float
30
+ green: float
31
+ blue: float
32
+
33
+ def __post_init__(self) -> None:
34
+ """Convert to int."""
35
+ self.red = int(self.red)
36
+ self.green = int(self.green)
37
+ self.blue = int(self.blue)
38
+
39
+ def __str__(self) -> str:
40
+ """Return string representation."""
41
+ return f'RGB({self.red}, {self.green}, {self.blue})'
42
+
43
+ def __repr__(self) -> str:
44
+ """Return string representation."""
45
+ return f'RGB(red={self.red}, green={self.green}, blue={self.blue})'
46
+
47
+ def to_tuple(self) -> tuple[float, float, float]:
48
+ """Return RGB values as tuple."""
49
+ return self.red, self.green, self.blue
50
+
51
+ def to_dict(self) -> dict[str, float]:
52
+ """Return RGB values as dict."""
53
+ return {
54
+ 'red': self.red,
55
+ 'green': self.green,
56
+ 'blue': self.blue,
57
+ }
58
+
59
+
60
+ @dataclass
61
+ class HSV:
62
+ """HSV color space dataclass, for internal use in `utils.colors.Color` dataclass.
63
+
64
+ Does not perform any validation and should not be used directly, only
65
+ by the `Color` dataclass through the Color.from_hsv(hue, saturation, value)
66
+ classmethod or Color.rgb_to_hsv(red, green, blue) method.
67
+
68
+ Attributes:
69
+ hue (float): The hue component of the color, typically in the range [0, 360).
70
+ saturation (float): The saturation component of the color,
71
+ typically in the range [0, 1].
72
+ value (float): The value (brightness) component of the color,
73
+ typically in the range [0, 1].
74
+ """
75
+
76
+ hue: float
77
+ saturation: float
78
+ value: float
79
+
80
+ def __post_init__(self) -> None:
81
+ """Convert to int."""
82
+ self.hue = int(self.hue)
83
+ self.saturation = int(self.saturation)
84
+ self.value = int(self.value)
85
+
86
+ def __str__(self) -> str:
87
+ """Return string representation."""
88
+ return f'HSV({self.hue}, {self.saturation}, {self.value})'
89
+
90
+ def __repr__(self) -> str:
91
+ """Return string representation."""
92
+ return f'HSV(hue={self.hue}, saturation={self.saturation}, value={self.value})'
93
+
94
+ def to_tuple(self) -> tuple[float, float, float]:
95
+ """Return HSV values as tuple."""
96
+ return self.hue, self.saturation, self.value
97
+
98
+ def to_dict(self) -> dict[str, float]:
99
+ """Return HSV values as dict."""
100
+ return {
101
+ 'hue': self.hue,
102
+ 'saturation': self.saturation,
103
+ 'value': self.value,
104
+ }
105
+
106
+
107
+ @dataclass
108
+ class Color:
109
+ """Dataclass for color values.
110
+
111
+ This class should be instantiated through the `from_rgb` or `from_hsv`
112
+ classmethods. It will return a `Color` object with the appropriate color
113
+ values in RGB and HSV.
114
+
115
+ Args:
116
+ color_object (HSV | RGB): Named tuple with color values.
117
+
118
+ Attributes:
119
+ hsv (namedtuple): hue (0-360), saturation (0-100), value (0-100)
120
+ see [`HSV dataclass`][pyvesync.utils.colors.HSV]
121
+ rgb (namedtuple): red (0-255), green (0-255), blue (0-255)
122
+ see [`RGB dataclass`][pyvesync.utils.colors.RGB]
123
+ """
124
+
125
+ color_object: InitVar[HSV | RGB]
126
+
127
+ def __post_init__(
128
+ self,
129
+ color_object: HSV | RGB,
130
+ ) -> None:
131
+ """Check HSV or RGB Values and create named tuples."""
132
+ if isinstance(color_object, HSV):
133
+ self.hsv = color_object
134
+ self.rgb = self.hsv_to_rgb(*self.hsv.to_tuple())
135
+ elif isinstance(color_object, RGB):
136
+ self.rgb = color_object
137
+ self.hsv = self.rgb_to_hsv(*self.rgb.to_tuple())
138
+
139
+ def __str__(self) -> str:
140
+ """Return string representation."""
141
+ return f'Color(hsv={self.hsv}, rgb={self.rgb})'
142
+
143
+ def __repr__(self) -> str:
144
+ """Return string representation."""
145
+ return f'Color(hsv={self.hsv}, rgb={self.rgb})'
146
+
147
+ def as_dict(self) -> dict[str, dict]:
148
+ """Return color values as dict."""
149
+ return {
150
+ 'hsv': {
151
+ 'hue': self.hsv.hue,
152
+ 'saturation': self.hsv.saturation,
153
+ 'value': self.hsv.value,
154
+ },
155
+ 'rgb': {
156
+ 'red': self.rgb.red,
157
+ 'green': self.rgb.green,
158
+ 'blue': self.rgb.blue,
159
+ },
160
+ }
161
+
162
+ @classmethod
163
+ def from_rgb(
164
+ cls, red: float | None, green: float | None, blue: float | None
165
+ ) -> Color | None:
166
+ """Create Color instance from RGB values.
167
+
168
+ Args:
169
+ red (NUMERIC_STRICT): The red component of the color,
170
+ typically in the range [0, 255].
171
+ green (NUMERIC_STRICT): The green component of the color,
172
+ typically in the range [0, 255].
173
+ blue (NUMERIC_STRICT): The blue component of the color,
174
+ typically in the range [0, 255].
175
+
176
+ Returns:
177
+ Color | None: A Color object with the appropriate color values in RGB and HSV,
178
+ or None if the input values are invalid.
179
+ """
180
+ if not Validators.validate_rgb(red, green, blue):
181
+ _LOGGER.debug('Invalid RGB values')
182
+ return None
183
+ return cls(RGB(float(red), float(green), float(blue))) # type: ignore[arg-type]
184
+
185
+ @classmethod
186
+ def from_hsv(
187
+ cls, hue: float | None, saturation: float | None, value: float | None
188
+ ) -> Color | None:
189
+ """Create Color instance from HSV values.
190
+
191
+ Args:
192
+ hue (float): The hue component of the color,
193
+ in the range [0, 360).
194
+ saturation (float): The saturation component of the color,
195
+ typically in the range [0, 1].
196
+ value (float): The value (brightness) component of the color,
197
+ typically in the range [0, 1].
198
+
199
+ Returns:
200
+ Color | None: A Color object with the appropriate color values in RGB and HSV,
201
+ or None if the input values are invalid.
202
+ """
203
+ if not Validators.validate_hsv(hue, saturation, value):
204
+ _LOGGER.debug('Invalid HSV values')
205
+ return None
206
+ return cls(
207
+ HSV(float(hue), float(saturation), float(value)) # type: ignore[arg-type]
208
+ )
209
+
210
+ @staticmethod
211
+ def hsv_to_rgb(hue: float, saturation: float, value: float) -> RGB:
212
+ """Convert HSV to RGB.
213
+
214
+ Args:
215
+ hue (float): The hue component of the color, in the range [0, 360).
216
+ saturation (float): The saturation component of the color,
217
+ in the range [0, 1].
218
+ value (float): The value (brightness) component of the color,
219
+ in the range [0, 1].
220
+
221
+ Returns:
222
+ RGB: An RGB dataclass with red, green, and blue components.
223
+ """
224
+ return RGB(
225
+ *tuple(
226
+ round(i * 255, 0)
227
+ for i in colorsys.hsv_to_rgb(hue / 360, saturation / 100, value / 100)
228
+ )
229
+ )
230
+
231
+ @staticmethod
232
+ def rgb_to_hsv(red: float, green: float, blue: float) -> HSV:
233
+ """Convert RGB to HSV.
234
+
235
+ Args:
236
+ red (float): The red component of the color, in the range [0, 255].
237
+ green (float): The green component of the color, in the range [0, 255].
238
+ blue (float): The blue component of the color, in the range [0, 255].
239
+
240
+ Returns:
241
+ HSV: An HSV dataclass with hue, saturation, and value components.
242
+ """
243
+ hsv_tuple = colorsys.rgb_to_hsv(red / 255, green / 255, blue / 255)
244
+ hsv_factors = [360, 100, 100]
245
+
246
+ return HSV(
247
+ float(round(hsv_tuple[0] * hsv_factors[0], 2)),
248
+ float(round(hsv_tuple[1] * hsv_factors[1], 2)),
249
+ float(round(hsv_tuple[2] * hsv_factors[2], 0)),
250
+ )