Skip to content

Instantly share code, notes, and snippets.

@ansgarm
Created October 26, 2023 20:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ansgarm/a4c7f67d9da735ce4929986356ebecae to your computer and use it in GitHub Desktop.
Save ansgarm/a4c7f67d9da735ce4929986356ebecae to your computer and use it in GitHub Desktop.
shelly-thermobeacon
// 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