Übersicht Shelly BLU

Mit den Funktionsbausteinen Shelly BLU (Button, Door/Window und Motion) werden die Shelly Geräte mit Bluetooth angebunden. Im Unterschied zu den anderen Shelly Geräten besitzen die BLU Geräte kein WLAN und benötigen daher ein Gateway.


Im Empfangsbereich muss ich ein Shelly Plus Gerät mit WLAN befinden. In den Einstellungen muss Bluetooth Gateway aktiviert werden. Zudem muss ein Script hinterlegt werden welches die Bluetooth Meldungen zum MQTT Server weiterleitet. Im Anhang finden Sie ein Beispielscript.

Wichtig: Das Script kann sich zukünftig ändern wenn z.B. neue Geräte hinzukommen oder die Bluetooth Telegramme geändert werden. Die Funktion kann daher für zukünftige Software Versionen der Geräte nicht garantiert werden.


Siehe auch MQTT Allgemein.

Siehe auch allgemeine Parameter aller Funktionsbausteine.

Beispielscript

Dieses Script wurde aus dem Github Repository von Shelly abgeleitet, siehe https://github.com/ALLTERCO/shelly-script-examples (universal-blu-to-mqtt.js)  In zukünftigen Änderungen an der Firmware der Shelly Geräte könnten auch Anpassungen nötig sein.



/**
 * This script uses the BLE scan functionality in scripting
 * Selects Shelly BLU DoorWindow from the aired advertisements, decodes
 * the service data payload and toggles a relay on the device on
 * button push
 
 https://github.com/ALLTERCO/shelly-script-examples/blob/main/ble-shelly-blu.js
 
 */


// CHANGE HERE
function triggerAutomation() {
  print("Window is opened, will toggle the output");
  Shelly.call("Switch.Set", { id: 0, on: true });
}

function printClosed() {
  print("Window is opened, will toggle the output");
  Shelly.call("Switch.Set", { id: 0, on: false });
}

// remove name prefix to not filter by device name
// remove address to not filter by address

let CONFIG = {
  //shelly_blu_name_prefix: 'SBDW',
  //"BIND" to only this address
  shelly_blu_address: "0c:ae:5f:62:b0:ea",
  shelly_blu_topic_prefix: "shelly-blu-",
 
  actions: [
    {
      cond: {
        Window: 0,
      },
      action: triggerAutomation,
    },
    {
      cond: {
        Window: 1,
      },
      action: printClosed,
    },
  ],
};
// END OF CHANGE

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

let ALLTERCO_MFD_ID = JSON.parse("0x" + ALLTERCO_MFD_ID_STR);
let BTHOME_SVC_ID = JSON.parse("0x" + BTHOME_SVC_ID_STR);

let SCAN_DURATION = BLE.Scanner.INFINITE_SCAN;
let ACTIVE_SCAN =  typeof CONFIG.shelly_blu_name_prefix !== "undefined" &&  CONFIG.shelly_blu_name_prefix !== null;

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

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

let BTH = [];
BTH[0x00] = { n: "pid", t: uint8 };
BTH[0x01] = { n: "Battery", t: uint8, u: "%" };
BTH[0x05] = { n: "Illuminance", t: uint24, f: 0.01 };
BTH[0x21] = { n: "motion", t: uint8 };
BTH[0x1a] = { n: "Door", t: uint8 };
BTH[0x20] = { n: "Moisture", t: uint8 };
BTH[0x2d] = { n: "Window", t: uint8 };
BTH[0x3a] = { n: "Button", t: uint8 };
BTH[0x3f] = { n: "Rotation", t: int16, f: 0.1 };
BTH[0x2e] = { n: 'humidity', t: uint8, u: '%' };
BTH[0x45] = { n: "temperature", t: int16, f: 0.1, u: "tC" };
BTH[0x54] = { n: "0x54 unknown", t: uint8 };
BTH[0x30] = { n: "0x30 unknown", t: uint8 };

