Keine Ahnung, hier sehe ich keine solchen Fehler.
Ich habe bereits die nächste Version fertig, weil es mir keine Ruhe gelassen hat

Weil aber die Rules eh schon komplex genug sind, habe ich mir gedacht, man kann sich das Leben auch unnötig kompliziert machen... Also etwas entschlacken und bestimmte Dinge als gegeben voraussetzen!
Wir benötigen eine Persistence, die alle Items auf eine gültige Zahl bringt, falls das System mal neu gestartet wird, am besten eignet sich hierzu mapDB, da sie praktisch keinen Platz braucht (wird über Paper UI Config->Addons->Persistence->mapDB installiert)
mapDB braucht keine spezielle Konfiguration, aber natürlich müssen wir dem Service mitteilen, welche Items er persistieren soll.
Die zugehörige Konfigurationsdatei für die Persistence, persistence/mapdb.persist:
Code: Alles auswählen
Strategies {
default=everyChange,restoreOnStartup
}
Items {
SchaltUhr* : strategy = everyChange,restoreOnStartup // Alle Member von SchaltUhr
BZ_SchaltMinute* : strategy = everyChange,restoreOnStartup // Alle Member von BZ_SchaltMinute
EG_SchaltMinute* : strategy = everyChange,restoreOnStartup // Alle Member von EG_SchaltMinute
Thermo* : strategy = everyChange,restoreOnStartup // Alle Member von Thermo
}
Die zugehörigen Items:
Code: Alles auswählen
// Hilfsitems
Group SchaltUhr // Alle Stell-Items
Group SchaltMinute // Alle Schaltzeiten
Group Thermo // Die restlichen zu persitierenden Items (Schalter usw.)
Switch planTimer // ein Trigger
// 1. Heizkreis
Group BZ_SchaltMinute (SchaltMinute) // Schaltzeiten BZ
// Stellitems
Number BZ_Uhr1_Ein_H "Uhr1 Ein Stunde [%d]" (SchaltUhr)
Number BZ_Uhr1_Ein_M "Uhr1 Ein Minute [%d]" (SchaltUhr)
Number BZ_Uhr1_Aus_H "Uhr1 Aus Stunde [%d]" (SchaltUhr)
Number BZ_Uhr1_Aus_M "Uhr1 Aus Minute [%d]" (SchaltUhr)
Number BZ_Uhr2_Ein_H "Uhr2 Ein Stunde [%d]" (SchaltUhr)
Number BZ_Uhr2_Ein_M "Uhr2 Ein Minute [%d]" (SchaltUhr)
Number BZ_Uhr2_Aus_H "Uhr2 Aus Stunde [%d]" (SchaltUhr)
Number BZ_Uhr2_Aus_M "Uhr2 Aus Minute [%d]" (SchaltUhr)
Number BZ_Uhr6_Aus_H "Uhr6 Aus Stunde [%d]" (SchaltUhr)
Number BZ_Uhr6_Aus_M "Uhr6 Aus Minute [%d]" (SchaltUhr)
Number BZ_Uhr7_Aus_H "Uhr7 Aus Stunde [%d]" (SchaltUhr)
Number BZ_Uhr7_Aus_M "Uhr7 Aus Minute [%d]" (SchaltUhr)
// Zeitspeicher
Number BZ_Uhr1_Ein (BZ_SchaltMinute)
Number BZ_Uhr1_Aus (BZ_SchaltMinute)
Number BZ_Uhr2_Ein (BZ_SchaltMinute)
Number BZ_Uhr2_Aus (BZ_SchaltMinute)
Number BZ_Uhr6_Aus (BZ_SchaltMinute)
Number BZ_Uhr7_Aus (BZ_SchaltMinute)
// restliche Items (Schalter plus Thermostat)
Switch BZ_Auto "BZ Automatik" (Thermo)
Switch BZ_SW_2 "2. Zeit" (Thermo)
Switch BZ_SW_Sa "Samstag Zeit" (Thermo)
Switch BZ_SW_So "Sonntag Zeit" (Thermo)
Number BZ_Thermostat "Nummer [%d]" (Thermo) // muss eventuell nicht persistiert werden? Aber für die Simulation!
// 2. Heizkreis
Group EG_SchaltMinute (SchaltMinute) // Schaltzeiten EG
// Stellitems
Number EG_Uhr1_Ein_H "Uhr1 Ein Stunde [%d]" (SchaltUhr)
Number EG_Uhr1_Ein_M "Uhr1 Ein Minute [%d]" (SchaltUhr)
Number EG_Uhr1_Aus_H "Uhr1 Aus Stunde [%d]" (SchaltUhr)
Number EG_Uhr1_Aus_M "Uhr1 Aus Minute [%d]" (SchaltUhr)
Number EG_Uhr2_Ein_H "Uhr2 Ein Stunde [%d]" (SchaltUhr)
Number EG_Uhr2_Ein_M "Uhr2 Ein Minute [%d]" (SchaltUhr)
Number EG_Uhr2_Aus_H "Uhr2 Aus Stunde [%d]" (SchaltUhr)
Number EG_Uhr2_Aus_M "Uhr2 Aus Minute [%d]" (SchaltUhr)
Number EG_Uhr6_Aus_H "Uhr6 Aus Stunde [%d]" (SchaltUhr)
Number EG_Uhr6_Aus_M "Uhr6 Aus Minute [%d]" (SchaltUhr)
Number EG_Uhr7_Aus_H "Uhr7 Aus Stunde [%d]" (SchaltUhr)
Number EG_Uhr7_Aus_M "Uhr7 Aus Minute [%d]" (SchaltUhr)
// Zeitspeicher
Number EG_Uhr1_Ein (EG_SchaltMinute)
Number EG_Uhr2_Ein (EG_SchaltMinute)
Number EG_Uhr1_Aus (EG_SchaltMinute)
Number EG_Uhr2_Aus (EG_SchaltMinute)
Number EG_Uhr6_Aus (EG_SchaltMinute)
Number EG_Uhr7_Aus (EG_SchaltMinute)
// restliche Items (Schalter plus Thermostat)
Switch EG_Auto "EG Automatik" (Thermo)
Switch EG_SW_2 "2. Zeit" (Thermo)
Switch EG_SW_Sa "Samstag Zeit" (Thermo)
Switch EG_SW_So "Sonntag Zeit" (Thermo)
Number EG_Thermostat "Nummer [%d]" (Thermo) // muss eventuell nicht persistiert werden? Aber für die Simulation!
Ich habe die Items so sortiert, dass man nur den gesamten Block "2. Heizkreis" kopieren muss, um einen weiteren Heizkreis anzulegen. Zu ändern ist dann nur die Vorsilbe bei allen Items, und natürlich die Gruppenzugehörigkeit der Zeitspeicher.
Da ich in den Rules keine Vorkehrungen mehr treffe, ungültige Werte abzufangen, ist nun der Zeitpunkt gekommen, jedem Stellitem einen gültigen Wert zuzuweisen. Meine Sitemap hierfür:
Code: Alles auswählen
sitemap test label="Test Sitemap" {
Frame label="Schaltuhren" {
Text label="BZ" {
Frame label="BZ Uhr" icon="icon" {
Setpoint item=BZ_Uhr1_Aus_H minValue=0 maxValue=23 step=1
Setpoint item=BZ_Uhr1_Aus_M minValue=0 maxValue=59 step=1
Setpoint item=BZ_Uhr1_Ein_H minValue=0 maxValue=23 step=1
Setpoint item=BZ_Uhr1_Ein_M minValue=0 maxValue=59 step=1
Setpoint item=BZ_Uhr2_Aus_H minValue=0 maxValue=23 step=1
Setpoint item=BZ_Uhr2_Aus_M minValue=0 maxValue=59 step=1
Setpoint item=BZ_Uhr2_Ein_H minValue=0 maxValue=23 step=1
Setpoint item=BZ_Uhr2_Ein_M minValue=0 maxValue=59 step=1
Setpoint item=BZ_Uhr6_Aus_H minValue=0 maxValue=23 step=1
Setpoint item=BZ_Uhr6_Aus_M minValue=0 maxValue=59 step=1
Setpoint item=BZ_Uhr7_Aus_H minValue=0 maxValue=23 step=1
Setpoint item=BZ_Uhr7_Aus_M minValue=0 maxValue=59 step=1
}
Frame label="BZ Aktivieren" {
Default item=BZ_Auto label="BZ Automatik"
Default item=BZ_SW_2 label="2. Zeit"
Default item=BZ_SW_Sa label="Samstag Zeit"
Default item=BZ_SW_So label="Sonntag Zeit"
Setpoint item=BZ_Thermostat label="Nummer"
}
}
Text label="EG" {
Frame label="EG Uhr" icon="icon" {
Setpoint item=EG_Uhr1_Aus_H minValue=0 maxValue=23 step=1
Setpoint item=EG_Uhr1_Aus_M minValue=0 maxValue=59 step=1
Setpoint item=EG_Uhr1_Ein_H minValue=0 maxValue=23 step=1
Setpoint item=EG_Uhr1_Ein_M minValue=0 maxValue=59 step=1
Setpoint item=EG_Uhr2_Aus_H minValue=0 maxValue=23 step=1
Setpoint item=EG_Uhr2_Aus_M minValue=0 maxValue=59 step=1
Setpoint item=EG_Uhr2_Ein_H minValue=0 maxValue=23 step=1
Setpoint item=EG_Uhr2_Ein_M minValue=0 maxValue=59 step=1
Setpoint item=EG_Uhr6_Aus_H minValue=0 maxValue=23 step=1
Setpoint item=EG_Uhr6_Aus_M minValue=0 maxValue=59 step=1
Setpoint item=EG_Uhr7_Aus_H minValue=0 maxValue=23 step=1
Setpoint item=EG_Uhr7_Aus_M minValue=0 maxValue=59 step=1
}
Frame label="EG Aktivieren" {
Default item=EG_Auto label="EG Automatik"
Default item=EG_SW_2 label="2. Zeit"
Default item=EG_SW_Sa label="Samstag Zeit"
Default item=EG_SW_So label="Sonntag Zeit"
Setpoint item=EG_Thermostat label="Nummer"
}
}
}
}
Kann man natürlich auch anders gestalten. für jeden Heizkreis muss der entsprechende Block mit rein, damit alle Stellitems (und die zugehörigen Schalter) von Beginn an einen gültigen Status haben. Der Thermostat ist nur für die Simulation mit drin.
Nun wird die erste Rule in Betrieb genommen, um alle Zeitspeicher zu füllen:
Code: Alles auswählen
var Boolean lock = false // lock für 2. Rule
// 1. Heizkreis, für 2.Rule
var NumberItem BZ_Item // Item des aktuellen BZ_Timers
var Timer BZ_Timer = null // der BZ_Timer
//2. Heizkreis, für 2.Rule
var NumberItem EG_Item // Item des aktuellen EG_Timers
var Timer EG_Timer = null // der EG_Timer
rule "Zeiten berechnen"
when
Member of SchaltUhr changed // Schaltzeit geändert
then
logInfo("timer","Zeiten berechnen")
SchaltMinute.allMembers.forEach[n| // Alle Schaltzeiten aller Heizkreise
var Number hour
var Number minute
var Number minutes
hour = SchaltUhr.members.filter[u|u.name.contains(n.name + "_H")].head.state as Number // Stunde des korrespondierenden StellItems
minute = SchaltUhr.members.filter[u|u.name.contains(n.name + "_M")].head.state as Number // Stunde des korrespondierenden StellItems
minutes = hour * 60 + minute // Sollzeit in Minuten
logInfo("timer","Schaltzeit {} auf {}:{} Uhr ({}) gesetzt.",n.name,hour,minute,hour * 60 + minute)
postUpdate(n.name.toString, minutes.toString) // im passenden Item speichern
]
logInfo("timer","Zeiten berechnen fertig")
end
Nun reicht es, einmalig ein Stellitem zu ändern, um die Rule zu triggern. Im Anschluss müssen alle Zeitspeicher einen gültigen Wert haben (wird geloggt).
Jetzt könne wir die 2. Rule in Betrieb nehmen:
Code: Alles auswählen
rule "Timer anlegen"
when
Time cron " 0 0 0 * * ?" or // Mitternacht
System started or // Neustart oder Rules neu geladen
Member of BZ_SchaltMinute changed or // Schaltzeit geändert
Member of EG_SchaltMinute changed or // Schaltzeit geändert
Item planTimer received command // Timertrigger (nächsten Timer anlegen)
then
if(lock) return; // Rule bereits aktiv?
lock= true // Rule sperren
logInfo("timer","Timer anlegen")
// ab hier BZ_Timer
if(BZ_Timer !== null) BZ_Timer.cancel // alten BZ_Timer löschen, falls aktiv
BZ_Item = BZ_SchaltMinute.members.filter[m|(m.state as Number) > now.getMinuteOfDay].sortBy[
(state as Number).intValue].head as NumberItem // Nächsten BZ_Timer ermitteln
if(BZ_Item !== null) { // noch mindestens ein Timer heute
logInfo("timer","Timer anlegen für {} ({})",BZ_Item.name,BZ_Item.state)
BZ_Timer = createTimer(now.withTimeAtStartOfDay.plusMinutes((BZ_Item.state as Number).intValue),[ // Timer anlegen
logInfo("timer","Timer ausgelöst: {}",BZ_Item.name)
if(BZ_Auto.state != ON) { // falls manueller Betrieb
logInfo("timer","Aber manueller Betrieb, Abbruch!")
planTimer.sendCommand(OFF) // nächsten BZ_Timer planen
return; // und Schluss
}
val Number soll = if(BZ_Item.name.contains("Ein")) 11 else 1 // Sollzustand nach BZ_Timer
logInfo("timer","geforderter Schaltvorgang: {}",soll)
if ((BZ_Item.name.contains("1")) || // Soll der Timer berücksichtigt werden?
(BZ_Item.name.contains("2") && BZ_SW_2.state == ON) ||
(BZ_Item.name.contains("6") && BZ_SW_Sa.state == ON && now.getDayOfWeek == 6) ||
(BZ_Item.name.contains("7") && BZ_SW_So.state == ON) && now.getDayOfWeek == 7) {
logInfo("timer","Schaltvorgang aktiv")
if((BZ_Thermostat.state as Number) != soll) // Aktueller Status abweichend?
BZ_Thermostat.sendCommand(soll)
else
logInfo("timer","aber Zustand bereits erreicht")
} else {
logInfo("timer","Schaltvorgang inaktiv")
}
planTimer.sendCommand(OFF) // nächsten BZ_Timer planen
])
}
// ab hier EG_Timer
if(EG_Timer !== null) EG_Timer.cancel
EG_Item = EG_SchaltMinute.members.filter[m|(m.state as Number) > now.getMinuteOfDay].sortBy[
(state as Number).intValue].head as NumberItem
if(EG_Item !== null) {
logInfo("timer","Timer anlegen für {} ({})",EG_Item.name,EG_Item.state)
EG_Timer = createTimer(now.withTimeAtStartOfDay.plusMinutes((EG_Item.state as Number).intValue),[
logInfo("timer","Timer ausgelöst: {}",EG_Item.name)
if(EG_Auto.state != ON) {
logInfo("timer","Aber manueller Betrieb, Abbruch!")
planTimer.sendCommand(OFF)
return;
}
val Number soll = if(EG_Item.name.contains("Ein")) 11 else 1
logInfo("timer","geforderter Schaltvorgang: {}",soll)
if ((EG_Item.name.contains("1")) ||
(EG_Item.name.contains("2") && EG_SW_2.state == ON) ||
(EG_Item.name.contains("6") && EG_SW_Sa.state == ON && now.getDayOfWeek == 6) ||
(EG_Item.name.contains("7") && EG_SW_So.state == ON && now.getDayOfWeek == 7)) {
logInfo("timer","Schaltvorgang aktiv")
if((EG_Thermostat.state as Number) != soll)
EG_Thermostat.sendCommand(soll)
else
logInfo("timer","aber Zustand bereits erreicht")
} else {
logInfo("timer","Schaltvorgang inaktiv")
}
planTimer.sendCommand(OFF)
])
}
// ab hier Rahmencode
logInfo("timer","Timer anlegen fertig")
lock = false
end
Ich habe mich bemüht, den Code so zu gestalten, dass klar ist, wo was anzupassen ist. Einfach den Block EG_Timer kopieren und hinten einfügen, als Block vor dem Rahmencode.
Wenn die Ruledatei neu geladen wird, triggert die 2. Rule automatisch und prüft auf heute auszuführende Timer. Dabei bekommt jeder Heizkreis seinen eigenen Timer.
Da ich nicht mehr auf NULL oder null prüfe, braucht ein Timer keine 30 Zeilen Code, mit sämtlichen Schaltern usw. Das bedeutet aber auch, dass jederzeit alle (!) Items einen gültigen Status haben müssen, sonst kracht die Rule. Wenn die Rule abschmiert, bleibt der lock hängen, das heißt, die Rule wird erst dann wieder ausgeführt, wenn die Ruledatei mindestens neu gespeichert wurde (dabei wird lock auf false gesetzt).
Mit entsprechendem Aufwand könnte die 2. Rule auch noch so konzipiert werden, dass sie für alle Heizkreise funktioniert, allerdings ist das um einiges aufwändiger, und ich denke, 40 Zeilen Code als Rumpf (inklusive 1. Rule) + 29 Zeilen Code pro Heizkreis sind ok für die gebotene Funktionalität

schließlich hatte der ursprüngliche Code ca. 200 Zeilen Code pro Heizkreis...