Table of Contents
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.
- semantische Gruppen: Du hast dein Zuhause bereits mit semantischen Gruppen definiert
- Triggergruppen: Deine Items müssen wissen, dass sie die Regel auslösen müssen. Daher fassen wir sie in Triggergruppen zusammen
- 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)
}
}
]
}
]
]
}
endconst { 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);
}
}
}
});
});
});
}
});
Pingback: OpenHAB: Wenn man vergisst das Fenster zu schließen - Smarthome DIY - Heimautomatisierung selbst gemacht
Pingback: Interaktiver Floorplan in OpenHAB mit Effekten