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 to 649b923804 (r1278620976), 649b923804 (r1278621039), 649b923804 (r1278620904), and 649b923804 (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:
Moriah Morgan 2024-03-20 19:40:14 -05:00 committed by GitHub
parent b637fb3adc
commit 13059805d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 449 additions and 80 deletions

View file

@ -7,15 +7,22 @@ from esphome.const import (
CONF_SWITCH_DATAPOINT, CONF_SWITCH_DATAPOINT,
CONF_SUPPORTS_COOL, CONF_SUPPORTS_COOL,
CONF_SUPPORTS_HEAT, CONF_SUPPORTS_HEAT,
CONF_PRESET,
CONF_SWING_MODE,
CONF_FAN_MODE,
CONF_TEMPERATURE,
) )
from .. import tuya_ns, CONF_TUYA_ID, Tuya from .. import tuya_ns, CONF_TUYA_ID, Tuya
DEPENDENCIES = ["tuya"] DEPENDENCIES = ["tuya"]
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
CONF_ACTIVE_STATE_DATAPOINT = "active_state_datapoint" CONF_ACTIVE_STATE = "active_state"
CONF_ACTIVE_STATE_HEATING_VALUE = "active_state_heating_value" CONF_DATAPOINT = "datapoint"
CONF_ACTIVE_STATE_COOLING_VALUE = "active_state_cooling_value" 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_HEATING_STATE_PIN = "heating_state_pin"
CONF_COOLING_STATE_PIN = "cooling_state_pin" CONF_COOLING_STATE_PIN = "cooling_state_pin"
CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint" CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint"
@ -23,9 +30,17 @@ CONF_CURRENT_TEMPERATURE_DATAPOINT = "current_temperature_datapoint"
CONF_TEMPERATURE_MULTIPLIER = "temperature_multiplier" CONF_TEMPERATURE_MULTIPLIER = "temperature_multiplier"
CONF_CURRENT_TEMPERATURE_MULTIPLIER = "current_temperature_multiplier" CONF_CURRENT_TEMPERATURE_MULTIPLIER = "current_temperature_multiplier"
CONF_TARGET_TEMPERATURE_MULTIPLIER = "target_temperature_multiplier" CONF_TARGET_TEMPERATURE_MULTIPLIER = "target_temperature_multiplier"
CONF_ECO_DATAPOINT = "eco_datapoint" CONF_ECO = "eco"
CONF_ECO_TEMPERATURE = "eco_temperature" CONF_SLEEP = "sleep"
CONF_SLEEP_DATAPOINT = "sleep_datapoint"
CONF_REPORTS_FAHRENHEIT = "reports_fahrenheit" 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) TuyaClimate = tuya_ns.class_("TuyaClimate", climate.Climate, cg.Component)
@ -67,30 +82,73 @@ def validate_temperature_multipliers(value):
return value return value
def validate_active_state_values(value): def validate_cooling_values(value):
if CONF_ACTIVE_STATE_DATAPOINT not in value: if CONF_SUPPORTS_COOL in value:
if CONF_ACTIVE_STATE_COOLING_VALUE 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( raise cv.Invalid(
f"{CONF_ACTIVE_STATE_DATAPOINT} required if using " f"Device does not support cooling, but {CONF_COOLING_VALUE} or {CONF_COOLING_STATE_PIN} specified."
f"{CONF_ACTIVE_STATE_COOLING_VALUE}" f" Please add '{CONF_SUPPORTS_COOL}: true' to your configuration."
) )
else: elif cooling_supported and CONF_ACTIVE_STATE in value:
if value[CONF_SUPPORTS_COOL] and CONF_ACTIVE_STATE_COOLING_VALUE not 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( raise cv.Invalid(
f"{CONF_ACTIVE_STATE_COOLING_VALUE} required if using " f"Either {CONF_ACTIVE_STATE} {CONF_COOLING_VALUE} or {CONF_COOLING_STATE_PIN} is required if"
f"{CONF_ACTIVE_STATE_DATAPOINT} and device supports cooling" f" {CONF_SUPPORTS_COOL}: true' is in your configuration."
) )
return value return value
def validate_eco_values(value): ACTIVE_STATES = cv.Schema(
if CONF_ECO_TEMPERATURE in value and CONF_ECO_DATAPOINT not in value: {
raise cv.Invalid( cv.Required(CONF_DATAPOINT): cv.uint8_t,
f"{CONF_ECO_DATAPOINT} required if using {CONF_ECO_TEMPERATURE}" cv.Optional(CONF_HEATING_VALUE, default=1): cv.uint8_t,
) cv.Optional(CONF_COOLING_VALUE): cv.uint8_t,
return value 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( CONFIG_SCHEMA = cv.All(
climate.CLIMATE_SCHEMA.extend( 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_HEAT, default=True): cv.boolean,
cv.Optional(CONF_SUPPORTS_COOL, default=False): cv.boolean, cv.Optional(CONF_SUPPORTS_COOL, default=False): cv.boolean,
cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_ACTIVE_STATE_DATAPOINT): cv.uint8_t, cv.Optional(CONF_ACTIVE_STATE): ACTIVE_STATES,
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_HEATING_STATE_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_COOLING_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_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_TEMPERATURE_MULTIPLIER): cv.positive_float,
cv.Optional(CONF_CURRENT_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_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_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), ).extend(cv.COMPONENT_SCHEMA),
cv.has_at_least_one_key(CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT), cv.has_at_least_one_key(CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT),
validate_temperature_multipliers, validate_temperature_multipliers,
validate_active_state_values, validate_cooling_values,
cv.has_at_most_one_key(CONF_ACTIVE_STATE_DATAPOINT, CONF_HEATING_STATE_PIN), cv.has_at_most_one_key(CONF_ACTIVE_STATE, CONF_HEATING_STATE_PIN),
cv.has_at_most_one_key(CONF_ACTIVE_STATE_DATAPOINT, CONF_COOLING_STATE_PIN), cv.has_at_most_one_key(CONF_ACTIVE_STATE, CONF_COOLING_STATE_PIN),
validate_eco_values,
) )
@ -133,61 +204,78 @@ async def to_code(config):
cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
if CONF_SWITCH_DATAPOINT in config: if switch_datapoint := config.get(CONF_SWITCH_DATAPOINT):
cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) cg.add(var.set_switch_id(switch_datapoint))
if CONF_ACTIVE_STATE_DATAPOINT in config:
cg.add(var.set_active_state_id(config[CONF_ACTIVE_STATE_DATAPOINT])) if active_state_config := config.get(CONF_ACTIVE_STATE):
if CONF_ACTIVE_STATE_HEATING_VALUE in config: cg.add(var.set_active_state_id(CONF_DATAPOINT))
cg.add( if (heating_value := active_state_config.get(CONF_HEATING_VALUE)) is not None:
var.set_active_state_heating_value( cg.add(var.set_active_state_heating_value(heating_value))
config[CONF_ACTIVE_STATE_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:
if CONF_ACTIVE_STATE_COOLING_VALUE in config: cg.add(var.set_active_state_drying_value(drying_value))
cg.add( if (fanonly_value := active_state_config.get(CONF_FANONLY_VALUE)) is not None:
var.set_active_state_cooling_value( cg.add(var.set_active_state_fanonly_value(fanonly_value))
config[CONF_ACTIVE_STATE_COOLING_VALUE]
)
)
else: 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( 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)) 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( 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)) 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 target_temperature_datapoint := config.get(CONF_TARGET_TEMPERATURE_DATAPOINT):
if CONF_CURRENT_TEMPERATURE_DATAPOINT in config: cg.add(var.set_target_temperature_id(target_temperature_datapoint))
cg.add( if current_temperature_datapoint := config.get(CONF_CURRENT_TEMPERATURE_DATAPOINT):
var.set_current_temperature_id(config[CONF_CURRENT_TEMPERATURE_DATAPOINT]) cg.add(var.set_current_temperature_id(current_temperature_datapoint))
)
if CONF_TEMPERATURE_MULTIPLIER in config: if temperature_multiplier := config.get(CONF_TEMPERATURE_MULTIPLIER):
cg.add( cg.add(var.set_target_temperature_multiplier(temperature_multiplier))
var.set_target_temperature_multiplier(config[CONF_TEMPERATURE_MULTIPLIER]) cg.add(var.set_current_temperature_multiplier(temperature_multiplier))
)
cg.add(
var.set_current_temperature_multiplier(config[CONF_TEMPERATURE_MULTIPLIER])
)
else: else:
if current_temperature_multiplier := config.get(
CONF_CURRENT_TEMPERATURE_MULTIPLIER
):
cg.add( cg.add(
var.set_current_temperature_multiplier( var.set_current_temperature_multiplier(current_temperature_multiplier)
config[CONF_CURRENT_TEMPERATURE_MULTIPLIER]
) )
) if target_temperature_multiplier := config.get(
cg.add( CONF_TARGET_TEMPERATURE_MULTIPLIER
var.set_target_temperature_multiplier( ):
config[CONF_TARGET_TEMPERATURE_MULTIPLIER] cg.add(var.set_target_temperature_multiplier(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 config[CONF_REPORTS_FAHRENHEIT]: if config[CONF_REPORTS_FAHRENHEIT]:
cg.add(var.set_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))

View file

@ -75,6 +75,41 @@ void TuyaClimate::setup() {
this->publish_state(); 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() { 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; const bool switch_state = *call.get_mode() != climate::CLIMATE_MODE_OFF;
ESP_LOGV(TAG, "Setting switch: %s", ONOFF(switch_state)); ESP_LOGV(TAG, "Setting switch: %s", ONOFF(switch_state));
this->parent_->set_boolean_datapoint_value(*this->switch_id_, 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()) { if (call.get_target_temperature().has_value()) {
float target_temperature = *call.get_target_temperature(); 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)); ESP_LOGV(TAG, "Setting eco: %s", ONOFF(eco));
this->parent_->set_boolean_datapoint_value(*this->eco_id_, 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); traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
if (supports_cool_) if (supports_cool_)
traits.add_supported_mode(climate::CLIMATE_MODE_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()) { if (this->eco_id_.has_value()) {
traits.add_supported_preset(climate::CLIMATE_PRESET_NONE);
traits.add_supported_preset(climate::CLIMATE_PRESET_ECO); 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; return traits;
} }
@ -166,16 +351,56 @@ void TuyaClimate::dump_config() {
if (this->eco_id_.has_value()) { if (this->eco_id_.has_value()) {
ESP_LOGCONFIG(TAG, " Eco has datapoint ID %u", *this->eco_id_); 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_() { void TuyaClimate::compute_preset_() {
if (this->eco_) { if (this->eco_) {
this->preset = climate::CLIMATE_PRESET_ECO; this->preset = climate::CLIMATE_PRESET_ECO;
} else if (this->sleep_) {
this->preset = climate::CLIMATE_PRESET_SLEEP;
} else { } else {
this->preset = climate::CLIMATE_PRESET_NONE; 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_() { void TuyaClimate::compute_target_temperature_() {
if (this->eco_ && this->eco_temperature_.has_value()) { if (this->eco_ && this->eco_temperature_.has_value()) {
this->target_temperature = *this->eco_temperature_; 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() && if (this->supports_heat_ && this->active_state_heating_value_.has_value() &&
this->active_state_ == this->active_state_heating_value_) { this->active_state_ == this->active_state_heating_value_) {
target_action = climate::CLIMATE_ACTION_HEATING; target_action = climate::CLIMATE_ACTION_HEATING;
this->mode = climate::CLIMATE_MODE_HEAT;
} else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() && } else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() &&
this->active_state_ == this->active_state_cooling_value_) { this->active_state_ == this->active_state_cooling_value_) {
target_action = climate::CLIMATE_ACTION_COOLING; 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) { } else if (this->heating_state_pin_ != nullptr || this->cooling_state_pin_ != nullptr) {
// Use state from input pins // Use state from input pins
if (this->heating_state_) { if (this->heating_state_) {
target_action = climate::CLIMATE_ACTION_HEATING; target_action = climate::CLIMATE_ACTION_HEATING;
this->mode = climate::CLIMATE_MODE_HEAT;
} else if (this->cooling_state_) { } else if (this->cooling_state_) {
target_action = climate::CLIMATE_ACTION_COOLING; target_action = climate::CLIMATE_ACTION_COOLING;
this->mode = climate::CLIMATE_MODE_COOL;
} }
} else { } else {
// Fallback to active state calc based on temp and hysteresis // 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 (std::abs(temp_diff) > this->hysteresis_) {
if (this->supports_heat_ && temp_diff > 0) { if (this->supports_heat_ && temp_diff > 0) {
target_action = climate::CLIMATE_ACTION_HEATING; target_action = climate::CLIMATE_ACTION_HEATING;
this->mode = climate::CLIMATE_MODE_HEAT;
} else if (this->supports_cool_ && temp_diff < 0) { } else if (this->supports_cool_ && temp_diff < 0) {
target_action = climate::CLIMATE_ACTION_COOLING; target_action = climate::CLIMATE_ACTION_COOLING;
this->mode = climate::CLIMATE_MODE_COOL;
} }
} }
} }

View file

@ -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_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_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_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_heating_state_pin(GPIOPin *pin) { this->heating_state_pin_ = pin; }
void set_cooling_state_pin(GPIOPin *pin) { this->cooling_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) { void set_target_temperature_id(uint8_t target_temperature_id) {
this->target_temperature_id_ = 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_id(uint8_t eco_id) { this->eco_id_ = eco_id; }
void set_eco_temperature(float eco_temperature) { this->eco_temperature_ = eco_temperature; } 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; } 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. /// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override; 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. /// Return the traits of this controller.
climate::ClimateTraits traits() override; climate::ClimateTraits traits() override;
@ -55,6 +76,12 @@ class TuyaClimate : public climate::Climate, public Component {
/// Re-compute the state of this climate controller. /// Re-compute the state of this climate controller.
void compute_state_(); 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. /// Switch the climate device to the given climate mode.
void switch_to_action_(climate::ClimateAction action); 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_id_{};
optional<uint8_t> active_state_heating_value_{}; optional<uint8_t> active_state_heating_value_{};
optional<uint8_t> active_state_cooling_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 *heating_state_pin_{nullptr};
GPIOPin *cooling_state_pin_{nullptr}; GPIOPin *cooling_state_pin_{nullptr};
optional<uint8_t> target_temperature_id_{}; optional<uint8_t> target_temperature_id_{};
@ -73,12 +102,25 @@ class TuyaClimate : public climate::Climate, public Component {
float target_temperature_multiplier_{1.0f}; float target_temperature_multiplier_{1.0f};
float hysteresis_{1.0f}; float hysteresis_{1.0f};
optional<uint8_t> eco_id_{}; optional<uint8_t> eco_id_{};
optional<uint8_t> sleep_id_{};
optional<float> eco_temperature_{}; optional<float> eco_temperature_{};
uint8_t active_state_; 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 heating_state_{false};
bool cooling_state_{false}; bool cooling_state_{false};
float manual_temperature_; float manual_temperature_;
bool eco_; bool eco_;
bool sleep_;
bool reports_fahrenheit_{false}; bool reports_fahrenheit_{false};
}; };