Nice graphic alarm (Pir sensors) visualization in HABPanel

I am quite new to openHAB but love it’s possibilities. I have spent a lot of hours reading topics (not only) on this forum which is very helpful and now, when I have some working solution of (I think) new idea, I decided to share it with you. Maybe this will help or inspire some of you, maybe you will have some suggestions how to improve it. So, stop talking, start showing :slight_smile:

The setup:

Hardware - I am using ESP8266 NodeMCU v1, where I connected (currently 2, for proof of concept) cheap PIR sensors HC-SR501. The ESP is connected to my MQTT Broker running on RaspberryPi 3 (where also openhabianpi is running). The ESP is submitting value 0 when no motion is detected and 1 when motion was detected to topics Garden/Alarm/Pir01 and Garden/Alarm/Pir02. H / L pin on HC-SR501 is set to H so sensor keeps HIGH signal while there is a motion being detected.

Software - I love dynamic icons and HABPanel. As I plan to have many more motion sensors (probably 20+) I wanted to have all of them nicely visualized. I dreamt of ortophoto of my house and garden where PIR icons are placed exactly where PIR sensors are physically placed.

I also wanted to be able to easily track the path of the person moving (for many reasons). So I decided to change the icon color (almost like a traffic lights) depending on a time elapsed from the motion detected. So when motion detected (+5 more seconds) the icon is red, then for next 5 secs orange, then orange light and finally yellow. Afterwards is set back to original grey. See also below…
Motion detected + 5s = dmotion-red > dmotion-orange > dmotion-orangelight > dmotion-yellow > dmotion

This will allow you to track the move easily when more sensors are on the way.

OK, that is really nice. But not enough for me :slight_smile: I would also like to change the icon content (so not only the color) to visualize the direction of the move, if known. I have already an idea but haven’t created a rule for that yet, so this is something to be done later. Generally this shouldn’t be hard as if you have e.g. 2 PIR sensors not far away from each other (typically two on each side of the house) then you can easilly get the direction depending which sensor was triggered first and which one the second. Then you can change the icon (keeping the color set by timeout from the trigger).
For example dmotion-red_left dmotion-orange_right dmotion-yellow_up dmotion-red_down

So I have created two items for each PIR sensor. One Number item receiving the raw data from ESP via MQTT (0/1 values). The other item is String and it’s value is changed by a rule, which is triggered when the raw item receives update 1 (= motion detected). The value of String item must change according to sub-icon from the group of icons. This is nicely described in Icons openhab guideline.

Just a short recap - icon named motion.svg is main and will be used always when there is no sub-icon name used in string item value. Sub icons are named motion-red.svg where the “-” is separator of the sub-icon name and the main icon name (which is also icons group name). The “red” is then the sub-icon identification. So when the item value is “red” the sub-icon motion-red.svg will be used by openhab. I named my main icon dmotion.svg to have different name from the standard openhab motion icon. Don’t forget NOT to use capital letters and special characters (?) when giving icons names. Finally, you should place your icons to openhabian config folder/icons/classic/ (in my case: o:\openhab2-conf\icons\classic\dmotion.svg). Of course, these dynamic icons work in the sitemap too.

OK, we have dynamic icons ready, now set the items:

Group gPirs // Groups not used in rules yet but I think, I will use them
Group gPirsOutside
Group gPirsInside
Number Pir01_raw "PIR1 [%s]" <dmotion> (gPirs, gPirsOutside) {mqtt="<[mosquitto:Garden/Alarm/Pir01:state:default]"} //receiving 0 (no motion detected) or 1 (motion detected)
Number Pir02_raw "PIR2 [%s]" <dmotion> (gPirs, gPirsOutside) {mqtt="<[mosquitto:Garden/Alarm/Pir02:state:default]"}

String Pir01 "PIR1 [%s]" <dmotion> (gPirs, gPirsOutside) //changed by rule
String Pir02 "PIR2 [%s]" <dmotion> (gPirs, gPirsOutside)

And finally the rule: (EDIT 9.1.2018: This rule was not very effective and even with some errors, see the discussion below. The improved and fully working rule is in discussion below in my post from 9.1.2018. New Items Pir01_Timer and Pir02_Timer also needed to be added as described by Rich)

