From e026e47446b9dce6e47abafc8c995cf301a8ff46 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Fri, 28 Sep 2018 01:37:38 -0400 Subject: [PATCH] Initial version of automating-your-hose --- content/post/automating-your-hose.md | 470 +++++++++++++++++++++++++++ 1 file changed, 470 insertions(+) create mode 100644 content/post/automating-your-hose.md diff --git a/content/post/automating-your-hose.md b/content/post/automating-your-hose.md new file mode 100644 index 0000000..60d395a --- /dev/null +++ b/content/post/automating-your-hose.md @@ -0,0 +1,470 @@ ++++ + +class = "post" +date = "2018-09-28T00:35:22-04:00" +tags = ["automation","gardening"] +title = "Automating your garden hose for fun and profit" +description = "Building a custom self-hosted MQTT water shutoff and controlling it with HomeAssistant" +type = "post" +weight = 1 + ++++ + +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](https://www.boniface.me/post/self-hosted-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](https://www.amazon.ca/gp/product/B00K0TKJCU/ref=oh_aui_detailpage_o01_s00?ie=UTF8&psc=1). 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 +#include + +/* + * 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](https://www.home-assistant.io/components/switch.mqtt/) component, which makes adding a switch UI element for the hose that actually 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](http://www.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` and `feh` since the constructs `24h` and `48h` break the sensor template! + +`configuration.yaml` +``` + # 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 < 4.0 %} + <4.0 + {% elif states.sensor.tfh_precipitation_stats_mean.attributes.max_value >= 4.0 %} + >4.0 + {% 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 < 4.0 %} + <4.0 + {% elif states.sensor.feh_precipitation_stats_mean.attributes.max_value >= 4.0 %} + >4.0 + {% 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 <4.0mm in the last 48h, run for 1h +# If it rained >4.0mm in the last 48h, but 0.0mm in the last 24h, run for 30m +# If it rained <4.0mm in the last 24h, run for 15m +# If it rained >4.0mm in the last 24h, don't run tonight +# Run every night at 12:00AM + +# Turn on the hose at 02:00 if there's been <4.0mm 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: '<4.0' + - 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 <4.0mm but >0.0mm of rain in the last 24h +- alias: 'Hose - 02:15 Timer - turn off (<4.0mm/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: '<4.0' + action: + service: homeassistant.turn_off + entity_id: switch.hose_a +# Turn off the hose at 02:30 if there's been >4.0mm in the last 48h but 0.0 in the last 24h +- alias: 'Hose - 02:30 Timer - turn off (>4.0mm/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: '>4.0' + 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!