# about
# / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

# TVS - TRAMANN VENTILATION SYSTEM
# version 3.17
#
# yaml code for ESPHome and Home Assistant
#
# table of contents
#   about (where you are right now)
#   setup (configurations, especially for communication over the air)
#   initial code (setting the sensors and actors, inputs and outputs (in particular the screen) and preparing them for Home Assistant)
#   deep sleep code (by default commented out, can be commented in to save power (but it's not recommended))
#   operating code (calculating, deciding and acting)
#     manual mode (control the device manually by pressing the button, turn on for 20 minutes and then resume to automatic mode)
#     automatic mode (the device decides independently, based on measured values and formulas (this setting is default and recommended))
#     programmed mode (if you want, you can insert your own code here to program the device yourself)


# setup
# / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

esphome:
  name: tvs-tramann-ventilation-system
  friendly_name: TVS-Tramann-Ventilation-System

esp32:
  board: wemos_d1_mini32

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "gPpyRTf3kx4IeRhtaDaOnErk9SZJ0KPcMrpSQuq3ByI="

ota:
  password: "c4356f8f88ed28d0070f120ba9e54b74"

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "TVS-Tramann-Ventilation-System"
    password: "TVSLocalNetwork"

captive_portal:


# initial code
# / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

# configuration of the buttons
binary_sensor:
  - platform: gpio
    pin: GPIO16
    name: "button 0"
    id: button_0
  - platform: gpio
    pin: GPIO17
    name: "button 1"
    id: button_1
  - platform: gpio
    pin: GPIO18
    name: "button 2"
    id: button_2

# configuration of the sensors for humidity and temperature
sensor:
  - platform: dht
    pin: GPIO19
    temperature:
      name: "outside temperature"
      id: outside_temperature
    humidity:
      name: "outside humidity"
      id: outside_humidity
    update_interval: 60s
    model: DHT22
  - platform: dht
    pin: GPIO23
    temperature:
      name: "inside temperature"
      id: inside_temperature
    humidity:
      name: "inside humidity"
      id: inside_humidity
    update_interval: 60s
    model: DHT22

# time configuration
time:
  - platform: sntp
    id: sntp_time
    servers:
      - 0.pool.ntp.org
      - 1.pool.ntp.org
      - 2.pool.ntp.org

# configuration of the OLED display
i2c:
  sda: GPIO21
  scl: GPIO22
  scan: true
  frequency: 400kHz

# configuration of the fonts
font:
  - file: "gfonts://Roboto"
    id: font_16
    size: 16
  - file: "gfonts://Roboto"
    id: font_12
    size: 12
  - file: "gfonts://Roboto"
    id: font_10
    size: 10

# display configuration
display:
  - platform: ssd1306_i2c
    model: "SSD1306 128x64"
    address: 0x3C
    lambda: |-
      static std::string button_status = "auto"; // default to "auto" at startup
      static bool button_0_prev_state = true;  // default ON
      static bool button_1_prev_state = true;  // default ON
      static bool button_2_prev_state = true;  // default ON

      // update the button status only when the state changes from ON to OFF
      if (!id(button_0).state && button_0_prev_state) {
        button_status = "man";
      }
      button_0_prev_state = id(button_0).state;

      if (!id(button_1).state && button_1_prev_state) {
        button_status = "auto";
      }
      button_1_prev_state = id(button_1).state;

      if (!id(button_2).state && button_2_prev_state) {
        button_status = "prog";
      }
      button_2_prev_state = id(button_2).state;

      static int state = 0;
      static unsigned long last_change = millis();

      const unsigned long interval_1 = 2000;
      const unsigned long interval_2 = 4000;

      if (millis() - last_change > interval_1 && state == 0) {
        state = 1;
        last_change = millis();
      } else if (millis() - last_change > interval_2 && state == 1) {
        state = 2;
        last_change = millis();
      }

      it.fill(COLOR_OFF);

      if (state == 0) {
        it.print(it.get_width() / 2, it.get_height() / 2, id(font_16), TextAlign::CENTER, "TRAMANN");
      } else if (state == 1) {
        it.print(it.get_width() / 2, it.get_height() / 2 - 8, id(font_16), TextAlign::CENTER, "TVS");
        it.print(it.get_width() / 2, it.get_height() / 2 + 8, id(font_10), TextAlign::CENTER, "Tramann Ventilation System");
      } else if (state == 2) {
        it.printf(128, 0, id(font_12), TextAlign::TOP_RIGHT, button_status.c_str());
        it.print(0, 0, id(font_12), "TVS");
        it.printf(0, 17, id(font_10), "intmp: %.1f°C, hum: %.1f%%", id(inside_temperature).state, id(inside_humidity).state);
        it.printf(0, 29, id(font_10), "outtmp: %.1f°C, hum: %.1f%%", id(outside_temperature).state, id(outside_humidity).state);
        auto time = id(sntp_time).now();
        it.strftime(0, 53, id(font_10), "%H:%M", time);
        it.strftime(74, 53, id(font_10), "%d-%m-%Y", time);
      }

