[Solved] JSON array? loop it in rule; rapid fire rule: concurrency?

OH 2.5.10 on rPi3

I am trying to dissect a JSON string I get from Sonoff door contacts via rtl_433 and MQTT.

Captured like so:

rtl_433 -X 'n=Sonoff-DW2,m=OOK_PWM,s=304,l=820,r=7992,g=752,t=204,y=0' -F json  | mosquitto_pub -h 192.168.1.5 -p 1883 -t ArgyleCourt/Shed/testing -l
{"time" : "2020-11-04 22:38:17", "model" : "Sonoff-DW2", "count" : 4, "num_rows" : 4, "rows" : [{"len" : 25, "data" : "5bd73e8"}, {"len" : 25, "data" : "5bd73e8"}, {"len" : 23, "data" : "5bd73e"}, {"len" : 0, "data" : ""}], "codes" : ["{25}5bd73e8", "{25}5bd73e8", "{23}5bd73e", "{0}"]}

I have done JSON strings before, but this one has a set of [] with “rows” of data.
I was thinking of looping through it, but do not know how to address the (what looks to me like an) array.
However, I only want to accept the data if the len = 25; after findign one, exit the while ().

rule "Sonoff sensors"
    when
        Item rtl_1_DataReceived changed
    then
        val String rtl433_json = (rtl_1_DataReceived.state as StringType).toString
        val String model = transform("JSONPATH", "$.model", rtl433_json)

        if (model == "Sonoff-DW2")
        {
            val Number msg_count = Integer::parseInt(transform("JSONPATH", "$.count", rtl433_json))

            // {"time" : "2020-11-04 22:29:35", "model" : "Sonoff-DW2", "count" : 2, "num_rows" : 2, "rows" : [{"len" : 25, "data" : "5bd73e8"}, {"len" : 6, "data" : "5c"}], "codes" : ["{25}5bd73e8", "{6}5c"]}
            // {"time" : "2020-11-04 22:29:37", "model" : "Sonoff-DW2", "count" : 2, "num_rows" : 2, "rows" : [{"len" : 25, "data" : "5bd73e8"}, {"len" : 16, "data" : "5bd7"}], "codes" : ["{25}5bd73e8", "{16}5bd7"]}
            // {"time" : "2020-11-04 22:38:17", "model" : "Sonoff-DW2", "count" : 4, "num_rows" : 4, "rows" : [{"len" : 25, "data" : "5bd73e8"}, {"len" : 25, "data" : "5bd73e8"}, {"len" : 23, "data" : "5bd73e"}, {"len" : 0, "data" : ""}], "codes" : ["{25}5bd73e8", "{25}5bd73e8", "{23}5bd73e", "{0}"]}

            var i = 0
            while( (i = i + 1) <= msg_count)
            {
              // pseudo code
              // if (len = 25)
              // {
              //     sensor_id = the string in "data"
              //     break 
              // }
              // switch sensor_id {...} I have got that...
            }
         }
end

In case you wonder about the array values… these contact sensors are supposed to send one data frame with an id (anything with len < 25 is incomplete). I suspect the reed contact bounces, hence the forming of the array… and the incomplete data (id).

This will be another rule problem to solve: suppress acceptance of further messages for x seconds. Toggle contact state (open|closed).

Any hints appreciated.

I’ve a bunch of shelly devices that respond with a JSON array like this:

["button","sensor","poweron","periodic"]

I iterate it using this loop:

    var String testString2 = ""
    var i = 0

    while (i < testStringLength ) {
        testString2 = transform("JSONPATH", "$.[" + i +"]", jsonString) 
        // logInfo(logger, "testString2: " + testString2)
        if (testString2 == "poweron")
        {
            logWarn(logger, "testString2: " + testString2)
        }
        
        i++
    }

YMMV, but you could use it as a starting point.

Regards,
Bjarne

1 Like

The "$.[" + i +"]" got me on the right path.

the rule looks like this now (I have commented it for better comprehension):

