Rule DSL to JS convert, help request

Hello,
I have a rule I wrote with Rule DSL and I want to rewrite it with JS.
But in my searches in the forum, I could not find an example of receiving data from the serial port or a relationship for a similar structure.
I want to take the information from the serial port into the Script and divide it into parts and process it.

runtimeInfo:
  version: 4.1.2
  buildString: Release Build
locale: default
systemInfo:
  configFolder: /etc/openhab`Preformatted text`
  userdataFolder: /var/lib/openhab
  logFolder: /var/log/openhab
  javaVersion: 17.0.10
  javaVendor: Debian
  osName: Linux
  osVersion: 6.8.2-edge-rockchip64
  osArchitecture: aarch64
  availableProcessors: 4
  freeMemory: 58068632
  totalMemory: 304087040
  uptime: 22125
  startLevel: 100

addons:
  - automation-jsscripting
  - automation-jsscriptingnashorn
  - automation-jythonscripting
  - binding-exec
  - binding-serial
  - binding-systeminfo
  - binding-telegram
  - misc-homekit
  - misc-openhabcloud
  - persistence-mapdb
  - persistence-rrd4j
  - transformation-exec
  - transformation-jsonpath
  - transformation-map
  - transformation-regex
  - ui-basic
  navigator:
    cookieEnabled: true
    deviceMemory: 4GB
    hardwareConcurrency: 8
    language: en-US
    languages:
      - en-US
      - en
      - tr
    onLine: true
    platform: Win32

Rule DSL is;

import org.openhab.core.model.script.ScriptServiceUtil
//import org.eclipse.smarthome.model.script.ScriptServiceUtil
//import org.eclipse.xtext.xbase.lib.Functions
import org.eclipse.smarthome.core.types.State
import org.eclipse.smarthome.core.types.Command
import org.eclipse.smarthome.core.types.TypeParser
var Number Log_Flag = 1
rule "INPUT"
when
    Item CANBus received update   // your condition here
then
var String CANBusData = CANBus.state.toString
   if( CANBusData.startsWith("VID_T")){
    val parts       = CANBusData.split(";")
    var dTYPE       = Integer::parseInt(parts.get(0).split("=").get(1)) as Number
    var dID         = Integer::parseInt(parts.get(1).split("=").get(1)) as Number
    var pID         = Integer::parseInt(parts.get(2).split("=").get(1)) as Number
    val String cID         = (parts.get(3).split("=").get(1)) 
    if ((cID == "S") || (cID == "L" )) {
        val name        = ScriptServiceUtil.getItemRegistry.getItem("I_"+ dID + "_" + pID + "_" + cID )
    if (Log_Flag == 1) {
        
        logInfo("CANBus", ">>>> String                              : {}", parts)
        logInfo("CANBus", ">>>> CANBUS) Input.Device.Type           : {}", dTYPE)
	    logInfo("CANBus", ">>>> CANBUS) Input.Device.ID             : {}", dID)
        logInfo("CANBus", ">>>> CANBUS) Input.Port.ID               : {}", pID)
	    logInfo("CANBus", ">>>> CANBUS) Input.Command.ID            : {}", cID)
        logInfo("CANBus", ">>>> CANBUS) Item.Name                   : {}", name)
        logInfo("CANBus", ">>>> CANBUS) Item.Type                   : {}", name.getType)
        //logInfo("CANBus", ">>>> CANBUS) Item.AcceptedCommandTypes   : '{}'", name.getAcceptedCommandTypes)
        //logInfo("CANBus", ">>>> CANBUS) Item.getAcceptedDataTypes   : '{}'", name.getAcceptedDataTypes)
        logInfo("CANBus", ">>>> CANBUS) Item.State                  : {}", name.getState)
   }
    if ( cID == "S"){
        if ((name.getType == "Switch" || name.getType == "Group" ) )
        {
            if (name.getState == OFF )
            {
                name.sendCommand(ON)                    
            }  
	            else
            {
                name.sendCommand(OFF)                      
            }                       
        }
        else if (name.getType == "Rollershutter" )
        {
            if(RS_Direction.getState==ON ) 
            {                                    // short STOP
                name.sendCommand(STOP)                                   
            }
            else 
            {                                                             // short UP/DOWN
                if (name.getState == 100 || name.getState == DOWN )
                {
                    name.sendCommand(UP)                            
                }                         
	                else
                {
                    name.sendCommand(DOWN)                        
                }                         
            } 
        }
    }
    else  if ( cID == "L" )
    {
        if (name.getType != "Rollershutter" ){
            var name        = ScriptServiceUtil.getItemRegistry.getItem("I_"+ dID + "_" + pID + "_" + cID)
        }
        if (name.getType == "Switch" || name.getType == "Group" )
        {
            if (name.getState == OFF )
            {
                name.sendCommand(ON) 
            }                      
	            else
            {
                name.sendCommand(OFF)                
            }                  
        }
        else if (name.getType == "Rollershutter" )
        {
            if(RS_Direction.getState==OFF ) 
            {                                    // short STOP
                name.sendCommand(STOP)               
            }
            else 
            {                                                             // short UP/DOWN
                if (name.getState == 100 || name.getState == DOWN )
                { 
                    name.sendCommand(UP)                
                } 
	                else
                { 
                    name.sendCommand(DOWN)                   
                }
            }            
        }
    } 
  }          
}
end

