Shelly BLU Button 1
Shelly BLU? Was sind das nun wieder für Geräte. Erst Plus, dann Pro und jetzt BLU? Wie der Name schon vermuten lässt, handelt es sich um Bluetooth-Komponenten. Also verlässt Shelly den bekannten Weg und setzt nicht mehr in jedem Fall auf WiFi / WLAN für die Kommunikation. Das war in der Vergangenheit immer wieder ein Problem. Gerade Batteriebetriebene Geräte müssen sehr komplex umgesetzt werden, damit die Batterie nicht nach wenigen Stunden platt ist. Man bedient sich also jeder Menge Tricks um die Batterielaufzeit zu verlängern. Weiterhin helfen große Akkus in vielen Produkten.
Allerdings ist das aus meiner Sicht nicht der richtige Weg. Nimmt man zum Beispiel einen Button oder Fensterkontakt, dann möchte man die Aktion so schnell wie möglich im Smart Home System auswerten und damit arbeiten. Wenn wir da von 2-3 Sekunden Verzögerung sprechen, fühlt es sich schon nicht mehr gut an. Und so lange kann es schonmal dauern, bis die WLAN-Verbindung aufgebaut wurde, der MQTT-Server kontaktiert wurde und dann erst die paar Byte an Information übertragen wird. Der technische Aufwand und die zu versendende Datenmenge stehen einfach in keinem sinnvollen Verhältnis.
Daher ist es umso schöner, dass Shelly das nun auch erkannt hat und Bluetooth-Geräte auf dem Markt bringt. Und das schöne dabei ist: Jeder Shelly Plus oder Shelly Pro kann als Gateway dienen. Sobald Du also ein paar dieser Geräte verbaut hast (optimimalerweise im ganzen Haus verteilt), sind alle Voraussetzungen erfüllt. Denn diese Geräte basieren auf einem ESP32, welcher automatisch eine Bluetooth-Schnittstelle mitbringt.
Wie genau man das Ganze einrichtet und welche Möglichkeiten sich ergeben, erfährst Du im heutigen Video.
Video
Firmware-Update
Da der Shelly BLU Button 1 mit der Firmware 1.0.2 ein echter Batterie-Killer ist (hält keine 14 Tage durch), muss man ein Update machen. Dafür gibt es eine Android-App im Google Play Store. Leider aktuell wirklich nur für Android und nicht für iOS. Damit konnte ich ein Update auf die Firmware-Version 1.0.5 durchführen. Hoffentlich sind die Probleme damit nun erstmal erledigt!
BLE MQTT publish
Das folgende Script ist nur ein Vorschlag, wie man die Events vom Shelly BLE Button 1 auf MQTT weiterleiten könnte. Dabei wird auf dem Topic shellies/ble
zum Beispiel der folgende Payload gepublished (1x kurz gedrückt):
{
"event": {
"Button": 1,
"Battery": 29,
"pid": 116,
"BTHome_version": 2,
"encryption": false
},
"sender": {
"mac": "b4:35:22:f6:48:31",
"type": "SBBT-002C"
},
"receiver": "shellyplus1-a8032abd007c"
}
// v0.1
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 = {};
let SHELLY_ID = undefined;
BTH[0x00] = { n: "pid", t: uint8 };
BTH[0x01] = { n: "Battery", t: uint8, u: "%" };
BTH[0x05] = { n: "Illuminance", t: uint24, f: 0.01 };
BTH[0x1a] = { n: "Door", t: uint8 };
BTH[0x20] = { n: "Moisture", t: uint8 };
BTH[0x2d] = { n: "Window", t: uint8 };
BTH[0x3a] = { n: "Button", t: uint8 };
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 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") {
console.log("BTH: unknown type");
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;
function bleScanCallback(event, result) {
if (event !== BLE.Scanner.SCAN_RESULT) {
return;
}
if (typeof result.local_name === "undefined" || typeof result.addr === "undefined" || result.local_name.indexOf("SBBT") !== 0) {
return;
}
let servData = result.service_data;
// exit if service data is null/device is encrypted
if (servData === null || typeof servData === "undefined" || typeof servData[BTHOME_SVC_ID_STR] === "undefined") {
console.log("Can't handle encrypted devices");
return;
}
let receivedData = BTHomeDecoder.unpack(servData[BTHOME_SVC_ID_STR]);
// exit if unpacked data is null or the device is encrypted
if (receivedData === null || typeof receivedData === "undefined" || receivedData["encryption"]) {
console.log("Can't handle encrypted devices");
return;
}
// exit if the event is duplicated
if (lastPacketId === receivedData.pid) {
return;
}
lastPacketId = receivedData["pid"];
let message = {
receiver: SHELLY_ID,
sender: {
type: result.local_name,
mac: result.addr
},
event: receivedData
};
console.log("Publishing " + JSON.stringify(message));
if (MQTT.isConnected()) {
MQTT.publish("shellies/ble", JSON.stringify(message));
}
}
function bleScan() {
let bleScanner = BLE.Scanner.Start({
duration_ms: BLE.Scanner.INFINITE_SCAN,
active: true
});
if (bleScanner === false) {
console.log("Error when starting the BLE scanner");
return;
}
BLE.Scanner.Subscribe(bleScanCallback);
console.log("BLE is successfully started");
}
Shelly.call("Mqtt.GetConfig", "", function (res, err_code, err_msg, ud) {
SHELLY_ID = res["topic_prefix"];
});
// Check for BLE config and print a message if BLE is not enabled on the device
let bleConfig = Shelly.getComponentConfig('ble');
if (bleConfig.enable) {
bleScan();
}
Beispiel ioBroker-Script
Und hier noch ein Beispiel Blockly-Script, wie man den Payload vom vorigen Script zerlegen könnte.
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable id="]W,lypbF#)iJs8=/+/Pm">obj</variable>
<variable id="TyTu[i!;Vvq~MWz1::02">deviceType</variable>
<variable id="ReQ,su)ll%UG`x^@t7%`">buttonMac</variable>
<variable id="mJU6BZgNQ.*Gz(y[yf}]">buttonId</variable>
</variables>
<block type="on" id="+C^TOo2VFiVv6CB2OqX?" x="88" y="88">
<field name="OID">0_userdata.0.ble-payload</field>
<field name="CONDITION">any</field>
<field name="ACK_CONDITION"></field>
<statement name="STATEMENT">
<block type="variables_set" id="8XF+u~_%0_zudE?C)(?i">
<field name="VAR" id="]W,lypbF#)iJs8=/+/Pm">obj</field>
<value name="VALUE">
<block type="convert_json2object" id="0C}Tt4nMBEbhe~dDlzqc">
<value name="VALUE">
<block type="on_source" id="eJ=JLq11+]aj2IxI!GfM">
<field name="ATTR">state.val</field>
</block>
</value>
</block>
</value>
<next>
<block type="variables_set" id="c_0ol+b[1CGNWm-I,;-.">
<field name="VAR" id="TyTu[i!;Vvq~MWz1::02">deviceType</field>
<value name="VALUE">
<block type="get_attr" id="9OT0h,A?wtfNu%d;E(8U">
<value name="PATH">
<shadow type="text" id="5GqB*TWU@;;)MNW^~d[=">
<field name="TEXT">sender.type</field>
</shadow>
</value>
<value name="OBJECT">
<block type="variables_get" id=")RVyl~L)uHDmX,8A9hiF">
<field name="VAR" id="]W,lypbF#)iJs8=/+/Pm">obj</field>
</block>
</value>
</block>
</value>
<next>
<block type="controls_if" id="[6Wp~Pla=7L8JUsrwRt(">
<value name="IF0">
<block type="logic_compare" id="s`LC(t+fcH4c82`lC7!G">
<field name="OP">EQ</field>
<value name="A">
<block type="text_getSubstring" id="92I/cmq;=db~=Vo.-ub;">
<mutation at1="false" at2="true"></mutation>
<field name="WHERE1">FIRST</field>
<field name="WHERE2">FROM_START</field>
<value name="STRING">
<block type="variables_get" id="LT9]^nMlM{ywFCiXC7~u">
<field name="VAR" id="TyTu[i!;Vvq~MWz1::02">deviceType</field>
</block>
</value>
<value name="AT2">
<block type="math_number" id="Ows_eKFoGpX-1POscoY(">
<field name="NUM">4</field>
</block>
</value>
</block>
</value>
<value name="B">
<block type="text" id="p;bfqgZyl.EMpH;t]RE_">
<field name="TEXT">SBBT</field>
</block>
</value>
</block>
</value>
<statement name="DO0">
<block type="variables_set" id="zK?3W/9vU9c}i6q%VK_s">
<field name="VAR" id="ReQ,su)ll%UG`x^@t7%`">buttonMac</field>
<value name="VALUE">
<block type="get_attr" id="rbx]wFFoexGh+AOL%hg~">
<value name="PATH">
<shadow type="text" id="-vzvDErW^yUXM]Av{XfT">
<field name="TEXT">sender.mac</field>
</shadow>
</value>
<value name="OBJECT">
<block type="variables_get" id="L}B^P`el22jO#Gmyyu1h">
<field name="VAR" id="]W,lypbF#)iJs8=/+/Pm">obj</field>
</block>
</value>
</block>
</value>
<next>
<block type="variables_set" id="Z3!zTp$qbc2^;S(yMfAm">
<field name="VAR" id="mJU6BZgNQ.*Gz(y[yf}]">buttonId</field>
<value name="VALUE">
<block type="get_attr" id="O.`8q,Q`_.ZptG6(`YyU">
<value name="PATH">
<shadow type="text" id="SRBiT_9p,@aQG+`Qa!cl">
<field name="TEXT">event.Button</field>
</shadow>
</value>
<value name="OBJECT">
<block type="variables_get" id="av^2Z8PK{,3ry@CS2Ji,">
<field name="VAR" id="]W,lypbF#)iJs8=/+/Pm">obj</field>
</block>
</value>
</block>
</value>
<next>
<block type="controls_if" id="j8^2Ap5$xLTT8%p7XFVj">
<mutation elseif="1"></mutation>
<value name="IF0">
<block type="logic_compare" id="BvQa^H4eW+0$,qC,Fmv#">
<field name="OP">EQ</field>
<value name="A">
<block type="variables_get" id="1wVt83Fo2t{J.k~o9H(@">
<field name="VAR" id="ReQ,su)ll%UG`x^@t7%`">buttonMac</field>
</block>
</value>
<value name="B">
<block type="text" id="gyHh,EPxB]k3-2w1K9g;">
<field name="TEXT">b4:35:22:f6:48:31</field>
</block>
</value>
</block>
</value>
<statement name="DO0">
<block type="controls_if" id=",Z)]v%_X5}@IKw#oL[Ts">
<mutation elseif="3"></mutation>
<value name="IF0">
<block type="logic_compare" id="5PViJtqKwUsE77jggPA*">
<field name="OP">EQ</field>
<value name="A">
<block type="variables_get" id="yCspep%7V,0g|qm.4bJ8">
<field name="VAR" id="mJU6BZgNQ.*Gz(y[yf}]">buttonId</field>
</block>
</value>
<value name="B">
<block type="math_number" id="WV@Xcrgk)i`W9uSIp5vF">
<field name="NUM">1</field>
</block>
</value>
</block>
</value>
<statement name="DO0">
<block type="comment" id="Crn~de?yX||xur4pZv()">
<field name="COMMENT">1x kurz</field>
<next>
<block type="debug" id="/8ZVaX[4a$D*jTY=iTN7">
<field name="Severity">log</field>
<value name="TEXT">
<shadow type="text" id="+NHB?ykAI6ZPShw4hmjW">
<field name="TEXT">1x kurz</field>
</shadow>
</value>
</block>
</next>
</block>
</statement>
<value name="IF1">
<block type="logic_compare" id="4!Nfr*!#f):V:Eu,.RA7">
<field name="OP">EQ</field>
<value name="A">
<block type="variables_get" id="$QGfD%|i?akr_sZ|FkdR">
<field name="VAR" id="mJU6BZgNQ.*Gz(y[yf}]">buttonId</field>
</block>
</value>
<value name="B">
<block type="math_number" id="Gy:WZ4|+}tT_NX0Bk)TY">
<field name="NUM">2</field>
</block>
</value>
</block>
</value>
<statement name="DO1">
<block type="comment" id="[NX)^MrLQB?NnA7VCM^5">
<field name="COMMENT">2x kurz</field>
<next>
<block type="debug" id="!pi8*_eoZ^,.Dio@mS]a">
<field name="Severity">log</field>
<value name="TEXT">
<shadow type="text" id="`gEW~X!WJ{TYFB]h;/{H">
<field name="TEXT">2x kurz</field>
</shadow>
</value>
</block>
</next>
</block>
</statement>
<value name="IF2">
<block type="logic_compare" id="Az8Jz^kTIBu,3):C%bMO">
<field name="OP">EQ</field>
<value name="A">
<block type="variables_get" id="-h6s$UcYk#BRV@FZP%9a">
<field name="VAR" id="mJU6BZgNQ.*Gz(y[yf}]">buttonId</field>
</block>
</value>
<value name="B">
<block type="math_number" id="wjFT)1BA36|@A{A}oUX{">
<field name="NUM">3</field>
</block>
</value>
</block>
</value>
<statement name="DO2">
<block type="comment" id=":]HHi#*HZ(9~{$8c?$Y]">
<field name="COMMENT">3x kurz</field>
<next>
<block type="debug" id="7D8QUV$[os=7=`hR1k$x">
<field name="Severity">log</field>
<value name="TEXT">
<shadow type="text" id="Hz#]^:n@]Lby+}Hct.Lx">
<field name="TEXT">3x kurz</field>
</shadow>
</value>
</block>
</next>
</block>
</statement>
<value name="IF3">
<block type="logic_compare" id="S)mGqdtD/[QKqr*t|SEx">
<field name="OP">EQ</field>
<value name="A">
<block type="variables_get" id="KfXAs.v%5-MRrD;UMS|@">
<field name="VAR" id="mJU6BZgNQ.*Gz(y[yf}]">buttonId</field>
</block>
</value>
<value name="B">
<block type="math_number" id="L|(bnG;br,FzKOr-+_Vm">
<field name="NUM">4</field>
</block>
</value>
</block>
</value>
<statement name="DO3">
<block type="comment" id="Wmn1_HFY3f$c_z#mBX0z">
<field name="COMMENT">1x lang</field>
<next>
<block type="debug" id="av@o}fpLw77iN$/;j|w=">
<field name="Severity">log</field>
<value name="TEXT">
<shadow type="text" id="xfq}u1*-0TR*RN!,_2fx">
<field name="TEXT">1x lang</field>
</shadow>
</value>
</block>
</next>
</block>
</statement>
</block>
</statement>
<value name="IF1">
<block type="logic_compare" id="^+iDea73.B,ICZ@2+zEL">
<field name="OP">EQ</field>
<value name="A">
<block type="variables_get" id="y|{(rlUor?{?z@j,52JB">
<field name="VAR" id="ReQ,su)ll%UG`x^@t7%`">buttonMac</field>
</block>
</value>
<value name="B">
<block type="text" id="vfm}PA$?Av@mlqzsT9cp">
<field name="TEXT">...</field>
</block>
</value>
</block>
</value>
</block>
</next>
</block>
</next>
</block>
</statement>
</block>
</next>
</block>
</next>
</block>
</statement>
</block>
</xml>
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.