ESPHome 2026.5.0-dev
Loading...
Searching...
No Matches
thermostat_climate.cpp
Go to the documentation of this file.
4#include "esphome/core/log.h"
5#include <cinttypes>
6
8
9static const char *const TAG = "thermostat.climate";
10
12 if (this->use_startup_delay_) {
13 // start timers so that no actions are called for a moment
19 }
20 // add a callback so that whenever the sensor state changes we can take action
21 this->sensor_->add_on_state_callback([this](float state) {
23 // required action may have changed, recompute, refresh, we'll publish_state() later
24 this->switch_to_action_(this->compute_action_(), false);
26 // current temperature and possibly action changed, so publish the new state
27 this->publish_state();
28 });
29 this->current_temperature = this->sensor_->state;
30
31 // register for humidity values and get initial state
32 if (this->humidity_sensor_ != nullptr) {
33 this->humidity_sensor_->add_on_state_callback([this](float state) {
34 this->current_humidity = state;
36 this->publish_state();
37 });
39 }
40
41 auto use_default_preset = true;
42
44 // restore all climate data, if possible
45 auto restore = this->restore_state_();
46 if (restore.has_value()) {
47 use_default_preset = false;
48 restore->to_call(this).perform();
49 }
50 }
51
52 // Either we failed to restore state or the user has requested we always apply the default preset
53 if (use_default_preset) {
56 } else if (this->default_custom_preset_ != nullptr) {
57 this->change_custom_preset_(this->default_custom_preset_);
58 }
59 }
60
61 // refresh the climate action based on the restored settings, we'll publish_state() later
62 this->switch_to_action_(this->compute_action_(), false);
64 this->setup_complete_ = true;
65 this->publish_state();
66}
67
70 for (uint8_t i = 0; i < THERMOSTAT_TIMER_COUNT; i++) {
71 auto &timer = this->timer_[i];
72 if (timer.active && (now - timer.started >= timer.time)) {
73 timer.active = false;
75 }
76 }
77}
78
83
95
97 bool state_mismatch = this->action != this->compute_action_(true);
98
99 switch (this->compute_action_(true)) {
102 return state_mismatch && (!this->idle_action_ready_());
104 return state_mismatch && (!this->cooling_action_ready_());
106 return state_mismatch && (!this->heating_action_ready_());
108 return state_mismatch && (!this->fanning_action_ready_());
110 return state_mismatch && (!this->drying_action_ready_());
111 default:
112 break;
113 }
114 return false;
115}
116
118 bool state_mismatch = this->fan_mode.value_or(climate::CLIMATE_FAN_ON) != this->prev_fan_mode_;
119 return state_mismatch && (!this->fan_mode_ready_());
120}
121
123
125
127 if ((this->supports_cool_ || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) &&
128 (std::isnan(this->cooling_deadband_) || std::isnan(this->cooling_overrun_)))
129 return false;
130
131 if (this->supports_heat_ && (std::isnan(this->heating_deadband_) || std::isnan(this->heating_overrun_)))
132 return false;
133
134 return true;
135}
136
138 return !std::isnan(this->humidity_hysteresis_) && this->humidity_hysteresis_ >= 0.0f &&
139 this->humidity_hysteresis_ < 100.0f;
140}
141
146
148 if (std::isnan(this->target_temperature)) {
149 // default to the midpoint between visual min and max
150 this->target_temperature =
153 } else {
154 // target_temperature must be between the visual minimum and the visual maximum
155 this->target_temperature = clamp(this->target_temperature, this->get_traits().get_visual_min_temperature(),
156 this->get_traits().get_visual_max_temperature());
157 }
158}
159
160void ThermostatClimate::validate_target_temperatures(const bool pin_target_temperature_high) {
161 if (!this->supports_two_points_) {
163 } else if (pin_target_temperature_high) {
164 // if target_temperature_high is set less than target_temperature_low, move down target_temperature_low
167 } else {
168 // if target_temperature_low is set greater than target_temperature_high, move up target_temperature_high
171 }
172}
173
175 if (std::isnan(this->target_temperature_low)) {
177 } else {
178 float target_temperature_low_upper_limit =
181 this->get_traits().get_visual_min_temperature(), this->get_traits().get_visual_max_temperature())
183 this->target_temperature_low = clamp(this->target_temperature_low, this->get_traits().get_visual_min_temperature(),
184 target_temperature_low_upper_limit);
185 }
186}
187
189 if (std::isnan(this->target_temperature_high)) {
191 } else {
192 float target_temperature_high_lower_limit =
195 this->get_traits().get_visual_min_temperature(), this->get_traits().get_visual_max_temperature())
197 this->target_temperature_high = clamp(this->target_temperature_high, target_temperature_high_lower_limit,
198 this->get_traits().get_visual_max_temperature());
199 }
200}
201
203 if (std::isnan(this->target_humidity)) {
204 this->target_humidity =
206 } else {
207 this->target_humidity = clamp<float>(this->target_humidity, this->get_traits().get_visual_min_humidity(),
208 this->get_traits().get_visual_max_humidity());
209 }
210}
211
213 bool target_temperature_high_changed = false;
214
215 auto preset = call.get_preset();
216 if (preset.has_value()) {
217 // setup_complete_ blocks modifying/resetting the temps immediately after boot
218 if (this->setup_complete_) {
219 this->change_preset_(*preset);
220 } else {
221 this->preset = preset;
222 }
223 }
224 if (call.has_custom_preset()) {
225 // setup_complete_ blocks modifying/resetting the temps immediately after boot
226 if (this->setup_complete_) {
228 } else {
229 // Use the base class method which handles pointer lookup internally
231 }
232 }
233
234 auto mode = call.get_mode();
235 if (mode.has_value()) {
236 this->mode = *mode;
237 }
238 auto fan_mode = call.get_fan_mode();
239 if (fan_mode.has_value()) {
240 this->fan_mode = fan_mode;
241 }
242 auto swing_mode = call.get_swing_mode();
243 if (swing_mode.has_value()) {
244 this->swing_mode = *swing_mode;
245 }
246 if (this->supports_two_points_) {
247 auto target_temp_low = call.get_target_temperature_low();
248 if (target_temp_low.has_value()) {
249 this->target_temperature_low = *target_temp_low;
250 }
251 auto target_temp_high = call.get_target_temperature_high();
252 if (target_temp_high.has_value()) {
253 target_temperature_high_changed = this->target_temperature_high != *target_temp_high;
254 this->target_temperature_high = *target_temp_high;
255 }
256 // ensure the two set points are valid and adjust one of them if necessary
257 this->validate_target_temperatures(target_temperature_high_changed ||
259 } else {
260 auto target_temp = call.get_target_temperature();
261 if (target_temp.has_value()) {
262 this->target_temperature = *target_temp;
264 }
265 }
267 if (target_humidity.has_value()) {
270 }
271 // make any changes happen
272 this->refresh();
273}
274
277
279
280 if (this->supports_two_points_)
282
283 if (this->humidity_sensor_ != nullptr)
285
288
289 if (this->supports_auto_)
291 if (this->supports_heat_cool_)
293 if (this->supports_cool_)
295 if (this->supports_dry_)
297 if (this->supports_fan_only_)
299 if (this->supports_heat_)
301
302 if (this->supports_fan_mode_on_)
304 if (this->supports_fan_mode_off_)
306 if (this->supports_fan_mode_auto_)
308 if (this->supports_fan_mode_low_)
312 if (this->supports_fan_mode_high_)
316 if (this->supports_fan_mode_focus_)
320 if (this->supports_fan_mode_quiet_)
322
327 if (this->supports_swing_mode_off_)
331
332 for (const auto &entry : this->preset_config_) {
333 traits.add_supported_preset(entry.preset);
334 }
335
336 // Custom presets are stored on Climate base class and wired via get_traits()
337
338 return traits;
339}
340
342 auto target_action = climate::CLIMATE_ACTION_IDLE;
343 // if any hysteresis values or current_temperature is not valid, we go to OFF;
344 if (std::isnan(this->current_temperature) || !this->hysteresis_valid()) {
346 }
347 // do not change the action if an "ON" timer is running
348 if ((!ignore_timers) && (this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) ||
352 return this->action;
353 }
354
355 // ensure set point(s) is/are valid before computing the action
357 // everything has been validated so we can now safely compute the action
358 switch (this->mode) {
359 // if the climate mode is OFF then the climate action must be OFF
361 target_action = climate::CLIMATE_ACTION_OFF;
362 break;
364 if (this->fanning_required_())
365 target_action = climate::CLIMATE_ACTION_FAN;
366 break;
368 target_action = climate::CLIMATE_ACTION_DRYING;
369 break;
371 if (this->cooling_required_() && this->heating_required_()) {
372 // this is bad and should never happen, so just stop.
373 // target_action = climate::CLIMATE_ACTION_IDLE;
374 } else if (this->cooling_required_()) {
375 target_action = climate::CLIMATE_ACTION_COOLING;
376 } else if (this->heating_required_()) {
377 target_action = climate::CLIMATE_ACTION_HEATING;
378 }
379 break;
381 if (this->cooling_required_()) {
382 target_action = climate::CLIMATE_ACTION_COOLING;
383 }
384 break;
386 if (this->heating_required_()) {
387 target_action = climate::CLIMATE_ACTION_HEATING;
388 }
389 break;
391 if (this->supports_two_points_) {
392 if (this->cooling_required_() && this->heating_required_()) {
393 // this is bad and should never happen, so just stop.
394 // target_action = climate::CLIMATE_ACTION_IDLE;
395 } else if (this->cooling_required_()) {
396 target_action = climate::CLIMATE_ACTION_COOLING;
397 } else if (this->heating_required_()) {
398 target_action = climate::CLIMATE_ACTION_HEATING;
399 }
400 } else if (this->supports_cool_ && this->cooling_required_()) {
401 target_action = climate::CLIMATE_ACTION_COOLING;
402 } else if (this->supports_heat_ && this->heating_required_()) {
403 target_action = climate::CLIMATE_ACTION_HEATING;
404 }
405 break;
406 default:
407 break;
408 }
409 // do not abruptly switch actions. cycle through IDLE, first. we'll catch this at the next update.
411 (target_action == climate::CLIMATE_ACTION_HEATING)) ||
413 ((target_action == climate::CLIMATE_ACTION_COOLING) || (target_action == climate::CLIMATE_ACTION_DRYING)))) {
415 }
416
417 return target_action;
418}
419
421 auto target_action = climate::CLIMATE_ACTION_IDLE;
422 // if any hysteresis values or current_temperature is not valid, we go to OFF;
423 if (std::isnan(this->current_temperature) || !this->hysteresis_valid()) {
425 }
426
427 // ensure set point(s) is/are valid before computing the action
429 // everything has been validated so we can now safely compute the action
430 switch (this->mode) {
431 // if the climate mode is OFF then the climate action must be OFF
433 target_action = climate::CLIMATE_ACTION_OFF;
434 break;
437 // this is bad and should never happen, so just stop.
438 // target_action = climate::CLIMATE_ACTION_IDLE;
439 } else if (this->supplemental_cooling_required_()) {
440 target_action = climate::CLIMATE_ACTION_COOLING;
441 } else if (this->supplemental_heating_required_()) {
442 target_action = climate::CLIMATE_ACTION_HEATING;
443 }
444 break;
446 if (this->supplemental_cooling_required_()) {
447 target_action = climate::CLIMATE_ACTION_COOLING;
448 }
449 break;
451 if (this->supplemental_heating_required_()) {
452 target_action = climate::CLIMATE_ACTION_HEATING;
453 }
454 break;
455 default:
456 break;
457 }
458
459 return target_action;
460}
461
463 auto target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
464 // if hysteresis value or current_humidity is not valid, we go to OFF
465 if (std::isnan(this->current_humidity) || !this->humidity_hysteresis_valid()) {
467 }
468
469 // ensure set point is valid before computing the action
471 // everything has been validated so we can now safely compute the action
473 // this is bad and should never happen, so just stop.
474 // target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
475 } else if (this->supports_dehumidification_ && this->dehumidification_required_()) {
477 } else if (this->supports_humidification_ && this->humidification_required_()) {
479 }
480
481 return target_action;
482}
483
485 // setup_complete_ helps us ensure an action is called immediately after boot
486 if ((action == this->action) && this->setup_complete_) {
487 // already in target mode
488 return;
489 }
490
491 if (((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) ||
493 this->setup_complete_) {
494 // switching from OFF to IDLE or vice-versa -- this is only a visual difference.
495 // OFF means user manually disabled, IDLE means the temperature is in target range.
496 this->action = action;
497 if (publish_state)
498 this->publish_state();
499 return;
500 }
501
502 bool action_ready = false;
503 Trigger<> *trig = &this->idle_action_trigger_, *trig_fan = nullptr;
504 switch (action) {
507 if (this->idle_action_ready_()) {
509 if (this->action == climate::CLIMATE_ACTION_COOLING)
511 if (this->action == climate::CLIMATE_ACTION_FAN) {
514 } else {
516 }
517 }
518 if (this->action == climate::CLIMATE_ACTION_HEATING)
520 // trig = this->idle_action_trigger_;
521 ESP_LOGVV(TAG, "Switching to IDLE/OFF action");
522 this->cooling_max_runtime_exceeded_ = false;
523 this->heating_max_runtime_exceeded_ = false;
524 action_ready = true;
525 }
526 break;
528 if (this->cooling_action_ready_()) {
531 if (this->supports_fan_with_cooling_) {
533 trig_fan = &this->fan_only_action_trigger_;
534 }
535 this->cooling_max_runtime_exceeded_ = false;
536 trig = &this->cool_action_trigger_;
537 ESP_LOGVV(TAG, "Switching to COOLING action");
538 action_ready = true;
539 }
540 break;
542 if (this->heating_action_ready_()) {
545 if (this->supports_fan_with_heating_) {
547 trig_fan = &this->fan_only_action_trigger_;
548 }
549 this->heating_max_runtime_exceeded_ = false;
550 trig = &this->heat_action_trigger_;
551 ESP_LOGVV(TAG, "Switching to HEATING action");
552 action_ready = true;
553 }
554 break;
556 if (this->fanning_action_ready_()) {
559 } else {
561 }
562 trig = &this->fan_only_action_trigger_;
563 ESP_LOGVV(TAG, "Switching to FAN_ONLY action");
564 action_ready = true;
565 }
566 break;
568 if (this->drying_action_ready_()) {
571 trig = &this->dry_action_trigger_;
572 ESP_LOGVV(TAG, "Switching to DRYING action");
573 action_ready = true;
574 }
575 break;
576 default:
577 // we cannot report an invalid mode back to HA (even if it asked for one)
578 // and must assume some valid value
580 // trig = this->idle_action_trigger_;
581 }
582
583 if (action_ready) {
584 if (this->prev_action_trigger_ != nullptr) {
586 this->prev_action_trigger_ = nullptr;
587 }
588 this->action = action;
589 this->prev_action_trigger_ = trig;
590 trig->trigger();
591 // if enabled, call the fan_only action with cooling/heating actions
592 if (trig_fan != nullptr) {
593 ESP_LOGVV(TAG, "Calling FAN_ONLY action with HEATING/COOLING action");
594 trig_fan->trigger();
595 }
596 if (publish_state)
597 this->publish_state();
598 }
599}
600
602 // Always cancel max-runtime timers and clear exceeded flags when transitioning to idle/off,
603 // even if supplemental_action_ is already idle (early-return path). This prevents a stale
604 // heating_max_runtime_exceeded_ flag from triggering supplemental on the next heating cycle
605 // when HEATING_MAX_RUN_TIME fires while the main action is already IDLE.
609 this->cooling_max_runtime_exceeded_ = false;
610 this->heating_max_runtime_exceeded_ = false;
611 }
612 // setup_complete_ helps us ensure an action is called immediately after boot
613 if ((action == this->supplemental_action_) && this->setup_complete_) {
614 // already in target mode
615 return;
616 }
617
618 switch (action) {
623 break;
626 break;
629 break;
630 default:
631 return;
632 }
633 ESP_LOGVV(TAG, "Updating supplemental action");
636}
637
639 Trigger<> *trig = nullptr;
640
641 switch (this->supplemental_action_) {
645 }
647 ESP_LOGVV(TAG, "Calling supplemental COOLING action");
648 break;
652 }
654 ESP_LOGVV(TAG, "Calling supplemental HEATING action");
655 break;
656 default:
657 break;
658 }
659
660 if (trig != nullptr) {
661 trig->trigger();
662 }
663}
664
666 // setup_complete_ helps us ensure an action is called immediately after boot
667 if ((action == this->humidification_action) && this->setup_complete_) {
668 // already in target mode
669 return;
670 }
671
673 switch (action) {
675 // trig = &this->humidity_control_off_action_trigger_;
676 ESP_LOGVV(TAG, "Switching to HUMIDIFICATION_OFF action");
677 break;
680 ESP_LOGVV(TAG, "Switching to DEHUMIDIFY action");
681 break;
684 ESP_LOGVV(TAG, "Switching to HUMIDIFY action");
685 break;
687 default:
689 // trig = &this->humidity_control_off_action_trigger_;
690 }
691
692 if (this->prev_humidity_control_trigger_ != nullptr) {
694 this->prev_humidity_control_trigger_ = nullptr;
695 }
698 trig->trigger();
699}
700
702 // setup_complete_ helps us ensure an action is called immediately after boot
703 if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) {
704 // already in target mode
705 return;
706 }
707
708 this->fan_mode = fan_mode;
709 if (publish_state)
710 this->publish_state();
711
712 if (this->fan_mode_ready_()) {
713 Trigger<> *trig = &this->fan_mode_auto_trigger_;
714 switch (fan_mode) {
716 trig = &this->fan_mode_on_trigger_;
717 ESP_LOGVV(TAG, "Switching to FAN_ON mode");
718 break;
720 trig = &this->fan_mode_off_trigger_;
721 ESP_LOGVV(TAG, "Switching to FAN_OFF mode");
722 break;
724 // trig = &this->fan_mode_auto_trigger_;
725 ESP_LOGVV(TAG, "Switching to FAN_AUTO mode");
726 break;
728 trig = &this->fan_mode_low_trigger_;
729 ESP_LOGVV(TAG, "Switching to FAN_LOW mode");
730 break;
732 trig = &this->fan_mode_medium_trigger_;
733 ESP_LOGVV(TAG, "Switching to FAN_MEDIUM mode");
734 break;
736 trig = &this->fan_mode_high_trigger_;
737 ESP_LOGVV(TAG, "Switching to FAN_HIGH mode");
738 break;
740 trig = &this->fan_mode_middle_trigger_;
741 ESP_LOGVV(TAG, "Switching to FAN_MIDDLE mode");
742 break;
744 trig = &this->fan_mode_focus_trigger_;
745 ESP_LOGVV(TAG, "Switching to FAN_FOCUS mode");
746 break;
748 trig = &this->fan_mode_diffuse_trigger_;
749 ESP_LOGVV(TAG, "Switching to FAN_DIFFUSE mode");
750 break;
752 trig = &this->fan_mode_quiet_trigger_;
753 ESP_LOGVV(TAG, "Switching to FAN_QUIET mode");
754 break;
755 default:
756 // we cannot report an invalid mode back to HA (even if it asked for one)
757 // and must assume some valid value
759 // trig = &this->fan_mode_auto_trigger_;
760 }
761 if (this->prev_fan_mode_trigger_ != nullptr) {
763 this->prev_fan_mode_trigger_ = nullptr;
764 }
766 trig->trigger();
767 this->prev_fan_mode_ = fan_mode;
768 this->prev_fan_mode_trigger_ = trig;
769 }
770}
771
773 // setup_complete_ helps us ensure an action is called immediately after boot
774 if ((mode == this->prev_mode_) && this->setup_complete_) {
775 // already in target mode
776 return;
777 }
778
779 if (this->prev_mode_trigger_ != nullptr) {
781 this->prev_mode_trigger_ = nullptr;
782 }
783 Trigger<> *trig = &this->off_mode_trigger_;
784 switch (mode) {
786 trig = &this->auto_mode_trigger_;
787 break;
789 trig = &this->heat_cool_mode_trigger_;
790 break;
792 trig = &this->cool_mode_trigger_;
793 break;
795 trig = &this->heat_mode_trigger_;
796 break;
798 trig = &this->fan_only_mode_trigger_;
799 break;
801 trig = &this->dry_mode_trigger_;
802 break;
804 default:
805 // we cannot report an invalid mode back to HA (even if it asked for one)
806 // and must assume some valid value
808 // trig = this->off_mode_trigger_;
809 }
810 trig->trigger();
811 this->mode = mode;
812 this->prev_mode_ = mode;
813 this->prev_mode_trigger_ = trig;
814 if (publish_state) {
815 this->publish_state();
816 }
817}
818
820 // setup_complete_ helps us ensure an action is called immediately after boot
821 if ((swing_mode == this->prev_swing_mode_) && this->setup_complete_) {
822 // already in target mode
823 return;
824 }
825
826 if (this->prev_swing_mode_trigger_ != nullptr) {
828 this->prev_swing_mode_trigger_ = nullptr;
829 }
830 Trigger<> *trig = &this->swing_mode_off_trigger_;
831 switch (swing_mode) {
833 trig = &this->swing_mode_both_trigger_;
834 break;
836 trig = &this->swing_mode_horizontal_trigger_;
837 break;
839 // trig = &this->swing_mode_off_trigger_;
840 break;
842 trig = &this->swing_mode_vertical_trigger_;
843 break;
844 default:
845 // we cannot report an invalid mode back to HA (even if it asked for one)
846 // and must assume some valid value
847 swing_mode = climate::CLIMATE_SWING_OFF;
848 // trig = &this->swing_mode_off_trigger_;
849 }
850 trig->trigger();
851 this->swing_mode = swing_mode;
853 this->prev_swing_mode_trigger_ = trig;
854 if (publish_state)
855 this->publish_state();
856}
857
868
875
882
884
892
899
901 if (this->timer_duration_(timer_index) > 0) {
902 this->timer_[timer_index].started = millis();
903 this->timer_[timer_index].active = true;
904 }
905}
906
908 auto ret = this->timer_[timer_index].active;
909 this->timer_[timer_index].active = false;
910 return ret;
911}
912
914 return this->timer_[timer_index].active;
915}
916
918 return this->timer_[timer_index].time;
919}
920
922 switch (timer_index) {
925 break;
928 break;
931 break;
934 break;
937 break;
940 break;
943 break;
946 break;
949 break;
952 break;
954 default:
955 break;
956 }
957}
958
960 ESP_LOGVV(TAG, "cooling_max_run_time timer expired");
964}
965
967 ESP_LOGVV(TAG, "cooling_off timer expired");
968 this->switch_to_action_(this->compute_action_());
970}
971
973 ESP_LOGVV(TAG, "cooling_on timer expired");
974 this->switch_to_action_(this->compute_action_());
976}
977
979 ESP_LOGVV(TAG, "fan_mode timer expired");
982 this->switch_to_action_(this->compute_action_());
984 }
985}
986
988 ESP_LOGVV(TAG, "fanning_off timer expired");
989 this->switch_to_action_(this->compute_action_());
990}
991
993 ESP_LOGVV(TAG, "fanning_on timer expired");
994 this->switch_to_action_(this->compute_action_());
995}
996
998 ESP_LOGVV(TAG, "heating_max_run_time timer expired");
1002}
1003
1005 ESP_LOGVV(TAG, "heating_off timer expired");
1006 this->switch_to_action_(this->compute_action_());
1008}
1009
1011 ESP_LOGVV(TAG, "heating_on timer expired");
1012 this->switch_to_action_(this->compute_action_());
1014}
1015
1017 ESP_LOGVV(TAG, "idle_on timer expired");
1018 this->switch_to_action_(this->compute_action_());
1020}
1021
1023 if ((this->prev_target_humidity_ == this->target_humidity) && this->setup_complete_) {
1024 return; // nothing changed, no reason to trigger
1025 } else {
1026 // save the new temperature so we can check it again later; the trigger will fire below
1028 }
1029 // trigger the action
1030 Trigger<> *trig = &this->humidity_change_trigger_;
1031 trig->trigger();
1032}
1033
1035 if (this->supports_two_points_) {
1036 // setup_complete_ helps us ensure an action is called immediately after boot
1039 return; // nothing changed, no reason to trigger
1040 } else {
1041 // save the new temperatures so we can check them again later; the trigger will fire below
1044 }
1045 } else {
1046 if ((this->prev_target_temperature_ == this->target_temperature) && this->setup_complete_) {
1047 return; // nothing changed, no reason to trigger
1048 } else {
1049 // save the new temperature so we can check it again later; the trigger will fire below
1051 }
1052 }
1053 // trigger the action
1055 trig->trigger();
1056}
1057
1060
1061 if (this->supports_cool_) {
1062 if (this->current_temperature >= temperature + this->cooling_deadband_) {
1063 // if the current temperature reaches or exceeds the target + deadband, cooling is required
1064 return true;
1066 // if the current temperature is less than or equal to the target - overrun, cooling should stop
1067 return false;
1068 } else {
1069 // if we get here, the current temperature is between target + deadband and target - overrun,
1070 // so the action should not change unless it conflicts with the current mode
1071 return (this->action == climate::CLIMATE_ACTION_COOLING) &&
1073 }
1074 }
1075 return false;
1076}
1077
1080
1081 if (this->supports_fan_only_) {
1082 if (this->supports_fan_only_cooling_) {
1083 if (this->current_temperature >= temperature + this->cooling_deadband_) {
1084 // if the current temperature reaches or exceeds the target + deadband, fanning is required
1085 return true;
1087 // if the current temperature is less than or equal to the target - overrun, fanning should stop
1088 return false;
1089 } else {
1090 // if we get here, the current temperature is between target + deadband and target - overrun,
1091 // so the action should not change unless it conflicts with the current mode
1093 }
1094 } else {
1095 return true;
1096 }
1097 }
1098 return false;
1099}
1100
1103
1104 if (this->supports_heat_) {
1106 // if the current temperature is below or equal to the target - deadband, heating is required
1107 return true;
1108 } else if (this->current_temperature >= temperature + this->heating_overrun_) {
1109 // if the current temperature is above or equal to the target + overrun, heating should stop
1110
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 preset changed trigger
1205 Trigger<> *trig = &this->preset_change_trigger_;
1206 this->set_preset_(preset);
1207 trig->trigger();
1208
1209 this->refresh();
1210 ESP_LOGI(TAG, "Preset %s applied", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
1211 } else {
1212 ESP_LOGI(TAG, "No changes required to apply preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
1213 }
1214 } else {
1215 ESP_LOGW(TAG, "Preset %s not configured; ignoring", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
1216 }
1217}
1218
1220 // Linear search through custom preset configurations
1221 const ThermostatClimateTargetTempConfig *config = nullptr;
1222 for (const auto &entry : this->custom_preset_config_) {
1223 // Compare first len chars, then verify entry.name ends there (same length)
1224 if (strncmp(entry.name, custom_preset, len) == 0 && entry.name[len] == '\0') {
1225 config = &entry.config;
1226 break;
1227 }
1228 }
1229
1230 if (config != nullptr) {
1231 ESP_LOGV(TAG, "Custom preset %s requested", custom_preset);
1232 if (this->change_preset_internal_(*config) || !this->has_custom_preset() ||
1233 this->get_custom_preset() != custom_preset) {
1234 // Fire preset changed trigger
1235 Trigger<> *trig = &this->preset_change_trigger_;
1236 // Use the base class method which handles pointer lookup and preset reset internally
1237 this->set_custom_preset_(custom_preset);
1238 trig->trigger();
1239
1240 this->refresh();
1241 ESP_LOGI(TAG, "Custom preset %s applied", custom_preset);
1242 } else {
1243 ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset);
1244 // Note: set_custom_preset_() above handles preset.reset() and custom_preset_ assignment internally.
1245 // The old code had these lines here unconditionally, which was a bug (double assignment, state modification
1246 // even when no changes were needed). Now properly handled by the protected setter with mutual exclusion.
1247 }
1248 } else {
1249 ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset);
1250 }
1251}
1252
1254 bool something_changed = false;
1255
1256 if (this->supports_two_points_) {
1257 if (this->target_temperature_low != config.default_temperature_low) {
1259 something_changed = true;
1260 }
1263 something_changed = true;
1264 }
1265 } else {
1266 if (this->target_temperature != config.default_temperature) {
1268 something_changed = true;
1269 }
1270 }
1271
1272 // Note: The mode, fan_mode and swing_mode can all be defined in the preset but if the climate.control call
1273 // also specifies them then the climate.control call's values will override the preset's values for that call
1274 if (config.mode_.has_value() && (this->mode != config.mode_.value())) {
1275 ESP_LOGV(TAG, "Setting mode to %s", LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_)));
1276 this->mode = *config.mode_;
1277 something_changed = true;
1278 }
1279
1280 if (config.fan_mode_.has_value() && (this->fan_mode != config.fan_mode_)) {
1281 ESP_LOGV(TAG, "Setting fan mode to %s", LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_)));
1282 this->fan_mode = config.fan_mode_;
1283 something_changed = true;
1284 }
1285
1286 if (config.swing_mode_.has_value() && (this->swing_mode != config.swing_mode_.value())) {
1287 ESP_LOGV(TAG, "Setting swing mode to %s", LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_)));
1288 this->swing_mode = *config.swing_mode_;
1289 something_changed = true;
1290 }
1291
1292 return something_changed;
1293}
1294
1295void ThermostatClimate::set_preset_config(std::initializer_list<PresetEntry> presets) {
1296 this->preset_config_ = presets;
1297}
1298
1299void ThermostatClimate::set_custom_preset_config(std::initializer_list<CustomPresetEntry> presets) {
1300 this->custom_preset_config_ = presets;
1301 // Populate Climate base class custom presets vector
1302 std::vector<const char *> names;
1303 names.reserve(presets.size());
1304 for (const auto &entry : this->custom_preset_config_) {
1305 names.push_back(entry.name);
1306 }
1307 this->set_supported_custom_presets(names);
1308}
1309
1311
1313 // Find the preset in custom_preset_config_ and store pointer from there
1314 for (const auto &entry : this->custom_preset_config_) {
1315 if (strcmp(entry.name, custom_preset) == 0) {
1316 this->default_custom_preset_ = entry.name;
1317 return;
1318 }
1319 }
1320 // If not found, it will be caught during validation
1321 this->default_custom_preset_ = nullptr;
1322}
1323
1325
1327 this->on_boot_restore_from_ = on_boot_restore_from;
1328}
1330 this->set_point_minimum_differential_ = differential;
1331}
1332void ThermostatClimate::set_cool_deadband(float deadband) { this->cooling_deadband_ = deadband; }
1333void ThermostatClimate::set_cool_overrun(float overrun) { this->cooling_overrun_ = overrun; }
1334void ThermostatClimate::set_heat_deadband(float deadband) { this->heating_deadband_ = deadband; }
1335void ThermostatClimate::set_heat_overrun(float overrun) { this->heating_overrun_ = overrun; }
1338
1340 uint32_t new_duration_ms = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
1341
1342 if (this->timer_[timer_index].active) {
1343 // Timer is running, calculate elapsed time and adjust if needed
1345 uint32_t elapsed = current_time - this->timer_[timer_index].started;
1346
1347 if (elapsed >= new_duration_ms) {
1348 // Timer should complete immediately (including when new_duration_ms is 0)
1349 ESP_LOGVV(TAG, "timer %d completing immediately (elapsed %" PRIu32 " >= new %" PRIu32 ")", timer_index, elapsed,
1350 new_duration_ms);
1351 this->timer_[timer_index].active = false;
1352 // Trigger the timer callback immediately
1353 this->call_timer_callback_(timer_index);
1354 return;
1355 } else {
1356 // Adjust timer to run for remaining time - keep original start time
1357 ESP_LOGVV(TAG, "timer %d adjusted: elapsed %" PRIu32 ", new total %" PRIu32 ", remaining %" PRIu32, timer_index,
1358 elapsed, new_duration_ms, new_duration_ms - elapsed);
1359 this->timer_[timer_index].time = new_duration_ms;
1360 return;
1361 }
1362 }
1363
1364 // Original logic for non-running timers
1365 this->timer_[timer_index].time = new_duration_ms;
1366}
1367
1400 this->humidity_sensor_ = humidity_sensor;
1401}
1402void ThermostatClimate::set_humidity_hysteresis(float humidity_hysteresis) {
1403 this->humidity_hysteresis_ = std::clamp<float>(humidity_hysteresis, 0.0f, 100.0f);
1404}
1405void ThermostatClimate::set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; }
1406void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) {
1407 this->supports_heat_cool_ = supports_heat_cool;
1408}
1409void ThermostatClimate::set_supports_auto(bool supports_auto) { this->supports_auto_ = supports_auto; }
1410void ThermostatClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
1411void ThermostatClimate::set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; }
1412void ThermostatClimate::set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; }
1414 bool supports_fan_only_action_uses_fan_mode_timer) {
1415 this->supports_fan_only_action_uses_fan_mode_timer_ = supports_fan_only_action_uses_fan_mode_timer;
1416}
1417void ThermostatClimate::set_supports_fan_only_cooling(bool supports_fan_only_cooling) {
1418 this->supports_fan_only_cooling_ = supports_fan_only_cooling;
1419}
1420void ThermostatClimate::set_supports_fan_with_cooling(bool supports_fan_with_cooling) {
1421 this->supports_fan_with_cooling_ = supports_fan_with_cooling;
1422}
1423void ThermostatClimate::set_supports_fan_with_heating(bool supports_fan_with_heating) {
1424 this->supports_fan_with_heating_ = supports_fan_with_heating;
1425}
1426void ThermostatClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
1427void ThermostatClimate::set_supports_fan_mode_on(bool supports_fan_mode_on) {
1428 this->supports_fan_mode_on_ = supports_fan_mode_on;
1429}
1430void ThermostatClimate::set_supports_fan_mode_off(bool supports_fan_mode_off) {
1431 this->supports_fan_mode_off_ = supports_fan_mode_off;
1432}
1433void ThermostatClimate::set_supports_fan_mode_auto(bool supports_fan_mode_auto) {
1434 this->supports_fan_mode_auto_ = supports_fan_mode_auto;
1435}
1436void ThermostatClimate::set_supports_fan_mode_low(bool supports_fan_mode_low) {
1437 this->supports_fan_mode_low_ = supports_fan_mode_low;
1438}
1439void ThermostatClimate::set_supports_fan_mode_medium(bool supports_fan_mode_medium) {
1440 this->supports_fan_mode_medium_ = supports_fan_mode_medium;
1441}
1442void ThermostatClimate::set_supports_fan_mode_high(bool supports_fan_mode_high) {
1443 this->supports_fan_mode_high_ = supports_fan_mode_high;
1444}
1445void ThermostatClimate::set_supports_fan_mode_middle(bool supports_fan_mode_middle) {
1446 this->supports_fan_mode_middle_ = supports_fan_mode_middle;
1447}
1448void ThermostatClimate::set_supports_fan_mode_focus(bool supports_fan_mode_focus) {
1449 this->supports_fan_mode_focus_ = supports_fan_mode_focus;
1450}
1451void ThermostatClimate::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) {
1452 this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse;
1453}
1454void ThermostatClimate::set_supports_fan_mode_quiet(bool supports_fan_mode_quiet) {
1455 this->supports_fan_mode_quiet_ = supports_fan_mode_quiet;
1456}
1457void ThermostatClimate::set_supports_swing_mode_both(bool supports_swing_mode_both) {
1458 this->supports_swing_mode_both_ = supports_swing_mode_both;
1459}
1460void ThermostatClimate::set_supports_swing_mode_off(bool supports_swing_mode_off) {
1461 this->supports_swing_mode_off_ = supports_swing_mode_off;
1462}
1463void ThermostatClimate::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) {
1464 this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal;
1465}
1466void ThermostatClimate::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) {
1467 this->supports_swing_mode_vertical_ = supports_swing_mode_vertical;
1468}
1469void ThermostatClimate::set_supports_two_points(bool supports_two_points) {
1470 this->supports_two_points_ = supports_two_points;
1471}
1472void ThermostatClimate::set_supports_dehumidification(bool supports_dehumidification) {
1473 this->supports_dehumidification_ = supports_dehumidification;
1474 if (supports_dehumidification) {
1475 this->supports_humidification_ = false;
1476 }
1477}
1478void ThermostatClimate::set_supports_humidification(bool supports_humidification) {
1479 this->supports_humidification_ = supports_humidification;
1480 if (supports_humidification) {
1481 this->supports_dehumidification_ = false;
1482 }
1483}
1484
1529
1531 LOG_CLIMATE("", "Thermostat", this);
1532
1533 ESP_LOGCONFIG(TAG,
1534 " On boot, restore from: %s\n"
1535 " Use Start-up Delay: %s",
1536 this->on_boot_restore_from_ == thermostat::DEFAULT_PRESET ? "DEFAULT_PRESET" : "MEMORY",
1537 YESNO(this->use_startup_delay_));
1538 if (this->supports_two_points_) {
1539 ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_);
1540 }
1541 if (this->supports_cool_) {
1542 ESP_LOGCONFIG(TAG,
1543 " Cooling Parameters:\n"
1544 " Deadband: %.1f°C\n"
1545 " Overrun: %.1f°C\n"
1546 " Minimum Off Time: %" PRIu32 "s\n"
1547 " Minimum Run Time: %" PRIu32 "s",
1551 if ((this->supplemental_cool_delta_ > 0) ||
1553 ESP_LOGCONFIG(TAG,
1554 " Maximum Run Time: %" PRIu32 "s\n"
1555 " Supplemental Delta: %.1f°C",
1558 }
1559 }
1560 if (this->supports_heat_) {
1561 ESP_LOGCONFIG(TAG,
1562 " Heating Parameters:\n"
1563 " Deadband: %.1f°C\n"
1564 " Overrun: %.1f°C\n"
1565 " Minimum Off Time: %" PRIu32 "s\n"
1566 " Minimum Run Time: %" PRIu32 "s",
1570 if ((this->supplemental_heat_delta_ > 0) ||
1572 ESP_LOGCONFIG(TAG,
1573 " Maximum Run Time: %" PRIu32 "s\n"
1574 " Supplemental Delta: %.1f°C",
1577 }
1578 }
1579 if (this->supports_fan_only_) {
1580 ESP_LOGCONFIG(TAG,
1581 " Fan Parameters:\n"
1582 " Minimum Off Time: %" PRIu32 "s\n"
1583 " Minimum Run Time: %" PRIu32 "s",
1586 }
1591 ESP_LOGCONFIG(TAG, " Minimum Fan Mode Switching Time: %" PRIu32 "s",
1593 }
1594 ESP_LOGCONFIG(TAG,
1595 " Minimum Idle Time: %" PRIu32 "s\n"
1596 " Supported MODES:\n"
1597 " AUTO: %s\n"
1598 " HEAT/COOL: %s\n"
1599 " HEAT: %s\n"
1600 " COOL: %s\n"
1601 " DRY: %s\n"
1602 " FAN_ONLY: %s\n"
1603 " FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n"
1604 " FAN_ONLY_COOLING: %s",
1605 this->timer_[thermostat::THERMOSTAT_TIMER_IDLE_ON].time / 1000, YESNO(this->supports_auto_),
1606 YESNO(this->supports_heat_cool_), YESNO(this->supports_heat_), YESNO(this->supports_cool_),
1607 YESNO(this->supports_dry_), YESNO(this->supports_fan_only_),
1609 if (this->supports_cool_) {
1610 ESP_LOGCONFIG(TAG, " FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_));
1611 }
1612 if (this->supports_heat_) {
1613 ESP_LOGCONFIG(TAG, " FAN_WITH_HEATING: %s", YESNO(this->supports_fan_with_heating_));
1614 }
1615 ESP_LOGCONFIG(TAG,
1616 " Supported FAN MODES:\n"
1617 " ON: %s\n"
1618 " OFF: %s\n"
1619 " AUTO: %s\n"
1620 " LOW: %s\n"
1621 " MEDIUM: %s\n"
1622 " HIGH: %s\n"
1623 " MIDDLE: %s\n"
1624 " FOCUS: %s\n"
1625 " DIFFUSE: %s\n"
1626 " QUIET: %s\n"
1627 " Supported SWING MODES:\n"
1628 " BOTH: %s\n"
1629 " OFF: %s\n"
1630 " HORIZONTAL: %s\n"
1631 " VERTICAL: %s\n"
1632 " Supports TWO SET POINTS: %s\n"
1633 " Supported Humidity Parameters:\n"
1634 " CURRENT: %s\n"
1635 " TARGET: %s\n"
1636 " DEHUMIDIFICATION: %s\n"
1637 " HUMIDIFICATION: %s",
1638 YESNO(this->supports_fan_mode_on_), YESNO(this->supports_fan_mode_off_),
1639 YESNO(this->supports_fan_mode_auto_), YESNO(this->supports_fan_mode_low_),
1640 YESNO(this->supports_fan_mode_medium_), YESNO(this->supports_fan_mode_high_),
1641 YESNO(this->supports_fan_mode_middle_), YESNO(this->supports_fan_mode_focus_),
1642 YESNO(this->supports_fan_mode_diffuse_), YESNO(this->supports_fan_mode_quiet_),
1643 YESNO(this->supports_swing_mode_both_), YESNO(this->supports_swing_mode_off_),
1645 YESNO(this->supports_two_points_),
1646 YESNO(this->get_traits().has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)),
1648 YESNO(this->supports_dehumidification_), YESNO(this->supports_humidification_));
1649
1650 if (!this->preset_config_.empty()) {
1651 ESP_LOGCONFIG(TAG, " Supported PRESETS:");
1652 for (const auto &entry : this->preset_config_) {
1653 const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(entry.preset));
1654 ESP_LOGCONFIG(TAG, " %s:%s", preset_name, entry.preset == this->default_preset_ ? " (default)" : "");
1655 this->dump_preset_config_(preset_name, entry.config);
1656 }
1657 }
1658
1659 if (!this->custom_preset_config_.empty()) {
1660 ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS:");
1661 for (const auto &entry : this->custom_preset_config_) {
1662 const auto *preset_name = entry.name;
1663 ESP_LOGCONFIG(TAG, " %s:%s", preset_name,
1664 (this->default_custom_preset_ != nullptr && strcmp(entry.name, this->default_custom_preset_) == 0)
1665 ? " (default)"
1666 : "");
1667 this->dump_preset_config_(preset_name, entry.config);
1668 }
1669 }
1670}
1671
1673
1675 : default_temperature(default_temperature) {}
1676
1678 float default_temperature_high)
1679 : default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {}
1680
1681} // 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 stop_action()
Stop any action connected to this trigger.
Definition automation.h:490
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
Definition automation.h:482
This class is used to encode all control actions on a climate device.
Definition climate.h:34
const optional< ClimateSwingMode > & get_swing_mode() const
Definition climate.cpp:314
const optional< float > & get_target_humidity() const
Definition climate.cpp:310
bool has_custom_preset() const
Definition climate.h:122
const optional< ClimateFanMode > & get_fan_mode() const
Definition climate.cpp:313
StringRef get_custom_preset() const
Definition climate.h:120
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
ClimateTraits get_traits()
Get the traits of this climate device with all overrides applied.
Definition climate.cpp:485
float target_temperature
The target temperature of the climate device.
Definition climate.h:274
float current_humidity
The current humidity of the climate device, as reported from the integration.
Definition climate.h:270
ClimateSwingMode swing_mode
The active swing mode of the climate device.
Definition climate.h:299
float target_temperature_low
The minimum target temperature of the climate device, for climate devices with split target temperatu...
Definition climate.h:277
void set_supported_custom_presets(std::initializer_list< const char * > presets)
Set the supported custom presets (stored on Climate, referenced by ClimateTraits).
Definition climate.h:250
bool set_preset_(ClimatePreset preset)
Set preset. Reset custom preset. Return true if preset has been changed.
Definition climate.cpp:695
bool set_custom_preset_(const char *preset)
Set custom preset. Reset primary preset. Return true if preset has been changed.
Definition climate.h:325
bool has_custom_preset() const
Check if a custom preset is currently active.
Definition climate.h:264
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
StringRef get_custom_preset() const
Get the active custom preset (read-only access). Returns StringRef.
Definition climate.h:305
void publish_state()
Publish the state of the climate device, to be called from integrations.
Definition climate.cpp:436
optional< ClimatePreset > preset
The active preset of the climate device.
Definition climate.h:290
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:284
float target_temperature_high
The maximum target temperature of the climate device, for climate devices with split target temperatu...
Definition climate.h:279
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 add_supported_swing_mode(ClimateSwingMode mode)
Base-class for all sensors.
Definition sensor.h:47
void add_on_state_callback(F &&callback)
Add a callback that will be called every time a filtered value arrives.
Definition sensor.h:119
float state
This member variable stores the last state that has passed through all filters.
Definition sensor.h:138
void set_supports_humidification(bool supports_humidification)
void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal)
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")
float cooling_deadband_
Hysteresis values used for computing climate actions.
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.
bool use_startup_delay_
Used to start "off" delay timers at boot.
Trigger temperature_change_trigger_
Trigger for target temperature changes.
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.
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)
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)
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.
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)
Trigger cool_action_trigger_
Trigger for cooling action/mode.
float prev_target_humidity_
Store previously-known humidity and temperatures.
void check_humidity_change_trigger_()
Check if the humidity change trigger should be called.
void switch_to_supplemental_action_(climate::ClimateAction action)
void switch_to_mode_(climate::ClimateMode mode, bool publish_state=true)
Switch the climate device to the given climate mode.
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.
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)
Trigger humidity_change_trigger_
Trigger for target humidity changes.
void set_supports_fan_mode_middle(bool supports_fan_mode_middle)
void set_supports_fan_mode_low(bool supports_fan_mode_low)
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)
Trigger idle_action_trigger_
Trigger for idle action/off mode.
uint32_t timer_duration_(ThermostatClimateTimerIndex timer_index)
Trigger humidity_control_dehumidify_action_trigger_
Humidity control triggers.
Trigger fan_only_action_trigger_
Trigger for fan-only action/mode.
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.
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.
Trigger swing_mode_both_trigger_
Swing mode triggers.
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 heat_cool_mode_trigger_
Trigger for heat/cool mode.
void set_supports_fan_with_cooling(bool supports_fan_with_cooling)
void set_supports_fan_mode_focus(bool supports_fan_mode_focus)
Trigger preset_change_trigger_
Trigger for preset mode changes.
void check_temperature_change_trigger_()
Check if the temperature change trigger should be called.
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.
Trigger fan_mode_on_trigger_
Fan mode triggers.
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)
Trigger auto_mode_trigger_
Trigger for auto mode.
bool idle_action_ready_()
Is the action ready to be called? Returns true if so.
Trigger dry_action_trigger_
Trigger for dry (dehumidification) mode.
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 heat_action_trigger_
Trigger for heating action/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:2
@ 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.
std::string size_t len
Definition helpers.h:1045
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:26
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
optional< climate::ClimateSwingMode > swing_mode_
uint16_t temperature
Definition sun_gtil2.cpp:12