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,1126 @@
|
|
|
1
|
+
"""Etekcity/Valceno Smart Light Bulbs.
|
|
2
|
+
|
|
3
|
+
This module provides classes for the following Etekcity/Valceno smart lights:
|
|
4
|
+
|
|
5
|
+
1. ESL100: Dimmable Bulb
|
|
6
|
+
2. ESL100CW: Tunable White Bulb
|
|
7
|
+
3. XYD0001: RGB Bulb
|
|
8
|
+
4. ESL100MC: Multi-Color Bulb
|
|
9
|
+
|
|
10
|
+
The classes all inherit from VeSyncBulb, which is a subclass of VeSyncBaseDevice and
|
|
11
|
+
[VeSyncToggleDevice][pyvesync.base_devices.vesyncbasedevice.VeSyncBaseToggleDevice].
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from collections.abc import Mapping
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from typing_extensions import deprecated
|
|
21
|
+
|
|
22
|
+
from pyvesync.base_devices import VeSyncBulb
|
|
23
|
+
from pyvesync.const import ConnectionStatus, DeviceStatus
|
|
24
|
+
from pyvesync.models import bulb_models
|
|
25
|
+
from pyvesync.models.base_models import DefaultValues
|
|
26
|
+
from pyvesync.models.bypass_models import TimerModels
|
|
27
|
+
from pyvesync.utils.colors import Color
|
|
28
|
+
from pyvesync.utils.device_mixins import (
|
|
29
|
+
BypassV1Mixin,
|
|
30
|
+
BypassV2Mixin,
|
|
31
|
+
process_bypassv1_result,
|
|
32
|
+
process_bypassv2_result,
|
|
33
|
+
)
|
|
34
|
+
from pyvesync.utils.helpers import Helpers, Timer, Validators
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from pyvesync import VeSync
|
|
38
|
+
from pyvesync.device_map import BulbMap
|
|
39
|
+
from pyvesync.models.vesync_models import ResponseDeviceDetailsModel
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
NUMERIC_OPT = int | float | str | None
|
|
44
|
+
NUMERIC_STRICT = float | int | str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class VeSyncBulbESL100MC(BypassV2Mixin, VeSyncBulb):
|
|
48
|
+
"""Etekcity ESL100 Multi Color Bulb device.
|
|
49
|
+
|
|
50
|
+
Inherits from [VeSyncBulb][pyvesync.base_devices.bulb_base.VeSyncBulb]
|
|
51
|
+
and [VeSyncBaseDevice][pyvesync.base_devices.vesyncbasedevice.VeSyncBaseDevice].
|
|
52
|
+
|
|
53
|
+
The state of the bulb is stored in the `state` attribute, which is an of
|
|
54
|
+
[BulbState][pyvesync.base_devices.bulb_base.BulbState]. The `state` attribute
|
|
55
|
+
contains all settable states for the bulb.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
details (dict): Dictionary of bulb state details.
|
|
59
|
+
manager (VeSync): Manager class used to make API calls.
|
|
60
|
+
feature_map (BulbMap): Device configuration map.
|
|
61
|
+
|
|
62
|
+
Attributes:
|
|
63
|
+
state (BulbState): Device state object
|
|
64
|
+
Each device has a separate state base class in the base_devices module.
|
|
65
|
+
last_response (ResponseInfo): Last response from API call.
|
|
66
|
+
manager (VeSync): Manager object for API calls.
|
|
67
|
+
device_name (str): Name of device.
|
|
68
|
+
device_image (str): URL for device image.
|
|
69
|
+
cid (str): Device ID.
|
|
70
|
+
connection_type (str): Connection type of device.
|
|
71
|
+
device_type (str): Type of device.
|
|
72
|
+
type (str): Type of device.
|
|
73
|
+
uuid (str): UUID of device, not always present.
|
|
74
|
+
config_module (str): Configuration module of device.
|
|
75
|
+
mac_id (str): MAC ID of device.
|
|
76
|
+
current_firm_version (str): Current firmware version of device.
|
|
77
|
+
device_region (str): Region of device. (US, EU, etc.)
|
|
78
|
+
pid (str): Product ID of device, pulled by some devices on update.
|
|
79
|
+
sub_device_no (int): Sub-device number of device.
|
|
80
|
+
product_type (str): Product type of device.
|
|
81
|
+
features (dict): Features of device.
|
|
82
|
+
|
|
83
|
+
Notes:
|
|
84
|
+
The details dictionary contains the device information retreived by the
|
|
85
|
+
`update()` method:
|
|
86
|
+
```python
|
|
87
|
+
details = {
|
|
88
|
+
'brightness': 50,
|
|
89
|
+
'colorMode': 'rgb',
|
|
90
|
+
'color' : Color(red=0, green=0, blue=0)
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
See pyvesync.helpers.color.Color for more information on the Color dataclass.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
__slots__ = ()
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self, details: ResponseDeviceDetailsModel, manager: VeSync, feature_map: BulbMap
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Instantiate ESL100MC Multicolor Bulb."""
|
|
102
|
+
super().__init__(details, manager, feature_map)
|
|
103
|
+
|
|
104
|
+
async def get_details(self) -> None:
|
|
105
|
+
r_dict = await self.call_bypassv2_api('getLightStatus')
|
|
106
|
+
|
|
107
|
+
result_model = process_bypassv2_result(
|
|
108
|
+
self, logger, 'get_details', r_dict, bulb_models.ResponseESL100MCResult
|
|
109
|
+
)
|
|
110
|
+
if result_model is None:
|
|
111
|
+
return
|
|
112
|
+
self._set_state(result_model)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
def _set_state(self, response: bulb_models.ResponseESL100MCResult) -> None:
|
|
116
|
+
"""Build detail dictionary from response."""
|
|
117
|
+
self.state.brightness = response.brightness
|
|
118
|
+
self.state.color_mode = response.colorMode
|
|
119
|
+
self.state.color = Color.from_rgb(
|
|
120
|
+
red=response.red, green=response.green, blue=response.blue
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
async def set_brightness(self, brightness: int) -> bool:
|
|
124
|
+
return await self.set_status(brightness=brightness)
|
|
125
|
+
|
|
126
|
+
async def set_rgb(self, red: float, green: float, blue: float) -> bool:
|
|
127
|
+
return await self.set_status(red=red, green=green, blue=blue)
|
|
128
|
+
|
|
129
|
+
async def set_hsv(self, hue: float, saturation: float, value: float) -> bool:
|
|
130
|
+
hsv = Color.from_hsv(hue=hue, saturation=saturation, value=value)
|
|
131
|
+
if hsv is not None:
|
|
132
|
+
return await self.set_status(
|
|
133
|
+
red=hsv.rgb.red, green=hsv.rgb.green, blue=hsv.rgb.blue
|
|
134
|
+
)
|
|
135
|
+
logger.debug('Invalid HSV values')
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
async def set_white_mode(self) -> bool:
|
|
139
|
+
return await self.set_status(brightness=100)
|
|
140
|
+
|
|
141
|
+
async def set_status(
|
|
142
|
+
self,
|
|
143
|
+
brightness: float | None = None,
|
|
144
|
+
red: float | None = None,
|
|
145
|
+
green: float | None = None,
|
|
146
|
+
blue: float | None = None,
|
|
147
|
+
) -> bool:
|
|
148
|
+
"""Set color of VeSync ESL100MC.
|
|
149
|
+
|
|
150
|
+
Brightness or RGB values must be provided. If RGB values are provided,
|
|
151
|
+
brightness is ignored.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
brightness (float | None): Brightness of bulb (0-100).
|
|
155
|
+
red (float | None): Red value of RGB color, 0-255.
|
|
156
|
+
green (float | None): Green value of RGB color, 0-255.
|
|
157
|
+
blue (float | None): Blue value of RGB color, 0-255.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
bool: True if successful, False otherwise.
|
|
161
|
+
"""
|
|
162
|
+
brightness_update = 100
|
|
163
|
+
if red is not None and green is not None and blue is not None:
|
|
164
|
+
new_color = Color.from_rgb(red, green, blue)
|
|
165
|
+
color_mode = 'color'
|
|
166
|
+
if (
|
|
167
|
+
self.state.device_status == DeviceStatus.ON
|
|
168
|
+
and new_color == self.state.color
|
|
169
|
+
):
|
|
170
|
+
logger.debug('New color is same as current color')
|
|
171
|
+
return True
|
|
172
|
+
else:
|
|
173
|
+
logger.debug('RGB Values not provided')
|
|
174
|
+
new_color = None
|
|
175
|
+
if brightness is not None:
|
|
176
|
+
if Validators.validate_zero_to_hundred(brightness):
|
|
177
|
+
brightness_update = int(brightness)
|
|
178
|
+
else:
|
|
179
|
+
logger.debug('Invalid brightness value')
|
|
180
|
+
return False
|
|
181
|
+
if (
|
|
182
|
+
self.state.device_status == DeviceStatus.ON
|
|
183
|
+
and brightness_update == self.state.brightness
|
|
184
|
+
):
|
|
185
|
+
logger.debug('Brightness already set to %s', brightness)
|
|
186
|
+
return True
|
|
187
|
+
color_mode = 'white'
|
|
188
|
+
else:
|
|
189
|
+
logger.debug('Brightness and RGB values are not set')
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
data = {
|
|
193
|
+
'action': DeviceStatus.ON,
|
|
194
|
+
'speed': 0,
|
|
195
|
+
'brightness': brightness_update,
|
|
196
|
+
'red': 0 if new_color is None else int(new_color.rgb.red),
|
|
197
|
+
'green': 0 if new_color is None else int(new_color.rgb.green),
|
|
198
|
+
'blue': 0 if new_color is None else int(new_color.rgb.blue),
|
|
199
|
+
'colorMode': 'color' if new_color is not None else 'white',
|
|
200
|
+
}
|
|
201
|
+
r_dict = await self.call_bypassv2_api('setLightStatus', data)
|
|
202
|
+
|
|
203
|
+
r = Helpers.process_dev_response(logger, 'set_status', self, r_dict)
|
|
204
|
+
if r is None:
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
if color_mode == 'color' and new_color is not None:
|
|
208
|
+
self.state.color_mode = 'color'
|
|
209
|
+
self.state.color = new_color
|
|
210
|
+
elif brightness is not None:
|
|
211
|
+
self.state.brightness = int(brightness_update)
|
|
212
|
+
self.state.color_mode = 'white'
|
|
213
|
+
|
|
214
|
+
self.state.device_status = DeviceStatus.ON
|
|
215
|
+
self.state.connection_status = ConnectionStatus.ONLINE
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
@deprecated(
|
|
219
|
+
'toggle() is deprecated, use toggle_switch(toggle: bool | None = None) instead'
|
|
220
|
+
)
|
|
221
|
+
async def toggle(self, status: str) -> bool:
|
|
222
|
+
"""Toggle switch of VeSync ESL100MC."""
|
|
223
|
+
status_bool = status == DeviceStatus.ON
|
|
224
|
+
return await self.toggle_switch(status_bool)
|
|
225
|
+
|
|
226
|
+
async def toggle_switch(self, toggle: bool | None = None) -> bool:
|
|
227
|
+
if toggle is None:
|
|
228
|
+
toggle = self.state.device_status == DeviceStatus.OFF
|
|
229
|
+
data = {'id': 0, 'enabled': toggle}
|
|
230
|
+
|
|
231
|
+
r_dict = await self.call_bypassv2_api('setSwitch', data)
|
|
232
|
+
|
|
233
|
+
r = Helpers.process_dev_response(logger, 'toggle', self, r_dict)
|
|
234
|
+
if r is None:
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
self.state.device_status = DeviceStatus.ON if toggle else DeviceStatus.OFF
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class VeSyncBulbESL100(BypassV1Mixin, VeSyncBulb):
|
|
242
|
+
"""Object to hold VeSync ESL100 light bulb.
|
|
243
|
+
|
|
244
|
+
Device state is held in the `state` attribute, which is an instance of
|
|
245
|
+
[BulbState][pyvesync.base_devices.bulb_base.BulbState]. The `state` attribute
|
|
246
|
+
contains all settable states for the bulb.
|
|
247
|
+
|
|
248
|
+
This bulb only has the dimmable feature. Inherits from
|
|
249
|
+
[VeSyncBulb][pyvesync.devices.vesyncbulb.VeSyncBulb] and
|
|
250
|
+
[VeSyncBaseToggleDevice][pyvesync.base_devices.vesyncbasedevice.VeSyncBaseToggleDevice].
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
details (dict): Dictionary of bulb state details.
|
|
254
|
+
manager (VeSync): Manager class used to make API calls
|
|
255
|
+
feature_map (BulbMap): Device configuration map.
|
|
256
|
+
|
|
257
|
+
Attributes:
|
|
258
|
+
state (BulbState): Device state object
|
|
259
|
+
Each device has a separate state base class in the base_devices module.
|
|
260
|
+
last_response (ResponseInfo): Last response from API call.
|
|
261
|
+
manager (VeSync): Manager object for API calls.
|
|
262
|
+
device_name (str): Name of device.
|
|
263
|
+
device_image (str): URL for device image.
|
|
264
|
+
cid (str): Device ID.
|
|
265
|
+
connection_type (str): Connection type of device.
|
|
266
|
+
device_type (str): Type of device.
|
|
267
|
+
type (str): Type of device.
|
|
268
|
+
uuid (str): UUID of device, not always present.
|
|
269
|
+
config_module (str): Configuration module of device.
|
|
270
|
+
mac_id (str): MAC ID of device.
|
|
271
|
+
current_firm_version (str): Current firmware version of device.
|
|
272
|
+
device_region (str): Region of device. (US, EU, etc.)
|
|
273
|
+
pid (str): Product ID of device, pulled by some devices on update.
|
|
274
|
+
sub_device_no (int): Sub-device number of device.
|
|
275
|
+
product_type (str): Product type of device.
|
|
276
|
+
features (dict): Features of device.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
__slots__ = ()
|
|
280
|
+
|
|
281
|
+
def __init__(
|
|
282
|
+
self, details: ResponseDeviceDetailsModel, manager: VeSync, feature_map: BulbMap
|
|
283
|
+
) -> None:
|
|
284
|
+
"""Initialize Etekcity ESL100 Dimmable Bulb."""
|
|
285
|
+
super().__init__(details, manager, feature_map)
|
|
286
|
+
|
|
287
|
+
async def get_details(self) -> None:
|
|
288
|
+
method_dict = {
|
|
289
|
+
'method': 'deviceDetail',
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
r_dict = await self.call_bypassv1_api(
|
|
293
|
+
bulb_models.RequestESL100Detail, method_dict, 'deviceDetail', 'deviceDetail'
|
|
294
|
+
)
|
|
295
|
+
model = process_bypassv1_result(
|
|
296
|
+
self, logger, 'get_details', r_dict, bulb_models.ResponseESL100DetailResult
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if model is None:
|
|
300
|
+
self.state.connection_status = ConnectionStatus.OFFLINE
|
|
301
|
+
return
|
|
302
|
+
self.state.brightness = model.brightness
|
|
303
|
+
self.state.device_status = model.deviceStatus
|
|
304
|
+
self.state.connection_status = model.connectionStatus
|
|
305
|
+
|
|
306
|
+
@deprecated(
|
|
307
|
+
'toggle() is deprecated, use toggle_switch(toggle: bool | None = None) instead'
|
|
308
|
+
)
|
|
309
|
+
async def toggle(self, status: str) -> bool:
|
|
310
|
+
"""Toggle switch of ESL100 bulb."""
|
|
311
|
+
status_bool = status != DeviceStatus.ON
|
|
312
|
+
return await self.toggle_switch(status_bool)
|
|
313
|
+
|
|
314
|
+
async def toggle_switch(self, toggle: bool | None = None) -> bool:
|
|
315
|
+
if toggle is None:
|
|
316
|
+
toggle = self.state.device_status != DeviceStatus.ON
|
|
317
|
+
status = DeviceStatus.ON if toggle else DeviceStatus.OFF
|
|
318
|
+
method_dict = {
|
|
319
|
+
'status': status,
|
|
320
|
+
}
|
|
321
|
+
r_dict = await self.call_bypassv1_api(
|
|
322
|
+
bulb_models.RequestESL100Status,
|
|
323
|
+
method_dict,
|
|
324
|
+
'smartBulbPowerSwitchCtl',
|
|
325
|
+
'smartBulbPowerSwitchCtl',
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
r = Helpers.process_dev_response(logger, 'toggle', self, r_dict)
|
|
329
|
+
if r is None:
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
self.state.device_status = status
|
|
333
|
+
return True
|
|
334
|
+
|
|
335
|
+
@deprecated('Use set_brightness() instead')
|
|
336
|
+
async def set_status(self, brightness: int) -> bool:
|
|
337
|
+
"""Set brightness of dimmable bulb.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
brightness (int): Brightness of bulb (0-100).
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
bool: True if successful, False otherwise.
|
|
344
|
+
"""
|
|
345
|
+
return await self.set_brightness(brightness=brightness)
|
|
346
|
+
|
|
347
|
+
async def set_brightness(self, brightness: int) -> bool:
|
|
348
|
+
if not self.supports_brightness:
|
|
349
|
+
logger.warning('%s is not dimmable', self.device_name)
|
|
350
|
+
return False
|
|
351
|
+
if not Validators.validate_zero_to_hundred(brightness):
|
|
352
|
+
logger.debug('Invalid brightness value')
|
|
353
|
+
return False
|
|
354
|
+
brightness_update = brightness
|
|
355
|
+
if (
|
|
356
|
+
self.state.device_status == DeviceStatus.ON
|
|
357
|
+
and brightness_update == self.supports_brightness
|
|
358
|
+
):
|
|
359
|
+
logger.debug('Device already in requested state')
|
|
360
|
+
return True
|
|
361
|
+
|
|
362
|
+
method_dict = {
|
|
363
|
+
'brightNess': str(brightness_update),
|
|
364
|
+
'status': 'on',
|
|
365
|
+
}
|
|
366
|
+
r_dict = await self.call_bypassv1_api(
|
|
367
|
+
bulb_models.RequestESL100Brightness,
|
|
368
|
+
method_dict,
|
|
369
|
+
'smartBulbBrightnessCtl',
|
|
370
|
+
'smartBulbBrightnessCtl',
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
r = Helpers.process_dev_response(logger, 'set_brightness', self, r_dict)
|
|
374
|
+
if r is None:
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
self.state.brightness = brightness_update
|
|
378
|
+
self.state.device_status = DeviceStatus.ON
|
|
379
|
+
self.state.connection_status = ConnectionStatus.ONLINE
|
|
380
|
+
return True
|
|
381
|
+
|
|
382
|
+
async def get_timer(self) -> None:
|
|
383
|
+
r_dict = await self.call_bypassv1_api(
|
|
384
|
+
TimerModels.RequestV1GetTimer, {}, 'getTimers', 'timer/getTimers'
|
|
385
|
+
)
|
|
386
|
+
result_model = process_bypassv1_result(
|
|
387
|
+
self, logger, 'get_timer', r_dict, TimerModels.ResultV1GetTimer
|
|
388
|
+
)
|
|
389
|
+
if result_model is None:
|
|
390
|
+
return
|
|
391
|
+
if not isinstance(result_model.timers, list) or not result_model.timers:
|
|
392
|
+
self.state.timer = None
|
|
393
|
+
logger.debug('No timers found')
|
|
394
|
+
return
|
|
395
|
+
timer = result_model.timers
|
|
396
|
+
if not isinstance(timer, TimerModels.TimerItemV1):
|
|
397
|
+
logger.debug('Invalid timer item type')
|
|
398
|
+
return
|
|
399
|
+
self.state.timer = Timer(
|
|
400
|
+
int(timer.counterTimer),
|
|
401
|
+
timer.action,
|
|
402
|
+
int(timer.timerID),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
async def set_timer(self, duration: int, action: str | None = None) -> bool:
|
|
406
|
+
if action is None:
|
|
407
|
+
action = (
|
|
408
|
+
DeviceStatus.ON
|
|
409
|
+
if self.state.device_status == DeviceStatus.OFF
|
|
410
|
+
else DeviceStatus.OFF
|
|
411
|
+
)
|
|
412
|
+
if action not in [DeviceStatus.ON, DeviceStatus.OFF]:
|
|
413
|
+
logger.debug("Invalid action value - must be 'on' or 'off'")
|
|
414
|
+
return False
|
|
415
|
+
update_dict = {
|
|
416
|
+
'action': action,
|
|
417
|
+
'counterTime': str(duration),
|
|
418
|
+
'status': '1',
|
|
419
|
+
}
|
|
420
|
+
r_dict = await self.call_bypassv1_api(
|
|
421
|
+
TimerModels.RequestV1SetTime, update_dict, 'addTimer', 'timer/addTimer'
|
|
422
|
+
)
|
|
423
|
+
result_model = process_bypassv1_result(
|
|
424
|
+
self, logger, 'set_timer', r_dict, TimerModels.ResultV1SetTimer
|
|
425
|
+
)
|
|
426
|
+
if result_model is None:
|
|
427
|
+
return False
|
|
428
|
+
self.state.timer = Timer(duration, action, int(result_model.timerID))
|
|
429
|
+
return True
|
|
430
|
+
|
|
431
|
+
async def clear_timer(self) -> bool:
|
|
432
|
+
if self.state.timer is None:
|
|
433
|
+
logger.debug('No timer set - run get_timer() first')
|
|
434
|
+
return False
|
|
435
|
+
timer = self.state.timer
|
|
436
|
+
r_dict = await self.call_bypassv1_api(
|
|
437
|
+
TimerModels.RequestV1ClearTimer,
|
|
438
|
+
{'timerId': str(timer.id), 'status': '1'},
|
|
439
|
+
'deleteTimer',
|
|
440
|
+
'timer/deleteTimer',
|
|
441
|
+
)
|
|
442
|
+
result = Helpers.process_dev_response(logger, 'clear_timer', self, r_dict)
|
|
443
|
+
if result is None:
|
|
444
|
+
return False
|
|
445
|
+
self.state.timer = None
|
|
446
|
+
return True
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class VeSyncBulbESL100CW(BypassV1Mixin, VeSyncBulb):
|
|
450
|
+
"""VeSync Tunable and Dimmable White Bulb.
|
|
451
|
+
|
|
452
|
+
This bulb only has the dimmable feature. Inherits from
|
|
453
|
+
[VeSyncBulb][pyvesync.devices.vesyncbulb.VeSyncBulb] and
|
|
454
|
+
[VeSyncBaseToggleDevice][pyvesync.base_devices.vesyncbasedevice.VeSyncBaseToggleDevice].
|
|
455
|
+
|
|
456
|
+
Device state is held in the `state` attribute, which is an instance of
|
|
457
|
+
[BulbState][pyvesync.base_devices.bulb_base.BulbState]. The `state` attribute
|
|
458
|
+
contains all settable states for the bulb.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
details (dict): Dictionary of bulb state details.
|
|
462
|
+
manager (VeSync): Manager class used to make API calls
|
|
463
|
+
feature_map (BulbMap): Device configuration map.
|
|
464
|
+
|
|
465
|
+
Attributes:
|
|
466
|
+
state (BulbState): Device state object
|
|
467
|
+
Each device has a separate state base class in the base_devices module.
|
|
468
|
+
last_response (ResponseInfo): Last response from API call.
|
|
469
|
+
manager (VeSync): Manager object for API calls.
|
|
470
|
+
device_name (str): Name of device.
|
|
471
|
+
device_image (str): URL for device image.
|
|
472
|
+
cid (str): Device ID.
|
|
473
|
+
connection_type (str): Connection type of device.
|
|
474
|
+
device_type (str): Type of device.
|
|
475
|
+
type (str): Type of device.
|
|
476
|
+
uuid (str): UUID of device, not always present.
|
|
477
|
+
config_module (str): Configuration module of device.
|
|
478
|
+
mac_id (str): MAC ID of device.
|
|
479
|
+
current_firm_version (str): Current firmware version of device.
|
|
480
|
+
device_region (str): Region of device. (US, EU, etc.)
|
|
481
|
+
pid (str): Product ID of device, pulled by some devices on update.
|
|
482
|
+
sub_device_no (int): Sub-device number of device.
|
|
483
|
+
product_type (str): Product type of device.
|
|
484
|
+
features (dict): Features of device.
|
|
485
|
+
"""
|
|
486
|
+
|
|
487
|
+
__slots__ = ()
|
|
488
|
+
|
|
489
|
+
def __init__(
|
|
490
|
+
self, details: ResponseDeviceDetailsModel, manager: VeSync, feature_map: BulbMap
|
|
491
|
+
) -> None:
|
|
492
|
+
"""Initialize Etekcity Tunable white bulb."""
|
|
493
|
+
super().__init__(details, manager, feature_map)
|
|
494
|
+
|
|
495
|
+
async def get_details(self) -> None:
|
|
496
|
+
r_dict = await self.call_bypassv1_api(
|
|
497
|
+
bulb_models.RequestESL100CWBase,
|
|
498
|
+
{'jsonCmd': {'getLightStatus': 'get'}},
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
light_resp = process_bypassv1_result(
|
|
502
|
+
self, logger, 'get_details', r_dict, bulb_models.ResponseESL100CWDetailResult
|
|
503
|
+
)
|
|
504
|
+
if light_resp is None:
|
|
505
|
+
self.state.connection_status = ConnectionStatus.OFFLINE
|
|
506
|
+
return
|
|
507
|
+
self._interpret_apicall_result(light_resp)
|
|
508
|
+
|
|
509
|
+
def _interpret_apicall_result(
|
|
510
|
+
self, response: bulb_models.ResponseESL100CWDetailResult
|
|
511
|
+
) -> None:
|
|
512
|
+
self.state.connection_status = ConnectionStatus.ONLINE
|
|
513
|
+
result = response.light
|
|
514
|
+
self.state.device_status = result.action
|
|
515
|
+
self.state.brightness = result.brightness
|
|
516
|
+
self.state.color_temp = result.colorTempe
|
|
517
|
+
|
|
518
|
+
@deprecated(
|
|
519
|
+
'toggle() is deprecated, use toggle_switch(toggle: bool | None = None) instead'
|
|
520
|
+
)
|
|
521
|
+
async def toggle(self, status: str) -> bool:
|
|
522
|
+
"""Deprecated - use toggle_switch()."""
|
|
523
|
+
status_bool = status == DeviceStatus.ON
|
|
524
|
+
return await self.toggle_switch(status_bool)
|
|
525
|
+
|
|
526
|
+
async def toggle_switch(self, toggle: bool | None = None) -> bool:
|
|
527
|
+
if toggle is None:
|
|
528
|
+
toggle = self.state.device_status == DeviceStatus.OFF
|
|
529
|
+
status = DeviceStatus.ON if toggle else DeviceStatus.OFF
|
|
530
|
+
|
|
531
|
+
r_dict = await self.call_bypassv1_api(
|
|
532
|
+
bulb_models.RequestESL100CWBase,
|
|
533
|
+
{'jsonCmd': {'light': {'action': status}}},
|
|
534
|
+
'bypass',
|
|
535
|
+
'bypass',
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
r = Helpers.process_dev_response(logger, 'toggle', self, r_dict)
|
|
539
|
+
if r is None:
|
|
540
|
+
logger.debug('%s offline', self.device_name)
|
|
541
|
+
return False
|
|
542
|
+
self.state.device_status = status
|
|
543
|
+
return True
|
|
544
|
+
|
|
545
|
+
async def set_brightness(self, brightness: int) -> bool:
|
|
546
|
+
"""Set brightness of tunable bulb."""
|
|
547
|
+
return await self.set_status(brightness=brightness)
|
|
548
|
+
|
|
549
|
+
async def set_status(
|
|
550
|
+
self, /, brightness: int | None = None, color_temp: int | None = None
|
|
551
|
+
) -> bool:
|
|
552
|
+
"""Set status of tunable bulb."""
|
|
553
|
+
if brightness is not None:
|
|
554
|
+
if Validators.validate_zero_to_hundred(brightness):
|
|
555
|
+
brightness_update = int(brightness)
|
|
556
|
+
else:
|
|
557
|
+
logger.debug('Invalid brightness value')
|
|
558
|
+
return False
|
|
559
|
+
elif self.state.brightness is not None:
|
|
560
|
+
brightness_update = self.state.brightness
|
|
561
|
+
else:
|
|
562
|
+
brightness_update = 100
|
|
563
|
+
if color_temp is not None:
|
|
564
|
+
if Validators.validate_zero_to_hundred(color_temp):
|
|
565
|
+
color_temp_update = color_temp
|
|
566
|
+
else:
|
|
567
|
+
logger.debug('Invalid color temperature value')
|
|
568
|
+
return False
|
|
569
|
+
elif self.state.color_temp is not None:
|
|
570
|
+
color_temp_update = self.state.color_temp
|
|
571
|
+
else:
|
|
572
|
+
color_temp_update = 100
|
|
573
|
+
if (
|
|
574
|
+
self.state.device_status == DeviceStatus.ON
|
|
575
|
+
and brightness_update == self.state.brightness
|
|
576
|
+
and color_temp_update == self.state.color_temp
|
|
577
|
+
):
|
|
578
|
+
logger.debug('Device already in requested state')
|
|
579
|
+
return True
|
|
580
|
+
light_dict: dict[str, NUMERIC_OPT | str] = {
|
|
581
|
+
'colorTempe': color_temp_update,
|
|
582
|
+
'brightness': brightness_update,
|
|
583
|
+
'action': DeviceStatus.ON,
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
r_dict = await self.call_bypassv1_api(
|
|
587
|
+
bulb_models.RequestESL100CWBase,
|
|
588
|
+
{'jsonCmd': {'light': light_dict}},
|
|
589
|
+
'bypass',
|
|
590
|
+
'bypass',
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
r = Helpers.process_dev_response(logger, 'set_brightness', self, r_dict)
|
|
594
|
+
if r is None:
|
|
595
|
+
return False
|
|
596
|
+
|
|
597
|
+
self.state.brightness = brightness_update
|
|
598
|
+
self.state.color_temp = color_temp_update
|
|
599
|
+
self.state.device_status = DeviceStatus.ON
|
|
600
|
+
self.state.connection_status = ConnectionStatus.ONLINE
|
|
601
|
+
return True
|
|
602
|
+
|
|
603
|
+
async def set_color_temp(self, color_temp: int) -> bool:
|
|
604
|
+
return await self.set_status(color_temp=color_temp)
|
|
605
|
+
|
|
606
|
+
async def get_timer(self) -> None:
|
|
607
|
+
r_dict = await self.call_bypassv1_api(
|
|
608
|
+
TimerModels.RequestV1GetTimer, {}, 'getTimers', 'timer/getTimers'
|
|
609
|
+
)
|
|
610
|
+
result_model = process_bypassv1_result(
|
|
611
|
+
self, logger, 'get_timer', r_dict, TimerModels.ResultV1GetTimer
|
|
612
|
+
)
|
|
613
|
+
if result_model is None:
|
|
614
|
+
return
|
|
615
|
+
if not isinstance(result_model.timers, list) or not result_model.timers:
|
|
616
|
+
logger.debug('No timers found')
|
|
617
|
+
return
|
|
618
|
+
timers = result_model.timers
|
|
619
|
+
if len(timers) > 1:
|
|
620
|
+
logger.debug('Multiple timers found, returning first timer')
|
|
621
|
+
timer = timers[0]
|
|
622
|
+
if not isinstance(timer, TimerModels.TimeItemV1):
|
|
623
|
+
logger.debug('Invalid timer item type')
|
|
624
|
+
return
|
|
625
|
+
self.state.timer = Timer(
|
|
626
|
+
int(timer.counterTime),
|
|
627
|
+
timer.action,
|
|
628
|
+
int(timer.timerID),
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
async def set_timer(self, duration: int, action: str | None = None) -> bool:
|
|
632
|
+
if action is None:
|
|
633
|
+
action = (
|
|
634
|
+
DeviceStatus.ON
|
|
635
|
+
if self.state.device_status == DeviceStatus.OFF
|
|
636
|
+
else DeviceStatus.OFF
|
|
637
|
+
)
|
|
638
|
+
if action not in [DeviceStatus.ON, DeviceStatus.OFF]:
|
|
639
|
+
logger.debug("Invalid action value - must be 'on' or 'off'")
|
|
640
|
+
return False
|
|
641
|
+
update_dict = {
|
|
642
|
+
'action': action,
|
|
643
|
+
'counterTime': str(duration),
|
|
644
|
+
'status': '1',
|
|
645
|
+
}
|
|
646
|
+
r_dict = await self.call_bypassv1_api(
|
|
647
|
+
TimerModels.RequestV1SetTime, update_dict, 'addTimer', 'timer/addTimer'
|
|
648
|
+
)
|
|
649
|
+
result_model = process_bypassv1_result(
|
|
650
|
+
self, logger, 'set_timer', r_dict, TimerModels.ResultV1SetTimer
|
|
651
|
+
)
|
|
652
|
+
if result_model is None:
|
|
653
|
+
return False
|
|
654
|
+
self.state.timer = Timer(duration, action, int(result_model.timerID))
|
|
655
|
+
return True
|
|
656
|
+
|
|
657
|
+
async def clear_timer(self) -> bool:
|
|
658
|
+
if self.state.timer is None:
|
|
659
|
+
logger.debug('No timer set - run get_timer() first')
|
|
660
|
+
return False
|
|
661
|
+
timer = self.state.timer
|
|
662
|
+
r_dict = await self.call_bypassv1_api(
|
|
663
|
+
TimerModels.RequestV1ClearTimer,
|
|
664
|
+
{'timerId': str(timer.id), 'status': '1'},
|
|
665
|
+
'deleteTimer',
|
|
666
|
+
'timer/deleteTimer',
|
|
667
|
+
)
|
|
668
|
+
r = Helpers.process_dev_response(logger, 'clear_timer', self, r_dict)
|
|
669
|
+
if r is None:
|
|
670
|
+
return False
|
|
671
|
+
self.state.timer = None
|
|
672
|
+
return True
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
class VeSyncBulbValcenoA19MC(VeSyncBulb):
|
|
676
|
+
"""VeSync Multicolor Bulb.
|
|
677
|
+
|
|
678
|
+
This bulb only has the dimmable feature. Inherits from
|
|
679
|
+
[VeSyncBulb][pyvesync.devices.vesyncbulb.VeSyncBulb] and
|
|
680
|
+
[VeSyncBaseToggleDevice][pyvesync.base_devices.vesyncbasedevice.VeSyncBaseToggleDevice].
|
|
681
|
+
|
|
682
|
+
Device state is held in the `state` attribute, which is an instance of
|
|
683
|
+
[BulbState][pyvesync.base_devices.bulb_base.BulbState]. The `state` attribute
|
|
684
|
+
contains all settable states for the bulb.
|
|
685
|
+
|
|
686
|
+
Args:
|
|
687
|
+
details (dict): Dictionary of bulb state details.
|
|
688
|
+
manager (VeSync): Manager class used to make API calls
|
|
689
|
+
feature_map (BulbMap): Device configuration map.
|
|
690
|
+
|
|
691
|
+
Attributes:
|
|
692
|
+
state (BulbState): Device state object
|
|
693
|
+
Each device has a separate state base class in the base_devices module.
|
|
694
|
+
last_response (ResponseInfo): Last response from API call.
|
|
695
|
+
manager (VeSync): Manager object for API calls.
|
|
696
|
+
device_name (str): Name of device.
|
|
697
|
+
device_image (str): URL for device image.
|
|
698
|
+
cid (str): Device ID.
|
|
699
|
+
connection_type (str): Connection type of device.
|
|
700
|
+
device_type (str): Type of device.
|
|
701
|
+
type (str): Type of device.
|
|
702
|
+
uuid (str): UUID of device, not always present.
|
|
703
|
+
config_module (str): Configuration module of device.
|
|
704
|
+
mac_id (str): MAC ID of device.
|
|
705
|
+
current_firm_version (str): Current firmware version of device.
|
|
706
|
+
device_region (str): Region of device. (US, EU, etc.)
|
|
707
|
+
pid (str): Product ID of device, pulled by some devices on update.
|
|
708
|
+
sub_device_no (int): Sub-device number of device.
|
|
709
|
+
product_type (str): Product type of device.
|
|
710
|
+
features (dict): Features of device.
|
|
711
|
+
"""
|
|
712
|
+
|
|
713
|
+
__slots__ = ()
|
|
714
|
+
|
|
715
|
+
def __init__(
|
|
716
|
+
self, details: ResponseDeviceDetailsModel, manager: VeSync, feature_map: BulbMap
|
|
717
|
+
) -> None:
|
|
718
|
+
"""Initialize Multicolor bulb."""
|
|
719
|
+
super().__init__(details, manager, feature_map)
|
|
720
|
+
self.request_keys = [
|
|
721
|
+
'acceptLanguage',
|
|
722
|
+
'accountID',
|
|
723
|
+
'appVersion',
|
|
724
|
+
'cid',
|
|
725
|
+
'configModule',
|
|
726
|
+
'deviceRegion',
|
|
727
|
+
'debugMode',
|
|
728
|
+
'phoneBrand',
|
|
729
|
+
'phoneOS',
|
|
730
|
+
'timeZone',
|
|
731
|
+
'token',
|
|
732
|
+
'traceId',
|
|
733
|
+
]
|
|
734
|
+
|
|
735
|
+
def _payload_base(self) -> bulb_models.ValcenoStatusPayload:
|
|
736
|
+
"""Return the payload base for the set status request.
|
|
737
|
+
|
|
738
|
+
Avoid duplicating code and ensure that the payload dict is
|
|
739
|
+
reset to its default state before each request.
|
|
740
|
+
"""
|
|
741
|
+
payload_dict: bulb_models.ValcenoStatusPayload = {
|
|
742
|
+
'force': 0,
|
|
743
|
+
'brightness': '',
|
|
744
|
+
'colorTemp': '',
|
|
745
|
+
'colorMode': '',
|
|
746
|
+
'hue': '',
|
|
747
|
+
'saturation': '',
|
|
748
|
+
'value': '',
|
|
749
|
+
}
|
|
750
|
+
return payload_dict
|
|
751
|
+
|
|
752
|
+
def _build_request(self, payload: dict) -> dict:
|
|
753
|
+
"""Build request for Valceno Smart Bulb.
|
|
754
|
+
|
|
755
|
+
The request is built from the keys in `self.request_keys` and the
|
|
756
|
+
`payload` dict. The payload is then added to the request body.
|
|
757
|
+
|
|
758
|
+
Args:
|
|
759
|
+
method (str): The method to use for the request.
|
|
760
|
+
payload (dict): The data to use for the payload.
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
dict: The request body.
|
|
764
|
+
|
|
765
|
+
Note:
|
|
766
|
+
The `payload` argument is the value of request_body['payload'].
|
|
767
|
+
The request structure is:
|
|
768
|
+
```json
|
|
769
|
+
{
|
|
770
|
+
"acceptLanguage": "en",
|
|
771
|
+
"accountID": "1234567890",
|
|
772
|
+
"appVersion": "1.0.0",
|
|
773
|
+
"cid": "1234567890",
|
|
774
|
+
"configModule": "default",
|
|
775
|
+
"deviceRegion": "US",
|
|
776
|
+
"debugMode": False,
|
|
777
|
+
"phoneBrand": "Apple",
|
|
778
|
+
"phoneOS": "iOS",
|
|
779
|
+
"timeZone": "New_York",
|
|
780
|
+
"token": "",
|
|
781
|
+
"traceId": "",
|
|
782
|
+
"payload": {
|
|
783
|
+
"method": "getLightStatusV2",
|
|
784
|
+
"source": "APP",
|
|
785
|
+
"data": {}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
```
|
|
789
|
+
"""
|
|
790
|
+
default_dict = Helpers.get_class_attributes(DefaultValues, self.request_keys)
|
|
791
|
+
default_dict.update(Helpers.get_class_attributes(self, self.request_keys))
|
|
792
|
+
default_dict.update(Helpers.get_class_attributes(self.manager, self.request_keys))
|
|
793
|
+
default_dict['method'] = 'bypassV2'
|
|
794
|
+
default_dict['payload'] = payload
|
|
795
|
+
return default_dict
|
|
796
|
+
|
|
797
|
+
async def _call_valceno_api(
|
|
798
|
+
self, payload_method: str, payload_data: Mapping
|
|
799
|
+
) -> dict | None:
|
|
800
|
+
"""Make API call to Valceno Smart Bulb.
|
|
801
|
+
|
|
802
|
+
Args:
|
|
803
|
+
payload_method (str): The method to use for the request.
|
|
804
|
+
payload_data (dict): The payload to use for the request.
|
|
805
|
+
|
|
806
|
+
Returns:
|
|
807
|
+
tuple[bytes, Any]: The response from the API call.
|
|
808
|
+
|
|
809
|
+
Note:
|
|
810
|
+
The request is built by the `_build_request` method and the payload is
|
|
811
|
+
added to the request body.
|
|
812
|
+
The payload structure is:
|
|
813
|
+
```json
|
|
814
|
+
{
|
|
815
|
+
"method": "getLightStatusV2",
|
|
816
|
+
"source": "APP",
|
|
817
|
+
"data": {}
|
|
818
|
+
}
|
|
819
|
+
"""
|
|
820
|
+
payload = {'method': payload_method, 'source': 'APP', 'data': payload_data}
|
|
821
|
+
request_body = self._build_request(payload)
|
|
822
|
+
r_dict, _ = await self.manager.async_call_api(
|
|
823
|
+
'/cloud/v2/deviceManaged/bypassV2',
|
|
824
|
+
'post',
|
|
825
|
+
headers=Helpers.req_header_bypass(),
|
|
826
|
+
json_object=request_body,
|
|
827
|
+
)
|
|
828
|
+
resp = Helpers.process_dev_response(logger, payload_method, self, r_dict)
|
|
829
|
+
if resp is None:
|
|
830
|
+
return None
|
|
831
|
+
return r_dict
|
|
832
|
+
|
|
833
|
+
async def get_details(self) -> None:
|
|
834
|
+
r_dict = await self._call_valceno_api('getLightStatusV2', {})
|
|
835
|
+
|
|
836
|
+
if r_dict is None:
|
|
837
|
+
return
|
|
838
|
+
|
|
839
|
+
status = bulb_models.ResponseValcenoStatus.from_dict(r_dict)
|
|
840
|
+
self._interpret_apicall_result(status)
|
|
841
|
+
|
|
842
|
+
def _interpret_apicall_result(
|
|
843
|
+
self, response: bulb_models.ResponseValcenoStatus
|
|
844
|
+
) -> None:
|
|
845
|
+
"""Process API response with device status.
|
|
846
|
+
|
|
847
|
+
Assigns the values to the state attributes of the device.
|
|
848
|
+
|
|
849
|
+
Args:
|
|
850
|
+
response (bulb_models.ResponseValcenoStatus): The response from the API call.
|
|
851
|
+
"""
|
|
852
|
+
result = response.result.result
|
|
853
|
+
self.state.connection_status = ConnectionStatus.ONLINE
|
|
854
|
+
self.state.device_status = result.enabled
|
|
855
|
+
self.state.brightness = result.brightness
|
|
856
|
+
self.state.color_temp = result.colorTemp
|
|
857
|
+
self.state.color_mode = result.colorMode
|
|
858
|
+
hue = float(round(result.hue / 250 * 9, 2))
|
|
859
|
+
sat = float(result.saturation / 100)
|
|
860
|
+
val = float(result.value)
|
|
861
|
+
self.state.color = Color.from_hsv(hue=hue, saturation=sat, value=val)
|
|
862
|
+
|
|
863
|
+
@deprecated(
|
|
864
|
+
'toggle() is deprecated, use toggle_switch(toggle: bool | None = None) instead'
|
|
865
|
+
)
|
|
866
|
+
async def toggle(self, status: str) -> bool:
|
|
867
|
+
"""Deprecated - use toggle_switch()."""
|
|
868
|
+
status_bool = status == DeviceStatus.ON
|
|
869
|
+
return await self.toggle_switch(status_bool)
|
|
870
|
+
|
|
871
|
+
async def toggle_switch(self, toggle: bool | None = None) -> bool:
|
|
872
|
+
if toggle is None:
|
|
873
|
+
toggle = self.state.device_status == DeviceStatus.OFF
|
|
874
|
+
|
|
875
|
+
if toggle == self.state.device_status:
|
|
876
|
+
logger.debug('Device already in requested state')
|
|
877
|
+
return True
|
|
878
|
+
|
|
879
|
+
payload_data = {
|
|
880
|
+
'id': 0,
|
|
881
|
+
'enabled': toggle,
|
|
882
|
+
}
|
|
883
|
+
method = 'setSwitch'
|
|
884
|
+
|
|
885
|
+
r_dict = await self._call_valceno_api(method, payload_data)
|
|
886
|
+
|
|
887
|
+
if r_dict is None:
|
|
888
|
+
self.state.device_status = DeviceStatus.OFF
|
|
889
|
+
return False
|
|
890
|
+
self.state.device_status = DeviceStatus.ON if toggle else DeviceStatus.OFF
|
|
891
|
+
self.state.connection_status = ConnectionStatus.ONLINE
|
|
892
|
+
return True
|
|
893
|
+
|
|
894
|
+
async def set_rgb(self, red: float, green: float, blue: float) -> bool:
|
|
895
|
+
new_color = Color.from_rgb(red=red, green=green, blue=blue)
|
|
896
|
+
if new_color is None:
|
|
897
|
+
logger.debug('Invalid RGB values')
|
|
898
|
+
return False
|
|
899
|
+
|
|
900
|
+
return await self.set_hsv(
|
|
901
|
+
hue=new_color.hsv.hue,
|
|
902
|
+
saturation=new_color.hsv.saturation,
|
|
903
|
+
value=new_color.hsv.value,
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
async def set_brightness(self, brightness: int) -> bool:
|
|
907
|
+
"""Set brightness of multicolor bulb."""
|
|
908
|
+
return await self.set_status(brightness=brightness)
|
|
909
|
+
|
|
910
|
+
async def set_color_temp(self, color_temp: int) -> bool:
|
|
911
|
+
"""Set White Temperature of Bulb in pct (0 - 100)."""
|
|
912
|
+
return await self.set_status(color_temp=color_temp)
|
|
913
|
+
|
|
914
|
+
async def set_color_hue(self, color_hue: float) -> bool:
|
|
915
|
+
"""Set Color Hue of Bulb (0 - 360)."""
|
|
916
|
+
return await self.set_status(color_hue=color_hue)
|
|
917
|
+
|
|
918
|
+
async def set_color_saturation(self, color_saturation: float) -> bool:
|
|
919
|
+
"""Set Color Saturation of Bulb in pct (1 - 100)."""
|
|
920
|
+
return await self.set_status(color_saturation=color_saturation)
|
|
921
|
+
|
|
922
|
+
async def set_color_value(self, color_value: float) -> bool:
|
|
923
|
+
"""Set Value of multicolor bulb in pct (1 - 100)."""
|
|
924
|
+
# Equivalent to brightness level, when in color mode.
|
|
925
|
+
return await self.set_status(color_value=color_value)
|
|
926
|
+
|
|
927
|
+
async def set_color_mode(self, color_mode: str) -> bool:
|
|
928
|
+
"""Set Color Mode of Bulb (white / hsv)."""
|
|
929
|
+
return await self.set_status(color_mode=color_mode)
|
|
930
|
+
|
|
931
|
+
async def set_hsv(self, hue: float, saturation: float, value: float) -> bool:
|
|
932
|
+
new_color = Color.from_hsv(hue=hue, saturation=saturation, value=value)
|
|
933
|
+
if new_color is None:
|
|
934
|
+
logger.warning('Invalid HSV values')
|
|
935
|
+
return False
|
|
936
|
+
|
|
937
|
+
# the api expects the hsv Value in the brightness parameter
|
|
938
|
+
payload_data = self._build_status_payload(
|
|
939
|
+
hue=hue,
|
|
940
|
+
saturation=saturation,
|
|
941
|
+
value=value,
|
|
942
|
+
)
|
|
943
|
+
if payload_data is None:
|
|
944
|
+
return False
|
|
945
|
+
resp = await self._call_valceno_api('setLightStatusV2', payload_data)
|
|
946
|
+
if resp is None:
|
|
947
|
+
return False
|
|
948
|
+
|
|
949
|
+
r_dict = Helpers.process_dev_response(logger, 'set_hsv', self, resp)
|
|
950
|
+
if r_dict is None:
|
|
951
|
+
return False
|
|
952
|
+
|
|
953
|
+
status = bulb_models.ResponseValcenoStatus.from_dict(r_dict)
|
|
954
|
+
self._interpret_apicall_result(status)
|
|
955
|
+
return True
|
|
956
|
+
|
|
957
|
+
async def set_white_mode(self) -> bool:
|
|
958
|
+
return await self.set_status(color_mode='white')
|
|
959
|
+
|
|
960
|
+
async def set_status(
|
|
961
|
+
self,
|
|
962
|
+
*,
|
|
963
|
+
brightness: float | None = None,
|
|
964
|
+
color_temp: float | None = None,
|
|
965
|
+
color_hue: float | None = None,
|
|
966
|
+
color_saturation: float | None = None,
|
|
967
|
+
color_value: float | None = None,
|
|
968
|
+
color_mode: str | None = None,
|
|
969
|
+
) -> bool:
|
|
970
|
+
"""Set multicolor bulb parameters.
|
|
971
|
+
|
|
972
|
+
No arguments turns bulb on. **Kwargs only**
|
|
973
|
+
|
|
974
|
+
Args:
|
|
975
|
+
brightness (int, optional): brightness between 0 and 100
|
|
976
|
+
color_temp (int, optional): color temperature between 0 and 100
|
|
977
|
+
color_mode (int, optional): color mode hsv or white
|
|
978
|
+
color_hue (float, optional): color hue between 0 and 360
|
|
979
|
+
color_saturation (float, optional): color saturation between 0 and 100
|
|
980
|
+
color_value (int, optional): color value between 0 and 100
|
|
981
|
+
|
|
982
|
+
Returns:
|
|
983
|
+
bool : True if call was successful, False otherwise
|
|
984
|
+
"""
|
|
985
|
+
payload_data = self._build_status_payload(
|
|
986
|
+
brightness=brightness,
|
|
987
|
+
color_temp=color_temp,
|
|
988
|
+
hue=color_hue,
|
|
989
|
+
saturation=color_saturation,
|
|
990
|
+
value=color_value,
|
|
991
|
+
color_mode=color_mode,
|
|
992
|
+
)
|
|
993
|
+
if payload_data == self._payload_base():
|
|
994
|
+
logger.debug('No state change.')
|
|
995
|
+
return False
|
|
996
|
+
if payload_data is None:
|
|
997
|
+
logger.debug('Invalid payload data')
|
|
998
|
+
return False
|
|
999
|
+
|
|
1000
|
+
r_dict = await self._call_valceno_api('setLightStatusV2', payload_data)
|
|
1001
|
+
if r_dict is None:
|
|
1002
|
+
return False
|
|
1003
|
+
|
|
1004
|
+
r_dict = Helpers.process_dev_response(logger, 'set_status', self, r_dict)
|
|
1005
|
+
if r_dict is None:
|
|
1006
|
+
return False
|
|
1007
|
+
|
|
1008
|
+
r_model = bulb_models.ResponseValcenoStatus.from_dict(r_dict)
|
|
1009
|
+
self._interpret_apicall_result(r_model)
|
|
1010
|
+
return True
|
|
1011
|
+
|
|
1012
|
+
def _build_color_payload(
|
|
1013
|
+
self,
|
|
1014
|
+
color_hue: float | None = None,
|
|
1015
|
+
color_saturation: float | None = None,
|
|
1016
|
+
color_value: float | None = None,
|
|
1017
|
+
) -> bulb_models.ValcenoStatusPayload | None:
|
|
1018
|
+
"""Create color payload for Valceno Bulbs.
|
|
1019
|
+
|
|
1020
|
+
This is called by `_build_status_payload` if any of the HSV values are set.
|
|
1021
|
+
This should not be called directly.
|
|
1022
|
+
|
|
1023
|
+
Args:
|
|
1024
|
+
color_hue (float, optional): Color hue of bulb (0-360).
|
|
1025
|
+
color_saturation (float, optional): Color saturation of bulb (0-100).
|
|
1026
|
+
color_value (float, optional): Color value of bulb (0-100).
|
|
1027
|
+
"""
|
|
1028
|
+
payload_dict = self._payload_base()
|
|
1029
|
+
new_color = Color.from_hsv(
|
|
1030
|
+
hue=color_hue, saturation=color_saturation, value=color_value
|
|
1031
|
+
)
|
|
1032
|
+
if new_color is None:
|
|
1033
|
+
logger.warning('Invalid HSV values')
|
|
1034
|
+
return None
|
|
1035
|
+
payload_dict['hue'] = int(new_color.hsv.hue * 250 / 9)
|
|
1036
|
+
payload_dict['saturation'] = int(new_color.hsv.saturation * 100)
|
|
1037
|
+
payload_dict['value'] = int(new_color.hsv.value)
|
|
1038
|
+
payload_dict['colorMode'] = 'hsv'
|
|
1039
|
+
payload_dict['force'] = 1
|
|
1040
|
+
return payload_dict
|
|
1041
|
+
|
|
1042
|
+
def _build_status_payload(
|
|
1043
|
+
self,
|
|
1044
|
+
brightness: float | None = None,
|
|
1045
|
+
color_temp: float | None = None,
|
|
1046
|
+
hue: float | None = None,
|
|
1047
|
+
saturation: float | None = None,
|
|
1048
|
+
value: float | None = None,
|
|
1049
|
+
color_mode: str | None = None,
|
|
1050
|
+
) -> bulb_models.ValcenoStatusPayload | None:
|
|
1051
|
+
"""Create status payload data for Valceno Bulbs.
|
|
1052
|
+
|
|
1053
|
+
If color_mode is set, hue, saturation and/or value must be set as well.
|
|
1054
|
+
If color_mode is not set, brightness and/or color_temp must be set.
|
|
1055
|
+
|
|
1056
|
+
This builds the `request_body['payload']['data']` dict for api calls that
|
|
1057
|
+
set teh status of the bulb.
|
|
1058
|
+
|
|
1059
|
+
Args:
|
|
1060
|
+
brightness (float, optional): Brightness of bulb (0-100).
|
|
1061
|
+
color_temp (float, optional): Color temperature of bulb (0-100).
|
|
1062
|
+
hue (float, optional): Color hue of bulb (0-360).
|
|
1063
|
+
saturation (float, optional): Color saturation of bulb (0-100).
|
|
1064
|
+
value (float, optional): Color value of bulb (0-100).
|
|
1065
|
+
color_mode (str, optional): Color mode of bulb ('white', 'hsv', 'color').
|
|
1066
|
+
|
|
1067
|
+
Returns:
|
|
1068
|
+
dict | None: Payload dictionary to be sent to the API.
|
|
1069
|
+
"""
|
|
1070
|
+
payload_dict = self._payload_base()
|
|
1071
|
+
if all(itm is not None for itm in [hue, saturation, value]):
|
|
1072
|
+
color_dict = self._build_color_payload(
|
|
1073
|
+
color_hue=hue, color_saturation=saturation, color_value=value
|
|
1074
|
+
)
|
|
1075
|
+
if color_dict is not None:
|
|
1076
|
+
return color_dict
|
|
1077
|
+
# Return None if HSV values are same as current state
|
|
1078
|
+
if self._check_color_state(hue=hue, saturation=saturation, value=value):
|
|
1079
|
+
return None
|
|
1080
|
+
elif color_mode in ['color', 'hsv', 'rgb']:
|
|
1081
|
+
logger.debug('HSV values must be provided when setting color mode.')
|
|
1082
|
+
else:
|
|
1083
|
+
if color_mode == 'white' and not Validators.validate_zero_to_hundred(
|
|
1084
|
+
color_temp
|
|
1085
|
+
):
|
|
1086
|
+
payload_dict['colorMode'] = 'white'
|
|
1087
|
+
if color_temp is not None and Validators.validate_zero_to_hundred(color_temp):
|
|
1088
|
+
payload_dict['colorTemp'] = int(color_temp)
|
|
1089
|
+
payload_dict['colorMode'] = 'white'
|
|
1090
|
+
if brightness is not None and Validators.validate_zero_to_hundred(brightness):
|
|
1091
|
+
payload_dict['brightness'] = int(brightness)
|
|
1092
|
+
force_keys = ['colorTemp', 'saturation', 'hue', 'colorMode', 'value']
|
|
1093
|
+
for key in force_keys:
|
|
1094
|
+
if payload_dict.get(key) != '':
|
|
1095
|
+
payload_dict['force'] = 1
|
|
1096
|
+
return payload_dict
|
|
1097
|
+
|
|
1098
|
+
def _check_color_state(
|
|
1099
|
+
self,
|
|
1100
|
+
hue: float | None = None,
|
|
1101
|
+
saturation: float | None = None,
|
|
1102
|
+
value: float | None = None,
|
|
1103
|
+
) -> bool:
|
|
1104
|
+
"""Check if color state is already set.
|
|
1105
|
+
|
|
1106
|
+
Returns True if color is already set, False otherwise.
|
|
1107
|
+
|
|
1108
|
+
Args:
|
|
1109
|
+
hue (float, optional): Color hue.
|
|
1110
|
+
saturation (float, optional): Color saturation.
|
|
1111
|
+
value (float, optional): Color value.
|
|
1112
|
+
"""
|
|
1113
|
+
if self.state.color is not None:
|
|
1114
|
+
set_hue = hue or self.state.color.hsv.hue
|
|
1115
|
+
set_saturation = saturation or self.state.color.hsv.saturation
|
|
1116
|
+
set_value = value or self.state.color.hsv.value
|
|
1117
|
+
set_color = Color.from_hsv(
|
|
1118
|
+
hue=set_hue, saturation=set_saturation, value=set_value
|
|
1119
|
+
)
|
|
1120
|
+
if (
|
|
1121
|
+
self.state.device_status == DeviceStatus.ON
|
|
1122
|
+
and set_color == self.state.color
|
|
1123
|
+
):
|
|
1124
|
+
logger.debug('Device already set to requested color')
|
|
1125
|
+
return True
|
|
1126
|
+
return False
|