Shelly BLU Motion - Bluetooth Bewegungsmelder

Mit ** gekennzeichnete Links auf dieser Seite sind Affiliatelinks.

Shelly BLU Motion - Bluetooth Bewegungsmelder
Shelly BLU Motion - Bluetooth Bewegungsmelder
  • 02.09.2023
  • Level 2
  • 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

ioBroker-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"
}

Transparenz-Hinweis (Level 2)

Für diesen Beitrag wurden mir Produkte kostenfrei zur Verfügung gestellt! Es wurden keinerlei Bedingungen, Richtlinien oder Vorgaben bezüglich der Inhalte, welche ich in meiner Bewertung äußern darf, auferlegt.

Darüber hinaus habe ich keine zusätzliche Vergütung erhalten.

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