10 Commits

Author SHA1 Message Date
GK
fa0b698afa Merge pull request #189 from GrKoR/dev
Feature/Fix: ESPHome 2025.11.0 Compatibility & Swing Mode Refactor
2025-12-02 23:07:53 -08:00
GK
3391368179 Merge pull request #188 from qbus00/esp2025.11_fix 2025-11-28 07:09:31 -08:00
qbus00
fc56fb7966 fix: remove TWO_POINT_TARGET_TEMPERATURE flag for ESPHome 2025.11+ 2025-11-28 15:09:49 +01:00
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,27 +4,37 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
namespace esphome {
namespace aux_ac {
namespace esphome
{
namespace aux_ac
{
// **************************************** DISPLAY ACTIONS ****************************************
template <typename... Ts>
class AirConDisplayOffAction : public Action<Ts...> {
class AirConDisplayOffAction : public Action<Ts...>
{
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:
AirCon *ac_;
};
template <typename... Ts>
class AirConDisplayOnAction : public Action<Ts...> {
class AirConDisplayOnAction : public Action<Ts...>
{
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:
AirCon *ac_;
@@ -32,92 +42,123 @@ class AirConDisplayOnAction : public Action<Ts...> {
// **************************************** VERTICAL LOUVER ACTIONS ****************************************
template <typename... Ts>
class AirConVLouverSwingAction : public Action<Ts...> {
class AirConVLouverSwingAction : public Action<Ts...>
{
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:
AirCon *ac_;
};
template <typename... Ts>
class AirConVLouverStopAction : public Action<Ts...> {
class AirConVLouverStopAction : public Action<Ts...>
{
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:
AirCon *ac_;
};
template <typename... Ts>
class AirConVLouverTopAction : public Action<Ts...> {
class AirConVLouverTopAction : public Action<Ts...>
{
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:
AirCon *ac_;
};
template <typename... Ts>
class AirConVLouverMiddleAboveAction : public Action<Ts...> {
class AirConVLouverMiddleAboveAction : public Action<Ts...>
{
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:
AirCon *ac_;
};
template <typename... Ts>
class AirConVLouverMiddleAction : public Action<Ts...> {
class AirConVLouverMiddleAction : public Action<Ts...>
{
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:
AirCon *ac_;
};
template <typename... Ts>
class AirConVLouverMiddleBelowAction : public Action<Ts...> {
class AirConVLouverMiddleBelowAction : public Action<Ts...>
{
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:
AirCon *ac_;
};
template <typename... Ts>
class AirConVLouverBottomAction : public Action<Ts...> {
class AirConVLouverBottomAction : public Action<Ts...>
{
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:
AirCon *ac_;
};
template <typename... Ts>
class AirConVLouverSetAction : public Action<Ts...> {
class AirConVLouverSetAction : public Action<Ts...>
{
public:
AirConVLouverSetAction(AirCon *ac) : ac_(ac) {}
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...);
this->ac_->setVLouverFrontendSequence((ac_vlouver_frontend)vlpos_);
}
};
protected:
AirCon *ac_;
@@ -126,22 +167,29 @@ class AirConVLouverSetAction : public Action<Ts...> {
// **************************************** SEND TEST PACKET ACTION ****************************************
template <typename... Ts>
class AirConSendTestPacketAction : public Action<Ts...> {
class AirConSendTestPacketAction : public Action<Ts...>
{
public:
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->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->static_ = true;
}
void play(Ts... x) override {
if (this->static_) {
void play(const Ts &...x) override
{
if (this->static_)
{
this->ac_->sendTestPacket(this->data_static_);
} else {
}
else
{
auto val = this->data_func_(x...);
this->ac_->sendTestPacket(val);
}
@@ -156,23 +204,30 @@ class AirConSendTestPacketAction : public Action<Ts...> {
// **************************************** POWER LIMITATION ACTIONS ****************************************
template <typename... Ts>
class AirConPowerLimitationOffAction : public Action<Ts...> {
class AirConPowerLimitationOffAction : public Action<Ts...>
{
public:
explicit AirConPowerLimitationOffAction(AirCon *ac) : ac_(ac) {}
void play(Ts... x) override { this->ac_->powerLimitationOffSequence(); }
void play(const Ts &...x) override
{
this->ac_->powerLimitationOffSequence();
}
protected:
AirCon *ac_;
};
template <typename... Ts>
class AirConPowerLimitationOnAction : public Action<Ts...> {
class AirConPowerLimitationOnAction : public Action<Ts...>
{
public:
AirConPowerLimitationOnAction(AirCon *ac) : ac_(ac) {}
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->ac_->powerLimitationOnSequence(this->pwr_lim_);
}

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
}
else if (this->custom_preset == Constants::HEALTH)
{
// 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) "";
}
#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
}
else if (this->custom_preset == Constants::CLEAN)
{
// 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) "";
}
#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;
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;
// 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);
// NOT setting CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE - this device uses single 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

View File

@@ -26,6 +26,7 @@ from esphome.const import (
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_POWER_FACTOR,
STATE_CLASS_MEASUREMENT,
__version__
)
from esphome.components.climate import (
ClimateMode,
@@ -33,7 +34,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
@@ -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):
minV = AC_PACKET_TIMEOUT_MIN
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]))
if CONF_SUPPORTED_PRESETS in config:
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:
cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS]))
if CONF_CUSTOM_FAN_MODES in config: