Skip to content

Instantly share code, notes, and snippets.

@liads
Created June 16, 2019 20:53
Show Gist options
  • Save liads/c702fd4b8529991af9cd52d03b694814 to your computer and use it in GitHub Desktop.
Save liads/c702fd4b8529991af9cd52d03b694814 to your computer and use it in GitHub Desktop.
ESPHome climate component for Electra AC (RC-3 IR remote)
#include "esphome.h"
static const char *TAG = "electra.climate";
typedef enum IRElectraMode {
IRElectraModeCool = 0b001,
IRElectraModeHeat = 0b010,
IRElectraModeAuto = 0b011,
IRElectraModeDry = 0b100,
IRElectraModeFan = 0b101,
IRElectraModeOff = 0b111
} IRElectraMode;
typedef enum IRElectraFan {
IRElectraFanLow = 0b00,
IRElectraFanMedium = 0b01,
IRElectraFanHigh = 0b10,
IRElectraFanAuto = 0b11
} IRElectraFan;
// That configuration has a total of 34 bits
// 33: Power bit, if this bit is ON, the A/C will toggle it's power.
// 32-30: Mode - Cool, heat etc.
// 29-28: Fan - Low, medium etc.
// 27-26: Zeros
// 25: Swing On/Off
// 24: iFeel On/Off
// 23: Zero
// 22-19: Temperature, where 15 is 0000, 30 is 1111
// 18: Sleep mode On/Off
// 17- 2: Zeros
// 1: One
// 0: Zero
typedef union ElectraCode {
uint64_t num;
struct {
uint64_t zeros1 : 1;
uint64_t ones1 : 1;
uint64_t zeros2 : 16;
uint64_t sleep : 1;
uint64_t temperature : 4;
uint64_t zeros3 : 1;
uint64_t ifeel : 1;
uint64_t swing : 1;
uint64_t zeros4 : 2;
uint64_t fan : 2;
uint64_t mode : 3;
uint64_t power : 1;
};
} ElectraCode;
const uint8_t ELECTRA_TEMP_MIN = 16; // Celsius
const uint8_t ELECTRA_TEMP_MAX = 30; // Celsius
#define ELECTRA_TIME_UNIT 1000
#define ELECTRA_NUM_BITS 34
class ElectraClimate : public climate::Climate, public Component {
public:
void setup() override
{
if (this->sensor_) {
this->sensor_->add_on_state_callback([this](float state) {
this->current_temperature = state;
// current temperature changed, publish state
this->publish_state();
});
this->current_temperature = this->sensor_->state;
} else
this->current_temperature = NAN;
// restore set points
auto restore = this->restore_state_();
if (restore.has_value()) {
restore->apply(this);
} else {
// restore from defaults
this->mode = climate::CLIMATE_MODE_AUTO;
// initialize target temperature to some value so that it's not NAN
this->target_temperature = roundf(this->current_temperature);
}
this->active_mode_ = this->mode;
}
void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
this->transmitter_ = transmitter;
}
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
/// Override control to change settings of the climate device
void control(const climate::ClimateCall &call) override
{
if (call.get_mode().has_value())
this->mode = *call.get_mode();
if (call.get_target_temperature().has_value())
this->target_temperature = *call.get_target_temperature();
this->transmit_state_();
this->publish_state();
this->active_mode_ = this->mode;
}
/// Return the traits of this controller
climate::ClimateTraits traits() override
{
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(this->sensor_ != nullptr);
traits.set_supports_auto_mode(true);
traits.set_supports_cool_mode(this->supports_cool_);
traits.set_supports_heat_mode(this->supports_heat_);
traits.set_supports_two_point_target_temperature(false);
traits.set_supports_away(false);
traits.set_visual_min_temperature(ELECTRA_TEMP_MIN);
traits.set_visual_max_temperature(ELECTRA_TEMP_MAX);
traits.set_visual_temperature_step(1);
return traits;
}
/// Transmit the state of this climate controller via IR
void transmit_state_()
{
ElectraCode code = { 0 };
code.ones1 = 1;
code.fan = IRElectraFan::IRElectraFanAuto;
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
code.mode = IRElectraMode::IRElectraModeCool;
code.power = this->active_mode_ == climate::CLIMATE_MODE_OFF ? 1 : 0;
break;
case climate::CLIMATE_MODE_HEAT:
code.mode = IRElectraMode::IRElectraModeHeat;
code.power = this->active_mode_ == climate::CLIMATE_MODE_OFF ? 1 : 0;
break;
case climate::CLIMATE_MODE_AUTO:
code.mode = IRElectraMode::IRElectraModeAuto;
code.power = this->active_mode_ == climate::CLIMATE_MODE_OFF ? 1 : 0;
break;
case climate::CLIMATE_MODE_OFF:
default:
code.mode = IRElectraMode::IRElectraModeOff;
break;
}
auto temp = (uint8_t) roundf(clamp(this->target_temperature, ELECTRA_TEMP_MIN, ELECTRA_TEMP_MAX));
code.temperature = temp - 15;
ESP_LOGD(TAG, "Sending electra code: %lld", code.num);
auto transmit = this->transmitter_->transmit();
auto data = transmit.get_data();
data->set_carrier_frequency(38000);
uint16_t repeat = 3;
for (uint16_t r = 0; r < repeat; r++) {
// Header
data->mark(3 * ELECTRA_TIME_UNIT);
uint16_t next_value = 3 * ELECTRA_TIME_UNIT;
bool is_next_space = true;
// Data
for (int j = ELECTRA_NUM_BITS - 1; j>=0; j--)
{
uint8_t bit = (code.num >> j) & 1;
// if current index is SPACE
if (is_next_space) {
// one is one unit low, then one unit up
// since we're pointing at SPACE, we should increase it by a unit
// then add another MARK unit
if (bit == 1) {
data->space(next_value + ELECTRA_TIME_UNIT);
next_value = ELECTRA_TIME_UNIT;
is_next_space = false;
} else {
// we need a MARK unit, then SPACE unit
data->space(next_value);
data->mark(ELECTRA_TIME_UNIT);
next_value = ELECTRA_TIME_UNIT;
is_next_space = true;
}
} else {
// current index is MARK
// one is one unit low, then one unit up
if (bit == 1) {
data->mark(next_value);
data->space(ELECTRA_TIME_UNIT);
next_value = ELECTRA_TIME_UNIT;
is_next_space = false;
} else {
data->mark(next_value + ELECTRA_TIME_UNIT);
next_value = ELECTRA_TIME_UNIT;
is_next_space = true;
}
}
}
// Last value must be SPACE
data->space(next_value);
}
// Footer
data->mark(4 * ELECTRA_TIME_UNIT);
transmit.perform();
}
ClimateMode active_mode_;
bool supports_cool_{true};
bool supports_heat_{true};
remote_transmitter::RemoteTransmitterComponent *transmitter_;
sensor::Sensor *sensor_{nullptr};
};
@schlumm
Copy link

