Regarding transforming received JSON string to uppercase at Thing/Item stage

Hi there. I’m sure that there are easy ways to do this, but I’m struggling.

I have a number of Raspberry Pi’s with an RTL-SDR attached, listening for ADS-B messages from aircraft using dump1090-mutability. Each Pi publishes a JSON-formatted message in lowercase.

In OpenHAB I have several Things defined as below:

UID: http:url:Livingroom-Pi
label: Living Room ADSB Pi
thingTypeUID: http:url
configuration:
  authMode: BASIC
  ignoreSSLErrors: true
  baseURL: http://192.168.x.x/dump1090/data/aircraft.json
  password: xxxxxxxxxxxx
  delay: 0
  stateMethod: GET
  refresh: 9
  commandMethod: GET
  contentType: application/json
  timeout: 1200
  bufferSize: 2048
  username: xxxxxxx
location: Living Room
channels:
  - id: String
    channelTypeUID: http:string
    label: Living Room Pi ADSB Packet as String
    description: ""
    configuration: {}

Here’s a example fragment of the JSON received:

{ "now" : 17356484.7, "messages" : 24734800, "aircraft" : [ {"hex":"A1B2C3","squawk":"0137","flight":"ABCDEF   ","lat":12.34567,"lon":0.123456,"nucp":7,"seen_pos":0.2,"altitude":36000,"vert_rate":0,"track":219,"speed":426,"category":"A5","mlat":[],"tisb":[],"messages":395,"seen":0.0,"rssi":-34.0},

Here’s a fragment from my Rule:

rule "adsb planes packet processing"
when 
Member of gADSB_Data changed 
then 
    var String packet_TXT = (triggeringItem.state as StringType).toString
    packet_TXT =  ((packet_TXT).toUpperCase) // case conversion
    packet_TXT =  (packet_TXT).replace(" ","") // strips out unwanted spaces

// Further input validation follows below

I want to convert the JSON received to uppercase at the Thing/Item stage rather than in a Rule.
I get the feeling I could use a regex or a transform, but I don’t know how to construct one. I’ve tried Regex101, read the OpenHAB documentation on transforms and regexes, but for something that seems like it should be easy, I’m lost.

Probably something really simple I’ve overlooked, as I did with the Tuya plugin (which you guys helped me understand).

Yes and no.

Some RegEx does have the uppercase transform ability, but I believe the flavor used by OH is not one of them, so that is a dead end.

The easiest transform for this job would probably be a script transform using JSScripting and .toUpperCase(), but then that wouldn’t be significantly different than doing it in the rule.

What is the reason to move this operation from the rule to the thing definition?

There’s a famous quite among computer scientists.

Some people, when confronted with a problem, think “I know, I’ll use regular expressions.” Now they have two problems.

Jamie Zawinski

It’s overly glib but the root of the quote is that overuse of regex can become a problem.

I’ll just elaborate on @JustinG’s response to say that using transform isn’t going to save anything in terms of processing. But I kind of like moving stuff like this as close to the device as possible, but in this case whether that makes sense or not is going to be driven by what “Further input validation follows below” does.

Would that move to the transform too or would you still need this rule? If so, you really are not saving anything by moving the toUpperCase() code to the Thing as now you’ll have two scripts that need to be invoked for every change and the toUpperCase() transform will run on updates too, not just changes. On-the-other-hand, who cares about the processing required? Home automation is rarely requires low latency/high throughput and even an RPi 2 has enough processor to run OH adequately (RAM is another story entirely).

But if you want to move everything to the transform, this could make some sense since since it frees your rules to focus on what to do in response and massaging the data stays close to the Things.

Note that in OH 4 you can write transforms in any language so your Rules DSL rule could be ported almost unchanged.

var packet_TXT = input;
packet_TXT = packet_TXT.toUpperCase
packet_TXT.repalce(" ", "")

or as a one liner

input.toUpperCase.replace(" ", "")

As with lambdas, the last line evaluated in a transform becomes the return value.

1 Like

Thank you all for your swift and helpful replies.
I agree that there’s no point trying to get closer to the metal if it doesn’t provide any benefit, so I’ll keep the transform in the rule where it is at the moment.

Regarding chaining transforms, I understand

var packet_TXT = input;
packet_TXT = packet_TXT.toUpperCase
packet_TXT.replace(" ", "")

becoming

input.toUpperCase.replace(" ", "")

I split them for ease of understanding for when I come back to my code months later.

This openhab coding is the only coding I’ve done since the days of ZX Spectrum and BBC Basic, so doubtless I’m going about some things the hard way, as my knowledge is limited.

Something in my plane rules file breaks on occasion and throws a NULL but I don’t know what, and it’s infrequent enough that I lose it in the logs.

My partner is obsessed with the Belugas that fly in and out of Hawarden airport, so I’ve coded up a monstrosity that flashes the house’s Tuya and Tasmota lights when a plane enters ADS-B range.

I built all of the code for OH2.
If anybody would like to see what three months of trial-and-error hacking look like, read on:

val Number LogLevel = 0
val Number BelugaTable_NameOffset_NUM = 20 // beluga.map
val Number BelugaTable_AirportOffset_NUM = 40 // beluga.map
val Number sizeOfBelugaFleet_NUM = 10
val Number max_Plane_Contact_Age_NUM = 10 // in seconds

var String BelugaName_TXT = "this is a test"
var Number BelugaDestination_NUM = -1
var String BelugaOrigin_TXT = "this is a test"
var String BelugaDestination_TXT = "this is a test"
var Number BelugaOrigin_NUM = 0
var String mqtt_message_TXT = "test"
var String plane_Altitude_TXT = "test"
var String plane_Coordinates_TXT = "test"

rule "adsb planes packet processing"
when 
Member of gADSB_Data changed 
then 
    var String packet_TXT = (triggeringItem.state as StringType).toString
    packet_TXT =  ((packet_TXT).toUpperCase) // case conversion
    packet_TXT =  (packet_TXT).replace(" ","") // strips out unwanted spaces

    logInfo("modified message",packet_TXT)

    if (packet_TXT.contains("AIRCRAFT")) { 
        var Number packetlength_NUM = Integer.parseInt(transform("JSONPATH", "$.AIRCRAFT.length()", packet_TXT))
        //logInfo("plane rules 0", "packet length {}", packetlength_NUM)
    if (packetlength_NUM > 0) {

    for ( var packetloop_NUM = 0; packetloop_NUM <= packetlength_NUM; packetloop_NUM = packetloop_NUM + 1) {

        var Number planeTransmissionValidity_NUM = 1

        var String plane_ICAO_TXT =         (transform("JSONPATH", "$.AIRCRAFT[" + packetloop_NUM.toString+"].HEX", packet_TXT))
        var String plane_Callsign_TXT =     (transform("JSONPATH", "$.AIRCRAFT[" + packetloop_NUM.toString+"].FLIGHT", packet_TXT))
        var String plane_Latitude_TXT =     (transform("JSONPATH", "$.AIRCRAFT[" + packetloop_NUM.toString+"].LAT", packet_TXT))
        var String plane_Longitude_TXT   =  (transform("JSONPATH", "$.AIRCRAFT[" + packetloop_NUM.toString+"].LON", packet_TXT))
        var String plane_Contact_Age_TXT =  (transform("JSONPATH", "$.AIRCRAFT[" + packetloop_NUM.toString+"].SEEN_POS", packet_TXT))
                   plane_Altitude_TXT =     (transform("JSONPATH", "$.AIRCRAFT[" + packetloop_NUM.toString+"].ALTITUDE", packet_TXT))
        var String plane_VertRate_TXT =     (transform("JSONPATH", "$.AIRCRAFT[" + packetloop_NUM.toString+"].VERT_RATE", packet_TXT))

        if (LogLevel > 0 ) {logInfo("plane rules 0","packet {} of {}",packetloop_NUM,packetlength_NUM)}
        
        if ((plane_ICAO_TXT == packet_TXT) && (planeTransmissionValidity_NUM == 1)) {
            planeTransmissionValidity_NUM = 0 
            if (LogLevel > 1) {logInfo("plane rules 2a","ICAO fail ")}
            }

        if ((plane_Callsign_TXT == packet_TXT) && (planeTransmissionValidity_NUM == 1)) {
            planeTransmissionValidity_NUM = 0 
            if (LogLevel > 1) {logInfo("plane rules 2b","callsign fail ")}
            }

        if ((plane_Contact_Age_TXT == packet_TXT) && (planeTransmissionValidity_NUM == 1)) {
            planeTransmissionValidity_NUM = 0 
            if (LogLevel > 1) {logInfo("plane rules 2c","age fail ")}
            }
        
        if ((plane_Altitude_TXT == packet_TXT) && (planeTransmissionValidity_NUM == 1)) {
            planeTransmissionValidity_NUM = 0
            if (LogLevel > 1) {logInfo("plane rules 2d","alt fail ")}
            }

        if ((plane_VertRate_TXT == packet_TXT) && (planeTransmissionValidity_NUM == 1)) {
            planeTransmissionValidity_NUM = 0
            if (LogLevel > 1) {logInfo("plane rules 2e","vert fail ")}
            }

        if ((plane_Latitude_TXT == packet_TXT) && (planeTransmissionValidity_NUM == 1)) {
            planeTransmissionValidity_NUM = 0
            if (LogLevel > 1) {logInfo("plane rules 2f","lat fail ")}
            }

        if ((plane_Longitude_TXT == packet_TXT) && (planeTransmissionValidity_NUM == 1)) {
            planeTransmissionValidity_NUM = 0
            if (LogLevel > 1) {logInfo("plane rules 2g","long fail ")}
            }

        if (planeTransmissionValidity_NUM == 1) {

            var Number plane_Contact_Age_NUM = Float::parseFloat(plane_Contact_Age_TXT)
            plane_Contact_Age_NUM = plane_Contact_Age_NUM.intValue

            if (plane_Contact_Age_NUM <= max_Plane_Contact_Age_NUM) {
            
            //logInfo("plane rules 3","valid data "+ plane_ICAO_TXT)

            if ((plane_ICAO_TXT.substring(0,2) == "40") || (plane_ICAO_TXT.substring(0,3) == "38F") || 
               (plane_ICAO_TXT.substring(0,3) == "394") || (plane_ICAO_TXT.substring(0,3) == "395")) { // filter to pass only packets containing beluga icao prefixes
            
            var Number plane_Altitude_NUM = (Integer::parseInt(plane_Altitude_TXT.toString)).intValue // in feet
            var Number plane_VertRate_NUM = (Integer::parseInt(plane_VertRate_TXT.toString)).intValue // in feet
            var String planeMovement_TXT = "flying level"

            if (plane_VertRate_NUM > 1) planeMovement_TXT = "climbing"
            if (plane_VertRate_NUM < -1) planeMovement_TXT = "descending"

            var String home_Coordinates_TXT = "12.34567,-7.654321,123"
                       plane_Coordinates_TXT = plane_Latitude_TXT + "," + plane_Longitude_TXT + "," + plane_Altitude_TXT
            var PointType home_Coordinates_NUM = PointType.valueOf(home_Coordinates_TXT)
            var PointType plane_Coordinates_NUM =  PointType.valueOf(plane_Coordinates_TXT)
            var Number distanceToPlane_NUM = ((plane_Coordinates_NUM).distanceFrom(home_Coordinates_NUM)/1609)
		    if (LogLevel > 0) {logInfo("plane rules 9","ICAO: " + plane_ICAO_TXT +", Callsign: " + plane_Callsign_TXT +", GPS: {}, Distance: {} miles, " + planeMovement_TXT, plane_Coordinates_NUM, distanceToPlane_NUM, plane_Altitude_NUM)}

                var String BelugaICAO_TXT = ""
                for (var BelugaTable_ICAOLookup_NUM = 1 ; BelugaTable_ICAOLookup_NUM <= sizeOfBelugaFleet_NUM ; BelugaTable_ICAOLookup_NUM = BelugaTable_ICAOLookup_NUM + 1) {
                    BelugaICAO_TXT = transform("MAP", "beluga.map", BelugaTable_ICAOLookup_NUM.toString)

                    if (LogLevel > 0) {logInfo("plane rules 9","comparison loop {} " + BelugaICAO_TXT, BelugaTable_ICAOLookup_NUM)}
                    
                    if  (BelugaICAO_TXT.equals (plane_ICAO_TXT)) {

                        BelugaName_TXT =             transform("MAP", "beluga.map", (BelugaTable_ICAOLookup_NUM + BelugaTable_NameOffset_NUM).toString)
                        BelugaOrigin_NUM =           (Integer::parseInt(plane_Callsign_TXT.substring(4,5)))
                        BelugaDestination_NUM =      (Integer::parseInt(plane_Callsign_TXT.substring(5,6)))
                        BelugaOrigin_TXT =           transform("MAP", "beluga.map", (BelugaOrigin_NUM + BelugaTable_AirportOffset_NUM).toString)
                        BelugaDestination_TXT =      transform("MAP", "beluga.map", (BelugaDestination_NUM + BelugaTable_AirportOffset_NUM).toString)
                        var String planeNumber_TXT = "Beluga" + BelugaTable_ICAOLookup_NUM.toString
                        
                        logInfo("plane rules 10", BelugaName_TXT +", "+ BelugaOrigin_TXT +", "+ BelugaDestination_TXT +", "+ plane_Callsign_TXT + BelugaOrigin_NUM.toString + ", " + BelugaDestination_NUM.toString)
                        sendCommand(planeNumber_TXT, plane_Coordinates_TXT)              
                        sendCommand(Beluga_Alarm,"ON")
                        val mqttActions = getActions("mqtt","mqtt:systemBroker:OpenHABMQTT")
                        mqttActions.publishMQTT("livingroom/pointy/cmnd/gps", plane_Coordinates_TXT)

                        } 
                    }
                }
                    //if (LogLevel == 1) {logInfo("plane rules 12","no match")}
            }
        }
    }
    }
    }
end

rule    "Beluga detection"

when    Member of gADSB_Alerts changed
//when    Time cron "0/20 * * ? * * *"   //every x seconds
then
        sendCommand(Beluga_Alarm,"ON")
end

rule "Beluga proximity alert"
when    Time cron "0/5 * * ? * * *"   //every x seconds
then

// A 3d-printed hand and finger mounted on two servomotors, supposed to point directly at the nearest Beluga. A work-in-progress, semi-abandoned as the maths is beyond me.
//        val mqttActions = getActions("mqtt","mqtt:systemBroker:OpenHABMQTT")
//        mqttActions.publishMQTT("livingroom/pointy/cmnd/gps", plane_Coordinates_TXT)
//        mqttActions.publishMQTT("livingroom/pointy/cmnd/gps", "00.0000,-0.000,1500")

    if (Beluga_Alarm.state == ON) {

        gFX_NEOs_All.sendCommand("0")
        gHSB_Tuyas_All.sendCommand("0,100,100")
        gHSB_LEDs_All.sendCommand("0,100,100")
        gHSB_NEOs_All.sendCommand("0,100,100")

        gPWR_LEDs_All.sendCommand("ON")
        gPWR_NEOs_All.sendCommand("ON")
        Thread::sleep(2000)
        gHSB_Tuyas_All.sendCommand("0,100,0")
        gPWR_LEDs_All.sendCommand("OFF")
        gPWR_NEOs_All.sendCommand("OFF")


//        logInfo("plane rules 11", plane_Coordinates_TXT)

}   else {
        //logInfo("plane rules","rule fired ok")
}
end


rule "Beluga Announcement"
when Item Beluga_Alarm changed to ON
then
playSound("polite_alert.mp3")
Thread::sleep(2000)
say("The Beluga "+ BelugaName_TXT + " is flying from " + BelugaOrigin_TXT + " to " + BelugaDestination_TXT)
end



Comments, tips and pointers would be warmly welcomed.

Just a feeling that it’s the ‘right’ way to do things?

You might have a better experience using Blockly. Syntax errors are much harder to make and frankly, Rules DSL is pretty limited in many way. It no longer even supports the full openHAB API (e.g. you cannot access Item metadata, call other rules, etc.).

Even JS Scripting would probably be a better long term choice. It’s no harder than Rules DSL to write in but has way more generic resources you can use (online docs, tutorials, classes, etc) to learn the language and the interactions with OH are very close to those you already know in Rules DSL.

If it ever becomes something you want to solve, you could catch that error and copy the log files for later analysis. But sometimes it’s not a big enough problem work worry about.

This is one of the coolest and most unique uses of OH I’ve seen in some time. Thanks for posting!

I live within sight of the USAF Academy. I might do something like this to catch all the stuff that files over there.

You asked for it. :wink: I love helping people make their rules better!

  • In Rules DSL, don’t force the type of the variable unless absolutely necessary.
  • Use the built in logging functionality instead. That means you can change the config on the fly without modifying the rule. Your LogLevel > 0 statements can be logDebug or logTrace level and you can throw out the if statements for those.

In OH 3.4+, use the cache instead of global variables. It’s more flexible and will work the same in managed rules and file based rules.

  • Use proper indentation. Every time you see a { the next lines should be indented until you see a }. This makes it much easier to tell the scope of code.
  • Fail fast. Since you don’t do anything when “AIRCRAFT” isn’t in the message, just exit and you can save one level on indentation.
  • This block of code makes a good argument for using JS instead of Rules DSL. The JS in JSON stands for JavaScript. JSON is natively supported meaning you don’t need to go through so many hoops to parse and extract the info you need. Below I’ll show a JS version of the rule for comparison.
  • You can test for this just once and then ignore it forever more, saving a bunch of if statements and perhaps some indents.
  • If you use the logger config as described above, it’s better to keep the logger name the same for the whole rule. Then you can more easily set the logging level of the whole rule.

OK, you win the internet today.

There is a surprisingly little amount of duplicated code.

Applying all of the above plus some reordering and such.

// Constants
val LOGNAME = 'ADSB Processing'
val MAX_PLANE_CONTACT_AGE = 10
val ICAO_PREFIXES = ["40", "38F", "394", "395"] // a better way to look up the ICAO prefixes
val sizeOfBelugaFleet_NUM = 10
val home_Coordinates_NUM = PointType.valueOf("12.34567,-7.654321,123")

// Variables have been moved to the shared cache

rule "adsb planes packet processing"
when 
Member of gADSB_Data changed 
then 
    var packet_TXT = (triggeringItem.state as StringType).toString
    packet_TXT =  ((packet_TXT).toUpperCase) // case conversion
    packet_TXT =  (packet_TXT).replace(" ","") // strips out unwanted spaces

    logInfo(LOGNAME, "modified message" + packet_TXT)

    // Fail fast, exit the rule if AIRCRAFT not in packet
    if(!packet_TXT.contains('AIRCRAFT')) return;

    // Fail fast, exit if the packet has no length
    val packetlength_NUM = Integer.parseInt(transform('JSONPATH', '$.AIRCRAFT.length()'))
    if(packetLength == 0) return;

    // Loop through the packets
    for ( var packetloop_NUM = 0; packetloop_NUM <= packetlength_NUM; packetloop_NUM = packetloop_NUM + 1) {

        var planeTransmissionValidity = true // use booleans for flags

        var plane_ICAO_TXT =               transform("JSONPATH", "$.AIRCRAFT[" + packetloop_NUM.toString+"].HEX", packet_TXT)
        var plane_Callsign_TXT =           transform("JSONPATH", "$.AIRCRAFT[" + packetloop_NUM.toString+"].FLIGHT", packet_TXT)
        var plane_Latitude_TXT =           transform("JSONPATH", "$.AIRCRAFT[" + packetloop_NUM.toString+"].LAT", packet_TXT)
        var plane_Longitude_TXT   =        transform("JSONPATH", "$.AIRCRAFT[" + packetloop_NUM.toString+"].LON", packet_TXT)
        var plane_Contact_Age_TXT =        transform("JSONPATH", "$.AIRCRAFT[" + packetloop_NUM.toString+"].SEEN_POS", packet_TXT)
        cache.shared.put('plane_Altitude_TXT', transform("JSONPATH", "$.AIRCRAFT[" + packetloop_NUM.toString+"].ALTITUDE", packet_TXT)
        var plane_VertRate_TXT =           transform("JSONPATH", "$.AIRCRAFT[" + packetloop_NUM.toString+"].VERT_RATE", packet_TXT)

        logDebug(LOGNAME, '0: packet {} of {}', packetloop_NUM, packetlength_NUM)
        
       // Lets use a little lambda to make the next section a little simpler
       // Test the value against the compareTo, if they are the same it's bad
       // so log it out with the passed in message. Return false if valid is false or 
       // isBad is true.
       // Usage: call with the previous result so as soon as one value is bad we mark
       // the whole packet as bad.
        val isValid = [ value, compareTo, valid, msg | 
            val isBad = value == compareTo;
             if(isBad) { 
                 logDebug(LOGNAME, msg);
             }
             valid || !isBad
        }
        planeTransmissionValidity = isValid.apply(plane_ICAO_TXT, packet_TXT, planeTransmissionValidity, "ICAO fail")
        planeTransmissionValidity = isValid.apply(plane_Callsign_TXT, packet_TXT, planeTransmissionValidity, "callsign fail")
        planeTransmissionValidity = isValid.apply(plane_Contact_Age_TXT, packet_TXT, planeTransmissionValidity, "age fail")
        planeTransmissionValidity = isValid.apply(plane_Altitude_TXT, packet_TXT, planeTransmissionValidity, "alt fail")
        planeTransmissionValidity = isValid.apply(plane_VertRate_TXT, packet_TXT, planeTransmissionValidity, "vert fail")
        planeTransmissionValidity = isValid.apply(plane_Latitude_TXT, packet_TXT, planeTransmissionValidity, "lat fail")
        planeTransmissionValidity = isValid.apply(plane_Longitude_TXT, packet_TXT, planeTransmissionValidity, "long fail")

        // Fail fast, if it's invalid jump to the next iteration of the loop
        if(!planeTransmissionValidity) continue;

        var plane_Contact_Age_NUM = Float::parseFloat(plane_Contact_Age_TXT).intValue

        // Fail fast, if it's too old jump to the next iteration of the loop
        if(plane_Contact_Age_NUM >= MAX_PLANE_CONTACT_AGE) continue;

        // filter to pass only packets containing beluga icao prefixes, jump to the next
        // iteration if this plane doesn't match one of the known prefixes
        if(!ICAO_PREFIXES.contains(plane_ICAO_TXT.substring(0,2) && ! ICAO_PREFIES.contains(plane_ICAO_TXT.substring(0,3)) continue;
            
        var plane_Altitude_NUM = (Integer::parseInt(plane_Altitude_TXT.toString)).intValue // in feet
        var plane_VertRate_NUM = (Integer::parseInt(plane_VertRate_TXT.toString)).intValue // in feet
        var planeMovement_TXT = "flying level"

        if (plane_VertRate_NUM > 1) planeMovement_TXT = "climbing"
        if (plane_VertRate_NUM < -1) planeMovement_TXT = "descending"

        cache.shared.put('plane_Coordinates_TXT', plane_Latitude_TXT + "," + plane_Longitude_TXT + "," + plane_Altitude_TXT)
        var plane_Coordinates_NUM =  PointType.valueOf(plane_Coordinates_TXT)
        var distanceToPlane_NUM = ((plane_Coordinates_NUM).distanceFrom(home_Coordinates_NUM)/1609)
        logDebug(LOGNAME, "ICAO: " + plane_ICAO_TXT +", Callsign: " + plane_Callsign_TXT +", GPS: {}, Distance: {} miles, " + planeMovement_TXT, plane_Coordinates_NUM, distanceToPlane_NUM, plane_Altitude_NUM)

        var BelugaICAO_TXT = ""

        // This code would be simpler if this were in a Map in this rule instead of .map files  
        for (var BelugaTable_ICAOLookup_NUM = 1 ; BelugaTable_ICAOLookup_NUM <= sizeOfBelugaFleet_NUM ; BelugaTable_ICAOLookup_NUM = BelugaTable_ICAOLookup_NUM + 1) {
            BelugaICAO_TXT = transform("MAP", "beluga.map", BelugaTable_ICAOLookup_NUM.toString)

            logDebug(LOGNAME, "comparison loop {} " + BelugaICAO_TXT, BelugaTable_ICAOLookup_NUM)
                    
            if  (BelugaICAO_TXT.equals (plane_ICAO_TXT)) {

                cache.shared.put('BelugaName_TXT',  transform("MAP", "beluga.map", (BelugaTable_ICAOLookup_NUM + BelugaTable_NameOffset_NUM).toString)
                cache.shared.put('BelugaOrigin_NUM', Integer::parseInt(plane_Callsign_TXT.substring(4,5))
                cache.shared.put('BelugaDestination_NUM', Integer::parseInt(plane_Callsign_TXT.substring(5,6))
                cache.shared.put('BelugaOrigin_TXT', transform("MAP", "beluga.map", (BelugaOrigin_NUM + BelugaTable_AirportOffset_NUM).toString)
                cache.shared.put('BelugaDestination_TXT', transform("MAP", "beluga.map", (BelugaDestination_NUM + BelugaTable_AirportOffset_NUM).toString)
                var planeNumber_TXT = "Beluga" + BelugaTable_ICAOLookup_NUM.toString
                        
                logInfo(LOGNAME, BelugaName_TXT +", "+ BelugaOrigin_TXT +", "+ BelugaDestination_TXT +", "+ plane_Callsign_TXT + BelugaOrigin_NUM.toString + ", " + BelugaDestination_NUM.toString)
                postUpdate(planeNumber_TXT, plane_Coordinates_TXT)              
                Beluga_Alarm.postUpdate(ON)
                val mqttActions = getActions("mqtt","mqtt:systemBroker:OpenHABMQTT")
                mqttActions.publishMQTT("livingroom/pointy/cmnd/gps", plane_Coordinates_TXT)

            } 
        }
    }
end

rule    "Beluga detection"
when    Member of gADSB_Alerts changed
then
        Beluga_Alarm.sendCommand(ON) // It's better to use the method over the sendCommand action where possible
end

rule "Beluga proximity alert"
when    Time cron "0/5 * * ? * * *"   //every x seconds
then

   if(Beluga_Alarm.state == OFF) return;

// A 3d-printed hand and finger mounted on two servomotors, supposed to point directly at the nearest Beluga. A work-in-progress, semi-abandoned as the maths is beyond me.
//        val mqttActions = getActions("mqtt","mqtt:systemBroker:OpenHABMQTT")
//        mqttActions.publishMQTT("livingroom/pointy/cmnd/gps", plane_Coordinates_TXT)
//        mqttActions.publishMQTT("livingroom/pointy/cmnd/gps", "00.0000,-0.000,1500")

    gFX_NEOs_All.sendCommand("0")
    gHSB_Tuyas_All.sendCommand("0,100,100")
    gHSB_LEDs_All.sendCommand("0,100,100")
    gHSB_NEOs_All.sendCommand("0,100,100")

    gPWR_LEDs_All.sendCommand("ON")
    gPWR_NEOs_All.sendCommand("ON")
    Thread::sleep(2000)
    gHSB_Tuyas_All.sendCommand("0,100,0")
    gPWR_LEDs_All.sendCommand("OFF")
    gPWR_NEOs_All.sendCommand("OFF")

end

The whole things gets even cleaner in JS because of it’s native support for JSON. I’ll just post the code instead of the full rule.

// This function wrapper is required to get fail fast if this is used in a UI Inline Script action
(function(event) {
  // Constants
  const PointType = Java.type('org.openhab.core.library.types.PointType'); // Need to import the Java class
  const MAX_PLANE_CONTACT_AGE = 10;
  const ICAO_PREFIXES = ["40", "38F", "394", "395"];
  const SIZE_OF_BELUGA_FLEET = 10;
  const HOME_COORDINATES = PointType.valueOf("12.34567,-7.654321,123");
  const REQUIRED_PROPS = ['HEX', 'FLIGHT', 'LAT', 'LON', 'SEEN_POS', 'ALTITUDE', 'VERT_RATE'];
  const BELUGA_TABLE_NAMEOFFSET = 20

  var packetStr = event.itemState.toString().toUpperCase().replace(' ', '');
  console.info("modified message" + packetStr);
  if(packetStr.contains('AIRCRAFT')) return;

  var packet = JSON.parse(packetStr); // depending on the original JSON the numbers will already be parsed
  if(packet.AIRCRAFT.length == 0) return;

  packet.AIRCRAFT.forEach((record, index) => {

    // Because everything is in the record, you don't really need to create all those variables

    console.debug("packet " + index + " of " packet.AIRCRAFT.length);

    // Make sure the record has all the properties we need
    const availableValues = record.keys;
    if(!record.keys.every(REQUIRED_PROPS)) {
      console.debug('The following properties are missing from the record: ' + REQUIRED_PROPS.filter(record.keys));
      continue;
    }

    // Return if record is too old
    if(record.SEEN_POS > MAX_PLANE_CONTACT_AGE) continue;

    // Return if the record is for some other plane
    if(!ICAO_PREFIXES.contains(record.HEX.substring(0,2)) 
      && !ICAO_PREFIXES.contains(record.HEX.substring(0,3)) continue;

    let planeMovement = "flying level";
    if(record.VERT_RATE > 1) planeMovement = "climbing";
    else if(record.VERT_RATE < -1) planeMovement = "descending";

    const planeCoordinates = PointType.valueOf(record.LAT + ', ' + record.LON + ', ' + record.ALTITUDE);
    const distance = planeCoordinates.distanceFrom(HOME_COORDINATES)/1609;
    console.debug('ICAO: ' + record.HEX + ', Callsign: ' + record.FLIGHT +  planeMovement 
                  +', GPS: ' + planeCoordinates.toString() + ', Distance: ' + distance.toString() 
                  + ' miles, ' + record.ALTITUDE); 

    // Let's put the mapping for the Beluga's into a JSON file instead of MAP files, we'll leave the
    // origin/dest stuff in the map file though
    const mappingsStr = actions.Exec.executeCommandLine('cat', '/path/top/JSON/beluga.json');
    const mappings = JSON.parse(mappingStr);
    const m = mappings[record.HEX]; // Assumes each ICAO has a record of mappings

    // Record the useful info
    // You don't actually use these in your other rules. I'm assuming there are some rules not shown that
    // use these. If not, just use local variables.
    cache.shared.put('plane_altitude', record.ALTITUDE);
    cache.shared.put('plane_coordinates', placeCoordinates);
    cache.shared.put('BelugaName', m.name);
    cache.shared.put('BelugaOrigin', actions.Transform.transform('MAP', 'beluga.map', record.FLIGHT.substring(4,5)));
    cache.shared.put('BelugaDestination', actions.Transform.transform('MAP', 'beluga.map', record.FLIGHT.substring(5,6)));
    
    items[m.locationItemName].postUpdate(planeCoordinates.toString());
    items.Beluga_Alarm.sendCommand('ON');
    const mqttAction = actions.Things.getActions('mqtt', 'mqtt:systemBroker:OpenHABMQTT');
    mqttAction.publishMQTT('livingroom/pointy/cmnd/gps', planeCoordinates.toString());

  });

})(event)

Note, I just typed in the the code above. There is almost certainly going to be typos.

Also, in the unlikely event that you have two Belugas being tracked at the same time I don’t think any of these rules will work properly.

Thank you for your kind words! I’m overwhelmed to hear from one of the creators of Openhab. What an honour.

The original post was incomplete. I’ve added a lot to it. Don’t miss them.

I mainly did the above to show some simpleish things you can do to make the code a little easier to use and understand. Let me know if you have any questions!

My goodness. So much useful information here to take in… and code to rewrite. You have given me many new ways to think.

My eyes have been opened. Can’t thank you enough.

Having trouble with quite a lot of the code you wrote for me. I’m aware it wasn’t supposed to just be a drop-in replacement, but I’ve caught the simple things like a single speech quote needs to be a double:

 if(!packet_TXT.contains('AIRCRAFT')) return;

doesn’t work whereas

if(!packet_TXT.contains("AIRCRAFT")) return;

does.
So I’ve fixed that.
I couldn’t get logDebug to show in tail (http://openhabian.local:9001) so I’ve replaced logDebug with logWarn in my code. Only logInfo, logWarn and logError messages appear.

        val isValid = [ value, compareTo, valid, msg | 
            val isBad = value == compareTo;
             if(isBad) { 
                 logWarn(LOGNAME, msg);
             }
             valid || !isBad
        }
        planeTransmissionValidity = isValid.apply(plane_ICAO_TXT, packet_TXT, planeTransmissionValidity, "ICAO fail")
        planeTransmissionValidity = isValid.apply(plane_Callsign_TXT, packet_TXT, planeTransmissionValidity, "callsign fail")
        planeTransmissionValidity = isValid.apply(plane_Contact_Age_TXT, packet_TXT, planeTransmissionValidity, "age fail")
        planeTransmissionValidity = isValid.apply(plane_Altitude_TXT, packet_TXT, planeTransmissionValidity, "alt fail")
        planeTransmissionValidity = isValid.apply(plane_VertRate_TXT, packet_TXT, planeTransmissionValidity, "vert fail")
        planeTransmissionValidity = isValid.apply(plane_Latitude_TXT, packet_TXT, planeTransmissionValidity, "lat fail")
        planeTransmissionValidity = isValid.apply(plane_Longitude_TXT, packet_TXT, planeTransmissionValidity, "long fail")

doesn’t work, and I think it’s due to something in the Lambda?
When I replaced the final curly bracket with a square one VSCode stopped complaining about it, but apart from that I think what’s supposed to happen here is that once one error occurs, planeTransmissionValidity is supposed to get set to false and all the successive tests should fail.

This doesn’t happen.
I added logging like this:

        planeTransmissionValidity = isValid.apply(plane_ICAO_TXT, packet_TXT, planeTransmissionValidity, "ICAO fail")
        logWarn(LOGNAME, "icao is {} " + plane_ICAO_TXT, planeTransmissionValidity)
        
        planeTransmissionValidity = isValid.apply(plane_Callsign_TXT, packet_TXT, planeTransmissionValidity, "callsign fail")
        logWarn(LOGNAME, "callsign is {} " + plane_Callsign_TXT, planeTransmissionValidity )

        planeTransmissionValidity = isValid.apply(plane_Contact_Age_TXT, packet_TXT, planeTransmissionValidity, "age fail")
        logWarn(LOGNAME, "age is {} " + plane_Contact_Age_TXT, planeTransmissionValidity)

        planeTransmissionValidity = isValid.apply(plane_Altitude_TXT, packet_TXT, planeTransmissionValidity, "alt fail")
        logWarn(LOGNAME, "alt is {} " + plane_Altitude_TXT, planeTransmissionValidity)

        planeTransmissionValidity = isValid.apply(plane_VertRate_TXT, packet_TXT, planeTransmissionValidity, "vert fail")
        logWarn(LOGNAME, "vert is {} " + plane_VertRate_TXT, planeTransmissionValidity)

        planeTransmissionValidity = isValid.apply(plane_Latitude_TXT, packet_TXT, planeTransmissionValidity, "lat fail")
        logWarn(LOGNAME, "lat is {} " + plane_Latitude_TXT, planeTransmissionValidity)

        planeTransmissionValidity = isValid.apply(plane_Longitude_TXT, packet_TXT, planeTransmissionValidity, "long fail")
        logWarn(LOGNAME, "long is {} " + plane_Longitude_TXT, planeTransmissionValidity)

and tail gives me this:

2024-04-17 14:56:56.758 [WARN ] [re.model.script.ADSB Processing test] - packet 0 of 14
2024-04-17 14:56:56.760 [WARN ] [re.model.script.ADSB Processing test] - icao is true 000000
2024-04-17 14:56:56.761 [WARN ] [re.model.script.ADSB Processing test] - callsign fail
2024-04-17 14:56:56.762 [WARN ] [re.model.script.ADSB Processing test] - callsign is true {"NOW":1713362215.8,
2024-04-17 14:56:56.763 [WARN ] [re.model.script.ADSB Processing test] - age fail
2024-04-17 14:56:56.764 [WARN ] [re.model.script.ADSB Processing test] - age is true {"NOW":1713362215.8,
2024-04-17 14:56:56.766 [WARN ] [re.model.script.ADSB Processing test] - alt is true 30300
2024-04-17 14:56:56.767 [WARN ] [re.model.script.ADSB Processing test] - vert fail
2024-04-17 14:56:56.768 [WARN ] [re.model.script.ADSB Processing test] - vert is true {"NOW":1713362215.8,
2024-04-17 14:56:56.769 [WARN ] [re.model.script.ADSB Processing test] - lat fail
2024-04-17 14:56:56.770 [WARN ] [re.model.script.ADSB Processing test] - lat is true {"NOW":1713362215.8,
2024-04-17 14:56:56.771 [WARN ] [re.model.script.ADSB Processing test] - long fail
2024-04-17 14:56:56.772 [WARN ] [re.model.script.ADSB Processing test] - long is true {"NOW":1713362215.8,
2024-04-17 14:56:56.773 [ERROR] [internal.handler.ScriptActionHandler] - Script execution of rule with UID 'testing-1' failed: For input string: "{"NOW":1713362215.8,

I’ve tried Googling “openhab lambdas” for help, and found this:

So while this may not apply to my situation, now I’m (even more) confused.

Also, although the problem above breaks the Rule, I’m also getting errors from

cache.shared.put("plane_Altitude_TXT", transform("JSONPATH", "$.AIRCRAFT[" + packetloop_NUM.toString+"].ALTITUDE", packet_TXT)

whereas

var plane_Altitude_TXT = transform("JSONPATH", "$.AIRCRAFT[" + packetLoop_NUM.toString+"].ALTITUDE", packet_TXT)

works.

I’m really sorry if I come across as ungrateful or picky. I really appreciate all the help you’ve given me and as always I suspect there are simple fixes that are hiding in the gaps of my knowledge.
Could these errors be down to a missing Binding?

Indeed it was more intended to be a demonstration of alternative ways you can do the same thing in the context of the rule you already undersand.

Hmmm, that shouldn’t matter. Both types result in a String. What was the error?

I forgot to write about that.

OK, the logging built into OH supports levels. The levels are, from most verbose to least: TRACE, DEBUG, INFO, WARN, and ERROR. In addition, each part of OH has it’s own logger. Through edits to the karaf console, the API Explorer, or by editing $OH_USERDATA/etc/log4j2.xml you can change the level that individual loggers write.

By default, rules log at the INFO level. But you can change that. I don’t exactly remember the logger names used by Rules DSL rules but I think they follow the format of org.openhab.core.model.script.<LOGNAME> where <LOGNAME> is the first argument you pass to the call to logInfo et. al.

So to see the debug level log statements, change that logger to DEBUG level and all DEBUG and above log statements will be shown. To do so by editing log4j2.xml, near the bottom in the loggers section add the line:

<Logger level="DEBUG" name="org.openhab.core.model.script.ADSB Processing"/>

With this, all the DEBUG and above log statements from this rule (assuming you use LOGNAME like I showed) will be logged. When you are done with those log statemetns, set it back to INFO and it will only show the INFO and above log statements.

This lets you adjust what gets logged without changing the actual code of the rule.

Doh!, the expression is wrong. I want it to return false if either valid is already false or isBad is true. It should be valid && !isBad I think. Only if valid is true and isBad is false will the lambda return true. In all other cases it shoudl return false, indicating that the packet is invalid.

And that’s a typo too and a good catch. It should close with a '] not }`.

Correct, that is what is supposed to happen. I messed up the return value expression though by using || instead of &&.

Indeed, that doesn’t apply to your situation. Lambdas that are local to a rule inherit the variables that exist up to the point where the lambda is created. That thread is about “global” lambdas defined at the top of the file.

Over all, I recommend against Rules DSL for new development of rules anyway because it’s limited in many ways including in how lambdas work as a poor replacement for functions and procedured.

I think, with the fix of the two errors above with the lambda (type with the closing ] and logic error in the return expression) it should work. If not let me know.

This comes from not double checking the docs before forging ahead. In JS Scripting the cache is accessed using cache.shared but in Rules DSL it’s accessed using sharedCache. So everywhere you see cache.shared replace that with sharedCache.

But again, if you are not actually using these variables in other rules, there’s no reason to put them into the cache in the first place. So double check whether you need them in the cache at all.

Not at all. No one creates code correctly the first time. And since I don’t know what the JSON looks like that comes into this rule there was really no way I could actually test it before posting. It is bound to have errors both typos and logical. I’m happy you are giving it a go at making it work and more than happy to help with the errors.

Heh. I caught this by RTFM mere seconds after posting.

Now… the early exit from an iteration thing?

(line 73)        if (!planeTransmissionValidity) continue;

throws the error

[internal.handler.ScriptActionHandler] - Script execution of rule with UID 'testing-1' failed: The name 'continue' cannot be resolved to an item or type; line 73, column 41, length 8 in testing

If I invert this check to

if (planeTransmissionValidity) return;

the error goes away and the code fails later (lol!), but I’m not sure if the code then runs the way it’s supposed to.

[edit]

if (planeTransmissionValidity) return;

ends the rule processing, so that’s not what I want.

I seem to need the equivalent to BASIC’s next x command.

Shoot! Again I should have looked it up. It looks like Xtend (and therefore Rules DSL) doesn’t actually have break and continue. :sob: I just assumed that it would since those are pretty standard programming concepts.

OK, that’s not going to work then. You can’t fail fast inside the for loop then. You’ll have use an if statement that doesn’t fail fast in that case.

You may start to notice that I’m no longer a fan of Rules DSL now that we have better options in OH which are just as easy but actually implement all the regular stuff you expect a programming language to have.

Yep, that’s why I didn’t use return there. You can see if break is supported even though it’s not mentioned in the docs. Maybe that will skip to the next iteration but I suspect it will break out of the for loop entirely if it’s supported at all.

I think in Rules DSL there will be no way to do it fail fast unfortunately.

But this all does make me realize I could have done the JS version a little better. This

  packet.AIRCRAFT.forEach((record, index) => {

    // Because everything is in the record, you don't really need to create all those variables

    console.debug("packet " + index + " of " packet.AIRCRAFT.length);

    // Make sure the record has all the properties we need
    const availableValues = record.keys;
    if(!record.keys.every(REQUIRED_PROPS)) {
      console.debug('The following properties are missing from the record: ' + REQUIRED_PROPS.filter(record.keys));
      continue;
    }

    // Return if record is too old
    if(record.SEEN_POS > MAX_PLANE_CONTACT_AGE) continue;

    // Return if the record is for some other plane
    if(!ICAO_PREFIXES.contains(record.HEX.substring(0,2)) 
      && !ICAO_PREFIXES.contains(record.HEX.substring(0,3)) continue;

should be replaced with:

  packet.AIRCRAFT.filter( record => record.keys.every(REQUIRED_PROPS) )
                 .filter( record => record.SEEN_POS <= MAX_PLANE_CONTACT_AGE )
                 .filter( record => ICAO_PREFIXES.contains(record.HEX.substring(0,2)) || ICAO_PREFIXES.contains(record.HEX.substring(0,3))
                 .forEach((record, index) => {

    // Because everything is in the record, you don't really need to create all those variables

    console.debug("packet " + index + " of " packet.AIRCRAFT.length);

Because we actually have the records as an array, we can filter down to just those records that are relevant. Using Rules DSL and the transform that’s not available to us and we need to explicitly loop through each record.

Please note, I’m not trying to be difficult, or demanding. I’m in a situation where nobody I know IRL is interested in coding, or electronics, or 3d printing, or science, physics, astronomy etc. Thank you so much for your help.

No worries at all. I love to help people with these sorts of things. This is my kind of fun!

I’m not sure Blockly is for me, I’m more into manipulating text files. But I’m definitely going to look into JS scripting, seeing as how I’m already working with JSON files. I have no training at programming, it’s all Google and working out what I got wrong last time.

Plus, please remember that some amazing Coding God recently told me that I’d won the Internet. My head is a little swollen, and my face hurts from smiling so much. Cheers, forum users!

1 Like