////////////// TAUPI 4.0 @ Shelly ////////////// // copyright by boeserbob und holzachr // Fragen an quirb@web.de // Dokumentation und aktuelle Versionen unter https://github.com/BoeserBob/Taupi-4.0 // // Dieses Skript verwandelt z.B. eine Shelly Plug in eine Taupunktlüftersteuerung. // Der Skript schaltet einen angeschlossenen Lüfter über den Schalter des Shellys auf dem er installiert ist entsprechend der Taupunktunterschiede innen - außen. // - Es empfängt Messwert-Events von BLE-Sensoren auf. // - Wenn die Messwerte von den angegebenen Innen- und Außen-Sensoren stammen, werden aus Temperatur und Luftfeuchte die jeweiligen Taupunkte berechnet. // - Eine Timerschleife überprüft regelmäßig, ob alle Einschaltbedingungen fuer den Lüfter erfüllt sind: // - Wenn der Taupunkt innen größer als der Taupunkt außen + einem Schwellwert ist wird der Lüfter eingeschaltet, sonst ausgeschaltet. // - Wenn die Innentermperatur unter 10 °C und die Innenraumfeuchte unter 50 % ist wird der Lüfer ausgeschaltet. //========== Sensor-Konfiguration ========== var sensor_aussen = "${sensor_aussen_mac}"; var sensor_innen = "${sensor_innen_mac}"; //========== Debug-Konfiguration ========== var DEBUG = false; //========== Schalt-Konfiguration ========== var taupunktschwelle = ${taupunktschwelle}; // [°C] Lüfter einschalten wenn TPinnen > (TPaussen + taupunktschwelle)... var mindesttemperatur = ${mindesttemperatur}; // [°C] ...und Tinnen > mindesttemperatur... var mindesthumi = ${mindesthumi}; // [%] ...und RHinnen > mindesthumi var schaltzeit = ${schaltzeit}; // [s] Schaltbedingung prüfen alle X Sekunden var battery_warngrenze = ${battery_warngrenze}; // [%] wenn dieser Schwellwert unterschritten ist blinkt der Plug rot var lost_connection = ${lost_connection}; // [s] Zeit nach der frische Sensordaten gekommen sein müssen um tote Verbindungen zu finden //========== Kritische Feuchte-Konfiguration (Schimmelgefahr) ========== var critical_humi_base = ${critical_humi_base}; // [%] Kritische relative Feuchte bei Referenztemp. var critical_humi_temp_coeff = ${critical_humi_temp_coeff}; // [%/°C] Steigung des Grenzwerts pro °C var critical_humi_ref_temp = ${critical_humi_ref_temp}; // [°C] Referenztemperatur var critical_humi_buffer = ${critical_humi_buffer}; // [%] Sicherheitspuffer //===== Ende Sensor-Konfiguration === AB HIER MUSS NICHTS MEHR GEÄNDERT WERDEN ===================================== var taupunkt_aussen; var taupunkt_innen; var temperatur_innen; var temperatur_aussen; var humidity_innen; var humidity_aussen; var battery_innen; var battery_aussen; var lost_connection_innen; var lost_connection_aussen; var luefterstatus = null; // Merkt sich letzten Schaltzustand, um unnötige Schaltvorgänge zu vermeiden // Taupunktberechnung function taupunkt(T, RH) { var a = (T >= 0) ? 17.27 : 21.875; var b = (T >= 0) ? 237.7 : 265.5; var alpha = (a * T) / (b + T) + Math.log(RH / 100); return (b * alpha) / (a - alpha); } // Lüftersteuerung function schalten() { // Sicherheitsprüfung: Sind alle benötigten Werte vorhanden? if (typeof taupunkt_innen === "undefined" || typeof taupunkt_aussen === "undefined" || typeof temperatur_innen === "undefined" || typeof humidity_innen === "undefined") { print("Nicht alle Sensorwerte vorhanden - Schaltung übersprungen."); farbring(80,80,0,100); return; } // Kritische Feuchte (Schimmelgrenzkurve) berechnen var rh_crit = critical_humi_base - critical_humi_temp_coeff * (temperatur_innen - critical_humi_ref_temp); var rh_activation = rh_crit - critical_humi_buffer; var is_critical = humidity_innen >= rh_activation; print("Schimmelprüfung: T_innen =", temperatur_innen, "°C, RH_innen =", humidity_innen, "%, RH_crit =", rh_crit, "%, Aktivierung ab =", rh_activation, "%, Kritisch =", is_critical); // Sicherheitsprüfung kommen regelmäßig frische Daten von den Sensoren? lost_connection_innen = lost_connection_innen + schaltzeit lost_connection_aussen = lost_connection_aussen + schaltzeit print("letzte Verbindung zum Sensor innen vor " ,lost_connection_innen, " Sekunden " ); print("letzte Verbindung zum Sensor außen vor " ,lost_connection_aussen, " Sekunden " ); if (lost_connection_innen > lost_connection || lost_connection_aussen > lost_connection ) { print("Verbindung zu Sensoren zu lange verloren, Lüfter ausschalten."); Shelly.call("Switch.Set", { id: 0, on: false }); farbring(80,80,0,100); return; } // Visualisierung Batteriefüllstand durch roten Blink if (battery_innen < battery_warngrenze || battery_aussen < battery_warngrenze) { print("Batteriestand niedrig"); farbring(100,0,0,100); } // Schaltlogik (immer schalten, der Shelly schaltet nur, wenn er schaltet muss). if ( is_critical && temperatur_innen > mindesttemperatur && humidity_innen > mindesthumi && taupunkt_innen > taupunkt_aussen + taupunktschwelle ) { print("Lüfter einschalten"); Shelly.call("Switch.Set", { id: 0, on: true }); farbring(80,10,0,100); } else { print("Lüfter ausschalten."); Shelly.call("Switch.Set", { id: 0, on: false }); farbring(0,0,80,100); } } // Farbe Farbring setzen für Standalone Betrieb. function farbring(red,green,blue,helligkeit) { Shelly.call( "PLUGS_UI.SetConfig",{ id:0, config:{"leds":{"mode":"switch","colors": {"switch:0": {"on":{"rgb":[red,green,blue],"brightness":helligkeit}, "off":{"rgb":[red,green,blue],"brightness":helligkeit}}}}}}, function (result, code, msg, ud) { }, null ); } // Event-Verarbeitung function checkBlu(event) { var addr = event.address.toLowerCase(); var aussen = sensor_aussen.toLowerCase(); var innen = sensor_innen.toLowerCase(); if (DEBUG) { print("DEBUG checkBlu: event.addr=", addr, " aussen=", aussen, " innen=", innen); } if (addr === aussen) { temperatur_aussen = event.temperature; humidity_aussen = event.humidity; taupunkt_aussen = taupunkt(event.temperature, event.humidity); battery_aussen = event.battery; lost_connection_aussen = 0; print("Neue Werte für Außen:", temperatur_aussen, "°C,", humidity_aussen, "%, Tp:", taupunkt_aussen, "°C, Batt: ", battery_aussen, " % "); } else if (addr === innen) { temperatur_innen = event.temperature; humidity_innen = event.humidity; taupunkt_innen = taupunkt(event.temperature, event.humidity); battery_innen = event.battery; lost_connection_innen = 0; print("Neue Werte für Innen:", temperatur_innen, "°C,", humidity_innen, "%, Tp:", taupunkt_innen, "°C, Batt: " , battery_innen, " % "); } } // Haupt-Timer für Steuerlogik Timer.set(schaltzeit * 1000, true, function () { var rh_crit_t = (typeof temperatur_innen !== "undefined") ? (critical_humi_base - critical_humi_temp_coeff * (temperatur_innen - critical_humi_ref_temp)) : undefined; var rh_act_t = (typeof rh_crit_t !== "undefined") ? (rh_crit_t - critical_humi_buffer) : undefined; print("----- Steuerung alle", schaltzeit, "s -----"); print("Innen: T =", temperatur_innen, "°C, RH =", humidity_innen, "%, Tp =", taupunkt_innen, "°C, Aktivierung ab RH =", rh_act_t, "% (Batterie:", battery_innen, "%)"); print("Außen: T =", temperatur_aussen, "°C, RH =", humidity_aussen, "%, Tp =", taupunkt_aussen, "°C (Batterie:", battery_aussen, "%)"); schalten(); }); ///////////////// BLE-Decoder /////////////////////// const BTHOME_SVC_ID_STR = "fcd2"; const uint8 = 0; const int8 = 1; const uint16 = 2; const int16 = 3; const uint24 = 4; const int24 = 5; const dimmert = 6; // special data type for dimmer event const uint32 = 7; const int32 = 8; // The BTH object defines the structure of the BTHome data const BTH = { 0x00: { n: "pid", t: uint8 }, 0x01: { n: "battery", t: uint8, u: "%" }, 0x02: { n: "temperature", t: int16, f: 0.01, u: "tC" }, 0x04: { n: "atm. pressure", t: int24, f: 0.01, u: "hPa" }, 0x03: { n: "humidity", t: uint16, f: 0.01, u: "%" }, 0x05: { n: "illuminance", t: uint24, f: 0.01 }, 0x08: { n: "dew point", t: uint16, f: 0.01, u: "tC" }, 0x0C: { n: "capacitor voltage", t: uint16, f: 0.001, u: "V" }, 0x1e: { n: "light", t: uint8 }, 0x20: { n: "raining", t: uint8}, 0x21: { n: "motion", t: uint8 }, 0x2d: { n: "window", t: uint8 }, 0x2e: { n: "humidity", t: uint8, u: "%" }, 0x3a: { n: "button", t: uint8 }, 0x3c: { n: "dimmer", t: dimmert }, 0x3f: { n: "rotation", t: int16, f: 0.1 }, 0x44: { n: "wind speed", t: int16, f: 0.01, u: "m/s" }, 0x45: { n: "temperature", t: int16, f: 0.1, u: "tC" }, 0x46: { n: "UV index", t: int8, f: 0.1 }, 0x5E: { n: "wind direction", t: int16, f: 0.01 }, 0x5F: { n: "precipitation", t: int16, f: 0.1, u: "mm" }, 0x60: { n: "channel", t: uint8 }, 0xf0: { n: "dev_type", t: uint16 }, 0xf1: { n: "fw_version", t: uint32 }, 0xf2: { n: "fw_version_short", t: uint24 }, }; function getByteSize(type) { if (type === uint8 || type === int8) return 1; if (type === uint16 || type === int16 || type === dimmert) return 2; if (type === uint24 || type === int24) return 3; if (type === uint32 || type === int32) return 4; return 255; } // Functions for decoding and unpacking the service data from Shelly BLU devices const BTHomeDecoder = { utoi: function (num, bitsz) { const mask = 1 << (bitsz - 1); return num & mask ? num - (1 << bitsz) : num; }, getUInt8: function (buffer) { return buffer.at(0); }, getDimmer: function (buffer) { return {"dimmer": buffer.at(0), "dimmersteps": buffer.at(1)}; }, 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); }, getUInt32LE: function (buffer) { return 0xffffffff & ((buffer.at(3) << 24) | (buffer.at(2) << 16) | (buffer.at(1) << 8) | buffer.at(0)); }, getInt32LE: function (buffer) { return this.utoi(this.getUInt32LE(buffer), 32); }, 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); if (type === uint32) res = this.getUInt32LE(buffer); if (type === int32) res = this.getInt32LE(buffer); if (type === dimmert) res = this.getDimmer(buffer); return res; }, unpack: function (buffer) { 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; 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") { print("BTH: Unknown type: ", buffer.at(0)); break; } buffer = buffer.slice(1); _value = this.getBufValue(_bth.t, buffer); // handle dimmer special case if (typeof _value === "object" && _bth.t === dimmert) { result["dimmersteps"] = _value["dimmersteps"]; _value = _value["dimmer"]; } if (_value === null) break; if (typeof _bth.f !== "undefined") _value = _value * _bth.f; 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 ]; } } buffer = buffer.slice(getByteSize(_bth.t)); } return result; }, }; let lastPacketId = 0x100; function BLEScanCallback(event, result) { if (event !== BLE.Scanner.SCAN_RESULT) { return; } if (DEBUG) { print("DEBUG BLEScanCallback: addr=", result.addr, " rssi=", result.rssi); } if (typeof result.service_data === "undefined" || typeof result.service_data[BTHOME_SVC_ID_STR] === "undefined") { return; } let unpackedData = BTHomeDecoder.unpack(result.service_data[BTHOME_SVC_ID_STR]); if (unpackedData === null || typeof unpackedData === "undefined" || unpackedData["encryption"]) { print("Error: Encrypted devices are not supported or unpacking failed"); return; } if (DEBUG) { print("DEBUG unpacked BTHome:", JSON.stringify(unpackedData)); } if (lastPacketId === unpackedData.pid) { return; } lastPacketId = unpackedData.pid; unpackedData.address = result.addr; checkBlu(unpackedData); } function initBLE() { const BLEConfig = Shelly.getComponentConfig("ble"); if (!BLEConfig.enable) { print("Error: The Bluetooth is not enabled, please enable it from settings"); return; } if (BLE.Scanner.isRunning()) { print("Info: The BLE gateway is running, the BLE scan configuration is managed by the device"); } else { const bleScanner = BLE.Scanner.Start({ duration_ms: BLE.Scanner.INFINITE_SCAN, active: false }); if(!bleScanner) { print("Error: Can not start new scanner"); } } BLE.Scanner.Subscribe(BLEScanCallback); } initBLE();