mirror of
https://github.com/Threnklyn/esphome-dev.git
synced 2026-05-19 04:33:27 +02:00
Midea support v2 (#2188)
This commit is contained in:
committed by
GitHub
parent
2790d72bff
commit
4e120a291e
@@ -0,0 +1,173 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "adapter.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace midea {
|
||||
|
||||
const char *const Constants::TAG = "midea";
|
||||
const std::string Constants::FREEZE_PROTECTION = "freeze protection";
|
||||
const std::string Constants::SILENT = "silent";
|
||||
const std::string Constants::TURBO = "turbo";
|
||||
|
||||
ClimateMode Converters::to_climate_mode(MideaMode mode) {
|
||||
switch (mode) {
|
||||
case MideaMode::MODE_AUTO:
|
||||
return ClimateMode::CLIMATE_MODE_HEAT_COOL;
|
||||
case MideaMode::MODE_COOL:
|
||||
return ClimateMode::CLIMATE_MODE_COOL;
|
||||
case MideaMode::MODE_DRY:
|
||||
return ClimateMode::CLIMATE_MODE_DRY;
|
||||
case MideaMode::MODE_FAN_ONLY:
|
||||
return ClimateMode::CLIMATE_MODE_FAN_ONLY;
|
||||
case MideaMode::MODE_HEAT:
|
||||
return ClimateMode::CLIMATE_MODE_HEAT;
|
||||
default:
|
||||
return ClimateMode::CLIMATE_MODE_OFF;
|
||||
}
|
||||
}
|
||||
|
||||
MideaMode Converters::to_midea_mode(ClimateMode mode) {
|
||||
switch (mode) {
|
||||
case ClimateMode::CLIMATE_MODE_HEAT_COOL:
|
||||
return MideaMode::MODE_AUTO;
|
||||
case ClimateMode::CLIMATE_MODE_COOL:
|
||||
return MideaMode::MODE_COOL;
|
||||
case ClimateMode::CLIMATE_MODE_DRY:
|
||||
return MideaMode::MODE_DRY;
|
||||
case ClimateMode::CLIMATE_MODE_FAN_ONLY:
|
||||
return MideaMode::MODE_FAN_ONLY;
|
||||
case ClimateMode::CLIMATE_MODE_HEAT:
|
||||
return MideaMode::MODE_HEAT;
|
||||
default:
|
||||
return MideaMode::MODE_OFF;
|
||||
}
|
||||
}
|
||||
|
||||
ClimateSwingMode Converters::to_climate_swing_mode(MideaSwingMode mode) {
|
||||
switch (mode) {
|
||||
case MideaSwingMode::SWING_VERTICAL:
|
||||
return ClimateSwingMode::CLIMATE_SWING_VERTICAL;
|
||||
case MideaSwingMode::SWING_HORIZONTAL:
|
||||
return ClimateSwingMode::CLIMATE_SWING_HORIZONTAL;
|
||||
case MideaSwingMode::SWING_BOTH:
|
||||
return ClimateSwingMode::CLIMATE_SWING_BOTH;
|
||||
default:
|
||||
return ClimateSwingMode::CLIMATE_SWING_OFF;
|
||||
}
|
||||
}
|
||||
|
||||
MideaSwingMode Converters::to_midea_swing_mode(ClimateSwingMode mode) {
|
||||
switch (mode) {
|
||||
case ClimateSwingMode::CLIMATE_SWING_VERTICAL:
|
||||
return MideaSwingMode::SWING_VERTICAL;
|
||||
case ClimateSwingMode::CLIMATE_SWING_HORIZONTAL:
|
||||
return MideaSwingMode::SWING_HORIZONTAL;
|
||||
case ClimateSwingMode::CLIMATE_SWING_BOTH:
|
||||
return MideaSwingMode::SWING_BOTH;
|
||||
default:
|
||||
return MideaSwingMode::SWING_OFF;
|
||||
}
|
||||
}
|
||||
|
||||
MideaFanMode Converters::to_midea_fan_mode(ClimateFanMode mode) {
|
||||
switch (mode) {
|
||||
case ClimateFanMode::CLIMATE_FAN_LOW:
|
||||
return MideaFanMode::FAN_LOW;
|
||||
case ClimateFanMode::CLIMATE_FAN_MEDIUM:
|
||||
return MideaFanMode::FAN_MEDIUM;
|
||||
case ClimateFanMode::CLIMATE_FAN_HIGH:
|
||||
return MideaFanMode::FAN_HIGH;
|
||||
default:
|
||||
return MideaFanMode::FAN_AUTO;
|
||||
}
|
||||
}
|
||||
|
||||
ClimateFanMode Converters::to_climate_fan_mode(MideaFanMode mode) {
|
||||
switch (mode) {
|
||||
case MideaFanMode::FAN_LOW:
|
||||
return ClimateFanMode::CLIMATE_FAN_LOW;
|
||||
case MideaFanMode::FAN_MEDIUM:
|
||||
return ClimateFanMode::CLIMATE_FAN_MEDIUM;
|
||||
case MideaFanMode::FAN_HIGH:
|
||||
return ClimateFanMode::CLIMATE_FAN_HIGH;
|
||||
default:
|
||||
return ClimateFanMode::CLIMATE_FAN_AUTO;
|
||||
}
|
||||
}
|
||||
|
||||
bool Converters::is_custom_midea_fan_mode(MideaFanMode mode) {
|
||||
switch (mode) {
|
||||
case MideaFanMode::FAN_SILENT:
|
||||
case MideaFanMode::FAN_TURBO:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) {
|
||||
switch (mode) {
|
||||
case MideaFanMode::FAN_SILENT:
|
||||
return Constants::SILENT;
|
||||
default:
|
||||
return Constants::TURBO;
|
||||
}
|
||||
}
|
||||
|
||||
MideaFanMode Converters::to_midea_fan_mode(const std::string &mode) {
|
||||
if (mode == Constants::SILENT)
|
||||
return MideaFanMode::FAN_SILENT;
|
||||
return MideaFanMode::FAN_TURBO;
|
||||
}
|
||||
|
||||
MideaPreset Converters::to_midea_preset(ClimatePreset preset) {
|
||||
switch (preset) {
|
||||
case ClimatePreset::CLIMATE_PRESET_SLEEP:
|
||||
return MideaPreset::PRESET_SLEEP;
|
||||
case ClimatePreset::CLIMATE_PRESET_ECO:
|
||||
return MideaPreset::PRESET_ECO;
|
||||
case ClimatePreset::CLIMATE_PRESET_BOOST:
|
||||
return MideaPreset::PRESET_TURBO;
|
||||
default:
|
||||
return MideaPreset::PRESET_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
ClimatePreset Converters::to_climate_preset(MideaPreset preset) {
|
||||
switch (preset) {
|
||||
case MideaPreset::PRESET_SLEEP:
|
||||
return ClimatePreset::CLIMATE_PRESET_SLEEP;
|
||||
case MideaPreset::PRESET_ECO:
|
||||
return ClimatePreset::CLIMATE_PRESET_ECO;
|
||||
case MideaPreset::PRESET_TURBO:
|
||||
return ClimatePreset::CLIMATE_PRESET_BOOST;
|
||||
default:
|
||||
return ClimatePreset::CLIMATE_PRESET_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
bool Converters::is_custom_midea_preset(MideaPreset preset) { return preset == MideaPreset::PRESET_FREEZE_PROTECTION; }
|
||||
|
||||
const std::string &Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; }
|
||||
|
||||
MideaPreset Converters::to_midea_preset(const std::string &preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; }
|
||||
|
||||
void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities) {
|
||||
if (capabilities.supportAutoMode())
|
||||
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT_COOL);
|
||||
if (capabilities.supportCoolMode())
|
||||
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_COOL);
|
||||
if (capabilities.supportHeatMode())
|
||||
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_HEAT);
|
||||
if (capabilities.supportDryMode())
|
||||
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_DRY);
|
||||
if (capabilities.supportTurboPreset())
|
||||
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_BOOST);
|
||||
if (capabilities.supportEcoPreset())
|
||||
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO);
|
||||
if (capabilities.supportFrostProtectionPreset())
|
||||
traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION);
|
||||
}
|
||||
|
||||
} // namespace midea
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
#include <Appliance/AirConditioner/AirConditioner.h>
|
||||
#include "esphome/components/climate/climate_traits.h"
|
||||
#include "appliance_base.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace midea {
|
||||
|
||||
using MideaMode = dudanov::midea::ac::Mode;
|
||||
using MideaSwingMode = dudanov::midea::ac::SwingMode;
|
||||
using MideaFanMode = dudanov::midea::ac::FanMode;
|
||||
using MideaPreset = dudanov::midea::ac::Preset;
|
||||
|
||||
class Constants {
|
||||
public:
|
||||
static const char *const TAG;
|
||||
static const std::string FREEZE_PROTECTION;
|
||||
static const std::string SILENT;
|
||||
static const std::string TURBO;
|
||||
};
|
||||
|
||||
class Converters {
|
||||
public:
|
||||
static MideaMode to_midea_mode(ClimateMode mode);
|
||||
static ClimateMode to_climate_mode(MideaMode mode);
|
||||
static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode);
|
||||
static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode);
|
||||
static MideaPreset to_midea_preset(ClimatePreset preset);
|
||||
static MideaPreset to_midea_preset(const std::string &preset);
|
||||
static bool is_custom_midea_preset(MideaPreset preset);
|
||||
static ClimatePreset to_climate_preset(MideaPreset preset);
|
||||
static const std::string &to_custom_climate_preset(MideaPreset preset);
|
||||
static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode);
|
||||
static MideaFanMode to_midea_fan_mode(const std::string &fan_mode);
|
||||
static bool is_custom_midea_fan_mode(MideaFanMode fan_mode);
|
||||
static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode);
|
||||
static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode);
|
||||
static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities);
|
||||
};
|
||||
|
||||
} // namespace midea
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,152 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "air_conditioner.h"
|
||||
#include "adapter.h"
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
#include "midea_ir.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace midea {
|
||||
|
||||
static void set_sensor(Sensor *sensor, float value) {
|
||||
if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value))
|
||||
sensor->publish_state(value);
|
||||
}
|
||||
|
||||
template<typename T> void update_property(T &property, const T &value, bool &flag) {
|
||||
if (property != value) {
|
||||
property = value;
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
void AirConditioner::on_status_change() {
|
||||
bool need_publish = false;
|
||||
update_property(this->target_temperature, this->base_.getTargetTemp(), need_publish);
|
||||
update_property(this->current_temperature, this->base_.getIndoorTemp(), need_publish);
|
||||
auto mode = Converters::to_climate_mode(this->base_.getMode());
|
||||
update_property(this->mode, mode, need_publish);
|
||||
auto swing_mode = Converters::to_climate_swing_mode(this->base_.getSwingMode());
|
||||
update_property(this->swing_mode, swing_mode, need_publish);
|
||||
// Preset
|
||||
auto preset = this->base_.getPreset();
|
||||
if (Converters::is_custom_midea_preset(preset)) {
|
||||
if (this->set_custom_preset_(Converters::to_custom_climate_preset(preset)))
|
||||
need_publish = true;
|
||||
} else if (this->set_preset_(Converters::to_climate_preset(preset))) {
|
||||
need_publish = true;
|
||||
}
|
||||
// Fan mode
|
||||
auto fan_mode = this->base_.getFanMode();
|
||||
if (Converters::is_custom_midea_fan_mode(fan_mode)) {
|
||||
if (this->set_custom_fan_mode_(Converters::to_custom_climate_fan_mode(fan_mode)))
|
||||
need_publish = true;
|
||||
} else if (this->set_fan_mode_(Converters::to_climate_fan_mode(fan_mode))) {
|
||||
need_publish = true;
|
||||
}
|
||||
if (need_publish)
|
||||
this->publish_state();
|
||||
set_sensor(this->outdoor_sensor_, this->base_.getOutdoorTemp());
|
||||
set_sensor(this->power_sensor_, this->base_.getPowerUsage());
|
||||
set_sensor(this->humidity_sensor_, this->base_.getIndoorHum());
|
||||
}
|
||||
|
||||
void AirConditioner::control(const ClimateCall &call) {
|
||||
dudanov::midea::ac::Control ctrl{};
|
||||
if (call.get_target_temperature().has_value())
|
||||
ctrl.targetTemp = call.get_target_temperature().value();
|
||||
if (call.get_swing_mode().has_value())
|
||||
ctrl.swingMode = Converters::to_midea_swing_mode(call.get_swing_mode().value());
|
||||
if (call.get_mode().has_value())
|
||||
ctrl.mode = Converters::to_midea_mode(call.get_mode().value());
|
||||
if (call.get_preset().has_value())
|
||||
ctrl.preset = Converters::to_midea_preset(call.get_preset().value());
|
||||
else if (call.get_custom_preset().has_value())
|
||||
ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().value());
|
||||
if (call.get_fan_mode().has_value())
|
||||
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value());
|
||||
else if (call.get_custom_fan_mode().has_value())
|
||||
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().value());
|
||||
this->base_.control(ctrl);
|
||||
}
|
||||
|
||||
ClimateTraits AirConditioner::traits() {
|
||||
auto traits = ClimateTraits();
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_visual_min_temperature(17);
|
||||
traits.set_visual_max_temperature(30);
|
||||
traits.set_visual_temperature_step(0.5);
|
||||
traits.set_supported_modes(this->supported_modes_);
|
||||
traits.set_supported_swing_modes(this->supported_swing_modes_);
|
||||
traits.set_supported_presets(this->supported_presets_);
|
||||
traits.set_supported_custom_presets(this->supported_custom_presets_);
|
||||
traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_);
|
||||
/* + MINIMAL SET OF CAPABILITIES */
|
||||
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF);
|
||||
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_FAN_ONLY);
|
||||
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO);
|
||||
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW);
|
||||
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM);
|
||||
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH);
|
||||
traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF);
|
||||
traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_VERTICAL);
|
||||
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE);
|
||||
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP);
|
||||
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK)
|
||||
Converters::to_climate_traits(traits, this->base_.getCapabilities());
|
||||
return traits;
|
||||
}
|
||||
|
||||
void AirConditioner::dump_config() {
|
||||
ESP_LOGCONFIG(Constants::TAG, "MideaDongle:");
|
||||
ESP_LOGCONFIG(Constants::TAG, " [x] Period: %dms", this->base_.getPeriod());
|
||||
ESP_LOGCONFIG(Constants::TAG, " [x] Response timeout: %dms", this->base_.getTimeout());
|
||||
ESP_LOGCONFIG(Constants::TAG, " [x] Request attempts: %d", this->base_.getNumAttempts());
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
ESP_LOGCONFIG(Constants::TAG, " [x] Using RemoteTransmitter");
|
||||
#endif
|
||||
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) {
|
||||
this->base_.getCapabilities().dump();
|
||||
} else if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_ERROR) {
|
||||
ESP_LOGW(Constants::TAG,
|
||||
"Failed to get 0xB5 capabilities report. Suggest to disable it in config and manually set your "
|
||||
"appliance options.");
|
||||
}
|
||||
this->dump_traits_(Constants::TAG);
|
||||
}
|
||||
|
||||
/* ACTIONS */
|
||||
|
||||
void AirConditioner::do_follow_me(float temperature, bool beeper) {
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
IrFollowMeData data(static_cast<uint8_t>(lroundf(temperature)), beeper);
|
||||
this->transmit_ir(data);
|
||||
#else
|
||||
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
|
||||
#endif
|
||||
}
|
||||
|
||||
void AirConditioner::do_swing_step() {
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
IrSpecialData data(0x01);
|
||||
this->transmit_ir(data);
|
||||
#else
|
||||
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
|
||||
#endif
|
||||
}
|
||||
|
||||
void AirConditioner::do_display_toggle() {
|
||||
if (this->base_.getCapabilities().supportLightControl()) {
|
||||
this->base_.displayToggle();
|
||||
} else {
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
IrSpecialData data(0x08);
|
||||
this->transmit_ir(data);
|
||||
#else
|
||||
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace midea
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
#include <Appliance/AirConditioner/AirConditioner.h>
|
||||
#include "appliance_base.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace midea {
|
||||
|
||||
using sensor::Sensor;
|
||||
using climate::ClimateCall;
|
||||
|
||||
class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner> {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; }
|
||||
void set_humidity_setpoint_sensor(Sensor *sensor) { this->humidity_sensor_ = sensor; }
|
||||
void set_power_sensor(Sensor *sensor) { this->power_sensor_ = sensor; }
|
||||
void on_status_change() override;
|
||||
|
||||
/* ############### */
|
||||
/* ### ACTIONS ### */
|
||||
/* ############### */
|
||||
|
||||
void do_follow_me(float temperature, bool beeper = false);
|
||||
void do_display_toggle();
|
||||
void do_swing_step();
|
||||
void do_beeper_on() { this->set_beeper_feedback(true); }
|
||||
void do_beeper_off() { this->set_beeper_feedback(false); }
|
||||
void do_power_on() { this->base_.setPowerState(true); }
|
||||
void do_power_off() { this->base_.setPowerState(false); }
|
||||
|
||||
protected:
|
||||
void control(const ClimateCall &call) override;
|
||||
ClimateTraits traits() override;
|
||||
Sensor *outdoor_sensor_{nullptr};
|
||||
Sensor *humidity_sensor_{nullptr};
|
||||
Sensor *power_sensor_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace midea
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
#include "esphome/components/remote_base/midea_protocol.h"
|
||||
#include "esphome/components/remote_transmitter/remote_transmitter.h"
|
||||
#endif
|
||||
#include <Appliance/ApplianceBase.h>
|
||||
#include <Helpers/Logger.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace midea {
|
||||
|
||||
using climate::ClimatePreset;
|
||||
using climate::ClimateTraits;
|
||||
using climate::ClimateMode;
|
||||
using climate::ClimateSwingMode;
|
||||
using climate::ClimateFanMode;
|
||||
|
||||
template<typename T> class ApplianceBase : public Component, public uart::UARTDevice, public climate::Climate {
|
||||
static_assert(std::is_base_of<dudanov::midea::ApplianceBase, T>::value,
|
||||
"T must derive from dudanov::midea::ApplianceBase class");
|
||||
|
||||
public:
|
||||
ApplianceBase() {
|
||||
this->base_.setStream(this);
|
||||
this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this));
|
||||
dudanov::midea::ApplianceBase::setLogger([](int level, const char *tag, int line, String format, va_list args) {
|
||||
esp_log_vprintf_(level, tag, line, format.c_str(), args);
|
||||
});
|
||||
}
|
||||
bool can_proceed() override {
|
||||
return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS;
|
||||
}
|
||||
float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; }
|
||||
void setup() override { this->base_.setup(); }
|
||||
void loop() override { this->base_.loop(); }
|
||||
void set_period(uint32_t ms) { this->base_.setPeriod(ms); }
|
||||
void set_response_timeout(uint32_t ms) { this->base_.setTimeout(ms); }
|
||||
void set_request_attempts(uint32_t attempts) { this->base_.setNumAttempts(attempts); }
|
||||
void set_beeper_feedback(bool state) { this->base_.setBeeper(state); }
|
||||
void set_autoconf(bool value) { this->base_.setAutoconf(value); }
|
||||
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
|
||||
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
|
||||
void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); }
|
||||
void set_custom_presets(std::set<std::string> presets) { this->supported_custom_presets_ = std::move(presets); }
|
||||
void set_custom_fan_modes(std::set<std::string> modes) { this->supported_custom_fan_modes_ = std::move(modes); }
|
||||
virtual void on_status_change() = 0;
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
|
||||
this->transmitter_ = transmitter;
|
||||
}
|
||||
void transmit_ir(remote_base::MideaData &data) {
|
||||
data.finalize();
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
remote_base::MideaProtocol().encode(transmit.get_data(), data);
|
||||
transmit.perform();
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
T base_;
|
||||
std::set<ClimateMode> supported_modes_{};
|
||||
std::set<ClimateSwingMode> supported_swing_modes_{};
|
||||
std::set<ClimatePreset> supported_presets_{};
|
||||
std::set<std::string> supported_custom_presets_{};
|
||||
std::set<std::string> supported_custom_fan_modes_{};
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace midea
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
#include "esphome/core/automation.h"
|
||||
#include "air_conditioner.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace midea {
|
||||
|
||||
template<typename... Ts> class MideaActionBase : public Action<Ts...> {
|
||||
public:
|
||||
void set_parent(AirConditioner *parent) { this->parent_ = parent; }
|
||||
|
||||
protected:
|
||||
AirConditioner *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class FollowMeAction : public MideaActionBase<Ts...> {
|
||||
TEMPLATABLE_VALUE(float, temperature)
|
||||
TEMPLATABLE_VALUE(bool, beeper)
|
||||
|
||||
void play(Ts... x) override {
|
||||
this->parent_->do_follow_me(this->temperature_.value(x...), this->beeper_.value(x...));
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class SwingStepAction : public MideaActionBase<Ts...> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->do_swing_step(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class DisplayToggleAction : public MideaActionBase<Ts...> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->do_display_toggle(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class BeeperOnAction : public MideaActionBase<Ts...> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->do_beeper_on(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class BeeperOffAction : public MideaActionBase<Ts...> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->do_beeper_off(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class PowerOnAction : public MideaActionBase<Ts...> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->do_power_on(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class PowerOffAction : public MideaActionBase<Ts...> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->do_power_off(); }
|
||||
};
|
||||
|
||||
} // namespace midea
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,284 @@
|
||||
from esphome.core import coroutine
|
||||
from esphome import automation
|
||||
from esphome.components import climate, sensor, uart, remote_transmitter
|
||||
from esphome.components.remote_base import CONF_TRANSMITTER_ID
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
from esphome.const import (
|
||||
CONF_AUTOCONF,
|
||||
CONF_BEEPER,
|
||||
CONF_CUSTOM_FAN_MODES,
|
||||
CONF_CUSTOM_PRESETS,
|
||||
CONF_ID,
|
||||
CONF_NUM_ATTEMPTS,
|
||||
CONF_PERIOD,
|
||||
CONF_SUPPORTED_MODES,
|
||||
CONF_SUPPORTED_PRESETS,
|
||||
CONF_SUPPORTED_SWING_MODES,
|
||||
CONF_TIMEOUT,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
ICON_POWER,
|
||||
ICON_THERMOMETER,
|
||||
ICON_WATER_PERCENT,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PERCENT,
|
||||
UNIT_WATT,
|
||||
)
|
||||
from esphome.components.climate import (
|
||||
ClimateMode,
|
||||
ClimatePreset,
|
||||
ClimateSwingMode,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@dudanov"]
|
||||
DEPENDENCIES = ["climate", "uart", "wifi"]
|
||||
AUTO_LOAD = ["sensor"]
|
||||
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
|
||||
CONF_POWER_USAGE = "power_usage"
|
||||
CONF_HUMIDITY_SETPOINT = "humidity_setpoint"
|
||||
midea_ns = cg.esphome_ns.namespace("midea")
|
||||
AirConditioner = midea_ns.class_("AirConditioner", climate.Climate, cg.Component)
|
||||
Capabilities = midea_ns.namespace("Constants")
|
||||
|
||||
|
||||
def templatize(value):
|
||||
if isinstance(value, cv.Schema):
|
||||
value = value.schema
|
||||
ret = {}
|
||||
for key, val in value.items():
|
||||
ret[key] = cv.templatable(val)
|
||||
return cv.Schema(ret)
|
||||
|
||||
|
||||
def register_action(name, type_, schema):
|
||||
validator = templatize(schema).extend(MIDEA_ACTION_BASE_SCHEMA)
|
||||
registerer = automation.register_action(f"midea_ac.{name}", type_, validator)
|
||||
|
||||
def decorator(func):
|
||||
async def new_func(config, action_id, template_arg, args):
|
||||
ac_ = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
cg.add(var.set_parent(ac_))
|
||||
await coroutine(func)(var, config, args)
|
||||
return var
|
||||
|
||||
return registerer(new_func)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
ALLOWED_CLIMATE_MODES = {
|
||||
"HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL,
|
||||
"COOL": ClimateMode.CLIMATE_MODE_COOL,
|
||||
"HEAT": ClimateMode.CLIMATE_MODE_HEAT,
|
||||
"DRY": ClimateMode.CLIMATE_MODE_DRY,
|
||||
"FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
|
||||
}
|
||||
|
||||
ALLOWED_CLIMATE_PRESETS = {
|
||||
"ECO": ClimatePreset.CLIMATE_PRESET_ECO,
|
||||
"BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
|
||||
"SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
|
||||
}
|
||||
|
||||
ALLOWED_CLIMATE_SWING_MODES = {
|
||||
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
|
||||
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
|
||||
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
|
||||
}
|
||||
|
||||
CUSTOM_FAN_MODES = {
|
||||
"SILENT": Capabilities.SILENT,
|
||||
"TURBO": Capabilities.TURBO,
|
||||
}
|
||||
|
||||
CUSTOM_PRESETS = {
|
||||
"FREEZE_PROTECTION": Capabilities.FREEZE_PROTECTION,
|
||||
}
|
||||
|
||||
validate_modes = cv.enum(ALLOWED_CLIMATE_MODES, upper=True)
|
||||
validate_presets = cv.enum(ALLOWED_CLIMATE_PRESETS, upper=True)
|
||||
validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True)
|
||||
validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True)
|
||||
validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
climate.CLIMATE_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(AirConditioner),
|
||||
cv.Optional(CONF_PERIOD, default="1s"): cv.time_period,
|
||||
cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period,
|
||||
cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5),
|
||||
cv.Optional(CONF_TRANSMITTER_ID): cv.use_id(
|
||||
remote_transmitter.RemoteTransmitterComponent
|
||||
),
|
||||
cv.Optional(CONF_BEEPER, default=False): cv.boolean,
|
||||
cv.Optional(CONF_AUTOCONF, default=True): cv.boolean,
|
||||
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(validate_modes),
|
||||
cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
|
||||
validate_swing_modes
|
||||
),
|
||||
cv.Optional(CONF_SUPPORTED_PRESETS): cv.ensure_list(validate_presets),
|
||||
cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(validate_custom_presets),
|
||||
cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(
|
||||
validate_custom_fan_modes
|
||||
),
|
||||
cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
icon=ICON_POWER,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
icon=ICON_WATER_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
# Actions
|
||||
FollowMeAction = midea_ns.class_("FollowMeAction", automation.Action)
|
||||
DisplayToggleAction = midea_ns.class_("DisplayToggleAction", automation.Action)
|
||||
SwingStepAction = midea_ns.class_("SwingStepAction", automation.Action)
|
||||
BeeperOnAction = midea_ns.class_("BeeperOnAction", automation.Action)
|
||||
BeeperOffAction = midea_ns.class_("BeeperOffAction", automation.Action)
|
||||
PowerOnAction = midea_ns.class_("PowerOnAction", automation.Action)
|
||||
PowerOffAction = midea_ns.class_("PowerOffAction", automation.Action)
|
||||
|
||||
MIDEA_ACTION_BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(AirConditioner),
|
||||
}
|
||||
)
|
||||
|
||||
# FollowMe action
|
||||
MIDEA_FOLLOW_ME_MIN = 0
|
||||
MIDEA_FOLLOW_ME_MAX = 37
|
||||
MIDEA_FOLLOW_ME_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_BEEPER, default=False): cv.templatable(cv.boolean),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@register_action("follow_me", FollowMeAction, MIDEA_FOLLOW_ME_SCHEMA)
|
||||
async def follow_me_to_code(var, config, args):
|
||||
template_ = await cg.templatable(config[CONF_BEEPER], args, cg.bool_)
|
||||
cg.add(var.set_beeper(template_))
|
||||
template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.float_)
|
||||
cg.add(var.set_temperature(template_))
|
||||
|
||||
|
||||
# Toggle Display action
|
||||
@register_action(
|
||||
"display_toggle",
|
||||
DisplayToggleAction,
|
||||
cv.Schema({}),
|
||||
)
|
||||
async def display_toggle_to_code(var, config, args):
|
||||
pass
|
||||
|
||||
|
||||
# Swing Step action
|
||||
@register_action(
|
||||
"swing_step",
|
||||
SwingStepAction,
|
||||
cv.Schema({}),
|
||||
)
|
||||
async def swing_step_to_code(var, config, args):
|
||||
pass
|
||||
|
||||
|
||||
# Beeper On action
|
||||
@register_action(
|
||||
"beeper_on",
|
||||
BeeperOnAction,
|
||||
cv.Schema({}),
|
||||
)
|
||||
async def beeper_on_to_code(var, config, args):
|
||||
pass
|
||||
|
||||
|
||||
# Beeper Off action
|
||||
@register_action(
|
||||
"beeper_off",
|
||||
BeeperOffAction,
|
||||
cv.Schema({}),
|
||||
)
|
||||
async def beeper_off_to_code(var, config, args):
|
||||
pass
|
||||
|
||||
|
||||
# Power On action
|
||||
@register_action(
|
||||
"power_on",
|
||||
PowerOnAction,
|
||||
cv.Schema({}),
|
||||
)
|
||||
async def power_on_to_code(var, config, args):
|
||||
pass
|
||||
|
||||
|
||||
# Power Off action
|
||||
@register_action(
|
||||
"power_off",
|
||||
PowerOffAction,
|
||||
cv.Schema({}),
|
||||
)
|
||||
async def power_off_to_code(var, config, args):
|
||||
pass
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
await climate.register_climate(var, config)
|
||||
cg.add(var.set_period(config[CONF_PERIOD].total_milliseconds))
|
||||
cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds))
|
||||
cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS]))
|
||||
if CONF_TRANSMITTER_ID in config:
|
||||
cg.add_define("USE_REMOTE_TRANSMITTER")
|
||||
transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID])
|
||||
cg.add(var.set_transmitter(transmitter_))
|
||||
cg.add(var.set_beeper_feedback(config[CONF_BEEPER]))
|
||||
cg.add(var.set_autoconf(config[CONF_AUTOCONF]))
|
||||
if CONF_SUPPORTED_MODES in config:
|
||||
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
|
||||
if CONF_SUPPORTED_SWING_MODES in config:
|
||||
cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES]))
|
||||
if CONF_SUPPORTED_PRESETS in config:
|
||||
cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS]))
|
||||
if CONF_CUSTOM_PRESETS in config:
|
||||
cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS]))
|
||||
if CONF_CUSTOM_FAN_MODES in config:
|
||||
cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES]))
|
||||
if CONF_OUTDOOR_TEMPERATURE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
|
||||
cg.add(var.set_outdoor_temperature_sensor(sens))
|
||||
if CONF_POWER_USAGE in config:
|
||||
sens = await sensor.new_sensor(config[CONF_POWER_USAGE])
|
||||
cg.add(var.set_power_sensor(sens))
|
||||
if CONF_HUMIDITY_SETPOINT in config:
|
||||
sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
|
||||
cg.add(var.set_humidity_setpoint_sensor(sens))
|
||||
cg.add_library("dudanov/MideaUART", "1.1.5")
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
#include "esphome/components/remote_base/midea_protocol.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace midea {
|
||||
|
||||
using IrData = remote_base::MideaData;
|
||||
|
||||
class IrFollowMeData : public IrData {
|
||||
public:
|
||||
// Default constructor (temp: 30C, beeper: off)
|
||||
IrFollowMeData() : IrData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {}
|
||||
// Copy from Base
|
||||
IrFollowMeData(const IrData &data) : IrData(data) {}
|
||||
// Direct from temperature and beeper values
|
||||
IrFollowMeData(uint8_t temp, bool beeper = false) : IrFollowMeData() {
|
||||
this->set_temp(temp);
|
||||
this->set_beeper(beeper);
|
||||
}
|
||||
|
||||
/* TEMPERATURE */
|
||||
uint8_t temp() const { return this->data_[4] - 1; }
|
||||
void set_temp(uint8_t val) { this->data_[4] = std::min(MAX_TEMP, val) + 1; }
|
||||
|
||||
/* BEEPER */
|
||||
bool beeper() const { return this->data_[3] & 128; }
|
||||
void set_beeper(bool val) { this->set_value_(3, 1, 7, val); }
|
||||
|
||||
protected:
|
||||
static const uint8_t MAX_TEMP = 37;
|
||||
};
|
||||
|
||||
class IrSpecialData : public IrData {
|
||||
public:
|
||||
IrSpecialData(uint8_t code) : IrData({MIDEA_TYPE_SPECIAL, code, 0xFF, 0xFF, 0xFF}) {}
|
||||
};
|
||||
|
||||
} // namespace midea
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user