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;
}
}