I have a 50m2 garden with sprinklers and drip irrigation. Planning and installation were done by my gardener (approx cost 360€ for 4x sprinklers, 3x electrovalves and their box, 30m drip irrigation, other tubes and installation).
I’ve built the controller (approx cost 50€) using these components:
- 1x Sonoff 4CH Pro R2
- 1x 24VAC transformer
- 1x ip65 box
The above costs do not include water socket and wifi infrastructure. I already had them in place. Water pressure is 3bar so it was possible to group 2 sprinklers per electrovalve. My connection scheme to sonoff relays is:
- One relay manages the 24VAC transformer. This is mainly to enlarge its usefull life but it also has a small benefit in the electricity bill
- Another relay manages the electrovalve connected to the drip irrigation
- The remainning two relays manage one electrovalve each, and each of these valves connect to two sprinklers
Scheme
The box is installed in the garage (valves are in another box under the ground, behind the wall shown in the picture)
I choose Sonoff 4CH Pro because of its ability to manage different voltages in each relay. I’ve flashed it with Tasmota. One very interesting Tasmota parameter is PulseTime
. I use it to limit irrigation times even if wifi goes down.
Usually irrigation is done daily at sunrise. Of course I do not want to waste water so I use the following logic in my rules:
- When it rains irrigation is interrupted (if active). If it rains too much the interval between irrigations can be enlarged by up to 3 days
- Irrigation can be delayed or performed in half-time depending on humidity
- If it’s too windy irrigation is done in the drip system only (I use sprinklers for the grass and it does not require daily irrigation)
- Irrigation will not take place if there is a rain forecast. Of course forecasts can be false, so if it does not rain for long I will force an irrigation
I am fortunate that OpenWheather produces reliable forecasts for my location, otherwise I would have to acquire sensors (rain, and probably wind).
Therefore my setup requires the following add-ons:
- Astro - to determine sunrise
- MQTT - to drive the Sonoff
- OpenWheatherMap - for rain/wind/humidity actuals and forecasts
- MapDB - to keep critical info whenever I need to restart OpenHab
- RRDj4 - to calculate accumulated rain
So here are my OH3 control files:
Version 1
astro.things
astro:sun:local "Posição do Sol" [geolocation="40.638900729572036,-8.648887092899434,30", interval=60]
bridge.things
//
// MQTT
//
Bridge mqtt:broker:aveiro "$Controller MQTT" [
host="192.168.130.1",
port=1883,
secure="AUTO",
qos=0,
retain=true,
keep_alive_time=30000,
reconnect_time=60000,
username="*****",
password="****"
]
//
// OPenWheatherMap
//
Bridge openweathermap:weather-api:api "$OpenWeatherMap" [apikey="****", refreshInterval=120, language="pt"]
relay.things
//
// Sonoff
//
Thing mqtt:topic:sonoff_rega "Rega do Quintal" (mqtt:broker:aveiro) {
Channels:
Type switch : zona1 "Zona 1" [stateTopic="quintal/stat/rega/POWER1", commandTopic="quintal/cmnd/rega/POWER1"]
Type string : zona1p "Tempo 1" [commandTopic="quintal/cmnd/rega/PulseTime1"]
Type switch : zona2 "Zona 2" [stateTopic="quintal/stat/rega/POWER2", commandTopic="quintal/cmnd/rega/POWER2"]
Type string : zona2p "Tempo 2" [commandTopic="quintal/cmnd/rega/PulseTime2"]
Type switch : zona3 "Zona 3" [stateTopic="quintal/stat/rega/POWER3", commandTopic="quintal/cmnd/rega/POWER3"]
Type string : zona3p "Tempo 3" [commandTopic="quintal/cmnd/rega/PulseTime3"]
Type switch : power "Transformador 24V" [stateTopic="quintal/stat/rega/POWER4", commandTopic="quintal/cmnd/rega/POWER4"]
Type string : powerp "Tempo limite" [commandTopic="quintal/cmnd/rega/PulseTime4"]
}
In OH3 it’s preferrable to define things using the Basic UI. In this case here is an example
UID: mqtt:topic:aveiro:sonoff_rega
label: Rega do Quintal
thingTypeUID: mqtt:topic
configuration:
payloadNotAvailable: Offline
availabilityTopic: tele/rega/LWT
payloadAvailable: Online
bridgeUID: mqtt:broker:aveiro
channels:
- id: zona1
channelTypeUID: mqtt:switch
label: Zona 1
description: ""
configuration:
commandTopic: cmnd/rega/POWER1
stateTopic: stat/rega/POWER1
- id: zona2
channelTypeUID: mqtt:switch
label: Zona 2
description: ""
configuration:
commandTopic: cmnd/rega/POWER2
stateTopic: stat/rega/POWER2
- id: zona3
channelTypeUID: mqtt:switch
label: Zona 3
description: ""
configuration:
commandTopic: cmnd/rega/POWER3
stateTopic: stat/rega/POWER3
- id: zona4
channelTypeUID: mqtt:switch
label: Zona 4
description: ""
configuration:
commandTopic: cmnd/rega/POWER4
stateTopic: stat/rega/POWER4
- id: zon1p
channelTypeUID: mqtt:number
label: Pulsetime 1
description: null
configuration:
commandTopic: cmnd/rega/PulseTime1
stateTopic: stat/rega/RESULT
transformationPattern: REGEX:(.*PulseTime1.*)∩JSONPATH:$.PulseTime1.Set
- id: zon2p
channelTypeUID: mqtt:number
label: Pulsetime 2
description: null
configuration:
commandTopic: cmnd/rega/PulseTime2
stateTopic: stat/rega/RESULT
transformationPattern: REGEX:(.*PulseTime2.*)∩JSONPATH:$.PulseTime2.Set
- id: zon3p
channelTypeUID: mqtt:number
label: Pulsetime 3
description: null
configuration:
commandTopic: cmnd/rega/PulseTime3
stateTopic: stat/rega/RESULT
transformationPattern: REGEX:(.*PulseTime3.*)∩JSONPATH:$.PulseTime3.Set
- id: zon4p
channelTypeUID: mqtt:number
label: Pulsetime 4
description: null
configuration:
commandTopic: cmnd/rega/PulseTime4
stateTopic: stat/rega/RESULT
transformationPattern: REGEX:(.*PulseTime4.*)∩JSONPATH:$.PulseTime4.Set
irrigation.items
//
// Parâmetros para rega automática
//
Group gRega "Rega" <lawnmower> (OU_Terrace)
Group gIrrigation
DateTime OU_Irrigation_Last "Last irrigation (or rain) [%1$td/%1$tm %1$tR]" <calendar> (gRega)
DateTime OU_Irrigation_Min "Irrigation disabled before [%1$td/%1$tm %1$tR]" <calendar> (gRega)
DateTime OU_Irrigation_Max "Irrigation mandatory after [%1$td/%1$tm %1$tR]" <calendar> (gRega)
//
// Comando da rega
//
Switch OU_Terrace_Irrigation_Power "Transformer [%s]" (gIrrigation,gRega) {channel="mqtt:topic:sonoff_rega:power"}
String OU_Terrace_Irrigation_Power_Time "Tempo" {channel="mqtt:topic:sonoff_rega:powerp"}
Switch OU_Terrace_Irrigation_Zone1 "Drip system" <faucet> (gIrrigation,gRega) {channel="mqtt:topic:sonoff_rega:zona1"}
String OU_Terrace_Irrigation_Zone1_Time "Tempo" {channel="mqtt:topic:sonoff_rega:zona1p"}
Switch OU_Terrace_Irrigation_Zone2 "Sprinklers NW" <faucet> (gIrrigation,gRega) {channel="mqtt:topic:sonoff_rega:zona2"}
String OU_Terrace_Irrigation_Zone2_Time "Tempo" {channel="mqtt:topic:sonoff_rega:zona2p"}
Switch OU_Terrace_Irrigation_Zone3 "Sprinklers SO" <faucet> (gIrrigation,gRega) {channel="mqtt:topic:sonoff_rega:zona3"}
String OU_Terrace_Irrigation_Zone3_Time "Tempo" {channel="mqtt:topic:sonoff_rega:zona3p"}
//
// Meteorologia
//
Group:Number:AVG gRain (Home)
Number OU_Weather_Actual_Rain "Actual rain" <rain> (gIrrigation) {channel="openweathermap:weather-and-forecast:api:local:current#rain"}
Number OU_Weather_Actual_Rain_avg "Average rain [%.2f mm]" <rain> (gRega)
Number OU_Weather_Prev_Rain_3h "Rain forecats 3h" <rain> (gIrrigation, gRain) {channel="openweathermap:weather-and-forecast:api:local:forecastHours03#rain"}
Number OU_Weather_Prev_Rain_6h "Rain forecats 6h" <rain> (gIrrigation, gRain) {channel="openweathermap:weather-and-forecast:api:local:forecastHours06#rain"}
Number OU_Weather_Prev_Rain_9h "Rain forecats 9h" <rain> (gIrrigation, gRain) {channel="openweathermap:weather-and-forecast:api:local:forecastHours09#rain"}
Number OU_Weather_Prev_Rain_12h "Rain forecats 12h" <rain> (gIrrigation, gRain) {channel="openweathermap:weather-and-forecast:api:local:forecastHours12#rain"}
Number OU_Weather_Prev_Rain_15h "Rain forecats 15h" <rain> (gIrrigation, gRain) {channel="openweathermap:weather-and-forecast:api:local:forecastHours15#rain"}
Number OU_Weather_Actual_Wind "Actual wind" <rain> (gIrrigation) {channel="openweathermap:weather-and-forecast:api:local:current#wind-speed"}
Switch OU_Weather_Actual_Rain_high
Number OU_Weather_Prev_Humidity "Humidity forecast" <humidity> (gIrrigation) {channel="openweathermap:weather-and-forecast:api:local:forecastHours06#humidity"}
mapdb.persist
//
// Requer persistence MapDB
//
Strategies {
default = everyUpdate
}
Items {
OU_Irrigation_Last : strategy = everyUpdate, restoreOnStartup
OU_Irrigation_Min : strategy = everyUpdate, restoreOnStartup
OU_Irrigation_Max : strategy = everyUpdate, restoreOnStartup
}
rrdj4.persist
//
// Requer persistence rrd4j
//
Strategies {
// for rrd charts, we need a cron strategy
everyMinute : "0 * * * * ?"
}
Items {
// persist items on every change and every minute
OU_Weather_Actual_Rain : strategy = everyMinute
}
The switching sequence is the following:
- If it’s too windy Relay 4 → Relay 1
- Otherwise Relay 4 → Relay 2 → Relay 3 → Relay 1
Garden.rules
rule "Calculate average rain 2h"
when
Time cron "0 0/15 * 1/1 * ? *"
then
OU_Weather_Actual_Rain_avg.postUpdate(OU_Weather_Actual_Rain.averageSince(now().minusMinutes(120),"rrd4j"))
end
rule "Sunrise irrigation"
when
// for test purposes Time cron "0 11 16 ? * * *" or
Channel 'astro:sun:local:rise#event' triggered START or // Normally astro binding is available
Time cron "0 30 8 ? * * *" // But if it fails irrigation will take place @ 08:30
then
// Decide if irrigation will take place in this cycle
var decision = "2" // 0=none 1=drop only 2=full
val Number n_now = now.toInstant.toEpochMilli
val Number n_min = (OU_Irrigation_Min.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli
val Number n_max = (OU_Irrigation_Max.state as DateTimeType).zonedDateTime.toInstant.toEpochMilli
if (n_now >= n_max) { // Force irrigation if last irrigation done too long ago
decision = "2"
} else if (n_now <= n_min) { // No need to irrigate, last irrigation was very recent
decision = "0"
} else if (gRain.state > 0) { // No need to irrigate, it's going to rain
decision = "0"
} else if (OU_Weather_Prev_Humidity.state >= 95) { // No need to irrigate, very high humidity prevision
decision = "0"
} else if (OU_Weather_Actual_Wind.state > 5) { // It's too windy, let's irrigate drop only
decision = "1"
}
if(decision != "0") {
// Decide irrigation time based on forecasted humidity
// PulseTime = 100 + 60 * minutes
if (OU_Weather_Prev_Humidity.state >= 80){ // Irrigation times for high humidity
OU_Terrace_Irrigation_Zone1_Time.sendCommand(250) // PulseTime 2.5 minutes
OU_Terrace_Irrigation_Zone2_Time.sendCommand(400) // PulseTime 5 minutes
OU_Terrace_Irrigation_Zone3_Time.sendCommand(400) // PulseTime 5 minutes
OU_Terrace_Irrigation_Power_Time.sendCommand(880) // PulseTime 13 minutes
} else { // Irrigation times for low humidity
OU_Terrace_Irrigation_Zone1_Time.sendCommand(400) // PulseTime 5 minutes
OU_Terrace_Irrigation_Zone2_Time.sendCommand(700) // PulseTime 10 minutes
OU_Terrace_Irrigation_Zone3_Time.sendCommand(700) // PulseTime 10 minutes
OU_Terrace_Irrigation_Power_Time.sendCommand(1630) // PulseTime 25.5 minutes
}
// Start irrigation cycle
OU_Terrace_Irrigation_Power.sendCommand(ON) // Switch on transformer
createTimer(now().plusSeconds(1), [ | // Wait 1 sec before starting irrigation
if(decision == "1") {
OU_Terrace_Irrigation_Zone1.sendCommand(ON) // turn on valve for drip irrigation
} else {
OU_Terrace_Irrigation_Zone2.sendCommand(ON) // turn on valve for North East sprinklers
val String a1 = now.toInstant.toString
OU_Irrigation_Last.postUpdate(a1) // set irrigation datetime
}
])
}
end
rule "South West sprinklers"
when
Item OU_Terrace_Irrigation_Zone2 changed from ON to OFF
then
if (OU_Terrace_Irrigation_Power.state == ON){
OU_Terrace_Irrigation_Zone3.sendCommand(ON) // turn on valve 3
}
end
rule "Drip irrigation"
when
Item OU_Terrace_Irrigation_Zone3 changed from ON to OFF
then
if (OU_Terrace_Irrigation_Power.state == ON){
OU_Terrace_Irrigation_Zone1.sendCommand(ON) // turn on valve 1
}
end
rule "Stop irrigation"
when
Item OU_Weather_Actual_Rain changed or
Item OU_Terrace_Irrigation_Zone1 changed from ON to OFF
then
// Set irrigation control variables
if(OU_Terrace_Irrigation_Power.state == ON) {
OU_Weather_Actual_Rain_high.sendCommand(OFF)
val a2 = new DateTimeType(now().plusHours(22).toInstant.toString)
OU_Irrigation_Min.postUpdate(a2) // set low interval for next irrigation
val a3 = new DateTimeType(now().plusHours(70).toInstant.toString)
OU_Irrigation_Max.postUpdate(a3) // set high interval for next irrigation
} else if (OU_Weather_Actual_Rain_avg.state >= 0.1 || OU_Weather_Actual_Rain_high.state == ON) {
OU_Weather_Actual_Rain_high.sendCommand(ON)
val String a1 = now.toInstant.toString
OU_Irrigation_Last.postUpdate(a1) // set irrigation datetime
val a2 = new DateTimeType(now().plusHours(70).toInstant.toString)
OU_Irrigation_Min.postUpdate(a2) // set low interval for next irrigation
val a3 = new DateTimeType(now().plusHours(118).toInstant.toString)
OU_Irrigation_Max.postUpdate(a3) // set high interval for next irrigation
} else {
val String a1 = now.toInstant.toString
OU_Irrigation_Last.postUpdate(a1) // set irrigation datetime
val a2 = new DateTimeType(now().plusHours(46).toInstant.toString)
OU_Irrigation_Min.postUpdate(a2) // set low interval for next irrigation
val a3 = new DateTimeType(now().plusHours(94).toInstant.toString)
OU_Irrigation_Max.postUpdate(a3) // set high interval for next irrigation
}
OU_Terrace_Irrigation_Power.sendCommand(OFF)
end
</details>