Startseite » automatische Heizregeln mit OpenHAB und semantischen Gruppen

automatische Heizregeln mit OpenHAB und semantischen Gruppen

Wenn dein Tagesablauf menschlich ist und sich häufig Änderungen ergeben wie unterschiedliche Anwesenheitszeiten, Urlaub, Ausflüge, Schichtarbeit, … dann willst du dein Heizsystem dem vielleicht auch ebenso dynamisch anpassen können. Vielleicht hast du auch ab und zu Änderungen in der Hardware und baust dein Smarthome gerade erst auf. Dann passieren in den Regeln schnell mal Fehler.

Im Folgenden will ich dir ein Beispiel vorstellen, wie eine Regel deine Heizungen ganz im Griff hat, abhängig von semantischen Gruppen.

notwendige Gruppen

Für das Konzept sind zwei Typen von Gruppen notwendig.

  1. semantische Gruppen: Du hast dein Zuhause bereits mit semantischen Gruppen definiert
  2. Triggergruppen: Deine Items müssen wissen, dass sie die Regel auslösen müssen. Daher fassen wir sie in Triggergruppen zusammen
  3. Item-Gruppen: Items, die zu einem Gegenstand als Gruppe zugeordnet werden

Gruppen sind technisch immer die gleichen Arten Gruppen. Die unterschiedliche Bezeichnung ist lediglich aufgrund der Funktion, die sie erfüllen sollen, gewählt. Das hilft beim Verstehen und Nachvollziehen.

/etc/openhab/items/semantic-model.items

//functional Groups
Group 						fgPersist							// for Items to preserve their history
Group:Number:COUNT(OPEN)	fgIntrusion		"Intrusion [%s]"	// All window- and doorsensors

Group						fgRadiatorBoost						// Radiator Boost
Group:DateTime:EARLIEST		fgFaultDetection					// Fault or disturbance detection for wireless connections (as "last seen")

//trigger groups
Group		tgHeatingEvents			// events, that will change temperature to a custom setpoint like a calendar evenent
Group		tgWindow				// Window- or door contact sensors
Group		tgMotionDetection		// PIR Sensors
Group		tgHausAus				// a special button to tell my home, that I will go to sleep now
Group		tgWatchdogEvents		// alarm system events
Group		tgLightshow				// dynamic scene picker
Group		tgLSchange				// dynamic scene influencer
Group		tgLSinterrupt			// dynamic scene interruptor
Group		tgWarnOnLimit			// Gas- and Rad-Sensors for warning
Group		tgCalc					// Item triggers a calculations for another Item (like Temperature or Humidity for Dewpoint)



// Semantic Groups
Group sgWacholderweg "Zuhause" 													["Location"]
	Group sgIndoor "Indoor" 															["Indoor"]
		Group sgHouse "Haus" 						<group> 		(sgIndoor) 			["House"] 		{ alexa="Other" }
			Group sgGroundFloor "EG" 				<groundfloor> 	(sgHouse) 			["GroundFloor"]
				Group sgBedroom "Schlafzimmer" 		<bedroom> 		(sgGroundFloor) 	["Bedroom"] 	{ alexa="Other" }
				Group sgCorridorEG "Flur"	 		<corridor> 		(sgGroundFloor) 	["Corridor"]	{ alexa="Other" }
				Group sgKitchen "Küche" 			<kitchen> 		(sgGroundFloor) 	["Kitchen"] 	{ alexa="Other" }
				Group sgBathroom "Bad" 				<bath> 			(sgGroundFloor) 	["Bathroom"] 	{ alexa="Other" }
				Group sgLivingRoom "Wohnzimmer" 					(sgGroundFloor) 	["LivingRoom"] 	{ alexa="Other" }
			Group sgCellar "Keller" 				<cellar> 		(sgHouse) 			["Cellar"] 		{ alexa="Other" }
				Group sgCorridorCellar "Flur Keller" <corridor> 	(sgCellar) 			["Corridor"]
				Group sgGarage "Garage" 			<garagedoor> 	(sgCellar) 			["Garage"] 		
	Group sgOutdoor "Outdoor" ["Outdoor"]
				Group sgEnvironment "Draussen" 		<flow> 			(sgOutdoor) 		["Garden"] 		{ alexa="Other" }
				Group sgGarden "Garten" 			<garden> 		(sgOutdoor) 		["Garden"]
				Group sgTerrace "Terrasse" 			<terrace> 		(sgOutdoor) 		["Terrace"] 	{ alexa="Other" }

