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.
- tinytuya/BulbDevice.py +929 -0
- tinytuya/Cloud.py +922 -0
- tinytuya/Contrib/AtorchTemperatureControllerDevice.py +139 -0
- tinytuya/Contrib/BlanketDevice.py +143 -0
- tinytuya/Contrib/ClimateDevice.py +148 -0
- tinytuya/Contrib/ColorfulX7Device.py +354 -0
- tinytuya/Contrib/DoorbellDevice.py +113 -0
- tinytuya/Contrib/IRRemoteControlDevice.py +1264 -0
- tinytuya/Contrib/InverterHeatPumpDevice.py +187 -0
- tinytuya/Contrib/PresenceDetectorDevice.py +133 -0
- tinytuya/Contrib/RFRemoteControlDevice.py +277 -0
- tinytuya/Contrib/SocketDevice.py +90 -0
- tinytuya/Contrib/SoriaInverterDevice.py +293 -0
- tinytuya/Contrib/ThermostatDevice.py +1148 -0
- tinytuya/Contrib/WiFiDualMeterDevice.py +243 -0
- tinytuya/Contrib/__init__.py +15 -0
- tinytuya/CoverDevice.py +300 -0
- tinytuya/OutletDevice.py +78 -0
- tinytuya/__init__.py +99 -0
- tinytuya/__main__.py +272 -0
- tinytuya/cli.py +300 -0
- tinytuya/core/Device.py +189 -0
- tinytuya/core/XenonDevice.py +1348 -0
- tinytuya/core/__init__.py +18 -0
- tinytuya/core/command_types.py +28 -0
- tinytuya/core/const.py +21 -0
- tinytuya/core/core.py +261 -0
- tinytuya/core/crypto_helper.py +208 -0
- tinytuya/core/error_helper.py +58 -0
- tinytuya/core/exceptions.py +5 -0
- tinytuya/core/header.py +30 -0
- tinytuya/core/message_helper.py +179 -0
- tinytuya/core/udp_helper.py +45 -0
- tinytuya/scanner.py +2129 -0
- tinytuya/wizard.py +324 -0
- tinytuya-1.18.0.dist-info/METADATA +1085 -0
- tinytuya-1.18.0.dist-info/RECORD +41 -0
- tinytuya-1.18.0.dist-info/WHEEL +5 -0
- tinytuya-1.18.0.dist-info/entry_points.txt +2 -0
- tinytuya-1.18.0.dist-info/licenses/LICENSE +21 -0
- 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
|