Let's work on creating a, almost, simpified irrigation controller configuration

The foundation is based on the Sample Rules - Irrigation Controller.. However, I would like to improve upon this design by using an actual rain gauge and simplifying the rules on how often & how much to run the sprinklers. My goal is to conservatively water my yard every other day with a weekly total of 1 inch of water.

Many of the values are changed by setpiont for testing purposes.

Let’s start with the ITEMS. I’ve changed them to more generic names. Check groups on items as they are not included.

//---------------Irrigation----------------
Number        Weather_Rain_Trigger            "Manual Trip for Rainfall [%.2f in]"                <rain>            (All)
// Weather_Rain_Trigger  is intended to simulate the reed switch rain guage
Number        Weather_Rain                    "Manual Rainfall Today (future tipping bucket)[%.2f in]"                <rain>            (All)
// Weather_Rain is intended to be the current days rainfall
Number        Weather_Rain_Cumulative            "Manual Rainfall Last 2 Days (future tipping bucket)[%.2f in]"        <rain>            (All)
// Weather_Rain is intended to be the rainfall from the last two days

Number        Precip_Probability_0            "Precip Probability Today [%d %%]"        <rain>            (Irrigation,Test)            {weather="locationId=home, type=precipitation, property=probability"}
Number        Precip_Probability_1            "Precip Probability Tomorrow[%d %%]"        <rain>            (Irrigation,Test)            {weather="locationId=home, forecast=1, type=precipitation, property=probability"}


Switch        Irrigation_Master                "irrigation Master"                    <sprinkler>        (All,Sate,Masters,Irrigation)
// On|Off switch for whole irrigation system
String        Irrigation_Start_Time            "Start Time [%s]"                    <calendar>        (Irrigation)
// Time the system would normally turn on
Number        Irrigation_Scale_Factor            "Scale Factor Master [%d %%]"                <water>            (Irrigation)
// Percentage of Runtime to conserve water

//---------------Runtime for each relay----------------
Number        Irrigation_Relay_1_Mins            "Relay 1 Runtime[%d mins]"                    <water>            (Irrigation)
Number        Irrigation_Relay_2_Mins            "Relay 2 Runtime[%d mins]"                    <water>            (Irrigation)
Number        Irrigation_Relay_3_Mins            "Relay 3 Runtime[%d mins]"                    <water>            (Irrigation)
Number        Irrigation_Relay_4_Mins            "Relay 4 Runtime[%d mins]"                    <water>            (Irrigation)
Number        Irrigation_Relay_5_Mins            "Relay 5 Runtime[%d mins]"                    <water>            (Irrigation)
Number        Irrigation_Relay_6_Mins            "Relay 6 Runtime[%d mins]"                    <water>            (Irrigation)

//---------------Last time each relay was actuated---------------- 
DateTime    Irrigation_Relay_1_Time            "Relay 1 Last Run [%1$tm/%1$td, %1$tI:%1$tM %1$tp]"    <DateTimeIcon> (All,Updates,Irrigation)
DateTime    Irrigation_Relay_2_Time            "Relay 2 Last Run [%1$tm/%1$td, %1$tI:%1$tM %1$tp]"    <DateTimeIcon> (All,Updates,Irrigation)
DateTime    Irrigation_Relay_3_Time            "Relay 3 Last Run [%1$tm/%1$td, %1$tI:%1$tM %1$tp]"    <DateTimeIcon> (All,Updates,Irrigation)
DateTime    Irrigation_Relay_4_Time            "Relay 4 Last Run [%1$tm/%1$td, %1$tI:%1$tM %1$tp]"    <DateTimeIcon> (All,Updates,Irrigation)
DateTime    Irrigation_Relay_5_Time            "Relay 5 Last Run [%1$tm/%1$td, %1$tI:%1$tM %1$tp]"    <DateTimeIcon> (All,Updates,Irrigation)
DateTime    Irrigation_Relay_6_Time            "Relay 6 Last Run [%1$tm/%1$td, %1$tI:%1$tM %1$tp]"    <DateTimeIcon> (All,Updates,Irrigation)

//---------------Relays that trigger the irrigation zones (can also bind with OpenSprkinker----------------
Switch        Irrigation_Relay_1                "Relay 1 Switch"                            <water>            (Irrigation_Relay,Irrigation)
Switch        Irrigation_Relay_2                "Relay 2 Switch"                            <water>            (Irrigation_Relay,Irrigation)
Switch        Irrigation_Relay_3                "Relay 3 Switch"                            <water>            (Irrigation_Relay,Irrigation)
Switch        Irrigation_Relay_4                "Relay 4 Switch"                            <water>            (Irrigation_Relay,Irrigation)
Switch        Irrigation_Relay_5                "Relay 5 Switch"                            <water>            (Irrigation_Relay,Irrigation)
Switch        Irrigation_Relay_6                "Relay 6 Switch"                            <water>            (Irrigation_Relay,Irrigation)

DateTime    Weather_Last_Update                "Weather Binding Last Update [%1$tm/%1$td, %1$tI:%1$tM %1$tp]"      <DateTimeIcon>                (Irrigation,Updates) 


Now onto the RULES… I think it is best to break them up as some may not need to be changed.

Variables

val String MailTo1 = "someone@yourdomain.comt"
var Number RainIncr = 0

rule "Irrigation startup"
when
    System started
then
    postUpdate(Irrigation_Start_Time, "05:00")
    Irrigation_Relay?.members.forEach(Switch | {
           Switch.sendCommand(OFF)
       })
end
rule "Irrigation run"
when
    Time cron "0 0 5 1/1 * ? *"
then
    if (Irrigation_Master.state == ON) {
        // get the scale factor - used to reduce the run times across the board
        var Number scaleFactor = Irrigation_Scale_Factor.state as DecimalType

        // convert our start time to a joda.time.DateTime for today
        var DateTime startTime = parse(now.getYear() + "-" + now.getMonthOfYear() + "-" + now.getDayOfMonth() + "T" + Irrigation_Start_Time.state + ":00")
        var DateTime endTime        

        // get the raw run times for each zone (in mins)
        var Number R1Mins = Irrigation_Relay_1_Mins.state as DecimalType
        var Number R2Mins = Irrigation_Relay_2_Mins.state as DecimalType
        var Number R3Mins = Irrigation_Relay_3_Mins.state as DecimalType
        var Number R4Mins = Irrigation_Relay_4_Mins.state as DecimalType
        var Number R5Mins = Irrigation_Relay_5_Mins.state as DecimalType
        var Number R6Mins = Irrigation_Relay_6_Mins.state as DecimalType

        // convert to the actual run times (by applying the scale factor)
        var int R1Time = ((R1Mins * scaleFactor) / 100).intValue
        var int R2Time = ((R2Mins * scaleFactor) / 100).intValue
        var int R3Time = ((R3Mins * scaleFactor) / 100).intValue
        var int R4Time = ((R4Mins * scaleFactor) / 100).intValue
        var int R5Time = ((R5Mins * scaleFactor) / 100).intValue
        var int R6Time = ((R6Mins * scaleFactor) / 100).intValue

        // turn on each zone in turn (with a minute gap between each zone activation)
        if (R1Time > 0) {
            endTime = startTime.plusMinutes(R1Time)
            createTimer(startTime) [| sendCommand(Irrigation_Relay_1, ON) ]
            createTimer(endTime) [| sendCommand(Irrigation_Relay_1, OFF) ]
            startTime = endTime.plusMinutes(1)
        }

        if (R2Time > 0) {
            endTime = startTime.plusMinutes(R2Time)
            createTimer(startTime) [| sendCommand(Irrigation_Relay_2, ON) ]
            createTimer(endTime) [| sendCommand(Irrigation_Relay_2, OFF) ]         
            startTime = endTime.plusMinutes(1)
        }

        if (R3Time > 0) {
            endTime = startTime.plusMinutes(R3Time)
            createTimer(startTime) [| sendCommand(Irrigation_Relay_3, ON) ]
            createTimer(endTime) [| sendCommand(Irrigation_Relay_3, OFF) ]
            startTime = endTime.plusMinutes(1)
        }

        if (R4Time > 0) {
            endTime = startTime.plusMinutes(R4Time)
            createTimer(startTime) [| sendCommand(Irrigation_Relay_4, ON) ]
            createTimer(endTime) [| sendCommand(Irrigation_Relay_4, OFF) ]
            startTime = endTime.plusMinutes(1)
        }

        if (R4Time > 0) {
            endTime = startTime.plusMinutes(R5Time)
            createTimer(startTime) [| sendCommand(Irrigation_Relay_5, ON) ]
            createTimer(endTime) [| sendCommand(Irrigation_Relay_5, OFF) ]
            startTime = endTime.plusMinutes(1)
        }

        if (R4Time > 0) {
            endTime = startTime.plusMinutes(R6Time)
            createTimer(startTime) [| sendCommand(Irrigation_Relay_6, ON) ]
            createTimer(endTime) [| sendCommand(Irrigation_Relay_6, OFF) ]
            startTime = endTime.plusMinutes(1)
        }
        
    }   
end

This is the rule that I believe we can simplify.

rule "Disable irrigation if any rain"
when
    Item Weather_Rain changed or
    Item Weather_Today_Icon changed or
    Item Weather_Tomorrow_Icon changed
or
     Time cron "0 0 10 1/1 * ? *"
then
    // the rainfall threshold where we shutdown off irrigation
    var rainThreshold = 1

    // check for any rain in the last 24 hours
    var rainInLast24Hours = Weather_Rain.maximumSince(now.minusHours(24), "rrd4j")

    // default to the current rain value in case there is nothing in our history
    var rain = Weather_Rain.state

    if (rainInLast24Hours != null)
        rain = rainInLast24Hours.state

    // check if any rain is forecast
    var rainToday = Weather_Today_Icon.state == "chanceflurries" ||
                    Weather_Today_Icon.state == "chancerain" ||
                    Weather_Today_Icon.state == "chancesleet" ||
                    Weather_Today_Icon.state == "chancesnow" ||
                    Weather_Today_Icon.state == "chancetstorms" ||
                    Weather_Today_Icon.state == "flurries" ||                    
                    Weather_Today_Icon.state == "rain" || 
                    Weather_Today_Icon.state == "sleet" ||
                    Weather_Today_Icon.state == "snow" ||
                    Weather_Today_Icon.state == "tstorms"

    var rainTomorrow = Weather_Tomorrow_Icon.state == "chanceflurries" ||
                       Weather_Tomorrow_Icon.state == "chancerain" ||
                       Weather_Tomorrow_Icon.state == "chancesleet" ||
                       Weather_Tomorrow_Icon.state == "chancesnow" ||
                       Weather_Tomorrow_Icon.state == "chancetstorms" ||
                       Weather_Tomorrow_Icon.state == "flurries" ||
                       Weather_Tomorrow_Icon.state == "rain" || 
                       Weather_Tomorrow_Icon.state == "sleet" ||
                       Weather_Tomorrow_Icon.state == "snow" ||
                       Weather_Tomorrow_Icon.state == "tstorms"

    // shutoff irrigation if there has been rain or rain is forecast
    var logMessage = ""
    if (rain > rainThreshold) {
        logMessage = "Rain in the last 24 hours (" + rain + " mm) is above our threshold (" + rainThreshold + " mm) - irrigation disabled!"
    } else if (rainToday) {
        logMessage = "Rain is forecast for today - irrigation disabled!"
    } else if (rainTomorrow) {
        logMessage = "Rain is forecast for tomorrow - irrigation disabled!"
    }

    if (logMessage != "") {
        if (Irrigation_Master.state == ON) {
            logInfo("Irrigation", logMessage)
            sendMail(MailTo1 , "Irrigation Notice", logMessage)
            postUpdate(Irrigation_Master, OFF)
        }
    } else {
        if (Irrigation_Master.state == OFF) {
            logInfo("Irrigation", "No rain in the last 24 hours or any rain forecast - irrigation enabled!")
            sendMail(MailTo1 , "Irrigation Notice", "No rain in the last 24 hours or any rain forecast - irrigation enabled!")
            postUpdate(Irrigation_Master, ON)
        }
    }
end

Hi there, those are my rules from a very long time ago - good to see people are still getting some use from them!

I have since updated my irrigation rules a bit, see below.

I now have two scale factor Number items - one is the user scale factor (0-100%) and the second is a system calculated factor (0-200%). The idea is you can still enter a user scale factor but instead of shutting off the irrigation if ANY rain is detected or forecast, the system will attempt to determine HOW MUCH rain has fallen and therefore how much we should scale back the zone times.

This takes into account temp, humidity and rain fall. It means in summer if there is a string a very warm/dry days, the system scale factor will increase over 100% meaning we get extra water for those dry days.

When calculating the zone times I just take the configured mins for each zone, and multiply by BOTH scale factors.

rule "Calculate irrigation scale factor"
when
    Time cron "0 0/15 * * * ?"
then
    // get the averages for the last 24 hours using our persistence engine
    
    // humidity
    var Number averageHumidity = 30.0
    if (OU_Weather_Humidity.state instanceof DecimalType)
        averageHumidity = OU_Weather_Humidity.averageSince(now.minusHours(24), "influxdb") as DecimalType
    
    // temperature
    var Number averageTemp = 20.0
    if (OU_Weather_Temp.state instanceof DecimalType)
        averageTemp = OU_Weather_Temp.averageSince(now.minusHours(24), "influxdb") as DecimalType
        
    // rainfall
    var Number rainToday = 0 
    var Number rainYesterday = 0 
    if (OU_Weather_Rain.state instanceof DecimalType)
        rainToday = OU_Weather_Rain.state as DecimalType
    if (OU_Weather_RainYesterday.state instanceof DecimalType)
        rainYesterday = OU_Weather_RainYesterday.state as DecimalType
    
    // calculate the various scale factors
    // https://github.com/rszimm/sprinklers_pi/blob/master/Weather.cpp
    var Number humidityFactor = (30.0 - averageHumidity) * 1
    var Number tempFactor = (averageTemp - 20.0) * 4
    var Number rainFactor = (rainToday * -10.0) + (rainYesterday * -6.0)
    
    // TODO: soil moisture?

    // calculate the overall scale factor
    var Number scaleFactor = (100 + humidityFactor + tempFactor + rainFactor)

    // apply limits (0 to 200%)
    if (scaleFactor < 0)
        scaleFactor = 0
    else if (scaleFactor > 200)
        scaleFactor = 200

    // post the update so the irrigation rules can use it
    VT_Irrigation_ScaleFactor.postUpdate(scaleFactor.intValue)
    
    // log the calculated values
    logDebug("irrigation", "Scale factor parameters: avgHumidity=" + String::format("%.2f", averageHumidity.floatValue) + ", avgTemp=" + String::format("%.2f", averageTemp.floatValue) + ", rainToday=" + String::format("%.1f", rainToday.floatValue) + ", rainYesterday=" + String::format("%.1f", rainYesterday.floatValue))
    logDebug("irrigation", "Calculated scale factor is " + scaleFactor.intValue + "% (humidity=" + String::format("%.2f", humidityFactor.floatValue) + ", temp=" + String::format("%.2f", tempFactor.floatValue) + ", rain=" + String::format("%.2f", rainFactor.floatValue) + ")")
end
4 Likes

I don’t have time to really analyze this right now and make suggestions (hopefully tomorrow).

A couple quick scan comments though:

  • Consider applying one of the Alarm Clock examples to trigger the irrigation run rather than a static cron trigger so you can change the start/stop time from the sitemap

  • You should probably hold on to the references to the Timers you create to turn on the zones so you can check whether they are running and/or cancel them later. Also, see this posting for an example of how to create a relay where the shutting off of one zone starts up the next zone instead of creating all the Timers all at once. This couple provide a bit more control.

  • I think you can get the predicted precipitation from Wunderground at a minimum. You would probably have to use an HTTP pull and XSLT transform to extract it but it would be nice to include both rain that has already fallen and rain predicted to fall, not just the weather conditions. You could have a 100% chance of rain tomorrow but a prediction of only .01" total in which case you might want to irrigate anyway. The Rachio (my current solution which I’m VERY happy with) does something like this. If the amount of rain today plus the amount predicted before tomorrow is above a certain amount it doesn’t water. I can post an example of how I get today’s precipitation in inches this way if desired for an example.

  • Why the cron trigger on “Disable irrigation if any rain?” Why not just run once a little before or immediately before the irrigation is scheduled to start. We really only care if we need to skip it at that time.

  • rainThreshold should be an Item you can adjust from the sitemap. Some user may want to water more in the heat of a dry summer and less in the cooler/wetter spring and fall.

  • It is a little cleaner to put your Icon sates into a Set and then just test to see if rainToday and rainTomorrow are in the list:

val Set<String> rainConditions = newImmutableSet("chanceflurries", "chancerain", ...)