mögliche beeinflussende Items

tgHeatingEvents

Die Kalender-Items basieren auf dem OpenHAB ical-Modul. So kann aus einem Google-Kalender die Beschreibung als Zahl ausgelesen werden und als Setpoint an ein Thermostat übermittelt werden. Die items werden zu einer itemGroup pro Kalender zusammengefasst. diese itemGroup wird zu einem Raum mittels semantischer Gruppe zugewiesen. Die semantischen Tags (“Webservice”) geben dem System Aufschluss darüber, worum es sich bei der Gruppe handelt.

Wenn sich der Start-Zeitpunkt oder der Titel ändert, soll damit die Heizregel ausgelöst werden. Das wird mit der Triggergruppe “tgHeatingEvents” deklariert.

/etc/openhab/items/calendars.items

Group:Switch:COUNT(ON)		igCalHeatingWohnzimmer							"Heizprofil Wohnzimmer"				(sgLivingRoom)["WebService"]
Switch				KalenderHeizprofilWohnzimmer_CurrentEventPresence		"Current Event Presence"			(igCalHeatingWohnzimmer)											{ channel="icalendar:calendar:Wohnzimmer:current_presence" }
Number:Temperature	KalenderHeizprofilWohnzimmer_CurrentEventTitle			"Current Event Title [%.1f °C]"		(igCalHeatingWohnzimmer,tgHeatingEvents)["Setpoint","Temperature"]	{ channel="icalendar:calendar:Wohnzimmer:current_title" }
DateTime			KalenderHeizprofilWohnzimmer_CurrentEventStart			"Current Event Start"				(igCalHeatingWohnzimmer,tgHeatingEvents)["Status","Timestamp"]		{ channel="icalendar:calendar:Wohnzimmer:current_start" }
DateTime			KalenderHeizprofilWohnzimmer_CurrentEventEnd			"Current Event End"					(igCalHeatingWohnzimmer)											{ channel="icalendar:calendar:Wohnzimmer:current_end" }

Group:Switch:COUNT(ON)		igCalHeatingBedroom								"Heizprofil Schlafzimmer"			(sgBedroom)["WebService"]
Switch				KalenderHeizprofilSchlafzimmer_CurrentEventPresence		"Current Event Presence"			(igCalHeatingBedroom)												{ channel="icalendar:calendar:Schlafzimmer:current_presence" }
Number:Temperature	KalenderHeizprofilSchlafzimmer_CurrentEventTitle		"Current Event Title [%.1f °C]"		(igCalHeatingBedroom,tgHeatingEvents)["Setpoint","Temperature"]		{ channel="icalendar:calendar:Schlafzimmer:current_title" }
DateTime			KalenderHeizprofilSchlafzimmer_CurrentEventStart		"Current Event Start"				(igCalHeatingBedroom,tgHeatingEvents)["Status","Timestamp"]			{ channel="icalendar:calendar:Schlafzimmer:current_start" }
DateTime			KalenderHeizprofilSchlafzimmer_CurrentEventEnd			"Current Event End"					(igCalHeatingBedroom)												{ channel="icalendar:calendar:Schlafzimmer:current_end" }

// ... eventually more calendars like these ...

tgWindow

Dabei handelt es sich um simple Items, die den Status von Fenstern und Türen aufzeigen. Ich hab mich für Sensoren auf Zigbee-Basis und MQTT entschieden. Die Batterien halten ewig.

Der Kontakt- und Batterie-Status reichen für die Zwecke vollkommen aus. Auch diese beiden Zustände werden zu einer itemGroup zusammengefasst. Diese itemGroup wird als “Window” getaggt und einem Raum zugeordnet. die fg-Gruppen sind an anderer Stelle interessant, jedoch nicht für die Heizregel. Auch diese Items müssen sauber getaggt werden.

Wenn sich ein Fenster öffnet, soll die Heizung in dem Raum abgeschaltet werden.

/etc/openhab/items/zigbee-sensors.items

Group:Number:COUNT(OPEN)	igWindowSensor_LivingRoom			"Fenster Wohnzimmer [%s]"	<Window>		(sgLivingRoom)["Window"]
Contact						WindowSensor_LivingRoom_contact		"Kontakt [%s]" 								(fgPersist,igWindowSensor_LivingRoom,fgIntrusion,tgWindow)["OpenState","Opening"]	{ channel="mqtt:topic:myMosquitto:WindowSensor_LivingRoom:contact" }
Number:Dimensionless		WindowSensor_LivingRoom_battery		"Batterie [%.1f %%]" 		<Battery>		(igWindowSensor_LivingRoom)["Battery","Measurement","Energy"]						{ channel="mqtt:topic:myMosquitto:WindowSensor_LivingRoom:battery" }

// ... eventually more sensors like these ...

tgHausAus (optional)

Das ist mein Knopf am Bett. Oder die Software-Variante als Switch-Item. Der soll das Haus abschalten, wenn ich ins Bett gehe und darauf drücke.

Tritt ein neues Ereignis ein, wie etwa eine Kalendergesteuerte Temperaturänderung, wird der Modus abgelöst.

/etc/openhab/items/hue.items

Group					igSmartButton1				"Haus-Aus-Knopf"								(sgBedroom)["RemoteControl"]
String					SmartButton1_DimmerSwitch													(igSmartButton1,tgHausAus)["Status"]				{ channel="mqtt:topic:myMosquitto:SmartButton1:action" } //on, off, skip_backward, skip_forward, press, hold, release
Number:Dimensionless	SmartButton1_Battery		"Batterie [%.1f %%]"	<Battery>				(igSmartButton1)["Battery","Measurement","Energy"]	{ channel="mqtt:topic:myMosquitto:SmartButton1:battery" }
Switch					SmartButton1_manual			"Haus"											(igSmartButton1,tgHausAus)["Switch"]				{ expire="2s,state=ON", alexa="Switch" }

tgWatchdogEvents (optional)

Das sind Events, die durch das Alarmsystem hervorgerufen werden. Hier bietet es sich an, das manuelle Einschalten der Alarmanlage das Heizsystem des Hauses in einen Urlaubsmodus zu schicken.

Heizungs-Items

Group					igThermostatWohnzimmer						"Wohnzimmer Heizung"				<radiator>		(sgLivingRoom)["RadiatorControl"]										{ synonyms="Wohnzimmer Thermostat", alexa="Thermostat" }
Number:Temperature		ThermostatWohnzimmer1_Temperature			"Aktuelle Temp. (l) [%.1f °C]"		<Temperature>	(fgPersist,igThermostatWohnzimmer)["Measurement","Temperature"]			{ channel="mqtt:topic:myMosquitto:ThermostatWohnzimmer1:local_temperature" }
Number:Temperature		ThermostatWohnzimmer1_SetTemperature		"Soll-Temp. (l) [%.1f °C]"			<Temperature>	(fgPersist,igThermostatWohnzimmer,sgLivingRoom)["Setpoint","Temperature"]{ alexa="TargetTemperature", channel="mqtt:topic:myMosquitto:ThermostatWohnzimmer1:current_heating_setpoint", channel="mqtt:topic:myMosquitto:ThermostatWohnzimmer2:current_heating_setpoint" }
Number:Temperature		ThermostatWohnzimmer1_SetTemperatureRO		"Soll-Temperatur (RO) [%.1f °C]"	<Temperature>	(igThermostatWohnzimmer)["Measurement","SetpointRO"]					{ channel="mqtt:topic:myMosquitto:ThermostatWohnzimmer1:current_heating_setpoint" }
String					ThermostatWohnzimmer1_RadiatorMode			"Radiator mode [%s]"								(igThermostatWohnzimmer)												{ expire="1s,manual", channel="mqtt:topic:myMosquitto:ThermostatWohnzimmer1:preset" }
Number:Dimensionless	ThermostatWohnzimmer1_Battery				"Battery level [%.1f %%]"			<Battery>		(igThermostatWohnzimmer)["Battery","Measurement","Energy"]				{ alexa="BatteryLevel", channel="mqtt:topic:myMosquitto:ThermostatWohnzimmer1:battery" }
Number:Dimensionless	ThermostatWohnzimmer1_ValvePosition			"Ventilposition [%.1f %%]"							(fgPersist,igThermostatWohnzimmer)["OpenLevel","Opening"]				{ channel="mqtt:topic:myMosquitto:ThermostatWohnzimmer1:valve_position" }
Switch					ThermostatWohnzimmer1_Boost					"Boost"												(igThermostatWohnzimmer,fgRadiatorBoost)["Switch","Opening"]			{ channel="mqtt:topic:myMosquitto:ThermostatWohnzimmer1:boost" }