I need help on how to convert the following part. I have no idea for a starting point at the moment, unfortunately. Despite reading a lot of documents and messages, it has become more and more complicated.

when
    Item CANBus received update   // your condition here
then
var String CANBusData = CANBus.state.toString
   if( CANBusData.startsWith("VID_T")){
    val parts       = CANBusData.split(";")
    var dTYPE       = Integer::parseInt(parts.get(0).split("=").get(1)) as Number
    var dID         = Integer::parseInt(parts.get(1).split("=").get(1)) as Number
    var pID         = Integer::parseInt(parts.get(2).split("=").get(1)) as Number
    val String cID         = (parts.get(3).split("=").get(1)) 
    if ((cID == "S") || (cID == "L" )) {
        val name        = ScriptServiceUtil.getItemRegistry.getItem("I_"+ dID + "_" + pID + "_" + cID )
    if (Log_Flag == 1) {
        
        logInfo("CANBus", ">>>> String                              : {}", parts)
        logInfo("CANBus", ">>>> CANBUS) Input.Device.Type           : {}", dTYPE)
	    logInfo("CANBus", ">>>> CANBUS) Input.Device.ID             : {}", dID)
        logInfo("CANBus", ">>>> CANBUS) Input.Port.ID               : {}", pID)
	    logInfo("CANBus", ">>>> CANBUS) Input.Command.ID            : {}", cID)
        logInfo("CANBus", ">>>> CANBUS) Item.Name                   : {}", name)
        logInfo("CANBus", ">>>> CANBUS) Item.Type                   : {}", name.getType)
        //logInfo("CANBus", ">>>> CANBUS) Item.AcceptedCommandTypes   : '{}'", name.getAcceptedCommandTypes)
        //logInfo("CANBus", ">>>> CANBUS) Item.getAcceptedDataTypes   : '{}'", name.getAcceptedDataTypes)
        logInfo("CANBus", ">>>> CANBUS) Item.State                  : {}", name.getState)
   }

Thank you.

So this is just a semicolon separated list of values, right? It would help to see an example message.

Do you want UI or file based rule?

You’d trigger the rule same as you do not with updates to the CANBus Item.

You might consider converting this to Blockly which might be easier for you. Create a rule in the UI, set the trigger to updates to CANBus. Add an action and select “Inline Script” and Blockly from the list of options. The top part of the script will look something like the following.

If you want to write it as JS yourself, I recommend opening in separate tabs JavaScript Scripting - Automation | openHAB and your search engine of choice. For each line of code that you may not know how to translate, first look at the reference docs for the JS Scripting addon. If you don’t find it documented there it’s probably a generic JS capability so search the internet (e.g. “JavaScript parse string to int”).

How to create a rule in a .js file? JavaScript Scripting - Automation | openHAB

How to test if a String starts with something? JavaScript string startswith at DuckDuckGo

How to split a String? JavaScript string split at DuckDuckGo

How to log from a JS Rule? JavaScript Scripting - Automation | openHAB

How to send a command to an Item? JavaScript Scripting - Automation | openHAB

And so on.

Going through a JavaScript 101 type tutorial or class might be useful as well. But over all, there will be a near one-for-one line by line translation between the Rules DSL and the JS equivalent. Assuming a managed rule, create the rule as described above for Blockly only choose “ECMAScript 262 Edition 11” instead of Blockly.

cache.private.put('Log_Flag', true); // use a boolean for binary flags, use the shared cache if you want to control this flag from other rules, even better use debug level and set the logging level in log4j2.xml
var CANBusData = event.itemState.toString();
if( CANBusData.startsWith('VID_T') ){
  const parts = CANBusData.split(';');
  var dTYPE = parseInt(parts[0].split('=')[1]);
  ...
  if(cID == 'S; || cID == 'L') {
    const name = 'I_' + dID + '_' + pID + '_' + cID;
    if(cache.private.get('Log_Flag`)) {
      console.info('>>>> String                              : ' + parts);
      console.info('>>>> CANBUS) Input.Device.Type           : ' + dTYPE);
      ...
    }
    if(cID == 'S') {
      if(items[name].type == 'Switch' || items[name].type == 'Group') {
        if(items[name].state == "OFF") {
          items[name].sendCommand('ON');
        }
        else {
          items[name].sendCommand('OFF');
        }
      }
      ...
    }
    ...
  }
}

Just do a one to one translation first and one that works, start looking into things you can do to make it more concise and easy to manage. For example, using a ternary operator (standard JavaScript the following

        if(items[name].state == 'OFF') {
          items[name].sendCommand('ON');
        }
        else {
          items[name].sendCommand('OFF');
        }

could become the one-liner

items[name].sendCommand((items[name].state == 'OFF') ? 'ON' : 'OFF');

Further consolidation could be achieved by following Design Pattern: How to Structure a Rule.

But that sort of thing is something to worry about after you get it working.

2 Likes

Maybe consider to simplify the rule before transition (or do it on-the-fly).

Imports: I’m pretty sure that only this import is needed:

import org.openhab.core.model.script.ScriptServiceUtil

Logging:
Instead of using a global var:

var Number Log_Flag = 1
...
if(Log_Flag == 1) {
    logInfo("CANBus",...)
}

use the builtin logger funcionality:

logDebug("CANBus",...)

To switch on/off the logging, go to karaf console and use

log:set DEBUG   org.openhab.core.model.script.CANBus
log:set DEFAULT org.openhab.core.model.script.CANBus

use early return instead of big conditionals:

if(CANBusData.startsWith("VID_T")) {
 // do something
}

becomes

if(!CANBusData.startsWith("VID_T")) 
    return;

Same is true for

if((cID == "S") ||(cID == "L")) {

becomes

if(cID != "S" && cID != "L")
    return;

Now the slightly more sinister parts…
As in the very beginning:

val name        = ScriptServiceUtil.getItemRegistry.getItem("I_"+ dID + "_" + pID + "_" + cID)

is set, there is no point for this part at all:

if(name.getType != "Rollershutter") {
    var name        = ScriptServiceUtil.getItemRegistry.getItem("I_"+ dID + "_" + pID + "_" + cID)
}

Not to mention, that you must not use name, as it is already set (but as there is no code in the block other than the definition, this won’t do nothing at all anyway.)

Next step: deduplicate code. Instead

    if(cID == "S") {
        if((name.getType == "Switch" || name.getType == "Group")) {
            if(name.getState == OFF) {
                name.sendCommand(ON)
            } else {
                name.sendCommand(OFF)
            }
        } else if(name.getType == "Rollershutter") {
            if(RS_Direction.getState==ON) {                                    // short STOP
                name.sendCommand(STOP)
            } else {                                                             // short UP/DOWN
                if(name.getState == 100 || name.getState == DOWN) {
                    name.sendCommand(UP)
                } else {
                    name.sendCommand(DOWN)
                }
            }
        }
    } else if(cID == "L") {
        if(name.getType == "Switch" || name.getType == "Group") {
            if(name.getState == OFF) {
                name.sendCommand(ON)
            } else {
                name.sendCommand(OFF)
            }
        } else if(name.getType == "Rollershutter") {
            if(RS_Direction.getState==OFF) {                                    // short STOP
                name.sendCommand(STOP)
            } else {                                                             // short UP/DOWN
                if(name.getState == 100 || name.getState == DOWN) {
                    name.sendCommand(UP)
                } else {
                    name.sendCommand(DOWN)
                }
            }
        }
    }

do it the other way:

    if(name.getType == "Switch" || name.getType == "Group") {
        if(name.getState == OFF) {
            name.sendCommand(ON)
        } else {
            name.sendCommand(OFF)
        }
    } else if(name.getType == "Rollershutter") {
        if(cID == "S") {
            if(RS_Direction.getState==ON) {                                    // short STOP
                name.sendCommand(STOP)
            } else {                                                             // short UP/DOWN
                if(name.getState == 100 || name.getState == DOWN) {
                    name.sendCommand(UP)
                } else {
                    name.sendCommand(DOWN)
                }
            }
        } else if(cID == "L") {
            if(RS_Direction.getState==OFF) {                                    // short STOP
                name.sendCommand(STOP)
            } else {                                                             // short UP/DOWN
                if(name.getState == 100 || name.getState == DOWN) {
                    name.sendCommand(UP)
                } else {
                    name.sendCommand(DOWN)
                }
            }
        }
    }

But now there is even more to simplify. instead:

        if(cID == "S") {
            if(RS_Direction.getState==ON) {                                    // short STOP
                name.sendCommand(STOP)
            } else {                                                             // short UP/DOWN
                if(name.getState == 100 || name.getState == DOWN) {
                    name.sendCommand(UP)
                } else {
                    name.sendCommand(DOWN)
                }
            }
        } else if(cID == "L") {
            if(RS_Direction.getState==OFF) {                                    // short STOP
                name.sendCommand(STOP)
            } else {                                                             // short UP/DOWN
                if(name.getState == 100 || name.getState == DOWN) {
                    name.sendCommand(UP)
                } else {
                    name.sendCommand(DOWN)
                }
            }
        }

simplified:

if((cID == "S" && RS_Direction.getState==ON) || (cID == "L" && RS_Direction.getState==OFF)) {     // short STOP
    name.sendCommand(STOP)
} else {                                                                                          // short UP/DOWN
    if(name.getState == 100 || name.getState == DOWN) {
        name.sendCommand(UP)
    } else {
        name.sendCommand(DOWN)
    }
}

And as rlkoshak already mentioned, the ternary operator Item.sendCommand(if(a) b else c)
will work in rules DSL as well:

name.sendCommand(if(name.getState == OFF) ON else OFF)
...
name.sendCommand(if(name.getState == 100 || name.getState == DOWN) UP else DOWN)

And as in the latter case, name is of type Rollershutter, there is no such state as DOWN, this is a command only, so the alternative is always false.

Now the complete rule looks like this:

import org.openhab.core.model.script.ScriptServiceUtil

rule "INPUT"
when
    Item CANBus received update   // your condition here
then
   var String CANBusData = CANBus.state.toString
   if(!CANBusData.startsWith("VID_T"))
       return;

    val parts       = CANBusData.split(";")
    val String cID  = parts.get(3).split("=").get(1)
    if(cID != "S" && cID == "L")
        return;

    var dTYPE       = Integer::parseInt(parts.get(0).split("=").get(1)) as Number
    var dID         = Integer::parseInt(parts.get(1).split("=").get(1)) as Number
    var pID         = Integer::parseInt(parts.get(2).split("=").get(1)) as Number

    val name        = ScriptServiceUtil.getItemRegistry.getItem("I_"+ dID + "_" + pID + "_" + cID)
    logDebug("CANBus", ">>>> String                              : {}", parts)
    logDebug("CANBus", ">>>> CANBUS) Input.Device.Type           : {}", dTYPE)
    logDebug("CANBus", ">>>> CANBUS) Input.Device.ID             : {}", dID)
    logDebug("CANBus", ">>>> CANBUS) Input.Port.ID               : {}", pID)
    logDebug("CANBus", ">>>> CANBUS) Input.Command.ID            : {}", cID)
    logDebug("CANBus", ">>>> CANBUS) Item.Name                   : {}", name)
    logDebug("CANBus", ">>>> CANBUS) Item.Type                   : {}", name.getType)
 // logDebug("CANBus", ">>>> CANBUS) Item.AcceptedCommandTypes   : {}", name.getAcceptedCommandTypes)
 // logDebug("CANBus", ">>>> CANBUS) Item.getAcceptedDataTypes   : {}", name.getAcceptedDataTypes)
    logDebug("CANBus", ">>>> CANBUS) Item.State                  : {}", name.getState)

    if(name.getType == "Switch" || name.getType == "Group") {
        name.sendCommand(if(name.getState == OFF) ON else OFF)
    } else if(name.getType == "Rollershutter") {
        if((cID == "S" && RS_Direction.getState==ON) || (cID == "L" && RS_Direction.getState==OFF))      // short STOP
            name.sendCommand(STOP)
        else                                                                                             // short UP/DOWN
            name.sendCommand(if(name.getState == 100) UP else DOWN)
    }
end

Ooops :wink:

The simplified rule should be easier to be converted.

2 Likes

Hello Dear Rich,
thank you for your very valuable time, I am grateful for your help.

2024-04-12 04:08:33.261 [INFO ] [openhab.event.ItemStateChangedEvent ] 
- Item 'CANBus' changed from VID_T=11;D=1;P=1;C=P  to VID_T=11;D=1;P=1;C=S


I prefer file-based Rules.


I will try to apply your advice and suggestions on coding. In line with your suggestions, I need to do some research now.

I will revert you with some result, I hope that i solve this and will be positive messages.
Thank you again.

Hello Dear Udo,
Thank you for your very detailed and rich answer, I am grateful for your help.

Very sensible and useful information that I did not know. I will pay attention to it and apply it. Thank you.


I understand your approach, you applying a pre-filter so as not to tie up the system. If the coming data and my formal do not match, break the rule, and back to listen to new dat stream, again.

I could not understand your meant.
The information from CANBus line does not know the system and/or Item information (RS or Switch Types etc.) in openhab.
There is an embedded device and its task is only very limited information about the key pressed. Which key (port) of which device (Input Module) was pressed and released and whether it was pressed long or short).

VID_T=11;D=1;P=1;C=P to VID_T=11;D=1;P=1;C=S
VID_T: Header of the message
D: 1 is the Device ID
P: 1 is the Port ID(mean which button pressed)
C: Command ID (It can be P> Pressed, R> Released, L> Long Pressed or S> Short Pressed)

So i need to determine to which Item is the target and how this command is dedicated to this Item, so i need to know if this Item Type that RS or Switch etc.


I will analyse it in detail. I will need some time to understand and digest it.


yes, I know that and I am using it in my codes normally. i do not know that why I did not use it in that time, maybe I wanted it to be more readable because I am not familiar with DSL coding.


As far as I know the state of RS can be any value between 0 and 100 percentage, Up, Down, and Stop.
My goal was, eliminate all irrelevant situations


I will review and apply. Thank you.


Yes, exactly.
Thanks to Rich and you, my road map and what I need to do has become very clear.

Not quite the correct understanding. It not a matter of tying up the system. Both his and your rule runs in the same amount of time with the same amount of resources. However, by failing fast like that, @Udo_Hartmann’s version of the code is less complex and therefore easier to read, understand, and maintain in the future.

In this case, by returning, the code that does stuff isn’t embedded in if/else clauses and because you’ve already tested and exited you don’t need to test those conditions later, simplifying your if caluses.

name is a reserved word. It is a variable that already exists. You’ve essentially overridden that already existing name with your own variable, essentially destroying the original.

That’s what he means but I’m not sure that’s correct. triggeringItemName is an implicit variable but name is not. But it’s been a very long time since I’ve messed with Rules DSL so maybe name is relatively new?

But the overall warning is the same. Don’t name your variables the same as variables that already exist in the rule (see Rules | openHAB).

The state of a RS can only be between 0 and 100. You can send UP, DOWN, and STOP as commands to an RS but the Item will never have these as it’s state.

1 Like

Take a look in your code. You set the local constant right after the first if clause. So it’s already set, you got the line twice within your code.
But as mentioned, it will do no harm, as there is no other code within the block for the second definition. var and val are only valid in the same context (but will be inherited to descendants context) .

2 Likes

Good catch, I didn’t notice that and thought you were referring to the implicit variables.

If that second line of code ever executed it would have generated an error for sure.

2 Likes

I understood what is my mistake. Not need for a second process.


It actually works and does not produce an error. However, the comparison and name retrieval is slower than expected. I don’t know if this is related to the point you noticed, or if this is of interest to you.

So I am working on DSL optimization now, I will test it and then start to convert it to JS.