Created
October 26, 2023 20:10
-
-
Save ansgarm/a4c7f67d9da735ce4929986356ebecae to your computer and use it in GitHub Desktop.
shelly-thermobeacon
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// inspired by: https://github.com/Bluetooth-Devices/thermobeacon-ble/blob/main/src/thermobeacon_ble/parser.py | |
// and: https://github.com/ALLTERCO/shelly-script-examples/blob/main/ble-ruuvi.js | |
let CONFIG = { | |
scan_duration: BLE.Scanner.INFINITE_SCAN, | |
temperature_thr: 18, | |
switch_id: 0, | |
mqtt_topic: "thermobeacon", | |
event_name: "thermobeacon.measurement", | |
}; | |
let THERMOBEACON_MFD_ID = 0x10; | |
//format is subset of https://docs.python.org/3/library/struct.html | |
let packedStruct = { | |
buffer: '', | |
setBuffer: function(buffer) { | |
this.buffer = buffer; | |
}, | |
utoi: function(u16) { | |
return (u16 & 0x8000) ? u16 - 0x10000 : u16; | |
}, | |
getUInt8: function() { | |
return this.buffer.at(0) | |
}, | |
getInt8: function() { | |
let int = this.getUInt8(); | |
if(int & 0x80) int = int - 0x100; | |
return int; | |
}, | |
getUInt16LE: function() { | |
return 0xffff & (this.buffer.at(1) << 8 | this.buffer.at(0)); | |
}, | |
getInt16LE: function() { | |
return this.utoi(this.getUInt16LE()); | |
}, | |
getUInt16BE: function() { | |
return 0xffff & (this.buffer.at(0) << 8 | this.buffer.at(1)); | |
}, | |
getInt16BE: function() { | |
return this.utoi(this.getUInt16BE(this.buffer)); | |
}, | |
unpack: function(fmt, keyArr) { | |
let b = '<>!'; | |
let le = fmt[0] === '<'; | |
if(b.indexOf(fmt[0]) >= 0) { | |
fmt = fmt.slice(1); | |
} | |
let pos = 0; | |
let jmp; | |
let bufFn; | |
let res = {}; | |
while(pos<fmt.length && pos<keyArr.length && this.buffer.length > 0) { | |
jmp = 0; | |
bufFn = null; | |
if(fmt[pos] === 'b' || fmt[pos] === 'B') jmp = 1; | |
if(fmt[pos] === 'h' || fmt[pos] === 'H') jmp = 2; | |
if(fmt[pos] === 'b') { | |
res[keyArr[pos]] = this.getInt8(); | |
} | |
else if(fmt[pos] === 'B') { | |
res[keyArr[pos]] = this.getUInt8(); | |
} | |
else if(fmt[pos] === 'h') { | |
res[keyArr[pos]] = le ? this.getInt16LE() : this.getInt16BE(); | |
} | |
else if(fmt[pos] === 'H') { | |
res[keyArr[pos]] = le ? this.getUInt16LE() : this.getUInt16BE(); | |
} | |
this.buffer = this.buffer.slice(jmp); | |
pos++; | |
} | |
return res; | |
} | |
}; | |
let RuuviParser = { | |
getData: function (res) { | |
let data = BLE.GAP.ParseManufacturerData(res.advData); | |
// Known Sensor | |
if (res.addr !== "be:25:00:00:2f:87") return null; | |
if (typeof data !== "string" || data.length !== 20) return null; | |
packedStruct.setBuffer(data); | |
let hdr = packedStruct.unpack('<H', ['mfd_id']); | |
if(hdr.mfd_id !== THERMOBEACON_MFD_ID) return null; | |
// skip ahead 8 bytes | |
packedStruct.setBuffer(packedStruct.buffer.slice(8)); | |
let rm = packedStruct.unpack('<HhH', ['volt', 'temp16', 'humi16']); | |
let temp = rm.temp16 / 16; | |
let humi = rm.humi16 / 16; | |
let volt = rm.volt; | |
let batt = -1; | |
if (temp > 100 || humi > 100) { | |
return null; | |
} | |
if (volt >= 3000) { | |
batt = 100; | |
} else if (volt >= 2600) { | |
batt = 60 + (volt - 2600) * 0.1; | |
} else if (volt >= 2500) { | |
batt = 40 + (volt - 2500) * 0.2; | |
} else if (volt >= 2450) { | |
batt = 20 + (volt - 2450) * 0.4; | |
} else { | |
batt = 0; | |
} | |
let mm = {batt: batt, temp: temp, humi: humi}; | |
print(mm); | |
return null; | |
/* | |
let rm = packedStruct.unpack('>hHHhhhHBHBBBBBB', [ | |
'temp', | |
'humidity', | |
'pressure', | |
'acc_x', | |
'acc_y', | |
'acc_z', | |
'pwr', | |
'cnt', | |
'sequence', | |
'mac_0','mac_1','mac_2','mac_3','mac_4','mac_5' | |
]); | |
rm.temp = rm.temp * 0.005; | |
rm.humidity = rm.humidity * 0.0025; | |
rm.pressure = rm.pressure + 50000; | |
rm.batt = (rm.pwr >> 5) + 1600; | |
rm.tx = (rm.pwr & 0x001f * 2) - 40; | |
rm.addr = res.addr.slice(0, -2); | |
rm.rssi = res.rssi; | |
return rm; | |
*/ | |
}, | |
}; | |
function publishToMqtt(measurement) { | |
MQTT.publish( | |
CONFIG.mqtt_topic + "/" + measurement.addr, | |
JSON.stringify(measurement) | |
); | |
} | |
function emitOverWs(measurement) { | |
Shelly.emitEvent(CONFIG.event_name, measurement); | |
} | |
function triggerAutomation(measurement) { | |
if (measurement.temp < CONFIG.temperature_thr) { | |
// turn the heater on | |
Shelly.call("Switch.Set", { id: CONFIG.switch_id, on: true }); | |
} | |
} | |
function scanCB(ev, res) { | |
if (ev !== BLE.Scanner.SCAN_RESULT) return; | |
let measurement = RuuviParser.getData(res); | |
if (measurement === null) return; | |
print("ruuvi measurement:", JSON.stringify(measurement)); | |
publishToMqtt(measurement); | |
emitOverWs(measurement); | |
triggerAutomation(measurement); | |
} | |
print("Started"); | |
BLE.Scanner.Start({ duration_ms: CONFIG.scan_duration }, scanCB); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment