mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Add support for new modes in Tuya Climate (#5159)
* Add support support for new modes Added support for Fan Only Mode, Dry Mode, Swing Mode and Fan Speed Control. Also added/fixed support for entity states syncing with current operation mode. * Add support for more climate modes in climate.tuya Added support for Fan Only Mode, Dry Mode, Swing Mode and Fan Speed Control. Also added/fixed support for entity states syncing with current operation mode. This commit fixes the namespace, because I uploaded the test files to start with. * Code Formatting Changes per Clang format. * More clang formatting fixes. * Breaking Change: Group YAML entries by type Add grouping to Preset, Swing Mode, Fan Speed and Active State. This is a breaking change. * Formatting Changes for validation Formatting changes to be compliant with black and flake8. Also changed constants to match expected format. * More constant value fixes * Final black formatting check? * Changes to init.py according to reviewer requests Make changes to _init_.py according to649b923804 (r1278620976),649b923804 (r1278621039),649b923804 (r1278620904), and649b923804 (r1278620549)Also put Sleep preset in its own config block to be consistent with other presets and fix logic for validate_cooling_values function to better align with existing documentation. * Commit reviewed change Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * update deprecated config option wording * add "this->" to member variables that were missed adding "this->" to some member variables in the swing_mode function. * Update _init_.py to use Python 3.8 Walrus operator Adding Walrus Operator in the to_code function for _init_.py similar to https://github.com/esphome/esphome/pull/5181 * Fix Temperature_Multiplier config entry for code generation --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
This commit is contained in:
		| @@ -7,15 +7,22 @@ from esphome.const import ( | ||||
|     CONF_SWITCH_DATAPOINT, | ||||
|     CONF_SUPPORTS_COOL, | ||||
|     CONF_SUPPORTS_HEAT, | ||||
|     CONF_PRESET, | ||||
|     CONF_SWING_MODE, | ||||
|     CONF_FAN_MODE, | ||||
|     CONF_TEMPERATURE, | ||||
| ) | ||||
| from .. import tuya_ns, CONF_TUYA_ID, Tuya | ||||
|  | ||||
| DEPENDENCIES = ["tuya"] | ||||
| 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_ACTIVE_STATE = "active_state" | ||||
| CONF_DATAPOINT = "datapoint" | ||||
| CONF_HEATING_VALUE = "heating_value" | ||||
| CONF_COOLING_VALUE = "cooling_value" | ||||
| CONF_DRYING_VALUE = "drying_value" | ||||
| CONF_FANONLY_VALUE = "fanonly_value" | ||||
| CONF_HEATING_STATE_PIN = "heating_state_pin" | ||||
| CONF_COOLING_STATE_PIN = "cooling_state_pin" | ||||
| CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint" | ||||
| @@ -23,9 +30,17 @@ CONF_CURRENT_TEMPERATURE_DATAPOINT = "current_temperature_datapoint" | ||||
| CONF_TEMPERATURE_MULTIPLIER = "temperature_multiplier" | ||||
| CONF_CURRENT_TEMPERATURE_MULTIPLIER = "current_temperature_multiplier" | ||||
| CONF_TARGET_TEMPERATURE_MULTIPLIER = "target_temperature_multiplier" | ||||
| CONF_ECO_DATAPOINT = "eco_datapoint" | ||||
| CONF_ECO_TEMPERATURE = "eco_temperature" | ||||
| CONF_ECO = "eco" | ||||
| CONF_SLEEP = "sleep" | ||||
| CONF_SLEEP_DATAPOINT = "sleep_datapoint" | ||||
| CONF_REPORTS_FAHRENHEIT = "reports_fahrenheit" | ||||
| CONF_VERTICAL_DATAPOINT = "vertical_datapoint" | ||||
| CONF_HORIZONTAL_DATAPOINT = "horizontal_datapoint" | ||||
| CONF_LOW_VALUE = "low_value" | ||||
| CONF_MEDIUM_VALUE = "medium_value" | ||||
| CONF_MIDDLE_VALUE = "middle_value" | ||||
| CONF_HIGH_VALUE = "high_value" | ||||
| CONF_AUTO_VALUE = "auto_value" | ||||
|  | ||||
| TuyaClimate = tuya_ns.class_("TuyaClimate", climate.Climate, cg.Component) | ||||
|  | ||||
| @@ -67,30 +82,73 @@ def validate_temperature_multipliers(value): | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def validate_active_state_values(value): | ||||
|     if CONF_ACTIVE_STATE_DATAPOINT not in value: | ||||
|         if CONF_ACTIVE_STATE_COOLING_VALUE in value: | ||||
| def validate_cooling_values(value): | ||||
|     if CONF_SUPPORTS_COOL in value: | ||||
|         cooling_supported = value[CONF_SUPPORTS_COOL] | ||||
|         if not cooling_supported and CONF_ACTIVE_STATE in value: | ||||
|             active_state_config = value[CONF_ACTIVE_STATE] | ||||
|             if ( | ||||
|                 CONF_COOLING_VALUE in active_state_config | ||||
|                 or CONF_COOLING_STATE_PIN in value | ||||
|             ): | ||||
|                 raise cv.Invalid( | ||||
|                 f"{CONF_ACTIVE_STATE_DATAPOINT} required if using " | ||||
|                 f"{CONF_ACTIVE_STATE_COOLING_VALUE}" | ||||
|                     f"Device does not support cooling, but {CONF_COOLING_VALUE} or {CONF_COOLING_STATE_PIN} specified." | ||||
|                     f" Please add '{CONF_SUPPORTS_COOL}: true' to your configuration." | ||||
|                 ) | ||||
|     else: | ||||
|         if value[CONF_SUPPORTS_COOL] and CONF_ACTIVE_STATE_COOLING_VALUE not in value: | ||||
|         elif cooling_supported and CONF_ACTIVE_STATE in value: | ||||
|             active_state_config = value[CONF_ACTIVE_STATE] | ||||
|             if ( | ||||
|                 CONF_COOLING_VALUE not in active_state_config | ||||
|                 and CONF_COOLING_STATE_PIN not in value | ||||
|             ): | ||||
|                 raise cv.Invalid( | ||||
|                 f"{CONF_ACTIVE_STATE_COOLING_VALUE} required if using " | ||||
|                 f"{CONF_ACTIVE_STATE_DATAPOINT} and device supports cooling" | ||||
|                     f"Either {CONF_ACTIVE_STATE} {CONF_COOLING_VALUE} or {CONF_COOLING_STATE_PIN} is required if" | ||||
|                     f" {CONF_SUPPORTS_COOL}: true' is in your configuration." | ||||
|                 ) | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def validate_eco_values(value): | ||||
|     if CONF_ECO_TEMPERATURE in value and CONF_ECO_DATAPOINT not in value: | ||||
|         raise cv.Invalid( | ||||
|             f"{CONF_ECO_DATAPOINT} required if using {CONF_ECO_TEMPERATURE}" | ||||
|         ) | ||||
|     return value | ||||
| ACTIVE_STATES = cv.Schema( | ||||
|     { | ||||
|         cv.Required(CONF_DATAPOINT): cv.uint8_t, | ||||
|         cv.Optional(CONF_HEATING_VALUE, default=1): cv.uint8_t, | ||||
|         cv.Optional(CONF_COOLING_VALUE): cv.uint8_t, | ||||
|         cv.Optional(CONF_DRYING_VALUE): cv.uint8_t, | ||||
|         cv.Optional(CONF_FANONLY_VALUE): cv.uint8_t, | ||||
|     }, | ||||
| ) | ||||
|  | ||||
|  | ||||
| PRESETS = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_ECO): { | ||||
|             cv.Required(CONF_DATAPOINT): cv.uint8_t, | ||||
|             cv.Optional(CONF_TEMPERATURE): cv.temperature, | ||||
|         }, | ||||
|         cv.Optional(CONF_SLEEP): { | ||||
|             cv.Required(CONF_DATAPOINT): cv.uint8_t, | ||||
|         }, | ||||
|     }, | ||||
| ) | ||||
|  | ||||
| FAN_MODES = cv.Schema( | ||||
|     { | ||||
|         cv.Required(CONF_DATAPOINT): cv.uint8_t, | ||||
|         cv.Optional(CONF_AUTO_VALUE): cv.uint8_t, | ||||
|         cv.Optional(CONF_LOW_VALUE): cv.uint8_t, | ||||
|         cv.Optional(CONF_MEDIUM_VALUE): cv.uint8_t, | ||||
|         cv.Optional(CONF_MIDDLE_VALUE): cv.uint8_t, | ||||
|         cv.Optional(CONF_HIGH_VALUE): cv.uint8_t, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| SWING_MODES = cv.Schema( | ||||
|     { | ||||
|         cv.Optional(CONF_VERTICAL_DATAPOINT): cv.uint8_t, | ||||
|         cv.Optional(CONF_HORIZONTAL_DATAPOINT): cv.uint8_t, | ||||
|     }, | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     climate.CLIMATE_SCHEMA.extend( | ||||
|         { | ||||
| @@ -99,9 +157,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|             cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, | ||||
|             cv.Optional(CONF_SUPPORTS_COOL, default=False): cv.boolean, | ||||
|             cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, | ||||
|             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_ACTIVE_STATE): ACTIVE_STATES, | ||||
|             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, | ||||
| @@ -109,17 +165,32 @@ CONFIG_SCHEMA = cv.All( | ||||
|             cv.Optional(CONF_TEMPERATURE_MULTIPLIER): cv.positive_float, | ||||
|             cv.Optional(CONF_CURRENT_TEMPERATURE_MULTIPLIER): cv.positive_float, | ||||
|             cv.Optional(CONF_TARGET_TEMPERATURE_MULTIPLIER): cv.positive_float, | ||||
|             cv.Optional(CONF_ECO_DATAPOINT): cv.uint8_t, | ||||
|             cv.Optional(CONF_ECO_TEMPERATURE): cv.temperature, | ||||
|             cv.Optional(CONF_REPORTS_FAHRENHEIT, default=False): cv.boolean, | ||||
|             cv.Optional(CONF_PRESET): PRESETS, | ||||
|             cv.Optional(CONF_FAN_MODE): FAN_MODES, | ||||
|             cv.Optional(CONF_SWING_MODE): SWING_MODES, | ||||
|             cv.Optional("active_state_datapoint"): cv.invalid( | ||||
|                 "'active_state_datapoint' has been moved inside of the 'active_state' config block as 'datapoint'" | ||||
|             ), | ||||
|             cv.Optional("active_state_heating_value"): cv.invalid( | ||||
|                 "'active_state_heating_value' has been moved inside of the 'active_state' config block as 'heating_value'" | ||||
|             ), | ||||
|             cv.Optional("active_state_cooling_value"): cv.invalid( | ||||
|                 "'active_state_cooling_value' has been moved inside of the 'active_state' config block as 'cooling_value'" | ||||
|             ), | ||||
|             cv.Optional("eco_datapoint"): cv.invalid( | ||||
|                 "'eco_datapoint' has been moved inside of the 'eco' config block under 'preset' as 'datapoint'" | ||||
|             ), | ||||
|             cv.Optional("eco_temperature"): cv.invalid( | ||||
|                 "'eco_temperature' has been moved inside of the 'eco' config block under 'preset' as 'temperature'" | ||||
|             ), | ||||
|         } | ||||
|     ).extend(cv.COMPONENT_SCHEMA), | ||||
|     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), | ||||
|     validate_eco_values, | ||||
|     validate_cooling_values, | ||||
|     cv.has_at_most_one_key(CONF_ACTIVE_STATE, CONF_HEATING_STATE_PIN), | ||||
|     cv.has_at_most_one_key(CONF_ACTIVE_STATE, CONF_COOLING_STATE_PIN), | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -133,61 +204,78 @@ async def to_code(config): | ||||
|  | ||||
|     cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) | ||||
|     cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) | ||||
|     if CONF_SWITCH_DATAPOINT in 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 switch_datapoint := config.get(CONF_SWITCH_DATAPOINT): | ||||
|         cg.add(var.set_switch_id(switch_datapoint)) | ||||
|  | ||||
|     if active_state_config := config.get(CONF_ACTIVE_STATE): | ||||
|         cg.add(var.set_active_state_id(CONF_DATAPOINT)) | ||||
|         if (heating_value := active_state_config.get(CONF_HEATING_VALUE)) is not None: | ||||
|             cg.add(var.set_active_state_heating_value(heating_value)) | ||||
|         if (cooling_value := active_state_config.get(CONF_COOLING_VALUE)) is not None: | ||||
|             cg.add(var.set_active_state_cooling_value(cooling_value)) | ||||
|         if (drying_value := active_state_config.get(CONF_DRYING_VALUE)) is not None: | ||||
|             cg.add(var.set_active_state_drying_value(drying_value)) | ||||
|         if (fanonly_value := active_state_config.get(CONF_FANONLY_VALUE)) is not None: | ||||
|             cg.add(var.set_active_state_fanonly_value(fanonly_value)) | ||||
|     else: | ||||
|         if CONF_HEATING_STATE_PIN in config: | ||||
|         if heating_state_pin_config := config.get(CONF_HEATING_STATE_PIN): | ||||
|             heating_state_pin = await cg.gpio_pin_expression( | ||||
|                 config[CONF_HEATING_STATE_PIN] | ||||
|                 config(heating_state_pin_config) | ||||
|             ) | ||||
|             cg.add(var.set_heating_state_pin(heating_state_pin)) | ||||
|         if CONF_COOLING_STATE_PIN in config: | ||||
|         if cooling_state_pin_config := config.get(CONF_COOLING_STATE_PIN): | ||||
|             cooling_state_pin = await cg.gpio_pin_expression( | ||||
|                 config[CONF_COOLING_STATE_PIN] | ||||
|                 config(cooling_state_pin_config) | ||||
|             ) | ||||
|             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: | ||||
|         cg.add( | ||||
|             var.set_current_temperature_id(config[CONF_CURRENT_TEMPERATURE_DATAPOINT]) | ||||
|         ) | ||||
|     if CONF_TEMPERATURE_MULTIPLIER in config: | ||||
|         cg.add( | ||||
|             var.set_target_temperature_multiplier(config[CONF_TEMPERATURE_MULTIPLIER]) | ||||
|         ) | ||||
|         cg.add( | ||||
|             var.set_current_temperature_multiplier(config[CONF_TEMPERATURE_MULTIPLIER]) | ||||
|         ) | ||||
|  | ||||
|     if target_temperature_datapoint := config.get(CONF_TARGET_TEMPERATURE_DATAPOINT): | ||||
|         cg.add(var.set_target_temperature_id(target_temperature_datapoint)) | ||||
|     if current_temperature_datapoint := config.get(CONF_CURRENT_TEMPERATURE_DATAPOINT): | ||||
|         cg.add(var.set_current_temperature_id(current_temperature_datapoint)) | ||||
|  | ||||
|     if temperature_multiplier := config.get(CONF_TEMPERATURE_MULTIPLIER): | ||||
|         cg.add(var.set_target_temperature_multiplier(temperature_multiplier)) | ||||
|         cg.add(var.set_current_temperature_multiplier(temperature_multiplier)) | ||||
|     else: | ||||
|         if current_temperature_multiplier := config.get( | ||||
|             CONF_CURRENT_TEMPERATURE_MULTIPLIER | ||||
|         ): | ||||
|             cg.add( | ||||
|             var.set_current_temperature_multiplier( | ||||
|                 config[CONF_CURRENT_TEMPERATURE_MULTIPLIER] | ||||
|                 var.set_current_temperature_multiplier(current_temperature_multiplier) | ||||
|             ) | ||||
|         ) | ||||
|         cg.add( | ||||
|             var.set_target_temperature_multiplier( | ||||
|                 config[CONF_TARGET_TEMPERATURE_MULTIPLIER] | ||||
|             ) | ||||
|         ) | ||||
|     if CONF_ECO_DATAPOINT in config: | ||||
|         cg.add(var.set_eco_id(config[CONF_ECO_DATAPOINT])) | ||||
|         if CONF_ECO_TEMPERATURE in config: | ||||
|             cg.add(var.set_eco_temperature(config[CONF_ECO_TEMPERATURE])) | ||||
|         if target_temperature_multiplier := config.get( | ||||
|             CONF_TARGET_TEMPERATURE_MULTIPLIER | ||||
|         ): | ||||
|             cg.add(var.set_target_temperature_multiplier(target_temperature_multiplier)) | ||||
|  | ||||
|     if config[CONF_REPORTS_FAHRENHEIT]: | ||||
|         cg.add(var.set_reports_fahrenheit()) | ||||
|  | ||||
|     if preset_config := config.get(CONF_PRESET, {}): | ||||
|         if eco_config := preset_config.get(CONF_ECO, {}): | ||||
|             cg.add(var.set_eco_id(CONF_DATAPOINT)) | ||||
|             if eco_temperature := eco_config.get(CONF_TEMPERATURE): | ||||
|                 cg.add(var.set_eco_temperature(eco_temperature)) | ||||
|         if CONF_SLEEP in preset_config: | ||||
|             cg.add(var.set_sleep_id(CONF_DATAPOINT)) | ||||
|  | ||||
|     if swing_mode_config := config.get(CONF_SWING_MODE): | ||||
|         if swing_vertical_datapoint := swing_mode_config.get(CONF_VERTICAL_DATAPOINT): | ||||
|             cg.add(var.set_swing_vertical_id(swing_vertical_datapoint)) | ||||
|         if swing_horizontal_datapoint := swing_mode_config.get( | ||||
|             CONF_HORIZONTAL_DATAPOINT | ||||
|         ): | ||||
|             cg.add(var.set_swing_horizontal_id(swing_horizontal_datapoint)) | ||||
|     if fan_mode_config := config.get(CONF_FAN_MODE): | ||||
|         cg.add(var.set_fan_speed_id(CONF_DATAPOINT)) | ||||
|         if (fan_auto_value := fan_mode_config.get(CONF_AUTO_VALUE)) is not None: | ||||
|             cg.add(var.set_fan_speed_auto_value(fan_auto_value)) | ||||
|         if (fan_low_value := fan_mode_config.get(CONF_LOW_VALUE)) is not None: | ||||
|             cg.add(var.set_fan_speed_low_value(fan_low_value)) | ||||
|         if (fan_medium_value := fan_mode_config.get(CONF_MEDIUM_VALUE)) is not None: | ||||
|             cg.add(var.set_fan_speed_medium_value(fan_medium_value)) | ||||
|         if (fan_middle_value := fan_mode_config.get(CONF_MIDDLE_VALUE)) is not None: | ||||
|             cg.add(var.set_fan_speed_middle_value(fan_middle_value)) | ||||
|         if (fan_high_value := fan_mode_config.get(CONF_HIGH_VALUE)) is not None: | ||||
|             cg.add(var.set_fan_speed_high_value(fan_high_value)) | ||||
|   | ||||
| @@ -75,6 +75,41 @@ void TuyaClimate::setup() { | ||||
|       this->publish_state(); | ||||
|     }); | ||||
|   } | ||||
|   if (this->sleep_id_.has_value()) { | ||||
|     this->parent_->register_listener(*this->sleep_id_, [this](const TuyaDatapoint &datapoint) { | ||||
|       this->sleep_ = datapoint.value_bool; | ||||
|       ESP_LOGV(TAG, "MCU reported sleep is: %s", ONOFF(this->sleep_)); | ||||
|       this->compute_preset_(); | ||||
|       this->compute_target_temperature_(); | ||||
|       this->publish_state(); | ||||
|     }); | ||||
|   } | ||||
|   if (this->swing_vertical_id_.has_value()) { | ||||
|     this->parent_->register_listener(*this->swing_vertical_id_, [this](const TuyaDatapoint &datapoint) { | ||||
|       this->swing_vertical_ = datapoint.value_bool; | ||||
|       ESP_LOGV(TAG, "MCU reported vertical swing is: %s", ONOFF(datapoint.value_bool)); | ||||
|       this->compute_swingmode_(); | ||||
|       this->publish_state(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   if (this->swing_horizontal_id_.has_value()) { | ||||
|     this->parent_->register_listener(*this->swing_horizontal_id_, [this](const TuyaDatapoint &datapoint) { | ||||
|       this->swing_horizontal_ = datapoint.value_bool; | ||||
|       ESP_LOGV(TAG, "MCU reported horizontal swing is: %s", ONOFF(datapoint.value_bool)); | ||||
|       this->compute_swingmode_(); | ||||
|       this->publish_state(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   if (this->fan_speed_id_.has_value()) { | ||||
|     this->parent_->register_listener(*this->fan_speed_id_, [this](const TuyaDatapoint &datapoint) { | ||||
|       ESP_LOGV(TAG, "MCU reported Fan Speed Mode is: %u", datapoint.value_enum); | ||||
|       this->fan_state_ = datapoint.value_enum; | ||||
|       this->compute_fanmode_(); | ||||
|       this->publish_state(); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TuyaClimate::loop() { | ||||
| @@ -110,7 +145,21 @@ void TuyaClimate::control(const climate::ClimateCall &call) { | ||||
|     const bool switch_state = *call.get_mode() != climate::CLIMATE_MODE_OFF; | ||||
|     ESP_LOGV(TAG, "Setting switch: %s", ONOFF(switch_state)); | ||||
|     this->parent_->set_boolean_datapoint_value(*this->switch_id_, switch_state); | ||||
|     const climate::ClimateMode new_mode = *call.get_mode(); | ||||
|  | ||||
|     if (new_mode == climate::CLIMATE_MODE_HEAT && this->supports_heat_) { | ||||
|       this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_heating_value_); | ||||
|     } else if (new_mode == climate::CLIMATE_MODE_COOL && this->supports_cool_) { | ||||
|       this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_cooling_value_); | ||||
|     } else if (new_mode == climate::CLIMATE_MODE_DRY && this->active_state_drying_value_.has_value()) { | ||||
|       this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_drying_value_); | ||||
|     } else if (new_mode == climate::CLIMATE_MODE_FAN_ONLY && this->active_state_fanonly_value_.has_value()) { | ||||
|       this->parent_->set_enum_datapoint_value(*this->active_state_id_, *this->active_state_fanonly_value_); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   control_swing_mode_(call); | ||||
|   control_fan_mode_(call); | ||||
|  | ||||
|   if (call.get_target_temperature().has_value()) { | ||||
|     float target_temperature = *call.get_target_temperature(); | ||||
| @@ -129,6 +178,106 @@ void TuyaClimate::control(const climate::ClimateCall &call) { | ||||
|       ESP_LOGV(TAG, "Setting eco: %s", ONOFF(eco)); | ||||
|       this->parent_->set_boolean_datapoint_value(*this->eco_id_, eco); | ||||
|     } | ||||
|     if (this->sleep_id_.has_value()) { | ||||
|       const bool sleep = preset == climate::CLIMATE_PRESET_SLEEP; | ||||
|       ESP_LOGV(TAG, "Setting sleep: %s", ONOFF(sleep)); | ||||
|       this->parent_->set_boolean_datapoint_value(*this->sleep_id_, sleep); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TuyaClimate::control_swing_mode_(const climate::ClimateCall &call) { | ||||
|   bool vertical_swing_changed = false; | ||||
|   bool horizontal_swing_changed = false; | ||||
|  | ||||
|   if (call.get_swing_mode().has_value()) { | ||||
|     const auto swing_mode = *call.get_swing_mode(); | ||||
|  | ||||
|     switch (swing_mode) { | ||||
|       case climate::CLIMATE_SWING_OFF: | ||||
|         if (swing_vertical_ || swing_horizontal_) { | ||||
|           this->swing_vertical_ = false; | ||||
|           this->swing_horizontal_ = false; | ||||
|           vertical_swing_changed = true; | ||||
|           horizontal_swing_changed = true; | ||||
|         } | ||||
|         break; | ||||
|  | ||||
|       case climate::CLIMATE_SWING_BOTH: | ||||
|         if (!swing_vertical_ || !swing_horizontal_) { | ||||
|           this->swing_vertical_ = true; | ||||
|           this->swing_horizontal_ = true; | ||||
|           vertical_swing_changed = true; | ||||
|           horizontal_swing_changed = true; | ||||
|         } | ||||
|         break; | ||||
|  | ||||
|       case climate::CLIMATE_SWING_VERTICAL: | ||||
|         if (!swing_vertical_ || swing_horizontal_) { | ||||
|           this->swing_vertical_ = true; | ||||
|           this->swing_horizontal_ = false; | ||||
|           vertical_swing_changed = true; | ||||
|           horizontal_swing_changed = true; | ||||
|         } | ||||
|         break; | ||||
|  | ||||
|       case climate::CLIMATE_SWING_HORIZONTAL: | ||||
|         if (swing_vertical_ || !swing_horizontal_) { | ||||
|           this->swing_vertical_ = false; | ||||
|           this->swing_horizontal_ = true; | ||||
|           vertical_swing_changed = true; | ||||
|           horizontal_swing_changed = true; | ||||
|         } | ||||
|         break; | ||||
|  | ||||
|       default: | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (vertical_swing_changed && this->swing_vertical_id_.has_value()) { | ||||
|     ESP_LOGV(TAG, "Setting vertical swing: %s", ONOFF(swing_vertical_)); | ||||
|     this->parent_->set_boolean_datapoint_value(*this->swing_vertical_id_, swing_vertical_); | ||||
|   } | ||||
|  | ||||
|   if (horizontal_swing_changed && this->swing_horizontal_id_.has_value()) { | ||||
|     ESP_LOGV(TAG, "Setting horizontal swing: %s", ONOFF(swing_horizontal_)); | ||||
|     this->parent_->set_boolean_datapoint_value(*this->swing_horizontal_id_, swing_horizontal_); | ||||
|   } | ||||
|  | ||||
|   // Publish the state after updating the swing mode | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| void TuyaClimate::control_fan_mode_(const climate::ClimateCall &call) { | ||||
|   if (call.get_fan_mode().has_value()) { | ||||
|     climate::ClimateFanMode fan_mode = *call.get_fan_mode(); | ||||
|  | ||||
|     uint8_t tuya_fan_speed; | ||||
|     switch (fan_mode) { | ||||
|       case climate::CLIMATE_FAN_LOW: | ||||
|         tuya_fan_speed = *fan_speed_low_value_; | ||||
|         break; | ||||
|       case climate::CLIMATE_FAN_MEDIUM: | ||||
|         tuya_fan_speed = *fan_speed_medium_value_; | ||||
|         break; | ||||
|       case climate::CLIMATE_FAN_MIDDLE: | ||||
|         tuya_fan_speed = *fan_speed_middle_value_; | ||||
|         break; | ||||
|       case climate::CLIMATE_FAN_HIGH: | ||||
|         tuya_fan_speed = *fan_speed_high_value_; | ||||
|         break; | ||||
|       case climate::CLIMATE_FAN_AUTO: | ||||
|         tuya_fan_speed = *fan_speed_auto_value_; | ||||
|         break; | ||||
|       default: | ||||
|         tuya_fan_speed = 0; | ||||
|         break; | ||||
|     } | ||||
|  | ||||
|     if (this->fan_speed_id_.has_value()) { | ||||
|       this->parent_->set_enum_datapoint_value(*this->fan_speed_id_, tuya_fan_speed); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -140,10 +289,46 @@ climate::ClimateTraits TuyaClimate::traits() { | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); | ||||
|   if (supports_cool_) | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_COOL); | ||||
|   if (this->active_state_drying_value_.has_value()) | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_DRY); | ||||
|   if (this->active_state_fanonly_value_.has_value()) | ||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY); | ||||
|   if (this->eco_id_.has_value()) { | ||||
|     traits.add_supported_preset(climate::CLIMATE_PRESET_NONE); | ||||
|     traits.add_supported_preset(climate::CLIMATE_PRESET_ECO); | ||||
|   } | ||||
|   if (this->sleep_id_.has_value()) { | ||||
|     traits.add_supported_preset(climate::CLIMATE_PRESET_SLEEP); | ||||
|   } | ||||
|   if (this->sleep_id_.has_value() || this->eco_id_.has_value()) { | ||||
|     traits.add_supported_preset(climate::CLIMATE_PRESET_NONE); | ||||
|   } | ||||
|   if (this->swing_vertical_id_.has_value() && this->swing_horizontal_id_.has_value()) { | ||||
|     std::set<climate::ClimateSwingMode> supported_swing_modes = { | ||||
|         climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, | ||||
|         climate::CLIMATE_SWING_HORIZONTAL}; | ||||
|     traits.set_supported_swing_modes(std::move(supported_swing_modes)); | ||||
|   } else if (this->swing_vertical_id_.has_value()) { | ||||
|     std::set<climate::ClimateSwingMode> supported_swing_modes = {climate::CLIMATE_SWING_OFF, | ||||
|                                                                  climate::CLIMATE_SWING_VERTICAL}; | ||||
|     traits.set_supported_swing_modes(std::move(supported_swing_modes)); | ||||
|   } else if (this->swing_horizontal_id_.has_value()) { | ||||
|     std::set<climate::ClimateSwingMode> supported_swing_modes = {climate::CLIMATE_SWING_OFF, | ||||
|                                                                  climate::CLIMATE_SWING_HORIZONTAL}; | ||||
|     traits.set_supported_swing_modes(std::move(supported_swing_modes)); | ||||
|   } | ||||
|  | ||||
|   if (fan_speed_id_) { | ||||
|     if (fan_speed_low_value_) | ||||
|       traits.add_supported_fan_mode(climate::CLIMATE_FAN_LOW); | ||||
|     if (fan_speed_medium_value_) | ||||
|       traits.add_supported_fan_mode(climate::CLIMATE_FAN_MEDIUM); | ||||
|     if (fan_speed_middle_value_) | ||||
|       traits.add_supported_fan_mode(climate::CLIMATE_FAN_MIDDLE); | ||||
|     if (fan_speed_high_value_) | ||||
|       traits.add_supported_fan_mode(climate::CLIMATE_FAN_HIGH); | ||||
|     if (fan_speed_auto_value_) | ||||
|       traits.add_supported_fan_mode(climate::CLIMATE_FAN_AUTO); | ||||
|   } | ||||
|   return traits; | ||||
| } | ||||
|  | ||||
| @@ -166,16 +351,56 @@ void TuyaClimate::dump_config() { | ||||
|   if (this->eco_id_.has_value()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Eco has datapoint ID %u", *this->eco_id_); | ||||
|   } | ||||
|   if (this->sleep_id_.has_value()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Sleep has datapoint ID %u", *this->sleep_id_); | ||||
|   } | ||||
|   if (this->swing_vertical_id_.has_value()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Swing Vertical has datapoint ID %u", *this->swing_vertical_id_); | ||||
|   } | ||||
|   if (this->swing_horizontal_id_.has_value()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Swing Horizontal has datapoint ID %u", *this->swing_horizontal_id_); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TuyaClimate::compute_preset_() { | ||||
|   if (this->eco_) { | ||||
|     this->preset = climate::CLIMATE_PRESET_ECO; | ||||
|   } else if (this->sleep_) { | ||||
|     this->preset = climate::CLIMATE_PRESET_SLEEP; | ||||
|   } else { | ||||
|     this->preset = climate::CLIMATE_PRESET_NONE; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TuyaClimate::compute_swingmode_() { | ||||
|   if (this->swing_vertical_ && this->swing_horizontal_) { | ||||
|     this->swing_mode = climate::CLIMATE_SWING_BOTH; | ||||
|   } else if (this->swing_vertical_) { | ||||
|     this->swing_mode = climate::CLIMATE_SWING_VERTICAL; | ||||
|   } else if (this->swing_horizontal_) { | ||||
|     this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; | ||||
|   } else { | ||||
|     this->swing_mode = climate::CLIMATE_SWING_OFF; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TuyaClimate::compute_fanmode_() { | ||||
|   if (this->fan_speed_id_.has_value()) { | ||||
|     // Use state from MCU datapoint | ||||
|     if (this->fan_speed_auto_value_.has_value() && this->fan_state_ == this->fan_speed_auto_value_) { | ||||
|       this->fan_mode = climate::CLIMATE_FAN_AUTO; | ||||
|     } else if (this->fan_speed_high_value_.has_value() && this->fan_state_ == this->fan_speed_high_value_) { | ||||
|       this->fan_mode = climate::CLIMATE_FAN_HIGH; | ||||
|     } else if (this->fan_speed_medium_value_.has_value() && this->fan_state_ == this->fan_speed_medium_value_) { | ||||
|       this->fan_mode = climate::CLIMATE_FAN_MEDIUM; | ||||
|     } else if (this->fan_speed_middle_value_.has_value() && this->fan_state_ == this->fan_speed_middle_value_) { | ||||
|       this->fan_mode = climate::CLIMATE_FAN_MIDDLE; | ||||
|     } else if (this->fan_speed_low_value_.has_value() && this->fan_state_ == this->fan_speed_low_value_) { | ||||
|       this->fan_mode = climate::CLIMATE_FAN_LOW; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TuyaClimate::compute_target_temperature_() { | ||||
|   if (this->eco_ && this->eco_temperature_.has_value()) { | ||||
|     this->target_temperature = *this->eco_temperature_; | ||||
| @@ -202,16 +427,28 @@ void TuyaClimate::compute_state_() { | ||||
|     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; | ||||
|       this->mode = climate::CLIMATE_MODE_HEAT; | ||||
|     } else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() && | ||||
|                this->active_state_ == this->active_state_cooling_value_) { | ||||
|       target_action = climate::CLIMATE_ACTION_COOLING; | ||||
|       this->mode = climate::CLIMATE_MODE_COOL; | ||||
|     } else if (this->active_state_drying_value_.has_value() && | ||||
|                this->active_state_ == this->active_state_drying_value_) { | ||||
|       target_action = climate::CLIMATE_ACTION_DRYING; | ||||
|       this->mode = climate::CLIMATE_MODE_DRY; | ||||
|     } else if (this->active_state_fanonly_value_.has_value() && | ||||
|                this->active_state_ == this->active_state_fanonly_value_) { | ||||
|       target_action = climate::CLIMATE_ACTION_FAN; | ||||
|       this->mode = climate::CLIMATE_MODE_FAN_ONLY; | ||||
|     } | ||||
|   } 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; | ||||
|       this->mode = climate::CLIMATE_MODE_HEAT; | ||||
|     } else if (this->cooling_state_) { | ||||
|       target_action = climate::CLIMATE_ACTION_COOLING; | ||||
|       this->mode = climate::CLIMATE_MODE_COOL; | ||||
|     } | ||||
|   } else { | ||||
|     // Fallback to active state calc based on temp and hysteresis | ||||
| @@ -219,8 +456,10 @@ void TuyaClimate::compute_state_() { | ||||
|     if (std::abs(temp_diff) > this->hysteresis_) { | ||||
|       if (this->supports_heat_ && temp_diff > 0) { | ||||
|         target_action = climate::CLIMATE_ACTION_HEATING; | ||||
|         this->mode = climate::CLIMATE_MODE_HEAT; | ||||
|       } else if (this->supports_cool_ && temp_diff < 0) { | ||||
|         target_action = climate::CLIMATE_ACTION_COOLING; | ||||
|         this->mode = climate::CLIMATE_MODE_COOL; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -18,8 +18,22 @@ 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_active_state_drying_value(uint8_t value) { this->active_state_drying_value_ = value; } | ||||
|   void set_active_state_fanonly_value(uint8_t value) { this->active_state_fanonly_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_swing_vertical_id(uint8_t swing_vertical_id) { this->swing_vertical_id_ = swing_vertical_id; } | ||||
|   void set_swing_horizontal_id(uint8_t swing_horizontal_id) { this->swing_horizontal_id_ = swing_horizontal_id; } | ||||
|   void set_fan_speed_id(uint8_t fan_speed_id) { this->fan_speed_id_ = fan_speed_id; } | ||||
|   void set_fan_speed_low_value(uint8_t fan_speed_low_value) { this->fan_speed_low_value_ = fan_speed_low_value; } | ||||
|   void set_fan_speed_medium_value(uint8_t fan_speed_medium_value) { | ||||
|     this->fan_speed_medium_value_ = fan_speed_medium_value; | ||||
|   } | ||||
|   void set_fan_speed_middle_value(uint8_t fan_speed_middle_value) { | ||||
|     this->fan_speed_middle_value_ = fan_speed_middle_value; | ||||
|   } | ||||
|   void set_fan_speed_high_value(uint8_t fan_speed_high_value) { this->fan_speed_high_value_ = fan_speed_high_value; } | ||||
|   void set_fan_speed_auto_value(uint8_t fan_speed_auto_value) { this->fan_speed_auto_value_ = fan_speed_auto_value; } | ||||
|   void set_target_temperature_id(uint8_t target_temperature_id) { | ||||
|     this->target_temperature_id_ = target_temperature_id; | ||||
|   } | ||||
| @@ -34,6 +48,7 @@ class TuyaClimate : public climate::Climate, public Component { | ||||
|   } | ||||
|   void set_eco_id(uint8_t eco_id) { this->eco_id_ = eco_id; } | ||||
|   void set_eco_temperature(float eco_temperature) { this->eco_temperature_ = eco_temperature; } | ||||
|   void set_sleep_id(uint8_t sleep_id) { this->sleep_id_ = sleep_id; } | ||||
|  | ||||
|   void set_reports_fahrenheit() { this->reports_fahrenheit_ = true; } | ||||
|  | ||||
| @@ -43,6 +58,12 @@ class TuyaClimate : public climate::Climate, public Component { | ||||
|   /// Override control to change settings of the climate device. | ||||
|   void control(const climate::ClimateCall &call) override; | ||||
|  | ||||
|   /// Override control to change settings of swing mode. | ||||
|   void control_swing_mode_(const climate::ClimateCall &call); | ||||
|  | ||||
|   /// Override control to change settings of fan mode. | ||||
|   void control_fan_mode_(const climate::ClimateCall &call); | ||||
|  | ||||
|   /// Return the traits of this controller. | ||||
|   climate::ClimateTraits traits() override; | ||||
|  | ||||
| @@ -55,6 +76,12 @@ class TuyaClimate : public climate::Climate, public Component { | ||||
|   /// Re-compute the state of this climate controller. | ||||
|   void compute_state_(); | ||||
|  | ||||
|   /// Re-Compute the swing mode of this climate controller. | ||||
|   void compute_swingmode_(); | ||||
|  | ||||
|   /// Re-Compute the fan mode of this climate controller. | ||||
|   void compute_fanmode_(); | ||||
|  | ||||
|   /// Switch the climate device to the given climate mode. | ||||
|   void switch_to_action_(climate::ClimateAction action); | ||||
|  | ||||
| @@ -65,6 +92,8 @@ class TuyaClimate : public climate::Climate, public Component { | ||||
|   optional<uint8_t> active_state_id_{}; | ||||
|   optional<uint8_t> active_state_heating_value_{}; | ||||
|   optional<uint8_t> active_state_cooling_value_{}; | ||||
|   optional<uint8_t> active_state_drying_value_{}; | ||||
|   optional<uint8_t> active_state_fanonly_value_{}; | ||||
|   GPIOPin *heating_state_pin_{nullptr}; | ||||
|   GPIOPin *cooling_state_pin_{nullptr}; | ||||
|   optional<uint8_t> target_temperature_id_{}; | ||||
| @@ -73,12 +102,25 @@ class TuyaClimate : public climate::Climate, public Component { | ||||
|   float target_temperature_multiplier_{1.0f}; | ||||
|   float hysteresis_{1.0f}; | ||||
|   optional<uint8_t> eco_id_{}; | ||||
|   optional<uint8_t> sleep_id_{}; | ||||
|   optional<float> eco_temperature_{}; | ||||
|   uint8_t active_state_; | ||||
|   uint8_t fan_state_; | ||||
|   optional<uint8_t> swing_vertical_id_{}; | ||||
|   optional<uint8_t> swing_horizontal_id_{}; | ||||
|   optional<uint8_t> fan_speed_id_{}; | ||||
|   optional<uint8_t> fan_speed_low_value_{}; | ||||
|   optional<uint8_t> fan_speed_medium_value_{}; | ||||
|   optional<uint8_t> fan_speed_middle_value_{}; | ||||
|   optional<uint8_t> fan_speed_high_value_{}; | ||||
|   optional<uint8_t> fan_speed_auto_value_{}; | ||||
|   bool swing_vertical_{false}; | ||||
|   bool swing_horizontal_{false}; | ||||
|   bool heating_state_{false}; | ||||
|   bool cooling_state_{false}; | ||||
|   float manual_temperature_; | ||||
|   bool eco_; | ||||
|   bool sleep_; | ||||
|   bool reports_fahrenheit_{false}; | ||||
| }; | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user