diff --git a/CODEOWNERS b/CODEOWNERS index fc53a28c0b..a770810490 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -243,6 +243,9 @@ esphome/components/haier/button/* @paveldn esphome/components/haier/sensor/* @paveldn esphome/components/haier/switch/* @paveldn esphome/components/haier/text_sensor/* @paveldn +esphome/components/haier_tundra/* @fauxpark +esphome/components/haier_tundra/button/* @fauxpark +esphome/components/haier_tundra/switch/* @fauxpark esphome/components/havells_solar/* @sourabhjaiswal esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/light/* @DotNetDann diff --git a/esphome/components/haier_tundra/__init__.py b/esphome/components/haier_tundra/__init__.py new file mode 100644 index 0000000000..d74ae6947f --- /dev/null +++ b/esphome/components/haier_tundra/__init__.py @@ -0,0 +1,2 @@ +DOMAIN = "haier_tundra" +CODEOWNERS = ["@fauxpark"] diff --git a/esphome/components/haier_tundra/button/__init__.py b/esphome/components/haier_tundra/button/__init__.py new file mode 100644 index 0000000000..2e919b8009 --- /dev/null +++ b/esphome/components/haier_tundra/button/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import CONF_LIGHT +from esphome.types import ConfigType + +from ..climate import HaierTundra, haier_tundra_ns + +CODEOWNERS = ["@fauxpark"] + +HaierTundraButton = haier_tundra_ns.class_( + "HaierTundraButton", button.Button, cg.Component +) +ButtonKind = haier_tundra_ns.enum("ButtonKind", is_class=True) + +ICON_LED_OUTLINE = "mdi:led-outline" +ICON_SPRAY_BOTTLE = "mdi:spray-bottle" + +CONF_HAIER_TUNDRA_ID = "haier_tundra_id" +CONF_SELF_CLEAN = "self_clean" + +CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_HAIER_TUNDRA_ID): cv.use_id(HaierTundra), + cv.Optional(CONF_LIGHT): button.button_schema( + HaierTundraButton, icon=ICON_LED_OUTLINE + ), + cv.Optional(CONF_SELF_CLEAN): button.button_schema( + HaierTundraButton, icon=ICON_SPRAY_BOTTLE + ), + } +) + + +async def to_code(config: ConfigType) -> None: + hub = await cg.get_variable(config[CONF_HAIER_TUNDRA_ID]) + for key, kind in ( + (CONF_LIGHT, ButtonKind.BUTTON_KIND_LIGHT), + (CONF_SELF_CLEAN, ButtonKind.BUTTON_KIND_SELF_CLEAN), + ): + if (conf := config.get(key)) is not None: + var = await button.new_button(conf, hub, kind) + await cg.register_component(var, conf) diff --git a/esphome/components/haier_tundra/button/haier_tundra_button.cpp b/esphome/components/haier_tundra/button/haier_tundra_button.cpp new file mode 100644 index 0000000000..39246530bc --- /dev/null +++ b/esphome/components/haier_tundra/button/haier_tundra_button.cpp @@ -0,0 +1,18 @@ +#include "haier_tundra_button.h" +#include "esphome/core/log.h" + +namespace esphome::haier_tundra { + +ESPHOME_LOG_TAG(TAG, "haier_tundra.button"); + +void HaierTundraButton::dump_config() { LOG_BUTTON("", "Haier Tundra Button", this); } + +void HaierTundraButton::press_action() { + if (this->kind_ == ButtonKind::BUTTON_KIND_LIGHT) { + this->parent_->toggle_light(); + } else { + this->parent_->start_self_clean(); + } +} + +} // namespace esphome::haier_tundra diff --git a/esphome/components/haier_tundra/button/haier_tundra_button.h b/esphome/components/haier_tundra/button/haier_tundra_button.h new file mode 100644 index 0000000000..c4888a4616 --- /dev/null +++ b/esphome/components/haier_tundra/button/haier_tundra_button.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/button/button.h" +#include "esphome/components/haier_tundra/haier_tundra.h" + +namespace esphome::haier_tundra { + +enum class ButtonKind : uint8_t { + BUTTON_KIND_LIGHT, + BUTTON_KIND_SELF_CLEAN, +}; + +class HaierTundraButton final : public button::Button, public Component, public Parented { + public: + HaierTundraButton(HaierTundra *parent, ButtonKind kind) : Parented(parent), kind_(kind) {} + void dump_config() override; + + protected: + void press_action() override; + + ButtonKind kind_; +}; + +} // namespace esphome::haier_tundra diff --git a/esphome/components/haier_tundra/climate.py b/esphome/components/haier_tundra/climate.py new file mode 100644 index 0000000000..c992d14bfe --- /dev/null +++ b/esphome/components/haier_tundra/climate.py @@ -0,0 +1,51 @@ +import esphome.codegen as cg +from esphome.components import climate_ir, remote_base +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_PLATFORM +import esphome.final_validate as fv +from esphome.types import ConfigType + +AUTO_LOAD = ["climate_ir"] + +haier_tundra_ns = cg.esphome_ns.namespace("haier_tundra") +HaierTundra = haier_tundra_ns.class_("HaierTundra", climate_ir.ClimateIR) + +CONF_UNIT = "unit" +Unit = haier_tundra_ns.enum("Unit") +UNITS = { + "a": Unit.HAIER_HEADER_UNIT_A, + "b": Unit.HAIER_HEADER_UNIT_B, +} + +CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HaierTundra).extend( + { + cv.Optional(CONF_UNIT, default="a"): cv.enum(UNITS, lower=True), + } +) + + +def _final_validate_unit(config: ConfigType) -> None: + # Each transmitter may drive at most one unit A and one unit B + climates = fv.full_config.get()["climate"] + seen = set() + + for climate in climates: + if CONF_PLATFORM in climate and climate[CONF_PLATFORM] == "haier_tundra": + unit_tx_pair = ( + climate[CONF_UNIT], + climate[remote_base.CONF_TRANSMITTER_ID], + ) + + if unit_tx_pair in seen and climate[CONF_ID] == config[CONF_ID]: + raise cv.Invalid( + "Only one of each unit is allowed per transmitter", path=[CONF_UNIT] + ) + seen.add(unit_tx_pair) + + +FINAL_VALIDATE_SCHEMA = _final_validate_unit + + +async def to_code(config: ConfigType) -> None: + remote_base.request_protocol("haier") + await climate_ir.new_climate_ir(config, config[CONF_UNIT]) diff --git a/esphome/components/haier_tundra/haier_tundra.cpp b/esphome/components/haier_tundra/haier_tundra.cpp new file mode 100644 index 0000000000..7a0b88d3aa --- /dev/null +++ b/esphome/components/haier_tundra/haier_tundra.cpp @@ -0,0 +1,260 @@ +#include "haier_tundra.h" + +namespace esphome::haier_tundra { + +// Number of bytes in decoded packet sans checksum +static constexpr uint8_t HAIER_TUNDRA_FRAME_SIZE = 13; + +void HaierTundra::transmit_state() { + // Button value does not seem to matter too much except + // when toggling light, as it has no bit flag of its own + // So in the default case we can simply set it to POWER + this->transmit_(HAIER_BUTTON_POWER); +} + +void HaierTundra::transmit_(uint8_t button) { + // Force Quiet and Turbo off if we're not in Cool or Heat mode + if (this->mode != climate::CLIMATE_MODE_COOL && this->mode != climate::CLIMATE_MODE_HEAT) { +#ifdef USE_SWITCH + if (this->quiet_mode_switch_ != nullptr) { + this->quiet_mode_switch_->publish_state(false); + } + if (this->turbo_mode_switch_ != nullptr) { + this->turbo_mode_switch_->publish_state(false); + } +#endif + + this->quiet_mode_ = false; + this->turbo_mode_ = false; + } + + remote_base::HaierData remote_state{{ + this->unit_, + static_cast(this->temperature_() << 4 | this->vertical_swing_()), + static_cast(this->horizontal_swing_() << 5), + static_cast(this->health_() << 1), + static_cast(this->power_() << 6), + static_cast(this->fan_speed_() << 5), + static_cast(this->quiet_() << 7 | this->turbo_() << 6), + static_cast(this->operation_mode_() << 5), + 0x00, + 0x00, + static_cast((button == HAIER_BUTTON_SELF_CLEAN ? 1 : 0) << 4), + 0x00, + button, + }}; + + auto transmit = this->transmitter_->transmit(); + remote_base::HaierProtocol().encode(transmit.get_data(), remote_state); + transmit.perform(); +} + +uint8_t HaierTundra::temperature_() { + return (uint8_t) roundf(clamp(this->target_temperature, HAIER_TEMP_MIN, HAIER_TEMP_MAX)) - HAIER_TEMP_MIN; +} + +uint8_t HaierTundra::vertical_swing_() { + if (this->mode == climate::CLIMATE_MODE_OFF) { + return HAIER_SWING_V_OFF; + } + switch (this->swing_mode) { + case climate::CLIMATE_SWING_VERTICAL: + case climate::CLIMATE_SWING_BOTH: + return HAIER_SWING_V_AUTO; + default: + if (this->mode == climate::CLIMATE_MODE_HEAT) { + return HAIER_SWING_V_1BOT; + } + return HAIER_SWING_V_1TOP; // Cool/Dry/Auto + } +} + +uint8_t HaierTundra::horizontal_swing_() { + switch (this->swing_mode) { + case climate::CLIMATE_SWING_HORIZONTAL: + case climate::CLIMATE_SWING_BOTH: + return HAIER_SWING_H_AUTO; + default: + return HAIER_SWING_H_MIDDLE; + } +} + +uint8_t HaierTundra::health_() { return this->health_mode_ ? 1 : 0; } + +uint8_t HaierTundra::power_() { return this->mode == climate::CLIMATE_MODE_OFF ? 0 : 1; } + +uint8_t HaierTundra::fan_speed_() { + switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) { + case climate::CLIMATE_FAN_LOW: + return HAIER_FAN_LOW; + case climate::CLIMATE_FAN_MEDIUM: + return HAIER_FAN_MED; + case climate::CLIMATE_FAN_HIGH: + return HAIER_FAN_HIGH; + case climate::CLIMATE_FAN_AUTO: + default: + return HAIER_FAN_AUTO; + } +} + +uint8_t HaierTundra::quiet_() { return this->quiet_mode_ ? 1 : 0; } + +uint8_t HaierTundra::turbo_() { return this->turbo_mode_ ? 1 : 0; } + +uint8_t HaierTundra::operation_mode_() { + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + return HAIER_MODE_COOL; + case climate::CLIMATE_MODE_DRY: + return HAIER_MODE_DRY; + case climate::CLIMATE_MODE_HEAT: + return HAIER_MODE_HEAT; + case climate::CLIMATE_MODE_FAN_ONLY: + return HAIER_MODE_FAN; + case climate::CLIMATE_MODE_HEAT_COOL: + default: + return HAIER_MODE_AUTO; + } +} + +bool HaierTundra::on_receive(remote_base::RemoteReceiveData data) { + auto haier = remote_base::HaierProtocol().decode(data); + if (haier.has_value()) { + return this->on_haier_(*haier); + } + return false; +} + +bool HaierTundra::on_haier_(const remote_base::HaierData &haier) { + // Verify frame length and header + if (haier.data.size() != HAIER_TUNDRA_FRAME_SIZE || haier.data[0] != this->unit_) { + return false; + } + + this->target_temperature = this->get_temperature_(haier.data[1] >> 4); + this->swing_mode = this->get_swing_mode_(haier.data[1] & 0xF, haier.data[2] >> 5); + this->health_mode_ = haier.data[3] & 0x2; + this->mode = this->get_operation_mode_(haier.data[4] & 0x40, haier.data[7] >> 5); + this->fan_mode = this->get_fan_speed_(haier.data[5] >> 5); + this->quiet_mode_ = haier.data[6] & 0x80; + this->turbo_mode_ = haier.data[6] & 0x40; + +#ifdef USE_SWITCH + if (this->health_mode_switch_ != nullptr) { + this->health_mode_switch_->publish_state(this->health_mode_); + } + if (this->quiet_mode_switch_ != nullptr) { + this->quiet_mode_switch_->publish_state(this->quiet_mode_); + } + if (this->turbo_mode_switch_ != nullptr) { + this->turbo_mode_switch_->publish_state(this->turbo_mode_); + } +#endif + + this->publish_state(); + + return true; +} + +float HaierTundra::get_temperature_(uint8_t temp) { return temp + HAIER_TEMP_MIN; } + +climate::ClimateSwingMode HaierTundra::get_swing_mode_(uint8_t swing_v, uint8_t swing_h) { + if (swing_v == HAIER_SWING_V_AUTO) { + if (swing_h == HAIER_SWING_H_AUTO) { + return climate::CLIMATE_SWING_BOTH; + } + + return climate::CLIMATE_SWING_VERTICAL; + } else if (swing_h == HAIER_SWING_H_AUTO) { + return climate::CLIMATE_SWING_HORIZONTAL; + } + + return climate::CLIMATE_SWING_OFF; +} + +climate::ClimateMode HaierTundra::get_operation_mode_(uint8_t power, uint8_t mode) { + if (!power) { + return climate::CLIMATE_MODE_OFF; + } + + switch (mode) { + case HAIER_MODE_COOL: + return climate::CLIMATE_MODE_COOL; + case HAIER_MODE_DRY: + return climate::CLIMATE_MODE_DRY; + case HAIER_MODE_HEAT: + return climate::CLIMATE_MODE_HEAT; + case HAIER_MODE_FAN: + return climate::CLIMATE_MODE_FAN_ONLY; + case HAIER_MODE_AUTO: + default: + return climate::CLIMATE_MODE_HEAT_COOL; + } +} + +climate::ClimateFanMode HaierTundra::get_fan_speed_(uint8_t fan) { + switch (fan) { + case HAIER_FAN_LOW: + return climate::CLIMATE_FAN_LOW; + case HAIER_FAN_MED: + return climate::CLIMATE_FAN_MEDIUM; + case HAIER_FAN_HIGH: + return climate::CLIMATE_FAN_HIGH; + case HAIER_FAN_AUTO: + default: + return climate::CLIMATE_FAN_AUTO; + } +} + +void HaierTundra::set_health_mode(bool health, bool transmit) { + this->health_mode_ = health; + + if (transmit && this->mode != climate::CLIMATE_MODE_OFF) { + this->transmit_(HAIER_BUTTON_HEALTH); + } +} + +void HaierTundra::set_quiet_mode(bool quiet, bool transmit) { + // Quiet & Turbo cannot be enabled at the same time + if (quiet) { +#ifdef USE_SWITCH + if (this->turbo_mode_switch_ != nullptr) { + this->turbo_mode_switch_->publish_state(false); + } +#endif + this->turbo_mode_ = false; + } + + this->quiet_mode_ = quiet; + if (transmit) + this->transmit_(HAIER_BUTTON_TURBO_QUIET); +} + +void HaierTundra::set_turbo_mode(bool turbo, bool transmit) { + // Quiet & Turbo cannot be enabled at the same time + if (turbo) { +#ifdef USE_SWITCH + if (this->quiet_mode_switch_ != nullptr) { + this->quiet_mode_switch_->publish_state(false); + } +#endif + this->quiet_mode_ = false; + } + + this->turbo_mode_ = turbo; + if (transmit) + this->transmit_(HAIER_BUTTON_TURBO_QUIET); +} + +void HaierTundra::toggle_light() { + if (this->mode != climate::CLIMATE_MODE_OFF) { + this->transmit_(HAIER_BUTTON_LIGHT); + } +} + +void HaierTundra::start_self_clean() { + // Can be started when off + this->transmit_(HAIER_BUTTON_SELF_CLEAN); +} + +} // namespace esphome::haier_tundra diff --git a/esphome/components/haier_tundra/haier_tundra.h b/esphome/components/haier_tundra/haier_tundra.h new file mode 100644 index 0000000000..b2bb1e51ce --- /dev/null +++ b/esphome/components/haier_tundra/haier_tundra.h @@ -0,0 +1,139 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" +#include "esphome/components/remote_base/haier_protocol.h" + +#ifdef USE_SWITCH +#include "esphome/components/switch/switch.h" +#endif + +namespace esphome::haier_tundra { + +// Byte 0 - Header value (Unit ID) +enum Unit : uint8_t { + HAIER_HEADER_UNIT_B = 0x59, + HAIER_HEADER_UNIT_A = 0xA6, +}; + +// Byte 1[7:4] - Temperature (˚C) 0x0..0xE +static constexpr uint8_t HAIER_TEMP_MIN = 16; +static constexpr uint8_t HAIER_TEMP_MAX = 30; + +// Byte 1[3:0] - Vertical Swing +static constexpr uint8_t HAIER_SWING_V_OFF = 0x0; // Power off value; from Auto, stops at current position +static constexpr uint8_t HAIER_SWING_V_2TOP = 0x1; // [|/ ] +static constexpr uint8_t HAIER_SWING_V_1TOP = 0x2; // [| ] (Not available in Heat mode) +static constexpr uint8_t HAIER_SWING_V_2BOT = 0x3; // [ //] +static constexpr uint8_t HAIER_SWING_V_1BOT = 0xA; // [ /] (Heat mode only) +static constexpr uint8_t HAIER_SWING_V_AUTO = 0xC; + +// Byte 2[7:5] - Horizontal Swing +static constexpr uint8_t HAIER_SWING_H_MIDDLE = 0x0; // [ || ] +static constexpr uint8_t HAIER_SWING_H_LEFT = 0x3; // [/ ] +static constexpr uint8_t HAIER_SWING_H_MIDDLE_LEFT = 0x4; // [ | ] +static constexpr uint8_t HAIER_SWING_H_MIDDLE_RIGHT = 0x5; // [ | ] +static constexpr uint8_t HAIER_SWING_H_RIGHT = 0x6; // [ \] +static constexpr uint8_t HAIER_SWING_H_AUTO = 0x7; // [/||\] + +// Byte 3[7:5] - Timer Mode +static constexpr uint8_t HAIER_TIMER_MODE_DISABLED = 0x0; +static constexpr uint8_t HAIER_TIMER_MODE_OFF = 0x1; // OFF +static constexpr uint8_t HAIER_TIMER_MODE_ON = 0x2; // ON +static constexpr uint8_t HAIER_TIMER_MODE_ON_OFF = 0x4; // ON → OFF +static constexpr uint8_t HAIER_TIMER_MODE_OFF_ON = 0x5; // ON ← OFF + +// Byte 3[1] - Health +// Byte 4[6] - Power + +// Byte 5[7:5] - Fan Speed +static constexpr uint8_t HAIER_FAN_HIGH = 0x1; +static constexpr uint8_t HAIER_FAN_MED = 0x2; +static constexpr uint8_t HAIER_FAN_LOW = 0x3; +static constexpr uint8_t HAIER_FAN_AUTO = 0x5; + +// Byte 5[4:0] - Timer Off Hours (0-23) 0x00..0x17 +// Byte 6[7] - Quiet Mode +// Byte 6[6] - Turbo Mode +// Byte 6[5:0] - Timer Off Minutes (0-59) 0x00..0x3B + +// Byte 7[7:5] - Mode +static constexpr uint8_t HAIER_MODE_AUTO = 0x0; +static constexpr uint8_t HAIER_MODE_COOL = 0x1; +static constexpr uint8_t HAIER_MODE_DRY = 0x2; +static constexpr uint8_t HAIER_MODE_HEAT = 0x4; +static constexpr uint8_t HAIER_MODE_FAN = 0x6; + +// Byte 7[4:0] - Timer On Hours (0-23) 0x00..0x17 +// Byte 8[7] - Sleep Mode +// Byte 8[5:0] - Timer On Minutes (0-59) 0x00..0x3B +// Byte 10[4] - Self Clean + +// Byte 12 - Button +static constexpr uint8_t HAIER_BUTTON_TEMP_UP = 0x00; +static constexpr uint8_t HAIER_BUTTON_TEMP_DOWN = 0x01; +static constexpr uint8_t HAIER_BUTTON_SWING_V = 0x02; +static constexpr uint8_t HAIER_BUTTON_SWING_H = 0x03; +static constexpr uint8_t HAIER_BUTTON_FAN_SPEED = 0x04; +static constexpr uint8_t HAIER_BUTTON_POWER = 0x05; +static constexpr uint8_t HAIER_BUTTON_MODE = 0x06; +static constexpr uint8_t HAIER_BUTTON_HEALTH = 0x07; +static constexpr uint8_t HAIER_BUTTON_TURBO_QUIET = 0x08; +static constexpr uint8_t HAIER_BUTTON_SLEEP = 0x0B; +static constexpr uint8_t HAIER_BUTTON_TIMER = 0x10; +static constexpr uint8_t HAIER_BUTTON_LIGHT = 0x15; +static constexpr uint8_t HAIER_BUTTON_SELF_CLEAN = 0x19; + +class HaierTundra final : public climate_ir::ClimateIR { + public: + explicit HaierTundra(Unit unit) + : climate_ir::ClimateIR(HAIER_TEMP_MIN, HAIER_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}), + unit_(unit) {} + +#ifdef USE_SWITCH + SUB_SWITCH(health_mode); + SUB_SWITCH(quiet_mode); + SUB_SWITCH(turbo_mode); +#endif + + void set_health_mode(bool health, bool transmit = true); + void set_quiet_mode(bool quiet, bool transmit = true); + void set_turbo_mode(bool turbo, bool transmit = true); + + void toggle_light(); + void start_self_clean(); + + protected: + // Transmit via IR the state of this climate controller. + void transmit_state() override; + + void transmit_(uint8_t button); + uint8_t temperature_(); + uint8_t vertical_swing_(); + uint8_t horizontal_swing_(); + uint8_t health_(); + uint8_t power_(); + uint8_t fan_speed_(); + uint8_t quiet_(); + uint8_t turbo_(); + uint8_t operation_mode_(); + + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool on_haier_(const remote_base::HaierData &haier); + + float get_temperature_(uint8_t temp); + climate::ClimateSwingMode get_swing_mode_(uint8_t swing_v, uint8_t swing_h); + climate::ClimateMode get_operation_mode_(uint8_t power, uint8_t mode); + climate::ClimateFanMode get_fan_speed_(uint8_t fan); + + Unit unit_; + bool health_mode_{false}; + bool quiet_mode_{false}; + bool turbo_mode_{false}; +}; + +} // namespace esphome::haier_tundra diff --git a/esphome/components/haier_tundra/switch/__init__.py b/esphome/components/haier_tundra/switch/__init__.py new file mode 100644 index 0000000000..f62769c135 --- /dev/null +++ b/esphome/components/haier_tundra/switch/__init__.py @@ -0,0 +1,60 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import DEVICE_CLASS_SWITCH, ENTITY_CATEGORY_CONFIG +from esphome.types import ConfigType + +from ..climate import HaierTundra, haier_tundra_ns + +CODEOWNERS = ["@fauxpark"] + +HaierTundraSwitch = haier_tundra_ns.class_( + "HaierTundraSwitch", switch.Switch, cg.Component +) +SwitchKind = haier_tundra_ns.enum("SwitchKind", is_class=True) + +ICON_LEAF = "mdi:leaf" +ICON_ARROW_DOWN_RIGHT = "mdi:arrow-down-right" +ICON_ARROW_UP_RIGHT_BOLD = "mdi:arrow-up-right-bold" + +CONF_HAIER_TUNDRA_ID = "haier_tundra_id" +CONF_HEALTH_MODE = "health_mode" +CONF_QUIET_MODE = "quiet_mode" +CONF_TURBO_MODE = "turbo_mode" + + +def _switch_schema(icon: str) -> cv.Schema: + return switch.switch_schema( + HaierTundraSwitch, + icon=icon, + default_restore_mode="RESTORE_DEFAULT_OFF", + device_class=DEVICE_CLASS_SWITCH, + entity_category=ENTITY_CATEGORY_CONFIG, + ) + + +CONFIG_SCHEMA = cv.Schema( + { + cv.Required(CONF_HAIER_TUNDRA_ID): cv.use_id(HaierTundra), + cv.Optional(CONF_HEALTH_MODE): _switch_schema(ICON_LEAF), + cv.Optional(CONF_QUIET_MODE): _switch_schema(ICON_ARROW_DOWN_RIGHT), + cv.Optional(CONF_TURBO_MODE): _switch_schema(ICON_ARROW_UP_RIGHT_BOLD), + } +) + + +async def to_code(config: ConfigType) -> None: + hub = await cg.get_variable(config[CONF_HAIER_TUNDRA_ID]) + switches = switch.sub_switches(config) + if sw := await switches( + CONF_HEALTH_MODE, hub.set_health_mode_switch, hub, SwitchKind.SWITCH_KIND_HEALTH + ): + await cg.register_component(sw, config[CONF_HEALTH_MODE]) + if sw := await switches( + CONF_QUIET_MODE, hub.set_quiet_mode_switch, hub, SwitchKind.SWITCH_KIND_QUIET + ): + await cg.register_component(sw, config[CONF_QUIET_MODE]) + if sw := await switches( + CONF_TURBO_MODE, hub.set_turbo_mode_switch, hub, SwitchKind.SWITCH_KIND_TURBO + ): + await cg.register_component(sw, config[CONF_TURBO_MODE]) diff --git a/esphome/components/haier_tundra/switch/haier_tundra_switch.cpp b/esphome/components/haier_tundra/switch/haier_tundra_switch.cpp new file mode 100644 index 0000000000..e24ada79b2 --- /dev/null +++ b/esphome/components/haier_tundra/switch/haier_tundra_switch.cpp @@ -0,0 +1,43 @@ +#include "haier_tundra_switch.h" +#include "esphome/core/log.h" + +namespace esphome::haier_tundra { + +ESPHOME_LOG_TAG(TAG, "haier_tundra.switch"); + +void HaierTundraSwitch::setup() { + auto initial = this->get_initial_state_with_restore_mode(); + if (initial.has_value()) { + const bool state = initial.value(); + this->apply_(state, false); + this->publish_state(state); + } +} + +void HaierTundraSwitch::dump_config() { LOG_SWITCH("", "Haier Tundra Switch", this); } + +void HaierTundraSwitch::write_state(bool state) { + // Quiet and Turbo can only be turned on in Cool or Heat mode + if (state && this->kind_ != SwitchKind::SWITCH_KIND_HEALTH && this->parent_->mode != climate::CLIMATE_MODE_COOL && + this->parent_->mode != climate::CLIMATE_MODE_HEAT) { + return; + } + this->apply_(state, true); + this->publish_state(state); +} + +void HaierTundraSwitch::apply_(bool state, bool transmit) { + switch (this->kind_) { + case SwitchKind::SWITCH_KIND_HEALTH: + this->parent_->set_health_mode(state, transmit); + break; + case SwitchKind::SWITCH_KIND_QUIET: + this->parent_->set_quiet_mode(state, transmit); + break; + case SwitchKind::SWITCH_KIND_TURBO: + this->parent_->set_turbo_mode(state, transmit); + break; + } +} + +} // namespace esphome::haier_tundra diff --git a/esphome/components/haier_tundra/switch/haier_tundra_switch.h b/esphome/components/haier_tundra/switch/haier_tundra_switch.h new file mode 100644 index 0000000000..d718b940a1 --- /dev/null +++ b/esphome/components/haier_tundra/switch/haier_tundra_switch.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/switch/switch.h" +#include "esphome/components/haier_tundra/haier_tundra.h" + +namespace esphome::haier_tundra { + +enum class SwitchKind : uint8_t { + SWITCH_KIND_HEALTH, + SWITCH_KIND_QUIET, + SWITCH_KIND_TURBO, +}; + +class HaierTundraSwitch final : public switch_::Switch, public Component, public Parented { + public: + HaierTundraSwitch(HaierTundra *parent, SwitchKind kind) : Parented(parent), kind_(kind) {} + void setup() override; + void dump_config() override; + + protected: + void write_state(bool state) override; + void apply_(bool state, bool transmit); + + SwitchKind kind_; +}; + +} // namespace esphome::haier_tundra diff --git a/tests/components/haier_tundra/common.yaml b/tests/components/haier_tundra/common.yaml new file mode 100644 index 0000000000..0388220cac --- /dev/null +++ b/tests/components/haier_tundra/common.yaml @@ -0,0 +1,45 @@ +climate: + - platform: haier_tundra + name: Haier AC A + id: my_haier_ac_a + transmitter_id: xmitr + + - platform: haier_tundra + name: Haier AC B + id: my_haier_ac_b + transmitter_id: xmitr + unit: b + +switch: + - platform: haier_tundra + haier_tundra_id: my_haier_ac_a + quiet_mode: + name: "Quiet Mode (Unit A)" + turbo_mode: + name: "Turbo Mode (Unit A)" + health_mode: + name: "Health Mode (Unit A)" + + - platform: haier_tundra + haier_tundra_id: my_haier_ac_b + quiet_mode: + name: "Quiet Mode (Unit B)" + turbo_mode: + name: "Turbo Mode (Unit B)" + health_mode: + name: "Health Mode (Unit B)" + +button: + - platform: haier_tundra + haier_tundra_id: my_haier_ac_a + light: + name: "Toggle Light (Unit A)" + self_clean: + name: "Start Self Clean (Unit A)" + + - platform: haier_tundra + haier_tundra_id: my_haier_ac_b + light: + name: "Toggle Light (Unit B)" + self_clean: + name: "Start Self Clean (Unit B)" diff --git a/tests/components/haier_tundra/test.esp32-idf.yaml b/tests/components/haier_tundra/test.esp32-idf.yaml new file mode 100644 index 0000000000..b88eb95efe --- /dev/null +++ b/tests/components/haier_tundra/test.esp32-idf.yaml @@ -0,0 +1,3 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml + haier_tundra: !include common.yaml diff --git a/tests/components/haier_tundra/test.esp8266-ard.yaml b/tests/components/haier_tundra/test.esp8266-ard.yaml new file mode 100644 index 0000000000..888edbf140 --- /dev/null +++ b/tests/components/haier_tundra/test.esp8266-ard.yaml @@ -0,0 +1,3 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml + haier_tundra: !include common.yaml