ESPHome Window Air Conditioner

Hi community,

I’ve just gotten started with esphome, and it’s fantastic. I’m looking at putting together a replacement for my window AC control interface. The hardware is pretty simple: three relays (one for compressor, one for fan on/off, one for fan speed high/low), and last summer I managed to get it working with tasmota, but it was pretty basic and simple. To be clear: this is not a window AC with an IR remote, I’ve completely gutted the control system.

I’m planning on integrating an i2c oled display, HTU21D temperature/humidity sensor, and some tactile switches as a physical interface on the window unit itself, and ideally it would integrate seamlessly with the Home Assistant MQTT generic thermostat component.

I’ve had some trouble finding resources that will help me learn about the lambda functions and how to integrate things. The cookbook has been immeasurably helpful, but I’ve hit a wall.

I’m looking for help figuring out a few things that have so far got me stumped:

  • how to set up the fan so it’s linked to one of the relays (e.g. when the fan is on, the relay is on) but that the speed is linked to a separate relay pin
  • ensuring that if/when compressor is turned on, the fan will run, and when the compressor shuts off, the fan will run for two minutes then shut itself off
  • setting a minimum run time of two minutes for the compressor (to prevent damage from power cycling too quickly)
  • using icons on the OLED to show state (e.g. fan off/running, ac mode cool or off)

Here’s what I’ve got so far:

esphome:
  name: airconditioner1
  platform: ESP8266
  board: d1_mini

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

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

mqtt:
  broker: 192.168.1.13
  username: !secret mqtt-user
  password: !secret mqtt-pass
  discovery: true
  
status_led:
  pin:
    number: D4
    inverted: true
  
time:
  - platform: homeassistant
    id: time

switch:
  - platform: gpio
    name: "Compressor"
    id: compressor
    pin: D6
    inverted: true
    icon: "mdi:snowflake"

  - platform: gpio
    name: "Fan"
    id: fan_output
    pin: D5
    inverted: true
    icon: "mdi:fan"
    
  - platform: gpio
    name: "Fan Speed"
    id: fan_speed
    pin: D7
    inverted: true
    icon: "mdi:weather-windy"

sensor:
  - platform: htu21d
    temperature:
      name: "Temperature"
      id: temp
    humidity:
      name: "Humidity"
      id: hum
    update_interval: 60s
  
    
# text_sensor:
#   - name: "Mode"
#     id: mode
#   - name: "State"
#     id: state
#     internal: true
    
font:
- file: 'slkscr.ttf'
  id: font1
  size: 8

- file: 'bebas.ttf'
  id: font2
  size: 36

- file: 'arial_narrow_7.ttf'
  id: font3
  size: 14
    
i2c:
  sda: D1
  scl: D2
  scan: False

display:
  - platform: ssd1306_i2c
    model: "SH1106 128x64"
    reset_pin: D0
    address: 0x3C
    lambda: |-
      
      it.printf(64, 0, id(font1), TextAlign::TOP_CENTER, "Air Conditioner");

      // Print time in HH:MM format
      it.strftime(0, 60, id(font2), TextAlign::BASELINE_LEFT, "%H:%M", id(time).now());

      // Print inside temperature (from homeassistant sensor)
      if (id(temp).has_state()) {
        it.printf(127, 23, id(font3), TextAlign::TOP_RIGHT , "%.1f°", id(temp).state);
      }
      
      // Print outside temperature (from homeassistant sensor)
      if (id(hum).has_state()) {
        it.printf(127, 60, id(font3), TextAlign::BASELINE_RIGHT , "%.0f%%", id(hum).state);
      }
2 Likes

Progress! I’ve now managed to set up logic to ensure that the AC compressor won’t cycle too quickly, and to run the fan for a short time after powering off the compressor, to help cool it down. Things are quickly getting complex, but I’m attempting to keep the code legible by using scripts.

It will also now display the current mode (compressor and fan, fan high, or fan low) on the display using iconography. I just grabbed a few simple icons from https://materialdesignicons.com.

Here’s the configuration for the device. [edit: I have created a gist on github to keep the latest code up to date, as well as protect myself from myself.] Note that for testing, I’ve modified the timeout values from 2 minutes to much shorter so that I can see the relay behaviour.

