381 lines
14 KiB
JavaScript
381 lines
14 KiB
JavaScript
////////////// 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();
|