From 4e120a291eb4ca444dfd6a8a488fa6700c554579 Mon Sep 17 00:00:00 2001 From: "Sergey V. DUDANOV" Date: Thu, 9 Sep 2021 01:10:02 +0400 Subject: [PATCH] Midea support v2 (#2188) --- CODEOWNERS | 3 +- esphome/components/climate/__init__.py | 1 + esphome/components/climate/climate.cpp | 69 +++++ esphome/components/climate/climate.h | 13 + esphome/components/climate/climate_traits.h | 2 + esphome/components/midea/__init__.py | 0 esphome/components/midea/adapter.cpp | 173 +++++++++++ esphome/components/midea/adapter.h | 42 +++ esphome/components/midea/air_conditioner.cpp | 152 ++++++++++ esphome/components/midea/air_conditioner.h | 41 +++ esphome/components/midea/appliance_base.h | 76 +++++ esphome/components/midea/automations.h | 56 ++++ esphome/components/midea/climate.py | 284 ++++++++++++++++++ esphome/components/midea/midea_ir.h | 42 +++ esphome/components/midea_ac/climate.py | 114 +------ esphome/components/midea_ac/midea_climate.cpp | 208 ------------- esphome/components/midea_ac/midea_climate.h | 68 ----- esphome/components/midea_ac/midea_frame.cpp | 238 --------------- esphome/components/midea_ac/midea_frame.h | 165 ---------- esphome/components/midea_dongle/__init__.py | 30 -- .../components/midea_dongle/midea_dongle.cpp | 98 ------ .../components/midea_dongle/midea_dongle.h | 56 ---- .../components/midea_dongle/midea_frame.cpp | 95 ------ esphome/components/midea_dongle/midea_frame.h | 104 ------- esphome/components/remote_base/__init__.py | 42 +++ .../components/remote_base/midea_protocol.cpp | 99 ++++++ .../components/remote_base/midea_protocol.h | 105 +++++++ esphome/const.py | 6 + esphome/core/component.cpp | 1 + esphome/core/component.h | 2 + platformio.ini | 1 + tests/test1.yaml | 79 ++++- tests/test3.yaml | 37 --- 33 files changed, 1276 insertions(+), 1226 deletions(-) create mode 100644 esphome/components/midea/__init__.py create mode 100644 esphome/components/midea/adapter.cpp create mode 100644 esphome/components/midea/adapter.h create mode 100644 esphome/components/midea/air_conditioner.cpp create mode 100644 esphome/components/midea/air_conditioner.h create mode 100644 esphome/components/midea/appliance_base.h create mode 100644 esphome/components/midea/automations.h create mode 100644 esphome/components/midea/climate.py create mode 100644 esphome/components/midea/midea_ir.h delete mode 100644 esphome/components/midea_ac/midea_climate.cpp delete mode 100644 esphome/components/midea_ac/midea_climate.h delete mode 100644 esphome/components/midea_ac/midea_frame.cpp delete mode 100644 esphome/components/midea_ac/midea_frame.h delete mode 100644 esphome/components/midea_dongle/__init__.py delete mode 100644 esphome/components/midea_dongle/midea_dongle.cpp delete mode 100644 esphome/components/midea_dongle/midea_dongle.h delete mode 100644 esphome/components/midea_dongle/midea_frame.cpp delete mode 100644 esphome/components/midea_dongle/midea_frame.h create mode 100644 esphome/components/remote_base/midea_protocol.cpp create mode 100644 esphome/components/remote_base/midea_protocol.h diff --git a/CODEOWNERS b/CODEOWNERS index ad670ede14..dedd8acc1e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -79,8 +79,7 @@ esphome/components/mcp23x17_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp9808/* @k7hpn -esphome/components/midea_ac/* @dudanov -esphome/components/midea_dongle/* @dudanov +esphome/components/midea/* @dudanov esphome/components/mitsubishi/* @RubyBailey esphome/components/network/* @esphome/core esphome/components/nextion/* @senexcrenshaw diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index f6a9fa2927..c2f07ce423 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -63,6 +63,7 @@ validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True) ClimatePreset = climate_ns.enum("ClimatePreset") CLIMATE_PRESETS = { + "NONE": ClimatePreset.CLIMATE_PRESET_NONE, "ECO": ClimatePreset.CLIMATE_PRESET_ECO, "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY, "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 8da2206f37..4861e7b8cb 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -494,5 +494,74 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { climate->publish_state(); } +template bool set_alternative(optional &dst, optional &alt, const T1 &src) { + bool is_changed = alt.has_value(); + alt.reset(); + if (is_changed || dst != src) { + dst = src; + is_changed = true; + } + return is_changed; +} + +bool Climate::set_fan_mode_(ClimateFanMode mode) { + return set_alternative(this->fan_mode, this->custom_fan_mode, mode); +} + +bool Climate::set_custom_fan_mode_(const std::string &mode) { + return set_alternative(this->custom_fan_mode, this->fan_mode, mode); +} + +bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } + +bool Climate::set_custom_preset_(const std::string &preset) { + return set_alternative(this->custom_preset, this->preset, preset); +} + +void Climate::dump_traits_(const char *tag) { + auto traits = this->get_traits(); + ESP_LOGCONFIG(tag, "ClimateTraits:"); + ESP_LOGCONFIG(tag, " [x] Visual settings:"); + ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature()); + ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature()); + ESP_LOGCONFIG(tag, " - Step: %.1f", traits.get_visual_temperature_step()); + if (traits.get_supports_current_temperature()) + ESP_LOGCONFIG(tag, " [x] Supports current temperature"); + if (traits.get_supports_two_point_target_temperature()) + ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature"); + if (traits.get_supports_action()) + ESP_LOGCONFIG(tag, " [x] Supports action"); + if (!traits.get_supported_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported modes:"); + for (ClimateMode m : traits.get_supported_modes()) + ESP_LOGCONFIG(tag, " - %s", climate_mode_to_string(m)); + } + if (!traits.get_supported_fan_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported fan modes:"); + for (ClimateFanMode m : traits.get_supported_fan_modes()) + ESP_LOGCONFIG(tag, " - %s", climate_fan_mode_to_string(m)); + } + if (!traits.get_supported_custom_fan_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported custom fan modes:"); + for (const std::string &s : traits.get_supported_custom_fan_modes()) + ESP_LOGCONFIG(tag, " - %s", s.c_str()); + } + if (!traits.get_supported_presets().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported presets:"); + for (ClimatePreset p : traits.get_supported_presets()) + ESP_LOGCONFIG(tag, " - %s", climate_preset_to_string(p)); + } + if (!traits.get_supported_custom_presets().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported custom presets:"); + for (const std::string &s : traits.get_supported_custom_presets()) + ESP_LOGCONFIG(tag, " - %s", s.c_str()); + } + if (!traits.get_supported_swing_modes().empty()) { + ESP_LOGCONFIG(tag, " [x] Supported swing modes:"); + for (ClimateSwingMode m : traits.get_supported_swing_modes()) + ESP_LOGCONFIG(tag, " - %s", climate_swing_mode_to_string(m)); + } +} + } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index b208e5946a..46d0fb1d77 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -245,6 +245,18 @@ class Climate : public Nameable { protected: friend ClimateCall; + /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. + bool set_fan_mode_(ClimateFanMode mode); + + /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. + bool set_custom_fan_mode_(const std::string &mode); + + /// Set preset. Reset custom preset. Return true if preset has been changed. + bool set_preset_(ClimatePreset preset); + + /// Set custom preset. Reset primary preset. Return true if preset has been changed. + bool set_custom_preset_(const std::string &preset); + /** Get the default traits of this climate device. * * Traits are static data that encode the capabilities and static data for a climate device such as supported @@ -270,6 +282,7 @@ class Climate : public Nameable { void save_state_(); uint32_t hash_base() override; + void dump_traits_(const char *tag); CallbackManager state_callback_{}; ESPPreferenceObject rtc_; diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 48493b500c..903ce085d8 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -72,6 +72,7 @@ class ClimateTraits { void set_supported_fan_modes(std::set modes) { supported_fan_modes_ = std::move(modes); } void add_supported_fan_mode(ClimateFanMode mode) { supported_fan_modes_.insert(mode); } + void add_supported_custom_fan_mode(const std::string &mode) { supported_custom_fan_modes_.insert(mode); } ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); } ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20") @@ -104,6 +105,7 @@ class ClimateTraits { void set_supported_presets(std::set presets) { supported_presets_ = std::move(presets); } void add_supported_preset(ClimatePreset preset) { supported_presets_.insert(preset); } + void add_supported_custom_preset(const std::string &preset) { supported_custom_presets_.insert(preset); } bool supports_preset(ClimatePreset preset) const { return supported_presets_.count(preset); } bool get_supports_presets() const { return !supported_presets_.empty(); } const std::set &get_supported_presets() const { return supported_presets_; } diff --git a/esphome/components/midea/__init__.py b/esphome/components/midea/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/midea/adapter.cpp b/esphome/components/midea/adapter.cpp new file mode 100644 index 0000000000..bd5b289095 --- /dev/null +++ b/esphome/components/midea/adapter.cpp @@ -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 diff --git a/esphome/components/midea/adapter.h b/esphome/components/midea/adapter.h new file mode 100644 index 0000000000..8d8d57e8f9 --- /dev/null +++ b/esphome/components/midea/adapter.h @@ -0,0 +1,42 @@ +#pragma once +#include +#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 diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp new file mode 100644 index 0000000000..a71f1dbdfb --- /dev/null +++ b/esphome/components/midea/air_conditioner.cpp @@ -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 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(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 diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h new file mode 100644 index 0000000000..895b6412f3 --- /dev/null +++ b/esphome/components/midea/air_conditioner.h @@ -0,0 +1,41 @@ +#pragma once +#include +#include "appliance_base.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace midea { + +using sensor::Sensor; +using climate::ClimateCall; + +class AirConditioner : public ApplianceBase { + 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 diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h new file mode 100644 index 0000000000..aa616ced36 --- /dev/null +++ b/esphome/components/midea/appliance_base.h @@ -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 +#include + +namespace esphome { +namespace midea { + +using climate::ClimatePreset; +using climate::ClimateTraits; +using climate::ClimateMode; +using climate::ClimateSwingMode; +using climate::ClimateFanMode; + +template class ApplianceBase : public Component, public uart::UARTDevice, public climate::Climate { + static_assert(std::is_base_of::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 modes) { this->supported_modes_ = std::move(modes); } + void set_supported_swing_modes(std::set modes) { this->supported_swing_modes_ = std::move(modes); } + void set_supported_presets(std::set presets) { this->supported_presets_ = std::move(presets); } + void set_custom_presets(std::set presets) { this->supported_custom_presets_ = std::move(presets); } + void set_custom_fan_modes(std::set 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 supported_modes_{}; + std::set supported_swing_modes_{}; + std::set supported_presets_{}; + std::set supported_custom_presets_{}; + std::set supported_custom_fan_modes_{}; +#ifdef USE_REMOTE_TRANSMITTER + remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr}; +#endif +}; + +} // namespace midea +} // namespace esphome diff --git a/esphome/components/midea/automations.h b/esphome/components/midea/automations.h new file mode 100644 index 0000000000..1f026c0c15 --- /dev/null +++ b/esphome/components/midea/automations.h @@ -0,0 +1,56 @@ +#pragma once +#include "esphome/core/automation.h" +#include "air_conditioner.h" + +namespace esphome { +namespace midea { + +template class MideaActionBase : public Action { + public: + void set_parent(AirConditioner *parent) { this->parent_ = parent; } + + protected: + AirConditioner *parent_; +}; + +template class FollowMeAction : public MideaActionBase { + 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 class SwingStepAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_swing_step(); } +}; + +template class DisplayToggleAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_display_toggle(); } +}; + +template class BeeperOnAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_beeper_on(); } +}; + +template class BeeperOffAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_beeper_off(); } +}; + +template class PowerOnAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_power_on(); } +}; + +template class PowerOffAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_power_off(); } +}; + +} // namespace midea +} // namespace esphome diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py new file mode 100644 index 0000000000..137fcdd607 --- /dev/null +++ b/esphome/components/midea/climate.py @@ -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") diff --git a/esphome/components/midea/midea_ir.h b/esphome/components/midea/midea_ir.h new file mode 100644 index 0000000000..2459d844a1 --- /dev/null +++ b/esphome/components/midea/midea_ir.h @@ -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 diff --git a/esphome/components/midea_ac/climate.py b/esphome/components/midea_ac/climate.py index 741741fd03..f336f84787 100644 --- a/esphome/components/midea_ac/climate.py +++ b/esphome/components/midea_ac/climate.py @@ -1,115 +1,3 @@ -from esphome.components import climate, sensor import esphome.config_validation as cv -import esphome.codegen as cg -from esphome.const import ( - CONF_CUSTOM_FAN_MODES, - CONF_CUSTOM_PRESETS, - CONF_ID, - CONF_PRESET_BOOST, - CONF_PRESET_ECO, - CONF_PRESET_SLEEP, - STATE_CLASS_MEASUREMENT, - UNIT_CELSIUS, - UNIT_PERCENT, - UNIT_WATT, - ICON_THERMOMETER, - ICON_POWER, - DEVICE_CLASS_POWER, - DEVICE_CLASS_TEMPERATURE, - ICON_WATER_PERCENT, - DEVICE_CLASS_HUMIDITY, -) -from esphome.components.midea_dongle import CONF_MIDEA_DONGLE_ID, MideaDongle -AUTO_LOAD = ["climate", "sensor", "midea_dongle"] -CODEOWNERS = ["@dudanov"] -CONF_BEEPER = "beeper" -CONF_SWING_HORIZONTAL = "swing_horizontal" -CONF_SWING_BOTH = "swing_both" -CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" -CONF_POWER_USAGE = "power_usage" -CONF_HUMIDITY_SETPOINT = "humidity_setpoint" -midea_ac_ns = cg.esphome_ns.namespace("midea_ac") -MideaAC = midea_ac_ns.class_("MideaAC", climate.Climate, cg.Component) - -CLIMATE_CUSTOM_FAN_MODES = { - "SILENT": "silent", - "TURBO": "turbo", -} - -validate_climate_custom_fan_mode = cv.enum(CLIMATE_CUSTOM_FAN_MODES, upper=True) - -CLIMATE_CUSTOM_PRESETS = { - "FREEZE_PROTECTION": "freeze protection", -} - -validate_climate_custom_preset = cv.enum(CLIMATE_CUSTOM_PRESETS, upper=True) - -CONFIG_SCHEMA = cv.All( - climate.CLIMATE_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(MideaAC), - cv.GenerateID(CONF_MIDEA_DONGLE_ID): cv.use_id(MideaDongle), - cv.Optional(CONF_BEEPER, default=False): cv.boolean, - cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list( - validate_climate_custom_fan_mode - ), - cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list( - validate_climate_custom_preset - ), - cv.Optional(CONF_SWING_HORIZONTAL, default=False): cv.boolean, - cv.Optional(CONF_SWING_BOTH, default=False): cv.boolean, - cv.Optional(CONF_PRESET_ECO, default=False): cv.boolean, - cv.Optional(CONF_PRESET_SLEEP, default=False): cv.boolean, - cv.Optional(CONF_PRESET_BOOST, default=False): cv.boolean, - 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(cv.COMPONENT_SCHEMA) -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await climate.register_climate(var, config) - paren = await cg.get_variable(config[CONF_MIDEA_DONGLE_ID]) - cg.add(var.set_midea_dongle_parent(paren)) - cg.add(var.set_beeper_feedback(config[CONF_BEEPER])) - if CONF_CUSTOM_FAN_MODES in config: - cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES])) - if CONF_CUSTOM_PRESETS in config: - cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS])) - cg.add(var.set_swing_horizontal(config[CONF_SWING_HORIZONTAL])) - cg.add(var.set_swing_both(config[CONF_SWING_BOTH])) - cg.add(var.set_preset_eco(config[CONF_PRESET_ECO])) - cg.add(var.set_preset_sleep(config[CONF_PRESET_SLEEP])) - cg.add(var.set_preset_boost(config[CONF_PRESET_BOOST])) - 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)) +CONFIG_SCHEMA = cv.invalid("This platform has been renamed to midea in 2021.9") diff --git a/esphome/components/midea_ac/midea_climate.cpp b/esphome/components/midea_ac/midea_climate.cpp deleted file mode 100644 index 72f7d23404..0000000000 --- a/esphome/components/midea_ac/midea_climate.cpp +++ /dev/null @@ -1,208 +0,0 @@ -#include "esphome/core/log.h" -#include "midea_climate.h" - -namespace esphome { -namespace midea_ac { - -static const char *const TAG = "midea_ac"; - -static void set_sensor(sensor::Sensor *sensor, float value) { - if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value)) - sensor->publish_state(value); -} - -template void set_property(T &property, T value, bool &flag) { - if (property != value) { - property = value; - flag = true; - } -} - -void MideaAC::on_frame(const midea_dongle::Frame &frame) { - const auto p = frame.as(); - if (p.has_power_info()) { - set_sensor(this->power_sensor_, p.get_power_usage()); - return; - } else if (!p.has_properties()) { - ESP_LOGW(TAG, "RX: frame has unknown type"); - return; - } - if (p.get_type() == midea_dongle::MideaMessageType::DEVICE_CONTROL) { - ESP_LOGD(TAG, "RX: control frame"); - this->ctrl_request_ = false; - } else { - ESP_LOGD(TAG, "RX: query frame"); - } - if (this->ctrl_request_) - return; - this->cmd_frame_.set_properties(p); // copy properties from response - bool need_publish = false; - set_property(this->mode, p.get_mode(), need_publish); - set_property(this->target_temperature, p.get_target_temp(), need_publish); - set_property(this->current_temperature, p.get_indoor_temp(), need_publish); - if (p.is_custom_fan_mode()) { - this->fan_mode.reset(); - optional mode = p.get_custom_fan_mode(); - set_property(this->custom_fan_mode, mode, need_publish); - } else { - this->custom_fan_mode.reset(); - optional mode = p.get_fan_mode(); - set_property(this->fan_mode, mode, need_publish); - } - set_property(this->swing_mode, p.get_swing_mode(), need_publish); - if (p.is_custom_preset()) { - this->preset.reset(); - optional preset = p.get_custom_preset(); - set_property(this->custom_preset, preset, need_publish); - } else { - this->custom_preset.reset(); - set_property(this->preset, p.get_preset(), need_publish); - } - if (need_publish) - this->publish_state(); - set_sensor(this->outdoor_sensor_, p.get_outdoor_temp()); - set_sensor(this->humidity_sensor_, p.get_humidity_setpoint()); -} - -void MideaAC::on_update() { - if (this->ctrl_request_) { - ESP_LOGD(TAG, "TX: control"); - this->parent_->write_frame(this->cmd_frame_); - } else { - ESP_LOGD(TAG, "TX: query"); - if (this->power_sensor_ == nullptr || this->request_num_++ % 32) - this->parent_->write_frame(this->query_frame_); - else - this->parent_->write_frame(this->power_frame_); - } -} - -bool MideaAC::allow_preset(climate::ClimatePreset preset) const { - switch (preset) { - case climate::CLIMATE_PRESET_ECO: - if (this->mode == climate::CLIMATE_MODE_COOL) { - return true; - } else { - ESP_LOGD(TAG, "ECO preset is only available in COOL mode"); - } - break; - case climate::CLIMATE_PRESET_SLEEP: - if (this->mode == climate::CLIMATE_MODE_FAN_ONLY || this->mode == climate::CLIMATE_MODE_DRY) { - ESP_LOGD(TAG, "SLEEP preset is not available in FAN_ONLY or DRY mode"); - } else { - return true; - } - break; - case climate::CLIMATE_PRESET_BOOST: - if (this->mode == climate::CLIMATE_MODE_HEAT || this->mode == climate::CLIMATE_MODE_COOL) { - return true; - } else { - ESP_LOGD(TAG, "BOOST preset is only available in HEAT or COOL mode"); - } - break; - case climate::CLIMATE_PRESET_NONE: - return true; - default: - break; - } - return false; -} - -bool MideaAC::allow_custom_preset(const std::string &custom_preset) const { - if (custom_preset == MIDEA_FREEZE_PROTECTION_PRESET) { - if (this->mode == climate::CLIMATE_MODE_HEAT) { - return true; - } else { - ESP_LOGD(TAG, "%s is only available in HEAT mode", MIDEA_FREEZE_PROTECTION_PRESET.c_str()); - } - } - return false; -} - -void MideaAC::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value() && call.get_mode().value() != this->mode) { - this->cmd_frame_.set_mode(call.get_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_target_temperature().has_value() && call.get_target_temperature().value() != this->target_temperature) { - this->cmd_frame_.set_target_temp(call.get_target_temperature().value()); - this->ctrl_request_ = true; - } - if (call.get_fan_mode().has_value() && - (!this->fan_mode.has_value() || this->fan_mode.value() != call.get_fan_mode().value())) { - this->custom_fan_mode.reset(); - this->cmd_frame_.set_fan_mode(call.get_fan_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_custom_fan_mode().has_value() && - (!this->custom_fan_mode.has_value() || this->custom_fan_mode.value() != call.get_custom_fan_mode().value())) { - this->fan_mode.reset(); - this->cmd_frame_.set_custom_fan_mode(call.get_custom_fan_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_swing_mode().has_value() && call.get_swing_mode().value() != this->swing_mode) { - this->cmd_frame_.set_swing_mode(call.get_swing_mode().value()); - this->ctrl_request_ = true; - } - if (call.get_preset().has_value() && this->allow_preset(call.get_preset().value()) && - (!this->preset.has_value() || this->preset.value() != call.get_preset().value())) { - this->custom_preset.reset(); - this->cmd_frame_.set_preset(call.get_preset().value()); - this->ctrl_request_ = true; - } - if (call.get_custom_preset().has_value() && this->allow_custom_preset(call.get_custom_preset().value()) && - (!this->custom_preset.has_value() || this->custom_preset.value() != call.get_custom_preset().value())) { - this->preset.reset(); - this->cmd_frame_.set_custom_preset(call.get_custom_preset().value()); - this->ctrl_request_ = true; - } - if (this->ctrl_request_) { - this->cmd_frame_.set_beeper_feedback(this->beeper_feedback_); - this->cmd_frame_.finalize(); - } -} - -climate::ClimateTraits MideaAC::traits() { - auto traits = climate::ClimateTraits(); - traits.set_visual_min_temperature(17); - traits.set_visual_max_temperature(30); - traits.set_visual_temperature_step(0.5); - traits.set_supported_modes({ - climate::CLIMATE_MODE_OFF, - climate::CLIMATE_MODE_HEAT_COOL, - climate::CLIMATE_MODE_COOL, - climate::CLIMATE_MODE_DRY, - climate::CLIMATE_MODE_HEAT, - climate::CLIMATE_MODE_FAN_ONLY, - }); - traits.set_supported_fan_modes({ - climate::CLIMATE_FAN_AUTO, - climate::CLIMATE_FAN_LOW, - climate::CLIMATE_FAN_MEDIUM, - climate::CLIMATE_FAN_HIGH, - }); - traits.set_supported_custom_fan_modes(this->traits_custom_fan_modes_); - traits.set_supported_swing_modes({ - climate::CLIMATE_SWING_OFF, - climate::CLIMATE_SWING_VERTICAL, - }); - if (traits_swing_horizontal_) - traits.add_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL); - if (traits_swing_both_) - traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH); - traits.set_supported_presets({ - climate::CLIMATE_PRESET_NONE, - }); - if (traits_preset_eco_) - traits.add_supported_preset(climate::CLIMATE_PRESET_ECO); - if (traits_preset_sleep_) - traits.add_supported_preset(climate::CLIMATE_PRESET_SLEEP); - if (traits_preset_boost_) - traits.add_supported_preset(climate::CLIMATE_PRESET_BOOST); - traits.set_supported_custom_presets(this->traits_custom_presets_); - traits.set_supports_current_temperature(true); - return traits; -} - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_ac/midea_climate.h b/esphome/components/midea_ac/midea_climate.h deleted file mode 100644 index 62bd4c339e..0000000000 --- a/esphome/components/midea_ac/midea_climate.h +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once - -#include - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/midea_dongle/midea_dongle.h" -#include "esphome/components/climate/climate.h" -#include "esphome/components/midea_dongle/midea_dongle.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/core/component.h" -#include "midea_frame.h" - -namespace esphome { -namespace midea_ac { - -class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, public Component { - public: - float get_setup_priority() const override { return setup_priority::LATE; } - void on_frame(const midea_dongle::Frame &frame) override; - void on_update() override; - void setup() override { this->parent_->set_appliance(this); } - void set_midea_dongle_parent(midea_dongle::MideaDongle *parent) { this->parent_ = parent; } - void set_outdoor_temperature_sensor(sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; } - void set_humidity_setpoint_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } - void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } - void set_beeper_feedback(bool state) { this->beeper_feedback_ = state; } - void set_swing_horizontal(bool state) { this->traits_swing_horizontal_ = state; } - void set_swing_both(bool state) { this->traits_swing_both_ = state; } - void set_preset_eco(bool state) { this->traits_preset_eco_ = state; } - void set_preset_sleep(bool state) { this->traits_preset_sleep_ = state; } - void set_preset_boost(bool state) { this->traits_preset_boost_ = state; } - bool allow_preset(climate::ClimatePreset preset) const; - void set_custom_fan_modes(std::set custom_fan_modes) { - this->traits_custom_fan_modes_ = std::move(custom_fan_modes); - } - void set_custom_presets(std::set custom_presets) { - this->traits_custom_presets_ = std::move(custom_presets); - } - bool allow_custom_preset(const std::string &custom_preset) const; - - protected: - /// Override control to change settings of the climate device. - void control(const climate::ClimateCall &call) override; - /// Return the traits of this controller. - climate::ClimateTraits traits() override; - - const QueryFrame query_frame_; - const PowerQueryFrame power_frame_; - CommandFrame cmd_frame_; - midea_dongle::MideaDongle *parent_{nullptr}; - sensor::Sensor *outdoor_sensor_{nullptr}; - sensor::Sensor *humidity_sensor_{nullptr}; - sensor::Sensor *power_sensor_{nullptr}; - uint8_t request_num_{0}; - bool ctrl_request_{false}; - bool beeper_feedback_{false}; - bool traits_swing_horizontal_{false}; - bool traits_swing_both_{false}; - bool traits_preset_eco_{false}; - bool traits_preset_sleep_{false}; - bool traits_preset_boost_{false}; - std::set traits_custom_fan_modes_{{}}; - std::set traits_custom_presets_{{}}; -}; - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_ac/midea_frame.cpp b/esphome/components/midea_ac/midea_frame.cpp deleted file mode 100644 index c0a5ce4b55..0000000000 --- a/esphome/components/midea_ac/midea_frame.cpp +++ /dev/null @@ -1,238 +0,0 @@ -#include "midea_frame.h" - -namespace esphome { -namespace midea_ac { - -static const char *const TAG = "midea_ac"; -const std::string MIDEA_SILENT_FAN_MODE = "silent"; -const std::string MIDEA_TURBO_FAN_MODE = "turbo"; -const std::string MIDEA_FREEZE_PROTECTION_PRESET = "freeze protection"; - -const uint8_t QueryFrame::INIT[] = {0xAA, 0x21, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x81, - 0x00, 0xFF, 0x03, 0xFF, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x37, 0x31}; - -const uint8_t PowerQueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x21, - 0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x17, 0x6A}; - -const uint8_t CommandFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x40, 0x00, - 0x00, 0x00, 0x7F, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -float PropertiesFrame::get_target_temp() const { - float temp = static_cast((this->pbuf_[12] & 0x0F) + 16); - if (this->pbuf_[12] & 0x10) - temp += 0.5; - return temp; -} - -void PropertiesFrame::set_target_temp(float temp) { - uint8_t tmp = static_cast(temp * 16.0) + 4; - tmp = ((tmp & 8) << 1) | (tmp >> 4); - this->pbuf_[12] &= ~0x1F; - this->pbuf_[12] |= tmp; -} - -static float i16tof(int16_t in) { return static_cast(in - 50) / 2.0; } -float PropertiesFrame::get_indoor_temp() const { return i16tof(this->pbuf_[21]); } -float PropertiesFrame::get_outdoor_temp() const { return i16tof(this->pbuf_[22]); } -float PropertiesFrame::get_humidity_setpoint() const { return static_cast(this->pbuf_[29] & 0x7F); } - -climate::ClimateMode PropertiesFrame::get_mode() const { - if (!this->get_power_()) - return climate::CLIMATE_MODE_OFF; - switch (this->pbuf_[12] >> 5) { - case MIDEA_MODE_AUTO: - return climate::CLIMATE_MODE_HEAT_COOL; - case MIDEA_MODE_COOL: - return climate::CLIMATE_MODE_COOL; - case MIDEA_MODE_DRY: - return climate::CLIMATE_MODE_DRY; - case MIDEA_MODE_HEAT: - return climate::CLIMATE_MODE_HEAT; - case MIDEA_MODE_FAN_ONLY: - return climate::CLIMATE_MODE_FAN_ONLY; - default: - return climate::CLIMATE_MODE_OFF; - } -} - -void PropertiesFrame::set_mode(climate::ClimateMode mode) { - uint8_t m; - switch (mode) { - case climate::CLIMATE_MODE_HEAT_COOL: - m = MIDEA_MODE_AUTO; - break; - case climate::CLIMATE_MODE_COOL: - m = MIDEA_MODE_COOL; - break; - case climate::CLIMATE_MODE_DRY: - m = MIDEA_MODE_DRY; - break; - case climate::CLIMATE_MODE_HEAT: - m = MIDEA_MODE_HEAT; - break; - case climate::CLIMATE_MODE_FAN_ONLY: - m = MIDEA_MODE_FAN_ONLY; - break; - default: - this->set_power_(false); - return; - } - this->set_power_(true); - this->pbuf_[12] &= ~0xE0; - this->pbuf_[12] |= m << 5; -} - -optional PropertiesFrame::get_preset() const { - if (this->get_eco_mode()) - return climate::CLIMATE_PRESET_ECO; - if (this->get_sleep_mode()) - return climate::CLIMATE_PRESET_SLEEP; - if (this->get_turbo_mode()) - return climate::CLIMATE_PRESET_BOOST; - return climate::CLIMATE_PRESET_NONE; -} - -void PropertiesFrame::set_preset(climate::ClimatePreset preset) { - this->clear_presets(); - switch (preset) { - case climate::CLIMATE_PRESET_ECO: - this->set_eco_mode(true); - break; - case climate::CLIMATE_PRESET_SLEEP: - this->set_sleep_mode(true); - break; - case climate::CLIMATE_PRESET_BOOST: - this->set_turbo_mode(true); - break; - default: - break; - } -} - -void PropertiesFrame::clear_presets() { - this->set_eco_mode(false); - this->set_sleep_mode(false); - this->set_turbo_mode(false); - this->set_freeze_protection_mode(false); -} - -bool PropertiesFrame::is_custom_preset() const { return this->get_freeze_protection_mode(); } - -const std::string &PropertiesFrame::get_custom_preset() const { return midea_ac::MIDEA_FREEZE_PROTECTION_PRESET; }; - -void PropertiesFrame::set_custom_preset(const std::string &preset) { - this->clear_presets(); - if (preset == MIDEA_FREEZE_PROTECTION_PRESET) - this->set_freeze_protection_mode(true); -} - -bool PropertiesFrame::is_custom_fan_mode() const { - switch (this->pbuf_[13]) { - case MIDEA_FAN_SILENT: - case MIDEA_FAN_TURBO: - return true; - default: - return false; - } -} - -climate::ClimateFanMode PropertiesFrame::get_fan_mode() const { - switch (this->pbuf_[13]) { - case MIDEA_FAN_LOW: - return climate::CLIMATE_FAN_LOW; - case MIDEA_FAN_MEDIUM: - return climate::CLIMATE_FAN_MEDIUM; - case MIDEA_FAN_HIGH: - return climate::CLIMATE_FAN_HIGH; - default: - return climate::CLIMATE_FAN_AUTO; - } -} - -void PropertiesFrame::set_fan_mode(climate::ClimateFanMode mode) { - uint8_t m; - switch (mode) { - case climate::CLIMATE_FAN_LOW: - m = MIDEA_FAN_LOW; - break; - case climate::CLIMATE_FAN_MEDIUM: - m = MIDEA_FAN_MEDIUM; - break; - case climate::CLIMATE_FAN_HIGH: - m = MIDEA_FAN_HIGH; - break; - default: - m = MIDEA_FAN_AUTO; - break; - } - this->pbuf_[13] = m; -} - -const std::string &PropertiesFrame::get_custom_fan_mode() const { - switch (this->pbuf_[13]) { - case MIDEA_FAN_SILENT: - return MIDEA_SILENT_FAN_MODE; - default: - return MIDEA_TURBO_FAN_MODE; - } -} - -void PropertiesFrame::set_custom_fan_mode(const std::string &mode) { - uint8_t m; - if (mode == MIDEA_SILENT_FAN_MODE) { - m = MIDEA_FAN_SILENT; - } else { - m = MIDEA_FAN_TURBO; - } - this->pbuf_[13] = m; -} - -climate::ClimateSwingMode PropertiesFrame::get_swing_mode() const { - switch (this->pbuf_[17] & 0x0F) { - case MIDEA_SWING_VERTICAL: - return climate::CLIMATE_SWING_VERTICAL; - case MIDEA_SWING_HORIZONTAL: - return climate::CLIMATE_SWING_HORIZONTAL; - case MIDEA_SWING_BOTH: - return climate::CLIMATE_SWING_BOTH; - default: - return climate::CLIMATE_SWING_OFF; - } -} - -void PropertiesFrame::set_swing_mode(climate::ClimateSwingMode mode) { - uint8_t m; - switch (mode) { - case climate::CLIMATE_SWING_VERTICAL: - m = MIDEA_SWING_VERTICAL; - break; - case climate::CLIMATE_SWING_HORIZONTAL: - m = MIDEA_SWING_HORIZONTAL; - break; - case climate::CLIMATE_SWING_BOTH: - m = MIDEA_SWING_BOTH; - break; - default: - m = MIDEA_SWING_OFF; - break; - } - this->pbuf_[17] = 0x30 | m; -} - -float PropertiesFrame::get_power_usage() const { - uint32_t power = 0; - const uint8_t *ptr = this->pbuf_ + 28; - for (uint32_t weight = 1;; weight *= 10, ptr--) { - power += (*ptr % 16) * weight; - weight *= 10; - power += (*ptr / 16) * weight; - if (weight == 100000) - return static_cast(power) * 0.1; - } -} - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_ac/midea_frame.h b/esphome/components/midea_ac/midea_frame.h deleted file mode 100644 index e1d6fed49d..0000000000 --- a/esphome/components/midea_ac/midea_frame.h +++ /dev/null @@ -1,165 +0,0 @@ -#pragma once -#include "esphome/components/climate/climate.h" -#include "esphome/components/midea_dongle/midea_frame.h" - -namespace esphome { -namespace midea_ac { - -extern const std::string MIDEA_SILENT_FAN_MODE; -extern const std::string MIDEA_TURBO_FAN_MODE; -extern const std::string MIDEA_FREEZE_PROTECTION_PRESET; - -/// Enum for all modes a Midea device can be in. -enum MideaMode : uint8_t { - /// The Midea device is set to automatically change the heating/cooling cycle - MIDEA_MODE_AUTO = 1, - /// The Midea device is manually set to cool mode (not in auto mode!) - MIDEA_MODE_COOL = 2, - /// The Midea device is manually set to dry mode - MIDEA_MODE_DRY = 3, - /// The Midea device is manually set to heat mode (not in auto mode!) - MIDEA_MODE_HEAT = 4, - /// The Midea device is manually set to fan only mode - MIDEA_MODE_FAN_ONLY = 5, -}; - -/// Enum for all modes a Midea fan can be in -enum MideaFanMode : uint8_t { - /// The fan mode is set to Auto - MIDEA_FAN_AUTO = 102, - /// The fan mode is set to Silent - MIDEA_FAN_SILENT = 20, - /// The fan mode is set to Low - MIDEA_FAN_LOW = 40, - /// The fan mode is set to Medium - MIDEA_FAN_MEDIUM = 60, - /// The fan mode is set to High - MIDEA_FAN_HIGH = 80, - /// The fan mode is set to Turbo - MIDEA_FAN_TURBO = 100, -}; - -/// Enum for all modes a Midea swing can be in -enum MideaSwingMode : uint8_t { - /// The sing mode is set to Off - MIDEA_SWING_OFF = 0b0000, - /// The fan mode is set to Both - MIDEA_SWING_BOTH = 0b1111, - /// The fan mode is set to Vertical - MIDEA_SWING_VERTICAL = 0b1100, - /// The fan mode is set to Horizontal - MIDEA_SWING_HORIZONTAL = 0b0011, -}; - -class PropertiesFrame : public midea_dongle::BaseFrame { - public: - PropertiesFrame() = delete; - PropertiesFrame(uint8_t *data) : BaseFrame(data) {} - PropertiesFrame(const Frame &frame) : BaseFrame(frame) {} - - bool has_properties() const { - return this->has_response_type(0xC0) && (this->has_type(0x03) || this->has_type(0x02)); - } - - bool has_power_info() const { return this->has_response_type(0xC1); } - - /* TARGET TEMPERATURE */ - - float get_target_temp() const; - void set_target_temp(float temp); - - /* MODE */ - climate::ClimateMode get_mode() const; - void set_mode(climate::ClimateMode mode); - - /* FAN SPEED */ - bool is_custom_fan_mode() const; - climate::ClimateFanMode get_fan_mode() const; - void set_fan_mode(climate::ClimateFanMode mode); - - const std::string &get_custom_fan_mode() const; - void set_custom_fan_mode(const std::string &mode); - - /* SWING MODE */ - climate::ClimateSwingMode get_swing_mode() const; - void set_swing_mode(climate::ClimateSwingMode mode); - - /* INDOOR TEMPERATURE */ - float get_indoor_temp() const; - - /* OUTDOOR TEMPERATURE */ - float get_outdoor_temp() const; - - /* HUMIDITY SETPOINT */ - float get_humidity_setpoint() const; - - /* ECO MODE */ - bool get_eco_mode() const { return this->pbuf_[19] & 0x10; } - void set_eco_mode(bool state) { this->set_bytemask_(19, 0x80, state); } - - /* SLEEP MODE */ - bool get_sleep_mode() const { return this->pbuf_[20] & 0x01; } - void set_sleep_mode(bool state) { this->set_bytemask_(20, 0x01, state); } - - /* TURBO MODE */ - bool get_turbo_mode() const { return this->pbuf_[18] & 0x20 || this->pbuf_[20] & 0x02; } - void set_turbo_mode(bool state) { - this->set_bytemask_(18, 0x20, state); - this->set_bytemask_(20, 0x02, state); - } - - /* FREEZE PROTECTION */ - bool get_freeze_protection_mode() const { return this->pbuf_[31] & 0x80; } - void set_freeze_protection_mode(bool state) { this->set_bytemask_(31, 0x80, state); } - - /* PRESET */ - optional get_preset() const; - void set_preset(climate::ClimatePreset preset); - void clear_presets(); - - bool is_custom_preset() const; - const std::string &get_custom_preset() const; - void set_custom_preset(const std::string &preset); - - /* POWER USAGE */ - float get_power_usage() const; - - /// Set properties from another frame - void set_properties(const PropertiesFrame &p) { memcpy(this->pbuf_ + 11, p.data() + 11, 10); } - - protected: - /* POWER */ - bool get_power_() const { return this->pbuf_[11] & 0x01; } - void set_power_(bool state) { this->set_bytemask_(11, 0x01, state); } -}; - -// Query state frame (read-only) -class QueryFrame : public midea_dongle::StaticFrame { - public: - QueryFrame() : StaticFrame(FPSTR(this->INIT)) {} - - private: - static const uint8_t PROGMEM INIT[]; -}; - -// Power query state frame (read-only) -class PowerQueryFrame : public midea_dongle::StaticFrame { - public: - PowerQueryFrame() : StaticFrame(FPSTR(this->INIT)) {} - - private: - static const uint8_t PROGMEM INIT[]; -}; - -// Command frame -class CommandFrame : public midea_dongle::StaticFrame { - public: - CommandFrame() : StaticFrame(FPSTR(this->INIT)) {} - void set_beeper_feedback(bool state) { this->set_bytemask_(11, 0x40, state); } - - private: - static const uint8_t PROGMEM INIT[]; -}; - -} // namespace midea_ac -} // namespace esphome diff --git a/esphome/components/midea_dongle/__init__.py b/esphome/components/midea_dongle/__init__.py deleted file mode 100644 index daa8ea6657..0000000000 --- a/esphome/components/midea_dongle/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import uart -from esphome.const import CONF_ID - -DEPENDENCIES = ["wifi", "uart"] -CODEOWNERS = ["@dudanov"] - -midea_dongle_ns = cg.esphome_ns.namespace("midea_dongle") -MideaDongle = midea_dongle_ns.class_("MideaDongle", cg.Component, uart.UARTDevice) - -CONF_MIDEA_DONGLE_ID = "midea_dongle_id" -CONF_STRENGTH_ICON = "strength_icon" -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(MideaDongle), - cv.Optional(CONF_STRENGTH_ICON, default=False): cv.boolean, - } - ) - .extend(cv.COMPONENT_SCHEMA) - .extend(uart.UART_DEVICE_SCHEMA) -) - - -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) - cg.add(var.use_strength_icon(config[CONF_STRENGTH_ICON])) diff --git a/esphome/components/midea_dongle/midea_dongle.cpp b/esphome/components/midea_dongle/midea_dongle.cpp deleted file mode 100644 index 7e3683a964..0000000000 --- a/esphome/components/midea_dongle/midea_dongle.cpp +++ /dev/null @@ -1,98 +0,0 @@ -#include "midea_dongle.h" -#include "esphome/core/log.h" -#include "esphome/core/helpers.h" - -namespace esphome { -namespace midea_dongle { - -static const char *const TAG = "midea_dongle"; - -void MideaDongle::loop() { - while (this->available()) { - const uint8_t rx = this->read(); - if (this->idx_ <= OFFSET_LENGTH) { - if (this->idx_ == OFFSET_LENGTH) { - if (rx <= OFFSET_BODY || rx >= sizeof(this->buf_)) { - this->reset_(); - continue; - } - this->cnt_ = rx; - } else if (rx != SYNC_BYTE) { - continue; - } - } - this->buf_[this->idx_++] = rx; - if (--this->cnt_) - continue; - this->reset_(); - const BaseFrame frame(this->buf_); - ESP_LOGD(TAG, "RX: %s", frame.to_string().c_str()); - if (!frame.is_valid()) { - ESP_LOGW(TAG, "RX: frame check failed!"); - continue; - } - if (frame.get_type() == QUERY_NETWORK) { - this->notify_.set_type(QUERY_NETWORK); - this->need_notify_ = true; - continue; - } - if (this->appliance_ != nullptr) - this->appliance_->on_frame(frame); - } -} - -void MideaDongle::update() { - const bool is_conn = WiFi.isConnected(); - uint8_t wifi_strength = 0; - if (!this->rssi_timer_) { - if (is_conn) - wifi_strength = 4; - } else if (is_conn) { - if (--this->rssi_timer_) { - wifi_strength = this->notify_.get_signal_strength(); - } else { - this->rssi_timer_ = 60; - const int32_t dbm = WiFi.RSSI(); - if (dbm > -63) - wifi_strength = 4; - else if (dbm > -75) - wifi_strength = 3; - else if (dbm > -88) - wifi_strength = 2; - else if (dbm > -100) - wifi_strength = 1; - } - } else { - this->rssi_timer_ = 1; - } - if (this->notify_.is_connected() != is_conn) { - this->notify_.set_connected(is_conn); - this->need_notify_ = true; - } - if (this->notify_.get_signal_strength() != wifi_strength) { - this->notify_.set_signal_strength(wifi_strength); - this->need_notify_ = true; - } - if (!--this->notify_timer_) { - this->notify_.set_type(NETWORK_NOTIFY); - this->need_notify_ = true; - } - if (this->need_notify_) { - ESP_LOGD(TAG, "TX: notify WiFi STA %s, signal strength %d", is_conn ? "connected" : "not connected", wifi_strength); - this->need_notify_ = false; - this->notify_timer_ = 600; - this->notify_.finalize(); - this->write_frame(this->notify_); - return; - } - if (this->appliance_ != nullptr) - this->appliance_->on_update(); -} - -void MideaDongle::write_frame(const Frame &frame) { - this->write_array(frame.data(), frame.size()); - ESP_LOGD(TAG, "TX: %s", frame.to_string().c_str()); -} - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/midea_dongle/midea_dongle.h b/esphome/components/midea_dongle/midea_dongle.h deleted file mode 100644 index a7dfb9cf25..0000000000 --- a/esphome/components/midea_dongle/midea_dongle.h +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once -#include "esphome/core/component.h" -#include "esphome/components/wifi/wifi_component.h" -#include "esphome/components/uart/uart.h" -#include "midea_frame.h" - -namespace esphome { -namespace midea_dongle { - -enum MideaApplianceType : uint8_t { DEHUMIDIFIER = 0xA1, AIR_CONDITIONER = 0xAC, BROADCAST = 0xFF }; -enum MideaMessageType : uint8_t { - DEVICE_CONTROL = 0x02, - DEVICE_QUERY = 0x03, - NETWORK_NOTIFY = 0x0D, - QUERY_NETWORK = 0x63, -}; - -struct MideaAppliance { - /// Calling on update event - virtual void on_update() = 0; - /// Calling on frame receive event - virtual void on_frame(const Frame &frame) = 0; -}; - -class MideaDongle : public PollingComponent, public uart::UARTDevice { - public: - MideaDongle() : PollingComponent(1000) {} - float get_setup_priority() const override { return setup_priority::LATE; } - void update() override; - void loop() override; - void set_appliance(MideaAppliance *app) { this->appliance_ = app; } - void use_strength_icon(bool state) { this->rssi_timer_ = state; } - void write_frame(const Frame &frame); - - protected: - MideaAppliance *appliance_{nullptr}; - NotifyFrame notify_; - unsigned notify_timer_{1}; - // Buffer - uint8_t buf_[36]; - // Index - uint8_t idx_{0}; - // Reverse receive counter - uint8_t cnt_{2}; - uint8_t rssi_timer_{0}; - bool need_notify_{false}; - - // Reset receiver state - void reset_() { - this->idx_ = 0; - this->cnt_ = 2; - } -}; - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/midea_dongle/midea_frame.cpp b/esphome/components/midea_dongle/midea_frame.cpp deleted file mode 100644 index acb3feee5f..0000000000 --- a/esphome/components/midea_dongle/midea_frame.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#include "midea_frame.h" - -namespace esphome { -namespace midea_dongle { - -const uint8_t BaseFrame::CRC_TABLE[] = { - 0x00, 0x5E, 0xBC, 0xE2, 0x61, 0x3F, 0xDD, 0x83, 0xC2, 0x9C, 0x7E, 0x20, 0xA3, 0xFD, 0x1F, 0x41, 0x9D, 0xC3, 0x21, - 0x7F, 0xFC, 0xA2, 0x40, 0x1E, 0x5F, 0x01, 0xE3, 0xBD, 0x3E, 0x60, 0x82, 0xDC, 0x23, 0x7D, 0x9F, 0xC1, 0x42, 0x1C, - 0xFE, 0xA0, 0xE1, 0xBF, 0x5D, 0x03, 0x80, 0xDE, 0x3C, 0x62, 0xBE, 0xE0, 0x02, 0x5C, 0xDF, 0x81, 0x63, 0x3D, 0x7C, - 0x22, 0xC0, 0x9E, 0x1D, 0x43, 0xA1, 0xFF, 0x46, 0x18, 0xFA, 0xA4, 0x27, 0x79, 0x9B, 0xC5, 0x84, 0xDA, 0x38, 0x66, - 0xE5, 0xBB, 0x59, 0x07, 0xDB, 0x85, 0x67, 0x39, 0xBA, 0xE4, 0x06, 0x58, 0x19, 0x47, 0xA5, 0xFB, 0x78, 0x26, 0xC4, - 0x9A, 0x65, 0x3B, 0xD9, 0x87, 0x04, 0x5A, 0xB8, 0xE6, 0xA7, 0xF9, 0x1B, 0x45, 0xC6, 0x98, 0x7A, 0x24, 0xF8, 0xA6, - 0x44, 0x1A, 0x99, 0xC7, 0x25, 0x7B, 0x3A, 0x64, 0x86, 0xD8, 0x5B, 0x05, 0xE7, 0xB9, 0x8C, 0xD2, 0x30, 0x6E, 0xED, - 0xB3, 0x51, 0x0F, 0x4E, 0x10, 0xF2, 0xAC, 0x2F, 0x71, 0x93, 0xCD, 0x11, 0x4F, 0xAD, 0xF3, 0x70, 0x2E, 0xCC, 0x92, - 0xD3, 0x8D, 0x6F, 0x31, 0xB2, 0xEC, 0x0E, 0x50, 0xAF, 0xF1, 0x13, 0x4D, 0xCE, 0x90, 0x72, 0x2C, 0x6D, 0x33, 0xD1, - 0x8F, 0x0C, 0x52, 0xB0, 0xEE, 0x32, 0x6C, 0x8E, 0xD0, 0x53, 0x0D, 0xEF, 0xB1, 0xF0, 0xAE, 0x4C, 0x12, 0x91, 0xCF, - 0x2D, 0x73, 0xCA, 0x94, 0x76, 0x28, 0xAB, 0xF5, 0x17, 0x49, 0x08, 0x56, 0xB4, 0xEA, 0x69, 0x37, 0xD5, 0x8B, 0x57, - 0x09, 0xEB, 0xB5, 0x36, 0x68, 0x8A, 0xD4, 0x95, 0xCB, 0x29, 0x77, 0xF4, 0xAA, 0x48, 0x16, 0xE9, 0xB7, 0x55, 0x0B, - 0x88, 0xD6, 0x34, 0x6A, 0x2B, 0x75, 0x97, 0xC9, 0x4A, 0x14, 0xF6, 0xA8, 0x74, 0x2A, 0xC8, 0x96, 0x15, 0x4B, 0xA9, - 0xF7, 0xB6, 0xE8, 0x0A, 0x54, 0xD7, 0x89, 0x6B, 0x35}; - -const uint8_t NotifyFrame::INIT[] = {0xAA, 0x1F, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0D, 0x01, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - -bool BaseFrame::is_valid() const { return /*this->has_valid_crc_() &&*/ this->has_valid_cs_(); } - -void BaseFrame::finalize() { - this->update_crc_(); - this->update_cs_(); -} - -void BaseFrame::update_crc_() { - uint8_t crc = 0; - uint8_t *ptr = this->pbuf_ + OFFSET_BODY; - uint8_t len = this->length_() - OFFSET_BODY; - while (--len) - crc = pgm_read_byte(BaseFrame::CRC_TABLE + (crc ^ *ptr++)); - *ptr = crc; -} - -void BaseFrame::update_cs_() { - uint8_t cs = 0; - uint8_t *ptr = this->pbuf_ + OFFSET_LENGTH; - uint8_t len = this->length_(); - while (--len) - cs -= *ptr++; - *ptr = cs; -} - -bool BaseFrame::has_valid_crc_() const { - uint8_t crc = 0; - uint8_t len = this->length_() - OFFSET_BODY; - const uint8_t *ptr = this->pbuf_ + OFFSET_BODY; - for (; len; ptr++, len--) - crc = pgm_read_byte(BaseFrame::CRC_TABLE + (crc ^ *ptr)); - return !crc; -} - -bool BaseFrame::has_valid_cs_() const { - uint8_t cs = 0; - uint8_t len = this->length_(); - const uint8_t *ptr = this->pbuf_ + OFFSET_LENGTH; - for (; len; ptr++, len--) - cs -= *ptr; - return !cs; -} - -void BaseFrame::set_bytemask_(uint8_t idx, uint8_t mask, bool state) { - uint8_t *dst = this->pbuf_ + idx; - if (state) - *dst |= mask; - else - *dst &= ~mask; -} - -static char u4hex(uint8_t num) { return num + ((num < 10) ? '0' : ('A' - 10)); } - -String Frame::to_string() const { - String ret; - char buf[4]; - buf[2] = ' '; - buf[3] = '\0'; - ret.reserve(3 * 36); - const uint8_t *it = this->data(); - for (size_t i = 0; i < this->size(); i++, it++) { - buf[0] = u4hex(*it >> 4); - buf[1] = u4hex(*it & 15); - ret.concat(buf); - } - return ret; -} - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/midea_dongle/midea_frame.h b/esphome/components/midea_dongle/midea_frame.h deleted file mode 100644 index ce89cc636e..0000000000 --- a/esphome/components/midea_dongle/midea_frame.h +++ /dev/null @@ -1,104 +0,0 @@ -#pragma once -#include "esphome/core/component.h" - -namespace esphome { -namespace midea_dongle { - -static const uint8_t OFFSET_START = 0; -static const uint8_t OFFSET_LENGTH = 1; -static const uint8_t OFFSET_APPTYPE = 2; -static const uint8_t OFFSET_BODY = 10; -static const uint8_t SYNC_BYTE = 0xAA; - -class Frame { - public: - Frame() = delete; - Frame(uint8_t *data) : pbuf_(data) {} - Frame(const Frame &frame) : pbuf_(frame.data()) {} - - // Frame buffer - uint8_t *data() const { return this->pbuf_; } - // Frame size - uint8_t size() const { return this->length_() + OFFSET_LENGTH; } - uint8_t app_type() const { return this->pbuf_[OFFSET_APPTYPE]; } - - template typename std::enable_if::value, T>::type as() const { - return T(*this); - } - String to_string() const; - - protected: - uint8_t *pbuf_; - uint8_t length_() const { return this->pbuf_[OFFSET_LENGTH]; } -}; - -class BaseFrame : public Frame { - public: - BaseFrame() = delete; - BaseFrame(uint8_t *data) : Frame(data) {} - BaseFrame(const Frame &frame) : Frame(frame) {} - - // Check for valid - bool is_valid() const; - // Prepare for sending to device - void finalize(); - uint8_t get_type() const { return this->pbuf_[9]; } - void set_type(uint8_t value) { this->pbuf_[9] = value; } - bool has_response_type(uint8_t type) const { return this->resp_type_() == type; } - bool has_type(uint8_t type) const { return this->get_type() == type; } - - protected: - static const uint8_t PROGMEM CRC_TABLE[256]; - void set_bytemask_(uint8_t idx, uint8_t mask, bool state); - uint8_t resp_type_() const { return this->pbuf_[OFFSET_BODY]; } - bool has_valid_crc_() const; - bool has_valid_cs_() const; - void update_crc_(); - void update_cs_(); -}; - -template class StaticFrame : public T { - public: - // Default constructor - StaticFrame() : T(this->buf_) {} - // Copy constructor - StaticFrame(const Frame &src) : T(this->buf_) { - if (src.length_() < sizeof(this->buf_)) { - memcpy(this->buf_, src.data(), src.length_() + OFFSET_LENGTH); - } - } - // Constructor for RAM data - StaticFrame(const uint8_t *src) : T(this->buf_) { - const uint8_t len = src[OFFSET_LENGTH]; - if (len < sizeof(this->buf_)) { - memcpy(this->buf_, src, len + OFFSET_LENGTH); - } - } - // Constructor for PROGMEM data - StaticFrame(const __FlashStringHelper *pgm) : T(this->buf_) { - const uint8_t *src = reinterpret_cast(pgm); - const uint8_t len = pgm_read_byte(src + OFFSET_LENGTH); - if (len < sizeof(this->buf_)) { - memcpy_P(this->buf_, src, len + OFFSET_LENGTH); - } - } - - protected: - uint8_t buf_[buf_size]; -}; - -// Device network notification frame -class NotifyFrame : public midea_dongle::StaticFrame { - public: - NotifyFrame() : StaticFrame(FPSTR(NotifyFrame::INIT)) {} - void set_signal_strength(uint8_t value) { this->pbuf_[12] = value; } - uint8_t get_signal_strength() const { return this->pbuf_[12]; } - void set_connected(bool state) { this->pbuf_[18] = state ? 0 : 1; } - bool is_connected() const { return !this->pbuf_[18]; } - - private: - static const uint8_t PROGMEM INIT[]; -}; - -} // namespace midea_dongle -} // namespace esphome diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index c9f1c611a8..d76dc6bc34 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -1085,3 +1085,45 @@ async def panasonic_action(var, config, args): cg.add(var.set_address(template_)) template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint32) cg.add(var.set_command(template_)) + + +# Midea +MideaData, MideaBinarySensor, MideaTrigger, MideaAction, MideaDumper = declare_protocol( + "Midea" +) +MideaAction = ns.class_("MideaAction", RemoteTransmitterActionBase) +MIDEA_SCHEMA = cv.Schema( + { + cv.Required(CONF_CODE): cv.All( + [cv.Any(cv.hex_uint8_t, cv.uint8_t)], + cv.Length(min=5, max=5), + ), + cv.GenerateID(CONF_CODE_STORAGE_ID): cv.declare_id(cg.uint8), + } +) + + +@register_binary_sensor("midea", MideaBinarySensor, MIDEA_SCHEMA) +def midea_binary_sensor(var, config): + arr_ = cg.progmem_array(config[CONF_CODE_STORAGE_ID], config[CONF_CODE]) + cg.add(var.set_code(arr_)) + + +@register_trigger("midea", MideaTrigger, MideaData) +def midea_trigger(var, config): + pass + + +@register_dumper("midea", MideaDumper) +def midea_dumper(var, config): + pass + + +@register_action( + "midea", + MideaAction, + MIDEA_SCHEMA, +) +async def midea_action(var, config, args): + arr_ = cg.progmem_array(config[CONF_CODE_STORAGE_ID], config[CONF_CODE]) + cg.add(var.set_code(arr_)) diff --git a/esphome/components/remote_base/midea_protocol.cpp b/esphome/components/remote_base/midea_protocol.cpp new file mode 100644 index 0000000000..baf64f246f --- /dev/null +++ b/esphome/components/remote_base/midea_protocol.cpp @@ -0,0 +1,99 @@ +#include "midea_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.midea"; + +uint8_t MideaData::calc_cs_() const { + uint8_t cs = 0; + for (const uint8_t *it = this->data(); it != this->data() + OFFSET_CS; ++it) + cs -= reverse_bits_8(*it); + return reverse_bits_8(cs); +} + +bool MideaData::check_compliment(const MideaData &rhs) const { + const uint8_t *it0 = rhs.data(); + for (const uint8_t *it1 = this->data(); it1 != this->data() + this->size(); ++it0, ++it1) { + if (*it0 != ~(*it1)) + return false; + } + return true; +} + +void MideaProtocol::data(RemoteTransmitData *dst, const MideaData &src, bool compliment) { + for (const uint8_t *it = src.data(); it != src.data() + src.size(); ++it) { + const uint8_t data = compliment ? ~(*it) : *it; + for (uint8_t mask = 128; mask; mask >>= 1) { + if (data & mask) + one(dst); + else + zero(dst); + } + } +} + +void MideaProtocol::encode(RemoteTransmitData *dst, const MideaData &data) { + dst->set_carrier_frequency(38000); + dst->reserve(2 + 48 * 2 + 2 + 2 + 48 * 2 + 2); + MideaProtocol::header(dst); + MideaProtocol::data(dst, data); + MideaProtocol::footer(dst); + MideaProtocol::header(dst); + MideaProtocol::data(dst, data, true); + MideaProtocol::footer(dst); +} + +bool MideaProtocol::expect_one(RemoteReceiveData &src) { + if (!src.peek_item(BIT_HIGH_US, BIT_ONE_LOW_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_zero(RemoteReceiveData &src) { + if (!src.peek_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_header(RemoteReceiveData &src) { + if (!src.peek_item(HEADER_HIGH_US, HEADER_LOW_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_footer(RemoteReceiveData &src) { + if (!src.peek_item(BIT_HIGH_US, MIN_GAP_US)) + return false; + src.advance(2); + return true; +} + +bool MideaProtocol::expect_data(RemoteReceiveData &src, MideaData &out) { + for (uint8_t *dst = out.data(); dst != out.data() + out.size(); ++dst) { + for (uint8_t mask = 128; mask; mask >>= 1) { + if (MideaProtocol::expect_one(src)) + *dst |= mask; + else if (!MideaProtocol::expect_zero(src)) + return false; + } + } + return true; +} + +optional MideaProtocol::decode(RemoteReceiveData src) { + MideaData out, inv; + if (MideaProtocol::expect_header(src) && MideaProtocol::expect_data(src, out) && MideaProtocol::expect_footer(src) && + out.is_valid() && MideaProtocol::expect_data(src, inv) && out.check_compliment(inv)) + return out; + return {}; +} + +void MideaProtocol::dump(const MideaData &data) { ESP_LOGD(TAG, "Received Midea: %s", data.to_string().c_str()); } + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h new file mode 100644 index 0000000000..9b0d156617 --- /dev/null +++ b/esphome/components/remote_base/midea_protocol.h @@ -0,0 +1,105 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +class MideaData { + public: + // Make zero-filled + MideaData() { memset(this->data_, 0, sizeof(this->data_)); } + // Make from initializer_list + MideaData(std::initializer_list data) { std::copy(data.begin(), data.end(), this->data()); } + // Make from vector + MideaData(const std::vector &data) { + memcpy(this->data_, data.data(), std::min(data.size(), sizeof(this->data_))); + } + // Make 40-bit copy from PROGMEM array + MideaData(const uint8_t *data) { memcpy_P(this->data_, data, OFFSET_CS); } + // Default copy constructor + MideaData(const MideaData &) = default; + + uint8_t *data() { return this->data_; } + const uint8_t *data() const { return this->data_; } + uint8_t size() const { return sizeof(this->data_); } + bool is_valid() const { return this->data_[OFFSET_CS] == this->calc_cs_(); } + void finalize() { this->data_[OFFSET_CS] = this->calc_cs_(); } + bool check_compliment(const MideaData &rhs) const; + std::string to_string() const { return hexencode(*this); } + // compare only 40-bits + bool operator==(const MideaData &rhs) const { return !memcmp(this->data_, rhs.data_, OFFSET_CS); } + enum MideaDataType : uint8_t { + MIDEA_TYPE_COMMAND = 0xA1, + MIDEA_TYPE_SPECIAL = 0xA2, + MIDEA_TYPE_FOLLOW_ME = 0xA4, + }; + MideaDataType type() const { return static_cast(this->data_[0]); } + template T to() const { return T(*this); } + + protected: + void set_value_(uint8_t offset, uint8_t val_mask, uint8_t shift, uint8_t val) { + data_[offset] &= ~(val_mask << shift); + data_[offset] |= (val << shift); + } + static const uint8_t OFFSET_CS = 5; + // 48-bits data + uint8_t data_[6]; + // Calculate checksum + uint8_t calc_cs_() const; +}; + +class MideaProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const MideaData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const MideaData &data) override; + + protected: + static const int32_t TICK_US = 560; + static const int32_t HEADER_HIGH_US = 8 * TICK_US; + static const int32_t HEADER_LOW_US = 8 * TICK_US; + static const int32_t BIT_HIGH_US = 1 * TICK_US; + static const int32_t BIT_ONE_LOW_US = 3 * TICK_US; + static const int32_t BIT_ZERO_LOW_US = 1 * TICK_US; + static const int32_t MIN_GAP_US = 10 * TICK_US; + static void one(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); } + static void zero(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); } + static void header(RemoteTransmitData *dst) { dst->item(HEADER_HIGH_US, HEADER_LOW_US); } + static void footer(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, MIN_GAP_US); } + static void data(RemoteTransmitData *dst, const MideaData &src, bool compliment = false); + static bool expect_one(RemoteReceiveData &src); + static bool expect_zero(RemoteReceiveData &src); + static bool expect_header(RemoteReceiveData &src); + static bool expect_footer(RemoteReceiveData &src); + static bool expect_data(RemoteReceiveData &src, MideaData &out); +}; + +class MideaBinarySensor : public RemoteReceiverBinarySensorBase { + public: + bool matches(RemoteReceiveData src) override { + auto data = MideaProtocol().decode(src); + return data.has_value() && data.value() == this->data_; + } + void set_code(const uint8_t *code) { this->data_ = code; } + + protected: + MideaData data_; +}; + +using MideaTrigger = RemoteReceiverTrigger; +using MideaDumper = RemoteReceiverDumper; + +template class MideaAction : public RemoteTransmitterActionBase { + TEMPLATABLE_VALUE(const uint8_t *, code) + void encode(RemoteTransmitData *dst, Ts... x) override { + MideaData data = this->code_.value(x...); + data.finalize(); + MideaProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index aff03b244b..07d0079172 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -69,6 +69,7 @@ CONF_ATTENUATION = "attenuation" CONF_ATTRIBUTE = "attribute" CONF_AUTH = "auth" CONF_AUTO_MODE = "auto_mode" +CONF_AUTOCONF = "autoconf" CONF_AUTOMATION_ID = "automation_id" CONF_AVAILABILITY = "availability" CONF_AWAY = "away" @@ -78,6 +79,7 @@ CONF_BASELINE = "baseline" CONF_BATTERY_LEVEL = "battery_level" CONF_BATTERY_VOLTAGE = "battery_voltage" CONF_BAUD_RATE = "baud_rate" +CONF_BEEPER = "beeper" CONF_BELOW = "below" CONF_BINARY = "binary" CONF_BINARY_SENSOR = "binary_sensor" @@ -615,6 +617,10 @@ CONF_SUPPLEMENTAL_COOLING_ACTION = "supplemental_cooling_action" CONF_SUPPLEMENTAL_COOLING_DELTA = "supplemental_cooling_delta" CONF_SUPPLEMENTAL_HEATING_ACTION = "supplemental_heating_action" CONF_SUPPLEMENTAL_HEATING_DELTA = "supplemental_heating_delta" +CONF_SUPPORTED_FAN_MODES = "supported_fan_modes" +CONF_SUPPORTED_MODES = "supported_modes" +CONF_SUPPORTED_PRESETS = "supported_presets" +CONF_SUPPORTED_SWING_MODES = "supported_swing_modes" CONF_SUPPORTS_COOL = "supports_cool" CONF_SUPPORTS_HEAT = "supports_heat" CONF_SWING_BOTH_ACTION = "swing_both_action" diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index e4535e77d9..cd3081998b 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -20,6 +20,7 @@ const float PROCESSOR = 400.0; const float BLUETOOTH = 350.0f; const float AFTER_BLUETOOTH = 300.0f; const float WIFI = 250.0f; +const float BEFORE_CONNECTION = 220.0f; const float AFTER_WIFI = 200.0f; const float AFTER_CONNECTION = 100.0f; const float LATE = -100.0f; diff --git a/esphome/core/component.h b/esphome/core/component.h index a84f612dd9..ea87ebcdfe 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -29,6 +29,8 @@ extern const float PROCESSOR; extern const float BLUETOOTH; extern const float AFTER_BLUETOOTH; extern const float WIFI; +/// For components that should be initialized after WiFi and before API is connected. +extern const float BEFORE_CONNECTION; /// For components that should be initialized after WiFi is connected. extern const float AFTER_WIFI; /// For components that should be initialized after a data connection (API/MQTT) is connected. diff --git a/platformio.ini b/platformio.ini index c280c54a21..88b1000d1d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -36,6 +36,7 @@ lib_deps = 6306@1.0.3 ; HM3301 glmnet/Dsmr@0.3 ; used by dsmr rweather/Crypto@0.2.0 ; used by dsmr + dudanov/MideaUART@1.1.0 ; used by midea build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE diff --git a/tests/test1.yaml b/tests/test1.yaml index da3289843d..cd4179f394 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1608,29 +1608,84 @@ climate: name: Toshiba Climate - platform: hitachi_ac344 name: Hitachi Climate - - platform: midea_ac + - platform: midea + id: midea_unit + uart_id: uart0 + name: Midea Climate + transmitter_id: + period: 1s + num_attempts: 5 + timeout: 2s + beeper: false + autoconf: true visual: - min_temperature: 18 °C - max_temperature: 25 °C - temperature_step: 0.1 °C - name: 'Electrolux EACS' - beeper: true + min_temperature: 17 °C + max_temperature: 30 °C + temperature_step: 0.5 °C + supported_modes: + - FAN_ONLY + - HEAT_COOL + - COOL + - HEAT + - DRY + custom_fan_modes: + - SILENT + - TURBO + supported_presets: + - ECO + - BOOST + - SLEEP + custom_presets: + - FREEZE_PROTECTION + supported_swing_modes: + - VERTICAL + - HORIZONTAL + - BOTH outdoor_temperature: - name: 'Temp' + name: "Temp" power_usage: - name: 'Power' + name: "Power" humidity_setpoint: - name: 'Hum' + name: "Humidity" - platform: anova name: Anova cooker ble_client_id: ble_blah unit_of_measurement: c -midea_dongle: - uart_id: uart0 - strength_icon: true +script: + - id: climate_custom + then: + - climate.control: + id: midea_unit + custom_preset: FREEZE_PROTECTION + custom_fan_mode: SILENT + - id: climate_preset + then: + - climate.control: + id: midea_unit + preset: SLEEP switch: + - platform: template + name: MIDEA_AC_TOGGLE_LIGHT + turn_on_action: + midea_ac.display_toggle: + - platform: template + name: MIDEA_AC_SWING_STEP + turn_on_action: + midea_ac.swing_step: + - platform: template + name: MIDEA_AC_BEEPER_CONTROL + optimistic: true + turn_on_action: + midea_ac.beeper_on: + turn_off_action: + midea_ac.beeper_off: + - platform: template + name: MIDEA_RAW + turn_on_action: + remote_transmitter.transmit_midea: + code: [0xA2, 0x08, 0xFF, 0xFF, 0xFF] - platform: gpio name: 'MCP23S08 Pin #0' pin: diff --git a/tests/test3.yaml b/tests/test3.yaml index e35c1e611c..c012871125 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -748,17 +748,6 @@ script: - id: my_script then: - lambda: 'ESP_LOGD("main", "Hello World!");' - - id: climate_custom - then: - - climate.control: - id: midea_ac_unit - custom_preset: FREEZE_PROTECTION - custom_fan_mode: SILENT - - id: climate_preset - then: - - climate.control: - id: midea_ac_unit - preset: SLEEP sm2135: data_pin: GPIO12 @@ -949,32 +938,6 @@ climate: kp: 0.0 ki: 0.0 kd: 0.0 - - platform: midea_ac - id: midea_ac_unit - visual: - min_temperature: 18 °C - max_temperature: 25 °C - temperature_step: 0.1 °C - name: "Electrolux EACS" - beeper: true - custom_fan_modes: - - SILENT - - TURBO - preset_eco: true - preset_sleep: true - preset_boost: true - custom_presets: - - FREEZE_PROTECTION - outdoor_temperature: - name: "Temp" - power_usage: - name: "Power" - humidity_setpoint: - name: "Hum" - -midea_dongle: - uart_id: uart1 - strength_icon: true cover: - platform: endstop