Skip to content

Instantly share code, notes, and snippets.

@nay-kang
Last active September 11, 2025 06:51
Show Gist options
  • Select an option

  • Save nay-kang/b6804827a131a2aa5226965949e04931 to your computer and use it in GitHub Desktop.

Select an option

Save nay-kang/b6804827a131a2aa5226965949e04931 to your computer and use it in GitHub Desktop.
homeassistant versoin 2025.9 to operate gree yadof(Kelvinator) remote AC

incompatiable IRremoteESP8266 libraries with arduino version 3.x

  • first we need install Terminal plugin to be able get access to homeassistant
  • Download IRremoteESP8266 to homeassistant /config/esphome/lib/ (or whatever path you like)
  • Download arduino version 3.x patch to /config/esphome/lib/IRremoteESP8266/src/ override IRrecv.cpp file
  • edit yaml to include this library
esphome:
  name: you-board
  libraries:
    - IRremoteESP8266=file:///config/esphome/lib/IRremoteESP8266

Add custom component

  • make a folder and create files like this.
/config/esphome/my_components/ac_kel
├── __init__.py
├── ac_kel.h
└── climate.py
#climate.py

import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import climate, sensor
from esphome.const import (
    CONF_ID,
    CONF_SENSOR,
)

# Define the C++ namespace and class name
ac_kel_ns = cg.esphome_ns.namespace("ac_kel")
AcKel = ac_kel_ns.class_("AcKel", climate.Climate, cg.Component)

# UPDATED CONFIG_SCHEMA - Add AcKel class as argument
CONFIG_SCHEMA = climate.climate_schema(AcKel).extend(
    {
        cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor),
    }
).extend(cv.COMPONENT_SCHEMA)

# The to_code function remains the same
async def to_code(config):
    sens = await cg.get_variable(config[CONF_SENSOR])
    var = cg.new_Pvariable(config[CONF_ID])
    
    await cg.register_component(var, config)
    await climate.register_climate(var, config)
    
    cg.add(var.set_sensor(sens))
#ac_kel.h

#pragma once

#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <ir_Kelvinator.h>

#include "esphome/components/climate/climate.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h"

namespace esphome {
namespace ac_kel {

const uint16_t kIrLed = 33;
IRKelvinatorAC ac(kIrLed);

class AcKel : public Component, public climate::Climate {
 public:
  void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
  void setup() override {
    if (this->sensor_) {
      this->sensor_->add_on_state_callback([this](float state) {
        this->current_temperature = state;
        this->publish_state();
      });
      this->current_temperature = this->sensor_->state;
    } else {
      this->current_temperature = NAN;
    }
    auto restore = this->restore_state_();
    if (restore.has_value()) {
      restore->apply(this);
    } else {
      this->mode = climate::CLIMATE_MODE_OFF;
      this->target_temperature =
          roundf(constrain(this->current_temperature, 16, 30));
      this->fan_mode = climate::CLIMATE_FAN_AUTO;
      this->swing_mode = climate::CLIMATE_SWING_OFF;
    }

    if (isnan(this->target_temperature)) {
      this->target_temperature = 30;
    }
    ac.begin();
  }
  void control(const climate::ClimateCall &call) override {
    if (call.get_mode().has_value()) {
      climate::ClimateMode mode = *call.get_mode();
      if (mode == climate::CLIMATE_MODE_OFF) {
        ac.off();
      } else if (mode == climate::CLIMATE_MODE_COOL) {
        ac.on();
        ac.setMode(kKelvinatorCool);
      } else if (mode == climate::CLIMATE_MODE_DRY) {
        ac.on();
        ac.setMode(kKelvinatorDry);
      } else if (mode == climate::CLIMATE_MODE_FAN_ONLY) {
        ac.on();
        ac.setMode(kKelvinatorFan);
      }
      this->mode = mode;
    }

    if (call.get_target_temperature().has_value()) {
      float temp = *call.get_target_temperature();
      ac.setTemp(temp);
      this->target_temperature = temp;
    }

    if (call.get_fan_mode().has_value()) {
      climate::ClimateFanMode fan_mode = *call.get_fan_mode();
      if (fan_mode == climate::CLIMATE_FAN_AUTO) {
        ac.setFan(kKelvinatorFanAuto);
      } else if (fan_mode == climate::CLIMATE_FAN_LOW) {
        ac.setFan(kKelvinatorFanMin);
      } else if (fan_mode == climate::CLIMATE_FAN_MEDIUM) {
        ac.setFan(3);
      } else if (fan_mode == climate::CLIMATE_FAN_HIGH) {
        ac.setFan(kKelvinatorFanMax);
      } else if (fan_mode == climate::CLIMATE_FAN_ON) {
        // Set fan to a default speed for ON mode. Using MEDIUM as a default.
        ac.setFan(3);
      }
      this->fan_mode = fan_mode;
    }

    if (call.get_swing_mode().has_value()) {
      climate::ClimateSwingMode swing_mode = *call.get_swing_mode();
      if (swing_mode == climate::CLIMATE_SWING_OFF) {
        ac.setSwingVertical(false,kKelvinatorSwingVOff);
      } else if (swing_mode == climate::CLIMATE_SWING_VERTICAL) {
        ac.setSwingVertical(false,kKelvinatorSwingVAuto);
      }
      this->swing_mode = swing_mode;
    }
    ac.setXFan(true);
    ac.setIonFilter(false);
    ac.setLight(false);
    ac.send();

    this->publish_state();

    ESP_LOGD("DEBUG", "A/C remote is in the following state:");
    ESP_LOGD("DEBUG", "  %s\n", ac.toString().c_str());
  }

  climate::ClimateTraits traits() override {
    auto traits = climate::ClimateTraits();
    traits.set_supports_current_temperature(this->sensor_ != nullptr);

    traits.set_supported_modes(
        {climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL,
         climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY,climate::CLIMATE_MODE_AUTO}); // Added FAN_ONLY mode
    traits.set_supported_fan_modes(
        {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_ON,
         climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
         climate::CLIMATE_FAN_HIGH});
    traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}); // Added swing modes
    traits.set_visual_max_temperature(30);
    traits.set_visual_min_temperature(16);
    traits.set_visual_temperature_step(1);

    return traits;
  }

 protected:
  sensor::Sensor *sensor_{nullptr};
};

}  // namespace ac_kel
}  // namespace esphome
  • then go back to edit yaml to add external_components
external_components:
  - source:
      type: local
      path: my_components
  • and finally add climate config
climate:
  - platform: ac_kel
    name: "AC"
    sensor: your_temperature_sensor

tips

  • I make wrong config using includes instead libraries.it meet compile error.but after I remove includes config and delete the folder it still show compile error.I had to go into inside esphome container to solve the problem
  • Tasmota seems has ability to control KELVINATOR AC but I don't want switch from esphome to that
  • SmartIR is another option but it seems need special hardware like broadlink
  • recently My IR remoter not work well.about only 5% successfully trigger. after many tries I found if I switch from esp32-supermini to standard esp32 board it will work fine.I don't know why.maybe my esp32-supermini has some damage or it just not support this very well.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment