From 77e7cffa4dd0ac7f8fe8530d4d4cb7360b54baea Mon Sep 17 00:00:00 2001 From: Ristomatti Airo Date: Sun, 29 Oct 2017 18:25:09 +0200 Subject: [PATCH 1/5] Add option to stop discovery if predefined lights found --- README.md | 6 ++++-- src/lifx/client.js | 48 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9cff1fe..27198cd 100644 --- a/README.md +++ b/README.md @@ -402,7 +402,9 @@ client.init({ debug: false, // logs all messages in console if turned on address: '0.0.0.0', // the IPv4 address to bind the udp connection to broadcast: '255.255.255.255', // set's the IPv4 broadcast address which is addressed to discover bulbs - lights: [] // Can be used provide a list of known light IPv4 ip addresses if broadcast packets in network are not allowed - // For example: ['192.168.0.112', '192.168.0.114'], this will then be addressed directly + lights: [], // Can be used provide a list of known light IPv4 ip addresses if broadcast packets in network are not allowed + // For example: ['192.168.0.112', '192.168.0.114'], this will then be addressed directly + stopAfterDiscovery: false // stops discovery process after discovering all known lights (requires list + // of addresses provided with "lights" setting) }); ``` diff --git a/src/lifx/client.js b/src/lifx/client.js index 759446d..45cdf35 100644 --- a/src/lifx/client.js +++ b/src/lifx/client.js @@ -3,7 +3,9 @@ const util = require('util'); const dgram = require('dgram'); const EventEmitter = require('eventemitter3'); -const {defaults, isArray, result, find, bind, forEach} = require('lodash'); +const { + defaults, isArray, isBoolean, isString, result, find, bind, forEach, keys, every, includes, filter +} = require('lodash'); const Packet = require('../lifx').packet; const {Light, constants, utils} = require('../lifx'); @@ -40,6 +42,9 @@ function Client() { this.resendMaxTimes = 5; this.source = utils.getRandomHexString(8); this.broadcastAddress = '255.255.255.255'; + this.lightAddresses = []; + this.stopAfterDiscovery = false; + this.discoveryCompleted = false; } util.inherits(Client, EventEmitter); @@ -58,6 +63,7 @@ util.inherits(Client, EventEmitter); * @param {String} [options.source] The source to send to light, must be 8 chars lowercase or digit * @param {Boolean} [options.startDiscovery] Weather to start discovery after initialization or not * @param {Array} [options.lights] Pre set list of ip addresses of known addressable lights + * @param {Boolean} [options.stopAfterDiscovery] Stop discovery after discovering known addressable lights defined with options.light * @param {String} [options.broadcast] The broadcast address to use for light discovery * @param {Number} [options.sendPort] The port to send messages to * @param {Function} [callback] Called after initialation @@ -72,6 +78,7 @@ Client.prototype.init = function(options, callback) { source: '', startDiscovery: true, lights: [], + stopAfterDiscovery: false, broadcast: '255.255.255.255', sendPort: constants.LIFX_DEFAULT_PORT, resendPacketDelay: 150, @@ -134,6 +141,13 @@ Client.prototype.init = function(options, callback) { throw new TypeError('LIFX Client lights option array element \'' + light + '\' is not expected IPv4 format'); } }); + this.lightAddresses = opts.lights; + + if (!isBoolean(opts.stopAfterDiscovery)) { + throw new TypeError('LIFX Client stopAfterDiscovery must be a boolean'); + } else { + this.stopAfterDiscovery = opts.stopAfterDiscovery; + } } if (opts.source !== '') { @@ -334,6 +348,14 @@ Client.prototype.startDiscovery = function(lights) { sendDiscoveryPacket(); }; +/** + * Checks if light discovery is in progress + * @return {Boolean} is discovery in progress + */ +Client.prototype.isDiscovering = function() { + return this.discoveryTimer !== null; +}; + /** * Checks all registered message handlers if they request the given message * @param {Object} msg message to check handler for @@ -418,6 +440,15 @@ Client.prototype.processDiscoveryPacket = function(err, msg, rinfo) { this.devices[msg.target].address = rinfo.address; this.devices[msg.target].seenOnDiscovery = this.discoveryPacketSequence; } + + // Check if discovery should be stopped + if (this.stopAfterDiscovery && !this.discoveryCompleted) { + if (this.predefinedDiscoveredAndOnline()) { + this.emit('discovery-completed'); + this.stopDiscovery(); + this.discoveryCompleted = true; + } + } } }; @@ -444,6 +475,21 @@ Client.prototype.stopDiscovery = function() { this.discoveryTimer = null; }; +/** + * Checks if all predefined lights are discovered and online + * @return {Boolean} are lights discovered and online + */ +Client.prototype.predefinedDiscoveredAndOnline = function() { + const predefinedDevices = filter(this.devices, (device) => includes(this.lightAddresses, device.address)); + + const numDiscovered = keys(this.devices).length; + const allDiscovered = numDiscovered >= this.lightAddresses.length; + const allOnline = every(predefinedDevices, (device) => device.status === 'on'); + const labelsReceived = every(predefinedDevices, (device) => isString(device.label)); + + return allDiscovered && allOnline && labelsReceived; +}; + /** * Send a LIFX message objects over the network * @param {Object} msg A message object or multiple with data to send From 18c09d8d834291cb1d6f665ec421ab6d22103ace Mon Sep 17 00:00:00 2001 From: Ristomatti Airo Date: Sun, 29 Oct 2017 20:05:28 +0200 Subject: [PATCH 2/5] Add assertions for missing client options --- test/unit/client-test.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/unit/client-test.js b/test/unit/client-test.js index 33a352f..8f4d486 100644 --- a/test/unit/client-test.js +++ b/test/unit/client-test.js @@ -56,7 +56,8 @@ suite('Client', () => { resendMaxTimes: 2, lights: ['192.168.0.100'], broadcast: '192.168.0.255', - sendPort: 65534 + sendPort: 65534, + stopAfterDiscovery: true }, () => { assert.equal(client.address().address, '127.0.0.1'); assert.equal(client.address().port, 65535); @@ -67,6 +68,8 @@ suite('Client', () => { assert.equal(client.resendMaxTimes, 2); assert.equal(client.broadcastAddress, '192.168.0.255'); assert.equal(client.sendPort, 65534); + assert.equal(client.stopAfterDiscovery, true); + assert.deepEqual(client.lightAddresses, ['192.168.0.100']); done(); }); }); @@ -120,6 +123,10 @@ suite('Client', () => { client.init({lights: '192.168.0.100'}); }, TypeError); + assert.throw(() => { + client.init({lights: ['192.168.0.100'], stopAfterDiscovery: 'false'}); + }, TypeError); + assert.throw(() => { client.init({lights: ['::1']}); }, TypeError); From 2c24457bc3560bd135a690cbf0b927c8ff82a4d3 Mon Sep 17 00:00:00 2001 From: Ristomatti Airo Date: Sun, 29 Oct 2017 20:16:34 +0200 Subject: [PATCH 3/5] Refactor client-test light props --- test/unit/client-test.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/test/unit/client-test.js b/test/unit/client-test.js index 8f4d486..e4a5619 100644 --- a/test/unit/client-test.js +++ b/test/unit/client-test.js @@ -17,15 +17,17 @@ suite('Client', () => { return Object.keys(client.devices).length; }; + const lightProps = { + client: client, + id: 'f37a4311b857', + address: '192.168.0.1', + port: constants.LIFX_DEFAULT_PORT, + seenOnDiscovery: 0 + }; + beforeEach(() => { client = new Client(); - client.devices.f37a4311b857 = new Light({ - client: client, - id: 'f37a4311b857', - address: '192.168.0.1', - port: constants.LIFX_DEFAULT_PORT, - seenOnDiscovery: 0 - }); + client.devices.f37a4311b857 = new Light(lightProps); }); afterEach(() => { @@ -312,7 +314,7 @@ suite('Client', () => { result = client.light('living room'); assert.isFalse(result, 'case sensitive search'); - result = client.light('192.168.0.1'); + result = client.light(lightProps.address); assert.isFalse(result); result = client.light('7812e9zonvwouv8754179410ufsknsuvsif724581419713947'); @@ -339,11 +341,11 @@ suite('Client', () => { assert.property(client.messagesQueue[0], 'data', 'has data'); assert.notProperty(client.messagesQueue[0], 'address', 'broadcast has no target address'); - client.send(packet.create('setPower', {level: 65535, duration: 0, target: 'f37a4311b857'}, '12345678')); + client.send(packet.create('setPower', {level: 65535, duration: 0, target: lightProps.id}, '12345678')); assert.equal(client.sequenceNumber, 1, 'sequence increased after specific targeting'); client.sequenceNumber = constants.PACKET_HEADER_SEQUENCE_MAX; - client.send(packet.create('setPower', {level: 65535, duration: 0, target: 'f37a4311b857'}, '12345678')); + client.send(packet.create('setPower', {level: 65535, duration: 0, target: lightProps.id}, '12345678')); assert.equal(client.sequenceNumber, 0, 'sequence starts over after maximum'); done(); }); From e2597645d61c66e23592ab5021a1ac913ffbcefc Mon Sep 17 00:00:00 2001 From: Ristomatti Airo Date: Sun, 29 Oct 2017 21:19:28 +0200 Subject: [PATCH 4/5] Add Sinon dev dependency --- package-lock.json | 93 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 1d8b6cb..a86ae26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2020,6 +2020,15 @@ "mime": "1.2.11" } }, + "formatio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", + "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=", + "dev": true, + "requires": { + "samsam": "1.3.0" + } + }, "fs-readdir-recursive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz", @@ -2619,6 +2628,12 @@ "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", "dev": true }, + "just-extend": { + "version": "1.1.26", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.26.tgz", + "integrity": "sha512-IIG0FXHB/XpUZ7vGbktoc2EGsF+fLHJ1tU+vaqoKkVRBwH2FDxLTmkGkSp0XHRp6Y3KGZPIldH1YW8lOluGYrA==", + "dev": true + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -2695,6 +2710,12 @@ "lodash._isiterateecall": "3.0.9" } }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -2877,6 +2898,19 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "nise": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.2.0.tgz", + "integrity": "sha512-q9jXh3UNsMV28KeqI43ILz5+c3l+RiNW8mhurEwCKckuHQbL+hTJIKKTiUlCPKlgQ/OukFvSnKB/Jk3+sFbkGA==", + "dev": true, + "requires": { + "formatio": "1.2.0", + "just-extend": "1.1.26", + "lolex": "1.6.0", + "path-to-regexp": "1.7.0", + "text-encoding": "0.6.4" + } + }, "node-uuid": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", @@ -3029,6 +3063,15 @@ "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", "dev": true }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + }, "pathval": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", @@ -3424,6 +3467,12 @@ "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", "dev": true }, + "samsam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", + "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", + "dev": true + }, "semver": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", @@ -3458,6 +3507,44 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "sinon": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.0.2.tgz", + "integrity": "sha512-4mUsjHfjrHyPFGDTtNJl0q8cv4VOJGvQykI1r3fnn05ys0sQL9M1Y+DyyGNWLD2PMcoyqjJ/nFDm4K54V1eQOg==", + "dev": true, + "requires": { + "diff": "3.2.0", + "formatio": "1.2.0", + "lodash.get": "4.4.2", + "lolex": "2.1.3", + "nise": "1.2.0", + "supports-color": "4.5.0", + "type-detect": "4.0.3" + }, + "dependencies": { + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "lolex": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.1.3.tgz", + "integrity": "sha512-BdHq78SeI+6PAUtl4atDuCt7L6E4fab3mSRtqxm4ywaXe4uP7jZ0TTcFNuU20syUjxZc2l7jFqKVMJ+AX0LnpQ==", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, "slash": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", @@ -3650,6 +3737,12 @@ "through": "2.3.8" } }, + "text-encoding": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", + "dev": true + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index 1fc6984..e467c0f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "lolex": "^1.6.0", "mkdirp": "^0.5.1", "mocha": "^3.5.3", - "rimraf": "^2.6.2" + "rimraf": "^2.6.2", + "sinon": "^4.0.2" }, "directories": { "test": "test", From 62a40521aa5364ff1d393b3d6340e50963ea932e Mon Sep 17 00:00:00 2001 From: Ristomatti Airo Date: Sun, 29 Oct 2017 21:20:53 +0200 Subject: [PATCH 5/5] Add unit test for stopAfterDiscovery client option --- test/unit/client-test.js | 47 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/unit/client-test.js b/test/unit/client-test.js index e4a5619..1d3f476 100644 --- a/test/unit/client-test.js +++ b/test/unit/client-test.js @@ -6,6 +6,7 @@ const packet = require('../../').packet; const constants = require('../../').constants; const assert = require('chai').assert; const lolex = require('lolex'); +const sinon = require('sinon'); suite('Client', () => { let client; @@ -637,5 +638,51 @@ suite('Client', () => { currMsgQueCnt -= 1; }); }); + + test('stops discovery after predefined lights found when stopAfterDiscovery is true', (done) => { + const discoveryMessage = { + size: 41, + addressable: true, + tagged: false, + origin: true, + protocolVersion: 1024, + source: '0c583dd9', + target: 'd073d5006d72', + site: 'LIFXV2', + ackRequired: false, + resRequired: false, + sequence: 0, + type: 'stateService', + service: 'udp', + port: 56700 + }; + const discoveryInfo = { + address: '192.168.2.108', + family: 'IPv4', + port: 56700, + size: 41 + }; + const discoveryMessage2 = Object.assign({}, discoveryMessage, {sequence: 1}); + const discoveryInfo2 = Object.assign({}, discoveryInfo, {address: '192.168.2.200'}); + const labelPacket = { + target: 'd073d5006d72', + label: 'test' + }; + const discoveryCompletedCallback = sinon.spy(); + + client.on('discovery-completed', discoveryCompletedCallback); + client.init({ + startDiscovery: false, + lights: ['192.168.2.108'], + stopAfterDiscovery: true + }, () => { + client.processDiscoveryPacket(null, discoveryMessage, discoveryInfo); + client.processLabelPacket(null, labelPacket); + client.processDiscoveryPacket(null, discoveryMessage2, discoveryInfo2); + + assert.isTrue((discoveryCompletedCallback.called)); + done(); + }); + }); }); });