7 Commits

Author SHA1 Message Date
GrKoR
efa0991dd0 fix: automations play() method fixed 2025-11-27 17:57:13 -08:00
GrKoR
2e0c421c9f fix: removed module dependency" 2025-11-26 23:28:20 -08:00
GrKoR
53414d8ab4 fix: python code generator fix for custom presets & custom fan modes 2025-11-26 21:14:46 -08:00
GrKoR
5a165d3a3d upd: version++ 2025-11-26 18:58:36 -08:00
GK
02887baa04 Merge pull request #187 from nonitex/master
Preserve swing mode and vertical vane position when toggling the othe…
2025-11-26 18:53:38 -08:00
GrKoR
5f9d2c0c0f fix: esphome 2025.11.0 breacing changes 2025-11-26 17:38:20 -08:00
nonitex
7a4bacfb9a Preserve swing mode and vertical vane position when toggling the other axis
Added helper functions to normalize louver states for consistent swing detection across different AUX models. Updated swing mode logic to utilize these helpers for improved clarity and functionality.
2025-10-20 03:52:29 +02:00
3 changed files with 475 additions and 181 deletions

View File

@@ -4,183 +4,238 @@
#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: {
explicit AirConDisplayOffAction(AirCon *ac) : ac_(ac) {} public:
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: {
explicit AirConDisplayOnAction(AirCon *ac) : ac_(ac) {} public:
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: {
explicit AirConVLouverSwingAction(AirCon *ac) : ac_(ac) {} public:
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: {
explicit AirConVLouverStopAction(AirCon *ac) : ac_(ac) {} public:
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: {
explicit AirConVLouverTopAction(AirCon *ac) : ac_(ac) {} public:
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: {
explicit AirConVLouverMiddleAboveAction(AirCon *ac) : ac_(ac) {} public:
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: {
explicit AirConVLouverMiddleAction(AirCon *ac) : ac_(ac) {} public:
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: {
explicit AirConVLouverMiddleBelowAction(AirCon *ac) : ac_(ac) {} public:
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: {
explicit AirConVLouverBottomAction(AirCon *ac) : ac_(ac) {} public:
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: {
AirConVLouverSetAction(AirCon *ac) : ac_(ac) {} public:
TEMPLATABLE_VALUE(uint8_t, value); TEMPLATABLE_VALUE(uint8_t, value);
void play(Ts... x) { AirConVLouverSetAction(AirCon *ac) : ac_(ac) {};
vlpos_ = this->value_.value(x...);
this->ac_->setVLouverFrontendSequence((ac_vlouver_frontend)vlpos_);
}
protected: void play(const Ts &...x) override
AirCon *ac_; {
uint8_t vlpos_; vlpos_ = this->value_.value(x...);
}; this->ac_->setVLouverFrontendSequence((ac_vlouver_frontend)vlpos_);
};
// **************************************** SEND TEST PACKET ACTION **************************************** protected:
template <typename... Ts> AirCon *ac_;
class AirConSendTestPacketAction : public Action<Ts...> { uint8_t vlpos_;
public: };
explicit AirConSendTestPacketAction(AirCon *ac) : ac_(ac) {}
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) {
this->data_func_ = func;
this->static_ = false;
}
void set_data_static(const std::vector<uint8_t> &data) {
this->data_static_ = data;
this->static_ = true;
}
void play(Ts... x) override { // **************************************** SEND TEST PACKET ACTION ****************************************
if (this->static_) { template <typename... Ts>
this->ac_->sendTestPacket(this->data_static_); class AirConSendTestPacketAction : public Action<Ts...>
} else { {
auto val = this->data_func_(x...); public:
this->ac_->sendTestPacket(val); explicit AirConSendTestPacketAction(AirCon *ac) : ac_(ac) {}
} void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func)
} {
this->data_func_ = func;
this->static_ = false;
}
void set_data_static(const std::vector<uint8_t> &data)
{
this->data_static_ = data;
this->static_ = true;
}
protected: void play(const Ts &...x) override
AirCon *ac_; {
bool static_{false}; if (this->static_)
std::function<std::vector<uint8_t>(Ts...)> data_func_{}; {
std::vector<uint8_t> data_static_{}; this->ac_->sendTestPacket(this->data_static_);
}; }
else
{
auto val = this->data_func_(x...);
this->ac_->sendTestPacket(val);
}
}
// **************************************** POWER LIMITATION ACTIONS **************************************** protected:
template <typename... Ts> AirCon *ac_;
class AirConPowerLimitationOffAction : public Action<Ts...> { bool static_{false};
public: std::function<std::vector<uint8_t>(Ts...)> data_func_{};
explicit AirConPowerLimitationOffAction(AirCon *ac) : ac_(ac) {} std::vector<uint8_t> data_static_{};
};
void play(Ts... x) override { this->ac_->powerLimitationOffSequence(); } // **************************************** POWER LIMITATION ACTIONS ****************************************
template <typename... Ts>
class AirConPowerLimitationOffAction : public Action<Ts...>
{
public:
explicit AirConPowerLimitationOffAction(AirCon *ac) : ac_(ac) {}
protected: void play(const Ts &...x) override
AirCon *ac_; {
}; this->ac_->powerLimitationOffSequence();
}
template <typename... Ts> protected:
class AirConPowerLimitationOnAction : public Action<Ts...> { AirCon *ac_;
public: };
AirConPowerLimitationOnAction(AirCon *ac) : ac_(ac) {}
TEMPLATABLE_VALUE(uint8_t, value);
void play(Ts... x) { template <typename... Ts>
this->pwr_lim_ = this->value_.value(x...); class AirConPowerLimitationOnAction : public Action<Ts...>
this->ac_->powerLimitationOnSequence(this->pwr_lim_); {
} public:
TEMPLATABLE_VALUE(uint8_t, value);
protected: AirConPowerLimitationOnAction(AirCon *ac) : ac_(ac) {};
AirCon *ac_;
uint8_t pwr_lim_;
};
} // namespace aux_ac void play(const Ts &...x) override
} // namespace esphome {
this->pwr_lim_ = this->value_.value(x...);
this->ac_->powerLimitationOnSequence(this->pwr_lim_);
}
protected:
AirCon *ac_;
uint8_t pwr_lim_;
};
} // namespace aux_ac
} // 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
} }
// 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) else if (this->custom_preset == Constants::HEALTH)
{ {
// AC_HEALTH_OFF
// только в том случае, если до этого пресет был установлен
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
} }
// 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) else if (this->custom_preset == Constants::CLEAN)
{ {
// AC_CLEAN_OFF
// только в том случае, если до этого пресет был установлен
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;
cmd.louver.louver_v = AC_LOUVERV_OFF; if (_current_ac_state.louver.louver_v == AC_LOUVERV_SWING_UPDOWN) {
// If vertical was swinging, stop it.
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;
cmd.louver.louver_v = AC_LOUVERV_OFF; // 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;
} 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);
_traits.add_feature_flags(climate::CLIMATE_REQUIRES_TWO_POINT_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
@@ -3899,4 +4121,4 @@ namespace esphome
}; };
} // namespace aux_ac } // namespace aux_ac
} // namespace esphome } // namespace esphome

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,10 +433,20 @@ 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 CONF_CUSTOM_PRESETS in config: if use_new_api():
cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS])) if CONF_CUSTOM_PRESETS in config:
if CONF_CUSTOM_FAN_MODES in config: presets = config[CONF_CUSTOM_PRESETS]
cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES])) 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:
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]))
DISPLAY_ACTION_SCHEMA = maybe_simple_id( DISPLAY_ACTION_SCHEMA = maybe_simple_id(