############################################################################### # # # ESPHome Automatische Bewässerung - KALIBRIERT # # # # Sensor-Kalibrierung (FINAL): # # - Sensor 1 Trocken: 2,370 V | Nass: 1,650 V # # - Sensor 2 Trocken: 2,361 V | Nass: 1,550 V # # - Spannweite: 0,77 V # # # # Schwellenwerte (gieße wenn TROCKENER als): # # - Wachstum: 2,15 V (alle 3 Tage) # # - Vorblüte: 2,05 V (alle 2 Tage) # # - Blüte: 1,95 V (alle 2 Tage) # # - Hauptblüte: 1,85 V (alle 2 Tage) # # - Spätblüte: 1,85 V (alle 2 Tage) # # - Spülen: 1,85 V (alle 2 Tage) # # # # Version: 4.0 (Kalibriert 2025-10-18) # # # ############################################################################### substitutions: # I²C Pins SDA_PIN: GPIO4 SCL_PIN: GPIO5 # I²C Adressen MCP23017_I2C_ADDRESS: "0x20" ADS1115_I2C_ADDRESS: "0x48" # Namen der Ausgänge OUTPUT1_NAME: "Pumpe 1" OUTPUT2_NAME: "Pumpe 2" # Namen der Sensoren ANALOG_IN1_NAME: "Feuchtigkeit Sensor 1" ANALOG_IN2_NAME: "Feuchtigkeit Sensor 2" WASSERSCHWIMMER_NAME: "Wassertank Schwimmer" # KALIBRIERUNG: Hier deine gemessene Pumpenleistung eintragen! PUMP_ML_PER_SECOND: "40.0" # ml/Sekunde (NACH MESSUNG ANPASSEN!) esphome: name: bewaesserung friendly_name: Bewässerung comment: "Automatische Bewässerung mit Grow-Kalender (2 Pflanzen)" esp8266: board: esp01_1m restore_from_flash: true logger: level: INFO logs: sensor: INFO i2c: WARN switch: INFO api: ota: platform: esphome # NTP Zeit-Synchronisation time: - platform: sntp id: sntp_time timezone: Europe/Berlin servers: - 0.pool.ntp.org - 1.pool.ntp.org - 2.pool.ntp.org on_time_sync: then: - logger.log: "⏰ Zeit synchronisiert" - script.execute: update_grow_phase # Tägliche Bewässerungsprüfung um 08:00 Uhr on_time: - seconds: 0 minutes: 0 hours: 8 then: - logger.log: "🔔 Tägliche Bewässerungsprüfung" - script.execute: daily_watering_check wifi: networks: - ssid: !secret wifi_ssid password: !secret wifi_password min_auth_mode: WPA2 output_power: 20dB reboot_timeout: 0s fast_connect: on ap: ssid: "Bewaesserung Fallback" password: !secret ap_password captive_portal: web_server: port: 80 # I²C Bus i2c: sda: ${SDA_PIN} scl: ${SCL_PIN} id: i2c_component frequency: 50kHz scan: true # 16x Binary GPIO Portexpander mcp23017: - id: mcp1 address: ${MCP23017_I2C_ADDRESS} # 4x Analog Input Portexpander ads1115: - address: ${ADS1115_I2C_ADDRESS} id: ads1 continuous_mode: true ############################################################################### # GLOBALE VARIABLEN ############################################################################### globals: # Startdatum als Unix Timestamp - id: grow_start_timestamp type: int restore_value: yes initial_value: '0' # Aktuelle Wachstumsphase # 0=Nicht gestartet, 1=Wachstum, 2=Vorblüte, 3=Blüte, 4=Hauptblüte, 5=Spätblüte, 6=Spülen, 7=Ernte - id: current_phase type: int restore_value: yes initial_value: '0' # Aktueller Grow-Tag - id: current_grow_day type: int restore_value: yes initial_value: '0' # Letzter Bewässerungstag (zur Intervall-Kontrolle) - id: last_watering_day type: int restore_value: yes initial_value: '-999' # Kalibrierte Pumpenleistung (kann über HA angepasst werden) - id: pump_calibration type: float restore_value: yes #initial_value: '${PUMP_ML_PER_SECOND}' ############################################################################### # BINARY SENSOREN ############################################################################### binary_sensor: # Wassertank Schwimmer - platform: gpio name: ${WASSERSCHWIMMER_NAME} id: wasserstand device_class: moisture pin: mcp23xxx: mcp1 number: 7 mode: input: true pullup: true inverted: true filters: - delayed_on: 100ms - delayed_off: 100ms on_press: - logger.log: "✅ Wassertank: VOLL" on_release: then: - script.execute: notaus - logger.log: format: "🚨 NOTAUS: Wassertank LEER!" level: ERROR # Überwässerungs-Warnung Topf 1 - platform: template name: "⚠️ Topf 1 zu nass" id: topf1_zu_nass icon: "mdi:water-alert" device_class: problem lambda: |- float h = id(feuchtigkeit1).state; if (isnan(h)) return false; return h < 1.5; filters: - delayed_on: 300s # Überwässerungs-Warnung Topf 2 - platform: template name: "⚠️ Topf 2 zu nass" id: topf2_zu_nass icon: "mdi:water-alert" device_class: problem lambda: |- float h = id(feuchtigkeit2).state; if (isnan(h)) return false; return h < 1.4; filters: - delayed_on: 300s # Status - platform: status name: "Status" ############################################################################### # PUMPEN (GPIO + Template-Switches mit Sicherheitsprüfung) ############################################################################### switch: # ═══════════════════════════════════════════════════════════════════════ # GPIO-Switches (intern, nicht in Home Assistant sichtbar) # ═══════════════════════════════════════════════════════════════════════ - platform: gpio id: pumpe1_gpio internal: true restore_mode: ALWAYS_OFF pin: mcp23xxx: mcp1 number: 8 mode: output: true inverted: false - platform: gpio id: pumpe2_gpio internal: true restore_mode: ALWAYS_OFF pin: mcp23xxx: mcp1 number: 9 mode: output: true inverted: false # ═══════════════════════════════════════════════════════════════════════ # Template-Switches (sichtbar in Home Assistant, mit Sicherheitsprüfung) # ═══════════════════════════════════════════════════════════════════════ - platform: template name: ${OUTPUT1_NAME} id: pumpe1 icon: "mdi:water-pump" optimistic: false lambda: |- return id(pumpe1_gpio).state; turn_on_action: - if: condition: binary_sensor.is_on: wasserstand then: - switch.turn_on: pumpe1_gpio - logger.log: "💧 Pumpe 1 EIN" else: - logger.log: format: "🚨 Pumpe 1 blockiert - Wassertank leer!" level: ERROR turn_off_action: - switch.turn_off: pumpe1_gpio - logger.log: "⚫ Pumpe 1 AUS" - platform: template name: ${OUTPUT2_NAME} id: pumpe2 icon: "mdi:water-pump" optimistic: false lambda: |- return id(pumpe2_gpio).state; turn_on_action: - if: condition: binary_sensor.is_on: wasserstand then: - switch.turn_on: pumpe2_gpio - logger.log: "💧 Pumpe 2 EIN" else: - logger.log: format: "🚨 Pumpe 2 blockiert - Wassertank leer!" level: ERROR turn_off_action: - switch.turn_off: pumpe2_gpio - logger.log: "⚫ Pumpe 2 AUS" # Automatische Bewässerung aktivieren/deaktivieren - platform: template name: "Auto-Bewässerung" id: auto_watering_enabled restore_mode: RESTORE_DEFAULT_ON optimistic: true icon: "mdi:water-pump-auto" on_turn_on: - logger.log: "✅ Auto-Bewässerung aktiviert" on_turn_off: - logger.log: "⏸️ Auto-Bewässerung deaktiviert" ############################################################################### # SENSOREN ############################################################################### sensor: # Bodenfeuchtesensoren (ADS1115) - platform: ads1115 ads1115_id: ads1 gain: 6.144 multiplexer: 'A0_GND' name: ${ANALOG_IN1_NAME} id: feuchtigkeit1 update_interval: 60s unit_of_measurement: "V" accuracy_decimals: 3 device_class: voltage state_class: measurement filters: - sliding_window_moving_average: window_size: 5 send_every: 1 - platform: ads1115 ads1115_id: ads1 gain: 6.144 multiplexer: 'A1_GND' name: ${ANALOG_IN2_NAME} id: feuchtigkeit2 update_interval: 60s unit_of_measurement: "V" accuracy_decimals: 3 device_class: voltage state_class: measurement filters: - sliding_window_moving_average: window_size: 5 send_every: 1 # Feuchtigkeit in Prozent (Topf 1) - KALIBRIERT - platform: template name: "Feuchtigkeit Topf 1 (%)" id: feuchtigkeit1_prozent icon: "mdi:water-percent" unit_of_measurement: "%" accuracy_decimals: 0 update_interval: 60s lambda: |- float v = id(feuchtigkeit1).state; if (isnan(v)) return NAN; // ✅ KALIBRIERT: 2,370V = 0%, 1,650V = 100% float trocken = 2.370; float nass = 1.650; float percent = ((trocken - v) / (trocken - nass)) * 100.0; if (percent < 0) return 0; if (percent > 100) return 100; return percent; # Feuchtigkeit in Prozent (Topf 2) - KALIBRIERT - platform: template name: "Feuchtigkeit Topf 2 (%)" id: feuchtigkeit2_prozent icon: "mdi:water-percent" unit_of_measurement: "%" accuracy_decimals: 0 update_interval: 60s lambda: |- float v = id(feuchtigkeit2).state; if (isnan(v)) return NAN; // ✅ KALIBRIERT: 2,361V = 0%, 1,550V = 100% float trocken = 2.361; float nass = 1.550; float percent = ((trocken - v) / (trocken - nass)) * 100.0; if (percent < 0) return 0; if (percent > 100) return 100; return percent; # WiFi Signal - platform: wifi_signal name: "WiFi Signal" update_interval: 60s filters: - sliding_window_moving_average: window_size: 3 # Uptime - platform: uptime name: "Uptime" update_interval: 60s # Grow-Tag - platform: template name: "Grow Tag" id: grow_day_sensor icon: "mdi:calendar-today" unit_of_measurement: "Tag" accuracy_decimals: 0 update_interval: 600s lambda: |- return id(current_grow_day); # Nächste Bewässerung in X Tagen - platform: template name: "Nächste Bewässerung in" icon: "mdi:calendar-clock" unit_of_measurement: "Tagen" accuracy_decimals: 0 update_interval: 600s lambda: |- if (id(current_phase) == 0 || id(current_phase) == 7) { return 0; } int phase = id(current_phase); int interval = 2; // Standard: alle 2 Tage if (phase == 1) interval = 3; // Wachstum: alle 3 Tage int days_since = id(current_grow_day) - id(last_watering_day); int days_until = interval - days_since; if (days_until < 0) return 0; return days_until; ############################################################################### # TEXT SENSOREN ############################################################################### text_sensor: - platform: version name: "ESPHome Version" hide_timestamp: true - platform: wifi_info ip_address: name: "IP Adresse" ssid: name: "SSID" mac_address: name: "MAC Adresse" # Wachstumsphase als Text - platform: template name: "Wachstumsphase" id: phase_text icon: "mdi:sprout" update_interval: 60s lambda: |- int phase = id(current_phase); if (phase == 0) return {"Nicht gestartet"}; else if (phase == 1) return {"Wachstum (Tag 20-35)"}; else if (phase == 2) return {"Vorblüte (Tag 36-42)"}; else if (phase == 3) return {"Blüte (Tag 43-56)"}; else if (phase == 4) return {"Hauptblüte (Tag 57-70)"}; else if (phase == 5) return {"Spätblüte (Tag 71-77)"}; else if (phase == 6) return {"Spülen (Tag 78-85)"}; else if (phase == 7) return {"🎉 Ernte bereit!"}; return {"Unbekannt"}; # Startdatum als lesbarer Text - platform: template name: "Grow Startdatum" id: start_date_text icon: "mdi:calendar-start" update_interval: 3600s lambda: |- if (id(grow_start_timestamp) == 0) { return {"Nicht gesetzt"}; } char buffer[20]; time_t start = id(grow_start_timestamp); strftime(buffer, sizeof(buffer), "%d.%m.%Y", localtime(&start)); return {buffer}; ############################################################################### # NUMBER INPUTS ############################################################################### number: # Startdatum Eingabe - platform: template name: "Grow Start Tag" id: grow_start_day_input icon: "mdi:calendar-edit" mode: box min_value: 1 max_value: 31 step: 1 optimistic: true - platform: template name: "Grow Start Monat" id: grow_start_month_input icon: "mdi:calendar-edit" mode: box min_value: 1 max_value: 12 step: 1 optimistic: true - platform: template name: "Grow Start Jahr" id: grow_start_year_input icon: "mdi:calendar-edit" mode: box min_value: 2024 max_value: 2030 step: 1 optimistic: true initial_value: 2025 # Pumpen-Kalibrierung - platform: template name: "Pumpenleistung (ml/s)" id: pump_ml_per_sec_input icon: "mdi:pump" mode: box min_value: 10 max_value: 200 step: 0.1 initial_value: ${PUMP_ML_PER_SECOND} optimistic: true on_value: then: - lambda: |- id(pump_calibration) = x; ESP_LOGI("calibration", "✅ Pumpenleistung: %.1f ml/s", x); ############################################################################### # BUTTONS ############################################################################### button: # Startdatum setzen - platform: template name: "Startdatum setzen" icon: "mdi:calendar-check" on_press: then: - lambda: |- int day = (int)id(grow_start_day_input).state; int month = (int)id(grow_start_month_input).state; int year = (int)id(grow_start_year_input).state; if (day < 1 || day > 31 || month < 1 || month > 12 || year < 2024) { ESP_LOGE("grow", "❌ Ungültiges Datum: %d.%d.%d", day, month, year); return; } struct tm timeinfo; timeinfo.tm_year = year - 1900; timeinfo.tm_mon = month - 1; timeinfo.tm_mday = day; timeinfo.tm_hour = 0; timeinfo.tm_min = 0; timeinfo.tm_sec = 0; timeinfo.tm_isdst = -1; time_t timestamp = mktime(&timeinfo); id(grow_start_timestamp) = timestamp; ESP_LOGI("grow", "✅ Startdatum: %02d.%02d.%d", day, month, year); - script.execute: update_grow_phase # Grow zurücksetzen - platform: template name: "Grow zurücksetzen" icon: "mdi:restart" on_press: then: - lambda: |- id(grow_start_timestamp) = 0; id(current_phase) = 0; id(current_grow_day) = 0; id(last_watering_day) = -999; ESP_LOGI("grow", "🔄 Grow zurückgesetzt"); # Manuelle Bewässerung jetzt - platform: template name: "Jetzt bewässern" icon: "mdi:water" on_press: then: - logger.log: "🔵 Manuelle Bewässerung gestartet" - script.execute: daily_watering_check # Kalibrierungs-Buttons - platform: template name: "🧪 Test Pumpe 1 (10s)" on_press: then: - logger.log: "🧪 Kalibrierung: Pumpe 1 für 10 Sekunden" - if: condition: binary_sensor.is_on: wasserstand then: - switch.turn_on: pumpe1_gpio - delay: 10s - switch.turn_off: pumpe1_gpio - logger.log: "✅ Test beendet - Miss das Wasser!" else: - logger.log: format: "🚨 Test abgebrochen - Kein Wasser!" level: ERROR - platform: template name: "🧪 Test Pumpe 2 (10s)" on_press: then: - logger.log: "🧪 Kalibrierung: Pumpe 2 für 10 Sekunden" - if: condition: binary_sensor.is_on: wasserstand then: - switch.turn_on: pumpe2_gpio - delay: 10s - switch.turn_off: pumpe2_gpio - logger.log: "✅ Test beendet - Miss das Wasser!" else: - logger.log: format: "🚨 Test abgebrochen - Kein Wasser!" level: ERROR - platform: template name: "🧪 Test 250ml (beide Pumpen)" on_press: then: - lambda: |- // Jede Pumpe hat die Hälfte der Gesamtleistung float pump_ml_per_sec_single = id(pump_calibration) / 2.0; int duration = (int)(250.0 / pump_ml_per_sec_single); ESP_LOGI("test", "🧪 Test: 250ml pro Pumpe @ %.1f ml/s = %d Sekunden", pump_ml_per_sec_single, duration); - script.execute: id: pumpe1_on duration: !lambda "return (int)(250.0 / (id(pump_calibration) / 2.0));" - script.execute: id: pumpe2_on duration: !lambda "return (int)(250.0 / (id(pump_calibration) / 2.0));" # Notaus Button - platform: template name: "🚨 NOTAUS" icon: "mdi:alert-octagon" on_press: then: - script.execute: notaus - logger.log: format: "🚨 NOTAUS ausgelöst!" level: WARN # Restart Button - platform: restart name: "Neustart" icon: "mdi:restart" ############################################################################### # SCRIPTS ############################################################################### script: # Notaus - alle Pumpen sofort ausschalten - id: notaus mode: restart then: - switch.turn_off: pumpe1_gpio - switch.turn_off: pumpe2_gpio - logger.log: format: "🚨 NOTAUS: Alle Pumpen AUS" level: ERROR # Wachstumsphase basierend auf aktuellem Datum berechnen - id: update_grow_phase mode: restart then: - lambda: |- if (id(grow_start_timestamp) == 0) { id(current_phase) = 0; id(current_grow_day) = 0; return; } auto time = id(sntp_time).now(); if (!time.is_valid()) { ESP_LOGW("grow", "⏰ Zeit nicht synchronisiert"); return; } time_t now = time.timestamp; time_t start = id(grow_start_timestamp); int days = (now - start) / 86400; id(current_grow_day) = days; int old_phase = id(current_phase); // Phase basierend auf Tag bestimmen if (days < 20) { id(current_phase) = 0; } else if (days >= 20 && days <= 35) { id(current_phase) = 1; // Wachstum } else if (days >= 36 && days <= 42) { id(current_phase) = 2; // Vorblüte } else if (days >= 43 && days <= 56) { id(current_phase) = 3; // Blüte } else if (days >= 57 && days <= 70) { id(current_phase) = 4; // Hauptblüte } else if (days >= 71 && days <= 77) { id(current_phase) = 5; // Spätblüte } else if (days >= 78 && days <= 85) { id(current_phase) = 6; // Spülen } else if (days > 85) { id(current_phase) = 7; // Ernte } if (old_phase != id(current_phase)) { ESP_LOGI("grow", "🌱 Phase %d → %d (Tag %d)", old_phase, id(current_phase), days); } # Tägliche Bewässerungsprüfung (Hybrid-Ansatz mit kalibrierten Werten) - id: daily_watering_check mode: single then: - lambda: |- ESP_LOGI("watering", "═══════════════════════════════════════"); ESP_LOGI("watering", "🔔 Bewässerungsprüfung"); if (!id(auto_watering_enabled).state) { ESP_LOGW("watering", "⏸️ Auto-Bewässerung deaktiviert"); return; } if (id(current_phase) == 0) { ESP_LOGW("watering", "⏸️ Grow nicht gestartet"); return; } if (id(current_phase) == 7) { ESP_LOGW("watering", "🎉 Ernte bereit!"); return; } int phase = id(current_phase); int day = id(current_grow_day); // Bewässerungsintervall int interval_days = 2; if (phase == 1) interval_days = 3; int days_since = day - id(last_watering_day); if (days_since < interval_days) { ESP_LOGI("watering", "⏳ Noch %d Tag(e) bis Bewässerung", interval_days - days_since); return; } ESP_LOGI("watering", "💧 BEWÄSSERUNGSTAG (Tag %d, Phase %d)", day, phase); // ═══════════════════════════════════════════════════════════════ // ✅ KALIBRIERTE SCHWELLENWERTE (gieße wenn TROCKENER als) // ═══════════════════════════════════════════════════════════════ int target_ml = 500; float threshold = 2.05; // Standard if (phase == 1) { target_ml = 200; threshold = 2.15; // Wachstum: eher trocken } else if (phase == 2) { target_ml = 400; threshold = 2.05; // Vorblüte: mittel } else if (phase == 3) { target_ml = 500; threshold = 1.95; // Blüte: eher feucht } else if (phase == 4) { target_ml = 650; threshold = 1.85; // Hauptblüte: feucht } else if (phase == 5) { target_ml = 600; threshold = 1.85; // Spätblüte: feucht } else if (phase == 6) { target_ml = 600; threshold = 1.85; // Spülen: feucht } float pump_ml_per_sec = id(pump_calibration); int duration = (int)(target_ml / pump_ml_per_sec); ESP_LOGI("watering", "📊 Ziel: %dml, Dauer: %ds, Schwelle: %.2fV", target_ml, duration, threshold); bool watered = false; // Topf 1 float h1 = id(feuchtigkeit1).state; ESP_LOGI("watering", "🌱 Topf 1: %.3fV", h1); if (isnan(h1)) { ESP_LOGW("watering", "⚠️ Topf 1: Sensor defekt - gieße trotzdem"); id(pumpe1_on).execute(duration); watered = true; } else if (h1 > threshold) { ESP_LOGI("watering", "💧 Topf 1: %.3fV > %.2fV → Gieße %dml", h1, threshold, target_ml); id(pumpe1_on).execute(duration); watered = true; } else { ESP_LOGI("watering", "✅ Topf 1: %.3fV ≤ %.2fV - noch feucht", h1, threshold); } // Topf 2 float h2 = id(feuchtigkeit2).state; ESP_LOGI("watering", "🌱 Topf 2: %.3fV", h2); if (isnan(h2)) { ESP_LOGW("watering", "⚠️ Topf 2: Sensor defekt - gieße trotzdem"); id(pumpe2_on).execute(duration); watered = true; } else if (h2 > threshold) { ESP_LOGI("watering", "💧 Topf 2: %.3fV > %.2fV → Gieße %dml", h2, threshold, target_ml); id(pumpe2_on).execute(duration); watered = true; } else { ESP_LOGI("watering", "✅ Topf 2: %.3fV ≤ %.2fV - noch feucht", h2, threshold); } if (watered) { id(last_watering_day) = day; ESP_LOGI("watering", "✅ Bewässerung abgeschlossen"); } else { ESP_LOGI("watering", "ℹ️ Keine Bewässerung nötig"); } ESP_LOGI("watering", "═══════════════════════════════════════"); # Pumpen-Steuerung mit Sicherheitsprüfung - id: pumpe1_on mode: restart parameters: duration: int then: - if: condition: binary_sensor.is_on: wasserstand then: - logger.log: format: "💧 Pumpe 1 EIN für %d Sekunden" args: ['duration'] - switch.turn_on: pumpe1_gpio - delay: !lambda "return duration * 1000;" - switch.turn_off: pumpe1_gpio - logger.log: "✅ Pumpe 1 AUS" else: - logger.log: format: "🚨 Pumpe 1 BLOCKIERT - Kein Wasser!" level: ERROR - id: pumpe2_on mode: restart parameters: duration: int then: - if: condition: binary_sensor.is_on: wasserstand then: - logger.log: format: "💧 Pumpe 2 EIN für %d Sekunden" args: ['duration'] - switch.turn_on: pumpe2_gpio - delay: !lambda "return duration * 1000;" - switch.turn_off: pumpe2_gpio - logger.log: "✅ Pumpe 2 AUS" else: - logger.log: format: "🚨 Pumpe 2 BLOCKIERT - Kein Wasser!" level: ERROR ############################################################################### # INTERVALLE ############################################################################### interval: # Regelmäßiges Update der Wachstumsphase (alle 6 Stunden) - interval: 6h then: - script.execute: update_grow_phase