Setting up a cron based sprinkler

I am a new user, and spent easily 40 hours learning the system to get this to work. I have a Pi Zero driving a SainSmart USB relay board. The Zero is running crelay that I recompiled to generate JSON formatted status. I’ll be submitting a pull request for that modification, and once I do, the URL for getting the status of the relay board will change slightly. If a person uses the native crelay web interface and turns on a relay, OH2 will get that status change and start a timer to turn the relay off at a later time, so the events are triggered on change in state. Please let me know if there’s more I need to add to this tutorial.

I’m using a web cache, and that’s set up in
http.cfg

http:sprinklerCache.url=http://192.168.10.142:8000/gpio
http:sprinklerCache.updateInterval=1000

The purpose of this is to fetch all relay states at once, every second. Then, each item uses that cached data.

sprinkler.items file

Switch sprinklerValve1 { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=1?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=1?status=0] <[sprinklerCache:5000:JSONPATH($.Relay1)]" }
Switch sprinklerValve2 { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=2?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=2?status=0] <[sprinklerCache:5000:JSONPATH($.Relay2)]" }
Switch sprinklerValve3 { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=3?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=3?status=0] <[sprinklerCache:5000:JSONPATH($.Relay3)]" }
Switch sprinklerValve4 { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=4?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=4?status=0] <[sprinklerCache:5000:JSONPATH($.Relay4)]" }
Switch sprinklerValve5 { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=5?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=5?status=0] <[sprinklerCache:5000:JSONPATH($.Relay5)]" }
Switch sprinklerValve6 { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=6?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=6?status=0] <[sprinklerCache:5000:JSONPATH($.Relay6)]" }
Switch sprinklerValve7 { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=7?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=7?status=0] <[sprinklerCache:5000:JSONPATH($.Relay7)]" }
Switch sprinklerValve8 { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=8?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=8?status=0] <[sprinklerCache:5000:JSONPATH($.Relay8)]" }

Each item has three commands, output to send when the virtual switch is turned off, output for when it is turned on, and status that comes back from the remote Pi web service.

mySitemap.sitemap

sitemap mySitemap label="My home automation" {
    Frame label="Sprinkler" {
        Switch item=sprinklerValve1
        Switch item=sprinklerValve2
        Switch item=sprinklerValve3
        Switch item=sprinklerValve4
        Switch item=sprinklerValve5
        Switch item=sprinklerValve6
        Switch item=sprinklerValve7
        Switch item=sprinklerValve8
    }
}

You will need to get the switches, each one representing a relay to show up in OH by adding them to your sitemap. I’m still not sure why this is a required step.

wateringSchedule.rules

import org.joda.DateTime
import org.eclipse.xtext.xbase.lib.Functions

val java.util.Map<String, Timer> sprinklerTimers = newHashMap("1"->null,"2"->null,"3"->null,"4"->null,"5"->null,"6"->null,"7"->null,"8"->null)

val org.eclipse.xtext.xbase.lib.Functions$Function4
                <SwitchItem,
                java.util.Map<String, Timer>,
                String,
                Number,
                Boolean> 
    timerLogic = [
                SwitchItem relayItem,
                java.util.Map<String,Timer> timers,
                String timerKey, 
                Number timeout |
        logInfo("Logger",relayItem.toString + " changed state");
		timers.get(timerKey)?.cancel
		timers.put(timerKey, createTimer(now.plusMinutes(timeout)) [|
            logInfo("Logger",relayItem.toString + " turning off");
			relayItem.sendCommand(OFF)
			timers.remove(timerKey)
		])
    true 
]

