ESPHome 2026.3.0
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 auto switch_id = this->switch_id_;
11 if (switch_id.has_value()) {
12 this->parent_->register_listener(*switch_id, [this](const TuyaDatapoint &datapoint) {
13 ESP_LOGV(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool));
15 if (datapoint.value_bool) {
16 if (this->supports_heat_ && this->supports_cool_) {
18 } else if (this->supports_heat_) {
20 } else if (this->supports_cool_) {
22 }
23 }
24 this->compute_state_();
25 this->publish_state();
26 });
27 }
28 if (this->heating_state_pin_ != nullptr) {
31 }
32 if (this->cooling_state_pin_ != nullptr) {
35 }
36 auto active_state_id = this->active_state_id_;
37 if (active_state_id.has_value()) {
38 this->parent_->register_listener(*active_state_id, [this](const TuyaDatapoint &datapoint) {
39 ESP_LOGV(TAG, "MCU reported active state is: %u", datapoint.value_enum);
40 this->active_state_ = datapoint.value_enum;
41 this->compute_state_();
42 this->publish_state();
43 });
44 }
45 auto target_temp_id = this->target_temperature_id_;
46 if (target_temp_id.has_value()) {
47 this->parent_->register_listener(*target_temp_id, [this](const TuyaDatapoint &datapoint) {
49 if (this->reports_fahrenheit_) {
50 this->manual_temperature_ = (this->manual_temperature_ - 32) * 5 / 9;
51 }
52
53 ESP_LOGV(TAG, "MCU reported manual target temperature is: %.1f", this->manual_temperature_);
55 this->compute_state_();
56 this->publish_state();
57 });
58 }
59 auto current_temp_id = this->current_temperature_id_;
60 if (current_temp_id.has_value()) {
61 this->parent_->register_listener(*current_temp_id, [this](const TuyaDatapoint &datapoint) {
63 if (this->reports_fahrenheit_) {
64 this->current_temperature = (this->current_temperature - 32) * 5 / 9;
65 }
66
67 ESP_LOGV(TAG, "MCU reported current temperature is: %.1f", this->current_temperature);
68 this->compute_state_();
69 this->publish_state();
70 });
71 }
72 auto eco_id = this->eco_id_;
73 if (eco_id.has_value()) {
74 this->parent_->register_listener(*eco_id, [this](const TuyaDatapoint &datapoint) {
75 // 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
76 this->eco_ = datapoint.value_bool;
77 this->eco_type_ = datapoint.type;
78 ESP_LOGV(TAG, "MCU reported eco is: %s", ONOFF(this->eco_));
79 this->compute_preset_();
81 this->publish_state();
82 });
83 }
84 auto sleep_id = this->sleep_id_;
85 if (sleep_id.has_value()) {
86 this->parent_->register_listener(*sleep_id, [this](const TuyaDatapoint &datapoint) {
87 this->sleep_ = datapoint.value_bool;
88 ESP_LOGV(TAG, "MCU reported sleep is: %s", ONOFF(this->sleep_));
89 this->compute_preset_();
91 this->publish_state();
92 });
93 }
94 auto swing_vert_id = this->swing_vertical_id_;
95 if (swing_vert_id.has_value()) {
96 this->parent_->register_listener(*swing_vert_id, [this](const TuyaDatapoint &datapoint) {
97 this->swing_vertical_ = datapoint.value_bool;
98 ESP_LOGV(TAG, "MCU reported vertical swing is: %s", ONOFF(datapoint.value_bool));
99 this->compute_swingmode_();
100 this->publish_state();
101 });
102 }
103
104 auto swing_horiz_id = this->swing_horizontal_id_;
105 if (swing_horiz_id.has_value()) {
106 this->parent_->register_listener(*swing_horiz_id, [this](const TuyaDatapoint &datapoint) {
107 this->swing_horizontal_ = datapoint.value_bool;
108 ESP_LOGV(TAG, "MCU reported horizontal swing is: %s", ONOFF(datapoint.value_bool));
109 this->compute_swingmode_();
110 this->publish_state();
111 });
112 }
113
114 auto fan_speed_id = this->fan_speed_id_;
115 if (fan_speed_id.has_value()) {
116 this->parent_->register_listener(*fan_speed_id, [this](const TuyaDatapoint &datapoint) {
117 ESP_LOGV(TAG, "MCU reported Fan Speed Mode is: %u", datapoint.value_enum);
118 this->fan_state_ = datapoint.value_enum;
119 this->compute_fanmode_();
120 this->publish_state();
121 });
122 }
123}
124
126 bool state_changed = false;
127 if (this->heating_state_pin_ != nullptr) {
128 bool heating_state = this->heating_state_pin_->digital_read();
129 if (heating_state != this->heating_state_) {
130 ESP_LOGV(TAG, "Heating state pin changed to: %s", ONOFF(heating_state));
131 this->heating_state_ = heating_state;
132 state_changed = true;
133 }
134 }
135 if (this->cooling_state_pin_ != nullptr) {
136 bool cooling_state = this->cooling_state_pin_->digital_read();
137 if (cooling_state != this->cooling_state_) {
138 ESP_LOGV(TAG, "Cooling state pin changed to: %s", ONOFF(cooling_state));
139 this->cooling_state_ = cooling_state;
140 state_changed = true;
141 }
142 }
143
144 if (state_changed) {
145 this->compute_state_();
146 this->publish_state();
147 }
148}
149
151 auto mode = call.get_mode();
152 if (mode.has_value()) {
153 const bool switch_state = *mode != climate::CLIMATE_MODE_OFF;
154 ESP_LOGV(TAG, "Setting switch: %s", ONOFF(switch_state));
155 auto switch_dp_id = this->switch_id_;
156 if (switch_dp_id.has_value()) {
157 this->parent_->set_boolean_datapoint_value(*switch_dp_id, switch_state);
158 }
159 const climate::ClimateMode new_mode = *mode;
160
161 auto active_state_dp_id = this->active_state_id_;
162 if (active_state_dp_id.has_value()) {
163 if (new_mode == climate::CLIMATE_MODE_HEAT && this->supports_heat_) {
164 auto heating_val = this->active_state_heating_value_;
165 if (heating_val.has_value())
166 this->parent_->set_enum_datapoint_value(*active_state_dp_id, *heating_val);
167 } else if (new_mode == climate::CLIMATE_MODE_COOL && this->supports_cool_) {
168 auto cooling_val = this->active_state_cooling_value_;
169 if (cooling_val.has_value())
170 this->parent_->set_enum_datapoint_value(*active_state_dp_id, *cooling_val);
171 } else if (new_mode == climate::CLIMATE_MODE_DRY) {
172 auto drying_val = this->active_state_drying_value_;
173 if (drying_val.has_value())
174 this->parent_->set_enum_datapoint_value(*active_state_dp_id, *drying_val);
175 } else if (new_mode == climate::CLIMATE_MODE_FAN_ONLY) {
176 auto fanonly_val = this->active_state_fanonly_value_;
177 if (fanonly_val.has_value())
178 this->parent_->set_enum_datapoint_value(*active_state_dp_id, *fanonly_val);
179 }
180 } else {
181 ESP_LOGW(TAG, "Active state (mode) datapoint not configured");
182 }
183 }
184
187
188 auto target_temp = call.get_target_temperature();
189 if (target_temp.has_value()) {
190 float target_temperature = *target_temp;
191 if (this->reports_fahrenheit_)
192 target_temperature = (target_temperature * 9 / 5) + 32;
193
194 ESP_LOGV(TAG, "Setting target temperature: %.1f", target_temperature);
195 auto target_temp_dp_id = this->target_temperature_id_;
196 if (target_temp_dp_id.has_value()) {
197 this->parent_->set_integer_datapoint_value(*target_temp_dp_id,
199 }
200 }
201
202 auto preset_val = call.get_preset();
203 if (preset_val.has_value()) {
204 const climate::ClimatePreset preset = *preset_val;
205 auto eco_dp_id = this->eco_id_;
206 if (eco_dp_id.has_value()) {
207 const bool eco = preset == climate::CLIMATE_PRESET_ECO;
208 ESP_LOGV(TAG, "Setting eco: %s", ONOFF(eco));
209 if (this->eco_type_ == TuyaDatapointType::ENUM) {
210 this->parent_->set_enum_datapoint_value(*eco_dp_id, eco);
211 } else {
212 this->parent_->set_boolean_datapoint_value(*eco_dp_id, eco);
213 }
214 }
215 auto sleep_dp_id = this->sleep_id_;
216 if (sleep_dp_id.has_value()) {
217 const bool sleep = preset == climate::CLIMATE_PRESET_SLEEP;
218 ESP_LOGV(TAG, "Setting sleep: %s", ONOFF(sleep));
219 this->parent_->set_boolean_datapoint_value(*sleep_dp_id, sleep);
220 }
221 }
222}
223
225 bool vertical_swing_changed = false;
226 bool horizontal_swing_changed = false;
227
228 auto swing_mode_val = call.get_swing_mode();
229 if (swing_mode_val.has_value()) {
230 const auto swing_mode = *swing_mode_val;
231
232 switch (swing_mode) {
235 this->swing_vertical_ = false;
236 this->swing_horizontal_ = false;
237 vertical_swing_changed = true;
238 horizontal_swing_changed = true;
239 }
240 break;
241
244 this->swing_vertical_ = true;
245 this->swing_horizontal_ = true;
246 vertical_swing_changed = true;
247 horizontal_swing_changed = true;
248 }
249 break;
250
253 this->swing_vertical_ = true;
254 this->swing_horizontal_ = false;
255 vertical_swing_changed = true;
256 horizontal_swing_changed = true;
257 }
258 break;
259
262 this->swing_vertical_ = false;
263 this->swing_horizontal_ = true;
264 vertical_swing_changed = true;
265 horizontal_swing_changed = true;
266 }
267 break;
268
269 default:
270 break;
271 }
272 }
273
274 auto vert_dp_id = this->swing_vertical_id_;
275 if (vertical_swing_changed && vert_dp_id.has_value()) {
276 ESP_LOGV(TAG, "Setting vertical swing: %s", ONOFF(swing_vertical_));
278 }
279
280 auto horiz_dp_id = this->swing_horizontal_id_;
281 if (horizontal_swing_changed && horiz_dp_id.has_value()) {
282 ESP_LOGV(TAG, "Setting horizontal swing: %s", ONOFF(swing_horizontal_));
284 }
285
286 // Publish the state after updating the swing mode
287 this->publish_state();
288}
289
291 auto fan_mode_val = call.get_fan_mode();
292 if (fan_mode_val.has_value()) {
293 climate::ClimateFanMode fan_mode = *fan_mode_val;
294
295 uint8_t tuya_fan_speed;
296 switch (fan_mode) {
298 tuya_fan_speed = this->fan_speed_low_value_.value_or(0);
299 break;
301 tuya_fan_speed = this->fan_speed_medium_value_.value_or(0);
302 break;
304 tuya_fan_speed = this->fan_speed_middle_value_.value_or(0);
305 break;
307 tuya_fan_speed = this->fan_speed_high_value_.value_or(0);
308 break;
310 tuya_fan_speed = this->fan_speed_auto_value_.value_or(0);
311 break;
312 default:
313 tuya_fan_speed = 0;
314 break;
315 }
316
317 auto fan_speed_dp_id = this->fan_speed_id_;
318 if (fan_speed_dp_id.has_value()) {
319 this->parent_->set_enum_datapoint_value(*fan_speed_dp_id, tuya_fan_speed);
320 }
321 }
322}
323
327 if (this->current_temperature_id_.has_value()) {
329 }
330
331 if (supports_heat_)
333 if (supports_cool_)
335 if (this->active_state_drying_value_.has_value())
337 if (this->active_state_fanonly_value_.has_value())
339 if (this->eco_id_.has_value()) {
341 }
342 if (this->sleep_id_.has_value()) {
344 }
345 if (this->sleep_id_.has_value() || this->eco_id_.has_value()) {
347 }
348 if (this->swing_vertical_id_.has_value() && this->swing_horizontal_id_.has_value()) {
351 } else if (this->swing_vertical_id_.has_value()) {
353 } else if (this->swing_horizontal_id_.has_value()) {
355 }
356
357 if (fan_speed_id_) {
368 }
369 return traits;
370}
371
373 LOG_CLIMATE("", "Tuya Climate", this);
374 auto switch_dp_id = this->switch_id_;
375 if (switch_dp_id.has_value()) {
376 ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *switch_dp_id);
377 }
378 auto active_state_dp_id = this->active_state_id_;
379 if (active_state_dp_id.has_value()) {
380 ESP_LOGCONFIG(TAG, " Active state has datapoint ID %u", *active_state_dp_id);
381 }
382 auto target_temp_dp_id = this->target_temperature_id_;
383 if (target_temp_dp_id.has_value()) {
384 ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *target_temp_dp_id);
385 }
386 auto current_temp_dp_id = this->current_temperature_id_;
387 if (current_temp_dp_id.has_value()) {
388 ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *current_temp_dp_id);
389 }
390 LOG_PIN(" Heating State Pin: ", this->heating_state_pin_);
391 LOG_PIN(" Cooling State Pin: ", this->cooling_state_pin_);
392 auto eco_dp_id = this->eco_id_;
393 if (eco_dp_id.has_value()) {
394 ESP_LOGCONFIG(TAG, " Eco has datapoint ID %u", *eco_dp_id);
395 }
396 auto sleep_dp_id = this->sleep_id_;
397 if (sleep_dp_id.has_value()) {
398 ESP_LOGCONFIG(TAG, " Sleep has datapoint ID %u", *sleep_dp_id);
399 }
400 auto swing_vert_dp_id = this->swing_vertical_id_;
401 if (swing_vert_dp_id.has_value()) {
402 ESP_LOGCONFIG(TAG, " Swing Vertical has datapoint ID %u", *swing_vert_dp_id);
403 }
404 auto swing_horiz_dp_id = this->swing_horizontal_id_;
405 if (swing_horiz_dp_id.has_value()) {
406 ESP_LOGCONFIG(TAG, " Swing Horizontal has datapoint ID %u", *swing_horiz_dp_id);
407 }
408}
409
411 if (this->eco_) {
413 } else if (this->sleep_) {
415 } else {
417 }
418}
419
421 if (this->swing_vertical_ && this->swing_horizontal_) {
423 } else if (this->swing_vertical_) {
425 } else if (this->swing_horizontal_) {
427 } else {
429 }
430}
431
433 if (this->fan_speed_id_.has_value()) {
434 // Use state from MCU datapoint
435 if (this->fan_speed_auto_value_.has_value() && this->fan_state_ == this->fan_speed_auto_value_) {
437 } else if (this->fan_speed_high_value_.has_value() && this->fan_state_ == this->fan_speed_high_value_) {
439 } else if (this->fan_speed_medium_value_.has_value() && this->fan_state_ == this->fan_speed_medium_value_) {
441 } else if (this->fan_speed_middle_value_.has_value() && this->fan_state_ == this->fan_speed_middle_value_) {
443 } else if (this->fan_speed_low_value_.has_value() && this->fan_state_ == this->fan_speed_low_value_) {
445 }
446 }
447}
448
450 if (this->eco_ && this->eco_temperature_.has_value()) {
452 } else {
454 }
455}
456
458 if (std::isnan(this->current_temperature) || std::isnan(this->target_temperature)) {
459 // if any control parameters are nan, go to OFF action (not IDLE!)
461 return;
462 }
463
464 if (this->mode == climate::CLIMATE_MODE_OFF) {
466 return;
467 }
468
470 if (this->heating_state_pin_ != nullptr || this->cooling_state_pin_ != nullptr) {
471 // Use state from input pins
472 if (this->heating_state_) {
473 target_action = climate::CLIMATE_ACTION_HEATING;
475 } else if (this->cooling_state_) {
476 target_action = climate::CLIMATE_ACTION_COOLING;
478 }
479 if (this->active_state_id_.has_value()) {
480 // Both are available, use MCU datapoint as mode
481 if (this->supports_heat_ && this->active_state_heating_value_.has_value() &&
482 this->active_state_ == this->active_state_heating_value_) {
484 } else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() &&
485 this->active_state_ == this->active_state_cooling_value_) {
487 } else if (this->active_state_drying_value_.has_value() &&
488 this->active_state_ == this->active_state_drying_value_) {
490 } else if (this->active_state_fanonly_value_.has_value() &&
491 this->active_state_ == this->active_state_fanonly_value_) {
493 }
494 }
495 } else if (this->active_state_id_.has_value()) {
496 // Use state from MCU datapoint
497 if (this->supports_heat_ && this->active_state_heating_value_.has_value() &&
498 this->active_state_ == this->active_state_heating_value_) {
499 target_action = climate::CLIMATE_ACTION_HEATING;
501 } else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() &&
502 this->active_state_ == this->active_state_cooling_value_) {
503 target_action = climate::CLIMATE_ACTION_COOLING;
505 } else if (this->active_state_drying_value_.has_value() &&
506 this->active_state_ == this->active_state_drying_value_) {
507 target_action = climate::CLIMATE_ACTION_DRYING;
509 } else if (this->active_state_fanonly_value_.has_value() &&
510 this->active_state_ == this->active_state_fanonly_value_) {
511 target_action = climate::CLIMATE_ACTION_FAN;
513 }
514 } else {
515 // Fallback to active state calc based on temp and hysteresis
516 const float temp_diff = this->target_temperature - this->current_temperature;
517 if (std::abs(temp_diff) > this->hysteresis_) {
518 if (this->supports_heat_ && temp_diff > 0) {
519 target_action = climate::CLIMATE_ACTION_HEATING;
521 } else if (this->supports_cool_ && temp_diff < 0) {
522 target_action = climate::CLIMATE_ACTION_COOLING;
524 }
525 }
526 }
527
528 this->switch_to_action_(target_action);
529}
530
532 // For now this just sets the current action but could include triggers later
533 this->action = action;
534}
535
536} // namespace tuya
537} // 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
ClimateMode mode
The active mode of the climate device.
Definition climate.h:266
optional< ClimateFanMode > fan_mode
The active fan mode of the climate device.
Definition climate.h:260
float target_temperature
The target temperature of the climate device.
Definition climate.h:247
ClimateSwingMode swing_mode
The active swing mode of the climate device.
Definition climate.h:272
float current_temperature
The current temperature of the climate device, as reported from the integration.
Definition climate.h:240
ClimateAction action
The active state of the climate device.
Definition climate.h:269
void publish_state()
Publish the state of the climate device, to be called from integrations.
Definition climate.cpp:445
optional< ClimatePreset > preset
The active preset of the climate device.
Definition climate.h:263
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:747
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
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
TuyaDatapointType type
Definition tuya.h:30