YAWS - Yet Another Watering Solution

Tags: #<Tag:0x00007fe746e87850> #<Tag:0x00007fe746e869c8>

This is an example of Yet Another Watering Solution I’ve developed for my garden.

Background:

The goal was to make a complete, fully configurable irrigation system, with the ability to easily expand the number of the zones, overwatering protection and correlation with the weather.

Assumptions:

  1. Use of the timers and cascading items pattern.
  2. Overwatering protection, after opening any valve, for a maximum of 2 hours of watering.
  3. Any number of watering zones, easy addition.
  4. Cascade valve opening.
  5. Manual watering lock.
  6. Taking into account temperature (min and max), wind, precipitation (last 24h and forecast for 24h)
  7. Correction factor which increases or decreases the valve opening time based on the temperature.
  8. Showing in the Basic UI how much watering time is left for the current zone.
  9. Watering on selected days of the week.
  10. Watering start based on the sunrise or the fixed time.

Prerequisites:

  1. Expire binding - https://www.openhab.org/addons/bindings/expire1/
  2. Astro binding - https://www.openhab.org/addons/bindings/astro/
  3. Configured persistence - https://www.openhab.org/docs/configuration/persistence.html
  4. OpenWeatherMap binding - https://www.openhab.org/addons/bindings/openweathermap/

I hope that the code is self-describing and easy to understand, as it is not overcomplicated. However, if something is not clear, feel free to ask.

Items

// settings group
Group Group_Irrigation_Settings

// irrigation lock
Switch IrrigationLock "Watering lock" (Group_Irrigation_Settings) { ga="Switch" }

// protection against too long watering, 2h default
Switch IrrigationTimerMax "Max irrigation time [MAP(irrigation.map):%s]" { expire = "2h,command=OFF" }

// all valves group
Group:Switch:OR(ON,OFF) GroupIrrigationValves "Irrigation valves [MAP(irrigation.map):%s]"
Group:Number:SUM GroupIrrigationTimes "Total irrigation time [%d min.]"

// cascading valves - current zone
String IrrigationCurrentValve "Current irrigation zone [MAP(irrigation.map):%s]"

// irrigation valves' switches
Switch IrrigationValveZone1 "Lawn - zone 1" (GroupIrrigationValves) { synonyms="Podlewanie w strefie pierwszej", ga="Switch" } 
Switch IrrigationValveZone2 "Lawn - zone 2" (GroupIrrigationValves) { synonyms="Podlewanie w strefie drugiej", ga="Switch" } 
Switch IrrigationValveZone3 "Lown - zone 3" (GroupIrrigationValves) { synonyms="Podlewanie w strefie trzeciej", ga="Switch" } 
Switch IrrigationValveZone4 "Garden flowers & plants - dripping line" (GroupIrrigationValves) { synonyms="Linia kropelkująca" } 

// irrigation times
Number IrrigationValveZone1Time "Lawn - zone 1 [%d min]" (GroupIrrigationTimes, Group_Irrigation_Settings)
Number IrrigationValveZone2Time "Lawn - zone 2 [%d min]" (GroupIrrigationTimes, Group_Irrigation_Settings)
Number IrrigationValveZone3Time "Lawn - zone 3 [%d min]" (GroupIrrigationTimes, Group_Irrigation_Settings)
Number IrrigationValveZone4Time "Flowers & plants - dripping line [%d min]" (GroupIrrigationTimes, Group_Irrigation_Settings)
Number IrrigationDurationCoefficientFactor "Correction factor"
Number IrrigationSectionRemainingTime "Remaining irrigation time [%d]"

// switch - whether to start at a particular hour or before the sunrise
Switch IrrigationStartAtSpecificHour "Start at a particular hour" (Group_Irrigation_Settings)
Number IrrigationStartTime "Irrigation start time" (Group_Irrigation_Settings)
Number IrrigationHoursBeforeSunrise "Start irrigation before sunrise [%d h]" (Group_Irrigation_Settings)

// irrigation week days
Switch IrrigationDay1 "Monday"
Switch IrrigationDay2 "Tuesday"
Switch IrrigationDay3 "Wednesday"
Switch IrrigationDay4 "Thursday"
Switch IrrigationDay5 "Friday"
Switch IrrigationDay6 "Saturday"
Switch IrrigationDay7 "Sunday"

Number:Length SumRainLast24h "Rainfall, last 24h [%.2f mm]"
Number:Length SumRainNext24h "Rainfall, forecast 24h [%.2f mm]"

Number:Speed MaxAllowedWindSpeed "Max allowed windspeed [%d km/h]" (Group_Irrigation_Settings)
Number:Length MaxAllowedRain "Max allowed rainfall sum [%.1f mm]" (Group_Irrigation_Settings)

Rules

/*
 * Irrigation rules
 */

import org.eclipse.smarthome.model.script.ScriptServiceUtil

val logName = "Irrigation"
var Timer irrigationTimer = null

// watering time correction factor - based on the average temperature
var coefficientFactor = 1

rule "Irrigation - system start"
when
	System started
then
	// initiall default settings

	if (SumRainLast24h.state == NULL) 
		SumRainLast24h.sendCommand(0)

	if (SumRainNext24h.state == NULL) 
		SumRainNext24h.sendCommand(0)

	if (MaxAllowedWindSpeed.state == NULL)
		MaxAllowedWindSpeed.sendCommand(35)

	if (MaxAllowedRain.state == NULL)
		MaxAllowedRain.sendCommand(3)

	if (IrrigationHoursBeforeSunrise.state == NULL)
		IrrigationHoursBeforeSunrise.sendCommand(1)

	if (IrrigationValveZone1Time.state == NULL)
		IrrigationValveZone1Time.sendCommand(1)

	if (IrrigationValveZone2Time.state == NULL)
		IrrigationValveZone2Time.sendCommand(1)

	if (IrrigationValveZone3Time.state == NULL)	
		IrrigationValveZone3Time.sendCommand(1)

	if (IrrigationValveZone4Time.state == NULL)	
		IrrigationValveZone4Time.sendCommand(1)

	if (IrrigationStartAtSpecificHour.state == NULL)
		IrrigationStartAtSpecificHour.sendCommand(OFF)

	if (IrrigationStartTime.state == NULL)	
		IrrigationStartTime.sendCommand(20)

	if (IrrigationHoursBeforeSunrise.state == NULL)
		IrrigationHoursBeforeSunrise.sendCommand(1)

	// close all valves
	GroupIrrigationValves.members.filter[valve | valve.state != OFF].forEach[valve | valve.sendCommand(OFF)]

	IrrigationCurrentValve.postUpdate(OFF)
end

rule "Irrigation - calculate whether to start watering"
when
    Time cron "0 * * ? * *" // every minute
then
	try {
		logInfo(logName, "Calculating whether to start irrigation")

		// calculate rainfall
		SumRainLast24h.sendCommand((WeatherAndForecastCurrentRain.sumSince(now.minusHours(24), "influxdb") as Number).doubleValue)
		SumRainNext24h.sendCommand(GroupForecastedRain24Sum.state as Number)

		// wait to propagate item states - not sure if necessary
		Thread.sleep(200)

		logDebug(logName, "Rain - last 24h (sum): {} mm", String::format("%.2f", (SumRainLast24h.state as Number).doubleValue))
		logDebug(logName, "Rain - forecast 24h (sum): {} mm", String::format("%.2f", (SumRainNext24h.state as Number).doubleValue))

		///////////////////		
		// start calculations, whether to start and for how long
		///////////////////

		// check for the manual lock
		if (IrrigationLock.state == ON) {
			logInfo(logName, "Irrigation lock is on")
			return
		}

		// check the week day
		val Number day = now.getDayOfWeek()
		val dayItem = ScriptServiceUtil.getItemRegistry.getItem("IrrigationDay" + day)
		
		if (dayItem === null || dayItem.state == OFF || dayItem.state == NULL) {
			logInfo(logName, "Inappropriate day to start irrigation", dayItem)
			return
		}

		// set the default irrigation hour to X hours before the sunrise
		val localSunRise = new DateTime(LocalSunRiseStart.state.toString).minusHours((IrrigationHoursBeforeSunrise.state as Number).intValue)
		var Number wateringHour = localSunRise.getHourOfDay()
		var Number wateringMinute = localSunRise.getMinuteOfHour()

		// if there is a specific hour in settings, then use it
		if (IrrigationStartAtSpecificHour.state == ON) {
			wateringHour = IrrigationStartTime.state as Number
			wateringMinute = 0
		}

		logInfo(logName, "Watering at: {}:{}", wateringHour, wateringMinute)

		// check if the current time is the watering time (full hour)
		if (now.getHourOfDay != wateringHour || now.getMinuteOfHour != wateringMinute) {
			// nope - good bye
			logInfo(logName, "Inappropriate time to start irrigation")
			return
		}
		logInfo(logName, "It is watering hour: {}:{}", wateringHour, wateringMinute)

		// check if the current wind speed is higher then the max allowed
		logInfo(logName, "Current wind speed: {} km/h", String::format("%.2f", (WeatherAndForecastCurrentWindSpeed.state as Number).doubleValue))
		if (WeatherAndForecastCurrentWindSpeed.state > MaxAllowedWindSpeed.state as Number) {
			logInfo(logName, "Wind speed too high to irrigate")
			return
		}

		// if the rainfall sum for the last 24h and the forecast for 24h is higher then set, then we are not going to irrigate
		val rainSum = (SumRainLast24h.state as Number).doubleValue + (SumRainNext24h.state as Number).doubleValue
		logInfo(logName, "Past and forcasted average rain: {} mm", String::format("%.2f", rainSum))
		if (rainSum > (MaxAllowedRain.state as Number).doubleValue) {
			logInfo(logName, "To heavy rain to irrigate (past and forcasted)")
			return
		}

		// check the wether, and based on that set the watering time coefficient factor
		// if the temperature is to low, don't start watering
		val avgTemperatureLast24h = (WeatherAndForecastCurrentTemperature.averageSince(now.minusHours(24), "influxdb") as Number).doubleValue

		logInfo(logName, "Average temperature for the last 24h: {}", avgTemperatureLast24h)

		if (avgTemperatureLast24h <= 10) {
			logInfo(logName, "Temperature too low to start irrigation")
			return
		} 
		else if (avgTemperatureLast24h > 30) {
			logInfo(logName, "Setting irrigation coefficient factor to 2")
		} 
		else {
			// coefficient factor should be between 1 and 2
			// this part could, and should be better
			coefficientFactor = avgTemperatureLast24h / 10 - 1;
			logInfo(logName, "Setting irrigation coefficient factor to {}", coefficientFactor)
		}

		///////////////////
		// ok, let's start watering, cascading all of the valves from the GroupIrrigationValves
		///////////////////

		// starting with the Zone 1, other zones will be turned on in sequence by the separate rule
		logInfo(logName, "Starting the irrigation sequence")
		IrrigationCurrentValve.sendCommand(IrrigationValveZone1.name)
	}
	catch (Exception e) {
        logError(logName, "Error calculating whether to start irrigation: " + e)
    }
end

rule "Irrigation - cascading"
when
    Item IrrigationCurrentValve received command
then
	try {
		// get the currently open valve
		val currValve = GroupIrrigationValves.members.findFirst[valve | valve.name == receivedCommand.toString]
		val currValveNum = Integer::parseInt(currValve.name.split("Zone").get(1))
		val currValveMins = GroupIrrigationTimes.members.findFirst[t | t.name == currValve.name+"Time" ].state as Number
		logDebug(logName, "Current valve {}, duration {}", currValve.name, currValveMins)

		// get the next valve in the sequence
		val nextValveNum = currValveNum + 1
		val nextValveName = "IrrigationValveZone" + nextValveNum
		val nextValve = GroupIrrigationValves.members.findFirst[valve | valve.name == nextValveName]
		
		// if there is no next valve in the sequence, then nextValve is null
		if (nextValve === null)
			logDebug(logName, "This is the last valve in the sequence")
		else
			logDebug(logName, "Next valve {}", nextValve.name)
		
		// open the current valve
		val valveOpenTime = currValveMins * coefficientFactor
		logInfo(logName, "Opening {} for {} mins", currValve.name, valveOpenTime)
		currValve.sendCommand(ON)

		IrrigationSectionRemainingTime.postUpdate(valveOpenTime.intValue)
		
		// set the timer, after expiring turn off the current valve and turn on the next one
		irrigationTimer = createTimer(now.plusMinutes(valveOpenTime.intValue), [ |
			if (nextValve !== null) {
				// this will invoke cascading valves, "Irrigation - cascading" rule
				IrrigationCurrentValve.sendCommand(nextValve.name)
			}
			else {
				logInfo(logName, "Irrigation is complete")
			}

			// let's wait for propagating item values
			Thread::sleep(500)

			// turn off current valve
			logInfo(logName, "Closing " + currValve.name)
			currValve.sendCommand(OFF)

			irrigationTimer = null
		])
	}
	catch (Exception e) {
        logError(logName, "Error controlling cascading valves: " + e)
    }
end

// for displaying remaining irrigation time purpose only
rule "Irrigation - update timer"
when
  Time cron "0 * * ? * *" // every minute
then
	if (IrrigationSectionRemainingTime.state as Number > 0)
		IrrigationSectionRemainingTime.postUpdate((IrrigationSectionRemainingTime.state as Number) - 1)
end

rule "Irrigation - all valves closed"
when
	Item GroupIrrigationValves changed to OFF
then
	// set the current valve to OFF
	logInfo(logName, "All irrigation valves closed")
	IrrigationCurrentValve.postUpdate(OFF)

	// reset the remaining time
	IrrigationSectionRemainingTime.postUpdate(0)
end

rule "Irrigation - valve updated, turn on the timer"
when
	Item GroupIrrigationValves changed 
then
	// protection against overwatering
	
	// log the state of all valves
	GroupIrrigationValves.members.forEach [valve | 
		logDebug(logName, "Irrigation valve: " + valve.name + " " + valve.state)
	]

	// a valve was turned on
	if (GroupIrrigationValves.state == ON) {
		if (IrrigationTimerMax.state == OFF) {
			// timer is not set yet, start the timer
			logDebug(logName, "Irrigation valve open, starting protection timer")
			IrrigationTimerMax.sendCommand(ON)
		}
		else {
			// the timer is already running
			logDebug(logName, "Irrigation valve open, timer already started, nothing to do")
		}
	}
	else {
		logDebug(logName, "All irrigation valves closed, stopping protection timer")
		IrrigationTimerMax.postUpdate(OFF)
	}
	triggeringItem.postUpdate(triggeringItem.state)
end

rule "Irrigation - protection timer off, close all valves"
when
	Item IrrigationTimerMax changed to OFF
then
	// protection timer expired - turn all valves off
	logWarn(logName, "Irrigation protection timer expired - close all valves")

	// close all valves from the group
	GroupIrrigationValves.members.forEach [valve | 
		logDebug(logName, "Closing valve: " + valve.name)
		valve.sendCommand(OFF)
	]
end

Sitemap

