Zeitversetzte Ausführung einer Aktion

Ab und zu möchte man eine Aktion programmieren, die erst zu einem späteren Zeitpunkt ausgeführt werden soll. Z.B. das Heimkino ausfahren…
Die naheliegende Lösung ist die Funktion IPS_Sleep(). Doch bekanntlich darf man die nicht verwenden für Verzögerungen länger als eine Sekunde.
Die „offizielle“ Lösung wird auf http://www.ip-symcon.de/service/dokumentation/vorgehensweisen/wie-kann-ich/ erklärt. Aber wirklich anfreunden konnte ich damit nie - ist mir ehrlich gesagt viel zu fummlig.

Wie schön wäre es doch, man hätte eine Lösung von der Eleganz und Einfachheit von IPS_Sleep()…
Nun, hier ist sie…

Zunächst das Beispiel eines Aufrufs:

<?
include('mylib.php');
exec_delayed("+10 sec", 'mylog("done");');
?>

Davor und danach kann selbstverständlich beliebiger anderer Code stehen, wie bei IPS_Sleep() auch.

mylib.php
Sehr oft hat man ja irgend eine Datei mit allerlei hilfreichen Funktionen.
Im Beispiel hier wäre das die Datei ‚mylib.php‘. Darin ist z.B. die Hilfsfunktion ‚mylog()‘, aber natürlich auch exec_delayed() selbst, sowie einige weitere. Die Idee ist, dass jeder die Library nach seinen Ansprüchen zurechtschneidert bzw. erweitert.

exec_delayed($delay /sec/, $code)
$delay kann entweder ein Integer sein (=Zeit in Sekunden) oder ein String, wie er von strtotime() interpretiert werden kann.
Damit ist fast alles möglich, wie „+10 Min“ oder „19:31“ etc.

$code enthält den auszuführenden Code.
Beachte: der Daemon lädt vorher automatisch mit ‚require_once(‚mylib.php‘);‘ die Library, das muss man also nicht mehr selber tun.

clear_scheduled_tasks()
→ löscht alle Aufträge, welche von diesem Script bisher aufgegeben worden sind.
Setzt man also diesen Aufruf vor exec_delayed(), so hat man eine Art „retriggerbares“ Verhalten.

clear_all_scheduled_tasks()
→ löscht sämtliche Aufträge, egal von welchem Script aufgegeben.

Zu beachten:
In der mylib.php muss man die erste Zeile anpassen, $daemon_id muss die IPS-ID des Ausführungs-Scriptes ‚DelayedExec_Daemon.php‘ enthalten.

Installation

[ol]
[li]an geeigneter Stelle im Objektbaum ein Script erzeugen mit dem Code von DelayedExec_Daemon.php
[/li][li]die Datei mylib.php ins Verzeichnis scripts\ kopieren (ggf. auch als Script in den Objektbaum einfügen)
[/li][li]die ObjektID des DelayedExec_Daemon’s in mylib.php einsetzen
[/li][li]Test-Script ausführen.
[/li][/ol]

Funktionsweise
exec_delayed() schreibt den Code in eine temporäre Variable unterhalb des DelayedExec_Daemon’s.
Ausserdem erzeugt es einen Timer für die gewünschte Zeit, welcher den DelayedExec_Daemon aufweckt.
Der DelayedExec_Daemon checkt alle Code-Variablen und prüft, ob deren Aufrufzeit erreicht (bzw. überschritten) ist, führt den Code aus und löscht Variable und Timer.

Viel Erfolg!
Dieter

mylib.php

<?
$daemon_id = 0; /* Modify: replace with ID of Daemon */

//-------------------------------------------------------------------------
function exec_delayed($delay /*sec*/, $code) {
	global $daemon_id, $IPS_SELF;
	$callers_name = getObjName($IPS_SELF);
	if (is_string($delay)) {
		$delay = intval(strtotime($delay)-time());
		if ($delay < 0) {
			$delay += 86400;
		}
	}
	if (!$daemon_id) {die("you need to set \$daemon_id first.");}
	$varname = date('Y_m_d_H_i_s', time()+$delay);

	$code .= " /*origin: $callers_name*/";
	$vID = IPS_CreateVariable(3);
	IPS_SetParent($vID, $daemon_id);
	IPS_SetName($vID, $varname);
	SetValueString($vID, $code);

	$eid = IPS_CreateEvent(1);
	IPS_SetParent($eid, $daemon_id);
	IPS_SetName($eid, "T{$varname}_$callers_name");
	IPS_SetEventScript($eid, $daemon_id);
	IPS_SetEventCyclic($eid, 0,0,0,0,1,$delay);
	IPS_SetEventActive($eid, true);

	if (strlen($code) > 40) {
		$code = substr($code, 0, 37).'...';
	}
	if ($delay < 600) {
		mylog(">>>  Cmd delayed for {$delay}s:  $code");
	} else {
		mylog(">>>  Cmd due at ".date('Y-m-d H:i:s', time()+$delay).":  $code");
	}
} // exec_delayed

//-------------------------------------------------------------------------
function clear_scheduled_tasks() {
	global $daemon_id, $IPS_SELF;
	$identifier = "($IPS_SELF)";
	$vars = IPS_GetChildrenIDs($daemon_id);
	foreach ($vars as $id) {
		$obj = IPS_GetObject($id);
		if ($obj['ObjectType'] == 2) {      // Variable
			$code = GetValueString($id);
			if ($code && (strpos($code, $identifier) !== false)) {
	        		IPS_DeleteVariable($id);
			}
		} elseif ($obj['ObjectType'] == 4) { // Event
			if (strpos($obj['ObjectName'], $identifier) !== false) {
				IPS_DeleteEvent($id);
			}
		}
	}
	mylog("Scheduled actions cleared for ".getObjName($IPS_SELF));
} // clear_scheduled_tasks

//-------------------------------------------------------------------------
function clear_all_scheduled_tasks() {
	global $daemon_id;
	$vars = IPS_GetChildrenIDs($daemon_id);
	foreach ($vars as $id) {
		$obj = IPS_GetObject($id);
		if ($obj['ObjectType'] == 2) {
	 		IPS_DeleteVariable($id);
		} elseif ($obj['ObjectType'] == 4) {
			IPS_DeleteEvent($id);
		}
	}
	mylog("All scheduled actions cleared");
} // clear_all_scheduled_tasks



//-----------------------------------------------------------------------
function mylog($text) {
	global $IPS_SELF;
	$objID = IPS_GetObject($IPS_SELF);
	$scriptName = $objID['ObjectName'];
	IPS_LogMessage($scriptName, $text);
} // mylog

//-------------------------------------------------------------------------
function getObjName($id = false) {
	global $IPS_SELF;
	if ($id == false) {
		$id = $IPS_SELF;
	}
	$obj = IPS_GetObject($id);
	$parentOjb = IPS_GetObject($obj['ParentID']);
	return "{$parentOjb['ObjectName']}/{$obj['ObjectName']}($id)";
} // getObjName

?>

DelayedExec_Daemon

<?
// DelayedExec_Daemon
$now = date('Y_m_d_H_i_s');
$vars = IPS_GetChildrenIDs($IPS_SELF);
foreach ($vars as $id) {
	$obj = IPS_GetObject($id);
	if ($obj['ObjectType'] == 2) {      // Variable
		if ($now > substr($obj['ObjectName'],0,19)) {
			$code = GetValueString($id);
			if ($code) {
				if (substr($code,-1) != ';') {
					$code = $code.';';
				}
				require_once('mylib.php');
				eval($code);
			}
			IPS_DeleteVariable($id);
		}
	} elseif ($obj['ObjectType'] == 4) { // Event
		if (substr($obj['ObjectName'],0,20) < 'T'.$now) {
			IPS_DeleteEvent($id);
		}
	}
}
?>

