Projektidee siehe unter Projektideen und -anstöße

Anlass war ein gewünschtes automatisiertes Schalten einer Lampe in einem Schuppen per Bewegungsmelder, welcher neben dem Einschaltbefehl auch eine Einschaltdauer festlegt. Bei längerem Aufenthalt kann per Schalter das Ausschalten verhindert werden. Die Implementationen weiterer Wünsche aus einem Forum haben die Einsatzmöglichkeiten des Skripts deutlich erweitert. Erforderlich ist ein skriptfähiger Shelly mit zumindest einem Schaltausgang, bspw. ein Shelly Plus 1.

Inhalt

Liste an zusätzlichen Teilen

Diese Teile können untereinander auch alternativ sein. Letztlich ist jedes dieser Teile optional, aber zumindest eines davon ist einzusetzen, wenn nicht per Smartphone, Tablet o.ä. geschaltet werden soll. Jedes dieser Teile kann auch mehr als einmal hierfür genutzt werden.

  • Bewegungsmelder, bspw. ein Shelly Motion
    Ein solcher kann per Nachrichten den Aktor schalten lassen oder elektrisch mit einem Eingang des Schalt-Aktors verbunden werden.
  • Schalter, welcher elektrisch mit einem Eingang des Schalt-Aktors verbunden wird
  • Taster, elektrisch mit einem Eingang des Schalt-Aktors verbunden oder remote das Schalten per Nachricht  auslösend

Tatsächlich ist hier jedes Teil geeignet, welches eine Nachricht per URL (s.u.) an den Schalt-Aktor senden kann. Wenn per elektrisch angeschlossenem Bewegungsmelder on- und off- Nachrichten verarbeitet werden sollen, dann ist der Eingang auf detached einzustellen und entsprechende Actions anzulegen. Gleiches gilt für einen Taster, der elektrisch angeschlossen ist.

Was ist ein URL und wozu wird er hier eingesetzt?

URL = Uniform Resource Locator kombiniert lokalisieren und identifizieren einer Ressource in Computer-Netzwerken. Zumeist wird ein URL per HTTP(S) als Webadresse verwendet. Hier dienen URL zur Kommunikation mit einem Shelly oder zwischen Shelly.

Ein solcher URL lokalisiert ein Gerät per Protokollangabe wie http:// und einer IP Adresse. Dahinter folgen Angaben, die auf dem Zielgerät zu einer Ressource führen. Oft ist diese Ressource eine Datei, sie kann aber auch zu einer Datenbankabfrage oder, wie in der hier vorliegenden Anwendung, zu einer Skriptfunktion führen. Dabei können an den eigentlichen URL Parameter angehängt werden, welche die Funktion verarbeiten sollte.

Zwecks Anwendung eines Shelly können auf einfache Weise bei der Konfiguration in sog. Actions URL eingesetzt werden. Diese Actions sind Ereignissen wie bspw. Signaländerungen an Eingängen zugeordnet. Auf diese Weise löst bspw. ein Tastendruck per Action das Senden einer Nachricht in Form eines URL ggf. mit Parametern aus. Mit dem Senden einer solchen Nachricht wird hier aus der Ferne in einem Skript eine Funktion aufgerufen, die das Gewünschte tut. Ein Gerät kann eine solche Nachricht auch an sich selbst senden. Dann ist die sog. localhost IP Adresse 127.0.0.1 zu verwenden.

Einrichten von beteiligten Geräten - die nutzbaren URL

Action URL auf Bewegungsmelder: Keine anderen als die aufgelisteten!

Die tatsächlichen, zum Schalt-Aktor passenden URL werden bei geöffnetem Skript nach dem Skriptstart im Protokollfenster ausgegeben. Somit kann von dort der benötigte URL kopiert und in einer Action eines Shelly eingefügt werden.

  1. http://<IP Zieladresse>/script/<Skript Id>/on?<Dauer in Sekunden>
    Schaltet für die Dauer ein, wenn ausgeschaltet ist - für "Motion detected" geeignet.
  2. http://<IP Zieladresse>/script/<Skript Id>/on
    Schaltet dauerhaft ein, wenn ausgeschaltet ist - für "Motion detected" geeignet.
  3. http://<IP Zieladresse>/script/<Skript Id>/off
    Schaltet aus, wenn per Bewegungsmelder, d.h. per "on" (s.o.), eingeschaltet wurde - für "End of motion detected" geeignet.