var Timer TimerPir01 = null
var Timer TimerPir02 = null

// ************************* PIR 01 ***************************
//Update PIR when PIR_raw received update
rule "Pir01 on"
when
    Item Pir01_raw received update 1
then
    Pir01.postUpdate("red")//by setting the item value to "red" you are also changing the dynamic icon
    TimerPir01?.cancel
    TimerPir01 = null //cancel any previously running timer
    if(TimerPir01 === null || TimerPir01.hasTerminated) {
        TimerPir01 = createTimer(now.plusSeconds(5), [|
            TimerPir01?.cancel
            TimerPir01 = null
            Pir01.postUpdate("orange")
            if(TimerPir01 === null || TimerPir01.hasTerminated) {
                TimerPir01 = createTimer(now.plusSeconds(5), [|
                    TimerPir01?.cancel
                    TimerPir01 = null
                    Pir01.postUpdate("orangelight")
                    if(TimerPir01 === null || TimerPir01.hasTerminated) {
                        TimerPir01 = createTimer(now.plusSeconds(5), [|
                            TimerPir01?.cancel
                            TimerPir01 = null
                            Pir01.postUpdate("yellow")
                            if(TimerPir01 === null || TimerPir01.hasTerminated) {
                                TimerPir01 = createTimer(now.plusSeconds(5), [|
                                TimerPir01?.cancel
                                TimerPir01 = null
                                Pir01.postUpdate("off")//finally switch the item to any value which is NOT used in dynamic sub-icons name will use the main icon from the group
                                ]) //yellow end
                            }   //yellow end
                        ]) //orangelight end
                    }   //orangelight end
                ]) //orange end
            }   //orange end
        ]) //red end
    } //red end
   
end


// ************************* PIR 02 ***************************
//Update PIR when PIR_raw received update
rule "Pir02 on"
when
    Item Pir02_raw received update 1
then
    Pir02.postUpdate("red") //by setting the item value to "red" you are also changing the dynamic icon
    TimerPir02?.cancel
    TimerPir02 = null //cancel any previously running timer
    if(TimerPir02 === null || TimerPir02.hasTerminated) {
        TimerPir02 = createTimer(now.plusSeconds(5), [|
            TimerPir02?.cancel
            TimerPir02 = null
            Pir02.postUpdate("orange")
            if(TimerPir02 === null || TimerPir02.hasTerminated) {
                TimerPir02 = createTimer(now.plusSeconds(5), [|
                    TimerPir02?.cancel
                    TimerPir02 = null
                    Pir02.postUpdate("orangelight")
                    if(TimerPir02 === null || TimerPir02.hasTerminated) {
                        TimerPir02 = createTimer(now.plusSeconds(5), [|
                            TimerPir02?.cancel
                            TimerPir02 = null
                            Pir02.postUpdate("yellow")
                            if(TimerPir02 === null || TimerPir02.hasTerminated) {
                                TimerPir02 = createTimer(now.plusSeconds(5), [|
                                TimerPir02?.cancel
                                TimerPir02 = null
                                Pir02.postUpdate("off") //finally switch the item to any value which is NOT used in dynamic sub-icons name will use the main icon from the group
                                ]) //yellow end
                            }   //yellow end
                        ]) //orangelight end
                    }   //orangelight end
                ]) //orange end
            }   //orange end
        ]) //red end
    } //red end
   
end

Nice, now we have dynamic icons changed by the rule. Now get back to the HABPanel. I created switch for raw item and dummy widget for string item.

image

In the dummy widget I set the dynamic icon:

This is not bad and look like this:

image

But I wanted more! I wanted something like a floorplan in HABmin but I read somewhere that it is (probably) not working in HABPanel. Damned!
So I searched for another solution and found this topic about habpanel templates where was exactly what I needed. With a great help of Yannick I got it working!

I needed the picture of my house and garden for the background. So I took it from google satellite maps and crop it to fit my widget nicely. I used 1064x768 pixels, however, you can use whatever fits your needs of course. Picture saved again in the config folder /html where I created a subfolder pic (o:\openhab2-conf\html\pic\white_house.jpg)

