Sensor de Chuva + ESPHome: Seco, Chovendo ou Secando

Este post traz uma opção de instalação de sensor de chuva sem usar o comparador que vem no sensor. É uma variação desta implementação.

Essa implementação foi adaptada do repositório GitHub - hugokernel/esphome-rain-detector: ESPHome Rain Detector, o qual é o autor original do código. Realizei algumas alterações e traduções para facilitar a implementação.

Algumas vantagens dessa implemetação:

  • Conseguir diferenciar quando está chovendo (o sensor está molhado) de quando a chuva já cessou e o sensor está secando. Isso é possível através da análise da resistência da placa medida pelo NodeMCU.

image

  • Diminuição da oxidação das trilhas da placa devido a aplicação de tensão na placa ocorrer por pequenos intervalos de tempo a cada período de tempo (configurável, no código está aplicação de tensão por 2 segundos a cada 5 segundos se estiver seco ou a cada 30 segundos se estiver molhado).

Materiais utilizados:

Ligação do sensor no ESP:

Código para o ESPHome:

Os comentários do código ajuda a entender a lógica de funcionamento.

substitutions:
  #Configurações da placa:
  Plataforma: ESP32 #Altere para o modelo da sua placa
  TipoPlaca: esp32dev #Altere para o tipo da sua placa
  
  #Configurações do dispositivo:
  hostname: 'casa3740espclima' #Hostname do dispositivo na rede
  PrefixoNomeDevice: "Clima - "
  PrefixoNomeChuvaPlaca: "Clima - Chuva Inst. - "
  
  #Configurações da Rede:
  RedeWifi: !secret RedeWifi_IoT #Nome da rede wifi que o dispositivo irá se conectar
  SenhaWifi: !secret SenhaWifi_IoT #Senha da rede wifi que o dispositivo irá se conectar
  SenhaWifiReconfig: !secret SenhaWifiReconfig #Senha do AP Wifi para reconfiguração do wifi do dispositivo (padrão: o nome do hostame)
  EndConfig: ${hostname}.local #10.10.103.104 #${hostname}.local #Endereço para configuração (IP que o esp está acessível atualmente na rede)
  WifiOculto: 'False'
  WifiFastConnect: 'False'
  
  #Senhas
  SenhaAPI: !secret SenhaAPI
  SenhaOTA: !secret SenhaOTA
  
  #Pinos
  PinoSensorChuvaTensaoPlaca: GPIO19
  PinoSensorChuvaMedidaPlaca: GPIO33

  #Logger
  LoggerLevel: DEBUG
  
  resistor_value: "9.82kOhm" #Usar um resistor de 10kOhm e medir o valor real dele
  
  # Intervalo de resistência mínima (imerso em água) e máxima (seco)
  # Igonora valores fora do intervalo
  min_resistance: "2700"
  max_resistance: "39154"

  # Valor da resistência em que uma mudança significativa é considerada como chovendo
  rain_detection_threshold: "2000"
 
  # Se a média da resistência estiver este valor acima da última média, o sensor é considerado secando
  dry_detection_threshold: "1000"

  # Ao inicializar, se a resistência for inferior a este valor, suponha que o sensor esteja molhado (resistência molhada)
  wet_resistance_when_booting: "35000"

  # +------------------------------+
  # | Delay entre 2 medida de chuva |
  # +------------------------------+
  # Tempo de delay se estiver no modo seco
  measure_interval_dry: "5000"

  # Tempo de delay se estiver no modo molhado
  # Deve ser grande o suficiente para não danificar as trilhas prematuramente, mas pequeno o suficiente para ser reativo o suficiente.
  measure_interval_wet: "30000"

  # Estabilização antes de ler a resistência
  # Um atraso muito curto não permitirá a leitura das resistências baixas
  stabilization_delay: 2sec

esphome:
  name: $hostname
  platform: ${Plataforma}
  board: ${TipoPlaca}
  on_boot:
    then:
      - script.execute: test_resistance
      - script.execute: measure_loop
      - text_sensor.template.publish:
          id: text_status
          state: "Verificando"
wifi:
  networks:
  - ssid: ${RedeWifi}
    password: ${SenhaWifi}
    hidden: ${WifiOculto}
  fast_connect: ${WifiFastConnect}
    
  use_address: ${EndConfig}
  
  # Ative o hotspot de fallback (captive portal) caso a conexão wifi falhe
  ap:
    ssid: WIFI
    password: ${SenhaWifiReconfig}

#Habilita um AP Wifi para reconfigurar em caso de perda de conexão com a rede configurada
captive_portal:

#Habilita a atualização de firmware por OTA
ota:
  password: ${SenhaOTA}
  
#Habilita as mensagens de logs pela porta serial e via MQTT
logger:
  level: ${LoggerLevel}

#Habilita a api para comunicar com o Home Assistant
api:
  password: ${SenhaAPI}

globals:
  - id: measure_delay
    type: int
    restore_value: yes
    initial_value: $measure_interval_dry

################################################################################
#                                      BUTTON                                  #
################################################################################
button:
  #----------------------------------------------------------------------------#
  #                                BUTTON -  DEVICE                            #
  #----------------------------------------------------------------------------#
  #Comando reinicilizar esp remotamente
  - platform: restart
    id: restart_button
    name: ${PrefixoNomeDevice} Reiniciar
    icon: mdi:restart

  #----------------------------------------------------------------------------#
  #                    BUTTON - SENSOR DE CHUVA INSTANTÂNEO (PLACA)            #
  #----------------------------------------------------------------------------#
  #Executar a medição
  - platform: template
    id: run_measure
    name: "${PrefixoNomeChuvaPlaca} Executa Medição"
    on_press:
    - script.execute: measure

################################################################################
#                                  BINARY SENSOR                              #
################################################################################
binary_sensor:
  #----------------------------------------------------------------------------#
  #                            BINARY SENSOR - DEVICE                          #
  #----------------------------------------------------------------------------#
  #Status (conectado ou desconectado)
  - platform: status
    id: device_status
    name: ${PrefixoNomeDevice} Status
    device_class: connectivity

  #----------------------------------------------------------------------------#
  #              BINARY SENSOR - SENSOR DE CHUVA INSTANTÂNEO (PLACA)           #
  #----------------------------------------------------------------------------#
  # Sensor binário do status chovendo
  - platform: template
    id: raining
    name: "${PrefixoNomeChuvaPlaca} Chovendo"
    device_class: moisture
  # Sensor binário do status secando
  - platform: template
    id: drying
    name: "${PrefixoNomeChuvaPlaca} Secando"
    device_class: moisture
  # Sensor binário do status secando
  - platform: template
    id: power
    name: "${PrefixoNomeChuvaPlaca} Tensão Aplicada"
    device_class: power

################################################################################
#                                    TEXT SENSOR                               #
################################################################################
text_sensor:
  #----------------------------------------------------------------------------#
  #                              TEXT SENSOR - DEVICE                          #
  #----------------------------------------------------------------------------#
  #Sensor de tempo ligado formatado
  - platform: template
    name: ${PrefixoNomeDevice} Uptime
    id: uptime_human
    icon: mdi:clock-start
    update_interval: 1s
    entity_category: diagnostic
  #Informações da conexão wifi
  - platform: wifi_info
    #Endereço IP
    ip_address:
      id: IP
      name: ${PrefixoNomeDevice} Endereço IP
      icon: mdi:ip-network
    #Nome da Rede
    ssid:
      id: SSID
      name: ${PrefixoNomeDevice} Rede Wifi
      icon: mdi:wifi
  #Informação da versão da compilação
  - platform: version
    id: versao
    name: ${PrefixoNomeDevice} Versão
    icon: mdi:information

  #----------------------------------------------------------------------------#
  #                TEXT SENSOR - SENSOR DE CHUVA INSTANTÂNEO (PLACA)           #
  #----------------------------------------------------------------------------#
  #Status do sensor de chuva instantaneo
  - platform: template
    id: text_status
    name: "${PrefixoNomeChuvaPlaca} Status"
    icon: mdi:information-outline
    #lambda: |-
    #  return {"Verificando"};


################################################################################
#                                       SWITCH                                 #
################################################################################
switch:
  #----------------------------------------------------------------------------#
  #                  SWITCH - SENSOR DE CHUVA INSTANTÂNEO (PLACA)              #
  #----------------------------------------------------------------------------#
  #Switch GPIO para controlar a aplicação de tensão  na placa
  #(com o objetivo de reduzir a oxidação)
  - platform: gpio
    id: resistance_bias
    name: "${PrefixoNomeChuvaPlaca} Liga Energia Placa"
    icon: "mdi:power"
    internal: true
    pin:
      number: ${PinoSensorChuvaTensaoPlaca}
      mode: OUTPUT
    on_turn_on:
      - binary_sensor.template.publish:
          id: power
          state: on
    on_turn_off:
      - binary_sensor.template.publish:
          id: power
          state: off
     

################################################################################
#                                       SENSOR                                 #
################################################################################
sensor:
  #----------------------------------------------------------------------------#
  #                                 SENSOR - DEVICE                            #
  #----------------------------------------------------------------------------#
  #Sensor de tempo ligado
  - platform: uptime
    id: device_uptime
    internal: true
    update_interval: 1s
    on_raw_value:
      then:
        - text_sensor.template.publish:
            id: uptime_human
            state: !lambda |-
              int seconds = round(id(device_uptime).raw_state);
              int days = seconds / (24 * 3600);
              seconds = seconds % (24 * 3600);
              int hours = seconds / 3600;
              seconds = seconds % 3600;
              int minutes = seconds /  60;
              seconds = seconds % 60;
              return (
                (days ? to_string(days) + "d " : "") +
                (hours ? to_string(hours) + "h " : "") +
                (minutes ? to_string(minutes) + "m " : "") +
                (to_string(seconds) + "s")
              ).c_str();

  #Sensor Intensidade Sinal Wifi
  - platform: wifi_signal
    id: wifi_sinal
    name: ${PrefixoNomeDevice} Intensidade Wifi
    icon: mdi:signal
    update_interval: 10s

  #----------------------------------------------------------------------------#
  #                   SENSOR - SENSOR DE CHUVA INSTANTÂNEO (PLACA)             #
  #----------------------------------------------------------------------------#
  #Leitura da resistência da placa
  - platform: adc
    id: source_sensor
    pin: ${PinoSensorChuvaMedidaPlaca}
    name: ADC
    attenuation: 11db
    internal: true

    # É importante ter um intervalo de atualização baixo para que
    # a medição tem tempo para ser feita corretamente durante
    # a ativação da tensão e levando em consideração o filtro mediano
    update_interval: 250ms

    filters:
      - multiply: 0.846153 # 3.9 (11db tensão de atenuação em escala real) -> 3.3V
      - median:
          window_size: 7
          send_every: 4
          send_first_at: 3

  #Calculo da resistência real da placa
  - platform: resistance
    sensor: source_sensor
    id: real_resistance_sensor
    name: "${PrefixoNomeChuvaPlaca} Resistência"
    configuration: DOWNSTREAM
    resistor: $resistor_value
    reference_voltage: 3.3V
    internal: true
    icon: "mdi:omega"
    filters:
      # Nenhum valor menor que 0
      - lambda: 'return max((float)$min_resistance, x);'
      # Nenhum valor maior que $max_resistance
      - lambda: 'return min((float)$max_resistance, x);'
    on_value:
      then:
        - if:
            condition:
              lambda: |-
                  return (
                      id(real_resistance_sensor).state > $min_resistance
                      &&
                      // <= é importante para forçar a resistência ao máximo
                      // para ter um valor para comparar se o
                      // queda de resistência
                      id(real_resistance_sensor).state <= $max_resistance
                  );
            then:
              - sensor.template.publish:
                  id: resistance_sensor
                  state: !lambda "return id(real_resistance_sensor).state;"

  # Sensor da última resistência medida
  - platform: template
    id: latest_resistance_sensor
    name: "${PrefixoNomeChuvaPlaca} Resistência - Última Medição"
    icon: "mdi:omega"
    unit_of_measurement: 'Ω'
    disabled_by_default: true

  # Sensor da última média de resistência medida
  - platform: template
    id: latest_average_resistance_sensor
    name: "${PrefixoNomeChuvaPlaca} Resistência - Última Média"
    icon: "mdi:omega"
    unit_of_measurement: 'Ω'
    disabled_by_default: true

  # Sensor da resistência medida
  - platform: template
    id: resistance_sensor
    name: "${PrefixoNomeChuvaPlaca} Resistência"
    icon: "mdi:omega"
    unit_of_measurement: 'Ω'

  # Sensor da média da resistência medida
  - platform: template
    id: average_resistance_sensor
    name: "${PrefixoNomeChuvaPlaca} Resistência - Média"
    icon: "mdi:omega"
    unit_of_measurement: 'Ω'
    disabled_by_default: true
    filters:
      - sliding_window_moving_average:
          window_size: 15
          send_every: 15
      #- heartbeat: 2min


