Merge pull request #189 from GrKoR/dev

Feature/Fix: ESPHome 2025.11.0 Compatibility & Swing Mode Refactor
This commit is contained in:
GK
2025-12-02 23:07:53 -08:00
committed by GitHub
3 changed files with 475 additions and 181 deletions

View File

@@ -4,144 +4,192 @@
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
namespace esphome { namespace esphome
namespace aux_ac { {
namespace aux_ac
{
// **************************************** DISPLAY ACTIONS **************************************** // **************************************** DISPLAY ACTIONS ****************************************
template <typename... Ts> template <typename... Ts>
class AirConDisplayOffAction : public Action<Ts...> { class AirConDisplayOffAction : public Action<Ts...>
{
public: public:
explicit AirConDisplayOffAction(AirCon *ac) : ac_(ac) {} explicit AirConDisplayOffAction(AirCon *ac) : ac_(ac) {}
void play(Ts... x) override { this->ac_->displayOffSequence(); } void play(const Ts &...x) override
{
this->ac_->displayOffSequence();
}
protected: protected:
AirCon *ac_; AirCon *ac_;
}; };
template <typename... Ts> template <typename... Ts>
class AirConDisplayOnAction : public Action<Ts...> { class AirConDisplayOnAction : public Action<Ts...>
{
public: public:
explicit AirConDisplayOnAction(AirCon *ac) : ac_(ac) {} explicit AirConDisplayOnAction(AirCon *ac) : ac_(ac) {}
void play(Ts... x) override { this->ac_->displayOnSequence(); } void play(const Ts &...x) override
{
this->ac_->displayOnSequence();
}
protected: protected:
AirCon *ac_; AirCon *ac_;
}; };
// **************************************** VERTICAL LOUVER ACTIONS **************************************** // **************************************** VERTICAL LOUVER ACTIONS ****************************************
template <typename... Ts> template <typename... Ts>
class AirConVLouverSwingAction : public Action<Ts...> { class AirConVLouverSwingAction : public Action<Ts...>
{
public: public:
explicit AirConVLouverSwingAction(AirCon *ac) : ac_(ac) {} explicit AirConVLouverSwingAction(AirCon *ac) : ac_(ac) {}
void play(Ts... x) override { this->ac_->setVLouverSwingSequence(); } void play(const Ts &...x) override
{
this->ac_->setVLouverSwingSequence();
}
protected: protected:
AirCon *ac_; AirCon *ac_;
}; };
template <typename... Ts> template <typename... Ts>
class AirConVLouverStopAction : public Action<Ts...> { class AirConVLouverStopAction : public Action<Ts...>
{
public: public:
explicit AirConVLouverStopAction(AirCon *ac) : ac_(ac) {} explicit AirConVLouverStopAction(AirCon *ac) : ac_(ac) {}
void play(Ts... x) override { this->ac_->setVLouverStopSequence(); } void play(const Ts &...x) override
{
this->ac_->setVLouverStopSequence();
}
protected: protected:
AirCon *ac_; AirCon *ac_;
}; };
template <typename... Ts> template <typename... Ts>
class AirConVLouverTopAction : public Action<Ts...> { class AirConVLouverTopAction : public Action<Ts...>
{
public: public:
explicit AirConVLouverTopAction(AirCon *ac) : ac_(ac) {} explicit AirConVLouverTopAction(AirCon *ac) : ac_(ac) {}
void play(Ts... x) override { this->ac_->setVLouverTopSequence(); } void play(const Ts &...x) override
{
this->ac_->setVLouverTopSequence();
}
protected: protected:
AirCon *ac_; AirCon *ac_;
}; };
template <typename... Ts> template <typename... Ts>
class AirConVLouverMiddleAboveAction : public Action<Ts...> { class AirConVLouverMiddleAboveAction : public Action<Ts...>
{
public: public:
explicit AirConVLouverMiddleAboveAction(AirCon *ac) : ac_(ac) {} explicit AirConVLouverMiddleAboveAction(AirCon *ac) : ac_(ac) {}
void play(Ts... x) override { this->ac_->setVLouverMiddleAboveSequence(); } void play(const Ts &...x) override
{
this->ac_->setVLouverMiddleAboveSequence();
}
protected: protected:
AirCon *ac_; AirCon *ac_;
}; };
template <typename... Ts> template <typename... Ts>
class AirConVLouverMiddleAction : public Action<Ts...> { class AirConVLouverMiddleAction : public Action<Ts...>
{
public: public:
explicit AirConVLouverMiddleAction(AirCon *ac) : ac_(ac) {} explicit AirConVLouverMiddleAction(AirCon *ac) : ac_(ac) {}
void play(Ts... x) override { this->ac_->setVLouverMiddleSequence(); } void play(const Ts &...x) override
{
this->ac_->setVLouverMiddleSequence();
}
protected: protected:
AirCon *ac_; AirCon *ac_;
}; };
template <typename... Ts> template <typename... Ts>
class AirConVLouverMiddleBelowAction : public Action<Ts...> { class AirConVLouverMiddleBelowAction : public Action<Ts...>
{
public: public:
explicit AirConVLouverMiddleBelowAction(AirCon *ac) : ac_(ac) {} explicit AirConVLouverMiddleBelowAction(AirCon *ac) : ac_(ac) {}
void play(Ts... x) override { this->ac_->setVLouverMiddleBelowSequence(); } void play(const Ts &...x) override
{
this->ac_->setVLouverMiddleBelowSequence();
}
protected: protected:
AirCon *ac_; AirCon *ac_;
}; };
template <typename... Ts> template <typename... Ts>
class AirConVLouverBottomAction : public Action<Ts...> { class AirConVLouverBottomAction : public Action<Ts...>
{
public: public:
explicit AirConVLouverBottomAction(AirCon *ac) : ac_(ac) {} explicit AirConVLouverBottomAction(AirCon *ac) : ac_(ac) {}
void play(Ts... x) override { this->ac_->setVLouverBottomSequence(); } void play(const Ts &...x) override
{
this->ac_->setVLouverBottomSequence();
}
protected: protected:
AirCon *ac_; AirCon *ac_;
}; };
template <typename... Ts> template <typename... Ts>
class AirConVLouverSetAction : public Action<Ts...> { class AirConVLouverSetAction : public Action<Ts...>
{
public: public:
AirConVLouverSetAction(AirCon *ac) : ac_(ac) {}
TEMPLATABLE_VALUE(uint8_t, value); TEMPLATABLE_VALUE(uint8_t, value);
void play(Ts... x) { AirConVLouverSetAction(AirCon *ac) : ac_(ac) {};
void play(const Ts &...x) override
{
vlpos_ = this->value_.value(x...); vlpos_ = this->value_.value(x...);
this->ac_->setVLouverFrontendSequence((ac_vlouver_frontend)vlpos_); this->ac_->setVLouverFrontendSequence((ac_vlouver_frontend)vlpos_);
} };
protected: protected:
AirCon *ac_; AirCon *ac_;
uint8_t vlpos_; uint8_t vlpos_;
}; };
// **************************************** SEND TEST PACKET ACTION **************************************** // **************************************** SEND TEST PACKET ACTION ****************************************
template <typename... Ts> template <typename... Ts>
class AirConSendTestPacketAction : public Action<Ts...> { class AirConSendTestPacketAction : public Action<Ts...>
{
public: public:
explicit AirConSendTestPacketAction(AirCon *ac) : ac_(ac) {} explicit AirConSendTestPacketAction(AirCon *ac) : ac_(ac) {}
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) { void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func)
{
this->data_func_ = func; this->data_func_ = func;
this->static_ = false; this->static_ = false;
} }
void set_data_static(const std::vector<uint8_t> &data) { void set_data_static(const std::vector<uint8_t> &data)
{
this->data_static_ = data; this->data_static_ = data;
this->static_ = true; this->static_ = true;
} }
void play(Ts... x) override { void play(const Ts &...x) override
if (this->static_) { {
if (this->static_)
{
this->ac_->sendTestPacket(this->data_static_); this->ac_->sendTestPacket(this->data_static_);
} else { }
else
{
auto val = this->data_func_(x...); auto val = this->data_func_(x...);
this->ac_->sendTestPacket(val); this->ac_->sendTestPacket(val);
} }
@@ -152,27 +200,34 @@ class AirConSendTestPacketAction : public Action<Ts...> {
bool static_{false}; bool static_{false};
std::function<std::vector<uint8_t>(Ts...)> data_func_{}; std::function<std::vector<uint8_t>(Ts...)> data_func_{};
std::vector<uint8_t> data_static_{}; std::vector<uint8_t> data_static_{};
}; };
// **************************************** POWER LIMITATION ACTIONS **************************************** // **************************************** POWER LIMITATION ACTIONS ****************************************
template <typename... Ts> template <typename... Ts>
class AirConPowerLimitationOffAction : public Action<Ts...> { class AirConPowerLimitationOffAction : public Action<Ts...>
{
public: public:
explicit AirConPowerLimitationOffAction(AirCon *ac) : ac_(ac) {} explicit AirConPowerLimitationOffAction(AirCon *ac) : ac_(ac) {}
void play(Ts... x) override { this->ac_->powerLimitationOffSequence(); } void play(const Ts &...x) override
{
this->ac_->powerLimitationOffSequence();
}
protected: protected:
AirCon *ac_; AirCon *ac_;
}; };
template <typename... Ts> template <typename... Ts>
class AirConPowerLimitationOnAction : public Action<Ts...> { class AirConPowerLimitationOnAction : public Action<Ts...>
{
public: public:
AirConPowerLimitationOnAction(AirCon *ac) : ac_(ac) {}
TEMPLATABLE_VALUE(uint8_t, value); TEMPLATABLE_VALUE(uint8_t, value);
void play(Ts... x) { AirConPowerLimitationOnAction(AirCon *ac) : ac_(ac) {};
void play(const Ts &...x) override
{
this->pwr_lim_ = this->value_.value(x...); this->pwr_lim_ = this->value_.value(x...);
this->ac_->powerLimitationOnSequence(this->pwr_lim_); this->ac_->powerLimitationOnSequence(this->pwr_lim_);
} }
@@ -180,7 +235,7 @@ class AirConPowerLimitationOnAction : public Action<Ts...> {
protected: protected:
AirCon *ac_; AirCon *ac_;
uint8_t pwr_lim_; uint8_t pwr_lim_;
}; };
} // namespace aux_ac } // namespace aux_ac
} // namespace esphome } // namespace esphome

View File

@@ -15,6 +15,7 @@
#include "esphome/components/uart/uart.h" #include "esphome/components/uart/uart.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/version.h"
#ifndef USE_ARDUINO #ifndef USE_ARDUINO
using String = std::string; using String = std::string;
@@ -43,6 +44,11 @@ namespace esphome
using climate::ClimatePreset; using climate::ClimatePreset;
using climate::ClimateSwingMode; using climate::ClimateSwingMode;
using climate::ClimateTraits; using climate::ClimateTraits;
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
using climate::ClimateModeMask;
using climate::ClimateSwingModeMask;
using climate::ClimatePresetMask;
#endif
//**************************************************************************************************************************************************** //****************************************************************************************************************************************************
//**************************************************** Packet logger configuration ******************************************************************* //**************************************************** Packet logger configuration *******************************************************************
@@ -852,12 +858,19 @@ namespace esphome
// как "в простое" (IDLE) // как "в простое" (IDLE)
bool _is_inverter = false; bool _is_inverter = false;
// поддерживаемые кондиционером опции #if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
ClimateModeMask _supported_modes{};
ClimateSwingModeMask _supported_swing_modes{};
ClimatePresetMask _supported_presets{};
std::vector<const char *> _supported_custom_fan_modes{};
std::vector<const char *> _supported_custom_presets{};
#else
std::set<ClimateMode> _supported_modes{}; std::set<ClimateMode> _supported_modes{};
std::set<ClimateSwingMode> _supported_swing_modes{}; std::set<ClimateSwingMode> _supported_swing_modes{};
std::set<ClimatePreset> _supported_presets{}; std::set<ClimatePreset> _supported_presets{};
std::set<std::string> _supported_custom_presets{}; std::set<std::string> _supported_custom_presets{};
std::set<std::string> _supported_custom_fan_modes{}; std::set<std::string> _supported_custom_fan_modes{};
#endif
// The capabilities of the climate device // The capabilities of the climate device
// Шаблон параметров отображения виджета // Шаблон параметров отображения виджета
@@ -2391,11 +2404,16 @@ namespace esphome
// первоначальная инициализация // первоначальная инициализация
this->preset = climate::CLIMATE_PRESET_NONE; this->preset = climate::CLIMATE_PRESET_NONE;
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
this->clear_custom_preset_();
this->clear_custom_fan_mode_();
#else
this->custom_preset = (std::string) ""; this->custom_preset = (std::string) "";
this->custom_fan_mode = (std::string) "";
#endif
this->mode = climate::CLIMATE_MODE_OFF; this->mode = climate::CLIMATE_MODE_OFF;
this->action = climate::CLIMATE_ACTION_IDLE; this->action = climate::CLIMATE_ACTION_IDLE;
this->fan_mode = climate::CLIMATE_FAN_LOW; this->fan_mode = climate::CLIMATE_FAN_LOW;
this->custom_fan_mode = (std::string) "";
}; };
float get_setup_priority() const override { return esphome::setup_priority::DATA; } float get_setup_priority() const override { return esphome::setup_priority::DATA; }
@@ -2416,6 +2434,22 @@ namespace esphome
bool get_hw_initialized() { return _hw_initialized; }; bool get_hw_initialized() { return _hw_initialized; };
bool get_has_connection() { return _has_connection; }; bool get_has_connection() { return _has_connection; };
// --- Helper functions for consistent louver interpretation ---
// Some AUX-based models use 0x20 for "horizontal off", while others (e.g., ROVEX, Royal Clima) use 0xE0.
// These helpers normalize those differences so swing detection stays consistent.
// Keeping both encodings here replaces the old workaround that caused HA to jump back to OFF
// when horizontal was swinging and vertical was fixed.
static inline bool is_h_off(uint8_t h) {
return (h == AC_LOUVERH_OFF_AUX) || (h == AC_LOUVERH_OFF_ALTERNATIVE);
}
static inline bool is_h_swing(uint8_t h) {
return (h == AC_LOUVERH_SWING_LEFTRIGHT);
}
static inline bool is_v_swing(uint8_t v) {
return (v == AC_LOUVERV_SWING_UPDOWN);
}
// возвращает, есть ли елементы в последовательности команд // возвращает, есть ли елементы в последовательности команд
bool hasSequence() bool hasSequence()
{ {
@@ -2619,15 +2653,24 @@ namespace esphome
switch (_current_ac_state.fanTurbo) switch (_current_ac_state.fanTurbo)
{ {
case AC_FANTURBO_ON: case AC_FANTURBO_ON:
// if ((_current_ac_state.mode == AC_MODE_HEAT) || (_current_ac_state.mode == AC_MODE_COOL)) { #if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
this->set_custom_fan_mode_(Constants::TURBO.c_str());
#else
this->custom_fan_mode = Constants::TURBO; this->custom_fan_mode = Constants::TURBO;
//} #endif
break; break;
case AC_FANTURBO_OFF: case AC_FANTURBO_OFF:
default: default:
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
if (this->has_custom_fan_mode()) {
if (strcmp(this->get_custom_fan_mode(), Constants::TURBO.c_str()) == 0)
this->clear_custom_fan_mode_();
}
#else
if (this->custom_fan_mode == Constants::TURBO) if (this->custom_fan_mode == Constants::TURBO)
this->custom_fan_mode = (std::string) ""; this->custom_fan_mode = (std::string) "";
#endif
break; break;
} }
@@ -2639,15 +2682,24 @@ namespace esphome
switch (_current_ac_state.fanMute) switch (_current_ac_state.fanMute)
{ {
case AC_FANMUTE_ON: case AC_FANMUTE_ON:
// if (_current_ac_state.mode == AC_MODE_FAN) { #if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
this->set_custom_fan_mode_(Constants::MUTE.c_str());
#else
this->custom_fan_mode = Constants::MUTE; this->custom_fan_mode = Constants::MUTE;
//} #endif
break; break;
case AC_FANMUTE_OFF: case AC_FANMUTE_OFF:
default: default:
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
if (this->has_custom_fan_mode()) {
if (strcmp(this->get_custom_fan_mode(), Constants::MUTE.c_str()) == 0)
this->clear_custom_fan_mode_();
}
#else
if (this->custom_fan_mode == Constants::MUTE) if (this->custom_fan_mode == Constants::MUTE)
this->custom_fan_mode = (std::string) ""; this->custom_fan_mode = (std::string) "";
#endif
break; break;
} }
@@ -2659,14 +2711,25 @@ namespace esphome
if (_current_ac_state.health == AC_HEALTH_ON && if (_current_ac_state.health == AC_HEALTH_ON &&
_current_ac_state.power == AC_POWER_ON) _current_ac_state.power == AC_POWER_ON)
{ {
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
this->set_custom_preset_(Constants::HEALTH.c_str());
#else
this->custom_preset = Constants::HEALTH; this->custom_preset = Constants::HEALTH;
#endif
} }
else if (this->custom_preset == Constants::HEALTH)
{
// AC_HEALTH_OFF // AC_HEALTH_OFF
// только в том случае, если до этого пресет был установлен // только в том случае, если до этого пресет был установлен
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
else if (this->has_custom_preset() && strcmp(this->get_custom_preset(), Constants::HEALTH.c_str()) == 0)
{
this->clear_custom_preset_();
}
#else
else if (this->custom_preset == Constants::HEALTH)
{
this->custom_preset = (std::string) ""; this->custom_preset = (std::string) "";
} }
#endif
_debugMsg(F("Climate HEALTH preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.health); _debugMsg(F("Climate HEALTH preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.health);
@@ -2694,14 +2757,25 @@ namespace esphome
if (_current_ac_state.clean == AC_CLEAN_ON && if (_current_ac_state.clean == AC_CLEAN_ON &&
_current_ac_state.power == AC_POWER_OFF) _current_ac_state.power == AC_POWER_OFF)
{ {
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
this->set_custom_preset_(Constants::CLEAN.c_str());
#else
this->custom_preset = Constants::CLEAN; this->custom_preset = Constants::CLEAN;
#endif
} }
else if (this->custom_preset == Constants::CLEAN)
{
// AC_CLEAN_OFF // AC_CLEAN_OFF
// только в том случае, если до этого пресет был установлен // только в том случае, если до этого пресет был установлен
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
else if (this->has_custom_preset() && strcmp(this->get_custom_preset(), Constants::CLEAN.c_str()) == 0)
{
this->clear_custom_preset_();
}
#else
else if (this->custom_preset == Constants::CLEAN)
{
this->custom_preset = (std::string) ""; this->custom_preset = (std::string) "";
} }
#endif
_debugMsg(F("Climate CLEAN preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.clean); _debugMsg(F("Climate CLEAN preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.clean);
@@ -2724,43 +2798,50 @@ namespace esphome
switch (_current_ac_state.mildew) switch (_current_ac_state.mildew)
{ {
case AC_MILDEW_ON: case AC_MILDEW_ON:
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
this->set_custom_preset_(Constants::ANTIFUNGUS.c_str());
#else
this->custom_preset = Constants::ANTIFUNGUS; this->custom_preset = Constants::ANTIFUNGUS;
#endif
break; break;
case AC_MILDEW_OFF: case AC_MILDEW_OFF:
default: default:
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
if (this->has_custom_preset() && strcmp(this->get_custom_preset(), Constants::ANTIFUNGUS.c_str()) == 0)
{
this->clear_custom_preset_();
}
#else
if (this->custom_preset == Constants::ANTIFUNGUS) if (this->custom_preset == Constants::ANTIFUNGUS)
this->custom_preset = (std::string) ""; this->custom_preset = (std::string) "";
break; break;
#endif
} }
_debugMsg(F("Climate ANTIFUNGUS preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.mildew); _debugMsg(F("Climate ANTIFUNGUS preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.mildew);
/*************************** LOUVERs ***************************/ /*************************** LOUVERs ***************************/
this->swing_mode = climate::CLIMATE_SWING_OFF; this->swing_mode = climate::CLIMATE_SWING_OFF;
if (_current_ac_state.power == AC_POWER_ON)
{ if (_current_ac_state.power == AC_POWER_ON) {
if (_current_ac_state.louver.louver_h == AC_LOUVERH_SWING_LEFTRIGHT && _current_ac_state.louver.louver_v == AC_LOUVERV_OFF) const uint8_t h = _current_ac_state.louver.louver_h;
{ const uint8_t v = _current_ac_state.louver.louver_v;
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
} const bool hSwing = is_h_swing(h);
else if (_current_ac_state.louver.louver_h == AC_LOUVERH_OFF_AUX && _current_ac_state.louver.louver_v == AC_LOUVERV_SWING_UPDOWN) const bool hOff = is_h_off(h);
{ const bool vSwing = is_v_swing(v);
// TODO: КОСТЫЛЬ!
this->swing_mode = climate::CLIMATE_SWING_VERTICAL; if (hSwing && vSwing) {
}
else if (_current_ac_state.louver.louver_h == AC_LOUVERH_OFF_ALTERNATIVE && _current_ac_state.louver.louver_v == AC_LOUVERV_SWING_UPDOWN)
{
// TODO: КОСТЫЛЬ!
// временно сделал так. Сделать нормально - это надо подумать.
// На AUX и многих других марках выключенный режим горизонтальных жалюзи равен 0x20, а на ROVEX и Royal Clima 0xE0
// Из-за этого происходил сброс на OFF во фронтенде Home Assistant. Пришлось городить это.
// Надо как-то изящнее решить эту историю
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
}
else if (_current_ac_state.louver.louver_h == AC_LOUVERH_SWING_LEFTRIGHT && _current_ac_state.louver.louver_v == AC_LOUVERV_SWING_UPDOWN)
{
this->swing_mode = climate::CLIMATE_SWING_BOTH; this->swing_mode = climate::CLIMATE_SWING_BOTH;
} else if (hSwing) {
// Horizontal swings even if vertical is fixed to a position
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
} else if (vSwing && hOff) {
// Vertical swings while horizontal is not swinging
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
} else {
this->swing_mode = climate::CLIMATE_SWING_OFF;
} }
} }
@@ -2821,10 +2902,17 @@ namespace esphome
{ {
state_str += "SLEEP"; state_str += "SLEEP";
} }
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
else if (this->has_custom_preset())
{
state_str += this->get_custom_preset();
}
#else
else if (this->custom_preset.has_value() && this->custom_preset.value().length() > 0) else if (this->custom_preset.has_value() && this->custom_preset.value().length() > 0)
{ {
state_str += this->custom_preset.value().c_str(); state_str += this->custom_preset.value().c_str();
} }
#endif
else else
{ {
state_str += "NONE"; state_str += "NONE";
@@ -3014,6 +3102,32 @@ namespace esphome
break; break;
} }
} }
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
else if (call.has_custom_fan_mode())
{
const char *customfanmode = call.get_custom_fan_mode();
if (strcmp(customfanmode, Constants::TURBO.c_str()) == 0)
{
// TURBO fan mode is suitable in COOL and HEAT modes.
// Other modes don't accept TURBO fan mode.
hasCommand = true;
cmd.fanTurbo = AC_FANTURBO_ON;
cmd.fanMute = AC_FANMUTE_OFF;
this->set_custom_fan_mode_(customfanmode);
}
else if (strcmp(customfanmode, Constants::MUTE.c_str()) == 0)
{
// MUTE fan mode is suitable in FAN mode only for Rovex air conditioner.
// In COOL mode AC receives command without any changes.
// May be other AUX-based air conditioners do the same.
hasCommand = true;
cmd.fanMute = AC_FANMUTE_ON;
cmd.fanTurbo = AC_FANTURBO_OFF;
this->set_custom_fan_mode_(customfanmode);
}
}
#else
else if (call.get_custom_fan_mode().has_value()) else if (call.get_custom_fan_mode().has_value())
{ {
std::string customfanmode = *call.get_custom_fan_mode(); std::string customfanmode = *call.get_custom_fan_mode();
@@ -3055,6 +3169,7 @@ namespace esphome
//} //}
} }
} }
#endif
// Пользователь выбрал пресет // Пользователь выбрал пресет
if (call.get_preset().has_value()) if (call.get_preset().has_value())
@@ -3105,6 +3220,80 @@ namespace esphome
break; break;
} }
} }
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
else if (call.has_custom_preset())
{
const char *custom_preset = call.get_custom_preset();
if (strcmp(custom_preset, Constants::CLEAN.c_str()) == 0)
{
// режим очистки кондиционера, включается (или должен включаться) при AC_POWER_OFF
// TODO: надо отдебажить выключение этого режима
if (cmd.power == AC_POWER_OFF or _current_ac_state.power == AC_POWER_OFF)
{
hasCommand = true;
cmd.clean = AC_CLEAN_ON;
cmd.mildew = AC_MILDEW_OFF;
this->set_custom_preset_(custom_preset);
}
else
{
_debugMsg(F("CLEAN preset is suitable in POWER_OFF mode only."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
}
}
else if (strcmp(custom_preset, Constants::HEALTH.c_str()) == 0)
{
if (cmd.power == AC_POWER_ON ||
_current_ac_state.power == AC_POWER_ON)
{
hasCommand = true;
cmd.health = AC_HEALTH_ON;
cmd.fanTurbo = AC_FANTURBO_OFF;
cmd.fanMute = AC_FANMUTE_OFF;
cmd.sleep = AC_SLEEP_OFF;
if (cmd.mode == AC_MODE_COOL ||
cmd.mode == AC_MODE_HEAT ||
cmd.mode == AC_MODE_AUTO ||
_current_ac_state.mode == AC_MODE_COOL ||
_current_ac_state.mode == AC_MODE_HEAT ||
_current_ac_state.mode == AC_MODE_AUTO)
{
cmd.fanSpeed = AC_FANSPEED_AUTO; // зависимость от health
}
else if (cmd.mode == AC_MODE_FAN ||
_current_ac_state.mode == AC_MODE_FAN)
{
cmd.fanSpeed = AC_FANSPEED_MEDIUM; // зависимость от health
}
this->set_custom_preset_(custom_preset);
}
else
{
_debugMsg(F("HEALTH preset is suitable in POWER_ON mode only."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
}
}
else if (strcmp(custom_preset, Constants::ANTIFUNGUS.c_str()) == 0)
{
// включение-выключение функции "Антиплесень".
// По факту: после выключения сплита он оставляет минут на 5 открытые жалюзи и глушит вентилятор.
// Уличный блок при этом гудит и тарахтит. Возможно, прогревается теплообменник для высыхания.
// Через некоторое время внешний блок замолкает и сплит закрывает жалюзи.
// Brokly:
// включение-выключение функции "Антиплесень".
// у меня пульт отправляет 5 посылок и на включение и на выключение, но реагирует на эту кнопку
// только в режиме POWER_OFF
// TODO: надо уточнить, в каких режимах штатно включается этот режим у кондиционера
cmd.mildew = AC_MILDEW_ON;
cmd.clean = AC_CLEAN_OFF; // для логики пресетов
hasCommand = true;
this->set_custom_preset_(custom_preset);
}
}
#else
else if (call.get_custom_preset().has_value()) else if (call.get_custom_preset().has_value())
{ {
std::string custom_preset = *call.get_custom_preset(); std::string custom_preset = *call.get_custom_preset();
@@ -3178,6 +3367,7 @@ namespace esphome
this->custom_preset = custom_preset; this->custom_preset = custom_preset;
} }
} }
#endif
// User requested swing_mode change // User requested swing_mode change
if (call.get_swing_mode().has_value()) if (call.get_swing_mode().has_value())
@@ -3191,8 +3381,16 @@ namespace esphome
// But the ROVEX IR-remote does not provide this features. Therefore this features haven't been tested. // But the ROVEX IR-remote does not provide this features. Therefore this features haven't been tested.
// May be suitable for other models of AUX-based ACs. // May be suitable for other models of AUX-based ACs.
case climate::CLIMATE_SWING_OFF: case climate::CLIMATE_SWING_OFF:
// Stop BOTH axes, but don't disturb vertical if it was already fixed (2..6)
cmd.louver.louver_h = AC_LOUVERH_OFF_ALTERNATIVE; cmd.louver.louver_h = AC_LOUVERH_OFF_ALTERNATIVE;
if (_current_ac_state.louver.louver_v == AC_LOUVERV_SWING_UPDOWN) {
// If vertical was swinging, stop it.
cmd.louver.louver_v = AC_LOUVERV_OFF; cmd.louver.louver_v = AC_LOUVERV_OFF;
} else {
// Keep existing fixed position (2..6).
cmd.louver.louver_v = _current_ac_state.louver.louver_v;
}
hasCommand = true; hasCommand = true;
this->swing_mode = swingmode; this->swing_mode = swingmode;
break; break;
@@ -3213,7 +3411,12 @@ namespace esphome
case climate::CLIMATE_SWING_HORIZONTAL: case climate::CLIMATE_SWING_HORIZONTAL:
cmd.louver.louver_h = AC_LOUVERH_SWING_LEFTRIGHT; cmd.louver.louver_h = AC_LOUVERH_SWING_LEFTRIGHT;
// Stop vertical only if it was swinging; otherwise preserve prior fixed position
if (_current_ac_state.louver.louver_v == AC_LOUVERV_SWING_UPDOWN) {
cmd.louver.louver_v = AC_LOUVERV_OFF; cmd.louver.louver_v = AC_LOUVERV_OFF;
} else {
cmd.louver.louver_v = _current_ac_state.louver.louver_v;
}
hasCommand = true; hasCommand = true;
this->swing_mode = swingmode; this->swing_mode = swingmode;
break; break;
@@ -3781,7 +3984,13 @@ namespace esphome
void set_optimistic(bool optimistic) { this->_optimistic = optimistic; } void set_optimistic(bool optimistic) { this->_optimistic = optimistic; }
bool get_optimistic() { return this->_optimistic; } bool get_optimistic() { return this->_optimistic; }
// возможно функции get и не нужны, но вроде как должны быть #if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
void set_supported_modes(ClimateModeMask modes) { this->_supported_modes = modes; }
void set_supported_swing_modes(ClimateSwingModeMask modes) { this->_supported_swing_modes = modes; }
void set_supported_presets(ClimatePresetMask presets) { this->_supported_presets = presets; }
void set_custom_presets(std::initializer_list<const char *> presets) { this->_supported_custom_presets = presets; }
void set_custom_fan_modes(std::initializer_list<const char *> modes) { this->_supported_custom_fan_modes = modes; }
#else
void set_supported_modes(const std::set<ClimateMode> &modes) { this->_supported_modes = modes; } void set_supported_modes(const std::set<ClimateMode> &modes) { this->_supported_modes = modes; }
std::set<ClimateMode> get_supported_modes() { return this->_supported_modes; } std::set<ClimateMode> get_supported_modes() { return this->_supported_modes; }
@@ -3796,6 +4005,7 @@ namespace esphome
void set_custom_fan_modes(const std::set<std::string> &modes) { this->_supported_custom_fan_modes = modes; } void set_custom_fan_modes(const std::set<std::string> &modes) { this->_supported_custom_fan_modes = modes; }
const std::set<std::string> &get_supported_custom_fan_modes() { return this->_supported_custom_fan_modes; } const std::set<std::string> &get_supported_custom_fan_modes() { return this->_supported_custom_fan_modes; }
#endif
#if defined(PRESETS_SAVING) #if defined(PRESETS_SAVING)
void set_store_settings(bool store_settings) { this->_store_settings = store_settings; } void set_store_settings(bool store_settings) { this->_store_settings = store_settings; }
@@ -3813,8 +4023,13 @@ namespace esphome
// заполнение шаблона параметров отображения виджета // заполнение шаблона параметров отображения виджета
// GK: всё же похоже правильнее это делать тут, а не в initAC() // GK: всё же похоже правильнее это делать тут, а не в initAC()
// initAC() в формируемом питоном коде вызывается до вызова aux_ac.set_supported_***() с установленными пользователем в конфиге параметрами // initAC() в формируемом питоном коде вызывается до вызова aux_ac.set_supported_***() с установленными пользователем в конфиге параметрами
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
_traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
// NOT setting CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE - this device uses single target temperature
#else
_traits.set_supports_current_temperature(true); _traits.set_supports_current_temperature(true);
_traits.set_supports_two_point_target_temperature(false); // if the climate device's target temperature should be split in target_temperature_low and target_temperature_high instead of just the single target_temperature _traits.set_supports_two_point_target_temperature(false); // if the climate device's target temperature should be split in target_temperature_low and target_temperature_high instead of just the single target_temperature
#endif
_traits.set_supported_modes(this->_supported_modes); _traits.set_supported_modes(this->_supported_modes);
_traits.set_supported_swing_modes(this->_supported_swing_modes); _traits.set_supported_swing_modes(this->_supported_swing_modes);
@@ -3843,7 +4058,14 @@ namespace esphome
//_traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP); //_traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP);
// if the climate device supports reporting the active current action of the device with the action property. // if the climate device supports reporting the active current action of the device with the action property.
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
if (this->_show_action)
{
_traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
}
#else
_traits.set_supports_action(this->_show_action); _traits.set_supports_action(this->_show_action);
#endif
}; };
void loop() override void loop() override

