-
-
Notifications
You must be signed in to change notification settings - Fork 20
/
index.js
592 lines (564 loc) · 20.8 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
const url = require('url');
const tcp = require('net');
const util = require('util');
const SSDP = require('ssdp2');
const EventEmitter = require('events');
/**
* debug
*/
console.debug = util.debuglog('yeelight');
/**
* Yeelight
* @class
* @docs http://www.yeelight.com/download/Yeelight_Inter-Operation_Spec.pdf
* @param {String} address address of yeelight device
* @param {Number} port port of yeelight device
* @return {Yeelight} Instance of Yeelight
*/
function Yeelight(address, port){
var u = url.parse(address);
if(u.protocol === 'yeelight:'){
address = u.hostname;
port = u.port;
}
if(!(this instanceof Yeelight)){
console.debug('creating new instance of Yeelight with addr & port', address, port)
return new Yeelight(address, port);
}
var buffer = '';
port = port || 55443;
EventEmitter.call(this);
this.queue = {};
this.socket = new tcp.Socket();
this.socket
.on('data', function(chunk){
buffer += chunk;
buffer.split(/\r\n/g).filter(function(line){
return !!line;
}).forEach(this.parse.bind(this));
buffer = '';
}.bind(this))
.on('error', function(err){
this.connected = false;
this.emit('error', err);
this.emit('disconnect', this);
}.bind(this))
.on('end', function(){
this.connected = false;
this.emit('disconnect', this);
}.bind(this))
.connect(port, address, function(err){
this.connected = true;
this.sync().then(function(){
this.emit('connect', this);
}.bind(this));
}.bind(this));
return this;
};
Yeelight.prototype.close = function(){
this.socket.end();
this.socket.destroy();
}
/**
* Yeelight extends EventEmitter
*/
util.inherits(Yeelight, EventEmitter);
/**
* Search Yeelight blub
* @param {Number} port ssdp port
* @param {Function} callback handle your device
* @return {SSDP} ssdp instance
*
* @example
*
* Yeelight.discover(function(light){
* console.log(light.name);
* });
*/
Yeelight.discover = function(port, callback) {
// TODO: `port` will rename to `opts` in next major version, then add `timeout` to opts
if(typeof port === 'function'){
callback = port; port = null;
}
const yeelights = [];
const discover = new SSDP({ port: port || 1982 });
discover.on('response', function(response) {
console.debug(response.headers);
const {
id, name, model, support,
color_mode, fw_ver,
Location: address,
} = response.headers;
if(address && (!~yeelights.indexOf(address))){
console.debug('received response from', address);
yeelights.push(address);
var yeelight = new Yeelight( address );
yeelight.id = id;
yeelight.model = model;
yeelight.address = address;
yeelight.firmware_version = fw_ver;
yeelight.supports = support && support.split(' ') || [];
yeelight.on('connect', function(){
callback.call(discover, this, response);
});
};
});
console.debug('start finding ...');
return discover.search('wifi_bulb');
};
/**
* Find Yeelight blub
*/
Yeelight.find = (opts = {}) => {
const { timeout = 12000 } = opts;
return new Promise((accept, reject) => {
const discover = Yeelight.discover(yeelight => {
clearTimeout(timer);
accept(yeelight);
discover.close();
});
var timer = setTimeout(() => {
discover.close();
reject(new Error('timeout'));
}, timeout);
});
};
/**
* is_support("set_rgb")
* @param {String} func
* @return {Boolean} isSupport
*/
Yeelight.prototype.is_support = function(func){
return !!~this.supports.indexOf(func);
};
/**
* Current support property and it's possible value is defined as below
* @type {Array}
*/
Yeelight.prototype.props = [
// The name of the device set by “set_name” command
"name",
// on: smart LED is turned on / off: smart LED is turned off
"power",
// Brightness percentage. Range 1 ~ 100
"bright",
// Color temperature. Range 1700 ~ 6500(k)
"ct",
// Color. Range 1 ~ 16777215
"rgb",
// Hue. Range 0 ~ 359
"hue",
// Saturation. Range 0 ~ 100
"sat",
// 1: rgb mode / 2: color temperature mode / 3: hsv mode
"color_mode",
// The remaining time of a sleep timer. Range 1 ~ 60 (minutes)
"delayoff",
// 0: no flow is running / 1:color flow is running
"flowing",
// Current flow parameters (only meaningful when 'flowing' is 1)
"flow_params",
// 1: Music mode is on / 0: Music mode is off
"music_on",
// Background light power status
"bg_power",
// Background light is flowing
"bg_flowing",
// Current flow parameters of background light
"bg_flow_params",
// Color temperature of background light
"bg_ct",
// 1: rgb mode / 2: color temperature mode / 3: hsv mode
"bg_lmode",
// Brightness percentage of background light
"bg_bright",
// Color of background light
"bg_rgb",
// Hue of background light
"bg_hue",
// Saturation of background light
"bg_sat",
// Brightness of night mode light
"nl_br",
// 0: daylight mode / 1: moonlight mode (ceiling light only)
// "active_mode"
];
/**
* [sync description]
* @return {Promise} Yeelight Instance
*/
Yeelight.prototype.sync = function(){
return this.get_prop.apply(this, this.props)
.then(function(res){
Object.keys(res).forEach(function(key){
this[ key ] = res[ key ];
}.bind(this));
return res;
}.bind(this));
};
/**
* Parse Yeelight Response
* @param {String} data
* @return {Yeelight} Yeelight Instance
*/
Yeelight.prototype.parse = function(data){
console.debug('->', data);
var yl = this;
function parseResult(result) {
var message = JSON.parse(result);
if(message.method === 'props'){
Object.keys(message.params).forEach(function(key){
yl[ key ] = message.params[ key ];
}.bind(yl));
}
yl.emit(message.method, message.params, message);
if(typeof yl.queue[ message.id ] === 'function'){
yl.queue[ message.id ](message);
yl.queue[ message.id ] = null;
delete yl.queue[ message.id ];
}
}
var results = data.toString().replace("}{","}}{{").split("}{");
for (i = 0; i < results.length; i++) {
parseResult(results[i]);
}
return this;
};
/**
* execute command
* @param {String} method The value of "method" is a string that specifies which control method the sender wants to
* invoke. The value must be chosen by sender from one of the methods that listed in
* "SUPPORT" header in advertisement request or search response message. Otherwise, the
* message will be rejected by smart LED.
* @param {String} params The value of "params" is an array. The values in the array are method specific.
* @return {Promise} promise
*/
Yeelight.prototype.command = function(method, params){
params = [].slice.call(params || []);
// The value of "id" is an integer filled by message sender. It will be echoed back in RESULT
// message. This is to help request sender to correlate request and response.
var id = (Math.random() * 1e3) & 0xff;
var request = { id, method, params };
var message = JSON.stringify(request);
request.promise = new Promise((accept, reject) => {
console.debug('<-', message);
this.socket.write(message + '\r\n', err => {
var respond = false;
var timeout = setTimeout(function(){
if(!respond) reject(new Error('Network timeout, Yeelight not response'));
}, 3000);
this.queue[ id ] = function(res){
if(respond) return;
respond = true;
clearTimeout(timeout);
var err = res.error;
if(err) return reject(err);
accept(res);
};
});
});
return request.promise;
};
/**
* get_prop
* This method is used to retrieve current property of smart LED.
* All the supported properties are defined in table 4-2, section 4.3
* @param {...*} props The parameter is a list of property names and the response contains a
* list of corresponding property values. If the requested property name is not recognized by
* smart LED, then a empty string value ("") will be returned.
*
* @returns {Promise} see {@link Yeelight#command}
*
* @example
*
* Request:
* {"id":1,"method":"get_prop","params":["power", "not_exist", "bright"]}
*
* Response:
* {"id":1, "result":["on", "", "100"]}
*
*/
Yeelight.prototype.get_prop = function (prop1, prop2, propN){
var props = [].concat.apply([], arguments);
return this.command('get_prop', props).then(function(res){
return props.reduce(function(item, name, index){
item[ name ] = res.result[ index ];
return item;
}, {});
});
};
/**
* set_name This method is used to name the device. The name will be stored on the
* device and reported in discovering response.
* User can also read the name through {@link Yeelight#get_prop} method.
* <p>
* When using Yeelight official App, the device name is stored on cloud.
* This method instead store the name on persistent memory of the device, so the two names
* could be different.
* </p>
* @param {String} name the name of the device.
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.set_name = function (name){
return this.command('set_name', [ name ]);
};
/**
* set_ct_abx
* This method is used to change the color temperature of a smart LED
*
* @param {Number} ct_value is the target color temperature. The type is integer and
* range is 1700 ~ 6500 (k).
* @param {String} effect support two values: "sudden" and "smooth". If effect is "sudden",
* then the color temperature will be changed directly to target value, under this case, the
* third parameter "duration" is ignored. If effect is "smooth", then the color temperature will
* be changed to target value in a gradual fashion, under this case, the total time of gradual
* change is specified in third parameter "duration".
* @param {Number} duration specifies the total time of the gradual changing. The unit is
* milliseconds. The minimum support duration is 30 milliseconds.
*
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.set_ct_abx = function (ct_value, effect, duration){
ct_value = Math.max(1700, Math.min(+ct_value || 3500, 6500));
return this.command('set_ct_abx', [ ct_value, effect || 'smooth', duration || 500 ]);
};
/**
* set_rgb This method is used to change the color of a smart LED.
* @param rgb_value is the target color, whose type is integer. It should be
* expressed in decimal integer ranges from 0 to 16777215 (hex: 0xFFFFFF).
* @param {String} effect [Refer to {@link Yeelight#set_ct_abx} method.]
* @param {Number} duration [Refer to {@link Yeelight#set_ct_abx} method.]
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.set_rgb = function (rgb_value, effect, duration){
rgb_value = Math.max(0, Math.min(+rgb_value, 0xffffff));
return this.command('set_rgb', [ rgb_value, effect || 'smooth', duration || 500 ]);
};
/**
* [set_hsv This method is used to change the color of a smart LED]
* @param {Number} hue is the target hue value, whose type is integer.
* It should be expressed in decimal integer ranges from 0 to 359.
* @param {Number} sat is the target saturation value whose type is integer. It's range is 0 to 100
* @param {Striung} effect [Refer to {@link Yeelight#set_ct_abx} method.]
* @param {Number} duration [Refer to {@link Yeelight#set_ct_abx} method.]
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.set_hsv = function (hue, sat, effect, duration){
hue = Math.max(0, Math.min(+hue, 359));
sat = Math.max(0, Math.min(+sat, 100));
return this.command('set_hsv', [ hue, sat, effect || 'smooth', duration || 500 ]);
};
/**
* set_bright This method is used to change the brightness of a smart LED.
* @param brightness is the target brightness. The type is integer and ranges
* from 1 to 100. The brightness is a percentage instead of a absolute value.
* 100 means maximum brightness while 1 means the minimum brightness.
* @param {String} effect [Refer to {@link Yeelight#set_ct_abx} method.]
* @param {Number} duration [Refer to {@link Yeelight#set_ct_abx} method.]
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.set_bright = function (brightness, effect, duration){
brightness = Math.max(1, Math.min(+brightness, 100));
return this.command('set_bright', [ brightness, effect || 'smooth', duration || 500 ]);
};
/**
* set_power This method is used to switch on or off the smart LED (software managed on/off).
* @param power can only be "on" or "off".
* <li>"on" means turn on the smart LED,
* <li>"off" means turn off the smart LED.
* @param {String} effect [description]
* @param {Number} duration [description]
* @param {Number} mode [description]
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.set_power = function (power, effect, duration, mode) {
power = ~[ 1, true, '1','on' ].indexOf(power) ? 'on' : 'off';
return this.command('set_power', [ power, effect || 'smooth', duration || 500 , mode || 0 ]);
};
/**
* toggle This method is used to toggle the smart LED.
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.toggle = function (){
return this.command('toggle');
};
/**
* set_default This method is used to save current state of smart LED in persistent
* memory. So if user powers off and then powers on the smart LED again (hard power reset),
* the smart LED will show last saved state.
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.set_default = function (){
return this.command('set_default', arguments);
};
/**
* This method is used to start a color flow. Color flow is a series of smart
* LED visible state changing. It can be brightness changing, color changing or color
* temperature changing.This is the most powerful command. All our recommended scenes,
* e.g. Sunrise/Sunset effect is implemented using this method. With the flow expression, user
* can actually “program” the light effect.
* @param {Number} count is the total number of visible state changing before color flowstopped.
* 0 means infinite loop on the state changing.
* @param {Number} action is the action taken after the flow is stopped.
* <li>0 means smart LED recover to the state before the color flow started.
* <li>1 means smart LED stay at the state when the flow is stopped.
* <li>2 means turn off the smart LED after the flow is stopped.
* @param {String} flow_expression is the expression of the state changing series.
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.start_cf = function (count, action, flow_expression){
return this.command('start_cf', arguments);
};
/**
* stop_cf This method is used to stop a running color flow.
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.stop_cf = function (){
return this.command('stop_cf');
};
/**
* set_scene This method is used to set the smart LED directly to specified state. If
* the smart LED is off, then it will turn on the smart LED firstly and then apply the specified
* command.
* @param {String} type can be "color", "hsv", "ct", "cf", "auto_dealy_off".
* <li>"color" means change the smart LED to specified color and brightness.
* <li>"hsv" means change the smart LED to specified color and brightness.
* <li>"ct" means change the smart LED to specified ct and brightness.
* <li>"cf" means start a color flow in specified fashion.
* <li>"auto_delay_off" means turn on the smart LED to specified brightness and start a sleep timer to turn off the light after the specified minutes.
* "val1", "val2", "val3" are class specific.
* @param {...Number} value
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.set_scene = function (type, val, val2, val3){
return this.command('set_scene', arguments);
};
/**
* [cron_add description]
* @param {Number} type [description]
* @param {Number} value [description]
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.cron_add = function (type, value){
return this.command('cron_add', arguments);
};
/**
* [cron_get description]
* @param {Number} type [description]
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.cron_get = function (type){
return this.command('cron_get', arguments);
};
/**
* [cron_del description]
* @param {Number} type [description]
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.cron_del = function (type){
return this.command('cron_del', arguments);
};
/**
* This method is used to change brightness, CT or color of a smart LED
* without knowing the current value, it's main used by controllers.
* @param {String} action the direction of the adjustment. The valid value can be:
* “increase": increase the specified property
* “decrease": decrease the specified property
* “circle": increase the specified property, after it reaches the max value, go back to minimum value.
* @param {String} prop the property to adjust. The valid value can be:
* “bright": adjust brightness.
* “ct": adjust color temperature.
* “color": adjust color.
* (When “prop" is “color", the “action" can only be “circle",
* otherwise, it will be deemed as invalid request.)
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.set_adjust = function (action, prop){
return this.command('set_adjust', [ action, prop ]);
};
/**
* set_music This method is used to start or stop music mode on a device.
* Under music mode, no property will be reported and no message quota is checked.
* <p>
* When control device wants to start music mode, it needs start a TCP
* server firstly and then call “set_music” command to let the device know the IP and Port of the
* TCP listen socket. After received the command, LED device will try to connect the specified
* peer address. If the TCP connection can be established successfully, then control device could
* send all supported commands through this channel without limit to simulate any music effect.
* The control device can stop music mode by explicitly send a stop command or just by closing
* the socket.
* </p>
*
* @param {Number} action the action of set_music command. The valid value can be:
* 0: turn off music mode.
* 1: turn on music mode.
* @param {String} host the IP address of the music server
* @param {Number} port the TCP port music application is listening on.
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.set_music = function (action, host, port){
action = action & 0xff;
return this.command('set_music', arguments);
};
/**
* Close Yeelight device
* @return {Yeelight} Yeelight Instance
*/
Yeelight.prototype.exit = function(){
this.socket.end();
return this;
};
/**
* These methods are used to control background light, for each command
* detail, refer to set_xxx command.
*
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.bg_set = function(){
return this.command(arguments);
};
/**
* This method is used to toggle the main light and background light at the same time.
*
* <p>
* When there is main light and background light, “toggle” is used to toggle
* main light, “bg_toggle” is used to toggle background light while “dev_toggle” is used to
* toggle both light at the same time
* </p>
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.dev_toggle = function(){
return this.command('dev_toggle');
}
/**
* This method is used to adjust the brightness by specified percentage
* within specified duration.
* @param {Number} percentage the percentage to be adjusted. The range is: -100 ~ 100
* @param {Number} duration Refer to "set_ct_abx" method.
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.adjust_bright = function(percentage, duration){
return this.command('adjust_bright', [ percentage, duration ]);
};
/**
* This method is used to adjust the color temperature by specified
* percentage within specified duration.
*
* @param {Number} percentage the percentage to be adjusted. The range is: -100 ~ 100
* @param {Number} duration Refer to "set_ct_abx" method.
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.adjust_ct = function(percentage, duration){
return this.command('adjust_bright', [ percentage, duration ]);
};
/**
* This method is used to adjust the color within specified duration.
*
* @param {Number} percentage the percentage to be adjusted. The range is: -100 ~ 100
* @param {Number} duration Refer to "set_ct_abx" method.
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.adjust_color = function(percentage, duration){
return this.command('adjust_bright', [ percentage, duration ]);
};
module.exports = Yeelight;