ESPHome 2026.5.1
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::tuya {
5
6static const char *const TAG = "tuya.climate";
7
9 auto switch_id = this->switch_id_;
10 if (switch_id.has_value()) {
11 this->parent_->register_listener(*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 auto active_state_id = this->active_state_id_;
36 if (active_state_id.has_value()) {
37 this->parent_->register_listener(*active_state_id, [this](const TuyaDatapoint &datapoint) {
38 ESP_LOGV(TAG, "MCU reported active state is: %u", datapoint.value_enum);
39 this->active_state_ = datapoint.value_enum;
40 this->compute_state_();
41 this->publish_state();
42 });
43 }
44 auto target_temp_id = this->target_temperature_id_;
45 if (target_temp_id.has_value()) {
46 this->parent_->register_listener(*target_temp_id, [this](const TuyaDatapoint &datapoint) {
48 if (this->reports_fahrenheit_) {
49 this->manual_temperature_ = (this->manual_temperature_ - 32) * 5 / 9;
50 }
51
52 ESP_LOGV(TAG, "MCU reported manual target temperature is: %.1f", this->manual_temperature_);
54 this->compute_state_();
55 this->publish_state();
56 });
57 }
58 auto current_temp_id = this->current_temperature_id_;
59 if (current_temp_id.has_value()) {
60 this->parent_->register_listener(*current_temp_id, [this](const TuyaDatapoint &datapoint) {
62 if (this->reports_fahrenheit_) {
63 this->current_temperature = (this->current_temperature - 32) * 5 / 9;
64 }
65
66 ESP_LOGV(TAG, "MCU reported current temperature is: %.1f", this->current_temperature);
67 this->compute_state_();
68 this->publish_state();
69 });
70 }
71 auto eco_id = this->eco_id_;
72 if (eco_id.has_value()) {
73 this->parent_->register_listener(*eco_id, [this](const TuyaDatapoint &datapoint) {
74 // 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
75 this->eco_ = datapoint.value_bool;
76 this->eco_type_ = datapoint.type;
77 ESP_LOGV(TAG, "MCU reported eco is: %s", ONOFF(this->eco_));
78 this->compute_preset_();
80 this->publish_state();
81 });
82 }
83 auto sleep_id = this->sleep_id_;
84 if (sleep_id.has_value()) {
85 this->parent_->register_listener(*sleep_id, [this](const TuyaDatapoint &datapoint) {
86 this->sleep_ = datapoint.value_bool;
87 ESP_LOGV(TAG, "MCU reported sleep is: %s", ONOFF(this->sleep_));
88 this->compute_preset_();
90 this->publish_state();
91 });
92 }
93 auto swing_vert_id = this->swing_vertical_id_;
94 if (swing_vert_id.has_value()) {
95 this->parent_->register_listener(*swing_vert_id, [this](const TuyaDatapoint &datapoint) {
96 this->swing_vertical_ = datapoint.value_bool;
97 ESP_LOGV(TAG, "MCU reported vertical swing is: %s", ONOFF(datapoint.value_bool));
98 this->compute_swingmode_();
99 this->publish_state();
100 });
101 }
102
103 auto swing_horiz_id = this->swing_horizontal_id_;
104 if (swing_horiz_id.has_value()) {
105 this->parent_->register_listener(*swing_horiz_id, [this](const TuyaDatapoint &datapoint) {
106 this->swing_horizontal_ = datapoint.value_bool;
107 ESP_LOGV(TAG, "MCU reported horizontal swing is: %s", ONOFF(datapoint.value_bool));
108 this->compute_swingmode_();
109 this->publish_state();
110 });
111 }
112
113 auto fan_speed_id = this->fan_speed_id_;
114 if (fan_speed_id.has_value()) {
115 this->parent_->register_listener(*fan_speed_id, [this](const TuyaDatapoint &datapoint) {
116 ESP_LOGV(TAG, "MCU reported Fan Speed Mode is: %u", datapoint.value_enum);
117 this->fan_state_ = datapoint.value_enum;
118 this->compute_fanmode_();
119 this->publish_state();
120 });
121 }
122}
123
125 bool state_changed = false;
126 if (this->heating_state_pin_ != nullptr) {
127 bool heating_state = this->heating_state_pin_->digital_read();
128 if (heating_state != this->heating_state_) {
129 ESP_LOGV(TAG, "Heating state pin changed to: %s", ONOFF(heating_state));
130 this->heating_state_ = heating_state;
131 state_changed = true;
132 }
133 }
134 if (this->cooling_state_pin_ != nullptr) {
135 bool cooling_state = this->cooling_state_pin_->digital_read();
136 if (cooling_state != this->cooling_state_) {
137 ESP_LOGV(TAG, "Cooling state pin changed to: %s", ONOFF(cooling_state));
138 this->cooling_state_ = cooling_state;
139 state_changed = true;
140 }
141 }
142
143 if (state_changed) {
144 this->compute_state_();
145 this->publish_state();
146 }
147}
148
150 auto mode = call.get_mode();
151 if (mode.has_value()) {
152 const bool switch_state = *mode != climate::CLIMATE_MODE_OFF;
153 ESP_LOGV(TAG, "Setting switch: %s", ONOFF(switch_state));
154 auto switch_dp_id = this->switch_id_;
155 if (switch_dp_id.has_value()) {
156 this->parent_->set_boolean_datapoint_value(*switch_dp_id, switch_state);
157 }
158 const climate::ClimateMode new_mode = *mode;
159
160 auto active_state_dp_id = this->active_state_id_;
161 if (active_state_dp_id.has_value()) {
162 if (new_mode == climate::CLIMATE_MODE_HEAT && this->supports_heat_) {
163 auto heating_val = this->active_state_heating_value_;
164 if (heating_val.has_value())
165 this->parent_->set_enum_datapoint_value(*active_state_dp_id, *heating_val);
166 } else if (new_mode == climate::CLIMATE_MODE_COOL && this->supports_cool_) {
167 auto cooling_val = this->active_state_cooling_value_;
168 if (cooling_val.has_value())
169 this->parent_->set_enum_datapoint_value(*active_state_dp_id, *cooling_val);
170 } else if (new_mode == climate::CLIMATE_MODE_DRY) {
171 auto drying_val = this->active_state_drying_value_;
172 if (drying_val.has_value())
173 this->parent_->set_enum_datapoint_value(*active_state_dp_id, *drying_val);
174 } else if (new_mode == climate::CLIMATE_MODE_FAN_ONLY) {
175 auto fanonly_val = this->active_state_fanonly_value_;
176 if (fanonly_val.has_value())
177 this->parent_->set_enum_datapoint_value(*active_state_dp_id, *fanonly_val);
178 }
179 } else {
180 ESP_LOGW(TAG, "Active state (mode) datapoint not configured");
181 }
182 }
183
186
187 auto target_temp = call.get_target_temperature();
188 if (target_temp.has_value()) {
189 float target_temperature = *target_temp;
190 if (this->reports_fahrenheit_)
191 target_temperature = (target_temperature * 9 / 5) + 32;
192
193 ESP_LOGV(TAG, "Setting target temperature: %.1f", target_temperature);
194 auto target_temp_dp_id = this->target_temperature_id_;
195 if (target_temp_dp_id.has_value()) {
196 this->parent_->set_integer_datapoint_value(*target_temp_dp_id,
198 }
199 }
200
201 auto preset_val = call.get_preset();
202 if (preset_val.has_value()) {
203 const climate::ClimatePreset preset = *preset_val;
204 auto eco_dp_id = this->eco_id_;
205 if (eco_dp_id.has_value()) {
206 const bool eco = preset == climate::CLIMATE_PRESET_ECO;
207 ESP_LOGV(TAG, "Setting eco: %s", ONOFF(eco));
208 if (this->eco_type_ == TuyaDatapointType::ENUM) {
209 this->parent_->set_enum_datapoint_value(*eco_dp_id, eco);
210 } else {
211 this->parent_->set_boolean_datapoint_value(*eco_dp_id, eco);
212 }
213 }
214 auto sleep_dp_id = this->sleep_id_;
215 if (sleep_dp_id.has_value()) {
216 const bool sleep = preset == climate::CLIMATE_PRESET_SLEEP;
217 ESP_LOGV(TAG, "Setting sleep: %s", ONOFF(sleep));
218 this->parent_->set_boolean_datapoint_value(*sleep_dp_id, sleep);
219 }
220 }
221}
222
224 bool vertical_swing_changed = false;
225 bool horizontal_swing_changed = false;
226
227 auto swing_mode_val = call.get_swing_mode();
228 if (swing_mode_val.has_value()) {
229 const auto swing_mode = *swing_mode_val;
230
231 switch (swing_mode) {
234 this->swing_vertical_ = false;
235 this->swing_horizontal_ = false;
236 vertical_swing_changed = true;
237 horizontal_swing_changed = true;
238 }
239 break;
240
243 this->swing_vertical_ = true;
244 this->swing_horizontal_ = true;
245 vertical_swing_changed = true;
246 horizontal_swing_changed = true;
247 }
248 break;
249
252 this->swing_vertical_ = true;
253 this->swing_horizontal_ = false;
254 vertical_swing_changed = true;
255 horizontal_swing_changed = true;
256 }
257 break;
258
261 this->swing_vertical_ = false;
262 this->swing_horizontal_ = true;
263 vertical_swing_changed = true;
264 horizontal_swing_changed = true;
265 }
266 break;
267
268 default:
269 break;
270 }
271 }
272
273 auto vert_dp_id = this->swing_vertical_id_;
274 if (vertical_swing_changed && vert_dp_id.has_value()) {
275 ESP_LOGV(TAG, "Setting vertical swing: %s", ONOFF(swing_vertical_));
277 }
278
279 auto horiz_dp_id = this->swing_horizontal_id_;
280 if (horizontal_swing_changed && horiz_dp_id.has_value()) {
281 ESP_LOGV(TAG, "Setting horizontal swing: %s", ONOFF(swing_horizontal_));
283 }
284
285 // Publish the state after updating the swing mode
286 this->publish_state();
287}
288
290 auto fan_mode_val = call.get_fan_mode();
291 if (fan_mode_val.has_value()) {
292 climate::ClimateFanMode fan_mode = *fan_mode_val;
293
294 uint8_t tuya_fan_speed;
295 switch (fan_mode) {
297 tuya_fan_speed = this->fan_speed_low_value_.value_or(0);
298 break;
300 tuya_fan_speed = this->fan_speed_medium_value_.value_or(0);
301 break;
303 tuya_fan_speed = this->fan_speed_middle_value_.value_or(0);
304 break;
306 tuya_fan_speed = this->fan_speed_high_value_.value_or(0);
307 break;
309 tuya_fan_speed = this->fan_speed_auto_value_.value_or(0);
310 break;
311 default:
312 tuya_fan_speed = 0;
313 break;
314 }
315
316 auto fan_speed_dp_id = this->fan_speed_id_;
317 if (fan_speed_dp_id.has_value()) {
318 this->parent_->set_enum_datapoint_value(*fan_speed_dp_id, tuya_fan_speed);
319 }
320 }
321}
322
326 if (this->current_temperature_id_.has_value()) {
328 }
329
330 if (supports_heat_)
332 if (supports_cool_)
334 if (this->active_state_drying_value_.has_value())
336 if (this->active_state_fanonly_value_.has_value())
338 if (this->eco_id_.has_value()) {
340 }
341 if (this->sleep_id_.has_value()) {
343 }
344 if (this->sleep_id_.has_value() || this->eco_id_.has_value()) {
346 }
347 if (this->swing_vertical_id_.has_value() && this->swing_horizontal_id_.has_value()) {
350 } else if (this->swing_vertical_id_.has_value()) {
352 } else if (this->swing_horizontal_id_.has_value()) {
354 }
355
356 if (fan_speed_id_) {
367 }
368 return traits;
369}
370
372 LOG_CLIMATE("", "Tuya Climate", this);
373 auto switch_dp_id = this->switch_id_;
374 if (switch_dp_id.has_value()) {
375 ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *switch_dp_id);
376 }
377 auto active_state_dp_id = this->active_state_id_;
378 if (active_state_dp_id.has_value()) {
379 ESP_LOGCONFIG(TAG, " Active state has datapoint ID %u", *active_state_dp_id);
380 }
381 auto target_temp_dp_id = this->target_temperature_id_;
382 if (target_temp_dp_id.has_value()) {
383 ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *target_temp_dp_id);
384 }
385 auto current_temp_dp_id = this->current_temperature_id_;
386 if (current_temp_dp_id.has_value()) {
387 ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *current_temp_dp_id);
388 }
389 LOG_PIN(" Heating State Pin: ", this->heating_state_pin_);
390 LOG_PIN(" Cooling State Pin: ", this->cooling_state_pin_);
391 auto eco_dp_id = this->eco_id_;
392 if (eco_dp_id.has_value()) {
393 ESP_LOGCONFIG(TAG, " Eco has datapoint ID %u", *eco_dp_id);
394 }
395 auto sleep_dp_id = this->sleep_id_;
396 if (sleep_dp_id.has_value()) {
397 ESP_LOGCONFIG(TAG, " Sleep has datapoint ID %u", *sleep_dp_id);
398 }
399 auto swing_vert_dp_id = this->swing_vertical_id_;
400 if (swing_vert_dp_id.has_value()) {
401 ESP_LOGCONFIG(TAG, " Swing Vertical has datapoint ID %u", *swing_vert_dp_id);
402 }
403 auto swing_horiz_dp_id = this->swing_horizontal_id_;
404 if (swing_horiz_dp_id.has_value()) {
405 ESP_LOGCONFIG(TAG, " Swing Horizontal has datapoint ID %u", *swing_horiz_dp_id);
406 }
407}
408
410 if (this->eco_) {
412 } else if (this->sleep_) {
414 } else {
416 }
417}
418
420 if (this->swing_vertical_ && this->swing_horizontal_) {
422 } else if (this->swing_vertical_) {
424 } else if (this->swing_horizontal_) {
426 } else {
428 }
429}
430
432 if (this->fan_speed_id_.has_value()) {
433 // Use state from MCU datapoint
434 if (this->fan_speed_auto_value_.has_value() && this->fan_state_ == this->fan_speed_auto_value_) {
436 } else if (this->fan_speed_high_value_.has_value() && this->fan_state_ == this->fan_speed_high_value_) {
438 } else if (this->fan_speed_medium_value_.has_value() && this->fan_state_ == this->fan_speed_medium_value_) {
440 } else if (this->fan_speed_middle_value_.has_value() && this->fan_state_ == this->fan_speed_middle_value_) {
442 } else if (this->fan_speed_low_value_.has_value() && this->fan_state_ == this->fan_speed_low_value_) {
444 }
445 }
446}
447
449 if (this->eco_ && this->eco_temperature_.has_value()) {
451 } else {
453 }
454}
455
457 if (std::isnan(this->current_temperature) || std::isnan(this->target_temperature)) {
458 // if any control parameters are nan, go to OFF action (not IDLE!)
460 return;
461 }
462
463 if (this->mode == climate::CLIMATE_MODE_OFF) {
465 return;
466 }
467
469 if (this->heating_state_pin_ != nullptr || this->cooling_state_pin_ != nullptr) {
470 // Use state from input pins
471 if (this->heating_state_) {
472 target_action = climate::CLIMATE_ACTION_HEATING;
474 } else if (this->cooling_state_) {
475 target_action = climate::CLIMATE_ACTION_COOLING;
477 }
478 if (this->active_state_id_.has_value()) {
479 // Both are available, use MCU datapoint as mode
480 if (this->supports_heat_ && this->active_state_heating_value_.has_value() &&
481 this->active_state_ == this->active_state_heating_value_) {
483 } else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() &&
484 this->active_state_ == this->active_state_cooling_value_) {
486 } else if (this->active_state_drying_value_.has_value() &&
487 this->active_state_ == this->active_state_drying_value_) {
489 } else if (this->active_state_fanonly_value_.has_value() &&
490 this->active_state_ == this->active_state_fanonly_value_) {
492 }
493 }
494 } else if (this->active_state_id_.has_value()) {
495 // Use state from MCU datapoint
496 if (this->supports_heat_ && this->active_state_heating_value_.has_value() &&
497 this->active_state_ == this->active_state_heating_value_) {
498 target_action = climate::CLIMATE_ACTION_HEATING;
500 } else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() &&
501 this->active_state_ == this->active_state_cooling_value_) {
502 target_action = climate::CLIMATE_ACTION_COOLING;
504 } else if (this->active_state_drying_value_.has_value() &&
505 this->active_state_ == this->active_state_drying_value_) {
506 target_action = climate::CLIMATE_ACTION_DRYING;
508 } else if (this->active_state_fanonly_value_.has_value() &&
509 this->active_state_ == this->active_state_fanonly_value_) {
510 target_action = climate::CLIMATE_ACTION_FAN;
512 }
513 } else {
514 // Fallback to active state calc based on temp and hysteresis
515 const float temp_diff = this->target_temperature - this->current_temperature;
516 if (std::abs(temp_diff) > this->hysteresis_) {
517 if (this->supports_heat_ && temp_diff > 0) {
518 target_action = climate::CLIMATE_ACTION_HEATING;
520 } else if (this->supports_cool_ && temp_diff < 0) {
521 target_action = climate::CLIMATE_ACTION_COOLING;
523 }
524 }
525 }
526
527 this->switch_to_action_(target_action);
528}
529
531 // For now this just sets the current action but could include triggers later
532 this->action = action;
533}
534
535} // namespace esphome::tuya
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:34
ClimateMode mode
The active mode of the climate device.
Definition climate.h:293
optional< ClimateFanMode > fan_mode
The active fan mode of the climate device.
Definition climate.h:287
float target_temperature
The target temperature of the climate device.
Definition climate.h:274
ClimateSwingMode swing_mode
The active swing mode of the climate device.
Definition climate.h:299
float current_temperature
The current temperature of the climate device, as reported from the integration.
Definition climate.h:267
ClimateAction action
The active state of the climate device.
Definition climate.h:296
void publish_state()
Publish the state of the climate device, to be called from integrations.
Definition climate.cpp:437
optional< ClimatePreset > preset
The active preset of the climate device.
Definition climate.h:290
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)
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:618
void set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value)
Definition tuya.cpp:630
void register_listener(uint8_t datapoint_id, const std::function< void(TuyaDatapoint)> &func)
Definition tuya.cpp:749
void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value)
Definition tuya.cpp:622
@ 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:7
TuyaDatapointType type
Definition tuya.h:29