Montag, 7. Januar 2019

Kalender in FHEM - auf bestimmte Termine reagieren

Die Grundlage schaffen - ein Calendar Device

Für dieses Beispiel: ein Google Kalender. Als Grundlage brauchen wir die "Privatadresse" im iCal Format (Google Kalender Einstellungen/<Kalendername>/Kalendereinstellungen).
Diese URL wird einfach in die Calendar Definition eingesetzt, das Aktualisierungsintervall setze ich auf einen Tag. Man kann jederzeit ein reload des Kalenders durchführen. Man muss bedenken, dass vor allem viele Serientermine, eine nicht unerheblich Zeit beim reload beanspruchen. Das kann schnell mal mehrere Minuten dauern. In der Grundeinstellung erfolgt das blockierend. Je nach Kalender, kann also nach dem define die Oberfläche für ein paar Minuten "stehen"!
define TestKalender Calendar ical url https://calendar.google.com/calendar/ical/xxx/basic.ics 86400
Um zu testen, ob der Kalender richtig gelesen wird, kann man ihn jetzt einfach abfragen. Eine direkte Anzeige der Termine in Readings erfolgt nämlich nicht.

Abfragen machen

Die Abfrage der Termine ist nicht offensichtlich, es gibt aber einige Beispiele in der englischen Doku. Als Ausgangspunkt für mein Beispiel nehme ich mal die Weboberfläche. In der zweiten Auswahlbox in der get Zeile wählt man einfach events aus.
Als Ergebnis bekommt man in einer Box eine Liste der Kalendereinträge (events), ich habe heute (6.1.) drei Einträge drin. Wie man sieht, auch vergangene.
04.01.2019 08:00 4h Sprechstunde
06.01.2019 08:00 14h Heute frei
07.01.2019 08:00 11h Sprechstunde
In der leeren Box kann man weiter format und filter Angaben machen.

Das Ziel in diesem Beispiel soll sein, auf einen ganz bestimmten Eintrag im Kalender zur Startzeit des Termins etwas auszulösen: z.B. den Server an den Tagen wo Sprechstunde ist, kurz vor Arbeitsbeginn zu starten.

Event auswählen

Dazu erzeugt man am Besten einen Termin in naher Zukunft und schaut sich mit dem Eventmonitor die Events an:
2019-01-06 20:00:00 Calendar TestKalender changed: 79vs3fq7siulo1hskdn4gtht7kgooglecom start
2019-01-06 20:00:00 Calendar TestKalender start: 79vs3fq7siulo1hskdn4gtht7kgooglecom 
2019-01-06 20:00:00 Calendar TestKalender modeUpcoming: 7fcbh4r7snu7iqovask7l8oq9qgooglecom
2019-01-06 20:00:00 Calendar TestKalender modeAlarmOrStart: 44318rlssm81janveuga0olanpgooglecom;79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:00:00 Calendar TestKalender modeChanged: 79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:00:00 Calendar TestKalender modeStart: 44318rlssm81janveuga0olanpgooglecom;79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:00:00 Calendar TestKalender modeStarted: 79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:00:00 Calendar TestKalender triggered
2019-01-06 20:00:00 Calendar TestKalender nextWakeup: 2019-01-06 20:05:00
2019-01-06 20:05:00 Calendar TestKalender changed: 79vs3fq7siulo1hskdn4gtht7kgooglecom end
2019-01-06 20:05:00 Calendar TestKalender end: 79vs3fq7siulo1hskdn4gtht7kgooglecom 
2019-01-06 20:05:00 Calendar TestKalender modeAlarmOrStart: 44318rlssm81janveuga0olanpgooglecom
2019-01-06 20:05:00 Calendar TestKalender modeStart: 44318rlssm81janveuga0olanpgooglecom
2019-01-06 20:05:00 Calendar TestKalender modeStarted: 
2019-01-06 20:05:00 Calendar TestKalender modeEnd: 26aq76ljdgjfmo444466faml09googlecom;79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:05:00 Calendar TestKalender modeEnded: 79vs3fq7siulo1hskdn4gtht7kgooglecom
2019-01-06 20:05:00 Calendar TestKalender triggered
2019-01-06 20:05:00 Calendar TestKalender nextWakeup: 2019-01-06 22:00:00
Man sieht jeweils 9 Events beim Start des Termines und 9 Events beim Ende. Allerdings keine Information über den "Lesbaren" Inhalt des Termines, diese kann man mittels der uid ($EVTPART1) auslesen. Es gibt zwei Events, jeweils den ersten und zweiten, die für einen trigger interessant sind.
TestKalender:changed:.*
TestKalender:changed:.*start
TestKalender:start:.*
Beim ersten regExp müsste/könnte man mit $EVTPART2 im Code abfragen ob es start oder end war. Bei den beiden anderen wird eindeutig auf start|end|alarm getriggert.

Exakte Abfrage auf einen Termin

Jetzt kann man im Code noch exakt abfragen, ob es sich um den richtigen Kalendereintrag handelt.
fhem('get '.$NAME.' events filter:uid=="'.$EVTPART1.'",field(summary)=~"(?i)sprechstunde" limit:count=1,from=0',1)
Die eigentliche Aktion ist damit recht simpel. Wenn die Abfrage des Events exakt genug ist, liefert sie nur bei Übereinstimmung ein Ergebnis. Damit sind alle notwendigen Komponenten komplett.
{fhem("set Server on") if defined fhem('get '.$NAME.' events filter:uid=="'.$EVTPART1.'",field(summary)=~"(?i)sprechstunde|notdienst" limit:count=1,from=0',1)}

Starte das Gerät zum Zeitpunkt

Ein notify, welches zu Beginn eines Kalenderevents, der den Begriff "Sprechstunde" in der Terminbeschreibung enthält, den Server startet:
define n_TestKalender notify TestKalender:changed:.*start {\
fhem("set Server on") if defined fhem('get '.$NAME.' events filter:uid=="'.$EVTPART1.'",field(summary)=~"(?i)sprechstunde" limit:count=1,from=0',1)\
}
Die Sache hat noch einen Schönheitsfehler: Die Sprechstunde beginnt zwar um 8:00 Uhr aber es wäre gut, wenn der Server  schon gestartet ist, wenn alle Mitarbeiter den Dienst beginnen. Dazu brauchen wir den Event nicht exakt zum Termin sondern vorher. Das kann das Calendar Modul erledigen. Mit diesem Attribute wird ein zusätzlicher alarm Event eine Stunde (3600 sec) vorm Termin in Abhängigkeit der Terminbeschreibung erzeugt.
attr TestKalender onCreateEvent { $e->{alarm}= $e->{start}-3600 if($e->{summary} =~ m/Sprechstunde/i)}

Starte das Gerät vor dem Zeitpunkt

Der Trigger im notify muss lediglich von start auf alarm geändert werden, der Ausführungsteil bleibt  identisch.
defmod n_TestKalender notify TestKalender:changed:.*alarm {}
Der Event für start und end bleibt erhalten, zu diesem Zeitpunkt kann man andere Aktionen ausführen.

Noch ein paar zusätzliche Tipps und Infos

Serientermine einer Serie (z.B. jeden Mittwoch 8:00) haben alle die gleiche UID. Bei einem Einzeltermin hätte man im notify über die uid den konkreten Zugriff auf genau den Termin, beim Serientermin erscheinen alle Termine der Serie, auch vergangene! Diese uid bleibt auch erhalten wenn man mal einen Termin der Serie modifiziert (Beschreibung, Zeit usw.)
Die Modi (alarm|start|end|upcoming) sind transient und geben quasi Auskunft über den aktuellen Status eines Termins (get <Kalender> events filter:mode=="<modus>").
  • upcoming - der Termin liegt in der Zukunft
  • end - der Termin ist abgelaufen und liegt in der Vergangenheit
  • start - der Termin ist gerade aktiv
  • alarm - die Alarmphase ist aktiv, also der Event alarm ist vorüber, der Event start noch nicht erreicht.
Achtung: will man auf end triggern und den Termin überprüfen (z.B. Inhalt von summary) dann darf  hideOlderThan nicht auf 0 stehen! Man kann sich leicht mit 1 oder 2 (in sec) behelfen. Nach dem end Event ist der Termin aus der Liste verschwunden und nicht mehr lesbar! 

Noch ein paar Codebeispiele

toDo

Tipp: Die Abfrage der Alarmzeit funktioniert z.B: mit
get TestKalender events filter:mode=="alarm" format:full limit:count=1,from=0
Will man die Tage bis zum nächsten Event wissen (hier einfach der nächste, man kann den Filter natürlich anders setzen). In einer verschachtelten zweiten Abfrage wird dann noch 1 in "morgen" und 0 in "heute" gewandelt.
{my $day = int((fhem('get '.$name.' events format:custom="$t1" limit:from=0,count=1',1) + 86399 - time)/86400);
$day?eval{$day>1?$day:"morgen"}:"heute"}
Will man z.B. die Geburtstage aus dem Google Kontakten in einen ical Kalender kopieren, habe ich hier ein Google Script gefunden. Direkt hat man leider dafür keine ical Adresse verfügbar.

10 Kommentare:

  1. Dieser Blogbeitrag hat mir sehr gut weitergeholfen, weil ich bei allen Versuchen, FHEM-Devices über Kalenderereignisse zu schalten bisher kläglich gescheitert bin.

    Aber gibt es eine Erklärung dafür, warum das nicht auch mit "end" und "off" funktioniert? Nach Ende der Sprechstunde könnte es ja auch Sinn machen, den Server wieder auszuschalten.

    define n_TestKalender notify TestKalender:changed:.*end {\
    fhem("set Server off") if defined fhem('get '.$NAME.' events filter:uid=="'.$EVTPART1.'",field(summary)=~"(?i)sprechstunde" limit:count=1,from=0',1)\

    AntwortenLöschen
    Antworten
    1. Ja, deswegen: end - der Termin ist abgelaufen und liegt in der Vergangenheit.
      Mit der Tatsache muss man den Filterteil "limit:count=1,from=0" anders setzen! Denn diese Bedingung ist nun nicht mehr wahr. Vielleicht braucht man die Zeiteinschränkung gar nicht, er liest ja nur die uid aus dem Event. Lass ",from=0" einfach weg? Oder versuch einfach auch das Beispiel aus der C-Ref mit $actor.

      Löschen
  2. Respekt und Hut ab! Zweieinhalb Jahre nach einem Blogbeitrag ein erster Kommentar und dann Reaktion innerhalb von weniger als drei Stunden.

    Mit dem Filterteil "limit:count=1,from=0" habe ich alle denkbaren Varianten - auch weglassen - ausprobiert, allerdings ohne Erfolg. Für weiterreichende Eingriffe fehlen mir leider die Kenntnisse. Das Beispiel mit $actor aus der commandref mag funktionieren, aber wer schreibt schon die Namen von FHEM-Devices in seinen Terminkalender. Der besondere Reitz an deinem Beispiel ist, dass es auch auf "Zahnarztsprechstundenhelfer" reagiert.
    Also werde ich noch etwas tüfteln und versuchen, mich irgendwo schlau zu lesen. Danke nach Leipzig.

    AntwortenLöschen
    Antworten
    1. Ich befürchte ich weiß warum, habe aber noch keine Lösung: Wenn man die *OlderThan Attribute im Calendar Device gesetzt hat, ist der Termin nach dem end ja quasi raus und wird nicht mehr angezeigt. Damit wird der Test nicht wahr. Das Beispiel mit dem Aktor meinte ich ja nur, weil es analog aufgebaut ist. Dort wird nur auf summary Inhalt getestet. Aber auch das dürfte nicht funktionieren, wenn man *OlderThan gesetzt hat.
      Besser wäre es das Thema im Forum zu diskutieren.

      Löschen
    2. Meine Vermutung war richtig, die Lösung "einfach":
      Anstatt hideOlderThan 0 auf hideOlderThan 2 setzen.
      Damit funktioniert ein notify auf end mit Abfrage der Eintrages.

      Löschen
  3. Ja, das war die Lösung. Wenn man dann noch den Filterteil "limit:count=1,from=0" ganz weglässt, funktioniert es.
    Ich könnte zu diesem Thema zwar im Forum eine Diskussion beginnen. Es fehlt mir aber an Kompetenz, um dort wirklich konstruktiv mitzuwirken. Ich weiß, dass du dort sehr aktiv bist. Evtl. könntest du das bei Bedarf übernehmen. Aber es gab ja zweieinhalb offensichtlich Jahre keinen Bedarf.
    Herzlichen Dank und Grüße nach Leipzig

    AntwortenLöschen
  4. Scheitere an der Ical-Verbindung zur Nextcloud. Kennt jemand einen Weg? Evtl. eine Beispiel-URL? Von meinen Äpfeln klappt das ohne Probleme.

    AntwortenLöschen
  5. Damit geht es mit der Nextcloud und dem fhem-Kalender
    ical url https://@/remote.php/dav/calendars//?export

    attr timeout 60

    AntwortenLöschen
  6. Super Beitrag. Den Google-Kalender kann ich schon mal auslesen. Jetzt würde ich gerne täglich um 04:00 Uhr die Termine vom aktuellen und nächsten Tag per Telegram versenden.

    AntwortenLöschen
    Antworten
    1. Da würde ich ein at definieren und beim get die Limits auf so etwas wie from=0,to=+44h setzen.

      Löschen