pyvesync 3.0.0b0__py3-none-any.whl

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