So I added one more widget, the template:

with this code inside (updated acc. to recommendations in comments below):

<div style="position: absolute;
            top: 0;
            bottom: 0;
            left: -10px; right: -10px;
            background: url('http://**here comes your openhab IP address, e.g. 192.1.1.1**:8080/static/pic/white_house.jpg');
            background-size: cover;
            background-repeat: no-repeat;">
  
   <div ng-init='model={"name": "PIR01", "item": "Pir01", "hidelabel": false, "hideonoff": false, "iconset": "eclipse-smarthome-classic", "icon": "dmotion" }'>
    <widget-dummy
        style="position: absolute;
               background: rgba(0,0,0,0);
               border-radius: 20px;
               top: 400px; left: 610px;
               width: 60px; height: 80px"
        ng-model="model" />
  </div>
  
  <div ng-init='model1={"name": "PIR02", "item": "Pir02", "hidelabel": false, "hideonoff": false, "iconset": "eclipse-smarthome-classic", "icon": "dmotion" }'>
    <widget-dummy
        style="position: absolute;
               background: rgba(0,0,0,0);
               border-radius: 20px;
               top: 400px; left: 830px;
               width: 60px; height: 80px"
        ng-model="model1" />
  </div>
  
</div>

You have to set the position of the icon/widget in the template by setting top and left values. This may, however, need to be adjusted for devices with other resolutions, I have not tested this yet.

And finally this is how it looks in the HABPanel:

Btw., is there any centralized way to share these (and in the future, maybe also other) icons, similar to share custom widgets in HABPanel?

13 Likes

Great work! Thank you for the in-depth tutorial! Also: Nice avatar :wink:

It seems like you are going to have A LOT of duplicated code within your rules. @rlkoshak has posted a design pattern how to tackle this using groups, so you only need that code once + a detection where you find out which of your PIRs actually triggered that rule

Be sure to have a look at the other design patterns.

Thanks! Yes, I know about this group rule and planned to use it. But now only wanted to easily proof the concept.

Still new to this coding and sometimes it is easier for me to do it worse way :slight_smile:

Thanks for sharing @FrankCZ!

Quick tip, the "state": itemState(Pir02).toString in the ng-inits are not useful, you should remove them.

You are right @ysc , it works without it. I didn’t know that! I will update the code above.

Thanks for posting this great tutorial. I’m on my phone right now so it’s hard to do a full review of the rule but I do notice some things with a quick scan:

  • done cancel a Timer from inside the Timer body. If the body is running then it is already cancelled. The line end up doing nothing.

  • similarly, it makes no sense I have an if statement inside the body of the timer to test if the timer is cancelled or null immediately after cancelling and setting the timer to null. You don’t need the if because you already know it’s cancelled and null.

  • have a look at the cascading timers design pattern for a more flexible way to do this, though in this case I think I would implement it with Expire Based Timers.

If you combined the Expire Based Timers DP and Associated Items DP and take advantage of the new triggeringItem implicit variable you can not only reduce this down to one Rule for all PIRS but reduce the lines of coffee and complexity of that rule by more than half.

Note I’m just typing this in from memory on my phone. It will be riddled with errors. It is meant as a guide, not workable code.

Group gPirTimers
String Pir01 "PIR1 [%s]" <dmotion> (gPirs, gPirsOutside)
Switch Pir01_Timer (gPirTimers)
 {expire="5s,command=OFF"}
String Pir02 "PIR2 [%s]" <dmotion> (gPirs, gPirsOutside)
Switch Pir02_Timer  (gPirTimers) {expire="5s,command=OFF"}
rule "Pir on"
when
    Item Pir01_raw received update 1 or
    Item Pir02_raw received update 1
then
    val pir = gPirs.members.findFirst[p| p.name == triggering item.name.split("_").get(1)]
    val timer = gPirTimers.members.findFirst[t| t.name = pir.name+"_Timer"]
    pir.postUpdate("red")
    timer.sendCommand(ON)
end

