A few months ago a group of friends approached me asking for help with a project they had started: Aquaponics.
A combination of aquaculture (raising fish, trout in this case) and hydroponics (cultivating vegetables in water). Won’t go into detail in the process here, but here is a simplified version of the setup: trouts live in 4 huge tanks, the water from the tanks is continuously pumped to a rotary filter to remove solids, then the filtered water rich with nitrites is passed to a series of smaller tanks where bacteria convert nitrites to nitrates (this is where I get lost every time they try to explain me the process) and then that is fed to two huge hydroponics tanks then back to the fish tank.
I’m a SysAdmin by trade (and most important by hobby), huge nerd and have been using Home Assistant at home for a few years now, which I’ve been showing off to them for as much of the time.
First call I had, was a few months ago, they were having problems with the rotary filter. It would clog, which caused it to fill with water, and then overflow. They manually pushed two buttons to turn on a motor to rotate the filter, and another to enable a water pump attached to line of sprinklers that cleaned the filter mesh. This every, roughly, 10 to 15 minutes, all day, all night. The caller (the agricultural engineer of the bunch) had spent the last 4 nights awake in the greenhouse taking care of that, in a place at 8 degrees C of temperature, and above 70% humidity, no TV, no WiFi.
They had designed the whole system with little to no automation in sight: shades are connected to a (kind of) off-the-shelf weather station and a temperature sensor inside (I plan to integrate this to HA in a few months), and they had installed a cable float switch in a tank to actuate both motors, but they were measuring two steps down the road, so it took about 10 minutes after the water started spilling from the filter before that sensor would trip, hence the need to do it by manually. Also, they gather water and ambient temperature, as well as pH, conductivity and oxygen concentration manually every couple of hours throughout the day.
They struck luck, a few weeks before, I had started playing around with ESP8266 (D1 minis) and ESP32 (NodeMCU) boards, relay boards, and some sensors. Up to this point I had never soldered anything in my life, had little to no experience with electronics, no idea how sensors worked, and what a resistor, a pullup or pulldown were, but being curious by nature and seeing how it would up my domotics game at home I became a quick learner.
First iteration of the setup was pretty simple, with no HomeAssistant on premises, as there was no infrastructure where to run it. After a few tests (and quite a lot a googling), I decided that to detect water I would use a simple UTP cable pair, the rising water triggered the “sensor” once it reached both stripped cables (worked marvellously in a bucket, not so much once installed, I learned a lot about ESPHome input filters after just 30 minutes in production).
This was my first soldering job. I got a lot to thank my friend Jairo, a total expert with electronics, who even took time to record himself soldering so I could learn. BTW, he still does not approve of my soldering skills for production boards, sorry Jairo xD.
This first board had 5 inputs:
- Ds18b20 water temperature sensor (useless at first, as there was no easy way for them to retrieve the data).
- Stripped UTP cable pair to act as the water level sensor
- 3 terminals for buttons to manually operate the motors
And 3 outputs:
- All connected to a 4 relay board, to operate contactors for the motors.
First things first, I needed at least local WiFi to control the device, so I loaned them an old TP-Link router, I then learned that in order to use the webserver option of ESPHome you need a working internet connection. That didn’t stop me anyway, at that moment, I only wanted to control the motors so these guys could catch some shuteye.
Software-wise I decided on ESPHome, which would allow me to automate right on the board, without any need for a server to control it (tried tasmota as well, as that is what I mostly use at home, but it didn’t feel practical enough to work with its rules). I coded everything in a DEV Home Assistant VM, so as not to use my production one, I’ll make everything available in my github account.
The algorithm was pretty simple, I ran a script on startup. This script would check for the “water level sensor”, once it tripped it would rotate the filter for 2 seconds (⅓ of a turn) and added to a counter. After 3 activations it entered another part of the script, the “cleaning cycle”: rotate the cylinder for 12 seconds (2 complete turns), while running the sprinklers to unclog the filtering mesh.
I jerry-rigged a support with a piece of PVC, ran one of the twisted pairs down it and installed it into the rotary filter and we were up and running… for like a day.
First time running on site
A few problems with my approach: First of all, for some reason, sometimes the board would just crash, and had to be power cycled (IT Crowd reference, anyone?). But most troublesome was my water detection method. The high humidity of where it was installed, the small distance between the two cables, etc. made it (sometimes) unable to detect the “all clear”… It would stay on (wet), so it didn’t triggered again. No detection, no automation, flooding followed.
esp32:
board: nodemcu-32s
framework:
type: arduino
esphome:
name: control-rotofiltro
on_boot:
then:
- delay: 20s
- script.execute: action
- logger.log: "Running scripts..."
# Enable logging
logger:
api:
reboot_timeout: 0s
web_server:
port: 80
/wifi-redacted/
ap:
ssid: "Test-Invernadero"
password: "12345678"
reboot_timeout: 0s
globals:
# - id: limpiar
# type: boolean
# initial_value: "false"
- id: counter
type: int
initial_value: '1'
#dallas:
# pin: GPIO21
#sensor:
# - platform: dallas
# address: 0xf2062140c0cf6628
# name: "Temperatura Agua Tanque"
binary_sensor:
- platform: gpio
name: "Lleno"
pin:
number: 19
mode: INPUT_PULLUP
inverted: true
device_class: moisture
id: sensor_encendido
filters:
- delayed_on_off: 5000ms
on_press:
- switch.turn_on: switch_bomba_limpieza
- switch.turn_on: switch_cilindro
- delay: 30s
- switch.turn_off: switch_bomba_limpieza
- switch.turn_off: switch_cilindro
- platform: gpio
name: "Vacío"
pin:
number: 18
mode: INPUT_PULLUP
inverted: true
filters:
- delayed_on_off: 5000ms
device_class: moisture
id: sensor_apagado
# on_release:
# then:
# - switch.turn_off: switch_bomba_limpieza
# - switch.turn_off: switch_cilindro
- platform: gpio
name: "Apagado Pileta"
pin:
number: 17
mode: INPUT_PULLUP
inverted: true
filters:
- delayed_on_off: 5000ms
device_class: moisture
id: sensor_apagado_pileta
# on_release:
# - switch.turn_on: switch_bomba_pileta
- platform: gpio
name: "Boton 1"
pin:
number: 4
mode: INPUT_PULLUP
inverted: true
on_press:
- switch.turn_on: switch_bomba_limpieza
on_release:
- switch.turn_off: switch_bomba_limpieza
- platform: gpio
name: "Boton 2"
pin:
number: 22
mode: INPUT_PULLUP
inverted: true
on_press:
- switch.turn_on: switch_cilindro
on_release:
- switch.turn_off: switch_cilindro
- platform: gpio
name: "Boton 3"
id: boton3
pin:
number: 33
mode: INPUT_PULLUP
inverted: true
switch:
- platform: gpio
pin: 25
name: "Bomba Limpieza"
id: switch_bomba_limpieza
inverted: true
restore_mode: ALWAYS_OFF
- platform: gpio
pin: 26
name: "Motor Cilindro"
id: switch_cilindro
inverted: true
restore_mode: ALWAYS_OFF
- platform: gpio
pin: 27
name: "Bomba Pileta"
id: switch_bomba_pileta
inverted: true
I bit the bullet, I coded a solution at about 1 am, on site, freezing my *** off… I disabled the “sensor”, and created a new time based script, same algorithm, but run every few minutes, while I could figure out a real solution. This time trigger had to be adjusted every couple of days and the board kept crashing every now and then.
script:
- id: action
then:
- while:
condition:
lambda: "return true;"
then:
- if:
condition:
lambda: |-
if (id(counter) < 3) {
return true;
} else {
return false;
}
then:
- switch.turn_on: switch_cilindro
- delay: 2000ms
- switch.turn_off: switch_cilindro
- lambda: "id(counter) += 1;"
- logger.log:
format: "Sali sin limpiar. Pasada número %i"
args: ['id(counter)']
else:
- switch.turn_on: switch_cilindro
- switch.turn_on: switch_bomba_limpieza
- delay: 12s
- switch.turn_off: switch_cilindro
- switch.turn_off: switch_bomba_limpieza
- globals.set:
id: counter
value: "1"
- logger.log:
format: "Sali de limpiar. El contador volvio a %i"
args: ['id(counter)']
- delay: 60s
At least we kept the morale high while testing the code…
This actually worked pretty well for a few days. It solved the urgency but was no long term solution, but gave them a bit of peace and me some time to think. I convinced them to spend some money on infrastructure and parts to make a more robust and reliable solution, and to be able to monitor from home.
They bought an Intel NUC, a couple of Steel Float Switch water level sensors, a UPS and installed an Internet service. I installed a proxmox server on the NUC, and started running Home Assistant OS. This video from Home Assistant Conference 2020 gave me courage, I was on the right path: Home Assistant in a factory - Home Assistant Conference 2020 - YouTube
I replaced the cables as water sensors with one of the steel float sensors, this understandably worked WAY better than my two cables method
Once everything installed, it allowed me to remove the automations from the board and run them on Home Assistant. I made the board “dumb”, the float switch defined as a binary sensor, and the relays as switches.
binary_sensor:
- platform: gpio
name: "Lleno"
pin:
number: 17
mode: INPUT_PULLUP
filters:
- delayed_on_off: 2000ms
device_class: moisture
id: sensor_encendido
switch:
- platform: gpio
pin: 25
name: "Bomba Limpieza"
id: switch_bomba_limpieza
inverted: true
restore_mode: ALWAYS_OFF
- platform: gpio
pin: 26
name: "Motor Cilindro"
id: switch_cilindro
inverted: true
restore_mode: ALWAYS_OFF
- platform: gpio
pin: 27
name: "Bomba Pileta"
id: switch_bomba_pileta
inverted: true
- platform: restart
name: "Reiniciar controladora"
This is the automation in HA. Pretty much the same as the first try on the board, with the exception they can now select the number of turns the cleaning cycle does, with an input number helper.
alias: Rotofiltro Sensor
description: ''
trigger:
- platform: state
entity_id:
- binary_sensor.lleno
to: 'on'
for:
hours: 0
minutes: 0
seconds: 1
condition: []
action:
- choose:
- conditions:
- condition: numeric_state
entity_id: counter.contador_giro
below: '3'
sequence:
- service: switch.turn_on
data: {}
target:
entity_id: switch.motor_cilindro
- delay:
hours: 0
minutes: 0
seconds: 2
milliseconds: 0
- service: switch.turn_off
data: {}
target:
entity_id: switch.motor_cilindro
- service: counter.increment
data: {}
target:
entity_id: counter.contador_giro
- conditions:
- condition: state
state: '3'
entity_id: counter.contador_giro
sequence:
- service: switch.turn_on
data: {}
target:
entity_id: switch.motor_cilindro
- delay:
hours: 0
minutes: 0
seconds: 1
milliseconds: 0
- service: switch.turn_on
data: {}
target:
entity_id: switch.bomba_limpieza
- delay:
hours: 0
minutes: 0
seconds: '{{ (states(''input_number.vueltas_de_limpieza'') | int) * 6 }}'
milliseconds: 0
- service: switch.turn_off
data: {}
target:
entity_id: switch.bomba_limpieza
- service: switch.turn_off
data: {}
target:
entity_id: switch.motor_cilindro
- service: counter.reset
data: {}
target:
entity_id: counter.contador_giro
default: []
mode: single
Now with a server running I had way more versatility. I installed InfluxDB and Grafana, this would allow them to retain historical data from the temperature sensor (more on that later).
I connected the UPS to the server and passed it through to HA and installed the NUT add-on. I had another important sensor now, power. Very important, as a power outage gave them around 15 minutes before the oxygen level on the tanks would fall to dangerous levels.
Now it was time for some notifications, I started with normal notifications… but those wouldn’t work for time critical problems, no matter how many times I could chime their phones, most of them would set them to DND or vibrate at night. Tried callmebot with Telegram, but couldn’t make it work reliably, and still it had pretty much the same fundamental problem as the regular HA notifications. And that’s when I learned about critical notifications and TTS, those were a game changer.
There’s a mix of critical and non-critical notification automations running. Non-critical will send regular ones, critical will send TTS notifications through the “alarm_stream_max” channel. I moved all this to a script now, I just pass the message as data and it takes care of it.
alias: Alerta - TTS - Paralelo
sequence:
- parallel:
- repeat:
count: '15'
sequence:
- service: notify.mobile_app_celular_jp
data:
message: TTS
data:
ttl: 0
priority: high
channel: alarm_stream_max
volume: 100
title: '{{ message }}'
- repeat:
count: '15'
sequence:
- service: notify.mobile_app_m2101k7bl
data:
message: TTS
data:
ttl: 0
priority: high
channel: alarm_stream_max
volume: 100
title: '{{ message }}'
- repeat:
count: '15'
sequence:
- service: notify.mobile_app_sm_j415g
data:
message: TTS
data:
ttl: 0
priority: high
channel: alarm_stream_max
volume: 100
title: '{{ message }}'
- repeat:
count: '15'
sequence:
- service: notify.mobile_app_santi
data:
message: TTS
data:
ttl: 0
priority: high
channel: alarm_stream_max
volume: 100
title: '{{ message }}'
- repeat:
count: '15'
sequence:
- service: notify.mobile_app_tito
data:
message: TTS
data:
ttl: 0
priority: high
channel: alarm_stream_max
volume: 100
title: '{{ message }}'
mode: queued
icon: mdi:text-to-speech
I know I can simplify this even further by using a notification group. I have created it, but as with everything mission critical I need more testing, and as I’m doing all this for free in my spare time, well, if it’s not critical, it goes to the backlog.
It all worked fine for a few weeks, a few kinks now and then, the router started crashing, swapped it, also crashed, bought a brand new one, problem solved.
Then something else happened. One of the pumps that circulate the water got stuck, this meant no aeration so low oxygen levels, fishy problems. So, I had to learn about water flow sensors. I decided to get a new board, a D1 Mini this time, and connect all sensors to it. Separate the waters, as one says. So I got to soldering and with a new toy, a 3D printer, to design casings for them. I ended up adding a DHT22, a water flow sensor, and moving the dallas water temperature sensor. In HA I created a threshold helper to trigger the alert in case of water flow decrease.
dallas:
pin: D5
sensor:
- platform: pulse_counter
pin:
number: D2
name: "Caudal"
update_interval: 5s
filters:
- lambda: return (x / 27.0);
unit_of_measurement: "L/m"
- platform: dht
pin:
number: D6
temperature:
name: "Temperatura Invernadero"
humidity:
name: "Humedad Invernadero"
update_interval: 300s
- platform: dallas
address: 0xf2062140c0cf6628
name: "Temperatura Agua Tanque"
binary_sensor:
- platform: gpio
name: "Rotofiltro Lleno"
pin:
number: D7
mode: INPUT_PULLUP
inverted: true
filters:
- delayed_on_off: 2000ms
device_class: moisture
switch:
- platform: restart
name: "Reiniciar Sensores"
There was no time for a dress rehearsal. Two days later, at 2:04 AM, the pump ingested something and got stuck again. HA notified 3 people. Hilariously, one of them didn’t hear it, his wife did, woke him up, he told her to disregard the alert, has no recollection of this conversation. The second one says he didn’t get it (doubt it, have some evidence he did, but he is my friend I’ll give him the benefit of the doubt). Third got it about an hour after it triggered. As far as I can tell this could be because his phone was not charging, was in Doze mode. I have since change the settings using ttl:0 and priority:high in the notification and prompted them to leave their phones charging at night.
As of now everything is running pretty much smoothly.
Added a few housekeeping automations: notify if the motor controlling board is unavailable for more than a couple of minutes (having this issue, it turns into unavailable randomly once or twice an hour, just for a second… This is why I need some kind of failsafe automation on the board itself when it’s not connected to HA, work in progress).
Also check if the motors are kept running for two long and switch them off, this can happen when the board becomes “unavailable” comes just at the time HA sends the switch off command. As far as I can tell this ran only once.
They’ve been converted to Home Assistant now, they understand the importance of automatic data collection, and automations. Some projects planned for the near future:
- pH sensor, saw a post in this forum about this.
- Dissolved oxygen level, haven’t seen it done, but pretty sure I can figure it out
- Water electrical conductivity, same as above.
- Swapping that moody ESP32 board, had a D1 mini almost ready, I fried it.
- Automating the water flow pumps, switching to the backup one in case of flow decrease
- Integrating the covers system
- Starting the gas powered generator in case of power outage, switching everything to auxiliary power, I think this is the most difficult task at hand, I honestly have no clue on how to do it.
- Installing a water level sensor and controlling a pump to add water to the system
- Automating a turbine to aerate the water in case of low oxygen level
Payday, celebrating the success of the first stage with an Argentinian asado, on site.