Making complex Rules reusable or simpler

There are a number of design pattern tutorials that should help:

Here are some thoughts:

  1. If your concern is running a bunch of rules all at the same time, instead of triggering them all based on every Azimuth change, create a single rule that monitors the Azimuth that then sets a flag (String Item like in Time of Day, Switch Items, you can even have that rule call other rules directly if using other rules languages). Then you can trigger your rules only at the times where the Azimuth has already been shown to be right.
    For a very simple example, I have an isCloudy Item which is set to ON when the cloudiness is above 50% and set to OFF when it’s below 50%. That Switch then triggers other rules or is checked by other rules to see if they should do something different based on cloudiness. If I decide I want to only worry if it’s 75% cloudy, I only need change that one rule.
    This will reduce how many rules you have running all the time and let you separate and consolidate the code that needs to decide whether the conditions are such that something should happen.
rule "Azimuth state machine"
when
    Item Elevation changed
then
    var newState = "STAY"
    switch(Azmuth.state) {
        case 6|°: newState = "EL_START"
        case 70|°: newState = "ZIMMER_AZ_START"
        case 230|°: newState = "ZIMMER_AZ_END"
        case 260|°: newState = "DF_AZ_END"
    }
    if(newState != "STAY" && Azumth_State.state != newState) {
        Azmuth_State.sendCommand(newState)
    }
end

Now you can trigger your rule using changes to Azmuth_State which will only change when it transitions from one state to another instead of every angle increment. And your conditions inside the rule become as simple as if(Azumth_State.state == "EL_START") instead of needing to check the raw numbers and check if the angle is between two states and the like.
NOTE: the states I listed are almost certainly not what you’d want, they are just for illustration purposes. I’m also assuming that Astro reports the Azmuth in whole number integer degrees. If it doesn’t you’ll need to use if statements to see if the Azmuth is greater than your threshold instead of the switch.

  1. Pay close attention to How to Structure a Rule (link above). This rule seems to follow a pattern that that design pattern was made for: a series of if/else statements that command the same Items in slightly different ways. Instead of repeating that over and over, set some variables to a default and your if/else if statements will modify those variables as necessary. Only at the end do you actually command the Item.
    This will greatly reduce the number of lines of code and consolidates where the commands are already sent which will make the rules easier to maintain.

  2. Long if conditions are hard to maintain. However, often you can greatly simplify things if you turn the conditions around. For example, in your “LamelleneffnugZ2” rule, if DG_Zimmer2_Beschattungsautomatik or DG_Zimmer1_LamelleAufAuto are OFF or DG_Zimmer2_Blind < 80, the rule does nothing. So put that test up front and the rule becomes:

then
    if(DG_Zimmer2_Beschattungsautomatik.state != ON 
       || DG_Zimmer1_LamelleAufAuto.state != ON
       || DG_Zimmer2_Blind <= 80) {
        // Nothing to do, exit
        return;
    }

    if(DG_Zimmer2_Presence_Long.state == ON && DG_Zimmer2_Lamella.state >74){
        DG_Zimmer2_Lamella.sendCommand(DG_Zimmer2_OpeningWidthOnEnter.state)
    }
    else {
        DG_Zimmer2_Lamella.sendCommand(90)
        DG_Zimmer2_ShadeManuallyOperated.sendCommand(OFF)
    }
end
  1. You will notice some differences in coding style between my version above and your version. Coding style can go a long way towards making code easier to read. For example, you’ve a lot of extraneous ( ) around your conditions that are simply not needed. It would be like writing math with parens around every number: (2) + (3) + (4). That’s not so bad when you’ve got a simple equation but it really gets in the way when things get more complex. See Coding Conventions in Rules for some additional advice.

  2. Some minor complexity is added because you use the sendCommand action instead of the method on the Item. When you use the action, the command sent needs to be a String or something Rules DSL can figure out how to convert to a String. When you use the method, it is much better at handling conversions for you which will let you eliminate a bunch of the as Number type statements.

  3. Use Associated Items to create one rule that can handle all of your “DetectManual Operation” rules. For example:

import org.eclipse.smarthome.model.script.ScriptServiceUtil

rule "Detect Manual Operation"
when
    Member of Zimmer changed
then
    val beschattungsautomatic = ScriptServiceUtil.getItemRegistry.getItem(triggeringItemName.replace('Blind', 'Beschattungsautomatik')
    val shadeSupressManualDetection = ScriptServiceUtil.getItemRegistry.getItem(triggeringItemName.replace('Blind', 'ShadeSupressManualDetection')
    if(beschattungsautomatic.state == ON && shadeSupressMaunalDetection.state != ON) {
        sendCommand(triggeringItemName.replace('Blind', 'ShadeManuallyOperated'), ON)
    }
end

Put all your Blind Items into that Zimmer Group and this one rule will replace all the rest that you have in place for your manual detection across all your floors. Use the same approach and you can also consolidate your Lamellen rules too. It’s much much easier to maintain code that doesn’t get repeated all over the place. And as you can see, the one rule to replace them all isn’t really any more complicated than the originals.
If you get clever with your naming conventions, you might even be able to replace all your DetectManual rules with just a single rule. You just need to figure out how to identify the parts of the triggering Item’s name that is the same for all the associated Items and how to replace the rest with what’s different.
The same approach can be used to replace all the DG_AutoShade rules with one rule for all.

rule "AutoShade Command"
when
    Member of ShadeControl received command
then
    val beschattungsautomatik = ScriptServiceUtil.getItemRegistry.getItem(triggeringItemName.replace('ShadeControl', 'Beschattungsautomatik')

    if(beschattungsautomatik != ON) {
        return;
    }

    sendCommand(triggeringItemName.replace('ShadeControl', 'ShadeSupressManualDetection'), ON)
    sendCommand(triggeringItemName.replace('ShadeControl', 'Dachfenster_Roll'), 
                                           if(receivedCommand == 1) UP else DOWN)
end

The code above would replace those three rules in the above plus any similar rules for your other floors.
Notice the use of the ternary operator to simplify the if/else and this is another example of exiting immediately if there’s nothing to do.

  1. Don’t fight against QuantityTypes. The code is easier to read and will actually work closer to what you want if you use them . Make DG_Zimmer1_El_Start an Angle using var DG_Zimmer1_EL_Start = 6 | °. I’ve shown this above in 1 also.

  2. If you put your constants into Items, not only can you use Associated Items to access them, you can adjust them through your UI. Just make sure they have restoreOnStartup.

  3. Pulling it all together:

import org.eclipse.smarthome.model.script.ScriptServiceUtil

val heatingTolerance = 1|°C // Toleranz(Hysterese) für Entscheidung  Beschattung/Heizung Notwendig (in°C)
val solarLightmax = 25500 // 2550 Is a good value for Summer maybe winter is different?
// If this is an Item you can create a rule that triggers on changes of the season to adjust it

rule "DG_HeatingOrCooling"
when
    Member of Beschattungautomatik changed or
    Member of SetTemp changed or
    Azimuth_State changed
then
    // Get the associated Items
    val rollorShutterNameParts = triggeringItemName.split('_')
    val rollorShutterName = rollorShutterNameParts.get(0) + '_' + rollorShutterNameParts.get(1)
    val setTemp = SetTemp.members.findFrist[ i | i.name = rollorShutterName+'_SetTemp'] // if you have a Group you don't need ScriptServiceUtil
    val actTemp = ScriptServiceUtil.getItemRegistry.getItem(rollorShutterName+'_ActTemp')
    val start = ScriptServiceUtil.getItemRegistry.getItem(rollorShutterName+'_Active') // holds a String matching one of the state of Azimuth_Start
    val blind = ScriptServiceUtil.getItemRegistry.getItem(rollorShutterName+'_Blind')
    val manuallyOperated = ScriptServiceUtil.getItemRegistry.getItem(rollorShutterName+'_ShadeManuallyOperated')
    val shadeControl = ScriptServiceUtil.getItemRegistry.getItem(rollorShutterName+'_ShadeControl')
    val beschattungsautomatik = ScriptServiceUtil.getItemRegistry.getItem(rollorShutterName+'_Beschattungsautomatik')
    
    // Calculate the Tolerances
    val setTempMinusTol = setTemp.state - heatingTolerance // assumes SetTemp is Number:Temperature
    val setTempPlusTol = setTemp.state + heatingTolerance

    // Check whether to open the Shades because sun cannot enter again
    if(AzimuthState.state != start && shadeControl.state == 10 && beschattungsautomatik.state == ON) {
        shadeControl.sendCommand(1)
        logInfo(rollorShutterName, 'Öffne weil sonne nicht mehr einfallen kann')
        return;
    }
    // Exit if there's nothing to do
    else if(AzimuthState.state != start) {
        logDebug(rollorShutterName, 'Nothing to do)
        return;
    }

    //Here is determined if Solar Heating through the Windows would be a good idea
    // -----------------------------------------------------------------------------------
    if(IW_ForecastTodayMaxTemperature.state < setTempMinusTol
       && actTemp.state < setTemp.state) {
        // Logging levels even for rules can be adjusted from the Karaf Console or log4j2.xml
        // change this to a logDebug and eliminate the DebugMode Item for further simplifications
        logDebug(rollorShutterName, 'Heizen durch öffnen der Verschattung macht momentan Sinn ')
            
        // I'm pretty certain a Dimmer Item can never have the state DOWN
        // The state can never be null, I think you mean NULL or UNDEF here
        if((blind.state > 50 || blind.state == NULL || blind.state == UNDEF)
           && shadeManuallyOperated.state != ON) {
            shadeControl.sendCommand(1)
        }
        else{
            logDebug(rollorShutterName, 'Kommando nicht gesendet weil Rolladen schon oben ist oder kürzlich von hand bewegt wurde')
        }
    }
    else {
        logDebug(rollorShutterName, 'Heizen durch öffnen der Verschattung macht momentan KEINEN Sinn')
    }

    // Here s determined if Closing the Blinds mit be a good ideo to not overheat the room
    // -----------------------------------------------------------------------------------
    logDebug(rollorShutterName, 'Beschattung könnte sinvoll sein weil sonne einfallen könnte (Sonnenstand)')
    // Determine if there is actually solar radiation, we already know the sun is in the right position
    // or else the rule would never get to this point.
    if(Out_Weather_Lux2.state > solarLightMax)) {
        logDebug(rollorShutterName, 'Beschattung könnte sinvoll sein weil sonne Sonnenstrahlung Grenzwert übersteigt')
        // determin if temperature is too hot
        // Use grouping and spacing of clauses to help explain what clauses go with which
        if((IW_ForecastTodayMaxTemperature.state > setTempPlusTol && actTemp.state > setTemp)
           || actTemp.state > setTempPlusTol) {
           logDebug(rollorShutterName, 'Beschattung ist sinvoll weil erwartete Aussentemperatur Solltemperatur übersteigt und Innentemperatur sollwert bereits überschreitet')
            if((blind.state < 80 || blind.state == NULL || blind.state === UNDEF)
               && (shadeManuallyOperated.state != ON || shadeManuallyOperated.state == NULL || shadeManuallyOperated.state == UNDEF)) {
                shadeControl.sendCommand(10)
            }
            else {
                logDebug(rollorShutterName, 'Kommando nicht gesendet weil Rolladen schon unten ist oder kürzlich von hand bewegt wurde')
            }
        }
        else {
            logInfo(rollorShutterName, 'Beschattung ist Nicht sinvoll weil erwartete Aussentemperatur Solltemperatur nicht übersteigt oder innentemperatur sollwert nicht übersteigt')
        }
    }
    // I'm not entirely certain the else log statements match up with the if statement any more
    else {
        logDebug(rollorShutterName, 'Beschattung ist blödsinn weil sonne nicht einfallen kann')
    }
end

Obviously I just typed in the above and there are likely many errors and typos. But it should be enough to illustrate how to go about creating reusable rules and how to simplify rules. I believe, using the approaches above, you can create just one of each of the rule types to manage all of your rollorshutters. You should not be copying and pasting and editing the same code over and over again.

In other rules languages, some of the above will be even simpler. For example, you don’t need to pull Items from the registry manually like that when you only have the Item’s name which will cut a bunch of that up front stuff.

1 Like