Simplification of Hashmap-Rule

Continuing the discussion from A beginners nightmare:

As discussed in the previous topic the idea is to analyse my rule and see if there is room for simplification.
Thank you in advance for you ideas and input, they already helped a lot.
The comments in the files are in German, I apologize in advance. :smile:

Here is what I have:
I have some LED-Strips in my sleeping room and I want to show some nice fades on them.
These fade should automatically start when there is movement or when I open my door.
This is why I have installed a movement sensor in my sleeping room and on its door.
Of course, I don’t always want the same fade to happen.
In the morning, I want a nice warm fade when I wake up and stand up. During the day I want a discreet fade, in the evening I want a bright fade so I don’t have to turn on the lights and in the night I want a low-light-fade so I don’t get blinded.
On Friday and Saturday the night-fade shall start later, because I go to bed later. The morning fade should also start later, because I like to sleep in.
When I walk in and out my sleeping room more frequent, I want the lights to be on longer, because I am obviously looking for something and don’t want the lights go off when I am in the room.
When I open the door, the time-out should be longer then when there is movement but when I close it, the time-out should be shorted, because I don’t want the light to be on when I am not in the room.

I tried to implement everything as described and it works fine but the lack of classes makes the code bloated and hard to read. As suggested in the other thread there may be a better and more elegant approach and this should be discussed in this topic.


This is are my fades. Unfortunately I have to create for every fade an item.