################################################################################
#                                      SCRIPTS                                 #
################################################################################
script:
  #----------------------------------------------------------------------------#
  #                    SCRIPTS - SENSOR DE CHUVA INSTANTÂNEO (PLACA)           #
  #----------------------------------------------------------------------------#
  #Script para iniciar a medição (aplica a tensão na placa e aguarda estabilizar)
  - id: begin_measure
    mode: single
    then:
      - switch.turn_on: resistance_bias
      - delay: $stabilization_delay
  #Script para finalizar a medição (retira a tensão da placa)
  - id: end_measure
    mode: single
    then:
      - switch.turn_off: resistance_bias
  #Script que testa se a resistência é menor que quando iniciou (para detectar se está secando)
  - id: test_resistance
    mode: single
    then:
      - script.execute: begin_measure
      - script.wait: begin_measure
      - if:
          condition:
            lambda: "return id(resistance_sensor).state < $wet_resistance_when_booting;"
          then:
            - script.execute: its_raining
      - script.execute: end_measure
      - script.wait: end_measure
  #Script que salva os últimos valores de resistência e média
  - id: save_current_resistance
    then:
      - sensor.template.publish:
          id: latest_resistance_sensor
          state: !lambda "return id(resistance_sensor).state;"
      - sensor.template.publish:
          id: latest_average_resistance_sensor
          state: !lambda "return id(average_resistance_sensor).state;"
  # Atualmente:
  # * A chuva é detectada com o valor mais recente comparado a um limite
  # * Para detectar quando está secando, usamos a média
  #   Para ser testado:
  #     - A resistência mais baixa que corresponde a uma saturação completa do sensor é gravado permanentemente:
  #        Se este valor não for alcançado mas a resistência não diminui durante um período de tempo, sabemos que não está mais chovendo.
  #Script para atualizar a média de resistência
  - id: update_average_values
    mode: single
    then:
      # Defina a resistência média
      - if:
          condition:
            lambda: "return (id(resistance_sensor).state >= 0 && id(resistance_sensor).state <= $max_resistance);"
          then:
            - sensor.template.publish:
                id: average_resistance_sensor
                state: !lambda "return id(resistance_sensor).state;"
  #Script de medição da resistência da placa
  - id: measure
    mode: single
    then:
      - script.execute: begin_measure
      - script.wait: begin_measure
      # Inicia o último valor de resistência se não for um número
      - if:
          condition:
            lambda: "return isnan(id(latest_resistance_sensor).state);"
          then:
            - script.execute: save_current_resistance
            - script.wait: save_current_resistance
      - if:
          condition:
            lambda: "return isnan(id(latest_average_resistance_sensor).state);"
          then:
            - script.execute: save_current_resistance
            - script.wait: save_current_resistance
      # Apenas para fins de depuração
      - logger.log:
          level: INFO
          format: "> Resistance: %.1f vs latest resistance: %.1f"
          args: ['id(resistance_sensor).state', 'id(latest_resistance_sensor).state']
      - script.execute: update_average_values
      # Teste para chover
      - if:
          condition:
            lambda: "return id(resistance_sensor).state + $rain_detection_threshold < id(latest_resistance_sensor).state;"
          then:
            - script.execute: its_raining
      # Teste para secagem
      - if:
          condition:
            lambda: "return id(average_resistance_sensor).state - $dry_detection_threshold > id(latest_average_resistance_sensor).state;"
          then:
            - script.execute: its_drying
      # Teste para seco
      # Assumimos que o sensor está seco quando a resistência atual == resistência máxima
      - if:
          condition:
            lambda: "return id(resistance_sensor).state == $max_resistance;"
          then:
            - script.execute: its_dry
      - script.execute: end_measure
      - script.wait: end_measure
  #Script para executar o loop de medição da resistência da placa
  - id: measure_loop
    mode: single
    then:
      - while:
          condition:
            lambda: "return true;"
          then:
            - logger.log:
                format: "[Start measure]"
                level: INFO
            - script.execute: measure
            - script.wait: measure
            - delay: !lambda "return id(measure_delay);"
  #Script para quando detectar que está chovendo
  - id: its_raining
    mode: single
    then:
      - logger.log: "It's raining !"
      - script.execute: save_current_resistance
      - script.wait: save_current_resistance
      - homeassistant.event:
          event: esphome.its_raining
      - text_sensor.template.publish:
          id: text_status
          state: "Chovendo"
      - binary_sensor.template.publish:
          id: raining
          state: on
      - binary_sensor.template.publish:
          id: drying
          state: off
      - globals.set:
          id: measure_delay
          value: $measure_interval_wet
  #Script para quando detectar que a placa está secando
  - id: its_drying
    mode: single
    then:
      - logger.log: "It's drying !"
      - script.execute: save_current_resistance
      - script.wait: save_current_resistance
      - text_sensor.template.publish:
          id: text_status
          state: "Secando"
      - binary_sensor.template.publish:
          id: raining
          state: off
      - binary_sensor.template.publish:
          id: drying
          state: on
      - globals.set:
          id: measure_delay
          value: $measure_interval_wet
  #Script para quando detectar que a placa está seca
  - id: its_dry
    mode: single
    then:
      - logger.log: "It's dry !"
      - script.execute: save_current_resistance
      - script.wait: save_current_resistance
      - text_sensor.template.publish:
          id: text_status
          state: "Seco"
      - binary_sensor.template.publish:
          id: raining
          state: off
      - binary_sensor.template.publish:
          id: drying
          state: off
      - globals.set:
          id: measure_delay
          value: $measure_interval_dry

