Skip to content

Commit

Permalink
Blu motion example (#64)
Browse files Browse the repository at this point in the history
* Add motion example

* Add to manifest

* Add to readme

* Compare processed mac addresses

* Cast message to string
  • Loading branch information
taulfsime authored Sep 14, 2023
1 parent a7e36e3 commit 59ffd34
Show file tree
Hide file tree
Showing 3 changed files with 317 additions and 0 deletions.
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
## 2023-09
- Shelly BLU Motion script example
## 2023-08
- Telegram interaction with Shelly script
## 2023-06
Expand Down
310 changes: 310 additions & 0 deletions ble-shelly-motion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
/******************* START CHANGE HERE *******************/
let CONFIG = {
// When set to true, debug messages will be logged to the console
debug: false,

// When set to true and the script ownes the scanner, the scan will be active.
// 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 `allowedMacAddresses` is set to null, evets from every bluetooth device are accepted.
// allowedMacAddresses: null,
allowedMacAddresses: [
"aa:bc:12:34:56:78", // events only from these mac addresses are allowed.
"11:22:33:45:5a:bc",
],

/**
* Called when motion is reported from the filtered Shelly BLU Motion devices.
* @param {Boolean} motion true, when there is a motion, false otherwise.
* @param {Object} eventData Object, containing all parameters received from the Shelly BLU Motion device. Example: {"encryption":false,"BTHome_version":2,"pid":16,"battery":100,"illuminance":109,"motion":1,"button":1,"rssi":-53,"address":"aa:bc:12:34:56:78"}
*/
motionHandler: function (motion, eventData) {
// Toggle the first replay ON/OFF based on the motion value.
Shelly.call("Switch.Set", { id: 0, on: motion });
console.log("Motion", motion);
},

/**
* Called when illuminance is reported from the filtered Shelly BLU Motion devices.
* @param {Number} illuminance Current illuminance value.
* @param {Object} eventData Object, containing all parameters received from the Shelly BLU Motion device. Example: {"encryption":false,"BTHome_version":2,"pid":16,"battery":100,"illuminance":109,"motion":1,"button":1,"rssi":-53,"address":"aa:bc:12:34:56:78"}
*/
illuminanceHandler: function (illuminance, eventData) {
// Compile the topic based on the mac address of the reporter.
let topic = eventData.address + "/illuminance";

// Publush the data.
MQTT.publish(topic, String(illuminance));
},

/**
* Called when packet from filtered Shelly BLU Motion devices is received.
* @param {Object} eventData Object, containing all parameters received from the Shelly BLU Motion device. Example: {"encryption":false,"BTHome_version":2,"pid":16,"battery":100,"illuminance":109,"motion":1,"button":1,"rssi":-53,"address":"aa:bc:12:34:56:78"}
*/
onStatusUpdate: function (eventData) {
// Do nothing at the moment.
}
};
/******************* STOP CHANGE HERE *******************/

let ALLTERCO_MFD_ID_STR = "0ba9";
let BTHOME_SVC_ID_STR = "fcd2";

let uint8 = 0;
let int8 = 1;
let uint16 = 2;
let int16 = 3;
let uint24 = 4;
let int24 = 5;

//Logs the provided message with an optional prefix to the console.
function logger(message, prefix) {
//exit if the debug isn't enabled
if (!CONFIG.debug) {
return;
}

let finalText = "";

//if the message is list loop over it
if (Array.isArray(message)) {
for (let i = 0; i < message.length; i++) {
finalText = finalText + " " + JSON.stringify(message[i]);
}
} else {
finalText = JSON.stringify(message);
}

//the prefix must be string
if (typeof prefix !== "string") {
prefix = "";
} else {
prefix = prefix + ":";
}

//log the result
console.log(prefix, finalText);
}

// The BTH object defines the structure of the BTHome data
let BTH = {};
BTH[0x00] = { n: "pid", t: uint8 };
BTH[0x01] = { n: "battery", t: uint8, u: "%" };
BTH[0x02] = { n: "temperature", t: int16, f: 0.01, u: "tC" };
BTH[0x03] = { n: "humidity", t: uint16, f: 0.01, u: "%" };
BTH[0x05] = { n: "illuminance", t: uint24, f: 0.01 };
BTH[0x21] = { n: "motion", t: uint8 };
BTH[0x2d] = { n: "window", t: uint8 };
BTH[0x3a] = { n: "button", t: uint8 };
BTH[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
let 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;
},
};

function onReceivedPacket (data) {
if(CONFIG._processedMacAddresses !== null) {
if(CONFIG._processedMacAddresses.indexOf(data.address) < 0) {
logger(["Received event from", data.address, "outside of the allowed addresses"], "Info");
return;
}
}

if (
typeof CONFIG.motionHandler === "function" &&
typeof data.motion !== "undefined"
) {
CONFIG.motionHandler(data.motion === 1, data);
logger("Motion handler called", "Info");
}

if (
typeof CONFIG.illuminanceHandler === "function" &&
typeof data.illuminance !== "undefined"
) {
CONFIG.illuminanceHandler(data.illuminance, data);
logger("Illuminance handler called", "Info");
}

if (typeof CONFIG.onStatusUpdate === "function") {
CONFIG.onStatusUpdate(data);
logger("New status update", "Info");
}
}

//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 unpackedData = BTHomeDecoder.unpack(
result.service_data[BTHOME_SVC_ID_STR]
);

//exit if unpacked data is null or the device is encrypted
if (
unpackedData === null ||
typeof unpackedData === "undefined" ||
unpackedData["encryption"]
) {
logger("Encrypted devices are not supported", "Error");
return;
}

//exit if the event is duplicated
if (lastPacketId === unpackedData.pid) {
return;
}

lastPacketId = unpackedData.pid;

unpackedData.rssi = result.rssi;
unpackedData.address = result.addr;

onReceivedPacket(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
let 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
let bleScanner = BLE.Scanner.Start({
duration_ms: BLE.Scanner.INFINITE_SCAN,
active: CONFIG.active
});

if(!bleScanner) {
console.log("Error: Can not start new scanner");
}
}

if (
typeof CONFIG.allowedMacAddresses !== "undefined"
) {
if(CONFIG.allowedMacAddresses !== null) {
// Process configured mac addresses all to lower case and remove duplicates.
CONFIG._processedMacAddresses =
CONFIG
.allowedMacAddresses
.map(function (mac) { return mac.toLowerCase(); })
.filter(function (value, index, array) { return array.indexOf(value) === index; })
}
else {
CONFIG._processedMacAddresses = null;
}
}

//subscribe a callback to BLE scanner
BLE.Scanner.Subscribe(BLEScanCallback);
}

init();
5 changes: 5 additions & 0 deletions examples-manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
[
{
"fname": "ble-shelly-motion.js",
"title": "BLE in Scripting - Shelly BLU Motion script actions",
"description": "Example how to use a Shelly BLU Motion, read advertising data, decode and turn on switch. (Requires firmware version\n0.12.0-beta1 or newer)"
},
{
"fname": "telegram-interaction.js",
"title": "Telegram interaction with Shelly script",
Expand Down

0 comments on commit 59ffd34

Please sign in to comment.