2026-07-22

Nach einer Test- und Verbesserungsphase von ca. drei Wochen stelle ich hier sukzessive die eingesetzten Skripte vor. Diese dürfen insbesondere für private Zwecke verwendet werden. Sollten in unserer weiteren Nutzung Probleme auftreten, werden solche hier dargelegt. Nach (vorläufiger) Komplettierung dieses Beitrags werde ich die Skripte zum einfachen Herunterladen bereitstellen.

Zu Teil 2, zu Teil 3, zum einführenden Beitrag

Inhalt

  1. Einführung
  2. Die Master-Konfiguration - u.a. im Skriptkopf
  3. Vermeiden von Skriptstopps wegen zu vieler parallel laufender Shelly.call
  4. Klasse zum speichern von Anwendungsparametern
    1. Beschreibung der Methoden (= Objekt interne Funktionen)

0. Einführung

Dieses Projekt ist in anderen Beiträgen unter verschiedenen Perspektiven beschrieben.

Für einen Bewässerungsstrang werden insgesamt bis zu drei Skripte genutzt - zwei für die Bewässerung funktionale und ein optionales, Zeitplan gesteuertes Überwachungs-Skript. Bei zwei Bewässerungssträngen in einem System kann ein zusätzliches Skript erforderlich sein. Diese Skripte sind unter folgenden Prämissen kreiert.

  1. Hohe Betriebssicherheit bzw. geringstmögliche Ausfallwahrscheinlichkeit - im Rahmen der Shelly Firmware
  2. Funktionsautonomie ohne Erfordernisse von zusätzlichen Systemen oder Cloud
  3. Benutzerfreundliche Parametrierbarkeit jedes Bewässerungsstrangs, auch bei fehlendem (W)LAN
  4. Kommunikationsschnittstellen für optional nutzbare übergeordnete Systeme - derzeit via MQTT
    (Unser Projekt nutzt MQTT, Node-RED, InfluxDB 2 und Grafana.)
  5. Skalierbarkeit - von Einkanal Steuerung bis zu Mehrkanal Regelung (max. drei Kanäle, besser zwei)
  6. Relativ leichte Anpassbarkeit an eigene Bedürfnisse bzw. Ausstattung

Wir nutzen die größte Ausbaustufe mit zweikanaliger Regelung der Erdfeuchtigkeit, Speicherung von Bewässerungsereignissen, deren Volumen und gemessener Erdfeuchtigkeitswerte sowie Visualisierung in Grafiken. Als Master dient ein Shelly Pro 2. Dieser wird durch zwei Assistenten Shelly Plus UNI unterstützt - wegen zwei Bewässerungssträngen. Die Assistenten messen und übertragen zu jedem Bewässerungsereignis das fließende Wasservolumen sowie das finale Gesamtvolumen. Sie sind auch für das Abschalten bei Überschreitung des eingestellten max. Volumens zuständig. Wer nur einen Bewässerungsstrang nutzen will, kann das Skript mit geringem Aufwand dafür anpassen.

Zum messen der Erdfeuchtigkeit nutzen wir acht Funksensoren Ecowitt WH51. Mit dieser hohen Anzahl an Sensoren lassen sich fundierte Erfahrungen zur Parametrierung von sparsamen (Tropf-)Bewässerungen sammeln. Im Skript gibt es genau einen MQTT Subscriber bzw. eine callback Funktion, die für den Empfang solcher Messwerte und die Weiterleitung an die Verarbeitungsprozesse zuständig ist. Bei Einsatz eines anderen Feuchtigkeits-Messsystems (incl. Gateway) ist ausschließlich dieser Subscriber bzw. diese callback Funktion anzupassen.

Der Master schaltet Bewässerungen und ist für die bidirektionale Kommunikation mit der optionalen IT-Umgebung zuständig. Die optionalen Assistenten liefern die Volumen-Messwerte und übertragen diese unidirektional zum Master (an virtuelle Komponenten) und zur IT-Umgebung.

