Here is the code. It's pretty messy, but it did the trick when SmartThings worked with Groovy. I'm still in the process of transitioning the app to Hubitat and this On Off issue is preventing the basic functionality from working. Although this app has grown with additional functionalities over the years, the most basic purpose is to allow a light to be turned on/off with motion with the added functionality of not turning the light back on with motion if it wasn't turned off from motion inactivity.
The problematic lines are the item.off() line in the checkMotion function and the item.on() in the motionDetectedHandler
/**
* Motion Plus
*
* Copyright 2016 Matthew Williams
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
//import groovy.time.TimeDuration
////import groovy.time.TimeCategory
definition(
name: "Motion Plus",
namespace: "MatthewWilliams",
author: "Matthew Williams",
description: "Turn on lights with motion sensor. Allow lights to be turned off without being turned back on by motion sensor.",
category: "Convenience",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
preferences {
section("Turn on when motion detected:") {
input "themotion", "capability.motionSensor", required: true, title: "Where?", multiple: true
}
section("Turn off when there's been no movement for") {
input "minutes", "number", required: true, title: "Minutes?"
}
section("Turn on this light") {
input "theswitch", "capability.switch", required: true, multiple: true
}
section("Enabled during daylight hours?") {
input "duringdaylight", "boolean", required: true, multiple: false, title: "Daylight hours?"
}
section("Daylight offset (default 60 minutes)") {
input "daylightoffset", "number", required: false, title: "Offset minutes"
}
section("Light sensor"){
input "lightsensor", "capability.illuminanceMeasurement", required: false, title: "Light sensor", multiple: false
}
section("Light threshold"){
input "lux", "number", required: false, title: "Lux"
}
section("Inverse?") {
input "isinverse", "boolean", required: true, multiple: false, title: "Inverse?"
}
section("Night time hours") {
input "nightDimmers", "capability.switchLevel", multiple: true, required: false, title: "Night dimmers"
input "nightTempLights", "capability.colorTemperature", multiple: true, required: false, title: "Night color temperature lights"
input "nightStart", "time", required: false, title: "Night start time"
input "nightEnd", "time", required: false, title: "Night end time"
input "dayLevel", "number", required: false, title: "Day brightness"
input "nightLevel", "number", required: false, title: "Night brightness"
input "dayTemp", "number", required: false, title: "Day color temp"
input "nightTemp", "number", required: false, title: "Night color temp"
}
}
def installed() {
initialize()
}
def updated() {
unsubscribe()
initialize()
}
def initialize() {
subscribe(themotion, "motion.active", motionDetectedHandler)
subscribe(themotion, "motion.inactive", motionStoppedHandler)
subscribe(theswitch, "switch.on", lightOnHandler)
subscribe(theswitch, "switch.off", lightOffHandler)
subscribe(lightsensor, "illuminance", lightChangeHandler)
sunsetHandler()
//Backup schedule to prevent lights from staying on if the runOnce fails
runEvery1Hour(checkMotion)
}
def sunsetHandler(){
//log.debug "sunsetHandler"
if(duringdaylight == "false"){
//The room lighting can change below the threshold while there is motion.
//If there is motion call the motion detected handler so lights will turn on if the threshold has been met.
if (themotion.currentState("motion").value == "active"){
motionDetectedHandler()
}
def sunsetTime = getSunsetTime()
def nextHour = now() + (1000*60*60)
if(nextHour > sunsetTime){
sunsetTime = nextHour
}
sunsetTime = new Date(sunsetTime)
//log.debug(sunsetTime)
runOnce(sunsetTime,sunsetHandler)
}
}
def getSunsetTime(){
use(groovy.time.TimeCategory){
//Get the time of midnight the next day
//For example if it is 3/2/2019 16:10 midnight time will be 3/3/2019 00:00 in epoch time
def midnightTime = (new Date().clearTime() + 1.days)
//Offset time is not included
midnightTime = midnightTime + 5.hours
midnightTime = midnightTime.time
//Get sunrise and sunset for current day
//def sunsetTime = Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", location.currentValue("sunsetTime"))
def sunsetTime = location.sunset
if (sunsetTime.time > midnightTime){
//if sunrise time is for the current day change it to the next day
sunsetTime = sunsetTime - 1.days
}
sunsetTime = sunsetTime.time
//Only turn the light on if it is set to go on during daylight hours or it is not daylight hours
def offsetMinutes = 60
if(daylightoffset != null){
offsetMinutes = daylightoffset
}
def sunOffset = 60 * 1000 * offsetMinutes
sunsetTime = (sunsetTime - sunOffset)
return sunsetTime
}
}
def getSunriseTime(){
use(groovy.time.TimeCategory){
//Get the time of midnight the next day
//For example if it is 3/2/2019 16:10 midnight time will be 3/3/2019 00:00 in epoch time
def midnightTime = (new Date().clearTime() + 1.days).time
//Get sunrise and sunset for current day
//def sunriseTime = Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", location.sunrise)
def sunriseTime = location.sunrise
if (sunriseTime.time > midnightTime){
//if sunrise time is for the next day change it to the current day
sunriseTime = sunriseTime - 1.days
}
sunriseTime = sunriseTime.time
//Only turn the light on if it is set to go on during daylight hours or it is not daylight hours
def offsetMinutes = 60
if(daylightoffset != null){
offsetMinutes = daylightoffset
}
def sunOffset = 60 * 1000 * offsetMinutes
sunriseTime = (sunriseTime + sunOffset)
return sunriseTime
}
}
def lightChangeHandler(evt){
//The room lighting can change below the threshold while there is motion.
//If there is motion call the motion detected handler so lights will turn on if the threshold has been met.
if (themotion.currentState("motion").value == "active"){
motionDetectedHandler()
}
}
def lightOnHandler(evt) {
//log.debug "lightOnHandler called: $evt"
use(groovy.time.TimeCategory){
def schedDate = new Date()
for(def i = 0; i < minutes; i++){
schedDate = schedDate + 61.seconds
}
runOnce(schedDate, checkMotion)
}
//runIn((61 * minutes), "checkMotion", [overwrite:true])
}
def lightOffHandler(evt) {
//log.debug "lightOffHandler called: $evt"
}
def motionDetectedHandler(evt) {
log.debug "motionDetectedHandler called: $evt"
int index = 0
log.debug "theswitch"
log.debug theswitch
for(item in theswitch){
def onState = item.currentState("switch")
log.debug "onState"
log.debug onstate
if((onState.value == "off" && isinverse == "false")||(onState.value == "on" && isinverse == "true")) {
use(groovy.time.TimeCategory){
def duration = 60.seconds//new TimeDuration(0, minutes, 0, 0)
def exactDuration = 60.seconds
//time the light turned off
def start = onState.date
def exactStart = onState.date
for(def i = 0; i < minutes; i++){
start = start - duration
exactStart = exactStart - exactDuration
}
def end = onState.date
//def switchStates = item.statesBetween("switch", start + 120.seconds, end - 120.seconds)
//def switchStates = item.statesSince("switch", start)
//Hubitat doesn't have states between. Use states since then remove the end range states
def switchStates = item.statesSince("switch", start + 10.seconds)
switchStates.removeAll { it.date >= (end - 10.seconds) }
// New - Sum up the motion states for all the motion sensors
def motionStateCount = 0
for(motionSensor in themotion){
def motionEvents = motionSensor.eventsBetween(start + 10.seconds, end - 10.seconds)
motionEvents.removeAll { it.name != "motion" }
motionStateCount = motionStateCount + motionEvents.size()
}
// Old
//def motionStates = themotion.statesBetween("motion", start + 120.seconds, end - 120.seconds)
//Check to see if the light was last on for less than the set number of minutes. If this is true then the light must have turned off from another source
def lastOnStatesBetween = item.eventsBetween(exactStart, end)
lastOnStatesBetween.removeAll { it.name != "switch" }
def lastOnState = lastOnStatesBetween[1]
def isOnLessThanMinutes = false
if(lastOnState != null){
def onStateDur = (onState.date.getTime() - lastOnState.date.getTime())/1000/60
//subtract a minute from minutes in case motion sensor turns off lights a little before minutes
isOnLessThanMinutes = onStateDur < (minutes/2)
}
else{
//log.debug "lastOnState is null the state before that is"
//log.debug lastOnStatesBetween[0]
if(lastOnStatesBetween[0] != null){
//log.debug lastOnStatesBetween[0].value
}
}
//Todo: check corner case of if motion has been active for longer than minutes when light is turned off
//...look into statesBetween to see look for motion active as the latest state during the specified date range
//...don't turn on lights if there are three or more events, or if the light wasn't even on for the configured inactive time.
if(!isOnLessThanMinutes && ((switchStates.size() + motionStateCount) < 3)){ // New code
def beforeSunrise = (getSunriseTime()) >= now()
def afterSunset = (getSunsetTime()) <= now()
if( (duringdaylight == "true") || beforeSunrise || afterSunset){
if((beforeSunrise || afterSunset || lightsensor == null) || (lightsensor != null && lightsensor.currentState("illuminance").value.toInteger() <= lux.toInteger())){
if(isinverse == "true"){
item.off()
}
else {
log.debug "turning on light"
setNightDimmerLevel(item)
setNightColorTemp(item)
log.debug "item"
log.debug item
item.on()
}
}
}
}
else {
//log.debug "detected shut off from other source. Switch and Motion states count:"
//log.debug switchStates.size()
//log.debug motionStates.size()
}
}
}
else {
//log.debug "light already on"
}
index++
}
}
def setNightDimmerLevel(dimmer){
if(nightStart != null && nightEnd != null){
for(dimmerItem in nightDimmers){
if(dimmerItem.id == dimmer.id){
use(groovy.time.TimeCategory){
def nightStartTime = timeToday(nightStart.substring(11,16), location.timeZone).time
def nightEndTime = timeToday(nightEnd.substring(11,16), location.timeZone).time
def isNight = false
if(nightStartTime > nightEndTime){
isNight = now() >= nightStartTime || now() < nightEndTime
}
else{
isNight = now() >= nightStartTime && now() < nightEndTime
}
if(isNight){
if(nightLevel != null){
dimmerItem.setLevel(nightLevel, 1)
}
}
else{
if(dayLevel != null){
dimmerItem.setLevel(dayLevel, 1)
}
}
}
}
}
}
}
def setNightColorTemp(tempLight){
if(nightStart != null && nightEnd != null){
for(tempItem in nightTempLights){
if(tempItem.id == tempLight.id){
use(groovy.time.TimeCategory){
def nightStartTime = timeToday(nightStart.substring(11,16), location.timeZone).time
def nightEndTime = timeToday(nightEnd.substring(11,16), location.timeZone).time
def isNight = false
if(nightStartTime > nightEndTime){
isNight = now() >= nightStartTime || now() < nightEndTime
}
else{
isNight = now() >= nightStartTime && now() < nightEndTime
}
if(isNight){
if(nightTemp != null){
tempItem.setColorTemperature(nightTemp)
}
}
else{
if(dayTemp != null){
tempItem.setColorTemperature(dayTemp)
}
}
}
}
}
}
}
def motionStoppedHandler(evt) {
//log.debug "motionStoppedHandler called: $evt"
//log.debug "Starting motion stopped handler"
use(groovy.time.TimeCategory){
def schedDate = new Date()
for(def i = 0; i < minutes; i++){
schedDate = schedDate + 61.seconds
}
runOnce(schedDate, checkMotion)
}
}
def checkMotion() {
log.debug "In checkMotion scheduled method"
//def motionState = themotion.currentState("motion") //Old code
// New - Loop through motion sensors to report inactive in below IF when all are inactive
// New - Loop through motion sensors to get the smallest elapsed time
def areAllInactive = true
def elapsed = -1
for(motionSensor in themotion){
def motionState = motionSensor.currentState("motion")
if(motionState.value != "inactive"){
areAllInactive = false
break
}
def curElapsed = now() - motionState.date.time
if(elapsed == -1 || curElapsed < elapsed){
elapsed = curElapsed
}
}
//if (motionState.value == "inactive") { // Old code
if(areAllInactive){ // New code
// get the time elapsed between now and when the motion reported inactive
//def elapsed = now() - motionState.date.time // Old code
log.debug "now"
log.debug now()
// elapsed time is in milliseconds, so the threshold must be converted to milliseconds too
def threshold = 1000 * 60 * minutes
if (elapsed >= threshold) {
log.debug "Motion has stayed inactive long enough since last check ($elapsed ms): turning switch off"
int index = 0
log.debug "theswitch"
log.debug theswitch
for(item in theswitch){
def lightState = item.currentState("switch")
log.debug "The light should be getting turned off... The state of the light is:"
log.debug lightState.value
if((lightState.value == "on" && isinverse == "false") || (lightState.value == "off" && isinverse == "true")){
def lightOnElapsed = now() - lightState.date.time
if(lightOnElapsed >= threshold){
use(groovy.time.TimeCategory){
def schedDate = new Date()
for(def i = 0; i < minutes; i++){
schedDate = schedDate + 61.seconds
}
runOnce(schedDate, checkMotion)
}
if (isinverse == "true"){
setNightDimmerLevel(item)
setNightColorTemp(item)
item.on()
}
else {
log.debug "item"
log.debug item
item.off()
}
log.debug "The light should have been turned off... The state of the light is:"
log.debug lightState.value
}
}
index++
}
//theswitch.off()
} else {
//log.debug "Motion has not stayed inactive long enough since last check ($elapsed ms): doing nothing"
//log.debug "There is no other scheduled task to check the motion... scheduling check motion..."
//runIn(61 * minutes, checkMotion)
}
} else {
// Motion active; just log it and do nothing
//log.debug "Motion is active, do nothing and wait for inactive"
}
//atomicState.isTurningOff = isTurningOff
}