schlumm commented Nov 3, 2024

i added the option for decode the IR signal, in case that someone is using the old IR transmitter.

/* #include <iostream>
#include <vector>
#include <string>
#include <cmath>
using namespace std; */

/* struct ac_code {
    int fan;
    int mode;
    int temp;
}; */

// A function to check if a value is within 5% tolerance of a target value
bool within_tolerance(int value, int target) {
    return std::abs(value - target) <= (0.05 * target);
}

std::vector<int> decode_ir_signal(std::vector<int>& vec) {
    // A constant to store the number of bits in the code
    const int ELECTRA_NUM_BITS2 = 34;
    std::vector<int> decoded_bits(ELECTRA_NUM_BITS2, 0); // bit has ELECTRA_NUM_BITS elements, all equal to 0
    
    //cout << "\n"; // print a newline

    // A constant to store the time unit in microseconds
    const int ELECTRA_TIME_UNIT2 = 1000;
    // A variable to store the next value to be pushed into the vector
    int next_value = 3 * ELECTRA_TIME_UNIT2;
    // A boolean flag to indicate whether the next value is a space or not
    bool is_next_space = true;
    int code_index = 0;

    for (int j = 1; j < vec.size() - 1; j++) {
        if (j > 5 && std::abs(vec[j]) > 2800) {
            break;
        }

        int x = vec[j];

        // if current index is SPACE
        if (is_next_space) {
            if (within_tolerance(abs(vec[j]), next_value + ELECTRA_TIME_UNIT2)) {
                decoded_bits[code_index] = 1;
/*                 cout << "s1b1 ";
                cout << "\n"; // print a newline */
                is_next_space = false;
                next_value = ELECTRA_TIME_UNIT2;
            } else {
                decoded_bits[code_index] = 0;
/*                 cout << "s1b0 ";
                cout << "\n"; // print a newline */
                next_value = ELECTRA_TIME_UNIT2;
                is_next_space = true;
                j = j + 1;
            }
        } else {
            if (within_tolerance(abs(vec[j]), next_value)) {
                decoded_bits[code_index] = 1;
/*                 cout << "s0b1 ";
                cout << "\n"; // print a newline */
                j = j + 1;
                next_value = ELECTRA_TIME_UNIT2;
                is_next_space = false;
            } else {
                decoded_bits[code_index] = 0;
/*                 cout << "s0b0 ";
                cout << "\n"; // print a newline */
                next_value = ELECTRA_TIME_UNIT2;
                is_next_space = true;
            }
        }
        code_index = code_index + 1;
    }

    return decoded_bits;
}