rule "Zone 1" when Time cron "0 0 8 1/4 * ?" then sprinklerValve1.sendCommand(ON) end
rule "Zone 2" when Time cron "0 0 9 1/4 * ?" then sprinklerValve2.sendCommand(ON) end
rule "Zone 3" when Time cron "0 0 8 2/4 * ?" then sprinklerValve3.sendCommand(ON) end
rule "Zone 4" when Time cron "0 0 9 2/4 * ?" then sprinklerValve4.sendCommand(ON) end
rule "Zone 5" when Time cron "0 0 8 3/4 * ?" then sprinklerValve5.sendCommand(ON) end
rule "Zone 6" when Time cron "0 0 9 3/4 * ?" then sprinklerValve6.sendCommand(ON) end
rule "Zone 7" when Time cron "0 0 8 4/4 * ?" then sprinklerValve7.sendCommand(ON) end
rule "Zone 8" when Time cron "0 0 9 4/4 * ?" then sprinklerValve8.sendCommand(ON) end
rule "Zone 1 timer" when Item sprinklerValve1 changed then timerLogic.apply(sprinklerValve1, sprinklerTimers,"1", 30) end 
rule "Zone 2 timer" when Item sprinklerValve2 changed then timerLogic.apply(sprinklerValve2, sprinklerTimers,"2", 30) end
rule "Zone 3 timer" when Item sprinklerValve3 changed then timerLogic.apply(sprinklerValve3, sprinklerTimers,"3", 30) end
rule "Zone 4 timer" when Item sprinklerValve4 changed then timerLogic.apply(sprinklerValve4, sprinklerTimers,"4", 30) end
rule "Zone 5 timer" when Item sprinklerValve5 changed then timerLogic.apply(sprinklerValve5, sprinklerTimers,"5", 30) end
rule "Zone 6 timer" when Item sprinklerValve6 changed then timerLogic.apply(sprinklerValve6, sprinklerTimers,"6", 30) end
rule "Zone 7 timer" when Item sprinklerValve7 changed then timerLogic.apply(sprinklerValve7, sprinklerTimers,"7", 30) end
rule "Zone 8 timer" when Item sprinklerValve8 changed then timerLogic.apply(sprinklerValve8, sprinklerTimers,"8", 30) end

I am testing the system now, but wanted to get this tutorial done while I have the motivation. I’ll update it if there are problems that need to be addressed.

Thanks for posting! It is great to get examples for stuff like this. Over all the code is pretty good but I see some areas for improvement.

You don’t need to import joda’s DateTime. It gets imported automatically for you.

There is no reason to initialize a Map with null values. When the Map doesn’t contain a given key, it returns null anyway. When you set a key with a null value it removes that key and that value from the Map. So

val java.util.Map<String, Timer> sprinklerTimers = newHashMap("1"->null,"2"->null,"3"->null,"4"->null,"5"->null,"6"->null,"7"->null,"8"->null)

is equivalent to

val java.util.Map<String, Timer> sprinklerTimers = newHashMap

in all respects.

Since you imported Functions, you do not need to use the full package name for Functions$Function4. And if you import Map you won’t have to use it’s full package name either.

Functions$Function4 
    <Switch,
      Map<String, Timer>,
...

Since you already defined the types of the arguments in the <SwitchItems, ...> section, there is no reason to repeat those types here. Or because you define the types here, there is no reason to use the <SwichItems...> part. Choose one or the other. Using both is redundant.

Either

