ical-Abfallkalender in openHAB verwenden

openhab-logo-square2Ein Beitrag in einem Forum brachte mich auf die Idee, mich in openHAB über anstehendene Abholtermine für die verschiedenen Abfallbehälter informieren zu lassen. Viele örtliche Abfallunternehmen bieten den jährlichen Kalender mittlerweile auch digital im ICAL-Format an. Diesen können wir nutzen, um ihn zunächst via JavaScript zu parsen und anschließend die anstehenden Termine in openHAB anzuzeigen. Dazu musste das ursprüngliche Script ein wenig angepasst und verschiedene Bereiche in openHAB konfiguriert werden. Näheres zeigt dieser Beitrag.

Verfügbare Artikel zu openHAB

little_blue_dot Einleitunglittle_blue_dot Einbinden der Internetgeschwindigkeitlittle_blue_dot RFID-Lesegerät einbinden
little_blue_dot Installationlittle_blue_dot Präsenzerkennung via Bluetooth und Pinglittle_blue_dot Einbinden von DS18B20-Sensoren
little_blue_dot Konfigurationlittle_blue_dot Kleines Netzwerkmonitoringlittle_blue_dot ical-Abfallkalender einbinden
little_blue_dot Reverse-Proxy für OpenHABlittle_blue_dot Einbinden von LG-Fernsehern [Update]little_blue_dot openHAB als Wecker
little_blue_dot Datenspeicherung und Statistikenlittle_blue_dot Einbinden Schaltern und Kontaktenlittle_blue_dot Einbinden von Bewegungssensoren
little_blue_dot Regeln planen und konfigurierenlittle_blue_dot Einbinden von Funksteckdosen (433 MHz)little_blue_dot Einbinden einer FritzBox
little_blue_dot Einbinden von USB-Steckdosenleisten (SIS-PMS)little_blue_dot Infrarotgesteuerte Geräte einbindenlittle_blue_dot Sprachausgabe auf einem Raspberry
little_blue_dot Einbinden von Temperatursensorenlittle_blue_dot Steuerung mit NFC-Tagslittle_blue_dot Schaltbare Steckdosenleiste mit Relais
little_blue_dot Externer Zugriff mit my.openhab

 

 

Quelle und Dank

Zunächst möchte ich jedoch den eigentlichen Ursprung dieser Methode erwähnen. Vielen Dank dazu an „seppy“, der den „icalendar JavaScript parser“ für seine Zwecke angepasst hat und die Umsetzung für diesen Beitrag deutlich erleichtert hat. Ich muss zugeben, dass meine Java(-Script)-Skills gegen Null tendieren.

http://openhabforum.de/viewtopic.php?t=11

 

Abfallkalender finden

Zunächst benötigen wir den Abfallkalender im bereits erwähnten ical-Format. Dazu bemühen wir am besten eine Suchmaschine unserer Wahl und kämpfen uns durch bis zu unserem örtlichen Entsorgungsunternehmen. In unserem Beispiel arbeiten wir mit der „Entsorgungswirtschaft Soest GmbH“. Wenn das Unternehmen vor Ort einen solchen Service nicht anbietet, könnte man vielleicht mit diesem Kontakt aufnehmen und die Idee der digitalen Bereitstellung vorschlagen. In unserem Fall haben wir die genaue Adresse (URL) für den ical-Abfallkalender herausgefunden:

http://esg-soest.de/ak_ics.php?ak_ortid=11&ak_strid=8033&ak_j=2016

 

Addon installieren

Falls wir das „HTTP-Addon“ bisher für kein anderes Item einsetzen und dieses folglich noch nicht „installiert“ wurde, holen wir dieses nun nach. Dazu verwenden wir einfach die apt-Paketverwaltung:

apt-get install openhab-addon-binding-http

In diesem Fall arbeite ich mit der Version 1.8.0. Das einbinden neuer Addons kann auch im laufenden Betrieb von openHAB durchgeführt werden.

 

Items definieren

Anschließend konfigurieren wir openHAB und beginnen mit dem Anlegen neuer Items. Dazu kann übersichtshalber eine neue .item-Datei verwendet werden. Für unsere Abfall-Events benötigen wir drei neue Items:

String ABFALL_ICAL { http="<[http://esg-soest.de/ak_ics.php?ak_ortid=11&ak_strid=8033&ak_j=2016:360000:JS(abfall_heute.js)]"
Switch ABFALL_AKTIV
String ABFALL_NAME

Das erste Item wird mit dem Inhalt des in der URL angegebenen Kalenders befüllt. Dieser Inhalt wird jedoch zuvor mit dem Script „abfall_heute.js“ geparst und erhält als Rückgabe entweder „false“ oder den Wert der heutigen Abholung. Im zweiten Item definieren wir einen Schalter, der später auf aktiv gesetzt wird, wenn heute eine Abholung ansteht. Und zu guter Letzt befindet sich im dritten Item der Name der Abholung, wenn diese aktiv sein sollte.

 

Das Transform-Script

Kommen wir zum eigentlichen Kernstück des heutigen Beitrages, dem JavaScript. Dieses basiert auf ein bereits veröffentlichtes Script, um ical-Kalender zu durchsuchen und bei bestimmten Übereinstimmungen den aktuellen Termin als Rückgabewert zu liefern. Dieses wurde leicht verändert, so dass ein Termin nur bei absoluter Übereinstimmung mit dem heutigen Tag ausgegeben wird. Dieses Script speichern wird beispielsweise unter dem Namen „abfall_heute.js“ im Ordner „/ect/openhab/configurations/transform„.

var abfall = false;
(function(i) {

	var Ical = function Ical(){
		this.version = '';
		this.prodid = '';
		this.events = [];
		this.todos = [];
		this.journals = [];
		this.freebusys = [];
	}
	var xprops = 'x-[^:;]+';
	var ianaprops = '[\\w]+[^:;]+'
	var icalParser = {
		icals : [],
		propsList : {
			'event':'(dtstamp|uid|dtstart|class|created|description|geo|last-mod|location|organizer|priority|seq|status|summary|transp|url|recurid|rrule|dtend|duration|attach|attendee|categories|comment|contact|exdate|rstatus|related|resources|rdate|'+xprops+'|'+ianaprops+')',
			'freebusy':'(dtstamp|uid|contact|dtstart|dtend|organizer|url|attendee|comment|freebusy|rstatus|'+xprops+'|'+ianaprops+')',
			'journal':'(dtstamp|uid|class|created|dtstart|last-mod|organizer|recurid|seq|status|summary|url|rrule|attach|attendee|categories|comment|contact|description|exdate|related|rdate|rstatus|'+xprops+'|'+ianaprops+')',
			'todo':'(dtstamp|uid|class|completed|created|description|dtstart|geo|last-mod|location|organizer|percent|priority|recurid|seq|status|summary|url|rrule|due|duration|attach|attendee|categories|comment|contact|exdate|rstatus|related|resources|rdate|'+xprops+'|'+ianaprops+')'
		},
		parseIcal : function(icsString){
			var cals = icsString.match(/BEGIN:VCALENDAR\r?\n(.*\r?\n)+?END:VCALENDAR/ig);
			for(var index in cals){
				//console.log("--->"+index+" "+cals[index]);
				var ical = new Ical(); 
				ical.version = this.getValue('VERSION',cals[index]);
				ical.prodid = this.getValue('PRODID',cals[index]);
				cals[index] = cals[index].replace(/\r\n /g,'');
				cals[index] = cals[index].replace(/BEGIN:VCALENDAR\r?\n/ig,'');
				var reg = /BEGIN:(V.*?)\r?\n(.*\r?\n)+?END:\1/gi;
				matches = cals[index].match(reg);
				if(matches){
					for(i=0;i<matches.length;i++){
						//console.log('---------->'+matches[i]+"\n<------------");
						this.parseVComponent(matches[i],ical);
					}
				}
				this.icals[this.icals.length] = ical;
			}
		},
		parseVComponent : function(vComponent,ical){
			var nameComponent = vComponent.match(/BEGIN:V([^\s]+)/i)[1].toLowerCase();
			vComponent = vComponent.replace(/\r?\n[\s]+/igm,''); //unfolding
			vComponent = vComponent.replace(/(^begin|^end):.*/igm,'');
			//console.log(nameComponent+' ++++ '+vComponent);
			var props = vComponent.match(new RegExp(this.propsList[nameComponent]+'[:;].*','gim'));
			if(props){
				var component=[];
				for(var index in props){
					var nom = props[index].replace(/[:;].*$/,'');
					//console.log("--vcompo "+index+" "+nom);
					var propKey = /*'prop_'+*/nom.toLowerCase();
					if(component[propKey]===undefined) component[propKey] = [];
					component[propKey][component[propKey].length] = this.getValue(nom,props[index]);
					component['raw'] = vComponent;
				}
				if(ical[nameComponent+'s'] !== undefined)
					ical[nameComponent+'s'][ical[nameComponent+'s'].length] = component;
			}
		},
		getValue: function(propName,line){
			//console.log(line);
			var prop={};
			line = line.replace(/^\s+/g,'').replace(/\s+$/gi,'');
			reg = new RegExp('('+propName+')((?:;[^=]*=[^;:\n]*)*):([^\n\r]*)','gi');
			var matches = reg.exec(line);
			if(matches){ //on a trouvé la propriété cherchée
				//console.log(propName+' ==] params='+RegExp.$2+' / valeur='+RegExp.$3);
				var valeur = RegExp.$3;
				var tab_params=[];
				if(RegExp.$2.length>0){ //il y a des paramètres associés
					var params = RegExp.$2.substr(1).split(';');
					var pair;
					for(k=0;k<params.length;k++){
						pair = params[k].split('=');
						if(!pair[1]) pair[1] = pair[0];
						tab_params[pair[0]] = pair[1];
					}
				}
				prop = { value:valeur,name:propName };
				if(Object.keys(tab_params).length>0)
					prop.params = tab_params;
			}
			return prop;
		},
	}
	
	
	/**
	* Funktion um ical Datumsstring (YYYYMMDD) in Date Objekt zu wandeln
	*/
	function calenDateFrom(icalStr)  {
		var strYear = icalStr.substr(0,4);
		var strMonth = parseInt(icalStr.substr(4,2),10)-1;
		var strDay = icalStr.substr(6,2);
		var strHour = 0;
		var strMin = 0;
		var strSec = 0;

		var oDate =  new Date(strYear,strMonth, strDay, strHour, strMin, strSec)

		return oDate;
	}
	
	function calenDateTo(icalStr)  {
		var strYear = icalStr.substr(0,4);
		var strMonth = parseInt(icalStr.substr(4,2),10)-1;
		var strDay = icalStr.substr(6,2);
		var strHour = 23;
		var strMin = 59;
		var strSec = 59;

		var oDate =  new Date(strYear,strMonth, strDay, strHour, strMin, strSec)

		return oDate;
	}
	
    icalParser.parseIcal(input);
	var event = icalParser.icals[0].events[0];
	icalParser.icals[0].events.forEach(function (currentValue, index, originalArray) {
		/**
		*	Prüfen ob das aktuelle Datum zwischen zwei Daten liegt, bzw. auf einen Tag fällt
		*/
		var dateFrom = calenDateFrom(currentValue.dtstart[0].value);
		var dateTo = calenDateTo(currentValue.dtend[0].value);
		var dateCheck = new Date();

		if (dateCheck >= dateFrom && dateCheck <= dateTo){
			abfall = currentValue.summary[0].value;
		}
	});
    return abfall;
	//return dateCheck;
	
	
	
})(input);

 

Möchten wir anstatt der heutigen lieber die morgigen Abholtermine anzeigen, ändern wir die Zeile 96 in

var strDay = icalStr.substr(6,2)-1;

und die Zeile 109 in

var strDay = icalStr.substr(6,2)-1;

 

Dieses Script speichern wir ebenfalls im Ordner „/etc/openhab/configurations/transform/“ unter dem Dateinamen „abfall_morgen.js„. Daraufhin erstellen wir zusätzliche Items für die morgige Abfallabholung.

 

openHAB-Regeln erstellen

Nun können wir bereits jetzt den openHAB-Stream beobachten und warten ab, ob ein etwaiger Termin in das Item „ABFALL_ICAL“ geschrieben wird. Für die weitere Verwendung benötigen wir jedoch noch ein paar Regeln, die wir wie gewohnt in einer „.rules„-Datei im Ordner „rules“ erstellen:

rule "Abfallkalender"
when
   Item ABFALL_ICAL changed
then
   //Prüfen ob Abfall abgeholt wird
	if (ABFALL_ICAL.state != "false" && ABFALL_ICAL.state != "Uninitialized"){
      postUpdate(ABFALL_AKTIV,ON)
      postUpdate(ABFALL_NAME,ABFALL_ICAL.state)
   } else {
      postUpdate(ABFALL_AKTIV,OFF)
      postUpdate(ABFALL_NAME,"false")
   }
end

Hier wird geprüft, ob das Item „ABFALL_ICAL“ über einen Terminwert verfügt. In diesem Fall wird das Item „ABFALL_AKTIV“ auf aktiv gesetzt und der Name der Abfallabholung in das Textitem „ABFALL_NAME“ geschrieben. Ansonsten wird das Item „ABFALL_AKTIV“ deaktiviert und das Item „ABFALL_NAME“ geleert.

 

Ein interessanter Anwendungszweck ist, sich beim Verlassen der Wohnung an das Rausstellen des Mülls erinnern zu lassen. Dazu erweitern wir beispielsweise eine bereits bestehenden Präsenz-Regel wie folgt:

rule "Wohnung verlassen"
	when
		Item RFID_TINA changed from ON to OFF
	then
		if (ABFALL_AKTIV.state == ON) {
			if(BOXEN_EZ.state == OFF) {
				sendCommand(BOXEN_EZ, ON)
				Thread::sleep(1000)
				executeCommandLine("/scripts/vorlesen.sh@@236@@Du musst an den " + ABFALL_NAME.state + " denken, Tina.@@" + BOXEN_EZ_VOL.state, 2000)
				Thread::sleep(10000)
				sendCommand(BOXEN_EZ, OFF)
			}
			if(BOXEN_EZ.state == ON) {
				executeCommandLine("/scripts/vorlesen.sh@@236@@Du musst an den " + ABFALL_NAME.state + " denken, Tina.@@" + BOXEN_EZ_VOL.state, 2000)
			}
		}
end

In diesem Beispiel, dass wie ich zugeben muss, stark an die eigenen Gegebenheiten angepasst werden muss, wird beim verlassen der Wohnung geprüft, ob ein Abholtermin vorliegt. In diesem Fall werden die Lautsprecher im Esszimmer aktiviert und ein entsprechender Warnhinweis vorgelesen. Das Vorlesen mittels Text-to-Speach auf dem Raspberry habe ich bereits in diesem Artikel vorgestellt.

 

Die Sitemap erweitern

Sollte kein Abholtermin vorliegen, fände ich es weniger interessant, darüber eine Meldung anzeigen zu lassen. Daher nutze ich in diesem Fall ein dynamisches Item. Unser oben gezeigtes Regelwerk erlaubt es, zunächst festzustellen, ob ein Termin vorliegt oder nicht (Item „ABFALL_AKTIV„). Wenn dem so sein sollte, können wir in unserer Sitemap an gewünschter Stelle den Namen der Abholung anzeigen lassen:

Text item=ABFALL_NAME label="Abholung heute: [%s]" icon="abfall" visibility=[ABFALL_AKTIV==ON]

 

Das Icon „abfall“ habe ich wie immer aus der Bildersuche einer Suchmaschine gefunden ( im Format 32×32 Pixel) und unter „/opt/openhab/webapps/images“ gespeichert. Für den Privatgebrauch sehe ich da kein Problem. Bei Fragen oder Anregungen kann gerne das Kommentarsystem genutzt werden.

 

 

Update
Peter fragte mich, ob es auch möglich wäre, die nächsten zwei Abholtermine anzuzeigen. Auch wenn meine Java-Script Kenntnisse gegen Null tendieren, habe ich eine Möglichkeit gefunden

 

Wir benötigen für die Umsetzung vier neue Transform-Scripte:

  • Script 1 für die Ausgabe des Namens der nächsten Abholung
  • Script 2 für die Ausgabe des Datums der nächsten Abholung
  • Script 3 für die Ausgabe des Namens der übernächsten Abholung
  • Script 4 für die Ausgabe des Datums der übernächsten Abholung

Die Scripte speichern wir wie gewohnt unter „/etc/openhab/configurations/transform„.

 

Zudem definieren wir 4 neue Items…

String ABF1_NAME { http="<[http://ADRESSE_DES_KALENDERS.ics:360000:JS(script1.js)]" }
DateTime ABF1_DATE { http="<[http://ADRESSE_DES_KALENDERS.ics:360000:JS(script2.js)]" }

String ABF2_NAME { http="<[http://ADRESSE_DES_KALENDERS.ics:360000:JS(script3.js)]" }
DateTime ABF2_DATE { http="<[http://ADRESSE_DES_KALENDERS.ics:360000:JS(script4.js)]" }

 

…und binden die beiden nächsten Termine wie folgt in die Sitemap ein:

Text item=ABF1_DATE label="Nächster Abholtermin: [%1$tA, %1$td.%1$tm.%1$tY]" icon="abfall" {
    Frame label="Nächster Abholtermin" {
        Text item=ABF1_DATE label="Datum: [%1$tA, %1$td.%1$tm.%1$tY]" icon="calendar"
        Text item=ABF1_NAME label="Abholung: [%s]" icon="abfall"
    }
    Frame label="Übernächster Abholtermin" {
        Text item=ABF2_DATE label="Datum: [%1$tA, %1$td.%1$tm.%1$tY]" icon="calendar"
        Text item=ABF2_NAME label="Abholung: [%s]" icon="abfall"
    }
} //Abfall Ende

 

Wenn jemand mit JavaScript-Kenntnissen einen bessere Weg kennt, möge er sich bitte in den Kommentaren melden.