the yaml should be like this:

esphome:
  name: ac-parents-electra
  friendly_name: Parents AC
  includes:
    - ElectraClimate.h
    - ElectraDecodeFinal.h

esp8266:
  board: esp01_1m

# Enable logging
logger:

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

ota:
  - platform: esphome
    password: "75ff13a"

wifi:
  networks:
    - ssid: !secret wifi_ssid
      password: !secret wifi_password
    - ssid: !secret wifi_ssid2
      password: !secret wifi_password2
    
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Ac-Parents-Electra"
    password: "ySYKNFeRJZk2"

captive_portal:

sensor:
  - platform: dht
    model: DHT22
    pin: 0
    id: dht_sensor
    temperature:
      name: "AC Parents Temperature"
      id: dht_temp
    humidity:
      name: "AC Parents Humidity"
    update_interval: 60s  

remote_transmitter:
  pin: 3  # RX pin
  carrier_duty_percent: 50%
  id: my_ir_transmitter

remote_receiver:
  pin: 
    number: 1  # TX pin
    inverted: True
    mode: INPUT_PULLUP
  id: ir_receiver
  dump: raw
  tolerance: 55%
  filter: 500us
  on_raw:
    then:
      - lambda: |-
          if ((std::abs(x[0]) > 2800) && (std::abs(x[1]) > 2800)) {// change the 2nd pulse to 2800 to includes bit power=0

            // Log the current AC status
            ElectraCode current;
            switch (id(climate_id).mode) {
              case climate::CLIMATE_MODE_COOL:
                current.mode=IRElectraMode::IRElectraModeCool;
                break;
              case climate::CLIMATE_MODE_AUTO:
                current.mode = IRElectraMode::IRElectraModeAuto;
                break;
              case climate::CLIMATE_MODE_HEAT:
                current.mode=IRElectraMode::IRElectraModeHeat;
                break;
              case climate::CLIMATE_MODE_OFF:
                current.mode = IRElectraMode::IRElectraModeOff;
                break;
              case climate::CLIMATE_MODE_DRY:
                current.mode = IRElectraMode::IRElectraModeDry;
                break;
            }

            switch (id(climate_id).fan_mode.value()) {
              case climate::CLIMATE_FAN_LOW:
                current.fan=IRElectraFan::IRElectraFanLow;
                break;
              case climate::CLIMATE_FAN_MEDIUM:
                current.fan = IRElectraFan::IRElectraFanMedium;
                break;
              case climate::CLIMATE_FAN_HIGH:
                current.fan=IRElectraFan::IRElectraFanHigh;
                break;
            }
            
            ESP_LOGD("climate", "Current AC mode: %d", current.mode);
            ESP_LOGD("climate", "Current Fan mode: %d", current.fan);
            // Log the current target_temperature
            float current_target_temperature = id(climate_id).target_temperature;
            ESP_LOGD("climate", "Current AC target Temperature: %.1f°C", current_target_temperature);

            // decodeding the IR signal
            auto decoded_bits = decode_ir_signal(x);           
            auto mode =0;
            for (int i = 1; i <= 3; i++) {
                mode = mode << 1 | (decoded_bits[i] & 1);
            }
            int target_temp=(15 + 8 * decoded_bits[11] + 4 * decoded_bits[12] + 2 * decoded_bits[13] + decoded_bits[14]);
            ESP_LOGD("IR Receiver", " target_temp- dec: %lld", target_temp);
            
            ElectraCode code;
            code.num = 0;
            for (int i = 0; i < decoded_bits.size(); i++) {
              code.num = code.num << 1 | (decoded_bits[i] & 1);
            }// code.num is reversed of the string decoded_bits
            ESP_LOGD("IR Receiver", "Received code dec: %lld", code.num);
            ESP_LOGD("IR Receiver", "AC mode from the code: %lld", code.mode);
            ESP_LOGD("IR Receiver", "AC fan from the code: %lld", code.fan);
            ESP_LOGD("IR Receiver", "AC target Temperature from the code: %lld", code.temperature+15);

            // update the target temperature and fan no matter what is the code.power state is
              if(code.temperature>0){// In case that the recieved code is not nonsense
                id(climate_id).target_temperature = code.temperature + 15;//Temperature, where 15 is 0000, 30 is 1111
              }
              switch (code.fan) {
                  case 0:
                    id(climate_id).fan_mode = esphome::climate::CLIMATE_FAN_LOW;
                    break;
                  case 1:
                    id(climate_id).fan_mode = esphome::climate::CLIMATE_FAN_MEDIUM;
                    break;
                  case 2:
                    id(climate_id).fan_mode = esphome::climate::CLIMATE_FAN_HIGH;
                    break;
                  case 3:
                    id(climate_id).fan_mode = esphome::climate::CLIMATE_FAN_ON;
                    break;
                  default:
                    id(climate_id).fan_mode.reset();  // Optional: if the value is out of range, reset the optional
                    break;
                }

            if (code.power){
              if (id(climate_id).mode!=climate::CLIMATE_MODE_OFF) {
                id(climate_id).mode=climate::CLIMATE_MODE_OFF;
              }
              else {           
                if (code.mode == IRElectraMode::IRElectraModeCool) {
                    id(climate_id).mode = climate::CLIMATE_MODE_COOL;
                  } else if (code.mode == IRElectraMode::IRElectraModeHeat) {
                    id(climate_id).mode = climate::CLIMATE_MODE_HEAT;
                  } else if (code.mode == IRElectraMode::IRElectraModeAuto) {
                    id(climate_id).mode = climate::CLIMATE_MODE_AUTO;
                  } else if (code.mode == IRElectraMode::IRElectraModeDry) {
                    id(climate_id).mode = climate::CLIMATE_MODE_DRY;
                  } else if (code.mode == IRElectraMode::IRElectraModeFan) {
                    id(climate_id).mode = climate::CLIMATE_MODE_FAN_ONLY;
                  } else if (code.mode == IRElectraMode::IRElectraModeOff) {
                    id(climate_id).mode = climate::CLIMATE_MODE_OFF;
                  } else if (code.mode == 0) {
                    id(climate_id).mode = climate::CLIMATE_MODE_OFF;
                  } 
                }
                ESP_LOGD("IR Receiver", "In the if, so something was changed - mode,fan or temp (code.mode=1 and mode !=off)");

              // if((id(climate_id).mode ==code.mode )&&(id(climate_id).target_temperature==code.temperature + 15)&&(id(climate_id).fan_mode==code.fan)){
              //   ESP_LOGD("IR Receiver", "Nothing was changed - mode,fan or temp");
              //   id(climate_id).mode = climate::CLIMATE_MODE_OFF;
              // }
              }
            else {//code.power=0
              ESP_LOGD("IR Receiver", "In the else statment (code.power=0). no update to the mode");
              }
            id(climate_id).publish_state();
            ESP_LOGD("IR Receiver", "Publish the state");    
            delay (5000) ;//delay in order not to includes multiple IR signal of the same state
          }
climate:
  - platform: custom
    lambda: |-
      auto electra_climate = new ElectraClimate();
      electra_climate->set_sensor(id(dht_temp)); // Optional
      electra_climate->set_transmitter(id(my_ir_transmitter));
      App.register_component(electra_climate);
      return {electra_climate};

    climates:
      - name: "Parents AC"
        id: climate_id

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment