4 Commits

Author SHA1 Message Date
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
2 changed files with 259 additions and 37 deletions

View File

@@ -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

View File

@@ -33,7 +33,7 @@ from esphome.components.climate import (
ClimateSwingMode,
)
AUX_AC_FIRMWARE_VERSION = '0.2.17'
AUX_AC_FIRMWARE_VERSION = '0.3.1'
AC_PACKET_TIMEOUT_MIN = 150
AC_PACKET_TIMEOUT_MAX = 600
AC_POWER_LIMIT_MIN = 30