rule "PIR Timer"
when
    Item Pir01_Timer received command OFF or
    Item Pir02_Timer received command OFF
then
    val pir = gPirs.members.findFirst[p| p.name == triggering item.name.split("_").get(0)]

    var color = "off"
    switch(pir.state.toString){
        case "red": color = "orange"
        case "orange": color = "orangelight"
        case "orangelight": color = "yellow"
        case "yellow" : color = "off"
    }

    pir.postUpdate(color)
    if(color != "off") triggeringItem.sendCommand(ON)
end

For now you have to add each new PIR as a trigger to the first rule and each timer as a trigger to the second. Soon we will be able to trigger of if a group I hope.

1 Like

Hi @rlkoshak, that logic is brilliant! And written on a mobile? :slight_smile: I knew already that you are the rules master.

I implemented it by adding new timer items, corrected minor typos as Visual Studio shown, ending up with this rule:

rule "Pir on"
when
    Item Pir01_raw received update 1 or
    Item Pir02_raw received update 1
then
    val pir = gPirs.members.findFirst[p| p.name == triggeringItem.name.split("_").get(1)]
    logInfo("alarm.rules", "triggeringItem, name: ", triggeringItem.name)
    logInfo("alarm.rules", "Pir name: " + pir)
    
    val timer = gPirTimers.members.findFirst[t| t.name == pir.name+"_Timer"]
    logInfo("alarm.rules", "Timer name: " + timer)
    pir.postUpdate("red")
    timer.sendCommand(ON)
end

However, still not working as expected… the log showing this:

2018-01-07 20:42:27.929 [INFO ] [e.smarthome.model.script.alarm.rules] - triggeringItem, name: 
2018-01-07 20:42:27.934 [INFO ] [e.smarthome.model.script.alarm.rules] - Pir name: null
2018-01-07 20:42:27.943 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'Pir on': cannot invoke method public abstract java.lang.String org.eclipse.smarthome.core.items.Item.getName() on null

So it seems the rule is somehow not getting the triggeringItem info I think. Tried to find more info but this feature (triggeringItem) seems to be added quite recently and I couldn’t find much more info… Do you have an idea what’s wrong?

Are you running 2.2 release? I think triggeringItem was added shortly before the release so if you are still on 2.1 or 2.2 snapshot you may not have it.

It does indeed appear that triggeringItem is not being populated.

It should work as written based on my understanding of how it is supposed to work. However, maybe it doesn’t like the 1.

Does:

rule "Pir on"
when
    Item Pir01_raw received update or
    Item Pir02_raw received update
then
    if(triggeringItem.state as Number != 1) return;

    val pir = gPirs.members.findFirst[p| p.name == triggeringItem.name.split("_").get(1)]
    logInfo("alarm.rules", "triggeringItem, name: ", triggeringItem.name)
    logInfo("alarm.rules", "Pir name: " + pir)
    
    val timer = gPirTimers.members.findFirst[t| t.name == pir.name+"_Timer"]
    logInfo("alarm.rules", "Timer name: " + timer)
    pir.postUpdate("red")
    timer.sendCommand(ON)
end

Does whatever populates Pir01_raw only update the Item or does it receive a command?

The Group and persistence hack would work here as well in a pinch but we shouldn’t have to resort to that anymore.

What is interesting is triggeringItem is not null, It just doesn’t have the name field filled out.

Log out triggeringItem.state. Maybe the name doesn’t get populated, in which case I need to file an issue.

Yes, running 2.2.0 Release Build
image

Pir01_raw receives update from MQTT broker via MQTT binding so no command.

Number Pir01_raw "PIR1 [%s]" <dmotion> (gPirs, gPirsOutside) {mqtt="<[mosquitto:Garden/Alarm/Pir01:state:default]"} //receiving 0 (no motion detected) or 1 (motion detected)

This is log when tried to get only the .state instead of .name

2018-01-08 19:04:58.323 [INFO ] [e.smarthome.model.script.alarm.rules] - triggeringItem.state: 
2018-01-08 19:04:58.972 [INFO ] [e.smarthome.model.script.alarm.rules] - triggeringItem.state: 
2018-01-08 19:04:58.994 [INFO ] [e.smarthome.model.script.alarm.rules] - triggeringItem, name: 
2018-01-08 19:04:59.000 [INFO ] [e.smarthome.model.script.alarm.rules] - Pir name: null
2018-01-08 19:04:59.020 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'Pir on': cannot invoke method public abstract java.lang.String org.eclipse.smarthome.core.items.Item.getName() on null

Additional info appeared in log when used your last rule:

2018-01-08 18:56:59.042 [ERROR] [org.quartz.core.JobRunShell         ] - Job DEFAULT.2018-01-08T18:56:59.037+01:00: Proxy for org.eclipse.xtext.xbase.lib.Procedures$Procedure0: [ | {
  <XFeatureCallImplCustom>.cancel
  <null>.TimerPir01 = <XNullLiteralImplCustom>
  <XFeatureCallImplCustom>.postUpdate(<XStringLiteralImpl>)
  org.eclipse.xtext.xbase.impl.XIfExpressionImpl@8e43dd
} ] threw an unhandled Exception: 
java.lang.NullPointerException: null
	at org.eclipse.smarthome.model.script.engine.ScriptError.<init>(ScriptError.java:66) [145:org.eclipse.smarthome.model.script:0.10.0.b1]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.invokeFeature(ScriptInterpreter.java:130) [145:org.eclipse.smarthome.model.script:0.10.0.b1]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:901) [164:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:864) [164:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:223) [164:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:215) [145:org.eclipse.smarthome.model.script:0.10.0.b1]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) [164:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:759) [164:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:219) [164:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:215) [145:org.eclipse.smarthome.model.script:0.10.0.b1]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) [164:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:446) [164:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:227) [164:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:215) [145:org.eclipse.smarthome.model.script:0.10.0.b1]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) [164:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluate(XbaseInterpreter.java:189) [164:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.ClosureInvocationHandler.doInvoke(ClosureInvocationHandler.java:46) [164:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at org.eclipse.xtext.xbase.interpreter.impl.AbstractClosureInvocationHandler.invoke(AbstractClosureInvocationHandler.java:29) [164:org.eclipse.xtext.xbase:2.12.0.v20170519-0752]
	at com.sun.proxy.$Proxy173.apply(Unknown Source) [?:?]
	at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:49) [145:org.eclipse.smarthome.model.script:0.10.0.b1]
	at org.quartz.core.JobRunShell.run(JobRunShell.java:202) [115:org.eclipse.smarthome.core.scheduler:0.10.0.b1]
	at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) [115:org.eclipse.smarthome.core.scheduler:0.10.0.b1]
2018-01-08 18:56:59.049 [ERROR] [org.quartz.core.ErrorLogger         ] - Job (DEFAULT.2018-01-08T18:56:59.037+01:00: Proxy for org.eclipse.xtext.xbase.lib.Procedures$Procedure0: [ | {
  <XFeatureCallImplCustom>.cancel
  <null>.TimerPir01 = <XNullLiteralImplCustom>
  <XFeatureCallImplCustom>.postUpdate(<XStringLiteralImpl>)
  org.eclipse.xtext.xbase.impl.XIfExpressionImpl@8e43dd
} ] threw an exception.
org.quartz.SchedulerException: Job threw an unhandled exception.
	at org.quartz.core.JobRunShell.run(JobRunShell.java:213) [115:org.eclipse.smarthome.core.scheduler:0.10.0.b1]
	at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573) [115:org.eclipse.smarthome.core.scheduler:0.10.0.b1]
Caused by: java.lang.NullPointerException
	at org.eclipse.smarthome.model.script.engine.ScriptError.<init>(ScriptError.java:66) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.invokeFeature(ScriptInterpreter.java:130) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:901) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:864) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:223) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:215) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:759) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:219) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:215) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter._doEvaluate(XbaseInterpreter.java:446) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.doEvaluate(XbaseInterpreter.java:227) ~[?:?]
	at org.eclipse.smarthome.model.script.interpreter.ScriptInterpreter.doEvaluate(ScriptInterpreter.java:215) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.internalEvaluate(XbaseInterpreter.java:203) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter.evaluate(XbaseInterpreter.java:189) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.ClosureInvocationHandler.doInvoke(ClosureInvocationHandler.java:46) ~[?:?]
	at org.eclipse.xtext.xbase.interpreter.impl.AbstractClosureInvocationHandler.invoke(AbstractClosureInvocationHandler.java:29) ~[?:?]
	at com.sun.proxy.$Proxy173.apply(Unknown Source) ~[?:?]
	at org.eclipse.smarthome.model.script.internal.actions.TimerExecutionJob.execute(TimerExecutionJob.java:49) ~[?:?]
	at org.quartz.core.JobRunShell.run(JobRunShell.java:202) ~[?:?]
	... 1 more
