@@ -15,6 +15,7 @@
# include "esphome/components/uart/uart.h"
# include "esphome/core/component.h"
# include "esphome/core/helpers.h"
# include "esphome/core/version.h"
# ifndef USE_ARDUINO
using String = std : : string ;
@@ -43,6 +44,11 @@ namespace esphome
using climate : : ClimatePreset ;
using climate : : ClimateSwingMode ;
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 *******************************************************************
@@ -852,12 +858,19 @@ namespace esphome
// как "в простое" (IDLE)
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 < ClimateSwingMode > _supported_swing_modes { } ;
std : : set < ClimatePreset > _supported_presets { } ;
std : : set < std : : string > _supported_custom_presets { } ;
std : : set < std : : string > _supported_custom_fan_modes { } ;
# endif
// The capabilities of the climate device
// Шаблон параметров отображения виджета
@@ -2391,11 +2404,16 @@ namespace esphome
// первоначальная инициализация
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_fan_mode = ( std : : string ) " " ;
# endif
this - > mode = climate : : CLIMATE_MODE_OFF ;
this - > action = climate : : CLIMATE_ACTION_IDLE ;
this - > fan_mode = climate : : CLIMATE_FAN_LOW ;
this - > custom_fan_mode = ( std : : string ) " " ;
} ;
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_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 ( )
{
@@ -2619,15 +2653,24 @@ namespace esphome
switch ( _current_ac_state . fanTurbo )
{
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 ;
//}
# endif
break ;
case AC_FANTURBO_OFF :
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 )
this - > custom_fan_mode = ( std : : string ) " " ;
# endif
break ;
}
@@ -2639,15 +2682,24 @@ namespace esphome
switch ( _current_ac_state . fanMute )
{
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 ;
//}
# endif
break ;
case AC_FANMUTE_OFF :
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 )
this - > custom_fan_mode = ( std : : string ) " " ;
# endif
break ;
}
@@ -2659,14 +2711,25 @@ namespace esphome
if ( _current_ac_state . health = = AC_HEALTH_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 ;
# 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 )
{
// AC_HEALTH_OFF
// только в том случае, если до этого пресет был установлен
this - > custom_preset = ( std : : string ) " " ;
}
# endif
_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 & &
_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 ;
# 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 )
{
// AC_CLEAN_OFF
// только в том случае, если до этого пресет был установлен
this - > custom_preset = ( std : : string ) " " ;
}
# endif
_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 )
{
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 ;
# endif
break ;
case AC_MILDEW_OFF :
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 )
this - > custom_preset = ( std : : string ) " " ;
break ;
# endif
}
_debugMsg ( F ( " Climate ANTIFUNGUS preset: %i " ) , ESPHOME_LOG_LEVEL_VERBOSE , __LINE__ , _current_ac_state . mildew ) ;
/*************************** LOUVERs ***************************/
this - > swing_mode = climate : : CLIMATE_SWING_OFF ;
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 )
{
this - > swing_mode = climate : : CLIMATE_SWING_HORIZONTAL ;
}
else if ( _current_ac_state . louver . louver_h = = AC_LOUVERH_OFF_AUX & & _current_ac_state . louver . louver_v = = AC_LOUVERV_SWING_UPDOWN )
{
// TODO: КОСТЫЛЬ!
this - > swing_mode = climate : : CLIMATE_SWING_VERTICAL ;
}
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 )
{
if ( _current_ac_state . power = = AC_POWER_ON ) {
const uint8_t h = _current_ac_state . louver . louver_h ;
const uint8_t v = _current_ac_state . louver . louver_v ;
const bool hSwing = is_h_swing ( h ) ;
const bool hOff = is_h_off ( h ) ;
const bool vSwing = is_v_swing ( v ) ;
if ( hSwing & & vSwing ) {
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 " ;
}
# 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 )
{
state_str + = this - > custom_preset . value ( ) . c_str ( ) ;
}
# endif
else
{
state_str + = " NONE " ;
@@ -3014,6 +3102,32 @@ namespace esphome
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 ( ) )
{
std : : string customfanmode = * call . get_custom_fan_mode ( ) ;
@@ -3055,6 +3169,7 @@ namespace esphome
//}
}
}
# endif
// Пользователь выбрал пресет
if ( call . get_preset ( ) . has_value ( ) )
@@ -3105,6 +3220,80 @@ namespace esphome
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 ( ) )
{
std : : string custom_preset = * call . get_custom_preset ( ) ;
@@ -3178,6 +3367,7 @@ namespace esphome
this - > custom_preset = custom_preset ;
}
}
# endif
// User requested swing_mode change
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.
// May be suitable for other models of AUX-based ACs.
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_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 ;
this - > swing_mode = swingmode ;
break ;
@@ -3213,7 +3411,12 @@ namespace esphome
case climate : : CLIMATE_SWING_HORIZONTAL :
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 ;
this - > swing_mode = swingmode ;
break ;
@@ -3781,7 +3984,13 @@ namespace esphome
void set_optimistic ( bool optimistic ) { this - > _optimistic = 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 ; }
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 ; }
const std : : set < std : : string > & get_supported_custom_fan_modes ( ) { return this - > _supported_custom_fan_modes ; }
# endif
# if defined(PRESETS_SAVING)
void set_store_settings ( bool store_settings ) { this - > _store_settings = store_settings ; }
@@ -3813,8 +4023,13 @@ namespace esphome
// заполнение шаблона параметров отображения виджета
// GK: всё же похоже правильнее это делать тут, а не в initAC()
// 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_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_swing_modes ( this - > _supported_swing_modes ) ;
@@ -3843,7 +4058,14 @@ namespace esphome
//_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 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 ) ;
# endif
} ;
void loop ( ) override
@@ -3899,4 +4121,4 @@ namespace esphome
} ;
} // namespace aux_ac
} // namespace esphome
} // namespace esphome