Um auch bei (temporär) fehlendem (W)LAN die Anlage vollumfänglich nutzen zu können, werden virtuelle Komponenten eingesetzt, deren Anzahl Firmware seitig auf 10 begrenzt ist. Damit vom Master mehr als ein Strang individuell parametrierbar ist, musste von zunächst numerischen virtuellen Komponenten auf Text-Komponenten umgestellt werden.

Sowohl das Web User Interface (WebUI) als auch die Skripte sind für eigene Zwecke zu konfigurieren. Keines der Skripte bietet eine automatisierte Konfiguration. Der Anwender muss sich somit mit dem Anlegen von virtuellen Komponenten und dem Eintragen derer Schlüssel wie "text:200" (Parametereingabe), "number:200" (nur-lese Wertanzeige) ... im Skript befassen.

1. Die Master-Konfiguration - u.a. im Skriptkopf

Im folgenden werden u.a. die Zusammenhänge von konfiguriertem WebUI und der Konfigurationssequenzen im Master Skript dargelegt. 

Eine Beispielkonfiguration der virtuellen Komponenten per WebUI

Virtuelle Komponenten komplett

Nach dem Anlegen der virtuellen Komponenten für die Parametereingabe (obligatorisch) und die numerischen Anzeigen (optional, View=Label) von Erdfeuchtigkeit, Volumen und finalem Volumen müssen deren Schlüssel in VirtKeys passend eingetragen werden.

Beispiel einer Anordnung von virtuellen Komponenten des Kanals 0 (Garten) - Anwendungssicht

WebUI - Anwendungssicht

Links oben ist das Textfeld für die Parametereingabe zu sehen. Die Parameterliste besteht aus Komma separierten Einträgen der Struktur Name:Wert. Die vom Anwender bevorzugten Parameternamen (Dauer, Volumen, ..., Sperre) sind änderbar. Eine solche Änderung muss im Params.Keys Array (=Datenfeld) vorgenommen und danach in der Parameterliste im WebUI (s.o.) fehlerfrei verwendet werden - somit nicht DAU sicher. Die Nutzung nur eines Textfeldes für alle Anwendungsparameter eines Kanals ist der Einsparung von virtuellen Komponenten geschuldet. Im laufenden Prozess wird auf die Parameter ausschließlich über deren Positionen (Namen unabhängig) in gleich strukturierten Arrays zugegriffen.

Die Liste an Parameterwerten wird bei Änderung auf Gültigkeit geprüft. Hierfür ist die Funktion Params.validate zuständig und kann leicht für eigenen Bedarf abgeändert werden. Wird bei Wertänderung eine Ungültigkeit erkannt, wird diese Änderung verworfen, damit keine unzulässigen Parameterwerte den Prozess stören können. Der Parameter v dieser Funktion muss die gleiche Struktur wie Params.Keys besitzen, was im vorliegenden Skript gewährleitet ist. v[0] ist der Dauer-Wert, v[1] der Volumen-Wert ...

Die messenden Assistenten sollten mit dem Master über dessen Access Point verbunden sein, damit die Anlage als Komplettsystem bei fehlendem (W)LAN uneingeschränkt nutzbar ist (Range extender aktivieren!). Hierfür sind die interne IP-Adresse, Skript-Id, Parameter-Empfangsfunktion der Assistenten und Parameterindex im Assist Array anzupassen. Normalerweise braucht der Anwender ausschließlich die IP-Adressen und die Skript-Id anzupassen. Die IP-Adressen der Assistenten sind nach deren Kopplung im Master unter Advanced  Device clients  AP clients ablesbar. (Über die externe IP-Adresse incl. Portnummer sind die Assistenten via (W)Lan erreichbar.) Param: [1] kennzeichnet, dass der Master bei Änderung des max. Volumens (Index=1) diesen Wert an den Assistenten überträgt. Das ist erforderlich, weil der Assistent das laufende Volumen misst und für das Abschalten des Kanals bei Überschreitung des max. Volumens zuständig ist. Für allgemeine Zwecke solcher Art ist als Param-Wert ein Array von Indizes vorgesehen, weil die Anzahl an zu übertragenden Werten anwendungsabhängig ist - kein Wert bis viele Werte. 

Ein per WatchEn (de)aktivierbarer Watchdog ist eingebaut. Er dient ausschließlich bei Verwendung von Erdfeuchtigkeitssensoren und aktivierter Feuchtigkeitsregelung dazu, alle Kanäle abzuschalten, wenn 180s lang (3 Minuten) keine Sensorwerte empfangen wurde. Diese Watchdog Latenz kann in CtrlWatch geändert werden. Wir nutzen diesen Watchdog nicht, weil die anderen Grenzwerte (Dauer, Volumen) hinreichende Sicherheit bieten und für den aktivierten Watchdog ein zusätzlicher Timer erforderlich ist.

Die Topic Einträge sind ausschließlich für die MQTT Kommunikation relevant. Die resultierenden Topics müssen der folgenden Struktur genügen. Pre-Topic / Kanal-Id / spezifischer Teil ...
Die Publisher Topics werden im Prozess zusammengesetzt, bspw. zu "Bewaesserung-2/0/status" - damit wird der Status des Kanals 0 veröffentlicht. Die Subscriber Topics beinhalten an den erforderlichen Stellen die Wildcard '+'. Bspw. lautet das Set Topic "Bewaesserung-2/+/set/+". Damit kann ein einzelner Parameterwert an den Master übertragen werden. Mit "Bewaesserung-2/1/set/0" wird der max. Dauer-Wert für Kanal 1 empfangen. Alternativ gelingt dies auch mit "Bewaesserung-2/1/set/Dauer", solange "Dauer" an der ersten Position (Index=0) in Params.Keys steht. Ich empfehle, im MQTT Frontend statt des Parameternamens dessen Index zu nutzen, damit bei Namensänderung weniger anzupassen ist.

Bei einkanaliger Anwendung, bspw. via Shelly 1 Generation 3+ mit einem Shelly Plus UNI, sind die Arrays VirtKeys und Assist auf einen (zusammengesetzten) Eintrag zu reduzieren.

Die Skriptteile werden hier sukzessive in der im Skript vorliegenden Reihenfolge aufgeführt. Diese Reihenfolge entspricht der von C++ herrührenden Notwendigkeit, Dinge erst dann zu verwenden, wenn sie zuvor deklariert wurden. Hierfür gibt es in C++ Forward Deklarationen. In JavaScript ist eine solche Reihenfolge nicht zwingend. Der in JavaScript kundige Nutzer kann somit diese von mir genutzte Reihenfolge ändern.

// an irrigation application
// GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007
// More information: https://www.gnu.org/licenses/gpl-3.0.txt
// ABSOLUTELY NO WARRANTY!!!
// Author: Gerhard Eichelsdörfer alias eiche
// Version 2026-07-18-01
// using virtual components of type text to parameterize channels
// You can change `MQTT.publish` to 'print' without using MQTT, or vice versa!

const
// begin of configuration
  PreTopic = "Bewaesserung-2/",
  Topic = {
    EventSwitch: "/event/switch",  // publisher
    Status: "/status",  // publisher
    CmdSwitch: PreTopic+"+/cmd/switch", // subscriber
    Set: PreTopic+"+/set/+"  // subscriber
  },
VirtKeys = [ // keys of the virtual components, Data[0]: soil moisture, Data[1]: volume, Data[2]: final volume
  {Param: "text:200", Data: ["number:204","number:200","number:202"]}, // channel 0
  {Param: "text:201", Data: ["number:201","number:203","number:205"]}  // channel 1
  ],
  
  MaxId = 1, // maximal output channel id of this device respective application
  
  // array of parameters for communicate with assistants
  // If there is no assistant, this array should be empty.
  Assist = [
  { IpAddr: "192.168.33.65", Script: 2, Funct: 'setParams',  Param: [1] /* list of parameter values indices */},
  { IpAddr: "192.168.33.50", Script: 2, Funct: 'setParams',  Param: [1] /* list of parameter values indices */}
  ],
  
  // Change exclusively keys or/and the validate function!
  // Params contains exclusively parameter keys and a validate function, no parameter values.
  Params =  {
    // Parameter keys can be easily changed if needed or wished.
    // Structure: [max. duration, max. volume, control enable, sensor id, on threshold,
// off threshold, control blind time]

    Keys: ["Dauer","Volumen","Regelung","Sensor","ein","aus","Sperre"],
    validate: function(v) {
      return v[0]>=0 && (v[1]==undefined || v[1]>=0) && v[3]>=1 && v[3]<=8 && v[5]>=v[4]+1 && v[6]>=0;
    },
  },
  
  WatchEn = false, // watchdog active?
  CtrlWatch = 180;  // time of control watchdog in seconds
// end of configuration

Nach obiger Konfigurationssequenz folgt eine Konstanten-Definition und eine globale Variablen-Deklaration. MinTopicParts=4 bezieht sich auf die zweckmäßige Topic-Struktur für potentiell mehrkanalig arbeitende Master zum setzen von einzelnen Parameterwerten. Diese Struktur erfordert mindestens 4 Topic-Teile.

ThWatch ist die globale Variable für den optionalen Watchdog-Timer. Diese darf auch bei deaktiviertem Watchdog nicht fehlen.

// The following parts should not be needed to change.
// topic structure for setting a parameter: <pretopic>/<channel-id>/set/<component-index or -name>
const MinTopicParts = 4;

let ThWatch = null; // Timer handle for control watchdog

2. Vermeiden von Skriptstopps wegen zu vieler parallel laufender Shelly.call

Die Grundlage der folgenden Skript-Sequenz verdanke ich dem Shelly Forum Mitglied towiat, dem dafür mein Dank gebührt. Hier wird ein Speichern von Shelly.call-Parametern in einer Warteschlange (first in first out Speicherstruktur) implementiert und zur Nutzung bereitgestellt. Hiermit werden Skriptstopps wegen zu vieler parallel laufender RPC via Shelly.call abgefangen. Die Firmware gestattet nur bis zu 4 parallel laufender RPC (remote procedure call) via Skript. Zu viele auflaufende Shelly.call werden so nacheinander abgearbeitet, dass dieses  Limit von 4 paralleler RPC nicht überschritten wird. Dafür muss im weiteren Skript jeder Shelly.call Aufruf durch den Aufruf von callShelly (s.u.) ersetzt werden. Ich habe diese Grundlage von towiat für die Zwecke dieses Skripts angepasst. Die Teilbedingung rpcCount > 3 statt vorgesehener rpcCount > 4 in der Funktion next() wurde als Workaround erforderlich, weil einer der Aufrufe von callShelly einen mir nicht erklärbaren Fehler mit Skriptstopp zur Folge hatte. Dieser eine Aufruf gelingt via Original Shelly.call ohne Fehler, weshalb die rpcCount Grenze von 4 auf 3 zu reduzieren ist.

// queuing RPC via Shelly.call, use callShelly instead!
const rpcQueue = []; // contains the parameters of all RPCS that are not yet processed
let rpcCount = 0; // number of RPCs currently in progress

// execute the next RPC from the queue (if there is one and concurrency limit is not exceeded)
function next() {
  if (!rpcQueue.length || rpcCount > 3) return; // no RPC in queue or too many RPCs in progress
  const args = rpcQueue.splice(0, 1)[0]; // take the next RPC from queue (poor man's Array.shift())
  rpcCount++; // increase count of RPCs in progress
  Shelly.call(args[0], args[1], callbackFactory(args[2]), args[3]); // call the RPC
}

// this function wraps the user-provided callback
function callbackFactory(callback, userdata) {
  return function (res, errc, errm, userdata) {
    rpcCount--; // the current RPC has finished, so reduce the active count
    if (callback) callback(res, errc, errm, userdata); // execute the user-provided callback
    next(); // start the next RPC after callback has finished
  };
}

function callShelly(method, params, callback, userdata) {
  try {
    rpcQueue.push([method, params, callback, userdata]); // append the RPC to the queue
    next(); // next() takes care of the execution
  } catch(e) {
    console.log(e.message);
  }
}
// end of queuing RPC

3. Klasse zum speichern von Anwendungsparametern

Wer die folgenden Erläuterungen in keiner Weise versteht, darf diese gerne übergehen. Sie sollen Lesern mit hinreichenden Kenntnissen die von mir kreierte Struktur verständlich machen. Das Wort oder der Wortteil Parameter wird in unterschiedlichen Zusammenhängen verwendet. Der Leser sollte klar zwischen Aktualparametern (in Funktionsaufrufen), Formalparameterlisten (=Signatur) und Anwendungsparametern unterscheiden können, um nicht verwirrt zu werden. Letztere sind hier Bewässerungsparameter.

Ich habe hier erstmalig JavaScript Klassen gecodet und hoffe, dies sachgerecht getan zu haben. In der JavaScript Terminologie werden Klassen Prototypen genannt. Hierbei wird eine Funktion erstellt, die den Konstruktor der gleichnamigen Klasse bzw. Prototypen bildet. An Hand des Prototypen kann mit dem Operator new ein Objekt (=Instanz) der Klasse erstellt werden.

let Obj = new Prototypname(Aktualparameterliste);

Die folgende Klasse bzw. Prototyp ParamVals gestattet auf einfache Weise das (mehrfache) Anlegen von Parameterwerten, hier für zwei Bewässerungsstränge bzw. -kanäle. Ein Objekt der Klasse ParamVals verwaltet Anwendungsprozess bezogene Parameterwerte und stellt diese den zuständigen Prozessen zur Verfügung. In diesem Master-Skript gibt es zwei Prozesse (ein Prozess je Kanal), die eintreffende Daten und Ereignisse an Hand von Parameterwerten verarbeiten. ParamVals Objekte verarbeiten Änderungen von Parameterwerten (Dauer, Volumen, ...) seitens des Anwenders. So wird bspw. ein geänderter max. Volumenwert an den zuständigen Assistenten-Prozess (Shelly Plus UNI Skript) übertragen. Alle anderen Parameterwerte werden von Master-Prozessen genutzt.

Die Klasse Process folgt weiter unten. Jedes Process-Objekt beinhaltet ein ParamVals-Objekt. Auf diese Weise sind die Zuständigkeiten klar und übersichtlich verteilt. Innerhalb eines Process-Objektes ist ausschließlich ein ParamVals-Objekt für die Verarbeitung von Parameterwert-Änderungen zuständig. Das ParamVals-Objekt verarbeitet aber keine eintreffenden Prozessdaten und -ereignisse wie neuer Messwert oder Ausgang geschaltet. Dafür ist ausschließlich das Process-Objekt zuständig. Alle Anwendungsparameterwerte liegen in einem ParamVals internen Array namens Values, dessen Struktur (Reihenfolge der Parameter) festliegt.

3.1 Beschreibung der Methoden (= Objekt interne Funktionen)

Ich bitte um Entschuldigung, wenn die folgenden Beschreibungen wenig verständlich erscheinen sollten. Ich versuche, diese Beschreibungen hinreichend vollständig und eindeutig zu verfassen.

