diff --git a/README.md b/README.md index ecc8744..6bdf860 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,12 @@ null, vendorName: 'LIFX', productId: 1, productName: 'Original 1000', - version: 6 + version: 6, + productFeatures: { + color: true, + infrared: false, + multizone: false + } } ``` diff --git a/example/interactive-cli.js b/example/interactive-cli.js index d371a04..baa87b9 100644 --- a/example/interactive-cli.js +++ b/example/interactive-cli.js @@ -13,7 +13,15 @@ client.on('light-new', function(light) { } console.log('Label: ' + info.label); console.log('Power:', (info.power === 1) ? 'on' : 'off'); - console.log('Color:', info.color, '\n'); + console.log('Color:', info.color); + }); + + light.getHardwareVersion(function(err, info) { + if (err) { + console.log(err); + } + console.log('Device Info: ' + info.vendorName + ' - ' + info.productName); + console.log('Features: ', info.productFeatures, '\n'); }); }); diff --git a/lib/lifx/constants.js b/lib/lifx/constants.js index d50094f..0535895 100644 --- a/lib/lifx/constants.js +++ b/lib/lifx/constants.js @@ -41,27 +41,9 @@ module.exports = { RGB_MAXIMUM_VALUE: 255, RGB_MINIMUM_VALUE: 0, - // Vendor ID values - LIFX_VENDOR_IDS: [ - {id: 1, name: 'LIFX'} - ], - - // Product ID values - LIFX_PRODUCT_IDS: [ - {id: 1, name: 'Original 1000'}, - {id: 3, name: 'Color 650'}, - {id: 10, name: 'White 800 (Low Voltage)'}, - {id: 11, name: 'White 800 (High Voltage)'}, - {id: 18, name: 'White 900 BR30 (Low Voltage)'}, - {id: 19, name: 'White 900 BR30 (High Voltage)'}, - {id: 20, name: 'Color 1000 BR30'}, - {id: 22, name: 'Color 1000'}, - {id: 27, name: 'LIFX A19'}, - {id: 28, name: 'LIFX BR30'}, - {id: 29, name: 'LIFX+ A19'}, - {id: 30, name: 'LIFX+ BR30'}, - {id: 31, name: 'LIFX Z'} - ], + // Infrared values + IR_MINIMUM_BRIGHTNESS: 0, + IR_MAXIMUM_BRIGHTNESS: 100, // Waveform values, order is important here LIGHT_WAVEFORMS: [ diff --git a/lib/lifx/light.js b/lib/lifx/light.js index b31e555..429aa6d 100644 --- a/lib/lifx/light.js +++ b/lib/lifx/light.js @@ -180,6 +180,30 @@ Light.prototype.colorRgbHex = function(hexString, duration, callback) { 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) { + if (typeof brightness !== 'number' || brightness < constants.IR_MINIMUM_BRIGHTNESS || brightness > constants.IR_MAXIMUM_BRIGHTNESS) { + throw new RangeError('LIFX light setMaxIR method expects brightness to be a number between ' + + constants.IR_MINIMUM_BRIGHTNESS + ' and ' + constants.IR_MAXIMUM_BRIGHTNESS + ); + } + 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'); + } + + var packetObj = packet.create('setInfrared', { + brightness: brightness + }, 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 @@ -211,6 +235,28 @@ Light.prototype.getState = function(callback) { }, sqnNumber); }; +/** + * Requests the current maximum setting for the infrared channel + * @param {Function} callback a function to accept the data + */ +Light.prototype.getMaxIR = function(callback) { + if (typeof callback !== 'function') { + throw new TypeError('LIFX light getMaxIR method expects callback to be a function'); + } + var packetObj = packet.create('getInfrared', {}, this.client.source); + packetObj.target = this.id; + var 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 @@ -227,13 +273,15 @@ Light.prototype.getHardwareVersion = function(callback) { if (err) { return callback(err, null); } - callback(null, _.pick(msg, [ + var versionInfo = _.pick(msg, [ 'vendorId', - 'vendorName', 'productId', - 'productName', 'version' - ])); + ]); + callback(null, _.assign( + versionInfo, + utils.getHardwareDetails(versionInfo.vendorId, versionInfo.productId) + )); }, sqnNumber); }; diff --git a/lib/lifx/packet.js b/lib/lifx/packet.js index b46101c..070d405 100644 --- a/lib/lifx/packet.js +++ b/lib/lifx/packet.js @@ -69,6 +69,9 @@ Packet.typeList = [ {id: 117, name: 'setPower'}, {id: 118, name: 'statePower'}, // {id: 119, name: 'setWaveformOptional'}, + {id: 120, name: 'getInfrared'}, + {id: 121, name: 'stateInfrared'}, + {id: 122, name: 'setInfrared'}, {id: 401, name: 'getAmbientLight'}, {id: 402, name: 'stateAmbientLight'} // {id: 403, name: 'getDimmerVoltage'}, diff --git a/lib/lifx/packets/getInfrared.js b/lib/lifx/packets/getInfrared.js new file mode 100644 index 0000000..574e163 --- /dev/null +++ b/lib/lifx/packets/getInfrared.js @@ -0,0 +1,7 @@ +'use strict'; + +var Packet = { + size: 0 +}; + +module.exports = Packet; diff --git a/lib/lifx/packets/index.js b/lib/lifx/packets/index.js index de4cd5c..eb48f42 100644 --- a/lib/lifx/packets/index.js +++ b/lib/lifx/packets/index.js @@ -55,6 +55,10 @@ packets.setWaveform = require('./setWaveform'); packets.getTemperature = require('./getTemperature'); packets.stateTemperature = require('./stateTemperature'); +packets.getInfrared = require('./getInfrared'); +packets.setInfrared = require('./setInfrared'); +packets.stateInfrared = require('./stateInfrared'); + /* * Sensor related packages */ diff --git a/lib/lifx/packets/setInfrared.js b/lib/lifx/packets/setInfrared.js new file mode 100644 index 0000000..90fe5ed --- /dev/null +++ b/lib/lifx/packets/setInfrared.js @@ -0,0 +1,46 @@ +'use strict'; + +var Packet = { + size: 2 +}; + +/** + * Converts packet specific data from a buffer to an object + * @param {Buffer} buf Buffer containing only packet specific data no header + * @return {Object} Information contained in packet + */ +Packet.toObject = function(buf) { + var obj = {}; + var offset = 0; + + if (buf.length !== this.size) { + throw new Error('Invalid length given for setInfrared LIFX packet'); + } + + obj.brightness = buf.readUInt16LE(offset); + offset += 2; + + return obj; +}; + +/** + * Converts the given packet specific object into a packet + * @param {Object} obj object with configuration data + * @param {Number} obj.brightness between 0 and 65535 + * @return {Buffer} packet + */ +Packet.toBuffer = function(obj) { + var buf = new Buffer(this.size); + buf.fill(0); + var offset = 0; + + if (typeof obj.brightness !== 'number' && obj.brightness < 0 && obj.brightness > 65535) { + throw new RangeError('Invalid brightness given for setInfrared LIFX packet, must be a number between 0 and 65535'); + } + buf.writeUInt16LE(obj.brightness, offset); + offset += 2; + + return buf; +}; + +module.exports = Packet; diff --git a/lib/lifx/packets/stateInfrared.js b/lib/lifx/packets/stateInfrared.js new file mode 100644 index 0000000..489a805 --- /dev/null +++ b/lib/lifx/packets/stateInfrared.js @@ -0,0 +1,42 @@ +'use strict'; + +var Packet = { + size: 2 +}; + +/** + * Converts packet specific data from a buffer to an object + * @param {Buffer} buf Buffer containing only packet specific data no header + * @return {Object} Information contained in packet + */ +Packet.toObject = function(buf) { + var obj = {}; + var offset = 0; + + if (buf.length !== this.size) { + throw new Error('Invalid length given for stateInfrared LIFX packet'); + } + + obj.brightness = buf.readUInt16LE(offset); + offset += 2; + + return obj; +}; + +/** + * Converts the given packet specific object into a packet + * @param {Object} obj object with configuration data + * @return {Buffer} packet + */ +Packet.toBuffer = function(obj) { + var buf = new Buffer(this.size); + buf.fill(0); + var offset = 0; + + buf.writeUInt16LE(obj.brightness, offset); + offset += 2; + + return buf; +}; + +module.exports = Packet; diff --git a/lib/lifx/packets/stateVersion.js b/lib/lifx/packets/stateVersion.js index 82d474f..281879c 100644 --- a/lib/lifx/packets/stateVersion.js +++ b/lib/lifx/packets/stateVersion.js @@ -29,10 +29,6 @@ Packet.toObject = function(buf) { offset += 4; obj.productId = buf.readUInt32LE(offset); - var product = _.find(constants.LIFX_PRODUCT_IDS, {id: obj.productId}); - if (product !== undefined) { - obj.productName = product.name; - } offset += 4; obj.version = buf.readUInt32LE(offset); diff --git a/lib/lifx/products.json b/lib/lifx/products.json new file mode 100644 index 0000000..a9f7ce6 --- /dev/null +++ b/lib/lifx/products.json @@ -0,0 +1,116 @@ +[ + { + "vid": 1, + "name": "LIFX", + "products": [ + { + "pid": 1, + "name": "Original 1000", + "features": { + "color": true, + "infrared": false, + "multizone": false + } + }, + { + "pid": 3, + "name": "Color 650", + "features": { + "color": true, + "infrared": false, + "multizone": false + } + }, + { + "pid": 10, + "name": "White 800 (Low Voltage)", + "features": { + "color": false, + "infrared": false, + "multizone": false + } + }, + { + "pid": 11, + "name": "White 800 (High Voltage)", + "features": { + "color": false, + "infrared": false, + "multizone": false + } + }, + { + "pid": 18, + "name": "White 900 BR30 (Low Voltage)", + "features": { + "color": false, + "infrared": false, + "multizone": false + } + }, + { + "pid": 20, + "name": "Color 1000 BR30", + "features": { + "color": true, + "infrared": false, + "multizone": false + } + }, + { + "pid": 22, + "name": "Color 1000", + "features": { + "color": true, + "infrared": false, + "multizone": false + } + }, + { + "pid": 27, + "name": "LIFX A19", + "features": { + "color": true, + "infrared": false, + "multizone": false + } + }, + { + "pid": 28, + "name": "LIFX BR30", + "features": { + "color": true, + "infrared": false, + "multizone": false + } + }, + { + "pid": 29, + "name": "LIFX+ A19", + "features": { + "color": true, + "infrared": true, + "multizone": false + } + }, + { + "pid": 30, + "name": "LIFX+ BR30", + "features": { + "color": true, + "infrared": true, + "multizone": false + } + }, + { + "pid": 31, + "name": "LIFX Z", + "features": { + "color": true, + "infrared": false, + "multizone": true + } + } + ] + } +] diff --git a/lib/lifx/utils.js b/lib/lifx/utils.js index 3bdb055..55ea7f1 100644 --- a/lib/lifx/utils.js +++ b/lib/lifx/utils.js @@ -2,6 +2,7 @@ var os = require('os'); var constants = require('../lifx').constants; +var productDetailList = require('./products.json'); var utils = exports; /** @@ -188,3 +189,28 @@ utils.rgbToHsb = function(rgbObj) { return hsb; }; + +/** + * Get's product and vendor details for the given id's + * hsb integer object + * @param {Number} vendorId id of the vendor + * @param {Number} productId id of the product + * @return {Object|Boolean} product and details vendor details or false if not found + */ +utils.getHardwareDetails = function(vendorId, productId) { + for (var i = 0; i < productDetailList.length; i += 1) { + if (productDetailList[i].vid === vendorId) { + for (var j = 0; j < productDetailList[i].products.length; j += 1) { + if (productDetailList[i].products[j].pid === productId) { + return { + vendorName: productDetailList[i].name, + productName: productDetailList[i].products[j].name, + productFeatures: productDetailList[i].products[j].features + }; + } + } + } + } + + return false; +}; diff --git a/test/unit/light-test.js b/test/unit/light-test.js index db2b760..95151e8 100644 --- a/test/unit/light-test.js +++ b/test/unit/light-test.js @@ -253,6 +253,42 @@ suite('Light', () => { currHandlerCnt += 1; }); + test('changing infrared maximum brightness', () => { + let currMsgQueCnt = getMsgQueueLength(); + let currHandlerCnt = getMsgHandlerLength(); + + // Error cases + assert.throw(() => { + // No arguments + bulb.maxIR(); + }, RangeError); + + assert.throw(() => { + // Brightness too low + bulb.maxIR(constant.IR_MINIMUM_BRIGHTNESS - 1); + }, RangeError); + + assert.throw(() => { + // Brightness too high + bulb.maxIR(constant.IR_MAXIMUM_BRIGHTNESS + 1); + }, RangeError); + + assert.throw(() => { + // Invalid callback + bulb.maxIR(constant.IR_MAXIMUM_BRIGHTNESS, 'someValue'); + }, TypeError); + + bulb.maxIR(50); + assert.equal(getMsgQueueLength(), currMsgQueCnt + 1, 'sends a packet to the queue'); + currMsgQueCnt += 1; + + bulb.maxIR(50, () => {}); + assert.equal(getMsgQueueLength(), currMsgQueCnt + 1, 'sends a packet to the queue'); + currMsgQueCnt += 1; + assert.equal(getMsgHandlerLength(), currHandlerCnt + 1, 'adds a handler'); + currHandlerCnt += 1; + }); + test('getting light summary', () => { assert.throw(() => { bulb.getState('test'); @@ -409,4 +445,15 @@ suite('Light', () => { assert.equal(getMsgHandlerLength(), currHandlerCnt + 1, 'adds a handler'); currHandlerCnt += 1; }); + + test('getting infrared', () => { + assert.throw(() => { + bulb.getMaxIR('someValue'); + }, TypeError); + + let currHandlerCnt = getMsgHandlerLength(); + bulb.getMaxIR(() => {}); + assert.equal(getMsgHandlerLength(), currHandlerCnt + 1, 'adds a handler'); + currHandlerCnt += 1; + }); }); diff --git a/test/unit/utils-test.js b/test/unit/utils-test.js index cdf3690..5d8ccb1 100644 --- a/test/unit/utils-test.js +++ b/test/unit/utils-test.js @@ -104,4 +104,35 @@ suite('Utils', () => { rgbObj = {r: 146, g: 108, b: 83}; assert.deepEqual(utils.rgbToHsb(rgbObj), {h: 24, s: 43, b: 57}); }); + + test('get hardware info', () => { + const vendorId = 1; + let hardwareId; + + hardwareId = 1; + assert.deepEqual(utils.getHardwareDetails(vendorId, hardwareId), { + vendorName: 'LIFX', + productName: 'Original 1000', + productFeatures: { + color: true, + infrared: false, + multizone: false + } + }); + + hardwareId = 10; + assert.deepEqual(utils.getHardwareDetails(vendorId, hardwareId), { + vendorName: 'LIFX', + productName: 'White 800 (Low Voltage)', + productFeatures: { + color: false, + infrared: false, + multizone: false + } + }); + + // Product and Vendor IDs start with 1 + assert.equal(utils.getHardwareDetails(0, 1), false); + assert.equal(utils.getHardwareDetails(1, 0), false); + }); });