rule "Sonoff sensors"
    when
        Item rtl_1_DataReceived changed
    then
        // rtl433_json contains recieved JSON string
        val String rtl433_json = (rtl_2_DataReceived.state as StringType).toString

        // rtl_433 capture string:
        // rtl_433 -X 'n=Sonoff-DW2,m=OOK_PWM,s=304,l=820,r=7992,g=752,t=204,y=0'
        // -F json | mosquitto_pub -h 192.168.1.5 -p 1883 -t ArgyleCourt/Shed/testing -l


        // model used by all sensors (added by RTL433)
        val String model = transform("JSONPATH", "$.model", rtl433_json)


        // The PIR sends two signals, identified as follows:

        // time      : 2020-11-04 12:45:44
        // model     : Generic-Remote
        // House Code: 54978
        // Command   : 190
        // Tri-State : 1ZZX100XX11X
        // {"time" : "2020-11-04 16:40:06", "model" : "Generic Remote", "id" : 54978, "cmd" : 190, "tristate" : "1ZZX100XX11X"}

        // time      : 2020-11-04 12:45:44
        // model     : Smoke-GS558
        // id        : 27163
        // unit      : 11
        // learn     : 0
        // Raw Code  : 7d436b
        // {"time" : "2020-11-04 16:40:06", "model" : "Smoke detector GS 558", "id" : 27163, "unit" : 11, "learn" : 0, "code" : "7d436b"}



        //val String sensor_id = transform("JSONPATH", "$.id", rtl433_json)
        //if (sensor_id == "54978")
        //{
        //    logInfo("Sonoff.0.00", "rtl433_json..............: {}", rtl433_json)
        //}



        // listening to the PIR sensor
        if (model == "Generic-Remote")
        {
            val String json_copy_1 = rtl433_json
            val String PIR_id = transform("JSONPATH", "$.id", json_copy_1)
            //logInfo("Sonoff.1.00", "rtl433_json..............: {}", json_copy_1)
            logInfo("Sonoff.1.01", "model....................: {}", model)
            logInfo("Sonoff.1.02", "PIR_id...................: {}", PIR_id)

            switch PIR_id
            {
                case "54978":
                {
                    // Shed_NLO_PIR
                    Shed_NLO_PIR_LUP.postUpdate(new DateTimeType())
                }
            }
        }

        // listening to the Sonoff-DW2 contact sensors
        if (model == "Sonoff-DW2")
        {
            val String json_copy = rtl433_json
            val Number num_rows = Integer::parseInt(transform("JSONPATH", "$.num_rows", json_copy))
            logInfo("Sonoff.1.03", "model....................: {}", model)
            logInfo("Sonoff.1.04", "num_rows.................: {}", num_rows)

            // {"time" : "2020-11-04 22:29:35", "model" : "Sonoff-DW2", "count" : 2, "num_rows" : 2, "rows" : [{"len" : 25, "data" : "5bd73e8"}, {"len" : 6, "data" : "5c"}], "codes" : ["{25}5bd73e8", "{6}5c"]}
            // {"time" : "2020-11-04 22:29:37", "model" : "Sonoff-DW2", "count" : 2, "num_rows" : 2, "rows" : [{"len" : 25, "data" : "5bd73e8"}, {"len" : 16, "data" : "5bd7"}], "codes" : ["{25}5bd73e8", "{16}5bd7"]}
            // {"time" : "2020-11-04 22:38:17", "model" : "Sonoff-DW2", "count" : 4, "num_rows" : 4, "rows" : [{"len" : 25, "data" : "5bd73e8"}, {"len" : 25, "data" : "5bd73e8"}, {"len" : 23, "data" : "5bd73e"}, {"len" : 0, "data" : ""}], "codes" : ["{25}5bd73e8", "{25}5bd73e8", "{23}5bd73e", "{0}"]}

            // checked: it is valid JSON!
            // {
            //   "time" : "2020-11-04 22:38:17",
            //   "model" : "Sonoff-DW2",
            //   "count" : 4,
            //   "num_rows" : 4,
            //   "rows" : [
            //     {"len" : 25, "data" : "5bd73e8"},
            //     {"len" : 25, "data" : "5bd73e8"},
            //     {"len" : 23, "data" : "5bd73e"},
            //     {"len" : 0, "data" : ""}
            //   ],
            //   "codes" : [
            //     "{25}5bd73e8",
            //     "{25}5bd73e8",
            //     "{23}5bd73e",
            //     "{0}"
            //   ]
            // }
/*
2020-11-05 11:04:52.403 [INFO ] [el.core.internal.ModelRepositoryImpl] - Refreshing model 'sonoff.rules'
2020-11-05 11:05:53.359 [INFO ] [marthome.model.script.Irrigation1.t1] - ..< turning ............................ Irrigation1_3 OFF
2020-11-05 11:05:59.518 [INFO ] [e.smarthome.model.script.Sonoff.1.03] - model....................: Sonoff-DW2
2020-11-05 11:05:59.523 [INFO ] [e.smarthome.model.script.Sonoff.1.04] - num_rows.................: 1
2020-11-05 11:05:59.537 [INFO ] [e.smarthome.model.script.Sonoff.1.05] - probeString..............: {len=1, data=8}
2020-11-05 11:05:59.550 [INFO ] [e.smarthome.model.script.Sonoff.1.06] - dataLength...............: 1
2020-11-05 11:05:59.564 [INFO ] [.smarthome.model.script.Sonoff.1.07a] - dataValue................: 8
2020-11-05 11:05:59.579 [INFO ] [e.smarthome.model.script.Sonoff.1.05] - probeString..............: {"time" : "2020-11-05 11:05:51", "model" : "Sonoff-DW2", "count" : 1, "num_rows" : 1, "rows" : [{"len" : 1, "data" : "8"}], "codes" : ["{1}8"]}
*/

            var i = 0
            var String dataLength = ""
            var String probeString = ""
            var String Sonoff_DW_id = "123456"

            while (i <= num_rows) {

                // should return the len:data pair; e.g. {"len" : 25, "data" : "5bd73e8"}
                // 2020-11-05 10:00:49.479 [INFO ] [e.smarthome.model.script.Sonoff.1.05] - probeString..............: {len=1, data=8}
                probeString = transform("JSONPATH", "$.rows.[" + i + "]", json_copy)
                //logInfo("Sonoff.1.05", "probeString..............: {}", probeString)

                if (probeString.length() < 25)
                {
                    logInfo("Sonoff.1.06", "probeString..............: {}", probeString)
                    // 2020-11-05 10:00:49.488 [INFO ] [e.smarthome.model.script.Sonoff.1.06] - dataLength...............: 1
                    dataLength = transform("JSONPATH", "$.rows.[" + i + "].len", json_copy)
                    logInfo("Sonoff.1.07", "dataLength...............: {}", dataLength)

                    if (dataLength == "25")
                    {
                        Sonoff_DW_id = transform("JSONPATH", "$.rows.[" + i + "].data", json_copy)
                        logInfo("Sonoff.1.08", "Sonoff_DW_id.............: {}", Sonoff_DW_id)

                        // exit out of while after having found one complete id (len =25)
                        // since break and return lead to undesired results,
                        // up the i to exit out of while()
                        i = num_rows.intValue
                    }
                }

                i++
            }


            switch Sonoff_DW_id
            {
                case "5bd73e8":
                {
                    // KDL kitchen window
                    Shed_NLO_Window_Kitchen_LUP.postUpdate(new DateTimeType())

                }

                // ... and all others :)
            }
        }