2018-01-08 18:57:00.480 [INFO ] [e.smarthome.model.script.alarm.rules] - triggeringItem, name: 
2018-01-08 18:57:00.484 [INFO ] [e.smarthome.model.script.alarm.rules] - Pir name: null
2018-01-08 18:57:00.490 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'Pir on': cannot invoke method public abstract java.lang.String org.eclipse.smarthome.core.items.Item.getName() on null
2018-01-08 18:57:05.902 [INFO ] [e.smarthome.model.script.alarm.rules] - triggeringItem, name: 
2018-01-08 18:57:05.908 [INFO ] [e.smarthome.model.script.alarm.rules] - Pir name: null

Maybe I could do a quick test with another Item and Item type to see if the problem is syntax, item type/value or the function itself. Is there any working example that I could apply and test?

If you change it to mosquitto:Garden/Alarm/Pir01:command:default it will be a command instead of an update.

That can be something else to try. You would change your trigger to received command and then you can use the implicit variable receivedCommand

if(receivedCommand != 1) return;

That doesn’t address why triggeringItem appears empty (not null which is interesting) but maybe it works better for command triggered Rules.

It is hard to say whether these errors are related to the rule or not. Typically Quartz errors happen inside Timers. Since we are not using Timers in these rules…

@5iver do you have any ideas or know someone who might have an idea?

1 Like

So I changed my item accordingly as well as the rule and no change… in log:

2018-01-08 21:12:54.278 [INFO ] [e.smarthome.model.script.alarm.rules] - receivedCommand: 
2018-01-08 21:12:54.312 [INFO ] [e.smarthome.model.script.alarm.rules] - triggeringItem, name: 
2018-01-08 21:12:54.320 [INFO ] [e.smarthome.model.script.alarm.rules] - Pir name: null
2018-01-08 21:12:54.333 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'Pir on': cannot invoke method public abstract java.lang.String org.eclipse.smarthome.core.items.Item.getName() on null

val pir = gPirs.members.findFirst[p| p.name == triggeringItem.name.split("_").get(1)]

This one is easy to spot with a fresh set of eyes… it looks like pir will always be null, because triggeringItem.name.split("_").get(1) == "raw", which is where the error is coming from (probably a phone keyboard typo :wink:) . The logInfo lines in the rule are also not formatted properly.

@FrankCZ, see if this works for you…

rule "Pir on"
when
    Item Pir01_raw received update 1 or
    Item Pir02_raw received update 1
then
    val pir = gPirs.members.findFirst[p| p.name == triggeringItem.name.split("_").get(0)]
    logInfo("alarm.rules", "triggeringItem.name=[{}]",triggeringItem.name)
    logInfo("alarm.rules", "pir.name=[{}]",pir.name)
    
    val timer = gPirTimers.members.findFirst[t| t.name == pir.name + "_Timer"]
    logInfo("alarm.rules", "timer.name=[{}]",timer.name)
    pir.postUpdate("red")
    timer.sendCommand(ON)
end
2 Likes

Works perfectly @5iver @rlkoshak! Thanks for great support. I can also confirm that both options of changing the item value = state update as well as sending command works fine and the triggeringItem works correctly. Just to make it fully clear to others, I will put here the complete enhanced and working rule.

rule "Pir on"
when
    Item Pir01_raw received update 1 or
    Item Pir02_raw received update 1
