Startseite » OpenHAB: Generische Alarme mit Schwellwerten und Einheiten

OpenHAB: Generische Alarme mit Schwellwerten und Einheiten

📅
•
⏱️ 35 Min. Lesezeit
•
✍️ Alexander Hacker

Es kann eine Vielzahl von Werten geben, die bestimmte Schwellwerte nicht überschreiten sollen. Die Luftfeuchte in Räumen, den CO2-Gehalt, Batterie-Status, Grenzwerte in Hydroponik.

ObenHAB Items

Um Items, die überwacht werden sollen, zu identifizieren, will ich Tags verwenden. Wo möglich sollen es die vordefinierten semantischen Tags sein. Aber es gibt auch Werte, die darüber hinaus gehen. Außerdem ist das Einheiten-System von OpenHAB an dieser Stelle ungemein nützlich, da es Auch verschiedene Einheiten für den gleichen Sachverhalt miteinander vergleichen kann.

Die unten stehenden Beispiele habe ich aus ihrem Kontext gerissen. Sie sollen lediglich verdeutlichen, dass an das, was du schon hast, lediglich noch Tags und eine „tg“ Trigger Group angefügt werden müssen und du im besten Fall sauber Einheiten definiert hast.

Für die Werte gilt die „tgWarnOnLimit“.

//Beispiel anhand von Wetterdaten
Number:Temperature		OpenWeatherMapWetterinformationen_Current_Aussentemperatur							"Aussentemperatur"				(fgPersist,igOWMCurrentHEF,tgWarnOnLimit,tgCalc)["Measurement","Temperature"]	{ channel="openweathermap:weather-and-forecast:api:hef:current#temperature" }
Number:Pressure			OpenWeatherMapWetterinformationen_Current_Luftdruck									"Luftdruck"						(fgPersist,igOWMCurrentHEF,tgWarnOnLimit)["Measurement","Pressure"]			{ channel="openweathermap:weather-and-forecast:api:hef:current#pressure" }
Number:Dimensionless	OpenWeatherMapWetterinformationen_Current_Luftfeuchtigkeit							"Luftfeuchtigkeit"				(fgPersist,igOWMCurrentHEF,tgWarnOnLimit,tgCalc)["Measurement","Humidity"]		{ channel="openweathermap:weather-and-forecast:api:hef:current#humidity" }
Number:Speed			OpenWeatherMapWetterinformationen_Current_Windgeschwindigkeit						"Windgeschwindigkeit"			(fgPersist,igOWMCurrentHEF,tgWarnOnLimit)["Measurement","Wind"]				{ channel="openweathermap:weather-and-forecast:api:hef:current#wind-speed" }
Number:Length			OpenWeatherMapWetterinformationen_Current_Regen										"Regen"							(fgPersist,igOWMCurrentHEF,tgWarnOnLimit)["Measurement","Rain"]				{ channel="openweathermap:weather-and-forecast:api:hef:current#rain" }
Number:Temperature		OpenWeatherMapWetterinformationen_Current_Taupunkt									"Taupunkt [%.2f °C]"			(fgPersist,igOWMCurrentHEF,tgWarnOnLimit)["Calculation","Dewpoint"]			//complex transformaion via rule

//Beispiel anhand von eigenen Messwerten
Number:Dimensionless	Arduino52_MQ2_CO		"CO [%.2f ppm]" 					<smoke>			(fgPersist,igArduino52,tgWarnOnLimit)["Measurement","CO"]									//complex transformation via rule
Number:Temperature		Arduino52_Temp			"Temperatur (52) [%.2f °C]" 		<temperature>	(fgPersist,igArduino52,igThermostatBueroAlex,sgOfficeAlex,tgWarnOnLimit)["Measurement","Temperature"]	{ alexa="CurrentTemperature", channel="mqtt:topic:myMosquitto:Arduino_52:temperature_C" }
Number:Temperature		Arduino52_DewPoint		"Taupukt [%.2f °C]"					<rain>			(fgPersist,igArduino52,sgOfficeAlex,tgWarnOnLimit)["Measurement","Dewpoint"]				{ channel="mqtt:topic:myMosquitto:Arduino_52:dew_point" }
Number:Pressure			Arduino52_Druck_hPa		"Luftdruck (52) [%.2f hPa]" 		<pressure>		(fgPersist,igArduino52,tgWarnOnLimit)["Measurement","Pressure"]							{ channel="mqtt:topic:myMosquitto:Arduino_52:air_pressure_hPa" }
Number:Dimensionless	Arduino52_LFeuch		"Luftfeuchte [%.2f %%]" 			<humidity>		(fgPersist,igArduino52,tgWarnOnLimit,sgOfficeAlex)["Measurement","Humidity"]{ alexa="RangeValue" [capabilityNames="Luftfeuchte,@Setting.Humidity", supportedRange="0:100:1", nonControllable=true], channel="mqtt:topic:myMosquitto:Arduino_52:humidity" }
Number:RadiationDoseEffective	Arduino52_Rad_mmSvph	"Rad [%.8f μSv]"			 				(fgPersist,igArduino52,tgWarnOnLimit)["Measurement","Radiation","mmSvph"]	{ channel="mqtt:topic:myMosquitto:Arduino_52:rad_mmSvph" }
Number:RadiationDoseEffective	Arduino52_Rad_mSvpy_Y	"Rad (calc) [%.3f mSv]"			 			(fgPersist,igArduino52,tgWarnOnLimit)["Measurement","Radiation","mSvpa"]
Number:RadiationDoseEffective	Arduino52_Rad_mSvpy_m	"Rad (calc) [%.3f mSv]"			 			(fgPersist,igArduino52,tgWarnOnLimit)["Measurement","Radiation","mSvpm"]

OpenHAB rule

Zunächste definiere ich über viele Zeilen hinweg mögliche Grenzwerte. Nicht alle verwende ich schon, aber wenn ich es später tue, habe ich die Recherche hier schon dokumentiert. Auch hier stehen an den Variablen überall die Einheiten dran.

Es folgt eine Funktion, die die Warnmeldungen dann per Telegram raussendet und prüft, dass Meldungen nicht zu oft gesendet werden. In den meisten Beispielen verwende ich 180min als Mindestabstand. Dein Telegram musst du hier natürlich noch anpassen.

Und zuletzt folgen die Kriterien, an welchen Grenzwerten gewarnt werden soll.

import java.util.Map
import java.util.List
import org.openhab.core.model.script.ScriptServiceUtil


//1 Vol-% = 10.000 ppm | ml/m³

//Quellen
//https://www.air-q.com/grenzwerte
//https://www.abc-gefahren.de/dateien/ausbildung/elh/3_gsg_grenzwerte_gase.pdf


//--Lux--
val Lux_Ref_moonless_overcast_night_sky=0.0001|"lx"
val Lux_Ref_moonless_clear_night_sky_with_airglow=0.002|"lx"	
val Lux_Ref_full_moon_on_a_clear_night=0.7|"lx"					//0.27 - 1.0
val Lux_Ref_civil_twilight_under_clear_sky=3.4|"lx"
val Lux_Ref_corridor=100|"lx"
val Lux_Ref_bedroom=150|"lx"
val Lux_Ref_living_room=200|"lx"
val Lux_Ref_kitchen_bathroom=300|"lx"
val Lux_Ref_sunrise_sunset=400|"lx"								
val Lux_Ref_office=500|"lx"										//320 - 500
val Lux_Ref_overcast_day=1000|"lx"						
val Lux_Ref_fully_daylight_without_direct_sun=17000|"lx"		//10.000 -  25.000
val Lux_Ref_direct_sunlight=50000|"lx"							//32.000 - 100.000


//--Radiation--
//https://www.bfs.de/DE/themen/ion/strahlenschutz/grenzwerte/grenzwerte.html//:~:text=Der%20Grenzwert%20f%C3%BCr%20die%20effektive,sind%20von%20diesen%20Begrenzungen%20ausgeschlossen.
//http://cholla.mmto.org/electronics/geiger/banggood/M4011.pdf

// Business : in milliSievert
val Rad_GW_business_year=20|"mSv"
val Rad_GW_emergency_case=250|"mSv"
val Rad_GW_business_lifetime=400|"mSv"
val Rad_GW_childbearing_women_per_month=2|"mSv"

// Privates : in milliSievert
val Rad_GW_underaged_and_unborn_people_per_year=1|"mSv"
val Rad_GW_organdosis_per_year=15|"mSv"

// Grenzwerte für kurze Zeiträume : in milliSievert
val Rad_GW_unborn_permanently_damaged=0.1|"mSv"		//Unterer Schätzwert des Schwellenwerts für Schädigungen des Ungeborenen
val Rad_GW_reversable_illness=1|"mSv"				//Bei akuter Exposition treten ab diesem Schwellenwert akute Strahleneffekte auf (zum Beispiel Kopfschmerzen, Übelkeit, Erbrechen)
val Rad_GW_potentially_deadly=3|"mSv"				//Ohne medizinische Eingreifen sterben bei dieser Dosis 50 Prozent der exponierten Personen nach 3-6 Wochen, wenn es sich um eine in kurzer Zeit erfahrene Strahlenbelastung handelte (LD50)
val Rad_GW_deadly=8|"mSv"							//Ohne entsprechende medizinische Behandlung bestehen nur geringe Überlebenschancen, wenn es sich um eine in kurzer Zeit erfahrene Strahlenbelastung handelte
val Rad_M4011_min_radiation_per_hour=20|"µSv"		// / h
val Rad_M4011_max_radiation_per_hour=120|"µSv"		// / h