# configuration of the propellers
output:
  - platform: gpio
    pin: GPIO25
    id: propeller_1
  - platform: gpio
    pin: GPIO26
    id: propeller_2

switch:
  - platform: output
    name: "propeller 1"
    output: propeller_1
  - platform: output
    name: "propeller 2"
    output: propeller_2


# deep sleep code
# / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

# warning: the device can't be reprogrammed over the air during the sleep periode, please use USB instead (or wait until the run periode begins)
#deep_sleep:
#  run_duration: 10min
#  sleep_duration: 50min


# operating code
# / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

# implement different operating modes
interval:
  - interval: 1s
    then:
      - lambda: |-
          // update mode and switch status
          static std::string button_status = "auto";
          if (!id(button_0).state) {
            button_status = "man";
          } else if (!id(button_1).state) {
            button_status = "auto";
          } else if (!id(button_2).state) {
            button_status = "prog";
          }

          // timestamp for manual mode
          static unsigned long manual_mode_start = 0;
          static bool propeller_state = false;

          // manual mode
          //  / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
          if (button_status == "man") {
            // toggle propellers for 20 minutes when button is pressed (1200000), after that turn off for 5 seconds (5000)
            if (!id(button_0).state && (millis() - manual_mode_start > 5000)) {
              manual_mode_start = millis();
              propeller_state = !propeller_state;
              if (propeller_state) {
                id(propeller_1).turn_on();
                id(propeller_2).turn_on();
              } else {
                id(propeller_1).turn_off();
                id(propeller_2).turn_off();
              }
            }
            // toggle propellers for 20 minutes when button is pressed (1200000), after that turn off for 5 seconds (5000)
            else if (propeller_state && millis() - manual_mode_start > 1200000) {
              id(propeller_1).turn_off();
              id(propeller_2).turn_off();
            }
          } else {
            // automatic mode
            //  / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
            //
            // The sensors have an inaccuracy of around 5°C and the aim of the automatic mode
            // is to achieve a temperature range of around 19°C to 21°C.
            // The humidity should be regulated to the target value of 40% to 60%.
            // The system is switched off at night, during the usual time
            // for falling asleep between 21:00 and 23:30.
            //
            if (button_status == "auto") {
              // calculate dew point
              auto dew_point = [](float temp, float humidity) {
                const float a = 17.27;
                const float b = 237.7;
                float alpha = ((a * temp) / (b + temp)) + log(humidity / 100.0);
                return (b * alpha) / (a - alpha);
              };

              // temperature and humidity conditions
              bool temp_condition = (id(inside_temperature).state > 24.0 && id(outside_temperature).state < 24.0) || 
                                    (id(inside_temperature).state < 16.0 && id(outside_temperature).state > 16.0);

              bool humidity_condition = ((id(inside_humidity).state > 60 && id(outside_humidity).state < 60) || 
                                         (id(inside_humidity).state < 40 && id(outside_humidity).state > 40));

              float inside_dew_point = dew_point(id(inside_temperature).state, id(inside_humidity).state);
              float outside_dew_point = dew_point(id(outside_temperature).state, id(outside_humidity).state);

              bool refined_humidity_condition = ((inside_dew_point > outside_dew_point && id(inside_humidity).state > 60) || 
                                                 (inside_dew_point < outside_dew_point && id(inside_humidity).state < 40));

              // combine conditions
              if (temp_condition || refined_humidity_condition) {
                id(propeller_1).turn_on();
                id(propeller_2).turn_on();
              } else {
                id(propeller_1).turn_off();
                id(propeller_2).turn_off();
              }
            }

            // programmed mode
            //  / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
            if (button_status == "prog") {
              // implement programmed control
              //  / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
              //  / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
              // if you want, you can insert your own code here to program the device yourself
              //  / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
              //  / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
            }
          }
