yeelight-awesome
Version:
The node.js client api to control yeelight device over WIFI
603 lines • 30.8 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
to[j] = from[i];
return to;
};
exports.__esModule = true;
exports.Yeelight = void 0;
var events_1 = require("events");
var net_1 = require("net");
var discover_1 = require("./discover");
var logger_1 = require("./logger");
var models_1 = require("./models");
var DEFAULT_PORT = 55443;
/**
* The client to connect and control the light
*/
var Yeelight = /** @class */ (function (_super) {
__extends(Yeelight, _super);
/**
* @constructor
* @param {IConfig} options : The client config initial the client
*/
function Yeelight(options, logger) {
var _this = _super.call(this) || this;
_this.options = options;
_this.logger = logger;
_this.autoReconnect = false;
_this.disablePing = false;
_this.autoReconnectTime = 1000;
_this.connectTimeout = 1000;
_this.commandId = 1;
_this.sentCommands = {};
_this.isConnecting = false;
_this.isClosing = false;
_this.isReconnecting = false;
_this.reconnectTimeout = null;
_this.pingTimeout = null;
_this.EVENT_NAME = "command_result";
_this.logger = logger || logger_1.defaultLogger;
_this.resultCommands = new Array();
_this.emit("ready", _this);
// Set default timeout if not provide
_this.options.timeout = _this.options.timeout || 5000;
return _this;
}
Yeelight.prototype.onData = function (data) {
var _this = this;
var messages = data.toString();
messages.split("\n").forEach(function (message) {
var json = message.toString();
if (json) {
var result = JSON.parse(json);
_this.onMessage(result);
}
});
};
Yeelight.prototype.onMessage = function (result) {
this.resultCommands.push(result);
var commandId = "" + result.id;
var originalCommand = this.sentCommands[commandId];
if (!originalCommand) {
return;
}
var eventData = {
action: originalCommand.method,
command: originalCommand,
result: result,
success: true
};
this.logger.info("Light data recieved: ", result);
this.emit(this.EVENT_NAME + "_" + result.id, eventData);
this.emit(originalCommand.method, eventData);
delete this.sentCommands[commandId];
if (result.id && result.result) {
this.emit("commandSuccess", eventData);
}
if (result && result.error) {
eventData.success = false;
this.emit("commandError", eventData);
}
};
/**
* Drop connection/listerners and clean up resources.
*/
Yeelight.prototype.disconnect = function () {
var _this = this;
this.removeAllListeners();
this.emit("end");
// this.client.destroy();
this.client.removeAllListeners("data");
this.isClosing = true;
return new Promise(function (resolve) { return _this.client.end(resolve); })
.then(function () {
return _this.closeConnection();
});
};
/**
* establish connection to light,
* @returns return promise of the current instance
*/
Yeelight.prototype.connect = function () {
return __awaiter(this, void 0, void 0, function () {
var discover, devices, device;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (this.isConnecting) {
throw new Error("Already connecting");
}
if (!this.options.lightIp) return [3 /*break*/, 1];
return [2 /*return*/, this.connectToIp(this.options.lightIp, this.options.lightPort)];
case 1:
if (!this.options.lightId) return [3 /*break*/, 4];
discover = new discover_1.Discover({
filter: function (d) { return d.id === _this.options.lightId; },
limit: 1,
timeout: this.connectTimeout
});
return [4 /*yield*/, discover.start()];
case 2:
devices = _a.sent();
return [4 /*yield*/, discover.destroy()];
case 3:
_a.sent();
device = devices[0];
if (device) {
return [2 /*return*/, this.connectToIp(device.host, device.port)];
}
else {
throw new Error("Unable to connect, no device with id '" + this.options.lightId + "' found");
}
return [3 /*break*/, 5];
case 4: throw new Error("Unable to connect, neither config.lightIp or options.lightId is set");
case 5: return [2 /*return*/];
}
});
});
};
/*
* This method is used to switch on or off the smart LED (software managed on/off)
* @param turnOn:boolean set status true is turn on, false is turn off
* @param {"smooth"| "sudden"} 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<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.setPower = function (turnOn, effect, duration) {
if (turnOn === void 0) { turnOn = true; }
if (effect === void 0) { effect = "sudden"; }
if (duration === void 0) { duration = 500; }
return this.sendCommand(new models_1.Command(1, models_1.CommandType.SET_POWER, [(turnOn ? "on" : "off"), effect, duration]));
};
/**
* This method is used to start a timer job on the smart LED.
* Only accepted if the smart LED is currently in "on" state
* @param type currently can only be 0. (means power off)
* @param time the length of the timer (in minutes). Request
* @returns {Promise<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.cronAdd = function (type, time) {
return this.sendCommand(new models_1.Command(1, models_1.CommandType.CRON_ADD, [0, time]));
};
/**
* This method is used to retrieve the setting of the current cron job of the specified type.
* @param type currently can only be 0. (means power off)
* @returns {Promise<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.cronGet = function (type) {
return this.sendCommand(new models_1.Command(1, models_1.CommandType.CRON_GET, [type]));
};
/**
* This method is used to retrieve the setting of the current cron job of the specified type.
* @param type currently can only be 0. (means power off)
* @returns {Promise<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.cronDelete = function (type) {
return this.sendCommand(new models_1.Command(1, models_1.CommandType.CRON_DEL, [type]));
};
/**
* This method is used to toggle the smart LED.
* This method is used to switch on or off the smart LED (software managed on/off)
* @returns {Promise<ICommandResult>} Return the promise indicate the command success or reject
* @returns {Promise<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.toggle = function () {
return this.sendCommand(new models_1.Command(1, models_1.CommandType.TOGGLE, []));
};
/**
* 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.
* For example, if user likes the current color (red) and brightness (50%)
* and want to make this state as a default initial state (every time the smart LED is powered),
* then he can use set_default to do a snapshot.
* @returns {Promise<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.setDefault = function () {
return this.sendCommand(new models_1.Command(1, models_1.CommandType.SET_DEFAULT, []));
};
/**
* 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 {FlowState[]} states: Each visible state changing is defined to be a flow tuple that contains 4 elements:
* [duration, mode, value, brightness]. A flow expression is a series of flow tuples.
* So for above request example, it means: change CT to 2700K & maximum brightness gradually in 1000ms,
* then change color to red & 10% brightness gradually in 500ms, then stay at this state for 5 seconds,
* then change CT to 5000K & minimum brightness gradually in 500ms.
* After 4 changes reached, stopped the flow and power off the smart LED.
* @param {StarFlowAction} action: is the action taken after the flow is stopped
* @param {number} repeat is the total number of visible state changing before color flow
* stopped. 0 means infinite loop on the state changing. @default infinity
* @returns {Promise<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.startColorFlow = function (states, action, repeat) {
if (action === void 0) { action = models_1.StartFlowAction.LED_STAY; }
if (repeat === void 0) { repeat = 0; }
var values = states.reduce(function (a, b) { return __spreadArray(__spreadArray([], a), b.getState()); }, []);
return this.sendCommand(new models_1.Command(1, models_1.CommandType.START_COLOR_FLOW, [repeat, action, values.join(",")]));
};
/**
* This method is used to stop a running color flow.
*/
Yeelight.prototype.stopColorFlow = function () {
return this.sendCommand(new models_1.Command(1, models_1.CommandType.STOP_COLOR_FLOW, []));
};
/**
* 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 scene type of scene to update
* @returns {Promise<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.setScene = function (scene) {
return this.sendCommand(new models_1.Command(1, models_1.CommandType.SET_SCENE, scene.getData()));
};
/**
* This method is used to retrieve current property of smart LED.
* @param {string[]} params The parameter is a list of property names and the response contains a
* list of corresponding property values.
* the requested property name is not recognized by smart LED, then a empty string value ("") will be returned.
* Request Example: {"id":1,"method":"get_prop","params":["power", "not_exist", "bright"]}
* Example: {"id":1, "result":["on", "", "100"]}
* @returns {Promise<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.getProperty = function (params) {
return this.sendCommand(new models_1.Command(1, models_1.CommandType.GET_PROPS, params));
};
/**
* This method is used to change the color temperature of a smart LED.
* @param {number} ct the target color temperature. The type is integer and range is 1700 ~ 6500 (k).
* @param {"smooth"| "sudden"} 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<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.setCtAbx = function (ct, effect, duration) {
if (effect === void 0) { effect = "sudden"; }
if (duration === void 0) { duration = 500; }
return this.sendCommand(new models_1.Command(1, models_1.CommandType.SET_CT_ABX, [ct, effect, duration]));
};
/**
* This method is used to change the color of a smart LED.
* Only accepted if the smart LED is currently in "on" state.
* @param color the target color, whose type is integer.
* It should be expressed in decimal integer ranges from 0 to 16777215 (hex: 0xFFFFFF).
* can be initial by new Color(233,255,244)
* @param {"smooth"| "sudden"} 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<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.setRGB = function (color, effect, duration) {
if (duration === void 0) { duration = 500; }
return this.sendCommand(new models_1.Command(1, models_1.CommandType.SET_RGB, [color.getValue(), effect, duration]));
};
/**
* This method is used to change the color of a smart LED.
* Only accepted if the smart LED is currently in "on" state.
* @param hue the target hue, whose type is integer.
* It should be expressed in decimal integer ranges from 0 to 359.
* @param sat the target saturation, whose type is integer.
* It should be expressed in decimal integer ranges from 0 to 100.
* @param {"smooth"| "sudden"} 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<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.setHSV = function (hue, sat, effect, duration) {
if (effect === void 0) { effect = "sudden"; }
if (duration === void 0) { duration = 500; }
return this.sendCommand(new models_1.Command(1, models_1.CommandType.SET_HSV, [hue, sat, effect, duration]));
};
/**
* This method is used to change the color of a smart LED.
* Only accepted if the smart LED is currently in "on" state.
* @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 {"smooth"| "sudden"} 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<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.setBright = function (brightness, effect, duration) {
if (effect === void 0) { effect = "sudden"; }
if (duration === void 0) { duration = 500; }
return this.sendCommand(new models_1.Command(1, models_1.CommandType.SET_BRIGHT, [brightness, effect, duration]));
};
/**
* @param command 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 {AdjustType} adjustType 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 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<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.setAdjust = function (adjustType, prop) {
return this.sendCommand(new models_1.Command(1, models_1.CommandType.SET_ADJUST, [adjustType, prop]));
};
/**
* 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.
* @param 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.
* 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.
* @returns {Promise<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.setMusic = function (action, host, port) {
return this.sendCommand(new models_1.Command(1, models_1.CommandType.SET_MUSIC, [action, host, port]));
};
/**
* 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 “get_prop” method
* @param {string} name the name of the device.
* 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.
* @returns {Promise<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.setName = function (name) {
return this.sendCommand(new models_1.Command(1, models_1.CommandType.SET_NAME, [name]));
};
/**
* 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 the milisecond of animation
* @returns {Promise<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.adjust = function (type, percentage, duration) {
if (percentage === void 0) { percentage = 0; }
if (duration === void 0) { duration = 500; }
return this.sendCommand(new models_1.Command(1, type, [percentage, duration]));
};
/**
* This method is used to just check if the connection is alive
*/
Yeelight.prototype.ping = function () {
return this.sendCommand(new models_1.Command(1, models_1.CommandType.PING, []))["catch"](function (command) {
// Expect a response: {"id":6, "error":{"code":-1, "message":"method not supported"}}
var result = command.result;
if (!result ||
!result.error ||
result.error.code !== -1 ||
!(result.error.message + "").match(/not supported/i)) {
throw command;
}
})
.then(function () {
// do nothing with the response
return null;
});
};
/**
* Use this function to send any command to the light,
* please refer to specification to know the structure of command data
* @param {Command} command The command to send to light via socket write
* @returns {Promise<IEventResult>} return a promise of IEventResult
*/
Yeelight.prototype.sendCommand = function (command) {
var _this = this;
if (!this.connected) {
return Promise.reject("Connection is closed");
}
return new Promise(function (resolve, reject) {
var commandId = _this.commandId++;
command.id = commandId;
_this.sentCommands["" + commandId] = command;
var timer = setTimeout(function () {
_this.removeAllListeners(_this.EVENT_NAME + "_" + command.id);
_this.emit("commandTimedout", command);
_this.wasDisconnected();
reject("Command timedout, no response from server.");
}, _this.options.timeout);
_this.once(_this.EVENT_NAME + "_" + command.id, function (commandResult) {
clearTimeout(timer);
var result = commandResult.result;
if (result.id && result.result) {
resolve(commandResult);
}
else if (result.error) {
reject(commandResult);
}
});
var msg = command.getString();
_this.client.write(msg + "\r\n", function () {
_this.emit(command.method + "_sent", command);
});
});
};
Yeelight.prototype.connectToIp = function (host, port) {
var _this = this;
return new Promise(function (resolve, reject) {
_this.isConnecting = true;
_this.isClosing = false;
if (_this.client) {
// close old socket:
_this.closeConnection();
}
_this.client = new net_1.Socket();
_this.client.on("data", _this.onData.bind(_this));
_this.client.on("connect", function () {
_this.wasConnected();
});
_this.client.on("close", function () {
_this.wasDisconnected();
});
_this.client.on("end", function () {
_this.wasDisconnected();
});
_this.client.on("error", function (err) {
_this.emit("error", err);
_this.wasDisconnected();
});
_this.client.connect(port || DEFAULT_PORT, host, function () {
_this.isConnecting = false;
_this.wasConnected();
// me.emit("connected", this);
resolve(_this);
});
_this.client.once("error", function (err) {
_this.isConnecting = false;
reject(err);
});
setTimeout(function () {
_this.isConnecting = false;
reject("Connection timeout");
}, _this.connectTimeout);
});
};
Yeelight.prototype.wasConnected = function () {
this.isReconnecting = false;
if (!this.connected) {
this.connected = true;
this.emit("connected");
this.triggerPing();
}
};
Yeelight.prototype.wasDisconnected = function () {
if (this.connected) {
this.connected = false;
this.emit("disconnected");
}
this._recoverNetworkError();
};
Yeelight.prototype._recoverNetworkError = function () {
var _this = this;
if (this.autoReconnect && !this.isClosing) {
this.isReconnecting = true;
if (!this.reconnectTimeout) {
this.reconnectTimeout = setTimeout(function () {
_this.reconnectTimeout = null;
if (!_this.connected) {
if (!_this.isConnecting) {
_this.connect()["catch"](function (err) {
if (!err.toString().match(/timeout/)) { // ignore connection timeout
_this.emit("error", "Erorr during reconnect: " + err);
}
_this._recoverNetworkError();
});
}
else {
_this._recoverNetworkError();
}
}
}, this.autoReconnectTime);
}
}
};
Yeelight.prototype.triggerPing = function () {
var _this = this;
if (this.connected && !this.disablePing) {
this.ping()
.then(function () {
if (!_this.pingTimeout) {
_this.pingTimeout = setTimeout(function () {
_this.pingTimeout = null;
_this.triggerPing();
}, _this.connectTimeout);
}
})["catch"](function () {
// swallow
});
}
};
Yeelight.prototype.closeConnection = function () {
this.wasDisconnected();
if (this.client) {
this.client.removeAllListeners();
this.client.destroy();
}
};
return Yeelight;
}(events_1.EventEmitter));
exports.Yeelight = Yeelight;
//# sourceMappingURL=yeelight.js.map