BTH[0x15] = { n: "Battery-OK", t: uint8 };
BTH[0x16] = { n: "Battery-Charging", t: uint8 };
BTH[0x02] = { n: "Temperature", t: int16, f: 0.01, u: "tC" };
BTH[0x04] = { n: "Pressure", t: uint24, f: 0.01};
BTH[0x03] = { n: "Humidity", t: uint16, f: 0.01, u: "%" };
BTH[0x08] = { n: "Dewpoint", t: uint16, f: 0.01};
BTH[0x14] = { n: "Moisture", t: uint16, f: 0.01};
BTH[0x2f] = { n: "Moisture", t: uint8, f: 1};
BTH[0x12] = { n: "co2", t: uint16};
BTH[0x17] = { n: "co", t: uint8 };
BTH[0x0c] = { n: "Voltage", t: uint16, f: 0.001};
BTH[0x4a] = { n: "Voltage", t: uint16, f: 0.1};
BTH[0x18] = { n: "Cold", t: uint8 };
BTH[0x1c] = { n: "Gas", t: uint8 };
BTH[0x1d] = { n: "Heat", t: uint8 };
BTH[0x1e] = { n: "Light", t: uint8 };
BTH[0x1f] = { n: "Lock", t: uint8 };
BTH[0x1b] = { n: "Garage-Door", t: uint8 };


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) {
       //console.log("BTH: " + buffer.at(0));
      _bth = BTH[buffer.at(0)];
      if (typeof _bth === "undefined") {
        console.log("BTH: unknown type: " + buffer.at(0));
        break;
      }
      buffer = buffer.slice(1);
      _value = this.getBufValue(_bth.t, buffer);
      if (_value === null) break;
      if (typeof _bth.f !== "undefined") _value = _value * _bth.f;
     
      // ---------------------------------------------------
      // --- Old begin
      //result[_bth.n] = _value;
      // --- Old end
      // ---------------------------------------------------
     
      // ---------------------------------------------------
      // --- new begin
      if (typeof result[_bth.n] === "undefined") {
        result[_bth.n] = _value;
      }
      else {
        if (Array.isArray(result[_bth.n])) {
          result[_bth.n].push(_value);
        }
        else {
          result[_bth.n] = [
            result[_bth.n],
            _value
          ];
        }
      }
      // --- new end
      // ---------------------------------------------------
     
     
     
     
      buffer = buffer.slice(getByteSize(_bth.t));
    }
    return result;
  },
};

let ShellyBLUParser = {
  getData: function (res) {
 
    let result = BTHomeDecoder.unpack(res.service_data[BTHOME_SVC_ID_STR]);
    result.addr = res.addr;
    result.rssi = res.rssi;
    return result;
   
  },
};

let last_packet_id = 0x100;
function scanCB(ev, res) {
  if (ev !== BLE.Scanner.SCAN_RESULT) return;
  // skip if there is no service_data member
  if (
    typeof res.service_data === "undefined" ||
    typeof res.service_data[BTHOME_SVC_ID_STR] === "undefined"
  )
    return;
  // skip if we are looking for name match but don't have active scan as we don't have name
  if (typeof CONFIG.shelly_blu_name_prefix !== "undefined" && (typeof res.local_name === "undefined" || res.local_name.indexOf(CONFIG.shelly_blu_name_prefix) !== 0) )
    return;
  // skip if we don't have address match
//  if ( typeof CONFIG.shelly_blu_address !== "undefined" &&  CONFIG.shelly_blu_address !== res.addr  )
//    return;

  let BTHparsed = ShellyBLUParser.getData(res);
  // skip if parsing failed
  if (BTHparsed === null) {
    console.log("Failed to parse BTH data");
    return;
  }
  // skip, we are deduping results
  if (last_packet_id === BTHparsed.pid) return;
  last_packet_id = BTHparsed.pid;

//  if (res.addr == "7c:c6:b6:74:c5:c3")
//    console.log("Shelly BTH packet: ", JSON.stringify(BTHparsed));
   
  if (MQTT.isConnected()==false)
    console.log("MQTT not connected");

  if ( MQTT.publish(CONFIG.shelly_blu_topic_prefix + res.addr, JSON.stringify(BTHparsed), 0, false) == false )
    console.log("MQTT error publish");

   
  //console.log( "MQTT-> " + CONFIG.shelly_blu_topic_prefix + res.addr + ": " + JSON.stringify(BTHparsed));
  //console.log(BTHparsed.Illuminance);

}
console.log("BLE.Scanner.Start");
BLE.Scanner.Start({ duration_ms: SCAN_DURATION, active: ACTIVE_SCAN }, scanCB);