UNPKG

lifx-lan-client

Version:

Node.js implementation of the LIFX LAN protocol

351 lines (303 loc) 9.95 kB
'use strict'; const {constants, utils} = require('../lifx'); const packets = require('./packets'); const {result, find, extend, assign} = require('lodash'); /* Package headers 36 bit in total consisting of size - 2 bit frameDescription - 2 bit source - 4 bit target - 6 bit 00 00 - site - 6 bit frameAddressDescription - 1 bit sequence - 1 bit time - 8 bit 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: 'setMultiZoneEffect'}, // {id: 509, name: 'stateEffectZone'}, {id: 816, name: 'getRelayPower'}, {id: 817, name: 'setRelayPower'}, {id: 818, name: 'stateRelayPower'} ]; /** * 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; 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; }; /** * 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); } } } 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; // 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; } if (obj.tagged !== undefined && obj.tagged === true) { frameDescription |= constants.TAGGED_BIT; } if (obj.origin !== undefined && obj.origin === true) { // 0 or 1 to the 14 bit frameDescription |= (1 << 14); } buf.writeUInt16LE(frameDescription, offset); offset += 2; 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'); } } offset += 4; // Frame address if (obj.target !== undefined && obj.target !== null) { buf.write(obj.target, offset, 6, 'hex'); } offset += 6; // reserved1 offset += 2; if (obj.site !== undefined && obj.site !== null) { buf.write(obj.site, offset, 6, 'utf8'); } offset += 6; let frameAddressDescription = 0; if (obj.ackRequired !== undefined && obj.ackRequired === true) { frameAddressDescription |= constants.ACK_REQUIRED_BIT; } if (obj.resRequired !== undefined && obj.resRequired === true) { frameAddressDescription |= constants.RESPONSE_REQUIRED_BIT; } buf.writeUInt8(frameAddressDescription, offset); offset += 1; if (typeof obj.sequence === 'number') { buf.writeUInt8(obj.sequence, offset); } offset += 1; // Protocol header if (obj.time !== undefined) { utils.writeUInt64LE(buf, offset, obj.time); } offset += 8; 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; // 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 ]); } return this.headerToBuffer(obj); } } 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 (obj.type === undefined) { return false; } } else { return false; } obj.size = constants.PACKET_HEADER_SIZE + packets[type].size; if (source !== undefined) { obj.source = source; } if (target !== undefined) { obj.target = target; } if (packets[type].tagged !== undefined) { obj.tagged = packets[type].tagged; } return assign(obj, params); }; module.exports = Packet;