Hier der funktionierende Code für ESPHome:
# ESPHome Konfiguration für Hydroponisches NFT-System
# Echtbetrieb mit Arduino-Kommunikation
esphome:
name: esphome-nft
friendly_name: ESPHome_NFT
esp32:
board: esp32dev
framework:
type: arduino
# WiFi Konfiguration
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Statische IP-Konfiguration
manual_ip:
static_ip: xxx
gateway: xxx
subnet: xxx
ap:
ssid: "Esphome-Nft Fallback"
password: "xxx"
captive_portal:
logger:
level: DEBUG
tx_buffer_size: 1024 # Erhöht für bessere Debug-Ausgaben
api:
reboot_timeout: 0s
web_server:
port: 80
# Globale Variablen für Sensordaten
globals:
- id: temperature_value
type: float
restore_value: no
initial_value: '25.0'
- id: ph_value
type: float
restore_value: no
initial_value: '7.0'
- id: ec_value
type: float
restore_value: no
initial_value: '1000.0'
- id: ph_pump_state
type: bool
restore_value: no
initial_value: 'false'
- id: nutrient_pump_state
type: bool
restore_value: no
initial_value: 'false'
- id: ph_down_volume_value
type: float
restore_value: yes
initial_value: '0.0'
- id: nutrient_volume_value
type: float
restore_value: yes
initial_value: '0.0'
- id: bridge_status_string
type: std::string
restore_value: no
initial_value: '"Warte auf Bridge..."'
- id: uart_line_buffer
type: std::string
restore_value: no
initial_value: '""'
# UART für die Kommunikation mit dem Arduino
uart:
id: arduino_serial
tx_pin: GPIO17
rx_pin: GPIO16
baud_rate: 115200
rx_buffer_size: 512
# Komponente zur Verarbeitung der UART-Daten
text_sensor:
- platform: template
name: "Bridge Status"
id: bridge_status
lambda: 'return id(bridge_status_string);'
- platform: template
name: "Last UART Data"
id: last_uart_data
lambda: 'return id(uart_line_buffer);'
update_interval: 2s
- platform: version
name: "ESPHome Version"
- platform: wifi_info
ip_address:
name: "NFT IP-Adresse"
ssid:
name: "NFT WLAN SSID"
bssid:
name: "NFT WLAN BSSID"
mac_address:
name: "NFT MAC-Adresse"
# Zeit von Home Assistant abrufen (für tägliche Reset-Funktionen)
time:
- platform: homeassistant
id: homeassistant_time
on_time:
- seconds: 0
minutes: 0
hours: 0
then:
- lambda: |-
// Täglicher Reset um Mitternacht
id(ph_down_volume_value) = 0.0;
id(nutrient_volume_value) = 0.0;
id(ph_down_daily_volume).publish_state(0.0);
id(nutrient_daily_volume).publish_state(0.0);
# Sensoren für die Hydroponik-Anlage (jetzt mit echten Werten vom Arduino)
sensor:
# Temperatur-Sensor
- platform: template
name: "NFT Wassertemperatur"
id: nft_temperature
unit_of_measurement: "°C"
accuracy_decimals: 1
device_class: temperature
state_class: measurement
update_interval: 30s
lambda: 'return id(temperature_value);'
# pH-Wert Sensor
- platform: template
name: "NFT PH-Wert"
id: nft_ph
unit_of_measurement: "pH"
accuracy_decimals: 2
state_class: measurement
update_interval: 30s
lambda: 'return id(ph_value);'
# EC-Wert Sensor
- platform: template
name: "NFT EC-Wert"
id: nft_ec
unit_of_measurement: "µS/cm"
accuracy_decimals: 0
state_class: measurement
update_interval: 30s
lambda: 'return id(ec_value);'
# Sensoren für Dosierpumpen-Volumen
- platform: template
name: "PH Down Zugabe heute"
id: ph_down_daily_volume
unit_of_measurement: "ml"
accuracy_decimals: 1
icon: mdi:beaker
lambda: 'return id(ph_down_volume_value);'
- platform: template
name: "Dünger Zugabe heute"
id: nutrient_daily_volume
unit_of_measurement: "ml"
accuracy_decimals: 1
icon: mdi:nutrition
lambda: 'return id(nutrient_volume_value);'
# Binäre Sensoren für Pumpenstatus
binary_sensor:
- platform: template
name: "PH Down Pumpe aktiv"
id: ph_down_pump_active
device_class: running
lambda: 'return id(ph_pump_state);'
- platform: template
name: "Dünger Pumpe aktiv"
id: nutrient_pump_active
device_class: running
lambda: 'return id(nutrient_pump_state);'
# Float Switch für Wasserstand
- platform: gpio
pin:
number: GPIO4
mode: INPUT_PULLUP
inverted: true
name: "Wasserstand OK"
id: water_level
filters:
- delayed_on: 100ms
- delayed_off: 100ms
# Schalter für manuelle Pumpensteuerung
switch:
# Manueller Schalter für pH-Down Dosierung
- platform: template
name: "PH Down Dosierung"
id: ph_down_dose
icon: mdi:test-tube
optimistic: true
turn_on_action:
- lambda: |-
id(bridge_status_string) = "PH-Down Pumpe aktiviert";
id(bridge_status).publish_state(id(bridge_status_string));
// Sende Befehl an Arduino
std::string cmd = "DOSE:PHDOWN:3.0\n";
id(arduino_serial).write_array((const uint8_t*)cmd.c_str(), cmd.length());
- delay: 3s
- switch.turn_off: ph_down_dose
# Manueller Schalter für Dünger Dosierung
- platform: template
name: "Dünger Dosierung"
id: nutrient_dose
icon: mdi:nutrition
optimistic: true
turn_on_action:
- lambda: |-
id(bridge_status_string) = "Dünger Pumpe aktiviert";
id(bridge_status).publish_state(id(bridge_status_string));
// Sende Befehl an Arduino
std::string cmd = "DOSE:NUTRIENT:5.0\n";
id(arduino_serial).write_array((const uint8_t*)cmd.c_str(), cmd.length());
- delay: 3s
- switch.turn_off: nutrient_dose
# Intervalle für verschiedene Aktionen
interval:
# UART-Daten verarbeiten
- interval: 50ms
then:
- lambda: |-
static std::string buffer;
uint8_t c;
while (id(arduino_serial).available() > 0) {
if (id(arduino_serial).read_byte(&c)) {
if (c == '\n') {
if (!buffer.empty()) {
// Speichere empfangene Zeile
id(uart_line_buffer) = buffer;
ESP_LOGD("uart", "Received: %s", buffer.c_str());
// Parse ATLAS:DATA:TEMP:xx.x:PH:x.xx:EC:xxxx:PHPUMP:x:NUTPUMP:x
std::string &data = buffer;
if (data.compare(0, 11, "ATLAS:DATA:") == 0) {
size_t temp_pos = data.find(":TEMP:");
size_t ph_pos = data.find(":PH:");
size_t ec_pos = data.find(":EC:");
size_t phpump_pos = data.find(":PHPUMP:");
size_t nutpump_pos = data.find(":NUTPUMP:");
if (temp_pos != std::string::npos && ph_pos != std::string::npos) {
std::string temp_str = data.substr(temp_pos + 6, ph_pos - (temp_pos + 6));
float temp = atof(temp_str.c_str());
if (temp > -10.0 && temp < 100.0) {
id(temperature_value) = temp;
id(nft_temperature).publish_state(temp);
}
}
if (ph_pos != std::string::npos && ec_pos != std::string::npos) {
std::string ph_str = data.substr(ph_pos + 4, ec_pos - (ph_pos + 4));
float ph = atof(ph_str.c_str());
if (ph > 0.0 && ph < 14.0) {
id(ph_value) = ph;
id(nft_ph).publish_state(ph);
}
}
if (ec_pos != std::string::npos && phpump_pos != std::string::npos) {
std::string ec_str = data.substr(ec_pos + 4, phpump_pos - (ec_pos + 4));
float ec = atof(ec_str.c_str());
if (ec >= 0.0 && ec < 10000.0) {
id(ec_value) = ec;
id(nft_ec).publish_state(ec);
}
}
if (phpump_pos != std::string::npos && nutpump_pos != std::string::npos) {
std::string phpump_str = data.substr(phpump_pos + 8, nutpump_pos - (phpump_pos + 8));
bool ph_active = (phpump_str == "1");
id(ph_pump_state) = ph_active;
id(ph_down_pump_active).publish_state(ph_active);
std::string nutpump_str = data.substr(nutpump_pos + 9);
bool nut_active = (nutpump_str == "1");
id(nutrient_pump_state) = nut_active;
id(nutrient_pump_active).publish_state(nut_active);
}
}
// Parse ATLAS:DOSING:PHDOWN:3.0 oder ATLAS:DOSING:NUTRIENT:5.0
else if (data.compare(0, 13, "ATLAS:DOSING:") == 0) {
if (data.find(":PHDOWN:") != std::string::npos) {
size_t val_pos = data.find(":PHDOWN:") + 8;
std::string volume_str = data.substr(val_pos);
float volume = atof(volume_str.c_str());
if (volume > 0.0) {
id(ph_down_volume_value) += volume;
id(ph_down_daily_volume).publish_state(id(ph_down_volume_value));
id(bridge_status_string) = "PH-Down Dosierung: " + volume_str + " ml";
id(bridge_status).publish_state(id(bridge_status_string));
}
}
else if (data.find(":NUTRIENT:") != std::string::npos) {
size_t val_pos = data.find(":NUTRIENT:") + 10;
std::string volume_str = data.substr(val_pos);
float volume = atof(volume_str.c_str());
if (volume > 0.0) {
id(nutrient_volume_value) += volume;
id(nutrient_daily_volume).publish_state(id(nutrient_volume_value));
id(bridge_status_string) = "Dünger Dosierung: " + volume_str + " ml";
id(bridge_status).publish_state(id(bridge_status_string));
}
}
}
else if (data.compare(0, 18, "ATLAS:BRIDGE:READY") == 0) {
id(bridge_status_string) = "Bridge verbunden";
id(bridge_status).publish_state(id(bridge_status_string));
}
// Buffer leeren
buffer.clear();
}
} else if (c != '\r') {
// Zeilenumbrüche ignorieren
buffer += static_cast<char>(c);
}
}
}
# Automatische Steuerung von pH-Wert und EC-Wert (basierend auf echten Werten)
- interval: 15min
then:
- lambda: |-
// Nur ausführen, wenn der Wasserstand OK ist
if (!id(water_level).state) {
return;
}
// pH-Wert Steuerung
if (id(ph_value) > 6.5 && !id(ph_pump_state)) {
// pH-Wert zu hoch, pH-Down dosieren
id(bridge_status_string) = "Auto: pH-Wert zu hoch";
id(bridge_status).publish_state(id(bridge_status_string));
std::string cmd = "DOSE:PHDOWN:2.0\n";
id(arduino_serial).write_array((const uint8_t*)cmd.c_str(), cmd.length());
}
// EC-Wert Steuerung
if (id(ec_value) < 1200 && !id(nutrient_pump_state)) {
// EC-Wert zu niedrig, Nährstoffe dosieren
id(bridge_status_string) = "Auto: EC-Wert zu niedrig";
id(bridge_status).publish_state(id(bridge_status_string));
std::string cmd = "DOSE:NUTRIENT:3.0\n";
id(arduino_serial).write_array((const uint8_t*)cmd.c_str(), cmd.length());
}
Hier der Code für den Arduino:
/**
* Arduino Bridge für Atlas Scientific Sensoren (speicheroptimiert)
*/
#include <Wire.h>
// I2C-Adressen der EZO-Module
#define RTD_ADDR 0x66 // Temperatur
#define PH_ADDR 0x63 // pH-Wert
#define EC_ADDR 0x64 // EC-Wert
#define PH_PUMP_ADDR 0x67 // pH-Down Pumpe
#define NUT_PUMP_ADDR 0x68 // Nährstoff-Pumpe
void setup() {
Serial.begin(115200);
Wire.begin();
delay(1000);
Serial.println("ATLAS:BRIDGE:READY");
}
void loop() {
// Lese die Sensoren
float temp = readSensor(RTD_ADDR, 600);
float ph = readSensor(PH_ADDR, 900);
float ec = readSensor(EC_ADDR, 900);
// Sende Daten
Serial.print("ATLAS:DATA:TEMP:");
Serial.print(temp);
Serial.print(":PH:");
Serial.print(ph);
Serial.print(":EC:");
Serial.print(ec);
Serial.print(":PHPUMP:0:NUTPUMP:0");
Serial.println();
// Auf Befehle prüfen
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
if (cmd.startsWith("DOSE:PHDOWN:")) {
float vol = cmd.substring(12).toFloat();
dosePump(PH_PUMP_ADDR, vol, "PHDOWN");
}
else if (cmd.startsWith("DOSE:NUTRIENT:")) {
float vol = cmd.substring(14).toFloat();
dosePump(NUT_PUMP_ADDR, vol, "NUTRIENT");
}
else if (cmd == "STATUS:REQ") {
// Sende Daten erneut
}
}
// Warte 2 Sekunden bis zum nächsten Zyklus
delay(2000);
}
// Funktion zum Lesen eines Sensors
float readSensor(byte addr, int delayTime) {
// Sende Lesebefehl
Wire.beginTransmission(addr);
Wire.write("R");
if (Wire.endTransmission() != 0) {
return -999.0; // Fehler
}
// Warte auf Antwort
delay(delayTime);
// Lese Antwort
Wire.requestFrom((int)addr, (int)20);
if (!Wire.available()) {
return -999.0; // Keine Antwort
}
// Erstes Byte ist der Status
byte status = Wire.read();
// Lese die restlichen Bytes
char data[20];
byte i = 0;
while (Wire.available() && i < 19) {
data[i] = Wire.read();
i++;
}
data[i] = '\0'; // String-Terminierung
// Konvertiere zu Fließkommazahl
return atof(data);
}
// Funktion zum Dosieren mit einer Pumpe
void dosePump(byte addr, float vol, const char* pumpName) {
if (vol <= 0.0 || vol > 20.0) {
Serial.println("ATLAS:ERROR:INVALID_VOLUME");
return;
}
// Sende Dosierbefehl
Wire.beginTransmission(addr);
Wire.print("D,");
Wire.print(vol);
byte error = Wire.endTransmission();
if (error == 0) {
Serial.print("ATLAS:DOSING:");
Serial.print(pumpName);
Serial.print(":");
Serial.println(vol);
} else {
Serial.print("ATLAS:ERROR:");
Serial.print(pumpName);
Serial.print("_COMM:");
Serial.println(error);
}
}
Hier die manuelle Karte für das Dashboard:
type: vertical-stack
cards:
# Sensor values
- type: entities
title: NFT Sensorwerte
entities:
- entity: sensor.esphome_nft_nft_wassertemperatur
name: Temperatur
icon: mdi:thermometer
- entity: sensor.esphome_nft_nft_ph_wert
name: pH-Wert
icon: mdi:flask
- entity: sensor.esphome_nft_nft_ec_wert
name: EC-Wert
icon: mdi:water-opacity
# System status
- type: glance
title: Systemstatus
entities:
- entity: binary_sensor.esphome_nft_wasserstand_ok
name: Wasserstand
icon: mdi:water-check
- entity: binary_sensor.esphome_nft_ph_down_pumpe_aktiv
name: pH-Down Pumpe
icon: mdi:water-pump
- entity: binary_sensor.esphome_nft_d_nger_pumpe_aktiv
name: Dünger Pumpe
icon: mdi:water-pump
# Manual Control
- type: entities
title: Manuelle Steuerung
show_header_toggle: false
entities:
- entity: switch.esphome_nft_ph_down_dosierung
name: pH-Down Pumpe aktivieren
icon: mdi:test-tube
- entity: switch.esphome_nft_d_nger_dosierung
name: Dünger Pumpe aktivieren
icon: mdi:nutrition
# History graph
- type: history-graph
title: Sensorverlauf
hours_to_show: 24
entities:
- entity: sensor.esphome_nft_nft_wassertemperatur
name: Temperatur
- entity: sensor.esphome_nft_nft_ph_wert
name: pH-Wert
- entity: sensor.esphome_nft_nft_ec_wert
name: EC-Wert
# Daily dosage
- type: entities
title: Tägliche Dosierung
entities:
- entity: sensor.esphome_nft_ph_down_zugabe_heute
name: pH-Down
icon: mdi:beaker
- entity: sensor.esphome_nft_d_nger_zugabe_heute
name: Dünger
icon: mdi:nutrition
Das System läuft jetzt erstmal, jetzt muss ich mir nur Gedanken zum EC-Wert machen, da dieser über die Zeit ansteigt, da Wasser verbraucht wird, und somit der Salzgehalt langsam steigt. Wenn ich somit mit einem EC-Wert von 1300 starte, habe ich nach zwei Woche (Wasser muss nachgefüllt werden), einen EC-Wert von 1500. Somit weiß ich leider nicht wie viele der guten Nährstoffe noch im Wasser vorhanden sind und wieviel davon Salz ist. Hat jemand von euch eine Idee, außer Wasserwechsel? Ich habe extra einen Filter (Poolfilter fein zweilagig, darunter Vulkansteine, darunter Aktivkohle), gebaut um den Wasserwechsel zu reduzieren.