Color   Schlafzimmer_Rechts_Oben_FADE_OFF            {dmx="CHANNEL[1/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Rechts_Unten_FADE_OFF            {dmx="CHANNEL[4/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Mitte_Oben_FADE_OFF            {dmx="CHANNEL[7/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Mitte_Unten_FADE_OFF            {dmx="CHANNEL[10/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Links_Oben_FADE_OFF            {dmx="CHANNEL[13/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Links_Unten_FADE_OFF            {dmx="CHANNEL[16/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Spiegel_FADE_OFF                {dmx="CHANNEL[19/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Bett_FADE_OFF                    {dmx="CHANNEL[22/3], ON[FADE|1000:0,0,0:-1]"}

Color   Schlafzimmer_Rechts_Oben_FADE_DAY            {dmx="CHANNEL[1/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Rechts_Unten_FADE_DAY            {dmx="CHANNEL[4/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Mitte_Oben_FADE_DAY            {dmx="CHANNEL[7/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Mitte_Unten_FADE_DAY            {dmx="CHANNEL[10/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Links_Oben_FADE_DAY            {dmx="CHANNEL[13/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Links_Unten_FADE_DAY            {dmx="CHANNEL[16/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Spiegel_FADE_DAY                {dmx="CHANNEL[19/3], ON[FADE|2000:0,255,255:-1]"}
Color   Schlafzimmer_Bett_FADE_DAY                    {dmx="CHANNEL[22/3], ON[FADE|1000:0,0,0:-1]"}

Color   Schlafzimmer_Rechts_Oben_FADE_NIGHT            {dmx="CHANNEL[1/3], ON[FADE|0:0,0,0:-1]"}
Color   Schlafzimmer_Rechts_Unten_FADE_NIGHT        {dmx="CHANNEL[4/3], ON[FADE|0:0,0,0:-1]"}
Color   Schlafzimmer_Mitte_Oben_FADE_NIGHT            {dmx="CHANNEL[7/3], ON[FADE|0:0,0,0:-1]"}
Color   Schlafzimmer_Mitte_Unten_FADE_NIGHT            {dmx="CHANNEL[10/3], ON[FADE|0:0,0,0:-1]"}
Color   Schlafzimmer_Links_Oben_FADE_NIGHT            {dmx="CHANNEL[13/3], ON[FADE|0:0,0,0:-1]"}
Color   Schlafzimmer_Links_Unten_FADE_NIGHT            {dmx="CHANNEL[16/3], ON[FADE|0:0,0,0:-1]"}
Color   Schlafzimmer_Spiegel_FADE_NIGHT                {dmx="CHANNEL[19/3], ON[FADE|0:0,1,1:-1]"}
Color   Schlafzimmer_Bett_FADE_NIGHT                {dmx="CHANNEL[22/3], ON[FADE|0:0,1,1:-1]"}

Color   Schlafzimmer_Rechts_Oben_FADE_DAWN            { dmx="CHANNEL[1/3], ON[FADE|1000:0,0,0:100|1000:255,255,255:-1]"}
Color   Schlafzimmer_Rechts_Unten_FADE_DAWN            { dmx="CHANNEL[4/3], ON[FADE|1000:255,255,255:100|2000:0,0,0:-1]"}
Color   Schlafzimmer_Mitte_Oben_FADE_DAWN            { dmx="CHANNEL[7/3], ON[FADE|1000:0,0,0:100|1000:255,255,255:-1]"}
Color   Schlafzimmer_Mitte_Unten_FADE_DAWN            {dmx="CHANNEL[10/3], ON[FADE|1000:255,255,255:100|2000:0,0,0:-1]"}
Color   Schlafzimmer_Links_Oben_FADE_DAWN            {dmx="CHANNEL[13/3], ON[FADE|1000:0,0,0:100|1000:255,255,255:-1]"}
Color   Schlafzimmer_Links_Unten_FADE_DAWN            {dmx="CHANNEL[16/3], ON[FADE|1000:255,255,255:100|2000:0,0,0:-1]"}
Color   Schlafzimmer_Spiegel_FADE_DAWN                {dmx="CHANNEL[19/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Bett_FADE_DAWN                    {dmx="CHANNEL[22/3], ON[FADE|1000:0,0,0:-1]"}

Color   Schlafzimmer_Rechts_Oben_FADE_MORNING        { dmx="CHANNEL[1/3], ON[FADE|10000:255,100,0:40000|10000:255,255,255:-1]"}
Color   Schlafzimmer_Rechts_Unten_FADE_MORNING        { dmx="CHANNEL[4/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Mitte_Oben_FADE_MORNING        { dmx="CHANNEL[7/3], ON[FADE|10000:255,100,0:40000|10000:255,255,255:-1]"}
Color   Schlafzimmer_Mitte_Unten_FADE_MORNING        {dmx="CHANNEL[10/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Links_Oben_FADE_MORNING        {dmx="CHANNEL[13/3], ON[FADE|10000:255,100,0:40000|10000:255,255,255:-1]"}
Color   Schlafzimmer_Links_Unten_FADE_MORNING        {dmx="CHANNEL[16/3], ON[FADE|1000:0,0,0:-1]"}
Color   Schlafzimmer_Spiegel_FADE_MORNING            {dmx="CHANNEL[19/3], ON[FADE|1000:0,0,0:-1]"}                     
Color   Schlafzimmer_Bett_FADE_MORNING                {dmx="CHANNEL[22/3], ON[FADE|10000:255,100,0:40000|10000:255,255,255:-1]"}      

These are my Sensors:

Schlafzimmer_Fenster_Alarm       //Turns 1 if there is movement in the sleeping room
Schlafzimmer_Fenster_Alarm_Anz   //Amount of alarms in the last 15 min
Schlafzimmer_Fenster_Helligkeit  //Brightness in the sleeping room
cSchlafzimmerBegewungsmelder     //possibility to swith the movement-sensor off
Flur_Schlafzimmertuere_Tuer      //Door-Contact

And here comes my rule:

// Imports
import org.openhab.core.library.types.*
import org.openhab.core.persistence.*
import org.openhab.model.script.actions.*

import java.util.HashMap
import java.util.LinkedHashMap

import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock

//Config
val HashMap< String, LinkedHashMap<String, Object>> FadeConfig = newHashMap(
    
    "Spiegel" -> (newLinkedHashMap(
            "FADE_OFF"        -> Schlafzimmer_Spiegel_FADE_OFF,
            "FADE_DAY"        -> Schlafzimmer_Spiegel_FADE_DAY,
            "FADE_NIGHT"    -> Schlafzimmer_Spiegel_FADE_NIGHT,
            "FADE_DAWN"        -> Schlafzimmer_Spiegel_FADE_DAWN,
            "FADE_MORNING"    -> Schlafzimmer_Spiegel_FADE_MORNING,
            "ACTIVE"        -> "" as String ))
        as LinkedHashMap<String, Object>,

    "Bett" -> (newLinkedHashMap(
            "FADE_OFF"        -> Schlafzimmer_Bett_FADE_OFF,
            "FADE_DAY"        -> Schlafzimmer_Bett_FADE_DAY,
            "FADE_NIGHT"    -> Schlafzimmer_Bett_FADE_NIGHT,
            "FADE_DAWN"        -> Schlafzimmer_Bett_FADE_DAWN,
            "FADE_MORNING"    -> Schlafzimmer_Bett_FADE_MORNING,
            "ACTIVE"        -> "" as String ))
        as LinkedHashMap<String, Object>,

    "Schlafzimmer_Rechts_Oben" -> (newLinkedHashMap(
            "FADE_OFF"        -> Schlafzimmer_Rechts_Oben_FADE_OFF,
            "FADE_DAY"        -> Schlafzimmer_Rechts_Oben_FADE_DAY,
            "FADE_NIGHT"    -> Schlafzimmer_Rechts_Oben_FADE_NIGHT,
            "FADE_DAWN"        -> Schlafzimmer_Rechts_Oben_FADE_DAWN,
            "FADE_MORNING"    -> Schlafzimmer_Rechts_Oben_FADE_MORNING,
            "ACTIVE"        -> "" as String ))
        as LinkedHashMap<String, Object>,

    "Schlafzimmer_Rechts_Unten" -> (newLinkedHashMap(
            "FADE_OFF"        -> Schlafzimmer_Rechts_Unten_FADE_OFF,
            "FADE_DAY"        -> Schlafzimmer_Rechts_Unten_FADE_DAY,
            "FADE_NIGHT"    -> Schlafzimmer_Rechts_Unten_FADE_NIGHT,
            "FADE_DAWN"        -> Schlafzimmer_Rechts_Unten_FADE_DAWN,
            "FADE_MORNING"    -> Schlafzimmer_Rechts_Unten_FADE_MORNING,
            "ACTIVE"        -> "" as String ))
        as LinkedHashMap<String, Object>,

    "Schlafzimmer_Mitte_Oben" -> (newLinkedHashMap(
            "FADE_OFF"        -> Schlafzimmer_Mitte_Oben_FADE_OFF,
            "FADE_DAY"        -> Schlafzimmer_Mitte_Oben_FADE_DAY,
            "FADE_NIGHT"    -> Schlafzimmer_Mitte_Oben_FADE_NIGHT,
            "FADE_DAWN"        -> Schlafzimmer_Mitte_Oben_FADE_DAWN,
            "FADE_MORNING"    -> Schlafzimmer_Mitte_Oben_FADE_MORNING,
            "ACTIVE"        -> "" as String ))
        as LinkedHashMap<String, Object>,

    "Schlafzimmer_Mitte_Unten" -> (newLinkedHashMap(
            "FADE_OFF"        -> Schlafzimmer_Mitte_Unten_FADE_OFF,
            "FADE_DAY"        -> Schlafzimmer_Mitte_Unten_FADE_DAY,
            "FADE_NIGHT"    -> Schlafzimmer_Mitte_Unten_FADE_NIGHT,
            "FADE_DAWN"        -> Schlafzimmer_Mitte_Unten_FADE_DAWN,
            "FADE_MORNING"    -> Schlafzimmer_Mitte_Unten_FADE_MORNING,
            "ACTIVE"        -> "" as String ))
        as LinkedHashMap<String, Object>,

    "Schlafzimmer_Links_Oben" -> (newLinkedHashMap(
            "FADE_OFF"        -> Schlafzimmer_Links_Oben_FADE_OFF,
            "FADE_DAY"        -> Schlafzimmer_Links_Oben_FADE_DAY,
            "FADE_NIGHT"    -> Schlafzimmer_Links_Oben_FADE_NIGHT,
            "FADE_DAWN"        -> Schlafzimmer_Links_Oben_FADE_DAWN,
            "FADE_MORNING"    -> Schlafzimmer_Links_Oben_FADE_MORNING,
            "ACTIVE"        -> "" as String ))
        as LinkedHashMap<String, Object>,

    "Schlafzimmer_Links_Unten" -> (newLinkedHashMap(
            "FADE_OFF"        -> Schlafzimmer_Links_Unten_FADE_OFF,
            "FADE_DAY"        -> Schlafzimmer_Links_Unten_FADE_DAY,
            "FADE_NIGHT"    -> Schlafzimmer_Links_Unten_FADE_NIGHT,
            "FADE_DAWN"        -> Schlafzimmer_Links_Unten_FADE_DAWN,
            "FADE_MORNING"    -> Schlafzimmer_Links_Unten_FADE_MORNING,
            "ACTIVE"        -> "" as String ))
        as LinkedHashMap<String, Object>
)


var HashMap<String, Object> FadeData = newHashMap(
    "TIMER"            ->    null as Timer,
    "TIMEOUT_IST"    -> 0 as Integer,
    "TIMEOUT_NEU"    -> 0 as Integer,
    "SOURCE_IST"    -> "woher kam die Anforderung" as String,
    "SOURCE_NEU"    -> "woher kommt die Anforderung" as String,
    "FADE_IST"        -> "aktueller Fade" as String,
    "FADE_NEU"        -> "neuer Fade" as String
) as HashMap





val org.eclipse.xtext.xbase.lib.Functions$Function2 ExecuteFade = [
    HashMap< String, LinkedHashMap<String, Object>> fade_config,
    HashMap< String, Object> fade_data|
    
    var timer        = fade_data.get( "TIMER") as Timer
    val new_fade    = fade_data.get( "FADE_NEU")

    val int timeout_neu    = fade_data.get( "TIMEOUT_NEU") as Integer
    var int timeout_ist    = fade_data.get( "TIMEOUT_IST") as Integer
    

    val fade_items_keys = fade_config.keySet()

    //Plausi
    if( new_fade != "FADE_OFF" && new_fade != "FADE_DAY" && new_fade != "FADE_NIGHT" && new_fade != "FADE_DAWN" && new_fade != "FADE_MORNING") {
        logDebug( "Schlafzimmer", String::format( "Wrong fade parameter: %s", new_fade))
        return false
    }


    //Wenn der Dimm-Anforderer der Gleiche ist darf er auch den Timeout ändern
    if( fade_data.get( "SOURCE_NEU") == fade_data.get( "SOURCE_IST") && timeout_ist != timeout_neu) {
        timeout_ist = timeout_neu
        logDebug( "Schlafzimmer", String::format( "Change timeout to %3ds", timeout_neu))
        
        //Timeout wurde ja geändert --> speichern!
        fade_data.put( "TIMEOUT_IST", timeout_ist)
    }


    //Wenn wir bewusst ausschalten macht natürlich ein Timer keinen sinn
    if( new_fade != "FADE_OFF") {
        
        //Timer zum Ausschalten schieben oder erstellen
        if( timer != null) {
            logDebug( "Schlafzimmer", String::format( "Reschedule timer: %3ds", timeout_ist))
            timer.reschedule( now.plusSeconds(timeout_ist))
        }
        else {
            logDebug( "Schlafzimmer", String::format( "Create FADE_OFF timer: %3ds", timeout_neu))
            
            //Timer, der alles aus schaltet
            timer = createTimer(now.plusSeconds(timeout_neu)) [|
                fade_data.put( "FADE_IST", "FADE_OFF")
                logDebug( "Schlafzimmer", String::format( "Execute Fade : %s", "FADE_OFF"))
            
                fade_items_keys.forEach[ key |
                    
                    //Alle Einträge der Hashmap durchnudeln
                    var LinkedHashMap<String, Object> items = fade_config.get(key)
                    var fade_item = items.get( "FADE_OFF") as org.openhab.core.items.GenericItem
                    
                    //Nur wenn der Schalter auf An ist senden wir den fade!
                    if( cSchlafzimmerBegewungsmelder.state == ON) sendCommand(fade_item, ON)
                ]
                fade_data.put( "TIMER", null) 
            ]
            
            //Speichern!
            fade_data.put( "TIMER", timer) 
            fade_data.put( "TIMEOUT_IST", timeout_neu) 
            fade_data.put( "SOURCE_IST", fade_data.get( "SOURCE_NEU"))
        }
    }
    else {
        //Falls noch einer läuft -> abbrechen!
        if( timer != null) {
            timer.cancel()
            fade_data.put( "TIMER", null) 
        }
    }



    //Wenn kein Fade gemacht wird, setzen wir nur den Timer zurück
    if( fade_data.get( "FADE_IST") == new_fade) return false

    fade_data.put( "FADE_IST", new_fade)

    //Fade-Code
    logDebug( "Schlafzimmer", String::format( "Execute Fade : %s", new_fade))

    fade_items_keys.forEach[ key |
        
        //Alle Einträge der Hashmap durchnudeln
        var LinkedHashMap<String, Object> items = fade_config.get(key)
        var fade_item = items.get( new_fade) as org.openhab.core.items.GenericItem
        
        sendCommand(fade_item, ON)
    ]
    
    return false
]




val org.eclipse.xtext.xbase.lib.Functions$Function3 IdentifyFade = [
    HashMap< String, LinkedHashMap<String, Object>> fade_config,
    HashMap< String, Object> fade_data,
    String caller|

    var Number    Helligkeit_Zimmer    = Schlafzimmer_Fenster_Helligkeit.state    as DecimalType
    var int    timeout                 = 60

    //Anzahl Bewegungen
    val int movement = (Schlafzimmer_Fenster_Alarm_Anz.state as DecimalType).intValue


    logDebug( "Schlafzimmer", String::format( "Fade Request from %s! Brightness: %s Movement: %d", caller, Helligkeit_Zimmer, movement))


    //------------------------------------------------------------------------
    //Timeoutbestimmtung
    if( caller == "Alarm") {
        //Timeout bei Bewegungsmelder dynamisch
        if( movement <= 2)        timeout =  1 * 60
        else if( movement <= 6)    timeout =  5 * 60
        else                     timeout = 10 * 60
    }
    else {
        logDebug( "Schlafzimmer", String::format( "Türe: %s", Flur_Schlafzimmertuere_Tuer.state))
        
        //Wenn wir die Türe öffnen ist der Timeout 10 min
        if( Flur_Schlafzimmertuere_Tuer.state == OPEN)    timeout = 10 * 60
        else                                            timeout =  1 * 45 
    }


    val int    Time_Hour        = now.getHourOfDay()
    val int dow                = now.getDayOfWeek()
    var int start_morning    = 5
    var int start_day        = 8
    var int start_night        = 22
    
    //Freitag + Samstag -> Abends länger hell
    if( dow == 4 || dow == 5) {
        start_night = 24
    }
    
    //Samstag + Sonntag -> Morgens später hell
    if( dow == 5 || dow == 6) {
        start_morning    = 8
        start_day         = 10
    }

    
    //Auswählen, was wir machen
    var String new_mode_tmp = "FADE_OFF"

    if( Time_Hour < start_morning || Time_Hour >= start_night) {
        new_mode_tmp = "FADE_NIGHT"
    }
    else if ( Time_Hour < start_day) {
        new_mode_tmp = "FADE_MORNING"
    }
    else if ( Time_Hour < start_night) {
        if( Helligkeit_Zimmer < 15)    new_mode_tmp = "FADE_DAWN"
        else                        new_mode_tmp = "FADE_DAY"
    }


    //Fade-Data speichern
    fade_data.put( "SOURCE_NEU", caller)
    fade_data.put( "FADE_NEU", new_mode_tmp)
    fade_data.put( "TIMEOUT_NEU", timeout)
    
    return false
]



//Damit wir nicht multithread auf die hashmaps zugreifen!
var Lock mylock = new ReentrantLock()

rule "Schlafzimmer Alarm"
when
    Item Schlafzimmer_Fenster_Alarm changed from 0 to 1
then
    //Bewegungsmelder ist aus
    if( cSchlafzimmerBegewungsmelder.state != ON) return false

    mylock.lock()

    IdentifyFade.apply(    FadeConfig, FadeData, "Alarm")
    ExecuteFade.apply(    FadeConfig, FadeData)

    mylock.unlock()
end


rule "Schlafzimmer Türe changed"
when
    Item Flur_Schlafzimmertuere_Tuer changed
then
    //Bewegungsmelder ist aus
    if( cSchlafzimmerBegewungsmelder.state != ON) return false

    mylock.lock()

    IdentifyFade.apply(    FadeConfig, FadeData, "Türe")
    ExecuteFade.apply(    FadeConfig, FadeData)

    mylock.unlock()
end

I’m currently on vacation and opening only have my phone so I probably won’t get to really look at this until I get back on Tuesday. I just don’t want you to think I’m ignoring you. I look forward to the challenge though.

Rich

1 Like

Here is my first stab at this. I’m mainly just typing stuff in here and I’ve skipped a few points of functionality for clarity and because it didn’t help me illustrate the approach. In other words you will have some more work to do to convert the below to working code.

The language barrier is slowing me down but not stopping me (my German isn’t even good enough to read the German language books we have for our two-year-old).

First, I want to make sure I understand what is going on here.

You have two rules, one which triggers when movement is detected in the sleeping room and another that gets triggered when a change in the door contact is detected. The primary difference between the two appears to be the timeout that is applied.

The IdentifyFade lambda calculates a timeout and determines which time of day fade should be applied.

The ExecuteFade lambda does some error checking, sets/reschedules a timer to turn off the lights after the number of seconds calculated in IdentifyFade. Then it applies the appropriate fade for each light based on the time of day calculated in IdentifyFade. There is some book keeping code in there that I’m going to skip over but it is clear you will probably need to keep around your FadeData, at least for this first pass.

There appears to be things in the rules that are not being used as well such as the “ACTIVE” key in the sub linkedHashMaps in FadeConfig so I’ll assume they are not needed.

Below are a couple of initial thoughts. I did a copy and paste of the rules and items and there are so many errors listed in Designer that I can’t say for sure if what I type below will work. These are in no particular order, though 1 and 2 help to facilitate 3:

  1. A caller String is passed to IdentifyFade and it appears to only be used to calculate the timeout. I would move the “Timeoutbestimmtung” if/else code to the rules themselves and pass the calculated timeout to the lambda. You might need to modify your logDebug statement above that to make sense. For me, I have a general approach that if the lambda does something different based on what rule calls it, the code probably belongs in the rule and not in the lambda. This will centralize the parts of the code that are different based on the rules and it is the first step in my elimination of the IdentifyFade lambda entirely.

  2. Rather than calculate the time of day fades in a lambda, consider defining a TimeOfDay item that gets populated based on time based rules. See below.

I would argue that it is a little easier to read and maintain. If at some point you want to base these rules on the position of the sun using Astro rather than a set clock time, all you have to do is create a new Astro switch and plop it into the when clauses. This collapses close to 30 lines of code to 22 simpler (in my opinion) lines of code and with 1 lets us get rid of one of the lambdas entirely.

  1. Put all your fades into one big group and use “members.filter” to find those that end with the desired fade string.

All three suggestions are mostly implemented below. I took shortcuts and left stuff out where I am not really changing the code based on these three suggestions.

NOTE: the code snippets below assume all three suggestions are being implemented. Also, since Designer isn’t working for me (see above) I can’t attest to whether any of this is valid code, but the concept is sound.

Items

Group gSchlafzimmer

Color Schlafzimer_Rechts_Oben_FADE_OFF (gSchlafzimmer) {dmx ...
Color Schlafzimer_Rechts_Unten_FADE_OFF (gSchlafzimmer) {dmx ...
// Each fade belongs to the group

String Schlafzimer_TimeOfDay

Rules

// includes, NOTE: GroupItem is in org.openhab.core.items, not org.openhab.core.library.types with SwitchItem et al

val Functions$Function4 ExecuteFade = [HashMap<String, Object> FadeData,
                                                             GroupItem fades,
                                                             String new_fade,
                                                             Integer timeout |
    //Plausi
    if( new_fade != ...) // probably a good idea to check because if persistence isn't working new_fade may be null or undefined until a time of day rule triggers. You can add a System Startup rule to calculate it to avoid this problem.

    // Wenn der Dimm-Anforder ...
    // I'll leave this and similar stuff as an exercise for the student, probably still need some of the data in FadeData

    if( new_fade != "FADE_OFF") { // with what I have here this if clause is not needed. I've left it in case it is needed for your bookkeeping code I've not included
        if(FadeData.get("TIMER") != null){
            timer.reschedule(now.plus(timeout))
        } else {
            FadeData.put("TIMER", createTimer(now.plusSeconds(timeout)) [|
                fades?.members.filter(fade|fade.name.endswith("FADE_OFF")).forEach(fade | fade.sendCommand(ON))
                FadeData.put("TIMER", null)
            ]
        }
    }  else {
        if(FadeData.get("TIMER") != null) {
            FadeData.get("TIMER").cancel
            FadeData,put("TIMER", null)
        }
    }

    //Wenn kein...
    // More book keeping stuff I'll leave to you

    // Execute Fade
    fades?.members.filter(fade|fade.name.endswith(new_fade)).forEach(fade|fade.sendCommand(ON))
]

val Lock mylock = new ReentrantLock() // use val if mylock is never to be reassigend
val HashMap<String, Object> FadeData = new HashMap ( "TIMER" -> null as Timer)

rule "Schlafzimmer Alarm"
when
    Item Schlafzimmer_Fenster_Alarm changed from 0 to 1
then
    if(cSchlafzimmerBegewungsmelder.state != ON) return false
    var timeout = 10*60
    if(movement <= 2) timeout = 1*60
    else if(movement <= 6) timeout = 5*60
    mylock.lock
    ExecuteFade.apply(FadeData, gSchlafzimmer, Schlafzimmer_TimeOfDay.state as String, timeout)
    mylock.unlock
end

rule "Schlafzimmer Alarm"
when
    Item Schlafzimmer_Fenster_Alarm changed from 0 to 1
then
    var timeout = 10*60
    if(Flur_Schlafzimmertuere_Tuer.state == OPEN) timeout = 1 * 45
    
    mylock.lock
    ExecuteFade.apply(FadeData, gSchlafzimmer, Schlafzimmer_TimeOfDay.state as String, timeout)
    mylock.unlock
end

rule "Morning"
when
    Time cron "0 0 5 * MON-THU *" or
    Time cron "0 0 8 * FRI,SAT,SUN"
then
    Schlafzimer_TimeOfDay.sendCommand("FADE_MORNING")
end

rule "Day"
when
    Time cron "0 0 8 * MON-THU *" or
    Time cron "0 0 10 * FRI,SAT,SUN *"
then
    Schlafzimer_TimeOfDay.sendCommand("FADE_DAY")
end

rule "Night"
when
    Time cron "0 0 22 * MON-THU *" or
    Time cron "0 0 24 * FRI,SAT,SUN *"
then
    Schlafzimer_TimeOfDay.sendCommand("FADE_NIGHT")
end
// NOTE: I saw a recent posting that there might be a bug that prevents you from using two different Time triggers on the same rule so if the above doesn't work, you may need to split it up.

It is a lot to take in. Let me know if you have any questions.

Rich

EDIT: formatting and added a comment to the code for clarification

Just to see I did a quick line count. This brings your code from around 310 lines down to 130 lines, give or take depending on how you reimplement some of the bookkeeping code I didn’t include (I added what you have line for line to my code just to generate the line count).

Rich

Hi,
sorry that I didn’t reply sooner. I have been quite busy and on the road.
First of all, thank you for your effort and time you put into this.
It helped me to learn some new tricks.

This is right, but I somehow struggled to define it with just the items and the designer gives me errors (I have no idea why).

You have the correct basic understanding of what is happening here.
But additionally (which I forgot to mention in my description - very sorry) there is a the following feature: When the fade was initiated by the alarm, only the alarm is allowed to change the timeout. Any movement on the door will just start the timeout again.
This is why I wrapped the timeout calculation in a lambda, because I need to know who initiated the fade anyway.
But I really like your suggestion nr 2. I have done something similar in my living room, but there I have to check every hour (for different reasons). But what you have suggested is far more elegant and better readable. I think I will change the fade determination accordingly, but still access this item in the lambda. It is easier to check if all keys for the the next fade are set correctly. I have to think of a startup rule though.

When I stated creating this rule I wasn’t sure about my fades and still changed them a lot. This is why I put them on startup in the hashmap. My idea was to get errors when the rule is loaded and not when the fade is executed. With your approach I won’t get errors, but maybe a fade won’t get executed. I created an issue for a function which allows to check whether an item exists, but unfortunately I doubt it’ll get implemented. :frowning:
I guess now that the fades are pretty static I could use your group/filter approach. But then it is only a little bit shorter than iterating over the hashmap. So I am not sure if I’ll go for the changes.

Once again, thank you for your time and effort. I really learned something. :smile:

Interesting. Can you elaborate on what error Designer is giving you when you only have Items in your hashmap? It sounds like a bug if you are forced to put something else in there that you won’t use. I’ve only used Items as the Key in my hashmaps , usually with a Timer as the object so I’ve not encountered this problem and I’m curious.

I have a similar feature in my lights rules. I let the lights turn on or off based on what Yahoo says the weather is like (if it is cloudy they come on, sunny they go off). However, if someone manually turns on or off the light it disables the weather rule for the rest of the day. I solved the problem though a rule which gets called every time a light is toggled and checks whether it was manually toggled, a Map<SwitchItem, Boolean> which gets set to true when the switch is toggled manually (either through OH or by physically pressing the switch), and a that lambda checks the override before turning on or off the light. At sunset, when the weather rule is turned off anyway, the override booleans are reset.

With this approach the lambda doesn’t really need to know who called it and whether or not a command is overridden is controlled in the rules, leaving the lambda completely independent of the rules. I don’t know if this is better or worse than other approaches but I’m happy and I like to keep my code chunks as generic as possible to maximize readability and re-usability. However, unlike you who seems to have his lighting pretty squared away, I am still adding lights and I like not needing to touch the rules file to add a light and get it to behave as I want.

I’ve included my full lights rules here as it shows what I did better than I can explain and it might prove useful to other readers.

Items

Group:Switch:OR(OFF,ON) gLights "All Lights" <light>
Group gWeatherLights "Lights controlled by weather conditions and twilight" <light>
Group gOfftimerLights "Off Timer Lights" <light>
Group gSunsetTimerLights "Sunset on Lights" <light>

Switch S_L_Front "Front Room Lamp"   <light> (gLights, gOffTimerLights, gWeatherLights)     {zwave...
Switch S_L_Family "Family Room Lamp" <light> (gLights, gOffTimerLights, gWeatherLights)     {zwave...
Switch S_L_Porch "Front Proch"       <light> (gLights, gOffTimerLights, gSunsetTimerLights) {zwave...
Switch S_L_All "All Lights"          <light>

Rules

import org.openhab.core.types.*
import org.openhab.core.library.items.*
import org.eclipse.xtext.xbase.lib.*
import java.util.Map
import java.util.Set

val String TIMER = "TIMER"
val String WEATHER = "WEATHER"
val String MAUNAL = "MANAUAL"
var String whoCalled = ""
var boolean day = true
val Map<SwitchItem, Boolean> overridden = newHashMap

val Set<String> cloudyIds = newImmutableSet("0",  "1",  "2",  "3",  "4",  "5",  "6",  "7",  "8",
                             "9", "10", "11", "12", "13", "14", "15", "16", "17",
                             "18", "19", "20", "26", "28", "35", "41", "43", "45",
                             "46", "30", "38")

val Functions$Function3 applySwitch = [State state, boolean override, SwitchItem light |
    if(state != light.state) {
        if(!override) sendCommand(light, state.toString)
    }
]

rule "Lights System Started"
when
    System started
then
    whoCalled = MANUAL
    gLights?.members.forEach[light |  overridden.put(light as SwitchItem, false) ]
end

// Triggered when any light in gLights receives a command, either through OH or from the switch
rule "Light triggered"
when
    Item gLights received update
then
    Thread::sleep(250) // give lastUpdate time to populate
    if(whoCalled == MANUAL)  overridden.put(gLights?.members.sortBy[lastUpdate].last as SwitchItem, true)
end

rule "All Lights"
when
    Item S_L_All received command
then
    gLights?.members.forEach(item | sendCommand(item, S_L_All.state.toString))
end

rule "Lights Twilight"
when
    Item Twilight_Event received update
then
    whoCalled = TIMER
    day = false // deactivates the weather rule
    gWeatherLights?.members.forEach[light | 
        overridden.put(light as SwitchItem, false)
        applySwitch.apply(ON, false, light)
    ]
end

rule "Lights Sunset"
when
    Item Sunset_Event received update
then
    whoCalled = TIMER
    gSunsetTimerLights?.members.forEach[light |
        overridden.put(light as SwitchItem, false)
        applySwitch.apply(ON, false, lights)
    ]
end

rule "Lights Bedtime"
when
    Time cron "0 0 23 * * ? *"
then
    whoCalled = TIMER
    gOffTimerLights?.members.forEach[light |
        overridden.put(light as SwitchItem, false)
        applySwitch(OFF, false, light)
    ]
end

rule "Weather Lights On"
when
    Item Contition_Id changed
then

if(day) {
    var State state = OFF
    if (cloudyIds.contains(Condition_Id.state)) state = ON

    whoCalled = WEATHER
    val i = gWeatherLights?.members.iterator
    while(i.hasNext){
        val light = i.next
        if(overridden.get(light) == null) overridden.put(light as SwitchItem, false)
        applySwitch.apply(state, overridden.get(light).booleanValue, light)
    }
    Thread::sleep(500)
    whoCalled = MANUAL
}
end

This is indeed a limitation of my approach. My way to work with this limitation is to create (sometimes temporarily) a switch item to trigger my rule and I just manually trigger it to make sure everything is as I want it to be as a test rather than relying on an error in the logs. If I do have an error it is usually because I have a typo in the group I’ve assigned an Item to and I would much rather the rest of the rules and lights work than have the whole system crash. The benefit of being able to add, remove, and modify the behavior of an Item without having to touch the rules (and potentially mucking things up) plus being able to have very easy to read and maintain rules when I do need to touch them is more than worth needing to do this part manually to me.

It is a different coding philosophy closer to weakly typed programming languages (Ruby/JavaScript/etc.) than strongly typed languages (C/C++/Java/C#/etc.). The former prioritizes flexibility over compile-time error checking. This flexibility comes at the cost of most errors only being detected at runtime. The latter prioritizes early error detection over the flexibility offered by weakly typed languages. XTend as a language is closer to a weakly typed language than a strongly typed language so if you want the kind of error checking you are looking for you need to code it yourself, as you have found.

Assuming your item is part of a Group, you can filter on the name you care about. This might get you by. I agree it won’t get implemented in OH1 but maybe in OH2.

if(group?.members.filter(s|s.name("Name")).empty){
    // Item doesn't exist
}

Even having studied your rules I’m not sure I understand why you need to do this. I guess what you are looking for is to have your code be static and crash if you have an error like one of your switches not part of a rule but it should be? This is probably a difference in coding philosophy again. I personally would much rather have the missing light not turn on than have the whole rule set crash so it wouldn’t occur to me to want to do this, even back when I was implementing everything with Maps instead of Groups.

It is actually a lot shorter if you count the code that populates the hashmap you are iterating over. I’d also argue that the code is easier to read and maintain because the declarative stuff (i.e. populating the hashmap) is all in one place (Items files) and the executive stuff is all in one place (Rules files). But we have different coding philosophies and you want some load time checks to be performed that you can’t really do with my approach so you would probably have the hashmap stuff in anyway to get your load time error checks

This was fun and I learned a lot. Even retyping in my lights rules above caused me to think of a few more ways to make them simpler and more concise.

Thank you!

Rich

[Edits: some formatting and a couple errors in the rules]