Automação IoT

Sistema de Irrigação Automática com Controle MQTT: Solo, Previsão e Válvulas

Combine leituras de umidade do solo, previsão de chuva e válvulas controladas por MQTT em um sistema de irrigação automática que só rega quando necessário e sempre fecha com segurança.

Sistema de irrigação automática com sonda de umidade do solo, válvula solenoide em linha de gotejamento e controlador ESP32 conectado via MQTT
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

Um sistema de irrigação automática só rega quando o solo está realmente seco e não há chuva a caminho, em vez de seguir um temporizador e regar até debaixo de aguaceiro. Neste tutorial você vai conectar leituras de umidade do solo, uma previsão do tempo gratuita e válvulas solenoides controladas por MQTT: um controlador em Python toma as decisões, e nós de válvula com ESP32 as executam com limites de segurança rígidos, para que uma mensagem perdida nunca deixe a água correndo.

Como o sistema se encaixa

 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)

Cada parte tem uma única função. Os sensores informam a umidade. O controlador combina umidade, previsão e uma janela de horário. Os nós de válvula abrem pelo tempo solicitado e se fecham sozinhos. Como tudo passa pelo broker, você pode adicionar zonas, trocar o controlador pelo Node-RED ou acompanhar tudo pelo celular sem alterar as outras partes. O guia de agricultura inteligente com IoT aborda o projeto mais amplo, e as leituras de umidade deste artigo vêm do nó construído no tutorial de sensor de umidade do solo com MQTT.

Tópicos e payloads

Os exemplos usam o servidor MQTT público (gratuito), feito para testes. Substitua i8r3c6 por uma sequência aleatória sua; a área pública é compartilhada e pode ser lida por qualquer pessoa.

Tópico Payload Retido
connectmq/demo/i8r3c6/irrigation/zone1/moisture {"moisture_pct":22.5} sim
connectmq/demo/i8r3c6/irrigation/zone1/set {"state":"open","duration_s":600} ou {"state":"close"} não
connectmq/demo/i8r3c6/irrigation/zone1/state {"state":"open","remaining_s":540} sim
connectmq/demo/i8r3c6/irrigation/valves/status online / offline (last will) sim
connectmq/demo/i8r3c6/irrigation/log {"zone":1,"action":"skip","reason":"rain_forecast"} não

O comando sempre leva uma duração. Um simples "open" seguido de um "close" separado é frágil: se a mensagem de fechamento se perder, a zona alaga. Com uma duração, o nó de válvula fecha por conta própria.

Regras de decisão para um sistema de irrigação automática

Escreva as regras antes de escrever o código. Um conjunto inicial prático:

  1. Regar apenas dentro das janelas permitidas (o começo da manhã é o mais comum, porque evaporação e vento são mínimos).
  2. Regar uma zona apenas se a umidade estiver abaixo do limite daquela zona.
  3. Pular a rega se a previsão para hoje ou amanhã ultrapassar um limite de chuva.
  4. Nunca regar a mesma zona duas vezes dentro de um intervalo mínimo.
  5. Limitar cada ciclo a uma duração máxima, imposta tanto pelo controlador quanto pelo nó de válvula.
  6. Se os dados de umidade tiverem mais de algumas horas, não regar automaticamente; em vez disso, emitir um alerta.

Os limites dependem do solo e da cultura. Comece com um limite conservador, observe com que rapidez a umidade se recupera após uma rega e ajuste a duração, não a frequência.

O controlador em Python

O controlador usa paho-mqtt 2.x e a API gratuita de previsão Open-Meteo, que dispensa chave. Instale com pip install paho-mqtt.

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          # sua localização

ZONES = {
    1: {"threshold": 28.0, "duration_s": 600},
    2: {"threshold": 30.0, "duration_s": 420},
}
WINDOWS = [(5, 8), (19, 21)]     # horas locais, início inclusivo, fim exclusivo
RAIN_LIMIT_MM = 3.0
MIN_INTERVAL_S = 12 * 3600
STALE_S = 3 * 3600

moisture = {}                    # zona -> (valor, timestamp)
last_run = {}                    # zona -> timestamp
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()      # não repetir o alerta a cada ciclo
            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)

Nesta versão simples, as zonas são regadas uma de cada vez apenas porque são avaliadas em sequência; se o seu abastecimento de água não der conta de duas zonas ao mesmo tempo, adicione uma verificação de que nenhuma outra zona está aberta antes de enviar o próximo comando, usando as mensagens state retidas.

O nó de válvula com ESP32

O nó de válvula assina suas zonas, impõe uma duração máxima rígida, fecha tudo se a conexão com o broker cair por mais de um minuto e informa o estado. Ele usa PubSubClient e ArduinoJson 7.

#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;            // limite absoluto
const uint32_t OFFLINE_CLOSE_MS = 60000;

uint32_t closeAt[NUM_ZONES] = {0, 0};       // prazo em millis(), 0 = fechada
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);        // fechada na inicialização
  }
  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);
  }
}

Acione as solenoides por meio de um módulo de relé ou placa MOSFET dimensionados para a tensão e a corrente da válvula, com um diodo de roda livre nas solenoides de corrente contínua. A maioria das válvulas de jardim é de 24 V AC; válvulas de trava (biestáveis), usadas em sistemas a bateria, precisam de um driver de pulso em vez de um relé mantido ligado. O artigo de relé com ESP32 e MQTT cobre a ligação dos relés.

Levando os dados de umidade ao controlador

O controlador espera uma mensagem retida por zona em zone<n>/moisture. O jeito mais simples de chegar lá é configurar o tópico de telemetria do nó de solo diretamente para esse caminho. Se os seus sensores já publicam em outro lugar, por exemplo sondas LoRaWAN que chegam por uma bridge de network server, algumas linhas na bridge podem republicar o campo de umidade no tópico da zona. Mantenha o mapeamento de sensor para zona em um só lugar, de preferência na configuração do controlador, para que mudar uma sonda de lugar não exija regravar nada.

Quando uma zona tiver duas sondas, use a média ou regue com base na mais seca, mas registre as duas. Uma única sonda com defeito lendo zero não deve conseguir disparar a rega sozinha; a regra de dados desatualizados cobre as sondas silenciosas, e uma faixa de plausibilidade (ignorar valores abaixo de 1 ou acima de 99 por cento) cobre as quebradas.

Escolhendo limites e tempos de rega

O limite diz quando regar; a duração diz quanto. Ajuste-os nessa ordem. Observe a curva de umidade por uma semana sem rega automática, anotando o valor em que as plantas começam a parecer estressadas, e defina o limite alguns pontos acima dele. Depois, acione uma zona manualmente por um tempo fixo e veja quanto e com que velocidade a umidade sobe. Se a leitura salta e volta a cair em poucas horas, a água está drenando além das raízes; regas mais curtas e frequentes são melhores. Se ela quase não se mexe, a rega é curta demais ou a sonda está fora da área do gotejamento.

Registre cada mudança no tópico de log. Ao longo de uma safra, o log publicado no broker se torna a evidência de quais configurações funcionam, e uma visualização ao vivo, como o explorador de tópicos da ConnectMQ, facilita acompanhar o raciocínio do controlador em tempo real. Em uma fazenda com vários talhões, esse histórico e os estados retidos de cada zona formam a base de um gêmeo digital da área irrigada.

Testando sem água

Antes de ligar uma mangueira, teste com LEDs nos pinos das válvulas:

# simula uma leitura seca para a zona 1
mosquitto_pub -h broker.connectmq.com -p 1883 -i t-i8r3c6 -r \
  -t 'connectmq/demo/i8r3c6/irrigation/zone1/moisture' -m '{"moisture_pct":19}'

# abre a zona 1 manualmente por 20 segundos
mosquitto_pub -h broker.connectmq.com -p 1883 -i t-i8r3c6 \
  -t 'connectmq/demo/i8r3c6/irrigation/zone1/set' -m '{"state":"open","duration_s":20}'

# acompanha tudo
mosquitto_sub -h broker.connectmq.com -p 1883 -i w-i8r3c6 \
  -t 'connectmq/demo/i8r3c6/irrigation/#' -v

Em seguida, desconecte a antena Wi-Fi do ESP32 ou desligue o roteador durante uma rega e confirme que a válvula fecha em até um minuto. Esse único teste vale mais do que qualquer revisão de código.

Checklist de robustez

  • A válvula fecha na inicialização, ao fim da duração, na perda do broker e diante de qualquer comando malformado.
  • Um medidor de vazão na linha principal confirma que a água realmente passou; publique os litros por rega e gere alerta se houver vazão com todas as válvulas fechadas (um cano estourado).
  • Comandos nunca são retidos; estados sempre são.
  • Cada controlador e nó de válvula tem suas próprias credenciais em um broker privado com TLS.
  • Existe um acionamento manual: um interruptor físico ou um botão no painel que publica {"state":"close"}.

Configuração de produção

Troque o servidor MQTT público por um broker privado antes de conectar válvulas reais, porque qualquer pessoa poderia publicar em um tópico set público. No ESP32, use WiFiClientSecure com a CA ISRG Root X1 na porta 8883 e mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", ...); em Python, chame client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD") e client.tls_set() e conecte na porta 8883. Um broker MQTT gerenciado mantém o broker disponível mesmo quando o PC da fazenda reinicia, e um broker MQTT na nuvem permite que nós de campo em links celulares o alcancem sem VPN. Para controle de clima em estufa junto com a rega, veja automação de estufa e, para mais padrões, visite o guia de automação IoT.

Irrigue de forma mais inteligente com a ConnectMQ

A ConnectMQ oferece ao seu sistema de irrigação um broker MQTT privado com TLS, um explorador de tópicos ao vivo para acompanhar cada comando e confirmação, e regras de alerta por limite para zonas secas ou vazões inesperadas. Crie um broker Serverless gratuito em app.connectmq.com, sem cartão de crédito, e leve para ele o seu controlador e as suas válvulas.

Perguntas frequentes

O que acontece se a conexão MQTT cair com uma válvula aberta?

O nó de válvula fecha todas as válvulas abertas após 60 segundos sem conexão com o broker, e cada rega tem um limite de duração, então a água não pode correr indefinidamente.

Posso usar o Home Assistant ou o Node-RED no lugar do controlador em Python?

Sim. Qualquer coisa que assine os tópicos de umidade e publique o mesmo comando JSON funciona; o nó de válvula não se importa com quem o enviou.

Quem deve decidir: a previsão ou o sensor de solo?

Use os dois. O sensor de solo diz se a água é necessária agora; a previsão diz se a natureza vai fornecê-la em breve. Pule a rega apenas quando ambos concordarem.

Quantas zonas um ESP32 consegue controlar?

Tantas quantas forem os pinos de saída livres e os canais de relé, normalmente de quatro a oito. Acione as zonas uma de cada vez se a pressão da água não der conta de várias ao mesmo tempo.

Artigos relacionados

Guia de Configuração do Zigbee2MQTT: do Coordenador ao Painel

Instale o Zigbee2MQTT com Docker, configure o coordenador e a conexão MQTT, pareie e renomeie dispositivos, controle-os via MQTT e veja seus estados em um painel, testando primeiro no servidor MQTT público (gratuito).

8 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.