und Scripttimer wäre nicht gegangen?

IPS_SetScriptTimer($ScriptID, 10);

Klar geht das auch. Nur kannst du mit einem ScriptTimer nur ganze Scripts aufrufen.
Das bedeutet, dass ich für jede kleine Aktion (z.B. Aktor ausschalten) ein eigenes Script bräuchte, was ich persönlich ziemlich umständlich finde, mal abgesehen von den vielen Script-Leichen, die sich bald im Script-Ordner ansammeln werden.
Oder kennst du da eine elegantere Lösung?

Und wie würdest du die ScriptTimer bei Bedarf wieder abräumen? Das brauche ich zwingend, z.B. wenn ich eine komplexere Sequenz wie Abwesenheitssimulation vorzeitig abbrechen will. Kann ja sein, dass ich frühzeitig nach hause komme…

Lass bitte hören, wenn du eine ähnlich einfache „fire and forget“ Lösung kennst.

Hi Zurisee,

ich denke, ich hab Deine Lösung immer noch nicht verstanden :wink: Ich habs gestern erst flüchtig gelesen, dann nochmal etwas intensiver, mich hat der viele Text erschlagen (ist wahrscheinlich die Strafe für meine Texte :smiley: ).
Mit meiner Frage wollte ich eher etwas mehr Erläuterung provozieren :wink:

Mir ist noch nicht klar geworden, warum man „Timer“ braucht, die hinterher wieder gelöscht werden sollen

warum man „Timer“ braucht, die hinterher wieder gelöscht werden sollen

Entweder reden wir hier total aneinander vorbei oder ich versteh wiederum deine Fragen nicht…
Geht es dir um die Optimierung der Lösung oder um die grundsätzliche Frage, ob das Teil überhaupt was bringt?

Was auch immer, so viel ich verstanden habe, gibt es pro Script nur einen IPS_SetScriptTimer.
Die Ausführung einer zeitversetzten Aktion erfolgt ja in diesem Daemon, also einer Art Proxy-Prozess - der entsprechend nur einen IPS_SetScriptTimer hat. Wie soll ich dem also mehrere Weckzeiten mitgeben, wenn nicht mit Ereignissen? Es können ja beliebige Scripts die exec_delayed() Funktion in Anspruch nehmen.

Dieter

Hi Dieter,

es sollte auf keinen Fall Genörgel oder In Frage stelle der Lösung sein:

sehr wahrscheinlich, und sehr wahrscheinlich liegt es an mir :wink:

nee, ich hab das Thema aus Anwendersicht nicht verstanden (abstrakt: was tut es?). Und aus dieser Perspektive stellte sich mir die Frage: warum nicht ein „IPS_SetScriptTimer($ScriptID, 10);“?
Und mit meinen Fragen wollte ich provozieren, dass Du darauf etwas näher eingehst. Ich denke, ich lass das an der Stelle mal gut sein. War keinesfalls wertend oder gar abwertend gemeint

Ich gehöre auch gerade zur Kategorie „Erklärbär versteht es nicht“ :D.

Was stört dich an den Ereignissen? Du kannst sie setzen, deaktiviern, löschen, im Script auswerten, …

Also eigentlich alles, was ich aus deiner Fukntion auch rauslese.

Also ich glaube es verstanden zu haben:rolleyes:.

Nehmen wir mal an, ich möchte aus einem Script heraus die Aufgabe erstellen meinen Weihnachtsbaum einmalig heute abend um 17Uhr einzuschalten.

Mal grob die bisherige Lösung in IPS:

  • IPS_CreateEvent (optional: IPS_SetParent, IPS_SetName, …)
  • IPS_SetEventCyclic
  • IPS_SetEventActive
  • IPS_SetEventScript ("-Weihnachtsbaum an-")
    (und dann muss dieses Event aber auch noch jemand wieder löschen…)

Lösung von zurisee:

exec_delayed("19:00", '-Weihnachtsbaum an-');

Ich seh da schon eine große Erleichterung, werd mir das mal näher anschauen :wink:

und genau hier hätte ich ein simples


IPS_SetScriptTimer($ScriptID, 10);

genommen. Und dafür brauche ich keine neue Funktion, dafür nehme ich den vorhandenen IPS-Befehl. Aber ich denke, es geht eher um das Davor (wie kreiere ich ein solches Event) und um das Danach (wie lösche ich die Objekte wieder).

Und genau da hab ich mein Verständnisproblem:
Das Ganze muss ich nach wie vor scripten (für ein einmaliges Event), egal, welche IPS-Befehle ich für den Timer nehme.

Ich grübel nun schon länger, welchen Anwendungsfall ich hätte, aber ich finde keinen.

Ich denke es geht darum, dass er ein gewisses Gerät zB schalten möchte mit Verzögerung:

Verzögerung 10Sek -> ZW_DimSet($ID, 60);

Und nicht erst ein komplettes Script anlegen will dafür.

Korrekt verstanden? :slight_smile:

So hab ichs auch verstanden.

PS_SetScriptTimer($ScriptID, 10); 

setzt voraus, dass es da schon ein Script gibt was genau macht was ich haben will. Das müsste ich händisch durch rumgeklicke erstmal erstellen und mit Inhalt befüllen.

So langsam verstehe ich auch, was vorher nicht angekommen war:)

Ja, es geht genau um das Drumherum, das ich in eine Library-Funktion und ein Daemon-Script verfrachte, sodass nur noch die eine Zeile übrig bleibt im aktuellen Script.

Selbst für eine komplexe Ablaufsteuerung, wie Anwesenheits-Simulation oder Heimkino-Ausfahren brauche ich also quasi pro Aktion nur grad eine Zeile.
Beispiel Heimkino:

[ul]
[li]Leinwand runter lassen
[/li][li]Beamer ausfahren
[/li][li]nach 20 Sec Sat-Empfänger einschalten
[/li][li]nach 30 Sec Beamer einschalten
[/li][li]nach 40 Sec Licht dimmen
[/li][/ul]
Mit IPS_SetScriptTimer($ScriptID, 10); müsste ich also 3 Hilfsscripts anlegen und die Timer setzen.
Alternativ könnte ich die Lösung von Steiner umsetzen und ein Script schreiben wie

$id = 22283 /*[0 OG\Flur\VELUX Rollläden\Velux Rollladen]*/;
$step =    GetValue(23228 /*[6 Szenarien\Beschatten\Step]*/);

if(($IPS_SENDER <> "TimerEvent")and($step == 0)){
    SetValue(21336 /*[6 Szenarien\Beschattung]*/, True);
    SetValue(23228 /*[6 Szenarien\Beschatten\Step]*/, 1);
    IPS_SetScriptTimer($IPS_SELF, 60);
    TMEX_F29_SetPin($id, 0, False);
    TMEX_F29_SetPin($id, 2, True);
    return;
}
if($IPS_SENDER == "TimerEvent"){
    switch($step) {
      case 1:
       SetValue(23228 /*[6 Szenarien\Beschatten\Step]*/, 2);
        IPS_SetScriptTimer($IPS_SELF, 10);
        TMEX_F29_SetPin($id, 2, False);
       break;
   case 2:
        SetValue(23228 /*[6 Szenarien\Beschatten\Step]*/, 3);
        IPS_SetScriptTimer($IPS_SELF, 60);
        TMEX_F29_SetPin($id, 1, True);
        break;
   case 3:
        SetValue(23228 /*[6 Szenarien\Beschatten\Step]*/, 4);
        IPS_SetScriptTimer($IPS_SELF, 10);
        TMEX_F29_SetPin($id, 2, False);
       break;
   case 4:
        SetValue(23228 /*[6 Szenarien\Beschatten\Step]*/, 5);
        IPS_SetScriptTimer($IPS_SELF, 20);
        HM_WriteValueFloat(42614 /*[0 OG\Schlafzimmer\Rollläden Kipp\HM R-Laden links]*/, "LEVEL", 1);  // runter
        break;
   case 5:
        SetValue(23228 /*[6 Szenarien\Beschatten\Step]*/, 6);
        IPS_SetScriptTimer($IPS_SELF, 20);
        HM_WriteValueFloat(27397 /*[0 OG\Schlafzimmer\Rollläden Kipp\HM R-Laden mitte]*/, "LEVEL", 1);  // runter
        break;
   case 6:
        // Ablauf  fertig
        SetValue(23228 /*[6 Szenarien\Beschatten\Step]*/, 0);
        IPS_SetScriptTimer($IPS_SELF, 0);
        HM_WriteValueFloat(58828 /*[0 OG\Schlafzimmer\Rollläden Kipp\HM R-Laden rechts]*/,"LEVEL", .5);  // runter
    }
}  