then
    val pir = gPirs.members.findFirst[p| p.name == triggeringItem.name.split("_").get(0)]
    //logInfo("alarm.rules", "triggeringItem.name=[{}]",triggeringItem.name)
    //logInfo("alarm.rules", "pir.name=[{}]",pir.name)
    
    val timer = gPirTimers.members.findFirst[t| t.name == pir.name + "_Timer"]
    //logInfo("alarm.rules", "timer.name=[{}]",timer.name)
    pir.postUpdate("red")
    timer.sendCommand(ON)
end

rule "PIR Timer"
when
    Item Pir01_Timer received command OFF or
    Item Pir02_Timer received command OFF
then
    val pir = gPirs.members.findFirst[p| p.name == triggeringItem.name.split("_").get(0)]

    var color = "off"
    switch(pir.state.toString){
        case "red": color = "orange"
        case "orange": color = "orangelight"
        case "orangelight": color = "yellow"
        case "yellow" : color = "off"
    }

    pir.postUpdate(color)
    if(color != "off") triggeringItem.sendCommand(ON)
end

Items definition:

Group gPirs
Group gPirsOutside
Group gPirsInside
Group gPirTimers
Number Pir01_raw "PIR1 [%s]" <dmotion> (gPirs, gPirsOutside) {mqtt="<[mosquitto:Garden/Alarm/Pir01:state:default]"} //receiving 0 (no motion detected) or 1 (motion detected)
Number Pir02_raw "PIR2 [%s]" <dmotion> (gPirs, gPirsOutside) {mqtt="<[mosquitto:Garden/Alarm/Pir02:state:default]"}

String Pir01 "PIR1 [%s]" <dmotion> (gPirs, gPirsOutside) 
String Pir02 "PIR2 [%s]" <dmotion> (gPirs, gPirsOutside)

Switch Pir01_Timer (gPirTimers) {expire="5s,command=OFF"}
Switch Pir02_Timer (gPirTimers) {expire="5s,command=OFF"}
2 Likes

Great stuff @FrankCZ,

I felt like playing with your example yesterday to implement the direction detection you wanted.
I’ve done it with tags - for each sensor you’ll have to put one or more tags in the format: neighbor:direction to indicate their neighbors and the direction they (the neighbors) would be facing if they were still “hot” and the sensor was triggered.

I also added new associated items for the direction (and a group).

Example:

Group gPirs
Group gPirsOutside
Group gPirsInside
Group gPirTimers
Group gPirDirections

Number Pir01_raw "PIR1 [%s]" <dmotion> (gPirs, gPirsOutside) {mqtt=...}
Number Pir02_raw "PIR2 [%s]" <dmotion> (gPirs, gPirsOutside) {mqtt=...}

String Pir01 "PIR1 [%s]" <dmotion> (gPirs, gPirsOutside) ["Pir02:north"]
String Pir02 "PIR2 [%s]" <dmotion> (gPirs, gPirsOutside) ["Pir01:south"]

String Pir01_Direction "PIR1 Direction [%s]" (gPirDirections)
String Pir02_Direction "PIR2 Direction [%s]" (gPirDirections)

Switch Pir01_Timer (gPirTimers) {expire="5s,command=OFF"}
Switch Pir02_Timer (gPirTimers) {expire="5s,command=OFF"}

In this example the tags mean:

  • Pir02 should face north if it’s not ‘off’ and Pir01 is triggered
  • Pir01 should face south if it’s not ‘off’ and Pir02 is triggered

Then the new rules, I think they are quite self-explanatory (the second one is actually optional):

rule "PIR Direction"
when
    Item Pir01 received update red or
    Item Pir02 received update red
then
    triggeringItem.tags.forEach[t| {
        //logInfo("alarm.rules", "found tag: {}", t)
        val neighbor = gPirs.members.findFirst[p| p.name == t.split(":").get(0)]
        //logInfo("alarm.rules", "neighbor {} state = {}", neighbor.name, neighbor.state)
        if (neighbor.state.toString != "off" && neighbor.state != NULL) {
            val direction = t.split(":").get(1)
            val neighborDirItem = gPirDirections.members.findFirst[d| d.name == neighbor.name + "_Direction"]
            neighborDirItem.postUpdate(direction)
        }
    }]
