mirror of
https://github.com/esphome/esphome.git
synced 2024-11-25 00:18:11 +01:00
Midea support v2 (#2188)
This commit is contained in:
parent
2790d72bff
commit
4e120a291e
33 changed files with 1276 additions and 1226 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -494,5 +494,74 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
|
|||
climate->publish_state();
|
||||
}
|
||||
|
||||
template<typename T1, typename T2> bool set_alternative(optional<T1> &dst, optional<T2> &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
|
||||
|
|
|
@ -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<void()> state_callback_{};
|
||||
ESPPreferenceObject rtc_;
|
||||
|
|
|
@ -72,6 +72,7 @@ class ClimateTraits {
|
|||
|
||||
void set_supported_fan_modes(std::set<ClimateFanMode> 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<ClimatePreset> 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<climate::ClimatePreset> &get_supported_presets() const { return supported_presets_; }
|
||||
|
|
0
esphome/components/midea/__init__.py
Normal file
0
esphome/components/midea/__init__.py
Normal file
173
esphome/components/midea/adapter.cpp
Normal file
173
esphome/components/midea/adapter.cpp
Normal file
|
@ -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
|
42
esphome/components/midea/adapter.h
Normal file
42
esphome/components/midea/adapter.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
#include <Appliance/AirConditioner/AirConditioner.h>
|
||||
#include "esphome/components/climate/climate_traits.h"
|
||||
#include "appliance_base.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace midea {
|
||||
|
||||
using MideaMode = dudanov::midea::ac::Mode;
|
||||
using MideaSwingMode = dudanov::midea::ac::SwingMode;
|
||||
using MideaFanMode = dudanov::midea::ac::FanMode;
|
||||
using MideaPreset = dudanov::midea::ac::Preset;
|
||||
|
||||
class Constants {
|
||||
public:
|
||||
static const char *const TAG;
|
||||
static const std::string FREEZE_PROTECTION;
|
||||
static const std::string SILENT;
|
||||
static const std::string TURBO;
|
||||
};
|
||||
|
||||
class Converters {
|
||||
public:
|
||||
static MideaMode to_midea_mode(ClimateMode mode);
|
||||
static ClimateMode to_climate_mode(MideaMode mode);
|
||||
static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode);
|
||||
static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode);
|
||||
static MideaPreset to_midea_preset(ClimatePreset preset);
|
||||
static MideaPreset to_midea_preset(const std::string &preset);
|
||||
static bool is_custom_midea_preset(MideaPreset preset);
|
||||
static ClimatePreset to_climate_preset(MideaPreset preset);
|
||||
static const std::string &to_custom_climate_preset(MideaPreset preset);
|
||||
static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode);
|
||||
static MideaFanMode to_midea_fan_mode(const std::string &fan_mode);
|
||||
static bool is_custom_midea_fan_mode(MideaFanMode fan_mode);
|
||||
static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode);
|
||||
static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode);
|
||||
static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities);
|
||||
};
|
||||
|
||||
} // namespace midea
|
||||
} // namespace esphome
|
152
esphome/components/midea/air_conditioner.cpp
Normal file
152
esphome/components/midea/air_conditioner.cpp
Normal file
|
@ -0,0 +1,152 @@
|
|||
#include "esphome/core/log.h"
|
||||
#include "air_conditioner.h"
|
||||
#include "adapter.h"
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
#include "midea_ir.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace midea {
|
||||
|
||||
static void set_sensor(Sensor *sensor, float value) {
|
||||
if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value))
|
||||
sensor->publish_state(value);
|
||||
}
|
||||
|
||||
template<typename T> void update_property(T &property, const T &value, bool &flag) {
|
||||
if (property != value) {
|
||||
property = value;
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
|
||||
void AirConditioner::on_status_change() {
|
||||
bool need_publish = false;
|
||||
update_property(this->target_temperature, this->base_.getTargetTemp(), need_publish);
|
||||
update_property(this->current_temperature, this->base_.getIndoorTemp(), need_publish);
|
||||
auto mode = Converters::to_climate_mode(this->base_.getMode());
|
||||
update_property(this->mode, mode, need_publish);
|
||||
auto swing_mode = Converters::to_climate_swing_mode(this->base_.getSwingMode());
|
||||
update_property(this->swing_mode, swing_mode, need_publish);
|
||||
// Preset
|
||||
auto preset = this->base_.getPreset();
|
||||
if (Converters::is_custom_midea_preset(preset)) {
|
||||
if (this->set_custom_preset_(Converters::to_custom_climate_preset(preset)))
|
||||
need_publish = true;
|
||||
} else if (this->set_preset_(Converters::to_climate_preset(preset))) {
|
||||
need_publish = true;
|
||||
}
|
||||
// Fan mode
|
||||
auto fan_mode = this->base_.getFanMode();
|
||||
if (Converters::is_custom_midea_fan_mode(fan_mode)) {
|
||||
if (this->set_custom_fan_mode_(Converters::to_custom_climate_fan_mode(fan_mode)))
|
||||
need_publish = true;
|
||||
} else if (this->set_fan_mode_(Converters::to_climate_fan_mode(fan_mode))) {
|
||||
need_publish = true;
|
||||
}
|
||||
if (need_publish)
|
||||
this->publish_state();
|
||||
set_sensor(this->outdoor_sensor_, this->base_.getOutdoorTemp());
|
||||
set_sensor(this->power_sensor_, this->base_.getPowerUsage());
|
||||
set_sensor(this->humidity_sensor_, this->base_.getIndoorHum());
|
||||
}
|
||||
|
||||
void AirConditioner::control(const ClimateCall &call) {
|
||||
dudanov::midea::ac::Control ctrl{};
|
||||
if (call.get_target_temperature().has_value())
|
||||
ctrl.targetTemp = call.get_target_temperature().value();
|
||||
if (call.get_swing_mode().has_value())
|
||||
ctrl.swingMode = Converters::to_midea_swing_mode(call.get_swing_mode().value());
|
||||
if (call.get_mode().has_value())
|
||||
ctrl.mode = Converters::to_midea_mode(call.get_mode().value());
|
||||
if (call.get_preset().has_value())
|
||||
ctrl.preset = Converters::to_midea_preset(call.get_preset().value());
|
||||
else if (call.get_custom_preset().has_value())
|
||||
ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().value());
|
||||
if (call.get_fan_mode().has_value())
|
||||
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value());
|
||||
else if (call.get_custom_fan_mode().has_value())
|
||||
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().value());
|
||||
this->base_.control(ctrl);
|
||||
}
|
||||
|
||||
ClimateTraits AirConditioner::traits() {
|
||||
auto traits = ClimateTraits();
|
||||
traits.set_supports_current_temperature(true);
|
||||
traits.set_visual_min_temperature(17);
|
||||
traits.set_visual_max_temperature(30);
|
||||
traits.set_visual_temperature_step(0.5);
|
||||
traits.set_supported_modes(this->supported_modes_);
|
||||
traits.set_supported_swing_modes(this->supported_swing_modes_);
|
||||
traits.set_supported_presets(this->supported_presets_);
|
||||
traits.set_supported_custom_presets(this->supported_custom_presets_);
|
||||
traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_);
|
||||
/* + MINIMAL SET OF CAPABILITIES */
|
||||
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF);
|
||||
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_FAN_ONLY);
|
||||
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO);
|
||||
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW);
|
||||
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM);
|
||||
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH);
|
||||
traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF);
|
||||
traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_VERTICAL);
|
||||
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE);
|
||||
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP);
|
||||
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK)
|
||||
Converters::to_climate_traits(traits, this->base_.getCapabilities());
|
||||
return traits;
|
||||
}
|
||||
|
||||
void AirConditioner::dump_config() {
|
||||
ESP_LOGCONFIG(Constants::TAG, "MideaDongle:");
|
||||
ESP_LOGCONFIG(Constants::TAG, " [x] Period: %dms", this->base_.getPeriod());
|
||||
ESP_LOGCONFIG(Constants::TAG, " [x] Response timeout: %dms", this->base_.getTimeout());
|
||||
ESP_LOGCONFIG(Constants::TAG, " [x] Request attempts: %d", this->base_.getNumAttempts());
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
ESP_LOGCONFIG(Constants::TAG, " [x] Using RemoteTransmitter");
|
||||
#endif
|
||||
if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_OK) {
|
||||
this->base_.getCapabilities().dump();
|
||||
} else if (this->base_.getAutoconfStatus() == dudanov::midea::AUTOCONF_ERROR) {
|
||||
ESP_LOGW(Constants::TAG,
|
||||
"Failed to get 0xB5 capabilities report. Suggest to disable it in config and manually set your "
|
||||
"appliance options.");
|
||||
}
|
||||
this->dump_traits_(Constants::TAG);
|
||||
}
|
||||
|
||||
/* ACTIONS */
|
||||
|
||||
void AirConditioner::do_follow_me(float temperature, bool beeper) {
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
IrFollowMeData data(static_cast<uint8_t>(lroundf(temperature)), beeper);
|
||||
this->transmit_ir(data);
|
||||
#else
|
||||
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
|
||||
#endif
|
||||
}
|
||||
|
||||
void AirConditioner::do_swing_step() {
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
IrSpecialData data(0x01);
|
||||
this->transmit_ir(data);
|
||||
#else
|
||||
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
|
||||
#endif
|
||||
}
|
||||
|
||||
void AirConditioner::do_display_toggle() {
|
||||
if (this->base_.getCapabilities().supportLightControl()) {
|
||||
this->base_.displayToggle();
|
||||
} else {
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
IrSpecialData data(0x08);
|
||||
this->transmit_ir(data);
|
||||
#else
|
||||
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace midea
|
||||
} // namespace esphome
|
41
esphome/components/midea/air_conditioner.h
Normal file
41
esphome/components/midea/air_conditioner.h
Normal file
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
#include <Appliance/AirConditioner/AirConditioner.h>
|
||||
#include "appliance_base.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace midea {
|
||||
|
||||
using sensor::Sensor;
|
||||
using climate::ClimateCall;
|
||||
|
||||
class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner> {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; }
|
||||
void set_humidity_setpoint_sensor(Sensor *sensor) { this->humidity_sensor_ = sensor; }
|
||||
void set_power_sensor(Sensor *sensor) { this->power_sensor_ = sensor; }
|
||||
void on_status_change() override;
|
||||
|
||||
/* ############### */
|
||||
/* ### ACTIONS ### */
|
||||
/* ############### */
|
||||
|
||||
void do_follow_me(float temperature, bool beeper = false);
|
||||
void do_display_toggle();
|
||||
void do_swing_step();
|
||||
void do_beeper_on() { this->set_beeper_feedback(true); }
|
||||
void do_beeper_off() { this->set_beeper_feedback(false); }
|
||||
void do_power_on() { this->base_.setPowerState(true); }
|
||||
void do_power_off() { this->base_.setPowerState(false); }
|
||||
|
||||
protected:
|
||||
void control(const ClimateCall &call) override;
|
||||
ClimateTraits traits() override;
|
||||
Sensor *outdoor_sensor_{nullptr};
|
||||
Sensor *humidity_sensor_{nullptr};
|
||||
Sensor *power_sensor_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace midea
|
||||
} // namespace esphome
|
76
esphome/components/midea/appliance_base.h
Normal file
76
esphome/components/midea/appliance_base.h
Normal file
|
@ -0,0 +1,76 @@
|
|||
#pragma once
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
#include "esphome/components/remote_base/midea_protocol.h"
|
||||
#include "esphome/components/remote_transmitter/remote_transmitter.h"
|
||||
#endif
|
||||
#include <Appliance/ApplianceBase.h>
|
||||
#include <Helpers/Logger.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace midea {
|
||||
|
||||
using climate::ClimatePreset;
|
||||
using climate::ClimateTraits;
|
||||
using climate::ClimateMode;
|
||||
using climate::ClimateSwingMode;
|
||||
using climate::ClimateFanMode;
|
||||
|
||||
template<typename T> class ApplianceBase : public Component, public uart::UARTDevice, public climate::Climate {
|
||||
static_assert(std::is_base_of<dudanov::midea::ApplianceBase, T>::value,
|
||||
"T must derive from dudanov::midea::ApplianceBase class");
|
||||
|
||||
public:
|
||||
ApplianceBase() {
|
||||
this->base_.setStream(this);
|
||||
this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this));
|
||||
dudanov::midea::ApplianceBase::setLogger([](int level, const char *tag, int line, String format, va_list args) {
|
||||
esp_log_vprintf_(level, tag, line, format.c_str(), args);
|
||||
});
|
||||
}
|
||||
bool can_proceed() override {
|
||||
return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS;
|
||||
}
|
||||
float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; }
|
||||
void setup() override { this->base_.setup(); }
|
||||
void loop() override { this->base_.loop(); }
|
||||
void set_period(uint32_t ms) { this->base_.setPeriod(ms); }
|
||||
void set_response_timeout(uint32_t ms) { this->base_.setTimeout(ms); }
|
||||
void set_request_attempts(uint32_t attempts) { this->base_.setNumAttempts(attempts); }
|
||||
void set_beeper_feedback(bool state) { this->base_.setBeeper(state); }
|
||||
void set_autoconf(bool value) { this->base_.setAutoconf(value); }
|
||||
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
|
||||
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
|
||||
void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); }
|
||||
void set_custom_presets(std::set<std::string> presets) { this->supported_custom_presets_ = std::move(presets); }
|
||||
void set_custom_fan_modes(std::set<std::string> modes) { this->supported_custom_fan_modes_ = std::move(modes); }
|
||||
virtual void on_status_change() = 0;
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
|
||||
this->transmitter_ = transmitter;
|
||||
}
|
||||
void transmit_ir(remote_base::MideaData &data) {
|
||||
data.finalize();
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
remote_base::MideaProtocol().encode(transmit.get_data(), data);
|
||||
transmit.perform();
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
T base_;
|
||||
std::set<ClimateMode> supported_modes_{};
|
||||
std::set<ClimateSwingMode> supported_swing_modes_{};
|
||||
std::set<ClimatePreset> supported_presets_{};
|
||||
std::set<std::string> supported_custom_presets_{};
|
||||
std::set<std::string> supported_custom_fan_modes_{};
|
||||
#ifdef USE_REMOTE_TRANSMITTER
|
||||
remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace midea
|
||||
} // namespace esphome
|
56
esphome/components/midea/automations.h
Normal file
56
esphome/components/midea/automations.h
Normal file
|
@ -0,0 +1,56 @@
|
|||
#pragma once
|
||||
#include "esphome/core/automation.h"
|
||||
#include "air_conditioner.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace midea {
|
||||
|
||||
template<typename... Ts> class MideaActionBase : public Action<Ts...> {
|
||||
public:
|
||||
void set_parent(AirConditioner *parent) { this->parent_ = parent; }
|
||||
|
||||
protected:
|
||||
AirConditioner *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class FollowMeAction : public MideaActionBase<Ts...> {
|
||||
TEMPLATABLE_VALUE(float, temperature)
|
||||
TEMPLATABLE_VALUE(bool, beeper)
|
||||
|
||||
void play(Ts... x) override {
|
||||
this->parent_->do_follow_me(this->temperature_.value(x...), this->beeper_.value(x...));
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class SwingStepAction : public MideaActionBase<Ts...> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->do_swing_step(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class DisplayToggleAction : public MideaActionBase<Ts...> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->do_display_toggle(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class BeeperOnAction : public MideaActionBase<Ts...> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->do_beeper_on(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class BeeperOffAction : public MideaActionBase<Ts...> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->do_beeper_off(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class PowerOnAction : public MideaActionBase<Ts...> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->do_power_on(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class PowerOffAction : public MideaActionBase<Ts...> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->do_power_off(); }
|
||||
};
|
||||
|
||||
} // namespace midea
|
||||
} // namespace esphome
|
284
esphome/components/midea/climate.py
Normal file
284
esphome/components/midea/climate.py
Normal file
|
@ -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")
|
42
esphome/components/midea/midea_ir.h
Normal file
42
esphome/components/midea/midea_ir.h
Normal file
|
@ -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
|
|
@ -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")
|
||||
|
|
|
@ -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<typename T> 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<PropertiesFrame>();
|
||||
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<std::string> mode = p.get_custom_fan_mode();
|
||||
set_property(this->custom_fan_mode, mode, need_publish);
|
||||
} else {
|
||||
this->custom_fan_mode.reset();
|
||||
optional<climate::ClimateFanMode> 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<std::string> 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
|
|
@ -1,68 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <utility>
|
||||
|
||||
#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<std::string> custom_fan_modes) {
|
||||
this->traits_custom_fan_modes_ = std::move(custom_fan_modes);
|
||||
}
|
||||
void set_custom_presets(std::set<std::string> 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<std::string> traits_custom_fan_modes_{{}};
|
||||
std::set<std::string> traits_custom_presets_{{}};
|
||||
};
|
||||
|
||||
} // namespace midea_ac
|
||||
} // namespace esphome
|
|
@ -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<float>((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<uint8_t>(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<float>(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<float>(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<climate::ClimatePreset> 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<float>(power) * 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace midea_ac
|
||||
} // namespace esphome
|
|
@ -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<climate::ClimatePreset> 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<midea_dongle::Frame> {
|
||||
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<midea_dongle::Frame> {
|
||||
public:
|
||||
PowerQueryFrame() : StaticFrame(FPSTR(this->INIT)) {}
|
||||
|
||||
private:
|
||||
static const uint8_t PROGMEM INIT[];
|
||||
};
|
||||
|
||||
// Command frame
|
||||
class CommandFrame : public midea_dongle::StaticFrame<PropertiesFrame> {
|
||||
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
|
|
@ -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]))
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 T> typename std::enable_if<std::is_base_of<Frame, T>::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<typename T = Frame, size_t buf_size = 36> 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<decltype(src)>(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<BaseFrame, 32> {
|
||||
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
|
|
@ -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_))
|
||||
|
|
99
esphome/components/remote_base/midea_protocol.cpp
Normal file
99
esphome/components/remote_base/midea_protocol.cpp
Normal file
|
@ -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<MideaData> 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
|
105
esphome/components/remote_base/midea_protocol.h
Normal file
105
esphome/components/remote_base/midea_protocol.h
Normal file
|
@ -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<uint8_t> data) { std::copy(data.begin(), data.end(), this->data()); }
|
||||
// Make from vector
|
||||
MideaData(const std::vector<uint8_t> &data) {
|
||||
memcpy(this->data_, data.data(), std::min<size_t>(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<MideaDataType>(this->data_[0]); }
|
||||
template<typename T> 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<MideaData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const MideaData &data) override;
|
||||
optional<MideaData> 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<MideaProtocol, MideaData>;
|
||||
using MideaDumper = RemoteReceiverDumper<MideaProtocol, MideaData>;
|
||||
|
||||
template<typename... Ts> class MideaAction : public RemoteTransmitterActionBase<Ts...> {
|
||||
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
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue