ESPHome 2026.10.2
Loading...
Searching...
No Matches
pid_climate.cpp
Go to the documentation of this file.
1#include "pid_climate.h"
2#include "esphome/core/log.h"
3
4namespace esphome::pid {
5
6static const char *const TAG = "pid.climate";
7
8bool PIDClimate::set_deadband_thresholds(float threshold_low, float threshold_high) {
9 if (threshold_low > threshold_high) {
10 ESP_LOGW(TAG, "Deadband threshold low %.2f must not be greater than high %.2f", threshold_low, threshold_high);
11 return false;
12 }
13
14 this->set_threshold_low(threshold_low);
15 this->set_threshold_high(threshold_high);
16 return true;
17}
18
20 this->sensor_->add_on_state_callback([this](float state) {
21 // only publish if state/current temperature has changed in two digits of precision
22 this->do_publish_ = roundf(state * 100) != roundf(this->current_temperature * 100);
24 this->update_pid_();
25 });
26 this->current_temperature = this->sensor_->state;
27
28 // register for humidity values and get initial state
29 if (this->humidity_sensor_ != nullptr) {
30 this->humidity_sensor_->add_on_state_callback([this](float state) {
31 this->current_humidity = state;
32 this->publish_state();
33 });
35 }
36
37 // restore set points
38 auto restore = this->restore_state_();
39 if (restore.has_value()) {
40 restore->to_call(this).perform();
41 } else {
42 // restore from defaults, change_away handles those for us
43 if (supports_heat_() && supports_cool_()) {
45 } else if (supports_cool_()) {
47 } else if (supports_heat_()) {
49 }
51 }
52}
54 auto call_mode = call.get_mode();
55 if (call_mode.has_value())
56 this->mode = *call_mode;
57 auto call_target = call.get_target_temperature();
58 if (call_target.has_value())
59 this->target_temperature = *call_target;
60
61 // If switching to off mode, set output immediately
63 this->write_output_(0.0f);
64
65 this->publish_state();
66}
85 LOG_CLIMATE("", "PID Climate", this);
86 ESP_LOGCONFIG(TAG,
87 " Control Parameters:\n"
88 " kp: %.5f, ki: %.5f, kd: %.5f, output samples: %d",
89 controller_.kp_, controller_.ki_, controller_.kd_, controller_.output_samples_);
90
91 if (controller_.threshold_low_ == 0 && controller_.threshold_high_ == 0) {
92 ESP_LOGCONFIG(TAG, " Deadband disabled.");
93 } else {
94 ESP_LOGCONFIG(TAG,
95 " Deadband Parameters:\n"
96 " threshold: %0.5f to %0.5f, multipliers(kp: %.5f, ki: %.5f, kd: %.5f), "
97 "output samples: %d",
98 controller_.threshold_low_, controller_.threshold_high_, controller_.kp_multiplier_,
99 controller_.ki_multiplier_, controller_.kd_multiplier_, controller_.deadband_output_samples_);
100 }
101
102 if (this->autotuner_ != nullptr) {
103 this->autotuner_->dump_config();
104 }
105}
106void PIDClimate::write_output_(float value) {
107 this->output_value_ = value;
108
109 // first ensure outputs are off (both outputs not active at the same time)
110 if (this->supports_cool_() && value >= 0)
111 this->cool_output_->set_level(0.0f);
112 if (this->supports_heat_() && value <= 0)
113 this->heat_output_->set_level(0.0f);
114
115 // value < 0 means cool, > 0 means heat
116 if (this->supports_cool_() && value < 0)
117 this->cool_output_->set_level(std::min(1.0f, -value));
118 if (this->supports_heat_() && value > 0)
119 this->heat_output_->set_level(std::min(1.0f, value));
120
121 // Update action variable for user feedback what's happening
122 climate::ClimateAction new_action;
123 if (this->supports_cool_() && value < 0) {
125 } else if (this->supports_heat_() && value > 0) {
127 } else if (this->mode == climate::CLIMATE_MODE_OFF) {
128 new_action = climate::CLIMATE_ACTION_OFF;
129 } else {
130 new_action = climate::CLIMATE_ACTION_IDLE;
131 }
132
133 if (new_action != this->action) {
134 this->action = new_action;
135 this->do_publish_ = true;
136 }
137 this->pid_computed_callback_.call();
138}
140 float value;
141 if (std::isnan(this->current_temperature) || std::isnan(this->target_temperature)) {
142 // if any control parameters are nan, turn off all outputs
143 value = 0.0;
144 } else {
145 // Update PID controller irrespective of current mode, to not mess up D/I terms
146 // In non-auto mode, we just discard the output value
147 value = this->controller_.update(this->target_temperature, this->current_temperature);
148
149 // Check autotuner
150 if (this->autotuner_ != nullptr && !this->autotuner_->is_finished()) {
151 auto res = this->autotuner_->update(this->target_temperature, this->current_temperature);
152 if (res.result_params.has_value()) {
153 this->controller_.kp_ = res.result_params->kp;
154 this->controller_.ki_ = res.result_params->ki;
155 this->controller_.kd_ = res.result_params->kd;
156 // keep autotuner instance so that subsequent dump_configs will print the long result message.
157 } else {
158 value = res.output;
159 }
160 }
161 }
162
163 if (this->mode == climate::CLIMATE_MODE_OFF) {
164 this->write_output_(0.0);
165 } else {
166 this->write_output_(value);
167 }
168
169 if (this->do_publish_)
170 this->publish_state();
171}
172void PIDClimate::start_autotune(float noiseband, float positive_output, float negative_output) {
173 auto tuner = make_unique<PIDAutotuner>();
174 tuner->set_noiseband(noiseband);
175 tuner->set_output_positive(positive_output);
176 tuner->set_output_negative(negative_output);
177 this->start_autotune(std::move(tuner));
178}
179
180void PIDClimate::start_autotune(std::unique_ptr<PIDAutotuner> &&autotune) {
181 this->autotuner_ = std::move(autotune);
182 float min_value = this->supports_cool_() ? -1.0f : 0.0f;
183 float max_value = this->supports_heat_() ? 1.0f : 0.0f;
184 this->autotuner_->config(min_value, max_value);
185 this->autotuner_->set_autotuner_id(this->get_name());
186
187 ESP_LOGI(TAG,
188 "%s: Autotune has started. This can take a long time depending on the "
189 "responsiveness of your system. Your system "
190 "output will be altered to deliberately oscillate above and below the setpoint multiple times. "
191 "Until your sensor provides a reading, the autotuner may display \'nan\'",
192 this->get_name().c_str());
193
194 this->set_interval("autotune-progress", 10000, [this]() {
195 if (this->autotuner_ != nullptr && !this->autotuner_->is_finished())
196 this->autotuner_->dump_config();
197 });
198
200 ESP_LOGW(TAG, "%s: !!! For PID autotuner you need to set AUTO (also called heat/cool) mode!",
201 this->get_name().c_str());
202 }
203}
204
206
207} // namespace esphome::pid
void set_interval(const char *name, uint32_t interval, std::function< void()> &&f)
Set an interval function with a const char* name.
Definition component.cpp:88
const StringRef & get_name() const
Definition entity_base.h:71
This class is used to encode all control actions on a climate device.
Definition climate.h:34
ClimateMode mode
The active mode of the climate device.
Definition climate.h:312
float target_temperature
The target temperature of the climate device.
Definition climate.h:293
float current_humidity
The current humidity of the climate device, as reported from the integration.
Definition climate.h:289
float current_temperature
The current temperature of the climate device, as reported from the integration.
Definition climate.h:286
ClimateAction action
The active state of the climate device.
Definition climate.h:315
void publish_state()
Publish the state of the climate device, to be called from integrations.
Definition climate.cpp:429
optional< ClimateDeviceRestoreState > restore_state_()
Restore the state of the climate device, call this from your setup() method.
Definition climate.cpp:362
void add_feature_flags(uint32_t feature_flags)
void set_supported_modes(ClimateModeMask modes)
void add_supported_mode(ClimateMode mode)
void set_level(float state)
Set the level of this float output, this is called from the front-end.
void start_autotune(std::unique_ptr< PIDAutotuner > &&autotune)
PIDController controller_
output::FloatOutput * cool_output_
sensor::Sensor * sensor_
The sensor used for getting the current temperature.
void set_threshold_high(float in)
Definition pid_climate.h:38
void dump_config() override
bool supports_cool_() const
Definition pid_climate.h:94
void write_output_(float value)
std::unique_ptr< PIDAutotuner > autotuner_
void control(const climate::ClimateCall &call) override
Override control to change settings of the climate device.
CallbackManager< void()> pid_computed_callback_
float output_value_
Output value as reported by the PID controller, for PIDClimateSensor.
bool set_deadband_thresholds(float threshold_low, float threshold_high)
sensor::Sensor * humidity_sensor_
The sensor used for getting the current humidity.
climate::ClimateTraits traits() override
Return the traits of this controller.
bool supports_heat_() const
Definition pid_climate.h:95
void set_threshold_low(float in)
Definition pid_climate.h:37
output::FloatOutput * heat_output_
void add_on_state_callback(F &&callback)
Add a callback that will be called every time a filtered value arrives.
Definition sensor.h:121
float state
This member variable stores the last state that has passed through all filters.
Definition sensor.h:140
bool state
Definition fan.h:2
@ CLIMATE_SUPPORTS_CURRENT_HUMIDITY
@ CLIMATE_SUPPORTS_CURRENT_TEMPERATURE
@ CLIMATE_MODE_HEAT
The climate device is set to heat to reach the target temperature.
@ CLIMATE_MODE_COOL
The climate device is set to cool to reach the target temperature.
@ CLIMATE_MODE_HEAT_COOL
The climate device is set to heat/cool to reach the target temperature.
@ CLIMATE_MODE_OFF
The climate device is off.
ClimateAction
Enum for the current action of the climate device. Values match those of ClimateMode.
@ CLIMATE_ACTION_OFF
The climate device is off (inactive or no power)
@ CLIMATE_ACTION_IDLE
The climate device is idle (monitoring climate but no action needed)
@ CLIMATE_ACTION_HEATING
The climate device is actively heating.
@ CLIMATE_ACTION_COOLING
The climate device is actively cooling.
float update(float setpoint, float process_value)