feat(monitoring): add prometheus metrics, alerts, and dashboard for taupi fan

This commit is contained in:
Moritz Graf
2026-07-03 11:28:36 +02:00
parent 1ad356a7b4
commit 6627372ec0
3 changed files with 420 additions and 68 deletions
+71 -68
View File
@@ -170,46 +170,20 @@ 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
// 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, 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 },
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 || type === dimmert) return 2;
if (type === uint24 || type === int24) return 3;
if (type === uint32 || type === int32) return 4;
if (type === uint16 || type === int16) return 2;
return 255;
}
@@ -222,9 +196,6 @@ const BTHomeDecoder = {
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);
},
@@ -234,33 +205,13 @@ const BTHomeDecoder = {
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;
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) {
@@ -278,18 +229,11 @@ const BTHomeDecoder = {
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;
@@ -378,3 +322,62 @@ function initBLE() {
}
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();
});
}