/* Switch/Valve Schedule Controller and More * * 2023 T. K. (kampto) * NOTES: Generate a schedule to Automate multiple Lights, Outlets, Switches, Relays, Sprinklers, Valves.... Use custom time, Sunset/Sunrise, other actions or triggers. * * 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. * * Change Revision History: * Ver Date: Who: What: * 2.3.2 2025-12-12 kampto Moved Mode trigger#4 and Motion Ontime's to table Run Time. Modified notifications and bug found. Added 'Last Info Log' to status area. New notify every 5min option if trigger still active. * 2.3.1 2025-11-22 kampto Add Action case #3, ability to manually turn on and have timed Off. Improved Notification options. Changed temp trigger to differential temp On/Off. Added toggle for Advanced/Basic display. Updated instructions. Make notifications per device and 3 Map options * 2.2.3 2025-09-18 kampto Add Pause Case #6 using Hub Variables. Clean up some Options labeling. Add Last time device active coloumn, !!requires remiving and re-adding devices. * 2.2.0 2025-08-22 kampto Seperated Triggers from Actions. Added Temperatiure trigger. Added option to clear start time if seledcted. Some clean up. * 2.1.5 2025-07-18 kampto Added trigger by Mode action. Icons in Option selections * 2.1.2 2025-07-11 kampto Fixed some spelling, addded icons to status area below table.Code clean up * 2.1.0 2025-06-20 kampto Added off at Sunrise option. Changed IgnoreCase to several SpecialPause cases. Add color to status bar values. Able to toggle device state in table. Add contact pause case. Added action options including blink, contact and switch triggers * 1.6.2 2025-05-20 kampto Added; Leak Detect option, Off @Sunrise, Modes per device, Motion sensor trig, Notification options. Other app enhancements. Improved notes. * 1.5.2 2025-05-11 kampto Add option ignore a pause from rain or moisture sensors. Modes for all. Other app enhancements. Improved notes. * 1.4.6 2024-07-16 kampto Fixed app error when removing a device, fixed null value error for run twice offset, fixed remove device unsubscribe * 1.4.2 2024-04-15 kampto Fixed 2nd run time and 'wet' pause bug * 1.4.0 2024-02-28 kampto Added all Off time user input * 1.3.1 2023-09-26 kampto Added Humidity/Moisture sensor to pause sprinkler schedule. Add status of sprinker pause sensors below table * 1.2.2 2023-07-28 kampto Added Valve capability and 2nd run time per day Hr offset per device, Bug fix on run twice a day. Added table display total time in days option. * 1.1.0 2023-05-18 kampto Add sunset/sunrise start options, format changes. * 1.0.3 2023-05-13 kampto Odd days only option. Sprinkler Options with moisture sensor. Pause a single device checkbox. Added Modes. More usage notes. * 1.0.0 2023-05-09 kampto First Build from scratch. Start times, Durations, Days. */ import groovy.time.TimeCategory import java.text.SimpleDateFormat def titleVersion() {state.name = "Switch/Valve Controller (Schedule/Control On/Off: Lights, Outlets, Sprinklers, Switches, Relays, Valves...)"; state.version = "2.3.2" } definition ( name: "Switch/Valve Scheduler and More", namespace: "kampto", author: "T. K.", description: "Automate/Schedule/Control Switches, Relays, Outlets, Sprinklers, Valves", category: "Control", iconUrl: "", iconX2Url: "", importUrl: "https://raw.githubusercontent.com/kampto/Hubitat/main/Apps/Switch%20Scheduler%20and%20More", documentationLink: "https://community.hubitat.com/t/app-switch-scheduler-and-more-schedule-lights-outlets-switches-relays-sprinklers-valves-and-more/118720", singleThreaded: true ) preferences { page(name: "mainPage") } //////////////////////////////////////////////////////////// Main Page Inputs/Set-Up ///////////////////////////////////////////////////////////// def mainPage() { if (app.getInstallationState() != "COMPLETE") {hide=false} else {hide=true} if (state.DeVices == null) state.DeVices = [:] if (state.DeVicesList == null) state.DeVicesList = [] if (pauseHumidValue == null) {pauseHumidValue = 50} if (runTwiceOffset == null) {runTwiceOffset = 12} if (state.sensorPause == null) {state.sensorPause = false} if (allOffTime == null) {allOffTime = "000000000000000000000000000000"} if (statsResetTime == null) {statsResetTime = "000000000000000000000000000000"} if (state.lastNotify == null) {state.lastNotify = "None Yet"} dynamicPage(name: "mainPage", title: "", install: true, uninstall: true) { displayTitle() section (getFormat("header","Initial Set-Up:"),hideable: true, hidden: hide){ label title: "1. Name this App", required: true, submitOnChange: true, width: 3 input "DeVices", "capability.switch, capability.valve", title: "2. Select Devices to Turn On/Off and Track Time", required: true, multiple: true, submitOnChange: true, width: 6 paragraph "Available capabilities include Switches(Lights, Outlets, Relays, etc.) and Valves. Combine multiple devices in single App table or create multiple instances of this App with differnt names. If you only need Basic On/Off timers uncheck 'Advanced features' icon in Options section. For more advnced funtionality like Triggers, Motion, Pauses, modes, etc.. keep Advanced On. Hit Update/Store button to load the Icons if you see 'null'. Hit 'Done' after creating first instance of App to save it." DeVices.each {dev -> if(!state.DeVices[dev.id]) { state.DeVices[dev.id] = [start: dev.currentSwitch == "on" || dev.currentValve == "open" ? now() : 0, total: 0, sunTime: false, sunrise: false, sunset: true, offset: 0, modes: "Any", blink: false, actions: 0, trigger: 0, sun: false, mon: false, tue: false, wed: false, thu: false, fri: false, sat: false, odd: false, onTime: "NotYet", notifyOptions: 0, notifyRepeat: 0, startTime: "000000000000000000000000000000", durTime: 0, cron: "", days: "", zone: 0, counts: 0, runsDay: 1, pause: false, specialPause: 0, sensorPause: false, motionCase: 0] state.DeVicesList += dev.id } } } section { if(DeVices) { if(DeVices.id.sort() != state.DeVicesList.sort()) { state.DeVicesList = DeVices.id Map newState = [:] DeVices.each{dev -> newState[dev.id] = state.DeVices[dev.id]} state.DeVices = newState } refreshHandler() // get latest times at app open paragraph displayTable() input "refresh", "button", title: "REFRESH Table Times, Counts, & States", width: 4 input "allOff", "button", title: "TURN OFF all switches/valves", width: 3 input name: "inDaysCBool", type: "bool", title:getFormat("important","Display Total Time in Days?"), defaultValue:false, submitOnChange:true, width: 3 //// ver1.1.6 if (advModeBool) {statusBar() } //// Toggle Advanced mode options statusBar() } } ////////////////////////////////////////////////////////////// Advanced Inputs ///////////////////////////////////////////////////////////// section(getFormat("header","Standard Options:"),hideable: true, hidden: false) { input "updateButton", "button", title: "Update/Store Schedules without hitting Done exiting App", width: 4, newLineAfter: false input name: "advModeBool", type: "bool", title: getFormat("important","Toggle On Advance Features, Off for Basic Times/Days Only.  
Note: If you have advance features set up and toggle to Basic Mode they will not be erased, only hidden and still run in background."), defaultValue:true, submitOnChange:true, newLineAfter: true, width: 7 input name: "pauseAllBool", type: "bool", title: getFormat("important","Pause All Devices upcoming Schedules and Triggers?   Overrides All Cases, Nothing will run!"), defaultValue:false, submitOnChange:true, style: 'margin-left:10px' input name: "allOffBool", type: "bool", title: getFormat("important","Switch All Off at a Specific daily time?
This will not pause upcoming schedules, only turn Off what is currently On. This must remain checked ON to enable."), defaultValue:false, submitOnChange:true, style: 'margin-left:10px' if (allOffBool) { input name: "allOffTime", type: "time", title:getFormat("blueRegular","Enter All Off time, Applies to all.   Uses 24hr time,   Hit Update"), defaultValue: "", required: false, submitOnChange:true, width: 5, newLineAfter: true, style: 'margin-left:70px' } input name: "remoteSwitchBool", type: "bool", title: getFormat("important","Remote Switch All Off and Pause/Resume Capability?   Overrides All Cases
Use dashboard or virtual switch. This must remain checked ON to enable"), defaultValue:false, submitOnChange:true, style: 'margin-left:10px' if (remoteSwitchBool) { input "remoteSwitch", "capability.switch", title: getFormat("important", "Select a Switch to Remotely turn Off all switches and Pause/Resume all schedules (Optional)
Real or Virtual Switch, Switch On is Off/Pause"), required: false, multiple: false, submitOnChange: true, style: 'margin-left:160px' } input name: "runTwiceOffset", type: "enum", title: getFormat("important","If device 'Runs per Day' = 2 in table; Select hours after 1st run start to start 2nd run of day
Default = 12hr,   Only applies to all devices with 2 'Runs a day' selected, max 2nd run time is at hr 23:00"), defaultValue: "12", submitOnChange: true, options: ["1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16"], required: false, width: 7, newLineAfter: true, style: 'margin-left:10px' input name: "sunriseOffset", type: "number", title: getFormat("important","If using 'Off @ Sunrise',   Enter +/- Sunrise Offset minutes to adjust Off time    Enter -360 to 360min, Default = -30min"), defaultValue: "-30", submitOnChange: true, required: false, width: 9, accepts: "-360 to 360", range: "-360..360", newLineAfter: true, style: 'margin-left:10px' input name: "statsResetBool", type: "bool", title: getFormat("important","Reset On Time and Count Stats at a specific time and day weekly?
Applys to all devices. This must remain checked ON to enable."), defaultValue:false, submitOnChange:true, style: 'margin-left:10px' if (statsResetBool) { input name: "statsResetTime", type: "time", title:getFormat("blueRegular","Enter All Off time, Applies to all.   Uses 24hr time,   Hit Update"), defaultValue: "", required: false, submitOnChange:true, width: 4, style: 'margin-left:70px' input name: "statsResetDay", type: "enum", title: getFormat("blueRegular","Select Day"), defaultValue: "SUN", submitOnChange: true, options: ["Everyday","SUN","MON","TUE","WED","THU","FRI","SAT"], required: false, width: 2, style: 'margin-left:10px' //// ver1.3.3 } input name: "formatBool", type: "bool", title: getFormat("important","Enable Alternative UI formatting, dark screen mode?"), defaultValue:false, submitOnChange:true, style: 'margin-left:10px' input name: "logEnableBool", type: "bool", title: getFormat("important","Enable Debug Logging of App based device activity and refreshes?
Shuts off in 1hr"), defaultValue:true, submitOnChange:true, style: 'margin-left:10px' } // logDebugEnableBool if (advModeBool) { //// Toggle Advanced mode options /////////////// Notifications section(getFormat("header","Push Notification Options:   Selectable per devices"),hideable: true, hidden: false) { input "pushDevice", "capability.notification", title: "Select Device(s) to send Notifications to (Optional)   Leave empty for no Notifications.", multiple: true, required: false, submitOnChange: true, width: 8, newLineAfter: true, style: 'margin-left:10px' if (pushDevice) { input "notifyOptions1", "enum", title: "Notify Options ${state.notifyOptionsIcon1}: Select Notify Events:", required: false, multiple: true, submitOnChange: true, width: 3, style: 'margin-left:60px', options:[ ["pushSchedOn": "Get Notified of Scheduled Device On"], ["pushSchedBlink": "Get Notified of Scheduled Device On with Blink (if Actions 1 or 2 used)"], ["pushManOn": "Get Notified of Manually turned On Devices with Auto Turn Off (if Action 3 used)"], ["pushDevOff": "Get Notified of Devices turning Off"], ["pushMotion": "Get Notified of Motion Detection (if Motion 1 or 2 used)"], ["pushLeak": "Get Notified of Leak Detection (if Leak detect used)"], ["pushContactTrig": "Get Notified of Device triggered On by Contact Sensor (if Trigger 1 used)"], ["pushSwitchTrig": "Get Notified of Device triggered On by Switch (if Trigger 2 used)"], ["pushTempTrig": "Get Notified of Device triggered On by Temperature (if Trigger 3 used)"], ["pushModeTrig": "Get Notified of Device triggered On by Mode change (if Trigger 4 used)"], ["pushTrigEvery5min": "Send repeat Notifications every 5min while Trigger remain active (if Trigger 1 to 3 used)"] ] input "notifyOptions2", "enum", title: "Notify Options ${state.notifyOptionsIcon2}: Select Notify Events:", required: false, multiple: true, submitOnChange: true, width: 3, style: 'margin-left:60px', options:[ ["pushSchedOn": "Get Notified of Scheduled Device On"], ["pushSchedBlink": "Get Notified of Scheduled Device On with Blink (if Actions 1 or 2 used)"], ["pushManOn": "Get Notified of Manually turned On Devices with Auto Turn Off (if Action 3 used)"], ["pushDevOff": "Get Notified of Devices turning Off"], ["pushMotion": "Get Notified of Motion Detection (if Motion 1 or 2 used)"], ["pushLeak": "Get Notified of Leak Detection (if Leak detect used)"], ["pushContactTrig": "Get Notified of Device triggered On by Contact Sensor (if Trigger 1 used)"], ["pushSwitchTrig": "Get Notified of Device triggered On by Switch (if Trigger 2 used)"], ["pushTempTrig": "Get Notified of Device triggered On by Temperature (if Trigger 3 used)"], ["pushModeTrig": "Get Notified of Device triggered On by Mode change (if Trigger 4 used)"], ["pushTrigEvery5min": "Send repeat Notifications every 5min while Trigger remains active (if Trigger 1 to 3 used)"] ] input "notifyOptions3", "enum", title: "Notify Options ${state.notifyOptionsIcon3}: Select Notify Events:", required: false, multiple: true, submitOnChange: true, width: 3, style: 'margin-left:60px', options:[ ["pushSchedOn": "Get Notified of Scheduled Device On"], ["pushSchedBlink": "Get Notified of Scheduled Device On with Blink (if Actions 1 or 2 used)"], ["pushManOn": "Get Notified of Manually turned On Devices with Auto Turn Off (if Action 3 used)"], ["pushDevOff": "Get Notified of Devices turning Off"], ["pushMotion": "Get Notified of Motion Detection (if Motion 1 or 2 used)"], ["pushLeak": "Get Notified of Leak Detection (if Leak detect used)"], ["pushContactTrig": "Get Notified of Device triggered On by Contact Sensor (if Trigger 1 used)"], ["pushSwitchTrig": "Get Notified of Device triggered On by Switch (if Trigger 2 used)"], ["pushTempTrig": "Get Notified of Device triggered On by Temperature (if Trigger 3 used)"], ["pushModeTrig": "Get Notified of Device triggered On by Mode change (if Trigger 4 used)"], ["pushTrigEvery5min": "Send repeat Notifications every 5min while Trigger remains active (if Trigger 1 to 3 used)"] ] input name: "pushMode", type: "mode", title: getFormat("blueRegular","Only send Notifications during selected Mode?"), defaultValue: "Any", width: 4, submitOnChange:true, style: 'margin-left:60px' } } ///////////// Triggers section(getFormat("header","TRIGGER Options:   Use devices/sensors to trigger On/Off switches or valves"),hideable: true, hidden: false) { input name: "contactTrigger", type: "capability.contactSensor", title: getFormat("important","Trigger $state.triggerIcon1: Select a Contact sensor to trigger device On.   Device will stay On as long as Contact is 'open'. Applies to devices with 'Trigger' case #1 in table. Will not run if Paused, Incorrect mode, or Special Pause case. Will work on devices with or without a start time/schedule defined in table."), multiple: true, width: 9, submitOnChange:true, style: 'margin-left:10px' //// ver2.1.0 input name: "switchTrigger", type: "capability.switch", title: getFormat("important","Trigger $state.triggerIcon2: Select a Switch to turn On a seperate device.   Device will stay On as long as selected Switch is 'on'. Applies to devices with 'Trigger' case #2 in table. Will not run if Paused, Incorrect mode, or Special Pause case. Will work on devices with or without a start time/schedule defined in table."), multiple: true, width: 9, submitOnChange:true, style: 'margin-left:10px' //// ver2.1.0 input name: "tempTrigger", type: "capability.temperatureMeasurement", title: getFormat("important","Trigger $state.triggerIcon3: Select a Temperature sensor to Trigger device.   Enter a turn On temp and a turn Off temp. They can be high to low or low to high. If they are equal temps it will only use the On trigger temp and turn device On on way up then Off on way down to On temp. Applies to devices with 'Trigger' case #3 in table. Will not run if Paused, Incorrect mode, or Special Pause case. Will work on devices with or without a start time/schedule defined in table."), multiple: true, width: 9, newLineAfter: true, submitOnChange:true, style: 'margin-left:10px' //// ver2.1.0 if (tempTrigger) { input name: "tempTrigOn", type: "decimal", title: getFormat("blueRegular","Enter On Trigger Temperature,  Default = 40"), defaultValue: "40", required: false, submitOnChange: true, width: 4, style: 'margin-left:30px' input name: "tempTrigOff", type: "decimal", title: getFormat("blueRegular","Enter Off Trigger Temperature,  Default = 40"), defaultValue: "40", required: false, submitOnChange: true, width: 4, style: 'margin-left:30px' if (tempTrigOn > tempTrigOff) {paragraph "      Will turn On if temp >= ${tempTrigOn}deg and Off when temp <= ${tempTrigOff}deg."} if (tempTrigOn < tempTrigOff) {paragraph "      Will turn On if temp <= ${tempTrigOn}deg and Off when temp >= ${tempTrigOff}deg."} } paragraph getFormat("important","Trigger $state.triggerIcon4: Select Mode to trigger Device On/Off.   Applies to Devices with 'Trigger' case #4 in table.   Switches/Valves will turn On/Off when mode changes to the Selected unless paused.
Will toggle back when mode changes back to not selected mode. Will use Run Time in table, If run time is 0 will stay On until mode changes back. Will work on devices with or without a start time/schedule defined in table.
") input name: "modeTrigger", type: "mode", title: getFormat("blueRegular","Select Mode Trigger"), width: 2, submitOnChange:true, style: 'margin-left:60px' input name: "modeInvertBool", type: "bool", title: getFormat("blueRegular","Turn Device Off if On with mode instaed?"), defaultValue:false, width: 3, submitOnChange:true, style: 'margin-left:30px' } ///////////// Motion Detection section(getFormat("header","MOTION DETECTION Options:   For Switches & Valves"),hideable: true, hidden: false) { input "motionSensor1", "capability.motionSensor", title: getFormat("important", "Select Motion Sensor(s) Group $state.motionIcon1 to trigger switch (Optional)
Applies to all devices with this Case # selected. Will not run if Paused, Incorrect mode, or Special Pause case. You can use Motion On triggers with or without an entered schedule. Will not use Blink Actions1,2 Cases."), required: false, multiple: true, submitOnChange: true, width: 8, style: 'margin-left:10px' if (motionSensor1) { input name: "motionActive1", type: "bool", title: getFormat("blueRegular","Keep Device On until motion is Inactive instead of using Table run time?   Will ignore entered time"), defaultValue:false, submitOnChange:true, style: 'margin-left:60px' //if (!motionActive1) {input name: "motionRunTime1", type: "decimal", title: getFormat("blueRegular","Enter switch run time after Motion triggered    Enter 0.1 to 120min, Default = 3min"), defaultValue: "3", submitOnChange: true, required: false, width: 7, accepts: ".1 to 120", range: ".1..120", newLineAfter: true, style: 'margin-left:60px'} input name: "invertBool1", type: "bool", title: getFormat("blueRegular","If Motion and a Switch/Valve is currently On, switch Off instead?   Off devices will still switch On
Will switch Off for run time or active entered above then back On. Toggle a night light to alert intruders!
"), defaultValue:false, submitOnChange:true, newLineAfter: true, style: 'margin-left:60px' } input "motionSensor2", "capability.motionSensor", title: getFormat("important", "Select Motion Sensor(s) Group $state.motionIcon2 to trigger switch (Optional)
Applies to all devices with this Case # selected. Will not run if Paused, Incorrect mode, or Special Pause case. You can use Motion On triggers with or without an entered schedule. Will not use Blink Actions Cases."), required: false, multiple: true, submitOnChange: true, width: 8, style: 'margin-left:10px' if (motionSensor2) { input name: "motionActive2", type: "bool", title: getFormat("blueRegular","Keep Device On until motion is Inactive instead of using Table run time?   Will ignore entered time"), defaultValue:false, submitOnChange:true, style: 'margin-left:60px' //if (!motionActive2) {input name: "motionRunTime2", type: "decimal", title: getFormat("blueRegular","Enter switch run time after Motion triggered    Enter 0.1 to 120min, Default = 3min"), defaultValue: "3", submitOnChange: true, required: false, width: 7, accepts: ".1 to 120", range: ".1..120", newLineAfter: true, style: 'margin-left:60px'} input name: "invertBool2", type: "bool", title: getFormat("blueRegular","If Motion and a Switch/Valve is currently On, switch Off instead?   Off devices will still switch On
Will switch Off for run time or active entered above then back On. Toggle a night light to alert intruders!
"), defaultValue:false, submitOnChange:true, newLineAfter: true, style: 'margin-left:60px' } } ///////////// Actions section(getFormat("header","ACTION Options:   Caese 1 & 2 Blink not recommended for valves"),hideable: true, hidden: false) { paragraph getFormat("important","Actions $state.actionsIcon1: Blink Timer1 device On/Off feature?   Applies to scheduled devices with 'Actions' case #1 in table.     Enter 0.1 to 60min, Default = 1min") input name: "blinkOn1", type: "decimal", title: getFormat("blueRegular","Input blink 'ON' time minutes"), defaultValue: "1", required: false, submitOnChange: true, accepts: "0.1 to 60", range: "0.1..60", width: 3, style: 'margin-left:60px' input name: "blinkOff1", type: "decimal", title: getFormat("blueRegular","Input blink 'OFF' time minutes"), defaultValue: "1", required: false, submitOnChange: true, accepts: "0.1 to 60", range: "0.1..60", width: 3, style: 'margin-left:60px' paragraph getFormat("important","Actions $state.actionsIcon2: Blink Timer2 device On/Off feature?   Applies to scheduled devices with 'Actions' case #2 in table.     Enter 0.1 to 60min, Default = 1min") input name: "blinkOn2", type: "decimal", title: getFormat("blueRegular","Input blink 'ON' time minutes"), defaultValue: "1", required: false, submitOnChange: true, accepts: "0.1 to 60", range: "0.1..60", width: 3, style: 'margin-left:60px' input name: "blinkOff2", type: "decimal", title: getFormat("blueRegular","Input blink 'OFF' time minutes"), defaultValue: "1", required: false, submitOnChange: true, accepts: "0.1 to 60", range: "0.1..60", width: 3, style: 'margin-left:60px' paragraph getFormat("important","Actions $state.actionsIcon3: If Manually or Triggered On, Use Run time that was set in table, then turn Off.   Applies to devices with 'Actions' case #3 in table.") } ///////////// Pause Cases section(getFormat("header","PAUSE CASE Options:   Schedule pause via; Wet, Moisture, Contact state, Voltage, ... For Switches & Valves"),hideable: true, hidden: false) { input "humidSensor", "capability.relativeHumidityMeasurement", title: getFormat("important", "Pause Case $state.pauseCaseIcon1 Humidity/Moisture: Select Humidity sensor(s) to Pause Schedules/Triggers
If Humidity above (or below) selected % Will pause upcoming, Applies to devices with 'Pause Case' #1 in table. Typically used to pause Sprinklers."), required: false, multiple: true, newLineAfter: true, submitOnChange: true, width: 8, style: 'margin-left:10px' if (humidSensor) {input name: "pauseHumidValue", type: "number", title: getFormat("blueRegular","Select Humidity level % to Pause    Default = 50%"), defaultValue: "50", submitOnChange: true, required: false, width: 4, style: 'margin-left:60px' input name: "invertHumidBool", type: "bool", title: getFormat("blueRegular","Pause if Humidity % is below selected level instead?"), defaultValue: false, submitOnChange: true, required: false, width: 3, newLineAfter: true } input "wetSensor", "capability.waterSensor", title: getFormat("important", "Pause Case $state.pauseCaseIcon2 Wet/Dry: Select Wet/Dry sensor(s) to Pause Schedules/Triggers
'Wet' will pause upcoming, Applies to devices with 'Pause Case' #2 in table. Typically used to pause Sprinklers."), required: false, multiple: true, submitOnChange: true, width: 8, style: 'margin-left:10px' input "voltageSensor", "capability.voltageMeasurement", title: getFormat("important", "Pause Case $state.pauseCaseIcon3 Voltage: Select Voltage sensor(s) to Pause Schedules/Triggers
Typically use for Solar situations. If <= threshold will pause upcoming, Applies to devices with 'Pause Case' #3 in table"), required: false, multiple: true, submitOnChange: true, width: 8, style: 'margin-left:10px' if (voltageSensor) {input name: "voltageThreshold", type: "decimal", title: getFormat("blueRegular","Enter Voltage to pause schedule if below    Default = 52.2v"), defaultValue: "52.2", submitOnChange: true, required: false, width: 6, newLineAfter: true, style: 'margin-left:60px' } input "contactSensor", "capability.contactSensor", title: getFormat("important", "Pause Case $state.pauseCaseIcon4 Contacts: Select Contact sensor(s) to Pause Schedules/Triggers
Doors, Windows, other.. Will pasue if Open (or closed). Applies to devices with 'Pause Case' #4 in table"), required: false, multiple: true, submitOnChange: true, width: 8, style: 'margin-left:10px' if (contactSensor) {input name: "invertContactBool", type: "bool", title: getFormat("blueRegular","Change logic to Pause if contact Closed instead?"), defaultValue:false, submitOnChange:true, newLineAfter: true, style: 'margin-left:60px' } input "tempSensor", "capability.temperatureMeasurement", title: getFormat("important", "Pause Case $state.pauseCaseIcon5 Temperature: Select Temperature sensor(s) to Pause Schedules/Triggers
If Temperature is above (or below) selected Value Will pause upcoming, Applies to devices with 'Pause Case' #5 in table."), required: false, multiple: true, newLineAfter: true, submitOnChange: true, width: 8, style: 'margin-left:10px' if (tempSensor) {input name: "pauseTempValue", type: "number", title: getFormat("blueRegular","Select Temperature to Pause    Default = 50deg"), defaultValue: "50", submitOnChange: true, required: false, width: 4, style: 'margin-left:60px' input name: "invertTempBool", type: "bool", title: getFormat("blueRegular","Pause if Temperature is below selected level instead?"), defaultValue: false, submitOnChange: true, required: false, width: 3, newLineAfter: true } /////// Get all variables just incase needed HashMap varList = getAllGlobalVars() ArrayList selectList = [] varList.each { selectList.add("${it.key}") } input "varConnect", "enum", options:selectList , description:"Select Variable", title:"Pause Case $state.pauseCaseIcon6 Variable: Select Variable(s) to Pause Schedules/Triggers   Must be a numerical value, Number or String. Not Decimal or Text ", submitOnChange:true, required: false, multiple: true, newLineAfter: true, width: 8, style: 'margin-left:10px' if (varConnect) {input name: "pauseVarValue", type: "number", title: getFormat("blueRegular","Enter Number to Pause if selected Variable(s) are Above/Below it
Default = 0, Integers only"), defaultValue: "0", submitOnChange: true, required: false, width: 4, style: 'margin-left:60px' input name: "invertVarBool", type: "bool", title: getFormat("blueRegular","Check this Off to Pause if Variable(s) are above entered Number, Check On to Pause if below"), defaultValue: false, submitOnChange: true, required: false, width: 3, newLineAfter: true } input name: "leakBool", type: "bool", title: getFormat("important","Leak Detect: Use Wet/Dry sensor as leak detect to turn Off All Devices and Pause All upcoming Schedules/Triggers?
Applies to all switches/valves, This must remain checked ON to work. See Usage Notes for more info"), defaultValue:false, submitOnChange:true, newLineAfter: true, style: 'margin-left:10px' if (leakBool) { input "wetLeakSensor", "capability.waterSensor", title: getFormat("important", "Select Wet/Dry sensor(s) to detect leak and trigger Shutdown
'Wet' will trigger, Not recommended to share same sensor with selected Sprinker wet/dry sensor"), required: false, multiple: true, submitOnChange: true, width: 6, newLineAfter: true, style: 'margin-left:60px' } } } /////////////////////////////////////////////////////////////// Usage Notes Section ////////////////////////////////////////////////////////////// section(getFormat("header","Usage Notes / Instructions:"), hideable: true, hidden: hide) { paragraph getFormat("lessImportant","") } } } ////////////////////////////////////////////////////////////////// Main Table Build /////////////////////////////////////////////////////////////// String displayTable() { //////////////////////////// Table Input Fields ///////////////////////////// if(state.newStartTime) {/////// Input Start Time input name: "newStartTime", type: "time", title:getFormat("noticable","Enter Start/On Time, Applies to all checked days for Switch.
Uses 24hr time,   Hit Update"), defaultValue: "", required: false, submitOnChange:true, width: 5, newLineAfter: true, style: 'margin-left:10px' input "clearStartTime", "bool", title: "Clear Start Time" + getFormat("blueRegular","Clearing Start time resets it to 'Select'. Triggers and Motion cases (if using) will still operate the device regardless if no Days or Start time has been set."), submitOnChange:true, style: 'margin-left:30px' if(newStartTime || clearStartTime) { state.DeVices[state.newStartTime].startTime = newStartTime if (clearStartTime) {state.DeVices[state.newStartTime].startTime = "000000000000000000000000000000"; app?.updateSetting("clearStartTime",[value:"false",type:"bool"])} state.remove("newStartTime") app.removeSetting("newStartTime") paragraph "" } } if(state.newDurTime) {//////// Input Run Duration input name: "newDurTime", type: "number", title:getFormat("noticable","Enter Run/On Duration in Minutes, Applies to all checked days for Switch.   Hit Enter"), defaultValue: 0, required: false, submitOnChange:true, width: 8, newLineAfter: true, style: 'margin-left:10px' input "clearDurTime", "bool", title: "Clear Duration Time" + getFormat("blueRegular","Clear Duration time to 'Zero'. Triggers and Motion cases (if using) will still operate the device regardless if no Duration time."), submitOnChange:true, style: 'margin-left:30px' if(newDurTime || clearDurTime) { state.DeVices[state.newDurTime].durTime = newDurTime if (clearDurTime) {state.DeVices[state.newDurTime].durTime = 0; app?.updateSetting("clearDurTime",[value:"false",type:"bool"])} state.remove("newDurTime") app.removeSetting("newDurTime") paragraph "" } } if(state.newOffsetTime) { ///////// Sunrise / Sunset Offset time //// ver1.1.0 input name: "newOffsetTime", type: "number", title:getFormat("noticable","Enter +/- Offset time from Sunrise or Sunset in minutes, Applies to all checked days for Switch.
EX: 30, -30, or -90,   Hit Enter"), defaultValue: 0, required: false, submitOnChange:true, accepts: "-1000 to 1000", range: "-1000..1000", width: 8, newLineAfter: true, style: 'margin-left:10px' if(newOffsetTime) { state.DeVices[state.newOffsetTime].offset = newOffsetTime state.remove("newOffsetTime") app.removeSetting("newOffsetTime") paragraph "" } } if(state.newMode) { //////// Input Modes input name: "newMode", type: "mode", title: getFormat("noticable","Select during which mode this device will only run.
Home, Away, etc..   Or Any if No Selection"), defaultValue: "", submitOnChange:true, width: 7, required: false, newLineAfter: true, style: 'margin-left:10px' if(newMode) { state.DeVices[state.newMode].modes = newMode state.remove("newMode") app.removeSetting("newMode") paragraph "" } } ////////////////////////////// Table Buttons / Entries ///////////////////////////// if(state.resetTotal) { def dev = DeVices.find{"$it.id" == state.resetTotal} //// Reset Cumulative time and counts per device Button state.DeVices[state.resetTotal].start = now() state.DeVices[state.resetTotal].total = 0 state.DeVices[state.resetTotal].counts = 0 state.remove("resetTotal") } if(state.deviceState) { def dev = DeVices.find{"$it.id" == state.deviceState} ////ver2.0.3 Toggle device state //log.debug "${app.label} -- TEST state.deviceState = ${state.deviceState}, dev = ${dev}" if (dev.currentSwitch == "on") {dev.off()} else if (dev.currentSwitch == "off") {dev.on()} else if (dev.currentValve == "closed") {dev.open()} else if (dev.currentValve == "open") {dev.close()} endif state.trigNotifyBool = false // keep this open for manual turn ons state.remove("deviceState") app.removeSetting("deviceState") paragraph "" } if(state.sunCheckedBox) { def dev = DeVices.find{"$it.id" == state.sunCheckedBox} //// Sunday - Saturday, odd day, Check Boxes state.DeVices[state.sunCheckedBox].sun = true; state.remove("sunCheckedBox") } else if(state.sunUnCheckedBox) {def dev = DeVices.find{"$it.id" == state.sunUnCheckedBox} state.DeVices[state.sunUnCheckedBox].sun = false; state.remove("sunUnCheckedBox") } endif if(state.monCheckedBox) {def dev = DeVices.find{"$it.id" == state.monCheckedBox} state.DeVices[state.monCheckedBox].mon = true; state.remove("monCheckedBox") } else if(state.monUnCheckedBox) {def dev = DeVices.find{"$it.id" == state.monUnCheckedBox} state.DeVices[state.monUnCheckedBox].mon = false; state.remove("monUnCheckedBox") } endif if(state.tueCheckedBox) {def dev = DeVices.find{"$it.id" == state.tueCheckedBox} state.DeVices[state.tueCheckedBox].tue = true; state.remove("tueCheckedBox") } else if(state.tueUnCheckedBox) {def dev = DeVices.find{"$it.id" == state.tueUnCheckedBox} state.DeVices[state.tueUnCheckedBox].tue = false; state.remove("tueUnCheckedBox") } endif if(state.wedCheckedBox) {def dev = DeVices.find{"$it.id" == state.wedCheckedBox} state.DeVices[state.wedCheckedBox].wed = true; state.remove("wedCheckedBox") } else if(state.wedUnCheckedBox) {def dev = DeVices.find{"$it.id" == state.wedUnCheckedBox} state.DeVices[state.wedUnCheckedBox].wed = false; state.remove("wedUnCheckedBox") } endif if(state.thuCheckedBox) {def dev = DeVices.find{"$it.id" == state.thuCheckedBox} state.DeVices[state.thuCheckedBox].thu = true; state.remove("thuCheckedBox") } else if(state.thuUnCheckedBox) {def dev = DeVices.find{"$it.id" == state.thuUnCheckedBox} state.DeVices[state.thuUnCheckedBox].thu = false; state.remove("thuUnCheckedBox") } endif if(state.friCheckedBox) {def dev = DeVices.find{"$it.id" == state.friCheckedBox} state.DeVices[state.friCheckedBox].fri = true; state.remove("friCheckedBox") } else if(state.friUnCheckedBox) {def dev = DeVices.find{"$it.id" == state.friUnCheckedBox} state.DeVices[state.friUnCheckedBox].fri = false; state.remove("friUnCheckedBox") } endif if(state.satCheckedBox) {def dev = DeVices.find{"$it.id" == state.satCheckedBox} state.DeVices[state.satCheckedBox].sat = true; state.remove("satCheckedBox") } else if(state.satUnCheckedBox) {def dev = DeVices.find{"$it.id" == state.satUnCheckedBox} state.DeVices[state.satUnCheckedBox].sat = false; state.remove("satUnCheckedBox") } endif if(state.oddCheckedBox) {def dev = DeVices.find{"$it.id" == state.oddCheckedBox} //// ver1.0.2 state.DeVices[state.oddCheckedBox].odd = true; state.remove("oddCheckedBox") } else if(state.oddUnCheckedBox) {def dev = DeVices.find{"$it.id" == state.oddUnCheckedBox} state.DeVices[state.oddUnCheckedBox].odd = false; state.remove("oddUnCheckedBox") } endif if(state.sunriseCheckedBox) {def dev = DeVices.find{"$it.id" == state.sunriseCheckedBox} //// ver2.0.3 Off at Sunrise state.DeVices[state.sunriseCheckedBox].sunrise = true; state.remove("sunriseCheckedBox") } else if(state.sunriseUnCheckedBox) {def dev = DeVices.find{"$it.id" == state.sunriseUnCheckedBox} state.DeVices[state.sunriseUnCheckedBox].sunrise = false; state.remove("sunriseUnCheckedBox") } endif if(state.pauseCase0) {def dev = DeVices.find{"$it.id" == state.pauseCase0} state.DeVices[state.pauseCase0].specialPause = 0; state.remove("pauseCase0") } else if(state.pauseCase1) {def dev = DeVices.find{"$it.id" == state.pauseCase1} state.DeVices[state.pauseCase1].specialPause = 1; state.remove("pauseCase1") } else if(state.pauseCase2) {def dev = DeVices.find{"$it.id" == state.pauseCase2} state.DeVices[state.pauseCase2].specialPause = 2; state.remove("pauseCase2") } else if(state.pauseCase3) {def dev = DeVices.find{"$it.id" == state.pauseCase3} state.DeVices[state.pauseCase3].specialPause = 3; state.remove("pauseCase3") } else if(state.pauseCase4) {def dev = DeVices.find{"$it.id" == state.pauseCase4} state.DeVices[state.pauseCase4].specialPause = 4; state.remove("pauseCase4") } else if(state.pauseCase5) {def dev = DeVices.find{"$it.id" == state.pauseCase5} state.DeVices[state.pauseCase5].specialPause = 5; state.remove("pauseCase5") } else if(state.pauseCase6) {def dev = DeVices.find{"$it.id" == state.pauseCase6} state.DeVices[state.pauseCase6].specialPause = 6; state.remove("pauseCase6") } endif if(state.motionCase0) {def dev = DeVices.find{"$it.id" == state.motionCase0} state.DeVices[state.motionCase0].motionCase = 0; state.remove("motionCase0") } else if(state.motionCase1) {def dev = DeVices.find{"$it.id" == state.motionCase1} state.DeVices[state.motionCase1].motionCase = 1; state.remove("motionCase1") } else if(state.motionCase2) {def dev = DeVices.find{"$it.id" == state.motionCase2} state.DeVices[state.motionCase2].motionCase = 2; state.remove("motionCase2") } endif if(state.actions0) {def dev = DeVices.find{"$it.id" == state.actions0} state.DeVices[state.actions0].actions = 0; state.remove("actions0") } else if(state.actions1) {def dev = DeVices.find{"$it.id" == state.actions1} state.DeVices[state.actions1].actions = 1; state.remove("actions1") } else if(state.actions2) {def dev = DeVices.find{"$it.id" == state.actions2} state.DeVices[state.actions2].actions = 2; state.remove("actions2") } else if(state.actions3) {def dev = DeVices.find{"$it.id" == state.actions3} state.DeVices[state.actions3].actions = 3; state.remove("actions3") } endif if(state.trigger0) {def dev = DeVices.find{"$it.id" == state.trigger0} state.DeVices[state.trigger0].trigger = 0; state.remove("trigger0") } else if(state.trigger1) {def dev = DeVices.find{"$it.id" == state.trigger1} state.DeVices[state.trigger1].trigger = 1; state.remove("trigger1") } else if(state.trigger2) {def dev = DeVices.find{"$it.id" == state.trigger2} state.DeVices[state.trigger2].trigger = 2; state.remove("trigger2") } else if(state.trigger3) {def dev = DeVices.find{"$it.id" == state.trigger3} state.DeVices[state.trigger3].trigger = 3; state.remove("trigger3") } else if(state.trigger4) {def dev = DeVices.find{"$it.id" == state.trigger4} state.DeVices[state.trigger4].trigger = 4; state.remove("trigger4") } endif if (state.notifyOptions0) {def dev = DeVices.find{"$it.id" == state.notifyOptions0} state.DeVices[state.notifyOptions0].notifyOptions = 0; state.remove("notifyOptions0") } else if (state.notifyOptions1) {def dev = DeVices.find{"$it.id" == state.notifyOptions1} state.DeVices[state.notifyOptions1].notifyOptions = 1; state.remove("notifyOptions1") } else if (state.notifyOptions2) {def dev = DeVices.find{"$it.id" == state.notifyOptions2} state.DeVices[state.notifyOptions2].notifyOptions = 2; state.remove("notifyOptions2") } else if (state.notifyOptions3) {def dev = DeVices.find{"$it.id" == state.notifyOptions3} state.DeVices[state.notifyOptions3].notifyOptions = 3; state.remove("notifyOptions3") } else if (state.notifyOptions4) {def dev = DeVices.find{"$it.id" == state.notifyOptions4} state.DeVices[state.notifyOptions4].notifyOptions = 4; state.remove("notifyOptions4") } endif if(state.pauseCheckedBox) {def dev = DeVices.find{"$it.id" == state.pauseCheckedBox} //// ver1.0.2 state.DeVices[state.pauseCheckedBox].pause = true; state.remove("pauseCheckedBox") } else if(state.pauseUnCheckedBox) {def dev = DeVices.find{"$it.id" == state.pauseUnCheckedBox} state.DeVices[state.pauseUnCheckedBox].pause = false; state.remove("pauseUnCheckedBox") } endif if(state.sunTimeCheckedBox) {def dev = DeVices.find{"$it.id" == state.sunTimeCheckedBox} ///// Sunrise / Sunset //// ver1.1.0 state.DeVices[state.sunTimeCheckedBox].sunTime = true; state.remove("sunTimeCheckedBox") } else if(state.sunTimeUnCheckedBox) {def dev = DeVices.find{"$it.id" == state.sunTimeUnCheckedBox} state.DeVices[state.sunTimeUnCheckedBox].sunTime = false; state.remove("sunTimeUnCheckedBox") } endif if(state.sunsetCheckedBox) {def dev = DeVices.find{"$it.id" == state.sunsetCheckedBox} state.DeVices[state.sunsetCheckedBox].sunset = true; state.remove("sunsetCheckedBox") } else if(state.sunsetUnCheckedBox) {def dev = DeVices.find{"$it.id" == state.sunsetUnCheckedBox} state.DeVices[state.sunsetUnCheckedBox].sunset = false; state.remove("sunsetUnCheckedBox") } endif if(state.runsDayBox1) {def dev = DeVices.find{"$it.id" == state.runsDayBox1} //// ver1.2.1 state.DeVices[state.runsDayBox1].runsDay = 1; state.remove("runsDayBox1") } else if(state.runsDayBox2) {def dev = DeVices.find{"$it.id" == state.runsDayBox2} state.DeVices[state.runsDayBox2].runsDay = 2; state.remove("runsDayBox2") } endif /////////////////////////////// Table Header Build String str = "" //////// font-weight: bold !important; word-wrap: break-word !important; white-space: normal!important str += "
" + "" + "" + "" + "" + "" + //// ver1.1.0 "" + //// ver1.1.0 "" + //// ver1.1.0 "" + //// ver2.0.3 "" + "" + //// ver1.2.1 "" + "" + "" + "" + "" + "" + "" + "" //// ver1.0.3 if (advModeBool) { //// Toggle Advanced mode options str += ""+ //// ver1.4.7 ""+ //// ver2.0.1 ""+ ""+ ""+ //// ver1.4.7 ""+ "" //// ver1.0.3 } str += ""+ //// ver1.0.3 ""+ "" sunHandler() // Get latest Sunrise/Sunset times and apply offset int zone = 0 DeVices.sort{it.displayName.toLowerCase()}.each {dev -> zone += 1 state.DeVices[dev.id].zone = zone String thisStartTime if (state.DeVices[dev.id].startTime == "000000000000000000000000000000") {thisStartTime = "Select"} // For app first load else thisStartTime = state.DeVices[dev.id].startTime.substring(11, state.DeVices[dev.id].startTime.length() - 12) String thisDurTime = state.DeVices[dev.id].durTime String thisOffsetTime = state.DeVices[dev.id].offset String thisCron = state.DeVices[dev.id].cron ////////////////////////// Active/On Time Calc int counts = state.DeVices[dev.id].counts int total = state.DeVices[dev.id].total / 1000 float totalDays = (total / 86400) as float //// ver1.1.1 int hours = total / 3600 total = total % 3600 int mins = total / 60 int secs = total % 60 String time = "$hours:${mins < 10 ? "0" : ""}$mins:${secs < 10 ? "0" : ""}$secs" if (inDaysCBool) {time = String.format("%.3f", totalDays)} // For table Only //// ver1.1.1 if (state.DeVices[dev.id].sunTime) {startTime = thisStartTime} //// ver1.1.0 else {startTime = buttonLink("o$dev.id", thisStartTime, "MediumBlue")} endif String devLink = "$dev" String deviceStateT //// Toggle Device State button if (dev.currentSwitch) {deviceStateT = buttonLink("Z$dev.id", dev.currentSwitch)} else if (dev.currentValve) {deviceStateT = buttonLink("Z$dev.id", dev.currentValve)} String resetTotal = buttonLink("z$dev.id", state.resetIcon) String durTime = buttonLink("q$dev.id", thisDurTime, "MediumBlue") String mode = buttonLink("1$dev.id", state.DeVices["$dev.id"].modes, "MediumBlue") String sunCheckBoxT = (state.DeVices[dev.id].sun) ? buttonLink("a$dev.id", state.greenCheckBox) : buttonLink("b$dev.id", state.unCheckedBox) String monCheckBoxT = (state.DeVices[dev.id].mon) ? buttonLink("c$dev.id", state.greenCheckBox) : buttonLink("d$dev.id", state.unCheckedBox) String tueCheckBoxT = (state.DeVices[dev.id].tue) ? buttonLink("e$dev.id", state.greenCheckBox) : buttonLink("f$dev.id", state.unCheckedBox) String wedCheckBoxT = (state.DeVices[dev.id].wed) ? buttonLink("g$dev.id", state.greenCheckBox) : buttonLink("h$dev.id", state.unCheckedBox) String thuCheckBoxT = (state.DeVices[dev.id].thu) ? buttonLink("i$dev.id", state.greenCheckBox) : buttonLink("j$dev.id", state.unCheckedBox) String friCheckBoxT = (state.DeVices[dev.id].fri) ? buttonLink("k$dev.id", state.greenCheckBox) : buttonLink("l$dev.id", state.unCheckedBox) String satCheckBoxT = (state.DeVices[dev.id].sat) ? buttonLink("m$dev.id", state.greenCheckBox) : buttonLink("n$dev.id", state.unCheckedBox) String oddCheckBoxT = (state.DeVices[dev.id].odd) ? buttonLink("r$dev.id", state.steelBlueCheckBox) : buttonLink("s$dev.id", state.unCheckedBox) //// ver1.0.3 String runsPerDayT = (state.DeVices[dev.id].runsDay == 1) ? buttonLink("u$dev.id", state.steelBlueCircle1) : buttonLink("p$dev.id", state.darkOrangeCircle2) //// ver1.2.1 String motionCaseT if (state.DeVices[dev.id].motionCase == 0) {motionCaseT = buttonLink("A$dev.id", "None", "blue", "14px")} else if (state.DeVices[dev.id].motionCase == 1) {motionCaseT = buttonLink("B$dev.id", state.motionIcon1)} else if (state.DeVices[dev.id].motionCase == 2) {motionCaseT = buttonLink("C$dev.id", state.motionIcon2)} //// ver1.2.1 endif String specialPauseT if (state.DeVices[dev.id].specialPause == 0) {specialPauseT = buttonLink("4$dev.id", "None", "blue", "14px")} else if (state.DeVices[dev.id].specialPause == 1) {specialPauseT = buttonLink("5$dev.id", state.pauseCaseIcon1)} else if (state.DeVices[dev.id].specialPause == 2) {specialPauseT = buttonLink("6$dev.id", state.pauseCaseIcon2)} //// ver1.2.1 else if (state.DeVices[dev.id].specialPause == 3) {specialPauseT = buttonLink("7$dev.id", state.pauseCaseIcon3)} //// ver2.0.4 else if (state.DeVices[dev.id].specialPause == 4) {specialPauseT = buttonLink("8$dev.id", state.pauseCaseIcon4)} else if (state.DeVices[dev.id].specialPause == 5) {specialPauseT = buttonLink("9$dev.id", state.pauseCaseIcon5)} else if (state.DeVices[dev.id].specialPause == 6) {specialPauseT = buttonLink("-$dev.id", state.pauseCaseIcon6)} endif String actionsT if (state.DeVices[dev.id].actions == 0) {actionsT = buttonLink("G$dev.id", "None", "blue", "14px")} else if (state.DeVices[dev.id].actions == 1) {actionsT = buttonLink("H$dev.id", state.actionsIcon1)} else if (state.DeVices[dev.id].actions == 2) {actionsT = buttonLink("I$dev.id", state.actionsIcon2)} else if (state.DeVices[dev.id].actions == 3) {actionsT = buttonLink("J$dev.id", state.actionsIcon3)} endif String triggerT if (state.DeVices[dev.id].trigger == 0) {triggerT = buttonLink("K$dev.id", "None", "blue", "14px")} else if (state.DeVices[dev.id].trigger == 1) {triggerT = buttonLink("L$dev.id", state.triggerIcon1)} else if (state.DeVices[dev.id].trigger == 2) {triggerT = buttonLink("M$dev.id", state.triggerIcon2)} else if (state.DeVices[dev.id].trigger == 3) {triggerT = buttonLink("N$dev.id", state.triggerIcon3)} else if (state.DeVices[dev.id].trigger == 4) {triggerT = buttonLink("O$dev.id", state.triggerIcon4)} endif String notifyOptionsT if (state.DeVices[dev.id].notifyOptions == 0) {notifyOptionsT = buttonLink("Qn$dev.id", state.pauseIcon)} else if (state.DeVices[dev.id].notifyOptions == 1) {notifyOptionsT = buttonLink("Rn$dev.id", state.notifyOptionsIcon1)} else if (state.DeVices[dev.id].notifyOptions == 2) {notifyOptionsT = buttonLink("Sn$dev.id", state.notifyOptionsIcon2)} else if (state.DeVices[dev.id].notifyOptions == 3) {notifyOptionsT = buttonLink("Tn$dev.id", state.notifyOptionsIcon3)} else if (state.DeVices[dev.id].notifyOptions == 4) {notifyOptionsT = buttonLink("Un$dev.id", state.notifyOptionsIcon4)} else {notifyOptionsT = buttonLink("Qn$dev.id", state.pauseIcon)} // Jump Start new feature from being Null endif String pauseCheckBoxT = (state.DeVices[dev.id].pause) ? buttonLink("t$dev.id", state.pauseIcon) : buttonLink("v$dev.id", state.playIcon) String sunTimeCheckBoxT = (state.DeVices[dev.id].sunTime) ? buttonLink("w$dev.id", state.purpleCheckBox) : buttonLink("x$dev.id", state.unCheckedBox) //// ver1.1.0 String sunsetCheckBoxT = (state.DeVices[dev.id].sunset) ? buttonLink("D$dev.id", state.moonIcon) : buttonLink("E$dev.id", state.sunIcon) //// ver1.1.0 String sunriseCheckBoxT = (state.DeVices[dev.id].sunrise) ? buttonLink("2$dev.id", state.steelBlueCheckBox) : buttonLink("3$dev.id", state.unCheckedBox) //// ver2.0.3 String offset = buttonLink("F$dev.id", thisOffsetTime, "MediumBlue") //// ver1.1.0 //// if (logEnableBool) {log.debug "${app.label} -- Page Refresh, **${dev}**, StartTime-${thisStartTime}...Duration-${thisDurTime}...Counts-${counts}...Cron-${thisCron}"} /////////////////////////////// Table Rows Build str += "" + "" if (dev.currentSwitch) {str += ""} //////xxxxxxxxxxxxxxxxxxxxxx///////// else if (dev.currentValve) {str += ""} //////xxxxxxxxxxxxxxxxxxxxx///////// endif if (state.DeVices[dev.id].sunTime) {str +=""} //// ver1.1.0 else str += "" endif str += "" //// ver1.0.4 if (state.DeVices[dev.id].startTime == "000000000000000000000000000000" && state.DeVices[dev.id].motionCase != 0) {str += ""} ////ver2.0.3 else if (state.DeVices[dev.id].sunTime) {str += "" + //// ver1.1.0 "" } else {str += ""} //// ver1.0.4 endif str += "" ////ver2.0.3 //// Run Time if (state.DeVices[dev.id].motionCase == 1 && motionActive1) {str += "" } ////ver2.1.0 else if (state.DeVices[dev.id].motionCase == 2 && motionActive2) {str += "" } else if (state.DeVices[dev.id].trigger == 1 && state.DeVices[dev.id].actions == 3) {str += "" } else if (state.DeVices[dev.id].trigger == 2 && state.DeVices[dev.id].actions == 3) {str += "" } else if (state.DeVices[dev.id].trigger == 3 && state.DeVices[dev.id].actions == 3) {str += "" } else if (state.DeVices[dev.id].trigger == 4) {str += "" } else if (state.DeVices[dev.id].trigger == 3) {str += "" } else if (state.DeVices[dev.id].trigger == 2) {str += "" } else if (state.DeVices[dev.id].trigger == 1) {str += "" } else if (!state.DeVices[dev.id].sunrise) {str += "" } ////ver2.03 else {str += ""} endif str += "" //// ver1.2.1 //// Days 7 Coloumns if (state.DeVices[dev.id].startTime == "000000000000000000000000000000" && state.DeVices[dev.id].motionCase != 0) {str += ""} //// ver2.0.3 else if (state.DeVices[dev.id].startTime == "000000000000000000000000000000" && state.DeVices[dev.id].motionCase == 0 && !state.DeVices[dev.id].trigger && state.DeVices[dev.id].actions == 3) {str += ""} else if (state.DeVices[dev.id].startTime == "000000000000000000000000000000" && state.DeVices[dev.id].motionCase == 0 && state.DeVices[dev.id].trigger && state.DeVices[dev.id].actions == 3) {str += ""} else if (state.DeVices[dev.id].startTime == "000000000000000000000000000000" && state.DeVices[dev.id].motionCase == 0 && (state.DeVices[dev.id].trigger == 1 || state.DeVices[dev.id].trigger == 2 || state.DeVices[dev.id].trigger == 3 || state.DeVices[dev.id].trigger == 4)) {str += ""} else if (state.DeVices[dev.id].odd) {str += ""} //// ver1.0.2 else {str += "" + "" + "" + "" + "" + "" + "" } str += "" if (advModeBool) { //// Toggle Advanced mode options if (state.DeVices[dev.id].trigger == 4) {str += ""} else {str += ""} str += "" + "" str += "" str += "" str += "" if (state.DeVices[dev.id].sensorPause) {str += ""} else {str += ""} } str += "" + ""+ "" if (dev.currentSwitch) {str += "" } else if (dev.currentValve) {str += "" } //// ver1.20 endif str += "" } str += "
#App ver${state.version}
Device
StateStart
Time
Use Sun
Set/Rise?
Rise or
Set?
Offset
+/-Min
Off @
Sunrise
Run
Time
Runs
A Day
SunMonTueWedThuFriSatOdd
Days?
Run
Mode
Use
Motion
Use
Trigger
Use
Actions
Pause
Case?
Push-
Notify?
Sensor
Paused
Manual
Pause?
Last
Active" + "
On
Counts" + "
Total
OnTime
Reset
OnTime
${state.DeVices[dev.id].zone}$devLink$deviceStateT$deviceStateT$startTime$startTime$sunTimeCheckBoxTStart @ Motion$sunsetCheckBoxT$offsetUser Time$sunriseCheckBoxTActiveActive$durTime$durTime$durTime$durTimeTempSwtchCntct$durTime$durTime$runsPerDayTUsing Motion Only, All daysUsing Manual On, Timed OffUsing Triggered On, Timed OffUsing Unscheduled Trigger CaseRun Every Odd day of Month$sunCheckBoxT$monCheckBoxT$tueCheckBoxT$wedCheckBoxT$thuCheckBoxT$friCheckBoxT$satCheckBoxT$oddCheckBoxTTrig4$mode$motionCaseT$triggerT$actionsT$specialPauseT$notifyOptionsT$state.pauseIconGrey$state.playIconGrey$pauseCheckBoxT${state.DeVices[dev.id].onTime}${state.DeVices["$dev.id"].counts}$time$time$resetTotal
" } String buttonLink(String btnName, String linkText, color = SteelBlue, font = "13px") { //// Device Link Format "
$linkText
" } /////////////////////////////////////////////////////////// Schedules and Subscribes ////////////////////////////////////////////////////////// void initialize() { subscribe(location, "mode", modeTrigHandler) subscribe(DeVices, "switch.on", onTimeHandler) subscribe(DeVices, "switch.off", offTimeHandler) subscribe(DeVices, "valve.open", onTimeHandler) //// ver1.20 subscribe(DeVices, "valve.closed", offTimeHandler) //// ver1.20 if (remoteSwitch) {subscribe(remoteSwitch, "switch.on", remoteOffHandler) } if (remoteSwitch) {subscribe(remoteSwitch, "switch.off", remoteOffHandler) } if (wetLeakSensor) {subscribe(wetLeakSensor, "water.wet", remoteOffHandler) } //// ver2.0.1 if (wetLeakSensor) {subscribe(wetLeakSensor, "water.dry", remoteOffHandler) } if (motionSensor1) {subscribe(motionSensor1, "motion.active", motionHandler1) } //// ver2.0.1 if (motionSensor2) { subscribe(motionSensor2, "motion.active", motionHandler2) } if (motionSensor1) { subscribe(motionSensor1, "motion.inactive", motionHandler1) } //// ver2.1.0 if (motionSensor2) { subscribe(motionSensor2, "motion.inactive", motionHandler2) } if (wetSensor) {subscribe(wetSensor, "water.wet", sensorUpdate) } //// ver1.0.1 if (wetSensor) {subscribe(wetSensor, "water.dry", sensorUpdate) } if (contactSensor) {subscribe(contactSensor, "contact.open", sensorUpdate) } if (contactSensor) {subscribe(contactSensor, "contact.closed", sensorUpdate) } if (humidSensor) {subscribe(humidSensor, "humidity", sensorUpdate) } //// ver1.3.0 if (voltageSensor) {subscribe(voltageSensor, "voltage", sensorUpdate) } //// ver2.0.3 if (contactTrigger) {subscribe(contactTrigger, "contact.open", contactTrigHandler) } if (contactTrigger) {subscribe(contactTrigger, "contact.closed", contactTrigHandler) } if (switchTrigger) {subscribe(switchTrigger, "switch.on", switchTrigHandler) } if (switchTrigger) {subscribe(switchTrigger, "switch.off", switchTrigHandler) } if (tempTrigger) {subscribe(tempTrigger, "temperature", tempTrigHandler) } if (varConnect) {varConnect.each{ subscribe(location,"variable:${it.toString()}","sensorUpdate") } } schedule("0 0 1 ? * * *", sunHandler) // Every day at 1am to get new sunrise/sunset times //// ver1.1.0 if (allOffBool) {schedule("${state.allOffCron}", allOffHandler)} // Set All off Time //// ver1.4.0 else {unschedule (allOffHandler)} if (statsResetBool) {schedule("${state.statsResetCron}", statsResetHandler)} // Reset Ontimes and counts else {unschedule (statsResetHandler)} if(logEnableBool) runIn(3600, logsOff) // Disable all debug Logging after time elapsed else unschedule(logsOff) ////////////// Set device cron schedules DeVices.sort{it.displayName.toLowerCase()}.each {dev -> zone = state.DeVices[dev.id].zone if (state.DeVices[dev.id].cron && state.DeVices[dev.id].durTime != 0) { schedule("${state.DeVices[dev.id].cron}", switchOnHandler, [data:zone, overwrite:false]) if (logEnableBool) {log.debug "${app.label} -- SCHEDULED Device **${dev}**......duration = ${state.DeVices[dev.id].durTime}.....cronString = ${state.DeVices[dev.id].cron}"} } } } /////////////////////////////////////////////////////////////// Sensor Updates ////////////////////////////////////////////////////////////////// def sensorUpdate(evt) { if (logEnableBool) {log.debug "${app.label} -- sensorUpdate() CALLED, ${evt.device}"} /// to ${evt}" sensorScan() // Update all sensor array states/values } //////////////////////////////////////////////?//////////// All Sensor State Scan ////////////////////////////////////////////////////////////// def sensorScan() { //// Scan all sensors and multiples and update status //// ver2.1.0 if (logEnableBool) {log.debug "${app.label} -- sensorScan() CALLED"} if (humidSensor) {for(int i = 0; i < humidSensor.currentHumidity.size(); i++) { // Scan arrary if (humidSensor.currentHumidity[i] >= pauseHumidValue && !invertHumidBool) {state.humidStateDisplay = "${humidSensor.currentHumidity}%, " + "  Pause schedules if >= ${pauseHumidValue}%"; state.humidPauseBool = true; break} else if (humidSensor.currentHumidity[i] <= pauseHumidValue && invertHumidBool) {state.humidStateDisplay = "${humidSensor.currentHumidity}%, " + "  Pause schedules if < ${pauseHumidValue}%"; state.humidPauseBool = true; break} else if (!invertHumidBool) {state.humidStateDisplay = "${humidSensor.currentHumidity}%, " + "  Pause schedules if >= ${pauseHumidValue}%"; state.humidPauseBool = false} else {state.humidStateDisplay = "${humidSensor.currentHumidity}%, " + "  Pause schedules if <= ${pauseHumidValue}%"; state.humidPauseBool = false} } } if (tempSensor) {for(int i = 0; i < tempSensor.currentTemperature.size(); i++) { // Scan arrary if (tempSensor.currentTemperature[i] >= pauseTempValue && !invertTempBool) {state.tempStateDisplay = "${tempSensor.currentTemperature}deg, " + "  Pause schedules if >= ${pauseTempValue}deg"; state.tempPauseBool = true; break} else if (tempSensor.currentTemperature[i] <= pauseTempValue && invertTempBool) {state.tempStateDisplay = "${tempSensor.currentTemperature}deg, " + "  Pause schedules if < ${pauseTempValue}deg"; state.tempPauseBool = true; break} else if (!invertTempBool) {state.tempStateDisplay = "${tempSensor.currentTemperature}deg, " + "  Pause schedules if >= ${pauseTempValue}deg"; state.tempPauseBool = false} else {state.tempStateDisplay = "${tempSensor.currentTemperature}deg, " + "  Pause schedules if <= ${pauseTempValue}deg"; state.tempPauseBool = false} } } if (varConnect) { varList = new ArrayList(); varConnect.each { varItem = getGlobalVar("$it") int varInt = Integer.valueOf("${varItem.value}") varList.add(varInt); } for(int i = 0; i < varList.size(); i++) { // Scan arrary //log.debug "TEST1 Variable-EACH: varList = ${varList[i]}...varListSize = ${varList.size()}" if (varList[i] >= pauseVarValue && !invertVarBool) {state.varStateDisplay = "${varList}, " + "  Pause schedules if >= ${pauseVarValue}"; state.varPauseBool = true; break} else if (varList[i] <= pauseVarValue && invertVarBool) {state.varStateDisplay = "${varList}, " + "  Pause schedules if < ${pauseVarValue}"; state.varPauseBool = true; break} else if (!invertVarBool) {state.varStateDisplay = "${varList}, " + "  Pause schedules if >= ${pauseVarValue}"; state.varPauseBool = false} else {state.varStateDisplay = "${varList}, " + "  Pause schedules if <= ${pauseVarValue}"; state.varPauseBool = false} } } if (wetSensor) {for(int i = 0; i < wetSensor.currentWater.size(); i++) { if (wetSensor.currentWater[i] == "wet") {state.wetStateDisplay = "${wetSensor.currentWater}"; state.wetPauseBool = true; break} else {state.wetStateDisplay = "${wetSensor.currentWater} "; state.wetPauseBool = false} } } if (voltageSensor) {for(int i = 0; i < voltageSensor.currentVoltage.size(); i++) { if (voltageSensor.currentVoltage[i] <= voltageThreshold) {state.voltageStateDisplay = "${voltageSensor.currentVoltage} "; state.voltPauseBool = true; break} else {state.voltageStateDisplay = "${voltageSensor.currentVoltage} "; state.voltPauseBool = false} } } if (contactSensor) {for(int i = 0; i < contactSensor.currentContact.size(); i++) { if (contactSensor.currentContact[i] == "open" && !invertContactBool) {state.contactStateDisplay = "${contactSensor.currentContact}, " + "  Pause schedules if Open"; state.contactPauseBool = true; break} else if (contactSensor.currentContact[i] == "closed" && invertContactBool) {state.contactStateDisplay = "${contactSensor.currentContact}, " + "  Pause schedules if Closed"; state.contactPauseBool = true; break} else if (!invertContactBool) {state.contactStateDisplay = "${contactSensor.currentContact}, " + "  Pause schedules if Open"; state.contactPauseBool = false} else {state.contactStateDisplay = "${contactSensor.currentContact}, " + "  Pause schedules if Closed"; state.contactPauseBool = false} } } if (leakBool && wetLeakSensor) {for(int i = 0; i < wetLeakSensor.currentWater.size(); i++) { if (wetLeakSensor.currentWater[i] == "wet") {state.wetLeakStateDisplay = "${wetLeakSensor.currentWater} "; break} else {state.wetLeakStateDisplay = "${wetLeakSensor.currentWater} "} } } //////// Triggers if (motionSensor1) {for(int i = 0; i < motionSensor1.currentMotion.size(); i++) { if (motionSensor1.currentMotion[i] == "active") {state.motionState1Display = "${motionSensor1.currentMotion} "; break} else {state.motionState1Display = "${motionSensor1.currentMotion} "} } } if (motionSensor2) {for(int i = 0; i < motionSensor2.currentMotion.size(); i++) { if (motionSensor2.currentMotion[i] == "active") {state.motionState2Display = "${motionSensor2.currentMotion} "; break} else {state.motionState2Display = "${motionSensor2.currentMotion} "} } } if (tempTrigger) {for(int i = 0; i < tempTrigger.currentTemperature.size(); i++) { // Scan arrary if (tempTrigOn < tempTrigOff) { if (tempTrigger.currentTemperature[i] <= tempTrigOn) {state.tempTrigBool = true} ////state.tempTrigStateDisplay = "${tempTrigger.currentTemperature}deg, " + "  Trigger On if Temp <= ${tempTrigOn}deg, Off when >= ${tempTrigOff}deg"; state.tempTrigBool = true; break} else if (tempTrigger.currentTemperature[i] >= tempTrigOff) {state.tempTrigBool = false} ////state.tempTrigStateDisplay = "${tempTrigger.currentTemperature}deg, " + "  Trigger On if Temp <= ${tempTrigOn}deg, Off when >= ${tempTrigOff}deg"; state.tempTrigBool = false; break} if (state.tempTrigBool == true) {state.tempTrigStateDisplay = "${tempTrigger.currentTemperature}deg, " + "  Trigger On if Temp <= ${tempTrigOn}deg, Off when >= ${tempTrigOff}deg"; break} if (state.tempTrigBool == false) {state.tempTrigStateDisplay = "${tempTrigger.currentTemperature}deg, " + "  Trigger On if Temp <= ${tempTrigOn}deg, Off when >= ${tempTrigOff}deg"; break} } if (tempTrigOn > tempTrigOff) { if (tempTrigger.currentTemperature[i] >= tempTrigOn) {state.tempTrigBool = true} ////state.tempTrigStateDisplay = "${tempTrigger.currentTemperature}deg, " + "  Trigger On if Temp >= ${tempTrigOn}deg, Off when <= ${tempTrigOff}deg"; state.tempTrigBool = true; break} else if (tempTrigger.currentTemperature[i] <= tempTrigOff) {state.tempTrigBool = false} ////state.tempTrigStateDisplay = "${tempTrigger.currentTemperature}deg, " + "  Trigger On if Temp >= ${tempTrigOn}deg, Off when <= ${tempTrigOff}deg"; state.tempTrigBool = false; break} if (state.tempTrigBool == true) {state.tempTrigStateDisplay = "${tempTrigger.currentTemperature}deg, " + "  Trigger On if Temp >= ${tempTrigOn}deg, Off when <= ${tempTrigOff}deg"; break} if (state.tempTrigBool == false) {state.tempTrigStateDisplay = "${tempTrigger.currentTemperature}deg, " + "  Trigger On if Temp >= ${tempTrigOn}deg, Off when <= ${tempTrigOff}deg"; break} } if (tempTrigOn == tempTrigOff && tempTrigger.currentTemperature[i] >= tempTrigOn) {state.tempTrigStateDisplay = "${tempTrigger.currentTemperature}deg, " + "  Trigger On if Temp >= ${tempTrigOn}deg, Off when < ${tempTrigOn}deg"; state.tempTrigBool = true; break} else if (tempTrigOn == tempTrigOff && tempTrigger.currentTemperature[i] < tempTrigOn) {state.tempTrigStateDisplay = "${tempTrigger.currentTemperature}deg, " + "  Trigger On if Temp >= ${tempTrigOn}deg, Off when < ${tempTrigOn}deg"; state.tempTrigBool = false; break} endif } } if (contactTrigger) {for(int i = 0; i < contactTrigger.currentContact.size(); i++) { // Scan arrary if (contactTrigger.currentContact[i] == "open") {state.contactTrigStateDisplay = "${contactTrigger.currentContact}, " + "  Trigger if Contact = Open"; state.contactTrigBool = true; break} else {state.contactTrigStateDisplay = "${contactTrigger.currentContact}, " + "  Trigger if Contact(s) = Open"; state.contactTrigBool = false} } } if (switchTrigger) {for(int i = 0; i < switchTrigger.currentSwitch.size(); i++) { // Scan arrary if (switchTrigger.currentSwitch[i] == "on") {state.switchTrigStateDisplay = "${switchTrigger.currentSwitch}, " + "  Trigger if Switch = On"; state.switchTrigBool = true; break} else {state.switchTrigStateDisplay = "${switchTrigger.currentSwitch}, " + "  Trigger if Switch(s) = On"; state.switchTrigBool = false} } } DeVices.each { dev -> //// Impliment Device Pauses by case if ((state.humidPauseBool && state.DeVices["$dev.id"].specialPause == 1) || (state.wetPauseBool && state.DeVices["$dev.id"].specialPause == 2) || (state.voltPauseBool && state.DeVices["$dev.id"].specialPause == 3) || (state.contactPauseBool && state.DeVices["$dev.id"].specialPause == 4) || (state.tempPauseBool && state.DeVices["$dev.id"].specialPause == 5) || (state.varPauseBool && state.DeVices["$dev.id"].specialPause == 6)) {state.DeVices["$dev.id"].sensorPause = true} //// ver 2.0.4 else {state.DeVices["$dev.id"].sensorPause = false} } } ////////////////////////////////////////////////////////////////// Handlers ////////////////////////////////////////////////////////////////// def onTimeHandler(evt) { //// Called by; Subscribes only state.DeVices[evt.device.id].start = now() state.DeVices[evt.device.id].counts += 1 state.DeVices[evt.device.id].onTime = new Date().format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}") // Last Update if (logEnableBool) {log.debug "${app.label} -- ONtime HANDLER, Device **${evt.device}**.....start= ${state.DeVices[evt.device.id].start}.....Last Ative = ${state.DeVices[evt.device.id].onTime}" } //// Manual On Notifier if (!state.trigNotifyBool) { if (state.DeVices[evt.device.id].actions == 3 && state.DeVices[evt.device.id].durTime !=0) { state.lastNotify = "${app.label} -- **${evt.device}** Turned ON Manually (Action3), Auto Off in ${state.DeVices[evt.device.id].durTime}min" runIn(60*state.DeVices[evt.device.id].durTime, switchOffHandler, [data:[zone:state.DeVices[evt.device.id].zone], overwrite:false]) } else state.lastNotify = "${app.label} -- **${evt.device}** Turned ON Manually" dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushManOn", zone:state.DeVices[evt.device.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) } state.trigNotifyBool = false // keep this open for manual turn ons } def offTimeHandler(evt) {//// Called by; Subscribes only state.DeVices[evt.device.id].total += now() - state.DeVices[evt.device.id].start state.DeVices[evt.device.id].onTime = new Date().format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}") // Last Update if (logEnableBool) {log.debug "${app.label} -- OFFtime HANDLER, Device **${evt.device}**.....total= ${state.DeVices[evt.device.id].total}.....Last Ative = ${state.DeVices[evt.device.id].onTime}"} //// Manual Off Notifier if (!state.trigNotifyBool) { state.lastNotify = "${app.label} -- **${evt.device}** Turned OFF Manually" state.DeVices[evt.device.id].blink = false // cancel a blink session dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushDevOff", zone:state.DeVices[evt.device.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) } state.trigNotifyBool = false } void switchOnHandler(data) {//// Called by; Scheduled Cron's sensorScan() // Update (Wet, Humid, Motion, ...) values if (pauseAllBool) {return} // All paused, get out, dont run //// ver1.4.7 revised DeVices.each { dev -> if (!state.DeVices[dev.id].pause && !state.DeVices["$dev.id"].sensorPause && ((state.DeVices["$dev.id"].modes == location.mode) || state.DeVices["$dev.id"].modes == "Any")) { //// Pasued and Mode per device in table //// ver1.4.7 zone = state.DeVices[dev.id].zone if (data.value == state.DeVices[dev.id].zone ) { if (state.DeVices[dev.id].actions == 1 || state.DeVices[dev.id].actions == 2) { //// Blink ON state.lastNotify = "${app.label} -- Device **${dev}** Scheduled Turn ON in Blink Mode for duration ${state.DeVices[dev.id].durTime}min" dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushSchedBlink", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) state.DeVices[dev.id].blink = true runIn(1, blinkOnHandler, [data:zone, overwrite:false]) runIn(60 * state.DeVices[dev.id].durTime, switchOffHandler, [data:dataMap, overwrite:false]) } else { //// ON state.trigNotifyBool = true // let onTimeHandler know notify already done if (dev.currentSwitch) {dev.on()} if (dev.currentValve) {dev.open()} state.lastNotify = "${app.label} -- Device **${dev}** Scheduled Turn ON for duration ${state.DeVices[dev.id].durTime}min" dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushSchedOn", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) runIn(60 * state.DeVices[dev.id].durTime, switchOffHandler, [data:dataMap, overwrite:false]) } } } } } void switchOffHandler(data) {//// Called by; End of Cron Sched Dur time, and various on handlers. //log.warn "Test ... switchOffHandler ... data = $data" DeVices.each { dev -> if (data.zone == state.DeVices[dev.id].zone) { state.trigNotifyBool = true // let offTimeHandler know notify already done if (dev.currentSwitch == "on") {dev.off() } if (dev.currentValve == "open") {dev.close() } state.lastNotify = "${app.label} -- SwitchOffHandler ... **${dev}** Turning OFF, Zone = **${data.zone}**" state.DeVices[dev.id].blink = false // cancel a blink session dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushDevOff", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) //unschedule(notifyRepeatHandler) //// Shut off all Repeat Notifications ************************ } } } void contactTrigHandler(evt) { if (pauseAllBool) {return} sensorScan() // Grab sensor states DeVices.each { dev -> if (!state.DeVices[dev.id].pause && !state.DeVices[dev.id].sensorPause && state.DeVices[dev.id].trigger == 1 && (state.DeVices[dev.id].modes == location.mode || state.DeVices[dev.id].modes == "Any")) { state.trigNotifyBool = true // let onTimeHandler know notify already done if (state.DeVices[dev.id].actions == 0) { if (state.contactTrigBool && (dev.currentSwitch == "off" || dev.currentValve == "closed")) { if (dev.currentSwitch == "off") {dev.on() } else if (dev.currentValve == "closed") {dev.open() } state.lastNotify = "${app.label} -- **${dev}** Turning ON triggered by Contact **${evt.device}** is *${evt.device.currentContact}*, Off when contact closes" dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushContactTrig", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) } else if (!state.contactTrigBool && (dev.currentSwitch == "on" || dev.currentValve == "open")) {runIn(1, switchOffHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false])} //// Off } else if (state.DeVices[dev.id].actions == 1 || state.DeVices[dev.id].actions == 2) { //// Blink if (state.contactTrigBool && state.DeVices[dev.id].blink == false) {state.DeVices[dev.id].blink = true; runIn(1, blinkOnHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false]) state.lastNotify = "${app.label} -- **${dev}** Turning ON in Blink Mode (Action1/2) triggered by Contact **${evt.device}** is *${evt.device.currentContact}*" dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushContactTrig", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) } else if (!state.contactTrigBool && state.DeVices[dev.id].blink == true) {runIn(1, switchOffHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false])} //// Off } else if (state.DeVices[dev.id].actions == 3) { if (state.contactTrigBool && state.DeVices[dev.id].durTime !=0 && (dev.currentSwitch == "off" || dev.currentValve == "closed")) { if (dev.currentSwitch == "off") {dev.on() } else if (dev.currentValve == "closed") {dev.open() } state.lastNotify = "${app.label} -- **${dev}** Turning ON for duration ${state.DeVices[dev.id].durTime}min (Action3) triggered by Contact **${evt.device}** is *${evt.device.currentContact}*" dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushContactTrig", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) runIn(60*state.DeVices[dev.id].durTime, switchOffHandler, [data:dataMap, overwrite:false]) } } endif } } } void switchTrigHandler(evt) { ////ver2.1.0 if (pauseAllBool) {return} sensorScan() // Grab sensor states DeVices.each { dev -> if (!state.DeVices[dev.id].pause && !state.DeVices[dev.id].sensorPause && (state.DeVices[dev.id].trigger == 2) && (state.DeVices[dev.id].modes == location.mode || state.DeVices["$dev.id"].modes == "Any")) { state.trigNotifyBool = true // let onTimeHandler know notify already done if (state.DeVices[dev.id].actions == 0) { if (state.switchTrigBool && (dev.currentSwitch == "off" || dev.currentValve == "closed")) { if (dev.currentSwitch == "off") {dev.on() } else if (dev.currentValve == "closed") {dev.open() } state.lastNotify = "${app.label} -- **${dev}** Turning ON triggered by Switch **${evt.device}** is *${evt.device.currentSwitch}*, Off when trigger Switch goes Off" dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushSwitchTrig", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) } else if (!state.switchTrigBool && (dev.currentSwitch == "on" || dev.currentValve == "open")) {runIn(1, switchOffHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false])} //// Off } else if ((state.DeVices[dev.id].actions == 1 || state.DeVices[dev.id].actions == 2)) { //// Blink if (state.switchTrigBool && state.DeVices[dev.id].blink == false) {state.DeVices[dev.id].blink = true; runIn(1, blinkOnHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false]) state.lastNotify = "${app.label} -- **${dev}** Turning ON in Blink Mode (Action1/2) triggered by Switch **${evt.device}** is *${evt.device.currentSwitch}*" dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushSwitchTrig", zone:state.DeVices["$dev.id"].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) } else if (!state.switchTrigBool && state.DeVices[dev.id].blink == true) {runIn(1, switchOffHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false])} //// Off } else if (state.DeVices[dev.id].actions == 3) { if (state.switchTrigBool && state.DeVices[dev.id].durTime !=0 && (dev.currentSwitch == "off" || dev.currentValve == "closed")) { if (dev.currentSwitch == "off") {dev.on() } else if (dev.currentValve == "closed") {dev.open() } state.lastNotify = "${app.label} -- **${dev}** Turning ON for duration ${state.DeVices[dev.id].durTime}min (Action3) triggered by Switch **${evt.device}** is *${evt.device.currentContact}*" dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushSwitchTrig", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) runIn(60*state.DeVices[dev.id].durTime, switchOffHandler, [data:dataMap, overwrite:false]) } } endif } } } void tempTrigHandler(evt) { if (pauseAllBool) {return} sensorScan() // Grab sensor states DeVices.each { dev -> if (!state.DeVices[dev.id].pause && !state.DeVices[dev.id].sensorPause && state.DeVices[dev.id].trigger == 3 && (state.DeVices[dev.id].modes == location.mode || state.DeVices[dev.id].modes == "Any")) { //// Pasued per device in table state.trigNotifyBool = true // let onTimeHandler know notify already done if (state.DeVices[dev.id].actions == 0) { if (state.tempTrigBool && (dev.currentSwitch == "off" || dev.currentValve == "closed")) { if (dev.currentSwitch == "off") {dev.on(); } else if (dev.currentValve == "closed") {dev.open(); } state.lastNotify = "${app.label} -- **${dev}** Turning ON triggered by Temp Sensor **${evt.device}** is *${evt.device.currentTemperature}deg*, turning Off when out of Temp range" dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushTempTrig", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) } else if (!state.tempTrigBool && (dev.currentSwitch == "on" || dev.currentValve == "open")) {runIn(1, switchOffHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false])} //// Off } else if ((state.DeVices[dev.id].actions == 1 || state.DeVices[dev.id].actions == 2)) { //// Blink if (state.tempTrigBool && state.DeVices[dev.id].blink == false) {state.DeVices[dev.id].blink = true; runIn(1, blinkOnHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false]) state.lastNotify = "${app.label} -- **${dev}** Turning ON in Blink Mode (Action1/2) triggered by Temp Sensor **${evt.device}** is *${evt.device.currentTemperature}deg*" dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushTempTrig", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) } else if (!state.tempTrigBool && state.DeVices[dev.id].blink == true) {runIn(1, switchOffHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false])} //// Off } else if (state.DeVices[dev.id].actions == 3) { if (state.tempTrigBool && state.DeVices[dev.id].durTime !=0 && (dev.currentSwitch == "off" || dev.currentValve == "closed")) { if (dev.currentSwitch == "off") {dev.on() } else if (dev.currentValve == "closed") {dev.open() } state.lastNotify = "${app.label} -- **${dev}** Turning ON for duration ${state.DeVices[dev.id].durTime}min (Action3) triggered by Temp Sensor **${evt.device}** is *${evt.device.currentTemperature}deg*" dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushTempTrig", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) runIn(60*state.DeVices[dev.id].durTime, switchOffHandler, [data:dataMap, overwrite:false]) } } endif } } } void modeTrigHandler(evt) { //// Called by; Subscribes only. ////ver2.1.3 if (pauseAllBool || !modeTrigger) {return} DeVices.each { dev -> if (!state.DeVices[dev.id].pause && !state.DeVices[dev.id].sensorPause && state.DeVices[dev.id].trigger == 4) { //// Pasued per device in table and motion enabled state.trigNotifyBool = true // let onTimeHandler know notify already done if (((modeTrigger != location.mode && !modeInvertBool) || (modeTrigger == location.mode && modeInvertBool)) && (state.DeVices[dev.id].blink == true || dev.currentSwitch == "on" || dev.curentValve == "open")) {runIn(1, switchOffHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false])} //// OFF else if (dev.currentSwitch == "off" || dev.curentValve == "closed") { //// ON if ((modeTrigger == location.mode && !modeInvertBool) || (modeTrigger != location.mode && modeInvertBool)) { if (state.DeVices[dev.id].actions == 0 || state.DeVices[dev.id].actions == 3) { //// Both Act0 and Act3 use Durtime if (dev.currentSwitch == "off") {dev.on() } else if (dev.currentValve == "closed") {dev.open() } if (state.DeVices[dev.id].durTime !=0) {state.lastNotify = "${app.label} -- **${dev}** Turning ON for duration ${state.DeVices["$dev.id"].durTime}min, triggered by Mode changed to **${location.mode}**" runIn(60 * state.DeVices[dev.id].durTime, switchOffHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false]) } else {state.lastNotify = "${app.label} -- **${dev}** Turning ON until Mode changes, triggered by Mode changed to **${location.mode}**,"} } else if ((state.DeVices[dev.id].actions == 1 || state.DeVices[dev.id].actions == 2) && state.DeVices[dev.id].blink == false) {state.DeVices[dev.id].blink = true //// Blink if (state.DeVices[dev.id].durTime !=0) { runIn(1, blinkOnHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false]) runIn(60 * state.DeVices[dev.id].durTime, switchOffHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false]) } else runIn(1, blinkOnHandler, [data:zone, overwrite:false]) //// On till mode change if (state.DeVices[dev.id].durTime !=0) {state.lastNotify = "${app.label} -- **${dev}** Turning ON in Blink Mode (Action1/2) for duration ${state.DeVices[dev.id].durTime}min, triggered by Mode changed to **${location.mode}**"} else {state.lastNotify = "${app.label} -- **${dev}** Turning ON in Blink Mode (Action1/2) until Mode changes, triggered by Mode changed to **${location.mode}**"} } endif dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushModeTrig", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) } } endif } } } void motionHandler1(evt) { //// Called by; Subscribes only. if (pauseAllBool) {return} // All paused, get out, dont run DeVices.each { dev -> if (!state.DeVices[dev.id].pause && !state.DeVices[dev.id].sensorPause && state.DeVices[dev.id].motionCase == 1 && ((state.DeVices[dev.id].modes == location.mode) || state.DeVices[dev.id].modes == "Any")) { //// Pasued per device in table and motion enabled state.trigNotifyBool = true // let onTimeHandler know notify already done if (!invertbool1) { if (!motionActive1 && evt.device.currentMotion == "active") { //// Turn On if Off, Use Duration Time state.lastNotify = "${app.label} -- Motion Detected by **${evt.device}** is ${evt.device.currentMotion}, Turning On **${dev}** for ${state.DeVices[dev.id].durTime}min .. (MotionGroup=${state.DeVices["$dev.id"].motionCase})" if (dev.currentSwitch == "off") {dev.on() } else if (dev.currentValve == "closed") {dev.open() } runIn(state.DeVices[dev.id].durTime, switchOffHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false]) } else if (motionActive1 && evt.device.currentMotion == "active") { //// Turn On if Off state.lastNotify = "${app.label} -- Motion Detected by **${evt.device}** is ${evt.device.currentMotion}, Turning On **${dev}** until Inactive .. (MotionGroup=${state.DeVices[dev.id].motionCase})" if (dev.currentSwitch == "off") {dev.on()} else if (dev.currentValve == "closed") {dev.open()} } else if (motionActive1 && evt.device.currentMotion == "inactive") {//// Turn back Off after Active state.lastNotify = "${app.label} -- Motion Detected by **${evt.device}** is ${evt.device.currentMotion}, Turning Off **${dev}** .. (MotionGroup=${state.DeVices[dev.id].motionCase})" if (dev.currentSwitch == "on") {dev.off()} //// Turn back Off after Active else if (dev.currentValve == "open") {dev.close()} } endif } if (invertbool1) { if (!motionActive1 && evt.device.currentMotion == "active") { //// Turn Off if On, Use Duration Time state.lastNotify = "${app.label} -- Motion Detected by **${evt.device}** is ${evt.device.currentMotion}, Turning Off **${dev}** for ${state.DeVices[dev.id].durTime}min .. (MotionGroup=${state.DeVices[dev.id].motionCase})" if (dev.currentSwitch == "on") {dev.off() } else if (dev.currentValve == "open") {dev.close() } runIn(state.DeVices[dev.id].durTime, motionBackOnHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false]) } else if (motionActive1 && evt.device.currentMotion == "active") { //// Turn Off if On state.lastNotify = "${app.label} -- Motion Detected by **${evt.device}** is ${evt.device.currentMotion}, Turning Off **${dev}** until Inactive .. (MotionGroup=${state.DeVices[dev.id].motionCase})" if (dev.currentSwitch == "on") {dev.off()} else if (dev.currentValve == "open") {dev.close()} } else if (motionActive1 && evt.device.currentMotion == "inactive") {//// Turn back On after Active state.lastNotify = "${app.label} -- Motion Detected by **${evt.device}** is ${evt.device.currentMotion}, Turning On **${dev}** .. (MotionGroup=${state.DeVices[dev.id].motionCase})" if (dev.currentSwitch == "off") {dev.on()} else if (dev.currentValve == "closed") {dev.open()} } endif } dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushMotion", zone:state.DeVices["$dev.id"].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) } } } void motionHandler2(evt) { //// Called by; Subscribes only. if (pauseAllBool) {return} // All paused, get out, dont run DeVices.each { dev -> if (!state.DeVices[dev.id].pause && !state.DeVices[dev.id].sensorPause && state.DeVices[dev.id].motionCase == 2 && ((state.DeVices[dev.id].modes == location.mode) || state.DeVices[dev.id].modes == "Any")) { //// Pasued per device in table and motion enabled state.trigNotifyBool = true // let onTimeHandler know notify already done if (!invertbool2) { if (!motionActive2 && evt.device.currentMotion == "active") { //// Turn On if Off, Use Duration Time state.lastNotify = "${app.label} -- Motion Detected by **${evt.device}** is ${evt.device.currentMotion}, Turning On **${dev}** for ${state.DeVices[dev.id].durTime}min .. (MotionGroup=${state.DeVices[dev.id].motionCase})" if (dev.currentSwitch && dev.currentSwitch == "off") {dev.on() } else if (dev.currentValve && dev.currentValve == "closed") {dev.open() } runIn(state.DeVices[dev.id].durTime, switchOffHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false]) } else if (motionActive2 && evt.device.currentMotion == "active") { //// Turn On if Off state.lastNotify = "${app.label} -- Motion Detected by **${evt.device}** is ${evt.device.currentMotion}, Turning On **${dev}** until Inactive .. (MotionGroup=${state.DeVices[dev.id].motionCase})" if (dev.currentSwitch && dev.currentSwitch == "off") {dev.on()} else if (dev.currentValve && dev.currentValve == "closed") {dev.open()} } else if (motionActive2 && evt.device.currentMotion == "inactive") {//// Turn back Off after Active state.lastNotify = "${app.label} -- Motion Detected by **${evt.device}** is ${evt.device.currentMotion}, Turning Off **${dev}** .. (MotionGroup=${state.DeVices[dev.id].motionCase})" if (dev.currentSwitch && dev.currentSwitch == "on") {dev.off()} //// Turn back Off after Active else if (dev.currentValve && dev.currentValve == "open") {dev.close()} } endif } if (invertbool2) { if (!motionActive2 && evt.device.currentMotion == "active") { //// Turn Off if On, Use Duration Time state.lastNotify = "${app.label} -- Motion Detected by **${evt.device}** is ${evt.device.currentMotion}, Turning Off **${dev}** for ${state.DeVices[dev.id].durTime}min .. (MotionGroup=${state.DeVices[dev.id].motionCase})" if (dev.currentSwitch && dev.currentSwitch == "on") {dev.off() } else if (dev.currentValve && dev.currentValve == "open") {dev.close() } runIn(state.DeVices[dev.id].durTime, motionBackOnHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false]) } else if (motionActive2 && evt.device.currentMotion == "active") { //// Turn Off if On state.lastNotify = "${app.label} -- Motion Detected by **${evt.device}** is ${evt.device.currentMotion}, Turning Off **${dev}** until Inactive .. (MotionGroup=${state.DeVices[dev.id].motionCase})" if (dev.currentSwitch && dev.currentSwitch == "on") {dev.off()} else if (dev.currentValve && dev.currentValve == "open") {dev.close()} } else if (motionActive2 && evt.device.currentMotion == "inactive") {//// Turn back On after Active state.lastNotify = "${app.label} -- Motion Detected by **${evt.device}** is ${evt.device.currentMotion}, Turning On **${dev}** .. (MotionGroup=${state.DeVices[dev.id].motionCase})" if (dev.currentSwitch && dev.currentSwitch == "off") {dev.on()} else if (dev.currentValve && dev.currentValve == "closed") {dev.open()} } endif } dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushMotion", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) } } } void motionBackOnHandler(data) {//// Called by; MotionHandler Invert mode. //// ver2.0.1 if (pauseAllBool) {return} // All paused, get out, dont run state.trigNotifyBool = true // let onTimeHandler know notify already done DeVices.each { dev -> if (!state.DeVices[dev.id].pause && !state.DeVices[dev.id].sensorPause && state.DeVices[dev.id].motion != 0) { if (data.zone == state.DeVices[dev.id].zone ) { if (dev.currentSwitch == "off") {dev.on() } else if (dev.currentValve == "closed") {dev.open() } state.lastNotify = "${app.label} -- Turning back On **${dev}** after Motion, in Invert Active mode." dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushMotion", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) } } } } def infoLogNotifyHandler(data) { //// NOTIFICATIONS AND LOG INFO's //log.warn "Test1 ... infoLogAndNotifyHandler ..pushOption = ${data.pushOption} ... ${data.dataLastNotify} ... data = $data" DeVices.each { dev -> if (data.zone == state.DeVices[dev.id].zone) { log.info "$data.dataLastNotify" if (pushDevice && (pushMode == location.mode || pushMode == "Any")) { if ((state.DeVices[dev.id].notifyOptions == 1 && settings.notifyOptions1?.contains(data.pushOption)) || (state.DeVices[dev.id].notifyOptions == 2 && settings.notifyOptions2?.contains(data.pushOption)) || (state.DeVices[dev.id].notifyOptions == 3 && settings.notifyOptions3?.contains(data.pushOption))) { if (((state.DeVices[dev.id].notifyOptions == 1 && settings.notifyOptions1?.contains("pushTrigEvery5min")) || (state.DeVices[dev.id].notifyOptions == 2 && settings.notifyOptions2?.contains("pushTrigEvery5min")) || (state.DeVices[dev.id].notifyOptions == 3 && settings.notifyOptions3?.contains("pushTrigEvery5min"))) && ((data.pushOption == "pushContactTrig" && state.contactTrigBool) || (data.pushOption == "pushSwitchTrig" && state.switchTrigBool) || (data.pushOption == "pushTempTrig" && state.tempTrigBool))) { state.DeVices[dev.id].notifyRepeat = 1; runIn(1, notifyRepeatHandler, [data:data, overwrite:false]) } /// Repeating Notification else state.DeVices[dev.id].notifyRepeat = 0; pushDevice.deviceNotification(data.dataLastNotify) /// Imediate Notification } } } } } def notifyRepeatHandler(data) { //// REPEATING NOTIFICATIONS AND LOG INFO's DeVices.each { dev -> if (data.zone == state.DeVices[dev.id].zone) { if (state.DeVices[dev.id].notifyRepeat == 1) { log.info "$data.dataLastNotify, Repeat Log every 5min until Trigger cleared" pushDevice.deviceNotification ("$data.dataLastNotify, Repeat every 5min until Trigger cleared, ") runIn(300, notifyRepeatHandler, [data:data, overwrite:false]) } else {log.warn "UNSCHEDULE All - notifyRepeatHandler ... **$dev**"; unschedule(notifyRepeatHandler) } } } } def blinkOnHandler(data) { //// BLINK ON if (pauseAllBool) {return} // All paused, get out, dont run int xRun DeVices.each { dev -> if (state.DeVices[dev.id].blink == false || state.DeVices[dev.id].sensorPause) {return} // do not blink valves, exit .each routine ////log.debug "BlinkOn2 ....**${dev}** ... dev-currentValve **${dev.currentValve}**...dev-currentSwitch **${dev.currentSwitch}**" if (state.DeVices[dev.id].actions == 1) {xRun = Math.round(60 * blinkOn1)} else {xRun = Math.round(60 * blinkOn2)} if (data.zone == state.DeVices[dev.id].zone ) { state.trigNotifyBool = true // let onTimeHandler know notify already done if (dev.currentSwitch == "off") {dev.on(); runIn(xRun, blinkOffHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false])} if (dev.currentValve == "closed") {dev.open(); runIn(xRun, blinkOffHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false])} if (logEnableBool) {log.info "${app.label} -- BlnikONHandler **${dev}**, Turning ON"} } } } def blinkOffHandler(data) { //// BLINK OFF int xRun DeVices.each { dev -> if (state.DeVices[dev.id].blink == false) {return} // do not blink valves, exit .each routine if (state.DeVices[dev.id].actions == 1) {xRun = Math.round(60 * blinkOff1)} else {xRun = Math.round(60 * blinkOff2)} if (data.zone == state.DeVices[dev.id].zone ) { state.trigNotifyBool = true // let offTimeHandler know notify already done if (dev.currentSwitch == "on") {dev.off(); runIn(xRun, blinkOnHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false])} if (dev.currentValve == "open") {dev.closed(); runIn(xRun, blinkOnHandler, [data:[zone:state.DeVices[dev.id].zone], overwrite:false])} if (logEnableBool) {log.info "${app.label} -- BlnikOFFHandler **${dev}**, Turning OFF"} } } } def allOffHandler(evt) { //// Called by; Sched All off time, Leak, Remote SW, in App Off button DeVices.each { dev -> state.trigNotifyBool = true // let offTimeHandler know notify already done state.lastNotify = "${app.label} -- $dev Turned Off by All-OFF Button Pushed or Triggered" dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushDevOff", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications if (dev.currentSwitch == "on" || dev.currentValve == "open") {runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false])} if (dev.currentSwitch) {dev.off()} if (dev.currentValve) {dev.close()} state.DeVices[dev.id].blink = false } unschedule(notifyRepeatHandler) // Shutdown Repeat notifications } def statsResetHandler(evt) { //// Called by; Sched Reset time state.DeVices.each {k, v -> def dev = DeVices.find{"$it.id" == k} state.DeVices[k].start = now() state.DeVices[k].total = 0 state.DeVices[k].counts = 0 } state.lastNotify = "${app.label} -- All Device On Times and Counts Stats Scheduled Reset" log.info "${state.lastNotify}" } def remoteOffHandler(evt) {//// Called by; Subscribes only. // Handles remote off/pause and Leak Sensor off if (evt.device.currentSwitch == "on" && remoteSwitchBool) { // remote switch control to turn off and pause everything state.pauseAllBool = true; app?.updateSetting("pauseAllBool",[value:"true",type:"bool"]) state.lastNotify = "${app.label} -- All Devices Triggered Off from Remote Switch **${evt.device}**" log.info "${state.lastNotify}" allOffHandler() /// Shut all Off } else if (leakBool && wetLeakSensor && evt.device.currentWater == "wet") { // Leak Detect turn off and pause everything state.pauseAllBool = true; app?.updateSetting("pauseAllBool",[value:"true",type:"bool"]) state.lastNotify = "${app.label} -- LEAK Detected by **${evt.device}**, turning all Devices OFF and Pause all schedules" dataMap = [dataLastNotify:state.lastNotify, pushOption:"pushLeak", zone:state.DeVices[dev.id].zone] //// Log.info & Notifications runIn(1, infoLogNotifyHandler, [data:dataMap, overwrite:false]) allOffHandler() /// Shut all Off } else if (evt.device.currentSwitch == "off" && remoteSwitchBool) {state.pauseAllBool = false; app?.updateSetting("pauseAllBool",[value:"false",type:"bool"])} // Unpause permanent pause } def sunHandler(evt) {//// Called by; Scheduled Cron and within functions. // update new sunrise/sunset times every day //// ver1.1.0 DeVices.each { dev -> if (!state.DeVices[dev.id].sunTime) {state.DeVices[dev.id].offset = 0} ////ver2.0.3 state.offsetRiseAndSet = getSunriseAndSunset(sunriseOffset: state.DeVices[dev.id].offset, sunsetOffset: state.DeVices["$dev.id"].offset) //// ver1.1.0 if (state.DeVices[dev.id].sunTime) { //// ver1.0.4 if (state.DeVices[dev.id].sunset) {state.DeVices[dev.id].startTime = state.offsetRiseAndSet.sunset.toString() } else {state.DeVices[dev.id].startTime = state.offsetRiseAndSet.sunrise.toString() } } if (state.DeVices[dev.id].sunrise) { ////ver2.0.3 Calculate @ Sunrise Off time and duration int startHours = Integer.valueOf(state.DeVices[dev.id].startTime.substring(11, state.DeVices[dev.id].startTime.length() - 15)) int startMin = Integer.valueOf(state.DeVices[dev.id].startTime.substring(14, state.DeVices[dev.id].startTime.length() - 12)) String strSunriseTime = state.offsetRiseAndSet.sunrise int sunriseHours = Integer.valueOf(strSunriseTime.substring(11, strSunriseTime.length() - 15)) int sunriseMin = Integer.valueOf(strSunriseTime.substring(14, strSunriseTime.length() - 12)) if (startHours > sunriseHours) {state.DeVices[dev.id].durTime = (24 - startHours + sunriseHours)*60 - startMin + sunriseMin + sunriseOffset - state.DeVices["$dev.id"].offset} else {state.DeVices[dev.id].durTime = (sunriseHours*60) + sunriseMin + sunriseOffset - state.DeVices[dev.id].offset} if (state.DeVices[dev.id].durTime < 0) {state.DeVices[dev.id].durTime = 0} // Prevent negative times } } updated() // Rebuild cron with new sunSet/Rise schedules } void appButtonHandler(btn) {//// Called by; In app Button pushes if (btn == "refresh") refreshHandler() else if (btn == "allOff") allOffHandler() else if (btn == "updateButton") updated() else if (btn.startsWith("a")) state.sunUnCheckedBox = btn.minus("a") else if (btn.startsWith("b")) state.sunCheckedBox = btn.minus("b") else if (btn.startsWith("c")) state.monUnCheckedBox = btn.minus("c") else if (btn.startsWith("d")) state.monCheckedBox = btn.minus("d") else if (btn.startsWith("e")) state.tueUnCheckedBox = btn.minus("e") else if (btn.startsWith("f")) state.tueCheckedBox = btn.minus("f") else if (btn.startsWith("g")) state.wedUnCheckedBox = btn.minus("g") else if (btn.startsWith("h")) state.wedCheckedBox = btn.minus("h") else if (btn.startsWith("i")) state.thuUnCheckedBox = btn.minus("i") else if (btn.startsWith("j")) state.thuCheckedBox = btn.minus("j") else if (btn.startsWith("k")) state.friUnCheckedBox = btn.minus("k") else if (btn.startsWith("l")) state.friCheckedBox = btn.minus("l") else if (btn.startsWith("m")) state.satUnCheckedBox = btn.minus("m") else if (btn.startsWith("n")) state.satCheckedBox = btn.minus("n") else if (btn.startsWith("o")) state.newStartTime = btn.minus("o") else if (btn.startsWith("q")) state.newDurTime = btn.minus("q") else if (btn.startsWith("r")) state.oddUnCheckedBox = btn.minus("r") else if (btn.startsWith("s")) state.oddCheckedBox = btn.minus("s") else if (btn.startsWith("t")) state.pauseUnCheckedBox = btn.minus("t") else if (btn.startsWith("v")) state.pauseCheckedBox = btn.minus("v") else if (btn.startsWith("w")) state.sunTimeUnCheckedBox = btn.minus("w") //// ver1.1.0 else if (btn.startsWith("x")) state.sunTimeCheckedBox = btn.minus("x") //// ver1.1.0 else if (btn.startsWith("p")) state.runsDayBox1 = btn.minus("p") //// ver1.2.1 else if (btn.startsWith("u")) state.runsDayBox2 = btn.minus("u") //// ver1.2.1 else if (btn.startsWith("z")) state.resetTotal = btn.minus("z") else if (btn.startsWith("1")) state.newMode = btn.minus("1") //// ver1.5.2 else if (btn.startsWith("2")) state.sunriseUnCheckedBox = btn.minus("2") //// ver2.0.3 else if (btn.startsWith("3")) state.sunriseCheckedBox = btn.minus("3") //// ver2.0.3 else if (btn.startsWith("-")) state.pauseCase0 = btn.minus("-") //// ver2.0.3 else if (btn.startsWith("4")) state.pauseCase1 = btn.minus("4") //// ver2.0.3 else if (btn.startsWith("5")) state.pauseCase2 = btn.minus("5") //// ver2.0.3 else if (btn.startsWith("6")) state.pauseCase3 = btn.minus("6") else if (btn.startsWith("7")) state.pauseCase4 = btn.minus("7") else if (btn.startsWith("8")) state.pauseCase5 = btn.minus("8") else if (btn.startsWith("9")) state.pauseCase6 = btn.minus("9") else if (btn.startsWith("C")) state.motionCase0 = btn.minus("C") //// ver2.1.0 else if (btn.startsWith("A")) state.motionCase1 = btn.minus("A") //// ver2.1.0 else if (btn.startsWith("B")) state.motionCase2 = btn.minus("B") //// ver2.1.0 else if (btn.startsWith("D")) state.sunsetUnCheckedBox = btn.minus("D") //// ver1.1.0 else if (btn.startsWith("E")) state.sunsetCheckedBox = btn.minus("E") //// ver1.1.0 else if (btn.startsWith("F")) state.newOffsetTime = btn.minus("F") //// ver1.1.0 else if (btn.startsWith("Z")) state.deviceState = btn.minus("Z") ////ver2.0.3 else if (btn.startsWith("J")) state.actions0 = btn.minus("J") else if (btn.startsWith("G")) state.actions1 = btn.minus("G") //// ver2.1.0 else if (btn.startsWith("H")) state.actions2 = btn.minus("H") else if (btn.startsWith("I")) state.actions3 = btn.minus("I") else if (btn.startsWith("O")) state.trigger0 = btn.minus("O") else if (btn.startsWith("K")) state.trigger1 = btn.minus("K") else if (btn.startsWith("L")) state.trigger2 = btn.minus("L") else if (btn.startsWith("M")) state.trigger3 = btn.minus("M") else if (btn.startsWith("N")) state.trigger4 = btn.minus("N") //else if (btn.startsWith("P")) state.actions10 = btn.minus("P") else if (btn.startsWith("Un")) state.notifyOptions0 = btn.minus("Un") else if (btn.startsWith("Qn")) state.notifyOptions1 = btn.minus("Qn") else if (btn.startsWith("Rn")) state.notifyOptions2= btn.minus("Rn") else if (btn.startsWith("Sn")) state.notifyOptions3= btn.minus("Sn") else if (btn.startsWith("Tn")) state.notifyOptions4= btn.minus("Tn") endif } def refreshHandler() {//// Called by; In App Button push and within functions // Update Times if Active/On state.DeVices.each {k, v -> def dev = DeVices.find{"$it.id" == k} if (dev.currentSwitch == "on" || dev.currentValve == "open") { ////////////////// state.DeVices[k].total += now() - state.DeVices[k].start state.DeVices[k].start = now() } } sensorScan() // Update sensor readings if any } /////////////////////////////////////////////////////////////// Functions ////////////////////////////////////////////////////////////////// def buildCron() {//// Called by; updated() function, via install or in App refresh/store buttons or within functions state.DeVices.each {k, v -> def dev = DeVices.find{"$it.id" == k} if(state.DeVices[k].startTime && state.DeVices[k].startTime != "000000000000000000000000000000" ) { if (runTwiceOffset == null) {runTwiceOffset = 12} String formattedTime = state.DeVices[k].startTime.substring(11, state.DeVices[k].startTime.length() - 12) String hours = formattedTime.substring(0, formattedTime.length() - 3) // Chop off the last 3 in string String minutes = formattedTime.substring(3) // Chop off the first 3 in string int runTwoHour = Integer.valueOf(hours) + Integer.valueOf(runTwiceOffset) // Add the 2nd run offset to the first run to get next run time if (runTwoHour >= 24) {runTwoHour = 23} // force 2nd run to be in same day, 23:00 max if (state.DeVices[k].odd && state.DeVices[k].runsDay == 1) { state.DeVices[k].cron = "0 ${minutes} ${hours} 1/2 * ? * " } //// ver1.2.1 else if (state.DeVices[k].odd && state.DeVices[k].runsDay == 2) {state.DeVices[k].cron = "0 ${minutes} ${hours},${runTwoHour} 1/2 * ? * "} else { String days = "" if (state.DeVices[k].sun) {days = "SUN,"} if (state.DeVices[k].mon) {days += "MON,"} if (state.DeVices[k].tue) {days += "TUE,"} if (state.DeVices[k].wed) {days += "WED,"} if (state.DeVices[k].thu) {days += "THU,"} if (state.DeVices[k].fri) {days += "FRI,"} if (state.DeVices[k].sat) {days += "SAT,"} if (days != "") { days = days.substring(0, days.length() - 1) // Chop off last "," state.DeVices[k].days = days if (state.DeVices[k].runsDay == 1) {state.DeVices[k].cron = "0 ${minutes} ${hours} ? * ${days} *"} //// ver1.2.1 else if (state.DeVices[k].runsDay == 2) {state.DeVices[k].cron = "0 ${minutes} ${hours},${runTwoHour} ? * ${days} *"} endif } } endif } else state.DeVices[k].cron = "" // Blank out Cron } //// Build All Off time Cron ////ver1.4.0 if (allOffBool) { String formattedTimeOff = allOffTime.substring(11, allOffTime.length() - 12) String hoursOff = formattedTimeOff.substring(0, formattedTimeOff.length() - 3) // Chop off the last 3 in string String minutesOff = formattedTimeOff.substring(3) // Chop off the first 3 in string state.allOffCron = "0 ${minutesOff} ${hoursOff} * * ? * " } //// Build All Stats Reset Cron if (statsResetBool) { String formattedTimeReset = statsResetTime.substring(11, statsResetTime.length() - 12) String hoursReset = formattedTimeReset.substring(0, formattedTimeReset.length() - 3) // Chop off the last 3 in string String minutesReset = formattedTimeReset.substring(3) // Chop off the first 3 in string if (statsResetDay == "Everyday") {state.statsResetCron = "0 ${minutesReset} ${hoursReset} * * ? * * *" } else state.statsResetCron = "0 ${minutesReset} ${hoursReset} * * ? * ${statsResetDay} *" } if (logEnableBool) {log.debug "${app.label} -- All Cron Schedules Successfully Built"} } ////////////////////////////////////////////////////////////// Status Bar //////////////////////////////////////////////////////////////////// String statusBar() { if (pauseAllBool) {paragraph getFormat("noticable","All Upcoming Schedules, Triggers, and Motion Paused!!!")} paragraph getFormat("smallLineSpace","Current Hub Mode:   ${location.mode}") if (humidSensor) {paragraph getFormat("smallLineSpace","Pause Moisture/Humidity Sensor(s):   ${humidSensor} = ${state.humidStateDisplay}" + ", Case" + "${state.pauseCaseIcon1}")} if (wetSensor) {paragraph getFormat("smallLineSpace","Pause Wet/Dry Sensor(s):   ${wetSensor} = ${state.wetStateDisplay},   Pause schedules if 'wet'" + ", Case" + "${state.pauseCaseIcon2}" )} if (leakBool && wetLeakSensor) {paragraph getFormat("smallLineSpace","Pause Leak Detect Sensor(s):   ${wetLeakSensor} = ${state.wetLeakStateDisplay},   Shutdown All if 'wet'" )} if (voltageSensor) {paragraph getFormat("smallLineSpace","Pause Voltage Sensor(s):   ${voltageSensor} = ${state.voltageStateDisplay}   Pause schedules if <${voltageThreshold}V" + ", Case" + "${state.pauseCaseIcon3}")} if (contactSensor) {paragraph getFormat("smallLineSpace","Pause Contact Sensor(s):   ${contactSensor} = ${state.contactStateDisplay}" + ", Case" + "${state.pauseCaseIcon4}")} if (tempSensor) {paragraph getFormat("smallLineSpace","Pause Temperature Sensor(s):   ${tempSensor} = ${state.tempStateDisplay}" + ", Case" + "${state.pauseCaseIcon5}")} if (varConnect) {paragraph getFormat("smallLineSpace","Pause Variable(s) Value:   ${varConnect} = ${state.varStateDisplay}" + ", Case" + "${state.pauseCaseIcon6}")} if (motionSensor1) { paragraph getFormat("smallLineSpace","Trigger Motion Sensor(s)" + "  ${motionSensor1} = ${state.motionState1Display}" + ", Group" + "${state.motionIcon1}")} if (motionSensor2) { paragraph getFormat("smallLineSpace","Trigger Motion Sensor(s)" + "  ${motionSensor2} = ${state.motionState2Display}" + ", Group" + "${state.motionIcon2}")} if (contactTrigger) {paragraph getFormat("smallLineSpace","Trigger Contact Sensor(s):   ${contactTrigger} = ${state.contactTrigStateDisplay}" + ", Case" + "${state.triggerIcon1}")} if (switchTrigger) {paragraph getFormat("smallLineSpace","Trigger Switch Sensor(s):   ${switchTrigger} = ${state.switchTrigStateDisplay}" + ", Case" + "${state.triggerIcon2}")} if (tempTrigger) {paragraph getFormat("smallLineSpace","Trigger Temperature Sensor(s):   ${tempTrigger} = ${state.tempTrigStateDisplay}" + ", Case" + "${state.triggerIcon3}")} if (pushDevice) {paragraph getFormat("smallLineSpace","Last Info Log:   ${state.lastNotify}")} } /////////////////////////////////////////////////////////// Icon Graphics ////////////////////////////////////////////////////////////////// def icons() { ////ver2.1.1 ////Boxes - Circles state.unCheckedBox = "" state.greenCheckBox = "" state.steelBlueCheckBox = "" state.darkOrangeCheckBox = "" state.purpleCheckBox = "" state.steelBlueCircle1 = "" state.darkOrangeCircle2 = "" ////Other state.sunIcon = "" state.moonIcon = "" state.playIcon = "" state.pauseIcon = "" state.playIconGrey = "" state.pauseIconGrey = "" state.resetIcon = "" ////PauseCase state.pauseCaseIcon1 = "" state.pauseCaseIcon2 = "" state.pauseCaseIcon3 = "" state.pauseCaseIcon4 = "" state.pauseCaseIcon5 = "" state.pauseCaseIcon6 = "" ////Actions state.actionsIcon1 = "" state.actionsIcon2 = "" state.actionsIcon3 = "" ////Triggers state.triggerIcon1 = "" state.triggerIcon2 = "" state.triggerIcon3 = "" state.triggerIcon4 = "" ////Motion state.motionIcon1 = "" state.motionIcon2 = "" ////Notify state.notifyOptionsIcon1 = "" state.notifyOptionsIcon2 = "" state.notifyOptionsIcon3 = "" state.notifyOptionsIcon4 = "" } ////////////////////////////////////////////////////////////// Other Stuff ////////////////////////////////////////////////////////////////// def updated() { // runs every 'Done' on already installed app unsubscribe() //// unschedule(switchOnHandler) // cancels all(or one) scheduled jobs including runIn icons() buildCron() // build schedules initialize() // set schedules and subscribes } def installed() { updated() } // only runs once for new app 'Done' or first time open def logsOff() {log.info "${app.label} -- All App 'debug' logging auto disabling itself"; app?.updateSetting("logEnableBool",[value:"false",type:"bool"]) } def getFormat(type, myText="") { if(type == "title") return "

${myText}

" // Steel-Blue if(type == "blueRegular") return "
${myText}
" // Steel-Blue if(type == "noticable") return "
${myText}
" // Burnt-Orange if(type == "lessImportant") return "
${myText}
" // Green if(type == "smallLineSpace") return "
${myText}
" // smallLineSpace if (formatBool) {if(type == "header") {return "
${myText}
"} // Burgandy-Red //// ver1.2.0 if(type == "important") return "
${myText}
" // Lime-Green } else {if(type == "header") return "
${myText}
" // Black if(type == "important") return "
${myText}
" // Black } } def displayTitle() {titleVersion(); section (getFormat("title", "App: ${state.name} - ${"ver " + state.version}")) {} }