//-- Humidity : Luftfeuchte in Prozent --
val Humidity_GW_lo=40.0|"%"
val Humidity_GW_hi=61.0|"%"

//-- Atemluft --
//		Gefahrbeschreibung = ""
//		explosiv = false
//		untereExplosionsgrenze = null //Vol %
//		HautresorptiverGefahrstoff = false	//Schutzkleidung tragen!
//		GewoehnungDesGeruchssinns = null

//	Acute Exposure Guideline Levels
//		AEGL1 = null	//ppm, Schwelle zum Unwohlsein
//		AEGL2 = null	//ppm, Schwelle zur Einschränkung
//		AEGL3 = null	//ppm, Schwelle zur tödl. Wirkung

//		ETW1 = null		//ppm, Einsatztoleranzwert nach 1h Expositionszeit
//		ETW4 = null		//ppm, Einsatztoleranzwert nach 4h Expositionszeit
//		MAK = null		//ppm, max. Arbeitsplatzkonzentration

	val Air_relGasdichte = 1
	val Air_mol = 29.949 //g/mol

//		GW = null	//eigens verwendeter Grenzwert


//-- H2 : Wasserstoff --
	val H2_Gefahrbeschreibung = "erstickend; explosiv"
	val H2_untereExplosionsgrenze = 40000|"ppm"
	val H2_relGasdichte = 0.06
	val H2_mol = 2.02 //g/mol

	val H2_GW = 40000|"ppm"		//eigens verwendeter Grenzwert: Explosionsgefahr


//-- LPG : leicht verflüssigbare Kohlenwasserstoff-Verbindungen (CmHn) --
	//	Propan C3H8, Propen (Propylen) C3H6, Butan C4H10, Buten (Butylen) C4H8, Isobutan (Methylpropan) C4H10, Isobuten (Methylpropen) C4H8
	val LPG_Gefahrbeschreibung = "erstickend / Wirkung auf Blut, Nerven und Zellen; explosiv"
	val LPG_untereExplosionsgrenze = 14000|"ppm"

	val LPG_MAK = 1000|"ppm"		//ppm, max. Arbeitsplatzkonzentration

	val LPG_relGasdichte = 2.1
	val LPG_mol = 42.08 //g/mol -> Propen

	val LPG_GW = 1000|"ppm"			//ppm, eigens verwendeter Grenzwert: MAK


//-- CH4 : Methan --
	val CH4_Gefahrbeschreibung = "erstickend; explosiv"
	val CH4_untereExplosionsgrenze = 44000|"ppm"
	val CH4_relGasdichte = 0.055
	val CH4_mol = 16.04 //g/mol

	val CH4_GW = 44000|"ppm"		//ppm, eigens verwendeter Grenzwert: Explosionsgefahr


//-- CO : Kohlenstoffmonoxid --
	val CO_Gefahrbeschreibung = "Wirkung auf Blut, Nerven und Zellen; explosiv"
	val CO_untereExplosionsgrenze = 130000|"ppm"

	//	Acute Exposure Guideline Levels
	val CO_AEGL2 = 33|"ppm"			//ppm, Schwelle zur Einschränkung

	val CO_ETW1 = 83|"ppm"			//ppm, Einsatztoleranzwert nach 1h Expositionszeit
	val CO_ETW4 = 33|"ppm"			//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	val CO_MAK = 30|"ppm"			//ppm, max. Arbeitsplatzkonzentration

	val CO_relGasdichte = 0.96
	val CO_mol = 17.03 				//g/mol

	val CO_GW = 8|"ppm"				//ppm, eigens verwendeter Grenzwert: Umweltbundesamt 8h-Mittelwert


//-- Smoke : Rauch --


//-- C3H8 : Propan --
	val C3H8_Gefahrbeschreibung = "erstickend; explosiv"
	val C3H8_untereExplosionsgrenze = 17000|"ppm"

	val C3H8_MAK = 1000|"ppm"		//ppm, max. Arbeitsplatzkonzentration

	val C3H8_GW = 1000|"ppm"		//ppm, eigens verwendeter Grenzwert: MAK


//-- C6H6 : Benzol --
	val C6H6_Gefahrbeschreibung = "Wirkung auf Blut, Nerven und Zellen; explosiv"
	val C6H6_untereExplosionsgrenze = 12000|"ppm"
	val C6H6_HautresorptiverGefahrstoff = true	//Schutzkleidung tragen!

	//	Acute Exposure Guideline Levels
	val C6H6_AEGL1 = 18|"ppm"		//ppm, Schwelle zum Unwohlsein
	val C6H6_AEGL2 = 400|"ppm"		//ppm, Schwelle zur Einschränkung

	val C6H6_ETW4 = 20|"ppm"		//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	val C6H6_MAK = 1|"ppm"			//ppm, max. Arbeitsplatzkonzentration

	val C6H6_relGasdichte = 2.7
	val C6H6_mol = 78.114 			//g/mol

	val C6H6_GW = 1|"ppm"			//ppm, eigens verwendeter Grenzwert


//-- C6H14 : Hexan --
	val C6H14_Gefahrbeschreibung = "Wirkung auf Blut, Nerven und Zellen; explosiv"
	val C6H14_untereExplosionsgrenze = 11000|"ppm"

	//	Acute Exposure Guideline Levels
	val C6H14_AEGL2 = 2900|"ppm"	//ppm, Schwelle zur Einschränkung

	val C6H14_ETW1 = 2900|"ppm"		//ppm, Einsatztoleranzwert nach 1h Expositionszeit
	val C6H14_ETW4 = 2900|"ppm"		//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	val C6H14_MAK =  50|"ppm"		//ppm, max. Arbeitsplatzkonzentration

	val C6H14_relGasdichte = 3.0
	val C6H14_mol = 86.17 			//g/mol

	val C6H14_GW = 50|"ppm"			//ppm, eigens verwendeter Grenzwert


//-- NOx : Stickstoffoxide --
	val NOx_Gefahrbeschreibung = "reizend"

	//	Acute Exposure Guideline Levels
	val NOx_AEGL1 = 0.5|"ppm"		//ppm, Schwelle zum Unwohlsein
	val NOx_AEGL2 = 8.2|"ppm"		//ppm, Schwelle zur Einschränkung

	val NOx_ETW1 = 12|"ppm"			//ppm, Einsatztoleranzwert nach 1h Expositionszeit
	val NOx_ETW4 = 8.2|"ppm"		//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	val NOx_MAK = 5|"ppm"			//ppm, max. Arbeitsplatzkonzentration

	val NOx_relGasdichte = 1.6
	val NOx_mol = 46.01				//g/mol

	val NOx_GW = 0.5|"ppm"			//ppm, eigens verwendeter Grenzwert: AGS Grenzwert, längerfristige Exposition


//-- CL2 : Chlor --
	val CL2_Gefahrbeschreibung = "reizend; explosiv"

	//	Acute Exposure Guideline Levels
	val CL2_AEGL1 = 0.5|"ppm"	//ppm, Schwelle zum Unwohlsein
	val CL2_AEGL2 = 1|"ppm"		//ppm, Schwelle zur Einschränkung

	val CL2_ETW1 = 1|"ppm"		//ppm, Einsatztoleranzwert nach 1h Expositionszeit
	val CL2_ETW4 = 2|"ppm"		//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	val CL2_MAK = 0.5|"ppm"		//ppm, max. Arbeitsplatzkonzentration

	val CL2_relGasdichte = 2.5
	val CL2_mol = 70.9			//g/mol

	val CL2_GW = 0.5|"ppm"		//ppm, eigens verwendeter Grenzwert


//-- O3 : Ozon --
	//	Acute Exposure Guideline Levels
	val O3_MAK = 0.1|"ppm"		//ppm, max. Arbeitsplatzkonzentration

	val O3_relGasdichte = 1.78
	val O3_mol = 48.00			//g/mol

	val O3_GW = 0.06|"ppm"		//ppm, eigens verwendeter Grenzwert: Umweltbundesamt
	//https://www.engineeringtoolbox.com/air-contaminants-limits-d_1538.html
	val O3_GW_mgm3=180|"mg/m³"


//-- CO2 : Kohlenstoffdioxid --
	val CO2_Gefahrbeschreibung = "erstickend, Wirkung auf Blut, Nerven und Zellen"

	val CO2_ETW4 = 10000|"ppm"	//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	val CO2_MAK = 5000|"ppm"	//ppm, max. Arbeitsplatzkonzentration

	val CO2_relGasdichte = 1.5
	val CO2_mol = 44.01 //g/mol
		
	val CO2_GW=1000|"ppm"				//ppm, eigens verwendeter Grenzwert: Lüften-Schwellwert
	val CO2_GW_MQ135=600|"ppm"			//ppm, Mess-Ende des MQ135

//-- C2H5OH : Ethanol (evtl. auch andere Alkohole) --
	val C2H5OH_Gefahrbeschreibung = "Wirkung auf Blut, Nerven und Zellen; explosiv"
	val C2H5OH_untereExplosionsgrenze = 31000|"ppm"

	val C2H5OH_ETW4 = 3000|"ppm"	//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	val C2H5OH_MAK = 500|"ppm"		//ppm, max. Arbeitsplatzkonzentration

	val C2H5OH_relGasdichte = 1.6
	val C2H5OH_mol = 46.07 //g/mol

	val C2H5OH_GW = 500|"ppm"	//ppm, eigens verwendeter Grenzwert: MAK


//-- NH4 : Ammonium --
	val NH4_mol = 18 //g/mol


//-- CH3 : Toluol --
	val CH3_Gefahrbeschreibung = "Wirkung auf Blut, Nerven und Zellen; explosiv"
	val CH3_untereExplosionsgrenze = 11000|"ppm"

	//	Acute Exposure Guideline Levels
	val CH3_AEGL1 = 67|"ppm"	//ppm, Schwelle zum Unwohlsein
	val CH3_AEGL2 = 310|"ppm"	//ppm, Schwelle zur Einschränkung

	val CH3_ETW1 = 560|"ppm"	//ppm, Einsatztoleranzwert nach 1h Expositionszeit
	val CH3_ETW4 = 310|"ppm"	//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	val CH3_MAK = 50|"ppm"		//ppm, max. Arbeitsplatzkonzentration

	val CH3_relGasdichte = 3.2
	val CH3_mol = 92.14 //g/mol

	val CH3_GW = 50|"ppm"		//ppm, eigens verwendeter Grenzwert: MAK


//-- CH3_2CO : Aceton --
	val CH3_2CO_Gefahrbeschreibung = "Wirkung auf Blut, Nerven und Zellen; explosiv"
	val CH3_2CO_untereExplosionsgrenze = 25000|"ppm"
	//		HautresorptiverGefahrstoff = null	//Schutzkleidung tragen!
	//		GewoehnungDesGeruchssinns = null

	//	//Acute Exposure Guideline Levels
	val CH3_2CO_AEGL1 = 200|"ppm"	//ppm, Schwelle zum Unwohlsein
	val CH3_2CO_AEGL2 = 1400|"ppm"	//ppm, Schwelle zur Einschränkung

	val CH3_2CO_ETW4 = 500|"ppm"	//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	val CH3_2CO_MAK = 500|"ppm"		//ppm, max. Arbeitsplatzkonzentration

	val CH3_2CO_relGasdichte = 2.0
	val CH3_2CO_mol = 58.08 //g/mol

	val CH3_2CO_GW = 200|"ppm"		//ppm, eigens verwendeter Grenzwert: AEGL1


//	// --- Athmosphärische Warnungen ---
//	// entsprechend #blf-unlinked/https://www.hlnug.de/fileadmin/dokumente/luft/monatsberichte/2018/LHMB_2018-03.pdf
//	
//-- PM10 : Feinstaub PM10 --
//https://www.umweltbundesamt.de/themen/luft/luftschadstoffe-im-ueberblick/feinstaub

val PM10_GW_mgm3=50|"mg/m³"						//max 35x im Jahr Jahresmittel
val PM10_GW_mgm3_averagePerYear=40|"mg/m³"		//im Jahresmittel

//-- PM2.5 : Feinstaub PM2.5 --
//https://www.umweltbundesamt.de/themen/luft/luftschadstoffe-im-ueberblick/feinstaub
val PM25_GW_mgm3_averagePerYear=25|"mg/m³"		//im Jahresmittel

//-- NO2 : Stickstoffdioxid --
//https://www.engineeringtoolbox.com/air-contaminants-limits-d_1538.html
val NO2_GW_mgm3=40|"mg/m³"

//-- SO2 : Schwefeldioxid --
//https://www.engineeringtoolbox.com/air-contaminants-limits-d_1538.html
val SO2_GW_mgm3=20|"mg/m³"


var lastSent = <String, ZonedDateTime>newHashMap

val sendOncePerPeriod = [ GenericItem triggeringItem, Map<String, ZonedDateTime> lastSent, String message, int minutes |
	if(!lastSent.containsKey(triggeringItem.name)){
		lastSent.put(triggeringItem.name, now.minusMinutes(minutes +1))
	}
	var ZonedDateTime oldLastSent = lastSent.get(triggeringItem.name)
	
	var tempTime = oldLastSent
	if(tempTime.plusMinutes(minutes).isBefore(now)){
		getActions("telegram","telegram:telegramBot:home_Bot").sendTelegram(-000000000L,message)
		lastSent.put(triggeringItem.name, now)
	}
]

rule "Sensor warn on limit"
when
	Member of tgWarnOnLimit changed
then
	//which room is affected due to the triggering item room?
	
	//detect itemGroup (ig) of the item
	val String igName = triggeringItem.getGroupNames.findFirst[ String groupName | groupName.startsWith("ig") ]
	if(igName === null){
		logInfo("Test", "triggeringItem: {}", triggeringItem )
	}
	val GroupItem igItem = ScriptServiceUtil.getItemRegistry.getItem(igName) as GroupItem
	
	//detect semanticGroup of the itemGroup
	val String sgName = igItem.getGroupNames.findFirst[ String groupName | groupName.startsWith("sg") ]
	var List<String> roomList = newArrayList(sgName)
	
	//Environment
	val float dewpointEnvironment = (OpenWeatherMapWetterinformationen_Current_Taupunkt.state as Number).floatValue()
	
	
	//do something with the rooms:
	roomList.forEach[ String sgName |
		val roomItem = ScriptServiceUtil.getItemRegistry.getItem(sgName) as GroupItem
		
		//what states has the room?
		val NumberItem dewpointItem = roomItem.allMembers.findFirst[ item | item.hasTag("Measurement") && item.hasTag("Dewpoint") ]
		val float dewpoint = (dewpointItem.state as Number).floatValue
		
		//val NumberItem humidityItem = roomItem.allMembers.findFirst[ item | item.hasTag("Measurement") && item.hasTag("Humidity") ]
		//val float humidity = (humidityItem.state as Number).floatValue
		
		//val NumberItem temperatureItem = roomItem.allMembers.findFirst[ item | item.hasTag("Measurement") && item.hasTag("Temperature") ]
		//val float temperature = (temperatureItem.state as Number).floatValue
		
		val int windows = roomItem.members.filter[ item | item.hasTag("Window")].size()
		val int openWindows = roomItem.members.filter[ item | item.hasTag("Window") && (item.state as Number).intValue > 0].size()
		
		//Was macht die Heizung?
		//Was steht im Heizkalender?
		
		if(triggeringItem.hasTag("Measurement") && triggeringItem.hasTag("Battery")){
			if((triggeringItem.state as Number).intValue <= 20){
				sendOncePerPeriod.apply(triggeringItem,lastSent,String::format("Batterie fast leer: %s in %s", triggeringItem.label , roomItem.label ), 180 )
			}
		}
		
		if(triggeringItem.hasTag("Measurement") && triggeringItem.hasTag("Humidity")){
			if(triggeringItem.state > Humidity_GW_hi){
				if(
					(windows == 0 && dewpoint >= dewpointEnvironment +2)
					|| (openWindows == 0 && dewpoint >= dewpointEnvironment +2 )
				){
					sendOncePerPeriod.apply(triggeringItem,lastSent,String::format("Luftfeuchte hoch: in %s, dringend lüften!", roomItem.label ), 180 )
				}
			}
			
			if(triggeringItem.state < Humidity_GW_lo){
				if(
					(windows == 0 && dewpoint <= dewpointEnvironment -2)
					|| (openWindows == 0 && dewpoint <= dewpointEnvironment -2 )
				){
					sendOncePerPeriod.apply(triggeringItem,lastSent,String::format("Luftfeuchte niedrig: in %s, dringend lüften!", roomItem.label ), 180 )
				}
			}
		}
		
		if(triggeringItem.hasTag("Measurement") && triggeringItem.hasTag("CO2")){
			if(triggeringItem.state > CO2_GW_MQ135){
				if(
					windows == 0
					|| (openWindows == 0 && dewpoint >= dewpointEnvironment +3 )
				){
					logInfo("Test", "triggeringItem: {}; Grenzwert: {}", triggeringItem, CO2_GW_MQ135 )
					sendOncePerPeriod.apply(triggeringItem,lastSent,String::format("CO2 hoch: in %s, dringend lüften!", roomItem.label ), 180 )
				}
			}
		}
		
		if(triggeringItem.hasTag("Measurement") && triggeringItem.hasTag("Radiation")){
			//Short-term Radiation
			if( triggeringItem.hasTag("mmSvph") ){
				if(triggeringItem.state > Rad_GW_childbearing_women_per_month){
					logInfo("Test", "triggeringItem: {}; Grenzwert: {}", triggeringItem, Rad_GW_childbearing_women_per_month )
					sendOncePerPeriod.apply(triggeringItem,lastSent,"Radioaktivität überschreitet kurzfristige Grenzwerte", 180 )
				}
			}
			
			//Mid-term Radiation
			if( triggeringItem.hasTag("mSvpm") ){
				if(triggeringItem.state > Rad_GW_childbearing_women_per_month){
					logInfo("Test", "triggeringItem: {}; Grenzwert: {}", triggeringItem, Rad_GW_childbearing_women_per_month )
					sendOncePerPeriod.apply(triggeringItem,lastSent,"Radioaktivität überschreitet mittelfristige Grenzwerte", 180 )
				}
			}
			
			//Long-term Radiation
			if( triggeringItem.hasTag("mSvpa") ){
				if(triggeringItem.state > Rad_GW_organdosis_per_year){
					logInfo("Test", "triggeringItem: {}; Grenzwert: {}", triggeringItem, Rad_GW_organdosis_per_year )
					sendOncePerPeriod.apply(triggeringItem,lastSent,"Radioaktivität überschreitet langfristige Grenzwerte", 180 )
				}
			}
		}
		
		if(triggeringItem.hasTag("Measurement") && triggeringItem.hasTag("O3")){
			if(triggeringItem.state > O3_GW_mgm3){
				logInfo("Test", "triggeringItem: {}; Grenzwert: {}", triggeringItem, O3_GW_mgm3 )
				sendOncePerPeriod.apply(triggeringItem,lastSent,"Athmosphärische Warnung: O3 (Ozon) kritisch!", 180 )
			}
		}
		
		if(triggeringItem.hasTag("Measurement") && triggeringItem.hasTag("PM10")){
			if(triggeringItem.state > PM10_GW_mgm3){
				logInfo("Test", "triggeringItem: {}; Grenzwert: {}", triggeringItem, PM10_GW_mgm3 )
				sendOncePerPeriod.apply(triggeringItem,lastSent,"Athmosphärische Warnung: PM10 (Feinstaubpartikel 10µm) kritisch!", 180 )
			}
		}
		
		if(triggeringItem.hasTag("Measurement") && triggeringItem.hasTag("PM25")){
			if(triggeringItem.state > PM25_GW_mgm3_averagePerYear){
				logInfo("Test", "triggeringItem: {}; Grenzwert: {}", triggeringItem, PM25_GW_mgm3_averagePerYear )
				sendOncePerPeriod.apply(triggeringItem,lastSent,"Athmosphärische Warnung: PM2.5 (Feinstaubpartikel 2.5µm) kritisch!", 180 )
			}
		}
		
		if(triggeringItem.hasTag("Measurement") && triggeringItem.hasTag("NO2")){
			if(triggeringItem.state > NO2_GW_mgm3){
				logInfo("Test", "triggeringItem: {}; Grenzwert: {}", triggeringItem, NO2_GW_mgm3 )
				sendOncePerPeriod.apply(triggeringItem,lastSent,"Athmosphärische Warnung: NO2 (Stickstoffdioxid) kritisch!", 180 )
			}
		}
		
		if(triggeringItem.hasTag("Measurement") && triggeringItem.hasTag("SO2")){
			if(triggeringItem.state > SO2_GW_mgm3){
				logInfo("Test", "triggeringItem: {}; Grenzwert: {}", triggeringItem, SO2_GW_mgm3 )
				sendOncePerPeriod.apply(triggeringItem,lastSent,"Athmosphärische Warnung: SO2 (Stickstoffdioxid) kritisch!", 180 )
			}
		}
	
	]
end

Ein Wort zu JS Automation und den Units. Es existiert ein Wrapper für die OpenHAB Units in den JS-Automations. Allerdings empfehle ich dringend Item-Werte auf eine Einheit zu normalisieren und dann als float ohne Einheiten weiterzubenutzen. Rechenoperationen werden derzeit (2026) auf unterschiedliche Weise verwendet.

Probleme werden deutlich an folgenden Beispielen

  • Unterschiedliche Bezugspunkte bei der Verrechnung: Versuchst du, eine Durchschnittstemperatur aus zwei Werten (z.B. 18°C und 22°C) zu berechnen, scheint es eine gute Idee zu sein, beide zu addieren und durch 2 zu dividieren. Addition und Division werden allerdings zu unterschiedlichen Bezugspunkten (°C und Kelvin) verrechnet und so entsteht ein unvorhergesehener Wert, der deutlich vom erwarteten Wert (20°C) abweicht.
  • Lesbarkeit: Die Rechenoperationen sind in JS nur über Methoden möglich, nicht mit +, -, *, /, … So entsteht schlecht lesbarer Code.
const { rules, triggers, items, actions, time, Quantity } = require('openhab');

// =========================================================================
// ZENTRALE TELEGRAM-HILFSFUNKTION (liest dynamisch aus siTelegramConfig)
// =========================================================================
const sendTelegramAlert = (message) => {
    const tgConfig = items.getItem('siTelegramConfig');
    const botUid = tgConfig?.getMetadata('bot')?.value;
    const chatIdVal = tgConfig?.getMetadata('chatId')?.value;

    if (!botUid || !chatIdVal) {
        console.warn("[Telegram] Metadaten (bot oder chatId) an 'siTelegramConfig' nicht gefunden!");
        return;
    }

    const telegram = actions.get("telegram", botUid);
    if (!telegram) {
        console.warn(`[Telegram] Action konnte nicht geladen werden für Bot UID: ${botUid}`);
        return;
    }

    try {
        const chatIdLong = Java.type('java.lang.Long').valueOf(chatIdVal);
        telegram.sendTelegram(chatIdLong, message);
    } catch (err) {
        console.error(`[Telegram] Fehler beim Senden: ${err.message}`, err);
    }
};

// Wiederverwendbare Quantity-Konstanten (spart wiederholte Instanziierung)
const Q_0_C  = Quantity('0 °C');
const Q_2_C  = Quantity('2 °C');
const Q_3_C  = Quantity('3 °C');
const Q_30_C = Quantity('30 °C');

// Grenzwerte (Auswahl der wichtigsten, um das Skript übersichtlich zu halten)
const LIMITS = {
	//1 Vol-% = 10.000 ppm | ml/m³

	//Quellen
	//https://www.air-q.com/grenzwerte
	//https://www.abc-gefahren.de/dateien/ausbildung/elh/3_gsg_grenzwerte_gase.pdf


	//--Lux--
	Lux_Ref_moonless_overcast_night_sky:			Quantity('0.0001 lx'),
	Lux_Ref_moonless_clear_night_sky_with_airglow:	Quantity('0.002 lx'),
	Lux_Ref_full_moon_on_a_clear_night:				Quantity('0.7 lx'),			//0.27 - 1.0
	Lux_Ref_civil_twilight_under_clear_sky:			Quantity('3.4 lx'),
	Lux_Ref_corridor:								Quantity('100 lx'),
	Lux_Ref_bedroom:								Quantity('150 lx'),
	Lux_Ref_living_room: 							Quantity('200 lx'),
	Lux_Ref_kitchen_bathroom: 						Quantity('300 lx'),
	Lux_Ref_sunrise_sunset: 						Quantity('400 lx'),								
	Lux_Ref_office: 								Quantity('500 lx'),			//320 - 500
	Lux_Ref_overcast_day: 							Quantity('1000 lx'),						
	Lux_Ref_fully_daylight_without_direct_sun: 		Quantity('17000 lx'),		//10.000 -  25.000
	Lux_Ref_direct_sunlight: 						Quantity('50000 lx'),		//32.000 - 100.000

	//-- AQI - Air Quality Index --
	AQI_Ref_Good :  				50.0,
	AQI_Ref_Moderate :  			100.0,
	AQI_Ref_UnhealthyForSensitive :	150,
	AQI_Ref_Unhealthy :  			200,
	AQI_Ref_VeryUnhealthy :  		300,
	//AQI_REF_Hazardous :  			300+

	//-- IAQ
	//https://svach.lbl.gov/iaq-index-scores-indoor-air-quality/
	IAQ_Ref_Better :  		40.0,
	IAQ_Ref_TypicalHome :  	100.0,
	IAQ_Ref_Worse :  		140.0,


	//--Radiation--
	//https://www.bfs.de/DE/themen/ion/strahlenschutz/grenzwerte/grenzwerte.html//:~:text: Der%20Grenzwert%20f%C3%BCr%20die%20effektive,sind%20von%20diesen%20Begrenzungen%20ausgeschlossen.
	//http://cholla.mmto.org/electronics/geiger/banggood/M4011.pdf

	// Business : in milliSievert
	Rad_GW_business_year: 							Quantity('20 mSv'),
	Rad_GW_emergency_case: 							Quantity('250 mSv'),
	Rad_GW_business_lifetime: 						Quantity('400 mSv'),
	Rad_GW_childbearing_women_per_month: 			Quantity('2 mSv'),

	// Privates : in milliSievert
	Rad_GW_underaged_and_unborn_people_per_year: 	Quantity('1 mSv'),
	Rad_GW_organdosis_per_year:						Quantity('15 mSv'),

	// Grenzwerte für kurze Zeiträume : in milliSievert
	Rad_GW_unborn_permanently_damaged: 				Quantity('0.1 mSv'),		//Unterer Schätzwert des Schwellenwerts für Schädigungen des Ungeborenen
	Rad_GW_reversable_illness: 						Quantity('1 mSv'),		//Bei akuter Exposition treten ab diesem Schwellenwert akute Strahleneffekte auf (zum Beispiel Kopfschmerzen, Übelkeit, Erbrechen)
	Rad_GW_potentially_deadly: 						Quantity('3 mSv'),		//Ohne medizinische Eingreifen sterben bei dieser Dosis 50 Prozent der exponierten Personen nach 3-6 Wochen, wenn es sich um eine in kurzer Zeit erfahrene Strahlenbelastung handelte (LD50)
	Rad_GW_deadly: 									Quantity('8 mSv'),		//Ohne entsprechende medizinische Behandlung bestehen nur geringe Überlebenschancen, wenn es sich um eine in kurzer Zeit erfahrene Strahlenbelastung handelte
	Rad_M4011_min_radiation_per_hour: 				Quantity('20 µSv'),		// / h
	Rad_M4011_max_radiation_per_hour: 				Quantity('120 µSv'),		// / h

	//-- Humidity : Luftfeuchte in Prozent --
	Humidity_GW_lo: Quantity('40.0 %'),
	Humidity_GW_hi: Quantity('61.0 %'),

	//-- Atemluft --
	//		Gefahrbeschreibung :  ""
	//		explosiv :  false
	//		untereExplosionsgrenze :  null //Vol %
	//		HautresorptiverGefahrstoff :  false	//Schutzkleidung tragen!
	//		GewoehnungDesGeruchssinns :  null

	//	Acute Exposure Guideline Levels
	//		AEGL1 :  null	//ppm, Schwelle zum Unwohlsein
	//		AEGL2 :  null	//ppm, Schwelle zur Einschränkung
	//		AEGL3 :  null	//ppm, Schwelle zur tödl. Wirkung

	//		ETW1 :  null		//ppm, Einsatztoleranzwert nach 1h Expositionszeit
	//		ETW4 :  null		//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	//		MAK :  null		//ppm, max. Arbeitsplatzkonzentration

	Air_relGasdichte :  1,
	Air_mol :  			29.949, //g/mol

	//		GW :  null	//eigens verwendeter Grenzwert


	//-- H2 : Wasserstoff --
	H2_Gefahrbeschreibung :  	"erstickend; explosiv",
	H2_untereExplosionsgrenze : Quantity('40000 ppm'),
	H2_relGasdichte :  			0.06,
	H2_mol :  					2.02, //g/mol

	H2_GW :  					Quantity('40000 ppm'),		//eigens verwendeter Grenzwert: Explosionsgefahr


	//-- LPG : leicht verflüssigbare Kohlenwasserstoff-Verbindungen (CmHn) --
	//	Propan C3H8, Propen (Propylen) C3H6, Butan C4H10, Buten (Butylen) C4H8, Isobutan (Methylpropan) C4H10, Isobuten (Methylpropen) C4H8
	LPG_Gefahrbeschreibung :  "erstickend / Wirkung auf Blut, Nerven und Zellen; explosiv",
	LPG_untereExplosionsgrenze :  	Quantity('14000 ppm'),

	LPG_MAK :  						Quantity('1000 ppm'),		//ppm, max. Arbeitsplatzkonzentration

	LPG_relGasdichte :  			2.1,
	LPG_mol : 						42.08, //g/mol -> Propen

	LPG_GW :  						Quantity('1000 ppm'),		//ppm, eigens verwendeter Grenzwert: MAK


	//-- CH4 : Methan --
	CH4_Gefahrbeschreibung :  		"erstickend; explosiv",
	CH4_untereExplosionsgrenze :  	Quantity('44000 ppm'),
	CH4_relGasdichte :  			0.055,
	CH4_mol :  						16.04, //g/mol

	CH4_GW :  						Quantity('44000 ppm'),		//ppm, eigens verwendeter Grenzwert: Explosionsgefahr


	//-- CO : Kohlenstoffmonoxid --
	CO_Gefahrbeschreibung :  		"Wirkung auf Blut, Nerven und Zellen; explosiv",
	CO_untereExplosionsgrenze :  	Quantity('130000 ppm'),

	//	Acute Exposure Guideline Levels
	CO_AEGL2 :  					Quantity('33 ppm'),			//ppm, Schwelle zur Einschränkung

	CO_ETW1 :  						Quantity('83 ppm'),			//ppm, Einsatztoleranzwert nach 1h Expositionszeit
	CO_ETW4 :  						Quantity('33 ppm'),			//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	CO_MAK :  						Quantity('30 ppm'),			//ppm, max. Arbeitsplatzkonzentration

	CO_relGasdichte :  				0.96,
	CO_mol :  						17.03, //g/mol

	CO_GW :  						Quantity('8 ppm'),			//ppm, eigens verwendeter Grenzwert: Umweltbundesamt 8h-Mittelwert
	CO_GW_mgm3 :  					Quantity('4 mg/m³'),


	//-- Smoke : Rauch --


	//-- C3H8 : Propan --
	C3H8_Gefahrbeschreibung :  		"erstickend; explosiv",
	C3H8_untereExplosionsgrenze :  	Quantity('17000 ppm'),

	C3H8_MAK :  					Quantity('1000 ppm'),		//ppm, max. Arbeitsplatzkonzentration

	C3H8_GW :  						Quantity('1000 ppm'),		//ppm, eigens verwendeter Grenzwert: MAK


	//-- C6H6 : Benzol --
	C6H6_Gefahrbeschreibung :  			"Wirkung auf Blut, Nerven und Zellen; explosiv",
	C6H6_untereExplosionsgrenze :  		Quantity('12000 ppm'),
	C6H6_HautresorptiverGefahrstoff :  	true,	//Schutzkleidung tragen!

	//	Acute Exposure Guideline Levels
	C6H6_AEGL1 :    					Quantity('18 ppm'),		//ppm, Schwelle zum Unwohlsein
	C6H6_AEGL2 :    					Quantity('400 ppm'),	//ppm, Schwelle zur Einschränkung

	C6H6_ETW4 :    						Quantity('20 ppm'),		//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	C6H6_MAK :    						Quantity('1 ppm'),		//ppm, max. Arbeitsplatzkonzentration

	C6H6_relGasdichte :  				2.7,
	C6H6_mol :  						78.114, //g/mol

	C6H6_GW :  				  			Quantity('1 ppm'),		//ppm, eigens verwendeter Grenzwert


	//-- C6H14 : Hexan --
	C6H14_Gefahrbeschreibung :  		"Wirkung auf Blut, Nerven und Zellen; explosiv",
	C6H14_untereExplosionsgrenze :  	Quantity('11000 ppm'),

	//	Acute Exposure Guideline Levels
	C6H14_AEGL2 :  						Quantity('2900 ppm'),	//ppm, Schwelle zur Einschränkung

	C6H14_ETW1 :   						Quantity('2900 ppm'),	//ppm, Einsatztoleranzwert nach 1h Expositionszeit
	C6H14_ETW4 :   						Quantity('2900 ppm'),	//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	C6H14_MAK :    						Quantity('50 ppm'),		//ppm, max. Arbeitsplatzkonzentration

	C6H14_relGasdichte :  				3.0,
	C6H14_mol :  						86.17, //g/mol

	C6H14_GW :  						Quantity('50 ppm'),		//ppm, eigens verwendeter Grenzwert


	//-- NOx : Stickstoffoxide --
	NOx_Gefahrbeschreibung :	"reizend",

	//	Acute Exposure Guideline Levels
	NOx_AEGL1 :  		Quantity('0.5 ppm'),		//ppm, Schwelle zum Unwohlsein
	NOx_AEGL2 :  		Quantity('8.2 ppm'),		//ppm, Schwelle zur Einschränkung

	NOx_ETW1 :  		Quantity('12 ppm'),			//ppm, Einsatztoleranzwert nach 1h Expositionszeit
	NOx_ETW4 :  		Quantity('8.2 ppm'),		//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	NOx_MAK :  			Quantity('5 ppm'),			//ppm, max. Arbeitsplatzkonzentration

	NOx_relGasdichte :  1.6,
	NOx_mol :  			46.01, //g/mol

	NOx_GW :  			Quantity('0.5 ppm'),		//ppm, eigens verwendeter Grenzwert: AGS Grenzwert, längerfristige Exposition


	//-- CL2 : Chlor --
	CL2_Gefahrbeschreibung :  	"reizend; explosiv",

	//	Acute Exposure Guideline Levels
	CL2_AEGL1 :  		Quantity('0.5 ppm'),	//ppm, Schwelle zum Unwohlsein
	CL2_AEGL2 :  		Quantity('1 ppm'),		//ppm, Schwelle zur Einschränkung

	CL2_ETW1 :  		Quantity('1 ppm'),		//ppm, Einsatztoleranzwert nach 1h Expositionszeit
	CL2_ETW4 :  		Quantity('2 ppm'),		//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	CL2_MAK :  			Quantity('0.5 ppm'),	//ppm, max. Arbeitsplatzkonzentration

	CL2_relGasdichte :  2.5,
	CL2_mol :  			70.9, //g/mol

	CL2_GW :  			Quantity('0.5 ppm'),	//ppm, eigens verwendeter Grenzwert


	//-- O3 : Ozon --
	//	Acute Exposure Guideline Levels
	O3_MAK :  			Quantity('0.1 ppm'),		//ppm, max. Arbeitsplatzkonzentration

	O3_relGasdichte :  	1.78,
	O3_mol :  			48.00, //g/mol

	O3_GW :  			Quantity('0.06 ppm'),		//ppm, eigens verwendeter Grenzwert: Umweltbundesamt
	//https://www.engineeringtoolbox.com/air-contaminants-limits-d_1538.html
	O3_GW_mgm3: 		Quantity('180 mg/m³'),


//-- CO2 : Kohlenstoffdioxid --
	CO2_Gefahrbeschreibung :  "erstickend, Wirkung auf Blut, Nerven und Zellen",

	CO2_ETW4 :  		Quantity('10000 ppm'),	//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	CO2_MAK :  			Quantity('5000 ppm'),	//ppm, max. Arbeitsplatzkonzentration

	CO2_relGasdichte :  1.5,
	CO2_mol :  			44.01, //g/mol
		
	CO2_GW: 			Quantity('1000 ppm'),	//ppm, eigens verwendeter Grenzwert: Lüften-Schwellwert
	CO2_GW_MQ135: 		Quantity('600 ppm'),	//ppm, Mess-Ende des MQ135

//-- C2H5OH : Ethanol (evtl. auch andere Alkohole) --
	C2H5OH_Gefahrbeschreibung :  "Wirkung auf Blut, Nerven und Zellen; explosiv",
	C2H5OH_untereExplosionsgrenze :  	Quantity('31000 ppm'),

	C2H5OH_ETW4 :  						Quantity('3000 ppm'),	//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	C2H5OH_MAK :  						Quantity('500 ppm'),	//ppm, max. Arbeitsplatzkonzentration

	C2H5OH_relGasdichte :  				1.6,
	C2H5OH_mol :  						46.07, //g/mol

	C2H5OH_GW :  						Quantity('500 ppm'),	//ppm, eigens verwendeter Grenzwert: MAK


//-- NH4 : Ammonium --
	NH4_mol :  18, //g/mol


//-- CH3 : Toluol --
	CH3_Gefahrbeschreibung :  "Wirkung auf Blut, Nerven und Zellen; explosiv",
	CH3_untereExplosionsgrenze :	Quantity('11000 ppm'),

	//	Acute Exposure Guideline Levels
	CH3_AEGL1 :  					Quantity('67 ppm'),		//ppm, Schwelle zum Unwohlsein
	CH3_AEGL2 :  					Quantity('310 ppm'),	//ppm, Schwelle zur Einschränkung

	CH3_ETW1 :  					Quantity('560 ppm'),	//ppm, Einsatztoleranzwert nach 1h Expositionszeit
	CH3_ETW4 :  					Quantity('310 ppm'),	//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	CH3_MAK :  						Quantity('50 ppm'),		//ppm, max. Arbeitsplatzkonzentration

	CH3_relGasdichte :  			3.2,
	CH3_mol :  						92.14, //g/mol

	CH3_GW :  						Quantity('50 ppm'),		//ppm, eigens verwendeter Grenzwert: MAK


//-- CH3_2CO : Aceton --
	CH3_2CO_Gefahrbeschreibung :  	"Wirkung auf Blut, Nerven und Zellen; explosiv",
	CH3_2CO_untereExplosionsgrenze:	Quantity('25000 ppm'),
	//		HautresorptiverGefahrstoff :  null	//Schutzkleidung tragen!
	//		GewoehnungDesGeruchssinns :  null

	//	//Acute Exposure Guideline Levels
	CH3_2CO_AEGL1 :  				Quantity('200 ppm'),	//ppm, Schwelle zum Unwohlsein
	CH3_2CO_AEGL2 :  				Quantity('1400 ppm'),	//ppm, Schwelle zur Einschränkung

	CH3_2CO_ETW4 :   				Quantity('500 ppm'),	//ppm, Einsatztoleranzwert nach 4h Expositionszeit
	CH3_2CO_MAK :   				Quantity('500 ppm'),	//ppm, max. Arbeitsplatzkonzentration

	CH3_2CO_relGasdichte :  		2.0,
	CH3_2CO_mol :   				58.08, //g/mol

	CH3_2CO_GW :   					Quantity('200 ppm'),	//ppm, eigens verwendeter Grenzwert: AEGL1


	//	// --- Athmosphärische Warnungen ---
	//	// entsprechend https://www.hlnug.de/fileadmin/dokumente/luft/monatsberichte/2018/LHMB_2018-03.pdf
	//	
	//-- PM10 : Feinstaub PM10 --
	//https://www.umweltbundesamt.de/themen/luft/luftschadstoffe-im-ueberblick/feinstaub

	PM10_GW_mgm3:  					Quantity('50 mg/m³'),						//max 35x im Jahr Jahresmittel
	PM10_GW_mgm3_averagePerYear: 	Quantity('40 mg/m³'),		//im Jahresmittel

	//-- PM2.5 : Feinstaub PM2.5 --
	//https://www.umweltbundesamt.de/themen/luft/luftschadstoffe-im-ueberblick/feinstaub
	PM25_GW_mgm3_averagePerYear: 	Quantity('25 mg/m³'),		//im Jahresmittel

	//-- NO2 : Stickstoffdioxid --
	//https://www.engineeringtoolbox.com/air-contaminants-limits-d_1538.html
	NO2_GW_mgm3:  					Quantity('40 mg/m³'),

	//-- SO2 : Schwefeldioxid --
	//https://www.engineeringtoolbox.com/air-contaminants-limits-d_1538.html
	SO2_GW_mgm3:  					Quantity('20 mg/m³'),

	//-- AQI : Allgemeiner AQI-Wert
	AQI_GW: 100
};

// --- GLOBAL STORAGE (Persistenz innerhalb des Skript-Lifecycles) ---
// In JS-Files bleiben diese Variablen erhalten, solange die Datei nicht neu geladen wird.
const lastSent = new Map();
const windowTimers = new Map();
// Global cache for persistence queries
const persistenceCache = new Map();

// --- HELPER FUNKTIONEN ---

/**
 * Fetches persistence values. Returns cached value if valid, otherwise queries InfluxDB.
 * 
 * @param {Object} item - The openHAB item object
 * @param {string} queryType - 'average', 'maximum', or 'minimum'
 * @param {Object} since - time.ZonedDateTime object indicating the timeframe
 * @param {string} timeLabel - A static string representing the timeframe for the cache key (e.g. "1h", "24h")
 * @param {number} cacheMinutes - Duration in minutes the cache is considered valid (default 30)
 * @returns {any} The numericState or quantityState from the database, or null on error
 */
const getCachedPersistence = (item, queryType, hoursAgo, timeLabel, cacheMinutes = 30) => {
    if (!item) return null;

    const cacheKey = `${item.name}_${queryType}_${timeLabel}`;
    const nowMs = Date.now();

    // 1. Schneller Cache-Hit Check (rein in JavaScript, erzeugt 0 Java-Objekte)
    if (persistenceCache.has(cacheKey)) {
        const entry = persistenceCache.get(cacheKey);
        if (entry.expiresAt > nowMs) {
            return entry.value;
        }
    }

    // 2. Cache-Miss: DB abfragen & Zeitobjekt erst HIER erzeugen
    let dbValue = null;
    try {
        const since = time.ZonedDateTime.now().minusHours(hoursAgo);
        const result = item.persistence[`${queryType}Since`](since);
        if (result !== null) {
            dbValue = result.quantityState !== null ? result.quantityState : result.numericState;
        }
    } catch (e) {
        console.error(`Error querying persistence for ${item.name}: ${e}`);
        return null;
    }

    if (dbValue !== null) {
        persistenceCache.set(cacheKey, {
            value: dbValue,
            expiresAt: nowMs + (cacheMinutes * 60 * 1000)
        });
    }

    return dbValue;
};

/**
 * Sendet eine Nachricht nur einmal pro Zeitraum
 */
const sendOncePerPeriod = (triggeringItem, message, minutes) => {
    const now = time.ZonedDateTime.now();
    const lastTime = lastSent.get(triggeringItem.name);

    if (!lastTime || lastTime.plusMinutes(minutes).isBefore(now)) {
        sendTelegramAlert(message);
        lastSent.set(triggeringItem.name, now);
    }
};

/**
 * Berechnet den Taupunkt
 */
const getDewpoint = (temp, hum) => {
    let a = (temp >= 0) ? 7.5 : 7.6;
    let b = (temp >= 0) ? 237.3 : 240.7;
    const sdd = 6.1078 * Math.pow(10, (a * temp) / (b + temp));
    const dd = (hum) * sdd;
    const v = Math.log10(dd / 6.107);
    return (b * v) / (a - v);
};

/**
 * Berechnet die absolute Feuchte in g/m³ aus Temperatur (°C) und relativer Feuchte (%)
 */
const getAbsoluteHumidity = (tempC, humPercent) => {
    const sdd = 6.112 * Math.exp((17.67 * tempC) / (tempC + 243.5));
    const dd = (humPercent / 100.0) * sdd;
    return (216.7 * dd) / (273.15 + tempC);
};

/**
 * Berechnet die absolute Feuchte direkt aus Taupunkt (°C) und Temperatur (°C)
 */
const getAbsoluteHumidityFromDewpoint = (tempC, dewC) => {
    const e = 6.112 * Math.exp((17.67 * dewC) / (dewC + 243.5));
    return (216.7 * e) / (273.15 + tempC);
};

/**
 * Bewertet den Nutzen des Lüftens gegenüber Energieverlust und Komfort.
 * Rückgabe:
 *   > 1.5  -> Fenster ÖFFNEN empfohlen
 *   < 0.0  -> Fenster SCHLIESSEN empfohlen
 *   0..1.5 -> Hysterese-Bereich (keine Änderung)
 */
const calculateVentilationScore = (tempRoom, humRoom, tempEnv, absHumEnv, setpointTemp = 21.0) => {
    const absHumRoom = getAbsoluteHumidity(tempRoom, humRoom);

    // 1. Physikalischer Ausschluss: Außenluft transportiert mehr Wasser als Innenluft
    const moistureRemoval = absHumRoom - absHumEnv; // g/m³
    if (moistureRemoval <= 0) return -100.0;

    // 2. Dringlichkeit (Basis 50 % r.F.): quadratischer Anstieg
    // 50% -> 0 | 55% -> 0.25 | 60% -> 1.0 | 65% -> 2.25 | 70% -> 4.0
    const excessHum = Math.max(0, humRoom - 50.0);
    const urgency = Math.pow(excessHum / 10.0, 2);

    // 3. Nutzen = transportierte Wassermenge * Dringlichkeit
    const benefit = moistureRemoval * urgency;

    // 4. Kosten A: Lüftungswärmeverlust im Winter (linear zum Temperaturunterschied)
    const heatLossDelta = Math.max(0, tempRoom - tempEnv);
    const heatLossCost = heatLossDelta * 0.15; // 20K Differenz = 3.0 Kosten

    // 5. Kosten B: Sommer-Hitzeschutz (straft das Hereinholen von Hitze über 23°C ab)
    const heatGainDelta = Math.max(0, tempEnv - Math.max(23.0, tempRoom));
    const heatGainCost = heatGainDelta * 1.0;

    // 6. Kosten C: Unterkühlungsstrafe (quadratisch, falls Raum bereits unter Sollwert)
    const undercooling = Math.max(0, setpointTemp - tempRoom);
    const comfortPenalty = Math.pow(undercooling, 2) * 2.5; // 1K = 2.5 | 2K = 10.0 | 3K = 22.5

    const totalCost = heatLossCost + heatGainCost + comfortPenalty;
    return benefit - totalCost;
};

// --- RULES ---

/**
 * 1. Arduinos neu starten, wenn sie sich 1h nicht gemeldet haben
 */
rules.JSRule({
    name: "Try restart Arduinos",
    triggers: [triggers.GenericCronTrigger("0 0 * * * ?")],
    execute: () => {
        items.getItem('fgStopDetection').members.forEach(item => {
            // Prüfen ob Item sich innerhalb der letzten Stunde geändert hat (Persistence benötigt!)
            if (!item.persistence.changedSince(time.ZonedDateTime.now().minusHours(1))) {
                const igGroup = item.groupNames.find(g => g.startsWith("ig"));
                if (igGroup) {
                    const restartItem = items.getItem(igGroup).members.find(m => m.tags.includes("Switch") && m.tags.includes("Restart"));
                    if (restartItem) restartItem.sendCommand("ON");
                }
            }
        });
    }
});

/**
 * 2. Tägliche Batterie- und Status-Alarme
 */
rules.JSRule({
    name: "Send alarms once a day",
    triggers: [triggers.GenericCronTrigger("0 0 17 * * ?")],
    execute: () => {
        // Batterie Prüfung
        items.getItemsByTag("Battery").forEach(item => {
            if (item.state !== 'NULL' && item.numericState <= 20) {
                const igName = item.groupNames.find(g => g.startsWith("ig"));
                const sgName = igName ? items.getItem(igName).groupNames.find(g => g.startsWith("sg")) : null;
                const igLabel = igName ? items.getItem(igName).label : item.name;
                const sgLabel = sgName ? items.getItem(sgName).label : "Unbekannt";
                sendTelegramAlert(`Batterie fast leer: ${igLabel} in ${sgLabel} (${item.state})`);
            }
        });

        // Inaktivitäts-Prüfung (24h)
        const oneDayAgo = time.ZonedDateTime.now().minusHours(24);
        items.getItem('fgStopDetection').members.forEach(item => {
            if (!item.persistence.changedSince(oneDayAgo)) {
                const igName = item.groupNames.find(g => g.startsWith("ig"));
                const sgName = igName ? items.getItem(igName).groupNames.find(g => g.startsWith("sg")) : "";
                sendTelegramAlert(`Info: ${item.name} hat sich seit 24h nicht verändert ${sgName}.`);
            }
        });
    }
});

/**
 * 3. Sensor Grenzwert-Überwachung (Die Haupt-Logik)
 */