...

    // in rule
    var boolean rainForcast = rainConditions.contains(Weather_Today_Icon.state.toString) || rainConditions.contains(Weather_Tomorrow_Icon.state.toString)

    if (rain > ...
    else if (rainForecast) {
        logMessage = "Rain is forecast - irrigation disabled!"
    }
  • Outsource your logMessage to a separate Item and Rule so you can centralize how you implement your notifications. See this posting for an example.
1 Like

Very cool Ben… Thank you.

OK… I think that I bit off more than I can chew.

Can someone help me with a rain rules that control a simple switch to allow the sprinklers to run? My thought is to use a simple relay to bring extra accuracy to my OpenSprinkler. The relay will take the place of a rain gauge.

Here are some thoughts on perimeters.

If the probability of rain for the next day is over a certain percentage the relay, OpenSprinkler rain sensor, closes to stop the irrigation system.

If the rain guage, tipping bucket, measures a certain amount of rain, then the relay, OpenSprinkler rain sensor, closes to stop the irrigation system.

If there has been no irrigation system operations within a certain amount of days, the irrigations systems runs regardless of rain probability.

First, set up persistence.

Items:

Number Weather_Precip_Today         "Precip Today" <rain> { however you get the amount of rain that has fallen in the past 24 hours }
Number Weather_Prob_Precip_Tomorrow "Probability of Rain Tomorrow" <rain> { however you get the probability of rain tomorrow }
Switch Rain_Delay                   "Turn Off Sprinkler"

Rules:

val maxOffDays = <maximum days to keep Rain_Delay ON>
val minPrecipPercent = <the minimum probability of precipitation to turn on Rain_Delay>
val minPrecip = <the minimum amount of rain over the past 24 hours to turn on Rain_Delay>

rule "Rain Delay"
when
    Time cron <an hour or so before scheduled to run the sprinkler>
then

    if(Rain_Delay.lastUpdate.before(now.minusDays(maxOffDays)) {
        Rain_Delay.sendCommand(OFF)
    }
    else {
        var boolean delay = false
        if (Weather_Precip_Today.state as DecimalType >= minPrecip) delay = true
        if(Weather_Prob_Precip_Tomorrow.state as DecimalType >=  minPrecipPercent ) delay = true

        if(delay) {
            Rain_Delay.sendCommand(ON)
        }
        else {
            Rain_Delay.sendCommand(OFF)
        }
    }
end

NOTES:

  • I just typed in the above. There are almost certainly errors as typed.
  • The Rachio system takes into account both the probability of rain and the predicted amount of rain and if the amount of rain that fell yesterday plus the amount of rain predicted to fall today is above a threshold it skips the watering. It works really well in practice and you might consider add in not just the probability of rain but the predicted amount as well.

I finally got around to building this out, but I’m getting this error

Expected java.util.Date but was org.joda.time.DateTime

on this line

if(Rain_Delay.lastUpdate.before(now.minusDays(maxOffDays))) {

Here’s the code

val int maxOffDays = 2 //maximum days to keep Rain_Delay ON
val int minPrecipPercent = 40 //the minimum probability of precipitation to turn on Rain_Delay
val int minPrecip = 1 //the minimum amount of rain over the past 24 hours to turn on Rain_Delay

/*---------------Test Rules----------------*/ 

rule "Rain Delay"
when
    Time cron "0 0 23 1/1 * ? *"
then
    if(Rain_Delay.lastUpdate.before(now.minusDays(maxOffDays))) {
        Rain_Delay.sendCommand(OFF)
    }
    else {
        var boolean delay = false
        if (RainTotal.state as DecimalType >= minPrecip) delay = true
        if(Rain_Prob_Precip_Tomorrow.state as DecimalType >=  minPrecipPercent ) delay = true
        if(delay) {
            Rain_Delay.sendCommand(ON)
        }
        else {
            Rain_Delay.sendCommand(OFF)
        }
    }
end

Anyone with an idea of what this error is? and how to fix it?

You have hit one of the frustrations of working with date times in OH. Sometimes you get a joda DateTime and sometimes you get a java.util.Date. The two are not compatible. In your case, it is because lastUpdate returns a java.util.Date and the .before expects a java.util.Date, not a joda DateTime.

There are a couple of ways around this. You can convert the now.minusDays(maxOffDays) to a java.util.Date or you can convert them both to longs. I’m sure there are other ways as well.

Convert:

if(Rain_Delay.lastUpdate.before(new Date(now.minusDays(maxOffDays).millis))) {

longs:

if(Rain_Delay.lastUpdate.time < now.minusDays(maxOffDays.millis)) {
1 Like

I’m sorry to be a bother, but I am still getting errors for both.

if(Rain_Delay.lastUpdate.before(new Date(now.minusDays(maxOffDays).millis))) {

error:

Couldn’t resolve reference to JvmConstructor ‘Date’.

if(Rain_Delay.lastUpdate.time < now.minusDays(maxOffDays.millis)) {

error:

Multiple markers at this line

  • Couldn’t resolve reference to JvmIdentifiableElement ‘millis’.
  • Incompatible types. Expected byte or java.lang.Byte but was
    org.joda.time.DateTime

Actual errors or errors in Designer? In this latest release of Designer it apparently lost its ability to recognize any of the joda classes including “now”. Did it run?

your rules helped me further in how to handle various things. tnx