end

rule "PIR Reset Direction"
when
    Item Pir01 received update off or
    Item Pir02 received update off
then
    val dirItem = gPirDirections.members.findFirst[d| d.name == triggeringItem.name + "_Direction"]
    dirItem.postUpdate(NULL)
end

Then the real reason I’ve done this is, I thought it was a good real use case for the SVG floorplan approach I explained a few days ago :grin: You should definitely consider it instead of positioning your sensor by hand in HTML!

So I created a quick example (right-click the image below and save in conf/html as floorplan.svg and/or open it in Inkscape to inspect how it’s done):
floorplan5
(Sources: https://upload.wikimedia.org/wikipedia/commons/a/af/White_House_State_Floor.svg, https://upload.wikimedia.org/wikipedia/commons/e/e0/EMVCoContactlessIndicator.svg)

The trick here is simply to use an ng-class attribute to add the states of both the PirXX item and the associated direction as CSS classes!

Here’s how it looks in the editor (right pane):

Then you define the CSS rules in conf/html/floorplan.css:


.pir-icon.red {
    fill: red !important;
}
.pir-icon.orange {
    fill: darkorange !important;
}
.pir-icon.orangelight {
    fill: orange !important;
}
.pir-icon.off {
    fill: black !important;
}
.pir-icon.south {
    transform: rotate(90deg);
    transform-origin: 50% 50%;
}
.pir-icon.west {
    transform: rotate(180deg);
    transform-origin: 50% 50%;
}
.pir-icon.north {
    transform: rotate(-90deg);
    transform-origin: 50% 50%;
}

(I had to read https://css-tricks.com/transforms-on-svg-elements/ to understand the transform-origin thing, and beware of translation transform attributes applied by Inkscape, the paths of the icons needed to be combined with the Path > Combine menu, http://www.inkscapeforum.com/viewtopic.php?t=17884 helped)

Then you can put it all together in a HABPanel template like this:

<div oc-lazy-load="'/static/floorplan.css'">
  <div ng-include="'/static/floorplan.svg'"></div>
</div>

and it will behave like you expect!

Hope you find this useful - continue to share your progress!

4 Likes

@ysc That is amazing! :slight_smile: The truth is that setting the position in px is different on each device depending on resolution and even magnifier used within the browser so I planned to try the % as you suggested in the original thread.

As I don’t know CSS at all, it will take me some time to absorb and implement it, but your example seems pretty straightforward so thanks a lot for your time! I will definitely work on that! I will also read the svg floorplan tutorial. The more I learn the more I see that possibilities are almost endless :+1: I love it!

1 Like

Lurking and learning. Thanks for putting up the project as a beginner, it has encouraged me seeing all the help and interactions that took place.

Nicely done, me thinks.

Thanks all.

1 Like

Thanks for the demo!

how you have been able to solve false positives with HC-SR501?

Good question! A false positive is usually coming from a weather (sunshine etc) or a pet (dog in my case).

To somehow avoid both I designed and 3D printed a housing for the sensor including a cover for lenses so it detects only in about 5mm high horizontal space in the cover (this one). Furthermore I placed the sensor in a place where sun is usually not shining.
But I still got some false positives anyway so I decided to place two HC-SR501 not far away from each other (about 1m) instead of one. Then I am waiting for confirmation by the second PIR and only after both PIRs detected movement I trigger the alarm (or whatever you want - I use it also for a night light).

This solution removed 99,99% of false positives! Furthemore, and that is also very nice feature, you can detect not only a movement, but also a direction of the movement! Obviously depending on which PIR detecting it first and which the second. So the night light is switched on only if I am moving into the dark part, and not when I am coming from the dark part of my garden (depends on your environment of course).

yeah that could work. And have quite a lot of advantages.
But for instalation where PIR is very near to the ESP (to be very compact devices) this literally false positive detects Wifi and therefore it’s falsing every couple of seconds.

I might give up of making compact devices and actually place PIR further away of actual board, maybe try two as you are suggesting.
But incorporating it to the house design to not look like shit, it’s quite challenging