mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-09 13:21:53 +00:00
Merge in bug fixes and renames to match with new repository layout. Deleted spruce-controller.groovy from 'Spruce Irrigation' Deleted spruce-sensor-v1.groovy from 'Spruce Irrigation' Deleted spruce-scheduler.groovy from 'Spruce Irrigation' Modifying 'Spruce Irrigation' Merge in bug fixes and renames to match with new repository layout.
1618 lines
61 KiB
Groovy
1618 lines
61 KiB
Groovy
/**
|
|
* Spruce Scheduler Pre-release V2.5 12/22/2016
|
|
*
|
|
*
|
|
* Copyright 2015 Plaid Systems
|
|
*
|
|
* 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.
|
|
*
|
|
|
|
-------v2.51---------------------
|
|
schedule function changed so runIn does not overwrite and cancel schedule
|
|
-ln 769 schedule cycleOn-> checkOn
|
|
-ln 841 checkOn function
|
|
-ln 863 state.run = false
|
|
|
|
-------Fixes
|
|
-changed weather from def to Map
|
|
-ln 968 if(runnowmap) -> pumpmap
|
|
|
|
-------Fixes V2.2-------------
|
|
-History log messages condensed
|
|
-Seasonal adjustment redefined -> weekly & daily
|
|
-Learn mode redefined
|
|
-No Learn redefined to operate any available days
|
|
-ZoneSettings page redefined -> required to setup zones
|
|
-Weather rain updated to fix error with some weather stations
|
|
-Contact time delay added
|
|
-new plants moisture and season redefined
|
|
*
|
|
*
|
|
-------Fixes V2.1-------------
|
|
-Many fixes, code cleanup by Jason C
|
|
-open fields leading to unexpected errors
|
|
-setting and summary improvements
|
|
-multi controller support
|
|
-Day to run mapping
|
|
-Contact delays optimized
|
|
-Warning notification added
|
|
-manual start subscription added
|
|
*
|
|
*/
|
|
|
|
definition(
|
|
name: "Spruce Scheduler",
|
|
namespace: "plaidsystems",
|
|
author: "NCauffman",
|
|
description: "Spruce automatic water scheduling app v2.5",
|
|
category: "Green Living",
|
|
iconUrl: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png",
|
|
iconX2Url: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png",
|
|
iconX3Url: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png")
|
|
|
|
preferences {
|
|
page(name: "startPage")
|
|
page(name: "autoPage")
|
|
page(name: "zipcodePage")
|
|
page(name: "weatherPage")
|
|
page(name: "globalPage")
|
|
page(name: "contactPage")
|
|
page(name: "delayPage")
|
|
page(name: "zonePage")
|
|
|
|
page(name: "zoneSettingsPage")
|
|
page(name: "zoneSetPage")
|
|
page(name: "plantSetPage")
|
|
page(name: "sprinklerSetPage")
|
|
page(name: "optionSetPage")
|
|
|
|
}
|
|
|
|
def startPage(){
|
|
dynamicPage(name: "startPage", title: "Spruce Smart Irrigation setup V2.51", install: true, uninstall: true)
|
|
{
|
|
section(""){
|
|
href(name: "globalPage", title: "Schedule settings", required: false, page: "globalPage",
|
|
image: "http://www.plaidsystems.com/smartthings/st_settings.png",
|
|
description: "Watering On: ${enableString()}\nWatering Time: ${startTimeString()}\nDays:${daysString()}\nNotifications:\n${notifyString()}")
|
|
|
|
}
|
|
|
|
section(""){
|
|
href(name: "weatherPage", title: "Weather Settings", required: false, page: "weatherPage",
|
|
image: "http://www.plaidsystems.com/smartthings/st_rain_225_r.png",
|
|
description: "Weather from: ${zipString()}\nSeasonal Adjust: ${seasonalAdjString()}")
|
|
}
|
|
|
|
section(""){
|
|
href(name: "zonePage", title: "Zone summary and setup", required: false, page: "zonePage",
|
|
image: "http://www.plaidsystems.com/smartthings/st_zone16_225.png",
|
|
description: "${getZoneSummary()}")
|
|
}
|
|
|
|
section(""){
|
|
href(name: "delayPage", title: "Valve and Contact delays", required: false, page: "delayPage",
|
|
image: "http://www.plaidsystems.com/smartthings/st_timer.png",
|
|
description: "Valve Delay: ${pumpDelayString()} s\nContact Sensor: ${contactSensorString()}\nSchedule Sync: ${syncString()}")
|
|
}
|
|
section(""){
|
|
href title: "Spruce Irrigation Knowledge Base", //page: "customPage",
|
|
description: "Explore our knowledge base for more information on Spruce and Spruce sensors. Contact from is also available here.",
|
|
required: false, style:"embedded",
|
|
image: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png",
|
|
url: "http://support.spruceirrigation.com"
|
|
}
|
|
}
|
|
}
|
|
|
|
def globalPage() {
|
|
dynamicPage(name: "globalPage", title: "") {
|
|
section("Spruce schedule Settings") {
|
|
label title: "Schedule Name:", description: "Name this schedule", required: false
|
|
input "switches", "capability.switch", title: "Spruce Irrigation Controller:", description: "Select a Spruce controller", required: true, multiple: false
|
|
}
|
|
|
|
|
|
section("Program Scheduling"){
|
|
input "enable", "bool", title: "Enable watering:", defaultValue: 'true', metadata: [values: ['true', 'false']]
|
|
input "startTime", "time", title: "Watering start time", required: true
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_calander.png",
|
|
title: "Selecting watering days",
|
|
"Selecting watering days is optional. Spruce will optimize your watering schedule automatically. If your area has water restrictions or you prefer set days, select the days to meet your requirements. "
|
|
input (name: "days", type: "enum", title: "Water only on these days...", required: false, multiple: true,
|
|
metadata: [values: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Even', 'Odd']])
|
|
}
|
|
|
|
section("Push Notifications") {
|
|
input (name: "notify", type: "enum", title: "Select what push notifications to receive.", required: false, multiple: true,
|
|
metadata: [values: ['Warnings', 'Daily', 'Weekly', 'Weather', 'Moisture']])
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
def weatherPage() {
|
|
dynamicPage(name: "weatherPage", title: "Weather settings") {
|
|
section("Location to get weather forecast and conditions:") {
|
|
href(name: "hrefWithImage", title: "${zipString()}", page: "zipcodePage",
|
|
description: "Set local weather station",
|
|
required: false,
|
|
image: "http://www.plaidsystems.com/smartthings/rain.png")
|
|
|
|
input "rainDelay", "decimal", title: "inches of rain that will delay watering, default: 0.2", required: false
|
|
input "isSeason", "bool", title: "Enable Seasonal Weather Adjustment:", metadata: [values: ['true', 'false']]
|
|
}
|
|
}
|
|
}
|
|
|
|
def zipcodePage() {
|
|
return dynamicPage(name: "zipcodePage", title: "Spruce weather station setup") {
|
|
section(""){input "zipcode", "text", title: "Zipcode or WeatherUnderground station id. Default value is current location.", defaultValue: "${location.zipCode}", required: false, submitOnChange: true
|
|
}
|
|
|
|
section(""){href title: "Search WeatherUnderground.com for weather stations",
|
|
description: "After page loads, select Change Station for a list of weather stations. You will need to copy the station code into the zipcode field above",
|
|
required: false, style:"embedded",
|
|
image: "http://www.plaidsystems.com/smartthings/wu.png",
|
|
url: "http://www.wunderground.com/q/${location.zipCode}"
|
|
}
|
|
}
|
|
}
|
|
|
|
private startTimeString()
|
|
{
|
|
def newtime = "${settings["startTime"]}"
|
|
if ("${settings["startTime"]}" == "null") return "Please set!"
|
|
else return "${hhmm(newtime)}"
|
|
}
|
|
|
|
def enableString()
|
|
{
|
|
if(enable) return "${enable}"
|
|
return "False"
|
|
}
|
|
|
|
def contactSensorString()
|
|
{
|
|
if(contact) return "${contact} \n Delay: ${contactDelay} mins"
|
|
return "None"
|
|
}
|
|
|
|
def seasonalAdjString()
|
|
{
|
|
if(isSeason) return "${isSeason}"
|
|
return "False"
|
|
}
|
|
def syncString()
|
|
{
|
|
if(sync) return "${sync}"
|
|
return "None"
|
|
}
|
|
def notifyString()
|
|
{
|
|
def notifyString = ""
|
|
if("${settings["notify"]}" != "null") {
|
|
if (notify.contains('Weekly')) notifyString = "${notifyString} Weekly"
|
|
if (notify.contains('Daily')) notifyString = "${notifyString} Daily"
|
|
if (notify.contains('Weather')) notifyString = "${notifyString} Weather"
|
|
if (notify.contains('Warnings')) notifyString = "${notifyString} Warnings"
|
|
if (notify.contains('Moisture')) notifyString = "${notifyString} Moisture"
|
|
}
|
|
if(notifyString == "")
|
|
notifyString = " None"
|
|
return notifyString
|
|
}
|
|
def daysString()
|
|
{
|
|
def daysString = ""
|
|
if ("${settings["days"]}" != "null") {
|
|
if(days.contains('Even') || days.contains('Odd')) {
|
|
if (days.contains('Even')) daysString = "${daysString} Even"
|
|
if (days.contains('Odd')) daysString = "${daysString} Odd"
|
|
} else {
|
|
if (days.contains('Monday')) daysString = "${daysString} M"
|
|
if (days.contains('Tuesday')) daysString = "${daysString} Tu"
|
|
if (days.contains('Wednesday')) daysString = "${daysString} W"
|
|
if (days.contains('Thursday')) daysString = "${daysString} Th"
|
|
if (days.contains('Friday')) daysString = "${daysString} F"
|
|
if (days.contains('Saturday')) daysString = "${daysString} Sa"
|
|
if (days.contains('Sunday')) daysString = "${daysString} Su"
|
|
}
|
|
}
|
|
if(daysString == "")
|
|
daysString = " Any"
|
|
return daysString
|
|
}
|
|
|
|
|
|
private hhmm(time, fmt = "h:mm a")
|
|
{
|
|
def t = timeToday(time, location.timeZone)
|
|
def f = new java.text.SimpleDateFormat(fmt)
|
|
f.setTimeZone(location.timeZone ?: timeZone(time))
|
|
f.format(t)
|
|
}
|
|
|
|
def pumpDelayString()
|
|
{
|
|
if ("${settings["pumpDelay"]}" == "null") return "5"
|
|
else return "${settings["pumpDelay"]}"
|
|
}
|
|
|
|
def delayPage() {
|
|
dynamicPage(name: "delayPage", title: "Additional Options") {
|
|
section(""){
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_timer.png",
|
|
title: "Pump and Master valve delay",
|
|
required: false,
|
|
"Setting a delay is optional. If you have master valves or a pump suppling water then you can set a delay here. The delay is the time between the valve or pump turning on and the water valves opening. This is also the delay between valves opening"
|
|
}
|
|
section("") {
|
|
input "pumpDelay", "number", title: "Set a delay in seconds?", defaultValue: '5', required: false
|
|
}
|
|
section(""){
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_pause.png",
|
|
title: "Contact delays",
|
|
required: false,
|
|
"Selecting contacts is optional. When a selected contact sensor is opened, water immediately stops and will not resume until closed. Caution: if a contact is set and left open, the watering program will never run."
|
|
}
|
|
section("") {
|
|
input name: "contact", title: "Select water delay contacts", type: "capability.contactSensor", multiple: true, required: false
|
|
|
|
input "contactDelay", "number", title: "How many minutes delay after contact is closed?", defaultValue: '1', required: false
|
|
}
|
|
section(""){
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_spruce_controller_250.png",
|
|
title: "Controller Sync",
|
|
required: false,
|
|
"For multiple controllers only. This schedule will wait for the selected controller to finish."
|
|
input "sync", "capability.switch", title: "Select Master Controller", description: "Select Spruce Controller to sync", required: false, multiple: false
|
|
}
|
|
}
|
|
}
|
|
|
|
def zonePage() {
|
|
def z1Par=[zoneP:"1"], z2Par=[zoneP:"2"], z3Par=[zoneP:"3"], z4Par=[zoneP:"4"], z5Par=[zoneP:"5"], z6Par=[zoneP:"6"], z7Par=[zoneP:"7"],
|
|
z8Par=[zoneP:"8"],z9Par = [zoneP:"9"], z10Par = [zoneP:"10"], z11Par = [zoneP:"11"], z12Par = [zoneP:"12"], z13Par = [zoneP:"13"],
|
|
z14Par = [zoneP:"14"], z15Par = [zoneP:"15"], z16Par = [zoneP:"16"]
|
|
|
|
dynamicPage(name: "zonePage", title: "Zone setup", install: false, uninstall: false) {
|
|
section("") {
|
|
href(name: "hrefWithImage", title: "Zone configuration", page: "zoneSettingsPage",
|
|
description: "${zoneString()}",
|
|
required: false,
|
|
image: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png")
|
|
}
|
|
|
|
if (zoneNumber >= 1){
|
|
section(""){
|
|
href(name: "z1Page", title: "1: ${getname("1")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("1")}",
|
|
params: z1Par,
|
|
description: "${display("1")}" )
|
|
}
|
|
}
|
|
if (zoneNumber >= 2){
|
|
section(""){
|
|
href(name: "z2Page", title: "2: ${getname("2")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("2")}",
|
|
params: z2Par,
|
|
description: "${display("2")}" )
|
|
}
|
|
}
|
|
if (zoneNumber >= 3){
|
|
section(""){
|
|
href(name: "z3Page", title: "3: ${getname("3")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("3")}",
|
|
params: z3Par,
|
|
description: "${display("3")}" )
|
|
}
|
|
}
|
|
if (zoneNumber >= 4){
|
|
section(""){
|
|
href(name: "z4Page", title: "4: ${getname("4")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("4")}",
|
|
params: z4Par,
|
|
description: "${display("4")}" )
|
|
}
|
|
}
|
|
if (zoneNumber >= 5){
|
|
section(""){
|
|
href(name: "z5Page", title: "5: ${getname("5")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("5")}",
|
|
params: z5Par,
|
|
description: "${display("5")}" )
|
|
}
|
|
}
|
|
if (zoneNumber >= 6){
|
|
section(""){
|
|
href(name: "z6Page", title: "6: ${getname("6")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("6")}",
|
|
params: z6Par,
|
|
description: "${display("6")}" )
|
|
}
|
|
}
|
|
if (zoneNumber >= 7){
|
|
section(""){
|
|
href(name: "z7Page", title: "7: ${getname("7")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("7")}",
|
|
params: z7Par,
|
|
description: "${display("7")}" )
|
|
}
|
|
}
|
|
if (zoneNumber >= 8){
|
|
section(""){
|
|
href(name: "z8Page", title: "8: ${getname("8")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("8")}",
|
|
params: z8Par,
|
|
description: "${display("8")}" )
|
|
}
|
|
}
|
|
if (zoneNumber >= 9){
|
|
section(""){
|
|
href(name: "z9Page", title: "9: ${getname("9")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("9")}",
|
|
params: z9Par,
|
|
description: "${display("9")}" )
|
|
}
|
|
}
|
|
if (zoneNumber >= 10){
|
|
section(""){
|
|
href(name: "z10Page", title: "10: ${getname("10")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("10")}",
|
|
params: z10Par,
|
|
description: "${display("10")}" )
|
|
}
|
|
}
|
|
if (zoneNumber >= 11){
|
|
section(""){
|
|
href(name: "z11Page", title: "11: ${getname("11")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("11")}",
|
|
params: z11Par,
|
|
description: "${display("11")}" )
|
|
}
|
|
}
|
|
if (zoneNumber >= 12){
|
|
section(""){
|
|
href(name: "z12Page", title: "12: ${getname("12")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("12")}",
|
|
params: z12Par,
|
|
description: "${display("12")}" )
|
|
}
|
|
}
|
|
if (zoneNumber >= 13){
|
|
section(""){
|
|
href(name: "z13Page", title: "13: ${getname("13")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("13")}",
|
|
params: z13Par,
|
|
description: "${display("13")}" )
|
|
}
|
|
}
|
|
if (zoneNumber >= 14){
|
|
section(""){
|
|
href(name: "z14Page", title: "14: ${getname("14")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("14")}",
|
|
params: z14Par,
|
|
description: "${display("14")}" )
|
|
}
|
|
}
|
|
if (zoneNumber >= 15){
|
|
section(""){
|
|
href(name: "z15Page", title: "15: ${getname("15")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("15")}",
|
|
params: z15Par,
|
|
description: "${display("15")}" )
|
|
}
|
|
}
|
|
if (zoneNumber >= 16){
|
|
section(""){
|
|
href(name: "z16Page", title: "16: ${getname("16")}", required: false, page: "zoneSetPage",
|
|
image: "${getimage("16")}",
|
|
params: z16Par,
|
|
description: "${display("16")}" )
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
|
|
def zoneString(){
|
|
def numberString = "Add zones to setup"
|
|
if (zoneNumber) numberString = "Setup: " + "${zoneNumber}" + " zones"
|
|
if (learn) numberString += "\nLearn: enabled"
|
|
else numberString += "\nLearn: disabled"
|
|
return numberString
|
|
}
|
|
|
|
def zoneSettingsPage() {
|
|
dynamicPage(name: "zoneSettingsPage", title: "Zone Configuration") {
|
|
section(""){
|
|
input (name: "zoneNumber", type: "number", title: "Enter number of zones to configure?",description: "How many valves do you have? 1-16", required: true)//, defaultValue: 16)
|
|
input "gain", "number", title: "Increase or decrease all water times by this %, enter a negative or positive value, Default: 0", required: false
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_sensor_200_r.png",
|
|
title: "Moisture sensor learn mode",
|
|
"Learn mode: Watering times will be adjusted based on the assigned moisture sensor and watering will follow a schedule.\n\nNo Learn mode: Zones with moisture sensors will water on any available days when the low moisture setpoint has been reached."
|
|
input "learn", "bool", title: "Enable learning (with moisture sensors)", metadata: [values: ['true', 'false']]
|
|
}
|
|
}
|
|
}
|
|
|
|
def zoneSetPage(params){
|
|
dynamicPage(name: "zoneSetPage", title: "Zone ${setPage("${params?.zoneP}")} Setup") {
|
|
section(""){
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_${state.app}.png",
|
|
title: "Current Settings",
|
|
"${display("${state.app}")}"
|
|
}
|
|
section(""){
|
|
input "name${state.app}", "text", title: "Zone name?", required: false, defaultValue: "Zone ${state.app}"
|
|
}
|
|
section(""){
|
|
href(name: "tosprinklerSetPage", title: "Sprinkler type: ${setString("zone")}", required: false, page: "sprinklerSetPage",
|
|
image: "${getimage("${settings["zone${state.app}"]}")}",
|
|
description: "Set sprinkler nozzle type or turn zone off")
|
|
}
|
|
section(""){
|
|
href(name: "toplantSetPage", title: "Landscape Select: ${setString("plant")}", required: false, page: "plantSetPage",
|
|
image: "${getimage("${settings["plant${state.app}"]}")}",
|
|
description: "Set landscape type")
|
|
}
|
|
|
|
section(""){
|
|
href(name: "tooptionSetPage", title: "Options: ${setString("option")}", required: false, page: "optionSetPage",
|
|
image: "${getimage("${settings["option${state.app}"]}")}",
|
|
description: "Set watering options")
|
|
}
|
|
section(""){
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_sensor_200_r.png",
|
|
title: "Moisture sensor settings",
|
|
"Select a soil moisture sensor to monitor and control watering. The soil moisture target value is optional and is the target low value. Spruce will use a default value based on settings, however you may override this setting to modify soil moisture threshold"
|
|
|
|
input "sensor${state.app}", "capability.relativeHumidityMeasurement", title: "Select moisture sensor?", required: false, multiple: false
|
|
|
|
input "sensorSp${state.app}", "number", title: "Minimum moisture sensor target value, Setpoint: ${getDrySp("${state.app}")}", required: false
|
|
}
|
|
section(""){
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_timer.png",
|
|
title: "Enter total watering time per week or ", ""
|
|
|
|
input "minWeek${state.app}", "number", title: "Water time per week (minutes). Default: 0 = autoadjust", required: false
|
|
|
|
input "perDay${state.app}", "number", title: "Guideline value for dividing minutes per week into watering days, a high value means more water per day. Default: 20", defaultValue: '20', required: false
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
def setString(i){
|
|
if (i == "zone"){
|
|
if (settings["zone${state.app}"] != null) return "${settings["zone${state.app}"]}"
|
|
else return "Not Set"
|
|
}
|
|
if (i == "plant"){
|
|
if (settings["plant${state.app}"] != null) return "${settings["plant${state.app}"]}"
|
|
else return "Not Set"
|
|
}
|
|
if (i == "option"){
|
|
if (settings["option${state.app}"] != null) return "${settings["option${state.app}"]}"
|
|
else return "Not Set"
|
|
}
|
|
}
|
|
|
|
def plantSetPage(){
|
|
dynamicPage(name: "plantSetPage", title: "${settings["name${state.app}"]} Landscape Select") {
|
|
section(""){
|
|
paragraph image: "http://www.plaidsystems.com/img/st_${state.app}.png",
|
|
title: "${settings["name${state.app}"]}",
|
|
"Current settings ${display("${state.app}")}"
|
|
|
|
input "plant${state.app}", "enum", title: "Landscape", multiple: false, required: false, submitOnChange: true, metadata: [values: ['Lawn', 'Garden', 'Flowers', 'Shrubs', 'Trees', 'Xeriscape', 'New Plants']]
|
|
}
|
|
section(""){
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_lawn_200_r.png",
|
|
title: "Lawn",
|
|
"Select Lawn for typical grass applications"
|
|
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_garden_225_r.png",
|
|
title: "Garden",
|
|
"Select Garden for vegetable gardens"
|
|
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_flowers_225_r.png",
|
|
title: "Flowers",
|
|
"Select Lawn for typical grass applications"
|
|
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_shrubs_225_r.png",
|
|
title: "Shrubs",
|
|
"Select Garden for vegetable gardens"
|
|
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_trees_225_r.png",
|
|
title: "Trees",
|
|
"Select Lawn for typical grass applications"
|
|
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_xeriscape_225_r.png",
|
|
title: "Xeriscape",
|
|
"Reduces water for native or drought tolorent plants"
|
|
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_newplants_225_r.png",
|
|
title: "New Plants",
|
|
"Increases watering time per week and reduces automatic adjustments to help establish new plants. No weekly seasonal adjustment and moisture setpoint set to 40."
|
|
}
|
|
}
|
|
}
|
|
|
|
def sprinklerSetPage(){
|
|
dynamicPage(name: "sprinklerSetPage", title: "${settings["name${state.app}"]} Sprinkler Select") {
|
|
section(""){
|
|
paragraph image: "http://www.plaidsystems.com/img/st_${state.app}.png",
|
|
title: "${settings["name${state.app}"]}",
|
|
"Current settings ${display("${state.app}")}"
|
|
input "zone${state.app}", "enum", title: "Sprinkler Type", multiple: false, required: false, defaultValue: 'Off', metadata: [values: ['Off', 'Spray', 'Rotor', 'Drip', 'Master Valve', 'Pump']]
|
|
|
|
}
|
|
section(""){
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_spray_225_r.png",
|
|
title: "Spray",
|
|
"Spray sprinkler heads spray a fan of water over the lawn. The water is applied evenly and can be turned on for a shorter duration of time."
|
|
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_rotor_225_r.png",
|
|
title: "Rotor",
|
|
"Rotor sprinkler heads rotate, spraying a stream over the lawn. Because they move back and forth across the lawn, they require a longer water period."
|
|
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_drip_225_r.png",
|
|
title: "Drip",
|
|
"Drip lines or low flow emitters water slowely to minimize evaporation, because they are low flow, they require longer watering periods."
|
|
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_master_225_r.png",
|
|
title: "Master",
|
|
"Master valves will open before watering begins. Set the delay between master opening and watering in delay settings."
|
|
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_pump_225_r.png",
|
|
title: "Pump",
|
|
"Attach a pump relay to this zone and the pump will turn on before watering begins. Set the delay between pump start and watering in delay settings."
|
|
}
|
|
}
|
|
}
|
|
|
|
def optionSetPage(){
|
|
dynamicPage(name: "optionSetPage", title: "${settings["name${state.app}"]} Options") {
|
|
section(""){
|
|
paragraph image: "http://www.plaidsystems.com/img/st_${state.app}.png",
|
|
title: "${settings["name${state.app}"]}",
|
|
"Current settings ${display("${state.app}")}"
|
|
input "option${state.app}", "enum", title: "Options", multiple: false, required: false, defaultValue: 'Cycle 2x', metadata: [values: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x']]
|
|
}
|
|
section(""){
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_slope_225_r.png",
|
|
title: "Slope",
|
|
"Slope sets the sprinklers to cycle 3x, each with a short duration to minimize runoff"
|
|
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_sand_225_r.png",
|
|
title: "Sand",
|
|
"Sandy soil drains quickly and requires more frequent but shorter intervals of water."
|
|
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_clay_225_r.png",
|
|
title: "Clay",
|
|
"Clay sets the sprinklers to cycle 2x, each with a short duration to maximize absorption"
|
|
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_cycle1x_225_r.png",
|
|
title: "No Cycle",
|
|
"The sprinklers will run for 1 long duration"
|
|
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_cycle2x_225_r.png",
|
|
title: "Cycle 2x",
|
|
"Cycle 2x will break the water period up into 2 shorter cycles to help minimize runoff and maximize adsorption"
|
|
|
|
paragraph image: "http://www.plaidsystems.com/smartthings/st_cycle3x_225_r.png",
|
|
title: "Cycle 3x",
|
|
"Cycle 3x will break the water period up into 3 shorter cycles to help minimize runoff and maximize adsorption"
|
|
}
|
|
}
|
|
}
|
|
|
|
def setPage(i){
|
|
if (i != "null") state.app = i
|
|
return state.app
|
|
}
|
|
|
|
def getaZoneSummary(zone)
|
|
{
|
|
def daysString = ""
|
|
def dpw = initDPW(zone)
|
|
def runTime = calcRunTime(initTPW(zone), dpw)
|
|
if ( !learn && (settings["sensor${zone}"] != null) ) {
|
|
daysString = "if Moisture is low on: "
|
|
dpw = daysAvailable()
|
|
}
|
|
if (days && (days.contains('Even') || days.contains('Odd'))) {
|
|
if(dpw == 1) daysString = "Every 8 days"
|
|
if(dpw == 2) daysString = "Every 4 days"
|
|
if(dpw == 4) daysString = "Every 2 days"
|
|
if(days.contains('Even') && days.contains('Odd')) daysString = "any day"
|
|
}
|
|
else {
|
|
def int[] dpwMap = [0,0,0,0,0,0,0]
|
|
dpwMap = getDPWDays(dpw)
|
|
daysString += getRunDays(dpwMap)
|
|
}
|
|
return "${zone}: ${runTime} minutes, ${daysString}"
|
|
}
|
|
|
|
def getZoneSummary()
|
|
{
|
|
def summary = ""
|
|
if (learn) summary = "Moisture Learning enabled"
|
|
else summary = "Moisture Learning disabled"
|
|
def zone = 1
|
|
createDPWMap()
|
|
while(zone <= 16) {
|
|
def zoneSum = getaZoneSummary(zone)
|
|
if (nozzle(zone) == 4) summary = "${summary}\n${zone}: ${settings["zone${zone}"]}"
|
|
else if ("${runTime}" != "0" && "${initDPW(zone)}" != "0") summary = "${summary}\n${zoneSum}"
|
|
zone++
|
|
}
|
|
if(summary == "") return zoneString() //"Setup all 16 zones"
|
|
|
|
return summary
|
|
}
|
|
|
|
def display(i)
|
|
{
|
|
def displayString = ""
|
|
def dpw = initDPW(i)
|
|
def runTime = calcRunTime(initTPW(i), dpw)
|
|
if ("${settings["zone${i}"]}" != "null") displayString += "${settings["zone${i}"]} : "
|
|
if ("${settings["plant${i}"]}" != "null") displayString += "${settings["plant${i}"]} : "
|
|
if ("${settings["option${i}"]}" != "null") displayString += "${settings["option${i}"]} : "
|
|
if ("${settings["sensor${i}"]}" != "null") displayString += "${settings["sensor${i}"]} : "
|
|
if ("${runTime}" != "0" && "${dpw}" != "0") displayString += "${runTime} minutes, ${dpw} days per week"
|
|
return "${displayString}"
|
|
}
|
|
|
|
def getimage(i){
|
|
if ("${settings["zone${i}"]}" == "Off") return "http://www.plaidsystems.com/smartthings/off2.png"
|
|
else if ("${settings["zone${i}"]}" == "Master Valve") return "http://www.plaidsystems.com/smartthings/master.png"
|
|
else if ("${settings["zone${i}"]}" == "Pump") return "http://www.plaidsystems.com/smartthings/pump.png"
|
|
else if ("${settings["plant${i}"]}" != "null" && "${settings["zone${i}"]}" != "null")// && "${settings["option${i}"]}" != "null")
|
|
i = "${settings["plant${i}"]}"
|
|
|
|
switch("${i}"){
|
|
case "null":
|
|
return "http://www.plaidsystems.com/smartthings/off2.png"
|
|
case "Off":
|
|
return "http://www.plaidsystems.com/smartthings/off2.png"
|
|
case "Lawn":
|
|
return "http://www.plaidsystems.com/smartthings/st_lawn_200_r.png"
|
|
case "Garden":
|
|
return "http://www.plaidsystems.com/smartthings/st_garden_225_r.png"
|
|
case "Flowers":
|
|
return "http://www.plaidsystems.com/smartthings/st_flowers_225_r.png"
|
|
case "Shrubs":
|
|
return "http://www.plaidsystems.com/smartthings/st_shrubs_225_r.png"
|
|
case "Trees":
|
|
return "http://www.plaidsystems.com/smartthings/st_trees_225_r.png"
|
|
case "Xeriscape":
|
|
return "http://www.plaidsystems.com/smartthings/st_xeriscape_225_r.png"
|
|
case "New Plants":
|
|
return "http://www.plaidsystems.com/smartthings/st_newplants_225_r.png"
|
|
case "Spray":
|
|
return "http://www.plaidsystems.com/smartthings/st_spray_225_r.png"
|
|
case "Rotor":
|
|
return "http://www.plaidsystems.com/smartthings/st_rotor_225_r.png"
|
|
case "Drip":
|
|
return "http://www.plaidsystems.com/smartthings/st_drip_225_r.png"
|
|
case "Master Valve":
|
|
return "http://www.plaidsystems.com/smartthings/st_master_225_r.png"
|
|
case "Pump":
|
|
return "http://www.plaidsystems.com/smartthings/st_pump_225_r.png"
|
|
case "Slope":
|
|
return "http://www.plaidsystems.com/smartthings/st_slope_225_r.png"
|
|
case "Sand":
|
|
return "http://www.plaidsystems.com/smartthings/st_sand_225_r.png"
|
|
case "Clay":
|
|
return "http://www.plaidsystems.com/smartthings/st_clay_225_r.png"
|
|
case "No Cycle":
|
|
return "http://www.plaidsystems.com/smartthings/st_cycle1x_225_r.png"
|
|
case "Cycle 2x":
|
|
return "http://www.plaidsystems.com/smartthings/st_cycle2x_225_r.png"
|
|
case "Cycle 3x":
|
|
return "http://www.plaidsystems.com/smartthings/st_cycle3x_225_r.png"
|
|
default:
|
|
return "http://www.plaidsystems.com/smartthings/off2.png"
|
|
}
|
|
}
|
|
|
|
def getname(i) {
|
|
if ("${settings["name${i}"]}" != "null") return "${settings["name${i}"]}"
|
|
else return "Zone $i"
|
|
}
|
|
|
|
def zipString() {
|
|
if (!zipcode) return "${location.zipCode}"
|
|
//add pws for correct weatherunderground lookup
|
|
if (!zipcode.isNumber()) return "pws:${zipcode}"
|
|
else return "${zipcode}"
|
|
}
|
|
|
|
//app install
|
|
def installed() {
|
|
state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
|
|
state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
|
|
state.Rain = [0,0,0,0,0,0,0]
|
|
state.fail = 0
|
|
state.seasonAdj = 0
|
|
state.weekseasonAdj = 0
|
|
log.debug "Installed with settings: ${settings}"
|
|
installSchedule()
|
|
}
|
|
|
|
def updated() {
|
|
unsubscribe()
|
|
unschedule()
|
|
log.debug "Installed with settings: ${settings}"
|
|
installSchedule()
|
|
}
|
|
|
|
def installSchedule(){
|
|
if(switches && startTime) {
|
|
def runTime = timeToday(startTime, location.timeZone)
|
|
def checktime = timeToday(startTime, location.timeZone).getTime() - 120000
|
|
log.debug "checktime: $checktime runtime: $runTime"
|
|
if(enable) {
|
|
subscribe switches, "switch.programOn", manualStart
|
|
schedule(checktime, Check)
|
|
schedule(runTime, checkOn)
|
|
note("schedule", "Schedule set to start at ${startTimeString()}", "w")
|
|
writeSettings()
|
|
}
|
|
else note("disable", "Automatic watering turned off, set active in app to resume scheduled watering.", "w")
|
|
}
|
|
}
|
|
|
|
//write initial zone settings to device at install/update
|
|
def writeSettings()
|
|
{
|
|
def cyclesMap = [:]
|
|
if(!state.tpwMap) state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
|
|
if(!state.dpwMap) state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
|
|
if(!state.seasonAdj) state.seasonAdj = 0
|
|
if(!state.weekseasonAdj) state.weekseasonAdj = 0
|
|
setSeason()
|
|
//add pumpdelay @ 1
|
|
cyclesMap."1" = pumpDelayString()
|
|
def zone = 1
|
|
def cycle = 0
|
|
while(zone <= 17)
|
|
{
|
|
if(nozzle(zone) == 4) cycle = 4
|
|
else cycle = cycles(zone)
|
|
//offset by 1, due to pumpdelay @ 1
|
|
cyclesMap."${zone+1}" = "${cycle}"
|
|
if (zone <= 16) {
|
|
state.tpwMap.putAt(zone-1, initTPW(zone))
|
|
state.dpwMap.putAt(zone-1, initDPW(zone))
|
|
}
|
|
zone++
|
|
}
|
|
switches.settingsMap(cyclesMap, 4001)
|
|
}
|
|
|
|
//get day of week integer
|
|
def getWeekDay(day)
|
|
{
|
|
def weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
|
def mapDay = [Monday:1, Tuesday:2, Wednesday:3, Thursday:4, Friday:5, Saturday:6, Sunday:7]
|
|
if(day && weekdays.contains(day)) {
|
|
return mapDay.get(day)
|
|
}
|
|
def today = new Date().format("EEEE", location.timeZone)
|
|
return mapDay.get(today)
|
|
}
|
|
|
|
// Get string of run days from dpwMap
|
|
def getRunDays(day1,day2,day3,day4,day5,day6,day7)
|
|
{
|
|
def str = ""
|
|
if(day1)
|
|
str += "M"
|
|
if(day2)
|
|
str += "T"
|
|
if(day3)
|
|
str += "W"
|
|
if(day4)
|
|
str += "Th"
|
|
if(day5)
|
|
str += "F"
|
|
if(day6)
|
|
str += "Sa"
|
|
if(day7)
|
|
str += "Su"
|
|
if(string == "")
|
|
str = "0 Days/week"
|
|
return str
|
|
}
|
|
|
|
def checkOn(){
|
|
cycleOn()
|
|
}
|
|
|
|
//start water program
|
|
def cycleOn(){
|
|
if (state.run == true){
|
|
subscribe switches, "switch.off", cycleOff
|
|
if (sync != null) subscribe sync, "status.finished", syncOn
|
|
if (contact != null){
|
|
subscribe contact, "contact.open", doorOpen
|
|
subscribe contact, "contact.closed", doorClosed
|
|
}
|
|
if (sync != null && !sync.currentValue('status').contains('finished')) note("pause", "waiting for $sync to complete before starting schedule", "w")
|
|
else if (contact == null || !contact.currentValue('contact').contains('open')) resume() //runIn(15, resume) //15 second delay to allow writesettings to finish resume -> switches.on
|
|
else note("pause", "$contact opened $switches paused watering", "w")
|
|
}
|
|
}
|
|
|
|
//when switch reports off, watering program is finished
|
|
def cycleOff(evt){
|
|
state.run = false
|
|
if (contact == null || !contact.currentValue('contact').contains('open')){
|
|
note("finished", "finished watering for today", "d")
|
|
unsubscribe(contact)
|
|
}
|
|
}
|
|
|
|
//start check
|
|
def manualStart(evt){
|
|
|
|
def runNowMap = []
|
|
runNowMap = cycleLoop()
|
|
if (runNowMap)
|
|
{
|
|
state.run = true
|
|
runNowMap = "Water will begin in 1 minute:\n" + runNowMap
|
|
note("active", "${runNowMap}", "d")
|
|
runIn(60, cycleOn) //start water program
|
|
}
|
|
|
|
else {
|
|
switches.programOff()
|
|
state.run = false
|
|
note("skipping", "No watering scheduled for today.", "d")
|
|
}
|
|
|
|
}
|
|
|
|
|
|
//run check each day at scheduled time
|
|
def Check(){
|
|
state.run = true
|
|
// Create weekly water summary, if requested, on Sunday
|
|
if(notify && notify.contains('Weekly') && (getWeekDay() == 7))
|
|
{
|
|
def zone = 1
|
|
def zoneSummary = ""
|
|
while(zone <= 16) {
|
|
if(settings["zone${zone}"] != null && settings["zone${zone}"] != 'Off' && nozzle(zone) != 4) {
|
|
def sum = getaZoneSummary(zone)
|
|
zoneSummary = "${zoneSummary} ${sum}"
|
|
}
|
|
zone++
|
|
}
|
|
log.debug "Weekly water summary: ${zoneSummary}"
|
|
sendPush "Weekly water summary: ${zoneSummary}"
|
|
}
|
|
|
|
def runNowMap = []
|
|
if (isDay() == false){
|
|
switches.programOff()
|
|
state.run = false
|
|
note("skipping", "No watering allowed today.", "d")
|
|
}
|
|
else if (isWeather() == false)
|
|
{
|
|
//get & set watering times for today
|
|
runNowMap = cycleLoop()
|
|
if (runNowMap)
|
|
{
|
|
state.run = true
|
|
runNowMap = "Water will begin in 2 minutes:\n" + runNowMap
|
|
note("active", "${runNowMap}", "d")
|
|
//cycleOn() //start water program
|
|
}
|
|
else {
|
|
switches.programOff()
|
|
state.run = false
|
|
note("skipping", "No watering scheduled for today.", "d")
|
|
}
|
|
}
|
|
else {
|
|
switches.programOff()
|
|
state.run = false
|
|
}
|
|
}
|
|
|
|
//get todays schedule
|
|
def cycleLoop()
|
|
{
|
|
def zone = 1
|
|
def cyc = 0
|
|
def rtime = 0
|
|
def timeMap = [:]
|
|
def pumpMap = ""
|
|
def runNowMap = ""
|
|
def soilString = ""
|
|
|
|
while(zone <= 16)
|
|
{
|
|
rtime = 0
|
|
//change to tpw(?)
|
|
if(settings["zone${zone}"] != null && settings["zone${zone}"] != 'Off' && nozzle(zone) != 4)
|
|
{
|
|
// First check if we run this zone today, use either dpwMap or even/odd date
|
|
def dpw = getDPW(zone)
|
|
def runToday = 0
|
|
if (days && (days.contains('Even') || days.contains('Odd'))) {
|
|
def daynum = new Date().format("dd", location.timeZone)
|
|
int dayint = Integer.parseInt(daynum)
|
|
if(days.contains('Odd') && (dayint +1) % Math.round(31 / (dpw * 4)) == 0) runToday = 1
|
|
if(days.contains('Even') && dayint % Math.round(31 / (dpw * 4)) == 0) runToday = 1
|
|
} else {
|
|
def weekDay = getWeekDay()-1
|
|
def dpwMap = getDPWDays(dpw)
|
|
def today = dpwMap[weekDay]
|
|
log.debug "Zone: ${zone} dpw: ${dpw} weekDay: ${weekDay} dpwMap: ${dpwMap} today: ${today}"
|
|
runToday = dpwMap[weekDay] //1 or 0
|
|
}
|
|
//if no learn check moisture sensors on available days
|
|
if (!learn && (settings["sensor${zone}"] != null) ) runToday = 1
|
|
|
|
if(runToday)
|
|
{
|
|
def soil = moisture(zone)
|
|
soilString += "${soil[1]}"
|
|
|
|
// Run this zone if soil moisture needed or if it is a weekly
|
|
// We run at least once a week and let moisture lower the time if needed
|
|
if ( (soil[0] == 1 ) || (learn && dpw == 1) )
|
|
{
|
|
cyc = cycles(zone)
|
|
dpw = getDPW(zone)
|
|
rtime = calcRunTime(getTPW(zone), dpw)
|
|
//daily weather adjust if no sensor
|
|
if(isSeason && settings["sensor${zone}"] == null) rtime = Math.round(rtime / cyc * state.seasonAdj / 100)
|
|
// runTime is total run time devided by num cycles
|
|
else rtime = Math.round(rtime / cyc)
|
|
runNowMap += "${settings["name${zone}"]}: ${cyc} x ${rtime} min\n"
|
|
log.debug"Zone ${zone} Map: ${cyc} x ${rtime} min"
|
|
}
|
|
}
|
|
}
|
|
if (nozzle(zone) == 4) pumpMap += "${settings["name${zone}"]}: ${settings["zone${zone}"]} on\n"
|
|
timeMap."${zone+1}" = "${rtime}"
|
|
zone++
|
|
}
|
|
if (soilString) {
|
|
soilString = "Moisture Sensors:\n" + soilString
|
|
note("moisture", "${soilString}","m")
|
|
}
|
|
//send settings to Spruce Controller
|
|
switches.settingsMap(timeMap,4002)
|
|
if (runNowMap) return runNowMap += pumpMap
|
|
return runNowMap
|
|
}
|
|
|
|
//Initialize Days per week, based on TPW, perDay and daysAvailable settings
|
|
def initDPW(i){
|
|
if(initTPW(i) > 0) {
|
|
def dpw
|
|
def perDay = 20
|
|
if(settings["perDay${i}"]) perDay = settings["perDay${i}"].toInteger()
|
|
dpw = Math.round(initTPW(i) / perDay)
|
|
if(dpw <= 1) return 1
|
|
// 3 days per week not allowed for even or odd day selection
|
|
if(dpw == 3 && days && (days.contains('Even') || days.contains('Odd')) && !(days.contains('Even') && days.contains('Odd')))
|
|
if(initTPW(i) / perDay < 3.0) return 2
|
|
else return 4
|
|
if(daysAvailable() < dpw) return daysAvailable()
|
|
return dpw
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Get current days per week value, calls init if not defined
|
|
def getDPW(zone)
|
|
{
|
|
def i = zone.toInteger()
|
|
if(state.dpwMap) return state.dpwMap.get(i-1)
|
|
return initDPW(i)
|
|
}
|
|
|
|
//Initialize Time per Week
|
|
def initTPW(i){
|
|
if("${settings["zone${i}"]}" == null || nozzle(i) == 0 || nozzle(i) == 4 || plant(i) == 0) return 0
|
|
|
|
// apply gain adjustment
|
|
def gainAdjust = 100
|
|
if (gain && gain != 0) gainAdjust += gain
|
|
|
|
// apply seasonal adjustment is enabled and not set to new plants
|
|
def seasonAdjust = 100
|
|
if (state.weekseasonAdj && isSeason && settings["plant${i}"] != "New Plants") seasonAdjust = state.weekseasonAdj
|
|
|
|
def zone = i.toInteger()
|
|
def tpw = 0
|
|
|
|
// Use learned, previous tpw if it is available
|
|
if(state.tpwMap) tpw = state.tpwMap.get(zone-1)
|
|
// set user time with season adjust
|
|
if(settings["minWeek${i}"] != null && settings["minWeek${i}"] != 0) tpw = Math.round(("${settings["minWeek${i}"]}").toInteger() * seasonAdjust / 100)
|
|
|
|
// initial tpw calculation
|
|
if (tpw == null || tpw == 0) tpw = Math.round(plant(i) * nozzle(i) * gainAdjust / 100 * seasonAdjust / 100)
|
|
// apply gain to all zones --obsolete with learn implementation--
|
|
//else if (gainAdjust != 100) twp = Math.round(tpw * gainAdjust / 100)
|
|
|
|
//if (tpw <= 3) tpw = 3
|
|
return tpw
|
|
}
|
|
|
|
// Get the current time per week, calls init if not defined
|
|
def getTPW(zone)
|
|
{
|
|
def i = zone.toInteger()
|
|
if(state.tpwMap) return state.tpwMap.get(i-1)
|
|
return initTPW(i)
|
|
}
|
|
|
|
// Calculate daily run time based on tpw and dpw
|
|
def calcRunTime(tpw, dpw)
|
|
{
|
|
def duration = 0
|
|
if(tpw > 0 && dpw > 0) {
|
|
duration = Math.round(tpw / dpw)
|
|
}
|
|
return duration
|
|
}
|
|
|
|
// Check the moisture level of a zone returning dry (1) or wet (0) and adjust tpw if overly dry/wet
|
|
def moisture(i)
|
|
{
|
|
// No Sensor on this zone
|
|
if(settings["sensor${i}"] == null) {
|
|
return [1,""]
|
|
}
|
|
|
|
// Check if sensor is reporting 6-> 12 hours
|
|
def sixHours = new Date(now() - (1000 * 60 * 60 * 12).toLong())
|
|
def recentActivity = (settings["sensor${i}"].eventsSince(sixHours)?.findAll { it.name == "temperature" })
|
|
if (recentActivity == []) {
|
|
//change to seperate warning note?
|
|
note("warning", "Please check ${settings["sensor${i}"]}, no activity in the last 12 hours", "w")
|
|
return [1, "Please check ${settings["sensor${i}"]}, no activity in the last 12 hours\n"] //change to 1
|
|
}
|
|
|
|
def latestHum = settings["sensor${i}"].latestValue("humidity")
|
|
def spHum = getDrySp(i).toInteger()
|
|
//def moistureList = []
|
|
if (!learn)
|
|
{
|
|
// no learn mode, only looks at target moisture level
|
|
if(latestHum <= spHum) {
|
|
//dry soil
|
|
return [1,"${settings["name${i}"]}, Watering ${settings["sensor${i}"]} reads ${latestHum}%, SP is ${spHum}%\n"]
|
|
} else {
|
|
//wet soil
|
|
return [0,"${settings["name${i}"]}, Skipping ${settings["sensor${i}"]} reads ${latestHum}%, SP is ${spHum}%\n"]
|
|
}
|
|
}
|
|
|
|
def tpw = getTPW(i)
|
|
def tpwAdjust = Math.round((spHum - latestHum) * getDPW(i))
|
|
// Only adjust tpw if outside of 1 percent, otherwise rounding will keep it the same anyways
|
|
if (tpwAdjust >= -1 && tpwAdjust <= 1) {
|
|
tpwAdjust = 0
|
|
}
|
|
|
|
def moistureSum = ""
|
|
if(tpwAdjust != 0) {
|
|
def newTPW = Math.round(tpw + (tpw * tpwAdjust / 100))
|
|
if (newTPW <= 5) note("warning", "Please check ${settings["sensor${i}"]}, Zone ${i} time per week is very low: ${newTPW} mins/week","w")
|
|
if (newTPW >= 150) note("warning", "Please check ${settings["sensor${i}"]}, Zone ${i} time per week is very high: ${newTPW} mins/week","w")
|
|
state.tpwMap.putAt(i-1, newTPW)
|
|
state.dpwMap.putAt(i-1, initDPW(i))
|
|
moistureSum = "Zone ${i}: ${settings["sensor${i}"]} moisture is: ${latestHum}%, SP is ${spHum}% time adjusted by ${tpwAdjust}% to ${newTPW} mins/week\n"
|
|
} else {
|
|
moistureSum = "Zone ${i}: ${settings["sensor${i}"]} moisture is: ${latestHum}%, SP is ${spHum}% no time adjustment\n"
|
|
}
|
|
//note("moisture", "${moistureSum}","m")
|
|
return [1, "${moistureSum}"]
|
|
}
|
|
|
|
//notifications to device, pushed if requested
|
|
def note(status, message, type){
|
|
log.debug "${status}: ${message}"
|
|
switches.notify("${status}", "${message}")
|
|
if(notify)
|
|
{
|
|
if (notify.contains('Daily') && type == "d"){
|
|
sendPush "${message}"
|
|
}
|
|
if (notify.contains('Weather') && type == "f"){
|
|
sendPush "${message}"
|
|
}
|
|
if (notify.contains('Warnings') && type == "w"){
|
|
sendPush "${message}"
|
|
}
|
|
if (notify.contains('Moisture') && type == "m"){
|
|
sendPush "${message}"
|
|
}
|
|
}
|
|
}
|
|
|
|
//days available
|
|
def daysAvailable(){
|
|
int dayCount = 0
|
|
if("${settings["days"]}" == "null") dayCount = 7
|
|
else if(days){
|
|
if (days.contains('Even') || days.contains('Odd')) {
|
|
dayCount = 4
|
|
if(days.contains('Even') && days.contains('Odd')) dayCount = 7
|
|
} else {
|
|
if (days.contains('Monday')) dayCount += 1
|
|
if (days.contains('Tuesday')) dayCount += 1
|
|
if (days.contains('Wednesday')) dayCount += 1
|
|
if (days.contains('Thursday')) dayCount += 1
|
|
if (days.contains('Friday')) dayCount += 1
|
|
if (days.contains('Saturday')) dayCount += 1
|
|
if (days.contains('Sunday')) dayCount += 1
|
|
}
|
|
}
|
|
return dayCount
|
|
}
|
|
|
|
//get moisture SP
|
|
def getDrySp(i){
|
|
if ("${settings["sensorSp${i}"]}" != "null") return "${settings["sensorSp${i}"]}"
|
|
else if (settings["plant${i}"] == "New Plants") return 40
|
|
else{
|
|
switch (settings["option${i}"]) {
|
|
case "Sand":
|
|
return 15
|
|
case "Clay":
|
|
return 35
|
|
default:
|
|
return 20
|
|
}
|
|
}
|
|
}
|
|
|
|
//zone: ['Off', 'Spray', 'rotor', 'Drip', 'Master Valve', 'Pump']
|
|
def nozzle(i){
|
|
def getT = settings["zone${i}"]
|
|
if (!getT) return 0
|
|
switch(getT) {
|
|
case "Spray":
|
|
return 1
|
|
case "Rotor":
|
|
return 1.4
|
|
case "Drip":
|
|
return 2.4
|
|
case "Master Valve":
|
|
return 4
|
|
case "Pump":
|
|
return 4
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
//plant: ['Lawn', 'Garden', 'Flowers', 'Shrubs', 'Trees', 'Xeriscape', 'New Plants']
|
|
def plant(i){
|
|
def getP = settings["plant${i}"]
|
|
if(!getP) return 0
|
|
switch(getP) {
|
|
case "Lawn":
|
|
return 60
|
|
case "Garden":
|
|
return 50
|
|
case "Flowers":
|
|
return 40
|
|
case "Shrubs":
|
|
return 30
|
|
case "Trees":
|
|
return 20
|
|
case "Xeriscape":
|
|
return 30
|
|
case "New Plants":
|
|
return 80
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
//option: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x']
|
|
def cycles(i){
|
|
def getC = settings["option${i}"]
|
|
if(!getC) return 2
|
|
switch(getC) {
|
|
case "Slope":
|
|
return 3
|
|
case "Sand":
|
|
return 1
|
|
case "Clay":
|
|
return 2
|
|
case "No Cycle":
|
|
return 1
|
|
case "Cycle 2x":
|
|
return 2
|
|
case "Cycle 3x":
|
|
return 3
|
|
default:
|
|
return 2
|
|
}
|
|
}
|
|
|
|
//check if day is allowed
|
|
def isDay() {
|
|
if ("${settings["days"]}" == "null") return true
|
|
|
|
def today = new Date().format("EEEE", location.timeZone)
|
|
def daynum = new Date().format("dd", location.timeZone)
|
|
int dayint = Integer.parseInt(daynum)
|
|
|
|
log.debug "today: ${today} ${dayint}, days: ${days}"
|
|
|
|
if (days.contains(today)) return true
|
|
if (days.contains("Even") && (dayint % 2 == 0)) return true
|
|
if (days.contains("Odd") && (dayint % 2 != 0)) return true
|
|
|
|
return false
|
|
}
|
|
|
|
//set season adjustment & remove season adjustment
|
|
def setSeason() {
|
|
|
|
def zone = 1
|
|
while(zone <= 16) {
|
|
if ( !learn || (settings["sensor${zone}"] == null) ) {
|
|
state.tpwMap.putAt(zone-1, 0)
|
|
def tpw = initTPW(zone)
|
|
//def newTPW = Math.round(tpw * tpwAdjust / 100)
|
|
state.tpwMap.putAt(zone-1, tpw)
|
|
state.dpwMap.putAt(zone-1, initDPW(zone))
|
|
log.debug "Zone ${zone}: seasonaly adjusted by ${state.weekseasonAdj-100}% to ${tpw}"
|
|
}
|
|
|
|
zone++
|
|
}
|
|
}
|
|
|
|
//check weather
|
|
def isWeather(){
|
|
def wzipcode = "${zipString()}"
|
|
|
|
// Forecast rain
|
|
Map sdata = getWeatherFeature("forecast10day", wzipcode)
|
|
|
|
log.debug sdata.response
|
|
if(sdata.response.containsKey('error') || sdata == null) {
|
|
note("season", "Weather API error, skipping weather check" , "f")
|
|
return false
|
|
}
|
|
def qpf = sdata.forecast.simpleforecast.forecastday.qpf_allday.mm
|
|
def qpfTodayIn = 0
|
|
if (qpf.get(0).isNumber()) qpfTodayIn = Math.round(qpf.get(0).toInteger() /25.4 * 100) /100
|
|
log.debug "qpfTodayIn ${qpfTodayIn}"
|
|
def qpfTomIn = 0
|
|
if (qpf.get(1).isNumber()) qpfTomIn = Math.round(qpf.get(1).toInteger() /25.4 * 100) /100
|
|
log.debug "qpfTomIn ${qpfTomIn}"
|
|
// current conditions
|
|
Map cond = getWeatherFeature("conditions", wzipcode)
|
|
|
|
def TRain = 0
|
|
if (cond.current_observation.precip_today_metric.isNumber()) TRain = Math.round(cond.current_observation.precip_today_metric.toInteger() /25.4 * 100) /100
|
|
log.debug "TRain ${TRain}"
|
|
// reported rain
|
|
Map yCond = getWeatherFeature("yesterday", wzipcode)
|
|
def YRain = 0
|
|
if (yCond.history.dailysummary.precipi.get(0).isNumber()) YRain = yCond.history.dailysummary.precipi.get(0)
|
|
|
|
if(TRain > qpfTodayIn) qpfTodayIn = TRain
|
|
log.debug "TRain ${TRain} qpfTodayIn ${qpfTodayIn}"
|
|
//state.Rain = [S,M,T,W,T,F,S]
|
|
//state.Rain = [0,0.43,3,0,0,0,0]
|
|
def day = getWeekDay()
|
|
state.Rain.putAt(day - 1, YRain)
|
|
def i = 0
|
|
def weeklyRain = 0
|
|
while (i <= 6){
|
|
def factor = 0
|
|
if ((day - i) > 0) factor = day - i
|
|
else factor = day + 7 - i
|
|
def getrain = state.Rain.get(i)
|
|
weeklyRain += Math.round(getrain.toFloat() / factor * 100)/100
|
|
i++
|
|
}
|
|
log.debug "weeklyRain ${weeklyRain}"
|
|
//note("season", "weeklyRain ${weeklyRain} ${state.Rain}", "d")
|
|
|
|
//get highs
|
|
def getHigh = sdata.forecast.simpleforecast.forecastday.high.fahrenheit
|
|
def avgHigh = Math.round((getHigh.get(0).toInteger() + getHigh.get(1).toInteger() + getHigh.get(2).toInteger() + getHigh.get(3).toInteger() + getHigh.get(4).toInteger())/5)
|
|
|
|
Map citydata = getWeatherFeature("geolookup", wzipcode)
|
|
def weatherString = "${citydata.location.city} weather\n Today: ${getHigh.get(0)}F, ${qpfTodayIn}in rain\n Tomorrow: ${getHigh.get(1)}F, ${qpfTomIn}in rain\n Yesterday: ${YRain}in rain "
|
|
|
|
if (isSeason)
|
|
{
|
|
//daily adjust
|
|
state.seasonAdj = Math.round(getHigh.get(0).toInteger()/avgHigh *100)
|
|
weatherString += "\n Adjusted ${state.seasonAdj - 100}% for Today"
|
|
|
|
// Apply seasonal adjustment on Monday each week or at install
|
|
if(getWeekDay() == 1 || state.weekseasonAdj == 0) {
|
|
|
|
//get humidity
|
|
def gethum = sdata.forecast.simpleforecast.forecastday.avehumidity
|
|
def humWeek = Math.round((gethum.get(0).toInteger() + gethum.get(1).toInteger() + gethum.get(2).toInteger() + gethum.get(3).toInteger() + gethum.get(4).toInteger())/5)
|
|
|
|
//get daylight
|
|
Map astro = getWeatherFeature("astronomy", wzipcode)
|
|
def getsunRH = astro.moon_phase.sunrise.hour
|
|
def getsunRM = astro.moon_phase.sunrise.minute
|
|
def getsunSH = astro.moon_phase.sunset.hour
|
|
def getsunSM = astro.moon_phase.sunset.minute
|
|
def daylight = ((getsunSH.toInteger() * 60) + getsunSM.toInteger())-((getsunRH.toInteger() * 60) + getsunRM.toInteger())
|
|
|
|
//set seasonal adjustment
|
|
state.weekseasonAdj = Math.round((daylight/700 * avgHigh/75) * ((1-(humWeek/100)) * avgHigh/75)*100)
|
|
|
|
//apply seasonal time adjustment
|
|
weatherString += "\n Applying seasonal adjustment of ${state.weekseasonAdj-100}% this week"
|
|
//note("season", "Applying seasonal adjustment of ${state.weekseasonAdj-100}% this week", "f")
|
|
setSeason()
|
|
}
|
|
}
|
|
|
|
note("season", weatherString , "f")
|
|
|
|
def setrainDelay = "0.2"
|
|
if (rainDelay) setrainDelay = rainDelay
|
|
if (switches.latestValue("rainsensor") == "rainsensoron"){
|
|
note("raintoday", "is skipping watering, rain sensor is on.", "d")
|
|
return true
|
|
}
|
|
else if (qpfTodayIn > setrainDelay.toFloat()){
|
|
note("raintoday", "is skipping watering, ${qpfTodayIn}in rain today.", "d")
|
|
return true
|
|
}
|
|
else if (qpfTomIn > setrainDelay.toFloat()){
|
|
note("raintom", "is skipping watering, ${qpfTomIn}in rain expected tomorrow.", "d")
|
|
return true
|
|
}
|
|
else if (weeklyRain > setrainDelay.toFloat()){
|
|
note("rainy", "is skipping watering, ${weeklyRain}in average rain over the past week.", "d")
|
|
return true
|
|
}
|
|
return false
|
|
|
|
}
|
|
|
|
def doorOpen(evt){
|
|
note("pause", "$contact opened $switches paused watering", "w")
|
|
switches.off()
|
|
}
|
|
|
|
def doorClosed(evt){
|
|
note("active", "$contact closed $switches will resume watering in $contactDelay minutes", "w")
|
|
runIn(contactDelay * 60, resume)
|
|
}
|
|
|
|
def resume(){
|
|
switches.on()
|
|
state.fail = 10
|
|
}
|
|
|
|
def syncOn(evt){
|
|
note("active", "$sync complete, starting scheduled program", "w")
|
|
cycleOn()
|
|
}
|
|
|
|
def getDPWDays(dpw)
|
|
{
|
|
if(dpw == 1)
|
|
return state.DPWDays1
|
|
if(dpw == 2)
|
|
return state.DPWDays2
|
|
if(dpw == 3)
|
|
return state.DPWDays3
|
|
if(dpw == 4)
|
|
return state.DPWDays4
|
|
if(dpw == 5)
|
|
return state.DPWDays5
|
|
if(dpw == 6)
|
|
return state.DPWDays6
|
|
if(dpw == 7)
|
|
return state.DPWDays7
|
|
return [0,0,0,0,0,0,0]
|
|
}
|
|
|
|
// Create a map of what days each possible DPW value will run on
|
|
// Example: User sets allowed days to Monday Wed and Fri
|
|
// Map would look like: DPWDays1:[1,0,0,0,0,0,0] (run on Monday)
|
|
// DPWDays2:[1,0,0,0,1,0,0] (run on Monday and Friday)
|
|
// DPWDays3:[1,0,1,0,1,0,0] (run on Monday Wed and Fri)
|
|
// Everything runs on the first day possible, starting with Monday.
|
|
def createDPWMap() {
|
|
state.DPWDays1 = []
|
|
state.DPWDays2 = []
|
|
state.DPWDays3 = []
|
|
state.DPWDays4 = []
|
|
state.DPWDays5 = []
|
|
state.DPWDays6 = []
|
|
state.DPWDays7 = []
|
|
def NDAYS = 7
|
|
// day Distance[NDAYS][NDAYS], easier to just define than calculate everytime
|
|
def int[][] dayDistance = [[0,1,2,3,3,2,1],[1,0,1,2,3,3,2],[2,1,0,1,2,3,3],[3,2,1,0,1,2,3],[3,3,2,1,0,1,2],[2,3,3,2,1,0,1],[1,2,3,3,2,1,0]]
|
|
def ndaysAvailable = daysAvailable()
|
|
def i = 0
|
|
def int[] daysAvailable = [0,1,2,3,4,5,6]
|
|
if(days)
|
|
{
|
|
if (days.contains('Even') || days.contains('Odd')) {
|
|
return
|
|
}
|
|
if (days.contains('Monday')) {
|
|
daysAvailable[i] = 0
|
|
i++
|
|
}
|
|
if (days.contains('Tuesday')) {
|
|
daysAvailable[i] = 1
|
|
i++
|
|
}
|
|
if (days.contains('Wednesday')) {
|
|
daysAvailable[i] = 2
|
|
i++
|
|
}
|
|
if (days.contains('Thursday')) {
|
|
daysAvailable[i] = 3
|
|
i++
|
|
}
|
|
if (days.contains('Friday')) {
|
|
daysAvailable[i] = 4
|
|
i++
|
|
}
|
|
if (days.contains('Saturday')) {
|
|
daysAvailable[i] = 5
|
|
i++
|
|
}
|
|
if (days.contains('Sunday')) {
|
|
daysAvailable[i] = 6
|
|
i++
|
|
}
|
|
|
|
if(i != ndaysAvailable) {
|
|
log.debug "ERROR: days and daysAvailable do not match."
|
|
log.debug "${i} ${ndaysAvailable}"
|
|
}
|
|
}
|
|
//log.debug "Ndays: ${ndaysAvailable} Available Days: ${daysAvailable}"
|
|
def maxday = -1
|
|
def max = -1
|
|
def days = new int[7]
|
|
def int[][] runDays = [[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0]]
|
|
for(def a=0; a < ndaysAvailable; a++) {
|
|
|
|
// Figure out next day using the dayDistance map, getting the farthest away day (max value)
|
|
if(a > 0 && ndaysAvailable >= 2 && a != ndaysAvailable-1) {
|
|
if(a == 1) {
|
|
for(def c=1; c < ndaysAvailable; c++) {
|
|
def d = dayDistance[daysAvailable[0]][daysAvailable[c]]
|
|
if(d > max) {
|
|
max = d
|
|
maxday = daysAvailable[c]
|
|
}
|
|
}
|
|
//log.debug "max: ${max} maxday: ${maxday}"
|
|
days[0] = maxday
|
|
}
|
|
|
|
// Find successive maxes for the following days
|
|
if(a > 1) {
|
|
def lmax = max
|
|
def lmaxday = maxday
|
|
max = -1
|
|
for(def c = 1; c < ndaysAvailable; c++) {
|
|
def d = dayDistance[daysAvailable[0]][daysAvailable[c]]
|
|
def t = d > max
|
|
if(a % 2 == 0)
|
|
t = d >= max
|
|
if(d < lmax && d >= max) {
|
|
if(d == max) {
|
|
d = dayDistance[lmaxday][daysAvailable[c]]
|
|
if(d > dayDistance[lmaxday][maxday]) {
|
|
max = d
|
|
maxday = daysAvailable[c]
|
|
}
|
|
} else {
|
|
max = d
|
|
maxday = daysAvailable[c]
|
|
}
|
|
}
|
|
}
|
|
lmax = 5
|
|
while(max == -1) {
|
|
lmax = lmax -1
|
|
for(def c = 1; c < ndaysAvailable; c++) {
|
|
def d = dayDistance[daysAvailable[0]][daysAvailable[c]]
|
|
if(d < lmax && d >= max) {
|
|
if(d == max) {
|
|
d = dayDistance[lmaxday][daysAvailable[c]]
|
|
if(d > dayDistance[lmaxday][maxday]) {
|
|
max = d
|
|
maxday = daysAvailable[c]
|
|
}
|
|
} else {
|
|
max = d
|
|
maxday = daysAvailable[c]
|
|
}
|
|
}
|
|
}
|
|
for(def d=0; d< a-2; d++)
|
|
if(maxday == days[d])
|
|
max = -1
|
|
}
|
|
//log.debug"max: ${max} maxday: ${maxday}"
|
|
days[a-1] = maxday
|
|
}
|
|
}
|
|
|
|
// Set the runDays map using the calculated maxdays
|
|
for(def b=0; b < 7; b++)
|
|
{
|
|
// Runs every day available
|
|
if(a == ndaysAvailable-1) {
|
|
runDays[a][b] = 0
|
|
for(def c=0; c < ndaysAvailable; c++)
|
|
if(b == daysAvailable[c])
|
|
runDays[a][b] = 1
|
|
|
|
} else
|
|
// runs weekly, use first available day
|
|
if(a == 0)
|
|
if(b == daysAvailable[0])
|
|
runDays[a][b] = 1
|
|
else
|
|
runDays[a][b] = 0
|
|
else {
|
|
// Otherwise, start with first available day
|
|
if(b == daysAvailable[0])
|
|
runDays[a][b] = 1
|
|
else {
|
|
runDays[a][b] = 0
|
|
for(def c=0; c < a; c++)
|
|
if(b == days[c])
|
|
runDays[a][b] = 1
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
//log.debug "DPW: ${runDays}"
|
|
state.DPWDays1 = runDays[0]
|
|
state.DPWDays2 = runDays[1]
|
|
state.DPWDays3 = runDays[2]
|
|
state.DPWDays4 = runDays[3]
|
|
state.DPWDays5 = runDays[4]
|
|
state.DPWDays6 = runDays[5]
|
|
state.DPWDays7 = runDays[6]
|
|
}
|