Automations-Regeln

Sind die Gruppen und Items sauber definiert, kann die Regel wie folgt (oder auf deine Bedürfnisse angepasst) verwendet werden. Zunächst werden die Bedingungen eines Ereignisses ermittelt. Erst im letzten Bereich werden diese dann zu einer Änderung des Sollwertes zusammengesetzt.

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

var Map<String, QuantityType<Temperature>> setpoints = newHashMap

//it is possible, that the signal gets lost. So try it again, till the not commanded item reacts to a change
//yes, i could set the "noautoupdate"-option. But alexa then will answer wrong temperature, when i ask... so: duplicate item
val setRadiator = [ NumberItem radiator, QuantityType<Temperature> setpoint, Map<String, Timer> setpoints |
	//when setpoint is changed, send it multiple times because sometimes the zigbee-signal gets lost
	val String igName = radiator.getGroupNames.findFirst[ String groupName | groupName.startsWith("ig") ]
	val GroupItem igItem = ScriptServiceUtil.getItemRegistry.getItem(igName) as GroupItem
	
	val NumberItem setpointROitem = igItem.members.findFirst[ item | item.hasTag("Measurement") && item.hasTag("SetpointRO") ]
	
	setpoints.put(radiator.name,setpoint)
	
	//2s, 4s, 8s, 16s ... make the timespan bigger between the tryouts
	createTimer(now, [ |
		var long sleep = 2000
		for(var i=0; i<10 && (setpointROitem.state == NULL || (setpointROitem.state as QuantityType<Temperature>) != setpoints.get(radiator.name)); i++) {
			radiator.sendCommand(setpoint)
			sleep *= 2
			Thread::sleep(sleep)
		}
	])
	
]

rule "heating"
when
	Member of tgHeatingEvents changed
	or Member of tgWindow changed
	or Member of tgHausAus changed
	or Member of tgWatchdogEvents changed
then {

	//find the affected rooms
	var List<String> roomList = newArrayList()
	
	//which rooms shall be affected due to the trigger?
	if(triggeringItem.name == "SmartButton1_DimmerSwitch" || triggeringItem.name == "SmartButton1_manual"){
		//which rooms shall be shut down, when I go to sleep and press my home-off-button
		roomList = newArrayList("sgOfficeJulia","sgKitchen","sgLivingRoom","sgOfficeAlex")
	}
	else if(triggeringItem.name == "WatchdogManual"){
		//which rooms shall be shut down, when I activate the alarm system manually?
		roomList = newArrayList("sgBedroom","sgOfficeJulia","sgKitchen","sgLivingRoom","sgOfficeAlex")
	}
	else{
		//which room is affected due to the triggering item room?
		
		//detect itemGroup (ig) of the item
		val igName = triggeringItem.getGroupNames.findFirst[ String groupName | groupName.startsWith("ig") ]
		val GroupItem igItem = ScriptServiceUtil.getItemRegistry.getItem(igName) as GroupItem
		
		//detect semanticGroup of the itemGroup
		val sgName = igItem.getGroupNames.findFirst[ String groupName | groupName.startsWith("sg") ]
		roomList = newArrayList(sgName)
	}
	
	
	//do something with the rooms:
	roomList.forEach[ String sgName |
		val GroupItem room = ScriptServiceUtil.getItemRegistry.getItem(sgName) as GroupItem
	
		//what states has the room?
		
		//window open?
		var window = CLOSED
		room.members.forEach[ NumberItem thing |
			if( thing.tags.contains("Window") || thing.tags.contains("FrontDoor") || thing.tags.contains("BackDoor") ){
				if(thing.state != NULL){
					if( (thing.state as Number).intValue > 0){
						window = OPEN
					}
				}
			}
		]
		
		//holiday or manual alarm is on?
		var watchdog = if(WatchdogManual.state == ON) true else false
		
		//home-off-button?
		var hausAusKnopf = if(triggeringItem.name == "SmartButton1_DimmerSwitch" || triggeringItem.name == "SmartButton1_manual") true else false
		
		//is there a heating event in the calendar for that room?
		var QuantityType<Temperature> calendarSetpoint = 0|°C
		room.members.forEach[ GroupItem gItem |
			if(gItem.tags.contains("WebService")){
				gItem.members.forEach[ NumberItem item |
					if( item.tags.contains("Setpoint") && item.tags.contains("Temperature") ){
						calendarSetpoint = if(item.state != UNDEF && item.state >= 6|°C) (item.state as QuantityType<Temperature>) else 17|°C
					}
				]
			}
		]
		
		
		//set the temperature
		room.members.forEach[ GroupItem gItem |
			if(gItem.tags.contains("RadiatorControl")){
				gItem.members.forEach[ NumberItem radiator |
					
					if( radiator.tags.contains("Setpoint") && radiator.tags.contains("Temperature") ){
						
						if(window == OPEN){
							setRadiator.apply(radiator,6|°C,setpoints)
							//logInfo("Test", "Set window OPEN: {}", triggeringItem)
						}
						else if(watchdog){
							setRadiator.apply(radiator,6|°C,setpoints)
							//logInfo("Test", "Set watchdog: {}", triggeringItem)
						}
						else if(hausAusKnopf && 17|°C < (radiator.state as QuantityType<Temperature>)){
							setRadiator.apply(radiator,17|°C,setpoints)
							//logInfo("Test", "Set Haus-Aus: {}", triggeringItem)
						}
						else if(calendarSetpoint > 0|°C){
							setRadiator.apply(radiator,calendarSetpoint,setpoints)
							//logInfo("Test", "Set calendar: {}", triggeringItem)
						}
						else if(17|°C < (radiator.state as QuantityType<Temperature>)){
							setRadiator.apply(radiator,17|°C,setpoints)
							//logInfo("Test", "Set default: {}", triggeringItem)
						}
						
					}
				]
			}
		]
	]
	
}
end

const { rules, triggers, items, actions, time } = require('openhab');

// --- Globale Konstanten & Variablen ---
const FALLBACK_TEMP = 16.0;
const FROSTSAVE_TEMP = 8.0;
const DECALC_TEMP = 40.0;

// Map zum Speichern der Sollwerte (um doppelte Befehle zu vermeiden)
// Key: ItemName, Value: String (z.B. "20 °C")
let setpointsMap = new Map();

/**
 * it is possible, that the signal gets lost. So try it again, till the not commanded item reacts to a change
 * yes, i could set the "noautoupdate"-option. But alexa then will answer wrong temperature, when i ask... so: duplicate item
 */
function setRadiator(radiatorItem, setpointStr) {
	// when setpoint is changed, send it multiple times because sometimes the zigbee-signal gets lost
	const igName = radiatorItem.groupNames.find(g => g.startsWith("ig"));
	if (!igName) return;

	const igItem = items.getItem(igName);

	const setpointROitem = igItem.members.find(i => i.tags.includes("Measurement") && i.tags.includes("SetpointRO"));
	const valveStateitem = igItem.members.find(i => i.tags.includes("Measurement") && i.tags.includes("ValveState"));

	// Prüfen: Wenn es eine Entkalkung ist ODER sich das Ventil in den letzten 10 Tagen nicht bewegt hat
	const valveChangedRecently = valveStateitem.persistence.changedSince(time.toZDT().minusDays(10));

	// if this is a regular setpoint or a necessary decalcification: do it
	if (setpointStr !== DECALC_TEMP || !valveChangedRecently) {
		setpointsMap.set(radiatorItem.name, setpointStr);

		// Initialer Versuch
		startRetryLoop(radiatorItem, setpointROitem, setpointStr, 2000, 0);
	}
}

/**
 * Rekursive Funktion für das wiederholte Senden (ersetzt die Thread::sleep Schleife)
 */
function startRetryLoop(radiator, checkItem, targetVal, delayMs, attempt) {
	// Wenn wir 10 Versuche durch haben, abbrechen
	if (attempt >= 10) return;

	// Aktuellen Status prüfen
	const currentVal = checkItem.state; // String status
	const targetStored = setpointsMap.get(radiator.name);

	if (currentVal !== "NULL" && currentVal !== "UNDEF" && targetStored === targetVal) {
		if (checkItem.quantityState && checkItem.quantityState.toUnit("°C").float == targetVal) {
			return; // Erfolg, aufhören.
		}
	} else if (targetStored !== targetVal) {
		return; // Ziel hat sich geändert, diese Loop ist veraltet.
	}

	radiator.sendCommand(targetVal);

	// Timer für nächsten Versuch (Exponential Backoff)
	actions.ScriptExecution.createTimer(time.toZDT(delayMs), () => {
		startRetryLoop(radiator, checkItem, targetVal, delayMs * 2, attempt + 1);
	});
}

// --- Regel 1: Start Decalcification ---
rules.JSRule({
	name: "start decalcification",
	description: "Startet Entkalkungsfahrt am Montag um 11:00",
	triggers: [triggers.GenericCronTrigger("0 0 11 ? * MON *")],
	execute: () => {
		items.getItem("siDecalcification").sendCommand("ON");
	}
});

// --- Regel 2: Stop Decalcification ---
rules.JSRule({
	name: "ensure to stop decalcification",
	triggers: [
		triggers.GenericCronTrigger("0 0/2 11 ? * MON *"),
		triggers.GenericCronTrigger("0 0 * ? * MON *"),
		triggers.GenericCronTrigger("0 0 12 ? * * *")
	],
	execute: () => {
		const siDecalc = items.getItem("siDecalcification");
		if (siDecalc.state === "ON") {
			siDecalc.sendCommand("OFF");
		}
	}
});

// --- Regel 3: Thermostat Calibration ---
rules.JSRule({
  name: "set thermostat temperature_calibration based on room thermostat",
  triggers: [triggers.GenericCronTrigger("0 0/12 * ? * * *")],
  execute: () => {
    // Alle Raumthermostate holen
    const roomThermostats = items.getItems().filter(i => i.tags.includes("Measurement") && i.tags.includes("Temperature"));

    roomThermostats.forEach(roomSensor => {
      // ItemGroup (ig) finden
      const igName = roomSensor.groupNames.find(g => g.startsWith("ig"));
      if (!igName) return;

      const igRoomThermostat = items.getItem(igName);
      
      // Hat sich Raumtemperatur in den letzten 15 min geändert?
      const roomChanged = roomSensor.persistence.changedSince(time.toZDT().minusMinutes(15));

      // SemanticGroup (sg) finden
      const sgName = igRoomThermostat.groupNames.find(g => g.startsWith("sg"));
      if (!sgName) return;

      // Im JS Script iterieren wir über die Liste (im Original war es newArrayList(sgName))
      const room = items.getItem(sgName);
      
      // Radiator Controls im Raum finden
      const radiatorControls = room.members.filter(m => m.tags.includes("RadiatorControl"));

      radiatorControls.forEach(radControl => {
        const radSensor = radControl.members.find(m => m.tags.includes("Measurement") && m.tags.includes("TempThermostat"));
        const currentCalItem = radControl.members.find(m => m.tags.includes("Setpoint") && m.tags.includes("Calibration"));

        if (radSensor && currentCalItem) {
            // Berechnungen mit Quantity Types
            // Wir nutzen hier .quantityState, das gibt ein Quantity-Objekt zurück, mit dem man rechnen kann.
            const roomVal = roomSensor.quantityState.toUnit("°C").float;
            const radVal = radSensor.quantityState.toUnit("°C").float;
            const curCalVal = currentCalItem.quantityState.toUnit("°C").float;

            // Formel: radiator_measurement = radiator_temp_value - current_calibration_value
            const radMeasurement = radVal - curCalVal;
            // Formel: target_calibration_value = room_temp_value - radiator_measurement
            const targetCalVal = roomVal - radMeasurement;
            
            const calDiff = curCalVal - targetCalVal;

            if (roomChanged) {
                if (Math.abs(calDiff) > 1.0) {
                    // Neuen Wert setzen (als Number/Int senden, wie im Original)
                    currentCalItem.sendCommand(targetCalVal); 
                }
            } else {
                // Fallback Logic (Durchschnitte)
                let avgCal = currentCalItem.persistence.averageSince(time.toZDT().minusHours(48)).numericState;
                if (avgCal === null) avgCal = currentCalItem.persistence.averageSince(time.toZDT().minusHours(24)).numericState;
                if (avgCal === null) avgCal = currentCalItem.persistence.averageSince(time.toZDT().minusHours(6)).numericState;
                
                
                let targetAvg = 0;
                if(avgCal !== null) targetAvg = avgCal;

                // Differenz prüfen (Manuell, da avgCal oft nur number ist)
                // Wir holen den aktuellen Wert als float
                const currentFloat = curCalVal.toUnit("°C").float;
                const diffAvg = Math.abs(currentFloat - targetAvg);

                if (diffAvg > 1) {
                    console.log(`Calibration Fallback: ${radControl.name} -> ${targetAvg}`);
                    currentCalItem.sendCommand(targetAvg);
                }
            }
        } else {
            console.warn(`Calibration Item oder Sensor fehlt für: ${radControl.name}`);
        }
      });
    });
  }
});


