diff --git a/components/aux_ac/automation.h b/components/aux_ac/automation.h index 50129e7..90df143 100644 --- a/components/aux_ac/automation.h +++ b/components/aux_ac/automation.h @@ -4,183 +4,238 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace aux_ac { +namespace esphome +{ + namespace aux_ac + { -// **************************************** DISPLAY ACTIONS **************************************** -template -class AirConDisplayOffAction : public Action { - public: - explicit AirConDisplayOffAction(AirCon *ac) : ac_(ac) {} + // **************************************** DISPLAY ACTIONS **************************************** + template + class AirConDisplayOffAction : public Action + { + 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_; -}; + protected: + AirCon *ac_; + }; -template -class AirConDisplayOnAction : public Action { - public: - explicit AirConDisplayOnAction(AirCon *ac) : ac_(ac) {} + template + class AirConDisplayOnAction : public Action + { + 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_; -}; + protected: + AirCon *ac_; + }; -// **************************************** VERTICAL LOUVER ACTIONS **************************************** -template -class AirConVLouverSwingAction : public Action { - public: - explicit AirConVLouverSwingAction(AirCon *ac) : ac_(ac) {} + // **************************************** VERTICAL LOUVER ACTIONS **************************************** + template + class AirConVLouverSwingAction : public Action + { + 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_; -}; + protected: + AirCon *ac_; + }; -template -class AirConVLouverStopAction : public Action { - public: - explicit AirConVLouverStopAction(AirCon *ac) : ac_(ac) {} + template + class AirConVLouverStopAction : public Action + { + 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_; -}; + protected: + AirCon *ac_; + }; -template -class AirConVLouverTopAction : public Action { - public: - explicit AirConVLouverTopAction(AirCon *ac) : ac_(ac) {} + template + class AirConVLouverTopAction : public Action + { + 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_; -}; + protected: + AirCon *ac_; + }; -template -class AirConVLouverMiddleAboveAction : public Action { - public: - explicit AirConVLouverMiddleAboveAction(AirCon *ac) : ac_(ac) {} + template + class AirConVLouverMiddleAboveAction : public Action + { + 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_; -}; + protected: + AirCon *ac_; + }; -template -class AirConVLouverMiddleAction : public Action { - public: - explicit AirConVLouverMiddleAction(AirCon *ac) : ac_(ac) {} + template + class AirConVLouverMiddleAction : public Action + { + 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_; -}; + protected: + AirCon *ac_; + }; -template -class AirConVLouverMiddleBelowAction : public Action { - public: - explicit AirConVLouverMiddleBelowAction(AirCon *ac) : ac_(ac) {} + template + class AirConVLouverMiddleBelowAction : public Action + { + 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_; -}; + protected: + AirCon *ac_; + }; -template -class AirConVLouverBottomAction : public Action { - public: - explicit AirConVLouverBottomAction(AirCon *ac) : ac_(ac) {} + template + class AirConVLouverBottomAction : public Action + { + 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_; -}; + protected: + AirCon *ac_; + }; -template -class AirConVLouverSetAction : public Action { - public: - AirConVLouverSetAction(AirCon *ac) : ac_(ac) {} - TEMPLATABLE_VALUE(uint8_t, value); + template + class AirConVLouverSetAction : public Action + { + public: + TEMPLATABLE_VALUE(uint8_t, value); - void play(Ts... x) { - vlpos_ = this->value_.value(x...); - this->ac_->setVLouverFrontendSequence((ac_vlouver_frontend)vlpos_); - } + AirConVLouverSetAction(AirCon *ac) : ac_(ac) {}; - protected: - AirCon *ac_; - uint8_t vlpos_; -}; + void play(const Ts &...x) override + { + vlpos_ = this->value_.value(x...); + this->ac_->setVLouverFrontendSequence((ac_vlouver_frontend)vlpos_); + }; -// **************************************** SEND TEST PACKET ACTION **************************************** -template -class AirConSendTestPacketAction : public Action { - public: - explicit AirConSendTestPacketAction(AirCon *ac) : ac_(ac) {} - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; - } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; - } + protected: + AirCon *ac_; + uint8_t vlpos_; + }; - void play(Ts... x) override { - if (this->static_) { - this->ac_->sendTestPacket(this->data_static_); - } else { - auto val = this->data_func_(x...); - this->ac_->sendTestPacket(val); - } - } + // **************************************** SEND TEST PACKET ACTION **************************************** + template + class AirConSendTestPacketAction : public Action + { + public: + explicit AirConSendTestPacketAction(AirCon *ac) : ac_(ac) {} + void set_data_template(std::function(Ts...)> func) + { + this->data_func_ = func; + this->static_ = false; + } + void set_data_static(const std::vector &data) + { + this->data_static_ = data; + this->static_ = true; + } - protected: - AirCon *ac_; - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; -}; + void play(const Ts &...x) override + { + if (this->static_) + { + this->ac_->sendTestPacket(this->data_static_); + } + else + { + auto val = this->data_func_(x...); + this->ac_->sendTestPacket(val); + } + } -// **************************************** POWER LIMITATION ACTIONS **************************************** -template -class AirConPowerLimitationOffAction : public Action { - public: - explicit AirConPowerLimitationOffAction(AirCon *ac) : ac_(ac) {} + protected: + AirCon *ac_; + bool static_{false}; + std::function(Ts...)> data_func_{}; + std::vector data_static_{}; + }; - void play(Ts... x) override { this->ac_->powerLimitationOffSequence(); } + // **************************************** POWER LIMITATION ACTIONS **************************************** + template + class AirConPowerLimitationOffAction : public Action + { + public: + explicit AirConPowerLimitationOffAction(AirCon *ac) : ac_(ac) {} - protected: - AirCon *ac_; -}; + void play(const Ts &...x) override + { + this->ac_->powerLimitationOffSequence(); + } -template -class AirConPowerLimitationOnAction : public Action { - public: - AirConPowerLimitationOnAction(AirCon *ac) : ac_(ac) {} - TEMPLATABLE_VALUE(uint8_t, value); + protected: + AirCon *ac_; + }; - void play(Ts... x) { - this->pwr_lim_ = this->value_.value(x...); - this->ac_->powerLimitationOnSequence(this->pwr_lim_); - } + template + class AirConPowerLimitationOnAction : public Action + { + public: + TEMPLATABLE_VALUE(uint8_t, value); - protected: - AirCon *ac_; - uint8_t pwr_lim_; -}; + AirConPowerLimitationOnAction(AirCon *ac) : ac_(ac) {}; -} // namespace aux_ac -} // namespace esphome \ No newline at end of file + void play(const Ts &...x) override + { + 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 \ No newline at end of file diff --git a/components/aux_ac/aux_ac.h b/components/aux_ac/aux_ac.h index 88d0015..4b7fb7b 100644 --- a/components/aux_ac/aux_ac.h +++ b/components/aux_ac/aux_ac.h @@ -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 _supported_custom_fan_modes{}; + std::vector _supported_custom_presets{}; +#else std::set _supported_modes{}; std::set _supported_swing_modes{}; std::set _supported_presets{}; std::set _supported_custom_presets{}; std::set _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 presets) { this->_supported_custom_presets = presets; } + void set_custom_fan_modes(std::initializer_list modes) { this->_supported_custom_fan_modes = modes; } +#else void set_supported_modes(const std::set &modes) { this->_supported_modes = modes; } std::set get_supported_modes() { return this->_supported_modes; } @@ -3796,6 +4005,7 @@ namespace esphome void set_custom_fan_modes(const std::set &modes) { this->_supported_custom_fan_modes = modes; } const std::set &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 @@ -3899,4 +4121,4 @@ namespace esphome }; } // namespace aux_ac -} // namespace esphome \ No newline at end of file +} // namespace esphome diff --git a/components/aux_ac/climate.py b/components/aux_ac/climate.py index 1a9d424..117f987 100644 --- a/components/aux_ac/climate.py +++ b/components/aux_ac/climate.py @@ -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,10 +433,20 @@ 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 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])) + 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: + cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES])) DISPLAY_ACTION_SCHEMA = maybe_simple_id(