mirror of
https://github.com/Threnklyn/esphome-dev.git
synced 2026-06-03 19:38:30 +02:00
New Midea IR component, improvements and fixes (#2847)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import climate_ir
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
AUTO_LOAD = ["climate_ir", "coolix"]
|
||||
CODEOWNERS = ["@dudanov"]
|
||||
|
||||
midea_ir_ns = cg.esphome_ns.namespace("midea_ir")
|
||||
MideaIR = midea_ir_ns.class_("MideaIR", climate_ir.ClimateIR)
|
||||
|
||||
CONF_USE_FAHRENHEIT = "use_fahrenheit"
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(MideaIR),
|
||||
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await climate_ir.register_climate_ir(var, config)
|
||||
cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT]))
|
||||
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/remote_base/midea_protocol.h"
|
||||
#include "esphome/components/climate/climate_mode.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace midea_ir {
|
||||
|
||||
using climate::ClimateMode;
|
||||
using climate::ClimateFanMode;
|
||||
using remote_base::MideaData;
|
||||
|
||||
class ControlData : public MideaData {
|
||||
public:
|
||||
// Default constructor (power: ON, mode: AUTO, fan: AUTO, temp: 25C)
|
||||
ControlData() : MideaData({MIDEA_TYPE_CONTROL, 0x82, 0x48, 0xFF, 0xFF}) {}
|
||||
// Copy from Base
|
||||
ControlData(const MideaData &data) : MideaData(data) {}
|
||||
|
||||
void set_temp(float temp);
|
||||
float get_temp() const;
|
||||
|
||||
void set_mode(ClimateMode mode);
|
||||
ClimateMode get_mode() const;
|
||||
|
||||
void set_fan_mode(ClimateFanMode mode);
|
||||
ClimateFanMode get_fan_mode() const;
|
||||
|
||||
void set_sleep_preset(bool value) { this->set_mask_(1, value, 64); }
|
||||
bool get_sleep_preset() const { return this->get_value_(1, 64); }
|
||||
|
||||
void set_fahrenheit(bool value) { this->set_mask_(2, value, 32); }
|
||||
bool get_fahrenheit() const { return this->get_value_(2, 32); }
|
||||
|
||||
void fix();
|
||||
|
||||
protected:
|
||||
enum Mode : uint8_t {
|
||||
MODE_COOL,
|
||||
MODE_DRY,
|
||||
MODE_AUTO,
|
||||
MODE_HEAT,
|
||||
MODE_FAN_ONLY,
|
||||
};
|
||||
enum FanMode : uint8_t {
|
||||
FAN_AUTO,
|
||||
FAN_LOW,
|
||||
FAN_MEDIUM,
|
||||
FAN_HIGH,
|
||||
};
|
||||
void set_fan_mode_(FanMode mode) { this->set_value_(1, mode, 3, 3); }
|
||||
FanMode get_fan_mode_() const { return static_cast<FanMode>(this->get_value_(1, 3, 3)); }
|
||||
void set_mode_(Mode mode) { this->set_value_(1, mode, 7); }
|
||||
Mode get_mode_() const { return static_cast<Mode>(this->get_value_(1, 7)); }
|
||||
void set_power_(bool value) { this->set_mask_(1, value, 128); }
|
||||
bool get_power_() const { return this->get_value_(1, 128); }
|
||||
};
|
||||
|
||||
class FollowMeData : public MideaData {
|
||||
public:
|
||||
// Default constructor (temp: 30C, beeper: off)
|
||||
FollowMeData() : MideaData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {}
|
||||
// Copy from Base
|
||||
FollowMeData(const MideaData &data) : MideaData(data) {}
|
||||
// Direct from temperature and beeper values
|
||||
FollowMeData(uint8_t temp, bool beeper = false) : FollowMeData() {
|
||||
this->set_temp(temp);
|
||||
this->set_beeper(beeper);
|
||||
}
|
||||
|
||||
/* TEMPERATURE */
|
||||
uint8_t temp() const { return this->get_value_(4) - 1; }
|
||||
void set_temp(uint8_t val) { this->set_value_(4, std::min(MAX_TEMP, val) + 1); }
|
||||
|
||||
/* BEEPER */
|
||||
bool beeper() const { return this->get_value_(3, 128); }
|
||||
void set_beeper(bool value) { this->set_mask_(3, value, 128); }
|
||||
|
||||
protected:
|
||||
static const uint8_t MAX_TEMP = 37;
|
||||
};
|
||||
|
||||
class SpecialData : public MideaData {
|
||||
public:
|
||||
SpecialData(uint8_t code) : MideaData({MIDEA_TYPE_SPECIAL, code, 0xFF, 0xFF, 0xFF}) {}
|
||||
static const uint8_t VSWING_STEP = 1;
|
||||
static const uint8_t VSWING_TOGGLE = 2;
|
||||
static const uint8_t TURBO_TOGGLE = 9;
|
||||
};
|
||||
|
||||
} // namespace midea_ir
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,201 @@
|
||||
#include "midea_ir.h"
|
||||
#include "midea_data.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/coolix/coolix.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace midea_ir {
|
||||
|
||||
static const char *const TAG = "midea_ir.climate";
|
||||
|
||||
void ControlData::set_temp(float temp) {
|
||||
uint8_t min;
|
||||
if (this->get_fahrenheit()) {
|
||||
min = MIDEA_TEMPF_MIN;
|
||||
temp = esphome::clamp<float>(celsius_to_fahrenheit(temp), MIDEA_TEMPF_MIN, MIDEA_TEMPF_MAX);
|
||||
} else {
|
||||
min = MIDEA_TEMPC_MIN;
|
||||
temp = esphome::clamp<float>(temp, MIDEA_TEMPC_MIN, MIDEA_TEMPC_MAX);
|
||||
}
|
||||
this->set_value_(2, lroundf(temp) - min, 31);
|
||||
}
|
||||
|
||||
float ControlData::get_temp() const {
|
||||
const uint8_t temp = this->get_value_(2, 31);
|
||||
if (this->get_fahrenheit())
|
||||
return fahrenheit_to_celsius(static_cast<float>(temp + MIDEA_TEMPF_MIN));
|
||||
return static_cast<float>(temp + MIDEA_TEMPC_MIN);
|
||||
}
|
||||
|
||||
void ControlData::fix() {
|
||||
// In FAN_AUTO, modes COOL, HEAT and FAN_ONLY bit #5 in byte #1 must be set
|
||||
const uint8_t value = this->get_value_(1, 31);
|
||||
if (value == 0 || value == 3 || value == 4)
|
||||
this->set_mask_(1, true, 32);
|
||||
// In FAN_ONLY mode we need to set all temperature bits
|
||||
if (this->get_mode_() == MODE_FAN_ONLY)
|
||||
this->set_mask_(2, true, 31);
|
||||
}
|
||||
|
||||
void ControlData::set_mode(ClimateMode mode) {
|
||||
switch (mode) {
|
||||
case ClimateMode::CLIMATE_MODE_OFF:
|
||||
this->set_power_(false);
|
||||
return;
|
||||
case ClimateMode::CLIMATE_MODE_COOL:
|
||||
this->set_mode_(MODE_COOL);
|
||||
break;
|
||||
case ClimateMode::CLIMATE_MODE_DRY:
|
||||
this->set_mode_(MODE_DRY);
|
||||
break;
|
||||
case ClimateMode::CLIMATE_MODE_FAN_ONLY:
|
||||
this->set_mode_(MODE_FAN_ONLY);
|
||||
break;
|
||||
case ClimateMode::CLIMATE_MODE_HEAT:
|
||||
this->set_mode_(MODE_HEAT);
|
||||
break;
|
||||
default:
|
||||
this->set_mode_(MODE_AUTO);
|
||||
break;
|
||||
}
|
||||
this->set_power_(true);
|
||||
}
|
||||
|
||||
ClimateMode ControlData::get_mode() const {
|
||||
if (!this->get_power_())
|
||||
return ClimateMode::CLIMATE_MODE_OFF;
|
||||
switch (this->get_mode_()) {
|
||||
case MODE_COOL:
|
||||
return ClimateMode::CLIMATE_MODE_COOL;
|
||||
case MODE_DRY:
|
||||
return ClimateMode::CLIMATE_MODE_DRY;
|
||||
case MODE_FAN_ONLY:
|
||||
return ClimateMode::CLIMATE_MODE_FAN_ONLY;
|
||||
case MODE_HEAT:
|
||||
return ClimateMode::CLIMATE_MODE_HEAT;
|
||||
default:
|
||||
return ClimateMode::CLIMATE_MODE_HEAT_COOL;
|
||||
}
|
||||
}
|
||||
|
||||
void ControlData::set_fan_mode(ClimateFanMode mode) {
|
||||
switch (mode) {
|
||||
case ClimateFanMode::CLIMATE_FAN_LOW:
|
||||
this->set_fan_mode_(FAN_LOW);
|
||||
break;
|
||||
case ClimateFanMode::CLIMATE_FAN_MEDIUM:
|
||||
this->set_fan_mode_(FAN_MEDIUM);
|
||||
break;
|
||||
case ClimateFanMode::CLIMATE_FAN_HIGH:
|
||||
this->set_fan_mode_(FAN_HIGH);
|
||||
break;
|
||||
default:
|
||||
this->set_fan_mode_(FAN_AUTO);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ClimateFanMode ControlData::get_fan_mode() const {
|
||||
switch (this->get_fan_mode_()) {
|
||||
case FAN_LOW:
|
||||
return ClimateFanMode::CLIMATE_FAN_LOW;
|
||||
case FAN_MEDIUM:
|
||||
return ClimateFanMode::CLIMATE_FAN_MEDIUM;
|
||||
case FAN_HIGH:
|
||||
return ClimateFanMode::CLIMATE_FAN_HIGH;
|
||||
default:
|
||||
return ClimateFanMode::CLIMATE_FAN_AUTO;
|
||||
}
|
||||
}
|
||||
|
||||
void MideaIR::control(const climate::ClimateCall &call) {
|
||||
// swing and preset resets after unit powered off
|
||||
if (call.get_mode() == climate::CLIMATE_MODE_OFF) {
|
||||
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
||||
this->preset = climate::CLIMATE_PRESET_NONE;
|
||||
} else if (call.get_swing_mode().has_value() && ((*call.get_swing_mode() == climate::CLIMATE_SWING_OFF &&
|
||||
this->swing_mode == climate::CLIMATE_SWING_VERTICAL) ||
|
||||
(*call.get_swing_mode() == climate::CLIMATE_SWING_VERTICAL &&
|
||||
this->swing_mode == climate::CLIMATE_SWING_OFF))) {
|
||||
this->swing_ = true;
|
||||
} else if (call.get_preset().has_value() &&
|
||||
((*call.get_preset() == climate::CLIMATE_PRESET_NONE && this->preset == climate::CLIMATE_PRESET_BOOST) ||
|
||||
(*call.get_preset() == climate::CLIMATE_PRESET_BOOST && this->preset == climate::CLIMATE_PRESET_NONE))) {
|
||||
this->boost_ = true;
|
||||
}
|
||||
climate_ir::ClimateIR::control(call);
|
||||
}
|
||||
|
||||
void MideaIR::transmit_(MideaData &data) {
|
||||
data.finalize();
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
remote_base::MideaProtocol().encode(transmit.get_data(), data);
|
||||
transmit.perform();
|
||||
}
|
||||
|
||||
void MideaIR::transmit_state() {
|
||||
if (this->swing_) {
|
||||
SpecialData data(SpecialData::VSWING_TOGGLE);
|
||||
this->transmit_(data);
|
||||
this->swing_ = false;
|
||||
return;
|
||||
}
|
||||
if (this->boost_) {
|
||||
SpecialData data(SpecialData::TURBO_TOGGLE);
|
||||
this->transmit_(data);
|
||||
this->boost_ = false;
|
||||
return;
|
||||
}
|
||||
ControlData data;
|
||||
data.set_fahrenheit(this->fahrenheit_);
|
||||
data.set_temp(this->target_temperature);
|
||||
data.set_mode(this->mode);
|
||||
data.set_fan_mode(this->fan_mode.value_or(ClimateFanMode::CLIMATE_FAN_AUTO));
|
||||
data.set_sleep_preset(this->preset == climate::CLIMATE_PRESET_SLEEP);
|
||||
data.fix();
|
||||
this->transmit_(data);
|
||||
}
|
||||
|
||||
bool MideaIR::on_receive(remote_base::RemoteReceiveData data) {
|
||||
auto midea = remote_base::MideaProtocol().decode(data);
|
||||
if (midea.has_value())
|
||||
return this->on_midea_(*midea);
|
||||
return coolix::CoolixClimate::on_coolix(this, data);
|
||||
}
|
||||
|
||||
bool MideaIR::on_midea_(const MideaData &data) {
|
||||
ESP_LOGV(TAG, "Decoded Midea IR data: %s", data.to_string().c_str());
|
||||
if (data.type() == MideaData::MIDEA_TYPE_CONTROL) {
|
||||
const ControlData status = data;
|
||||
if (status.get_mode() != climate::CLIMATE_MODE_FAN_ONLY)
|
||||
this->target_temperature = status.get_temp();
|
||||
this->mode = status.get_mode();
|
||||
this->fan_mode = status.get_fan_mode();
|
||||
if (status.get_sleep_preset())
|
||||
this->preset = climate::CLIMATE_PRESET_SLEEP;
|
||||
else if (this->preset == climate::CLIMATE_PRESET_SLEEP)
|
||||
this->preset = climate::CLIMATE_PRESET_NONE;
|
||||
this->publish_state();
|
||||
return true;
|
||||
}
|
||||
if (data.type() == MideaData::MIDEA_TYPE_SPECIAL) {
|
||||
switch (data[1]) {
|
||||
case SpecialData::VSWING_TOGGLE:
|
||||
this->swing_mode = this->swing_mode == climate::CLIMATE_SWING_VERTICAL ? climate::CLIMATE_SWING_OFF
|
||||
: climate::CLIMATE_SWING_VERTICAL;
|
||||
break;
|
||||
case SpecialData::TURBO_TOGGLE:
|
||||
this->preset = this->preset == climate::CLIMATE_PRESET_BOOST ? climate::CLIMATE_PRESET_NONE
|
||||
: climate::CLIMATE_PRESET_BOOST;
|
||||
break;
|
||||
}
|
||||
this->publish_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace midea_ir
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
#include "midea_data.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace midea_ir {
|
||||
|
||||
// Temperature
|
||||
const uint8_t MIDEA_TEMPC_MIN = 17; // Celsius
|
||||
const uint8_t MIDEA_TEMPC_MAX = 30; // Celsius
|
||||
const uint8_t MIDEA_TEMPF_MIN = 62; // Fahrenheit
|
||||
const uint8_t MIDEA_TEMPF_MAX = 86; // Fahrenheit
|
||||
|
||||
class MideaIR : public climate_ir::ClimateIR {
|
||||
public:
|
||||
MideaIR()
|
||||
: climate_ir::ClimateIR(
|
||||
MIDEA_TEMPC_MIN, MIDEA_TEMPC_MAX, 1.0f, true, true,
|
||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_HIGH},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL},
|
||||
{climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_SLEEP, climate::CLIMATE_PRESET_BOOST}) {}
|
||||
|
||||
/// Override control to change settings of the climate device.
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
|
||||
/// Set use of Fahrenheit units
|
||||
void set_fahrenheit(bool value) {
|
||||
this->fahrenheit_ = value;
|
||||
this->temperature_step_ = value ? 0.5f : 1.0f;
|
||||
}
|
||||
|
||||
protected:
|
||||
/// Transmit via IR the state of this climate controller.
|
||||
void transmit_state() override;
|
||||
void transmit_(MideaData &data);
|
||||
/// Handle received IR Buffer
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
bool on_midea_(const MideaData &data);
|
||||
bool fahrenheit_{false};
|
||||
bool swing_{false};
|
||||
bool boost_{false};
|
||||
};
|
||||
|
||||
} // namespace midea_ir
|
||||
} // namespace esphome
|
||||
Reference in New Issue
Block a user