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,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."""
|
pyvesync/utils/colors.py
ADDED
|
@@ -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
|
+
)
|