esphome:
  name: airconditioner1
  platform: ESP8266
  board: d1_mini
  on_boot:
    then:
      - output.turn_off: compressor_output
      - output.turn_off: fan_output
      - output.turn_off: fan_speed_output
      - display.page.show: ac_idle

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

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

mqtt:
  broker: 192.168.1.13
  username: !secret mqtt-user
  password: !secret mqtt-pass
  discovery: true
  id: mqtt_client

status_led:
  pin:
    number: D4
    inverted: true

time:
  - platform: homeassistant
    id: time

binary_sensor:
  - platform: status
    name: "Status"

  - platform: template
    id: compressorSafeToTurnOff
    internal: true

  - platform: template
    id: fanSafeToTurnOff
    internal: true

output:
  - platform: gpio
    id: compressor_output
    pin: D6
    inverted: true

  - platform: gpio
    id: fan_output
    pin: D5
    inverted: true

  - platform: gpio
    id: fan_speed_output
    pin: D7
    inverted: true


globals:
  - id: opMode # operation mode, 0 (off), 1 (auto), 2 (cool), 3 (fan_only)
    type: byte
    restore_value: no
    initial_value: '0'

  - id: targetTemp # target temperature
    type: byte
    restore_value: no
    initial_value: '21'

  - id: currentTemp
    type: byte
    restore_value: no

script:
  - id: compressor_on
    then:
      - script.execute: fan_on
      - output.turn_on: compressor_output
      - lambda: 'id(compressor_state).publish_state(true);'
      - lambda: 'id(compressorSafeToTurnOff).publish_state(false);'
      # - delay: 2min
      - delay: 5s
      - lambda: 'id(compressorSafeToTurnOff).publish_state(true);'
      # - delay: 5s

  - id: compressor_off
    then:
        - lambda: 'id(compressor_state).publish_state(false);'
        - wait_until:
            binary_sensor.is_on: compressorSafeToTurnOff
        - if:
            condition:
              lambda: 'return id(compressor_state).state == false;'
            then:
              - logger.log: "Turning off compressor"
              - output.turn_off: compressor_output
              - script.execute: fan_timeout
              - script.execute: fan_off

  - id: fan_timeout
    then:
      - lambda: 'id(fanSafeToTurnOff).publish_state(false);'
      # - delay: 2min
      - delay: 15s
      - lambda: 'id(fanSafeToTurnOff).publish_state(true);'

  - id: fan_on
    then:
      - output.turn_on: fan_output
      - lambda: 'id(fan_state).publish_state(true);'

  - id: fan_off
    then:
      - logger.log: "Turn off fan"
      - lambda: 'id(fan_state).publish_state(false);'
      - wait_until:
          binary_sensor.is_on: fanSafeToTurnOff
      - if:
          condition:
            lambda: 'return id(fan_state).state == false;'
          then:
            - output.turn_off: fan_output

sensor:
  # HTU21D used to determine ambient temperature. Can be replaced by any other temperature sensor (provided the logic remains the same)
  - platform: mqtt_subscribe
    id: setTemp
    topic: climate/airconditioner1/temp/set
    internal: true
    on_value:
      - then:
          - lambda: |-
              id(targetTemp) = (int)(id(setTemp).state);
          - mqtt.publish:
              topic: "climate/airconditioner1/temp/state"
              payload: !lambda "return to_string(id(targetTemp));"

  - platform: htu21d
    temperature:
      name: "Temperature"
      id: temp
      on_value:
        - then:
            - lambda: |-
                id(currentTemp) = id(temp).state;
            - mqtt.publish:
                topic: "climate/airconditioner1/temp"
                payload: !lambda "return to_string(id(currentTemp));"
    humidity:
      name: "Humidity"
      id: hum
    update_interval: 60s

  # BH1750 Lux Sensor - not required for operation
  - platform: bh1750
    name: "Lux"
    id: lux
    address: 0x23
    update_interval: 60s

text_sensor:
  - platform: mqtt_subscribe
    id: setMode
    name: "Mode"
    topic: climate/airconditioner1/mode/set
    # internal: true
    on_value:
      - then:
        - if:
            condition:
              lambda: |-
                if (id(setMode).state == "off") {
                  id(opMode) = 0;
                  return true;
                } else if (id(setMode).state == "auto") {
                  id(opMode) = 1;
                  return true;
                } else if (id(setMode).state == "cool") {
                  id(opMode) = 2;
                  return true;
                } else if (id(setMode).state == "fan_only") {
                  id(opMode) = 4;
                  return true;
                } else {
                  return false;
                }
            then:
              - mqtt.publish:
                  topic: "climate/airconditioner1/mode"
                  payload: !lambda "return id(setMode).state;"

light:
  - platform: fastled_clockless
    chipset: WS2812B
    pin: D3
    num_leds: 8
    rgb_order: GRB
    name: "Air Conditioner Light"

switch:
  - platform: template
    name: "Compressor State"
    id: compressor_state
    icon: "mdi:snowflake"
    turn_on_action:
      - script.execute: compressor_on
    turn_off_action:
      - script.execute: compressor_off

  - platform: template
    name: "Fan State"
    id: fan_state
    icon: "mdi:fan"
    turn_on_action:
      - script.execute: fan_on
    turn_off_action:
      - script.execute: fan_off

  - platform: template
    name: "Fan Speed"
    id: fan_speed


font:
- file: 'slkscr.ttf'
  id: font1
  size: 8

- file: 'bebas.ttf'
  id: font2
  size: 36
  glyphs: "0123456789: "

- file: 'arial_narrow_7.ttf'
  id: font3
  size: 12
  glyphs: "0123456789°%."

image:
  - file: "fan-large.png"
    id: fanHighIcon
  - file: "fan_outline-large.png"
    id: fanLowIcon
  - file: "snowflake-large.png"
    id: coolIcon

i2c:
  sda: D1
  scl: D2
  scan: False

display:
  - platform: ssd1306_i2c
    model: "SH1106 128x64"
    reset_pin: D0
    address: 0x3C
    id: oled
    pages:
      - id: ac_idle
        lambda: |-

          it.printf(64, 0, id(font1), TextAlign::TOP_CENTER, "Air Conditioner");

          // Print time in HH:MM format
          it.strftime(42, 0, id(font2), TextAlign::TOP_CENTER, "%H:%M", id(time).now());

          // Print temperature
          if (id(temp).has_state()) {
            it.printf(3, 50, id(font3), TextAlign::TOP_LEFT , "%.1f°", id(temp).state);
          }

          // Print humidity
          if (id(hum).has_state()) {
            it.printf(42, 50, id(font3), TextAlign::TOP_CENTER , "%.0f%%", id(hum).state);
          }

          // Print lux
          if (id(lux).has_state()) {
            it.printf(80, 50, id(font3), TextAlign::TOP_RIGHT , "%.0f", id(lux).state);
          }


          if (id(compressor_state).state) {
            it.image(80, 12, id(coolIcon));
          } else if (id(fan_state).state) {
            if (id(fan_speed).state) {
              it.image(80, 12, id(fanHighIcon));
            } else {
              it.image(80, 12, id(fanLowIcon));
            }
          }

# interval:
#   - interval: 5s
#     then:
#       - display.page.show_next: oled
#       - component.update: oled
1 Like

Amazing. I want to do the same thing but for a cold room for my homebrewery!!!

The very first stable alpha should be in a place where it’s functional. You can take a look at the readme file in the git repository to get started on your device. I haven’t yet drawn up a schematic, but from the pin definitions, it seems pretty self-explanatory.

I moved from the gist to a full-blown git repository, as I needed a way to bundle the multiple files this project depends on for easy execution (e.g. the icon files and fonts). You can find it all here.

I’ll be continuing to add to the project, but likely not for a few months. I’ll be moving at the start of the summer, so need to wrap things up, and I’m not going to be installing the AC units here. Until then, I’ll keep on tinkering. I’ve gotten a basic proof-of-concept for an IR remote control, so I’ll be integrating that, and when my rotary encoders arrive, I’ll be looking at building a better interface.

Until then, I’m going to be trying to get the LEDs to indicate status of the device.

Hope it helps you out in your cold room. In theory, it should be able to work with any device where you’re running it to reduce the sensed value (e.g. a dehumidifier would operate the same way) and with a couple of small tweaks to the comparison, you could use it for heat/humidification as well.

1 Like