Action URL auf einem manuell bedienbarem Teil, wie Taster oder Schalter, bspw. Shelly i4 oder Shelly Button 1: Kein anderer URL!

http://<IP Zieladresse>/script/<Skript Id>/toggle

Die Verwendung des jeweils geeigneten URL ist entscheidend. Die obigen Punkte gelten somit für jedes eingesetzte Teil. Wird die Nachricht vom Aktor an sich selbst gesendet, dann ist als IP Adresse 127.0.0.1 einzusetzen.

Konfiguration der Anwendung

Es gibt drei Parameter zur Konfiguration. Zwei davon können auch im KVS (Key Value Storage) eingetragen werden und überschreiben dann die im Skript vorhandenen Werte.

  1. Retrigger - im KVS als motion_retrigger
    Beim Wert true retriggert eine on-Nachricht mit Zeitdauer-Parameter den Timer, welcher nach abgelaufener Dauer den Verbraucher ausschaltet. Mit jeder solchen Nachricht wird somit die Einschaltdauer verlängert. Mit false wird die Retriggerung deaktiviert. Falls in deiner Anwendung keine on-Nachricht mit Einschaltdauer verwendet wird, hat der Retrigger-Wert keine Wirkung.

  2. LatTime - im KVS als motion_latency
    Der Wert gibt eine Dauer in Sekunden an. Damit realisiert das Skript eine Latenz nach dem Ausschalten per toggle oder angeschlossenem Taster/Schalter. Trifft während dieser Latenz eine on-Nachricht ein, wird diese nicht verarbeitet. Dies kann nützlich bei Verwendung von Bewegungsmeldern sein. Nach dem manuellen Ausschalten kann dann der Bewegungsmelder erst nach Ablauf der Latenzzeit auf Grund einer Bewegungserkennung per on einschalten lassen. Damit hat der Nutzer nach manuellem Ausschalten genügend Zeit, den Erfassungsbereich des Bewegungsmelders zu verlassen, ohne dass Letzterer erneut einschalten lässt. Soll diese Latenz nicht genutzt werden, ist der Wert auf 0 (oder negativ) zu setzen. Wenn Nachkommastellen verwendet werden, wie in "4,5" sind diese per Dezimalpunkt einzusetzen, bspw. "4.5".

  3. out gibt die Id des zu schaltenden Ausgangs an. Da dieser Wert typischerweise höchstens einmal im selben Schalt-Aktor geändert wird, habe ich dafür keinen Eintrag im KVS vorgesehen.

Ob du die ersten zwei Parameter lieber im Skript oder im KVS änderst bzw. einträgst, bleibt dir überlassen.

Das Skript

Das Skript protokolliert wesentliche Teile des Ablaufs im Protokollfenster unterhalb des Skript-Editierfensters. Diese Protokollierung kann bei Bedarf zur Analyse von Fehlern bei unerwartetem Verhalten genutzt werden. Wenn jedoch das Skript abgebrochen wurde, ist eine Fehlermeldung vorzufinden, welche mir in einem solchen Fall mitgeteilt werden sollte - ggf. im Forum.

Es kann wie jedes andere Skript per Web UI installiert werden. Dazu kann einfach der folgende Code kopiert und in ein neu angelegtes, noch leeres Skript eingefügt werden ...

// Created by Gerhard Eichelsdörfer alias eiche - 2024-03-15

// --------- configuration ---------------
let Retrigger = true, // Set to false if you don't want the motion to retrigger!
LatTime = 10, // a latency time after switched off via "off" - in seconds
out = 0, // output id
// ------ end of configuration -----------

// can be changed, but only if necessary
MotionName = "motion", // for all items that switch via "on" or "off"
RemoteName = "remote", // for all items that switch via "toggle"
// -------------------------------------

// Please don't change anything from here!
on = null, // output status
th = null, // timer handle
tlat = null, // timer handle for latency after switched via "off"
onEn = true, // status for locking switching via "on"
src = null; // source of switching

