tinytuya 1.18.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. tinytuya/BulbDevice.py +929 -0
  2. tinytuya/Cloud.py +922 -0
  3. tinytuya/Contrib/AtorchTemperatureControllerDevice.py +139 -0
  4. tinytuya/Contrib/BlanketDevice.py +143 -0
  5. tinytuya/Contrib/ClimateDevice.py +148 -0
  6. tinytuya/Contrib/ColorfulX7Device.py +354 -0
  7. tinytuya/Contrib/DoorbellDevice.py +113 -0
  8. tinytuya/Contrib/IRRemoteControlDevice.py +1264 -0
  9. tinytuya/Contrib/InverterHeatPumpDevice.py +187 -0
  10. tinytuya/Contrib/PresenceDetectorDevice.py +133 -0
  11. tinytuya/Contrib/RFRemoteControlDevice.py +277 -0
  12. tinytuya/Contrib/SocketDevice.py +90 -0
  13. tinytuya/Contrib/SoriaInverterDevice.py +293 -0
  14. tinytuya/Contrib/ThermostatDevice.py +1148 -0
  15. tinytuya/Contrib/WiFiDualMeterDevice.py +243 -0
  16. tinytuya/Contrib/__init__.py +15 -0
  17. tinytuya/CoverDevice.py +300 -0
  18. tinytuya/OutletDevice.py +78 -0
  19. tinytuya/__init__.py +99 -0
  20. tinytuya/__main__.py +272 -0
  21. tinytuya/cli.py +300 -0
  22. tinytuya/core/Device.py +189 -0
  23. tinytuya/core/XenonDevice.py +1348 -0
  24. tinytuya/core/__init__.py +18 -0
  25. tinytuya/core/command_types.py +28 -0
  26. tinytuya/core/const.py +21 -0
  27. tinytuya/core/core.py +261 -0
  28. tinytuya/core/crypto_helper.py +208 -0
  29. tinytuya/core/error_helper.py +58 -0
  30. tinytuya/core/exceptions.py +5 -0
  31. tinytuya/core/header.py +30 -0
  32. tinytuya/core/message_helper.py +179 -0
  33. tinytuya/core/udp_helper.py +45 -0
  34. tinytuya/scanner.py +2129 -0
  35. tinytuya/wizard.py +324 -0
  36. tinytuya-1.18.0.dist-info/METADATA +1085 -0
  37. tinytuya-1.18.0.dist-info/RECORD +41 -0
  38. tinytuya-1.18.0.dist-info/WHEEL +5 -0
  39. tinytuya-1.18.0.dist-info/entry_points.txt +2 -0
  40. tinytuya-1.18.0.dist-info/licenses/LICENSE +21 -0
  41. tinytuya-1.18.0.dist-info/top_level.txt +1 -0
tinytuya/BulbDevice.py ADDED
@@ -0,0 +1,929 @@
1
+ # TinyTuya Bulb Device
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Python module to interface with Tuya WiFi smart devices
5
+
6
+ Author: Jason A. Cox
7
+ For more information see https://github.com/jasonacox/tinytuya
8
+
9
+ Local Control Classes
10
+ BulbDevice(...)
11
+ See OutletDevice() for constructor arguments
12
+
13
+ Functions
14
+ BulbDevice Class methods
15
+ rgb_to_hexvalue(r, g, b, hexformat):
16
+ hsv_to_hexvalue(h, s, v, hexformat):
17
+ hexvalue_to_rgb(hexvalue, hexformat=None):
18
+ hexvalue_to_hsv(hexvalue, hexformat=None):
19
+
20
+ BulbDevice
21
+ set_mode(self, mode="white", nowait=False):
22
+ set_scene(self, scene, scene_data=None, nowait=False):
23
+ set_timer(self, num_secs, nowait=False):
24
+ set_musicmode(self, transition, modify_settings=True, nowait=False):
25
+ unset_musicmode( self ):
26
+ set_music_colour( self, red, green, blue, brightness=None, colourtemp=None, transition=None, nowait=False ):
27
+ set_colour(r, g, b, nowait):
28
+ set_hsv(h, s, v, nowait):
29
+ set_white_percentage(brightness=100, colourtemp=0, nowait):
30
+ set_brightness_percentage(brightness=100, nowait):
31
+ set_colourtemp_percentage(colourtemp=100, nowait):
32
+ result = get_value(self, feature, state=None, nowait=False):
33
+ result = get_mode(self, state=None, nowait=False):
34
+ result = get_brightness_percentage(self, state=None, nowait=False):
35
+ result = get_colourtemp_percentage(self, state=None, nowait=False):
36
+ (r, g, b) = colour_rgb():
37
+ (h,s,v) = colour_hsv()
38
+ result = state():
39
+ bool = bulb_has_capability( self, feature, nowait=False ):
40
+ detect_bulb(self, response=None, nowait=False):
41
+ set_bulb_type(self, bulb_type=None, mapping=None):
42
+ set_bulb_capabilities(self, mapping):
43
+
44
+ Inherited
45
+ Every device function from core.py
46
+ """
47
+
48
+ import colorsys
49
+
50
+ from .core import Device, log
51
+ from .core import error_json, ERR_JSON, ERR_FUNCTION # ERR_RANGE, ERR_STATE, ERR_TIMEOUT
52
+
53
+ # pylint: disable=R0904
54
+ class BulbDevice(Device):
55
+ """
56
+ Represents a Tuya based Smart Light/Bulb.
57
+ """
58
+
59
+ DPS_MODE_WHITE = "white"
60
+ DPS_MODE_COLOUR = "colour"
61
+ DPS_MODE_SCENE = "scene"
62
+ DPS_MODE_MUSIC = "music"
63
+ DPS_MODE_SCENE_1 = "scene_1" # nature
64
+ DPS_MODE_SCENE_2 = "scene_2"
65
+ DPS_MODE_SCENE_3 = "scene_3" # rave
66
+ DPS_MODE_SCENE_4 = "scene_4" # rainbow
67
+
68
+ BULB_FEATURE_MODE = 'mode'
69
+ BULB_FEATURE_BRIGHTNESS = 'brightness'
70
+ BULB_FEATURE_COLOURTEMP = 'colourtemp'
71
+ BULB_FEATURE_COLOUR = 'colour'
72
+ BULB_FEATURE_SCENE = 'scene'
73
+ BULB_FEATURE_SCENE_DATA = 'scene_data'
74
+ BULB_FEATURE_TIMER = 'timer'
75
+ BULB_FEATURE_MUSIC = 'music'
76
+
77
+ MUSIC_TRANSITION_JUMP = 0
78
+ MUSIC_TRANSITION_FADE = 1
79
+
80
+ DEFAULT_DPSET = {}
81
+ DEFAULT_DPSET['A'] = {
82
+ 'switch': 1,
83
+ 'mode': 2,
84
+ 'brightness': 3,
85
+ 'colourtemp': 4,
86
+ 'colour': 5,
87
+ 'scene': 6,
88
+ 'scene_data': None, # Type A sets mode to 'scene_N'
89
+ 'timer': 7,
90
+ 'music': 8,
91
+ 'value_min': 25,
92
+ 'value_max': 255,
93
+ 'value_hexformat': 'rgb8',
94
+ }
95
+ DEFAULT_DPSET['B'] = {
96
+ 'switch': 20,
97
+ 'mode': 21,
98
+ 'brightness': 22,
99
+ 'colourtemp': 23,
100
+ 'colour': 24,
101
+ 'scene': 25,
102
+ 'scene_data': 25, # Type B prefixes scene data with idx
103
+ 'timer': 26,
104
+ 'music': 28,
105
+ 'value_min': 10,
106
+ 'value_max': 1000,
107
+ 'value_hexformat': 'hsv16',
108
+ }
109
+ DEFAULT_DPSET['C'] = {
110
+ 'switch': 1,
111
+ 'mode': None,
112
+ 'brightness': 2,
113
+ 'colourtemp': 3,
114
+ 'colour': None,
115
+ 'scene': None,
116
+ 'scene_data': None,
117
+ 'timer': None,
118
+ 'music': None,
119
+ 'value_min': 25,
120
+ 'value_max': 255,
121
+ 'value_hexformat': 'rgb8',
122
+ }
123
+ DEFAULT_DPSET['None'] = {
124
+ 'switch': 1,
125
+ 'mode': None,
126
+ 'brightness': None,
127
+ 'colourtemp': None,
128
+ 'colour': None,
129
+ 'scene': None,
130
+ 'scene_data': None,
131
+ 'timer': None,
132
+ 'music': None,
133
+ 'value_min': 0,
134
+ 'value_max': 255,
135
+ 'value_hexformat': 'rgb8',
136
+ }
137
+
138
+
139
+ # These attributes are obsolete and only kept for backwards compatibility
140
+ DPS_INDEX_SETS = [20, 1] # starts at either DP 20 (Type B) or 1 (all others)
141
+ DPS_INDEX_ON = {"A": "1", "B": "20", "C": "1"}
142
+ DPS_INDEX_MODE = {"A": "2", "B": "21", "C": "1"}
143
+ DPS_INDEX_BRIGHTNESS = {"A": "3", "B": "22", "C": "2"}
144
+ DPS_INDEX_COLOURTEMP = {"A": "4", "B": "23", "C": None}
145
+ DPS_INDEX_COLOUR = {"A": "5", "B": "24", "C": None}
146
+ DPS_INDEX_SCENE = {"A": "2", "B": "25", "C": None}
147
+ DPS_INDEX_TIMER = {"A": None, "B": "26", "C": None}
148
+ DPS_INDEX_MUSIC = {"A": None, "B": "27", "C": None}
149
+ DPS = "dps"
150
+
151
+ def __init__(self, *args, **kwargs):
152
+ # Set Default Bulb Types
153
+ self.bulb_configured = False
154
+ self.bulb_type = None
155
+ self.has_brightness = None
156
+ self.has_colourtemp = None
157
+ self.has_colour = None
158
+ self.tried_status = False
159
+ self.dpset = {
160
+ 'switch': None,
161
+ 'mode': None,
162
+ 'brightness': None,
163
+ 'colourtemp': None,
164
+ 'colour': None,
165
+ 'scene': None,
166
+ 'scene_data': None,
167
+ 'timer': None,
168
+ 'music': None,
169
+ 'value_min': -1,
170
+ 'value_max': -1,
171
+ 'value_hexformat': 'hsv16',
172
+ }
173
+
174
+ # set the default version to None so we do not immediately connect and call status()
175
+ if 'version' not in kwargs or not kwargs['version']:
176
+ kwargs['version'] = None
177
+ super(BulbDevice, self).__init__(*args, **kwargs)
178
+
179
+ def status(self, nowait=False):
180
+ result = super(BulbDevice, self).status(nowait=nowait)
181
+ self.tried_status = True
182
+ if result and (not self.bulb_configured) and ('dps' in result):
183
+ self.detect_bulb(result, nowait=nowait)
184
+ return result
185
+
186
+ @staticmethod
187
+ def rgb_to_hexvalue(r, g, b, hexformat):
188
+ """
189
+ Convert an RGB value to the hex representation expected by Tuya Bulb.
190
+
191
+ While r, g and b are just hexadecimal values of the corresponding
192
+ Red, Green and Blue values, the h, s and v values (which are values
193
+ between 0 and 1) are scaled:
194
+ hexformat="rgb8": 360 (h) and 255 (s and v)
195
+ hexformat="hsv16": 360 (h) and 1000 (s and v)
196
+
197
+ Args:
198
+ r(int): Value for the colour red as int from 0-255.
199
+ g(int): Value for the colour green as int from 0-255.
200
+ b(int): Value for the colour blue as int from 0-255.
201
+ hexformat(str): Selects the return format
202
+ "rgb8": rrggbb0hhhssvv
203
+ "hsv16": hhhhssssvvvv
204
+ """
205
+ err = ''
206
+ if not 0 <= r <= 255.0:
207
+ err += '/red'
208
+ if not 0 <= g <= 255.0:
209
+ err += '/green'
210
+ if not 0 <= b <= 255.0:
211
+ err += '/blue'
212
+ if err:
213
+ raise ValueError('rgb_to_hexvalue: The value for %s needs to be between 0 and 255.' % err[1:])
214
+
215
+ hsv = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)
216
+
217
+ if hexformat == 'rgb8':
218
+ # r:0-255,g:0-255,b:0-255|rgb|
219
+ hexvalue = '%02x%02x%02x' % (r, g, b)
220
+ # h:0-360,s:0-255,v:0-255|hsv|
221
+ hexvalue += '%04x%02x%02x' % (int(hsv[0] * 360), int(hsv[1] * 255), int(hsv[2] * 255))
222
+ elif hexformat == 'hsv16':
223
+ # h:0-360,s:0-1000,v:0-1000|hsv|
224
+ hexvalue = '%04x%04x%04x' % (int(hsv[0] * 360), int(hsv[1] * 1000), int(hsv[2] * 1000))
225
+ else:
226
+ raise ValueError('rgb_to_hexvalue: hexformat must be either "rgb8" or "hsv16"')
227
+
228
+ return hexvalue
229
+
230
+ # Deprecated. Kept for backwards compatibility
231
+ @staticmethod
232
+ def _rgb_to_hexvalue(r, g, b, bulb="A"):
233
+ if bulb == "A":
234
+ hexformat = 'rgb8'
235
+ elif bulb == "B":
236
+ hexformat = 'hsv16'
237
+ else:
238
+ # Unsupported bulb type
239
+ raise ValueError("Unsupported bulb type %r - unable to determine hexvalue." % bulb)
240
+
241
+ return BulbDevice.rgb_to_hexvalue(r, g, b, hexformat)
242
+
243
+ @staticmethod
244
+ def hsv_to_hexvalue(h, s, v, hexformat):
245
+ """
246
+ Convert an HSV value to the hex representation expected by Tuya Bulb.
247
+
248
+ Args:
249
+ h(float): colour Hue as float from 0-1
250
+ s(float): colour Saturation as float from 0-1
251
+ v(float): colour Value as float from 0-1
252
+ hexformat(str): Selects the return format
253
+ "rgb8": rrggbb0hhhssvv
254
+ "hsv16": hhhhssssvvvv
255
+ """
256
+ err = ''
257
+ if not 0 <= h <= 1.0:
258
+ err += '/Hue'
259
+ if not 0 <= s <= 1.0:
260
+ err += '/Saturation'
261
+ if not 0 <= v <= 1.0:
262
+ err += '/Value'
263
+
264
+ if err:
265
+ raise ValueError( 'hsv_to_hexvalue: The value for %s needs to be between 0 and 1.' % err[1:])
266
+
267
+ if hexformat == 'rgb8':
268
+ (r, g, b) = colorsys.hsv_to_rgb(h, s, v)
269
+ return BulbDevice.rgb_to_hexvalue( r * 255.0, g * 255.0, b * 255.0, hexformat )
270
+ elif hexformat == 'hsv16':
271
+ # h:0-360,s:0-1000,v:0-1000|hsv|
272
+ hexvalue = '%04x%04x%04x' % (int(h * 360), int(s * 1000), int(v * 1000))
273
+ return hexvalue
274
+ else:
275
+ raise ValueError('hsv_to_hexvalue: hexformat must be either "rgb8" or "hsv16"')
276
+
277
+ @staticmethod
278
+ def hexvalue_to_rgb(hexvalue, hexformat=None):
279
+ """
280
+ Converts the hexvalue used by Tuya for colour representation into
281
+ an RGB value.
282
+
283
+ Args:
284
+ hexvalue(string): The hex representation generated by BulbDevice.rgb_to_hexvalue()
285
+ hexformat(str or None):
286
+ "rgb8": The hex is in rrggbb0hhhssvv format
287
+ "hsv16": The hex is in hhhhssssvvvv format
288
+ None: Try to auto-detect the format
289
+ """
290
+ hexvalue_len = len(hexvalue)
291
+ if not hexformat:
292
+ if hexvalue_len == 6 or hexvalue_len == 14:
293
+ hexformat = 'rgb8'
294
+ elif hexvalue_len == 12:
295
+ hexformat = 'hsv16'
296
+ else:
297
+ # Unsupported bulb type
298
+ raise ValueError("Unable to detect hexvalue format. Value string must have 6, 12 or 14 hex digits.")
299
+
300
+ if hexformat == 'rgb8':
301
+ if hexvalue_len < 6:
302
+ raise ValueError("RGB value string must have 6 or 14 hex digits.")
303
+ r = int(hexvalue[0:2], 16)
304
+ g = int(hexvalue[2:4], 16)
305
+ b = int(hexvalue[4:6], 16)
306
+ elif hexformat == 'hsv16':
307
+ # hexvalue is in hsv
308
+ if hexvalue_len < 12:
309
+ raise ValueError("HSV value string must have 12 hex digits.")
310
+ h = float(int(hexvalue[0:4], 16) / 360.0)
311
+ s = float(int(hexvalue[4:8], 16) / 1000.0)
312
+ v = float(int(hexvalue[8:12], 16) / 1000.0)
313
+ rgb = colorsys.hsv_to_rgb(h, s, v)
314
+ r = int(rgb[0] * 255)
315
+ g = int(rgb[1] * 255)
316
+ b = int(rgb[2] * 255)
317
+ else:
318
+ raise ValueError('hexvalue_to_rgb: hexformat must be None, "rgb8" or "hsv16"')
319
+
320
+ return (r, g, b)
321
+
322
+ # Deprecated. Kept for backwards compatibility
323
+ @staticmethod
324
+ def _hexvalue_to_rgb(hexvalue, bulb="A"):
325
+ if bulb == "A":
326
+ hexformat = 'rgb8'
327
+ elif bulb == "B":
328
+ hexformat = 'hsv16'
329
+ else:
330
+ # Unsupported bulb type, attempt to auto-detect format
331
+ hexformat = None
332
+ return BulbDevice.hexvalue_to_rgb(hexvalue, hexformat)
333
+
334
+ @staticmethod
335
+ def hexvalue_to_hsv(hexvalue, hexformat=None):
336
+ """
337
+ Converts the hexvalue used by Tuya for colour representation into
338
+ an HSV value.
339
+
340
+ Args:
341
+ hexvalue(string): The hex representation generated by BulbDevice.rgb_to_hexvalue()
342
+ hexformat(str or None):
343
+ "rgb8": The hex is in rrggbb0hhhssvv format
344
+ "hsv16": The hex is in hhhhssssvvvv format
345
+ None: Try to auto-detect the format
346
+ """
347
+ hexvalue_len = len(hexvalue)
348
+ if not hexformat:
349
+ if hexvalue_len == 6 or hexvalue_len == 14:
350
+ hexformat = 'rgb8'
351
+ elif hexvalue_len == 12:
352
+ hexformat = 'hsv16'
353
+ else:
354
+ # Unsupported bulb type
355
+ raise ValueError("Unable to detetect hexvalue format. Value string must have 6, 12 or 14 hex digits.")
356
+
357
+ if hexformat == 'rgb8':
358
+ if hexvalue_len < 6:
359
+ raise ValueError("RGB[HSV] value string must have 6 or 14 hex digits.")
360
+ if hexvalue_len < 14:
361
+ # hexvalue is in rgb
362
+ rgb = BulbDevice.hexvalue_to_rgb(hexvalue, 'rgb8')
363
+ h, s, v = colorsys.rgb_to_hsv(rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0)
364
+ else:
365
+ # hexvalue is in rgb+hsv
366
+ h = int(hexvalue[7:10], 16) / 360.0
367
+ s = int(hexvalue[10:12], 16) / 255.0
368
+ v = int(hexvalue[12:14], 16) / 255.0
369
+ elif hexformat == 'hsv16':
370
+ # hexvalue is in hsv
371
+ if hexvalue_len < 12:
372
+ raise ValueError("HSV value string must have 12 hex digits.")
373
+ h = int(hexvalue[0:4], 16) / 360.0
374
+ s = int(hexvalue[4:8], 16) / 1000.0
375
+ v = int(hexvalue[8:12], 16) / 1000.0
376
+ else:
377
+ raise ValueError('hexvalue_to_hsv: hexformat must be None, "rgb8" or "hsv16"')
378
+
379
+ return (h, s, v)
380
+
381
+ # Deprecated. Kept for backwards compatibility
382
+ @staticmethod
383
+ def _hexvalue_to_hsv(hexvalue, bulb="A"):
384
+ if bulb == "A":
385
+ hexformat = 'rgb8'
386
+ elif bulb == "B":
387
+ hexformat = 'hsv16'
388
+ else:
389
+ # Unsupported bulb type, attempt to auto-detect format
390
+ hexformat = None
391
+ return BulbDevice.hexvalue_to_hsv(hexvalue, hexformat)
392
+
393
+ def _set_values_check( self, check_values, nowait=False ):
394
+ dps_values = {}
395
+
396
+ # check to see which DPs need to be set
397
+ state = self.cached_status(nowait=nowait)
398
+ if state and 'dps' in state and state['dps']:
399
+ # last state is cached, so check to see if 'mode' needs to be set
400
+ for k in check_values:
401
+ dp = self.dpset[k]
402
+ if dp and ((dp not in state['dps']) or (state['dps'][dp] != check_values[k])):
403
+ dps_values[dp] = check_values[k]
404
+ elif not dp:
405
+ log.debug('Device does not support capability, skipping: %r:%r', k, check_values[k])
406
+
407
+ if dps_values:
408
+ log.debug('Only sending changed DPs: %r', dps_values)
409
+ else:
410
+ # last state not cached or everything already set, so send them all
411
+ for k in check_values:
412
+ if self.dpset[k]:
413
+ dps_values[self.dpset[k]] = check_values[k]
414
+ else:
415
+ log.debug('Device does not support capability, skipping: %r:%r', k, check_values[k])
416
+ log.debug('No DPs have changed, sending full update to refresh: %r', dps_values)
417
+
418
+ return self.set_multiple_values( dps_values, nowait=nowait )
419
+
420
+ def turn_onoff(self, on, switch=0, nowait=False):
421
+ """Turn the device on or off"""
422
+ if not switch:
423
+ if not self.tried_status:
424
+ self.detect_bulb( nowait=nowait )
425
+ # some people may use BulbDevice as the default even for non-bulb
426
+ # devices, so default to '1' if we can't detect it
427
+ switch = self.dpset['switch'] if self.dpset['switch'] else 1
428
+ return self.set_status(on, switch, nowait=nowait)
429
+
430
+ def turn_on(self, switch=0, nowait=False):
431
+ """Turn the device on"""
432
+ return self.turn_onoff( True, switch=switch, nowait=nowait )
433
+
434
+ def turn_off(self, switch=0, nowait=False):
435
+ """Turn the device off"""
436
+ return self.turn_onoff( False, switch=switch, nowait=nowait )
437
+
438
+ def set_mode(self, mode="white", nowait=False):
439
+ """
440
+ Set bulb mode
441
+
442
+ Args:
443
+ mode(string): white,colour,scene,music
444
+ nowait(bool): True to send without waiting for response.
445
+ """
446
+ if not self.bulb_has_capability( 'mode', nowait=nowait ):
447
+ return error_json(ERR_FUNCTION, 'Bulb does not support mode setting.')
448
+
449
+ check_values = {
450
+ 'mode': mode,
451
+ 'switch': True,
452
+ }
453
+
454
+ return self._set_values_check( check_values, nowait=nowait )
455
+
456
+ def set_scene(self, scene, scene_data=None, nowait=False):
457
+ """
458
+ Set to scene mode
459
+
460
+ Args:
461
+ scene(int): Value for the scene as int from 1-4 (Type A bulbs) or 1-N (Type B bulbs).
462
+ nowait(bool): True to send without waiting for response.
463
+ """
464
+ if not self.bulb_has_capability( 'scene', nowait=nowait ):
465
+ return error_json(ERR_FUNCTION, 'set_scene: Bulb does not support scenes.')
466
+
467
+ # Type A, scene idx is part of the mode
468
+ if (not self.dpset['scene_data']) or (self.dpset['scene_data'] == self.dpset['mode']):
469
+ if (not 1 <= scene <= 4):
470
+ raise ValueError('set_scene: The value for scene needs to be between 1 and 4.')
471
+ dps_values = {
472
+ self.dpset['mode']: self.DPS_MODE_SCENE + '_' + str(scene)
473
+ }
474
+ else:
475
+ scene = '%02x' % int(scene)
476
+ dps_values = {
477
+ 'scene': scene,
478
+ 'mode': self.DPS_MODE_SCENE,
479
+ }
480
+
481
+ if scene_data:
482
+ if (self.dpset['scene_data'] is True) or (self.dpset['scene_data'] == self.dpset['scene']):
483
+ dps_values['scene'] += scene_data
484
+ else:
485
+ dps_values['scene_data'] = scene_data
486
+
487
+ return self._set_values_check( dps_values, nowait=nowait )
488
+
489
+ def set_timer(self, num_secs, dps_id=0, nowait=False):
490
+ """
491
+ Set the timer
492
+
493
+ Args:
494
+ num_secs: data to send to bulb
495
+ dps_id: do not use, kept for compatibility with Device.set_timer()
496
+ """
497
+ if dps_id:
498
+ return self.set_value(dps_id, num_secs, nowait=nowait)
499
+
500
+ if not self.bulb_has_capability( 'timer', nowait=nowait ):
501
+ return error_json(ERR_FUNCTION, 'set_timer: Bulb does not support timer.')
502
+ return self.set_value(self.dpset['timer'], num_secs, nowait=nowait)
503
+
504
+ def set_music_colour( self, transition, red, green, blue, brightness=None, colourtemp=None, nowait=False ):
505
+ """
506
+ Set a colour while in music mode
507
+
508
+ Args:
509
+ red(float): red value, 0.0 - 255.0
510
+ green(float): green value, 0.0 - 255.0
511
+ blue(float): blue value, 0.0 - 255.0
512
+ brightness(float): optional white light brightness
513
+ colourtemp(float): optional white light colourtemp
514
+ transition(int): optional transition. will use transition provided in set_musicmode() if not provided
515
+ """
516
+ if not self.bulb_has_capability( 'music', nowait=nowait ):
517
+ return error_json(ERR_FUNCTION, "set_music_colour: Device does not support music mode.")
518
+
519
+ colour = '%x' % transition
520
+ colour += self.rgb_to_hexvalue( red, green, blue, self.dpset['value_hexformat'] )
521
+
522
+ if (not brightness) or (brightness < 0):
523
+ brightness = 0
524
+ brightness = int(self.dpset['value_max'] * brightness // 100)
525
+
526
+ if (not colourtemp) or (colourtemp < 0):
527
+ colourtemp = 0
528
+ colourtemp = int(self.dpset['value_max'] * colourtemp // 100)
529
+
530
+ fmt = '%02x' if self.dpset['value_hexformat'] == 'rgb8' else '%04x'
531
+ colour += fmt % brightness
532
+ colour += fmt % colourtemp
533
+
534
+ return self.set_value(self.dpset['music'], colour, nowait=nowait)
535
+
536
+ def set_colour(self, r, g, b, nowait=False):
537
+ """
538
+ Set colour of an rgb bulb.
539
+
540
+ Args:
541
+ r(float): Value for the colour Red from 0.0-255.0.
542
+ g(float): Value for the colour Green from 0.0-255.0.
543
+ b(float): Value for the colour Blue from 0.0-255.0.
544
+ nowait(bool): True to send without waiting for response.
545
+ """
546
+ if not self.bulb_has_capability( 'colour', nowait=nowait ):
547
+ return error_json(ERR_FUNCTION, "set_colour: Device does not support color.")
548
+
549
+ check_values = {
550
+ 'colour': self.rgb_to_hexvalue(r, g, b, self.dpset['value_hexformat']),
551
+ 'mode': self.DPS_MODE_COLOUR,
552
+ 'switch': True
553
+ }
554
+
555
+ return self._set_values_check( check_values, nowait=nowait )
556
+
557
+ def set_hsv(self, h, s, v, nowait=False):
558
+ """
559
+ Set colour of an rgb bulb using h, s, v.
560
+
561
+ Args:
562
+ h(float): colour Hue as float from 0-1
563
+ s(float): colour Saturation as float from 0-1
564
+ v(float): colour Value as float from 0-1
565
+ nowait(bool): True to send without waiting for response.
566
+ """
567
+ if not self.bulb_has_capability( 'colour', nowait=nowait ):
568
+ return error_json(ERR_FUNCTION, "set_colour: Device does not support color.")
569
+
570
+ check_values = {
571
+ 'colour': self.hsv_to_hexvalue( h, s, v, self.dpset['value_hexformat'] ),
572
+ 'mode': self.DPS_MODE_COLOUR,
573
+ 'switch': True
574
+ }
575
+ return self._set_values_check( check_values, nowait=nowait )
576
+
577
+ def set_white_percentage(self, brightness=100, colourtemp=0, nowait=False):
578
+ """
579
+ Set white coloured theme of an rgb bulb.
580
+
581
+ Args:
582
+ brightness(int): Value for the brightness in percent (0-100)
583
+ colourtemp(int): Value for the colour temperature in percent (0-100)
584
+ nowait(bool): True to send without waiting for response.
585
+
586
+ Note: unlike set_colourtemp(), the colour temp will be silently ignored if the bulb does not support it
587
+ """
588
+ err = ''
589
+ if not 0 <= brightness <= 100:
590
+ err += '/Brightness'
591
+ if not 0 <= colourtemp <= 100:
592
+ err += '/Colourtemp'
593
+ if err:
594
+ raise ValueError( 'set_white_percentage: %s percentage needs to be between 0 and 100.' % err[1:])
595
+
596
+ b = int(self.dpset['value_max'] * brightness // 100)
597
+ c = int(self.dpset['value_max'] * colourtemp // 100)
598
+
599
+ return self.set_white( b, c, nowait=nowait )
600
+
601
+ # Deprecated. Please use set_white_percentage() instead.
602
+ def set_white(self, brightness=-1, colourtemp=-1, nowait=False):
603
+ """
604
+ DEPRECATED Set white coloured theme of an rgb bulb.
605
+
606
+ Args:
607
+ brightness(int): Value for the brightness (A:25-255 or B:10-1000)
608
+ colourtemp(int): Value for the colour temperature (A:0-255, B:0-1000).
609
+ nowait(bool): True to send without waiting for response.
610
+
611
+ Default: Max Brightness and Min Colourtemp
612
+
613
+ Note: unlike set_colourtemp(), the colour temp will be silently ignored if the bulb does not support it
614
+ """
615
+ if not self.bulb_has_capability( 'brightness', nowait=nowait ):
616
+ return error_json(ERR_FUNCTION, 'set_white: Device does not support brightness.')
617
+
618
+ # Brightness (default: Max)
619
+ brightness = int(brightness)
620
+ if brightness < 0:
621
+ brightness = self.dpset['value_max']
622
+ elif brightness > self.dpset['value_max']:
623
+ raise ValueError('set_white: The brightness needs to be between %d and %d.' % (self.dpset['value_min'], self.dpset['value_max']))
624
+
625
+ # Colourtemp (default: Min)
626
+ # It will be silently ignored if the bulb does not support it
627
+ if colourtemp is not None:
628
+ colourtemp = int(colourtemp)
629
+ if colourtemp < 0:
630
+ colourtemp = 0
631
+ if colourtemp > self.dpset['value_max']:
632
+ raise ValueError('set_white: The colour temperature needs to be between 0 and %d.' % self.dpset['value_max'])
633
+
634
+ # do this the hard way as brightness=0 means we should turn off, but if colourtemp is set then
635
+ # turn_on() should turn it on at that colourtemp
636
+ check_values = {}
637
+ if brightness >= self.dpset['value_min']:
638
+ check_values['brightness'] = brightness
639
+ if colourtemp is not None:
640
+ check_values['colourtemp'] = colourtemp
641
+ if check_values:
642
+ # we're setting brightness and/or colourtemp
643
+ check_values['mode'] = self.DPS_MODE_WHITE
644
+ check_values['switch'] = bool(brightness >= self.dpset['value_min'])
645
+
646
+ # _set_values_check() will skip colourtemp if the bulb does not have it
647
+ return self._set_values_check( check_values, nowait=nowait )
648
+
649
+ def set_brightness_percentage(self, brightness=100, nowait=False):
650
+ """
651
+ Set the brightness value of an rgb bulb.
652
+
653
+ Args:
654
+ brightness(int): Value for the brightness in percent (0-100)
655
+ nowait(bool): True to send without waiting for response.
656
+ """
657
+ if not 0 <= brightness <= 100:
658
+ raise ValueError('set_brightness_percentage: The brightness needs to be between 0 and 100.')
659
+ b = int(self.dpset['value_max'] * brightness // 100)
660
+ return self.set_brightness(b, nowait=nowait)
661
+
662
+ # Deprecated. Please use set_brightness_percentage() instead.
663
+ def set_brightness(self, brightness, nowait=False):
664
+ """
665
+ DEPRECATED Set the brightness value of an rgb bulb.
666
+
667
+ Args:
668
+ brightness(int): Value for the brightness (25-255).
669
+ nowait(bool): True to send without waiting for response.
670
+ """
671
+ if not self.bulb_has_capability( 'brightness', nowait=nowait ):
672
+ return error_json(ERR_FUNCTION, 'set_brightness: Device does not support brightness.')
673
+
674
+ # Brightness (default Max)
675
+ if brightness < 0:
676
+ brightness = self.dpset['value_max']
677
+ elif brightness < self.dpset['value_min']:
678
+ return self.turn_off(0, nowait=nowait)
679
+ elif brightness > self.dpset['value_max']:
680
+ raise ValueError('set_brightness: The brightness needs to be between %d and %d.' % (self.dpset['value_min'], self.dpset['value_max']))
681
+
682
+ # Determine which mode bulb is in and adjust accordingly
683
+ state = self.state(nowait=nowait)
684
+
685
+ if ('Error' in state) or ('mode' not in state):
686
+ return state
687
+
688
+ if state['mode'] != self.DPS_MODE_COLOUR:
689
+ # use white mode, changing to it if needed
690
+ return self.set_white(brightness=brightness, colourtemp=None, nowait=nowait)
691
+ else:
692
+ # for colour mode use hsv to increase brightness
693
+ value = brightness / float(self.dpset['value_max'])
694
+ (h, s, v) = self.colour_hsv(state=state, nowait=nowait)
695
+ return self.set_hsv(h, s, value, nowait=nowait)
696
+
697
+ def set_colourtemp_percentage(self, colourtemp=100, nowait=False):
698
+ """
699
+ Set the colour temperature of an rgb bulb.
700
+
701
+ Args:
702
+ colourtemp(int): Value for the colour temperature in percentage (0-100).
703
+ nowait(bool): True to send without waiting for response.
704
+ """
705
+ if not 0 <= colourtemp <= 100:
706
+ raise ValueError( 'set_colourtemp_percentage: Colourtemp percentage needs to be between 0 and 100.')
707
+ c = int(self.dpset['value_max'] * colourtemp // 100)
708
+ return self.set_colourtemp( c, nowait=nowait )
709
+
710
+ # Deprecated. Please use set_white_percentage() instead.
711
+ def set_colourtemp(self, colourtemp, nowait=False):
712
+ """
713
+ DEPRECATED Set the colour temperature of an rgb bulb.
714
+
715
+ Args:
716
+ colourtemp(int): Value for the colour temperature (0-255).
717
+ nowait(bool): True to send without waiting for response.
718
+ """
719
+ if not self.bulb_has_capability( self.BULB_FEATURE_COLOURTEMP, nowait=nowait ):
720
+ return error_json(ERR_FUNCTION, 'set_colourtemp: Device does not support colourtemp.')
721
+
722
+ if not 0 <= colourtemp <= self.dpset['value_max']:
723
+ raise ValueError('set_colourtemp: The colour temperature needs to be between 0 and %d.' % self.dpset['value_max'])
724
+
725
+ check_values = {
726
+ 'colourtemp': colourtemp,
727
+ 'mode': self.DPS_MODE_WHITE,
728
+ 'switch': True,
729
+ }
730
+
731
+ return self._set_values_check( check_values, nowait=nowait )
732
+
733
+ def get_value(self, feature, state=None, nowait=False):
734
+ if not state:
735
+ state = self.state(nowait=nowait)
736
+ if 'Error' in state:
737
+ raise RuntimeError('Error getting device current state.')
738
+ if feature not in state:
739
+ raise ValueError("Unknown parameter %r." % feature)
740
+ return state[feature]
741
+
742
+ def get_mode(self, state=None, nowait=False):
743
+ """Return current working mode"""
744
+ return self.get_value('mode', state=state, nowait=nowait)
745
+
746
+ def white_percentage(self, state=None, nowait=False):
747
+ if not state:
748
+ state = self.state(nowait=nowait)
749
+ return (self.brightness_percentage(state=state, nowait=nowait), self.colourtemp_percentage(state=state, nowait=nowait))
750
+
751
+ #def white(self, state=None, nowait=False):
752
+ # pass
753
+
754
+ def get_brightness_percentage(self, state=None, nowait=False):
755
+ if (not self.dpset['value_max']) or (self.dpset['value_max'] < 1):
756
+ raise RuntimeError("Bulb capabilitiy 'value_max' not set, unable to calculate percentage.")
757
+ return self.brightness(state=state, nowait=nowait) / self.dpset['value_max'] * 100.0
758
+
759
+ def brightness(self, state=None, nowait=False):
760
+ """Return brightness value"""
761
+ return self.get_value('brightness', state=state, nowait=nowait)
762
+
763
+ def get_colourtemp_percentage(self, state=None, nowait=False):
764
+ if (not self.dpset['value_max']) or (self.dpset['value_max'] < 1):
765
+ raise RuntimeError("Bulb capabilitiy 'value_max' not set, unable to calculate percentage.")
766
+ return self.colourtemp(state=state, nowait=nowait) / self.dpset['value_max'] * 100.0
767
+
768
+ def colourtemp(self, state=None, nowait=False):
769
+ """Return colour temperature"""
770
+ return self.get_value('colourtemp', state=state, nowait=nowait)
771
+
772
+ def colour_rgb(self, state=None, nowait=False):
773
+ """Return colour as RGB value"""
774
+ hexvalue = self.get_value('colour', state=state, nowait=nowait)
775
+ if isinstance( hexvalue, dict ):
776
+ return hexvalue # Error
777
+ return BulbDevice.hexvalue_to_rgb(hexvalue, self.dpset['value_hexformat'])
778
+
779
+ def colour_hsv(self, state=None, nowait=False):
780
+ """Return colour as HSV value"""
781
+ hexvalue = self.get_value('colour', state=state, nowait=nowait)
782
+ if isinstance( hexvalue, dict ):
783
+ return hexvalue # Error
784
+ return BulbDevice.hexvalue_to_hsv(hexvalue, self.dpset['value_hexformat'])
785
+
786
+ def state(self, nowait=False):
787
+ """Return state of Bulb"""
788
+ if not self.bulb_configured:
789
+ self.detect_bulb(nowait=nowait)
790
+ if not self.bulb_configured:
791
+ raise RuntimeError('Bulb not configured, cannot get device current state.')
792
+
793
+ status = self.cached_status(nowait=nowait)
794
+ state = {}
795
+ if not status:
796
+ return error_json(ERR_JSON, "state: empty response")
797
+
798
+ if "Error" in status:
799
+ return error_json(ERR_JSON, status["Error"])
800
+
801
+ if 'dps' not in status:
802
+ return error_json(ERR_JSON, "state: no data points")
803
+
804
+ for key in self.dpset:
805
+ dp = self.dpset[key]
806
+ if '_' in key:
807
+ # skip scene_data, value_min, value_max, etc
808
+ state[key] = None
809
+ elif dp in status['dps']:
810
+ state[key] = status['dps'][dp]
811
+ else:
812
+ state[key] = None
813
+
814
+ if 'switch' in state:
815
+ state['is_on'] = state['switch']
816
+
817
+ #print( 'state:', state )
818
+ return state
819
+
820
+ def bulb_has_capability( self, feature, nowait=False ):
821
+ if not self.bulb_configured:
822
+ self.detect_bulb( nowait=nowait )
823
+ if not self.bulb_configured:
824
+ raise RuntimeError('Bulb not configured, cannot get device capabilities.')
825
+ return bool( self.dpset[feature] )
826
+
827
+ def detect_bulb(self, response=None, nowait=False):
828
+ """
829
+ Attempt to determine BulbDevice Type A, B or C based on:
830
+ Type A has keys 1-9
831
+ Type B has keys 20-28
832
+ Type C is basic (non-CCT) and only has 1-2 (i.e Feit type bulbs from Costco)
833
+
834
+ Example status data:
835
+ Sylvania BR30 [v3.3, RGB+CCT]:
836
+ {'20': True, '21': 'colour', '22': 750, '23': 278, '24': '00f003e803e8', '25': '000e0d0000000000000000c803e8', '26': 0}
837
+
838
+ Geeni BW229 Smart Filament Bulb [v3.3, CCT only]:
839
+ {'1': True, '2': 25, '3': 0}
840
+ 1: switch, 2: brightness, 3: colour temperature
841
+
842
+ No-name RGB+CCT (LED BULB W5K) [v3.5, RGB+CCT]:
843
+ {'20': True, '21': 'white', '22': 10, '23': 0, '24': '000003e803e8', '25': '000e0d0000000000000000c80000', '26': 0, '34': False}
844
+
845
+ Feit soft white Filament [v3.5, 2700K only]:
846
+ {'20': True, '21': 'white', '22': 60, '25': '000e0d0000000000000000c803e8', '26': 0, '34': False, '41': True}
847
+ (No CCT (23) or colour (24), but does support scenes (25) and music mode (28))
848
+
849
+ Feit dimmer switch [v3.3, not a bulb]:
850
+ {'1': True, '2': 10, '3': 10, '4': 'incandescent'}
851
+ Note: after a power cycle, only DP 2 is returned! The rest are not returned until after they are set
852
+ 1: switch, 2: brightness, 3: minimum dim %, 4: installed bulb type (LED/incandescent)
853
+ """
854
+ if not response:
855
+ response = self.cached_status(historic=True, nowait=nowait)
856
+ if (not response) or ('dps' not in response):
857
+ if nowait:
858
+ log.debug('No cached status, but nowait set! detect_bulb() exiting without detecting bulb!')
859
+ else:
860
+ response = self.status()
861
+ # return here as self.status() will call us again
862
+ return
863
+ if response and 'dps' in response and isinstance(response['dps'], dict):
864
+ # Try to determine type of BulbDevice Type based on DPS indexes
865
+ # 1+2 or 20+21 are required per https://developer.tuya.com/en/docs/iot/product-function-definition?id=K9tp155s4th6b
866
+ # The rest are optional
867
+ if '20' in response['dps'] and '1' in response['dps']:
868
+ # both 1 and 20 in response, this probably isn't a bulb
869
+ self.bulb_configured = True
870
+ self.bulb_type = 'None'
871
+ elif '20' in response['dps'] and '21' in response['dps']:
872
+ self.bulb_configured = True
873
+ self.bulb_type = 'B'
874
+ elif '1' in response['dps'] and '2' in response['dps']:
875
+ self.bulb_configured = True
876
+
877
+ # if DP 2 is a string, it is the mode (Type A). If it is an int, it is the brightness (Type C)
878
+ self.bulb_type = 'A' if isinstance(response['dps']['2'], str) else 'C'
879
+
880
+ if self.bulb_type and self.bulb_type in self.DEFAULT_DPSET:
881
+ # The 'music' DP is not returned in status(), so use the default value
882
+ self.dpset['music'] = self.DEFAULT_DPSET[self.bulb_type]['music']
883
+
884
+ self.dpset['value_min'] = self.DEFAULT_DPSET[self.bulb_type]['value_min']
885
+ self.dpset['value_max'] = self.DEFAULT_DPSET[self.bulb_type]['value_max']
886
+ self.dpset['value_hexformat'] = self.DEFAULT_DPSET[self.bulb_type]['value_hexformat']
887
+
888
+ for k in self.DEFAULT_DPSET[self.bulb_type]:
889
+ if k[:6] == 'value_':
890
+ continue
891
+ if not self.DEFAULT_DPSET[self.bulb_type][k]:
892
+ continue
893
+ dp = str( self.DEFAULT_DPSET[self.bulb_type][k] )
894
+ if dp in response['dps']:
895
+ self.dpset[k] = dp
896
+
897
+ # set has_* attributes for backwards compatibility
898
+ for k in ('brightness', 'colourtemp', 'colour'):
899
+ setattr( self, 'has_'+k, bool(self.dpset[k]) )
900
+
901
+ log.debug("Bulb type set to %r. has brightness: %r, has colourtemp: %r, has colour: %r",
902
+ self.bulb_type, self.dpset['brightness'], self.dpset['colourtemp'], self.dpset['colour']
903
+ )
904
+ elif not self.bulb_configured:
905
+ # response has no dps
906
+ log.debug("No DPs in response, cannot detect bulb type!")
907
+
908
+ def set_bulb_type(self, bulb_type=None, mapping=None):
909
+ self.bulb_type = bulb_type
910
+ self.set_bulb_capabilities(mapping)
911
+
912
+ def set_bulb_capabilities(self, mapping):
913
+ if self.bulb_type in self.DEFAULT_DPSET:
914
+ default_dpset = self.DEFAULT_DPSET[self.bulb_type]
915
+ else:
916
+ default_dpset = {}
917
+
918
+ if not isinstance( mapping, dict ):
919
+ mapping = {}
920
+
921
+ for k in self.dpset:
922
+ if k in mapping:
923
+ self.dpset[k] = mapping[k]
924
+ elif self.dpset[k] is None:
925
+ dp = default_dpset.get(k, None)
926
+ self.dpset[k] = str(dp) if (dp and k[:6] != 'value_') else dp
927
+
928
+ if self.dpset['switch'] and self.dpset['brightness']:
929
+ self.bulb_configured = True