Um die Flexibilität beim Konfigurieren zu wahren, werden beim Erzeugen von ParamVals Objekten Referenzen auf externe Objekte übergeben, auf welche die ParamVals Objekte via intern gespeicherter Refrenzen zugreifen. Dies ist die gegenüber einem direkten Zugriff auf andere Objekte (ohne ParamVals interne Referenzen) flexiblere Implementation, weil diese Variante zulässt, in verschiedenen ParamVals Objekten unterschiedliche externe Objekte zu nutzen.

  • toKVstring() erstellt zu den in Values liegenden Werten einen Text (Zeichenkette, String), wie er in der virtuellen Komponente für die Anwendungsparameter dargestellt wird, bspw. "Dauer:10,Volumen:8,...". Hierfür nutzt diese Methode das ihr via Konstruktoraufruf zugewiesene Anwendungsparameter-Objekt. Dieses Objekt wird dem ParamVals-Objekt beim anlegen übergeben. In diesem Skript ist dies das oben aufgeführte Objekt Params mit den Parameternamen in Keys und der Funktion validate.
  • toValues() importiert einen Text, der eine Anwendungsparameterliste darstellt, und einen Defaultwert für nicht in der Liste enthaltene Anwendungsparameter. Als Resultat wird ein Array mit den aus der Liste extrahierten Anwendungsparameterwerten geliefert.
  • sendParameter() überträgt die Anwendungsparameterwerte in Values an die IoT-Außenwelt, hier via MQTT publishing. Die Payload ist das Values-Array in Textform. Der Empfänger, hier MQTT subscriber, muss die Werte an Hand deren Position im Datenfeld "erkennen". Bspw. ist der Wert des max. Volumens unter dem Index 1 zu finden. Das Topic wird aus dem PreTopic, der Kanal-Id Nummer und "/params" zusammengesetzt, hier bspw. "Bewaesserung-2/0/params" für die Anwendungsparameter des Kanal 0.
  • iset() schreibt den importierten Wert in eine Kopie von Values unter dem importierten Index und lässt die Gültigkeit dieser Anwendungsparameterwerte prüfen. Bei deren Gültigkeit werden die neuen Werte in Values geschrieben, die neue Anwendungsparameterliste in der virtuellen Textkomponente aktualisiert und via sendParameter() an die IoT-Außenwelt übertragen. 
  • set() lässt den Index zum importierten Schlüssel (key, Name des Wertes) finden. Ist der Index gültig (>=0) wird iset() mit diesem Index und dem importierten Wert aufgerufen. Diese Methode gestattet das Setzen eines Anwendungsparameterwertes an Hand seines Namens.
  • process() verarbeitet den dem Assistenten zugeordneten Konfigurationseintrag - s.o. in der Konfiguration das Array Assist. Es werden via HTTP.Get die Values-Werte unter den in Assist.Param eingetragenen Indizes an den Assistenten übertragen. Hier ist das immer der Wert des max. Volumens (Index=1). Dieser Wert wird vom empfangenden Assistenten-Prozess sofort übernommen und einbezogen. Wenn die Assistenten, wie empfohlen, mit dem Master über dessen AP verbunden sind, gelingt diese Übertragung auch bei nicht verfügbarem sonstigem Netzwerk.
  • Handle.on("change", callback) ist der Eventhandler der vom System aufgerufen wird, wenn der Anwender in der zugeordneten Anwendungsparameterliste etwas ändert. Die neue Liste wird in ein geordnetes Array konvertiert, mit dem bisherigen Inhalt von Values verglichen und bei Ungleichheit wie folgt verarbeitet. Bei Ungültigkeit wird der Inhalt von Values (ohne Änderung) als Liste in die virtuelle Komponente geschrieben. Andernfalls wird der Inhalt von Values aktualisiert, die neuen Werte an die IoT-Außenelt gesendet und process aufgerufen, damit auch die Assistenten ggf. neue Anwendungsparameterwerte erhalten.

Wenn ein Objekt dieser Klasse angelegt wird, wird automatisch auch der Inhalt der zugeordneten virtuellen Textkomponente nach Values konvertiert und via process die für die Assistenten erforderlichen Anwendungsparameterwerte (hier nur max. Volumen) übertragen.

