Otomatik bir sulama sistemi, her yağmurda bile zamanlayıcıyla çalışmak yerine yalnızca toprak gerçekten kuruduğunda ve yağmur beklenmediğinde sulama yapar. Bu eğitimde toprak nemi ölçümlerini, ücretsiz bir hava tahminini ve MQTT ile kontrol edilen solenoid vanaları birbirine bağlayacaksınız: kararları bir Python kontrolcüsü verir, ESP32 vana düğümleri ise bunları katı güvenlik sınırlarıyla uygular; böylece kaybolan bir mesaj suyu asla açık bırakamaz.
Sistemin Parçaları Nasıl Bir Araya Gelir?
Soil sensors ---- telemetry ---->+
|
Weather API --> Python controller (decides) <---- MQTT broker ----> Dashboard
| ^
zone/<n>/set ---+ |
v |
ESP32 valve node --- zone/<n>/state
(relay -> 24 V AC or 12 V DC solenoid)
Her parçanın tek bir görevi vardır. Sensörler nemi bildirir. Kontrolcü nemi, hava tahminini ve bir zaman penceresini birleştirir. Vana düğümleri istenen süre boyunca açılır ve kendiliğinden kapanır. Her şey broker üzerinden geçtiği için diğer parçaları değiştirmeden bölge ekleyebilir, kontrolcüyü Node-RED ile değiştirebilir veya her şeyi telefondan izleyebilirsiniz. Akıllı tarım IoT rehberi daha geniş tasarımı ele alır; bu makaledeki nem ölçümleri ise toprak nemi sensörü MQTT eğitiminde yapılan düğümden gelir.
Topic'ler ve Payload'lar
Örnekler herkese açık test broker'ını kullanır. i8r3c6 değerini kendi rastgele dizenizle değiştirin; herkese açık alan paylaşımlıdır ve herkes tarafından okunabilir.
| Topic | Payload | Retained |
|---|---|---|
connectmq/demo/i8r3c6/irrigation/zone1/moisture |
{"moisture_pct":22.5} |
evet |
connectmq/demo/i8r3c6/irrigation/zone1/set |
{"state":"open","duration_s":600} veya {"state":"close"} |
hayır |
connectmq/demo/i8r3c6/irrigation/zone1/state |
{"state":"open","remaining_s":540} |
evet |
connectmq/demo/i8r3c6/irrigation/valves/status |
online / offline (son vasiyet) |
evet |
connectmq/demo/i8r3c6/irrigation/log |
{"zone":1,"action":"skip","reason":"rain_forecast"} |
hayır |
Komut her zaman bir süre taşır. Düz bir "open" ve sonradan gönderilen ayrı bir "close" kırılgan bir yaklaşımdır: kapatma mesajı kaybolursa bölge su altında kalır. Süre belirtildiğinde vana düğümü kendiliğinden kapanır.
Otomatik Sulama Sistemi için Karar Kuralları
Kod yazmadan önce kuralları yazıya dökün. Pratik bir başlangıç seti:
- Yalnızca izin verilen zaman pencerelerinde sulayın (buharlaşma ve rüzgâr en düşük olduğu için genellikle sabah erken saatler tercih edilir).
- Bir bölgeyi yalnızca nemi o bölgenin eşiğinin altındaysa sulayın.
- Bugün veya yarın için tahmin edilen yağış bir sınırı aşıyorsa sulamayı atlayın.
- Aynı bölgeyi asgari bir aralık içinde asla iki kez sulamayın.
- Her çalışmayı, hem kontrolcü hem de vana düğümü tarafından uygulanan maksimum bir süreyle sınırlayın.
- Nem verisi birkaç saatten eskiyse otomatik sulama yapmayın; bunun yerine uyarı verin.
Eşikler toprağa ve ürüne bağlıdır. Temkinli bir eşikle başlayın, bir çalışmadan sonra nemin ne kadar hızlı toparlandığını izleyin ve sıklık yerine süreyi ayarlayın.
Python Kontrolcüsü
Kontrolcü, paho-mqtt 2.x'i ve anahtar gerektirmeyen ücretsiz Open-Meteo tahmin API'sini kullanır. pip install paho-mqtt ile kurun.
import json
import time
import urllib.request
from datetime import datetime
import paho.mqtt.client as mqtt
BROKER = "broker.connectmq.com"
BASE = "connectmq/demo/i8r3c6/irrigation"
LAT, LON = 41.01, 28.97 # konumunuz
ZONES = {
1: {"threshold": 28.0, "duration_s": 600},
2: {"threshold": 30.0, "duration_s": 420},
}
WINDOWS = [(5, 8), (19, 21)] # yerel saatler, başlangıç dahil, bitiş hariç
RAIN_LIMIT_MM = 3.0
MIN_INTERVAL_S = 12 * 3600
STALE_S = 3 * 3600
moisture = {} # bölge -> (değer, zaman damgası)
last_run = {} # bölge -> zaman damgası
forecast_cache = {"ts": 0, "rain": None}
def get_rain_forecast_mm():
if time.time() - forecast_cache["ts"] < 3600:
return forecast_cache["rain"]
url = (f"https://api.open-meteo.com/v1/forecast?latitude={LAT}&longitude={LON}"
"&daily=precipitation_sum&forecast_days=2&timezone=auto")
try:
with urllib.request.urlopen(url, timeout=10) as r:
daily = json.load(r)["daily"]["precipitation_sum"]
rain = max(v or 0.0 for v in daily)
except Exception as exc:
print("forecast unavailable:", exc)
rain = None
forecast_cache.update(ts=time.time(), rain=rain)
return rain
def log(client, **fields):
client.publish(f"{BASE}/log", json.dumps(fields), qos=1)
print(fields)
def on_connect(client, userdata, flags, reason_code, properties):
print("connected:", reason_code)
client.subscribe(f"{BASE}/+/moisture", qos=1)
client.subscribe(f"{BASE}/+/state", qos=1)
def on_message(client, userdata, msg):
parts = msg.topic.split("/")
zone_name, kind = parts[-2], parts[-1]
if not zone_name.startswith("zone"):
return
zone = int(zone_name[4:])
data = json.loads(msg.payload)
if kind == "moisture":
moisture[zone] = (float(data["moisture_pct"]), time.time())
elif kind == "state":
print(f"zone {zone} state:", data)
def in_window(now):
return any(start <= now.hour < end for start, end in WINDOWS)
def evaluate(client):
now = datetime.now()
if not in_window(now):
return
rain = get_rain_forecast_mm()
for zone, cfg in ZONES.items():
if time.time() - last_run.get(zone, 0) < MIN_INTERVAL_S:
continue
value, ts = moisture.get(zone, (None, 0))
if value is None or time.time() - ts > STALE_S:
log(client, zone=zone, action="alert", reason="stale_moisture")
last_run[zone] = time.time() # uyarıyı her döngüde tekrarlama
continue
if value >= cfg["threshold"]:
continue
if rain is not None and rain >= RAIN_LIMIT_MM:
log(client, zone=zone, action="skip", reason="rain_forecast", rain_mm=rain)
last_run[zone] = time.time()
continue
cmd = {"state": "open", "duration_s": cfg["duration_s"]}
client.publish(f"{BASE}/zone{zone}/set", json.dumps(cmd), qos=1)
last_run[zone] = time.time()
log(client, zone=zone, action="water", moisture=value, duration_s=cfg["duration_s"])
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="irrigation-ctl-i8r3c6")
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER, 1883, keepalive=60)
client.loop_start()
while True:
evaluate(client)
time.sleep(300)
Bu basit sürümde bölgeler yalnızca sırayla değerlendirildikleri için tek tek sulanır; su kaynağınız iki bölgeyi aynı anda besleyemiyorsa, retained state mesajlarını kullanarak bir sonraki komutu göndermeden önce başka bir bölgenin açık olmadığını kontrol eden bir denetim ekleyin.
ESP32 Vana Düğümü
Vana düğümü kendi bölgelerine abone olur, katı bir maksimum süre uygular, broker bağlantısı bir dakikadan uzun süre kesilirse her şeyi kapatır ve durumu bildirir. PubSubClient ve ArduinoJson 7 kullanır.
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const char* CLIENT_ID = "valves-i8r3c6";
const char* BASE = "connectmq/demo/i8r3c6/irrigation";
const int NUM_ZONES = 2;
const int VALVE_PIN[NUM_ZONES] = {26, 27};
const uint32_t MAX_RUN_S = 1800; // mutlak üst sınır
const uint32_t OFFLINE_CLOSE_MS = 60000;
uint32_t closeAt[NUM_ZONES] = {0, 0}; // millis() son tarihi, 0 = kapalı
uint32_t lastConnected = 0;
uint32_t lastReport = 0;
WiFiClient net;
PubSubClient mqtt(net);
void publishState(int z) {
char topic[96], buf[64];
snprintf(topic, sizeof(topic), "%s/zone%d/state", BASE, z + 1);
uint32_t rem = closeAt[z] ? (closeAt[z] - millis()) / 1000 : 0;
snprintf(buf, sizeof(buf), "{\"state\":\"%s\",\"remaining_s\":%lu}",
closeAt[z] ? "open" : "closed", (unsigned long)rem);
mqtt.publish(topic, buf, true);
}
void setValve(int z, bool open, uint32_t seconds) {
if (open) {
seconds = min(seconds, MAX_RUN_S);
closeAt[z] = millis() + seconds * 1000UL;
if (closeAt[z] == 0) closeAt[z] = 1;
} else {
closeAt[z] = 0;
}
digitalWrite(VALVE_PIN[z], open ? HIGH : LOW);
if (mqtt.connected()) publishState(z);
}
void onMessage(char* topic, byte* payload, unsigned int len) {
int z = -1;
for (int i = 0; i < NUM_ZONES; i++) {
char t[96];
snprintf(t, sizeof(t), "%s/zone%d/set", BASE, i + 1);
if (strcmp(topic, t) == 0) z = i;
}
if (z < 0) return;
JsonDocument doc;
if (deserializeJson(doc, payload, len)) return;
const char* state = doc["state"] | "close";
uint32_t dur = doc["duration_s"] | 0;
setValve(z, strcmp(state, "open") == 0 && dur > 0, dur);
}
void connectMqtt() {
char willTopic[96];
snprintf(willTopic, sizeof(willTopic), "%s/valves/status", BASE);
if (mqtt.connect(CLIENT_ID, willTopic, 1, true, "offline")) {
mqtt.publish(willTopic, "online", true);
for (int i = 0; i < NUM_ZONES; i++) {
char t[96];
snprintf(t, sizeof(t), "%s/zone%d/set", BASE, i + 1);
mqtt.subscribe(t, 1);
publishState(i);
}
}
}
void setup() {
for (int i = 0; i < NUM_ZONES; i++) {
pinMode(VALVE_PIN[i], OUTPUT);
digitalWrite(VALVE_PIN[i], LOW); // açılışta kapalı
}
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
mqtt.setServer(MQTT_HOST, 1883);
mqtt.setCallback(onMessage);
}
void loop() {
if (WiFi.status() == WL_CONNECTED && !mqtt.connected()) {
static uint32_t lastTry = 0;
if (millis() - lastTry > 5000) { lastTry = millis(); connectMqtt(); }
}
if (mqtt.connected()) lastConnected = millis();
mqtt.loop();
for (int i = 0; i < NUM_ZONES; i++) {
if (closeAt[i] && (int32_t)(millis() - closeAt[i]) >= 0) setValve(i, false, 0);
if (closeAt[i] && millis() - lastConnected > OFFLINE_CLOSE_MS) setValve(i, false, 0);
}
if (mqtt.connected() && millis() - lastReport > 30000) {
lastReport = millis();
for (int i = 0; i < NUM_ZONES; i++) if (closeAt[i]) publishState(i);
}
}
Solenoidleri, vananın voltajına ve akımına uygun değerde bir röle modülü veya MOSFET kartı üzerinden sürün; DC solenoidlerde bir flyback diyotu kullanın. Bahçe vanalarının çoğu 24 V AC'dir; pille çalışan sistemlerde kullanılan mandallı (bistabil) vanalar, sürekli enerjili tutulan bir röle yerine bir darbe sürücüsü gerektirir. ESP32 röle MQTT makalesi röle bağlantılarını ele alır.
Nem Verilerini Kontrolcüye Aktarma
Kontrolcü, zone<n>/moisture üzerinde bölge başına bir retained mesaj bekler. Buna ulaşmanın en basit yolu, toprak düğümünün telemetri topic'ini doğrudan bu yola ayarlamaktır. Sensörleriniz zaten başka bir yere yayın yapıyorsa, örneğin bir ağ sunucusu köprüsü üzerinden gelen LoRaWAN probları, köprüye eklenecek birkaç satır nem alanını bölge topic'ine yeniden yayınlayabilir. Sensör-bölge eşlemesini tek bir yerde, ideal olarak kontrolcünün yapılandırmasında tutun; böylece bir probun yerini değiştirmek hiçbir şeyin yeniden yüklenmesini gerektirmez.
Bir bölgede iki prob varsa ortalamalarını alın veya daha kuru olana göre sulayın, ancak ikisini de kaydedin. Sıfır okuyan tek bir arızalı prob kendi başına sulamayı tetikleyememelidir; eskimiş veri kuralı sessiz kalan probları, bir makullük aralığı (yüzde 1'in altındaki veya 99'un üstündeki değerleri yok saymak) ise bozuk olanları kapsar.
Eşikleri ve Çalışma Sürelerini Seçme
Eşik ne zaman sulanacağını, süre ise ne kadar sulanacağını söyler. Bunları bu sırayla ayarlayın. Bir hafta boyunca otomatik sulama olmadan nem eğrisini izleyin, bitkilerin strese girmiş görünmeye başladığı değeri not edin ve eşiği bunun birkaç puan üzerine ayarlayın. Ardından bir bölgeyi sabit bir süre elle çalıştırın ve nemin ne kadar ve ne hızla yükseldiğine bakın. Değer sıçrayıp birkaç saat içinde geri düşüyorsa su köklerin ötesine sızıyordur; daha kısa ve daha sık çalışmalar daha iyidir. Değer neredeyse hiç değişmiyorsa çalışma çok kısadır veya prob damlama alanının dışındadır.
Her değişikliği log topic'ine kaydedin. Bir sezon boyunca broker'a yayınlanan bu günlük, hangi ayarların işe yaradığının kanıtı haline gelir; ConnectMQ topic gezgini gibi canlı bir görünüm de kontrolcünün mantığını gerçekleştiği anda görmenizi kolaylaştırır.
Su Olmadan Test Etme
Bir hortum bağlamadan önce vana pinlerine LED takarak test edin:
# bölge 1 için sahte bir kuru ölçüm gönder
mosquitto_pub -h broker.connectmq.com -p 1883 -i t-i8r3c6 -r \
-t 'connectmq/demo/i8r3c6/irrigation/zone1/moisture' -m '{"moisture_pct":19}'
# bölge 1'i elle 20 saniyeliğine aç
mosquitto_pub -h broker.connectmq.com -p 1883 -i t-i8r3c6 \
-t 'connectmq/demo/i8r3c6/irrigation/zone1/set' -m '{"state":"open","duration_s":20}'
# her şeyi izle
mosquitto_sub -h broker.connectmq.com -p 1883 -i w-i8r3c6 \
-t 'connectmq/demo/i8r3c6/irrigation/#' -v
Ardından bir çalışma sırasında ESP32'nin Wi-Fi antenini çıkarın veya modemi kapatın ve vananın bir dakika içinde kapandığını doğrulayın. Bu tek test, her türlü kod incelemesinden daha değerlidir.
Sağlamlaştırma Kontrol Listesi
- Vana açılışta, süre dolduğunda, broker bağlantısı kaybolduğunda ve hatalı biçimlendirilmiş her komutta kapanır.
- Ana hattaki bir debimetre suyun gerçekten aktığını doğrular; çalışma başına litreyi yayınlayın ve tüm vanalar kapalıyken akış olursa (patlamış bir boru) uyarı verin.
- Komutlar asla retained olmaz, durumlar her zaman retained olur.
- Her kontrolcü ve vana düğümünün TLS'li özel bir broker üzerinde kendi kimlik bilgileri vardır.
- Elle müdahale imkânı bulunur: fiziksel bir anahtar veya
{"state":"close"}yayınlayan bir pano düğmesi.
Üretim Kurulumu
Gerçek vanaları bağlamadan önce herkese açık test alanını özel bir broker ile değiştirin, çünkü herkes açık bir set topic'ine yayın yapabilir. ESP32'de 8883 portunda ISRG Root X1 CA ile WiFiClientSecure ve mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", ...) kullanın; Python'da client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD") ve client.tls_set() çağırıp 8883 portuna bağlanın. Yönetilen bir MQTT broker, çiftlikteki bilgisayar yeniden başladığında bile broker'ı erişilebilir tutar; bulut MQTT broker ise hücresel bağlantı üzerindeki saha düğümlerinin VPN olmadan ona ulaşmasını sağlar. Sulamanın yanında sera iklim kontrolü için sera otomasyonu yazısına, daha fazla örnek için IoT otomasyon rehberine göz atın.
ConnectMQ ile Daha Akıllı Sulayın
ConnectMQ, sulama sisteminize TLS'li özel bir MQTT broker, her komutu ve onayı izlemek için canlı bir topic gezgini ve kuru bölgeler ya da beklenmeyen akış için eşik uyarı kuralları sunar. app.connectmq.com adresinde kredi kartı gerektirmeden ücretsiz bir Serverless broker oluşturun ve kontrolcünüzü ve vanalarınızı ona taşıyın.
Sık Sorulan Sorular
Bir vana açıkken MQTT bağlantısı koparsa ne olur?
Vana düğümü, broker bağlantısı olmadan geçen 60 saniyenin ardından açık olan tüm vanaları kapatır ve her çalışmanın bir süre sınırı vardır; dolayısıyla su süresiz akamaz.
Python kontrolcüsü yerine Home Assistant veya Node-RED kullanabilir miyim?
Evet. Nem topic'lerine abone olan ve aynı JSON komutunu yayınlayan her şey çalışır; vana düğümü komutu kimin gönderdiğiyle ilgilenmez.
Kararı hava tahmini mi yoksa toprak sensörü mü vermeli?
İkisini birlikte kullanın. Toprak sensörü şu anda suya ihtiyaç olup olmadığını, hava tahmini ise doğanın yakında bu suyu sağlayıp sağlamayacağını söyler. Sulamayı yalnızca ikisi de aynı fikirde olduğunda atlayın.
Bir ESP32 kaç bölgeyi kontrol edebilir?
Boş çıkış pininiz ve röle kanalınız kadar; genellikle dört ile sekiz arası. Su basıncınız birkaç bölgeyi aynı anda besleyemiyorsa bölgeleri tek tek çalıştırın.