Todas essas informações importantes (senhas OTA e API, configuração da rede wifi, etc) estão armazenadas no arquivo secrets.yaml (ver aqui e aqui para mais detalhes de configuração e uso).

Exemplos de estatísticas com a informação do sensor:

image

sensor:
  - platform: history_stats
    name: Sensor de Chuva - Chovendo - N° Ativações - Hoje
    entity_id: binary_sensor.clima_chuva_inst_chovendo
    state: "on"
    type: count
    start: "{{ now().replace(hour=0, minute=0, second=0) }}"
    end: "{{ now() }}"

  - platform: history_stats
    name: Sensor de Chuva - Chovendo - N° Ativações - Últimas 24h
    entity_id: binary_sensor.clima_chuva_inst_chovendo
    state: "on"
    type: count
    duration: 24:00:00
    end: "{{ now() }}"

  - platform: statistics
    entity_id: binary_sensor.clima_chuva_inst_chovendo
    name: Sensor de Chuva - Tempo Chovendo - Últimas 24h
    state_characteristic: average_step
    max_age:
      hours: 24
      
  - platform: history_stats
    name: Sensor de Chuva - Secando - N° Ativações - Hoje
    entity_id: binary_sensor.clima_chuva_inst_secando
    state: "on"
    type: count
    start: "{{ now().replace(hour=0, minute=0, second=0) }}"
    end: "{{ now() }}"

  - platform: history_stats
    name: Sensor de Chuva - Secando - N° Ativações - Últimas 24h
    entity_id: binary_sensor.clima_chuva_inst_secando
    state: "on"
    type: count
    duration: 24:00:00
    end: "{{ now() }}"

  - platform: statistics
    entity_id: binary_sensor.clima_chuva_inst_secando
    name: Sensor de Chuva - Tempo Secando - Últimas 24h
    state_characteristic: average_step
    max_age:
      hours: 24

Ideias de uso no Lovelace:

  • Além de incluir a informação em cards, fica lega incluir em alguma parte do seu dashboard a lista de eventos do status do sensor para verificar diretamente quando iniciou e parou a chuva.

Lista de eventos

Código do card:

type: logbook
entities:
  - sensor.clima_chuva_inst_status
hours_to_show: 48
title: Sensor de Chuva
7 Likes

Excelente tutorial, funcionou tudo perfeitamente. A unica coisa que não entendi muito bem foi com fazer esse teste.

1 Like

O ideal seria medir a resistência real do resistor com um multímetro. Mas caso não tiver pode usar esse valor sem interferir tanto no resultado final.

1 Like

Agora tudo fez sentido hahahahaha.
Eu tava tentando medir com o próprio ESP32 tava dando tudo errado, agora deu certo.
Perfeito

1 Like

Dá para fazer com o mesmo código no esp8266 ?

Amigão! Parabéns! Otimo tutorial! Mas é normal a tensão aplicada ficar ligando e desligando a cada segundo?

1 Like

Ela vai ligar a cada tempo definido no código no ESPHome, quanto está no seu?

Por exemplo, o padrão do código é efetuar a medição (ligando a saída de tensão) a cada 5 segundos se estiver seco ou a cada 30 segundos se estiver molhado. Esses tempos são configurados no código pelas variáveis abaixo:

Apesar de eu não ter testado, acredito que sim.

O meu a entidade tensão aplicada fica piscando em desligado e ligado! E nos teste ela nao fica como secando? Nao mudei nada do código original! Somente tirei iot do secret do wifi


Fica mesmo em seco fica piscando! Ela n teria q ficar desligado para nao danificar a placa?


Douglas! Esses sensores eu coloco onde? Em sensores no configurations ou no código do esphome? Obrigado