From 7bf9657e2455815eee9daf9510cd356368a3425a Mon Sep 17 00:00:00 2001 From: Sagi Dayan Date: Tue, 2 Jul 2024 13:40:00 +0300 Subject: [PATCH] Initial commit - hard fork --- LICENSE | 22 +++++ README.md | 43 +++++++++ components/electra_ac/__init__.py | 0 components/electra_ac/climate.py | 29 ++++++ components/electra_ac/electra_ac.cpp | 130 +++++++++++++++++++++++++++ components/electra_ac/electra_ac.h | 37 ++++++++ example.yaml | 37 ++++++++ 7 files changed, 298 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 components/electra_ac/__init__.py create mode 100644 components/electra_ac/climate.py create mode 100644 components/electra_ac/electra_ac.cpp create mode 100644 components/electra_ac/electra_ac.h create mode 100644 example.yaml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..80d3e4f --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2023 Roman Klassen +Copyright (c) 2024 Sagi Dayan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..252422d --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# ESPHome Electra AC IR remote control component + +This implementation of the ESPHome component to control HVAC with IR channel. + +Base implementation of the protocol can be found here: [https://github.com/crankyoldgit/IRremoteESP8266](https://github.com/crankyoldgit/IRremoteESP8266) + +This repo was originally forked from [https://github.com/rozh1/esphome-haier-ac-ir](https://github.com/rozh1/esphome-haier-ac-ir) + +## Supported AC + - Brand: AUX, Model: KFR-35GW/BpNFW=3 A/C + - Brand: AUX, Model: YKR-T/011 remote + - Brand: Electra, Model: Classic INV 17 / AXW12DCS A/C + - Brand: Electra, Model: YKR-M/003E remote + - Brand: Frigidaire, Model: FGPC102AB1 A/C + - Brand: Subtropic, Model: SUB-07HN1_18Y A/C + - Brand: Subtropic, Model: YKR-H/102E remote + - Brand: Centek, Model: SCT-65Q09 A/C + - Brand: Centek, Model: YKR-P/002E remote + - Brand: AEG, Model: Chillflex Pro AXP26U338CW A/C + - Brand: Electrolux, Model: YKR-H/531E A/C + - Brand: Delonghi, Modell: PAC EM90 + +## How to use + +1. Add sensor configuration. It can be a connected temperature sensor or home assistant provided sensor. For example: + +``` +sensor: + # External temperature sensor from Home Assistant + - platform: homeassistant + id: current_temperature + entity_id: ${temperature_sensor} +``` + +2. Add climate component, set platform as `electra_ac`, set `sensor_id` and `pin` number for IR LED. + +``` +climate: + - platform: electra_ac + sensor_id: current_temperature # Sensor ID with the current temperature + pin: 3 # Pin with IR led + name: "Electra A/C" +``` diff --git a/components/electra_ac/__init__.py b/components/electra_ac/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/components/electra_ac/climate.py b/components/electra_ac/climate.py new file mode 100644 index 0000000..48d14db --- /dev/null +++ b/components/electra_ac/climate.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate, sensor +from esphome.const import CONF_ID, CONF_PIN, CONF_SENSOR_ID +from esphome.core import CORE + +AUTO_LOAD = ["climate"] + +electra_ac_ns = cg.esphome_ns.namespace("electra_ac") +ElectraClimate = electra_ac_ns.class_("ElectraClimate", climate.Climate) + +CONFIG_SCHEMA = climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ElectraClimate), + cv.Required(CONF_SENSOR_ID): cv.use_id(sensor.Sensor), + cv.Required(CONF_PIN): cv.int_, + } +) + + +async def to_code(config): + if CORE.is_esp8266 or CORE.is_esp32: + cg.add_library("crankyoldgit/IRremoteESP8266", "2.8.4") + + var = cg.new_Pvariable(config[CONF_ID]) + await climate.register_climate(var, config) + + sens = await cg.get_variable(config[CONF_SENSOR_ID]) + cg.add(var.init(sens, config[CONF_PIN])) diff --git a/components/electra_ac/electra_ac.cpp b/components/electra_ac/electra_ac.cpp new file mode 100644 index 0000000..14e9607 --- /dev/null +++ b/components/electra_ac/electra_ac.cpp @@ -0,0 +1,130 @@ +#include "electra_ac.h" + +namespace esphome { +namespace electra_ac { + +static const char *const TAG = "climate.electra_ac"; + +void ElectraClimate::init(sensor::Sensor *sensor, uint16_t pin) { + this->set_sensor(sensor); + ac_ = new IRElectraAc(pin); + 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 = 24; + this->fan_mode = climate::CLIMATE_FAN_AUTO; + this->swing_mode = climate::CLIMATE_SWING_OFF; + this->preset = climate::CLIMATE_PRESET_NONE; + } + + if (isnan(this->target_temperature)) { + this->target_temperature = 25; + } + + ac_->begin(); + this->setup_ir_cmd(); + + ESP_LOGD("DEBUG", "Electra A/C remote is in the following state:"); + ESP_LOGD("DEBUG", " %s\n", ac_->toString().c_str()); +} + +void ElectraClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + +void ElectraClimate::setup_ir_cmd() { + if (this->mode == climate::CLIMATE_MODE_OFF) { + ac_->off(); + } else { + ac_->on(); + if (this->mode == climate::CLIMATE_MODE_AUTO) { + ac_->setMode(kElectraAcAuto); + } else if (this->mode == climate::CLIMATE_MODE_COOL) { + ac_->setMode(kElectraAcCool); + } else if (this->mode == climate::CLIMATE_MODE_HEAT) { + ac_->setMode(kElectraAcHeat); + } else if (this->mode == climate::CLIMATE_MODE_DRY) { + ac_->setMode(kElectraAcDry); + } else if (this->mode == climate::CLIMATE_MODE_FAN_ONLY) { + ac_->setMode(kElectraAcFan); + } + + ac_->setTemp((uint8_t)this->target_temperature); + + if (this->fan_mode == climate::CLIMATE_FAN_AUTO) { + ac_->setFan(kElectraAcFanAuto); + } else if (this->fan_mode == climate::CLIMATE_FAN_LOW) { + ac_->setFan(kElectraAcFanLow); + } else if (this->fan_mode == climate::CLIMATE_FAN_MEDIUM) { + ac_->setFan(kElectraAcFanMed); + } else if (this->fan_mode == climate::CLIMATE_FAN_HIGH) { + ac_->setFan(kElectraAcFanHigh); + } + + if (this->swing_mode == climate::CLIMATE_SWING_OFF) { + ac_->setSwing(kElectraAcSwingOff); + } else if (this->swing_mode == climate::CLIMATE_SWING_ON) { + ac_->setSwing(kElectraAcSwingOn); + } + + ac_->setSleep(this->preset == climate::CLIMATE_PRESET_SLEEP); + ac_->setHealth(this->preset == climate::CLIMATE_PRESET_COMFORT); + ac_->setTurbo(this->preset == climate::CLIMATE_PRESET_BOOST); + } +} + +climate::ClimateTraits ElectraClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_AUTO}); + traits.set_supported_fan_modes( + {climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_AUTO}); + traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_ON}); + traits.set_supported_presets({climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_SLEEP, + climate::CLIMATE_PRESET_COMFORT, climate::CLIMATE_PRESET_BOOST}); + + traits.set_visual_max_temperature(ELECTRA_AC_TEMP_MAX); + traits.set_visual_min_temperature(ELECTRA_AC_TEMP_MIN); + traits.set_visual_temperature_step(1); + traits.set_supports_current_temperature(true); + + return traits; +} + +void ElectraClimate::control(const climate::ClimateCall &call) { + 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(); + + if (call.get_fan_mode().has_value()) + this->fan_mode = *call.get_fan_mode(); + + if (call.get_swing_mode().has_value()) + this->swing_mode = *call.get_swing_mode(); + + if (call.get_preset().has_value()) + this->preset = *call.get_preset(); + + this->setup_ir_cmd(); + ac_->send(); + + this->publish_state(); + + ESP_LOGD("DEBUG", "Electra A/C remote is in the following state:"); + ESP_LOGD("DEBUG", " %s\n", ac_->toString().c_str()); +} + +} // namespace electra_ac +} // namespace esphome diff --git a/components/electra_ac/electra_ac.h b/components/electra_ac/electra_ac.h new file mode 100644 index 0000000..99f714f --- /dev/null +++ b/components/electra_ac/electra_ac.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/sensor/sensor.h" + +#include "IRremoteESP8266.h" +#include "IRsend.h" +#include "ir_Electra.h" + + +namespace esphome { +namespace electra_ac { + +const uint8_t ELECTRA_AC_TEMP_MIN = 16; // 16C +const uint8_t ELECTRA_AC_TEMP_MAX = 30; // 32C + +class ElectraClimate : public climate::Climate { + public: + ElectraClimate() : climate::Climate() {} + public: + void set_sensor(sensor::Sensor *sensor); + void init(sensor::Sensor *sensor, uint16_t pin); + + protected: + IRElectraAc *ac_{nullptr}; + sensor::Sensor *sensor_{nullptr}; + + void setup_ir_cmd(); + + climate::ClimateTraits traits() override; + void control(const climate::ClimateCall &call) override; + +}; + +} // namespace haier_acyrw02 +} // namespace esphome diff --git a/example.yaml b/example.yaml new file mode 100644 index 0000000..8dded57 --- /dev/null +++ b/example.yaml @@ -0,0 +1,37 @@ +esphome: + name: electra-ac-ir-remote + +esp8266: + board: esp01_1m + +logger: + baud_rate: 0 + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "electra-ac-ir-remote Fallback Hotspot" + password: "0000" + +web_server: + port: 80 + +external_components: + - source: + type: git + url: https://git.dayanhub.com/sagi/esphome-electra-ac-ir + +sensor: + # External temperature sensor from Home Assistant + - platform: homeassistant + id: current_temperature + entity_id: ${temperature_sensor} + +climate: + - platform: electra_ac + sensor_id: current_temperature + pin: 3 # Pin with IR led + name: "Electra A/C"