UNPKG

yeelight-awesome

Version:

The node.js client api to control yeelight device over WIFI

603 lines 30.8 kB
"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