/* 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","
"+
"Basics"+
"
Devices: Use for any Switch or Valve capability; switches, outlets, relays, lights, security lights, sprinklers, etc.. Add as many as you want on table.
"+
"
Refresh:T able will not auto refresh values or states, you must hit in App Refresh button.
"+
"
Update/Store: If you make/change a schedule change it wont take unless you hit 'Done' exiting the App or hitting the 'Update/Store' button.
"+
"
Advanced Features vs Basic: If you only need Basic On/Off timers, to simplify things uncheck 'Advanced features' icon in Options section. For more advnced funtionality like Triggers, Motion, Pauses, modes, etc.. keep Advanced On. Note: If you have advance features set up and toggle to Basic Mode they will still run in background, you need to clear/undo them to run truly in basic mode.
"+
"
State: Shows current device state after hitting in app Refresh. Clicking to toggle device state will not effect scheduleing or use the set Run time.
"+
"
Use Sunset/Sunrise: Option for start time, if checcked combine with selectable sunrise or sunset icon and with start +/- offset time in minutes. New Set/Rise times will auto update daily throughout the year if your hub is connected to the internet.
"+
"
Select day(s): Or odd days check boxes, enter Start time and Run Duration. Start times are in 24 hour format. Run Time in Minutes only.
"+
"
Odd Days: Will run days 1st, 3rd, 5th,...29th, if 31st, then 1st again of month.
"+
"
Last Active: Time the device was last turned On or Off.
"+
"
Total On Time and Counts: Will track all switch activity, from app and outside app.
"+
"
Reset: From table per row Will reset the total On Time and Counts. It does not reset schedule or turn off the device
"+
"
Off @ Sunrise: Instead of setting a time duration, use Sunrise time. Set the Sunrise offset in standard options section. Easier option for all night lights. Run time will appear in Minutes.
"+
"
Remote Switch: Optional, Select a virtual or real switch to remotely turn off all switches and pause/resume schedules (optional). Use case EX: Rain delay for spinklers, dashboard access.
"+
"
Leak Detect: This will shut off and stop every device schedule and actions permanently until a user unchecks 'Pause All Devices' in advanced options. Use case: if you have a water leak in house, can shut off an installed water valve listed in table. Not recommended to use if using 'wet' (rain) sensor for sprinklers.Only triggers if sensor goes from Dry to Wet. If still 'wet' this will not prevent the valve from being manually turned on outside of app.
"+
"
Runs Per Day: Optional, Typically for sprinklers to run in morning and evening. Select 2 in table. Then select Hours after 1st run start in standard options section. Note: If start time plus 2nd run offset exceeds hour 24 it will run at hour 23 2nd time. Start will not roll over into next day.
"+
"
You must hit DONE at page bottom to save App after first making.
"+
" "+
"Advanced Features"+
"
Trigger and Pause cases: Will only act when the selected Sensor changes state. It subscribes to the Sensor and needs to see a change in order to act on the new state.
"+
"
Modes: If your not using modes keep at 'Any'. If you want to only run during a specific Mode click and select that mode. If changed and need to get back to 'Any', go to Hub settings / Modes, and add new mode 'Any'"+
"
Motion: Selecting a motion group # will toggle a device if Motion device is triggerd. Select Motion device in Options section below table, Run or while active time in Advanced options. Will not change the scheduled on times if any.If On to Off enabled the, the switch if On will turn Off for the time selected then back On. Makes it look like someone is Home! For both Off-On-Off or On-Off-On cases the Motion Sensor needs to clear back to inactive to trigger again. Can use Motion triggers with or without a schedule start time entered."+
"
Pause Cases: Select 'None' or case # in table, set up in Pause Options section below table to pause upcoming schedule for that device only. Pause cases will work with schedules, triggers, motion, or anything.
"+
"
Pause Case #6 Variables: Select Hub varibale(s), These variables must be a set up as a Number or String Datatype. Will only pause when variable value 'changes' and then pause criteria is met
"+
"
Trigger Cases: Select 'None' or case # in table, set up in Trigger Options section below table. Each case uses another device and its state change to trigger the target device in table. Some like temperature or modes have additional settings. Triggers can be combined with Actions
"+
"
Actions Cases: Select 'None' or case # in table, set up in Action Options section below table. Cases 1 - 2 (blink) will work with startime defined schedules or triggers. Actions can be combined with Triggers but note Triggers + Actions could compete with each other giving unexpected On/Off results. So choose wisely.
"+
"
")
}
}
}
////////////////////////////////////////////////////////////////// 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 += "