rules.JSRule({
    name: "Sensor warn on limit",
    triggers: [triggers.GroupStateChangeTrigger('tgWarnOnLimit')],
    execute: (event) => {
        const triggeringItem = items.getItem(event.itemName);
        const tags = triggeringItem.tags;
        if (!tags.includes("Measurement")) return;

        // =========================================================================
        // STUFE 1: SCHNELLE CHECKS (Kein Raum, kein DB-Zugriff, sofortiger Exit)
        // =========================================================================

        // --- Batterie (Früher Abbruch, wenn Batterie voll ist) ---
        if (tags.includes("Battery")) {
            if (triggeringItem.numericState <= 20) {
                sendOncePerPeriod(triggeringItem, `Batterie fast leer: ${triggeringItem.label} (${triggeringItem.state})`, 480);
            }
            return;
        }

        // --- AQI ---
        if (tags.includes("AQI")) {
            if (triggeringItem.numericState > 100) {
                sendOncePerPeriod(triggeringItem, `Athmosphärische Warnung: ${triggeringItem.name} kritisch! (${triggeringItem.numericState} AQI)`, 180);
            }
            return;
        }

        // --- Radioaktivität ---
        if (tags.includes("Radiation")) {
            const radVal = triggeringItem.quantityState;
            if (tags.includes("mmSvph") && radVal.greaterThan(LIMITS.Rad_GW_childbearing_women_per_month)) {
                sendOncePerPeriod(triggeringItem, `Radioaktivität kurzfristig kritisch (${triggeringItem.label}: ${radVal})`, 180);
            } else if (tags.includes("mSvpm") && radVal.greaterThan(LIMITS.Rad_GW_childbearing_women_per_month)) {
                sendOncePerPeriod(triggeringItem, `Radioaktivität mittelfristig kritisch (${triggeringItem.label}: ${radVal})`, 180);
            } else if (tags.includes("mSvpa") && radVal.greaterThan(LIMITS.Rad_GW_organdosis_per_year)) {
                sendOncePerPeriod(triggeringItem, `Radioaktivität langfristig kritisch (${triggeringItem.label}: ${radVal})`, 180);
            }
            return;
        }

        // --- Atmosphärische Gase & Schadstoffe ---
        if (tags.includes("O3") && triggeringItem.quantityState.greaterThan(LIMITS.O3_GW_mgm3)) {
            sendOncePerPeriod(triggeringItem, `Athmosphärische Warnung: O3 kritisch! (${triggeringItem.quantityState} > ${LIMITS.O3_GW_mgm3})`, 180);
            return;
        }
        if (tags.includes("CO") && triggeringItem.quantityState.greaterThan(LIMITS.CO_GW_mgm3)) {
            sendOncePerPeriod(triggeringItem, `Athmosphärische Warnung: CO kritisch! (${triggeringItem.quantityState} > ${LIMITS.CO_GW_mgm3})`, 180);
            return;
        }
        if (tags.includes("PM10") && triggeringItem.quantityState.greaterThan(LIMITS.PM10_GW_mgm3)) {
            sendOncePerPeriod(triggeringItem, `Athmosphärische Warnung: PM10 kritisch! (${triggeringItem.quantityState} > ${LIMITS.PM10_GW_mgm3})`, 180);
            return;
        }
        if (tags.includes("PM25") && triggeringItem.quantityState.greaterThan(LIMITS.PM25_GW_mgm3_averagePerYear)) {
            sendOncePerPeriod(triggeringItem, `Athmosphärische Warnung: PM2.5 kritisch! (${triggeringItem.quantityState} > ${LIMITS.PM25_GW_mgm3_averagePerYear})`, 180);
            return;
        }
        if (tags.includes("Air") && tags.includes("IAQ") && triggeringItem.quantityState.greaterThan(LIMITS.IAQ_Ref_Worse)) {
            sendOncePerPeriod(triggeringItem, `Athmosphärische Warnung: Schlechte Luftqualität! (${triggeringItem.quantityState} > ${LIMITS.IAQ_Ref_Worse})`, 180);
            return;
        }
        if (tags.includes("NO2") && triggeringItem.quantityState.greaterThan(LIMITS.NO2_GW_mgm3)) {
            sendOncePerPeriod(triggeringItem, `Athmosphärische Warnung: NO2 kritisch! (${triggeringItem.quantityState} > ${LIMITS.NO2_GW_mgm3})`, 180);
            return;
        }
        if (tags.includes("SO2") && triggeringItem.quantityState.greaterThan(LIMITS.SO2_GW_mgm3)) {
            sendOncePerPeriod(triggeringItem, `Athmosphärische Warnung: SO2 kritisch! (${triggeringItem.quantityState} > ${LIMITS.SO2_GW_mgm3})`, 180);
            return;
        }

        // --- CO2 Schnell-Check (Bricht ab, wenn CO2 unkritisch ist) ---
        if (tags.includes("CO2") && !triggeringItem.quantityState.greaterThan(LIMITS.CO2_GW_MQ135)) {
            return;
        }

        // =========================================================================
        // STUFE 2: RAUM-ZUORDNUNG (Erreicht nur noch Heizung, Feuchte & erhöhtes CO2)
        // =========================================================================

        const igName = triggeringItem.groupNames.find(g => g.startsWith("ig"));
        if (!igName) return;
        const igItem = items.getItem(igName);
        const roomList = igItem.groupNames.filter(g => g.startsWith("sg"));
        if (roomList.length === 0) return;

        const currentHour = time.ZonedDateTime.now().hour();

        roomList.forEach(sgName => {
            const roomItem = items.getItem(sgName);

            // --- A. CO2 LÜFTUNGSWARNUNG ---
            if (tags.includes("CO2")) {
                const windowItems = roomItem.members.filter(m => m.tags.includes("Window"));
                const openWindows = windowItems.filter(m => m.numericState > 0).length;

                if (windowItems.length === 0 || openWindows === 0) {
                    sendOncePerPeriod(triggeringItem, `CO2 hoch in ${roomItem.label}, dringend lüften!`, 180);
                }
                return;
            }

            // --- B. HEIZKÖRPER-LOGIK ---
            if (tags.includes("TempRadiator")) {
                const radiatorTempMin = getCachedPersistence(triggeringItem, 'minimum', 1, '1h', 10);
                const radiatorTempMax = getCachedPersistence(triggeringItem, 'maximum', 1, '1h', 10);
                if (!radiatorTempMin || !radiatorTempMax) return;

                const setpointItem = roomItem.members.find(m => m.tags.includes("Setpoint") && m.tags.includes("Temperature"));
                const setpointValueMax = setpointItem ? getCachedPersistence(setpointItem, 'maximum', 1, '1h', 10) : null;

                const envTempItem = items.getItem("siTemperatureEnvironment");
                const envTempMax = (envTempItem && envTempItem.state !== 'NULL') 
                    ? getCachedPersistence(envTempItem, 'maximum', 24, '24h') 
                    : null;

                const roomTempItem = roomItem.members.find(m => m.tags.includes("Measurement") && m.tags.includes("Temperature"));
                const roomTempMin = roomTempItem ? getCachedPersistence(roomTempItem, 'minimum', 1, '1h', 10) : null;

                // Heizkörper heizt unnötig
                if (envTempMax && roomTempMin && setpointValueMax) {
                    if (
                        radiatorTempMin.greaterThan(Q_30_C) &&
                        radiatorTempMin.greaterThan(envTempMax) &&
                        setpointValueMax.lessThan(roomTempMin.add(Q_2_C)) &&
                        (currentHour >= 7 && currentHour < 23)
                    ) {
                        sendOncePerPeriod(triggeringItem, `Heizkörper zu warm in: ${roomItem.label}`, 180);
                    }
                }

                // Heizkörper zu kalt (Schwelle auf Sollwert oder Raumtemperatur bezogen)
                if (setpointValueMax && radiatorTempMax.lessThan(setpointValueMax) && (currentHour >= 6 && currentHour <= 23)) {
                    sendOncePerPeriod(triggeringItem, `Heizkörper zu kalt in: ${roomItem.label}`, 180);
                }
                return;
            }

            // --- C. RAUMKLIMA & LÜFTUNGSEMPFEHLUNG (Humidity) ---
            if (tags.includes("Humidity")) {
                const envTempItem = items.getItem("siTemperatureEnvironment");
                const envDewItem  = items.getItem("siDewpointEnvironment");

                if (!envTempItem || envTempItem.state === 'NULL' || !envDewItem || envDewItem.state === 'NULL') {
                    return;
                }

                const temperatureRoomItem = roomItem.members.find(m => m.tags.includes("Measurement") && m.tags.includes("Temperature"));
                if (!temperatureRoomItem || temperatureRoomItem.state === 'NULL') return;

                // Temperaturen & Feuchten einlesen
                const tempRoom = temperatureRoomItem.quantityState.toUnit("°C").float;
                const humRoom  = triggeringItem.quantityState ? triggeringItem.quantityState.toUnit("%").float : triggeringItem.numericState;
                const tempEnv  = envTempItem.quantityState.toUnit("°C").float;
                const dewEnv   = envDewItem.quantityState.toUnit("°C").float;

                // Absolute Außenfeuchte direkt aus Außentemperatur und Außentaupunkt bestimmen
                const absHumEnv = getAbsoluteHumidityFromDewpoint(tempEnv, dewEnv);

                // Sollwert des Raums ermitteln (Default 21°C falls kein Thermostat vorhanden)
                const setpointItem = roomItem.members.find(m => m.tags.includes("Setpoint") && m.tags.includes("Temperature"));
                const setpointTemp = (setpointItem && setpointItem.state !== 'NULL') 
                    ? setpointItem.quantityState.toUnit("°C").float 
                    : 21.0;

                const score = calculateVentilationScore(tempRoom, humRoom, tempEnv, absHumEnv, setpointTemp);

                const windowItems = roomItem.members.filter(m => m.tags.includes("Window"));
                const openWindows = windowItems.filter(m => m.numericState > 0).length;

                // Nur tagsüber / ab 6 Uhr morgens Empfehlungen versenden
                if (currentHour >= 6) {
                    // Öffnen-Schwelle: Score > 1.5
                    if (score > 2.5 && openWindows === 0) {
                        sendOncePerPeriod(
                            triggeringItem,
                            `Guter Zeitpunkt zum Lüften in ${roomItem.label}: T=${tempRoom.toFixed(1)}°C, Feuchte=${humRoom.toFixed(0)}% (Draußen: ${tempEnv.toFixed(1)}°C, Score=${score.toFixed(1)})`,
                            180
                        );
                    // Schließen-Schwelle: Score < 0.0
                    } else if (score < -0.5 && openWindows > 0) {
                        sendOncePerPeriod(
                            triggeringItem,
                            `Fenster schließen in ${roomItem.label}: Lüftungsnutzen erschöpft (T=${tempRoom.toFixed(1)}°C, Feuchte=${humRoom.toFixed(0)}%, Score=${score.toFixed(1)})`,
                            60
                        );
                    }
                }

                // Langzeit-Feuchtigkeitswarnung (>3 Tage)
                const isLivingArea = !roomItem.tags.some(t => ["Garden", "Terrace", "Outdoor", "Garage"].includes(t));
                if (isLivingArea) {
                    const longTermHum = getCachedPersistence(triggeringItem, 'average', 72, '72h', 60);
                    if (longTermHum && longTermHum.float > 0.62) {
                        sendOncePerPeriod(triggeringItem, `Luftfeuchte seit >3 Tagen über 62 % in ${roomItem.label}, dringend lüften!`, 720);
                    }
                }
            }
        });
    }
});

/**
 * 4. Fenster/Tür zu lange offen
 */
rules.JSRule({
    name: "Fenster/Tür zu lange offen",
    triggers: [triggers.GroupStateUpdateTrigger('tgWindow', 'OPEN')],
    execute: (event) => {
        const itemName = event.itemName;
        
        // Falls Timer bereits läuft, nichts tun (oder resetten, falls gewünscht)
        if (windowTimers.has(itemName)) return;

        const timer = actions.ScriptExecution.createTimer(time.ZonedDateTime.now().plusMinutes(30), () => {
            const item = items.getItem(itemName);
            if (item.state === 'OPEN') {
                const igName = item.groupNames.find(g => g.startsWith("ig"));
                const label = igName ? items.getItem(igName).label : item.name;
                sendTelegramAlert(`Erinnerung: ${label} ist noch offen!`);
                
                // Timer erneut planen (Reschedule)
                windowTimers.get(itemName).reschedule(time.ZonedDateTime.now().plusMinutes(20));
            } else {
                windowTimers.delete(itemName);
            }
        });

        windowTimers.set(itemName, timer);
    }
});

// Timer aufräumen, wenn Fenster geschlossen wird
rules.JSRule({
    name: "Fenster Timer Stop",
    triggers: [triggers.GroupStateUpdateTrigger('tgWindow', 'CLOSED')],
    execute: (event) => {
        if (windowTimers.has(event.itemName)) {
            windowTimers.get(event.itemName).cancel();
            windowTimers.delete(event.itemName);
        }
    }
});

0
0
Alexander Hacker
Autor & Smart-Home-Entwickler

Alexander Hacker

Dokumentiert auf SmartHome Node praxisnahe Do-It-Yourself-Projekte, offene Architekturen und Automatisierungen mit openHAB 5, Zigbee2MQTT, ESPHome und Linux – ohne Abhängigkeit von Hersteller-Clouds.

3 Gedanken zu „OpenHAB: Generische Alarme mit Schwellwerten und Einheiten“

  1. Pingback: Automatischer Taupunkt-Lüfter - Smarthome DIY - Heimautomatisierung selbst gemacht

  2. Pingback: Automatic Dew Point Ventilation – Smart Home DIY - Smarthome DIY - Heimautomatisierung selbst gemacht

  3. Pingback: Automatischer Taupunkt-Lüfter – Smarthome DIY

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre, wie deine Kommentardaten verarbeitet werden.

Translate »