View File

@@ -26,6 +26,7 @@ from esphome.const import (
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_POWER_FACTOR,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
__version__
) )
from esphome.components.climate import ( from esphome.components.climate import (
ClimateMode, ClimateMode,
@@ -33,7 +34,7 @@ from esphome.components.climate import (
ClimateSwingMode, ClimateSwingMode,
) )
AUX_AC_FIRMWARE_VERSION = '0.2.17' AUX_AC_FIRMWARE_VERSION = '0.3.1'
AC_PACKET_TIMEOUT_MIN = 150 AC_PACKET_TIMEOUT_MIN = 150
AC_PACKET_TIMEOUT_MAX = 600 AC_PACKET_TIMEOUT_MAX = 600
AC_POWER_LIMIT_MIN = 30 AC_POWER_LIMIT_MIN = 30
@@ -127,6 +128,12 @@ AirConPowerLimitationOnAction = aux_ac_ns.class_(
) )
def use_new_api():
esphome_current_version = tuple(map(int, __version__.split('.')))
esphome_bc_version = tuple(map(int, "2025.11.0".split('.')))
return esphome_current_version >= esphome_bc_version
def validate_packet_timeout(value): def validate_packet_timeout(value):
minV = AC_PACKET_TIMEOUT_MIN minV = AC_PACKET_TIMEOUT_MIN
maxV = AC_PACKET_TIMEOUT_MAX maxV = AC_PACKET_TIMEOUT_MAX
@@ -426,6 +433,16 @@ async def to_code(config):
cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES]))
if CONF_SUPPORTED_PRESETS in config: if CONF_SUPPORTED_PRESETS in config:
cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS]))
if use_new_api():
if CONF_CUSTOM_PRESETS in config:
presets = config[CONF_CUSTOM_PRESETS]
c_str_presets = [cg.RawExpression(f"aux_ac::Constants::{p}.c_str()") for p in presets]
cg.add(var.set_custom_presets(c_str_presets))
if CONF_CUSTOM_FAN_MODES in config:
fan_modes = config[CONF_CUSTOM_FAN_MODES]
c_str_fan_modes = [cg.RawExpression(f"aux_ac::Constants::{p}.c_str()") for p in fan_modes]
cg.add(var.set_custom_fan_modes(c_str_fan_modes))
else:
if CONF_CUSTOM_PRESETS in config: if CONF_CUSTOM_PRESETS in config:
cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS])) cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS]))
if CONF_CUSTOM_FAN_MODES in config: if CONF_CUSTOM_FAN_MODES in config: