/** * Universal BLU to MQTT Script * Modified: Added timestamp to MQTT payload for staleness detection */ // *********************** Decoding Method *********************** const uint8 = 0; const int8 = 1; const uint16 = 2; const int16 = 3; const uint24 = 4; const int24 = 5; const BTH = { 0x00: { n: "pid", t: uint8 }, 0x01: { n: "battery", t: uint8, u: "%" }, 0x2C: { n: "vibration", t: uint8 }, 0x3a: { n: "button", t: uint16 }, 0x40: { n: "distance", t: uint16, f: 1, u: "mm" } }; function getByteSize(type) { if (type === uint8 || type === int8) return 1; if (type === uint16 || type === int16) return 2; if (type === uint24 || type === int24) return 3; return 255; } const BTHomeDecoder = { utoi: function (num, bitsz) { let 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; }, unpack: function (buffer) { 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; if (result["encryption"]) return result; buffer = buffer.slice(1); let _bth; let _value; while (buffer.length > 0) { _bth = BTH[buffer.at(0)]; if (typeof _bth === "undefined") { break; } buffer = buffer.slice(1); _value = this.getBufValue(_bth.t, buffer); if (_value === null) break; if (typeof _bth.f !== "undefined") _value = _value * _bth.f; result[_bth.n] = _value; buffer = buffer.slice(getByteSize(_bth.t)); } return result; }, }; // *********************** Main Methods *********************** const BTHOME_SVC_ID_STR = "fcd2"; const SCAN_OPTION = { duration_ms: BLE.Scanner.INFINITE_SCAN, active: false, } function pushToMQ(addr, message) { if (!MQTT.isConnected()) return false; MQTT.publish(addr, message); return true } function scanCB(ev, res) { if (ev !== BLE.Scanner.SCAN_RESULT) return; const addr = 'BLE/'+res.addr; if (typeof res.service_data === 'undefined' || typeof res.service_data[BTHOME_SVC_ID_STR] === 'undefined') return; if (typeof addr === 'undefined') return; try { const decodeData = BTHomeDecoder.unpack(res.service_data[BTHOME_SVC_ID_STR]); // Combine data with timestamp const postMessage = { addr: addr, rssi: res.rssi, local_name: res.local_name || "", service_data: decodeData, timestamp: Date.now(), // Epoch milliseconds from Shelly's clock }; pushToMQ(addr, JSON.stringify(postMessage)); } catch(err) { console.log(err) } } function startBLEScan() { if (!BLE.Scanner.isRunning()) { BLE.Scanner.Start(SCAN_OPTION, scanCB); } else { BLE.Scanner.Subscribe(scanCB); } } startBLEScan()