/*
=============================================================================
Hubitat Elevation Application
Sprinkler Schedule (child application) Sprinkler Valve Timetable
Inspiration: Lighting Schedules https://github.com/matt-hammond-001/hubitat-code
Inspiration: github example from Hubitat of lightsUsage.groovy
This fork: Sprinkler Schedules https://github.com/csteele-PD/Hubitat-public/tree/master/SprinklerSchedule
-----------------------------------------------------------------------------
This code is licensed as follows:
Portions:
Copyright (c) 2022 Hubitat, Inc. All Rights Reserved Bruce Ravenel
BSD 3-Clause License
Copyright (c) 2023, C Steele
Copyright (c) 2020, Matt Hammond
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-----------------------------------------------------------------------------
*
*
*
* csteele: v1.0.11 corrected updateMyLabel().
* csteele: v1.0.10 initialize state.rainDeviceOutdoor in setOutdoorRain.
* csteele: v1.0.9 Skip when no valves in a schedule.
* csteele: v1.0.8 Adjustments to recvOutdoorRainHandler().
* csteele: v1.0.7 After carefully fixing schEnable display, it wasn't used in scheduleNext logic.
* csteele: v1.0.6 Allow multiple Rain Sensors to be integrated.
* csteele: v1.0.5 corrected schEnable, so that enaDis is correct initially.
* null safe currentValve?.label/name.
* refactor rainHold to be by timetable,
* don't offer rain hold if no rain sensor device is selected.
* remove rainHold reset at midnight.
* csteele: v1.0.4 adjusted valve open/close messages to use device label or name.
* minimized Temp and Rain device attributes.
* csteele: v1.0.3 Initial Release (end Beta).
* csteele: v1.0.2 Add Over Temp and Rain Detection to be used as a Conditional
* csteele: v1.0.1 Added month2month and dayGroupMaster from Parent
* csteele: v1.0.0 Inspired by Matt Hammond's Lighting Schedule (child)
* Converted to capability.valve from switch
*
*/
public static String version() { return "v1.0.11" }
definition(
name: "Sprinkler Valve Timetable",
namespace: "csteele",
parent: "csteele:Sprinkler Schedule Manager",
author: "C Steele",
description: "Controls valves to a timing schedule",
importUrl: "https://raw.githubusercontent.com/csteele-PD/Hubitat-public/refs/heads/master/SprinklerSchedule/SprinklerSchedule_child.groovy",
documentationLink: "https://www.hubitatcommunity.com/QuikRef/sprinklerScheduleManagerInfo/index.html",
iconUrl: "",
iconX2Url: "",
)
preferences {
page(name: "main")
}
def main(){
init(1) // during first time install of child, along with installCheck(), pre-populate any un-initialized elements
dynamicPage(name: "main", title: "", uninstall: true, install: true){
updateMyLabel(1)
displayHeader()
state.appInstalled = app.getInstallationState() // validate that the Done button has been clicked the first time
if (state.appInstalled != 'COMPLETE') return installCheck()
section(menuHeader("General")) {
label title: "Name for this application", required: false, submitOnChange: true
if (app.label.contains('Valve Select"
input "valves",
"capability.valve",
title: "Control which valves?",
multiple: true,
required: false,
submitOnChange: true
}
section(menuHeader("Timetable Status & Logging")) {
if (valves) {
currentMonth = new Date().format("M") // Get the current month as a number (1-12)
currentMonthPercentage = state.month2month ? state.month2month[currentMonth].toDouble() : 1 // Lookup the percent in month2month or 1
// provide some feedback on which valves are On
String str = "
"
if (state.month2month) {
str += "Adjust valve timing by Month is active. Current month is: $currentMonthPercentage% " +
"Rain hold is $state.rainHold "
}
if (state.overTempToday) { str += "Sometime today, the outside temperature exceeded the limit you set of $state.maxOutdoorTemp. " }
str += valves?.collect { dev -> "${dev.label ?: dev.name} is ${dev.currentValue('valve', true) == 'open' ? 'On' : 'Off'}"}?.join(', ') ?: ""
str += "
"
paragraph str
}
input "infoEnable", "bool",
title: "Enable activity logging",
required: false,
defaultValue: true, width: 2
input "debugEnable", "bool",
title: "Enable debug logging",
required: false,
defaultValue: false,
submitOnChange: true, width: 2
if (debugEnable) {
input "debugTimeout", "enum", required: false, defaultValue: "0", title: "Automatic debug Log Disable Timeout?", width: 3, \
options: [ "0":"None", "1800":"30 Minutes", "3600":"60 Minutes", "86400":"1 Day" ]
}
}
if (valves) {
section(menuHeader("Schedule")) {
paragraph "Select Days into Groups"
paragraph displayDayGroups() // display day-of-week groups - Section I
paragraph "Select Period Settings by Group"
paragraph displayTable() // display groups for scheduling - Section II
displayDuration()
displayStartTime()
paragraph "Select Valves into Day Groups"
paragraph displayGrpSched() // display mapping of Valve to DayGroup - Section III
selectDayGroup()
if (state.rainDeviceOutdoor != [:]) {
def rainVars = ["0": "no rainHold"]
state.rainDeviceOutdoor.each { key, info ->
rainVars[key] = info.name
}
input "rainEnableDevice", "enum",
title: "Choose the Rain Sensor for this Timetable
leave unselected for no Rain Hold
",
submitOnChange: true,
defaultValue: "0",
options: rainVars
}
paragraph "\n"
}
}
}
}
/*
-----------------------------------------------------------------------------
Main Page handlers
-----------------------------------------------------------------------------
*/
String displayDayGroups() { // display day-of-week groups - Section I
incM = state.dayGroupMaster?.size() ?: 0
if(state.dayGroupBtn) { // toggle the daily checkmarks
dgK = state.dayGroupBtn[0].toInteger() - incM // dayGroupBtn Key
dgI = state.dayGroupBtn.substring(1); // dayGroupBtn value (mon-sun)
state.dayGroup["$dgK"]["$dgI"] = !state.dayGroup["$dgK"]["$dgI"] // Toggle state
state.remove("dayGroupBtn") // only once
logDebug "displayDayGroups Item: $dgK.$dgI"
}
if(state.overTempBtn) { // toggle the overTemp checkmarks
dgK = state.overTempBtn[0].toInteger() - incM // overTempBtn Key
if (state.dayGroup.containsKey(dgK.toString())) { state.dayGroup[dgK.toString()].ot = !state.dayGroup[dgK.toString()].ot } // Toggle state
state.remove("overTempBtn") // only once
}
masterGroupMerge() // merge or riffle merge if there's a new mMap from Parent.
String str = ""
str += "