Ja, das hast du richtig verstanden, danke! War mir nicht bewusst, dass in Sachen Bluetooth ein Unterschied zwischen Plus 2PM und späteren Generationen besteht.
Ich konnte es nicht lassen und habe mal ein bissl mit Hilfe von Grok gebastelt und ein Skript erstellt, das auf einem Shelly Plus 2PM läuft und einen anderen Shelly steuert. Nach all dem Grübeln bin ich aber zu dem Schluss gekommen, dass es vielleicht sinnvoller ist, eine etwas komplexere Regelung in Home Assistant laufen zu lassen und dem die Markise steuernden 2PM Gen4 den Auftrag zu geben, zu überwachen, ob Home Assistant online ist, und wenn nicht, dann die Markise einzufahren.
Vielleicht kann ja jemand das Skript gebrauchen oder weiterentwickeln, ich lasse es mal hier:
Code
/////////////// KONFIGURATION ////////////////
var ACTIVE_SCAN = false; // Aktives Scannen (mehr Stromverbrauch, bessere Reichweite)
var allowedMacAddresses = [
"08:b9:23:43:db:23" // MAC-Adresse des BRESSER WS90 Wetterstations-Sensors (nur dieser wird ausgewertet)
];
var DEBUG_LEVEL = 2; // 0 = keine Ausgaben, 1 = Fehler, 2 = Infos, 3+ = sehr viel Debug
var WIND_SPEED_LIMIT_MS = 1.0; // Durchschnittswindgeschwindigkeit in m/s, ab der Schutz greift
var WIND_GUST_LIMIT_MS = 1.5; // Böengeschwindigkeit in m/s, ab der Schutz greift
var DEBOUNCE_SECONDS = 60; // Mindestabstand zwischen zwei Schutzauslösungen in Sekunden (0 = deaktiviert)
// ------------------- MARKISE / ROLLLADEN STEUERUNG -------------------
var COVER_IP = "192.168.150.55"; // IP-Adresse des Shelly-Geräts, das die Markise/Rollladen steuert
var COVER_ID = 0; // ID des Covers am Shelly (meist 0)
var TARGET_POSITION = 100; // Zielposition in % bei Windalarm: 0 = ganz zu, 100 = ganz offen
// ------------------------------------------------------------
//////////////// HILFSFUNKTIONEN //////////////////
/**
* Entfernt Doppelpunkte aus MAC-Adresse und wandelt in Kleinbuchstaben um
*/
function macNoColonLower(m) {
return (m || "").toLowerCase().split(":").join("");
}
/**
* Debug-Ausgabe mit einstellbarem Level
* Wird nur angezeigt, wenn DEBUG_LEVEL hoch genug ist
*/
function debug(lvl, msg) {
if (DEBUG_LEVEL >= lvl) {
print("[DEBUG " + lvl + "] " + msg);
}
}
// ------------------- HTTP-FUNKTIONEN (funktioniert mit allen Shelly-Firmwares) -------------------
/**
* Führt einen HTTP-GET-Request aus und liefert das geparste JSON zurück
*/
function httpGet(url, callback) {
Shelly.call("http.request", {
method: "GET",
url: url,
timeout: 8
}, function(res, error_code, error_msg) {
if (error_code !== 0 || !res || !res.body) {
debug(1, "HTTP GET fehlgeschlagen: " + (error_msg || "keine Antwort"));
callback(null);
return;
}
var json;
try {
json = JSON.parse(res.body);
} catch (e) {
debug(1, "JSON-Parsen fehlgeschlagen: " + res.body);
callback(null);
return;
}
callback(json);
});
}
/**
* Fragt den aktuellen Öffnungsgrad der Markise/Rollladen ab
* Ruft callback mit der Position in Prozent auf (0-100) oder null bei Fehler
*/
function queryCoverPosition(cb) {
var url = "http://" + COVER_IP + "/rpc/Cover.GetStatus?id=" + COVER_ID;
debug(2, "Frage Markisen-Position ab: " + url);
httpGet(url, function(data) {
if (data && typeof data.current_pos === "number") {
cb(data.current_pos);
} else {
cb(null);
}
});
}
/**
* Fährt die Markise auf die gewünschte Position (0-100%)
* Wird bei Windalarm aufgerufen → meist teilweise öffnen, damit sie nicht flattern kann
*/
function setCoverPosition(pos) {
pos = Math.max(0, Math.min(100, pos | 0)); // Sicherstellen, dass pos zwischen 0 und 100 liegt
var payload = {
id: 1,
method: "Cover.GoToPosition",
params: { id: COVER_ID, pos: pos }
};
Shelly.call("http.request", {
method: "POST",
url: "http://" + COVER_IP + "/rpc",
body: JSON.stringify(payload),
timeout: 10
}, function(res, error_code, error_msg) {
if (error_code !== 0 || !res || res.code !== 200) {
print("Markisen-Befehl FEHLGESCHLAGEN: " + (error_msg || JSON.stringify(res)));
}
});
print("Windschutz aktiv → Markise fährt auf " + pos + " %");
}
// ------------------- BTHOME PARSER (für BRESSER WS90) -------------------
/**
* Definition der vom WS90 gesendeten Datenfelder (BTHome v2 Format)
* Wichtig sind besonders:
* 0x44 = Windgeschwindigkeit Durchschnitt (m/s mit Faktor 0.01)
* 0x44 (zweiter Wert im Array) = Windböe (m/s)
*/
var T = {
0x00:{n:"pid",len:1,s:false,f:1},
0x01:{n:"battery_pct",len:1,s:false,f:1},
0x45:{n:"temperature_c",len:2,s:true,f:0.1},
0x2E:{n:"humidity_pct",len:1,s:false,f:1},
0x04:{n:"pressure_hpa",len:3,s:false,f:0.01},
0x05:{n:"illuminance_lux",len:3,s:false,f:0.01},
0x08:{n:"dew_point_c",len:2,s:true,f:0.01},
0x0C:{n:"capacitor_voltage_v",len:2,s:false,f:0.001},
0x20:{n:"moisture",len:1,s:false,f:1},
0x44:{n:"wind_speed_ms",len:2,s:false,f:0.01}, // <--- wichtig für uns
0x46:{n:"uv_index",len:1,s:false,f:0.1},
0x5E:{n:"wind_direction_deg",len:2,s:false,f:0.01},
0x5F:{n:"precip_mm",len:2,s:false,f:0.1}
};
/**
* Wandelt String oder anderes Format sicher in Byte-Array um
*/
function ensureBytes(x) {
if (!x) return null;
if (typeof x === "string") {
var b = [];
for (var i = 0; i < x.length; i++) b.push(x.charCodeAt(i));
return b;
}
return x;
}
/**
* Parsed die BTHome v2 Daten des WS90
* Liefert ein Objekt mit den ausgelesenen Werten zurück
*/
function parseBTHome(buf) {
if (!buf || !buf.length) return null;
var di = buf[0], enc = (di & 1) === 1, ver = (di >> 5) & 7;
if (ver !== 2 || enc) return null; // Nur unverschlüsselte BTHome v2 Pakete
var i = 1, out = {};
while (i < buf.length) {
var id = buf[i++], def = T[id];
if (!def) break; // Unbekannte ID → Ende
if (i + def.len > buf.length) break; // Zu wenig Daten → Abbrechen
var v = 0;
for (var j = 0; j < def.len; j++) v |= (buf[i + j] << (8 * j));
i += def.len;
if (def.s) { // Vorzeichenbit prüfen bei signed Werten
var bits = 8 * def.len, sign = 1 << (bits - 1);
if (v & sign) v -= (1 << bits);
}
var val = v * def.f;
// Falls derselbe Typ mehrfach vorkommt (z.B. Wind Durchschnitt + Böe) → Array
if (out[def.n] !== undefined) {
if (Array.isArray(out[def.n])) out[def.n].push(val);
else out[def.n] = [out[def.n], val];
} else {
out[def.n] = val;
}
}
return out;
}
// ------------------- LAUFZEIT-LOGIK -------------------
var allowedSet = {};
// MAC-Adressen ohne Doppelpunkt in Set für schnellen Lookup
for (var i = 0; i < allowedMacAddresses.length; i++) {
allowedSet[macNoColonLower(allowedMacAddresses[i])] = true;
}
var cache = {}; // Letzte empfangene Werte pro MAC zwischenspeichern
var lastWarningTs = {}; // Zeitstempel der letzten Wind-Warnung pro Sensor (für Debouncing)
print("WS90 Windschutz-Script gestartet");
debug(1, "Markisen-IP: " + COVER_IP + " | Zielposition bei Wind: " + TARGET_POSITION + " %");
// BLE-Scanner Callback – wird bei jedem empfangenen Werbepaket aufgerufen
BLE.Scanner.Subscribe(function(ev, res) {
if (ev !== BLE.Scanner.SCAN_RESULT || !res) return;
var mac = macNoColonLower(res.addr || res.address || "");
// Nur bekannte Sensoren verarbeiten
if (!allowedSet[mac]) return;
// BTHome-Daten aus Service-Data UUID FCD2 holen
var raw = res.service_data && (res.service_data["fcd2"] || res.service_data["FCD2"]);
if (!raw) return;
var data = parseBTHome(ensureBytes(raw));
if (!data) return;
// Aktuelle Werte mit evtl. bereits gecachten Werten mergen
var merged = cache[mac] || {};
for (var k in data) {
if (k !== "_ids") merged[k] = data[k];
}
// Bei Windgeschwindigkeit kommen oft zwei Werte: [Durchschnitt, Böe]
if (Array.isArray(merged.wind_speed_ms)) {
merged.wind_gust_ms = merged.wind_speed_ms[1];
merged.wind_speed_ms = merged.wind_speed_ms[0];
}
merged.ts = (Date.now() / 1000) | 0; // Unix-Timestamp in Sekunden
cache[mac] = merged;
// Ausgabe der aktuellen Windwerte ins Log
print("Wind: " + (merged.wind_speed_ms || 0).toFixed(2) + " m/s | Böe: " + (merged.wind_gust_ms || 0).toFixed(2) + " m/s");
// Prüfen, ob ein Grenzwert überschritten wurde
var exceeded = (merged.wind_speed_ms >= WIND_SPEED_LIMIT_MS) ||
(merged.wind_gust_ms >= WIND_GUST_LIMIT_MS);
// Windalarm auslösen – aber nur, wenn Debouncing erlaubt oder genug Zeit vergangen ist
if (exceeded && (DEBOUNCE_SECONDS === 0 || merged.ts - (lastWarningTs[mac] || 0) >= DEBOUNCE_SECONDS)) {
print("WINDGRENZWERT ÜBERSCHRITTEN → prüfe Markisenposition");
lastWarningTs[mac] = merged.ts;
// Aktuelle Position der Markise abfragen
queryCoverPosition(function(curr) {
if (curr === null) {
debug(1, "Konnte Markisenposition nicht auslesen");
return;
}
if (curr < TARGET_POSITION) {
print("Markise steht bei " + curr + " % → öffne auf " + TARGET_POSITION + " %");
setCoverPosition(TARGET_POSITION);
} else {
debug(2, "Markise bereits ≥ " + TARGET_POSITION + " % (aktuell " + curr + " %) – nichts zu tun");
}
});
}
});
// ------------------- SCANNER STARTEN -------------------
var DUR = (BLE.Scanner && BLE.Scanner.INFINITE_SCAN) ? BLE.Scanner.INFINITE_SCAN : -1;
BLE.Scanner.Start({ duration_ms: DUR, active: ACTIVE_SCAN });
debug(1, "BLE-Scanner gestartet (aktiv=" + ACTIVE_SCAN + ")");
Alles anzeigen