diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py index 06c80964ee..471a8146e1 100644 --- a/esphome/components/tuya/climate/__init__.py +++ b/esphome/components/tuya/climate/__init__.py @@ -1,3 +1,4 @@ +from esphome import pins from esphome.components import climate import esphome.config_validation as cv import esphome.codegen as cg @@ -15,6 +16,8 @@ CODEOWNERS = ["@jesserockz"] CONF_ACTIVE_STATE_DATAPOINT = "active_state_datapoint" CONF_ACTIVE_STATE_HEATING_VALUE = "active_state_heating_value" CONF_ACTIVE_STATE_COOLING_VALUE = "active_state_cooling_value" +CONF_HEATING_STATE_PIN = "heating_state_pin" +CONF_COOLING_STATE_PIN = "cooling_state_pin" CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint" CONF_CURRENT_TEMPERATURE_DATAPOINT = "current_temperature_datapoint" CONF_TEMPERATURE_MULTIPLIER = "temperature_multiplier" @@ -69,14 +72,21 @@ def validate_temperature_multipliers(value): def validate_active_state_values(value): if CONF_ACTIVE_STATE_DATAPOINT not in value: - return value - if value[CONF_SUPPORTS_COOL] and CONF_ACTIVE_STATE_COOLING_VALUE not in value: - raise cv.Invalid( - ( - f"{CONF_ACTIVE_STATE_COOLING_VALUE} required if using " - f"{CONF_ACTIVE_STATE_DATAPOINT} and device supports cooling" + if CONF_ACTIVE_STATE_COOLING_VALUE in value: + raise cv.Invalid( + ( + f"{CONF_ACTIVE_STATE_DATAPOINT} required if using " + f"{CONF_ACTIVE_STATE_COOLING_VALUE}" + ) + ) + else: + if value[CONF_SUPPORTS_COOL] and CONF_ACTIVE_STATE_COOLING_VALUE not in value: + raise cv.Invalid( + ( + f"{CONF_ACTIVE_STATE_COOLING_VALUE} required if using " + f"{CONF_ACTIVE_STATE_DATAPOINT} and device supports cooling" + ) ) - ) return value @@ -91,6 +101,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ACTIVE_STATE_DATAPOINT): cv.uint8_t, cv.Optional(CONF_ACTIVE_STATE_HEATING_VALUE, default=1): cv.uint8_t, cv.Optional(CONF_ACTIVE_STATE_COOLING_VALUE): cv.uint8_t, + cv.Optional(CONF_HEATING_STATE_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_COOLING_STATE_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_TARGET_TEMPERATURE_DATAPOINT): cv.uint8_t, cv.Optional(CONF_CURRENT_TEMPERATURE_DATAPOINT): cv.uint8_t, cv.Optional(CONF_TEMPERATURE_MULTIPLIER): cv.positive_float, @@ -101,6 +113,8 @@ CONFIG_SCHEMA = cv.All( cv.has_at_least_one_key(CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT), validate_temperature_multipliers, validate_active_state_values, + cv.has_at_most_one_key(CONF_ACTIVE_STATE_DATAPOINT, CONF_HEATING_STATE_PIN), + cv.has_at_most_one_key(CONF_ACTIVE_STATE_DATAPOINT, CONF_COOLING_STATE_PIN), ) @@ -118,14 +132,29 @@ async def to_code(config): cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) if CONF_ACTIVE_STATE_DATAPOINT in config: cg.add(var.set_active_state_id(config[CONF_ACTIVE_STATE_DATAPOINT])) - if CONF_ACTIVE_STATE_HEATING_VALUE in config: - cg.add( - var.set_active_state_heating_value(config[CONF_ACTIVE_STATE_HEATING_VALUE]) - ) - if CONF_ACTIVE_STATE_COOLING_VALUE in config: - cg.add( - var.set_active_state_cooling_value(config[CONF_ACTIVE_STATE_COOLING_VALUE]) - ) + if CONF_ACTIVE_STATE_HEATING_VALUE in config: + cg.add( + var.set_active_state_heating_value( + config[CONF_ACTIVE_STATE_HEATING_VALUE] + ) + ) + if CONF_ACTIVE_STATE_COOLING_VALUE in config: + cg.add( + var.set_active_state_cooling_value( + config[CONF_ACTIVE_STATE_COOLING_VALUE] + ) + ) + else: + if CONF_HEATING_STATE_PIN in config: + heating_state_pin = await cg.gpio_pin_expression( + config[CONF_HEATING_STATE_PIN] + ) + cg.add(var.set_heating_state_pin(heating_state_pin)) + if CONF_COOLING_STATE_PIN in config: + cooling_state_pin = await cg.gpio_pin_expression( + config[CONF_COOLING_STATE_PIN] + ) + cg.add(var.set_cooling_state_pin(cooling_state_pin)) if CONF_TARGET_TEMPERATURE_DATAPOINT in config: cg.add(var.set_target_temperature_id(config[CONF_TARGET_TEMPERATURE_DATAPOINT])) if CONF_CURRENT_TEMPERATURE_DATAPOINT in config: diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index a4ce06476d..ae4424e438 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -31,6 +31,15 @@ void TuyaClimate::setup() { this->compute_state_(); this->publish_state(); }); + } else { + if (this->heating_state_pin_ != nullptr) { + this->heating_state_pin_->setup(); + this->heating_state_ = this->heating_state_pin_->digital_read(); + } + if (this->cooling_state_pin_ != nullptr) { + this->cooling_state_pin_->setup(); + this->cooling_state_ = this->cooling_state_pin_->digital_read(); + } } if (this->target_temperature_id_.has_value()) { this->parent_->register_listener(*this->target_temperature_id_, [this](const TuyaDatapoint &datapoint) { @@ -50,6 +59,34 @@ void TuyaClimate::setup() { } } +void TuyaClimate::loop() { + if (this->active_state_id_.has_value()) + return; + + bool state_changed = false; + if (this->heating_state_pin_ != nullptr) { + bool heating_state = this->heating_state_pin_->digital_read(); + if (heating_state != this->heating_state_) { + ESP_LOGV(TAG, "Heating state pin changed to: %s", ONOFF(heating_state)); + this->heating_state_ = heating_state; + state_changed = true; + } + } + if (this->cooling_state_pin_ != nullptr) { + bool cooling_state = this->cooling_state_pin_->digital_read(); + if (cooling_state != this->cooling_state_) { + ESP_LOGV(TAG, "Cooling state pin changed to: %s", ONOFF(cooling_state)); + this->cooling_state_ = cooling_state; + state_changed = true; + } + } + + if (state_changed) { + this->compute_state_(); + this->publish_state(); + } +} + void TuyaClimate::control(const climate::ClimateCall &call) { if (call.get_mode().has_value()) { const bool switch_state = *call.get_mode() != climate::CLIMATE_MODE_OFF; @@ -86,6 +123,8 @@ void TuyaClimate::dump_config() { ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *this->target_temperature_id_); if (this->current_temperature_id_.has_value()) ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *this->current_temperature_id_); + LOG_PIN(" Heating State Pin: ", this->heating_state_pin_); + LOG_PIN(" Cooling State Pin: ", this->cooling_state_pin_); } void TuyaClimate::compute_state_() { @@ -102,6 +141,7 @@ void TuyaClimate::compute_state_() { climate::ClimateAction target_action = climate::CLIMATE_ACTION_IDLE; if (this->active_state_id_.has_value()) { + // Use state from MCU datapoint if (this->supports_heat_ && this->active_state_heating_value_.has_value() && this->active_state_ == this->active_state_heating_value_) { target_action = climate::CLIMATE_ACTION_HEATING; @@ -109,6 +149,13 @@ void TuyaClimate::compute_state_() { this->active_state_ == this->active_state_cooling_value_) { target_action = climate::CLIMATE_ACTION_COOLING; } + } else if (this->heating_state_pin_ != nullptr || this->cooling_state_pin_ != nullptr) { + // Use state from input pins + if (this->heating_state_) { + target_action = climate::CLIMATE_ACTION_HEATING; + } else if (this->cooling_state_) { + target_action = climate::CLIMATE_ACTION_COOLING; + } } else { // Fallback to active state calc based on temp and hysteresis const float temp_diff = this->target_temperature - this->current_temperature; diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h index f015bc337c..f1a0c13a77 100644 --- a/esphome/components/tuya/climate/tuya_climate.h +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -10,6 +10,7 @@ namespace tuya { class TuyaClimate : public climate::Climate, public Component { public: void setup() override; + void loop() override; void dump_config() override; void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } @@ -17,6 +18,8 @@ class TuyaClimate : public climate::Climate, public Component { void set_active_state_id(uint8_t state_id) { this->active_state_id_ = state_id; } void set_active_state_heating_value(uint8_t value) { this->active_state_heating_value_ = value; } void set_active_state_cooling_value(uint8_t value) { this->active_state_cooling_value_ = value; } + void set_heating_state_pin(GPIOPin *pin) { this->heating_state_pin_ = pin; } + void set_cooling_state_pin(GPIOPin *pin) { this->cooling_state_pin_ = pin; } void set_target_temperature_id(uint8_t target_temperature_id) { this->target_temperature_id_ = target_temperature_id; } @@ -51,12 +54,16 @@ class TuyaClimate : public climate::Climate, public Component { optional active_state_id_{}; optional active_state_heating_value_{}; optional active_state_cooling_value_{}; + GPIOPin *heating_state_pin_{nullptr}; + GPIOPin *cooling_state_pin_{nullptr}; optional target_temperature_id_{}; optional current_temperature_id_{}; float current_temperature_multiplier_{1.0f}; float target_temperature_multiplier_{1.0f}; float hysteresis_{1.0f}; uint8_t active_state_; + bool heating_state_{false}; + bool cooling_state_{false}; }; } // namespace tuya