(Hab’s jetzt nicht an mein Beispiel angepasst, aber so in etwa würde es aussehen)
Da ist mir ehrlich gesagt folgendes Script lieber:

send_ir('Leinwand','down');
ENO_SwitchMode(11111 /*Beamer-Schublade*/, true);
exec_delayed(20, 'send_ir("SAT", "power");');
exec_delayed(30, 'send_ir("Beamer", "power");');
exec_delayed(40, 'ENO_DimSet(22222 /* Licht WZ */, 40);');

Genauso einfach sieht’s aus, wenn ich eine Aktion zu einer bestimmten Zeit auslösen will:

exec_delayed("21:30", 'say("Kinder, Zeit für\'s Bett!")');

Wie man sieht, hab ich’s halt gerne einfach und übersichtlich. Das gilt auch für die Funktionen send_ir() und say(), die ich mir mal zusammen gebaut und in mylib.php abgelegt habe…

So, jetzt hoffe ich, ist die Sache etwas klarer geworden. Sonst ist’s auch kein Problem, es kann schliesslich jeder nach seinem Gusto programmieren:)

Dieter

Aloha,

als Script-Newbee finde ich die Lösung mit exec_delayed sehr sinnvoll.

Mein Anwendungsfall:
Ich möchte alle 18 Rollläden herunterfahren. Ich möchte die Rollläden mit einer konfigurierbaren Zeit (ca 1-3 Sekunden) verzögert starten sodass eine Wellenbewegung beim Herunterfahren entsteht. Außerdem möchte ich auch nicht alle Motoren auf einmal starten.
Das soll (erstmal?) nur ein Script sein.

Nach meiner bisherigen Recherche müsste ich für IPS_SetScriptTimer für jeden Jalousieaktor ein Script zum Herunterfahren und eins zum Hochfahren schreiben und vor allem müsste ich mich auch um das Aufräumen der Timer kümmern. Das will ich aber gar nicht, ich will nur quasi per fire-and-forget die Jalousieaktoren zeitversetzt starten.

Für mich klingt exec_delayed sehr praktikabel. Ich bin aber auch für alles offen, wenn es für mein Szenario andere sinnvolle Lösungen gibt: Nur her damit! :slight_smile:

Danke!

cu
ALDITuete

Hallo,

auch ich bin begeistert von der Funktion.
Allerdings gibt es bei mir 1 Problem:
Wenn ich eine Zeit im „mktime ()“-Format eingebe, ist die zeitliche Ausführung des Befehl sehr ungenau.

Beispiel: Es ist 18 Uhr und ich ich starte das Script mit

exec_delayed("19:30", 'FHT_SetTemperature (55953 /*[Erdgeschoß\Eßzimmer\FHT80b]*/, 16)')

dann wird der Befehl erst um ca. 19:32 Uhr ausgeführt.
Gibt es dazu eine Lösung? Mache ich was falsch?

P.S. Das Beispiel ist vorerst nur ein Test ohne Sinn, zeigt aber schön die Problematik auf.