sitemap irrigation label="Irrigation"
{		
	Frame label="Status" {
		Switch item=IrrigationLock icon="lock"
		Text item=GroupIrrigationValves label="Irrigation" icon="water"
		Text item=IrrigationSectionRemainingTime visibility=[IrrigationSectionRemainingTime > 0] label="Section irrigation remaining time [%d min]" icon="time"
	}
	Frame label="Sections" {
		Switch item=IrrigationValveZone1 label="Lawn - zone 1" /*visibility=[IrrigationLock.state==OFF]*/ icon="faucet"
		Switch item=IrrigationValveZone2 label="Lawn - zone 2" icon="faucet"
		Switch item=IrrigationValveZone3 label="Lawn - zone 3" icon="faucet"
		Switch item=IrrigationValveZone4 label="Flowers & plants - dripping line" icon="faucet"
	}
	
	Frame label="Others" {
		// rainfall past 24h
		Text item=SumRainLast24h icon="rain"

		// rainfall forecast 24h
		Text item=SumRainNext24h icon="rain"

		Text label="Settings" icon="settings" {
			Frame label="Irrigation days" {
				Switch item=IrrigationDay1 mappings=[ON="Tak", OFF="Nie"]
				Switch item=IrrigationDay2 mappings=[ON="Tak", OFF="Nie"]
				Switch item=IrrigationDay3 mappings=[ON="Tak", OFF="Nie"]
				Switch item=IrrigationDay4 mappings=[ON="Tak", OFF="Nie"]
				Switch item=IrrigationDay5 mappings=[ON="Tak", OFF="Nie"]
				Switch item=IrrigationDay6 mappings=[ON="Tak", OFF="Nie"]
				Switch item=IrrigationDay7 mappings=[ON="Tak", OFF="Nie"]
			}

			Frame label="Irrigation time" {
				Selection item=IrrigationStartAtSpecificHour icon="sunrise" label="Start irrigation" mappings=["OFF"="Before sunrise", "ON"="At the specific hour"]
				
				Setpoint item=IrrigationHoursBeforeSunrise icon="time" minValue=0 maxValue=23 visibility=[IrrigationStartAtSpecificHour==OFF]

				Selection item=IrrigationStartTime visibility=[IrrigationStartAtSpecificHour==ON] mappings=[
														"0"="00:00", "1"="01:00", "2"="02:00",
														"3"="03:00", "4"="04:00", "5"="05:00", 
														"6"="06:00", "7"="07:00", "8"="08:00",
														"9"="09:00", "10"="10:00", "11"="11:00", 
														"12"="12:00", "13"="13:00", "14"="14:00",
														"15"="15:00", "16"="16:00", "17"="17:00",
														"18"="18:00", "19"="19:00", "20"="20:00", 
														"21"="21:00", "22"="22:00", "23"="23:00"
				]
			}

			Frame label="Irrigation duration" {
				Setpoint item=IrrigationValveZone1Time minValue=1 maxValue=60 step=5 icon="time"
				Setpoint item=IrrigationValveZone2Time minValue=1 maxValue=60 step=5 icon="time"
				Setpoint item=IrrigationValveZone3Time minValue=1 maxValue=60 step=5 icon="time"
				Setpoint item=IrrigationValveZone4Time minValue=1 maxValue=60 step=5 icon="time"
			}

			Frame label="Weather conditions" {
				Setpoint item=MaxAllowedRain label="Max allowed rainfall" minValue=0 maxValue=10 step=0.1 icon="rain"
				Setpoint item=MaxAllowedWindSpeed label="Max allowed wind speed" minValue=5 maxValue=150 step=5 icon="wind"
			}
		}
	}	
	Frame label="Rainfall (last 24h)" {
		Image refresh=60000 url="http://openhab:3000/render/d-solo/lPgA48Wgz/rain-current-and-forecast?tab=advanced&panelId=2&orgId=1&from=now-24h&to=now&theme=light&width=480&height=240&tz=Europe%2FWarsaw"
	}
 }

Final thoughts

Protection timer could be set in a physical device (relay) if possible - a better solution rather than software.
I’m not using any rain sensor, I just rely on the OpenWeatherMap. This is not a very accurate solution, however, it works well.
I don’t have enough knowledge yet on how to correlate the average temperature with the irrigation, so some additional research is required. I just wanted to have it already in the code.

Any ideas and comments are welcome.

3 Likes