Hi,
This is my ongoing project.
It is a Aquarium monitoring system. It has a circuit board part, with some sensors (pH, Flux, Water Level and temperature) and the HomeAssistant part, that will show the values and control some equipment connected.
It has a lot of improvements to make. Any suggestions are welcome. I’m not a graphic designer, so all graphics are from internet. Also, I’m quite new on HA programming.
I'll be updating this post until the full project is finished.
Lovelace Dashboard:
All controls are executed clicking on the corresponding device on the picture. And the state is shown on the device:
-
The water level is shown with fluid-level and reflects the sensor that’s installed on the aquarium. There’s the possibility to make automatic the refill (I don’t have).
-
Canister flux is shown as a fan with the value below. I defined some severities so that it will be red/green/orange if it’s too slow.
-
Clicking on the circulation pump activates it and a fan spins green.
-
Clicking on the frog (I hava a “bubble frog”), activates the air comprassor connected to a frog in my aquarium. The water on the fliud changes, full with bubbles.
-
The CO2 shows the estimated level of gas. For this I created a helper to store the last fill and another that has the estimated duration. Clicking on the valve opens/closes the solenoid to liberate the gas. Clicking on the canister, the date is set to today, indicating the REFILL was done and the level is updated.
-
The light still has the basic on/of behavior, but I intend to reflect the intensity later.
-
UV light on/off clicking on it.
-
pH level will reflect the sensor. But I need a more representable gauge. I’m using the canvas because it was the closest I need, but it does not accept percentage to define the size. I will look for another one.
-
Temperature bar: I 'd like to make it the same size as the picture. And the number n vertical. I will search how can I do that. Still couldn’t find out how .
Todo:
-
Better light control, reflecting the brightness level, changing the opacity
-
better pH gauge.
-
Better positioning. Still learning how to do this correctly. I’m having issues when accessing in little screens, such as a phone.
-
Controls to turn on devices not in the picture, like heater,
or refill CO2. -
Adjust temperature bar
-
Show other parameters (amnonia, nitrite, nitrate, etc…)
Here are the codes and project details (yes, they can be immproved, but it works):
======================================
AQUAEYE BUILD AND CONFIGURATION
This is the heart of the project. It’s a circuit board based on ESP32 (or ESP8266), with ESPHome firmware and connected with some sensors that returns to HomeAssistant the sensors data.
Actually it has 4 sensors:
-
PH: PH-4502C
-
Temperature: DS18B20
-
Water Level: HC-SR04
-
Flux from Canister: YF-B5-S
Details about construction will be posted as soon as I finished the soldering.
AquaEye Electronic Schema:
ESPHome code:
substitutions:
name: "aquaeye-sala"
friendly_name: "AquaEye Sala"
dns: "aqyaeye-sala.casa"
flux_time: "60s"
ph_time: "60s"
temp_time: "60s"
level_time: "60s"
esphome:
name: ${name}
friendly_name: ${friendly_name}
min_version: 2024.11.0
name_add_mac_suffix: false
project:
name: "alexantao.aquaeye"
version: dev
esp32:
board: esp32-c3-devkitm-1
variant: ESP32C3
#restore_from_flash: true
framework:
type: esp-idf
# Enable logging
logger:
# Enable Home Assistant API
api:
# Allow Over-The-Air updates
ota:
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
output_power: 8.5dB
#use_address: ${dns}
globals:
- id: retorno
type: float
initial_value: "0.0"
- id: freq_fluxo # pulses per second per litre/minute of flow
type: float
restore_value: yes
initial_value: "6.6"
- id: sonic_offset # Offset do Sensor até a água quando o tanque está cheio (em mm)
type: int
restore_value: yes
initial_value: '23'
- id: coluna_agua_total # Medida da coluna de água quando o tanque está cheio (em mm)
type: int
restore_value: yes
initial_value: '420'
####################################################################################
# LUZES
output:
- platform: ledc
id: luz_verde
pin: GPIO10
light:
- platform: status_led
name: "Status LED"
id: esp_status_led
icon: "mdi:alarm-light"
pin:
number: GPIO8
inverted: true
restore_mode: ALWAYS_OFF
- platform: binary
name: "Luz Verde"
output: luz_verde
####################################################################################
# TEMPERATURA
one_wire:
- platform: gpio
pin: GPIO4
####################################################################################
# SENSORES
sensor:
#------------------------------------------------------------#
- platform: homeassistant
id: ha_freq_fluxo
entity_id: input_number.freq_fluxo_sala
on_value:
then:
if:
condition:
lambda: 'return id(freq_fluxo) != float(x);'
then:
- logger.log:
level: DEBUG
format: 'Mudança na Freq Fluxo : de %f para %f'
args: ['id(freq_fluxo)', 'float(x)']
- globals.set:
id: freq_fluxo
value: !lambda 'return float(x);'
#------------------------------------------------------------#
- platform: homeassistant
id: ha_sonic_offset
entity_id: input_number.sonic_offset_sala
on_value:
then:
if:
condition:
lambda: 'return id(sonic_offset) != int(x);'
then:
- logger.log:
level: DEBUG
format: 'Mudança no Offset Sonico : de %d para %d'
args: ['id(sonic_offset)', 'int(x)']
- globals.set:
id: sonic_offset
value: !lambda 'return int(x);'
#------------------------------------------------------------#
- platform: homeassistant
id: ha_coluna_agua_total
entity_id: input_number.coluna_agua_total_sala
on_value:
then:
if:
condition:
lambda: 'return id(coluna_agua_total) != int(x);'
then:
- logger.log:
level: DEBUG
format: 'Mudança na Coluna de Agua : de %d para %d'
args: ['id(coluna_agua_total)', 'int(x)']
- globals.set:
id: sonic_offset
value: !lambda 'return int(x);'
#------------------------------------------------------------#
- platform: wifi_signal
name: 'Sinal Wifi'
update_interval: 60s
accuracy_decimals: 0
- platform: uptime
name: "Tempo de Monitoração"
unit_of_measurement: days
update_interval: 300s
accuracy_decimals: 1
filters:
- multiply: 0.000011574
#------------------------------------------------------------#
- platform: dallas_temp
name: "Temperatura"
update_interval: "${temp_time}"
id: temperatura
filters:
- filter_out: nan
- clamp:
min_value: 0
#------------------------------------------------------------#
- platform: ultrasonic
id: distancia_agua
trigger_pin: GPIO1
echo_pin: GPIO0
name: "Distância da Água"
unit_of_measurement: "m"
icon: "mdi:waves-arrow-up"
accuracy_decimals: 2
update_interval: ${level_time}
#timeout: 2.0m
pulse_time: 20us
filters:
- filter_out: nan
on_value:
- sensor.template.publish:
id: nivel
state: !lambda return ( (id(coluna_agua_total) - (id(distancia_agua).state*1000.0 - id(sonic_offset))) * 100 ) / id(coluna_agua_total);
# x = leitura atual em m
# Altura Total : 50 cm AT
# Offset do Sensor= 2 cm OS
# Coluna de Agua = 42 cm CA
# FALTA = (x*100 - OS) = (x*100 - 2)
# SOBRANDO = (CA - FALTA) = CA - (x*100 - OS)
# % = (SOBRANDO * 100) / CA
- platform: template
name: "Nível da Água"
id: nivel
unit_of_measurement: '%'
accuracy_decimals: 0
icon: "mdi:water-percent"
filters:
- filter_out: nan
- median:
send_first_at: 1
window_size: 4
send_every: 2
#------------------------------------------------------------#
- platform: pulse_counter
state_class: total_increasing
name: "Frequência do Fluxo" # É o valor retornado pelo sensor, em pulsos/min
id: freq_fluxo_sala
pin: GPIO3
update_interval: ${flux_time}
icon: "mdi:wave-undercurrent"
unit_of_measurement: "pul/min"
accuracy_decimals: 0
filters:
- filter_out: nan
- clamp:
min_value: 0
on_value:
- sensor.template.publish:
id: fluxo
state: !lambda return ( (id(freq_fluxo_sala).state / (id(freq_fluxo)*60) ) *60); #Flow pulse: F=(6.68Q)±5% with Q=L/min
- platform: template
name: "Fluxo Canister"
id: fluxo
unit_of_measurement: 'L/h'
accuracy_decimals: 1
icon: "mdi:waves-arrow-right"
filters:
- filter_out: nan
#- platform: integration
# id: fluxo_atual
# device_class: water
# state_class: measurement
# name: "Fluxo da Filtragem"
# unit_of_measurement: 'L/h'
# accuracy_decimals: 2
# sensor: freq_fluxo_sala
# time_unit: min
# icon: "mdi:waves-arrow-right"
# filters:
# - lambda: return ( (x / (id(freq_fluxo)*60) ) *60); #Flow pulse: F=(6.68Q)±5% with Q=L/min
- platform: integration
device_class: water
state_class: total_increasing
name: "Litros Filtrados"
unit_of_measurement: 'L'
accuracy_decimals: 1
sensor: fluxo
time_unit: min
icon: "mdi:cup-water"
#------------------------------------------------------------#
- platform: adc
pin: GPIO2
id: ph_ads
name: "pH ads"
icon: "mdi:flash-triangle-outline"
update_interval: ${ph_time}
unit_of_measurement: "mV"
accuracy_decimals: 3
filters:
- median:
window_size: 6
send_every: 6
send_first_at: 2
#Measured voltage -> Actual pH (buffer solution)
- calibrate_linear:
- 0.59 -> 7.0
- 0.71 -> 4.0
####################################################################################
======================================
HOMEASSISTANT CONFIGURATION
Helpers
There are some temporary helpers, because aquaeye if not fully finished yet (like ph, level, etc…).
Some of these Helpers are connectors for the AquaEye Project and others for the UI on HomeAssistant.
- input_number.sonic_offset_sala
- input_number.freq_fluxo_sala
- input_number.aqua_sala_prof
- input_number.coluna_agua_total_sala
- input_boolean.calib_ph
- input_number.v_calib_mv4
- input_number.v_calib_mv7
- input_number.v_calib_mv9
- input_select.c_calib_ph4
- input_select.c_calib_ph7
- input_select.c_calib_ph9_10
Timers
You will need to create a timer to control de time the light will be lit. Mine is:
- timer.timer_luz_aquario
HA templates.yaml
- sensor:
- name: "pH do Aquário da Sala"
unique_id: sala_ph
unit_of_measurement: "pH"
device_class: ph
icon: mdi:ph
state: "{{ (states('input_select.c_calib_ph9_10')| float) - ( (((states('input_number.v_calib_mv9') | float)-(states('sensor.aquaeye_sala_ph_ads') | float(0)))*((states('input_select.c_calib_ph7')| float)-(states('input_select.c_calib_ph9_10') | float))) / ((states('input_number.v_calib_mv7') | float)-(states('input_number.v_calib_mv9') | float)) ) | round(2) }}"
- name: "Nível do CO2 da Sala"
icon: mdi:co2
unique_id: co2_sala_nivel
state: >
{% set abastecimento = states('input_datetime.dia_reabastecimento_co2_sala') | as_datetime | as_local %}
{% set duracao = states('input_number.duracao_co2_sala') |int %}
{% set hoje = now() %}
{% set dias_passados = (hoje - abastecimento).days |int %}
{% set nivel = 100 - ((dias_passados * 100) / duracao) |int %}
{{ nivel | int }}
- name: "Tempo Restante Luz da Sala"
icon: mdi:timer-outline
unique_id: tempo_restante_luz_sala
value_template: >
{% set f = state_attr('timer.timer_luz_aquario', 'finishes_at') %}
{{ '00:00:00' if f == None else
(as_datetime(f) - now()).total_seconds() | timestamp_custom('%H:%M:%S', false) }}
lovelace panel
Requirements:
- custom:pool-monitor-card (for now, for the pH gouge)
- custom:fluid-level-background-card
- custom:button-card
- custom:slider-entity-row
- custom:hui-element
- custom:bar-card
- custom:card-templater
type: picture-elements
image: /local/itens/aquario-fundo.png
elements:
- type: image
entity: switch.yuri_aquecedor
title: Aquecedor
tap_action:
action: none
state_image:
"on": /local/itens/aquario-aquecedor-LIG.png
style:
z-index: 1
left: "0"
top: "0"
transform: none
- type: custom:pool-monitor-card
display:
compact: true
sensors:
ph:
- entity: input_number.ph_temporario
style:
z-index: 1
top: 80%
left: 50%
width: 40%
- type: custom:fluid-level-background-card
entity: sensor.co2_sala_nivel
background_color: transparent
show_state: true
show_icon: false
full_value: 100
severity:
- value: 10
color: red
- value: 30
color: yellow
- value: 80
color: rgba(80, 80, 60, 1)
style:
z-index: 3
top: 78%
left: 10.4%
width: 6.3%
height: 60%
card_mod:
style: |
ha-card {
text-align: center;
--ha-card-border-color: transparent !important;
box-shadow: none !important;
background: none !important;
border-radius: 10px;
overflow: hidden;
}
#container, .container {
width: 100% !important;
height: 40% !important;
position: relative !important;
border-radius: 14px !important;
margin-left: 0%;
margin-top: 0%;
opacity: 0.4;
overflow: hidden;
}
card:
type: custom:button-card
entity: sensor.co2_sala_nivel
title_template: "{{ states('sensor.co2_sala_nivel')|round(0) }} %"
show_header_toggle: false
show_name: false
show_icon: false
show_title: true
tap_action:
action: call-service
confirmation:
text: Marcar hoje como dia de recarga ?
service: input_datetime.set_datetime
service_data:
entity_id: input_datetime.dia_reabastecimento_co2_sala
date: "[[[ return new Date().toLocaleDateString('en-CA') ]]]"
value: "off"
card_mod:
style: |
ha-card {
--ha-card-header-font-size: 20px;
height: 100px !important;
color: white;
font-weight: 800;
}
.card-header {
justify-content: center !important;
}
.name {
overflow: unset !important;
}
- type: image
entity: switch.alimentador_sala
title: Alimentar Peixinhos
tap_action:
action: toggle
state_image:
"on": /local/itens/feeder.png
"off": /local/itens/feeder-off.png
unknown: /local/itens/feeder-off.png
style:
z-index: 3
left: 30%
top: 25%
transform: scale(0.2, 0.2) translate(-50%, -50%)
- type: image
entity: switch.aeracao_aquario
title: Aeração do Aquário
tap_action:
action: toggle
state_image:
"on": /local/itens/sapo-ligado.png
"off": /local/itens/sapo-desligado.png
style:
z-index: 3
left: 50%
top: 35%
transform: scale(0.2, 0.2) translate(-50%, -50%)
- type: image
entity: switch.energia_aquario_uv
title: Luz UV
tap_action:
action: toggle
state_image:
"on": /local/itens/uv-ligada.png
"off": /local/itens/uv-desligada.png
style:
z-index: 2
left: "-8%"
top: "-5%"
transform: scale(0.1, 0.1) translate(-50%, -50%)
- type: state-label
entity: sensor.tempo_restante_luz_da_sala
style:
z-index: 3
left: 35%
top: 9%
color: lightgreen
font-size: 200%
conditions:
- entity: sensor.tempo_restante_luz_da_sala
state_not: "00:00"
- type: custom:slider-entity-row
entity: light.luz_do_aquario
style:
z-index: 3
left: 50%
top: 9%
card_mod:
style: |
ha-card {
--ha-card-header-font-size: 20px;
height: 100px !important;
color: white;
font-weight: 800;
}
.card-header {
justify-content: center !important;
}
.name {
overflow: unset !important;
}
- type: image
entity: light.luz_do_aquario
tap_action: none
hold_action: none
state_image:
"on": /local/itens/luz-ligada.png
"off": /local/itens/luz-desligada.png
style:
z-index: 2
top: "-12%"
width: 100%
transform: scale(0.7, 0.7) translate(0%, 0%)
opacity: ${ vars[0] / 255.0 }
- type: image
image: /local/itens/circulação.png
style:
z-index: 2
top: 65%
left: 80%
height: 28%
transform: scale(0.6, 0.6) translate(-50%, -50%)
- type: image
entity: switch.aquario_co2
title: CO2 - Clique na válvula para ligar/desligar
tap_action:
action: toggle
state_image:
"on": /local/itens/co2-cilindro-ON.png
"off": /local/itens/co2-cilindro-OFF.png
style:
z-index: 2
top: "-10%"
left: "-13%"
transform: scale(0.2, 0.2) translate(-50%, -50%)
- type: conditional
conditions:
- entity: input_boolean.aquaeye_ui_config
state: "on"
elements:
- type: custom:hui-element
card_type: vertical-stack
cards:
- type: horizontal-stack
cards:
- type: entities
title: Sensores
entities:
- entity: input_number.coluna_agua_total_sala
name: Coluna de Água
- entity: input_number.sonic_offset_sala
name: Distância do Sensor
- entity: input_number.freq_fluxo_sala
name: Fator do Sensor de Fluxo
- type: entities
title: Equipamentos
entities:
- entity: input_number.aquario_max_luz
name: Iluminação Máx
- entity: input_number.aquario_tempo_circulacao
- entity: input_number.aquario_tempo_da_aeracao
- entity: input_number.fader_luz_sala
- type: horizontal-stack
cards:
- type: entities
title: pH - Temp Sala / Calibração
entities:
- entity: sensor.aquaeye_sala_ph_ads
name: pH ads
- entity: sensor.ph_do_aquario_da_sala
- entity: sensor.aquaeye_sala_temperatura
- entity: input_boolean.calib_ph
- type: entities
title: Calibração pH Sala
entities:
- entity: input_select.c_calib_ph4
- entity: input_number.v_calib_mv4
name: mV4
- type: divider
- entity: input_select.c_calib_ph7
- entity: input_number.v_calib_mv7
name: mV7
- type: divider
- entity: input_select.c_calib_ph9_10
- entity: input_number.v_calib_mv9
name: mV9-10
visibility:
- condition: state
entity: input_boolean.calib_ph
state: "on"
style:
z-index: 3
top: 35%
left: 30%
- type: custom:button-card
entity: input_boolean.aquaeye_ui_config
icon: mdi:tune-vertical
show_title: false
show_name: false
title: Configurações
tap_action:
action: toggle
style:
z-index: 3
top: 5%
left: 5%
height: 8%
styles:
icon:
- width: 40%
- color: red
card_mod:
style: |
ha-card {
text-align: center;
--ha-card-border-color: transparent !important;
box-shadow: none !important;
background: none !important;
border-radius: 10px;
overflow: hidden;
}
- type: custom:button-card
entity: switch.circulacao_aquario
icon: mdi:fan
show_name: false
title: Liga/Desliga Circulação
style:
z-index: 3
top: 67.5%
left: 82%
height: 8%
styles:
icon:
- width: 40%
state:
- value: "on"
styles:
icon:
- animation: rotating 1s linear infinite
- color: green
- value: "off"
styles:
icon:
- animation: none
- color: red
card_mod:
style: |
ha-card {
text-align: center;
--ha-card-border-color: transparent !important;
box-shadow: none !important;
background: none !important;
border-radius: 10px;
overflow: hidden;
}
- type: custom:button-card
entity: input_number.fluxo_temporario
icon: mdi:fan
tap_action: none
tltle: Fluxo do Filtro
"show_label:": false
show_name: false
style:
z-index: 1
top: 23%
left: 74%
height: 8%
styles:
card:
- font-size: 15px
- font-weight: bold
grid:
- grid-template-areas: "\"i\" \"fluxo\""
- grid-template-columns: 1fr
custom_fields:
fluxo:
- align-self: middle
- color: white
icon:
- color: |
[[[
if (entity.state < 400) return 'red';
if (entity.state >= 400 && entity.state < 649) return 'orange';
else return 'green';
]]]
- width: 30%
- animation: rotating 1s linear infinite
custom_fields:
fluxo: |
[[[
return `<span>${entity.state}L/h</span>`
]]]
card_mod:
style: |
ha-card {
text-align: center;
--ha-card-border-color: transparent !important;
box-shadow: none !important;
background: none !important;
border-radius: 10px;
overflow: hidden;
}
- type: custom:bar-card
animation: "on"
bar-card-color: transparent
bar-card-border-radius: 0
direction: up
entity_row: true
icon: false
max: 32
width: 8%
unit_of_measurement: ºC
positions:
icon: "off"
name: "off"
value: outside
style:
z-index: 1
top: 38.5%
left: 17.3%
severity:
- color: Red
from: 0
to: 24
- color: Orange
from: 24.1
to: 25
- color: Green
from: 25.1
to: 27.9
- color: Red
from: 28
to: 31
card_mod:
style: |
ha-card {
--ha-card-border-width: 0px;
vertical-align: middle;
text-align: center;
}
bar-card-value {
margin-top: auto;
font-size: 16px;
font-weight: bold;
text-shadow: 1px 1px #0005;
text-orientation: mixed;
transform-origin: 0 0;
transform: rotate(270deg);
}
bar-card-max {
margin: 0px;
margin-left: auto;
margin-top: -20px;
top: 10px;
}
entities:
- entity: sensor.aquaeye_sala_temperatura
- type: custom:fluid-level-background-card
entity: sensor.aquaeye_sala_nivel
fill_entity: switch.aeracao_aquario
background_color: transparent
show_state: true
show_icon: false
camera_view: auto
level_color: rgba(82, 171, 255, 1)
style:
z-index: 2
top: 80%
left: 65%
width: 100%
height: 110%
card_mod:
style: |
ha-card {
text-align: center;
--ha-card-border-color: transparent !important;
box-shadow: none !important;
background: none !important;
border-radius: 10px;
overflow: hidden;
}
#container, .container {
width: 71% !important;
height: 40% !important;
position: relative !important;
border-radius: 14px !important;
margin-left: 0%;
margin-top: 0%;
opacity: 0.4;
overflow: hidden;
}
card:
type: custom:card-templater
card:
type: entity
entity: sensor.aquaeye_sala_nivel
title_template: "{{ states(''sensor.aquaeye_sala_nivel'')|round(0) }} %"
unit_of_measurement: "%"
show_header_toggle: false
show_name: false
show_icon: false
show_title: true
name: " "
positions:
value: "off"
card_mod:
style: |
ha-card {
--ha-card-header-font-size: 20px;
height: 100px !important;
color: white;
font-weight: 800;
}
.card-header {
justify-content: center !important;
}
.name {
overflow: unset !important;
}
Hope you like…