Automating your garden hose for fun and profit
Building a custom self-hosted MQTT water shutoff and controlling it with HomeAssistant
DIY, Home Automation
Table of Contents
I love gardening - over the last couple years it’s become a great summer pasttime for me. And after a backyard revamp, I’m planning a massive flower garden to create my own little oasis.
One of the parts of gardening that I’m not the biggest fan of is watering. Don’t get me wrong - it’s relaxing. But in small doses. And a big garden demands a lot of watering on a set schedule, which is often hard to do. The first part of the solution was to lay down some soaker hose throughout the garden, which works wonderfully, with no more having to point a hose for several minutes at each part of the garden. Once the whole thing is finalized I’ll install some permanent watering solutions, but for now this does the job.
But turning it on and off at the tap was hardly satisfying. I’ve already got HomeAssistant set up to automate various lights around my house and implement voice control, so why not build something to automate watering the garden?
The first thing I did is check for any premade solutions. And while there’s plenty of controllers out there, it doesn’t look like any of them support HomeAssistant. So I endeavoured to build my own instead - and the controller can be used both for a temporary setup or a permanent one.
Getting some parts
I knew I’d need a couple things for this setup to work:
- A powered water valve/solenoid of some kind.
- A controller board.
- 24/7 off-grid power.
- Connectivity to HomeAssistant.
The first option was actually the hardest to figure out, but I did find the following on Amazon: Water Solenoid Valve. It’s a little pricey, but it’s the cheapest one.
The remaining items were fairly standard: a standard Arduino-compatible 12-volt relay to control the valve, a NodeMCU for control allowing me to use MQTT to connect to HomeAssistant, and a 4.7AH 12-volt lead-acid battery and solar panel allows for continuous off-grid operation. I needed a 12-volt to 5-volt converter to power the NodeMCU (and relays) off the 12-volt battery as well which was fairly easy to come by.
Building the water distributor
The solenoid has 1/2" diameter threads, so that also meant I needed some adapters from the standard hose ends to match up with the valve. Luckily, my local Home Depot had (almost) all the parts I needed. The resulting loop is laid out as follows:
_______________
| ___ ___ |
| | | | | |
| | | | | |
|V| | | |V|
| | | | | |
| | | | | |
B S A
Where S is the incoming water source, V is a solenoid valve, and A/B are the outputs to hoses.
I did have to improvise a bit since 1/2" threaded female T connectors weren’t available for me, but the 1/2" 90° male-to-female elbows, as well as hose reducing pieces were. Putting it all together worked perfectly with no leaks allowing the next stage to begin.
Setting up the circuitry
The circuit itself is fairly simple. The NodeMCU digital 1 and 2 pins connect to the two relay controllers, with 12-volt lines coming from the battery into each relay and out to the solenoid valves, wired with the “signal applies power” poles of the relay. Connecting it all to the battery and solar panel gave the full circuit.
Power-on testing was a great success, and the next step was programming the Node MCU.
NodeMCU programming
The code for the NodeMCU I wrote originally for a door lock device that never succeeded, but I was able to quickly repurpose it for this task. The main input comes via MQTT, with one topic per relay, and output goes out a similar set of topics to return state.
The full code is as follows and can be easily scaled to many more valves if needed. The onboard LED is used as a quick debug for operations.
HoseController.ino
#include <PubSubClient.h>
#include <ESP8266WiFi.h>
/*
* Garden hose WiFi control module
*/
// D0 - 16
#define BUILTIN_LED 16
// D1 - 5
#define RELAY_A_PIN 5
// D2 - 4
#define RELAY_B_PIN 4
// D3 - 0
// D5 - 14
// D6 - 12
// Control constants
const char* mqtt_server = "my.mqtt.server";
const int mqtt_port = 1883;
const char* mqtt_user = "myuser";
const char* mqtt_password = "mypass";
const char mqtt_control_topic[16] = "hose-control";
const char mqtt_state_topic[16] = "hose-state";
const char* wifi_ssid = "myssid";
const char* wifi_psk = "mypsk";
char mqtt_topic[32];
char topic_A[32];
char topic_B[32];
int getRelayPin(char* relay_string) {
char relay = relay_string[0];
int pin;
switch (relay) {
case 'a':
pin = RELAY_A_PIN;
break;
case 'b':
pin = RELAY_B_PIN;
break;
default:
pin = BUILTIN_LED;
break;
}
return pin;
}
WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;
char msg[50];
int value = 0;
int state = 0; // 0 = unlocked, 1 = locked
int last_state = 0; // 0 = unlocked, 1 = locked
void setup() {
pinMode(RELAY_A_PIN, OUTPUT); // RELAY A pin
pinMode(RELAY_B_PIN, OUTPUT); // RELAY B pin
pinMode(BUILTIN_LED, OUTPUT); // LED pin
digitalWrite(RELAY_A_PIN, HIGH); // Turn off relay
digitalWrite(RELAY_B_PIN, HIGH); // Turn off relay
digitalWrite(BUILTIN_LED, LOW); // Turn on LED
Serial.begin(9600); // Start serial console
setup_wifi(); // Connect to WiFi
client.setServer(mqtt_server, mqtt_port); // Connect to MQTT broker
client.setCallback(callback);
}
void relay_on(char* relay) {
int pin = getRelayPin(relay);
digitalWrite(pin, LOW);
}
void relay_off(char* relay) {
int pin = getRelayPin(relay);
digitalWrite(pin, HIGH);
}
void setup_wifi() {
delay(10);
// We start by connecting to a WiFi network
Serial.println();
Serial.print("Connecting to ");
Serial.println(wifi_ssid);
WiFi.begin(wifi_ssid, wifi_psk);
while (WiFi.status() != WL_CONNECTED) {
digitalWrite(BUILTIN_LED, HIGH); // Turn off LED
delay(250);
digitalWrite(BUILTIN_LED, LOW); // Turn on LED
delay(250);
}
Serial.println("");
Serial.println("WiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");
String command;
for (int i = 0; i < length; i++) {
command.concat((char)payload[i]);
}
Serial.print(command);
Serial.println();
// Get the specific topic
String relay_str = getValue(topic, '/', 1);
char relay[8];
relay_str.toCharArray(relay, 8);
strcpy(mqtt_topic, mqtt_state_topic);
strcat(mqtt_topic, "/");
strcat(mqtt_topic, relay);
// Blink LED for debugging
digitalWrite(BUILTIN_LED, HIGH); // Turn off LED
delay(250);
digitalWrite(BUILTIN_LED, LOW); // Turn on LED
// Either enable or disable the relay
if ( command == "on" ) {
relay_on(relay);
Serial.println(String(relay) + ": ON");
client.publish(mqtt_topic, "on");
} else {
relay_off(relay);
Serial.println(String(relay) + ": OFF");
client.publish(mqtt_topic, "off");
}
}
void reconnect() {
// Loop until we're reconnected
while (!client.connected()) {
Serial.print("Attempting MQTT connection...");
// Attempt to connect
if (client.connect("hose", mqtt_user, mqtt_password)) {
Serial.println("connected");
digitalWrite(BUILTIN_LED, HIGH); // Turn on LED
// ... and resubscribe
strcpy(topic_A, mqtt_control_topic);
strcat(topic_A, "/");
strcat(topic_A, "a");
strcpy(topic_B, mqtt_control_topic);
strcat(topic_B, "/");
strcat(topic_B, "b");
client.subscribe(topic_A);
client.subscribe(topic_B);
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 4 seconds");
// Wait 4 seconds before retrying
digitalWrite(BUILTIN_LED, HIGH); // Turn off LED
delay(1000);
digitalWrite(BUILTIN_LED, LOW); // Turn on LED
delay(1000);
digitalWrite(BUILTIN_LED, HIGH); // Turn off LED
delay(1000);
digitalWrite(BUILTIN_LED, LOW); // Turn on LED
delay(1000);
}
}
}
void loop() {
if (!client.connected()) {
reconnect();
digitalWrite(BUILTIN_LED, LOW); // Turn on LED
}
client.loop();
delay(1000);
}
String getValue(String data, char separator, int index)
{
int found = 0;
int strIndex[] = { 0, -1 };
int maxIndex = data.length() - 1;
for (int i = 0; i <= maxIndex && found <= index; i++) {
if (data.charAt(i) == separator || i == maxIndex) {
found++;
strIndex[0] = strIndex[1] + 1;
strIndex[1] = (i == maxIndex) ? i+1 : i;
}
}
String string = found > index ? data.substring(strIndex[0], strIndex[1]) : "";
return string;
}
Controlling the hose with HomeAssistant
With the outdoor box portion completed and running as expected in response to MQTT messages, the next step was configuring HomeAssistant to talk to it. I ended up wasting a bunch of time trying to get a useful UI set up before realizing that HomeAssistant has an awesome feature: the MQTT switch component, which makes adding a switch UI element for the hose that actually works easy! Here is the configuration:
configuration.yaml
switch:
- platform: mqtt
name: "Hose A"
state_topic: "hose-state/a"
command_topic: "hose-control/a"
payload_on: "on"
payload_off: "off"
state_on: "on"
state_off: "off"
optimistic: false
qos: 0
retain: true
- platform: mqtt
name: "Hose B"
state_topic: "hose-state/b"
command_topic: "hose-control/b"
payload_on: "on"
payload_off: "off"
state_on: "on"
state_off: "off"
optimistic: false
qos: 0
retain: true
Has it rained recently?
The next step in the automtion was to set up a timer to turn on and off the water for me automatically. The easiest solution is simply to run it every night, but that’s not ideal if we’ve had rain recently! I thought about a number of ways to get this information within HomeAssistant, and came up with the following:
- The built-in
yr
component provides excellent weather information from yr.no, including exposing a number of optional conditions. I enabled it with all of them:
configuration.yaml
sensor:
# Weather for Burlington (a.k.a. home)
- name: Burlington
platform: yr
monitored_conditions:
- temperature
- symbol
- precipitation
- windSpeed
- pressure
- windDirection
- humidity
- fog
- cloudiness
- lowClouds
- mediumClouds
- highClouds
- dewpointTemperature
- From this basic weather information, I built up a set of statistics to obtain 24-hour and 48-hour information about precipitation:
configuration.yaml
sensor:
# Statistics sensors for precipitation history means
- name: "tfh precipitation stats"
platform: statistics
entity_id: sensor.burlington_precipitation
max_age:
hours: 24
- name: "feh precipitation stats"
platform: statistics
entity_id: sensor.burlington_precipitation
max_age:
hours: 48
- From the statistics, I built a sensor template to display the maximum amount of rain that was forecast over the last 24- and 48- hour periods, effectively telling me “has it rained at all, a little, or a lot?” Note that I use the names
tfh
andfeh
since the constructs24h
and48h
break the sensor template!
configuration.yaml
sensor:
# Template sensors to display the max
- name: "Precipitation history"
platform: template
sensors:
24h_precipitation_history:
friendly_name: "24h precipitation history"
unit_of_measurement: "mm"
entity_id: sensor.tfh_precipitation_stats_mean
value_template: >-
{% if states.sensor.tfh_precipitation_stats_mean.attributes.max_value <= 0.1 %}
0.0
{% elif states.sensor.tfh_precipitation_stats_mean.attributes.max_value < 0.5 %}
<0.5
{% elif states.sensor.tfh_precipitation_stats_mean.attributes.max_value >= 0.5 %}
>0.5
{% endif %}
48h_precipitation_history:
friendly_name: "48h precipitation history"
unit_of_measurement: "mm"
entity_id: sensor.feh_precipitation_stats_mean
value_template: >-
{% if states.sensor.feh_precipitation_stats_mean.attributes.max_value <= 0.1 %}
0.0
{% elif states.sensor.feh_precipitation_stats_mean.attributes.max_value < 0.5 %}
<0.5
{% elif states.sensor.feh_precipitation_stats_mean.attributes.max_value >= 0.5 %}
>0.5
{% endif %}
- These histories are used in a set of automations that turn on the hose at 2:00AM if it hasn’t rained today, and keeps it on for a set amount of time based on the previous day’s precipitation:
automations/hose_control.yaml
---
#
# Hose control structures
#
# Basic idea:
# If it rained <0.5mm in the last 48h, run for 1h
# If it rained >0.5mm in the last 48h, but 0.0mm in the last 24h, run for 30m
# If it rained <0.5mm in the last 24h, run for 15m
# If it rained >0.5mm in the last 24h, don't run tonight
# Turn on the hose at 02:00 if there's been <0.5mm of rain in the last 24h
- alias: 'Hose - 02:00 Timer - turn on'
trigger:
platform: time
at: '02:00:00'
condition:
condition: or
conditions:
- condition: state
entity_id: sensor.24h_precipitation_history
state: '<0.5'
- condition: state
entity_id: sensor.24h_precipitation_history
state: '0.0'
action:
service: homeassistant.turn_on
entity_id: switch.hose_a
# Turn off the hose at 02:15 if there's been <0.5mm but >0.0mm of rain in the last 24h
- alias: 'Hose - 02:15 Timer - turn off (<0.5mm/24h)'
trigger:
platform: time
at: '02:15:00'
condition:
condition: and
conditions:
- condition: state
entity_id: switch.hose_a
state: 'on'
- condition: state
entity_id: sensor.24h_precipitation_history
state: '<0.5'
action:
service: homeassistant.turn_off
entity_id: switch.hose_a
# Turn off the hose at 02:30 if there's been >0.5mm in the last 48h but 0.0 in the last 24h
- alias: 'Hose - 02:30 Timer - turn off (>0.5mm/48h + 0.0mm/24h)'
trigger:
platform: time
at: '02:30:00'
condition:
condition: and
conditions:
- condition: state
entity_id: switch.hose_a
state: 'on'
- condition: state
entity_id: sensor.24h_precipitation_history
state: '0.0'
- condition: state
entity_id: sensor.48h_precipitation_history
state: '>0.5'
action:
service: homeassistant.turn_off
entity_id: switch.hose_a
# Turn off the hose at 03:00 otherwise
- alias: 'Hose - 03:00 Timer - turn off'
trigger:
platform: time
at: '03:00:00'
condition:
condition: state
entity_id: switch.hose_a
state: 'on'
action:
service: homeassistant.turn_off
entity_id: switch.hose_a
Tada! Automated watering based on the rainfall!
Conclusion
This was a fun little staycation project that I’m certain has great expandability. Next year once the garden is arranged I’ll probably start work on a larger, multi-zone version to better support the huge garden. But for today I love knowing that my hose will turn itself on and water the garden every night if it needs it, no involvement from me! I hope you find this useful to you, and of course, I’m open to suggestions for improvement or questions - just send me an email!
Errata
2018-10-03: I realized that 4.0mm of rain as an automation value was quite high. Even over a few days of light rain, it never reported above 1.0mm at any single ~2h interval, let alone 4.0mm. So 0.5mm seems like a much nicer value than 4.0mm for the “it’s just lightly showered”/“it’s actually rained” distinction I was going for. The code and descriptions above have been updated. One could also modify the templates to add up the 24h/48h total and base the condition off the total, which is a little more clunky but would be more accurate if that matters in your usecase.