UNPKG

node-red-contrib-shelly

Version:
283 lines (248 loc) 9.69 kB
/** * This script will use BLE observer to listen for advertising data from nearby Shelly BLU devices, * decodes the data using a BTHome data structure, and emits the decoded data for further processing. * * This script DOESN'T execute actions, only emit events. Can be used with `ble-events-handler.js` example. * You can configure the event name, by default its `shelly-blu`, the body of the event contains all the data * parsed from the BLE device * * Represents data provided by each device. * Every value illustrating a sensor reading (e.g., button) may be a singular sensor value or * an array of values if the object has multiple instances. * * @typedef {Object} DeviceData * @property {number} pid - Packet ID. * @property {number} battery - The battery level of the device in percentage (%). * @property {number} rssi - The signal strength in decibels (dB). * @property {string} address - The MAC address of the Shelly BLU device. * @property {string} model - The model of the Shelly BLU device. * @property {number | number[]} [temperature] - The temperature value in degrees Celsius if the device has a temperature sensor. (Can be an array if has multiple instances) * @property {number | number[]} [humidity] - The humidity value in percentage (%) if the device has a humidity sensor. (Can be an array if has multiple instances) * @property {number | number[]} [illuminance] - The illuminance value in lux if the device has a light sensor. (Can be an array if has multiple instances) * @property {number | number[]} [motion] - Motion status: 0 for clear, 1 for motion (if the device has a motion sensor). (Can be an array if has multiple instances) * @property {number | number[]} [window] - Window status: 0 for closed, 1 for open (if the device has a reed switch). (Can be an array if has multiple instances) * @property {number | number[]} [button] - The number of presses if the device has a button. (Can be an array if has multiple instances) * @property {number | number[]} [rotation] - The angle of rotation in degrees if the device has a gyroscope. (Can be an array if has multiple instances) * * @example * {"component":"script:*","name":"script","id":*,"now":*,"info":{"component":"script:*","id":*,"event":"shelly-blu","data":{"encryption":false,"BTHome_version":2,"pid":118,"battery":100,"button":1,"rssi":-76,"address":*},"ts":*}} */ /******************* START CHANGE HERE *******************/ const CONFIG = { // Specify the destination event where the decoded BLE data will be emitted. It allows for easy identification by other applications/scripts eventName: "shelly-blu", // If the script owns the scanner and this value is set to true, the scan will be active. // If the script does not own the scanner, it may remain passive even when set to true. // Active scan means the scanner will ping back the Bluetooth device to receive all its data, but it will drain the battery faster active: false, // When set to true, debug messages will be logged to the console debug: false, includeRawData: true, }; /******************* STOP CHANGE HERE *******************/ const BTHOME_SVC_ID_STR = "fcd2"; const uint8 = 0; const int8 = 1; const uint16 = 2; const int16 = 3; const uint24 = 4; const int24 = 5; // The BTH object defines the structure of the BTHome data const BTH = { 0x00: { n: "pid", t: uint8 }, 0x01: { n: "battery", t: uint8, u: "%" }, 0x02: { n: "temperature", t: int16, f: 0.01, u: "tC" }, 0x03: { n: "humidity", t: uint16, f: 0.01, u: "%" }, 0x05: { n: "illuminance", t: uint24, f: 0.01 }, 0x21: { n: "motion", t: uint8 }, 0x2d: { n: "window", t: uint8 }, 0x2e: { n: "humidity", t: uint8, u: "%" }, 0x3a: { n: "button", t: uint8 }, 0x3f: { n: "rotation", t: int16, f: 0.1 }, 0x45: { n: "temperature", t: int16, f: 0.1, u: "tC" }, }; function getByteSize(type) { if (type === uint8 || type === int8) return 1; if (type === uint16 || type === int16) return 2; if (type === uint24 || type === int24) return 3; //impossible as advertisements are much smaller; return 255; } // functions for decoding and unpacking the service data from Shelly BLU devices const BTHomeDecoder = { utoi: function (num, bitsz) { const mask = 1 << (bitsz - 1); return num & mask ? num - (1 << bitsz) : num; }, getUInt8: function (buffer) { return buffer.at(0); }, getInt8: function (buffer) { return this.utoi(this.getUInt8(buffer), 8); }, getUInt16LE: function (buffer) { return 0xffff & ((buffer.at(1) << 8) | buffer.at(0)); }, getInt16LE: function (buffer) { return this.utoi(this.getUInt16LE(buffer), 16); }, getUInt24LE: function (buffer) { return ( 0x00ffffff & ((buffer.at(2) << 16) | (buffer.at(1) << 8) | buffer.at(0)) ); }, getInt24LE: function (buffer) { return this.utoi(this.getUInt24LE(buffer), 24); }, getBufValue: function (type, buffer) { if (buffer.length < getByteSize(type)) return null; let res = null; if (type === uint8) res = this.getUInt8(buffer); if (type === int8) res = this.getInt8(buffer); if (type === uint16) res = this.getUInt16LE(buffer); if (type === int16) res = this.getInt16LE(buffer); if (type === uint24) res = this.getUInt24LE(buffer); if (type === int24) res = this.getInt24LE(buffer); return res; }, // Unpacks the service data buffer from a Shelly BLU device unpack: function (buffer) { //beacons might not provide BTH service data if (typeof buffer !== "string" || buffer.length === 0) return null; let result = {}; let _dib = buffer.at(0); result["encryption"] = _dib & 0x1 ? true : false; result["BTHome_version"] = _dib >> 5; if (result["BTHome_version"] !== 2) return null; //can not handle encrypted data if (result["encryption"]) return result; buffer = buffer.slice(1); let _bth; let _value; while (buffer.length > 0) { let objectId = buffer.at(0); _bth = BTH[objectId]; if (typeof _bth === "undefined") { console.log("BTH: Unknown type: " + objectId); break; } buffer = buffer.slice(1); _value = this.getBufValue(_bth.t, buffer); if (_value === null) break; if (typeof _bth.f !== "undefined") _value = _value * _bth.f; if (typeof result[_bth.n] === "undefined") { result[_bth.n] = _value; } else { if (Array.isArray(result[_bth.n])) { result[_bth.n].push(_value); } else { result[_bth.n] = [ result[_bth.n], _value ]; } } buffer = buffer.slice(getByteSize(_bth.t)); } return result; }, // converts the service data buffer to a byte array toByteArray: function (buffer) { if (typeof buffer !== "string" || buffer.length === 0) return null; let result = []; for (let i = 0; i < buffer.length; i++){ result.push(buffer.at(i)); } return result; }, }; /** * Еmitting the decoded BLE data to a specified event. It allows other scripts to receive and process the emitted data * @param {DeviceData} data */ function emitData(data) { if (typeof data !== "object") { return; } Shelly.emitEvent(CONFIG.eventName, data); } //saving the id of the last packet, this is used to filter the duplicated packets let lastPacketId = 0x100; // Callback for the BLE scanner object function BLEScanCallback(event, result) { //exit if not a result of a scan if (event !== BLE.Scanner.SCAN_RESULT) { return; } //exit if service_data member is missing if ( typeof result.service_data === "undefined" || typeof result.service_data[BTHOME_SVC_ID_STR] === "undefined" ) { return; } let packedData = result.service_data[BTHOME_SVC_ID_STR]; let unpackedData = BTHomeDecoder.unpack(packedData); //exit if unpacked data is null or the device is encrypted if ( unpackedData === null || typeof unpackedData === "undefined" || unpackedData["encryption"] ) { console.log("Error: Encrypted devices are not supported"); return; } //exit if the event is duplicated if (lastPacketId === unpackedData.pid) { return; } lastPacketId = unpackedData.pid; unpackedData.rssi = result.rssi; unpackedData.address = result.addr; unpackedData.model = result.local_name; if (CONFIG.includeRawData) { let rawData = BTHomeDecoder.toByteArray(packedData); unpackedData.rawData = rawData ; } emitData(unpackedData); } // Initializes the script and performs the necessary checks and configurations function init() { //exit if can't find the config if (typeof CONFIG === "undefined") { console.log("Error: Undefined config"); return; } //get the config of ble component const BLEConfig = Shelly.getComponentConfig("ble"); //exit if the BLE isn't enabled if (!BLEConfig.enable) { console.log( "Error: The Bluetooth is not enabled, please enable it from settings" ); return; } //check if the scanner is already running if (BLE.Scanner.isRunning()) { console.log("Info: The BLE gateway is running, the BLE scan configuration is managed by the device"); } else { //start the scanner const bleScanner = BLE.Scanner.Start({ duration_ms: BLE.Scanner.INFINITE_SCAN, active: CONFIG.active }); if(!bleScanner) { console.log("Error: Can not start new scanner"); } } //subscribe a callback to BLE scanner BLE.Scanner.Subscribe(BLEScanCallback); // disable console.log when logs are disabled if (!CONFIG.debug) { console.log = function() {}; } } init();