infrapuzzle/terraform_shelly/scripts/Taupi-4.0.js

400 lines
16 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) {
if (typeof event.temperature !== "undefined") {
temperatur_aussen = event.temperature;
}
if (typeof event.humidity !== "undefined") {
humidity_aussen = event.humidity;
}
if (typeof temperatur_aussen !== "undefined" && typeof humidity_aussen !== "undefined") {
taupunkt_aussen = taupunkt(temperatur_aussen, humidity_aussen);
}
if (typeof event.battery !== "undefined") {
battery_aussen = event.battery;
}
lost_connection_aussen = 0;
print("Neue Werte für Außen: T=", temperatur_aussen, "°C, RH=", humidity_aussen, "%, Tp=", taupunkt_aussen, "°C, Batt=", battery_aussen, "%");
} else if (addr === innen) {
if (typeof event.temperature !== "undefined") {
temperatur_innen = event.temperature;
}
if (typeof event.humidity !== "undefined") {
humidity_innen = event.humidity;
}
if (typeof temperatur_innen !== "undefined" && typeof humidity_innen !== "undefined") {
taupunkt_innen = taupunkt(temperatur_innen, humidity_innen);
}
if (typeof event.battery !== "undefined") {
battery_innen = event.battery;
}
lost_connection_innen = 0;
print("Neue Werte für Innen: T=", temperatur_innen, "°C, RH=", 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;
// The BTH object defines the structure of the BTHome data (trimmed to temperature/humidity/battery)
const BTH = {
0x00: { n: "pid", t: uint8 },
0x01: { n: "battery", t: uint8 },
0x02: { n: "temperature", t: int16, f: 0.01 },
0x03: { n: "humidity", t: uint16, f: 0.01 },
0x2e: { n: "humidity", t: uint8 },
0x45: { n: "temperature", t: int16, f: 0.1 }
};
function getByteSize(type) {
if (type === uint8 || type === int8) return 1;
if (type === uint16 || type === int16) return 2;
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);
},
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);
},
getBufValue: function (type, buffer) {
if (buffer.length < getByteSize(type)) return null;
if (type === uint8) return this.getUInt8(buffer);
if (type === int8) return this.getInt8(buffer);
if (type === uint16) return this.getUInt16LE(buffer);
if (type === int16) return this.getInt16LE(buffer);
return null;
},
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") {
break;
}
buffer = buffer.slice(1);
_value = this.getBufValue(_bth.t, buffer);
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();
// Expose metrics for Prometheus
if (typeof HTTPServer !== "undefined" && typeof HTTPServer.registerEndpoint === "function") {
HTTPServer.registerEndpoint("prometheus", function(req, res) {
var body = "";
function addGauge(name, val, help) {
if ((typeof val === "number" && !isNaN(val)) || typeof val === "boolean") {
var numVal = (typeof val === "boolean") ? (val ? 1 : 0) : val;
if (help) {
body += "# HELP " + name + " " + help + "\n";
}
body += "# TYPE " + name + " gauge\n";
body += name + " " + numVal + "\n";
}
}
// Calculate helper values
var rh_crit = (typeof temperatur_innen !== "undefined" && !isNaN(temperatur_innen)) ? (critical_humi_base - critical_humi_temp_coeff * (temperatur_innen - critical_humi_ref_temp)) : undefined;
var rh_activation = (typeof rh_crit !== "undefined" && !isNaN(rh_crit)) ? (rh_crit - critical_humi_buffer) : undefined;
var is_critical_val = (typeof humidity_innen !== "undefined" && !isNaN(humidity_innen) && typeof rh_activation !== "undefined" && !isNaN(rh_activation)) ? (humidity_innen >= rh_activation) : undefined;
var would_fan_activate = false;
if (typeof is_critical_val !== "undefined" && is_critical_val !== null &&
typeof temperatur_innen !== "undefined" && !isNaN(temperatur_innen) &&
typeof humidity_innen !== "undefined" && !isNaN(humidity_innen) &&
typeof taupunkt_innen !== "undefined" && !isNaN(taupunkt_innen) &&
typeof taupunkt_aussen !== "undefined" && !isNaN(taupunkt_aussen)) {
would_fan_activate = is_critical_val &&
(temperatur_innen > mindesttemperatur) &&
(humidity_innen > mindesthumi) &&
(taupunkt_innen > taupunkt_aussen + taupunktschwelle);
}
addGauge("taupi_temperature_celsius_innen", temperatur_innen, "Indoor temperature in Celsius");
addGauge("taupi_temperature_celsius_aussen", temperatur_aussen, "Outdoor temperature in Celsius");
addGauge("taupi_humidity_percent_innen", humidity_innen, "Indoor relative humidity in percent");
addGauge("taupi_humidity_percent_aussen", humidity_aussen, "Outdoor relative humidity in percent");
addGauge("taupi_dewpoint_celsius_innen", taupunkt_innen, "Indoor dew point in Celsius");
addGauge("taupi_dewpoint_celsius_aussen", taupunkt_aussen, "Outdoor dew point in Celsius");
addGauge("taupi_battery_percent_innen", battery_innen, "Indoor sensor battery percentage");
addGauge("taupi_battery_percent_aussen", battery_aussen, "Outdoor sensor battery percentage");
addGauge("taupi_lost_connection_seconds_innen", lost_connection_innen, "Seconds since last contact with indoor sensor");
addGauge("taupi_lost_connection_seconds_aussen", lost_connection_aussen, "Seconds since last contact with outdoor sensor");
addGauge("taupi_critical_humidity_threshold_percent", rh_activation, "Calculated relative humidity threshold above which it is critical");
addGauge("taupi_is_critical", is_critical_val, "Flag indicating mold danger inside (humidity exceeds threshold)");
addGauge("taupi_would_fan_activate", would_fan_activate, "Flag indicating if all fan activation criteria are met");
var relay_status = Shelly.getComponentStatus("switch", 0);
if (relay_status && typeof relay_status.output !== "undefined") {
addGauge("taupi_relay_status", relay_status.output, "Physical switch status (1 = on, 0 = off)");
}
res.body = body;
res.headers = [["Content-Type", "text/plain; version=0.0.4"]];
res.code = 200;
res.send();
});
}