function send_response(response, body) {
  response.code = 200;
  response.body = body;
  response.send();
}

function switchSet(id, state) {
  Shelly.call("Switch.set", {id:id, on:state},
    function(result, errcode, errmsg, ud) {
      if(errcode!==0) print("error in switchSet(" + ud.id + ", " + ud.state + "): ", errcode, ", ", errmsg);
    }, {id:id, state:state}
  );
}

function startTimer(dur) {
  Timer.clear(th);
  if(dur>0) {
    dur = Math.floor(1000*dur);
    th = Timer.set(dur, false, function () {switchSet(out, false);});
    print("timer started for " + (dur/1000) +" seconds");
  }
}

// from a remote motion sensor - with an optional duration value
HTTPServer.registerEndpoint('on',
  function (request, response) {
    send_response(response, "OK");
    if(onEn) {
      let para = 0;
      if(request.query.length > 0) para = JSON.parse(request.query);
      if(!isNaN(para) && para>0 && (!on || (Retrigger && src===MotionName))) startTimer(para);
      if(!on) {
        src = MotionName;
        switchSet(out, true);
      }
    }
  }
);

// from a remote motion sensor - e.g. at the end of motion detected
HTTPServer.registerEndpoint('off',
  function (request, response) {
    send_response(response, "OK");
    if(on && src===MotionName) switchSet(out, false);
  }
);

// from a remote toggle call, e.g. from an i4.
HTTPServer.registerEndpoint('toggle',
  function (request, response) {
    send_response(response, "OK");
    let tmp = src;
    src = RemoteName;
    let st = Shelly.getComponentStatus("switch:" + out);
    if(st.output && tmp===MotionName) {
      Timer.clear(th);
      Timer.clear(tlat);
      print("source: ", src, ", Timer cleared")
      return;
    }
    Shelly.call("Switch.Toggle", {id:out},
      function (result, errcode, errmsg, ud) {
        if(errcode!==0) {
          src = ud.src;
          print(ud.msg, errcode, ", ", errmsg); 
        }
      }, {src:tmp, msg:"error in remote toggle: "}
    );
  }
);

Shelly.addEventHandler(function(e) {
  //print(JSON.stringify(e));
  if(e.info.event==="toggle") {
    on = e.info.state;
    let st = Shelly.getComponentStatus("switch:" + out);
    if(st.source!==undefined && st.source!=="loopback") src = st.source;
    if(!on && src!==MotionName && LatTime>0) {
      onEn = false;
      tlat = Timer.set(LatTime*1000, false, function(){onEn = true;});
      print("motion latency for " + LatTime + "s")
    }
    if(on) Timer.clear(tlat);
    print("source: ", src, ", status: ", on ? "on" : "off");
  }
});

//Do some user friendly printout, thanks to ostfriese
function messages() {
  let ipAddress = Shelly.getComponentStatus("wifi").sta_ip;
  if(ipAddress===null) ipAddress = '<IP address of your switching Shelly>';
  let myId = Shelly.getCurrentScriptId();
  print('The action URL of your motion with an optional duration is:');
  print('http://' + ipAddress + '/script/' + myId + '/on?<duration in seconds>');
  print('For switching off via motion the action URL is:');
  print('http://' + ipAddress + '/script/' + myId + '/off'); 
  print('When switching via remote command, e.g. Shelly i4, the URL for the remote action is:');
  print('http://' + ipAddress + '/script/' + myId + '/toggle');
}

// if available, get configuration parameters
Shelly.call("KVS.GetMany", {key:"motion_"},
  function(result) {
    // print(JSON.stringify(result.items));
    if(result.items.motion_retrigger!==undefined)
      Retrigger = JSON.parse(result.items.motion_retrigger.value);
    if(result.items.motion_latency!==undefined) {
      let t = JSON.parse(result.items.motion_latency.value);
      if(!isNaN(t)) LatTime = t;
    }
    print("motion retrigger: " + Retrigger, ", motion latency time: " + LatTime + "s");
  }
);

let info = Shelly.getComponentStatus("switch:" + out);
//print(JSON.stringify(info));
src = info.source;
on = info.output;

