From c36dbfa1f6039eaf3c6e4a7551b757f9f97af35b Mon Sep 17 00:00:00 2001 From: Ristomatti Airo Date: Sun, 29 Oct 2017 23:46:39 +0200 Subject: [PATCH 1/7] Add Babel plugin transform-class-properties --- .babelrc | 1 + package-lock.json | 107 ++++++++++++++++++++++++++++++++++++++-------- package.json | 1 + 3 files changed, 92 insertions(+), 17 deletions(-) diff --git a/.babelrc b/.babelrc index 7ead2dd..886d1ab 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,5 @@ { + "plugins": ["transform-class-properties"], "presets": [ ["env", { "targets": { diff --git a/package-lock.json b/package-lock.json index bd86d5c..6b05e65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -705,6 +705,12 @@ "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", "dev": true }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", + "dev": true + }, "babel-plugin-syntax-exponentiation-operator": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", @@ -728,6 +734,43 @@ "babel-runtime": "^6.22.0" } }, + "babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-plugin-syntax-class-properties": "^6.8.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + }, + "dependencies": { + "babel-runtime": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.23.0.tgz", + "integrity": "sha1-CpSJ8UTecO+zzkMArM2zKeL8VDs=", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.10.0" + } + }, + "babel-template": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.24.1.tgz", + "integrity": "sha1-BK5RTx+Ts6JTfyoPYKWkX7gwgzM=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1", + "babylon": "^6.11.0", + "lodash": "^4.2.0" + } + } + } + }, "babel-plugin-transform-es2015-arrow-functions": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", @@ -1224,6 +1267,7 @@ "resolved": "https://registry.npmjs.org/boom/-/boom-0.4.2.tgz", "integrity": "sha1-emNune1O/O+xnO9JR6PGffrukRs=", "dev": true, + "optional": true, "requires": { "hoek": "0.9.x" } @@ -2140,7 +2184,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2161,12 +2206,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2181,17 +2228,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -2308,7 +2358,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2320,6 +2371,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2334,6 +2386,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2341,12 +2394,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2365,6 +2420,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2445,7 +2501,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2457,6 +2514,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2542,7 +2600,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -2578,6 +2637,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2597,6 +2657,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2640,12 +2701,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -2712,6 +2775,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", "dev": true, + "optional": true, "requires": { "is-glob": "^2.0.0" } @@ -2825,7 +2889,8 @@ "version": "0.9.1", "resolved": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz", "integrity": "sha1-PTIkYrrfB3Fup+uFuviAec3c5QU=", - "dev": true + "dev": true, + "optional": true }, "home-or-tmp": { "version": "2.0.0", @@ -3005,7 +3070,8 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", - "dev": true + "dev": true, + "optional": true }, "is-callable": { "version": "1.1.4", @@ -3047,7 +3113,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "dev": true + "dev": true, + "optional": true }, "is-finite": { "version": "1.0.2", @@ -3069,6 +3136,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "dev": true, + "optional": true, "requires": { "is-extglob": "^1.0.0" } @@ -3293,6 +3361,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -3690,6 +3759,7 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dev": true, + "optional": true, "requires": { "remove-trailing-separator": "^1.0.1" } @@ -4073,7 +4143,8 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true + "dev": true, + "optional": true }, "string_decoder": { "version": "1.0.3", @@ -4164,13 +4235,15 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true + "dev": true, + "optional": true }, "repeat-element": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", - "dev": true + "dev": true, + "optional": true }, "repeat-string": { "version": "1.6.1", diff --git a/package.json b/package.json index 2661b7d..baf9af0 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "babel-cli": "^6.26.0", "babel-core": "^6.26.3", "babel-eslint": "^10.0.3", + "babel-plugin-transform-class-properties": "^6.24.1", "babel-preset-env": "^1.7.0", "chai": "^4.2.0", "codecov.io": "^0.1.6", From 325821f5a9af71204f3dcda87ac584aa519f5b7a Mon Sep 17 00:00:00 2001 From: Ristomatti Airo Date: Sat, 5 Oct 2019 19:04:31 +0300 Subject: [PATCH 2/7] Refactor Packet into a class --- src/lifx.js | 2 +- src/lifx/packet.js | 576 +++++++++++++-------------- test/unit/client-test.js | 14 +- test/unit/packet-test.js | 2 +- test/unit/packets/getService-test.js | 2 +- test/unit/packets/setColor-test.js | 2 +- test/unit/packets/setPower-test.js | 2 +- 7 files changed, 300 insertions(+), 300 deletions(-) diff --git a/src/lifx.js b/src/lifx.js index ff0857b..f36e381 100644 --- a/src/lifx.js +++ b/src/lifx.js @@ -10,7 +10,7 @@ lifx.validate = require('./lifx/validate'); lifx.utils = require('./lifx/utils'); // Export packet parser -lifx.packet = require('./lifx/packet'); +lifx.Packet = require('./lifx/packet'); // Export light device object lifx.Light = require('./lifx/light').Light; diff --git a/src/lifx/packet.js b/src/lifx/packet.js index 090ef05..5b8516b 100644 --- a/src/lifx/packet.js +++ b/src/lifx/packet.js @@ -20,328 +20,328 @@ const {result, find, extend, assign} = require('lodash'); type - 2 bit 00 00 */ -const Packet = {}; - -/** - * Mapping for types - * @type {Array} - */ -Packet.typeList = [ - {id: 2, name: 'getService'}, - {id: 3, name: 'stateService'}, - {id: 12, name: 'getHostInfo'}, - {id: 13, name: 'stateHostInfo'}, - {id: 14, name: 'getHostFirmware'}, - {id: 15, name: 'stateHostFirmware'}, - {id: 16, name: 'getWifiInfo'}, - {id: 17, name: 'stateWifiInfo'}, - {id: 18, name: 'getWifiFirmware'}, - {id: 19, name: 'stateWifiFirmware'}, - // {id: 20, name: 'getPower'}, // These are for device level - // {id: 21, name: 'setPower'}, // and do not support duration value - // {id: 22, name: 'statePower'}, // since that we don't use them - {id: 23, name: 'getLabel'}, - {id: 24, name: 'setLabel'}, - {id: 25, name: 'stateLabel'}, - {id: 32, name: 'getVersion'}, - {id: 33, name: 'stateVersion'}, - {id: 34, name: 'getInfo'}, - {id: 35, name: 'stateInfo'}, - {id: 38, name: 'rebootRequest'}, - {id: 43, name: 'rebootResponse'}, - {id: 45, name: 'acknowledgement'}, - {id: 48, name: 'getLocation'}, - {id: 50, name: 'stateLocation'}, - {id: 51, name: 'getGroup'}, - {id: 53, name: 'stateGroup'}, - {id: 54, name: 'getOwner'}, - {id: 56, name: 'stateOwner'}, - {id: 58, name: 'echoRequest'}, - {id: 59, name: 'echoResponse'}, - // {id: 60, name: 'getStatistic'}, - // {id: 61, name: 'stateStatistic'}, - {id: 101, name: 'getLight'}, - {id: 102, name: 'setColor'}, - {id: 103, name: 'setWaveform'}, - {id: 107, name: 'stateLight'}, - {id: 110, name: 'getTemperature'}, - {id: 111, name: 'stateTemperature'}, - // {id: 113, name: 'setSimpleEvent'}, - // {id: 114, name: 'getSimpleEvent'}, - // {id: 115, name: 'stateSimpleEvent'}, - {id: 116, name: 'getPower'}, - {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'}, - // {id: 404, name: 'stateDimmerVoltage'}, - {id: 501, name: 'setColorZones'}, - {id: 502, name: 'getColorZones'}, - {id: 503, name: 'stateZone'}, - {id: 504, name: 'getCountZone'}, - {id: 505, name: 'stateCountZone'}, - {id: 506, name: 'stateMultiZone'} - // {id: 507, name: 'getEffectZone'}, - // {id: 508, name: 'setEffectZone'}, - // {id: 509, name: 'stateEffectZone'} -]; - -/** - * Parses a lifx packet header - * @param {Buffer} buf Buffer containg lifx packet including header - * @return {Object} parsed packet header - */ -Packet.headerToObject = function(buf) { - const obj = {}; - let offset = 0; - - // Frame - obj.size = buf.readUInt16LE(offset); - offset += 2; - - const frameDescription = buf.readUInt16LE(offset); - obj.addressable = (frameDescription & constants.ADDRESSABLE_BIT) !== 0; - obj.tagged = (frameDescription & constants.TAGGED_BIT) !== 0; - obj.origin = ((frameDescription & constants.ORIGIN_BITS) >> 14) !== 0; - obj.protocolVersion = (frameDescription & constants.PROTOCOL_VERSION_BITS); - offset += 2; - - obj.source = buf.toString('hex', offset, offset + 4); - offset += 4; - - // Frame address - obj.target = buf.toString('hex', offset, offset + 6); - offset += 6; +class Packet { + /** + * Mapping for types + * @type {Array} + */ + static typeList = [ + {id: 2, name: 'getService'}, + {id: 3, name: 'stateService'}, + {id: 12, name: 'getHostInfo'}, + {id: 13, name: 'stateHostInfo'}, + {id: 14, name: 'getHostFirmware'}, + {id: 15, name: 'stateHostFirmware'}, + {id: 16, name: 'getWifiInfo'}, + {id: 17, name: 'stateWifiInfo'}, + {id: 18, name: 'getWifiFirmware'}, + {id: 19, name: 'stateWifiFirmware'}, + // {id: 20, name: 'getPower'}, // These are for device level + // {id: 21, name: 'setPower'}, // and do not support duration value + // {id: 22, name: 'statePower'}, // since that we don't use them + {id: 23, name: 'getLabel'}, + {id: 24, name: 'setLabel'}, + {id: 25, name: 'stateLabel'}, + {id: 32, name: 'getVersion'}, + {id: 33, name: 'stateVersion'}, + {id: 34, name: 'getInfo'}, + {id: 35, name: 'stateInfo'}, + {id: 38, name: 'rebootRequest'}, + {id: 43, name: 'rebootResponse'}, + {id: 45, name: 'acknowledgement'}, + {id: 48, name: 'getLocation'}, + {id: 50, name: 'stateLocation'}, + {id: 51, name: 'getGroup'}, + {id: 53, name: 'stateGroup'}, + {id: 54, name: 'getOwner'}, + {id: 56, name: 'stateOwner'}, + {id: 58, name: 'echoRequest'}, + {id: 59, name: 'echoResponse'}, + // {id: 60, name: 'getStatistic'}, + // {id: 61, name: 'stateStatistic'}, + {id: 101, name: 'getLight'}, + {id: 102, name: 'setColor'}, + {id: 103, name: 'setWaveform'}, + {id: 107, name: 'stateLight'}, + {id: 110, name: 'getTemperature'}, + {id: 111, name: 'stateTemperature'}, + // {id: 113, name: 'setSimpleEvent'}, + // {id: 114, name: 'getSimpleEvent'}, + // {id: 115, name: 'stateSimpleEvent'}, + {id: 116, name: 'getPower'}, + {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'}, + // {id: 404, name: 'stateDimmerVoltage'}, + {id: 501, name: 'setColorZones'}, + {id: 502, name: 'getColorZones'}, + {id: 503, name: 'stateZone'}, + {id: 504, name: 'getCountZone'}, + {id: 505, name: 'stateCountZone'}, + {id: 506, name: 'stateMultiZone'} + // {id: 507, name: 'getEffectZone'}, + // {id: 508, name: 'setEffectZone'}, + // {id: 509, name: 'stateEffectZone'} + ]; + + /** + * Parses a lifx packet header + * @param {Buffer} buf Buffer containg lifx packet including header + * @return {Object} parsed packet header + */ + static headerToObject(buf) { + const obj = {}; + let offset = 0; + + // Frame + obj.size = buf.readUInt16LE(offset); + offset += 2; + + const frameDescription = buf.readUInt16LE(offset); + obj.addressable = (frameDescription & constants.ADDRESSABLE_BIT) !== 0; + obj.tagged = (frameDescription & constants.TAGGED_BIT) !== 0; + obj.origin = ((frameDescription & constants.ORIGIN_BITS) >> 14) !== 0; + obj.protocolVersion = (frameDescription & constants.PROTOCOL_VERSION_BITS); + offset += 2; + + obj.source = buf.toString('hex', offset, offset + 4); + offset += 4; + + // Frame address + obj.target = buf.toString('hex', offset, offset + 6); + offset += 6; + + obj.reserved1 = buf.slice(offset, offset + 2); + offset += 2; + + obj.site = buf.toString('utf8', offset, offset + 6); + obj.site = obj.site.replace(/\0/g, ''); + offset += 6; + + const frameAddressDescription = buf.readUInt8(offset); + obj.ackRequired = (frameAddressDescription & constants.ACK_REQUIRED_BIT) !== 0; + obj.resRequired = (frameAddressDescription & constants.RESPONSE_REQUIRED_BIT) !== 0; + offset += 1; + + obj.sequence = buf.readUInt8(offset); + offset += 1; + + // Protocol header + obj.time = utils.readUInt64LE(buf, offset); + offset += 8; + + obj.type = buf.readUInt16LE(offset); + offset += 2; + + obj.reserved2 = buf.slice(offset, offset + 2); + offset += 2; + + return obj; + } - obj.reserved1 = buf.slice(offset, offset + 2); - offset += 2; + /** + * Parses a lifx packet + * @param {Buffer} buf Buffer with lifx packet + * @return {Object} parsed packet + */ + static toObject(buf) { + let obj = {}; + + // Try to read header of packet + try { + obj = this.headerToObject(buf); + } catch (err) { + // If this fails return with error + return err; + } - obj.site = buf.toString('utf8', offset, offset + 6); - obj.site = obj.site.replace(/\0/g, ''); - offset += 6; + if (obj.type !== undefined) { + const typeName = result(find(this.typeList, {id: obj.type}), 'name'); + if (packets[typeName] !== undefined) { + if (typeof packets[typeName].toObject === 'function') { + const specificObj = packets[typeName].toObject(buf.slice(constants.PACKET_HEADER_SIZE)); + obj = extend(obj, specificObj); + } + } + } - const frameAddressDescription = buf.readUInt8(offset); - obj.ackRequired = (frameAddressDescription & constants.ACK_REQUIRED_BIT) !== 0; - obj.resRequired = (frameAddressDescription & constants.RESPONSE_REQUIRED_BIT) !== 0; - offset += 1; + return obj; + } - obj.sequence = buf.readUInt8(offset); - offset += 1; + /** + * Creates a lifx packet header from a given object + * @param {Object} obj Object containg header configuration for packet + * @return {Buffer} packet header buffer + */ + static headerToBuffer(obj) { + const buf = Buffer.alloc(36); + buf.fill(0); + let offset = 0; + + // Frame + buf.writeUInt16LE(obj.size, offset); + offset += 2; + + if (obj.protocolVersion === undefined) { + obj.protocolVersion = constants.PROTOCOL_VERSION_CURRENT; + } + let frameDescription = obj.protocolVersion; - // Protocol header - obj.time = utils.readUInt64LE(buf, offset); - offset += 8; + if (obj.addressable !== undefined && obj.addressable === true) { + frameDescription |= constants.ADDRESSABLE_BIT; + } else if (obj.source !== undefined && obj.source.length > 0 && obj.source !== '00000000') { + frameDescription |= constants.ADDRESSABLE_BIT; + } - obj.type = buf.readUInt16LE(offset); - offset += 2; + if (obj.tagged !== undefined && obj.tagged === true) { + frameDescription |= constants.TAGGED_BIT; + } - obj.reserved2 = buf.slice(offset, offset + 2); - offset += 2; + if (obj.origin !== undefined && obj.origin === true) { + // 0 or 1 to the 14 bit + frameDescription |= (1 << 14); + } - return obj; -}; + buf.writeUInt16LE(frameDescription, offset); + offset += 2; -/** - * Parses a lifx packet - * @param {Buffer} buf Buffer with lifx packet - * @return {Object} parsed packet - */ -Packet.toObject = function(buf) { - let obj = {}; - - // Try to read header of packet - try { - obj = this.headerToObject(buf); - } catch (err) { - // If this fails return with error - return err; - } - - if (obj.type !== undefined) { - const typeName = result(find(this.typeList, {id: obj.type}), 'name'); - if (packets[typeName] !== undefined) { - if (typeof packets[typeName].toObject === 'function') { - const specificObj = packets[typeName].toObject(buf.slice(constants.PACKET_HEADER_SIZE)); - obj = extend(obj, specificObj); + if (obj.source !== undefined && obj.source.length > 0) { + if (obj.source.length === 8) { + buf.write(obj.source, offset, 4, 'hex'); + } else { + throw new RangeError('LIFX source must be given in 8 characters'); } } - } - - return obj; -}; - -/** - * Creates a lifx packet header from a given object - * @param {Object} obj Object containg header configuration for packet - * @return {Buffer} packet header buffer - */ -Packet.headerToBuffer = function(obj) { - const buf = Buffer.alloc(36); - buf.fill(0); - let offset = 0; + offset += 4; - // Frame - buf.writeUInt16LE(obj.size, offset); - offset += 2; - - if (obj.protocolVersion === undefined) { - obj.protocolVersion = constants.PROTOCOL_VERSION_CURRENT; - } - let frameDescription = obj.protocolVersion; - - if (obj.addressable !== undefined && obj.addressable === true) { - frameDescription |= constants.ADDRESSABLE_BIT; - } else if (obj.source !== undefined && obj.source.length > 0 && obj.source !== '00000000') { - frameDescription |= constants.ADDRESSABLE_BIT; - } + // Frame address + if (obj.target !== undefined && obj.target !== null) { + buf.write(obj.target, offset, 6, 'hex'); + } + offset += 6; - if (obj.tagged !== undefined && obj.tagged === true) { - frameDescription |= constants.TAGGED_BIT; - } + // reserved1 + offset += 2; - if (obj.origin !== undefined && obj.origin === true) { - // 0 or 1 to the 14 bit - frameDescription |= (1 << 14); - } + if (obj.site !== undefined && obj.site !== null) { + buf.write(obj.site, offset, 6, 'utf8'); + } + offset += 6; - buf.writeUInt16LE(frameDescription, offset); - offset += 2; + let frameAddressDescription = 0; + if (obj.ackRequired !== undefined && obj.ackRequired === true) { + frameAddressDescription |= constants.ACK_REQUIRED_BIT; + } - if (obj.source !== undefined && obj.source.length > 0) { - if (obj.source.length === 8) { - buf.write(obj.source, offset, 4, 'hex'); - } else { - throw new RangeError('LIFX source must be given in 8 characters'); + if (obj.resRequired !== undefined && obj.resRequired === true) { + frameAddressDescription |= constants.RESPONSE_REQUIRED_BIT; } - } - offset += 4; + buf.writeUInt8(frameAddressDescription, offset); + offset += 1; - // Frame address - if (obj.target !== undefined && obj.target !== null) { - buf.write(obj.target, offset, 6, 'hex'); - } - offset += 6; + if (typeof obj.sequence === 'number') { + buf.writeUInt8(obj.sequence, offset); + } + offset += 1; - // reserved1 - offset += 2; + // Protocol header + if (obj.time !== undefined) { + utils.writeUInt64LE(buf, offset, obj.time); + } + offset += 8; - if (obj.site !== undefined && obj.site !== null) { - buf.write(obj.site, offset, 6, 'utf8'); - } - offset += 6; + if (typeof obj.type === 'number') { + obj.type = result(find(this.typeList, {id: obj.type}), 'id'); + } else if (typeof obj.type === 'string' || obj.type instanceof String) { + obj.type = result(find(this.typeList, {name: obj.type}), 'id'); + } + if (obj.type === undefined) { + throw new Error('Unknown lifx packet of type: ' + obj.type); + } + buf.writeUInt16LE(obj.type, offset); + offset += 2; - let frameAddressDescription = 0; - if (obj.ackRequired !== undefined && obj.ackRequired === true) { - frameAddressDescription |= constants.ACK_REQUIRED_BIT; - } + // reserved2 + offset += 2; - if (obj.resRequired !== undefined && obj.resRequired === true) { - frameAddressDescription |= constants.RESPONSE_REQUIRED_BIT; + return buf; } - buf.writeUInt8(frameAddressDescription, offset); - offset += 1; - if (typeof obj.sequence === 'number') { - buf.writeUInt8(obj.sequence, offset); - } - offset += 1; + /** + * Creates a packet from a configuration object + * @param {Object} obj Object with configuration for packet + * @return {Buffer|Boolean} the packet or false in case of error + */ + static toBuffer(obj) { + if (obj.type !== undefined) { + // Map id to string if needed + if (typeof obj.type === 'number') { + obj.type = result(find(this.typeList, {id: obj.type}), 'name'); + } else if (typeof obj.type === 'string' || obj.type instanceof String) { + obj.type = result(find(this.typeList, {name: obj.type}), 'name'); + } - // Protocol header - if (obj.time !== undefined) { - utils.writeUInt64LE(buf, offset, obj.time); - } - offset += 8; + if (obj.type !== undefined) { + if (typeof packets[obj.type].toBuffer === 'function') { + const packetTypeData = packets[obj.type].toBuffer(obj); + return Buffer.concat([ + this.headerToBuffer(obj), + packetTypeData + ]); + } + return this.headerToBuffer(obj); + } + } - if (typeof obj.type === 'number') { - obj.type = result(find(this.typeList, {id: obj.type}), 'id'); - } else if (typeof obj.type === 'string' || obj.type instanceof String) { - obj.type = result(find(this.typeList, {name: obj.type}), 'id'); - } - if (obj.type === undefined) { - throw new Error('Unknown lifx packet of type: ' + obj.type); + return false; } - buf.writeUInt16LE(obj.type, offset); - offset += 2; - - // reserved2 - offset += 2; - return buf; -}; - -/** - * Creates a packet from a configuration object - * @param {Object} obj Object with configuration for packet - * @return {Buffer|Boolean} the packet or false in case of error - */ -Packet.toBuffer = function(obj) { - if (obj.type !== undefined) { - // Map id to string if needed - if (typeof obj.type === 'number') { - obj.type = result(find(this.typeList, {id: obj.type}), 'name'); - } else if (typeof obj.type === 'string' || obj.type instanceof String) { - obj.type = result(find(this.typeList, {name: obj.type}), 'name'); - } - - if (obj.type !== undefined) { - if (typeof packets[obj.type].toBuffer === 'function') { - const packetTypeData = packets[obj.type].toBuffer(obj); - return Buffer.concat([ - this.headerToBuffer(obj), - packetTypeData - ]); + /** + * Creates a new packet by the given type + * Note: This does not validate the given params + * @param {String|Number} type the type of packet to create as number or string + * @param {Object} params further settings to pass + * @param {String} [source] the source of the packet, length 8 + * @param {String} [target] the target of the packet, length 12 + * @return {Object} The prepared packet object including header + */ + static create(type, params, source, target) { + const obj = {}; + if (type !== undefined) { + // Check if type is valid + if (typeof type === 'string' || type instanceof String) { + obj.type = result(find(this.typeList, {name: type}), 'id'); + } else if (typeof type === 'number') { + const typeMatch = find(this.typeList, {id: type}); + obj.type = result(typeMatch, 'id'); + type = result(typeMatch, 'name'); } - return this.headerToBuffer(obj); + if (obj.type === undefined) { + return false; + } + } else { + return false; } - } + obj.size = constants.PACKET_HEADER_SIZE + packets[type].size; - return false; -}; - -/** - * Creates a new packet by the given type - * Note: This does not validate the given params - * @param {String|Number} type the type of packet to create as number or string - * @param {Object} params further settings to pass - * @param {String} [source] the source of the packet, length 8 - * @param {String} [target] the target of the packet, length 12 - * @return {Object} The prepared packet object including header - */ -Packet.create = function(type, params, source, target) { - const obj = {}; - if (type !== undefined) { - // Check if type is valid - if (typeof type === 'string' || type instanceof String) { - obj.type = result(find(this.typeList, {name: type}), 'id'); - } else if (typeof type === 'number') { - const typeMatch = find(this.typeList, {id: type}); - obj.type = result(typeMatch, 'id'); - type = result(typeMatch, 'name'); + if (source !== undefined) { + obj.source = source; } - if (obj.type === undefined) { - return false; + if (target !== undefined) { + obj.target = target; + } + if (packets[type].tagged !== undefined) { + obj.tagged = packets[type].tagged; } - } else { - return false; - } - obj.size = constants.PACKET_HEADER_SIZE + packets[type].size; - if (source !== undefined) { - obj.source = source; - } - if (target !== undefined) { - obj.target = target; + return assign(obj, params); } - if (packets[type].tagged !== undefined) { - obj.tagged = packets[type].tagged; - } - - return assign(obj, params); -}; +} module.exports = Packet; diff --git a/test/unit/client-test.js b/test/unit/client-test.js index 208a3c5..03f2bc1 100644 --- a/test/unit/client-test.js +++ b/test/unit/client-test.js @@ -2,7 +2,7 @@ const Client = require('../../').Client; const Light = require('../../').Light; -const packet = require('../../').packet; +const Packet = require('../../').Packet; const constants = require('../../').constants; const assert = require('chai').assert; const lolex = require('lolex'); @@ -338,17 +338,17 @@ describe('Client', () => { }, () => { assert.equal(client.sequenceNumber, 0, 'starts sequence with 0'); assert.lengthOf(client.getMessageQueue(), 0, 'is empty'); - client.send(packet.create('getService', {}, '12345678')); + client.send(Packet.create('getService', {}, '12345678')); assert.equal(client.sequenceNumber, 0, 'sequence is the same after broadcast'); assert.lengthOf(client.getMessageQueue(), 1, 'added to message queue'); assert.property(client.getMessageQueue()[0], 'data', 'has data'); assert.notProperty(client.getMessageQueue()[0], 'address', 'broadcast has no target address'); - client.send(packet.create('setPower', {level: 65535, duration: 0, target: lightProps.id}, '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: lightProps.id}, '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(); }); @@ -563,7 +563,7 @@ describe('Client', () => { } done(); }; - const packetObj = packet.create('setPower', {level: 65535}, client.source); + const packetObj = Packet.create('setPower', {level: 65535}, client.source); const queueAddress = client.broadcastAddress; client.init({ @@ -593,7 +593,7 @@ describe('Client', () => { } done(); }; - const packetObj = packet.create('setPower', {level: 65535}, client.source); + const packetObj = Packet.create('setPower', {level: 65535}, client.source); const queueAddress = client.broadcastAddress; client.init({ @@ -626,7 +626,7 @@ describe('Client', () => { assert.isNull(rinfo); done(); }; - const packetObj = packet.create('setPower', {level: 65535}, client.source); + const packetObj = Packet.create('setPower', {level: 65535}, client.source); const queueAddress = client.broadcastAddress; client.init({ diff --git a/test/unit/packet-test.js b/test/unit/packet-test.js index 9a38d02..35021b6 100644 --- a/test/unit/packet-test.js +++ b/test/unit/packet-test.js @@ -1,6 +1,6 @@ 'use strict'; -const Packet = require('../../').packet; +const Packet = require('../../').Packet; const assert = require('chai').assert; describe('Packet', () => { diff --git a/test/unit/packets/getService-test.js b/test/unit/packets/getService-test.js index 038c8fe..5d9dded 100644 --- a/test/unit/packets/getService-test.js +++ b/test/unit/packets/getService-test.js @@ -1,6 +1,6 @@ 'use strict'; -const Packet = require('../../../').packet; +const Packet = require('../../../').Packet; const assert = require('chai').assert; describe('Packet getService', () => { diff --git a/test/unit/packets/setColor-test.js b/test/unit/packets/setColor-test.js index 4e571ec..0acf30f 100644 --- a/test/unit/packets/setColor-test.js +++ b/test/unit/packets/setColor-test.js @@ -1,6 +1,6 @@ 'use strict'; -const Packet = require('../../../').packet; +const Packet = require('../../../').Packet; const assert = require('chai').assert; describe('Packet setColor', () => { diff --git a/test/unit/packets/setPower-test.js b/test/unit/packets/setPower-test.js index f9ed28b..63819f9 100644 --- a/test/unit/packets/setPower-test.js +++ b/test/unit/packets/setPower-test.js @@ -1,6 +1,6 @@ 'use strict'; -const Packet = require('../../../').packet; +const Packet = require('../../../').Packet; const assert = require('chai').assert; describe('Packet setPower', () => { From 2f2e609fc88a997bd39d8c1f4d3ea842a9e3b29e Mon Sep 17 00:00:00 2001 From: Ristomatti Airo Date: Sun, 29 Oct 2017 23:48:01 +0200 Subject: [PATCH 3/7] Refactor Client into a class --- src/lifx/client.js | 1155 ++++++++++++++++++++++---------------------- 1 file changed, 580 insertions(+), 575 deletions(-) diff --git a/src/lifx/client.js b/src/lifx/client.js index b4470d4..3831d24 100644 --- a/src/lifx/client.js +++ b/src/lifx/client.js @@ -6,463 +6,466 @@ const EventEmitter = require('eventemitter3'); const { defaults, isArray, isBoolean, isString, isNil, result, find, bind, forEach, keys, every, includes, filter } = require('lodash'); -const Packet = require('../lifx').packet; +const Packet = require('../lifx').Packet; const {Light, constants, utils} = require('../lifx'); /** * Creates a lifx client * @extends EventEmitter */ -function Client() { - EventEmitter.call(this); - - this.debug = false; - this.socket = dgram.createSocket('udp4'); - this.isSocketBound = false; - this.devices = {}; - this.port = null; - this.messageQueues = {}; - this.sendTimers = {}; - this.discoveryTimer = null; - this.discoveryPacketSequence = 0; - this.messageHandlers = [{ - type: 'stateService', - callback: this.processDiscoveryPacket.bind(this) - }, { - type: 'stateLabel', - callback: this.processLabelPacket.bind(this) - }, { - type: 'stateLight', - callback: this.processLabelPacket.bind(this) - }]; - this.sequenceNumber = 0; - this.lightOfflineTolerance = 3; - this.messageHandlerTimeout = 45000; // 45 sec - this.resendPacketDelay = 150; - 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); +class Client { + constructor() { + EventEmitter.call(this); -/** - * Creates a new socket and starts discovery - * @example - * init({debug: true}, function() { - * console.log('Client started'); - * }) - * @param {Object} [options] Configuration to use - * @param {String} [options.address] The IPv4 address to bind to - * @param {Number} [options.port] The port to bind to - * @param {Boolean} [options.debug] Show debug output - * @param {Number} [options.lightOfflineTolerance] If light hasn't answered for amount of discoveries it is set offline - * @param {Number} [options.messageHandlerTimeout] Message handlers not called will be removed after this delay in ms - * @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 - */ -Client.prototype.init = function(options, callback) { - const defaultOpts = { - address: '0.0.0.0', - port: 0, - debug: false, - lightOfflineTolerance: 3, - messageHandlerTimeout: 45000, - source: '', - startDiscovery: true, - lights: [], - stopAfterDiscovery: false, - broadcast: '255.255.255.255', - sendPort: constants.LIFX_DEFAULT_PORT, - resendPacketDelay: 150, - resendMaxTimes: 3 - }; - - options = options || {}; - const opts = defaults(options, defaultOpts); - - if (typeof opts.port !== 'number') { - throw new TypeError('LIFX Client port option must be a number'); - } else if (opts.port > 65535 || opts.port < 0) { - throw new RangeError('LIFX Client port option must be between 0 and 65535'); - } + this.debug = false; + this.socket = dgram.createSocket('udp4'); + this.isSocketBound = false; + this.devices = {}; + this.port = null; + this.messageQueues = {}; + this.sendTimers = {}; + this.discoveryTimer = null; + this.discoveryPacketSequence = 0; + this.messageHandlers = [{ + type: 'stateService', + callback: this.processDiscoveryPacket.bind(this) + }, { + type: 'stateLabel', + callback: this.processLabelPacket.bind(this) + }, { + type: 'stateLight', + callback: this.processLabelPacket.bind(this) + }]; + this.sequenceNumber = 0; + this.lightOfflineTolerance = 3; + this.messageHandlerTimeout = 45000; // 45 sec + this.resendPacketDelay = 150; + this.resendMaxTimes = 5; + this.source = utils.getRandomHexString(8); + this.broadcastAddress = '255.255.255.255'; + this.lightAddresses = []; + this.stopAfterDiscovery = false; + this.discoveryCompleted = false; + } + + /** + * Creates a new socket and starts discovery + * @example + * init({debug: true}, function() { + * console.log('Client started'); + * }) + * @param {Object} [options] Configuration to use + * @param {String} [options.address] The IPv4 address to bind to + * @param {Number} [options.port] The port to bind to + * @param {Boolean} [options.debug] Show debug output + * @param {Number} [options.lightOfflineTolerance] If light hasn't answered for amount of discoveries it is set offline + * @param {Number} [options.messageHandlerTimeout] Message handlers not called will be removed after this delay in ms + * @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 + */ + init(options, callback) { + const defaultOpts = { + address: '0.0.0.0', + port: 0, + debug: false, + lightOfflineTolerance: 3, + messageHandlerTimeout: 45000, + source: '', + startDiscovery: true, + lights: [], + stopAfterDiscovery: false, + broadcast: '255.255.255.255', + sendPort: constants.LIFX_DEFAULT_PORT, + resendPacketDelay: 150, + resendMaxTimes: 3 + }; + + options = options || {}; + const opts = defaults(options, defaultOpts); + + if (typeof opts.port !== 'number') { + throw new TypeError('LIFX Client port option must be a number'); + } else if (opts.port > 65535 || opts.port < 0) { + throw new RangeError('LIFX Client port option must be between 0 and 65535'); + } - if (typeof opts.debug !== 'boolean') { - throw new TypeError('LIFX Client debug option must be a boolean'); - } - this.debug = opts.debug; + if (typeof opts.debug !== 'boolean') { + throw new TypeError('LIFX Client debug option must be a boolean'); + } + this.debug = opts.debug; - if (typeof opts.lightOfflineTolerance !== 'number') { - throw new TypeError('LIFX Client lightOfflineTolerance option must be a number'); - } - this.lightOfflineTolerance = opts.lightOfflineTolerance; + if (typeof opts.lightOfflineTolerance !== 'number') { + throw new TypeError('LIFX Client lightOfflineTolerance option must be a number'); + } + this.lightOfflineTolerance = opts.lightOfflineTolerance; - if (typeof opts.messageHandlerTimeout !== 'number') { - throw new TypeError('LIFX Client messageHandlerTimeout option must be a number'); - } - this.messageHandlerTimeout = opts.messageHandlerTimeout; + if (typeof opts.messageHandlerTimeout !== 'number') { + throw new TypeError('LIFX Client messageHandlerTimeout option must be a number'); + } + this.messageHandlerTimeout = opts.messageHandlerTimeout; - if (typeof opts.resendPacketDelay !== 'number') { - throw new TypeError('LIFX Client resendPacketDelay option must be a number'); - } - this.resendPacketDelay = opts.resendPacketDelay; + if (typeof opts.resendPacketDelay !== 'number') { + throw new TypeError('LIFX Client resendPacketDelay option must be a number'); + } + this.resendPacketDelay = opts.resendPacketDelay; - if (typeof opts.resendMaxTimes !== 'number') { - throw new TypeError('LIFX Client resendMaxTimes option must be a number'); - } - this.resendMaxTimes = opts.resendMaxTimes; + if (typeof opts.resendMaxTimes !== 'number') { + throw new TypeError('LIFX Client resendMaxTimes option must be a number'); + } + this.resendMaxTimes = opts.resendMaxTimes; - if (typeof opts.broadcast !== 'string') { - throw new TypeError('LIFX Client broadcast option must be a string'); - } else if (!utils.isIpv4Format(opts.broadcast)) { - throw new TypeError('LIFX Client broadcast option does only allow IPv4 address format'); - } - this.broadcastAddress = opts.broadcast; + if (typeof opts.broadcast !== 'string') { + throw new TypeError('LIFX Client broadcast option must be a string'); + } else if (!utils.isIpv4Format(opts.broadcast)) { + throw new TypeError('LIFX Client broadcast option does only allow IPv4 address format'); + } + this.broadcastAddress = opts.broadcast; - if (typeof opts.sendPort !== 'number') { - throw new TypeError('LIFX Client sendPort option must be a number'); - } else if (opts.sendPort > 65535 || opts.sendPort < 1) { - throw new RangeError('LIFX Client sendPort option must be between 1 and 65535'); - } - this.sendPort = opts.sendPort; - - if (!isArray(opts.lights)) { - throw new TypeError('LIFX Client lights option must be an array'); - } else { - opts.lights.forEach(function(light) { - if (!utils.isIpv4Format(light)) { - throw new TypeError('LIFX Client lights option array element \'' + light + '\' is not expected IPv4 format'); - } - }); - this.lightAddresses = opts.lights; + if (typeof opts.sendPort !== 'number') { + throw new TypeError('LIFX Client sendPort option must be a number'); + } else if (opts.sendPort > 65535 || opts.sendPort < 1) { + throw new RangeError('LIFX Client sendPort option must be between 1 and 65535'); + } + this.sendPort = opts.sendPort; - if (!isBoolean(opts.stopAfterDiscovery)) { - throw new TypeError('LIFX Client stopAfterDiscovery must be a boolean'); + if (!isArray(opts.lights)) { + throw new TypeError('LIFX Client lights option must be an array'); } else { - this.stopAfterDiscovery = opts.stopAfterDiscovery; - } - } + opts.lights.forEach(function(light) { + if (!utils.isIpv4Format(light)) { + throw new TypeError('LIFX Client lights option array element \'' + light + '\' is not expected IPv4 format'); + } + }); + this.lightAddresses = opts.lights; - if (opts.source !== '') { - if (typeof opts.source === 'string') { - if (/^[0-9A-F]{8}$/.test(opts.source)) { - this.source = opts.source; + if (!isBoolean(opts.stopAfterDiscovery)) { + throw new TypeError('LIFX Client stopAfterDiscovery must be a boolean'); } else { - throw new RangeError('LIFX Client source option must be 8 hex chars'); + this.stopAfterDiscovery = opts.stopAfterDiscovery; } - } else { - throw new TypeError('LIFX Client source option must be given as string'); } - } - this.socket.on('error', function(err) { - this.isSocketBound = false; - console.error('LIFX Client UDP error'); - console.trace(err); - this.socket.close(); - this.emit('error', err); - }.bind(this)); - - this.socket.on('message', function(msg, rinfo) { - // Ignore own messages and false formats - if (utils.getHostIPs().indexOf(rinfo.address) >= 0 || !Buffer.isBuffer(msg)) { - return; - } - - /* istanbul ignore if */ - if (this.debug) { - console.log('DEBUG - ' + msg.toString('hex') + ' from ' + rinfo.address); + if (opts.source !== '') { + if (typeof opts.source === 'string') { + if (/^[0-9A-F]{8}$/.test(opts.source)) { + this.source = opts.source; + } else { + throw new RangeError('LIFX Client source option must be 8 hex chars'); + } + } else { + throw new TypeError('LIFX Client source option must be given as string'); + } } - // Parse packet to object - const parsedMsg = Packet.toObject(msg); + this.socket.on('error', function(err) { + this.isSocketBound = false; + console.error('LIFX Client UDP error'); + console.trace(err); + this.socket.close(); + this.emit('error', err); + }.bind(this)); + + this.socket.on('message', function(msg, rinfo) { + // Ignore own messages and false formats + if (utils.getHostIPs().indexOf(rinfo.address) >= 0 || !Buffer.isBuffer(msg)) { + return; + } - // Check if packet is read successfully - if (parsedMsg instanceof Error) { - console.error('LIFX Client invalid packet header error'); - console.error('Packet: ', msg.toString('hex')); - console.trace(parsedMsg); - } else { - // Convert type before emitting - const messageTypeName = result(find(Packet.typeList, {id: parsedMsg.type}), 'name'); - if (messageTypeName !== undefined) { - parsedMsg.type = messageTypeName; + /* istanbul ignore if */ + if (this.debug) { + console.log('DEBUG - ' + msg.toString('hex') + ' from ' + rinfo.address); } - // Check for handlers of given message and rinfo - this.processMessageHandlers(parsedMsg, rinfo); - this.emit('message', parsedMsg, rinfo); - } - }.bind(this)); + // Parse packet to object + const parsedMsg = Packet.toObject(msg); - this.socket.bind(opts.port, opts.address, function() { - this.isSocketBound = true; - this.socket.setBroadcast(true); - this.emit('listening'); - this.port = opts.port; + // Check if packet is read successfully + if (parsedMsg instanceof Error) { + console.error('LIFX Client invalid packet header error'); + console.error('Packet: ', msg.toString('hex')); + console.trace(parsedMsg); + } else { + // Convert type before emitting + const messageTypeName = result(find(Packet.typeList, { + id: parsedMsg.type + }), 'name'); + if (messageTypeName !== undefined) { + parsedMsg.type = messageTypeName; + } + // Check for handlers of given message and rinfo + this.processMessageHandlers(parsedMsg, rinfo); - // Start scanning - if (opts.startDiscovery) { - this.startDiscovery(opts.lights); - } - if (typeof callback === 'function') { - return callback(); - } - }.bind(this)); -}; + this.emit('message', parsedMsg, rinfo); + } + }.bind(this)); -/** - * Destroy an instance - */ -Client.prototype.destroy = function() { - this.stopDiscovery(); - forEach(keys(this.messageQueues), (queueAddress) => { - this.stopSendingProcess(queueAddress); - }); - if (this.isSocketBound) { - this.socket.close(); - } -}; + this.socket.bind(opts.port, opts.address, function() { + this.isSocketBound = true; + this.socket.setBroadcast(true); + this.emit('listening'); + this.port = opts.port; -/** - * Gets the message queue for the given address. If no address is defined, - * defaults to broadcast address. - * @param {String} queueAddress Message queue address - * @return {Array} Message queue for the address - */ -Client.prototype.getMessageQueue = function(queueAddress = this.broadcastAddress) { - if (isNil(this.messageQueues[queueAddress])) { - this.messageQueues[queueAddress] = []; + // Start scanning + if (opts.startDiscovery) { + this.startDiscovery(opts.lights); + } + if (typeof callback === 'function') { + return callback(); + } + }.bind(this)); } - return this.messageQueues[queueAddress]; -}; -/** - * Sends a packet from the messages queue or stops the sending process - * if queue is empty - * @param {String} queueAddress Message queue id - * @return {Function} Sending process for the message queue - **/ -Client.prototype.sendingProcess = function(queueAddress) { - const messageQueue = this.getMessageQueue(queueAddress); - return () => { - if (!this.isSocketBound) { + /** + * Destroy an instance + */ + destroy() { + this.stopDiscovery(); + forEach(keys(this.messageQueues), (queueAddress) => { this.stopSendingProcess(queueAddress); - console.log('LIFX Client stopped sending due to unbound socket'); - return; + }); + if (this.isSocketBound) { + this.socket.close(); } + } - if (messageQueue.length > 0) { - const msg = messageQueue.pop(); - if (msg.address === undefined) { - msg.address = this.broadcastAddress; + /** + * Gets the message queue for the given address. If no address is defined, + * defaults to broadcast address. + * @param {String} queueAddress Message queue address + * @return {Array} Message queue for the address + */ + getMessageQueue(queueAddress = this.broadcastAddress) { + if (isNil(this.messageQueues[queueAddress])) { + this.messageQueues[queueAddress] = []; + } + return this.messageQueues[queueAddress]; + } + + /** + * Sends a packet from the messages queue or stops the sending process + * if queue is empty + * @param {String} queueAddress Message queue id + * @return {Function} Sending process for the message queue + **/ + sendingProcess(queueAddress) { + const messageQueue = this.getMessageQueue(queueAddress); + return () => { + if (!this.isSocketBound) { + this.stopSendingProcess(queueAddress); + console.log('LIFX Client stopped sending due to unbound socket'); + return; } - if (msg.transactionType === constants.PACKET_TRANSACTION_TYPES.ONE_WAY) { - this.socket.send(msg.data, 0, msg.data.length, this.sendPort, msg.address); - /* istanbul ignore if */ - if (this.debug) { - console.log('DEBUG - ' + msg.data.toString('hex') + ' to ' + msg.address); + + if (messageQueue.length > 0) { + const msg = messageQueue.pop(); + if (msg.address === undefined) { + msg.address = this.broadcastAddress; } - } else if (msg.transactionType === constants.PACKET_TRANSACTION_TYPES.REQUEST_RESPONSE) { - if (msg.timesSent < this.resendMaxTimes) { - if (Date.now() > (msg.timeLastSent + this.resendPacketDelay)) { - this.socket.send(msg.data, 0, msg.data.length, this.sendPort, msg.address); - msg.timesSent += 1; - msg.timeLastSent = Date.now(); - /* istanbul ignore if */ - if (this.debug) { - console.log( - 'DEBUG - ' + msg.data.toString('hex') + ' to ' + msg.address + - ', send ' + msg.timesSent + ' time(s)' - ); - } + if (msg.transactionType === constants.PACKET_TRANSACTION_TYPES.ONE_WAY) { + this.socket.send(msg.data, 0, msg.data.length, this.sendPort, msg.address); + /* istanbul ignore if */ + if (this.debug) { + console.log('DEBUG - ' + msg.data.toString('hex') + ' to ' + msg.address); } - // Add to the end of the queue again - messageQueue.unshift(msg); - } else { - this.messageHandlers.forEach(function(handler, hdlrIndex) { - if (handler.type === 'acknowledgement' && handler.sequenceNumber === msg.sequence) { - this.messageHandlers.splice(hdlrIndex, 1); - const err = new Error('No LIFX response after max resend limit of ' + this.resendMaxTimes); - return handler.callback(err, null, null); + } else if (msg.transactionType === constants.PACKET_TRANSACTION_TYPES.REQUEST_RESPONSE) { + if (msg.timesSent < this.resendMaxTimes) { + if (Date.now() > (msg.timeLastSent + this.resendPacketDelay)) { + this.socket.send(msg.data, 0, msg.data.length, this.sendPort, msg.address); + msg.timesSent += 1; + msg.timeLastSent = Date.now(); + /* istanbul ignore if */ + if (this.debug) { + console.log( + 'DEBUG - ' + msg.data.toString('hex') + ' to ' + msg.address + + ', send ' + msg.timesSent + ' time(s)' + ); + } } - }.bind(this)); + // Add to the end of the queue again + messageQueue.unshift(msg); + } else { + this.messageHandlers.forEach(function(handler, hdlrIndex) { + if (handler.type === 'acknowledgement' && handler.sequenceNumber === msg.sequence) { + this.messageHandlers.splice(hdlrIndex, 1); + const err = new Error('No LIFX response after max resend limit of ' + this.resendMaxTimes); + return handler.callback(err, null, null); + } + }.bind(this)); + } } + } else { + this.stopSendingProcess(queueAddress); } - } else { - this.stopSendingProcess(queueAddress); - } - }; -}; + }; + } -/** - * Starts the sending of all packages in the queue - * @param {String} queueAddress Message queue id - */ -Client.prototype.startSendingProcess = function(queueAddress = this.broadcastAddress) { - if (isNil(this.sendTimers[queueAddress])) { // Already running? - const sendingProcess = this.sendingProcess(queueAddress); - this.sendTimers[queueAddress] = setInterval(sendingProcess, constants.MESSAGE_RATE_LIMIT); + /** + * Starts the sending of all packages in the queue + * @param {String} queueAddress Message queue id + */ + startSendingProcess(queueAddress = this.broadcastAddress) { + if (isNil(this.sendTimers[queueAddress])) { // Already running? + const sendingProcess = this.sendingProcess(queueAddress); + this.sendTimers[queueAddress] = setInterval(sendingProcess, constants.MESSAGE_RATE_LIMIT); + } } -}; -/** - * Stops sending of all packages in the queue - * @param {String} queueAddress Message queue id - */ -Client.prototype.stopSendingProcess = function(queueAddress = this.broadcastAddress) { - if (!isNil(this.sendTimers[queueAddress])) { - clearInterval(this.sendTimers[queueAddress]); - delete this.sendTimers[queueAddress]; + /** + * Stops sending of all packages in the queue + * @param {String} queueAddress Message queue id + */ + stopSendingProcess(queueAddress = this.broadcastAddress) { + if (!isNil(this.sendTimers[queueAddress])) { + clearInterval(this.sendTimers[queueAddress]); + delete this.sendTimers[queueAddress]; + } } -}; -/** - * Start discovery of lights - * This will keep the list of lights updated, finds new lights and sets lights - * offline if no longer found - * @param {Array} [lights] Pre set list of ip addresses of known addressable lights to request directly - */ -Client.prototype.startDiscovery = function(lights) { - lights = lights || []; - const sendDiscoveryPacket = function() { - // Sign flag on inactive lights - forEach(this.devices, bind(function(info, deviceId) { - if (this.devices[deviceId].status !== 'off') { - const diff = this.discoveryPacketSequence - info.seenOnDiscovery; - if (diff >= this.lightOfflineTolerance) { - this.devices[deviceId].status = 'off'; - this.emit('bulb-offline', info); // deprecated - this.emit('light-offline', info); + /** + * Start discovery of lights + * This will keep the list of lights updated, finds new lights and sets lights + * offline if no longer found + * @param {Array} [lights] Pre set list of ip addresses of known addressable lights to request directly + */ + startDiscovery(lights) { + lights = lights || []; + const sendDiscoveryPacket = function() { + // Sign flag on inactive lights + forEach(this.devices, bind(function(info, deviceId) { + if (this.devices[deviceId].status !== 'off') { + const diff = this.discoveryPacketSequence - info.seenOnDiscovery; + if (diff >= this.lightOfflineTolerance) { + this.devices[deviceId].status = 'off'; + this.emit('bulb-offline', info); // deprecated + this.emit('light-offline', info); + } } - } - }, this)); + }, this)); - // Send a discovery packet broadcast - this.send(Packet.create('getService', {}, this.source)); - - // Send a discovery packet to each light given directly - lights.forEach(function(lightAddress) { - const msg = Packet.create('getService', {}, this.source); - msg.address = lightAddress; - this.send(msg); - }, this); + // Send a discovery packet broadcast + this.send(Packet.create('getService', {}, this.source)); - // Keep track of a sequent number to find not answering lights - if (this.discoveryPacketSequence >= Number.MAX_VALUE) { - this.discoveryPacketSequence = 0; - } else { - this.discoveryPacketSequence += 1; - } - }.bind(this); + // Send a discovery packet to each light given directly + lights.forEach(function(lightAddress) { + const msg = Packet.create('getService', {}, this.source); + msg.address = lightAddress; + this.send(msg); + }, this); - this.discoveryTimer = setInterval( - sendDiscoveryPacket, - constants.DISCOVERY_INTERVAL - ); + // Keep track of a sequent number to find not answering lights + if (this.discoveryPacketSequence >= Number.MAX_VALUE) { + this.discoveryPacketSequence = 0; + } else { + this.discoveryPacketSequence += 1; + } + }.bind(this); - sendDiscoveryPacket(); -}; + this.discoveryTimer = setInterval( + sendDiscoveryPacket, + constants.DISCOVERY_INTERVAL + ); -/** - * Checks if light discovery is in progress - * @return {Boolean} is discovery in progress - */ -Client.prototype.isDiscovering = function() { - return this.discoveryTimer !== null; -}; + sendDiscoveryPacket(); + } -/** - * Checks all registered message handlers if they request the given message - * @param {Object} msg message to check handler for - * @param {Object} rinfo rinfo address info to check handler for - */ -Client.prototype.processMessageHandlers = function(msg, rinfo) { - const messageQueue = this.getMessageQueue(rinfo.address); - // Process only packages for us - if (msg.source.toLowerCase() !== this.source.toLowerCase()) { - return; + /** + * Checks if light discovery is in progress + * @return {Boolean} is discovery in progress + */ + isDiscovering() { + return this.discoveryTimer !== null; } - // We check our message handler if the answer received is requested - this.messageHandlers.forEach(function(handler, hdlrIndex) { - if (msg.type === handler.type) { - if (handler.sequenceNumber !== undefined) { - if (handler.sequenceNumber === msg.sequence) { - // Remove if specific packet was request, since it should only be called once - this.messageHandlers.splice(hdlrIndex, 1); - messageQueue.forEach(function(packet, packetIndex) { - if (packet.transactionType === constants.PACKET_TRANSACTION_TYPES.REQUEST_RESPONSE && - packet.sequence === msg.sequence) { - messageQueue.splice(packetIndex, 1); - } - }); + /** + * Checks all registered message handlers if they request the given message + * @param {Object} msg message to check handler for + * @param {Object} rinfo rinfo address info to check handler for + */ + processMessageHandlers(msg, rinfo) { + const messageQueue = this.getMessageQueue(rinfo.address); + // Process only packages for us + if (msg.source.toLowerCase() !== this.source.toLowerCase()) { + return; + } + // We check our message handler if the answer received is requested + this.messageHandlers.forEach(function(handler, hdlrIndex) { + if (msg.type === handler.type) { + if (handler.sequenceNumber !== undefined) { + if (handler.sequenceNumber === msg.sequence) { + // Remove if specific packet was request, since it should only be called once + this.messageHandlers.splice(hdlrIndex, 1); + messageQueue.forEach(function(packet, packetIndex) { + if (packet.transactionType === constants.PACKET_TRANSACTION_TYPES.REQUEST_RESPONSE && + packet.sequence === msg.sequence) { + messageQueue.splice(packetIndex, 1); + } + }); + + // Call the function requesting the packet + return handler.callback(null, msg, rinfo); + } + } else { // Call the function requesting the packet return handler.callback(null, msg, rinfo); } - } else { - // Call the function requesting the packet - return handler.callback(null, msg, rinfo); } - } - // We want to call expired request handlers for specific packages after the - // messageHandlerTimeout set in options, to specify an error - if (handler.sequenceNumber !== undefined) { - if (Date.now() > (handler.timestamp + this.messageHandlerTimeout)) { - this.messageHandlers.splice(hdlrIndex, 1); + // We want to call expired request handlers for specific packages after the + // messageHandlerTimeout set in options, to specify an error + if (handler.sequenceNumber !== undefined) { + if (Date.now() > (handler.timestamp + this.messageHandlerTimeout)) { + this.messageHandlers.splice(hdlrIndex, 1); - const err = new Error('No LIFX response in time'); - return handler.callback(err, null, null); + const err = new Error('No LIFX response in time'); + return handler.callback(err, null, null); + } } - } - }, this); -}; - -/** - * Processes a discovery report packet to update internals - * @param {Object} err Error if existant - * @param {Object} msg The discovery report package - * @param {Object} rinfo Remote host details - */ -Client.prototype.processDiscoveryPacket = function(err, msg, rinfo) { - if (err) { - return; + }, this); } - if (msg.service === 'udp' && msg.port === constants.LIFX_DEFAULT_PORT) { - // Add / update the found gateway - if (!this.devices[msg.target]) { - const lightDevice = new Light({ - client: this, - id: msg.target, - address: rinfo.address, - port: msg.port, - seenOnDiscovery: this.discoveryPacketSequence - }); - this.devices[msg.target] = lightDevice; - - // Request label - const labelRequest = Packet.create('getLabel', {}, this.source); - labelRequest.target = msg.target; - this.send(labelRequest); - this.emit('bulb-new', lightDevice); // deprecated - this.emit('light-new', lightDevice); - } else { - if (this.devices[msg.target].status === 'off') { - this.devices[msg.target].status = 'on'; - this.emit('bulb-online', this.devices[msg.target]); // deprecated - this.emit('light-online', this.devices[msg.target]); + /** + * Processes a discovery report packet to update internals + * @param {Object} err Error if existant + * @param {Object} msg The discovery report package + * @param {Object} rinfo Remote host details + */ + processDiscoveryPacket(err, msg, rinfo) { + if (err) { + return; + } + if (msg.service === 'udp' && msg.port === constants.LIFX_DEFAULT_PORT) { + // Add / update the found gateway + if (!this.devices[msg.target]) { + const lightDevice = new Light({ + client: this, + id: msg.target, + address: rinfo.address, + port: msg.port, + seenOnDiscovery: this.discoveryPacketSequence + }); + this.devices[msg.target] = lightDevice; + + // Request label + const labelRequest = Packet.create('getLabel', {}, this.source); + labelRequest.target = msg.target; + this.send(labelRequest); + + this.emit('bulb-new', lightDevice); // deprecated + this.emit('light-new', lightDevice); + } else { + if (this.devices[msg.target].status === 'off') { + this.devices[msg.target].status = 'on'; + this.emit('bulb-online', this.devices[msg.target]); // deprecated + this.emit('light-online', this.devices[msg.target]); + } + this.devices[msg.target].address = rinfo.address; + this.devices[msg.target].seenOnDiscovery = this.discoveryPacketSequence; } - this.devices[msg.target].address = rinfo.address; - this.devices[msg.target].seenOnDiscovery = this.discoveryPacketSequence; } // Check if discovery should be stopped @@ -474,222 +477,224 @@ Client.prototype.processDiscoveryPacket = function(err, msg, rinfo) { } } } -}; -/** - * Processes a state label packet to update internals - * @param {Object} err Error if existant - * @param {Object} msg The state label package - */ -Client.prototype.processLabelPacket = function(err, msg) { - if (err) { - return; - } - if (this.devices[msg.target] !== undefined) { - this.devices[msg.target].label = msg.label; + /** + * Processes a state label packet to update internals + * @param {Object} err Error if existant + * @param {Object} msg The state label package + */ + processLabelPacket(err, msg) { + if (err) { + return; + } + if (this.devices[msg.target] !== undefined) { + this.devices[msg.target].label = msg.label; + } } -}; - -/** - * This stops the discovery process - * The client will be no longer updating the state of lights or find lights - */ -Client.prototype.stopDiscovery = function() { - clearInterval(this.discoveryTimer); - 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 - * @param {Function} [callback] Function to handle error and success after send - * @return {Number} The sequence number of the request - */ -Client.prototype.send = function(msg, callback) { - const packet = { - timeCreated: Date.now(), - timeLastSent: 0, - timesSent: 0, - transactionType: constants.PACKET_TRANSACTION_TYPES.ONE_WAY - }; - - // Add the target ip address if target given - if (msg.address !== undefined) { - packet.address = msg.address; - } - if (msg.target !== undefined) { - const targetBulb = this.light(msg.target); - if (targetBulb) { - packet.address = targetBulb.address; - // If we would exceed the max value for the int8 field start over again - if (this.sequenceNumber >= constants.PACKET_HEADER_SEQUENCE_MAX) { - this.sequenceNumber = 0; - } else { - this.sequenceNumber += 1; + /** + * This stops the discovery process + * The client will be no longer updating the state of lights or find lights + */ + stopDiscovery() { + clearInterval(this.discoveryTimer); + this.discoveryTimer = null; + } + + /** + * Checks if all predefined lights are discovered and online + * @return {Boolean} are lights discovered and online + */ + predefinedDiscoveredAndOnline() { + 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 + * @param {Function} [callback] Function to handle error and success after send + * @return {Number} The sequence number of the request + */ + send(msg, callback) { + const packet = { + timeCreated: Date.now(), + timeLastSent: 0, + timesSent: 0, + transactionType: constants.PACKET_TRANSACTION_TYPES.ONE_WAY + }; + + // Add the target ip address if target given + if (msg.address !== undefined) { + packet.address = msg.address; + } + if (msg.target !== undefined) { + const targetBulb = this.light(msg.target); + if (targetBulb) { + packet.address = targetBulb.address; + // If we would exceed the max value for the int8 field start over again + if (this.sequenceNumber >= constants.PACKET_HEADER_SEQUENCE_MAX) { + this.sequenceNumber = 0; + } else { + this.sequenceNumber += 1; + } } } - } - - msg.sequence = this.sequenceNumber; - packet.sequence = this.sequenceNumber; - if (typeof callback === 'function') { - msg.ackRequired = true; - this.addMessageHandler('acknowledgement', callback, msg.sequence); - packet.transactionType = constants.PACKET_TRANSACTION_TYPES.REQUEST_RESPONSE; - } - packet.data = Packet.toBuffer(msg); - - const queueAddress = packet.address; - const messageQueue = this.getMessageQueue(packet.address); - messageQueue.unshift(packet); - this.startSendingProcess(queueAddress); + msg.sequence = this.sequenceNumber; + packet.sequence = this.sequenceNumber; + if (typeof callback === 'function') { + msg.ackRequired = true; + this.addMessageHandler('acknowledgement', callback, msg.sequence); + packet.transactionType = constants.PACKET_TRANSACTION_TYPES.REQUEST_RESPONSE; + } + packet.data = Packet.toBuffer(msg); - return this.sequenceNumber; -}; + const queueAddress = packet.address; + const messageQueue = this.getMessageQueue(packet.address); + messageQueue.unshift(packet); -/** - * Get network address data from connection - * @return {Object} Network address data - */ -Client.prototype.address = function() { - let address = null; - try { - address = this.socket.address(); - } catch (e) {} - return address; -}; + this.startSendingProcess(queueAddress); -/** - * Sets debug on or off at runtime - * @param {Boolean} debug debug messages on - */ -Client.prototype.setDebug = function(debug) { - if (typeof debug !== 'boolean') { - throw new TypeError('LIFX Client setDebug expects boolean as parameter'); + return this.sequenceNumber; } - this.debug = debug; -}; -/** - * Adds a message handler that calls a function when the requested - * info was received - * @param {String} type A type of packet to listen for, like stateLight - * @param {Function} callback the function to call if the packet was received, - * this will be called with parameters msg and rinfo - * @param {Number} [sequenceNumber] Expects a specific sequenceNumber on which will - * be called, this will call it only once. If not - * given the callback handler is permanent - */ -Client.prototype.addMessageHandler = function(type, callback, sequenceNumber) { - if (typeof type !== 'string') { - throw new TypeError('LIFX Client addMessageHandler expects type parameter to be string'); - } - if (typeof callback !== 'function') { - throw new TypeError('LIFX Client addMessageHandler expects callback parameter to be a function'); + /** + * Get network address data from connection + * @return {Object} Network address data + */ + address() { + let address = null; + try { + address = this.socket.address(); + } catch (e) {} + return address; } - const typeName = find(Packet.typeList, {name: type}); - if (typeName === undefined) { - throw new RangeError('LIFX Client addMessageHandler unknown packet type: ' + type); - } - - const handler = { - type: type, - callback: callback.bind(this), - timestamp: Date.now() - }; + /** + * Sets debug on or off at runtime + * @param {Boolean} debug debug messages on + */ + setDebug(debug) { + if (typeof debug !== 'boolean') { + throw new TypeError('LIFX Client setDebug expects boolean as parameter'); + } + this.debug = debug; + } + + /** + * Adds a message handler that calls a function when the requested + * info was received + * @param {String} type A type of packet to listen for, like stateLight + * @param {Function} callback the function to call if the packet was received, + * this will be called with parameters msg and rinfo + * @param {Number} [sequenceNumber] Expects a specific sequenceNumber on which will + * be called, this will call it only once. If not + * given the callback handler is permanent + */ + addMessageHandler(type, callback, sequenceNumber) { + if (typeof type !== 'string') { + throw new TypeError('LIFX Client addMessageHandler expects type parameter to be string'); + } + if (typeof callback !== 'function') { + throw new TypeError('LIFX Client addMessageHandler expects callback parameter to be a function'); + } - if (sequenceNumber !== undefined) { - if (typeof sequenceNumber !== 'number') { - throw new TypeError('LIFX Client addMessageHandler expects sequenceNumber to be a integer'); - } else { - handler.sequenceNumber = sequenceNumber; + const typeName = find(Packet.typeList, {name: type}); + if (typeName === undefined) { + throw new RangeError('LIFX Client addMessageHandler unknown packet type: ' + type); } - } - this.messageHandlers.push(handler); -}; + const handler = { + type: type, + callback: callback.bind(this), + timestamp: Date.now() + }; -/** - * Returns the list of all known lights - * @example client.lights() - * @param {String} [status='on'] Status to filter for, empty string for all - * @return {Array} Lights - */ -Client.prototype.lights = function(status) { - if (status === undefined) { - status = 'on'; - } else if (typeof status !== 'string') { - throw new TypeError('LIFX Client lights expects status to be a string'); + if (sequenceNumber !== undefined) { + if (typeof sequenceNumber !== 'number') { + throw new TypeError('LIFX Client addMessageHandler expects sequenceNumber to be a integer'); + } else { + handler.sequenceNumber = sequenceNumber; + } + } + + this.messageHandlers.push(handler); } - if (status.length > 0) { - if (status !== 'on' && status !== 'off') { - throw new TypeError('Lifx Client lights expects status to be \'on\', \'off\' or \'\''); + /** + * Returns the list of all known lights + * @example client.lights() + * @param {String} [status='on'] Status to filter for, empty string for all + * @return {Array} Lights + */ + lights(status) { + if (status === undefined) { + status = 'on'; + } else if (typeof status !== 'string') { + throw new TypeError('LIFX Client lights expects status to be a string'); } - const result = []; - forEach(this.devices, function(light) { - if (light.status === status) { - result.push(light); + if (status.length > 0) { + if (status !== 'on' && status !== 'off') { + throw new TypeError('Lifx Client lights expects status to be \'on\', \'off\' or \'\''); } - }); - return result; - } - return this.devices; -}; + const result = []; + forEach(this.devices, function(light) { + if (light.status === status) { + result.push(light); + } + }); + return result; + } -/** - * Find a light by label, id or ip - * @param {String} identifier label, id or ip to search for - * @return {Object|Boolean} the light object or false if not found - */ -Client.prototype.light = function(identifier) { - let result; - if (typeof identifier !== 'string') { - throw new TypeError('LIFX Client light expects identifier for LIFX light to be a string'); + return this.devices; } - // There is no ip or id longer than 45 chars, no label longer than 32 bit - if (identifier.length > 45 && Buffer.byteLength(identifier, 'utf8') > 32) { - return false; - } + /** + * Find a light by label, id or ip + * @param {String} identifier label, id or ip to search for + * @return {Object|Boolean} the light object or false if not found + */ + light(identifier) { + let result; + if (typeof identifier !== 'string') { + throw new TypeError('LIFX Client light expects identifier for LIFX light to be a string'); + } + + // There is no ip or id longer than 45 chars, no label longer than 32 bit + if (identifier.length > 45 && Buffer.byteLength(identifier, 'utf8') > 32) { + return false; + } + + // Dots or colons is high likely an ip + if (identifier.indexOf('.') >= 0 || identifier.indexOf(':') >= 0) { + result = find(this.devices, {address: identifier}) || false; + if (result !== false) { + return result; + } + } - // Dots or colons is high likely an ip - if (identifier.indexOf('.') >= 0 || identifier.indexOf(':') >= 0) { - result = find(this.devices, {address: identifier}) || false; + // Search id + result = find(this.devices, {id: identifier}) || false; if (result !== false) { return result; } - } - // Search id - result = find(this.devices, {id: identifier}) || false; - if (result !== false) { + // Search label + result = find(this.devices, {label: identifier}) || false; + return result; } +} - // Search label - result = find(this.devices, {label: identifier}) || false; - - return result; -}; +util.inherits(Client, EventEmitter); exports.Client = Client; From 56bac1238dcddea339a0314b59520559169082b1 Mon Sep 17 00:00:00 2001 From: Ristomatti Airo Date: Sun, 29 Oct 2017 23:48:28 +0200 Subject: [PATCH 4/7] Refactor Light into a class --- src/lifx/light.js | 971 +++++++++++++++++++++++----------------------- 1 file changed, 493 insertions(+), 478 deletions(-) diff --git a/src/lifx/light.js b/src/lifx/light.js index aa63cfd..336e907 100644 --- a/src/lifx/light.js +++ b/src/lifx/light.js @@ -1,532 +1,547 @@ 'use strict'; -const {packet, constants, validate, utils} = require('../lifx'); +const {Packet, constants, validate, utils} = require('../lifx'); const {assign, pick} = 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'); +class Light { + /** + * @param {Object} 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 + */ + constructor(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; + } - const hsbObj = utils.rgbToHsb({r: red, g: green, b: blue}); - this.color(hsbObj.h, hsbObj.s, hsbObj.b, 3500, duration, callback); -}; + /** + * 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 + */ + off(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); + } -/** - * 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'); + /** + * 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 + */ + on(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); } - validate.optionalDuration(duration, 'light colorRgbHex method'); - validate.optionalCallback(callback, 'light colorRgbHex method'); + /** + * 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 + */ + color(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); + } - const rgbObj = utils.rgbHexStringToObject(hexString); - const hsbObj = utils.rgbToHsb(rgbObj); - 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(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 + */ + colorRgb(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); + } -/** - * 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'); + /** + * 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 + */ + colorRgbHex(hexString, duration, callback) { + if (typeof hexString !== 'string') { + throw new TypeError('LIFX light colorRgbHex method expects first parameter hexString to a string'); + } - brightness = Math.round(brightness / constants.IR_MAXIMUM_BRIGHTNESS * 65535); + validate.optionalDuration(duration, 'light colorRgbHex method'); + validate.optionalCallback(callback, 'light colorRgbHex method'); - if (callback !== undefined && typeof callback !== 'function') { - throw new TypeError('LIFX light setMaxIR method expects callback to be a function'); + const rgbObj = utils.rgbHexStringToObject(hexString); + const hsbObj = utils.rgbToHsb(rgbObj); + this.color(hsbObj.h, hsbObj.s, hsbObj.b, 3500, duration, callback); } - const packetObj = packet.create('setInfrared', { - brightness: brightness - }, this.client.source); - packetObj.target = this.id; - this.client.send(packetObj, callback); -}; + /** + * Sets the Maximum Infrared brightness + * @param {Number} brightness infrared brightness from 0 - 100 (in %) + * @param {Function} [callback] called when light did receive message + */ + maxIR(brightness, callback) { + validate.irBrightness(brightness, 'light setMaxIR method'); -/** - * 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); -}; + brightness = Math.round(brightness / constants.IR_MAXIMUM_BRIGHTNESS * 65535); -/** - * 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; - const sqnNumber = this.client.send(packetObj); - this.client.addMessageHandler('stateInfrared', function(err, msg) { - if (err) { - return callback(err, null); + if (callback !== undefined && typeof callback !== 'function') { + throw new TypeError('LIFX light setMaxIR method expects callback to be a function'); } - msg.brightness = Math.round(msg.brightness * (constants.HSBK_MAXIMUM_BRIGHTNESS / 65535)); + const packetObj = Packet.create('setInfrared', { + brightness: brightness + }, this.client.source); + packetObj.target = this.id; + this.client.send(packetObj, callback); + } - callback(null, msg.brightness); - }, sqnNumber); -}; + /** + * Requests the current state of the light + * @param {Function} callback a function to accept the data + */ + getState(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 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 the current maximum setting for the infrared channel + * @param {Function} callback a function to accept the data + */ + getMaxIR(callback) { + validate.callback(callback, 'light getMaxIR method'); + + const packetObj = Packet.create('getInfrared', {}, this.client.source); + packetObj.target = this.id; + const sqnNumber = this.client.send(packetObj); + this.client.addMessageHandler('stateInfrared', function(err, msg) { + if (err) { + return callback(err, null); + } -/** - * 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'); + msg.brightness = Math.round(msg.brightness * (constants.HSBK_MAXIMUM_BRIGHTNESS / 65535)); + + callback(null, msg.brightness); + }, sqnNumber); } - 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'); + /** + * Requests hardware info from the light + * @param {Function} callback a function to accept the data with error and + * message as parameters + */ + getHardwareVersion(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); } - 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); + /** + * Requests uptime from the light + * @param {Function} callback a function to accept the data with error and + * message as parameters + */ + getUptime(callback) { + if (typeof callback !== 'function') { + throw new TypeError('LIFX light getUptime method expects callback to be a function'); } - callback(null, pick(msg, [ - 'majorVersion', - 'minorVersion' - ])); - }, sqnNumber); -}; + 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); + } -/** - * 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); + /** + * Reboots the light + * @param {Function} callback called when light did receive message + */ + reboot(callback) { + if (typeof callback !== 'function') { + throw new TypeError('LIFX light reboot method expects callback to be a function'); } - callback(null, pick(msg, [ - 'signal', - 'tx', - 'rx' - ])); - }, sqnNumber); -}; + 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 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 microcontroller unit of the light + * @param {Function} callback a function to accept the data + */ + getFirmwareVersion(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 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 infos from the microcontroller unit of the light + * @param {Function} callback a function to accept the data + */ + getFirmwareInfo(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 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'); + /** + * Requests wifi infos from for the light + * @param {Function} callback a function to accept the data + */ + getWifiInfo(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); + } - if (cache !== undefined && typeof cache !== 'boolean') { - throw new TypeError('LIFX light getLabel method expects cache to be a boolean'); + /** + * Requests used version from the wifi controller unit of the light (wifi firmware version) + * @param {Function} callback a function to accept the data + */ + getWifiVersion(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); } - if (cache === true) { - if (typeof this.label === 'string' && this.label.length > 0) { - return callback(null, this.label); + + /** + * 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) + */ + getLabel(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'); } - } - 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); + if (cache === true) { + if (typeof this.label === 'string' && this.label.length > 0) { + return callback(null, this.label); + } } - 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'); + 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); } - 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); + /** + * 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 + */ + setLabel(label, callback) { + if (label === undefined || typeof label !== 'string') { + throw new TypeError('LIFX light setLabel method expects label to be a string'); } - 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 (Buffer.byteLength(label, 'utf8') > 32) { + throw new RangeError('LIFX light setLabel method expects a maximum of 32 bytes as label'); } - if (msg.level === 65535) { - msg.level = 1; + if (label.length < 1) { + throw new RangeError('LIFX light setLabel method expects a minimum of one char as label'); } - return callback(null, msg.level); - }, sqnNumber); -}; + validate.optionalCallback(callback, 'light setLabel method'); -/** - * 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) { + 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 + */ + getAmbientLight(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); } - // 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 - }); + return callback(null, msg.flux); }, sqnNumber); - } else { - this.client.addMessageHandler('stateMultiZone', function(err, msg) { + } + + /** + * Requests the power level of the light + * @param {Function} callback a function to accept the data + */ + getPower(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); } - // 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 - }); + if (msg.level === 65535) { + msg.level = 1; + } + return callback(null, msg.level); }, 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); -}; - -exports.Light = Light; + /** + * 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 + */ + getColorZones(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 + */ + colorZones(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); + } +} + +module.exports.Light = Light; From 95e3145160a92f6a001ca20372ead9f39fc8d974 Mon Sep 17 00:00:00 2001 From: Ristomatti Airo Date: Sun, 29 Oct 2017 23:54:05 +0200 Subject: [PATCH 5/7] Unify module.exports use --- src/lifx.js | 4 ++-- src/lifx/client.js | 2 +- src/lifx/light.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lifx.js b/src/lifx.js index f36e381..e205ae0 100644 --- a/src/lifx.js +++ b/src/lifx.js @@ -13,7 +13,7 @@ lifx.utils = require('./lifx/utils'); lifx.Packet = require('./lifx/packet'); // Export light device object -lifx.Light = require('./lifx/light').Light; +lifx.Light = require('./lifx/light'); // Export client -lifx.Client = require('./lifx/client').Client; +lifx.Client = require('./lifx/client'); diff --git a/src/lifx/client.js b/src/lifx/client.js index 3831d24..c8a7c3c 100644 --- a/src/lifx/client.js +++ b/src/lifx/client.js @@ -697,4 +697,4 @@ class Client { util.inherits(Client, EventEmitter); -exports.Client = Client; +module.exports = Client; diff --git a/src/lifx/light.js b/src/lifx/light.js index 336e907..d5d4443 100644 --- a/src/lifx/light.js +++ b/src/lifx/light.js @@ -544,4 +544,4 @@ class Light { } } -module.exports.Light = Light; +module.exports = Light; From 546c1f7c65a067f6cceab183cf262521320dc53f Mon Sep 17 00:00:00 2001 From: Ristomatti Airo Date: Sun, 29 Oct 2017 23:56:54 +0200 Subject: [PATCH 6/7] Rename Client constructor params to options --- src/lifx/light.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lifx/light.js b/src/lifx/light.js index d5d4443..03c0b90 100644 --- a/src/lifx/light.js +++ b/src/lifx/light.js @@ -8,22 +8,22 @@ const {assign, pick} = require('lodash'); */ class Light { /** - * @param {Object} 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 + * @param {Object} options constructor object + * @param {Lifx/Client} options.client the client the light belongs to + * @param {String} options.id the id used to target the light + * @param {String} options.address ip address of the light + * @param {Number} options.port port of the light + * @param {Number} options.seenOnDiscovery on which discovery the light was last seen */ - constructor(constr) { - this.client = constr.client; - this.id = constr.id; // Used to target the light - this.address = constr.address; - this.port = constr.port; + constructor(options) { + this.client = options.client; + this.id = options.id; // Used to target the light + this.address = options.address; + this.port = options.port; this.label = null; this.status = 'on'; - this.seenOnDiscovery = constr.seenOnDiscovery; + this.seenOnDiscovery = options.seenOnDiscovery; } /** From e23561642843bf108f11a268c06002e0c6317fdf Mon Sep 17 00:00:00 2001 From: Ristomatti Airo Date: Mon, 30 Oct 2017 00:16:22 +0200 Subject: [PATCH 7/7] Use arrow functions to avoid explicit bind --- src/lifx/client.js | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/lifx/client.js b/src/lifx/client.js index c8a7c3c..e4c055f 100644 --- a/src/lifx/client.js +++ b/src/lifx/client.js @@ -28,13 +28,13 @@ class Client { this.discoveryPacketSequence = 0; this.messageHandlers = [{ type: 'stateService', - callback: this.processDiscoveryPacket.bind(this) + callback: this.processDiscoveryPacket }, { type: 'stateLabel', - callback: this.processLabelPacket.bind(this) + callback: this.processLabelPacket }, { type: 'stateLight', - callback: this.processLabelPacket.bind(this) + callback: this.processLabelPacket }]; this.sequenceNumber = 0; this.lightOfflineTolerance = 3; @@ -162,15 +162,15 @@ class Client { } } - this.socket.on('error', function(err) { + this.socket.on('error', (err) => { this.isSocketBound = false; console.error('LIFX Client UDP error'); console.trace(err); this.socket.close(); this.emit('error', err); - }.bind(this)); + }); - this.socket.on('message', function(msg, rinfo) { + this.socket.on('message', (msg, rinfo) => { // Ignore own messages and false formats if (utils.getHostIPs().indexOf(rinfo.address) >= 0 || !Buffer.isBuffer(msg)) { return; @@ -202,9 +202,9 @@ class Client { this.emit('message', parsedMsg, rinfo); } - }.bind(this)); + }); - this.socket.bind(opts.port, opts.address, function() { + this.socket.bind(opts.port, opts.address, () => { this.isSocketBound = true; this.socket.setBroadcast(true); this.emit('listening'); @@ -217,7 +217,7 @@ class Client { if (typeof callback === 'function') { return callback(); } - }.bind(this)); + }); } /** @@ -289,13 +289,13 @@ class Client { // Add to the end of the queue again messageQueue.unshift(msg); } else { - this.messageHandlers.forEach(function(handler, hdlrIndex) { + this.messageHandlers.forEach((handler, hdlrIndex) => { if (handler.type === 'acknowledgement' && handler.sequenceNumber === msg.sequence) { this.messageHandlers.splice(hdlrIndex, 1); const err = new Error('No LIFX response after max resend limit of ' + this.resendMaxTimes); return handler.callback(err, null, null); } - }.bind(this)); + }); } } } else { @@ -334,7 +334,7 @@ class Client { */ startDiscovery(lights) { lights = lights || []; - const sendDiscoveryPacket = function() { + const sendDiscoveryPacket = () => { // Sign flag on inactive lights forEach(this.devices, bind(function(info, deviceId) { if (this.devices[deviceId].status !== 'off') { @@ -363,7 +363,7 @@ class Client { } else { this.discoveryPacketSequence += 1; } - }.bind(this); + }; this.discoveryTimer = setInterval( sendDiscoveryPacket, @@ -399,7 +399,7 @@ class Client { if (handler.sequenceNumber === msg.sequence) { // Remove if specific packet was request, since it should only be called once this.messageHandlers.splice(hdlrIndex, 1); - messageQueue.forEach(function(packet, packetIndex) { + messageQueue.forEach((packet, packetIndex) => { if (packet.transactionType === constants.PACKET_TRANSACTION_TYPES.REQUEST_RESPONSE && packet.sequence === msg.sequence) { messageQueue.splice(packetIndex, 1); @@ -434,7 +434,7 @@ class Client { * @param {Object} msg The discovery report package * @param {Object} rinfo Remote host details */ - processDiscoveryPacket(err, msg, rinfo) { + processDiscoveryPacket = (err, msg, rinfo) => { if (err) { return; } @@ -476,21 +476,21 @@ class Client { this.discoveryCompleted = true; } } - } + }; /** * Processes a state label packet to update internals * @param {Object} err Error if existant * @param {Object} msg The state label package */ - processLabelPacket(err, msg) { + processLabelPacket = (err, msg) => { if (err) { return; } if (this.devices[msg.target] !== undefined) { this.devices[msg.target].label = msg.label; } - } + }; /** * This stops the discovery process