Ich habe versucht, Messwerte eines im Wall Display (WD) an einen Shelly Generation 3 (im folgenden G3 genannt) zu übertragen - nur mal aus Interesse. Solches kann aber interessant sein, wenn eine anwendungsorientierte Lösung per Shelly Skript gewünscht ist - bspw. in einem Wohnmobil/Wohnw agen. Ich habe im WD statt des dort eingebauten Sensors einen externen BLU H&T Sensor konfiguriert, was aber hier unwesentlich ist. (Nein, mit G3 ist kein Gewehr gemeint.
)
Firmware Version auf dem G3: 1.6.2
Ziel
Eine skalierbare Anwendung, ausschließlich per Shelly Skript - also ohne Cloud und ohne die Notwendigkeit eines übergeordneten Systems. Die Skalierbarkeit bezieht sich auf die Verarbeitung von vielen Messwerten per Skript, nicht nur zweien.
Bei meinen Versuchen habe ich folgende Schwierigkeiten erlebt und etwas aufwändige Lösungen kreiert - vielleicht kann das jemand besser, aber bitte keine Cloud Szenen.
- Da im WD keine Skripte möglich sind, bleiben Aktionen.
- Im WD lässt sich keine Aktion dazu bringen, einen aktuellen Messwert zu verwenden, wie in anderen Shelly per $value.
- Vermutlich lässt sich ein komplexer URL zusammenstellen, welcher den G3 per RPC dazu bringt, einen Messwert vom WD abzuholen - Methode "temperature.getstatus" bzw. "humidity.getstatus". Gründe dagegen s.u.
- Ich erstelle zu jedem Messwert eine Aktion, in welchem URL die RPC Methode "script.eval" auf dem G3 genutzt wird.
- Der Parameter zu 4. ist ein Objekt, dessen Struktur ich kreierte. Das G3 Skript und die Objektstruktur müssen zueinander passen.
- Das G3 Skript (s.u.) fragt nicht unmittelbar nach Erhalt des RPC Aufrufs die Werte vom WD ab, sondern speichert das Objekt (als String) zwischen. Dies bevorzuge ich, damit das Skript nicht wegen zu vieler Methodenaufrufe abgebrochen wird. Hierzu ist irgendein Zeitmanagement erforderlich. Nach meiner Erfahrung kann Ausnahmebehandlung (try, catch) einen solchen Abbruch gegenwärtig nicht verhindern.
Zu 3. Ein Skriptloser, komplexer URL täte unmittelbar auf dem G3 den Messwert vom WD holen. Wie auch in 6. bereits dargelegt, kann dies zu Fehlern wegen zu vieler RPC Aufrufe führen, wenn es auch keinen Skriptabbruch deswegen gäbe. Zusätzlich wäre ohne Skript der Spielraum für Verarbeitungsprozesse und die Usability stark eingeschränkt.
Das folgende Skript ist etwas speziell, lässt sich aber für andere Zwecke relativ leicht anpassen.
// created by eiche, 2025-05-26
// This script, running on a Shelly generation 3 or higher, sends MQTT messages with data from all BTHome sensor values of a BTHome device.
// This happens immediately after the script receives a status message from a BTHome device. Nothing further needs to be done.
// The function bths() requires a composition of sensor name and optional unit in the component name.
// Sensor name and unit must be separated by a separator containing the variable Sep, e.g. a colon.
// In function bths you may do something else with the sensor data, e.g. use the "HTTP.Get" method with an URL.
// But that method can be in an action and does not need a script.
// Have fun with it anyway!
//Additionally, puschRPC() stores incoming messages to process them at specific times.
// A small configuration
let
Topic = "versuch/bthome", // the MQTT topic
Sep = ':', // the seperator symbol - it may be a string instead of one character
Dt = 2000, // delay milliseconds before each call of function next
Max = 10; // maximum of RPC entries
// end of configuration
let RPC = [], Th = null;
function next() { // process next queued message
if(RPC.length===0) {Timer.clear(Th); return;}
let msg = RPC.pop();
//print(msg);
msg = JSON.parse(msg);
Shelly.call("http.get", {url:"http://"+msg.target+"/rpc/"+msg.method+(msg.param===undefined?"":msg.param)},
function(res, errc, errm, msg) {
if(errc) {console.log("next", errc, errm); return;}
//print(res.body);
let val = JSON.parse(res.body.split('"'+msg.comp+'":')[1]);
let nu = msg.name.split(Sep);
print(nu[0], val, (nu[1]===undefined?"":nu[1]));
}, msg
);
}
function pushRPC(msg) { // stores msg in a queue named RPC
if(msg===undefined || msg===null || msg.target===undefined || msg.method===undefined) {
console.log("push() invalid parameter:", msg);
return;
}
//print(JSON.stringify(msg));
msg = JSON.stringify(msg);
if(RPC.length>=Max) {console.log("RPC.push overfow"); return;}
let i = RPC.indexOf(msg);
if(i<0) RPC.push(msg) else RPC[i] = msg; // place the newest message into RPC
}
function procRPC() { // start processing the queued messages
Th = Timer.set(Dt, true, next);
}
// Get BTHome sensor data an send those via MQTT.
function bths(id) {
//print(id);
Shelly.call("bthomesensor.getstatus", {id:id}, function(res, errc, errm) {
if(errc) {console.log(errc, errm); return;}
//print(JSON.stringify(res));
let Sensor = {id:res.id, value:res.value, ts:res.last_updated_ts, name:"", unit:""};
Shelly.call("bthomesensor.getconfig", {id:id}, function(res, errc, errm, s) {
if(errc) {console.log(errc, errm); return;}
//print(JSON.stringify(res));
let n = res.name.split(Sep);
s.name = n[0];
if(n.length>1) s.unit = n[1];
let p = JSON.stringify(s);
//print(p);
MQTT.publish(Topic, p); // or do something else with object s respective Sensor
}, Sensor);
});
}
function bthStatus(st) {
if(st.name!=="bthomedevice") return;
//print(JSON.stringify(st));
Shelly.call("BTHomeDevice.GetKnownObjects", {id:st.id}, function(res, errc, errm) {
if(errc) {console.log(errc, errm); return;}
//print(JSON.stringify(res));
for(let i=res.objects.length-1; i>=0; --i) {
//print(res.objects[i].component);
if(res.objects[i].component!==null) bths(res.objects[i].component.split(':')[1]);
}
procRPC();
});
}
Shelly.addStatusHandler(bthStatus);
Alles anzeigen
Dieses Skript verarbeitet die Messdaten vom selben BLU H&T wie das WD, es können aber auch ganz andere Sensoren sein. Ich habe derzeit nur ein BLU H&T.
Das Zeitmanagement zur Verarbeitung der vom WD eintreffenden RPC Nachrichten unterliegt den Statusmitteilungen (an den Statushandler), die nach eintreffen der BLU H&T Nachrichten dem Skript mitgeteilt werden. Diese kommen nicht ganz regelmäßig, aber verlässlich ca. minütlich an. Mit deren Eintreffen wird per procRPC() ein periodisch arbeitender Timer gestartet, der bspw. jeweils um 2s versetzt rückwärts die in der Queue (RPC) als nächstes gespeicherte Nachricht verarbeitet, hier die Messdaten des WD abfragen. Die in der Queue gespeicherten RPC Nachrichten können aber von beliebigen anderen Geräten kommen. Trotz dieses vorsichtigen Zeitmanagements kommt es mitunter vor, wenn auch eher selten, dass sich ein timeout bei der Abfrage des WD ereignet. Dies erscheint mir aber wenig störend, weil ca. 1 Minute später ein neuer Wert i.d.R erfolgreich geholt wird.
Die in next() berücksichtigte Komponente msg.param ist für parametrierte RPC vorgesehen, aber bisher nicht genutzt und nicht getestet.
Ich arbeite an einer noch flexibleren Lösung, die sich auf mindestens zwei Schedule Jobs stützen wird. Diese wird ein noch verlässlicheres Zeitmanagement implementieren.
Die im WD - oder einem anderen Shelly - konfigurierten Aktionen benötigen folgende Struktur.
Zunächst für die Luftfeuchtigkeit:
http://<IP Adr. G3>/rpc/script.eval?id=1&code="pushRPC({target:'<IP Adr. WD>',method:'humidity.getstatus',comp:'rh',name:'Luftfeuchtigkeit:%'})"
Der name Wert 'Luftfeuchtigkeit:%' ist, wie bereits an anderer Stelle erläutert eine Zusammensetzung aus Name und Einheit und kann selbstverständlich frei gewählt werden, bis auf den trennenden Doppelpunkt. Der id Wert 1 bezieht sich auf die Skript Id und muss ggf. einen anderen Wert haben.
Nun zur Temperatur, entsprechend zur Luftfeuchtigkeit:
http://<IP Adr. G3>/rpc/script.eval?id=1&code="pushRPC({target:'<IP Adr. WD>',method:'temperature.getstatus',comp:'tC',name:'Temperatur:°C'})"
Falls Interesse daran besteht, auch an spezifischen Anpassungen, oder es Fragen dazu gibt, bin ich gerne zur Mitarbeit bereit. Diese kann allerdings durch meine reduzierte Ausstattung begrenzt sein.
Edit:
Da gab es noch etwas offenbar unschädlichen Müll im Skript Zeile 25 (List = [];). Diesen habe ich entfernt.
Ich versuche, solche Skripte möglichst kurz und leichter verständlich zu halten. Bei komplexeren Skripten kommen oft komplexere Datenstrukturen zum Einsatz, um den Code kurz zu halten. Objektorientierung wird von mir zwar beherrscht, aber nicht angestrebt.