Here is the code:
/**
* Smart Timer
* Loosely based on "Light Follows Me"
*
* This prevent them from turning off when the timer expires, if they were already turned on
*
* If the switch is already on, it won't be affected by the timer (Must be turned of manually)
* If the switch is toggled while in timeout-mode, it will remain on and ignore the timer (Must be turned of manually)
*
* The timeout perid begins when the contact is closed, or motion stops, so leaving a door open won't start the timer until it's closed.
*
* Author: Twisty81
* Date: 2015-02-20
*
* Modified by Twisty81 to include interlocking between motion and contact, if motion stops while the contact is open, or contact closes while there is motion, it won't start the timer.
* Yet one doesn't require the other, it can be just contact or just motion, or both.
* Also if timer is running but contact was re-opened and/or motion went to active again, it will stop the timer, till the contact is closed and/or motion stops.
* Added Sunset and Sunrise functionality, this will trigger the light only between sunset and sunrise when a contact is open and/or motion was detected.
*
*/
definition(
name: "Smarter Light Timer, with check",
namespace:"",
author: "Twisty81",
description: "Turns ON a switch for X minutes, then turns it OFF. Unless, the switch is already ON in which case it stays ON. If the switch is toggled while the timer is running, the timer is canceled",
category: "Convenience",
iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet-luminance.png",
iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_contact-outlet-luminance@2x.png"
)
preferences {
section("Turn on when..."){
input "motions", "capability.motionSensor", multiple: true, title: "...there is Motion", required: false
input "contacts", "capability.contactSensor", multiple: true, title: "...this Opens", required: false
}
section("Turn on this light(s)..."){
input "lights", "capability.switch", multiple: true, title: "Select Lights"
}
section("When contact is closed and/or motion stops, Turn off after how many seconds? (60s minimum)") {
input "time", "number", title: "Enter 0 to not auto-off", defaultValue: "60"
}
section("And it's dark...(optional)") {
input "luminance1", "capability.illuminanceMeasurement", title: "Where?", required: false
input "lux", "number", title: "Less than? (Lux 0 - 1000)", defaultValue: "10", required: false
input "luxBool", "bool", title: "Should lux be a priority over Sunset/Sunrise?"
}
section("Based on Sunset/Sunrise for the location? (optional)") {
input "sunBool", "bool", title: "No/Yes"
}
section ("Sunrise offset (optional)...") {
input "sunriseOffsetValue", "text", title: "HH:MM", required: false
input "sunriseOffsetDir", "enum", title: "Before or After", required: false, options: ["Before","After"]
}
section ("Sunset offset (optional)...") {
input "sunsetOffsetValue", "text", title: "HH:MM", required: false
input "sunsetOffsetDir", "enum", title: "Before or After", required: false, options: ["Before","After"]
}
section ("Zip code (optional, defaults to location coordinates)...") {
input "zipCode", "text", required: false
}
}
def installed() {
initialize()
}
def updated() {
unsubscribe()
initialize()
}
def initialize() {
subscribe(lights, "switch", switchChange)
subscribe(motions, "motion", motionHandler)
subscribe(contacts, "contact", contactHandler)
state.myState = "ready"
log.debug "state: " + state.myState
if(sunBool == true) {
log.debug "RiseSet?: " + sunBool
subscribe(location, "position", locationPositionChange)
subscribe(location, "sunriseTime", sunriseSunsetTimeHandler)
subscribe(location, "sunsetTime", sunriseSunsetTimeHandler)
astroCheck()
// schedule("0 1 * * * ?", astroCheck) // check every hour since location can change without event?
}
}
def locationPositionChange(evt) {
log.trace "locationChange()"
astroCheck()
}
def sunriseSunsetTimeHandler(evt) {
log.trace "sunriseSunsetTimeHandler()"
astroCheck()
}
def astroCheck() {
def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset)
state.riseTime = s.sunrise.time
state.setTime = s.sunset.time
log.debug "rise: ${new Date(state.riseTime)}($state.riseTime), set: ${new Date(state.setTime)}($state.setTime)"
if(state.riseTime < now()) { //often I found the system returning the past sunrise, which for us is not important anymore. Therefore we are checking if sunrise is in the past, if so, add one day.
log.debug "adding one day"
state.riseTime = s.sunrise.time + 86400000
log.debug "New rise: ${new Date(state.riseTime)}($state.riseTime), set: ${new Date(state.setTime)}($state.setTime)"
}
}
def switchChange(evt) {
log.debug "SwitchChange: $evt.name: $evt.value"
/* def currSwitches = lights.currentSwitch
def onSwitches = currSwitches.findAll {switchVal -> switchVal == "on" ? true : false}
if(onSwitches.size() == lights.size()) {
log.debug "${onSwitches.size()} out of ${lights.size()} switches are on"
log.debug "all already on"
}
else if(onSwitches.size() << lights.size()) {
log.debug "${onSwitches.size()} out of ${lights.size()} switches are on"
log.debug "some are on"
}
else if(onSwitches.size() == 0) {
log.debug "${onSwitches.size()} out of ${lights.size()} switches are on"
log.debug "ready"
}
*/
if(evt.value == "on") {
// Slight change of Race condition between motion or contact turning the switch on,
// versus user turning the switch on. Since we can't pass event parameters :-(, we rely
// on the state and hope for the best.
if(state.myState == "activating") {
// Activating from the Motion or Contact handlers and not the switch itself. Go to Active mode.
state.myState = "active"
} else if(state.myState != "active") {
state.myState = "already on"
// If the switch went to "ON" and it wasn't this app, then set "already on" to inform the app not to
// run throught the timer when the contact closes and/or motion stops.
}
} else {
// If active and switch is turned off manually, then stop the schedule and go to "ready" state
if(state.myState == "active" || state.myState == "activating") {
unschedule(turnOffLight)
}
state.myState = "ready"
}
log.debug "state: " + state.myState
}
def contactHandler(evt) {
log.debug "contactHandler: $evt.name: $evt.value"
def currContacts = contacts.currentContact
def openContacts = currContacts.findAll {contactVal -> contactVal == "open" ? true : false}
log.debug "${openContacts.size()} out of ${contacts.size()} contacts are open"
// log.debug "openContacts: $openContacts"
if(openContacts) {
log.debug "Some contact(s) just opened"
state.myContact = "open" // to interlock with motion handler, this way if the contact is open and the motion goes inactive it won't start the timer.
logicCheck()
}
else {
state.myContact = "closed" // to interlock with motion handler
log.debug "contact(s) closed, checking condition"
if (state.myState == "active" && state.myMotion == "inactive" || state.myState == "active" && !state.myMotion) {
// when the contact closes, it checks if motion is inactive which will allow the timer to run OR in case
// the motion was not set up by the user, the state will be null and the timer will run since nothing is impeding.
log.debug "condition passed"
setCheckAndSchedule()
}
}
log.debug "state: " + state.myState
}
def motionHandler(evt) {
log.debug "motionHandler: $evt.name: $evt.value"
def currMotions = motions.currentMotion
def activeMotions = currMotions.findAll {motionVal -> motionVal == "active" ? true : false}
log.debug "${activeMotions.size()} out of ${motions.size()} motion sensors are active"
// log.debug "active Motions: $activeMotions"
if(activeMotions) {
state.myMotion = "active" // to interlock with contact handler, this way if the contact is closed
logicCheck() // while the motion is active, it won't start the timer
}
else {
state.myMotion = "inactive" // to interlock with contact handler
log.debug "motion inactive, checking condition"
if (state.myState == "active" && state.myContact == "closed" || state.myState == "active" && !state.myContact) {
// when motion goes inactive, it checks if the contact is closed which will allow the timer to run OR in
// case the contact was not set up by the user, the state will be null and the timer will run since nothing is impeding.
log.debug "condition passed"
setCheckAndSchedule()
}
}
log.debug "state: " + state.myState
}
def logicCheck() {
if(luminance1 && state.myState == "ready") { // if a lux sensor was selected on preferences and the app is ready...
def lightSensorState = luminance1.currentIlluminance //...then, get the illuminance from the sensor
log.debug "Illuminance: $lightSensorState < $lux"
if(lightSensorState < lux) { // compare the illuminance with user lux setting, if the condition passes, turn on the lights
log.debug "Turning on lights by contact opening and lux"
lightOn()
}
if(sunBool == true && luxBool == false && state.myState == "ready") { //this is used when sunSet/Rise was selected by the user when he/she also selected a lux sensor, in this case the lux has NO priority and whichever is true, lux or sunset/rise, then turn on the lights
if(now() > state.setTime && now() < state.riseTime) {
log.debug "Turning on lights by contact opening and RiseSet non priority"
lightOn()
}
}
}
else if(sunBool == true && state.myState == "ready") { //if the user selected to use surRise/Set and the app is ready...
if(now() > state.setTime && now() < state.riseTime) { //checks if now is within dark period SET to RISE, if so, turn on the lights
log.debug "Turning on lights by contact opening and RiseSet"
lightOn()
}
}
else if(state.myState == "ready") { //if no lux and no sunset was selected then it will fall here and turn on the light
log.debug "Turning on lights by contact opening only"
lightOn()
}
else if(state.myState == "active") { // if the app was already active, by motion or contact, and the contact re-opened
log.debug "unscheduling" // stop the timer
unschedule()
}
}
def lightOn() {
state.myState = "activating"
lights.on()
// runIn(60, checkmyState)
}
/*
def checkmyState() {
log.debug "checking myState"
if(state.myState == "activating") {
state.myState = "ready"
log.debug "lights failed to turn on, setting state to ready again"
}
log.debug "state: " + state.myState
}
*/
def setCheckAndSchedule() {
if(time == 0)
log.debug "user set lights to not turn off" // self-explanatory
else if(state.myState != "already on") { // checking if switch state did not change
runIn(time, turnOffLight)
log.debug "Timer Counting"
}
}
def turnOffLight() {
log.debug "double checking if light was not turned Off/On by user"
if (state.myState == "active") { // after the timer is done, it double check to see if nothing changed
// before commanding the lights to off
log.debug "turning off lights"
lights.off()
state.myState = "ready" // set the state to "ready" again
log.debug "state: " + state.myState
}
}
private getSunriseOffset() {
sunriseOffsetValue ? (sunriseOffsetDir == "Before" ? "-$sunriseOffsetValue" : sunriseOffsetValue) : null
}
private getSunsetOffset() {
sunsetOffsetValue ? (sunsetOffsetDir == "Before" ? "-$sunsetOffsetValue" : sunsetOffsetValue) : null
}