ESPHome 2025.12.0-dev
Loading...
Searching...
No Matches
tuya_climate.cpp
Go to the documentation of this file.
1#include "tuya_climate.h"
2#include "esphome/core/log.h"
3
4namespace esphome {
5namespace tuya {
6
7static const char *const TAG = "tuya.climate";
8
10 if (this->switch_id_.has_value()) {
11 this->parent_->register_listener(*this->switch_id_, [this](const TuyaDatapoint &datapoint) {
12 ESP_LOGV(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool));
14 if (datapoint.value_bool) {
15 if (this->supports_heat_ && this->supports_cool_) {
17 } else if (this->supports_heat_) {
19 } else if (this->supports_cool_) {
21 }
22 }
23 this->compute_state_();
24 this->publish_state();
25 });
26 }
27 if (this->heating_state_pin_ != nullptr) {
30 }
31 if (this->cooling_state_pin_ != nullptr) {
34 }
35 if (this->active_state_id_.has_value()) {
36 this->parent_->register_listener(*this->active_state_id_, [this](const TuyaDatapoint &datapoint) {
37 ESP_LOGV(TAG, "MCU reported active state is: %u", datapoint.value_enum);
38 this->active_state_ = datapoint.value_enum;
39 this->compute_state_();
40 this->publish_state();
41 });
42 }
44 this->parent_->register_listener(*this->target_temperature_id_, [this](const TuyaDatapoint &datapoint) {
46 if (this->reports_fahrenheit_) {
47 this->manual_temperature_ = (this->manual_temperature_ - 32) * 5 / 9;
48 }
49
50 ESP_LOGV(TAG, "MCU reported manual target temperature is: %.1f", this->manual_temperature_);
52 this->compute_state_();
53 this->publish_state();
54 });
55 }
57 this->parent_->register_listener(*this->current_temperature_id_, [this](const TuyaDatapoint &datapoint) {
59 if (this->reports_fahrenheit_) {
60 this->current_temperature = (this->current_temperature - 32) * 5 / 9;
61 }
62
63 ESP_LOGV(TAG, "MCU reported current temperature is: %.1f", this->current_temperature);
64 this->compute_state_();
65 this->publish_state();
66 });
67 }
68 if (this->eco_id_.has_value()) {
69 this->parent_->register_listener(*this->eco_id_, [this](const TuyaDatapoint &datapoint) {
70 // Whether data type is BOOL or ENUM, it will still be a 1 or a 0, so the functions below are valid in both cases
71 this->eco_ = datapoint.value_bool;
72 this->eco_type_ = datapoint.type;
73 ESP_LOGV(TAG, "MCU reported eco is: %s", ONOFF(this->eco_));
74 this->compute_preset_();
76 this->publish_state();
77 });
78 }
79 if (this->sleep_id_.has_value()) {
80 this->parent_->register_listener(*this->sleep_id_, [this](const TuyaDatapoint &datapoint) {
81 this->sleep_ = datapoint.value_bool;
82 ESP_LOGV(TAG, "MCU reported sleep is: %s", ONOFF(this->sleep_));
83 this->compute_preset_();
85 this->publish_state();
86 });
87 }
88 if (this->swing_vertical_id_.has_value()) {
89 this->parent_->register_listener(*this->swing_vertical_id_, [this](const TuyaDatapoint &datapoint) {
90 this->swing_vertical_ = datapoint.value_bool;
91 ESP_LOGV(TAG, "MCU reported vertical swing is: %s", ONOFF(datapoint.value_bool));
92 this->compute_swingmode_();
93 this->publish_state();
94 });
95 }
96
97 if (this->swing_horizontal_id_.has_value()) {
98 this->parent_->register_listener(*this->swing_horizontal_id_, [this](const TuyaDatapoint &datapoint) {
99 this->swing_horizontal_ = datapoint.value_bool;
100 ESP_LOGV(TAG, "MCU reported horizontal swing is: %s", ONOFF(datapoint.value_bool));
101 this->compute_swingmode_();
102 this->publish_state();
103 });
104 }
105
106 if (this->fan_speed_id_.has_value()) {
107 this->parent_->register_listener(*this->fan_speed_id_, [this](const TuyaDatapoint &datapoint) {
108 ESP_LOGV(TAG, "MCU reported Fan Speed Mode is: %u", datapoint.value_enum);
109 this->fan_state_ = datapoint.value_enum;
110 this->compute_fanmode_();
111 this->publish_state();
112 });
113 }
114}
115
117 bool state_changed = false;
118 if (this->heating_state_pin_ != nullptr) {
119 bool heating_state = this->heating_state_pin_->digital_read();
120 if (heating_state != this->heating_state_) {
121 ESP_LOGV(TAG, "Heating state pin changed to: %s", ONOFF(heating_state));
122 this->heating_state_ = heating_state;
123 state_changed = true;
124 }
125 }
126 if (this->cooling_state_pin_ != nullptr) {
127 bool cooling_state = this->cooling_state_pin_->digital_read();
128 if (cooling_state != this->cooling_state_) {
129 ESP_LOGV(TAG, "Cooling state pin changed to: %s", ONOFF(cooling_state));
130 this->cooling_state_ = cooling_state;
131 state_changed = true;
132 }
133 }
134
135 if (state_changed) {
136 this->compute_state_();
137 this->publish_state();
138 }
139}
140
142 if (call.get_mode().has_value()) {
143 const bool switch_state = *call.get_mode() != climate::CLIMATE_MODE_OFF;
144 ESP_LOGV(TAG, "Setting switch: %s", ONOFF(switch_state));
145 this->parent_->set_boolean_datapoint_value(*this->switch_id_, switch_state);
146 const climate::ClimateMode new_mode = *call.get_mode();
147
148 if (this->active_state_id_.has_value()) {
149 if (new_mode == climate::CLIMATE_MODE_HEAT && this->supports_heat_) {
151 } else if (new_mode == climate::CLIMATE_MODE_COOL && this->supports_cool_) {
153 } else if (new_mode == climate::CLIMATE_MODE_DRY && this->active_state_drying_value_.has_value()) {
155 } else if (new_mode == climate::CLIMATE_MODE_FAN_ONLY && this->active_state_fanonly_value_.has_value()) {
157 }
158 } else {
159 ESP_LOGW(TAG, "Active state (mode) datapoint not configured");
160 }
161 }
162
165
166 if (call.get_target_temperature().has_value()) {
167 float target_temperature = *call.get_target_temperature();
168 if (this->reports_fahrenheit_)
169 target_temperature = (target_temperature * 9 / 5) + 32;
170
171 ESP_LOGV(TAG, "Setting target temperature: %.1f", target_temperature);
173 (int) (target_temperature / this->target_temperature_multiplier_));
174 }
175
176 if (call.get_preset().has_value()) {
177 const climate::ClimatePreset preset = *call.get_preset();
178 if (this->eco_id_.has_value()) {
179 const bool eco = preset == climate::CLIMATE_PRESET_ECO;
180 ESP_LOGV(TAG, "Setting eco: %s", ONOFF(eco));
181 if (this->eco_type_ == TuyaDatapointType::ENUM) {
182 this->parent_->set_enum_datapoint_value(*this->eco_id_, eco);
183 } else {
184 this->parent_->set_boolean_datapoint_value(*this->eco_id_, eco);
185 }
186 }
187 if (this->sleep_id_.has_value()) {
188 const bool sleep = preset == climate::CLIMATE_PRESET_SLEEP;
189 ESP_LOGV(TAG, "Setting sleep: %s", ONOFF(sleep));
190 this->parent_->set_boolean_datapoint_value(*this->sleep_id_, sleep);
191 }
192 }
193}
194
196 bool vertical_swing_changed = false;
197 bool horizontal_swing_changed = false;
198
199 if (call.get_swing_mode().has_value()) {
200 const auto swing_mode = *call.get_swing_mode();
201
202 switch (swing_mode) {
205 this->swing_vertical_ = false;
206 this->swing_horizontal_ = false;
207 vertical_swing_changed = true;
208 horizontal_swing_changed = true;
209 }
210 break;
211
214 this->swing_vertical_ = true;
215 this->swing_horizontal_ = true;
216 vertical_swing_changed = true;
217 horizontal_swing_changed = true;
218 }
219 break;
220
223 this->swing_vertical_ = true;
224 this->swing_horizontal_ = false;
225 vertical_swing_changed = true;
226 horizontal_swing_changed = true;
227 }
228 break;
229
232 this->swing_vertical_ = false;
233 this->swing_horizontal_ = true;
234 vertical_swing_changed = true;
235 horizontal_swing_changed = true;
236 }
237 break;
238
239 default:
240 break;
241 }
242 }
243
244 if (vertical_swing_changed && this->swing_vertical_id_.has_value()) {
245 ESP_LOGV(TAG, "Setting vertical swing: %s", ONOFF(swing_vertical_));
247 }
248
249 if (horizontal_swing_changed && this->swing_horizontal_id_.has_value()) {
250 ESP_LOGV(TAG, "Setting horizontal swing: %s", ONOFF(swing_horizontal_));
252 }
253
254 // Publish the state after updating the swing mode
255 this->publish_state();
256}
257
259 if (call.get_fan_mode().has_value()) {
260 climate::ClimateFanMode fan_mode = *call.get_fan_mode();
261
262 uint8_t tuya_fan_speed;
263 switch (fan_mode) {
265 tuya_fan_speed = *fan_speed_low_value_;
266 break;
268 tuya_fan_speed = *fan_speed_medium_value_;
269 break;
271 tuya_fan_speed = *fan_speed_middle_value_;
272 break;
274 tuya_fan_speed = *fan_speed_high_value_;
275 break;
277 tuya_fan_speed = *fan_speed_auto_value_;
278 break;
279 default:
280 tuya_fan_speed = 0;
281 break;
282 }
283
284 if (this->fan_speed_id_.has_value()) {
285 this->parent_->set_enum_datapoint_value(*this->fan_speed_id_, tuya_fan_speed);
286 }
287 }
288}
289
295 }
296
297 if (supports_heat_)
299 if (supports_cool_)
305 if (this->eco_id_.has_value()) {
307 }
308 if (this->sleep_id_.has_value()) {
310 }
311 if (this->sleep_id_.has_value() || this->eco_id_.has_value()) {
313 }
314 if (this->swing_vertical_id_.has_value() && this->swing_horizontal_id_.has_value()) {
317 } else if (this->swing_vertical_id_.has_value()) {
319 } else if (this->swing_horizontal_id_.has_value()) {
321 }
322
323 if (fan_speed_id_) {
334 }
335 return traits;
336}
337
339 LOG_CLIMATE("", "Tuya Climate", this);
340 if (this->switch_id_.has_value()) {
341 ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_);
342 }
343 if (this->active_state_id_.has_value()) {
344 ESP_LOGCONFIG(TAG, " Active state has datapoint ID %u", *this->active_state_id_);
345 }
346 if (this->target_temperature_id_.has_value()) {
347 ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *this->target_temperature_id_);
348 }
350 ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *this->current_temperature_id_);
351 }
352 LOG_PIN(" Heating State Pin: ", this->heating_state_pin_);
353 LOG_PIN(" Cooling State Pin: ", this->cooling_state_pin_);
354 if (this->eco_id_.has_value()) {
355 ESP_LOGCONFIG(TAG, " Eco has datapoint ID %u", *this->eco_id_);
356 }
357 if (this->sleep_id_.has_value()) {
358 ESP_LOGCONFIG(TAG, " Sleep has datapoint ID %u", *this->sleep_id_);
359 }
360 if (this->swing_vertical_id_.has_value()) {
361 ESP_LOGCONFIG(TAG, " Swing Vertical has datapoint ID %u", *this->swing_vertical_id_);
362 }
363 if (this->swing_horizontal_id_.has_value()) {
364 ESP_LOGCONFIG(TAG, " Swing Horizontal has datapoint ID %u", *this->swing_horizontal_id_);
365 }
366}
367
369 if (this->eco_) {
371 } else if (this->sleep_) {
373 } else {
375 }
376}
377
379 if (this->swing_vertical_ && this->swing_horizontal_) {
381 } else if (this->swing_vertical_) {
383 } else if (this->swing_horizontal_) {
385 } else {
387 }
388}
389
391 if (this->fan_speed_id_.has_value()) {
392 // Use state from MCU datapoint
393 if (this->fan_speed_auto_value_.has_value() && this->fan_state_ == this->fan_speed_auto_value_) {
395 } else if (this->fan_speed_high_value_.has_value() && this->fan_state_ == this->fan_speed_high_value_) {
397 } else if (this->fan_speed_medium_value_.has_value() && this->fan_state_ == this->fan_speed_medium_value_) {
399 } else if (this->fan_speed_middle_value_.has_value() && this->fan_state_ == this->fan_speed_middle_value_) {
401 } else if (this->fan_speed_low_value_.has_value() && this->fan_state_ == this->fan_speed_low_value_) {
403 }
404 }
405}
406
408 if (this->eco_ && this->eco_temperature_.has_value()) {
410 } else {
412 }
413}
414
416 if (std::isnan(this->current_temperature) || std::isnan(this->target_temperature)) {
417 // if any control parameters are nan, go to OFF action (not IDLE!)
419 return;
420 }
421
422 if (this->mode == climate::CLIMATE_MODE_OFF) {
424 return;
425 }
426
428 if (this->heating_state_pin_ != nullptr || this->cooling_state_pin_ != nullptr) {
429 // Use state from input pins
430 if (this->heating_state_) {
431 target_action = climate::CLIMATE_ACTION_HEATING;
433 } else if (this->cooling_state_) {
434 target_action = climate::CLIMATE_ACTION_COOLING;
436 }
437 if (this->active_state_id_.has_value()) {
438 // Both are available, use MCU datapoint as mode
440 this->active_state_ == this->active_state_heating_value_) {
442 } else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() &&
443 this->active_state_ == this->active_state_cooling_value_) {
445 } else if (this->active_state_drying_value_.has_value() &&
446 this->active_state_ == this->active_state_drying_value_) {
448 } else if (this->active_state_fanonly_value_.has_value() &&
449 this->active_state_ == this->active_state_fanonly_value_) {
451 }
452 }
453 } else if (this->active_state_id_.has_value()) {
454 // Use state from MCU datapoint
456 this->active_state_ == this->active_state_heating_value_) {
457 target_action = climate::CLIMATE_ACTION_HEATING;
459 } else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() &&
460 this->active_state_ == this->active_state_cooling_value_) {
461 target_action = climate::CLIMATE_ACTION_COOLING;
463 } else if (this->active_state_drying_value_.has_value() &&
464 this->active_state_ == this->active_state_drying_value_) {
465 target_action = climate::CLIMATE_ACTION_DRYING;
467 } else if (this->active_state_fanonly_value_.has_value() &&
468 this->active_state_ == this->active_state_fanonly_value_) {
469 target_action = climate::CLIMATE_ACTION_FAN;
471 }
472 } else {
473 // Fallback to active state calc based on temp and hysteresis
474 const float temp_diff = this->target_temperature - this->current_temperature;
475 if (std::abs(temp_diff) > this->hysteresis_) {
476 if (this->supports_heat_ && temp_diff > 0) {
477 target_action = climate::CLIMATE_ACTION_HEATING;
479 } else if (this->supports_cool_ && temp_diff < 0) {
480 target_action = climate::CLIMATE_ACTION_COOLING;
482 }
483 }
484 }
485
486 this->switch_to_action_(target_action);
487}
488
490 // For now this just sets the current action but could include triggers later
491 this->action = action;
492}
493
494} // namespace tuya
495} // namespace esphome
virtual void setup()=0
virtual bool digital_read()=0
This class is used to encode all control actions on a climate device.
Definition climate.h:33
const optional< ClimatePreset > & get_preset() const
Definition climate.cpp:295
ClimateMode mode
The active mode of the climate device.
Definition climate.h:256
optional< ClimateFanMode > fan_mode
The active fan mode of the climate device.
Definition climate.h:250
float target_temperature
The target temperature of the climate device.
Definition climate.h:237
ClimateSwingMode swing_mode
The active swing mode of the climate device.
Definition climate.h:262
float current_temperature
The current temperature of the climate device, as reported from the integration.
Definition climate.h:230
ClimateAction action
The active state of the climate device.
Definition climate.h:259
void publish_state()
Publish the state of the climate device, to be called from integrations.
Definition climate.cpp:426
optional< ClimatePreset > preset
The active preset of the climate device.
Definition climate.h:253
void add_feature_flags(uint32_t feature_flags)
void add_supported_fan_mode(ClimateFanMode mode)
void add_supported_preset(ClimatePreset preset)
void set_supported_swing_modes(ClimateSwingModeMask modes)
void add_supported_mode(ClimateMode mode)
bool has_value() const
Definition optional.h:92
optional< uint8_t > fan_speed_high_value_
void switch_to_action_(climate::ClimateAction action)
Switch the climate device to the given climate mode.
optional< uint8_t > swing_vertical_id_
void compute_fanmode_()
Re-Compute the fan mode of this climate controller.
optional< uint8_t > fan_speed_low_value_
optional< uint8_t > current_temperature_id_
void control_fan_mode_(const climate::ClimateCall &call)
Override control to change settings of fan mode.
void compute_target_temperature_()
Re-compute the target temperature of this climate controller.
optional< uint8_t > active_state_id_
optional< uint8_t > fan_speed_auto_value_
void compute_swingmode_()
Re-Compute the swing mode of this climate controller.
TuyaDatapointType eco_type_
void compute_preset_()
Re-compute the active preset of this climate controller.
optional< uint8_t > swing_horizontal_id_
optional< float > eco_temperature_
optional< uint8_t > active_state_fanonly_value_
optional< uint8_t > fan_speed_medium_value_
void control_swing_mode_(const climate::ClimateCall &call)
Override control to change settings of swing mode.
optional< uint8_t > sleep_id_
void compute_state_()
Re-compute the state of this climate controller.
optional< uint8_t > fan_speed_middle_value_
optional< uint8_t > active_state_cooling_value_
optional< uint8_t > active_state_drying_value_
climate::ClimateTraits traits() override
Return the traits of this controller.
optional< uint8_t > fan_speed_id_
optional< uint8_t > active_state_heating_value_
optional< uint8_t > target_temperature_id_
void control(const climate::ClimateCall &call) override
Override control to change settings of the climate device.
optional< uint8_t > eco_id_
optional< uint8_t > switch_id_
void set_boolean_datapoint_value(uint8_t datapoint_id, bool value)
Definition tuya.cpp:594
void set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value)
Definition tuya.cpp:606
void register_listener(uint8_t datapoint_id, const std::function< void(TuyaDatapoint)> &func)
Definition tuya.cpp:722
void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value)
Definition tuya.cpp:598
@ CLIMATE_SUPPORTS_CURRENT_TEMPERATURE
ClimatePreset
Enum for all preset modes NOTE: If adding values, update ClimatePresetMask in climate_traits....
@ CLIMATE_PRESET_NONE
No preset is active.
@ CLIMATE_PRESET_SLEEP
Device is prepared for sleep.
@ CLIMATE_PRESET_ECO
Device is running an energy-saving preset.
@ CLIMATE_SWING_OFF
The swing mode is set to Off.
@ CLIMATE_SWING_HORIZONTAL
The fan mode is set to Horizontal.
@ CLIMATE_SWING_VERTICAL
The fan mode is set to Vertical.
@ CLIMATE_SWING_BOTH
The fan mode is set to Both.
ClimateMode
Enum for all modes a climate device can be in.
@ CLIMATE_MODE_DRY
The climate device is set to dry/humidity mode.
@ CLIMATE_MODE_FAN_ONLY
The climate device only has the fan enabled, no heating or cooling is taking place.
@ CLIMATE_MODE_HEAT
The climate device is set to heat to reach the target temperature.
@ CLIMATE_MODE_COOL
The climate device is set to cool to reach the target temperature.
@ CLIMATE_MODE_HEAT_COOL
The climate device is set to heat/cool to reach the target temperature.
@ CLIMATE_MODE_OFF
The climate device is off.
ClimateAction
Enum for the current action of the climate device. Values match those of ClimateMode.
@ CLIMATE_ACTION_OFF
The climate device is off (inactive or no power)
@ CLIMATE_ACTION_IDLE
The climate device is idle (monitoring climate but no action needed)
@ CLIMATE_ACTION_DRYING
The climate device is drying.
@ CLIMATE_ACTION_HEATING
The climate device is actively heating.
@ CLIMATE_ACTION_COOLING
The climate device is actively cooling.
@ CLIMATE_ACTION_FAN
The climate device is in fan only mode.
ClimateFanMode
NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value.
@ CLIMATE_FAN_MEDIUM
The fan mode is set to Medium.
@ CLIMATE_FAN_AUTO
The fan mode is set to Auto.
@ CLIMATE_FAN_LOW
The fan mode is set to Low.
@ CLIMATE_FAN_MIDDLE
The fan mode is set to Middle.
@ CLIMATE_FAN_HIGH
The fan mode is set to High.
const char *const TAG
Definition spi.cpp:8
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
TuyaDatapointType type
Definition tuya.h:30