Shelly BLU Motion - Bluetooth Bewegungsmelder

Mit ** gekennzeichnete Links auf dieser Seite sind Affiliatelinks.

Shelly BLU Motion - Bluetooth Bewegungsmelder
Shelly BLU Motion - Bluetooth Bewegungsmelder
  • Matthias Kleine
  • 02.09.2023
  • Hardware
  • Produkt-Review

Vor wenigen Monaten habe ich das erste Shelly BLU Gerät vorgestellt: Den Shelly BLU Button 1. Schon damals war klar: Das wird nicht das einzige Gerät der BLU-Reihe bleiben. So wurde zwischendurch ein Shelly BLU Door/Window vorgestellt (dazu habe ich nichts gezeigt) und jetzt eben auch ein kleiner Bewegungsmelder: Der Shelly BLU Motion. Das Gerät ist deutlich kleiner als seine WiFi-Verwandtschaft aus der Shelly-Produktpalette und die Laufzeit der Batterie soll bis zu 5 Jahre betragen. Das konnte ich natürlich nicht testen, aber Bluetooth Low Energy ist natürlich deutlich sparsamer als WiFi.

Video

Hausbau-Kurs

Firmware-Update

Wie schon beim Shelly BLU Button 1 kann ein Firmware-Update durchgeführt werden. Dafür gibt es eine Android-App im Google Play Store. Leider aktuell wirklich nur für Android und nicht für iOS. Im

BLE Script

Wichtig: Leider ist es bis heute nötig, ein eigenes Script auf dem Shelly-Gerät zu erstellen. Schon im Mai 2023 wurde mir zugesichert, dass ein Firmware-Update für die Shellies kommen soll, welche die BLE-Payloads automatisch per MQTT published. Das ist bisher nicht passiert! Daher konnte ich z.B. den ioBroker-Adapter auch nicht in die Richtung erweitern. Ich hoffe da kommt noch etwas in der Richtung!

Hier ein Beispiel-Script, welches den ersten Ausgang des Shellies bei Bewegung schaltet, auf welchem das Script läuft:

// v0.1

/******************* START CHANGE HERE *******************/
let CONFIG = {
    // Specify the destination event where the decoded BLE data will be emitted. It allows for easy identification by other applications/scripts
    eventName: "shelly-blu",

    // 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,

    // The mac address of Shelly BLU Motion
    mac: "",

    onMotionChange: function(motion) {
        Shelly.call("Switch.set", { id: 0, on: motion });
        console.log("Motion", motion);
    },
};
/******************* STOP CHANGE HERE *******************/

let BTHOME_SVC_ID_STR = "fcd2";

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

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 };

// 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);
}

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

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"
    ) {
        logger("Missing service_data member", "Error");
        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;

    console.log("Received " + JSON.stringify(unpackedData));

    if (unpackedData.address === CONFIG.mac && typeof unpackedData.motion !== "undefined") {
        CONFIG.onMotionChange(unpackedData.motion === 1);
    }
}

// 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");
        }
    }

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

init();

Ein BTHome-Payload sieht z.B. wie folgt aus:

{
  "encryption": false,
  "BTHome_version": 2,
  "pid": 57,
  "battery": 100,
  "illuminance": 147,
  "motion": 0,
  "rssi": -58,
  "address": "bc:02:6e:c3:93:c3"
}
Du willst mehr?

Smart-Home-Trainings von A-Z

Steig noch tiefer in die Themen ein und meistere Deine Projekte! Über 13.000 Teilnehmer konnten sich schon von der Qualität der Online-Kurse überzeugen.

ioBroker-Master-Kurs

ioBroker-Master-Kurs

Mehr Infos
Hausbau-Kurs

Hausbau mit KNX

Mehr Infos
Lox-Kurs

Lox-Kurs

Mehr Infos
Node-RED-Master-Kurs

Node-RED-Master-Kurs

Mehr Infos