diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py new file mode 100644 index 0000000000..b938be7b8c --- /dev/null +++ b/esphome/components/tuya/climate/__init__.py @@ -0,0 +1,41 @@ +from esphome.components import climate +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_SWITCH_DATAPOINT +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] + +CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint" +CONF_CURRENT_TEMPERATURE_DATAPOINT = "current_temperature_datapoint" +# CONF_ECO_MODE_DATAPOINT = "eco_mode_datapoint" + +TuyaClimate = tuya_ns.class_('TuyaClimate', climate.Climate, cg.Component) + +CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TuyaClimate), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_TARGET_TEMPERATURE_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_CURRENT_TEMPERATURE_DATAPOINT): cv.uint8_t, + # cv.Optional(CONF_ECO_MODE_DATAPOINT): cv.uint8_t, +}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key( + CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + if CONF_SWITCH_DATAPOINT in config: + cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) + 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_ECO_MODE_DATAPOINT in config: + # cg.add(var.set_eco_mode_id(config[CONF_ECO_MODE_DATAPOINT])) diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp new file mode 100644 index 0000000000..cfd5acccbd --- /dev/null +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -0,0 +1,154 @@ +#include "esphome/core/log.h" +#include "tuya_climate.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.climate"; + +void TuyaClimate::setup() { + if (this->switch_id_.has_value()) { + this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) { + if (datapoint.value_bool) { + this->mode = climate::CLIMATE_MODE_HEAT; + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + this->compute_state_(); + this->publish_state(); + ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); + }); + } + if (this->target_temperature_id_.has_value()) { + this->parent_->register_listener(*this->target_temperature_id_, [this](TuyaDatapoint datapoint) { + this->target_temperature = datapoint.value_int; + this->compute_state_(); + this->publish_state(); + ESP_LOGD(TAG, "MCU reported target temperature is: %d", datapoint.value_int); + }); + } + if (this->current_temperature_id_.has_value()) { + this->parent_->register_listener(*this->current_temperature_id_, [this](TuyaDatapoint datapoint) { + this->current_temperature = datapoint.value_int; + this->compute_state_(); + this->publish_state(); + ESP_LOGD(TAG, "MCU reported current temperature is: %d", datapoint.value_int); + }); + } + // if (this->eco_mode_id_.has_value()) { + // this->parent_->register_listener(*this->eco_mode_id_, [this](TuyaDatapoint datapoint) { + // this->eco_mode = datapoint.value_bool; + // this->compute_state_(); + // this->publish_state(); + // ESP_LOGD(TAG, "MCU reported eco mode of: %s", ONOFF(datapoint.value_bool)); + // }); + // } +} + +void TuyaClimate::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) { + this->mode = *call.get_mode(); + + TuyaDatapoint datapoint{}; + datapoint.id = *this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = this->mode != climate::CLIMATE_MODE_OFF; + this->parent_->set_datapoint_value(datapoint); + ESP_LOGD(TAG, "Setting switch: %s", ONOFF(datapoint.value_bool)); + } + if (call.get_target_temperature_low().has_value()) + this->target_temperature_low = *call.get_target_temperature_low(); + if (call.get_target_temperature_high().has_value()) + this->target_temperature_high = *call.get_target_temperature_high(); + if (call.get_target_temperature().has_value()) { + this->target_temperature = *call.get_target_temperature(); + + TuyaDatapoint datapoint{}; + datapoint.id = *this->target_temperature_id_; + datapoint.type = TuyaDatapointType::INTEGER; + datapoint.value_int = (int) this->target_temperature; + this->parent_->set_datapoint_value(datapoint); + ESP_LOGD(TAG, "Setting target temperature: %d", datapoint.value_int); + } + // if (call.get_eco_mode().has_value()) { + // this->eco_mode = *call.get_eco_mode(); + + // TuyaDatapoint datapoint{}; + // datapoint.id = *this->eco_mode_id_; + // datapoint.type = TuyaDatapointType::BOOLEAN; + // datapoint.value_bool = this->eco_mode; + // this->parent_->set_datapoint_value(datapoint); + // ESP_LOGD(TAG, "Setting eco mode: %s", ONOFF(datapoint.value_bool)); + // } + + this->compute_state_(); + this->publish_state(); +} + +climate::ClimateTraits TuyaClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(this->current_temperature_id_.has_value()); + traits.set_supports_heat_mode(true); + // traits.set_supports_eco_mode(this->eco_mode_id_.has_value()); + traits.set_supports_action(true); + return traits; +} + +void TuyaClimate::dump_config() { + LOG_CLIMATE("", "Tuya Climate", this); + if (this->switch_id_.has_value()) + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); + if (this->target_temperature_id_.has_value()) + 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_); + // if (this->eco_mode_id_.has_value()) + // ESP_LOGCONFIG(TAG, " Eco Mode has datapoint ID %u", *this->mode_id_); +} + +void TuyaClimate::compute_state_() { + if (isnan(this->current_temperature) || isnan(this->target_temperature)) { + // if any control parameters are nan, go to OFF action (not IDLE!) + this->switch_to_action_(climate::CLIMATE_ACTION_OFF); + return; + } + + if (this->mode == climate::CLIMATE_MODE_OFF) { + this->switch_to_action_(climate::CLIMATE_ACTION_OFF); + return; + } + + const bool too_cold = this->current_temperature < this->target_temperature - 1; + const bool too_hot = this->current_temperature > this->target_temperature + 1; + const bool on_target = this->current_temperature == this->target_temperature; + + climate::ClimateAction target_action; + if (too_cold) { + // too cold -> show as heating if possible, else idle + if (this->traits().supports_mode(climate::CLIMATE_MODE_HEAT)) { + target_action = climate::CLIMATE_ACTION_HEATING; + } else { + target_action = climate::CLIMATE_ACTION_IDLE; + } + } else if (too_hot) { + // too hot -> show as cooling if possible, else idle + if (this->traits().supports_mode(climate::CLIMATE_MODE_COOL)) { + target_action = climate::CLIMATE_ACTION_COOLING; + } else { + target_action = climate::CLIMATE_ACTION_IDLE; + } + } else if (on_target) { + target_action = climate::CLIMATE_ACTION_IDLE; + } else { + target_action = this->action; + } + this->switch_to_action_(target_action); +} + +void TuyaClimate::switch_to_action_(climate::ClimateAction action) { + // For now this just sets the current action but could include triggers later + this->action = action; +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h new file mode 100644 index 0000000000..a073b0996c --- /dev/null +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/climate/climate.h" + +namespace esphome { +namespace tuya { + +class TuyaClimate : public climate::Climate, public Component { + public: + void setup() override; + void dump_config() override; + void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + void set_target_temperature_id(uint8_t target_temperature_id) { + this->target_temperature_id_ = target_temperature_id; + } + void set_current_temperature_id(uint8_t current_temperature_id) { + this->current_temperature_id_ = current_temperature_id; + } + // void set_eco_mode_id(uint8_t eco_mode_id) { this->eco_mode_id_ = eco_mode_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override; + /// Return the traits of this controller. + climate::ClimateTraits traits() override; + + /// Re-compute the state of this climate controller. + void compute_state_(); + + /// Switch the climate device to the given climate mode. + void switch_to_action_(climate::ClimateAction action); + + Tuya *parent_; + optional switch_id_{}; + optional target_temperature_id_{}; + optional current_temperature_id_{}; + // optional eco_mode_id_{}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/fan/__init__.py b/esphome/components/tuya/fan/__init__.py index 8b4a0fa25f..e8492fd71b 100644 --- a/esphome/components/tuya/fan/__init__.py +++ b/esphome/components/tuya/fan/__init__.py @@ -1,13 +1,12 @@ from esphome.components import fan import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_OUTPUT_ID +from esphome.const import CONF_OUTPUT_ID, CONF_SWITCH_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ['tuya'] CONF_SPEED_DATAPOINT = "speed_datapoint" -CONF_SWITCH_DATAPOINT = "switch_datapoint" CONF_OSCILLATION_DATAPOINT = "oscillation_datapoint" TuyaFan = tuya_ns.class_('TuyaFan', cg.Component) diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py index adaeb52531..d014f8a763 100644 --- a/esphome/components/tuya/light/__init__.py +++ b/esphome/components/tuya/light/__init__.py @@ -2,13 +2,12 @@ from esphome.components import light import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import CONF_OUTPUT_ID, CONF_MIN_VALUE, CONF_MAX_VALUE, CONF_GAMMA_CORRECT, \ - CONF_DEFAULT_TRANSITION_LENGTH + CONF_DEFAULT_TRANSITION_LENGTH, CONF_SWITCH_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ['tuya'] CONF_DIMMER_DATAPOINT = "dimmer_datapoint" -CONF_SWITCH_DATAPOINT = "switch_datapoint" TuyaLight = tuya_ns.class_('TuyaLight', light.LightOutput, cg.Component) diff --git a/esphome/components/tuya/switch/__init__.py b/esphome/components/tuya/switch/__init__.py index bbc8122801..0eadcf0a22 100644 --- a/esphome/components/tuya/switch/__init__.py +++ b/esphome/components/tuya/switch/__init__.py @@ -1,13 +1,11 @@ from esphome.components import switch import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_SWITCH_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ['tuya'] -CONF_SWITCH_DATAPOINT = "switch_datapoint" - TuyaSwitch = tuya_ns.class_('TuyaSwitch', switch.Switch, cg.Component) CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend({ diff --git a/esphome/const.py b/esphome/const.py index 8b727b615c..61ac2563ed 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -472,6 +472,7 @@ CONF_SWING_HORIZONTAL_ACTION = 'swing_horizontal_action' CONF_SWING_MODE = 'swing_mode' CONF_SWING_OFF_ACTION = 'swing_off_action' CONF_SWING_VERTICAL_ACTION = 'swing_vertical_action' +CONF_SWITCH_DATAPOINT = "switch_datapoint" CONF_SWITCHES = 'switches' CONF_SYNC = 'sync' CONF_TABLET = 'tablet' diff --git a/tests/test4.yaml b/tests/test4.yaml index 852179f9b5..a7fbacc99d 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -75,7 +75,14 @@ sensor: # type: blue # name: APDS9960 Blue +climate: + - platform: tuya + id: tuya_climate + switch_datapoint: 1 + target_temperature_datapoint: 3 + switch: - platform: tuya id: tuya_switch switch_datapoint: 1 +