Smart Digital Humidistat

After tearing my hair out with the inaccurate Honeywell Digital Humidistat and not having enough conductors on my thermostat cable to control my whole-home humidifier via Ecobee, I went the DIY route. My first ESPHome project.

Parts are simply ESP32, a relay, and SHT45 humidity sensor (very accurate!). Weather forecast is pulled via API and used to predict the temperature of the indoor pane of the windows. Using this temperature, dew point is calculated, and target RH% is a few % below this to prevent condensation. With this the relay is switched on/off using a bit of hysteresis.

I still have some work to do to cover the scenario where wifi / internet / API fails and I can’t get a forecast, I’m not sure what will happen as things stand.

esphome:
  name: esp32_humidistat
  friendly_name: esp32_humidistat

esp32:
  board: adafruit_feather_esp32_v2
  framework:
    type: arduino

# Enable logging
logger:
  level: DEBUG

# Enable Home Assistant API
api:
  encryption:
    key: "<redacted>"
  services:
  - service: set_rh_headroom
    variables:
      headroom: float
    then:
      - globals.set:
          id: RH_headroom
          value: !lambda 'return headroom;'
  - service: set_rh_ceiling
    variables:
      ceiling: float
    then:
      - globals.set:
          id: RH_ceiling
          value: !lambda 'return ceiling;'

ota:
  password: "<redacted>"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Esp32-Humidistat"
    password: "<redacted>"

captive_portal:
    
#TODO: check https://www.connaxio.com/projects/co2_sensor/

i2c:
  sda: GPIO22
  scl: GPIO20
  scan: True
  id: bus_a

switch:
  - platform: gpio
    pin: GPIO5
    name: "Humidifier Control Relay"
    id: my_relay
    internal: true

binary_sensor:
  - platform: template
    name: "Humidifier Control Relay State"
    lambda: |-
      if (id(my_relay).state) {
        return true;
      } else {
        return false;
      }

http_request:
  useragent: esphome/device
  timeout: 10s
  id: http_request_data

sensor:
  - platform: sht4x
    temperature:
      name: "Temperature"
      id: my_temperature
    humidity:
      name: "Relative Humidity"
      id: my_humidity
      unit_of_measurement: "%"
  - platform: template
    name: "Minimum Forecast Outdoor Temperature"
    id: min_temp
    unit_of_measurement: "°C"
    device_class: "temperature"
    state_class: "measurement"
  - platform: template
    name: "Relative Humidity Headroom"
    id: RH_headroom_template
    unit_of_measurement: "%"
    device_class: "humidity"
    state_class: "measurement"
    update_interval: 60s
    lambda: |-
      return id(RH_headroom);
  - platform: template
    name: "Relative Humidity Ceiling"
    id: RH_ceiling_template
    unit_of_measurement: "%"
    device_class: "humidity"
    state_class: "measurement"
    update_interval: 60s
    lambda: |-
      return id(RH_ceiling);
  - platform: template
    name: "Target Humidity"
    id: humidity_target
    unit_of_measurement: "%"
    update_interval: 1min
    device_class: "humidity"
    state_class: "measurement"

globals:
  - id: RH_headroom # we want some margin from the dew point
    type: float
    restore_value: true
    initial_value: '5.0'
  - id: RH_ceiling # in summer we don't want to keep humidifying
    type: double
    restore_value: True
    initial_value: '50'

interval:
  - interval: 1min
    then:
      - http_request.get:
          url: https://api.openweathermap.org/data/2.5/forecast?lat=43.631770&lon=-79.516250&units=metric&appid=<redacted>
          headers:
            Content-Type: application/json
          verify_ssl: false
          on_response:
            then:
              - lambda: |- 
                  auto value = id(http_request_data).get_string();
                  json::parse_json(value, [](JsonObject root) {
                    float min_temp_min = root["list"][0]["main"]["temp_min"]; # find the minimum forecast temp over next several hours 
                    for(int i = 1; i < 4; i++) {   # need to paramterize time window
                      float temp_min = root["list"][i]["main"]["temp_min"];
                      if(temp_min < min_temp_min) {
                          min_temp_min = temp_min;
                      }
                    }
                    id(min_temp).publish_state(min_temp_min); 
                  });  
                  float window_interior_thermal_resistance = 1.4; # based on IR gun readings inside and out, this my window performance 
                  float window_exterior_thermal_resistance = 2.6; 
                  double predicted_window_temp = (id(my_temperature).state * window_interior_thermal_resistance + id(min_temp).state * window_exterior_thermal_resistance) / (window_interior_thermal_resistance + window_exterior_thermal_resistance);
                  ESP_LOGD("update forecast", "predicted_window_temp: %f", predicted_window_temp);
                  double humidity_target_value = 100* exp(17.27 * predicted_window_temp / (243.04 + predicted_window_temp) - 17.27 * id(my_temperature).state / (243.04 + id(my_temperature).state)); # dew point calc
                  humidity_target_value -= id(RH_headroom);
                  humidity_target_value = min(humidity_target_value, id(RH_ceiling));
                  id(humidity_target).publish_state(humidity_target_value);
  - interval: 1min
    then:
      - lambda: |- 
          static bool relay_on = false;
          if (relay_on) {
            if (id(my_humidity).state > id(humidity_target).state + 2.5) { # 2.5 for hysteresis
              id(my_relay).turn_off();
              relay_on = false;
            }
          } else {
            if (id(my_humidity).state < id(humidity_target).state - 2.5) {
              id(my_relay).turn_on();
              relay_on = true;
            }
          }
1 Like