Mit den Geräten der Shelly BLU Serie – vom BLU Button über den BLU Button Tough 1 bis hin zu BLU Motion oder dem BLU Door/Window Sensor – kannst du dein Smarthome per Bluetooth flexibel erweitern. Jeder Tastendruck oder Sensor-Event lässt sich in individuelle Aktionen übersetzen, die z. B. Relais schalten, Lampen aktivieren oder Abfragen an andere Geräte senden.
Für die meisten Anwendungsfälle reicht es, eine Szene mit dem Shelly Wizard zu erstellen – das geht schnell und ohne Programmierkenntnisse. Doch Szenen funktionieren in erster Linie innerhalb des Shelly-Ökosystems. Wenn du allerdings auch andere Geräte oder Dienste über REST-APIs ansteuern möchtest, die nicht Teil deines Shelly-Netzwerks sind, stößt du mit Szenen schnell an Grenzen.
Genau hier kommt Shelly Scripting ins Spiel: Mit einem eigenen Script kannst du die BLU Events flexibel auswerten und beliebige HTTP-Requests absetzen – egal ob an einen anderen Shelly, einen ESP32, ein Smarthome-Gateway oder einen Webservice.





In diesem Beitrag zeige ich dir, wie du am Beispiel des Shelly BLU Button ein Script erstellst, das Tastendrücke erkennt und daraufhin beliebige HTTP-Befehle ausführt. Das Script ist universell gehalten, sodass du es mit wenigen Anpassungen auch für andere Shelly BLU Geräte nutzen kannst.
Inhaltsverzeichnis
- Benötigte Hardware
- Shelly BLU verbinden
- Offizielle Vorlage von Shelly
- Wie reagiert man auf Shelly BLU Aktoren wie Button, Button Tough, Door/Window und Motion?
- Wie man den Shelly BLU H&T via BLE ausliest
Benötigte Hardware
Um die Aktionen mit Shelly Scripting umzusetzen, brauchen wir zunächst die passende Hardware:
- Shelly BLU Gerät
Für dieses Beispiel nutze ich den bereits etwas älteren, aber immer noch sehr praktischen Shelly BLU Button 1. Er eignet sich hervorragend zum Testen, da er verschiedene Events wie Single, Double, Triple oder Long Press unterstützt. - Gateway für die BLU-Signale
Damit die Signale des BLU Buttons im Netzwerk ankommen, benötigen wir ein Shelly BLU Gateway. Alternativ kann jedes andere Bluetooth-fähige Shelly Gerät der 2. Generation (oder neuer) als Gateway eingesetzt werden. Das bedeutet: auch ein Shelly Plus Plug S oder ein Shelly Pro Gerät mit aktiviertem BLE kann die Signale aufnehmen und im lokalen Netz verfügbar machen.
Damit haben wir die Grundausstattung, um die BLU-Events zu empfangen und anschließend mit unserem Script weiterzuverarbeiten.
Shelly BLU verbinden
Bevor wir mit dem eigentlichen Script starten, müssen wir den Shelly BLU Button (oder ein anderes BLU-Gerät) mit unserem Gateway koppeln. Dieser Schritt ist wichtig, damit die Signale zuverlässig erkannt und verarbeitet werden.
Eine ausführliche Schritt-für-Schritt-Anleitung zur Kopplung findest du in meinem YouTube Video:
Offizielle Vorlage von Shelly
Die Grundlage für dieses Projekt stammt aus der offiziellen Shelly Script Library. Dort findest du den ursprünglichen Blogbeitrag von Shelly mit einem Beispielscript für die Integration des Shelly BLU Button1:
🔗 Shelly Gen2 BLE Gateway Script for BLU Button1 Integration
In diesem Beitrag gehe ich einen Schritt weiter:
- Das Script ist vollständig kommentiert,
- um einige praktische Funktionen erweitert (z. B. NONE-Konstante, flexible Logging-Funktion, HTTP-Queue mit Retry),
- und lässt sich damit einfach für andere Shelly BLU Geräte (z. B. BLU Motion, BLU Door/Window Sensor oder BLU Button Tough 1) adaptieren.
So profitierst du von der offiziellen Basis, bekommst aber gleichzeitig eine auf Praxis ausgelegte Version, die sofort einsatzbereit ist.
Wie reagiert man auf Shelly BLU Aktoren wie Button, Button Tough, Door/Window und Motion?
In diesem Abschnitt zeige ich, wie du die wichtigsten Shelly BLU Aktoren mit dem Script nutzt. Die Basis ist immer gleich:
- BLU-Frames werden per BTHome v2 empfangen,
- das Script dekodiert die Daten,
- und löst je nach Event HTTP-Aktionen aus (
CONFIG.actions).
Nachfolgend das gesamte Script welches alle drei Geräte behandelt, du musst hier lediglich deine MAC Adresse eintragen. Diese findest du in der Shelly Cloud oder der App Shelly Smart Control unter den Geräteinformationen.
Script für das reagieren auf Events von den BLU Button, Tough, Door/Window und Motion
/***********************************************************************************
* Shelly BLU Button → HTTP-Aktionen (Single/Double/Triple/Long Press)
* ---------------------------------------------------------------------------------
* Getestet auf Shelly Gen2/Gen3 Geräten mit aktivem BLE-Scanner und Scripting.
*
* KONFIGURATION:
* 1) MAC-Adresse des BLU Button (SBBT) eintragen (Kleinbuchstaben, mit Doppelpunkten).
* 2) Pro Event (single/double/triple/long) eine Liste von URLs hinterlegen (GET-Aufrufe).
* 3) Optional: gleichzeitige HTTP-Requests begrenzen, Retry-Optionen setzen.
*
* HINWEISE:
* - Der Shelly BLU Button sendet BTHome v2 (Service UUID 0xFCD2).
* - Button-Event-Codes (BTHome "Button"): 1=Single, 2=Double, 3=Triple, 4=Long.
* - Encrypted-Broadcasts werden ignoriert (BTHome-Flag).
***********************************************************************************/
/* ========================== BENUTZER-KONFIGURATION =========================== */
const NONE = [];
// Zustände für Door/Window & BLU Motion merken
let lastWin = null;
let lastDoor = null;
let lastMotion = null;
const CONFIG = {
bluButtonMac: "3c:2e:f5:bb:c3:d2", // <- MAC-Adresse deines Shelly BLU Button (SBBT), exakt eintragen
actions: {
// BLU Button Zustände
single: [ "http://192.168.178.118/relay/0?turn=on" ],
double: [ "http://192.168.178.118/relay/0?turn=off" ],
triple: NONE,
long: NONE,
// Door/Window Zustände
windowOpen: [ "http://192.168.178.118/relay/0?turn=on" ],
windowClosed: [ "http://192.168.178.118/relay/0?turn=off" ],
doorOpen: NONE,
doorClosed: NONE,
// Motion Events
motionDetected: [ "http://192.168.178.118/relay/0?turn=on" ],
motionCleared: [ "http://192.168.178.118/relay/0?turn=off" ]
},
// Request-Queue / Durchsatzbegrenzung
http: {
maxInFlight: 3, // gleichzeitig laufende Requests
retryCount: 1, // wie oft fehlgeschlagene Requests erneut versucht werden
retryDelayMs: 800, // Wartezeit vor einem Retry
perTickBurst: 3, // wie viele neue Requests pro Tick gestartet werden dürfen
tickMs: 250, // Takt für die Queue-Abarbeitung
timeoutSec: 5, // HTTP GET Timeout je Request
},
// Deduplizierung und Entprellung
dedupe: {
rememberLastPid: true, // BTHome-PID gegen doppelte Pakete prüfen
debounceMs: 350, // Mindestabstand zwischen zwei Events (gleicher Button) in ms
},
// Debug-Ausgaben steuern
logLevel: "info", // "silent" | "info" | "debug"
};
/* ============================================================================ */
/* ============================ KONSTANTEN / MAPPING =========================== */
const BTHOME_SVC_ID = "fcd2"; // Service-UUID (16-bit) als hex-String (Shelly liefert so)
const NAME_PREFIX = "SBBT"; // Shelly BLU Button1 Prefix in local_name
const BTN_MAP = { // BTHome Button-Codes ������� unsere Aktionsnamen
0: "none",
1: "single",
2: "double",
3: "triple",
4: "long",
};
/* ============================================================================ */
/* =============================== LOGGING-HILFE =============================== */
function log(level) {
// Level in Großbuchstaben formatieren
let prefix = level.toUpperCase() + ":";
// Alle weiteren Argumente aus "arguments" zusammensetzen
let msg = "";
for (let i = 1; i < arguments.length; i++) {
msg += " " + arguments[i];
}
// Ausgabe
print(prefix + msg);
}
function nowMs() {
let sys = Shelly.getComponentStatus("sys"); // { uptime: <sekunden>, ... }
let up = (sys && typeof sys.uptime === "number") ? sys.uptime : 0;
return Math.floor(up * 1000);
}
/* ============================================================================ */
/* ============================ EINFACHE REQUEST-QUEUE ========================= */
let queue = [];
let inFlight = 0;
function enqueue(url, attempt) {
queue.push({ url: url, attempt: attempt || 0 });
}
function tickQueue() {
if (inFlight >= CONFIG.http.maxInFlight || queue.length === 0) return;
let started = 0;
while (started < CONFIG.http.perTickBurst &&
inFlight < CONFIG.http.maxInFlight &&
queue.length > 0) {
let job = queue[0]; // erstes Element holen
queue = queue.slice(1); // Queue um eins kürzen
started++;
inFlight++;
Shelly.call(
"HTTP.GET",
{ url: job.url, timeout: CONFIG.http.timeoutSec },
function (res, code, msg, ctx) {
inFlight--;
if (code === 0) {
log("info", "HTTP OK:", ctx.url);
} else {
log("error", "HTTP FAIL:", ctx.url, "code:", code, "msg:", msg);
if (job.attempt < CONFIG.http.retryCount) {
const nextAttempt = job.attempt + 1;
Timer.set(CONFIG.http.retryDelayMs, false, function () {
enqueue(ctx.url, nextAttempt);
tickQueue();
});
}
}
tickQueue();
},
{ url: job.url }
);
}
}
// Queue regelmäßig takten (sanfte Abarbeitung, auch wenn keine Antworten kommen)
Timer.set(CONFIG.http.tickMs, true, tickQueue);
/* ============================================================================ */
/* ============================= BTHOME V2 DECODER ============================ */
/* Minimaler Decoder für BTHome v2 ohne Verschlüsselung.
Nur was wir benötigen (PID + Button + optional Battery etc.). */
const SZ = { u8:1, i8:1, u16:2, i16:2, u24:3, i24:3 };
const T = { u8:0, i8:1, u16:2, i16:3, u24:4, i24:5 };
const BTH = {
0x00: { n:"pid", t:T.u8 },
0x01: { n:"battery", t:T.u8 },
0x05: { n:"illuminance",t:T.u24, f:0.01 },
0x1a: { n:"door", t:T.u8 },
0x20: { n:"moisture", t:T.u8 },
0x2d: { n:"window", t:T.u8 }, // für den Shelly Door/Window
0x21: { n:"motion", t:T.u8 }, // für den Shelly BLU Motion
0x3a: { n:"button", t:T.u8 }, // den den Shelly BLU Button
};
function utoi(num, bits) {
const mask = 1 << (bits - 1);
return (num & mask) ? (num - (1 << bits)) : num;
}
function getLE(buf, size) {
if (buf.length < size) return null;
let v = 0;
for (let i = size - 1; i >= 0; i--) v = (v << 8) | buf.at(i);
return v >>> 0;
}
function readTyped(t, buf) {
if (t === T.u8) return buf.length >= 1 ? buf.at(0) : null;
if (t === T.i8) return buf.length >= 1 ? utoi(buf.at(0), 8) : null;
if (t === T.u16) return utoi(getLE(buf, 2), 16) & 0xFFFF;
if (t === T.i16) return utoi(getLE(buf, 2), 16);
if (t === T.u24) return getLE(buf, 3) & 0xFFFFFF;
if (t === T.i24) return utoi(getLE(buf, 3), 24);
return null;
}
function bthomeUnpack(sd /* string */) {
if (typeof sd !== "string" || sd.length === 0) return null;
const dib = sd.at(0);
const encrypted = !!(dib & 0x01);
const ver = dib >> 5;
if (ver !== 2) return null; // nur V2
if (encrypted) return { encryption: true };
let buf = sd.slice(1);
let out = { encryption: false, version: 2 };
while (buf.length > 0) {
const id = buf.at(0);
const spec = BTH[id];
if (!spec) {
log("debug", "BTH: unbekannter Typ:", id);
break; // unbekannte Felder überspringen → Abbruch
}
buf = buf.slice(1);
const need = (spec.t === T.u8 || spec.t === T.i8) ? SZ.u8 :
(spec.t === T.u16 || spec.t === T.i16) ? SZ.u16 :
SZ.u24;
const raw = readTyped(spec.t, buf);
if (raw === null) break;
let val = raw;
if (typeof spec.f === "number") val = val * spec.f;
out[spec.n] = val;
buf = buf.slice(need);
}
return out;
}
/* ============================================================================ */
/* ============================ EVENT-HANDLING / BLE =========================== */
let lastPid = -1;
let lastEventAt = { single:0, double:0, triple:0, long:0 };
function handleButtonEvent(action) {
const urls = CONFIG.actions[action];
// NONE oder leeres Array → keine Aktion
if (!urls || urls.length === 0) {
log("info", "Keine URLs f��r Event:", action);
return;
}
// alle URLs in die Queue legen
for (let i = 0; i < urls.length; i++) enqueue(urls[i], 0);
tickQueue();
}
function onBleScan(event, res) {
if (event !== BLE.Scanner.SCAN_RESULT) return;
if (!res || res.addr !== CONFIG.bluButtonMac) return;
if (typeof res.local_name !== "string") return; // Prefix-Check optional
const sd = res.service_data;
if (!sd || typeof sd[BTHOME_SVC_ID] === "undefined") {
log("debug", "Kein BTHome Service (evtl. verschlüsselt?)");
return;
}
const data = bthomeUnpack(sd[BTHOME_SVC_ID]);
if (!data || data.encryption) {
log("debug", "Verschlüsselte oder ungültige Daten, übersprungen.");
return;
}
// Dedupe per PID (nur wenn vorhanden)
if (CONFIG.dedupe.rememberLastPid && typeof data.pid === "number") {
if (data.pid === lastPid) {
log("debug", "Duplikat (PID)", data.pid, "→ ignoriert");
return;
}
lastPid = data.pid;
}
// Dispatch: erst Button, dann Door/Window, dann Motion
var handled = false;
handled = handleBluButtonData(data) || handled;
handled = handleDoorWindowData(data) || handled;
handled = handleMotionData(data) || handled;
if (!handled) {
log("debug", "BTHome-Daten empfangen, kein passender Handler:", JSON.stringify(data));
}
}
function debounceEvent(action) {
const now = nowMs();
if (CONFIG.dedupe.debounceMs > 0 && lastEventAt[action] && (now - lastEventAt[action]) < CONFIG.dedupe.debounceMs) {
log("debug", "Entprellt:", action);
return false; // Event verwerfen
}
lastEventAt[action] = now;
return true; // Event zulassen
}
function handleBluButtonData(data) {
if (typeof data.button !== "number") return false; // kein Button-Datensatz
// Button-Code (1..4) → Aktion
const action = BTN_MAP[data.button];
if (!action) {
log("debug", "Unbekannter Button-Wert:", data.button);
return false;
}
if (action === "none") return true;
// Entprellung pro Event-Typ
if (!debounceEvent(action)) return true;
log("info", "Button-Event:", action, "PID:", data.pid, "Battery:", data.battery);
handleButtonEvent(action);
return true;
}
function handleDoorWindowData(data) {
var emitted = false;
if (typeof data.window === "number") {
// Nur bei Zustandswechsel auslösen
if (lastWin !== data.window) {
lastWin = data.window;
if (data.window === 1) handleButtonEvent("windowOpen");
else handleButtonEvent("windowClosed");
log("info", "Window:", data.window === 1 ? "OPEN" : "CLOSED", "Lux:", data.illuminance);
}
emitted = true;
}
if (!emitted && typeof data.door === "number") {
if (lastDoor !== data.door) {
lastDoor = data.door;
if (data.door === 1) handleButtonEvent("doorOpen");
else handleButtonEvent("doorClosed");
log("info", "Door:", data.door === 1 ? "OPEN" : "CLOSED", "Lux:", data.illuminance);
}
emitted = true;
}
return emitted;
}
function handleMotionData(data) {
if (typeof data.motion !== "number") return false; // kein Motion-Datensatz
// Nur bei Zustandswechsel auslösen
if (lastMotion !== data.motion) {
lastMotion = data.motion;
if (data.motion === 1) {
// Optional: nur bei Dunkelheit reagieren (z.B. < 20 Lux)
// if (typeof data.illuminance === "number" && data.illuminance > 20) return true;
handleButtonEvent("motionDetected");
log("info", "Motion: DETECTED", "Lux:", data.illuminance);
} else {
handleButtonEvent("motionCleared");
log("info", "Motion: CLEARED", "Lux:", data.illuminance);
}
}
return true;
}
function startBle() {
const cfg = Shelly.getComponentConfig("ble");
if (!cfg || cfg.enable !== true) {
log("info", "BLE ist auf dem Gerät deaktiviert – bitte unter Settings aktivieren.");
return;
}
const ok = BLE.Scanner.Start({ duration_ms: BLE.Scanner.INFINITE_SCAN, active: true });
if (!ok) {
log("info", "BLE-Scanner konnte nicht gestartet werden.");
return;
}
BLE.Scanner.Subscribe(onBleScan);
log("info", "BLE-Scanner läuft… (warte auf SBBT Events)");
}
/* ============================================================================ */
/* ================================== START =================================== */
(function init() {
// Basiskontrollen
if (typeof CONFIG.bluButtonMac !== "string" || CONFIG.bluButtonMac.length !== 17) {
log("info", "Konfiguration fehlerhaft: bluButtonMac fehlt/ungültig.");
return;
}
if (!CONFIG.actions) {
log("info", "Konfiguration fehlerhaft: actions-Objekt fehlt.");
return;
}
startBle();
})();
Wie reagiert man auf den Shelly BLU Button & Button Tough?
Events
single,double,triple,long(Button-Werte 1–4)- Der Tough sendet zusätzlich häufig Keepalive-Frames mit
button: 0(kein Ereignis) – die ignoriert das Script still.
Konfiguration (CONFIG.actions)
actions: {
single: [ "http://192.168.1.118/relay/0?turn=on" ],
double: [ "http://192.168.1.118/relay/0?turn=off" ],
triple: NONE, // bewusst keine Aktion
long: NONE
}
Tipps
- Entprellung:
CONFIG.dedupe.debounceMs(z. B. 350–600 ms) verhindert Doppelauslösungen. - Idle-Frames (
button:0) vom Tough: im Handler bereits abgefangen → kein Log-Spam.
Wie reagiert man auf den Shelly BLU Door/Window Sensor?
Der Kontakt meldet Zustandswechsel (offen/geschlossen) – je nach Firmware als window oder door (0/1).
Events & Actions
actions: {
windowOpen: [ "http://192.168.1.40/rpc/Switch.Set?id=0&on=true" ],
windowClosed: [ "http://192.168.1.40/rpc/Switch.Set?id=0&on=false" ],
doorOpen: NONE,
doorClosed: NONE
}
Was macht das Script?
- Erkennt
windowoderdoorautomatisch. - Löst nur bei Zustandswechsel aus (interner Speicher
lastWin/lastDoor).
Optional
illuminance(Lux) wird mitgeloggt. Du kannst leicht eine Bedingung hinzufügen, z. B. „nur bei offen und < 20 Lux Licht an“.
Wie reagiert man auf den Shelly BLU Motion Sensor?
Meldet Bewegung (motion: 0/1) und meist auch Helligkeit (illuminance).
Events & Actions
actions: {
motionDetected: [ "http://192.168.1.50/rpc/Switch.Set?id=0&on=true" ],
motionCleared: NONE // oft durch Szene/Controller mit Nachlauf gelöst
}
Empfohlene Logik
- Script feuert bei Bewegung erkannt sofort.
- „Aus“ macht ein Zeit-Nachlauf im Ziel (Szene, Lampe, Controller).
- Optional: nur bei Dunkelheit schalten (Lux-Schwelle prüfen).
Zustandslogik
- Interner Speicher
lastMotionverhindert doppeltes Feuern. - Debug-Log zeigt „Motion: DETECTED/CLEARED“ inkl. Lux.
Wie man den Shelly BLU H&T via BLE ausliest
Der Shelly BLU H&T sendet per BTHome v2 regelmäßig Temperatur und Luftfeuchte als BLE-Beacons. Wir hören diese Frames am Shelly (Gen2/Gen3, BLE aktiviert) passiv mit, dekodieren sie und loggen die Werte. Optional kannst du Schwellwerte (mit Hysterese) definieren und bei „zu warm/zu kalt/zu feucht/zu trocken“ URLs triggern.
Copy-&-Paste Script (H&T, minimal & robust)
/************* KONFIGURATION *************/
const NONE = [];
const CONFIG = {
bluMac: "38:39:8f:8c:7d:f2", // <- MAC deines Shelly BLU H&T (klein, mit Doppelpunkten)
// Optional: Aktionen bei Grenzwertwechseln (URLs per GET)
actions: {
tempAbove: NONE, // z.B. Klimaanlage an
tempBelow: NONE, // z.B. Heizung an
tempNormal: NONE, // zurück in Normalbereich
humAbove: NONE, // z.B. Entfeuchter an
humBelow: NONE, // z.B. Befeuchter an
humNormal: NONE
},
// Schwellwerte + Hysterese (Flattern vermeiden)
sensors: {
ht: {
temp: { low: 18.0, high: 26.0, hyst: 0.5 }, // °C
hum: { low: 30.0, high: 70.0, hyst: 3.0 } // % rF
}
},
// Optionales „Pushen“ der Rohwerte als HTTP GET (Query-Params)
push: {
enabled: false,
urlBase: "http://192.168.1.10/ht", // ergibt: /ht?temp=...&hum=...&bat=...
timeoutSec: 5
},
logLevel: "info" // "silent" | "info" | "debug"
};
/******************************************/
/************* LOGGING *************/
function log(level) {
// level: "info","warn","error","debug"
var allow = (CONFIG.logLevel === "debug") ||
(CONFIG.logLevel === "info" && level !== "debug") ||
(CONFIG.logLevel === "silent" ? false : true);
if (!allow) return;
var s = level.toUpperCase() + ":";
for (var i = 1; i < arguments.length; i++) s += " " + arguments[i];
print(s);
}
/***********************************/
/************* BTHome v2 Decoder (minimal) *************/
const BTHOME_SVC_ID = "fcd2";
const SZ = { u8:1, i8:1, u16:2, i16:2, u24:3, i24:3 };
const T = { u8:0, i8:1, u16:2, i16:3, u24:4, i24:5 };
const BTH = {
0x00: { n:"pid", t:T.u8 },
0x01: { n:"battery", t:T.u8 },
0x02: { n:"temperature", t:T.i16, f:0.01 }, // °C
0x03: { n:"humidity", t:T.u8, f:0.5 } // %rF
};
function utoi(num, bits){ var m=1<<(bits-1); return (num&m)?(num-(1<<bits)):num; }
function getLE(buf, size){
if (buf.length < size) return null;
var v=0; for (var i=size-1;i>=0;i--) v=(v<<8)|buf.at(i);
return v>>>0;
}
function readTyped(t, buf){
if (t===T.u8) return buf.length>=1 ? buf.at(0) : null;
if (t===T.i8) return buf.length>=1 ? utoi(buf.at(0),8) : null;
if (t===T.u16) return utoi(getLE(buf,2),16) & 0xFFFF;
if (t===T.i16) return utoi(getLE(buf,2),16);
if (t===T.u24) return getLE(buf,3) & 0xFFFFFF;
if (t===T.i24) return utoi(getLE(buf,3),24);
return null;
}
function bthomeUnpack(sd){
if (typeof sd!=="string" || sd.length===0) return null;
var dib = sd.at(0);
var enc = !!(dib & 0x01);
var ver = dib >> 5;
if (ver!==2) return null;
if (enc) return { encryption:true };
var buf = sd.slice(1);
var out = { encryption:false, version:2 };
while (buf.length>0){
var id = buf.at(0), spec = BTH[id];
if (!spec) break; // unbekannt → abbrechen (minimal)
buf = buf.slice(1);
var need = (spec.t===T.u8||spec.t===T.i8)?SZ.u8:(spec.t===T.u16||spec.t===T.i16)?SZ.u16:SZ.u24;
var raw = readTyped(spec.t, buf); if (raw===null) break;
var val = (typeof spec.f==="number") ? raw*spec.f : raw;
out[spec.n] = val;
buf = buf.slice(need);
}
return out;
}
/******************************************************/
/************* STATES *************/
var lastPid = -1;
var lastTemp = null, lastHum = null;
var tempState = "unknown"; // "low" | "normal" | "high"
var humState = "unknown";
/**********************************/
/************* OPTIONAL: Werte pushen *************/
function pushValues(t, h, bat){
if (!CONFIG.push || !CONFIG.push.enabled) return;
var url = CONFIG.push.urlBase + "?temp=" + t + "&hum=" + h + "&bat=" + bat;
Shelly.call("HTTP.GET", { url:url, timeout: (CONFIG.push.timeoutSec||5) },
function(res, code, msg, ctx){
if (code===0) log("debug","PUSH OK", ctx.url);
else log("warn","PUSH FAIL", ctx.url, msg);
},
{ url:url }
);
}
/**************************************************/
/************* GRENZWERTE + HYSTERESE *************/
function evalTemp(t){
var cfg = CONFIG.sensors && CONFIG.sensors.ht ? CONFIG.sensors.ht.temp : null;
if (!cfg) return;
var lowOn = cfg.low, lowOff = cfg.low + cfg.hyst;
var highOn = cfg.high, highOff = cfg.high - cfg.hyst;
if (tempState==="unknown"){
tempState = (t<=lowOn) ? "low" : (t>=highOn) ? "high" : "normal";
return;
}
if (tempState==="normal"){
if (t<=lowOn){ tempState="low"; triggerAction("tempBelow"); log("info","Temp LOW:", t); }
else if (t>=highOn){ tempState="high"; triggerAction("tempAbove"); log("info","Temp HIGH:", t); }
} else if (tempState==="low" && t>=lowOff){
tempState="normal"; triggerAction("tempNormal"); log("info","Temp NORMAL:", t);
} else if (tempState==="high" && t<=highOff){
tempState="normal"; triggerAction("tempNormal"); log("info","Temp NORMAL:", t);
}
}
function evalHum(h){
var cfg = CONFIG.sensors && CONFIG.sensors.ht ? CONFIG.sensors.ht.hum : null;
if (!cfg) return;
var lowOn = cfg.low, lowOff = cfg.low + cfg.hyst;
var highOn = cfg.high, highOff = cfg.high - cfg.hyst;
if (humState==="unknown"){
humState = (h<=lowOn) ? "low" : (h>=highOn) ? "high" : "normal";
return;
}
if (humState==="normal"){
if (h<=lowOn){ humState="low"; triggerAction("humBelow"); log("info","Hum LOW:", h); }
else if (h>=highOn){ humState="high"; triggerAction("humAbove"); log("info","Hum HIGH:", h); }
} else if (humState==="low" && h>=lowOff){
humState="normal"; triggerAction("humNormal"); log("info","Hum NORMAL:", h);
} else if (humState==="high" && h<=highOff){
humState="normal"; triggerAction("humNormal"); log("info","Hum NORMAL:", h);
}
}
/**************************************************/
/************* Aktionen auslösen (ohne Queue) *************/
function triggerAction(key){
var urls = CONFIG.actions ? CONFIG.actions[key] : null;
if (!urls || urls.length===0) return;
for (var i=0;i<urls.length;i++){
(function(url){
Shelly.call("HTTP.GET", { url:url, timeout:5 },
function(res, code, msg, ctx){
if (code===0) log("info","HTTP OK:", ctx.url);
else log("error","HTTP FAIL:", ctx.url, msg);
},
{ url:url }
);
})(urls[i]);
}
}
/**************************************************/
/************* BLE-Scan *************/
function onBleScan(ev, res){
if (ev !== BLE.Scanner.SCAN_RESULT) return;
if (!res || res.addr !== CONFIG.bluMac) return;
var sd = res.service_data;
if (!sd || typeof sd[BTHOME_SVC_ID]==="undefined"){
log("debug","Kein BTHome Service");
return;
}
var data = bthomeUnpack(sd[BTHOME_SVC_ID]);
if (!data || data.encryption) return;
// Duplikate (PID) filtern
if (typeof data.pid==="number"){
if (data.pid===lastPid) return;
lastPid = data.pid;
}
// Werte lesen
if (typeof data.temperature==="number") lastTemp = data.temperature;
if (typeof data.humidity==="number") lastHum = data.humidity;
log("info","H&T:", "T="+lastTemp+"°C", "H="+lastHum+"%", "Bat="+data.battery+"%");
// Optional pushen
pushValues(lastTemp, lastHum, data.battery);
// Grenzwerte bewerten
if (typeof lastTemp==="number") evalTemp(lastTemp);
if (typeof lastHum==="number") evalHum(lastHum);
}
function startBle(){
var cfg = Shelly.getComponentConfig("ble");
if (!cfg || cfg.enable!==true){ log("warn","BLE ist deaktiviert."); return; }
var ok = BLE.Scanner.Start({ duration_ms: BLE.Scanner.INFINITE_SCAN, active:true });
if (!ok){ log("error","BLE-Scanner konnte nicht gestartet werden."); return; }
BLE.Scanner.Subscribe(onBleScan);
log("info","BLE-Scanner läuft (H&T) …");
}
startBle();
Letzte Aktualisierung am: 10. Oktober 2025




