Skip to content

Technik Blog

Programmieren | Arduino | ESP32 | MicroPython | Python | Raspberry Pi | Raspberry Pi Pico

Menu
  • Smarthome
  • Gartenautomation
  • Mikrocontroller
    • Arduino
    • ESP32 & Co.
    • Calliope Mini
    • Raspberry Pi & Pico
  • Solo Mining
  • Deutsch
  • English
Menu

Shelly Scripting – Shelly BLU Aktionen programmieren

Veröffentlicht am 2. Oktober 202510. Oktober 2025 von Stefan Draeger

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.

Shelly Scripting – Shelly BLU Aktionen programmieren
Dieses Video auf YouTube ansehen.

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.

Shelly BLU Button Tough 1
Shelly BLU Button Tough 1
Shelly Blu Button 1
Shelly Blu Button 1
Shelly BLU Motion
Shelly BLU Motion
Shelly BLU Door / Window
Shelly BLU Door / Window
Shelly BLU H&T
Shelly BLU H&T

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 reagiert man auf den Shelly BLU Button & Button Tough?
    • Wie reagiert man auf den Shelly BLU Door/Window Sensor?
    • Wie reagiert man auf den Shelly BLU Motion Sensor?
  • Wie man den Shelly BLU H&T via BLE ausliest
    • Copy-&-Paste Script (H&T, minimal & robust)

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:

Shelly BLU Gerät in der Shelly Smart Control App einrichten
Dieses Video auf YouTube ansehen.

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.

ermitteln der MAC-Adresse eines Shelly BLU Gerätes
ermitteln der MAC-Adresse eines Shelly BLU Gerätes
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.
Shelly BLU Button - Ausgabe der Aktionen für einen einfachen und doppelten Klick
Shelly BLU Button – Ausgabe der Aktionen für einen einfachen und doppelten Klick

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 window oder door automatisch.
  • 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“.
Shelly BLU Door/Window – Ausgabe der Aktionen für Fenster offen/geschlossen

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 lastMotion verhindert doppeltes Feuern.
  • Debug-Log zeigt „Motion: DETECTED/CLEARED“ inkl. Lux.
Shelly BLU Motion - Ausgabe der Events für eine Bewegung sowie der Quittierung der selbigen
Shelly BLU Motion – Ausgabe der Events für eine Bewegung sowie der Quittierung der selbigen

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.

Shelly H&T – Ausgabe der Sensordaten via BLE in der Console

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

Foto von Stefan Draeger
Über den Autor

Stefan Draeger — Entwickler & Tech-Blogger

Ich zeige praxisnah, wie du Projekte mit Arduino, ESP32 und Smarthome-Komponenten umsetzt – Schritt für Schritt, mit Code und Schaltplänen.

Mehr Artikel von Stefan →

Schreibe einen Kommentar Antwort abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Fragen oder Feedback?

Du hast eine Idee, brauchst Hilfe oder möchtest Feedback loswerden?
Support-Ticket erstellen

Newsletter abonnieren

Bleib auf dem Laufenden: Erhalte regelmäßig Updates zu neuen Projekten, Tutorials und Tipps rund um Arduino, ESP32 und mehr – direkt in dein Postfach.

Jetzt Newsletter abonnieren

Unterstütze meinen Blog

Wenn dir meine Inhalte gefallen, freue ich mich über deine Unterstützung auf Tipeee.
So hilfst du mit, den Blog am Leben zu halten und neue Beiträge zu ermöglichen.

draeger-it.blog auf Tipeee unterstützen

Vielen Dank für deinen Support!
– Stefan Draeger

Kategorien

Tools

  • Unix-Zeitstempel-Rechner
  • ASCII Tabelle
  • Spannung, Strom, Widerstand und Leistung berechnen
  • Widerstandsrechner
  • 8×8 LED Matrix Tool
  • 8×16 LED Matrix Modul von Keyestudio
  • 16×16 LED Matrix – Generator

Links

Blogverzeichnis Bloggerei.de TopBlogs.de das Original - Blogverzeichnis | Blog Top Liste Blogverzeichnis trusted-blogs.com

Stefan Draeger
Königsberger Str. 13
38364 Schöningen
Tel.: 015565432686
E-Mail: info@draeger-it.blog

Folge mir auf

link zu Fabook
link zu LinkedIn
link zu YouTube
link zu TikTok
link zu Pinterest
link zu Instagram
  • Impressum
  • Datenschutzerklärung
  • Disclaimer
  • Cookie-Richtlinie (EU)
©2025 Technik Blog | Built using WordPress and Responsive Blogily theme by Superb
Cookie-Zustimmung verwalten
Wir verwenden Technologien wie Cookies, um Geräteinformationen zu speichern und/oder darauf zuzugreifen. Wir tun dies, um das Surferlebnis zu verbessern und um personalisierte Werbung anzuzeigen. Wenn Sie diesen Technologien zustimmen, können wir Daten wie das Surfverhalten oder eindeutige IDs auf dieser Website verarbeiten. Wenn Sie Ihre Zustimmung nicht erteilen oder zurückziehen, können bestimmte Funktionen beeinträchtigt werden.
Funktional Immer aktiv
Die technische Speicherung oder der Zugang ist unbedingt erforderlich für den rechtmäßigen Zweck, die Nutzung eines bestimmten Dienstes zu ermöglichen, der vom Teilnehmer oder Nutzer ausdrücklich gewünscht wird, oder für den alleinigen Zweck, die Übertragung einer Nachricht über ein elektronisches Kommunikationsnetz durchzuführen.
Vorlieben
Die technische Speicherung oder der Zugriff ist für den rechtmäßigen Zweck der Speicherung von Präferenzen erforderlich, die nicht vom Abonnenten oder Benutzer angefordert wurden.
Statistiken
Die technische Speicherung oder der Zugriff, der ausschließlich zu statistischen Zwecken erfolgt. Die technische Speicherung oder der Zugriff, der ausschließlich zu anonymen statistischen Zwecken verwendet wird. Ohne eine Vorladung, die freiwillige Zustimmung deines Internetdienstanbieters oder zusätzliche Aufzeichnungen von Dritten können die zu diesem Zweck gespeicherten oder abgerufenen Informationen allein in der Regel nicht dazu verwendet werden, dich zu identifizieren.
Marketing
Die technische Speicherung oder der Zugriff ist erforderlich, um Nutzerprofile zu erstellen, um Werbung zu versenden oder um den Nutzer auf einer Website oder über mehrere Websites hinweg zu ähnlichen Marketingzwecken zu verfolgen.
  • Optionen verwalten
  • Dienste verwalten
  • Verwalten von {vendor_count}-Lieferanten
  • Lese mehr über diese Zwecke
Einstellungen anzeigen
  • {title}
  • {title}
  • {title}