Created
June 16, 2019 20:53
-
-
Save liads/c702fd4b8529991af9cd52d03b694814 to your computer and use it in GitHub Desktop.
ESPHome climate component for Electra AC (RC-3 IR remote)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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}; | |
}; |
No
But it’s possible
Can anyone knows how to add the support for the IR receiver, so a change with the regular remote will be reflected in HA?
Yes, in the github repository I have made https://github.com/omersht/ElectraEspHome
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
Did you personally utilized a temperature sensor?