ESPHome 2026.1.0-dev
Loading...
Searching...
No Matches
thermostat_climate.cpp
Go to the documentation of this file.
4#include "esphome/core/log.h"
5
7
8static const char *const TAG = "thermostat.climate";
9
11 if (this->use_startup_delay_) {
12 // start timers so that no actions are called for a moment
18 }
19 // add a callback so that whenever the sensor state changes we can take action
20 this->sensor_->add_on_state_callback([this](float state) {
22 // required action may have changed, recompute, refresh, we'll publish_state() later
23 this->switch_to_action_(this->compute_action_(), false);
25 // current temperature and possibly action changed, so publish the new state
26 this->publish_state();
27 });
28 this->current_temperature = this->sensor_->state;
29
30 // register for humidity values and get initial state
31 if (this->humidity_sensor_ != nullptr) {
32 this->humidity_sensor_->add_on_state_callback([this](float state) {
33 this->current_humidity = state;
35 this->publish_state();
36 });
38 }
39
40 auto use_default_preset = true;
41
43 // restore all climate data, if possible
44 auto restore = this->restore_state_();
45 if (restore.has_value()) {
46 use_default_preset = false;
47 restore->to_call(this).perform();
48 }
49 }
50
51 // Either we failed to restore state or the user has requested we always apply the default preset
52 if (use_default_preset) {
55 } else if (this->default_custom_preset_ != nullptr) {
56 this->change_custom_preset_(this->default_custom_preset_);
57 }
58 }
59
60 // refresh the climate action based on the restored settings, we'll publish_state() later
61 this->switch_to_action_(this->compute_action_(), false);
63 this->setup_complete_ = true;
64 this->publish_state();
65}
66
68 uint32_t now = App.get_loop_component_start_time();
69 for (uint8_t i = 0; i < THERMOSTAT_TIMER_COUNT; i++) {
70 auto &timer = this->timer_[i];
71 if (timer.active && (now - timer.started >= timer.time)) {
72 timer.active = false;
74 }
75 }
76}
77
82
94
96 bool state_mismatch = this->action != this->compute_action_(true);
97
98 switch (this->compute_action_(true)) {
101 return state_mismatch && (!this->idle_action_ready_());
103 return state_mismatch && (!this->cooling_action_ready_());
105 return state_mismatch && (!this->heating_action_ready_());
107 return state_mismatch && (!this->fanning_action_ready_());
109 return state_mismatch && (!this->drying_action_ready_());
110 default:
111 break;
112 }
113 return false;
114}
115
117 bool state_mismatch = this->fan_mode.value_or(climate::CLIMATE_FAN_ON) != this->prev_fan_mode_;
118 return state_mismatch && (!this->fan_mode_ready_());
119}
120
122
124
126 if ((this->supports_cool_ || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) &&
127 (std::isnan(this->cooling_deadband_) || std::isnan(this->cooling_overrun_)))
128 return false;
129
130 if (this->supports_heat_ && (std::isnan(this->heating_deadband_) || std::isnan(this->heating_overrun_)))
131 return false;
132
133 return true;
134}
135
137 return !std::isnan(this->humidity_hysteresis_) && this->humidity_hysteresis_ >= 0.0f &&
138 this->humidity_hysteresis_ < 100.0f;
139}
140
145
147 if (std::isnan(this->target_temperature)) {
148 // default to the midpoint between visual min and max
149 this->target_temperature =
152 } else {
153 // target_temperature must be between the visual minimum and the visual maximum
154 this->target_temperature = clamp(this->target_temperature, this->get_traits().get_visual_min_temperature(),
155 this->get_traits().get_visual_max_temperature());
156 }
157}
158
159void ThermostatClimate::validate_target_temperatures(const bool pin_target_temperature_high) {
160 if (!this->supports_two_points_) {
162 } else if (pin_target_temperature_high) {
163 // if target_temperature_high is set less than target_temperature_low, move down target_temperature_low
166 } else {
167 // if target_temperature_low is set greater than target_temperature_high, move up target_temperature_high
170 }
171}
172
174 if (std::isnan(this->target_temperature_low)) {
176 } else {
177 float target_temperature_low_upper_limit =
180 this->get_traits().get_visual_min_temperature(), this->get_traits().get_visual_max_temperature())
182 this->target_temperature_low = clamp(this->target_temperature_low, this->get_traits().get_visual_min_temperature(),
183 target_temperature_low_upper_limit);
184 }
185}
186
188 if (std::isnan(this->target_temperature_high)) {
190 } else {
191 float target_temperature_high_lower_limit =
194 this->get_traits().get_visual_min_temperature(), this->get_traits().get_visual_max_temperature())
196 this->target_temperature_high = clamp(this->target_temperature_high, target_temperature_high_lower_limit,
197 this->get_traits().get_visual_max_temperature());
198 }
199}
200
202 if (std::isnan(this->target_humidity)) {
203 this->target_humidity =
205 } else {
206 this->target_humidity = clamp<float>(this->target_humidity, this->get_traits().get_visual_min_humidity(),
207 this->get_traits().get_visual_max_humidity());
208 }
209}
210
212 bool target_temperature_high_changed = false;
213
214 if (call.get_preset().has_value()) {
215 // setup_complete_ blocks modifying/resetting the temps immediately after boot
216 if (this->setup_complete_) {
217 this->change_preset_(call.get_preset().value());
218 } else {
219 this->preset = call.get_preset().value();
220 }
221 }
222 if (call.has_custom_preset()) {
223 // setup_complete_ blocks modifying/resetting the temps immediately after boot
224 if (this->setup_complete_) {
226 } else {
227 // Use the base class method which handles pointer lookup internally
229 }
230 }
231
232 if (call.get_mode().has_value()) {
233 this->mode = call.get_mode().value();
234 }
235 if (call.get_fan_mode().has_value()) {
236 this->fan_mode = call.get_fan_mode().value();
237 }
238 if (call.get_swing_mode().has_value()) {
239 this->swing_mode = call.get_swing_mode().value();
240 }
241 if (this->supports_two_points_) {
244 }
245 if (call.get_target_temperature_high().has_value()) {
246 target_temperature_high_changed = this->target_temperature_high != call.get_target_temperature_high().value();
248 }
249 // ensure the two set points are valid and adjust one of them if necessary
250 this->validate_target_temperatures(target_temperature_high_changed ||
252 } else {
253 if (call.get_target_temperature().has_value()) {
256 }
257 }
258 if (call.get_target_humidity().has_value()) {
261 }
262 // make any changes happen
263 this->refresh();
264}
265
268
270
271 if (this->supports_two_points_)
273
274 if (this->humidity_sensor_ != nullptr)
276
279
280 if (this->supports_auto_)
282 if (this->supports_heat_cool_)
284 if (this->supports_cool_)
286 if (this->supports_dry_)
288 if (this->supports_fan_only_)
290 if (this->supports_heat_)
292
293 if (this->supports_fan_mode_on_)
295 if (this->supports_fan_mode_off_)
297 if (this->supports_fan_mode_auto_)
299 if (this->supports_fan_mode_low_)
303 if (this->supports_fan_mode_high_)
307 if (this->supports_fan_mode_focus_)
311 if (this->supports_fan_mode_quiet_)
313
318 if (this->supports_swing_mode_off_)
322
323 for (const auto &entry : this->preset_config_) {
324 traits.add_supported_preset(entry.preset);
325 }
326
327 // Extract custom preset names from the custom_preset_config_ vector
328 if (!this->custom_preset_config_.empty()) {
329 std::vector<const char *> custom_preset_names;
330 custom_preset_names.reserve(this->custom_preset_config_.size());
331 for (const auto &entry : this->custom_preset_config_) {
332 custom_preset_names.push_back(entry.name);
333 }
334 traits.set_supported_custom_presets(custom_preset_names);
335 }
336
337 return traits;
338}
339
341 auto target_action = climate::CLIMATE_ACTION_IDLE;
342 // if any hysteresis values or current_temperature is not valid, we go to OFF;
343 if (std::isnan(this->current_temperature) || !this->hysteresis_valid()) {
345 }
346 // do not change the action if an "ON" timer is running
347 if ((!ignore_timers) && (this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) ||
351 return this->action;
352 }
353
354 // ensure set point(s) is/are valid before computing the action
356 // everything has been validated so we can now safely compute the action
357 switch (this->mode) {
358 // if the climate mode is OFF then the climate action must be OFF
360 target_action = climate::CLIMATE_ACTION_OFF;
361 break;
363 if (this->fanning_required_())
364 target_action = climate::CLIMATE_ACTION_FAN;
365 break;
367 target_action = climate::CLIMATE_ACTION_DRYING;
368 break;
370 if (this->cooling_required_() && this->heating_required_()) {
371 // this is bad and should never happen, so just stop.
372 // target_action = climate::CLIMATE_ACTION_IDLE;
373 } else if (this->cooling_required_()) {
374 target_action = climate::CLIMATE_ACTION_COOLING;
375 } else if (this->heating_required_()) {
376 target_action = climate::CLIMATE_ACTION_HEATING;
377 }
378 break;
380 if (this->cooling_required_()) {
381 target_action = climate::CLIMATE_ACTION_COOLING;
382 }
383 break;
385 if (this->heating_required_()) {
386 target_action = climate::CLIMATE_ACTION_HEATING;
387 }
388 break;
390 if (this->supports_two_points_) {
391 if (this->cooling_required_() && this->heating_required_()) {
392 // this is bad and should never happen, so just stop.
393 // target_action = climate::CLIMATE_ACTION_IDLE;
394 } else if (this->cooling_required_()) {
395 target_action = climate::CLIMATE_ACTION_COOLING;
396 } else if (this->heating_required_()) {
397 target_action = climate::CLIMATE_ACTION_HEATING;
398 }
399 } else if (this->supports_cool_ && this->cooling_required_()) {
400 target_action = climate::CLIMATE_ACTION_COOLING;
401 } else if (this->supports_heat_ && this->heating_required_()) {
402 target_action = climate::CLIMATE_ACTION_HEATING;
403 }
404 break;
405 default:
406 break;
407 }
408 // do not abruptly switch actions. cycle through IDLE, first. we'll catch this at the next update.
410 (target_action == climate::CLIMATE_ACTION_HEATING)) ||
412 ((target_action == climate::CLIMATE_ACTION_COOLING) || (target_action == climate::CLIMATE_ACTION_DRYING)))) {
414 }
415
416 return target_action;
417}
418
420 auto target_action = climate::CLIMATE_ACTION_IDLE;
421 // if any hysteresis values or current_temperature is not valid, we go to OFF;
422 if (std::isnan(this->current_temperature) || !this->hysteresis_valid()) {
424 }
425
426 // ensure set point(s) is/are valid before computing the action
428 // everything has been validated so we can now safely compute the action
429 switch (this->mode) {
430 // if the climate mode is OFF then the climate action must be OFF
432 target_action = climate::CLIMATE_ACTION_OFF;
433 break;
436 // this is bad and should never happen, so just stop.
437 // target_action = climate::CLIMATE_ACTION_IDLE;
438 } else if (this->supplemental_cooling_required_()) {
439 target_action = climate::CLIMATE_ACTION_COOLING;
440 } else if (this->supplemental_heating_required_()) {
441 target_action = climate::CLIMATE_ACTION_HEATING;
442 }
443 break;
445 if (this->supplemental_cooling_required_()) {
446 target_action = climate::CLIMATE_ACTION_COOLING;
447 }
448 break;
450 if (this->supplemental_heating_required_()) {
451 target_action = climate::CLIMATE_ACTION_HEATING;
452 }
453 break;
454 default:
455 break;
456 }
457
458 return target_action;
459}
460
462 auto target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
463 // if hysteresis value or current_humidity is not valid, we go to OFF
464 if (std::isnan(this->current_humidity) || !this->humidity_hysteresis_valid()) {
466 }
467
468 // ensure set point is valid before computing the action
470 // everything has been validated so we can now safely compute the action
472 // this is bad and should never happen, so just stop.
473 // target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
474 } else if (this->supports_dehumidification_ && this->dehumidification_required_()) {
476 } else if (this->supports_humidification_ && this->humidification_required_()) {
478 }
479
480 return target_action;
481}
482
484 // setup_complete_ helps us ensure an action is called immediately after boot
485 if ((action == this->action) && this->setup_complete_) {
486 // already in target mode
487 return;
488 }
489
490 if (((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) ||
492 this->setup_complete_) {
493 // switching from OFF to IDLE or vice-versa -- this is only a visual difference.
494 // OFF means user manually disabled, IDLE means the temperature is in target range.
495 this->action = action;
496 if (publish_state)
497 this->publish_state();
498 return;
499 }
500
501 bool action_ready = false;
502 Trigger<> *trig = this->idle_action_trigger_, *trig_fan = nullptr;
503 switch (action) {
506 if (this->idle_action_ready_()) {
508 if (this->action == climate::CLIMATE_ACTION_COOLING)
510 if (this->action == climate::CLIMATE_ACTION_FAN) {
513 } else {
515 }
516 }
517 if (this->action == climate::CLIMATE_ACTION_HEATING)
519 // trig = this->idle_action_trigger_;
520 ESP_LOGVV(TAG, "Switching to IDLE/OFF action");
521 this->cooling_max_runtime_exceeded_ = false;
522 this->heating_max_runtime_exceeded_ = false;
523 action_ready = true;
524 }
525 break;
527 if (this->cooling_action_ready_()) {
530 if (this->supports_fan_with_cooling_) {
532 trig_fan = this->fan_only_action_trigger_;
533 }
534 this->cooling_max_runtime_exceeded_ = false;
535 trig = this->cool_action_trigger_;
536 ESP_LOGVV(TAG, "Switching to COOLING action");
537 action_ready = true;
538 }
539 break;
541 if (this->heating_action_ready_()) {
544 if (this->supports_fan_with_heating_) {
546 trig_fan = this->fan_only_action_trigger_;
547 }
548 this->heating_max_runtime_exceeded_ = false;
549 trig = this->heat_action_trigger_;
550 ESP_LOGVV(TAG, "Switching to HEATING action");
551 action_ready = true;
552 }
553 break;
555 if (this->fanning_action_ready_()) {
558 } else {
560 }
561 trig = this->fan_only_action_trigger_;
562 ESP_LOGVV(TAG, "Switching to FAN_ONLY action");
563 action_ready = true;
564 }
565 break;
567 if (this->drying_action_ready_()) {
570 trig = this->dry_action_trigger_;
571 ESP_LOGVV(TAG, "Switching to DRYING action");
572 action_ready = true;
573 }
574 break;
575 default:
576 // we cannot report an invalid mode back to HA (even if it asked for one)
577 // and must assume some valid value
579 // trig = this->idle_action_trigger_;
580 }
581
582 if (action_ready) {
583 if (this->prev_action_trigger_ != nullptr) {
585 this->prev_action_trigger_ = nullptr;
586 }
587 this->action = action;
588 this->prev_action_trigger_ = trig;
589 if (trig != nullptr) {
590 trig->trigger();
591 }
592 // if enabled, call the fan_only action with cooling/heating actions
593 if (trig_fan != nullptr) {
594 ESP_LOGVV(TAG, "Calling FAN_ONLY action with HEATING/COOLING action");
595 trig_fan->trigger();
596 }
597 if (publish_state)
598 this->publish_state();
599 }
600}
601
603 // setup_complete_ helps us ensure an action is called immediately after boot
604 if ((action == this->supplemental_action_) && this->setup_complete_) {
605 // already in target mode
606 return;
607 }
608
609 switch (action) {
614 break;
617 break;
620 break;
621 default:
622 return;
623 }
624 ESP_LOGVV(TAG, "Updating supplemental action");
627}
628
630 Trigger<> *trig = nullptr;
631
632 switch (this->supplemental_action_) {
636 }
638 ESP_LOGVV(TAG, "Calling supplemental COOLING action");
639 break;
643 }
645 ESP_LOGVV(TAG, "Calling supplemental HEATING action");
646 break;
647 default:
648 break;
649 }
650
651 if (trig != nullptr) {
652 trig->trigger();
653 }
654}
655
657 // setup_complete_ helps us ensure an action is called immediately after boot
658 if ((action == this->humidification_action) && this->setup_complete_) {
659 // already in target mode
660 return;
661 }
662
664 switch (action) {
666 // trig = this->humidity_control_off_action_trigger_;
667 ESP_LOGVV(TAG, "Switching to HUMIDIFICATION_OFF action");
668 break;
671 ESP_LOGVV(TAG, "Switching to DEHUMIDIFY action");
672 break;
675 ESP_LOGVV(TAG, "Switching to HUMIDIFY action");
676 break;
678 default:
680 // trig = this->humidity_control_off_action_trigger_;
681 }
682
683 if (this->prev_humidity_control_trigger_ != nullptr) {
685 this->prev_humidity_control_trigger_ = nullptr;
686 }
689 if (trig != nullptr) {
690 trig->trigger();
691 }
692}
693
695 // setup_complete_ helps us ensure an action is called immediately after boot
696 if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) {
697 // already in target mode
698 return;
699 }
700
701 this->fan_mode = fan_mode;
702 if (publish_state)
703 this->publish_state();
704
705 if (this->fan_mode_ready_()) {
706 Trigger<> *trig = this->fan_mode_auto_trigger_;
707 switch (fan_mode) {
709 trig = this->fan_mode_on_trigger_;
710 ESP_LOGVV(TAG, "Switching to FAN_ON mode");
711 break;
713 trig = this->fan_mode_off_trigger_;
714 ESP_LOGVV(TAG, "Switching to FAN_OFF mode");
715 break;
717 // trig = this->fan_mode_auto_trigger_;
718 ESP_LOGVV(TAG, "Switching to FAN_AUTO mode");
719 break;
721 trig = this->fan_mode_low_trigger_;
722 ESP_LOGVV(TAG, "Switching to FAN_LOW mode");
723 break;
725 trig = this->fan_mode_medium_trigger_;
726 ESP_LOGVV(TAG, "Switching to FAN_MEDIUM mode");
727 break;
729 trig = this->fan_mode_high_trigger_;
730 ESP_LOGVV(TAG, "Switching to FAN_HIGH mode");
731 break;
733 trig = this->fan_mode_middle_trigger_;
734 ESP_LOGVV(TAG, "Switching to FAN_MIDDLE mode");
735 break;
737 trig = this->fan_mode_focus_trigger_;
738 ESP_LOGVV(TAG, "Switching to FAN_FOCUS mode");
739 break;
741 trig = this->fan_mode_diffuse_trigger_;
742 ESP_LOGVV(TAG, "Switching to FAN_DIFFUSE mode");
743 break;
745 trig = this->fan_mode_quiet_trigger_;
746 ESP_LOGVV(TAG, "Switching to FAN_QUIET mode");
747 break;
748 default:
749 // we cannot report an invalid mode back to HA (even if it asked for one)
750 // and must assume some valid value
752 // trig = this->fan_mode_auto_trigger_;
753 }
754 if (this->prev_fan_mode_trigger_ != nullptr) {
756 this->prev_fan_mode_trigger_ = nullptr;
757 }
759 if (trig != nullptr) {
760 trig->trigger();
761 }
762 this->prev_fan_mode_ = fan_mode;
763 this->prev_fan_mode_trigger_ = trig;
764 }
765}
766
768 // setup_complete_ helps us ensure an action is called immediately after boot
769 if ((mode == this->prev_mode_) && this->setup_complete_) {
770 // already in target mode
771 return;
772 }
773
774 if (this->prev_mode_trigger_ != nullptr) {
776 this->prev_mode_trigger_ = nullptr;
777 }
778 Trigger<> *trig = this->off_mode_trigger_;
779 switch (mode) {
781 trig = this->auto_mode_trigger_;
782 break;
784 trig = this->heat_cool_mode_trigger_;
785 break;
787 trig = this->cool_mode_trigger_;
788 break;
790 trig = this->heat_mode_trigger_;
791 break;
793 trig = this->fan_only_mode_trigger_;
794 break;
796 trig = this->dry_mode_trigger_;
797 break;
799 default:
800 // we cannot report an invalid mode back to HA (even if it asked for one)
801 // and must assume some valid value
803 // trig = this->off_mode_trigger_;
804 }
805 if (trig != nullptr) {
806 trig->trigger();
807 }
808 this->mode = mode;
809 this->prev_mode_ = mode;
810 this->prev_mode_trigger_ = trig;
811 if (publish_state) {
812 this->publish_state();
813 }
814}
815
817 // setup_complete_ helps us ensure an action is called immediately after boot
818 if ((swing_mode == this->prev_swing_mode_) && this->setup_complete_) {
819 // already in target mode
820 return;
821 }
822
823 if (this->prev_swing_mode_trigger_ != nullptr) {
825 this->prev_swing_mode_trigger_ = nullptr;
826 }
828 switch (swing_mode) {
830 trig = this->swing_mode_both_trigger_;
831 break;
834 break;
836 // trig = this->swing_mode_off_trigger_;
837 break;
839 trig = this->swing_mode_vertical_trigger_;
840 break;
841 default:
842 // we cannot report an invalid mode back to HA (even if it asked for one)
843 // and must assume some valid value
844 swing_mode = climate::CLIMATE_SWING_OFF;
845 // trig = this->swing_mode_off_trigger_;
846 }
847 if (trig != nullptr) {
848 trig->trigger();
849 }
850 this->swing_mode = swing_mode;
852 this->prev_swing_mode_trigger_ = trig;
853 if (publish_state)
854 this->publish_state();
855}
856
867
874
881
883
891
898
900 if (this->timer_duration_(timer_index) > 0) {
901 this->timer_[timer_index].started = millis();
902 this->timer_[timer_index].active = true;
903 }
904}
905
907 auto ret = this->timer_[timer_index].active;
908 this->timer_[timer_index].active = false;
909 return ret;
910}
911
913 return this->timer_[timer_index].active;
914}
915
917 return this->timer_[timer_index].time;
918}
919
921 switch (timer_index) {
924 break;
927 break;
930 break;
933 break;
936 break;
939 break;
942 break;
945 break;
948 break;
951 break;
953 default:
954 break;
955 }
956}
957
959 ESP_LOGVV(TAG, "cooling_max_run_time timer expired");
963}
964
966 ESP_LOGVV(TAG, "cooling_off timer expired");
967 this->switch_to_action_(this->compute_action_());
969}
970
972 ESP_LOGVV(TAG, "cooling_on timer expired");
973 this->switch_to_action_(this->compute_action_());
975}
976
978 ESP_LOGVV(TAG, "fan_mode timer expired");
981 this->switch_to_action_(this->compute_action_());
982}
983
985 ESP_LOGVV(TAG, "fanning_off timer expired");
986 this->switch_to_action_(this->compute_action_());
987}
988
990 ESP_LOGVV(TAG, "fanning_on timer expired");
991 this->switch_to_action_(this->compute_action_());
992}
993
995 ESP_LOGVV(TAG, "heating_max_run_time timer expired");
999}
1000
1002 ESP_LOGVV(TAG, "heating_off timer expired");
1003 this->switch_to_action_(this->compute_action_());
1005}
1006
1008 ESP_LOGVV(TAG, "heating_on timer expired");
1009 this->switch_to_action_(this->compute_action_());
1011}
1012
1014 ESP_LOGVV(TAG, "idle_on timer expired");
1015 this->switch_to_action_(this->compute_action_());
1017}
1018
1020 if ((this->prev_target_humidity_ == this->target_humidity) && this->setup_complete_) {
1021 return; // nothing changed, no reason to trigger
1022 } else {
1023 // save the new temperature so we can check it again later; the trigger will fire below
1025 }
1026 // trigger the action
1027 Trigger<> *trig = this->humidity_change_trigger_;
1028 if (trig != nullptr) {
1029 trig->trigger();
1030 }
1031}
1032
1034 if (this->supports_two_points_) {
1035 // setup_complete_ helps us ensure an action is called immediately after boot
1038 return; // nothing changed, no reason to trigger
1039 } else {
1040 // save the new temperatures so we can check them again later; the trigger will fire below
1043 }
1044 } else {
1045 if ((this->prev_target_temperature_ == this->target_temperature) && this->setup_complete_) {
1046 return; // nothing changed, no reason to trigger
1047 } else {
1048 // save the new temperature so we can check it again later; the trigger will fire below
1050 }
1051 }
1052 // trigger the action
1054 if (trig != nullptr) {
1055 trig->trigger();
1056 }
1057}
1058
1061
1062 if (this->supports_cool_) {
1064 // if the current temperature exceeds the target + deadband, cooling is required
1065 return true;
1067 // if the current temperature is less than the target - overrun, cooling should stop
1068 return false;
1069 } else {
1070 // if we get here, the current temperature is between target + deadband and target - overrun,
1071 // so the action should not change unless it conflicts with the current mode
1072 return (this->action == climate::CLIMATE_ACTION_COOLING) &&
1074 }
1075 }
1076 return false;
1077}
1078
1081
1082 if (this->supports_fan_only_) {
1083 if (this->supports_fan_only_cooling_) {
1085 // if the current temperature exceeds the target + deadband, fanning is required
1086 return true;
1088 // if the current temperature is less than the target - overrun, fanning should stop
1089 return false;
1090 } else {
1091 // if we get here, the current temperature is between target + deadband and target - overrun,
1092 // so the action should not change unless it conflicts with the current mode
1094 }
1095 } else {
1096 return true;
1097 }
1098 }
1099 return false;
1100}
1101
1104
1105 if (this->supports_heat_) {
1107 // if the current temperature is below the target - deadband, heating is required
1108 return true;
1109 } else if (this->current_temperature > temperature + this->heating_overrun_) {
1110 // if the current temperature is above the target + overrun, heating should stop
1111 return false;
1112 } else {
1113 // if we get here, the current temperature is between target - deadband and target + overrun,
1114 // so the action should not change unless it conflicts with the current mode
1115 return (this->action == climate::CLIMATE_ACTION_HEATING) &&
1117 }
1118 }
1119 return false;
1120}
1121
1124 // the component must supports_cool_ and the climate action must be climate::CLIMATE_ACTION_COOLING. then...
1125 // supplemental cooling is required if the max delta or max runtime was exceeded or the action is already engaged
1126 return this->supports_cool_ && (this->action == climate::CLIMATE_ACTION_COOLING) &&
1130}
1131
1134 // the component must supports_heat_ and the climate action must be climate::CLIMATE_ACTION_HEATING. then...
1135 // supplemental heating is required if the max delta or max runtime was exceeded or the action is already engaged
1136 return this->supports_heat_ && (this->action == climate::CLIMATE_ACTION_HEATING) &&
1140}
1141
1143 if (this->current_humidity > this->target_humidity + this->humidity_hysteresis_) {
1144 // if the current humidity exceeds the target + hysteresis, dehumidification is required
1145 return true;
1147 // if the current humidity is less than the target - hysteresis, dehumidification should stop
1148 return false;
1149 }
1150 // if we get here, the current humidity is between target + hysteresis and target - hysteresis,
1151 // so the action should not change
1153}
1154
1157 // if the current humidity is below the target - hysteresis, humidification is required
1158 return true;
1159 } else if (this->current_humidity > this->target_humidity + this->humidity_hysteresis_) {
1160 // if the current humidity is above the target + hysteresis, humidification should stop
1161 return false;
1162 }
1163 // if we get here, the current humidity is between target - hysteresis and target + hysteresis,
1164 // so the action should not change
1166}
1167
1169 if (this->supports_heat_) {
1170 ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C",
1172 }
1173 if ((this->supports_cool_) || (this->supports_fan_only_)) {
1174 ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C",
1176 }
1177
1178 if (config.mode_.has_value()) {
1179 ESP_LOGCONFIG(TAG, " Default Mode: %s", LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_)));
1180 }
1181 if (config.fan_mode_.has_value()) {
1182 ESP_LOGCONFIG(TAG, " Default Fan Mode: %s",
1183 LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_)));
1184 }
1185 if (config.swing_mode_.has_value()) {
1186 ESP_LOGCONFIG(TAG, " Default Swing Mode: %s",
1188 }
1189}
1190
1192 // Linear search through preset configurations
1193 const ThermostatClimateTargetTempConfig *config = nullptr;
1194 for (const auto &entry : this->preset_config_) {
1195 if (entry.preset == preset) {
1196 config = &entry.config;
1197 break;
1198 }
1199 }
1200
1201 if (config != nullptr) {
1202 ESP_LOGV(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
1203 if (this->change_preset_internal_(*config) || (!this->preset.has_value()) || this->preset.value() != preset) {
1204 // Fire any preset changed trigger if defined
1205 Trigger<> *trig = this->preset_change_trigger_;
1206 this->set_preset_(preset);
1207 if (trig != nullptr) {
1208 trig->trigger();
1209 }
1210
1211 this->refresh();
1212 ESP_LOGI(TAG, "Preset %s applied", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
1213 } else {
1214 ESP_LOGI(TAG, "No changes required to apply preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
1215 }
1216 } else {
1217 ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
1218 }
1219}
1220
1222 // Linear search through custom preset configurations
1223 const ThermostatClimateTargetTempConfig *config = nullptr;
1224 for (const auto &entry : this->custom_preset_config_) {
1225 if (strcmp(entry.name, custom_preset) == 0) {
1226 config = &entry.config;
1227 break;
1228 }
1229 }
1230
1231 if (config != nullptr) {
1232 ESP_LOGV(TAG, "Custom preset %s requested", custom_preset);
1233 if (this->change_preset_internal_(*config) || !this->has_custom_preset() ||
1234 strcmp(this->get_custom_preset(), custom_preset) != 0) {
1235 // Fire any preset changed trigger if defined
1236 Trigger<> *trig = this->preset_change_trigger_;
1237 // Use the base class method which handles pointer lookup and preset reset internally
1238 this->set_custom_preset_(custom_preset);
1239 if (trig != nullptr) {
1240 trig->trigger();
1241 }
1242
1243 this->refresh();
1244 ESP_LOGI(TAG, "Custom preset %s applied", custom_preset);
1245 } else {
1246 ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset);
1247 // Note: set_custom_preset_() above handles preset.reset() and custom_preset_ assignment internally.
1248 // The old code had these lines here unconditionally, which was a bug (double assignment, state modification
1249 // even when no changes were needed). Now properly handled by the protected setter with mutual exclusion.
1250 }
1251 } else {
1252 ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset);
1253 }
1254}
1255
1257 bool something_changed = false;
1258
1259 if (this->supports_two_points_) {
1260 if (this->target_temperature_low != config.default_temperature_low) {
1262 something_changed = true;
1263 }
1266 something_changed = true;
1267 }
1268 } else {
1269 if (this->target_temperature != config.default_temperature) {
1271 something_changed = true;
1272 }
1273 }
1274
1275 // Note: The mode, fan_mode and swing_mode can all be defined in the preset but if the climate.control call
1276 // also specifies them then the climate.control call's values will override the preset's values for that call
1277 if (config.mode_.has_value() && (this->mode != config.mode_.value())) {
1278 ESP_LOGV(TAG, "Setting mode to %s", LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_)));
1279 this->mode = *config.mode_;
1280 something_changed = true;
1281 }
1282
1283 if (config.fan_mode_.has_value() && (this->fan_mode != config.fan_mode_.value())) {
1284 ESP_LOGV(TAG, "Setting fan mode to %s", LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_)));
1285 this->fan_mode = *config.fan_mode_;
1286 something_changed = true;
1287 }
1288
1289 if (config.swing_mode_.has_value() && (this->swing_mode != config.swing_mode_.value())) {
1290 ESP_LOGV(TAG, "Setting swing mode to %s", LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_)));
1291 this->swing_mode = *config.swing_mode_;
1292 something_changed = true;
1293 }
1294
1295 return something_changed;
1296}
1297
1298void ThermostatClimate::set_preset_config(std::initializer_list<PresetEntry> presets) {
1299 this->preset_config_ = presets;
1300}
1301
1302void ThermostatClimate::set_custom_preset_config(std::initializer_list<CustomPresetEntry> presets) {
1303 this->custom_preset_config_ = presets;
1304}
1305
1307 : cool_action_trigger_(new Trigger<>()),
1308 supplemental_cool_action_trigger_(new Trigger<>()),
1309 cool_mode_trigger_(new Trigger<>()),
1310 dry_action_trigger_(new Trigger<>()),
1311 dry_mode_trigger_(new Trigger<>()),
1312 heat_action_trigger_(new Trigger<>()),
1313 supplemental_heat_action_trigger_(new Trigger<>()),
1314 heat_mode_trigger_(new Trigger<>()),
1315 heat_cool_mode_trigger_(new Trigger<>()),
1316 auto_mode_trigger_(new Trigger<>()),
1317 idle_action_trigger_(new Trigger<>()),
1318 off_mode_trigger_(new Trigger<>()),
1319 fan_only_action_trigger_(new Trigger<>()),
1320 fan_only_mode_trigger_(new Trigger<>()),
1321 fan_mode_on_trigger_(new Trigger<>()),
1322 fan_mode_off_trigger_(new Trigger<>()),
1323 fan_mode_auto_trigger_(new Trigger<>()),
1324 fan_mode_low_trigger_(new Trigger<>()),
1325 fan_mode_medium_trigger_(new Trigger<>()),
1326 fan_mode_high_trigger_(new Trigger<>()),
1327 fan_mode_middle_trigger_(new Trigger<>()),
1328 fan_mode_focus_trigger_(new Trigger<>()),
1329 fan_mode_diffuse_trigger_(new Trigger<>()),
1330 fan_mode_quiet_trigger_(new Trigger<>()),
1331 swing_mode_both_trigger_(new Trigger<>()),
1332 swing_mode_off_trigger_(new Trigger<>()),
1333 swing_mode_horizontal_trigger_(new Trigger<>()),
1334 swing_mode_vertical_trigger_(new Trigger<>()),
1335 humidity_change_trigger_(new Trigger<>()),
1336 temperature_change_trigger_(new Trigger<>()),
1337 preset_change_trigger_(new Trigger<>()),
1338 humidity_control_dehumidify_action_trigger_(new Trigger<>()),
1339 humidity_control_humidify_action_trigger_(new Trigger<>()),
1340 humidity_control_off_action_trigger_(new Trigger<>()) {}
1341
1343 // Find the preset in custom_preset_config_ and store pointer from there
1344 for (const auto &entry : this->custom_preset_config_) {
1345 if (strcmp(entry.name, custom_preset) == 0) {
1346 this->default_custom_preset_ = entry.name;
1347 return;
1348 }
1349 }
1350 // If not found, it will be caught during validation
1351 this->default_custom_preset_ = nullptr;
1352}
1353
1355
1357 this->on_boot_restore_from_ = on_boot_restore_from;
1358}
1360 this->set_point_minimum_differential_ = differential;
1361}
1362void ThermostatClimate::set_cool_deadband(float deadband) { this->cooling_deadband_ = deadband; }
1363void ThermostatClimate::set_cool_overrun(float overrun) { this->cooling_overrun_ = overrun; }
1364void ThermostatClimate::set_heat_deadband(float deadband) { this->heating_deadband_ = deadband; }
1365void ThermostatClimate::set_heat_overrun(float overrun) { this->heating_overrun_ = overrun; }
1368
1370 uint32_t new_duration_ms = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
1371
1372 if (this->timer_[timer_index].active) {
1373 // Timer is running, calculate elapsed time and adjust if needed
1374 uint32_t current_time = App.get_loop_component_start_time();
1375 uint32_t elapsed = current_time - this->timer_[timer_index].started;
1376
1377 if (elapsed >= new_duration_ms) {
1378 // Timer should complete immediately (including when new_duration_ms is 0)
1379 ESP_LOGVV(TAG, "timer %d completing immediately (elapsed %d >= new %d)", timer_index, elapsed, new_duration_ms);
1380 this->timer_[timer_index].active = false;
1381 // Trigger the timer callback immediately
1382 this->call_timer_callback_(timer_index);
1383 return;
1384 } else {
1385 // Adjust timer to run for remaining time - keep original start time
1386 ESP_LOGVV(TAG, "timer %d adjusted: elapsed %d, new total %d, remaining %d", timer_index, elapsed, new_duration_ms,
1387 new_duration_ms - elapsed);
1388 this->timer_[timer_index].time = new_duration_ms;
1389 return;
1390 }
1391 }
1392
1393 // Original logic for non-running timers
1394 this->timer_[timer_index].time = new_duration_ms;
1395}
1396
1429 this->humidity_sensor_ = humidity_sensor;
1430}
1431void ThermostatClimate::set_humidity_hysteresis(float humidity_hysteresis) {
1432 this->humidity_hysteresis_ = std::clamp<float>(humidity_hysteresis, 0.0f, 100.0f);
1433}
1434void ThermostatClimate::set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; }
1435void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) {
1436 this->supports_heat_cool_ = supports_heat_cool;
1437}
1438void ThermostatClimate::set_supports_auto(bool supports_auto) { this->supports_auto_ = supports_auto; }
1439void ThermostatClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
1440void ThermostatClimate::set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; }
1441void ThermostatClimate::set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; }
1443 bool supports_fan_only_action_uses_fan_mode_timer) {
1444 this->supports_fan_only_action_uses_fan_mode_timer_ = supports_fan_only_action_uses_fan_mode_timer;
1445}
1446void ThermostatClimate::set_supports_fan_only_cooling(bool supports_fan_only_cooling) {
1447 this->supports_fan_only_cooling_ = supports_fan_only_cooling;
1448}
1449void ThermostatClimate::set_supports_fan_with_cooling(bool supports_fan_with_cooling) {
1450 this->supports_fan_with_cooling_ = supports_fan_with_cooling;
1451}
1452void ThermostatClimate::set_supports_fan_with_heating(bool supports_fan_with_heating) {
1453 this->supports_fan_with_heating_ = supports_fan_with_heating;
1454}
1455void ThermostatClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
1456void ThermostatClimate::set_supports_fan_mode_on(bool supports_fan_mode_on) {
1457 this->supports_fan_mode_on_ = supports_fan_mode_on;
1458}
1459void ThermostatClimate::set_supports_fan_mode_off(bool supports_fan_mode_off) {
1460 this->supports_fan_mode_off_ = supports_fan_mode_off;
1461}
1462void ThermostatClimate::set_supports_fan_mode_auto(bool supports_fan_mode_auto) {
1463 this->supports_fan_mode_auto_ = supports_fan_mode_auto;
1464}
1465void ThermostatClimate::set_supports_fan_mode_low(bool supports_fan_mode_low) {
1466 this->supports_fan_mode_low_ = supports_fan_mode_low;
1467}
1468void ThermostatClimate::set_supports_fan_mode_medium(bool supports_fan_mode_medium) {
1469 this->supports_fan_mode_medium_ = supports_fan_mode_medium;
1470}
1471void ThermostatClimate::set_supports_fan_mode_high(bool supports_fan_mode_high) {
1472 this->supports_fan_mode_high_ = supports_fan_mode_high;
1473}
1474void ThermostatClimate::set_supports_fan_mode_middle(bool supports_fan_mode_middle) {
1475 this->supports_fan_mode_middle_ = supports_fan_mode_middle;
1476}
1477void ThermostatClimate::set_supports_fan_mode_focus(bool supports_fan_mode_focus) {
1478 this->supports_fan_mode_focus_ = supports_fan_mode_focus;
1479}
1480void ThermostatClimate::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) {
1481 this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse;
1482}
1483void ThermostatClimate::set_supports_fan_mode_quiet(bool supports_fan_mode_quiet) {
1484 this->supports_fan_mode_quiet_ = supports_fan_mode_quiet;
1485}
1486void ThermostatClimate::set_supports_swing_mode_both(bool supports_swing_mode_both) {
1487 this->supports_swing_mode_both_ = supports_swing_mode_both;
1488}
1489void ThermostatClimate::set_supports_swing_mode_off(bool supports_swing_mode_off) {
1490 this->supports_swing_mode_off_ = supports_swing_mode_off;
1491}
1492void ThermostatClimate::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) {
1493 this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal;
1494}
1495void ThermostatClimate::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) {
1496 this->supports_swing_mode_vertical_ = supports_swing_mode_vertical;
1497}
1498void ThermostatClimate::set_supports_two_points(bool supports_two_points) {
1499 this->supports_two_points_ = supports_two_points;
1500}
1501void ThermostatClimate::set_supports_dehumidification(bool supports_dehumidification) {
1502 this->supports_dehumidification_ = supports_dehumidification;
1503 if (supports_dehumidification) {
1504 this->supports_humidification_ = false;
1505 }
1506}
1507void ThermostatClimate::set_supports_humidification(bool supports_humidification) {
1508 this->supports_humidification_ = supports_humidification;
1509 if (supports_humidification) {
1510 this->supports_dehumidification_ = false;
1511 }
1512}
1513
1558
1560 LOG_CLIMATE("", "Thermostat", this);
1561
1562 ESP_LOGCONFIG(TAG,
1563 " On boot, restore from: %s\n"
1564 " Use Start-up Delay: %s",
1565 this->on_boot_restore_from_ == thermostat::DEFAULT_PRESET ? "DEFAULT_PRESET" : "MEMORY",
1566 YESNO(this->use_startup_delay_));
1567 if (this->supports_two_points_) {
1568 ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_);
1569 }
1570 if (this->supports_cool_) {
1571 ESP_LOGCONFIG(TAG,
1572 " Cooling Parameters:\n"
1573 " Deadband: %.1f°C\n"
1574 " Overrun: %.1f°C\n"
1575 " Minimum Off Time: %" PRIu32 "s\n"
1576 " Minimum Run Time: %" PRIu32 "s",
1580 if ((this->supplemental_cool_delta_ > 0) ||
1582 ESP_LOGCONFIG(TAG,
1583 " Maximum Run Time: %" PRIu32 "s\n"
1584 " Supplemental Delta: %.1f°C",
1587 }
1588 }
1589 if (this->supports_heat_) {
1590 ESP_LOGCONFIG(TAG,
1591 " Heating Parameters:\n"
1592 " Deadband: %.1f°C\n"
1593 " Overrun: %.1f°C\n"
1594 " Minimum Off Time: %" PRIu32 "s\n"
1595 " Minimum Run Time: %" PRIu32 "s",
1599 if ((this->supplemental_heat_delta_ > 0) ||
1601 ESP_LOGCONFIG(TAG,
1602 " Maximum Run Time: %" PRIu32 "s\n"
1603 " Supplemental Delta: %.1f°C",
1606 }
1607 }
1608 if (this->supports_fan_only_) {
1609 ESP_LOGCONFIG(TAG,
1610 " Fan Parameters:\n"
1611 " Minimum Off Time: %" PRIu32 "s\n"
1612 " Minimum Run Time: %" PRIu32 "s",
1615 }
1620 ESP_LOGCONFIG(TAG, " Minimum Fan Mode Switching Time: %" PRIu32 "s",
1622 }
1623 ESP_LOGCONFIG(TAG,
1624 " Minimum Idle Time: %" PRIu32 "s\n"
1625 " Supported MODES:\n"
1626 " AUTO: %s\n"
1627 " HEAT/COOL: %s\n"
1628 " HEAT: %s\n"
1629 " COOL: %s\n"
1630 " DRY: %s\n"
1631 " FAN_ONLY: %s\n"
1632 " FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n"
1633 " FAN_ONLY_COOLING: %s",
1634 this->timer_[thermostat::THERMOSTAT_TIMER_IDLE_ON].time / 1000, YESNO(this->supports_auto_),
1635 YESNO(this->supports_heat_cool_), YESNO(this->supports_heat_), YESNO(this->supports_cool_),
1636 YESNO(this->supports_dry_), YESNO(this->supports_fan_only_),
1638 if (this->supports_cool_) {
1639 ESP_LOGCONFIG(TAG, " FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_));
1640 }
1641 if (this->supports_heat_) {
1642 ESP_LOGCONFIG(TAG, " FAN_WITH_HEATING: %s", YESNO(this->supports_fan_with_heating_));
1643 }
1644 ESP_LOGCONFIG(TAG,
1645 " Supported FAN MODES:\n"
1646 " ON: %s\n"
1647 " OFF: %s\n"
1648 " AUTO: %s\n"
1649 " LOW: %s\n"
1650 " MEDIUM: %s\n"
1651 " HIGH: %s\n"
1652 " MIDDLE: %s\n"
1653 " FOCUS: %s\n"
1654 " DIFFUSE: %s\n"
1655 " QUIET: %s\n"
1656 " Supported SWING MODES:\n"
1657 " BOTH: %s\n"
1658 " OFF: %s\n"
1659 " HORIZONTAL: %s\n"
1660 " VERTICAL: %s\n"
1661 " Supports TWO SET POINTS: %s\n"
1662 " Supported Humidity Parameters:\n"
1663 " CURRENT: %s\n"
1664 " TARGET: %s\n"
1665 " DEHUMIDIFICATION: %s\n"
1666 " HUMIDIFICATION: %s",
1667 YESNO(this->supports_fan_mode_on_), YESNO(this->supports_fan_mode_off_),
1668 YESNO(this->supports_fan_mode_auto_), YESNO(this->supports_fan_mode_low_),
1669 YESNO(this->supports_fan_mode_medium_), YESNO(this->supports_fan_mode_high_),
1670 YESNO(this->supports_fan_mode_middle_), YESNO(this->supports_fan_mode_focus_),
1671 YESNO(this->supports_fan_mode_diffuse_), YESNO(this->supports_fan_mode_quiet_),
1672 YESNO(this->supports_swing_mode_both_), YESNO(this->supports_swing_mode_off_),
1674 YESNO(this->supports_two_points_),
1675 YESNO(this->get_traits().has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)),
1677 YESNO(this->supports_dehumidification_), YESNO(this->supports_humidification_));
1678
1679 if (!this->preset_config_.empty()) {
1680 ESP_LOGCONFIG(TAG, " Supported PRESETS:");
1681 for (const auto &entry : this->preset_config_) {
1682 const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(entry.preset));
1683 ESP_LOGCONFIG(TAG, " %s:%s", preset_name, entry.preset == this->default_preset_ ? " (default)" : "");
1684 this->dump_preset_config_(preset_name, entry.config);
1685 }
1686 }
1687
1688 if (!this->custom_preset_config_.empty()) {
1689 ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS:");
1690 for (const auto &entry : this->custom_preset_config_) {
1691 const auto *preset_name = entry.name;
1692 ESP_LOGCONFIG(TAG, " %s:%s", preset_name,
1693 (this->default_custom_preset_ != nullptr && strcmp(entry.name, this->default_custom_preset_) == 0)
1694 ? " (default)"
1695 : "");
1696 this->dump_preset_config_(preset_name, entry.config);
1697 }
1698 }
1699}
1700
1702
1704 : default_temperature(default_temperature) {}
1705
1707 float default_temperature_high)
1708 : default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {}
1709
1710} // namespace esphome::thermostat
BedjetMode mode
BedJet operating mode.
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void trigger(const Ts &...x)
Inform the parent automation that the event has triggered.
Definition automation.h:204
void stop_action()
Stop any action connected to this trigger.
Definition automation.h:212
This class is used to encode all control actions on a climate device.
Definition climate.h:32
const optional< ClimateSwingMode > & get_swing_mode() const
Definition climate.cpp:306
const optional< float > & get_target_humidity() const
Definition climate.cpp:302
const optional< float > & get_target_temperature_low() const
Definition climate.cpp:300
const optional< float > & get_target_temperature() const
Definition climate.cpp:299
const optional< ClimatePreset > & get_preset() const
Definition climate.cpp:307
const char * get_custom_preset() const
Definition climate.h:114
const optional< float > & get_target_temperature_high() const
Definition climate.cpp:301
const optional< ClimateFanMode > & get_fan_mode() const
Definition climate.cpp:305
const optional< ClimateMode > & get_mode() const
Definition climate.cpp:304
ClimateMode mode
The active mode of the climate device.
Definition climate.h:261
optional< ClimateFanMode > fan_mode
The active fan mode of the climate device.
Definition climate.h:255
ClimateTraits get_traits()
Get the traits of this climate device with all overrides applied.
Definition climate.cpp:487
float target_temperature
The target temperature of the climate device.
Definition climate.h:242
float current_humidity
The current humidity of the climate device, as reported from the integration.
Definition climate.h:238
ClimateSwingMode swing_mode
The active swing mode of the climate device.
Definition climate.h:267
float target_temperature_low
The minimum target temperature of the climate device, for climate devices with split target temperatu...
Definition climate.h:245
bool set_preset_(ClimatePreset preset)
Set preset. Reset custom preset. Return true if preset has been changed.
Definition climate.cpp:693
bool set_custom_preset_(const char *preset)
Set custom preset. Reset primary preset. Return true if preset has been changed.
Definition climate.cpp:695
const char * get_custom_preset() const
Get the active custom preset (read-only access).
Definition climate.h:273
bool has_custom_preset() const
Check if a custom preset is currently active.
Definition climate.h:232
float current_temperature
The current temperature of the climate device, as reported from the integration.
Definition climate.h:235
ClimateAction action
The active state of the climate device.
Definition climate.h:264
void publish_state()
Publish the state of the climate device, to be called from integrations.
Definition climate.cpp:438
optional< ClimatePreset > preset
The active preset of the climate device.
Definition climate.h:258
optional< ClimateDeviceRestoreState > restore_state_()
Restore the state of the climate device, call this from your setup() method.
Definition climate.cpp:362
float target_humidity
The target humidity of the climate device.
Definition climate.h:252
float target_temperature_high
The maximum target temperature of the climate device, for climate devices with split target temperatu...
Definition climate.h:247
void add_feature_flags(uint32_t feature_flags)
void add_supported_fan_mode(ClimateFanMode mode)
void add_supported_preset(ClimatePreset preset)
void add_supported_mode(ClimateMode mode)
void set_supported_custom_presets(std::initializer_list< const char * > presets)
void add_supported_swing_mode(ClimateSwingMode mode)
bool has_value() const
Definition optional.h:92
value_type value_or(U const &v) const
Definition optional.h:98
value_type const & value() const
Definition optional.h:94
Base-class for all sensors.
Definition sensor.h:43
void add_on_state_callback(std::function< void(float)> &&callback)
Add a callback that will be called every time a filtered value arrives.
Definition sensor.cpp:90
float state
This member variable stores the last state that has passed through all filters.
Definition sensor.h:117
Trigger * swing_mode_off_trigger_
The trigger to call when the controller should switch the swing mode to "off".
void set_supports_humidification(bool supports_humidification)
void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal)
Trigger * idle_action_trigger_
The trigger to call when the controller should switch to idle action/off mode.
void switch_to_action_(climate::ClimateAction action, bool publish_state=true)
Switch the climate device to the given climate action.
void set_custom_preset_config(std::initializer_list< CustomPresetEntry > presets)
void set_supports_fan_mode_on(bool supports_fan_mode_on)
FixedVector< CustomPresetEntry > custom_preset_config_
The set of custom preset configurations this thermostat supports (eg. "My Custom Preset")
Trigger * preset_change_trigger_
The trigger to call when the preset mode changes.
float cooling_deadband_
Hysteresis values used for computing climate actions.
Trigger * heat_action_trigger_
The trigger to call when the controller should switch to heating action/mode.
void control(const climate::ClimateCall &call) override
Override control to change settings of the climate device.
void validate_target_temperatures(bool pin_target_temperature_high)
HumidificationAction humidification_action
The current humidification action.
void set_timer_duration_in_sec_(ThermostatClimateTimerIndex timer_index, uint32_t time)
Enhanced timer duration setter with running timer adjustment.
Trigger * humidity_control_dehumidify_action_trigger_
The trigger to call when dehumidification is required.
bool use_startup_delay_
Used to start "off" delay timers at boot.
void set_on_boot_restore_from(OnBootRestoreFrom on_boot_restore_from)
float cool_deadband()
Get current hysteresis values.
bool climate_action_change_delayed()
Returns true if a climate action/fan mode transition is being delayed.
void set_supports_fan_with_heating(bool supports_fan_with_heating)
bool cooling_max_runtime_exceeded_
Flags indicating if maximum allowable run time was exceeded.
const uint8_t min_timer_duration_
Minimum allowable duration in seconds for action timers.
Trigger * fan_mode_low_trigger_
The trigger to call when the controller should switch the fan to "low" speed.
OnBootRestoreFrom on_boot_restore_from_
If set to DEFAULT_PRESET then the default preset is always used.
void change_custom_preset_(const char *custom_preset)
Change to a provided custom preset setting; will reset temperature, mode, fan, and swing modes accord...
void set_supports_two_points(bool supports_two_points)
Trigger * fan_mode_auto_trigger_
The trigger to call when the controller should switch the fan to "auto" mode.
void set_preset_config(std::initializer_list< PresetEntry > presets)
void set_supports_fan_only_cooling(bool supports_fan_only_cooling)
bool supports_dehumidification_
Whether the controller supports dehumidification and/or humidification.
bool supports_fan_mode_on_
Whether the controller supports turning on or off just the fan.
void set_supports_fan_only_action_uses_fan_mode_timer(bool fan_only_action_uses_fan_mode_timer)
Trigger * humidity_control_off_action_trigger_
The trigger to call when (de)humidification should stop.
float set_point_minimum_differential_
Minimum differential required between set points.
bool supports_fan_with_cooling_
Special flags – enables fan_only action to be called with cooling/heating actions.
void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse)
void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode, bool publish_state=true)
Switch the climate device to the given climate swing mode.
Trigger * fan_mode_diffuse_trigger_
The trigger to call when the controller should switch the fan to "diffuse" position.
Trigger * fan_only_action_trigger_
The trigger to call when the controller should switch to fan-only action/mode.
Trigger * cool_action_trigger_
The trigger to call when the controller should switch to cooling action/mode.
Trigger * swing_mode_vertical_trigger_
The trigger to call when the controller should switch the swing mode to "vertical".
bool change_preset_internal_(const ThermostatClimateTargetTempConfig &config)
Applies the temperature, mode, fan, and swing modes of the provided config.
void set_supports_dehumidification(bool supports_dehumidification)
bool hysteresis_valid()
Set point and hysteresis validation.
float humidity_hysteresis_
Hysteresis values used for computing humidification action.
void set_supports_fan_mode_auto(bool supports_fan_mode_auto)
float prev_target_humidity_
Store previously-known humidity and temperatures.
void check_humidity_change_trigger_()
Check if the humidity change trigger should be called.
Trigger * humidity_control_humidify_action_trigger_
The trigger to call when humidification is required.
void switch_to_supplemental_action_(climate::ClimateAction action)
Trigger * dry_action_trigger_
The trigger to call when the controller should switch to dry (dehumidification) mode.
void switch_to_mode_(climate::ClimateMode mode, bool publish_state=true)
Switch the climate device to the given climate mode.
Trigger * swing_mode_both_trigger_
The trigger to call when the controller should switch the swing mode to "both".
void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical)
void set_supports_fan_only(bool supports_fan_only)
bool supports_fan_only_action_uses_fan_mode_timer_
Special flag – enables fan_modes to share timer with fan_only climate action.
void switch_to_humidity_control_action_(HumidificationAction action)
void set_supports_swing_mode_off(bool supports_swing_mode_off)
void switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bool publish_state=true)
Switch the climate device to the given climate fan mode.
void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config)
bool supports_swing_mode_both_
Whether the controller supports various swing modes.
climate::ClimateTraits traits() override
Return the traits of this controller.
Trigger * swing_mode_horizontal_trigger_
The trigger to call when the controller should switch the swing mode to "horizontal".
Trigger * fan_mode_focus_trigger_
The trigger to call when the controller should switch the fan to "focus" position.
climate::ClimateFanMode locked_fan_mode()
Returns the fan mode that is locked in (check fan_mode_change_delayed(), first!)
void set_humidity_sensor(sensor::Sensor *humidity_sensor)
void set_supports_fan_mode_middle(bool supports_fan_mode_middle)
Trigger * temperature_change_trigger_
The trigger to call when the target temperature(s) change(es).
void set_supports_fan_mode_low(bool supports_fan_mode_low)
Trigger * fan_mode_off_trigger_
The trigger to call when the controller should switch off the fan.
bool timer_active_(ThermostatClimateTimerIndex timer_index)
float supplemental_cool_delta_
Maximum allowable temperature deltas before engaging supplemental cooling/heating actions.
void set_supports_heat_cool(bool supports_heat_cool)
void set_supports_fan_mode_quiet(bool supports_fan_mode_quiet)
climate::ClimateAction delayed_climate_action()
Returns the climate action that is being delayed (check climate_action_change_delayed(),...
sensor::Sensor * sensor_
The sensor used for getting the current temperature.
void set_default_preset(const char *custom_preset)
void call_timer_callback_(ThermostatClimateTimerIndex timer_index)
Call the appropriate timer callback based on timer index.
void set_supports_fan_mode_medium(bool supports_fan_mode_medium)
uint32_t timer_duration_(ThermostatClimateTimerIndex timer_index)
void set_supports_swing_mode_both(bool supports_swing_mode_both)
void start_timer_(ThermostatClimateTimerIndex timer_index)
Start/cancel/get status of climate action timer.
Trigger * fan_mode_high_trigger_
The trigger to call when the controller should switch the fan to "high" speed.
void set_set_point_minimum_differential(float differential)
bool supports_fan_mode_low_
Whether the controller supports various fan speeds and/or positions.
bool supports_fan_only_cooling_
Special flag – enables fan to be switched based on target_temperature_high.
Trigger * prev_action_trigger_
A reference to the trigger that was previously active.
bool supports_auto_
Whether the controller supports auto/cooling/drying/fanning/heating.
climate::ClimateAction supplemental_action_
The current supplemental action.
std::array< ThermostatClimateTimer, THERMOSTAT_TIMER_COUNT > timer_
Climate action timers.
void set_fan_mode_minimum_switching_time_in_sec(uint32_t time)
Trigger * humidity_change_trigger_
The trigger to call when the target humidity changes.
void set_supports_fan_with_cooling(bool supports_fan_with_cooling)
Trigger * fan_mode_medium_trigger_
The trigger to call when the controller should switch the fan to "medium" speed.
Trigger * fan_mode_middle_trigger_
The trigger to call when the controller should switch the fan to "middle" position.
void set_supports_fan_mode_focus(bool supports_fan_mode_focus)
void check_temperature_change_trigger_()
Check if the temperature change trigger should be called.
Trigger * fan_mode_on_trigger_
The trigger to call when the controller should switch on the fan.
HumidificationAction compute_humidity_control_action_()
void set_supports_fan_mode_off(bool supports_fan_mode_off)
FixedVector< PresetEntry > preset_config_
The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO,...
climate::ClimateFanMode prev_fan_mode_
Store previously-known states.
void set_use_startup_delay(bool use_startup_delay)
void set_humidity_hysteresis(float humidity_hysteresis)
bool cancel_timer_(ThermostatClimateTimerIndex timer_index)
climate::ClimateAction compute_supplemental_action_()
climate::ClimatePreset default_preset_
Default standard preset to use on start up.
sensor::Sensor * humidity_sensor_
The sensor used for getting the current humidity.
void cooling_max_run_time_timer_callback_()
set_timeout() callbacks for various actions (see above)
bool supports_fan_mode_auto_
Whether the controller supports fan auto mode.
climate::ClimateAction compute_action_(bool ignore_timers=false)
Re-compute the required action of this climate controller.
bool supports_two_points_
Whether the controller supports two set points.
bool setup_complete_
setup_complete_ blocks modifying/resetting the temps immediately after boot
void set_supports_fan_mode_high(bool supports_fan_mode_high)
bool idle_action_ready_()
Is the action ready to be called? Returns true if so.
void change_preset_(climate::ClimatePreset preset)
Change to a provided preset setting; will reset temperature, mode, fan, and swing modes accordingly.
void refresh()
Call triggers based on updated climate states (modes/actions)
Trigger * fan_mode_quiet_trigger_
The trigger to call when the controller should switch the fan to "quiet" position.
Trigger * heat_cool_mode_trigger_
The trigger to call when the controller should switch to heat/cool mode.
Trigger * auto_mode_trigger_
The trigger to call when the controller should switch to auto mode.
bool cooling_required_()
Check if cooling/fanning/heating actions are required; returns true if so.
ClimateSwingMode swing_mode
Definition climate.h:11
uint8_t custom_preset
Definition climate.h:9
ClimateFanMode fan_mode
Definition climate.h:3
ClimatePreset preset
Definition climate.h:8
bool state
Definition fan.h:0
@ CLIMATE_SUPPORTS_CURRENT_HUMIDITY
@ CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE
@ CLIMATE_SUPPORTS_CURRENT_TEMPERATURE
const LogString * climate_swing_mode_to_string(ClimateSwingMode swing_mode)
Convert the given ClimateSwingMode to a human-readable string.
const LogString * climate_preset_to_string(ClimatePreset preset)
Convert the given PresetMode to a human-readable string.
ClimatePreset
Enum for all preset modes NOTE: If adding values, update ClimatePresetMask in climate_traits....
@ CLIMATE_PRESET_NONE
No preset is active.
const LogString * climate_fan_mode_to_string(ClimateFanMode fan_mode)
Convert the given ClimateFanMode to a human-readable string.
ClimateSwingMode
Enum for all modes a climate swing can be in NOTE: If adding values, update ClimateSwingModeMask in c...
@ 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.
@ CLIMATE_MODE_AUTO
The climate device is adjusting the temperature dynamically.
const LogString * climate_mode_to_string(ClimateMode mode)
Convert the given ClimateMode to a human-readable string.
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_DIFFUSE
The fan mode is set to Diffuse.
@ CLIMATE_FAN_ON
The fan mode is set to On.
@ CLIMATE_FAN_AUTO
The fan mode is set to Auto.
@ CLIMATE_FAN_FOCUS
The fan mode is set to Focus.
@ CLIMATE_FAN_LOW
The fan mode is set to Low.
@ CLIMATE_FAN_MIDDLE
The fan mode is set to Middle.
@ CLIMATE_FAN_QUIET
The fan mode is set to Quiet.
@ CLIMATE_FAN_OFF
The fan mode is set to Off.
@ CLIMATE_FAN_HIGH
The fan mode is set to High.
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
optional< climate::ClimateSwingMode > swing_mode_
uint16_t temperature
Definition sun_gtil2.cpp:12