diff --git a/CODEOWNERS b/CODEOWNERS index 95d8c49c23e..1282e9d66b9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -240,6 +240,7 @@ esphome/components/gt911/* @clydebarrow @jesserockz esphome/components/haier/* @paveldn esphome/components/haier/binary_sensor/* @paveldn esphome/components/haier/button/* @paveldn +esphome/components/haier/select/* @paveldn esphome/components/haier/sensor/* @paveldn esphome/components/haier/switch/* @paveldn esphome/components/haier/text_sensor/* @paveldn diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 72b1e26818b..59484aab16d 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -2,6 +2,8 @@ #include #include "esphome/components/climate/climate.h" #include "esphome/components/uart/uart.h" +#include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/progmem.h" #include "hon_climate.h" @@ -12,6 +14,26 @@ using namespace esphome::uart; namespace esphome::haier { +static bool vertical_axis_is_auto(climate::ClimateSwingMode swing_mode) { + return (swing_mode == climate::CLIMATE_SWING_VERTICAL) || (swing_mode == climate::CLIMATE_SWING_BOTH); +} + +static bool horizontal_axis_is_auto(climate::ClimateSwingMode swing_mode) { + return (swing_mode == climate::CLIMATE_SWING_HORIZONTAL) || (swing_mode == climate::CLIMATE_SWING_BOTH); +} + +static bool is_auto(hon_protocol::VerticalSwingMode mode) { + return (mode == hon_protocol::VerticalSwingMode::AUTO) || (mode == hon_protocol::VerticalSwingMode::AUTO_SPECIAL); +} + +static bool is_auto(hon_protocol::HorizontalSwingMode mode) { return mode == hon_protocol::HorizontalSwingMode::AUTO; } + +// A position the user just set wins over the appliance's echo of the old one for this long +constexpr uint32_t AIRFLOW_SELECT_DEBOUNCE_MS = 10000; +static bool in_debounce(uint32_t set_time, uint32_t now) { + return set_time != 0 && now - set_time < AIRFLOW_SELECT_DEBOUNCE_MS; +} + ESPHOME_LOG_TAG(TAG, "haier.climate"); constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64; @@ -89,21 +111,60 @@ bool HonClimate::get_quiet_mode_state() const { } esphome::optional HonClimate::get_vertical_airflow() const { + if (!this->current_vertical_swing_.has_value()) { + return this->current_vertical_swing_; + } + // While the appliance swings, or right after the user chose a position, report the chosen position + if (is_auto(this->current_vertical_swing_.value()) || + in_debounce(this->vertical_direction_set_time_, App.get_loop_component_start_time())) { + return this->settings_.last_vertiacal_swing; + } return this->current_vertical_swing_; -}; +} void HonClimate::set_vertical_airflow(hon_protocol::VerticalSwingMode direction) { + if (is_auto(direction)) { + ESP_LOGW(TAG, "Vertical AUTO mode is not allowed for airflow position selector"); + return; + } + if (this->settings_.last_vertiacal_swing != direction) { + this->settings_.last_vertiacal_swing = direction; + this->hon_rtc_.save(&this->settings_); + } this->pending_vertical_direction_ = direction; this->force_send_control_ = true; + this->vertical_direction_set_time_ = App.get_loop_component_start_time(); +#ifdef USE_SELECT + this->publish_airflow_selects_(); +#endif } esphome::optional HonClimate::get_horizontal_airflow() const { + if (!this->current_horizontal_swing_.has_value()) { + return this->current_horizontal_swing_; + } + if (is_auto(this->current_horizontal_swing_.value()) || + in_debounce(this->horizontal_direction_set_time_, App.get_loop_component_start_time())) { + return this->settings_.last_horizontal_swing; + } return this->current_horizontal_swing_; } void HonClimate::set_horizontal_airflow(hon_protocol::HorizontalSwingMode direction) { + if (is_auto(direction)) { + ESP_LOGW(TAG, "Horizontal AUTO mode is not allowed for airflow position selector"); + return; + } + if (this->settings_.last_horizontal_swing != direction) { + this->settings_.last_horizontal_swing = direction; + this->hon_rtc_.save(&this->settings_); + } this->pending_horizontal_direction_ = direction; this->force_send_control_ = true; + this->horizontal_direction_set_time_ = App.get_loop_component_start_time(); +#ifdef USE_SELECT + this->publish_airflow_selects_(); +#endif } const char *HonClimate::get_cleaning_status_text() const { @@ -533,6 +594,18 @@ void HonClimate::initialization() { } else { this->settings_ = {hon_protocol::VerticalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::CENTER, true, false}; } + bool settings_fixed = false; + if (is_auto(this->settings_.last_vertiacal_swing)) { + this->settings_.last_vertiacal_swing = hon_protocol::VerticalSwingMode::CENTER; + settings_fixed = true; + } + if (is_auto(this->settings_.last_horizontal_swing)) { + this->settings_.last_horizontal_swing = hon_protocol::HorizontalSwingMode::CENTER; + settings_fixed = true; + } + if (settings_fixed) { + this->hon_rtc_.save(&this->settings_); + } this->current_vertical_swing_ = this->settings_.last_vertiacal_swing; this->current_horizontal_swing_ = this->settings_.last_horizontal_swing; this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF; @@ -668,12 +741,21 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { } } } + const ClimateSwingMode target_swing_mode = this->current_hvac_settings_.swing_mode.value_or(this->swing_mode); if (this->pending_vertical_direction_.has_value()) { - out_data->vertical_swing_mode = (uint8_t) this->pending_vertical_direction_.value(); + if (!vertical_axis_is_auto(target_swing_mode)) { + out_data->vertical_swing_mode = (uint8_t) this->pending_vertical_direction_.value(); + } else { + ESP_LOGD(TAG, "Vertical swing is on; the position is kept for when it is turned off"); + } this->pending_vertical_direction_.reset(); } if (this->pending_horizontal_direction_.has_value()) { - out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value(); + if (!horizontal_axis_is_auto(target_swing_mode)) { + out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value(); + } else { + ESP_LOGD(TAG, "Horizontal swing is on; the position is kept for when it is turned off"); + } this->pending_horizontal_direction_.reset(); } { @@ -840,6 +922,41 @@ void HonClimate::set_quiet_mode_switch(switch_::Switch *sw) { } #endif // USE_SWITCH +#ifdef USE_SELECT +template +static void publish_airflow_index(select::Select *sel, const std::array &order, Mode mode) { + if (sel == nullptr) { + return; + } + // Saved settings never hold AUTO after initialization(); any other value outside the table shows as Center + size_t index = N; + size_t center = 0; + for (size_t i = 0; i < N; i++) { + const Mode entry = airflow_order_at(order, i); + if (entry == mode) { + index = i; + } else if (entry == Mode::CENTER) { + center = i; + } + } + if (index == N) { + index = center; + } + const auto active = sel->active_index(); + if (active.has_value() && *active == index) { + return; + } + sel->publish_state(index); +} + +void HonClimate::publish_airflow_selects_() { + publish_airflow_index(this->vertical_airflow_select_, VERTICAL_SWING_MODES_ORDER, + this->settings_.last_vertiacal_swing); + publish_airflow_index(this->horizontal_airflow_select_, HORIZONTAL_SWING_MODES_ORDER, + this->settings_.last_horizontal_swing); +} +#endif // USE_SELECT + haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { size_t expected_size = 2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_; @@ -1074,16 +1191,25 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * // Saving last known non auto mode for vertical and horizontal swing this->current_vertical_swing_ = (hon_protocol::VerticalSwingMode) packet.control.vertical_swing_mode; this->current_horizontal_swing_ = (hon_protocol::HorizontalSwingMode) packet.control.horizontal_swing_mode; - bool save_settings = ((this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO) && - (this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO_SPECIAL) && - (this->current_vertical_swing_.value() != this->settings_.last_vertiacal_swing)) || - ((this->current_horizontal_swing_.value() != hon_protocol::HorizontalSwingMode::AUTO) && - (this->current_horizontal_swing_.value() != this->settings_.last_horizontal_swing)); - if (save_settings) { - this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value(); - this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value(); + const uint32_t now_ms = App.get_loop_component_start_time(); + bool save_vertical = !in_debounce(this->vertical_direction_set_time_, now_ms) && + !is_auto(this->current_vertical_swing_.value()) && + (this->current_vertical_swing_.value() != this->settings_.last_vertiacal_swing); + bool save_horizontal = !in_debounce(this->horizontal_direction_set_time_, now_ms) && + !is_auto(this->current_horizontal_swing_.value()) && + (this->current_horizontal_swing_.value() != this->settings_.last_horizontal_swing); + if (save_vertical || save_horizontal) { + if (save_vertical) { + this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value(); + } + if (save_horizontal) { + this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value(); + } this->hon_rtc_.save(&this->settings_); } +#ifdef USE_SELECT + this->publish_airflow_selects_(); +#endif should_publish = should_publish || (old_swing_mode != this->swing_mode); } this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); @@ -1259,8 +1385,10 @@ void HonClimate::fill_control_messages_queue_() { (uint8_t) hon_protocol::DataParameters::SET_POINT, buffer, 2); } + ClimateSwingMode target_swing_mode = this->swing_mode; // Vertical swing mode if (climate_control.swing_mode.has_value()) { + target_swing_mode = climate_control.swing_mode.value(); uint8_t vertical_swing_buf[] = {0x00, (uint8_t) hon_protocol::VerticalSwingMode::AUTO}; uint8_t horizontal_swing_buf[] = {0x00, (uint8_t) hon_protocol::HorizontalSwingMode::AUTO}; switch (climate_control.swing_mode.value()) { @@ -1286,6 +1414,32 @@ void HonClimate::fill_control_messages_queue_() { (uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE, vertical_swing_buf, 2); } + // A swing change above already sent both axes from settings_.last_*, which set_*_airflow() updated + const bool swing_sent = climate_control.swing_mode.has_value(); + if (this->pending_vertical_direction_.has_value()) { + if (vertical_axis_is_auto(target_swing_mode)) { + ESP_LOGD(TAG, "Vertical swing is on; the position is kept for when it is turned off"); + } else if (!swing_sent) { + uint8_t vertical_swing_buf[] = {0x00, (uint8_t) this->pending_vertical_direction_.value()}; + this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE, + vertical_swing_buf, 2); + } + this->pending_vertical_direction_.reset(); + } + if (this->pending_horizontal_direction_.has_value()) { + if (horizontal_axis_is_auto(target_swing_mode)) { + ESP_LOGD(TAG, "Horizontal swing is on; the position is kept for when it is turned off"); + } else if (!swing_sent) { + uint8_t horizontal_swing_buf[] = {0x00, (uint8_t) this->pending_horizontal_direction_.value()}; + this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, + (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + + (uint8_t) hon_protocol::DataParameters::HORIZONTAL_SWING_MODE, + horizontal_swing_buf, 2); + } + this->pending_horizontal_direction_.reset(); + } // Fan mode if (climate_control.fan_mode.has_value()) { switch (climate_control.fan_mode.value()) { diff --git a/esphome/components/haier/hon_climate.h b/esphome/components/haier/hon_climate.h index a34b4422c69..db4ef4590bf 100644 --- a/esphome/components/haier/hon_climate.h +++ b/esphome/components/haier/hon_climate.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #ifdef USE_SENSOR @@ -14,12 +15,36 @@ #ifdef USE_SWITCH #include "esphome/components/switch/switch.h" #endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif #include "esphome/core/automation.h" +#include "esphome/core/hal.h" #include "haier_base.h" #include "hon_packet.h" namespace esphome::haier { +#ifdef USE_SELECT +// Positions in the order the select platform lists them; in flash on ESP8266, so read with airflow_order_at() +inline constexpr std::array VERTICAL_SWING_MODES_ORDER PROGMEM{ + hon_protocol::VerticalSwingMode::HEALTH_UP, hon_protocol::VerticalSwingMode::MAX_UP, + hon_protocol::VerticalSwingMode::UP, hon_protocol::VerticalSwingMode::CENTER, + hon_protocol::VerticalSwingMode::DOWN, hon_protocol::VerticalSwingMode::MAX_DOWN, + hon_protocol::VerticalSwingMode::HEALTH_DOWN, +}; +inline constexpr std::array HORIZONTAL_SWING_MODES_ORDER PROGMEM{ + hon_protocol::HorizontalSwingMode::MAX_LEFT, hon_protocol::HorizontalSwingMode::LEFT, + hon_protocol::HorizontalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::RIGHT, + hon_protocol::HorizontalSwingMode::MAX_RIGHT, +}; + +template Mode airflow_order_at(const std::array &order, size_t index) { + static_assert(sizeof(Mode) == 1); + return static_cast(progmem_read_byte(reinterpret_cast(order.data() + index))); +} +#endif + enum class CleaningState : uint8_t { NO_CLEANING = 0, SELF_CLEAN = 1, @@ -100,6 +125,16 @@ class HonClimate final : public HaierClimateBase { protected: switch_::Switch *beeper_switch_{nullptr}; switch_::Switch *quiet_mode_switch_{nullptr}; +#endif +#ifdef USE_SELECT + public: + void set_vertical_airflow_select(select::Select *sel) { this->vertical_airflow_select_ = sel; } + void set_horizontal_airflow_select(select::Select *sel) { this->horizontal_airflow_select_ = sel; } + + protected: + void publish_airflow_selects_(); + select::Select *vertical_airflow_select_{nullptr}; + select::Select *horizontal_airflow_select_{nullptr}; #endif public: HonClimate(); @@ -195,6 +230,9 @@ class HonClimate final : public HaierClimateBase { uint8_t big_data_counter_{0}; esphome::optional current_vertical_swing_{}; esphome::optional current_horizontal_swing_{}; + // millis() of the last user-set position per axis, 0 when none + uint32_t vertical_direction_set_time_{0}; + uint32_t horizontal_direction_set_time_{0}; HonSettings settings_{}; ESPPreferenceObject hon_rtc_; SwitchState quiet_mode_state_{SwitchState::SWITCH_OFF}; diff --git a/esphome/components/haier/select/__init__.py b/esphome/components/haier/select/__init__.py new file mode 100644 index 00000000000..4198ba82081 --- /dev/null +++ b/esphome/components/haier/select/__init__.py @@ -0,0 +1,67 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import ENTITY_CATEGORY_CONFIG +from esphome.types import ConfigType + +from ..climate import ( + CONF_HAIER_ID, + CONF_HORIZONTAL_AIRFLOW, + CONF_VERTICAL_AIRFLOW, + HonClimate, + haier_ns, +) + +CODEOWNERS = ["@paveldn"] +VerticalAirflowSelect = haier_ns.class_("VerticalAirflowSelect", select.Select) +HorizontalAirflowSelect = haier_ns.class_("HorizontalAirflowSelect", select.Select) + +# Same order as VERTICAL_SWING_MODES_ORDER and HORIZONTAL_SWING_MODES_ORDER in hon_climate.h +AIRFLOW_VERTICAL_DIRECTION_OPTIONS = [ + "Health Up", + "Max Up", + "Up", + "Center", + "Down", + "Max Down", + "Health Down", +] + +AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = [ + "Max Left", + "Left", + "Center", + "Right", + "Max Right", +] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate), + cv.Optional(CONF_VERTICAL_AIRFLOW): select.select_schema( + VerticalAirflowSelect, + icon="mdi:arrow-expand-vertical", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + cv.Optional(CONF_HORIZONTAL_AIRFLOW): select.select_schema( + HorizontalAirflowSelect, + icon="mdi:arrow-expand-horizontal", + entity_category=ENTITY_CATEGORY_CONFIG, + ), + } +) + + +async def to_code(config: ConfigType) -> None: + hub = await cg.get_variable(config[CONF_HAIER_ID]) + selects = select.sub_selects(config, parent=hub) + await selects( + CONF_VERTICAL_AIRFLOW, + hub.set_vertical_airflow_select, + options=AIRFLOW_VERTICAL_DIRECTION_OPTIONS, + ) + await selects( + CONF_HORIZONTAL_AIRFLOW, + hub.set_horizontal_airflow_select, + options=AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS, + ) diff --git a/esphome/components/haier/select/horizontal_airflow.h b/esphome/components/haier/select/horizontal_airflow.h new file mode 100644 index 00000000000..54f46bfcb19 --- /dev/null +++ b/esphome/components/haier/select/horizontal_airflow.h @@ -0,0 +1,15 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../hon_climate.h" + +namespace esphome::haier { + +class HorizontalAirflowSelect : public select::Select, public Parented { + protected: + void control(size_t index) override { + this->parent_->set_horizontal_airflow(airflow_order_at(HORIZONTAL_SWING_MODES_ORDER, index)); + } +}; + +} // namespace esphome::haier diff --git a/esphome/components/haier/select/vertical_airflow.h b/esphome/components/haier/select/vertical_airflow.h new file mode 100644 index 00000000000..81ac1b39d2c --- /dev/null +++ b/esphome/components/haier/select/vertical_airflow.h @@ -0,0 +1,15 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../hon_climate.h" + +namespace esphome::haier { + +class VerticalAirflowSelect : public select::Select, public Parented { + protected: + void control(size_t index) override { + this->parent_->set_vertical_airflow(airflow_order_at(VERTICAL_SWING_MODES_ORDER, index)); + } +}; + +} // namespace esphome::haier diff --git a/tests/components/haier/common.yaml b/tests/components/haier/common.yaml index ab39dfa61d5..2c0283215f6 100644 --- a/tests/components/haier/common.yaml +++ b/tests/components/haier/common.yaml @@ -121,3 +121,11 @@ switch: name: Haier health mode quiet_mode: name: Haier quiet mode + +select: + - platform: haier + haier_id: haier_ac + horizontal_airflow: + name: Haier airflow horizontal + vertical_airflow: + name: Haier airflow vertical