Compare commits

...
Author SHA1 Message Date
56cc03c4d4 [haier] Haier Select component added (#16148)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Pavlo Dudnytskyi <Pavlo.Dudnytskyi@addsecure.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 23:27:18 -10:00
35b416c6c0 [web_server_idf] Keep active SSE sessions out of the LRU purge (#17383)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 22:19:06 -10:00
9 changed files with 351 additions and 15 deletions
+1
View File
@@ -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
+165 -11
View File
@@ -2,6 +2,8 @@
#include <string>
#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<hon_protocol::VerticalSwingMode> 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<hon_protocol::HorizontalSwingMode> 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<typename Mode, size_t N>
static void publish_airflow_index(select::Select *sel, const std::array<Mode, N> &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()) {
+38
View File
@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <chrono>
#include <queue>
#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<hon_protocol::VerticalSwingMode, 7> 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<hon_protocol::HorizontalSwingMode, 5> 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<typename Mode, size_t N> Mode airflow_order_at(const std::array<Mode, N> &order, size_t index) {
static_assert(sizeof(Mode) == 1);
return static_cast<Mode>(progmem_read_byte(reinterpret_cast<const uint8_t *>(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<hon_protocol::VerticalSwingMode> current_vertical_swing_{};
esphome::optional<hon_protocol::HorizontalSwingMode> 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};
@@ -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,
)
@@ -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<HonClimate> {
protected:
void control(size_t index) override {
this->parent_->set_horizontal_airflow(airflow_order_at(HORIZONTAL_SWING_MODES_ORDER, index));
}
};
} // namespace esphome::haier
@@ -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<HonClimate> {
protected:
void control(size_t index) override {
this->parent_->set_vertical_airflow(airflow_order_at(VERTICAL_SWING_MODES_ORDER, index));
}
};
} // namespace esphome::haier
@@ -959,10 +959,39 @@ void AsyncEventSourceResponse::process_close_() {
}
}
void AsyncEventSourceResponse::refresh_lru_() {
// httpd only advances a session's LRU counter when a request arrives, and an event stream receives one
// request ever, so the purge that frees a socket for a new client would evict the live streams first.
// The HTTPD task owns the session table, so the refresh runs there.
// A stalled stream is the socket the purge should take, and a zeroed fd has no session left to refresh.
if (this->send_failure_started_ms_ != 0 || this->fd_.load() == 0)
return;
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_lru_refresh_ms_ < LRU_REFRESH_INTERVAL_MS ||
this->lru_work_queued_.load(std::memory_order_acquire)) {
return;
}
this->last_lru_refresh_ms_ = now; // a full queue also waits for the next interval
this->lru_work_queued_.store(true, std::memory_order_release);
if (httpd_queue_work(this->hd_, &AsyncEventSourceResponse::refresh_lru_work, this) != ESP_OK) {
this->lru_work_queued_.store(false, std::memory_order_release);
}
}
void AsyncEventSourceResponse::refresh_lru_work(void *arg) {
auto *response = static_cast<AsyncEventSourceResponse *>(arg);
const int fd = response->fd_.load();
if (response->owns_session_(fd)) {
httpd_sess_update_lru_counter(response->hd_, fd);
}
// Release self only after the HTTPD-task callback has finished every access.
response->lru_work_queued_.store(false, std::memory_order_release);
}
void AsyncEventSourceResponse::close_session_work(void *arg) {
auto *response = static_cast<AsyncEventSourceResponse *>(arg);
const int fd = response->fd_.load();
if (fd != 0 && httpd_sess_get_ctx(response->hd_, fd) == response) {
if (response->owns_session_(fd)) {
// The HTTPD task remains the session owner. Shutting the socket down makes
// its next select/recv path delete the session and invoke destroy().
shutdown(fd, SHUT_RDWR);
@@ -976,7 +1005,7 @@ ssize_t AsyncEventSourceResponse::send_(struct iovec *iov, int iovcnt) {
// httpd frees a session before closing its socket, so the fd may already be a new client's.
// Treated as would-block; the stall timer ends a session that never becomes ours again.
const int fd = this->fd_.load();
if (httpd_sess_get_ctx(this->hd_, fd) != this) {
if (!this->owns_session_(fd)) {
return 0;
}
struct msghdr msg {};
@@ -1085,6 +1114,7 @@ void AsyncEventSourceResponse::loop() {
this->process_close_();
return;
}
this->refresh_lru_();
drain_tail_();
process_deferred_queue_();
if (this->close_requested_)
@@ -339,10 +339,15 @@ class AsyncEventSourceResponse {
};
void process_close_();
static void close_session_work(void *arg);
void refresh_lru_();
static void refresh_lru_work(void *arg);
// httpd frees a session before closing its socket, so the fd may already belong to a new client
bool owns_session_(int fd) const { return fd != 0 && httpd_sess_get_ctx(this->hd_, fd) == this; }
// Deletable only after destroy() zeroed fd_ and no queued HTTPD close work still references this object.
// Deletable only after destroy() zeroed fd_ and no queued HTTPD work still references this object.
bool safe_to_delete_() const {
return this->fd_.load() == 0 && !this->close_work_queued_.load(std::memory_order_acquire);
return this->fd_.load() == 0 && !this->close_work_queued_.load(std::memory_order_acquire) &&
!this->lru_work_queued_.load(std::memory_order_acquire);
}
static void destroy(void *p);
@@ -357,11 +362,13 @@ class AsyncEventSourceResponse {
RAMUniquePtr<uint8_t[]> tail_;
uint32_t send_failure_started_ms_{0}; // Zero means no send stall in progress.
uint32_t next_close_attempt_ms_{0};
uint32_t last_lru_refresh_ms_{0};
uint16_t tail_cap_{0};
uint16_t tail_len_{0}; // Zero means nothing pending
uint16_t tail_sent_{0};
// Set on the main loop before queueing close work, cleared by the HTTPD-task callback when done.
std::atomic<bool> close_work_queued_{false};
std::atomic<bool> lru_work_queued_{false};
// Main-loop only; the HTTPD task never reads or writes these flags.
bool close_requested_{false};
bool close_retry_warning_logged_{false};
@@ -383,6 +390,7 @@ class AsyncEventSourceResponse {
static constexpr uint32_t SEND_STALL_TIMEOUT_MS = 20000;
static constexpr uint32_t CLOSE_RETRY_INTERVAL_MS = 250;
static constexpr uint32_t CLOSE_CONFIRM_INTERVAL_MS = 1000;
static constexpr uint32_t LRU_REFRESH_INTERVAL_MS = 1000;
};
using AsyncEventSourceClient = AsyncEventSourceResponse;
+8
View File
@@ -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