I am using motion sensors to determine when people in the house are awake. When openhab sees 12 motions within a running 30 minutes, people are awake. When this triggers, I want to obtain the timestamp (millis) of the first motion in the range. How do I do that?
gMotion - group which records motion updates from 5 sensors into Influxdb
Rule "Check for awake"
when
Time cron "45 0/1 * 1/1 * ? *" // every minute
then
val awakeMotions = gMotion.sumSince(now.minusMinutes(30), "influxdb") as Number
if (awakeMotions >= 12) {
val TimeofFirstMotion = ?? help ??
logInfo("awake", "People are awake and millis timestamp of first motion = " + TimeofFirstMotion.toString)
}
end
You probably can’t using this approach. There is no way to query for “the 12th entry from now” and you can’t use historicState(now.minus(30)) because that will give you the state that occurred closest before 30 minutes ago. The 12 motions could have started only 5 minutes ago.
You will instead need to trigger the rule when motion occurs. In this rule keep a count of the number of motion detections. When the rule triggers and the count is 0, also save a timestamp. Create a new timer each time the rule triggers to decrement the count after 30 minutes. When the count gets to 12, execute the awakeMotions code. Presumably you would want to cancel the running timers at this point and reset everything to zero. You might also want to create some time checks to ignore motion detection events after the first 12 until the next day.
You can do this using the REST API. A call like this…
curl -X GET --header "Accept: application/json" "http://localhost:8080/rest/persistence/items/DS_HallwayStairwell_Motion?starttime=2020-11-18T12%3A06%3A00"
… made using executeCommandLine will return all motion detections since the starttime. In your rule, you will need to calculate the time from 30 minutes ago. Example results…
You would need to do the same for all 5 motion sensors. SwitchItems and ContactItems are stored in persistence with two values, except for the latest one, in order to make a nice step in graphs. So, if you add up the datapoints value from all of the sensors and it is >=23, there have been 12 motion detections within the last 30 minutes. Double check the logic though, because something seems off. For 5 sensors, maybe >=28?
Thanks for the clues. Using your suggestion, here’s my code. Any suggestions for tweeks? The only downside is that several timers will be started, be running after one of them triggers.
rule "Look for awake status"
when
Item gMotion received update ON
then
if (awake_checking.state == OFF) return;
val motionTime = now.getMillis()
createTimer(nowplusMinutes(30), [|
if (awake_checking.state == ON) {
val awakeMotions = gMotion.sumSince(now.minusMinutes(30), "influxdb") as Number
if (awakeMotions >= 12) {
logInfo("awake", "People are awake and millis timestamp of first motion = " + motionTime.toString)
awake_checking.sendCommand(OFF)
}
}
]
end
Another thought … would this approach even work? It shuts down all launched awake timers within one minute of hitting 12 motions.
rule "Look for awake status"
when
Item gMotion received update ON
then
if (awake_checking.state == OFF) return;
var awake_min_left = 30
val motionTime = now.getMillis()
var awake_timer = createTimer(nowplusMinutes(1), [|
if (awake_checking.state == ON) {
val awakeMotions = gMotion.sumSince(now.minusMinutes(30), "influxdb") as Number
if (awakeMotions >= 12) {
< execute actions >
awake_checking.sendCommand(OFF)
}
else {
if (awake_min_left-- > 0)
awake_timer.reschedule(now.plusMinutes(1) )
}
}
]
end
I updated my code for some syntax errors. However, the code fails with an exception. Since the timer is called multiple times, I suspect the reference to the timer name is not unique, throwing the error. That means the timers can only be launched for the 30 minute duration and not extended. Is there a way to make this code work?
rule "Look for awake status"
when
Item gMotion received update ON
then
if (awake_checking.state == OFF) return;
var awake_min_left = 30
val motionTime = now.getMillis()
var Timer awake_timer = createTimer(now.plusMinutes(1), [|
if (awake_checking.state == ON) {
val awakeMotions = gMotion.sumSince(now.minusMinutes(30), "influxdb") as Number
if (awakeMotions >= 12) {
< execute actions >
awake_checking.sendCommand(OFF)
}
else {
if (awake_min_left-- > 0)
awake_timer.reschedule(now.plusMinutes(1) )
}
}
])
end
Exception seen (trimmed):
Job DEFAULT.Timer 23 2020-11-18T11:10:41.029-08:00:
Proxy for org.eclipse.xtext.xbase.lib.Procedures$Procedure0: [ | {
org.eclipse.xtext.xbase.impl.XIfExpressionImpl@6b440ac4
(conditionalExpression: false)} ] threw an unhandled Exception:
Caused by: org.eclipse.smarthome.model.script.engine.ScriptExecutionException:
The name 'awake_timer' cannot be resolved to an item or type; line 67, column 21, length 11
I don’t think you understood what I was referring to. Persistence wouldn’t play a part in this.
var motionCount = 0
var firstMotionTime = now.minusDays(1) // set it sometime way in the past just to have a value and type
var timers = newArrayList
rule "Look for awake status"
when
Item gMotion received update ON
then
if(awake_checking.state == OFF) return;
// first motion of the day, take a timestamp
if(motionCount == 0) {
firstMotionTime = now // why only save the millis? A DateTime is so much more useful
}
// We reached 12 motions in 30 minutes, do the awake stuff and reset everything
else if(motionCount >= 12){
logInfo("awake", "People are awake and timestamp of first motion is " + firstMotionTime);
awake_checking.sendCommand(OFF);
timers.forEach[timer | timer.cancel ]
timers.clear()
}
// Increment the counter and schedule a timer to decrement it in 30 minutes
motionCount = motionCount + 1;
timers.add(createTimer(now.plusMinutes(30), [ | moitionCount -1 ] );
end
Presumably you have a rule that runs after midnight to reset awake_checking back to ON.
Any variable you want to survive one run of the rule to the next needs to be defined as a global variable. In this case you need to keep the count and the reference to the Timers in global variables.
The above throws an exception: ‘cancel’ is not a member of ‘java.lang.Object’. Is there an import statement needed?
The code also does not capture the timestamp of the first motion in the time range once the timers start shifting the count. I want to know the timestamp of the first motion of 12 that occur within 30 minutes.
If I capture the timestamp for each motion in an array and use the timer to shift a pointer to that array, I can obtain the correct timestamp. However, I can’t figure out the proper code for this.
With the change, I am still getting this error when implementing the code.
var List<Timer> timers = newArrayList
I also get an execution error when it triggers on this line:
firstMotionTime = motionTimes.peek()
The error:
Rule 'Look for awake status': An error occurred during the script execution: Couldn't invoke 'assignVa
lueTo' for feature JvmVoid: (eProxyURI: misc.rules#|::0.2.3.2.0.1.1.0.2::0::/1)
Does using peek() return the first value in the queue?
Resolved all issues and code Rich provided works without problems. I integrated the code and the implementation error went away. Not sure why - probably typos.
Rich, thanks so much for your help. I hope this forum post helps others.
At long last, I am converting to OH3. I have resolved most conversion issues except one, this code. I am at a loss as to how to get this working in OH3. Appreciate any guidance from the community.
This code is a rolling 30-minute window looking for 12 motions to indicate someone is awake and returns the timestamp of the first motion of the 12.
Sorry, I should have included the log and specific code.
2021-12-01 10:04:53.587 [INFO ] [el.core.internal.ModelRepositoryImpl] - Validation issues found in configuration model 'dash.rules', using it anyway: │
The field Tmp_dashRules.motionTimes refers to the missing type Object
Here’s the code in question.
import java.util.List
import java.util.Queue
var motionCount = 0
var Queue<DateTime> motionTimes = newLinkedList
var List<Timer> timers = newArrayList
var awake_timer_first_motion = null
var sleep_timer_first_motion = null
var Timer sleep_looking_timer = null
rule "Check if awake by counting inside motions during rolling 30 min"
when
Item gMotion received update ON
then
if (vsleep_state.state == "awake" || traveling.state == ON) return;
if (awake_checking.state == OFF) {
if (peeps_home.state == ON &&
now.getHourOfDay >= 4 &&
now.getHourOfDay < 10) {
awake_checking.sendCommand(ON)
motionCount = 0
}
else return;
}
motionCount = motionCount + 1
if(motionCount >= 12) {
awake_timer_first_motion = motionTimes.peek().getMillis()
to_awake.sendCommand(ON) // set awake
// another rule acts on awake change and unwinds the timmers
}
else {
motionTimes.add(now)
timers.add(createTimer(now.plusMinutes(30), [ |
motionCount = motionCount - 1
motionTimes.remove()
]))
}
end