Nachdem ich nun die Bodenfeuchtigkeit meiner Topfpflanzen messen kann, möchte ich natürlich auch die Bewässerung automatisieren.
Ein wichtige Vorüberlegung dazu ist, dass dieses Projekt potentiell für eine Überschwemmung sorgen kann. Dem möchte ich mit verschiedenen Sicherheitsvorkehrungen entgegenwirken:
- Nicht zu oft gießen.
Egal, was die Feuchtigkeitssensoren sagen, die Pflanzen sollen nicht häufiger als alle zwei Tage gegossen werden. Diesen Mechanismus werde ich im Skript in ioBroker realisieren, welches das Signal zum Gießen der Pflanzen erzeugt. - Die Dauer der Pumpenansteuerung limitieren.
Wenn der ioBroker das Signal zum Gießen einer Pflanze gibt, soll die Pumpe auf jeden Fall wieder ausgeschaltet werden, auch wenn der ioBroker zwischendurch abstürzen sollte oder das WLAN ausfällt. D.h. auf dem steuernden ESP8266 wird eine Logik implementiert, die nach einer vorgegebenen Zeit die Pumpe wieder abschaltet. - Die Logik muss so konzipiert sein, dass die Pumpen nicht anspringen wenn der ESP8266 startet. D.h. ESPHome wird so eingestellt, dass die entsprechenden Ausgänge beim Starten auf Low gezogen werden. Solange die GPIOs als Eingang konfiguriert sind darf die Pumpe nicht laufen.
- Die Durchflussmenge sollte reduziert werden können, damit die Töpfe nicht überlaufen. Es darf also nicht schneller Wasser in den Topf gepumpt werden als in der Erde versickern kann.
Ursprünglich wollte ich zur Ansteuerung der Pumpen ein Relais-Modul verwenden. Die Durchflussmenge hätte ich in diesem Fall über ein Ventil reguliert. Das Relais-Modul habe ich bereits besorgt und auch ein Ventil habe ich bereits konstruiert und gedruckt.
Den Plan habe ich dann jedoch über den Haufen geworfen und die Ansteuerung der Pumpen über einen Transistor realisiert. Dadurch ist die Durchflussmenge über PWM-Steuerung der Pumpen möglich, außerdem ist die Schaltung dadurch günstiger und hat einen geringeren Stromverbrauch. Da die Stromversorgung bei mir nicht über Batterien sondern über Netzteil erfolgen soll, ist der Verbrauch zwar nicht so entscheidend und die ca. 50 mA zusätzlich für das Relais während der Aktivierung der Pumpen wären auch gut zu verkraften, aber nett ist das natürlich trotzdem.
Als Transistor habe ich einen S8050 gewählt, da dieser mit 500mA Collector-Strom (IC – angegeben bei Weitron) auf jeden Fall genug Leistung hat um die Motoren anzutreiben. Vermutlich hätte auch ein schwächerer Transistor genügt, da die Motoren mit max. 200mA bei 5V angegeben sind. Da Motoren jedoch eine induktive Last sind und damit der Einschaltstrom etwas höher ausfällt, habe ich lieber zu einem etwas zu hoch dimensionierten Bauteil gegriffen.
Für den Transistor ist eine Base-Emitter Saturation Voltage (VBE) von 1,2V angegeben. Damit ergibt sich, bei einer Spannung von 3,3V und einer maximalen Belastung von 12mA am GPIO des ESP8266 ein Basis-Vorwiderstand von mindestens (3,3V-1,2V)/0,012A=175 Ohm. Hier habe ich einen Widerstand von 220 Ohm (5%) gewählt, um den Ausgang nicht zu hoch zu belasten. Der Basis-Strom wird damit bei 9,1 – 11 mA liegen (gerechnet für VBE von 1,0 – 1,2V (1,0V angegeben bei UTC für IC von 10mA) und einem tatsächlichen Widerstandswert von 209 – 231 Ohm). Das reicht in jedem Fall um den Transistor bei einem Collector-Strom von max. 200mA voll durchzuschalten (niedrigster Verstärkungsfaktor hFE ist angegeben bei Weitron mit 85).
Zusätzlich wird noch eine Freilaufdiode zum Schutz des Transistors benötigt.
Bei ESPHome konnte ich dieses Mal leider nicht die API verwenden um die entsprechenden Elemente direkt im ESPHome-Adapter zur Verfügung zu haben, da der Adapter zur Zeit number noch nicht akzeptiert. Die Kommunikation muss deshalb über MQTT laufen. Dabei ist es ein wenig lästig, dass lesen und setzen eines Wertes über zwei unterschiedliche Topics laufen muss. Im ioBroker habe ich mir damit beholfen, zu den zwei Topics einen Alias zu definieren und dabei unterschiedliche Datenpunkte für Lesen und Schreiben zu definieren.
esphome: name: pflanzenwasser esp8266: board: d1_mini wifi: ssid: !secret wifi_ssid password: !secret wifi_password # Enable fallback hotspot (captive portal) in case wifi connection fails ap: ssid: !secret ap_ssid password: !secret ap_password ota: password: !secret ota_password # Enable logging logger: # Enable Home Assistant API #api: # password: !secret api_password # encryption: # key: !secret api_key mqtt: broker: 192.168.1.2 username: username topic_prefix: pflanzenwasser captive_portal: web_server: port: 80 sensor: - platform: wifi_signal name: "Wifi Power" update_interval: 10s state_topic: pflanzenwasser/wifi_power number: - platform: template name: "Dauer1" id: dauer1 unit_of_measurement: ms optimistic: true min_value: 0 max_value: 30000 step: 1 restore_value: true initial_value: 3500 state_topic: pflanzenwasser/dauer1 command_topic: pflanzenwasser/dauer1-set - platform: template name: "Dauer2" id: dauer2 unit_of_measurement: ms optimistic: true min_value: 0 max_value: 30000 step: 1 restore_value: true initial_value: 3500 state_topic: pflanzenwasser/dauer2 command_topic: pflanzenwasser/dauer2-set output: - platform: esp8266_pwm pin: D1 frequency: 200 Hz id: wasser1_pwm min_power: 0.3 max_power: 1.0 zero_means_zero: true - platform: esp8266_pwm pin: D2 frequency: 200 Hz id: wasser2_pwm min_power: 0.3 max_power: 1.0 zero_means_zero: true fan: - platform: speed id: wasser1 output: wasser1_pwm name: "Wasser1" on_turn_on: - delay: !lambda |- return id(dauer1).state; - fan.turn_off: wasser1 - platform: speed id: wasser2 output: wasser2_pwm name: "Wasser2" on_turn_on: - delay: !lambda |- return id(dauer2).state; - fan.turn_off: wasser2
Jetzt fehlt nur noch das Script im ioBroker, mit dem dann die Pumpen aktiviert werden. Hier mit einem schönen Gemisch aus Deutsch und Englisch:
var cacheSelectorBodenfeuchte = $('state[id=*](functions="Bodenfeuchte")'); let warning_timer = null; const warning_timeout = 240; // Minuten const watering = true; cacheSelectorBodenfeuchte.on (checkAllFeuchtigkeit); checkAllFeuchtigkeit (null); //cacheSelectorBodenfeuchte.each (function(id, i) { // console.log ("id: " + JSON.stringify (id)); // console.log ("i: " + JSON.stringify (i)); //}); function holePflanzen () { const obj = getObject("enum.functions.Bodenfeuchte"); if (obj === null) return []; let pflanzen = []; for (let i in obj.common.members) { const c = getObject (obj.common.members[i]).common; let warn_low = 40; let warn_high = 50; if ("warning_range" in c) { //console.log // ("Warning range: " // + JSON.stringify (c.warning_range)); warn_low = c.warning_range[0]; warn_high = c.warning_range[1]; } pflanzen.push ({ id: obj.common.members[i], warn_low: warn_low, warn_high: warn_high }); } return pflanzen; } async function checkAllFeuchtigkeit (obj) { let r = {finish: false, enough_water: true }; let pflanzen = holePflanzen (); let trockene_pflanzen = []; let trockene_pflanzen_ids = []; for (let i in pflanzen) { r = checkFeuchtigkeit (pflanzen[i], r.enough_water); if (r.finish) { trockene_pflanzen.push (pflanzen[i].id.replace (/^.*\.([^.]+)$/, "$1")); trockene_pflanzen_ids.push (pflanzen[i].id); } } if (trockene_pflanzen.length > 0) { if (watering) waterPlants (trockene_pflanzen_ids); if (warning_timer === null) { warning_timer = setTimeout (async function () { warning_timer = null; }, warning_timeout * 60 * 1000); let msg = 'Pflanzen benötigen Wasser: '; for (let i=0; i < trockene_pflanzen.length; ++i) { if (i > 0) msg += ', '; msg += trockene_pflanzen[i] + ' (' + String (getState( trockene_pflanzen_ids[i]).val) + '%)'; } sendTo ("telegram", "send", {text: msg, disable_notification: true}); } return; } if (r.enough_water) setState ("0_userdata.0.Warnings.Pflanze_ohne_Wasser", false, true); } function checkFeuchtigkeit (pflanze, previous_enough_water) { const f = getState (pflanze.id).val; let finish = false; if (f < pflanze.warn_low) { setState ("0_userdata.0.Warnings.Pflanze_ohne_Wasser", true, true); finish = true; } return { finish: finish, enough_water: previous_enough_water && f > pflanze.warn_high }; } var warning_timer_not_watering = null; function msToReadable (time) { if (time < 5) return String (time / 1000) + " s"; let s = Math.round (time / 1000); if (s < 60) return String (s) + " s"; let m = Math.floor (s / 60); s -= m * 60; if (m < 5) return String (m) + " min, " + String (s) + " s"; if (m < 60) return String (m) + " min"; let h = Math.floor (m / 60); m -= h * 60; if (h < 5) return String (h) + ":" + String (m) + " Stunden"; if (h < 24) return String (h) + " Stunden"; let d = Math.floor (h / 24); h -= d * 24; if (d < 5) return String (d) + " Tage, " + String (h) + " Stunden"; if (d < 14) return String (d) + " Tage"; let w = Math.floor (d / 7); d -= w * 7; if (w < 5) return String (w) + " Wochen, " + String (d) + " Tage"; return String (w) + " Wochen"; } function getWasserIdFromPflanzenId (pflanzen_id) { switch (trockene_pflanzen_ids[i]) { case "alias.0.Wohnzimmer.Bodenfeuchte_Bitterorange": return "alias.0.Wohnzimmer.Pflanzenbewässerung" + ".Bitterorange.state"; break; case "alias.0.Wohnzimmer.Bodenfeuchte_Schefflera": return "alias.0.Wohnzimmer.Pflanzenbewässerung" + ".Schefflera.state"; break; } return null; } function waterPlants (trockene_pflanzen_ids) { for (let i in trockene_pflanzen_ids) { let wasser_id = getWasserIdFromPflanzenId (trockene_pflanzen_ids[i]); if (wasser_id !== null) { let s = getState (wasser_id); let last = Date.now () - s.lc; let msg = null; if (last > 5 * 24 * 60 * 60 * 1000) { let szene = getState (SZENE_ID).val; if (getState ("0_userdata.0.Presence.Gast_anwesend") .val && (!compareTime ("10:00", "20:00", "between") || szene === SZENE_NACHT || szene === SZENE_FILM)) { msg = "Wasser für " + wasser_id + " nicht aktiviert wegen Gast" + " anwesend, Feuchte: " + String (getState (trockene_pflanzen_ids[i]).val) + "%"; } else { msg = "Wasser für " + wasser_id + " aktiviert, Feuchte: " + String (getState (trockene_pflanzen_ids[i]).val) + "%"; setState (wasser_id, true); } } else { if (warning_timer_not_watering === null) { warning_timer_not_watering = setTimeout (async function () { warning_timer_not_watering = null; }, warning_timeout * 60 * 1000); msg = "Wasser für " + wasser_id + " nicht aktiviert, Feuchte: " + String (getState (trockene_pflanzen_ids[i]).val) + "%, letzte Wässerung vor " + msToReadable (last); } } if (msg !== null) { console.log (msg); sendTo ("telegram", "send", {text: msg, disable_notification: true}); } } } }