end

However, I feel there is a concurrency problem.
Both sensors, the PIR and door contact send multiple messages; the PIR being the worst, sending 20 plus messages every time something moves.

I thought I could trick the rule in finishing one execution before being triggered again. (That was a silly idea.)
Maybe I have to use locks??

I read Rich’s Explain lock() command?, and locks should be avoided, but how else can I prevent the rapid firing of the rule?

1 Like

Take a look at the Design Pattern: Debounce described by @rlkoshak.That might be a better avenue.

Thanks… yes… good ol’ Rich :slight_smile: It looks like he’s solved every problem :slight_smile: Thankfully.

This will become a monster rule. I have then sensors, all need to be debounced! :frowning:

Well, the whole thing was a test exercise, and it has proven that a reed contact on a window and a wire in the wall to a micro-controller somewhere is the cleanest option. I am glad I am building and testing tech before settling on a solution in the house :slight_smile:

1 Like

I’m glad you could use my suggestion as a starting point!!!

And, yes, if you look for an answer, it is likely that Rich has it :wink:

So there are a few options available to you.

  1. If you have control over the sensor, can you filter the messages there? It’s always best when you can move the debounce closer to the device itself.

  2. Do you have to process every message? If not you can lock the rule and throw away any messages that come in while the Rule is locked. There is only a problem when you queue the rules up with the lock.

  3. Take this as a reason/opportunity to explore OH 3 or at least the NGRE in 2.5 and programming rules in Jython or JavaScript. This rule engine: 1. does not have the same problem with running out of rules threads so locking isn’t dangerous and 2. actually locks the rule for you making it so only one instance of the rule can run at a time, queueing up the triggers in a FIFIO, which is essentially what you want.

  4. There is now a Debounce Profile I think, but it’s only available on OH 3.

If you were to move to Jython rules, you could download https://github.com/rkoshak/openhab-rules-tools/tree/main/debounce and add debounce metadata to your Items that you want to debounce. If you are willing to wait a few weeks, there will be a JavaScript version posted there as I’m currently working on that.

But even if you stick with Rules DSL, if you apply Design Pattern: Associated Items you only need one rule, one Group, and careful naming of your Items.

Sometimes it’s even right! Not always though. :smiley:

Anyway, this is a problem that has been solved for you but taking advantage of it requires updating.

2 Likes