2026-07-26
In Teil 1 habe ich das Master Skript teilweise beschrieben - vom Anfang mit Konfiguration bis zur Klasse ParamVals. Hier wird die Skriptbeschreibung mit Teil 2 fortgesetzt.
Zu Teil 1, zu Teil 3, zum einführenden Beitrag
Inhalt
- Klasse Process zur Steuerung, Regelung der Bewässerung und für Statusmitteilungen nach außen
- Anlegen der Process Objekte im Array "Channel"
- Was noch gebraucht wird oder werden kann
- Empfang der Feuchtigkeitswerte
Einführung
Ein Objekt der Klasse ParamVals (von Parameter Values) reagiert auf Änderungen von Bewässerungsparameterwerten durch Benutzer. Es überprüft solche Änderungen auf Gültigkeit und sendet ggf. Assistenten (hier für Wasserdurchflussmessungen) den/die benötigten Parameterwert(e) zu. Zusätzlich sendet es aktualisierte Parameterwerte via MQTT an die IoT-Außenwelt, damit Darstellungen im bevorzugten Frontend aktualisiert werden. Über das Frontend kann ein Anwender auch die Bewässerungsparameter via MQTT einzeln einstellen. Ein ParamVals-Objekt ist ausschließlich für die Verarbeitung von Änderungen an Bewässerungsparametern verantwortlich. Kein anderes Objekt hat sich damit zu befassen. Es stellt einem übergeordneten Objekt der im folgenden beschriebenen Klasse Process die benötigten Bewässerungsparameter zur Verfügung.
Die Messwerte des Wasservolumens werden nicht vom Master verarbeitet. Hierfür ist aus Gründen der Robustheit der jeweilige Assistent (Shelly Plus UNI Skript) zuständig, was an anderer Stelle beschrieben wird.
4. Klasse Process zur Steuerung, Regelung der Bewässerung und für Statusmitteilungen nach außen
Ein Objekt der Klasse Process (s.u.) steuert oder regelt die Bewässerung eines Strangs unter Verwendung von o.a. Parameterwerten, Messwerten und Ereignissen, insbesondere des Einschaltens seines Ausgangs. Es implementiert also die Funktionalität der smarten Bewässerung. Hierfür werden ihm Ereignisse von der Firmware und Daten von externen Funktionen und assistierenden Prozessen (hier Shelly Plus UNI Skript) zugeführt. Ein Process-Objekt beinhaltet bzw. nutzt ein ParamVals-Objekt, welches die aktuellen Bewässerungsparameter zur Verfügung stellt. Es besitzt einen Status und sendet Informationen darüber via MQTT nach außen, damit diese von Frontends dargestellt werden können. Auch setzt es den Feuchtigkeitswert des zugeordneten Sensors, falls vorhanden, in einer virtuellen Komponente des WebUI.
Process.id beinhaltet die Kanal id (hier 0 oder 1). Sie ist mit dem Ausgangs Id assoziiert und wird beim schalten des Ausgangs verwendet.
Process.Params referenziert das zugehörige ParamVals Objekt. Damit kann ein Process Objekt auf die zu verwendenden Bewässerungsparameter zugreifen.
Process.vKeys referenziert ein Objekt, das die Schlüssel der zugeordneten virtuellen Komponenten beinhaltet.
{Param:<Parametereingabe>, Data:[<Feuchtigkeitsanzeige>, <Volumenanzeige>, <Anzeige finales Volumen>]}
Die Schlüsselwerte sind im Konfigurationsbereich des Skripts eingetragen bzw. einzutragen, s. ebd..
Process.Handle ist ein Datenfeld aus drei Handles zu den virtuellen Komponenten für die Feuchtigkeitsanzeige, die Volumenanzeige und die finale Gesamtvolumenanzeige.
Mit Process.enGimmick (enable gimmick) wird ein Countdown via Timer freigegeben (true) bzw. gesperrt (false), der in 10% Schritten die Restzeit per MQTT überträgt, die sich auf den Wert des max. Dauer Parameters bezieht. Dies ermöglicht die Darstellung dieser Restzeit in einem Frontend. Process.THandle ist die Variable für das zugehörende Timer Handle.
Das Process.Status Objekt beinhaltet die Statuseigenschaften Erdfeuchtigkeit (moisture), aktuelles Volumen (volume), finales Volumen (volfin), temporäre Regelungssperre (ctrlEn), Einschaltzeitstempel (onTs), Ausschaltzeitstempel (offTs) und die relative Restdauer bei freigegebenem Countdown Zähler (remain).
4.1 Beschreibung der Methoden
- sendStatus() sendet entweder den kompletten Status oder einen einzelnen Wert, dessen Schlüssel als Parameter importiert wird. Der Aufruf sendStatus("moisture") sendet bspw. den Wert von Status.moisture. Das MQTT Topic lautet demzufolge entweder <PreTopic>/<Kanal Id>/status oder <PreTopic>/<Kanal Id>/status/<Schlüssel>. Im ersen Fall beinhaltet die Payload den Status im JSON Format, im zweiten Fall den einzelnen numerischen Wert.
- setParam() ist eine Adaptermethode zum aufrufen der ParamVals Methode set() oder iset(), abhängig davon ob der erste importierte Parameter eine Zahl oder ein Name (=Schlüssel) ist.
- doswitch() hat die Aufgabe, den Ausgang zu schalten. Beim einschalten wird toggle_after mit dem Wert des Anwendungsparameters "Dauer" hinzugefügt, womit das automatische Ausschalten erwirkt wird. Zusätzlich wird Status.ctrlEn auf false gesetzt und der Sperrzeit (blind time) Timer mit dem Anwendungsparameterwert von "Sperre" gestartet. Dies deaktiviert die Regelung in diesem Kanal temporär.
Es folgen drei Eventhandler, die von der Firmware bei Änderung ("change") aufgerufen werden. Diese gehören zu den drei virtuellen nur-lese Komponenten für die Feuchtigkeit, das Volumen und das finale Volumen. Fehlt eine solche virtuelle Komponente, ist der zugeordnete Eventhandler wirkungslos, stört aber nicht. Bei Wertänderung wird der neue Wert in die entsprechende Statuseigenschaft kopiert.
- Handle[0].on("change", callback) ist der Eventhandler für die Änderung der Erdfeuchtigkeit.
- Handle[1].on("change", callback) ist der Eventhandler für die Änderung des aktuellen Volumens.
- Handle[2].on("change", callback) ist der Eventhandler für die Änderung des finalen Gesamtvolumens.
- countdown() ist die callback Funktion des "Gimmick" Timers. Sie sendet die relative Restdauer (in %) via sendStatus("remain") und verringert Status.remain um 10 (%).
- procEvent() ist der Eventhandler des Kanalprozesses. Er wird vom globalen Eventhandler passend zum event.id Wert aufgerufen. Die Prozess Objekte werden an anderer Stelle als Elemte eines Datenfeldes angelegt. Deren Indizes müssen dabei mit denen der Process.id übereinstimmen. So kann der globale Eventhandler an Hand des event.id Wertes den zugeordneten Process Eventhandler aufrufen.
- control() ist insbesondere für die optionale Regelung des Kanalprozesses zuständig. Sie wird immer dann aufgerufen, wenn (bspw. via MQTT) Feuchtigkeitswerte eintreffen. Das Process Objekt nutzt die zugeordneten Anwendungsparameter wie folgt. Der dem Sensor zugeordnete Feuchtigkeitswert wird bei Gültigkeit (Zahlenwert?) Status.moisture zugewiesen, in der virtuellen Komponente gesetzt und gesendet. Ist die Regelung aktiviert (Regelung:1) und außerhalb der Sperrzeit (blind time), wird die Zweipunktregelung durchgeführt.
Nach dem Anlegen bzw. Konstruieren eines Process Objektes wartet es auf Ereignisse wie Ein- oder Ausschalten des ihm zugeordneten Ausgangs und Eintreffen von Messwerten. Man beachte, dass es verschiedene Quellen für das Schalten eines Ausgangs geben kann wie interne Regelung, Taster/Schalter am Eingang, Volumen-Messassistent, Sprachassistent, übergeordnetes System, jegliches genutztes Frontend, RPC Aufruf von irgendwoher. Unabhängig von der jeweiligen Quelle des Schaltens reagiert das Process Objekt immer gleich.
4.2 Der Code
// prototype constructing an object for processing data// id = switch id, vKeys = keys of virtual components - see VirtKeys above,function Process(id, params, vKeys, enGimmick) { this.id = id<0 ? 0 : (id>MaxId ? MaxId : id); this.Params = params; this.vKeys = vKeys; this.Handle = [Virtual.getHandle(this.vKeys[0]), Virtual.getHandle(this.vKeys[1]), Virtual.getHandle(this.vKeys[2])]; this.enGimmick = enGimmick===undefined ? false : enGimmick;
this.THandle = null; // timer handle for reporting relative remaining time
this.Status = { moisture: null, // measured moisture volume: 0, // actual volume volfin: 0, // finished volume ctrlEn: true, // temporary control enable onTs: null, // timestamp of switched on offTs: null, // timestamp of switch off remain: 0 // relative remaining time }; // for sending one value via key or the complete status this.sendStatus = function(key) { let t = PreTopic+String(this.id)+Topic.Status, p; if(key===undefined) p = this.Status; else { p = eval('this.Status.'+key); t += '/' + key; } // print("send Status."+key, ", topic:", t, ", payload:", p); MQTT.publish(t, JSON.stringify(p), 1, true); };
// an adapter function this.setParam = function(key, val) { if(isNaN(key)) this.Params.set(key, val); // key is a name else this.Params.iset(key, val); // key is an index }; // switch the assigned output with a parameterized duration this.doswitch = function(on) { let obj = {id:this.id, on:on}, dur = this.Params.Values[0]; if(on && dur!==undefined && dur!==0) obj.toggle_after = Math.round(60*dur); callShelly("Switch.Set", obj); //if(!on) print("channel: ", this.id, " -> off"); };
// this data change handlers may be changed this.Handle[0].on("change", function(e) { this.Status.moisture = e.value; // this.sendStatus("moisture"); // this does the method control }.bind(this));
// obsolete? this.Handle[1].on("change", function(e) { this.Status.volume = e.value; // this.sendStatus("volume"); // should be sent by the assistant }.bind(this));
// obsolete? this.Handle[2].on("change", function(e) { this.Status.volfin = e.value; // this.sendStatus("volfin"); // should be sent by the assistant }.bind(this));
this.countdown = function() { this.sendStatus("remain"); //print(this.Status.remain); this.Status.remain -= 10; //if(this.Status.remain<=0) this.doswitch(false); };
// the process event handler this.procEvent = function(e) { //print(JSON.stringify(e)); let on = e.info.state, idStr = String(this.id), dur = this.Params.Values[0]; if(on) { this.doswitch(true); //print("Start"); this.Status.onTs = Math.round(e.info.ts); if(this.enGimmick) { this.Status.remain = 100; this.countdown(); this.THandle = Timer.set(Math.round(dur*6000), true, this.countdown.bind(this)); // as 10% steps } } else { //print("Stop"); Timer.clear(this.THandle); this.Status.offTs = Math.round(e.info.ts); // perhaps this message will be nice MQTT.publish(PreTopic+"info/"+idStr+"/duration", String(Math.round(this.Status.offTs-this.Status.onTs)), 1, true); this.Status.remain = 0; this.sendStatus("remain"); } MQTT.publish(PreTopic+idStr+Topic.EventSwitch, String(on), 1, true); };
// the process control function this.control = function(data) { //print("channel", this.id, ", vKeys:", JSON.stringify(this.vKeys), ", moisture data:", data); let sensor = this.Params.Values[3], val = data[sensor-1]; if(isNaN(val)) {console.log("invalid control value", val); return;} // invalid sensor data? this.Status.moisture = val; callShelly("Number.Set", {id:this.vKeys[0].split(':')[1], value:val}); this.sendStatus("moisture"); if(this.Params.Values[2]===0) return; // control disabled
let onThr = this.Params.Values[4], offThr = this.Params.Values[5];
if(val<=onThr && this.Status.ctrlEn) { this.doswitch(true); //print("channel:", this.id, ", sensor:", sensor, ", value:", val, ", on threshold:", onThr, ", off threshold:", offThr, " -> on"); let tBlind = this.Params.Values[6]; if(tBlind>=1) { this.Status.ctrlEn = false; Timer.set(tBlind*60000, false, function() {this.Status.ctrlEn = true;}.bind(this)); } } else if(val>=offThr) { this.doswitch(false); //print("channel:", this.id, ", sensor:", sensor, ", value:", val, ", on threshold:", onThr, ", off threshold:", offThr, " -> off"); } }; }
5. Anlegen der Process Objekte im Array "Channel"
Mit Kanal bezeichne ich hier eine Zusammenstellung von einem Schaltausgang und einem zugeordneten Eingang. Ein Shelly Pro 2 hat bspw. zwei Kanäle solcher Art. Jedem verfügbaren Kanal ist ein Prozess zugeordnet, damit jeder Kanal eigenständig nutzbar ist, unabhängig von anderen Kanälen. Deshalb wird hier jedem Kanal ein Process Objekt zugeordnet, welches denselben Identifier (id) braucht wie der des Kanals, hier also 0 oder 1. Solche Identifier nutzt auch die Shelly Firmware bzw. deren RPC Methoden für das Application Programming Interface (API).
Separate Funktionen (solche außerhalb der Kanäle) müssen Methoden des einem Kanal zugeordnete Process Objektes aufrufen können. Damit dies auf einfache Art gelingt, setze ich ein Array namens Channel ein. Darin wird unter dem Index 0 das Process Objekt mit id=0, unter dem Index 1 das Process Objekt mit id=1 abgelegt - bzw. jeweils eine Referenz. Shelly Ereignisse beinhalten oftmals einen solchen Identifier, der als Index im Channel Array genutzt wird, um Methoden des dort vorliegenden Process Objektes aufzurufen.
Mit dem Anlegen eines Process Objekts wird dessen Konstruktor (die gleichnamige Funktion, s.o.) aufgerufen, die das Objekt initialisiert. Danach steht der dem Kanal zugeordnete Prozess zur Verfügung und kann genutzt werden. Dieses gelingt prinzipiell mit nur einem Kanal ebenso wie mit zwei, drei oder mehr Kanälen. Jeder Kanal erhält so sein Process Objekt. Diese Process Objekte können sich sehr unterschiedlich verhalten, hier verhalten sie sich gleich, weil beide gleiche Aufgaben erfüllen - smarte Bewässerungen.
new Process(0, new ParamVals(0, VirtKeys[0].Param, Params, Assist[0]), VirtKeys[0].Data, true)
Von innen nach außen. Zuerst wird ein ParamVals Objekt erstellt, das vier Parameter wie in Teil 1 beschrieben erhält. Dann wird ein Process Objekt via Konstruktor erstellt, dem der Identifier (0), das erstellte ParamVals Objekt sowie zwei weitere Parameter VirtKeys[0].Data und true übergeben werden. VirtKeys , Params und Assist sind Teile der Skript Konfiguration und in Teil 1 beschrieben. Der letzte Parameter true bewirkt, dass das Gimmick freigegeben bzw. zu aktivieren ist, das der Darstellung der relativen Bewässerungs-Restdauer in Frontends dient.
Das erste new liefert das Process Objekt für Kanal 0. Was ist damit zu tun? Es wird als erstes (Index 0) Element in das Array Channel gesteckt. Es ist via Channel[0] erreichbar.
Angenommen, es soll der Bewässerungs-Parameterwert von "Dauer" für Kanal 0 gesetzt werden, weil dieser von einem Frontend kommend eintrifft. Das gelingt mit
Channel[0].setParam("Dauer", <Wert>); oder mitChannel[0].setParam(0, <Wert>); 0 ist der Index für den Dauer-Wert in ParamsVal.Values.
Mit Channel[0] erreicht man das Kanal 0 Process Objekt. setParam ist eine Methode dieses Objekts. Übrigens, setParam ruft seinerseits die Methode set bzw. iset des ParamVals Objekts auf, das im Process Objekt (via Referenz) eingebettet ist. Das ist nicht etwa zu kompliziert sondern geeignet strukturiert. 😉
// construct an object to each used output channel// construct parameters:// * the channel id, must match the array index// * a ParamVals object with keys of virtual components for parametrizing, processing on change and displaying,// application parameter object and assistant object// * an object with keys assigning virtual components for displaying values of interest// * gimmicks enable (default=false), true will need an additional Timer// usable construct (id should be in every of those entries the same value):// new Process(id, new ParamVals(id, VirtKeys[id].Param, Params, Assist[id], VirtKeys[id].Data, true|false)const Channel = [ new Process(0, new ParamVals(0, VirtKeys[0].Param, Params, Assist[0]), VirtKeys[0].Data, true), new Process(1, new ParamVals(1, VirtKeys[1].Param, Params, Assist[1]), VirtKeys[1].Data, true)];
Es mag bessere Codes für einen solchen Zweck geben. Dieser bietet jedenfalls eine Grundlage für Skalierbarkeit und flexible Konfiguration.
6. Was noch gebraucht wird oder werden kann
procEvent() ist der globale Eventhandler zur Verarbeitung von Ereignissen. Er filtert switch toggle Ereignisse. Hier sind nur Umschalt-Ereignisse relevant, die selektiv an den Eventhandler des zuständigen Kanal-Prozesses weitergereicht werden.
startCtrlWatch() startet den Watchdog Timer für die optionale Feuchtigkeitsregelung, wenn in der Konfiguration am Skriptanfang WatchEn auf true gesetzt ist. Nach Timerende werden alle Ausgänge ausgeschaltet und der Anwendungsparameterwert von "Regelung" auf 0 gesetzt. Damit wird die Regelung gesperrt, bis der Anwender sie bei Bedarf freigibt. Dieser Watchdog ist nicht unbedingt erforderlich, weshalb er in diesem Skript deaktiviert ist. Nachteilig ist, dass er einen Timer braucht. Dies kann der fünfte parallel laufende Timer sein, womit das Timerlimit erreicht würde.
Mit MQTT.subscribe (Topic.Set, callback) werden MQTT Nachrichten zum setzen einzelner Anwendungsparameterwerte abonniert. So kann die Anwenderin mit ihrem Frontend auf einfache Weise Bewässerungsprozesse parametrieren.
Mit MQTT.subscribe(Topic.CmdSwitch, callback) werden MQTT Nachrichten zum schalten eines Ausgangs abonniert. So kann die Anwenderin via Frontend ad hoc einen Ausgang ein- oder ausschalten.
// the event handler forwards to the responsible channel objectfunction procEvent(e) { if(e.name!="switch" || e.info.event!="toggle") return; Channel[e.id].procEvent(e);}
// start control watchdog, only effective if WatchEn is as true configuredfunction startCtrlWatch() { Timer.clear(ThWatch); if(WatchEn) ThWatch = Timer.set(CtrlWatch*1000, false, function() { //print("control watchdog: disable control"); for(let i=0; i<Channel.length; ++i) { Channel[i].doswitch(false); Channel[i].setParam(Params.Keys[2], 0); // key = control enable key, disable control } });}
// mqtt subscriber for setting single parameters with number values// topic structure: <pretopic>/<channel id>/set/<component|index>// payload: value as stringMQTT.subscribe(Topic.Set, function(topic, payload) { //print(topic, payload); let t = topic.split('/'), n = t.length; if(n<MinTopicParts) return; let key = t[n-1], id = Number(t[n-3]); if(isNaN(id) || id<0 || id>Channel.length-1) { console.log("error wrong id:", id); return; } Channel[id].setParam(key, Number(payload.replace(',','.')));});
// mqtt subscriber for processing a commandMQTT.subscribe(Topic.CmdSwitch, function(topic, payload) { let p = topic.split('/'), id = p[p.length-3]; //print(id, payload); if(isNaN(id)) return; id = Number(id); if(id<0 || id>MaxId) return; Channel[id].external = false; Channel[id].doswitch(payload=="true");});
Shelly.addEventHandler(procEvent);
startCtrlWatch();
7. Empfang der Feuchtigkeitswerte
Der folgende Code am Ende des Master Skripts zum empfangen der Erdfeuchtigkeitswerte bezieht sich auf unsere Anlage mit Ecowitt Sensoren WH 51 und einem Ecowitt Gateway. Das Gateway sendet die Daten ca. jede Minute per MQTT zusammen mit denen unserer kleinen Wetterstation WS90 in einer komplexen Zeichenkette. Dieser Code kann und muss somit bei Bedarf gegen einen anderen ausgetauscht werden, wenn eine andere Kommunikation erforderlich ist. Ohne Erdfeuchtigkeitssensoren kann dieser Code selbstverständlich entfernt werden, was aber nicht notwendig ist. Wenn keine Feuchtigkeitswerte empfangen werden, wird die Methode Process.control() nicht aufgerufen und es gibt keine Regelung.
Hier werden Konstanten definiert, die der MQTT Subscriber nutzt. Der Subscriber selektiert aus dem komplexen, vom Gateway kommenden String die zu nutzenden Teile und packt diese, geordnet nach Sensor Id, in ein Array - Sm genannt (soil moisture). Dann ruft er die control Methoden (s.o.) aller existierender Kanal-Prozesse auf, denen er jeweils das Array Sm übergibt. Jede control Methode selektiert den von der Anwenderin per "Sensor:<Id>" festgelegten Feuchtigkeitswert für die Kanal spezifische Regelung.
... Channel[<Index>].control(Sm);
Damit hat der Subscriber seine Aufgaben erfüllt. Nun sind die Process Objekte am Zug.
// ----- the moisture data section -----const SoilMoistureTopic = "wetterstation/ecowitt", // Topic of soil moisture messages SoilMoistureKey = "soilmoisture", // with attached identifier 1, 2, ... SmKlen = SoilMoistureKey.length;
// This subscriber get all soil moisture values for regulation.// It process a string getting from an ecowitt gateway, testet with GW 2000A.MQTT.subscribe(SoilMoistureTopic, function(topic, payload) { startCtrlWatch(); // start control watchdog let Sm = []; // array of soilmoisture values //print(payload); let p = payload.split('&'); let e = []; for(let i=p.length-1; i>=0; --i) { e = p[i].split('='); if(e[0].slice(0,SmKlen)==SoilMoistureKey) { //w += (w.length>1 ? ',"' : '"') + e[0] + '":' + e[1]; let id = Number(e[0][SmKlen]); Sm[id-1] = Number(e[1]); } } for(let i=0; i<Channel.length; ++i) Channel[i].control(Sm);});