UNPKG

conectric-usb-gateway-beta

Version:
1,207 lines (1,000 loc) 45.4 kB
const usb = require('usb') const serialport = require('serialport'); const Readline = serialport.parsers.Readline; const Joi = require('joi'); const cache = require('memory-cache-ttl'); const moment = require('moment'); const roundTo = require('round-to'); const conectricUsbGateway = { macAddress: undefined, parser: undefined, serialPort: undefined, contikiVersion: undefined, conectricVersion: undefined, nodeModuleVersion: require('./package.json').version, BROADCAST_LOCAL_ADDRESS: 'ffff', BROADCAST_ALL_ADDRESS: '0000', PARITY_NONE: 'none', PARITY_ODD: 'odd', PARTITY_EVEN: 'even', STANDARD_HEADER: 0, EXTENDED_HEADER: 128, MESSAGE_TYPES: { '30': 'tempHumidity', '31': 'switch', '32': 'motion', '36': 'rs485Request', '37': 'rs485Response', '38': 'rs485ChunkRequest', '39': 'rs485ChunkResponse', '40': 'pulse', '41': 'echoStatus', '42': 'rs485ChunkEnvelopeResponse', '43': 'rs485Status', '44': 'moisture', '45': 'tempHumidityLight', '46': 'tempHumidityAdc', '60': 'boot', '61': 'text', '70': 'rs485Config' }, TRACKABLE_MESSAGES: [ '37', // rs485Response '39', // rs485ChunkResponse '42' // r485ChunkEnvelopeResponse ], BROADCAST_MESSAGE_TYPES: [ '30', '31', '32', '37', '39', '40', '41', '42', '43', '44', '45', '46', '60', '61' ], TX_LED_DEFAULT_COLOR: '02', // Red RX_LED_DEFAULT_COLOR: '01', // Green ACTIVITY_LED_DEFAULT_COLOR: '04', // Yellow MOISTURE_ENABLE_ALL_EVENTS: '00', MOISTURE_DISABLE_BECOMES_WET_EVENT: '01', MOISTURE_DISABLE_BECOMES_DRY_EVENT: '02', MOISTURE_DISABLE_ALL_EVENTS: '03', MOTION_ENABLE_ALL_EVENTS: '00', MOTION_DISABLE_ALL_EVENTS: '01', PULSE_ENABLE_ALL_EVENTS: '00', PULSE_DISABLE_ALL_EVENTS: '01', SWITCH_ENABLE_ALL_EVENTS: '00', SWITCH_DISABLE_CLOSE_EVENT: '01', SWITCH_DISABLE_OPEN_EVENT: '02', SWITCH_DISABLE_ALL_EVENTS: '03', TEMP_HUMIDITY_EVENT_CONFIG: '00', PARAM_SCHEMA: Joi.object().keys({ onSensorMessage: Joi.func().required(), onGatewayReady: Joi.func().optional(), sendAdcWithLux: Joi.boolean().optional(), sendRawData: Joi.boolean().optional(), sendRawLux: Joi.boolean().optional(), sendBootMessages: Joi.boolean().optional(), sendStatusMessages: Joi.boolean().optional(), sendDecodedPayload: Joi.boolean().optional(), sendEventCount: Joi.boolean().optional(), useFahrenheitTemps: Joi.boolean().optional(), useMillisecondTimestamps: Joi.boolean().optional(), switchOpenValue: Joi.boolean().optional(), deDuplicateBursts: Joi.boolean().optional(), decodeTextMessages: Joi.boolean().optional(), debugMode: Joi.boolean().optional(), sendHopData: Joi.boolean().optional(), useTrackingId: Joi.boolean().optional(), }).required().options({ allowUnknown: false }), TEXT_MESSAGE_SCHEMA: Joi.object().keys({ message: Joi.string().min(1).max(250).required(), destination: Joi.string().length(4).required() }).required().options({ allowUnknown: false }), RS485_MESSAGE_SCHEMA: Joi.object().keys({ message: Joi.string().min(1).max(250).required(), destination: Joi.string().length(4).required(), hexEncodePayload: Joi.boolean().optional(), trackingId: Joi.string().length(4).regex(/([A-Fa-f0-9]{4})/gi).optional() }).required().options({ allowUnknown: false }), RS485_CHUNKED_MESSAGE_SCHEMA: Joi.object().keys({ chunkNumber: Joi.number().integer().min(0).required(), chunkSize: Joi.number().integer().min(1).required(), destination: Joi.string().length(4).required(), trackingId: Joi.string().length(4).regex(/([A-Fa-f0-9]{4})/gi).optional() }).required().options({ allowUnknown: false }), RS485_CONFIG_MESSAGE_SCHEMA: Joi.object().keys({ baudRate: Joi.number().valid( 2400, 4800, 9600, 19200 ), parity: Joi.string().valid( 'none', 'odd', 'even' ), stopBits: Joi.number().valid( 1, 2 ), bitMask: Joi.number().valid( 7, 8 ), destination: Joi.string().length(4).required() }).required().options({ allowUnknown: false }), EVENT_CONFIG_MESSAGE_SCHEMA: Joi.object().keys({ sensorType: Joi.string().valid( 'moisture', 'motion', 'pulse', 'switch', 'tempHumidity', 'tempHumidityLight' ).required(), sleepTime: Joi.number().integer().min(2).max(60).required(), reportEvery: Joi.number().integer().min(1).max(1440).required(), eventConfig: Joi.string().valid('00', '01', '02', '03').required(), // TODO constrain by value of sensorType. moistureWetReportEvery: Joi.number().integer().min(1).optional(), // TODO required for moisture sensorType otherwise must not be present. deploymentLifetime: Joi.number().integer().min(0).required() }).required().options({ allowUnknown: false }), LED_CONFIG_MESSAGE_SCHEMA: Joi.object().keys({ destination: Joi.string().length(4).required(), sensorType: Joi.string().valid( 'moisture', 'motion', 'pulse', 'switch', 'tempHumidity', 'tempHumidityLight' ).required(), leds: Joi.object().keys({ tx: Joi.boolean().required(), rx: Joi.boolean().required(), activity: Joi.boolean().required() }).required().options({ allowUnknown: false }), deploymentLifetime: Joi.number().integer().min(0).required() }).required().options({ allowUnknown: false }), IGNORABLE_MESSAGE_TYPES: [ '33', '34', '35' ], KNOWN_COMMANDS: [ 'DP', 'MR', 'SS', 'VER' ], runGateway: async function(params) { const validationResult = Joi.validate(params, conectricUsbGateway.PARAM_SCHEMA); if (validationResult.error) { console.error(validationResult.error.message); return; } // sendBootMessages is on by default if (! params.hasOwnProperty('sendBootMessages')) { params.sendBootMessages = true; } // sendDecodedPayload is on by default if (! params.hasOwnProperty('sendDecodedPayload')) { params.sendDecodedPayload = true; } // deDuplicateBursts is on by default if (! params.hasOwnProperty('deDuplicateBursts')) { params.deDuplicateBursts = true; } // decodeTextMessages is on by default if (! params.hasOwnProperty('decodeTextMessages')) { params.decodeTextMessages = true; } // Establish cache if needed. if (params.deDuplicateBursts) { cache.init({ ttl: 30, interval: 3, randomize: false }); } conectricUsbGateway.params = params; conectricUsbGateway.handleUSBEvents(); conectricUsbGateway.startGateway(); }, handleUSBEvents: () => { usb.on('attach', function(device) { if (conectricUsbGateway.isConectricRouter(device)) { console.log('USB Router device attached.'); setTimeout(conectricUsbGateway.startGateway, 200); } }); usb.on('detach', function(device) { if (conectricUsbGateway.isConectricRouter(device)) { console.log('USB Router device removed.'); setTimeout(conectricUsbGateway.startGateway, 100); } }); }, startGateway: async function () { console.log(`Gateway node module version ${conectricUsbGateway.nodeModuleVersion}.`); try { await conectricUsbGateway.findRouterDevice(); console.log(`Found USB router device at ${conectricUsbGateway.comName}.`); } catch(e) { console.log('Waiting for USB router device.'); conectricUsbGateway.macAddress = undefined; conectricUsbGateway.parser = undefined; conectricUsbGateway.serialPort = undefined; conectricUsbGateway.conectricVersion = undefined; conectricUsbGateway.contikiVersion = undefined; return; } conectricUsbGateway.startSerial(); conectricUsbGateway.parser = new Readline(); conectricUsbGateway.serialPort.pipe(conectricUsbGateway.parser); conectricUsbGateway.serialPort.on('open', function() { console.log('Gateway opened.'); }); conectricUsbGateway.serialPort.on('close', function() { console.log('Gateway closed.'); }); conectricUsbGateway.parser.on('data', function(data) { if (data.startsWith('>') && conectricUsbGateway.conectricVersion && conectricUsbGateway.contikiVersion && conectricUsbGateway.macAddress) { // Found a message and we have started up properly. conectricUsbGateway.parseMessage(`${data.substring(1)}`); } else if (data.startsWith('MR:')) { // Found mac address. conectricUsbGateway.macAddress = `${data.substring(3)}`; console.log(`USB router mac address is ${conectricUsbGateway.macAddress}.`); } else if (data === 'DP:Ok') { // Dump buffer was acknowledged OK. console.log('Switched gateway to dump payload mode.'); } else if (data === 'SS:Ok') { // Sink was acknowledged OK. console.log('Switched gateway to sink mode.'); // Notify caller gateway is ready, if interested. if (conectricUsbGateway.params.onGatewayReady) { conectricUsbGateway.params.onGatewayReady(); } } else if (data.toLowerCase().startsWith('ver:contiki')) { conectricUsbGateway.contikiVersion = data.substring(12); console.log(`USB router Contiki version: ${conectricUsbGateway.contikiVersion}`); } else if (data.toLowerCase().startsWith('ver:conectric-v')) { conectricUsbGateway.conectricVersion = data.substring(15); console.log(`USB router Conectric version: ${conectricUsbGateway.conectricVersion}`); } else { if (! conectricUsbGateway.KNOWN_COMMANDS.includes(data)) { if (conectricUsbGateway.params.debugMode) { console.log(`Unprocessed: ${data}`); } } } }); setTimeout(function() { conectricUsbGateway.serialPort.write('DP\n'); setTimeout(function() { conectricUsbGateway.serialPort.write('V'); conectricUsbGateway.serialPort.write('E'); conectricUsbGateway.serialPort.write('R\n'); setTimeout(function() { conectricUsbGateway.serialPort.write('MR\n'); setTimeout(function() { conectricUsbGateway.serialPort.write('SS\n'); }, 500); }, 500); }, 500); // Original: //conectricUsbGateway.serialPort.write('DP\nMR\nVER\nSS\n'); }, 1500); }, isConectricRouter: (device) => { const descriptor = device.deviceDescriptor; if (descriptor) { return (descriptor.idVendor && descriptor.idVendor === 1027 && descriptor.idProduct && descriptor.idProduct === 24597); } return false; }, isBroadcastMessageType: (messageType) => { return conectricUsbGateway.BROADCAST_MESSAGE_TYPES.includes(messageType); }, findRouterDevice: () => { return new Promise((resolve, reject) => { serialport.list((err, ports) => { for (let n = 0; n < ports.length; n++) { const port = ports[n]; const lowerPortName = port.comName.toLowerCase(); if (port.manufacturer && port.manufacturer === 'FTDI' && ( lowerPortName.indexOf('usbserial-') !== -1 || lowerPortName.indexOf('ttyusb') !== -1 || lowerPortName.indexOf('com') !== -1) ) { conectricUsbGateway.comName = port.comName; return resolve(port.comName); } } // No suitable port found. conectricUsbGateway.comName = null; return reject(); }); }); }, startSerial: () => { conectricUsbGateway.serialPort = new serialport(conectricUsbGateway.comName, { baudRate: 230400 }); return conectricUsbGateway.serialPort; }, hexEncode: (message) => { let encodedMessage = ''; for (let n = 0; n < message.length; n++) { let encodedChar = message.charCodeAt(n).toString(16); if (encodedChar.length === 1) { encodedChar = `0${encodedChar}`; } encodedMessage = `${encodedMessage}${encodedChar}`; } return encodedMessage; }, hexDecode: (message) => { let decodedMessage = ''; for (let n = 0; n < message.length; n += 2) { decodedMessage = `${decodedMessage}${String.fromCharCode(parseInt(message.substr(n, 2), 16))}`; } return decodedMessage; }, calculateTemperature: (tempRaw) => { const temperature = roundTo((-46.85 + ((parseInt(tempRaw, 16) / 65536) * 175.72)), 2); // C if (conectricUsbGateway.params.useFahrenheitTemps) { return { temperature: roundTo(((temperature * (9 / 5)) + 32), 2), // F temperatureUnit: 'F' } } else { return { temperature, temperatureUnit: 'C' } message.payload.temperatureUnit = 'C'; } }, calculateHumidity: (humidityRaw) => { return roundTo((-6 + (125 * (parseInt(humidityRaw, 16) / 65536))), 2); // percentage }, sendTextMessage: (params) => { const validationResult = Joi.validate(params, conectricUsbGateway.TEXT_MESSAGE_SCHEMA); if (validationResult.error) { console.error(validationResult.error.message); return false; } let encodedPayload = conectricUsbGateway.hexEncode(params.message); // length: // 1 for the message type // 1 for the length // 2 for the destination // 1 for the reserved part // 1 for each letter in the message let msgLen = 5 + params.message.length; let hexLen = msgLen.toString(16); if (hexLen.length === 1) { hexLen = `0${hexLen}`; } let outboundMessage = `<${hexLen}61${params.destination}01${encodedPayload}`; if (conectricUsbGateway.params.debugMode) { console.log(`Outbound text message: ${outboundMessage}`); } conectricUsbGateway.serialPort.write(`${outboundMessage}\n`); return true; }, _sendRS485Message: (params) => { // hexEncodePayload is on by default if (! params.hasOwnProperty('hexEncodePayload')) { params.hexEncodePayload = true; } let encodedPayload; if (params.hexEncodePayload) { if (conectricUsbGateway.params.debugMode) { console.log('Hex encoding outbound RS485 request message.'); } encodedPayload = conectricUsbGateway.hexEncode(params.message); } else { encodedPayload = params.message; } // length: // 1 for the message type // 1 for the length // 2 for the destination // 1 for the reserved part // 1 for each letter in the message if encoding let msgLen = 5 + (params.hexEncodePayload ? params.message.length : params.message.length / 2); // Add another 2 if trackingId is set. if (params.trackingId) { msgLen += 2; } let hexLen = msgLen.toString(16); if (hexLen.length === 1) { hexLen = `0${hexLen}`; } let outboundMessage = `<${hexLen}${params.msgCode}${params.destination}01${encodedPayload}`; if (params.trackingId) { outboundMessage = `${outboundMessage}${params.trackingId}`; } if (conectricUsbGateway.params.debugMode) { console.log(`Outbound RS485 request: ${outboundMessage}`); } conectricUsbGateway.serialPort.write(`${outboundMessage}\n`); return true; }, sendRS485ChunkRequest: (params) => { const validationResult = Joi.validate(params, conectricUsbGateway.RS485_CHUNKED_MESSAGE_SCHEMA); if (validationResult.error) { console.error(validationResult.error.message); return false; } params.msgCode = 38; let chunkNumberHex = params.chunkNumber.toString(16); if (chunkNumberHex.length === 1) { chunkNumberHex = `0${chunkNumberHex}`; } let chunkSizeHex = params.chunkSize.toString(16); if (chunkSizeHex.length === 1) { chunkSizeHex = `0${chunkSizeHex}`; } params.message = `${chunkNumberHex}${chunkSizeHex}`; params.hexEncodePayload = false; return conectricUsbGateway._sendRS485Message(params); }, getSensorCodeFromType: (sensorType) => { let r = ''; switch (sensorType) { case 'moisture': r = '59'; break; case 'motion': r = '04'; break; case 'pulse': r = '3d'; break; case 'switch': r = '05'; break; case 'tempHumidity': r = '29'; break; case 'tempHumidityLight': r = '5a'; break; } return r; }, convertToLittleEndianHex: (valueToConvert) => { let working = valueToConvert.toString(16); // Pad out to 2 hex bytes while (working.length < 4) { working = `0${working}`; } // Swap the bytes as reportEvery is little endian return `${working.substring(2)}${working.substring(0, 2)}`; }, sendEventConfigMessage: (params) => { const validationResult = Joi.validate(params, conectricUsbGateway.EVENT_CONFIG_MESSAGE_SCHEMA); if (validationResult.error) { console.error(validationResult.error.message); return false; } params.msgCode = '1c'; const destinationSensorType = conectricUsbGateway.getSensorCodeFromType(params.sensorType); let deploymentLifetime = params.deploymentLifetime.toString(16); if (deploymentLifetime.length < 2) { deploymentLifetime = `0${deploymentLifetime}`; } const sleepTime = conectricUsbGateway.convertToLittleEndianHex(params.sleepTime); let reportEvery = conectricUsbGateway.convertToLittleEndianHex(params.reportEvery); let moistureWetReportEvery = '0000'; if (params.sensorType === 'moisture') { moistureWetReportEvery = conectricUsbGateway.convertToLittleEndianHex(params.moistureWetReportEvery); } let msg = `${params.msgCode}000001${deploymentLifetime}c208${destinationSensorType}${sleepTime}${params.eventConfig}00${reportEvery}${moistureWetReportEvery}` let msgLen = Math.round(1 + (msg.length / 2)); // 1 is the length byte. let hexLen = msgLen.toString(16); if (hexLen.length === 1) { hexLen = `0${hexLen}`; } let outboundMessage = `<${hexLen}${msg}`; if (conectricUsbGateway.params.debugMode) { console.log(`Outbound interval config message: ${outboundMessage}`); } conectricUsbGateway.serialPort.write(`${outboundMessage}\n`); return true; }, sendLEDConfigMessage: (params) => { const validationResult = Joi.validate(params, conectricUsbGateway.LED_CONFIG_MESSAGE_SCHEMA); if (validationResult.error) { console.error(validationResult.error.message); return false; } params.msgCode = '1c'; // set here const destinationSensorType = conectricUsbGateway.getSensorCodeFromType(params.sensorType); const txLED = params.leds.tx === true ? conectricUsbGateway.TX_LED_DEFAULT_COLOR : '00'; const rxLED = params.leds.rx === true ? conectricUsbGateway.RX_LED_DEFAULT_COLOR : '00'; const activityLED = params.leds.activity === true ? conectricUsbGateway.ACTIVITY_LED_DEFAULT_COLOR : '00'; let deploymentLifetime = params.deploymentLifetime.toString(16); if (deploymentLifetime.length < 2) { deploymentLifetime = `0${deploymentLifetime}`; } let msg = `${params.msgCode}${params.destination}01${deploymentLifetime}c108${destinationSensorType}07010204${txLED}${rxLED}${activityLED}00` let msgLen = Math.round(1 + (msg.length / 2)); // 1 is the length byte. let hexLen = msgLen.toString(16); if (hexLen.length === 1) { hexLen = `0${hexLen}`; } let outboundMessage = `<${hexLen}${msg}`; if (conectricUsbGateway.params.debugMode) { console.log(`Outbound LED config message: ${outboundMessage}`); } conectricUsbGateway.serialPort.write(`${outboundMessage}\n`); return true; }, sendRS485Request: (params) => { const validationResult = Joi.validate(params, conectricUsbGateway.RS485_MESSAGE_SCHEMA); if (validationResult.error) { console.error(validationResult.error.message); return false; } params.msgCode = 36; return conectricUsbGateway._sendRS485Message(params); }, sendRS485ConfigMessage: (params) => { const validationResult = Joi.validate(params, conectricUsbGateway.RS485_CONFIG_MESSAGE_SCHEMA); if (validationResult.error) { console.error(validationResult.error.message); return false; } let baudRate; switch (params.baudRate) { case 2400: baudRate = '00'; break; case 4800: baudRate = '01'; break; case 9600: baudRate = '02'; break; case 19200: baudRate = '03'; break; } let parity; switch (params.parity) { case conectricUsbGateway.PARITY_NONE: parity = '00'; break; case conectricUsbGateway.PARITY_ODD: parity = '01'; break; case conectricUsbGateway.PARITY_EVEN: parity = '02'; break; } const stopBits = (params.stopBits === 1 ? '00' : '01'); const bitMask = (params.bitMask === 8 ? '00' : '01'); let outboundMessage = `<0970${params.destination}01${baudRate}${parity}${stopBits}${bitMask}`; if (conectricUsbGateway.params.debugMode) { console.log(`Outbound RS485 config message: ${outboundMessage}`); } conectricUsbGateway.serialPort.write(`${outboundMessage}\n`); return true; }, parseMessage: (data) => { const fullMessage = data; let trackingId; if (conectricUsbGateway.params.debugMode) { console.log(fullMessage); } if (conectricUsbGateway.params.useTrackingId) { trackingId = data.substring(data.length - 4); } // Chop off the last 4 which are CRC. data = data.substring(0, data.length - 4); if (conectricUsbGateway.params.debugMode) { console.log(`Removed tracking ID from data, leaving: ${data}`); } // Get to the message type value first so we can drop message // types that are not intended for the end user. const hexHeader = parseInt(data.substring(0, 2), 16); const headerLength = (hexHeader & 31); // 0x1F const headerType = (hexHeader & 128); // 0x80 const payloadType = (hexHeader & 96); // 0x60 // Right now we don't support the extended header, and // we only support the simple payload. if (headerType !== 0) { if (conectricUsbGateway.params.debugMode) { console.log(`Dropping message "${fullMessage}" as messages with extended headers are unsupported in this version.`); } return; } if (payloadType !== 32) { if (conectricUsbGateway.params.debugMode) { console.log(`Dropping message "${fullMessage}" as this payload type is unsupported in this version.`); } return; } const messageType = data.substring(2 + (headerLength * 2), 4 + (headerLength * 2)); if (conectricUsbGateway.IGNORABLE_MESSAGE_TYPES.includes(messageType)) { // Drop this message and do no more work on it. if (conectricUsbGateway.params.debugMode) { console.log(`Dropping message "${fullMessage}" as it is ignorable.`); } return; } const messageTypeString = conectricUsbGateway.MESSAGE_TYPES[messageType]; if (! messageTypeString || messageTypeString.length === 0) { if (conectricUsbGateway.params.debugMode) { console.log(`Ignoring unknown message type "${messageType}".`); } return; } const sourceAddr = data.substring(8, 12); const sequenceNumber = parseInt(data.substring(2, 4), 16); const payloadLength = parseInt(data.substring(0 + (headerLength * 2), 2 + (headerLength * 2)), 16); const battery = parseInt(data.substring(4 + (headerLength * 2), 6 + (headerLength * 2)), 16) / 10; const messageData = data.substring(6 + (headerLength * 2)); // Check if we have cached this message before if (conectricUsbGateway.params.deDuplicateBursts) { const cacheKey = `${sourceAddr}${sequenceNumber}${messageData}`; if (! cache.get(cacheKey)) { // We have not dealt with this burst before. cache.set(cacheKey, true); } else { // We have seen this recently and processed it so drop it. if (conectricUsbGateway.params.debugMode) { console.log(`Dropping message "${data}", already processed message from this burst.`); } return; } } const message = { type: messageTypeString, payload: {}, sensorId: sourceAddr, sequenceNumber }; message.timestamp= (conectricUsbGateway.params.useMillisecondTimestamps ? moment().valueOf() : moment().unix()); if (conectricUsbGateway.params.useTrackingId && conectricUsbGateway.TRACKABLE_MESSAGES.includes(messageType)) { message.trackingId = trackingId; } if (conectricUsbGateway.isBroadcastMessageType(messageType)) { // Broadcast message detected add extra fields. if (conectricUsbGateway.params.debugMode) { console.log(`Message type "${messageType}" is a broadcast message type.`); } if (conectricUsbGateway.params.sendHopData) { message.numHops = parseInt(data.substring(4, 6), 16); message.maxHops = parseInt(data.substring(6, 8), 16); } } else if (conectricUsbGateway.params.debugMode) { console.log(`Message type "${messageType}" is not a broadcast message type.`); } if (conectricUsbGateway.params.sendRawData) { message.rawData = fullMessage; } if (! conectricUsbGateway.params.sendDecodedPayload) { delete(message.payload); } else { switch (messageTypeString) { case 'tempHumidity': let tempRaw let humidityRaw if (messageData.length === 8) { // Older style tempRaw = messageData.substring(0, 4); humidityRaw = messageData.substring(4); } else { // Newer style tempRaw = messageData.substring(10, 14); humidityRaw = messageData.substring(14); if (conectricUsbGateway.params.sendEventCount) { message.payload.eventCount = parseInt(messageData.substring(2, 10), 16) } } message.payload = { ...message.payload, battery, ...conectricUsbGateway.calculateTemperature(tempRaw), humidity: conectricUsbGateway.calculateHumidity(humidityRaw) }; break; case 'tempHumidityAdc': { message.payload.battery = battery; if (conectricUsbGateway.params.sendEventCount) { message.payload.eventCount = parseInt(messageData.substring(2, 10), 16) } const rawTemp = messageData.substring(10, 14); const rawHumidity = messageData.substring(14, 18); const rawAdcMax = messageData.substring(22, 26); const rawAdcIn = messageData.substring(26); message.payload = { ...message.payload, ...conectricUsbGateway.calculateTemperature(rawTemp), humidity: conectricUsbGateway.calculateHumidity(rawHumidity), adcIn: rawAdcIn, adcMax: rawAdcMax } if (conectricUsbGateway.params.debugMode) { console.log(`Raw adc in: ${rawAdcIn}`); console.log(`Raw adc max: ${rawAdcMax}`); console.log(`Raw battery: ${messageData.substring(18, 22)}`); } } break; case 'tempHumidityLight': { message.payload.battery = battery; if (conectricUsbGateway.params.sendEventCount) { message.payload.eventCount = parseInt(messageData.substring(2, 10), 16); } const rawTemp = messageData.substring(10, 14); const rawHumidity = messageData.substring(14, 18); const rawAdcMax = messageData.substring(22, 26); const rawAdcIn = messageData.substring(26); const lux = roundTo(0.003 * Math.pow(parseInt(rawAdcIn, 16), (1.89 - (3.7 - battery) / 25)), 0); let bucketedLux = Math.round(lux / 100); if (bucketedLux > 15) { bucketedLux = 15; } message.payload = { ...message.payload, ...conectricUsbGateway.calculateTemperature(rawTemp), humidity: conectricUsbGateway.calculateHumidity(rawHumidity), bucketedLux }; if (conectricUsbGateway.params.sendRawLux) { message.payload.lux = lux; } if (conectricUsbGateway.params.sendAdcWithLux) { message.payload.adcIn = rawAdcIn; message.payload.adcMax = rawAdcMax; } if (conectricUsbGateway.params.debugMode) { console.log(`Raw adc in: ${rawAdcIn}`); console.log(`Raw adc max: ${rawAdcMax}`); console.log(`Raw battery: ${messageData.substring(18, 22)}`); } } break; case 'moisture': message.payload.battery = battery; // This is a new protocol only sensor, so event count // data will always be present... if (messageData.startsWith('21') || messageData.startsWith('22')) { // This is a status report not an actual event. if (! conectricUsbGateway.params.sendStatusMessages) { // Not sending status message to callback. return; } message.type = 'moistureStatus'; message.payload = { ...message.payload, moisture: messageData.startsWith('21'), ...conectricUsbGateway.calculateTemperature(messageData.substring(10, 14)), humidity: conectricUsbGateway.calculateHumidity(messageData.substring(14)) }; } else { // 81 = event caused because now wet where was dry. // 82 = event caused because now dry where was wet. message.payload.moisture = messageData.startsWith('81'); } if (conectricUsbGateway.params.sendEventCount) { message.payload.eventCount = parseInt(messageData.substring(2, 10), 16) } break; case 'echoStatus': case 'rs485Status': message.payload.battery = battery; if (! conectricUsbGateway.params.sendStatusMessages) { // Not sending status message to callback. return; } // Add eventCount for messages that have it. if (messageData.length === 10 && conectricUsbGateway.params.sendEventCount) { message.payload.eventCount = parseInt(messageData.substring(2), 16) } break; case 'motion': message.payload.battery = battery; if (messageData.startsWith('20')) { // This is a status report not an actual event. if (! conectricUsbGateway.params.sendStatusMessages) { // Not sending status message to callback. return; } message.type = 'motionStatus'; } else { // Only indicate motion for real motion events. message.payload.motion = true; } // Add eventCount for messages that have it. if (messageData.length === 10 && conectricUsbGateway.params.sendEventCount) { message.payload.eventCount = parseInt(messageData.substring(2), 16) } break; case 'pulse': message.payload.battery = battery; if (messageData.startsWith('20')) { // This is a status report not an actual event. if (! conectricUsbGateway.params.sendStatusMessages) { // Not sending status message to callback. return; } message.type = 'pulseStatus'; } else { // Only indicate motion for real pulse events. message.payload.pulse = true; } // Add eventCount for messages that have it. if (messageData.length === 10 && conectricUsbGateway.params.sendEventCount) { message.payload.eventCount = parseInt(messageData.substring(2), 16) } break; case 'switch': message.payload.battery = battery; // Add eventCount for messages that have it. if (messageData.length === 10 && conectricUsbGateway.params.sendEventCount) { message.payload.eventCount = parseInt(messageData.substring(2), 16) } if ((messageData.startsWith('21')) || (messageData.startsWith('22'))) { // This is a status report not an actual event. if (! conectricUsbGateway.params.sendStatusMessages) { // Not sending status message to callback. return; } message.type = 'switchStatus'; message.payload.switch = (conectricUsbGateway.params.switchOpenValue ? (messageData.startsWith('21')) : (messageData.startsWith('22'))); } else { // This is an actual switch event. message.payload.switch = (conectricUsbGateway.params.switchOpenValue ? (messageData.startsWith('81')) : (messageData.startsWith('82'))); } break; case 'boot': if (conectricUsbGateway.params.sendBootMessages) { message.payload.battery = battery; switch (messageData) { case '00': message.payload.resetCause = 'powerOn'; break; case '01': message.payload.resetCause = 'externalReset'; break; case '02': message.payload.resetCause = 'watchdogReset'; break; default: // Unknown message.payload.resetCause = 'unknown'; if (conectricUsbGateway.params.debugMode) { console.error(`Boot message received with unknown reset cause "${messageData}", full message was "${data}".`) } } } else { // Not sending boot message to callback. return; } break; case 'rs485Config': if (messageData.length !== 7) { if (conectricUsbGateway.params.debugMode) { console.error(`Ignoring rs485Config message with payload length ${messageData.length}, was expecting length 7.`); } return; } let baudRate = messageData.substring(0, 2); let parity = messageData.substring(2, 4); let stopBits = messageData.substring(4, 6); let bitMask = messageData.substring(6); switch (baudRate) { case '00': message.payload.baudRate = '2400'; break; case '01': message.payload.baudRate = '4800'; break; case '02': message.payload.baudRate = '9600'; break; case '03': message.payload.baudRate = '19200'; break; default: message.payload.baudRate = '?'; if (conectricUsbGateway.params.debugMode) { console.error(`Invalid baudRate received in rs485Config message, hex was "${baudRate}".`); } } switch (parity) { case '00': message.payload.parity = conectricUsbGateway.PARITY_NONE; break; case '01': message.payload.parity = conectricUsbGateway.PARITY_ODD; break; case '02': message.payload.parity = conectricUsbGateway.PARITY_EVEN; break; default: message.payload.parity = '?'; if (conectricUsbGateway.params.debugMode) { console.error(`Invalid parity received in rs485Config message, hex was "${parity}".`); } } switch (stopBits) { case '00': message.payload.stopBits = 1; break; case '01': message.payload.stopBits = 2; break; default: message.payload.stopBits = -1; if (conectricUsbGateway.params.debugMode) { console.error(`Invalid stopBits received in rs485Config message, hex was "${stopBits}".`); } } switch (bitMask) { case '00': message.payload.bitMask = 8; break; case '01': message.payload.bitMask = 7; break; default: message.payload.bitMask = -1; if (conectricUsbGateway.params.debugMode) { console.error(`Invalid bitMask received in rs485Config message, hex was "${bitMask}".`); } } break; case 'rs485Request': message.payload.data = messageData; break; case 'rs485Response': message.payload.battery = battery; message.payload.rs485 = messageData; break; case 'rs485ChunkEnvelopeResponse': message.payload.battery = battery; message.payload.numChunks = parseInt(messageData.substring(0, 2), 16); message.payload.chunkSize = parseInt(messageData.substring(2), 16); break; case 'rs485ChunkResponse': message.payload.battery = battery; message.payload.data = messageData; break; case 'text': message.payload.battery = battery; if (conectricUsbGateway.params.decodeTextMessages) { message.payload.text = conectricUsbGateway.hexDecode(messageData); } else { message.payload.text = messageData; } break; default: if (conectricUsbGateway.params.debugMode) { console.log(`Ignoring unknown message type "${messageType}"`); } } } conectricUsbGateway.params.onSensorMessage(message); } } module.exports = conectricUsbGateway;