UNPKG

lifx-lan-client

Version:

Node.js implementation of the LIFX LAN protocol

678 lines (609 loc) 25.1 kB
'use strict'; const {packet, constants, validate, utils} = require('../lifx'); const {assign, pick, includes, isNumber} = require('lodash'); /** * A representation of a light bulb * @class * @param {Obj} constr constructor object * @param {Lifx/Client} constr.client the client the light belongs to * @param {String} constr.id the id used to target the light * @param {String} constr.address ip address of the light * @param {Number} constr.port port of the light * @param {Number} constr.seenOnDiscovery on which discovery the light was last seen */ function Light(constr) { this.client = constr.client; this.id = constr.id; // Used to target the light this.address = constr.address; this.port = constr.port; this.label = null; this.status = 'on'; this.seenOnDiscovery = constr.seenOnDiscovery; } /** * Turns the light off * @example light('192.168.2.130').off() * @param {Number} [duration] transition time in milliseconds * @param {Function} [callback] called when light did receive message */ Light.prototype.off = function(duration, callback) { validate.optionalDuration(duration, 'light off method'); validate.optionalCallback(callback, 'light off method'); const packetObj = packet.create('setPower', {level: 0, duration: duration}, this.client.source); packetObj.target = this.id; this.client.send(packetObj, callback); }; /** * Turns the light on * @example light('192.168.2.130').on() * @param {Number} [duration] transition time in milliseconds * @param {Function} [callback] called when light did receive message */ Light.prototype.on = function(duration, callback) { validate.optionalDuration(duration, 'light on method'); validate.optionalCallback(callback, 'light on method'); const packetObj = packet.create('setPower', {level: 65535, duration: duration}, this.client.source); packetObj.target = this.id; this.client.send(packetObj, callback); }; /** * Changes the color to the given HSBK value * @param {Number} hue color hue from 0 - 360 (in °) * @param {Number} saturation color saturation from 0 - 100 (in %) * @param {Number} brightness color brightness from 0 - 100 (in %) * @param {Number} [kelvin=3500] color kelvin between 2500 and 9000 * @param {Number} [duration] transition time in milliseconds * @param {Function} [callback] called when light did receive message */ Light.prototype.color = function(hue, saturation, brightness, kelvin, duration, callback) { validate.colorHsb(hue, saturation, brightness, 'light color method'); validate.optionalKelvin(kelvin, 'light color method'); validate.optionalDuration(duration, 'light color method'); validate.optionalCallback(callback, 'light color method'); // Convert HSB values to packet format hue = Math.round(hue / constants.HSBK_MAXIMUM_HUE * 65535); saturation = Math.round(saturation / constants.HSBK_MAXIMUM_SATURATION * 65535); brightness = Math.round(brightness / constants.HSBK_MAXIMUM_BRIGHTNESS * 65535); const packetObj = packet.create('setColor', { hue: hue, saturation: saturation, brightness: brightness, kelvin: kelvin, duration: duration }, this.client.source); packetObj.target = this.id; this.client.send(packetObj, callback); }; /** * Changes the color to the given rgb value * Note RGB poorly represents the color of light, prefer setting HSBK values with the color method * @example light.colorRgb(255, 0, 0) * @param {Integer} red value between 0 and 255 representing amount of red in color * @param {Integer} green value between 0 and 255 representing amount of green in color * @param {Integer} blue value between 0 and 255 representing amount of blue in color * @param {Number} [duration] transition time in milliseconds * @param {Function} [callback] called when light did receive message */ Light.prototype.colorRgb = function(red, green, blue, duration, callback) { validate.colorRgb(red, green, blue, 'light colorRgb method'); validate.optionalDuration(duration, 'light colorRgb method'); validate.optionalCallback(callback, 'light colorRgb method'); const hsbObj = utils.rgbToHsb({r: red, g: green, b: blue}); this.color(hsbObj.h, hsbObj.s, hsbObj.b, 3500, duration, callback); }; /** * Changes the color to the given rgb value * Note RGB poorly represents the color of light, prefer setting HSBK values with the color method * @example light.colorRgb('#FF0000') * @param {String} hexString rgb hex string starting with # char * @param {Number} [duration] transition time in milliseconds * @param {Function} [callback] called when light did receive message */ Light.prototype.colorRgbHex = function(hexString, duration, callback) { if (typeof hexString !== 'string') { throw new TypeError('LIFX light colorRgbHex method expects first parameter hexString to a string'); } validate.optionalDuration(duration, 'light colorRgbHex method'); validate.optionalCallback(callback, 'light colorRgbHex method'); const rgbObj = utils.rgbHexStringToObject(hexString); const hsbObj = utils.rgbToHsb(rgbObj); this.color(hsbObj.h, hsbObj.s, hsbObj.b, 3500, duration, callback); }; /** * Sets the Maximum Infrared brightness * @param {Number} brightness infrared brightness from 0 - 100 (in %) * @param {Function} [callback] called when light did receive message */ Light.prototype.maxIR = function(brightness, callback) { validate.irBrightness(brightness, 'light setMaxIR method'); brightness = Math.round(brightness / constants.IR_MAXIMUM_BRIGHTNESS * 65535); if (callback !== undefined && typeof callback !== 'function') { throw new TypeError('LIFX light setMaxIR method expects callback to be a function'); } const packetObj = packet.create('setInfrared', { brightness: brightness }, this.client.source); packetObj.target = this.id; this.client.send(packetObj, callback); }; /** * Apply a waveform effect to the bulb. * @param {Number} hue color hue from 0 - 360 (in °) * @param {Number} saturation color saturation from 0 - 100 (in %) * @param {Number} brightness color brightness from 0 - 100 (in %) * @param {Number} [kelvin=3500] color kelvin between 2500 and 9000 * @param {Boolean} [transient=false] color does not persist * @param {Number} [period=500] duration of a cycle in miliseconds * @param {Number} [cycles=10e30] number of cycles * @param {Number} [skewRatio=0.5] waveform skew, between 0 and 1 * @param {Number} [waveform=0] waveform to use for transition * @param {Function} [callback] called when light did receive message */ Light.prototype.waveform = function(hue, saturation, brightness, kelvin, transient = false, period = 500, cycles = 10e30, skewRatio = 0.5, waveform = 0, callback) { validate.colorHsb(hue, saturation, brightness, 'light waveform method'); validate.optionalKelvin(kelvin, 'light waveform method'); validate.optionalBoolean(transient, 'transient', 'light waveform method'); validate.optionalNumber(period, 'period', 'light waveform method'); validate.optionalNumber(cycles, 'cycles', 'light waveform method'); validate.optionalNumber(skewRatio, 'skewRatio', 'light waveform method'); validate.optionalWaveform(waveform, 'light waveform method'); validate.optionalCallback(callback, 'light waveform method'); // Convert HSB values to packet format hue = Math.round(hue / constants.HSBK_MAXIMUM_HUE * 65535); saturation = Math.round(saturation / constants.HSBK_MAXIMUM_SATURATION * 65535); brightness = Math.round(brightness / constants.HSBK_MAXIMUM_BRIGHTNESS * 65535); const packetObj = packet.create('setWaveform', { color: { hue: hue, saturation: saturation, brightness: brightness, kelvin: kelvin }, isTransient: transient, period, cycles, skewRatio: (skewRatio * 65535) - 32768, waveform }, this.client.source); packetObj.target = this.id; this.client.send(packetObj, callback); }; /** * Requests the current state of the light * @param {Function} callback a function to accept the data */ Light.prototype.getState = function(callback) { validate.callback(callback, 'light getState method'); const packetObj = packet.create('getLight', {}, this.client.source); packetObj.target = this.id; const sqnNumber = this.client.send(packetObj); this.client.addMessageHandler('stateLight', function(err, msg) { if (err) { return callback(err, null); } // Convert HSB to readable format msg.color.hue = Math.round(msg.color.hue * (constants.HSBK_MAXIMUM_HUE / 65535)); msg.color.saturation = Math.round(msg.color.saturation * (constants.HSBK_MAXIMUM_SATURATION / 65535)); msg.color.brightness = Math.round(msg.color.brightness * (constants.HSBK_MAXIMUM_BRIGHTNESS / 65535)); // Convert power to readable format if (msg.power === 65535) { msg.power = 1; } callback(null, { color: msg.color, power: msg.power, label: msg.label }); }, sqnNumber); }; /** * Requests the current maximum setting for the infrared channel * @param {Function} callback a function to accept the data */ Light.prototype.getMaxIR = function(callback) { validate.callback(callback, 'light getMaxIR method'); const packetObj = packet.create('getInfrared', {}, this.client.source); packetObj.target = this.id; packetObj.resRequired = true; const sqnNumber = this.client.send(packetObj); this.client.addMessageHandler('stateInfrared', function(err, msg) { if (err) { return callback(err, null); } msg.brightness = Math.round(msg.brightness * (constants.HSBK_MAXIMUM_BRIGHTNESS / 65535)); callback(null, msg.brightness); }, sqnNumber); }; /** * Requests hardware info from the light * @param {Function} callback a function to accept the data with error and * message as parameters */ Light.prototype.getHardwareVersion = function(callback) { validate.callback(callback, 'light getHardwareVersion method'); const packetObj = packet.create('getVersion', {}, this.client.source); packetObj.target = this.id; const sqnNumber = this.client.send(packetObj); this.client.addMessageHandler('stateVersion', function(err, msg) { if (err) { return callback(err, null); } const versionInfo = pick(msg, [ 'vendorId', 'productId', 'version' ]); callback(null, assign( versionInfo, utils.getHardwareDetails(versionInfo.vendorId, versionInfo.productId) )); }, sqnNumber); }; /** * Requests uptime from the light * @param {Function} callback a function to accept the data with error and * message as parameters */ Light.prototype.getUptime = function(callback) { if (typeof callback !== 'function') { throw new TypeError('LIFX light getUptime method expects callback to be a function'); } const packetObj = packet.create('getInfo', {}, this.client.source); packetObj.target = this.id; const sqnNumber = this.client.send(packetObj); this.client.addMessageHandler('stateInfo', function(err, msg) { if (err) { return callback(err, null); } callback(null, msg.uptime); }, sqnNumber); }; /** * Reboots the light * @param {Function} callback called when light did receive message */ Light.prototype.reboot = function(callback) { if (typeof callback !== 'function') { throw new TypeError('LIFX light reboot method expects callback to be a function'); } const packetObj = packet.create('rebootRequest', {}, this.client.source); packetObj.target = this.id; const sqnNumber = this.client.send(packetObj); this.client.addMessageHandler('rebootResponse', callback, sqnNumber); }; /** * Requests used version from the microcontroller unit of the light * @param {Function} callback a function to accept the data */ Light.prototype.getFirmwareVersion = function(callback) { validate.callback(callback, 'light getFirmwareIgetFirmwareVersion method'); const packetObj = packet.create('getHostFirmware', {}, this.client.source); packetObj.target = this.id; const sqnNumber = this.client.send(packetObj); this.client.addMessageHandler('stateHostFirmware', function(err, msg) { if (err) { return callback(err, null); } callback(null, pick(msg, [ 'majorVersion', 'minorVersion' ])); }, sqnNumber); }; /** * Requests infos from the microcontroller unit of the light * @param {Function} callback a function to accept the data */ Light.prototype.getFirmwareInfo = function(callback) { validate.callback(callback, 'light getFirmwareInfo method'); const packetObj = packet.create('getHostInfo', {}, this.client.source); packetObj.target = this.id; const sqnNumber = this.client.send(packetObj); this.client.addMessageHandler('stateHostInfo', function(err, msg) { if (err) { return callback(err, null); } callback(null, pick(msg, [ 'signal', 'tx', 'rx' ])); }, sqnNumber); }; /** * Requests wifi infos from for the light * @param {Function} callback a function to accept the data */ Light.prototype.getWifiInfo = function(callback) { validate.callback(callback, 'light getWifiInfo method'); const packetObj = packet.create('getWifiInfo', {}, this.client.source); packetObj.target = this.id; const sqnNumber = this.client.send(packetObj); this.client.addMessageHandler('stateWifiInfo', function(err, msg) { if (err) { return callback(err, null); } callback(null, pick(msg, [ 'signal', 'tx', 'rx' ])); }, sqnNumber); }; /** * Requests used version from the wifi controller unit of the light (wifi firmware version) * @param {Function} callback a function to accept the data */ Light.prototype.getWifiVersion = function(callback) { validate.callback(callback, 'light getWifiVersion method'); const packetObj = packet.create('getWifiFirmware', {}, this.client.source); packetObj.target = this.id; const sqnNumber = this.client.send(packetObj); this.client.addMessageHandler('stateWifiFirmware', function(err, msg) { if (err) { return callback(err, null); } return callback(null, pick(msg, [ 'majorVersion', 'minorVersion' ])); }, sqnNumber); }; /** * Requests the label of the light * @param {Function} callback a function to accept the data * @param {Boolean} [cache=false] return cached result if existent * @return {Function} callback(err, label) */ Light.prototype.getLabel = function(callback, cache) { validate.callback(callback, 'light getLabel method'); if (cache !== undefined && typeof cache !== 'boolean') { throw new TypeError('LIFX light getLabel method expects cache to be a boolean'); } if (cache === true) { if (typeof this.label === 'string' && this.label.length > 0) { return callback(null, this.label); } } const packetObj = packet.create('getLabel', { target: this.id }, this.client.source); const sqnNumber = this.client.send(packetObj); this.client.addMessageHandler('stateLabel', function(err, msg) { if (err) { return callback(err, null); } return callback(null, msg.label); }, sqnNumber); }; /** * Sets the label of light * @example light.setLabel('Kitchen') * @param {String} label new label to be set, maximum 32 bytes * @param {Function} [callback] called when light did receive message */ Light.prototype.setLabel = function(label, callback) { if (label === undefined || typeof label !== 'string') { throw new TypeError('LIFX light setLabel method expects label to be a string'); } if (Buffer.byteLength(label, 'utf8') > 32) { throw new RangeError('LIFX light setLabel method expects a maximum of 32 bytes as label'); } if (label.length < 1) { throw new RangeError('LIFX light setLabel method expects a minimum of one char as label'); } validate.optionalCallback(callback, 'light setLabel method'); const packetObj = packet.create('setLabel', {label: label}, this.client.source); packetObj.target = this.id; this.client.send(packetObj, callback); }; /** * Requests ambient light value of the light * @param {Function} callback a function to accept the data */ Light.prototype.getAmbientLight = function(callback) { validate.callback(callback, 'light getAmbientLight method'); const packetObj = packet.create('getAmbientLight', {}, this.client.source); packetObj.target = this.id; const sqnNumber = this.client.send(packetObj); this.client.addMessageHandler('stateAmbientLight', function(err, msg) { if (err) { return callback(err, null); } return callback(null, msg.flux); }, sqnNumber); }; /** * Requests the power level of the light * @param {Function} callback a function to accept the data */ Light.prototype.getPower = function(callback) { validate.callback(callback, 'light getPower method'); const packetObj = packet.create('getPower', {}, this.client.source); packetObj.target = this.id; const sqnNumber = this.client.send(packetObj); this.client.addMessageHandler('statePower', function(err, msg) { if (err) { return callback(err, null); } if (msg.level === 65535) { msg.level = 1; } return callback(null, msg.level); }, sqnNumber); }; /** * Requests the current color zone states from a light * @param {Number} startIndex start color zone index * @param {Number} [endIndex] end color zone index * @param {Function} callback a function to accept the data */ Light.prototype.getColorZones = function(startIndex, endIndex, callback) { validate.zoneIndex(startIndex, 'light getColorZones method'); validate.optionalZoneIndex(endIndex, 'light getColorZones method'); validate.optionalCallback(callback, 'light getColorZones method'); const packetObj = packet.create('getColorZones', {}, this.client.source); packetObj.target = this.id; packetObj.startIndex = startIndex; packetObj.endIndex = endIndex; const sqnNumber = this.client.send(packetObj); if (endIndex === undefined || startIndex === endIndex) { this.client.addMessageHandler('stateZone', function(err, msg) { if (err) { return callback(err, null); } // Convert HSB to readable format msg.color.hue = Math.round(msg.color.hue * (constants.HSBK_MAXIMUM_HUE / 65535)); msg.color.saturation = Math.round(msg.color.saturation * (constants.HSBK_MAXIMUM_SATURATION / 65535)); msg.color.brightness = Math.round(msg.color.brightness * (constants.HSBK_MAXIMUM_BRIGHTNESS / 65535)); callback(null, { count: msg.count, index: msg.index, color: msg.color }); }, sqnNumber); } else { this.client.addMessageHandler('stateMultiZone', function(err, msg) { if (err) { return callback(err, null); } // Convert HSB values to readable format msg.color.forEach(function(color) { color.hue = Math.round(color.hue * (constants.HSBK_MAXIMUM_HUE / 65535)); color.saturation = Math.round(color.saturation * (constants.HSBK_MAXIMUM_SATURATION / 65535)); color.brightness = Math.round(color.brightness * (constants.HSBK_MAXIMUM_BRIGHTNESS / 65535)); }); callback(null, { count: msg.count, index: msg.index, color: msg.color }); }, sqnNumber); } }; /** * Changes a color zone range to the given HSBK value * @param {Number} startIndex start zone index from 0 - 255 * @param {Number} endIndex start zone index from 0 - 255 * @param {Number} hue color hue from 0 - 360 (in °) * @param {Number} saturation color saturation from 0 - 100 (in %) * @param {Number} brightness color brightness from 0 - 100 (in %) * @param {Number} [kelvin=3500] color kelvin between 2500 and 9000 * @param {Number} [duration] transition time in milliseconds * @param {Boolean} [apply=true] apply changes immediately or leave pending for next apply * @param {Function} [callback] called when light did receive message */ Light.prototype.colorZones = function(startIndex, endIndex, hue, saturation, brightness, kelvin, duration, apply, callback) { validate.zoneIndex(startIndex, 'color zones method'); validate.zoneIndex(endIndex, 'color zones method'); validate.colorHsb(hue, saturation, brightness, 'color zones method'); validate.optionalKelvin(kelvin, 'color zones method'); validate.optionalDuration(duration, 'color zones method'); validate.optionalBoolean(apply, 'apply', 'color zones method'); validate.optionalCallback(callback, 'color zones method'); // Convert HSB values to packet format hue = Math.round(hue / constants.HSBK_MAXIMUM_HUE * 65535); saturation = Math.round(saturation / constants.HSBK_MAXIMUM_SATURATION * 65535); brightness = Math.round(brightness / constants.HSBK_MAXIMUM_BRIGHTNESS * 65535); const appReq = apply === false ? constants.APPLICATION_REQUEST_VALUES.NO_APPLY : constants.APPLICATION_REQUEST_VALUES.APPLY; const packetObj = packet.create('setColorZones', { startIndex: startIndex, endIndex: endIndex, hue: hue, saturation: saturation, brightness: brightness, kelvin: kelvin, duration: duration, apply: appReq }, this.client.source); packetObj.target = this.id; this.client.send(packetObj, callback); }; /** * Changes a color zone range to the given HSBK value * @param {String} effectName sets the desired effect, currently available options are: MOVE, OFF * @param {Number} speed sets duration of one cycle of the effect, the higher the value the slower the effect animation * @param {String} direction whether to animate from or towards the controller, available options are: TOWARDS, AWAY * @param {Function} [callback] called when light did receive message */ Light.prototype.setMultiZoneEffect = function(effectName, speed, direction, callback) { if (!includes(constants.MULTIZONE_EFFECTS, effectName)) { throw new TypeError('light.setMultiZoneEffect expects effectName to be one of "MOVE" or "OFF"'); } if (!isNumber(speed)) { throw new TypeError('light.setMultiZoneEffect expects speed to be a number'); } if (!includes(constants.MULTIZONE_EFFECTS_MOVE_DIRECTION, direction)) { throw new TypeError('light.setMultiZoneEffect expects direction to be one of "TOWARDS" or "AWAY"'); } validate.optionalCallback(callback, 'light.setMultiZoneEffect'); const packetObj = packet.create('setMultiZoneEffect', { effectType: constants.MULTIZONE_EFFECTS.indexOf(effectName), speed: speed, parameter2: constants.MULTIZONE_EFFECTS_MOVE_DIRECTION.indexOf(direction) }, this.client.source); packetObj.target = this.id; this.client.send(packetObj, callback); }; /** * Turns the relay off * @example light('192.168.2.130').relayOff(0) * @param {Number} [relayIndex] transition time in milliseconds * @param {Function} [callback] called when light did receive message */ Light.prototype.relayOff = function(relayIndex, callback) { validate.relayIndex(relayIndex, 'relay off method'); validate.optionalCallback(callback, 'relay off method'); const packetObj = packet.create('setRelayPower', {relayLevel: 0, relayIndex: relayIndex}, this.client.source); packetObj.target = this.id; this.client.send(packetObj, callback); }; /** * Turns the relay on * @example light('192.168.2.130').relayOn(0) * @param {Number} [relayIndex] transition time in milliseconds * @param {Function} [callback] called when light did receive message */ Light.prototype.relayOn = function(relayIndex, callback) { validate.relayIndex(relayIndex, 'relay on method'); validate.optionalCallback(callback, 'relay on method'); const packetObj = packet.create('setRelayPower', {relayLevel: 65535, relayIndex: relayIndex}, this.client.source); packetObj.target = this.id; this.client.send(packetObj, callback); }; /** * Requests the power level of the relay * @param {Number} relayIndex the index of the relay on the switch (0-3) * @param {Function} callback a function to accept the data */ Light.prototype.getRelayPower = function(relayIndex, callback) { validate.relayIndex(relayIndex, 'light on method'); validate.callback(callback, 'light getRelayPower method'); const packetObj = packet.create('getRelayPower', {relayIndex: relayIndex}, this.client.source); packetObj.target = this.id; const sqnNumber = this.client.send(packetObj); this.client.addMessageHandler('stateRelayPower', function(err, msg) { if (err) { return callback(err, null); } if (msg.relayLevel === 65535) { msg.relayLevel = 1; } return callback(null, msg.relayLevel); }, sqnNumber); }; /** * Does the light have relays (is it a switch) * @example light('192.168.2.130').hasRelays() * @param {Function} [callback] called when light did receive message */ Light.prototype.hasRelays = function(callback) { validate.callback(callback, 'light hasRelays method'); this.getHardwareVersion(function(error, data) { if (error) { return callback(null); } return callback(data && data.productFeatures && data.productFeatures.relays); }); }; exports.Light = Light;