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 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){
if(typeof port === 'function'){
callback = port; port = 1982;
}
var yeelights = [];
var discover = ssdp({ port: port || 1982 });
discover.on('response', function(response){
var address = response.headers['Location'];
console.debug('received response from', address);
if(address && (!~yeelights.indexOf(address))){
yeelights.push(address);
var yeelight = new Yeelight( address );
yeelight.id = response.headers.id;
yeelight.model = response.headers.model;
const { support } = response.headers;
yeelight.supports = support && support.split(' ') || [];
yeelight.on('connect', function(){
callback.call(discover, this, response);
});
};
});
console.debug('start finding ...');
return discover.search('wifi_bulb');
};
/**
* is_support("set_rgb")
* @param {String} func
* @return {Boolean} isSupport
*/
Yeelight.prototype.is_support = function(func){
return !!~this.supports.indexOf(func);
};
/**
* props
* @type {Array}
*/
Yeelight.prototype.props = [
"name", "power", "bright", "rgb",
"ct", "hue", "sat", "color_mode",
"delayoff", "flowing", "flow_params",
"music_on"
];
/**
* [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]
* @returns {Promise} see {@link Yeelight#command}
*/
Yeelight.prototype.set_power = function (power, effect, duration){
power = ~[ 1, true, '1','on' ].indexOf(power) ? 'on' : 'off';
return this.command('set_power', [ power, effect || 'smooth', duration || 500 ]);
};
/**
* 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;