Include external script in rule file

Ever since I met openHAB with rules functionality I feel lack of include functionality known in other languages like JAVA or C++.

I decided to build some workaround allowing me to achieve similar functionality.

Main assumption have been that:

  • Syntax have to be compatible with openHAB rules syntax
  • Included scripts have to be editable using SmartHome-Designer or Microsoft VS Code + openHAB VS Code Extension
  • Included scripts will be stored in include subfolder of rules folder
  • Include functionality will be realized by external shell (bash + awk) script executed on openHAB startup or manually

How to use:

  1. Create include rule and save in include sub-folder of rules folder, for example: ExampleExternalRule.rules
val Functions$Function1<String, Boolean> myExternalRule = [ 
	String someText |
	
        logInfo("ExternalScript", someText)
	
	return true
]
  1. Put into main rule file lines like bellow and next execute Tools-RulesInclude.sh
//#include ExampleExternalRule.rules
//#endinclude ExampleExternalRule.rules

After execution of Tools-RulesInclude.sh script, content of ExampleExternalRule.rules will be added between //#include and //#endinclude lines.
Result:

//#include ExampleExternalRule.rules
val Functions$Function1<String, Boolean> myExternalRule = [ 
	String someText |
	
        logInfo("ExternalScript", someText)
	
	return true
]
//#endinclude ExampleExternalRule.rules
  1. Use external script in main rule (useful if You have to use the same code in more then one rule)
rule "MyTestRule"
when
	Item TestItem received update 1 
then
    myExternalRule.apply("Hello world")
end

Script: Tools-RulesInclude.sh

#!/bin/bash

PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:$PATH"

# openHAB root folder
if [ -z "${openHAB_root}" ] ; then openHAB_root=/opt/openhab ; fi

openhab_log=${openHAB_root}/userdata/logs/openhab.log

cd "${openHAB_root}/conf/rules"

IFS=$'\n'
for FileToProccess in `ls *.rules` ; do

    rm /tmp/${FileToProccess} > /dev/null 2>&1

    awk -v rulesFile="${FileToProccess}" '
        function ltrim(s) { sub(/^[ \t\r\n]+/, "", s); return s }
        function rtrim(s) { sub(/[ \t\r\n]+$/, "", s); return s }
        function trim(s) { return rtrim(ltrim(s)); }
        BEGIN {

            iStart = 0
            iEnd = 0
            includeFileName = ""
            rulesTmpFile = "/tmp/" rulesFile

            while (getline < rulesFile)
            {

                if(iStart == iEnd)
                {
                    print $0 > rulesTmpFile
                }

                if(index($0, "//#include") > 0)
                {
                    iStart = iStart + 1
                    includeFileName = trim(substr($0, index($0, "//#include") + length("//#include")))
                    includeFileName = "include/" includeFileName

                    if(system("[ -f " includeFileName " ]") == 0)
                    {

                        while (getline < includeFileName)
                            print $0 > rulesTmpFile

                        close(includeFileName)

                    }
                    else
                    {
                        iStart = 0    # Critical error
                        print "'`date +"%Y-%m-%d %H:%M:%S.%3N"`' [ERROR] [pt/openhab/bin/Tools-RulesInclude.sh] - File: " includeFileName " included by " rulesFile " does not exists" >> "'${openhab_log}'"
                    }


                }
                else if(index($0, "//#endinclude") > 0 && iStart = iEnd + 1)
                {
                    iEnd = iEnd + 1
                    print $0 > rulesTmpFile
                }

            }
            close(rulesFile);

            if(iStart == 0 || iStart != iEnd)
                system("rm \"" rulesTmpFile "\"")
            else
                print "'`date +"%Y-%m-%d %H:%M:%S.%3N"`' [INFO ] [pt/openhab/bin/Tools-RulesInclude.sh] - Processing file: " rulesFile >> "'${openhab_log}'"

            if(iStart != iEnd)
                print "'`date +"%Y-%m-%d %H:%M:%S.%3N"`' [ERROR] [pt/openhab/bin/Tools-RulesInclude.sh] - Syntax error in file: " rulesFile >> "'${openhab_log}'"


        }
    '

    if [ -f "/tmp/${FileToProccess}" ] ; then
        mv "/tmp/${FileToProccess}" ./
    fi

done

I hope that this solution will be useful for openHAB community.

Thanks for sharing. I believe others have developed similar pre-parsers but I don’t think anyone has posted on yet.

Some comments, mainly for future readers who come along later:

  • Be aware that the ability to use libraries and reusable code is one of the drivers behind the creation of the Experimental Rules Engine. There is also the JSR223 add-on that lets you write Rules using Jython, JavaScript, or Groovy. So if one truely wants all the features of a general purpose programming language instead of some of the designed in limitations of the Rules DSL they can use either of these.

  • There is also support for Scripts, though they have a limitation that you cannot pass values into the script which greatly reduces their utility.

  • There used to be a problem with having global variables of the same name in multiple rules files. I don’t know if this is still the case. However, there is absolutely a problem with having Rules with the same name so this script would not work to include whole Rules unless you edit the .rules files after the include.

  • One can often avoid the need to do includes like this at all by organizing your Rules by function rather than location (e.g. have lighting.rules rather than bedroom.rules). For cross-cutting concerns like alerting or calculating time of day you can add in Design Pattern: Separation of Behaviors which centralizes all that logic into one set of Rules, again aleviating the need for performing includes like this.