messages();

 

Weitere Ideen

Das Skript kann bisher ausschließlich auf einem Schalt-Aktor genutzt werden, weil ein lokaler Ausgang geschaltet wird. Wenn man einen Shelly der ersten Generation schalten oder gar steuern (bspw. Leuchtenhelligkeit) lassen will, gelingt das mit dem derzeitigen Skript nicht. Ich denke darüber nach, auf welche relativ einfache Weise per URL ein remote Gerät vom Skript ausgehend gesteuert werden kann, möglichst konfigurierbar.

Gedanken zu einer Konfiguration

Prinzipiell besteht ein Shelly RPC aus einer Methode und einem Parameter-Objekt. Diese Kombination findet sich auch in Schedule Jobs wieder. Evtl. ist somit dafür eine solche Objektstruktur geeignet:

Action = {method:<Methodenbezeichner>, parameter:<Parameter-Objekt>}

Dies kann den oben beschrieben URL zugeordnet werden.

=> Für eine lokale Steuerung bspw.
onAction = {method:"Switch.Set", parameter: {id:out, on:true}, callback: onCallback}
offAction = {method:"Switch.Set", parameter: {id:out, on:false}, callback: offCallback}
toggleAction = {method:"Switch.Toggle", parameter: {id:out}, callback: toggleCallback}

=> Für eine remote Steuerung bspw.
onAction = {method:"HTTP.Get", parameter: {url:"http://172.16.10.1/light/0?turn=on"}, callback: httpResponse}
offAction = {method:"HTTP.Get", parameter: {url:"http://172.16.10.1/light/0?turn=off"}, callback: httpResponse}
toggleAction = {method:"HTTP.Get", parameter: {url:"http://172.16.10.1/light/0?turn=toggle"}, callback: httpResponse}

Selbstverständlich kann jegliche dieser Action-Objekte per Methode "HTTP.Get" verarbeitet werden, auch lokale. Für letztere wäre dann als IP Adresse 127.0.0.1 einzusetzen. Ich bin allerdings ein Fan von rein lokaler Verarbeitung, wann immer dies möglich ist.😉

Weil eine solche Konfiguration ein gewisses Grundverständnis im JSON-Format erfordert, kann leider nicht jeder interessierte Anwender solche Objektstrukturen zusammensetzen. Hier mag eine aufgeteilte Konfiguration im KVS hilfreich sein.
Z.B. zur lokalen Steuerung per onAction - für remote  Steuerung entsprechend:
"onAction_type": "local"
"onAction_method": "Switch.Set"
"onAction_parameter_id": "0"
"onAction_parameter_on": "true"

Solche Einträge sind relativ leicht zu verstehen und einzutragen. onAction_type = "local" kann dabei zu einer Standard-Funktion führen, die die Parameter result, errcode und errmsg importiert. Das Problem besteht in einer Methoden abhängigen Anzahl an Parameter-Komponenten - Switch.Set benötigt id und on, Switch.Toggle nur id. Ein Parameter-Objekt kann dies lösen, ist aber weniger Anwender freundlich.

Wie gut mir das Einbinden in das bisherige Skript gelingen wird, unter Berücksichtigung von Anwenderfreundlichkeit, weiß ich noch nicht. Hier ist eine sorgfältige Planung inkl. brainstorming sehr wichtig.

Nach Tests werde ich voraussichtlich die folgende Parameter-Notation anstreben.
"onAction_parameter":'{"id":0, "on":true}'
Man beachte die single quotes (Hochkomma) um die Objekt-Notation. Dies ist erforderlich, wenn der Anwender einen solchen Eintrag per Web UI lesen und editieren können soll. Dieser String lässt sich mit JSON.parse() verarbeiten, also in ein JavaScript Objekt konvertieren.

Oder ich begnüge mich vielleicht doch mit immer der gleichen Methode "HTTP.Get" und als Parameter einen URL. Das wäre deutlich einfacher zu implementieren und vielleicht auch leichter zu konfigurieren. 🤔
"onURL": "http://127.0.0.1/rpc/Switch.Set?id=0&on=true" - bspw. für ein lokales Einschalten.

2024-03-17