Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: add a universal-blu-to-mqtt script example #129

Merged
merged 1 commit into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Initial support for Shelly Script comes with firmware version 0.9, September
2021 for Gen2 Shellies based on ESP32.

# Changelog
## 2024-11
- Add a universal BLU to MQTT script
## 2024-06
- Advanced Load shedding with schedules and notifications
- Add a second meter to advanced load shedding with a companion script
Expand Down
5 changes: 5 additions & 0 deletions examples-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -279,5 +279,10 @@
"fname": "ip-assignment-watchdog.js",
"title": "Reboot on DHCP IP assignment issues",
"description": "Monitor for valid IP assignment from DHCP server and reboot if not received within a certain time period."
},
{
"fname": "universal-blu-to-mqtt.js",
"title": "Example - Universal BLU to MQTT Script",
"description": "This script is about shares any BLU product's complete payload to MQTT.."
}
]
184 changes: 184 additions & 0 deletions universal-blu-to-mqtt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* Universal BLU to MQTT Script

Short description

- This script is about shares any BLU product's complete payload to MQTT.

- The script should not require an individual BLU device's MAC address, rather it will "listen" for any BLU device.

- It would use the MAC address to create the topic, with a key-value for each data point in the payload.

Running Process

Here is the summary of how it works:

- Scanning: The script will open a BLU passive scan to receive any BLU device data. Using the BLE.Scanner.Start and BLE.Scanner.Subscribe modules.

- Data Extraction: For each device found, it checks for service data associated with the BTHome standard (Service ID: fcd2).

- Decoding: The BTHomeDecoder decodes the service data into human-readable format, including details such as temperature, battery, etc.

- MQTT Push: The MQTT uses each device MAC address as a topic, then publishes device service_data or any other data you want to the MQTT.

*/


// *********************** Decoding Method ***********************
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 },
0x3a: { n: "button", t: uint8 },
0x3f: { n: "rotation", t: int16, f: 0.1 },
};

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) {
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;
},

// 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) {
_bth = BTH[buffer.at(0)];
if (typeof _bth === "undefined") {
// logger("unknown type", "BTH");
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";

// Bluetooth scan options
const SCAN_OPTION = {
duration_ms: BLE.Scanner.INFINITE_SCAN, // Scan duration
active: false,
}

// Push BLE devices to MQTT
function pushToMQ(addr, message) {
if (!MQTT.isConnected()) return false; // Check the MQTT status

MQTT.publish(addr, message);

return true
}

// BLE scan callback
function scanCB(ev, res) {
if (ev !== BLE.Scanner.SCAN_RESULT) return;
const addr = res.addr; // Get devive's MAC address
if (typeof res.service_data === 'undefined' || typeof res.service_data[BTHOME_SVC_ID_STR] === 'undefined') return;
if (typeof addr === 'undefined') return; // If the device addredd is empty, return

try {
const decodeData = BTHomeDecoder.unpack(res.service_data[BTHOME_SVC_ID_STR]); // Decode service data

// Combine data
const postMessage = {
addr: addr,
rssi: res.rssi,
local_name: res.local_name || "",
service_data: decodeData,
};

// console.log(res.local_name, JSON.stringify(postMessage));

// post data to MQTT
pushToMQ(addr, JSON.stringify(postMessage));

} catch(err) {
console.log(err)
}

}

// Start Scan BLE Devices
function startBLEScan() {
if (!BLE.Scanner.isRunning()) {
BLE.Scanner.Start(SCAN_OPTION, scanCB);
} else {
// If scanner is running, create a subscribe method
BLE.Scanner.Subscribe(scanCB);
}
}

startBLEScan()