// --- Regel 4: Heating Logic ---
rules.JSRule({
	name: "heating",
	description: "Hauptheizungslogik basierend auf Events",
	triggers: [
		triggers.GroupStateChangeTrigger("tgHeatingEvents"),
		triggers.GroupStateChangeTrigger("tgWindow"),
		triggers.GroupStateChangeTrigger("tgHausAus"),
		triggers.GroupStateChangeTrigger("tgWatchdogEvents")
	],
	execute: (event) => {
		const triggeringItem = items.getItem(event.itemName);
		const triggerName = triggeringItem.name;
		const newState = event.newState; // String state

		// 1. Haus Aus Knopf?
		const hausAusKnopf = (triggerName === "SmartButton1_DimmerSwitch" || triggerName === "SmartButton1_manual");

		// 2. Betroffene Räume finden
		let roomList = [];

		if (hausAusKnopf) {
			// WatchdogAffected ohne Bedroom
			roomList = items.getItemsByTag("WatchdogAffected")
							.filter(i => !i.tags.includes("Bedroom"));
		} else if (triggerName === "WatchdogManual") {
			// Alle WatchdogAffected
			roomList = items.getItemsByTag("WatchdogAffected");
		} else {
			// Logik um Raum über Gruppen zu finden
			const igName = triggeringItem.groupNames.find(g => g.startsWith("ig"));
			let sgNames = [];

			if (igName) {
				const igItem = items.getItem(igName);
				sgNames = igItem.groupNames.filter(g => g.startsWith("sg"));
			} else {
				sgNames = triggeringItem.groupNames.filter(g => g.startsWith("sg"));
			}
			
			roomList = sgNames.map(name => items.getItem(name));
		}

		// 3. Global "All Off" Bedingungen prüfen
		const watchdogManual = items.getItem("WatchdogManual");
		const presence = items.getItem("KalenderWatchdog_CurrentEventPresence");
		const titleS = items.getItem("KalenderWatchdog_CurrentEventTitleS");
		const maxTemp = items.getItem("siFC_MAX_Temp");

		let all_off = false;
		
		// Check Watchdog
		if (watchdogManual.state === "ON") all_off = true;
		
		// Check Calendar
		if (presence.state === "ON" && titleS.state !== "nightly") all_off = true;

		// Check Temperature (Vergleich Quantity)
		if (maxTemp.quantityState && maxTemp.quantityState.toUnit("°C").float > 22.0) all_off = true;


		// 4. Räume durchgehen
		roomList.forEach(room => {
			// Fenster Status prüfen
			let windowOpen = false;
			// Filtern nach Tags Window, FrontDoor, BackDoor
			const windowSensors = room.members.filter(m => m.tags.includes("Window") || m.tags.includes("FrontDoor") || m.tags.includes("BackDoor"));
			
			windowSensors.forEach(sensor => {
				// Prüfung: State != NULL und Wert > 0 (für Contact oft OPEN/CLOSED, hier Logik aus DSL übernommen -> number check)
				// DSL: (thing.state as Number).intValue > 0. 
				// Bei ContactItem ist OPEN=OpenClosedType.OPEN. Wenn es NumberItems sind (Dimmer/Rollershutter als Fenster?):
				if (sensor.state !== "NULL" && sensor.state !== "UNDEF") {
					if (sensor.type === "Contact" && sensor.state === "OPEN") windowOpen = true;
					else if (sensor.numericState > 0) windowOpen = true; 
				}
			});

			// Kalender Setpoint prüfen
			let calendarSetpoint = "0 °C"; // String als Quantity Ersatz
			// Suche WebService -> HeatingProfile
			const heatingProfiles = room.members.filter(m => m.tags.includes("WebService") && m.tags.includes("HeatingProfile"));
			heatingProfiles.forEach(gItem => {
				const setpointItems = gItem.members.filter(i => i.tags.includes("Setpoint") && i.tags.includes("Temperature"));
				setpointItems.forEach(item => {
					if (item.state !== "UNDEF") {
						// Prüfen ob >= Frostschutz (Quantity Vergleich)
						if (item.quantityState && item.quantityState.toUnit("°C").float >= FROSTSAVE_TEMP) {
							calendarSetpoint = item.state;
						} else {
							calendarSetpoint = FALLBACK_TEMP;
						}
					}
				});
			});

			// Gäste Anwesenheit
			const guestPresenceItem = room.members.find(m => m.tags.includes("GuestPresence"));
			
			// Heizkörper steuern
			const radiatorControls = room.members.filter(m => m.tags.includes("RadiatorControl"));
			
			radiatorControls.forEach(gItem => {
				const radiators = gItem.members.filter(r => r.tags.includes("Setpoint") && r.tags.includes("Temperature"));
				
				radiators.forEach(radiator => {
					// Entscheidungshierarchie
					
					if (windowOpen) {
						setRadiator(radiator, FROSTSAVE_TEMP);
					}
					else if (triggerName === "siDecalcification" && newState === "ON") {
						setRadiator(radiator, DECALC_TEMP);
					}
					else if (guestPresenceItem && guestPresenceItem.state === "OFF") {
						setRadiator(radiator, FROSTSAVE_TEMP);
					}
					else if (all_off) {
						setRadiator(radiator, FROSTSAVE_TEMP);
					}
					else if (hausAusKnopf) {
						// Prüfen ob aktueller Wert größer als Fallback ist
						if (radiator.quantityState && radiator.quantityState.toUnit("°C").float > FALLBACK_TEMP) {
							setRadiator(radiator, FALLBACK_TEMP);
						}
					}
					else if (items.getItem("siDecalcification").state === "ON") { 
					   // Edge case: Wenn Decalcification läuft, aber Regel durch anderen Trigger aufgerufen wurde
					   // Im Originalcode nicht explizit drin, aber durch "newState == ON" oben abgedeckt.
					   // Hier ignorieren wir es, falls nicht explizit getriggert.
					}
					else {
						// Kalender oder Fallback
						// Wir müssen Quantity Strings vergleichen
						const calQty = (calendarSetpoint === "0 °C") ? 0 : 1; // Vereinfacht

						if (calQty > 0) { // Eigentlich: if calendarSetpoint > 0°C
							 setRadiator(radiator, calendarSetpoint);
						}
						else {
							// Default Check
							 if (radiator.quantityState && radiator.quantityState.toUnit("°C").float > FALLBACK_TEMP) {
								 setRadiator(radiator, FALLBACK_TEMP);
							 }
						}
					}
				});
			});

		});
	}
});

0
0

2 Gedanken zu „automatische Heizregeln mit OpenHAB und semantischen Gruppen“

  1. Pingback: OpenHAB: Wenn man vergisst das Fenster zu schließen - Smarthome DIY - Heimautomatisierung selbst gemacht

  2. Pingback: Interaktiver Floorplan in OpenHAB mit Effekten

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 »