val Functions$Function4
                <SwitchItem,
                Map<String, Timer>,
                String,
                Number,
                Boolean> 
    timerLogic = [
                relayItem,
                timers,
                timerKey, 
                timeout |

or

val Functions$Function4
    timerLogic = [
                SwitchItem relayItem,
                Map<String,Timer> timers,
                String timerKey, 
                Number timeout |

Note, this second version assumes OH 2.3 Release. Prior versions will generate a warning in the logs.

You can collapse your “Zone X timer” Rules down to one Rule.

First, put the sprinklerValveX Items into a Group, let’s call it sprinklerValves

rule "Zone timer"
when
    Member of sprinklerValves changed to ON // presumably we don't want to set the timer again when it changes to OFF
then
    val num = triggeringItem.name.substring(triggeringItem.name.length() - 1));
    timerLogic.apply(triggeringItem, sprinklerTimers, num, 30)
end

Of course, now that there is only one Rule, there really is no need for the lambda any more.

rule "Zone timer"
when
    Member of sprinklerValves changed to ON // presumably we don't want to set the timer again when it changes to OFF
then
    logInfo("Logger",triggeringItem.name + " changed state") // note that the semicolons are not required except with the `return;`
    timers.get(triggeringItem.name)?.cancel // why not just use the full Item name as the key?
    timers.put(triggeringItem.name, createTimer(now.plusMinutes(30), [ |
        logInfo("Logger",triggeringItem.name + " turning off")
        triggeringItem.sendCommand(OFF)
        timers.remove(triggeringItem.name)
    ])) // I'm not a fan of putting the lambda outside the parens, it's an argument to the method call so don't hide that fact
end

But what if I want to have variable times for each zone? There are several approaches you can use. One is you can populate a Map with the Times desired. A more flexible approach would be to put the runtime in an Item so you can change it on your sitemap. We have use Design Pattern: Associated Items for this. The key is to name the Items so we can easily construct its name based on the valve’s Item name. Let’s say we append “_Runtime” to the valve name for the Number Item’s name that stores the amount of time for the zone to Run.

NOTE: There isn’t a good way to give the Item an initial value. See Design Pattern: Encoding and Accessing Values in Rules for details on how to boot strap the values stored in the _Runtime Items.

Group:Switch sprinklerValves
Switch sprinklerValve1 (sprinklerValves) { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=1?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=1?status=0] <[sprinklerCache:5000:JSONPATH($.Relay1)]" }
Number sprinklerValve1_Runtime "Valve 1's Runtime [%d]"
Switch sprinklerValve2 (sprinklerValves) { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=2?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=2?status=0] <[sprinklerCache:5000:JSONPATH($.Relay2)]" }
Number sprinklerValve2_Runtime "Valve 2's Runtime [%d]"
...

You can use Setpoint on the sitemap so you can adjust the Runtime for each valve from your sitemap. You can even set a min ana a max

sitemap mySitemap label="My home automation" {
    Frame label="Sprinkler" {
        Switch item=sprinklerValve1
        Setpoint item=sprinklerValve1_Runtime minValue=1 maxValue=59
...

I’ll use the Item Registry example from Associated Items DP (I’ll post the full set of Rules).

import org.eclipse.smarthome.model.script.ScriptServiceUtil
import org.eclipse.xtext.xbase.lib.Functions
import java.util.Map

val Map<String, Timer> sprinklerTimers = newHashMap

rule "Zone 1" when Time cron "0 0 8 1/4 * ?" then sprinklerValve1.sendCommand(ON) end
rule "Zone 2" when Time cron "0 0 9 1/4 * ?" then sprinklerValve2.sendCommand(ON) end
rule "Zone 3" when Time cron "0 0 8 2/4 * ?" then sprinklerValve3.sendCommand(ON) end
rule "Zone 4" when Time cron "0 0 9 2/4 * ?" then sprinklerValve4.sendCommand(ON) end
rule "Zone 5" when Time cron "0 0 8 3/4 * ?" then sprinklerValve5.sendCommand(ON) end
rule "Zone 6" when Time cron "0 0 9 3/4 * ?" then sprinklerValve6.sendCommand(ON) end
rule "Zone 7" when Time cron "0 0 8 4/4 * ?" then sprinklerValve7.sendCommand(ON) end
rule "Zone 8" when Time cron "0 0 9 4/4 * ?" then sprinklerValve8.sendCommand(ON) end

rule "Zone timer"
when
    Member of sprinklerValves changed to ON
then
    logInfo("Logger",triggeringItem.name + " changed state")
    timers.get(triggeringItem.name)?.cancel

    // Get the runtime for this valve
    val runtime = (ScriptServiceUtil.getItemRegistry.getItem(triggeringItem.name+"_Runtime").state as Number).intValue

    timers.put(triggeringItem.name, createTimer(now.plusMinutes(runtime), [ |
        logInfo("Logger",triggeringItem.name + " turning off")
        triggeringItem.sendCommand(OFF)
        timers.remove(triggeringItem.name)
    ]))
end

Wow. This is amazing feedback, and I’m very grateful.

After reading tons of posts, what you see here is essentially me duplicating what I’ve seen elsewhere.

Okay, got it. These changes are accepted without warnings.

I am running OH 2.3.0-1. I did try it several ways. Your first suggestion is parsed without warnings. If I attempt your second format, I get:

2018-08-02 16:13:35.579 [INFO ] [el.core.internal.ModelRepositoryImpl] - Validation issues found in configuration model 'wateringSchedule.rules', using it anyway:

Function4 is a raw type. References to generic type Function4<P1, P2, P3, P4, Result> should be parameterized

2018-08-02 16:13:35.616 [INFO ] [el.core.internal.ModelRepositoryImpl] - Loading model 'wateringSchedule.rules'

Thanks for the refactoring advice.

I’ll make the changes and reply back here.

I know what the difference is. Looking back at the Xtext docs you don’t need the Functions$Function4. The language will figure that part out for you. But if you do list it, you have to add the < > with the types of the arguments. So you should be able to use:

val timerLogic = [ 
                SwitchItem relayItem,
                Map<String,Timer> timers,
                String timerKey, 
                Number timeout |
...

should work without warning. If it does, then you don’t need the import of Functions any longer either.

You are correct. It does not complain when I rip all of that boilerplate out. Thanks.

Here is my final solution. I am going to add more to it to show time remaining, but for the sake of the tutorial, I think the bare minimum is better. Who knows, if I figure out how to get the OpenSprinkler to work for me I may go that route.

I have submitted a pull request for crelay to include the JSON URL response that works with this tutorial.

I am also using MAPDB to persist my settings across reboot, which is not shown here, but it’s trivial to set up, and there are other tutorials just for that. Persistence is not required for this tutorial to work.

http.cfg

http:sprinklerCache.url=http://192.168.10.142:8000/json
http:sprinklerCache.updateInterval=5000

mySitemap.sitemap

sitemap mySitemap label="My home automation" {
    Frame label="Sprinkler" {
        Switch item=sprinklerValve1 label="1:By Runway"
        Setpoint item=sprinklerValve1_Runtime minValue=1 maxValue=59
        Switch item=sprinklerValve2 label="2:not close to runway"
        Setpoint item=sprinklerValve2_Runtime minValue=1 maxValue=59
        Switch item=sprinklerValve3 label="3:closer to hangar"
        Setpoint item=sprinklerValve3_Runtime minValue=1 maxValue=59
        Switch item=sprinklerValve4 label="4:by porch"
        Setpoint item=sprinklerValve4_Runtime minValue=1 maxValue=59
        Switch item=sprinklerValve5 label="5:not connected"
        Setpoint item=sprinklerValve5_Runtime minValue=1 maxValue=59
        Switch item=sprinklerValve6 label="6:broken-big garden ring"
        Setpoint item=sprinklerValve6_Runtime minValue=1 maxValue=59
        Switch item=sprinklerValve7 label="7:over septic field"
        Setpoint item=sprinklerValve7_Runtime minValue=1 maxValue=59
        Switch item=sprinklerValve8 label="8:front lawn"
        Setpoint item=sprinklerValve8_Runtime minValue=1 maxValue=59
    }
}

sprinkler.items

Group:Switch sprinklerValves
Switch sprinklerValve1 (sprinklerValves) { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=1?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=1?status=0] <[sprinklerCache:5000:JSONPATH($.Relay1)]" }
Switch sprinklerValve2 (sprinklerValves) { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=2?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=2?status=0] <[sprinklerCache:5000:JSONPATH($.Relay2)]" }
Switch sprinklerValve3 (sprinklerValves) { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=3?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=3?status=0] <[sprinklerCache:5000:JSONPATH($.Relay3)]" }
Switch sprinklerValve4 (sprinklerValves) { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=4?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=4?status=0] <[sprinklerCache:5000:JSONPATH($.Relay4)]" }
Switch sprinklerValve5 (sprinklerValves) { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=5?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=5?status=0] <[sprinklerCache:5000:JSONPATH($.Relay5)]" }
Switch sprinklerValve6 (sprinklerValves) { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=6?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=6?status=0] <[sprinklerCache:5000:JSONPATH($.Relay6)]" }
Switch sprinklerValve7 (sprinklerValves) { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=7?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=7?status=0] <[sprinklerCache:5000:JSONPATH($.Relay7)]" }
Switch sprinklerValve8 (sprinklerValves) { http=">[ON:GET:http://192.168.10.142:8000/gpio?pin=8?status=1] >[OFF:GET:http://192.168.10.142:8000/gpio?pin=8?status=0] <[sprinklerCache:5000:JSONPATH($.Relay8)]" }

Number sprinklerValve1_Runtime "1: Runtime [%d]"
Number sprinklerValve2_Runtime "2: Runtime [%d]"
Number sprinklerValve3_Runtime "3: Runtime [%d]"
Number sprinklerValve4_Runtime "4: Runtime [%d]"
Number sprinklerValve5_Runtime "5: Runtime [%d]"
Number sprinklerValve6_Runtime "6: Runtime [%d]"
Number sprinklerValve7_Runtime "7: Runtime [%d]"
Number sprinklerValve8_Runtime "8: Runtime [%d]"

wateringschedule.rules*

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

val java.util.Map<String, Timer> sprinklerTimers = newHashMap

rule "Sprinkler timer shutoff delay"
when
    // There's benefit to being looser on this trigger, 
    // because during testing, if the valve is already on due to some strange precondition, 
    // a timer will not get set.
    Member of sprinklerValves changed to ON
then
    val runtime = (ScriptServiceUtil.getItemRegistry.getItem(triggeringItem.name+"_Runtime").state as Number).intValue
    logInfo("Logger",triggeringItem.name + " changed state.  Setting timer for " + triggeringItem.name + " : " + runtime + " minutes")

    sprinklerTimers.get(triggeringItem.name)?.cancel
    sprinklerTimers.put(triggeringItem.name, createTimer(now.plusMinutes(runtime), [ |
        logInfo("Logger",triggeringItem.name + " turning off")
        triggeringItem.sendCommand(OFF)
        sprinklerTimers.remove(triggeringItem.name) // I'm not sure why we need to remove the timer
    ]))
end

rule "Zone 1" when Time cron "0 0 8 1/3 * ?" then sprinklerValve1.sendCommand(ON) end
rule "Zone 2" when Time cron "0 0 9 1/3 * ?" then sprinklerValve2.sendCommand(ON) end
rule "Zone 3" when Time cron "0 0 8 2/3 * ?" then sprinklerValve3.sendCommand(ON) end
rule "Zone 4" when Time cron "0 0 9 2/3 * ?" then sprinklerValve4.sendCommand(ON) end
rule "Zone 7" when Time cron "0 0 8 3/3 * ?" then sprinklerValve7.sendCommand(ON) end
rule "Zone 8" when Time cron "0 0 9 3/3 * ?" then sprinklerValve8.sendCommand(ON) end

Most of the time the existence of the Timer is an indication that an action is still occurring. So we remove the Timer from the Map as an indication that the activity is done. Therefore, in other rules you can

if(sprinklerTimers.get("sprinklerValve1") !== null)
    // Valve1 is on

You don’t strictly need it in this set of rules but I always null out the timer it of habit and frankly every time I use timers I have other rules that care whether the timer is running.

I worry that there’s an edge case where a valve could be ON, but no timers are set to turn it off. Or, that the remote controller went offline while it was supposed to receive the command. In my mind, there’s not a way this could happen, because if OH has the state of a valve to be OFF, and then discovers that it’s on from the remote controller, then a timer will be created, causing it to then change to OFF after the timeout. Perhaps the risk is during initialization. If the valve is ON, when OH is initialized, that would also trigger a timer. What about if mapdb restores a valve to ON at initialization. Would that start a timer?
I’m on well water, and for some reason in the last few weeks the family has forgotten to turn off hoses outside, draining the cistern. I’ll be installing a flow meter, and using that as a trigger in OH if thresholds are exceeded. But for now, I worry about a valve staying open. It’s not the end of the world if it happens. There’s a low switch that shuts off the water when it reaches 200 gallons, which is plenty of water to last while the problem is fixed.

I’ve taken this approach

I’ll be installing a flow meter, and using that as a trigger in OH if thresholds are exceeded.

If the sprinkler state is offering and there is high water flow into the sprinkler system then there is a fault. Positive feedback.

The flow meter is £7 from the banggood.

The restoreOnStartup “should” trigger the Rule as the state changes from NULL to ON. However, timing can become an issue here if the Rules do not start executing until well after the restoreOnStartup occurred.

I would create a System started Rule that sends an OFF command to all the valves and let OH start in a known good state. You can generate an alert at this point and manually turn the valves back on if you need to.

Thanks. I was thinking about counting pulses with a Pi or ESP32. How did you interface your meter?

I used a purpose-built wemos for it. I found some code on stack-overflow (is that now how all good things start?) and now have it sending back L/m flow-rate to me via mqtt.
I can post the code when I get back home.


this is the one I used. £4, not £7 :slight_smile: