UNPKG

node-red-contrib-xiaomi-ble

Version:
242 lines (209 loc) 9 kB
module.exports = function(RED) { "use strict"; var noble = require('@abandonware/noble'); function XiaomiBleNode(config) { RED.nodes.createNode(this, config); var node = this; node.peripheral = null; node.scanningActive = false; node.stopScanningTimeout = null; node.requestActive = false; function mijiaTemperatureRead(peripheral, payload, send) { var dataCount = 0; // read battery peripheral.readHandle(0x18, function (error, data) { if (error != null) { node.status({fill:"red", shape:"dot", text:"cannot read battery: " + error}); return; } payload.battery = data.toString().charCodeAt(0); if (++dataCount == 2) send(); }); // subscribe for data (temperature+humidity) peripheral.discoverSomeServicesAndCharacteristics(['226c000064764566756266734470666d', 'ebe0ccb07a0a4b0c8a1a6ff2997da3a6'], ['226caa5564764566756266734470666d', 'ebe0ccc17a0a4b0c8a1a6ff2997da3a6'], function(error, services, characteristics) { if (error != null) { node.status({fill:"red", shape:"dot", text:"cannot discover services: " + error}); return; } for (var i = 0; i < characteristics.length; i++) { var chr = characteristics[i]; if (chr.uuid === '226caa5564764566756266734470666d') { var dataFunction = function(data, isNotification) { var result = /T=(\-?\d+\.\d+) H=(\d+\.\d+)/.exec(data.toString()); if (result != null ) { payload.temperature = parseFloat(result[1]); payload.humidity = parseFloat(result[2]); if (++dataCount == 2) send(); } else { node.error('Incorrect data: ' + data); } }; chr.once('data', dataFunction); setTimeout(function() {chr.removeListener('data', dataFunction);}, 30000); // remove handler if no data received chr.subscribe(function(error) { if (error) node.error('Subscribe error: ' + error); }); } else if (chr.uuid === 'ebe0ccc17a0a4b0c8a1a6ff2997da3a6') { var dataFunction = function(data, isNotification) { // Code from https://github.com/jipema/xiaomi-mijia-thermometer/blob/master/XiaomiMijiaThermometer.js {{{{ const prep = typeof data === typeof 's' ? data : JSON.stringify(data.toString('hex')).replace(/\"/gi, ''); const humidity = parseInt(prep.substr(4, 2), 16); const tempRawHex = prep.substr(2, 2) + prep.substr(0, 2); let tempRaw; let isNegative = tempRawHex.substr(0, 1) === 'f'; if (isNegative) { tempRaw = String(parseInt('ffff', 16) - parseInt(tempRawHex, 16)); } else { tempRaw = parseInt(tempRawHex, 16).toString(); } const temperature = (isNegative ? -1 : 1) * parseFloat(tempRaw.substr(0, tempRaw.length - 2) + '.' + tempRaw.substr(tempRaw.length - 2, 2)); // }}}} payload.temperature = temperature; payload.humidity = humidity; if (++dataCount == 2) send(); }; chr.once('data', dataFunction); setTimeout(function() {chr.removeListener('data', dataFunction);}, 30000); // remove handler if no data received chr.subscribe(function(error) { if (error) node.error('Subscribe error: ' + error); }); } } }); } function mifloraRead(peripheral, payload, send) { var dataCount = 0; // read battery peripheral.readHandle(0x038, function (error, data) { if (error != null) { node.status({fill:"red", shape:"dot", text:"cannot read battery: " + error}); return; } payload.battery = data.toString().charCodeAt(0); if (++dataCount == 2) send(); }); // read data peripheral.writeHandle(0x33, new Buffer([0xA0, 0x1F]), false, function (error) { if (error != null) { node.status({fill:"red", shape:"dot", text:"cannot write: " + error}); return; } peripheral.readHandle(0x35, function (error, data) { if (error != null) { node.status({fill:"red", shape:"dot", text:"cannot read data: " + error}); return; } payload.temperature = (256 * data[1] + data[0]) / 10.0; payload.light = 256 * data[4] + data[3]; payload.moisture = data[7]; payload.conductivity = 256 * data[9] + data[8]; if (++dataCount == 2) send(); }); }); } function getData(peripheral, msg) { if (node.requestActive) { node.status({fill:"yellow", shape:"dot", text:"requesting"}); return; } node.status({fill:"green", shape:"dot", text:"requesting"}); node.requestActive = true; var payload = {}; var sent = false; var send = function() { if (!sent) { if (Object.keys(payload).length > 0) { //reuse received message msg.payload = payload; msg.address = peripheral.address; node.send(msg); node.status({}); } else { node.status({fill:"red", shape:"dot", text:"no data"}); } sent = true; node.requestActive = false; peripheral.disconnect(); clearTimeout(disconnectTimeout); peripheral.removeListener('disconnect', send); } } peripheral.once('disconnect', send); var disconnectTimeout = setTimeout(send, 30000); var connectCallback = function(error) { if (error != null) { node.status({fill:"red", shape:"dot", text:"cannot connect: " + error}); node.requestActive = false; clearTimeout(disconnectTimeout); return; } if (peripheral.advertisement.serviceUuids.indexOf('fe95') >= 0) { mifloraRead(peripheral, payload, send); } else { mijiaTemperatureRead(peripheral, payload, send); } }; (peripheral.state !== 'connected') ? peripheral.connect(connectCallback) : connectCallback(); } node.on('input', function(msg) { // if address from message was changed: start scanning var forceScan = 'scan' in msg && msg.scan; var addressChanged = node.peripheral != null && 'address' in msg && msg.address && node.peripheral.address != msg.address.toLowerCase(); if (forceScan || addressChanged) { node.peripheral = null; } if (node.peripheral != null) { getData(node.peripheral, msg); } else if (node.scanningActive) { node.status({fill:"yellow", shape:"dot", text:"searching"}); } else { var address = msg.address || config.address; if (!address) { node.status({fill:"red", shape:"dot", text:"address is not specified"}); return; } node.scanningActive = true; node.status({fill:"green", shape:"dot", text:"searching"}); node.stopScanningTimeout = setTimeout(function() { noble.stopScanning(); }, parseInt(config.scanningTimeout) * 1000); var foundDevices = []; var discover = function(peripheral) { foundDevices.push(peripheral.address); if (peripheral.address === address.toLowerCase()) { node.peripheral = peripheral; noble.removeListener('discover', discover); node.scanningActive = false; getData(node.peripheral, msg); } } noble.on('discover', discover); noble.once('scanStop', function() { noble.removeListener('discover', discover); node.scanningActive = false; if (node.peripheral == null) { node.status({fill:"red", shape:"dot", text:"not found"}); node.error('Device ' + address + ' not found among [' + foundDevices + ']'); } }); if (noble.state === 'poweredOn') { noble.startScanning(); } else { noble.once('stateChange', function(state) { if (state === 'poweredOn') noble.startScanning(); else node.status({fill:"red", shape:"dot", text:"device status: " + state}); }); } } }); this.on('close', function() { if (node.stopScanningTimeout) clearTimeout(node.stopScanningTimeout); noble.stopScanning(); node.status({}); }); } RED.nodes.registerType("Xiaomi BLE", XiaomiBleNode); }