// prototype constructing an object for parameterization
// id = channel id, vKeys = keys of virtual components - see VirtKeys above,
// params = parameter objekt with keys and validator function
function ParamVals(id, vKeys, params, assist) {
  this.id = id<0 ? 0 : (id>MaxId ? MaxId : id);
  this.vKeys = vKeys;
  this.Handle = Virtual.getHandle(this.vKeys),
  this.Params = params;
  this.Assist = assist;

  this.toKVstring = function() {
    let r = "", v = this.Values;
    for(let i in v) {
      if(v[i]!==undefined || v[i]===null) r += (i==0 ? '' : ',')+this.Params.Keys[i]+':'+v[i];
    }
    return r;     
  };

  this.toValues = function(list, def) { // def = optional default value
    let p = list.split(','), r = [];
    for(let i=0; i<p.length; ++i) {
      //print(p[i]);
      let e = p[i].split(':'), j = this.Params.Keys.indexOf(e[0].trim());
      if(j<0) {console.log("unknown parameter key", e[0]); continue;}
      else if(e.length===1 || e[1]=='') {
        console.log("missing a value");
        if(def!==undefined) r[j] = def;
        continue;
      }
      if(isNaN(e[1])) {
        console.log("value", "'"+e[1]+"'","is no number");
        if(def!==undefined) r[j] = def;
        continue;
      }
      r[j] = Number(e[1]);
    }
    if(!this.Params.validate(r)) console.log("invalid parameter value(s) in", JSON.stringify(list));
    return r;
  };

  // initiate the parameter values, non existing values are initiating with null
  this.Values = this.toValues(this.Handle.getValue(), null);

  // for sending the parameter values completely
  this.sendParameter = function() {
    MQTT.publish(PreTopic+String(this.id)+"/params", JSON.stringify(this.Values), 1, true);
  };
  
  // for setting a single value to its index in Parameter
  this.iset = function(i, val) {
    if(i<0 || i>=this.Values.length) {
      console.log("iset: wrong index", i);
      return;
    }
    if (val != this.Values[i]) {
      let p = this.Values.slice(); // flat copy
      p[i] = val; // assign temporary
      //print(JSON.stringify(this.Values), "  vs. ", JSON.stringify(p));
      if (this.Params.validate(p)) {
        this.Values[i] = val;
        this.Handle.setValue(this.toKVstring());
      }
    }
    this.sendParameter();
  };
  
  // for setting a single value to its key in Parameter
  this.set = function(key, val) {
    //print(key, val);
    let i = this.Params.Keys.indexOf(key);
    if(i<0) {
      console.log("unknown key:", key);
      return;
    }
    this.iset(i, val);
  };
  
  // Process the parameter values, independent of a changed value.
  this.process = function() {
    // Processes parameter values for use by an assistant.
    // The parameter values must be embedded in an array.
    let a = this.Assist;
    if(a!=undefined && a!=null && a!={}) {
      p = [];
      for(let i=0; i<a.Param.length; ++i) p.push(this.Values[a.Param[i]]); 
      let url = 'http://'+a.IpAddr+'/rpc/Script.Eval?id='+a.Script+'&code="'+a.Funct+'('+JSON.stringify(p)+')"';
      //print(url);
      Shelly.call("HTTP.Get", {url: url}, function(res, errc, errm) {
        if(errc) {console.log(errc, errm); return;}
        //print("result from assist:", JSON.parse(res.body).result);
      });
    }
  };
  
  // event handler on changed virtual "text" component as a list of key value pairs
  this.Handle.on("change", function(e) {
    let v = this.toValues(e.value, null);
    if(v!=this.Values) { // process only if value changes
      if(!this.Params.validate(v)) {
        this.Handle.setValue(this.toKVstring());
        return;
      }
      //print("changed to ", JSON.stringify(v));
      this.Values = v;
      print("new parameter:", JSON.stringify(this.Values));
      this.sendParameter();
      this.process();
    }
  }.bind(this));

  this.process();
}
// end of ParamVals prototype