ESPHome  2024.11.1
mqtt_climate.cpp
Go to the documentation of this file.
1 #include "mqtt_climate.h"
2 #include "esphome/core/log.h"
3 
4 #include "mqtt_const.h"
5 
6 #ifdef USE_MQTT
7 #ifdef USE_CLIMATE
8 
9 namespace esphome {
10 namespace mqtt {
11 
12 static const char *const TAG = "mqtt.climate";
13 
14 using namespace esphome::climate;
15 
17  auto traits = this->device_->get_traits();
18  // current_temperature_topic
19  if (traits.get_supports_current_temperature()) {
20  root[MQTT_CURRENT_TEMPERATURE_TOPIC] = this->get_current_temperature_state_topic();
21  }
22  // current_humidity_topic
23  if (traits.get_supports_current_humidity()) {
24  root[MQTT_CURRENT_HUMIDITY_TOPIC] = this->get_current_humidity_state_topic();
25  }
26  // mode_command_topic
27  root[MQTT_MODE_COMMAND_TOPIC] = this->get_mode_command_topic();
28  // mode_state_topic
29  root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic();
30  // modes
31  JsonArray modes = root.createNestedArray(MQTT_MODES);
32  // sort array for nice UI in HA
33  if (traits.supports_mode(CLIMATE_MODE_AUTO))
34  modes.add("auto");
35  modes.add("off");
36  if (traits.supports_mode(CLIMATE_MODE_COOL))
37  modes.add("cool");
38  if (traits.supports_mode(CLIMATE_MODE_HEAT))
39  modes.add("heat");
40  if (traits.supports_mode(CLIMATE_MODE_FAN_ONLY))
41  modes.add("fan_only");
42  if (traits.supports_mode(CLIMATE_MODE_DRY))
43  modes.add("dry");
44  if (traits.supports_mode(CLIMATE_MODE_HEAT_COOL))
45  modes.add("heat_cool");
46 
47  if (traits.get_supports_two_point_target_temperature()) {
48  // temperature_low_command_topic
49  root[MQTT_TEMPERATURE_LOW_COMMAND_TOPIC] = this->get_target_temperature_low_command_topic();
50  // temperature_low_state_topic
51  root[MQTT_TEMPERATURE_LOW_STATE_TOPIC] = this->get_target_temperature_low_state_topic();
52  // temperature_high_command_topic
53  root[MQTT_TEMPERATURE_HIGH_COMMAND_TOPIC] = this->get_target_temperature_high_command_topic();
54  // temperature_high_state_topic
55  root[MQTT_TEMPERATURE_HIGH_STATE_TOPIC] = this->get_target_temperature_high_state_topic();
56  } else {
57  // temperature_command_topic
58  root[MQTT_TEMPERATURE_COMMAND_TOPIC] = this->get_target_temperature_command_topic();
59  // temperature_state_topic
60  root[MQTT_TEMPERATURE_STATE_TOPIC] = this->get_target_temperature_state_topic();
61  }
62 
63  if (traits.get_supports_target_humidity()) {
64  // target_humidity_command_topic
65  root[MQTT_TARGET_HUMIDITY_COMMAND_TOPIC] = this->get_target_humidity_command_topic();
66  // target_humidity_state_topic
67  root[MQTT_TARGET_HUMIDITY_STATE_TOPIC] = this->get_target_humidity_state_topic();
68  }
69 
70  // min_temp
71  root[MQTT_MIN_TEMP] = traits.get_visual_min_temperature();
72  // max_temp
73  root[MQTT_MAX_TEMP] = traits.get_visual_max_temperature();
74  // target_temp_step
75  root[MQTT_TARGET_TEMPERATURE_STEP] = traits.get_visual_target_temperature_step();
76  // current_temp_step
77  root[MQTT_CURRENT_TEMPERATURE_STEP] = traits.get_visual_current_temperature_step();
78  // temperature units are always coerced to Celsius internally
79  root[MQTT_TEMPERATURE_UNIT] = "C";
80 
81  // min_humidity
82  root[MQTT_MIN_HUMIDITY] = traits.get_visual_min_humidity();
83  // max_humidity
84  root[MQTT_MAX_HUMIDITY] = traits.get_visual_max_humidity();
85 
86  if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
87  // preset_mode_command_topic
88  root[MQTT_PRESET_MODE_COMMAND_TOPIC] = this->get_preset_command_topic();
89  // preset_mode_state_topic
90  root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic();
91  // presets
92  JsonArray presets = root.createNestedArray("preset_modes");
93  if (traits.supports_preset(CLIMATE_PRESET_HOME))
94  presets.add("home");
95  if (traits.supports_preset(CLIMATE_PRESET_AWAY))
96  presets.add("away");
97  if (traits.supports_preset(CLIMATE_PRESET_BOOST))
98  presets.add("boost");
99  if (traits.supports_preset(CLIMATE_PRESET_COMFORT))
100  presets.add("comfort");
101  if (traits.supports_preset(CLIMATE_PRESET_ECO))
102  presets.add("eco");
103  if (traits.supports_preset(CLIMATE_PRESET_SLEEP))
104  presets.add("sleep");
105  if (traits.supports_preset(CLIMATE_PRESET_ACTIVITY))
106  presets.add("activity");
107  for (const auto &preset : traits.get_supported_custom_presets())
108  presets.add(preset);
109  }
110 
111  if (traits.get_supports_action()) {
112  // action_topic
113  root[MQTT_ACTION_TOPIC] = this->get_action_state_topic();
114  }
115 
116  if (traits.get_supports_fan_modes()) {
117  // fan_mode_command_topic
118  root[MQTT_FAN_MODE_COMMAND_TOPIC] = this->get_fan_mode_command_topic();
119  // fan_mode_state_topic
120  root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic();
121  // fan_modes
122  JsonArray fan_modes = root.createNestedArray("fan_modes");
123  if (traits.supports_fan_mode(CLIMATE_FAN_ON))
124  fan_modes.add("on");
125  if (traits.supports_fan_mode(CLIMATE_FAN_OFF))
126  fan_modes.add("off");
127  if (traits.supports_fan_mode(CLIMATE_FAN_AUTO))
128  fan_modes.add("auto");
129  if (traits.supports_fan_mode(CLIMATE_FAN_LOW))
130  fan_modes.add("low");
131  if (traits.supports_fan_mode(CLIMATE_FAN_MEDIUM))
132  fan_modes.add("medium");
133  if (traits.supports_fan_mode(CLIMATE_FAN_HIGH))
134  fan_modes.add("high");
135  if (traits.supports_fan_mode(CLIMATE_FAN_MIDDLE))
136  fan_modes.add("middle");
137  if (traits.supports_fan_mode(CLIMATE_FAN_FOCUS))
138  fan_modes.add("focus");
139  if (traits.supports_fan_mode(CLIMATE_FAN_DIFFUSE))
140  fan_modes.add("diffuse");
141  if (traits.supports_fan_mode(CLIMATE_FAN_QUIET))
142  fan_modes.add("quiet");
143  for (const auto &fan_mode : traits.get_supported_custom_fan_modes())
144  fan_modes.add(fan_mode);
145  }
146 
147  if (traits.get_supports_swing_modes()) {
148  // swing_mode_command_topic
149  root[MQTT_SWING_MODE_COMMAND_TOPIC] = this->get_swing_mode_command_topic();
150  // swing_mode_state_topic
151  root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic();
152  // swing_modes
153  JsonArray swing_modes = root.createNestedArray("swing_modes");
154  if (traits.supports_swing_mode(CLIMATE_SWING_OFF))
155  swing_modes.add("off");
156  if (traits.supports_swing_mode(CLIMATE_SWING_BOTH))
157  swing_modes.add("both");
158  if (traits.supports_swing_mode(CLIMATE_SWING_VERTICAL))
159  swing_modes.add("vertical");
160  if (traits.supports_swing_mode(CLIMATE_SWING_HORIZONTAL))
161  swing_modes.add("horizontal");
162  }
163 
164  config.state_topic = false;
165  config.command_topic = false;
166 }
168  auto traits = this->device_->get_traits();
169  this->subscribe(this->get_mode_command_topic(), [this](const std::string &topic, const std::string &payload) {
170  auto call = this->device_->make_call();
171  call.set_mode(payload);
172  call.perform();
173  });
174 
175  if (traits.get_supports_two_point_target_temperature()) {
176  this->subscribe(this->get_target_temperature_low_command_topic(),
177  [this](const std::string &topic, const std::string &payload) {
178  auto val = parse_number<float>(payload);
179  if (!val.has_value()) {
180  ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
181  return;
182  }
183  auto call = this->device_->make_call();
184  call.set_target_temperature_low(*val);
185  call.perform();
186  });
187  this->subscribe(this->get_target_temperature_high_command_topic(),
188  [this](const std::string &topic, const std::string &payload) {
189  auto val = parse_number<float>(payload);
190  if (!val.has_value()) {
191  ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
192  return;
193  }
194  auto call = this->device_->make_call();
195  call.set_target_temperature_high(*val);
196  call.perform();
197  });
198  } else {
199  this->subscribe(this->get_target_temperature_command_topic(),
200  [this](const std::string &topic, const std::string &payload) {
201  auto val = parse_number<float>(payload);
202  if (!val.has_value()) {
203  ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
204  return;
205  }
206  auto call = this->device_->make_call();
207  call.set_target_temperature(*val);
208  call.perform();
209  });
210  }
211 
212  if (traits.get_supports_target_humidity()) {
213  this->subscribe(this->get_target_humidity_command_topic(),
214  [this](const std::string &topic, const std::string &payload) {
215  auto val = parse_number<float>(payload);
216  if (!val.has_value()) {
217  ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
218  return;
219  }
220  auto call = this->device_->make_call();
221  call.set_target_humidity(*val);
222  call.perform();
223  });
224  }
225 
226  if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
227  this->subscribe(this->get_preset_command_topic(), [this](const std::string &topic, const std::string &payload) {
228  auto call = this->device_->make_call();
229  call.set_preset(payload);
230  call.perform();
231  });
232  }
233 
234  if (traits.get_supports_fan_modes()) {
235  this->subscribe(this->get_fan_mode_command_topic(), [this](const std::string &topic, const std::string &payload) {
236  auto call = this->device_->make_call();
237  call.set_fan_mode(payload);
238  call.perform();
239  });
240  }
241 
242  if (traits.get_supports_swing_modes()) {
243  this->subscribe(this->get_swing_mode_command_topic(), [this](const std::string &topic, const std::string &payload) {
244  auto call = this->device_->make_call();
245  call.set_swing_mode(payload);
246  call.perform();
247  });
248  }
249 
250  this->device_->add_on_state_callback([this](Climate & /*unused*/) { this->publish_state_(); });
251 }
254 std::string MQTTClimateComponent::component_type() const { return "climate"; }
255 const EntityBase *MQTTClimateComponent::get_entity() const { return this->device_; }
256 
258  auto traits = this->device_->get_traits();
259  // mode
260  const char *mode_s = "";
261  switch (this->device_->mode) {
262  case CLIMATE_MODE_OFF:
263  mode_s = "off";
264  break;
265  case CLIMATE_MODE_AUTO:
266  mode_s = "auto";
267  break;
268  case CLIMATE_MODE_COOL:
269  mode_s = "cool";
270  break;
271  case CLIMATE_MODE_HEAT:
272  mode_s = "heat";
273  break;
275  mode_s = "fan_only";
276  break;
277  case CLIMATE_MODE_DRY:
278  mode_s = "dry";
279  break;
281  mode_s = "heat_cool";
282  break;
283  }
284  bool success = true;
285  if (!this->publish(this->get_mode_state_topic(), mode_s))
286  success = false;
287  int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
288  int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
289  if (traits.get_supports_current_temperature() && !std::isnan(this->device_->current_temperature)) {
290  std::string payload = value_accuracy_to_string(this->device_->current_temperature, current_accuracy);
291  if (!this->publish(this->get_current_temperature_state_topic(), payload))
292  success = false;
293  }
294  if (traits.get_supports_two_point_target_temperature()) {
295  std::string payload = value_accuracy_to_string(this->device_->target_temperature_low, target_accuracy);
296  if (!this->publish(this->get_target_temperature_low_state_topic(), payload))
297  success = false;
298  payload = value_accuracy_to_string(this->device_->target_temperature_high, target_accuracy);
299  if (!this->publish(this->get_target_temperature_high_state_topic(), payload))
300  success = false;
301  } else {
302  std::string payload = value_accuracy_to_string(this->device_->target_temperature, target_accuracy);
303  if (!this->publish(this->get_target_temperature_state_topic(), payload))
304  success = false;
305  }
306 
307  if (traits.get_supports_current_humidity() && !std::isnan(this->device_->current_humidity)) {
308  std::string payload = value_accuracy_to_string(this->device_->current_humidity, 0);
309  if (!this->publish(this->get_current_humidity_state_topic(), payload))
310  success = false;
311  }
312  if (traits.get_supports_target_humidity() && !std::isnan(this->device_->target_humidity)) {
313  std::string payload = value_accuracy_to_string(this->device_->target_humidity, 0);
314  if (!this->publish(this->get_target_humidity_state_topic(), payload))
315  success = false;
316  }
317 
318  if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
319  std::string payload;
320  if (this->device_->preset.has_value()) {
321  switch (this->device_->preset.value()) {
322  case CLIMATE_PRESET_NONE:
323  payload = "none";
324  break;
325  case CLIMATE_PRESET_HOME:
326  payload = "home";
327  break;
328  case CLIMATE_PRESET_AWAY:
329  payload = "away";
330  break;
332  payload = "boost";
333  break;
335  payload = "comfort";
336  break;
337  case CLIMATE_PRESET_ECO:
338  payload = "eco";
339  break;
341  payload = "sleep";
342  break;
344  payload = "activity";
345  break;
346  }
347  }
348  if (this->device_->custom_preset.has_value())
349  payload = this->device_->custom_preset.value();
350  if (!this->publish(this->get_preset_state_topic(), payload))
351  success = false;
352  }
353 
354  if (traits.get_supports_action()) {
355  const char *payload = "unknown";
356  switch (this->device_->action) {
357  case CLIMATE_ACTION_OFF:
358  payload = "off";
359  break;
361  payload = "cooling";
362  break;
364  payload = "heating";
365  break;
366  case CLIMATE_ACTION_IDLE:
367  payload = "idle";
368  break;
370  payload = "drying";
371  break;
372  case CLIMATE_ACTION_FAN:
373  payload = "fan";
374  break;
375  }
376  if (!this->publish(this->get_action_state_topic(), payload))
377  success = false;
378  }
379 
380  if (traits.get_supports_fan_modes()) {
381  std::string payload;
382  if (this->device_->fan_mode.has_value()) {
383  switch (this->device_->fan_mode.value()) {
384  case CLIMATE_FAN_ON:
385  payload = "on";
386  break;
387  case CLIMATE_FAN_OFF:
388  payload = "off";
389  break;
390  case CLIMATE_FAN_AUTO:
391  payload = "auto";
392  break;
393  case CLIMATE_FAN_LOW:
394  payload = "low";
395  break;
396  case CLIMATE_FAN_MEDIUM:
397  payload = "medium";
398  break;
399  case CLIMATE_FAN_HIGH:
400  payload = "high";
401  break;
402  case CLIMATE_FAN_MIDDLE:
403  payload = "middle";
404  break;
405  case CLIMATE_FAN_FOCUS:
406  payload = "focus";
407  break;
408  case CLIMATE_FAN_DIFFUSE:
409  payload = "diffuse";
410  break;
411  case CLIMATE_FAN_QUIET:
412  payload = "quiet";
413  break;
414  }
415  }
416  if (this->device_->custom_fan_mode.has_value())
417  payload = this->device_->custom_fan_mode.value();
418  if (!this->publish(this->get_fan_mode_state_topic(), payload))
419  success = false;
420  }
421 
422  if (traits.get_supports_swing_modes()) {
423  const char *payload = "";
424  switch (this->device_->swing_mode) {
425  case CLIMATE_SWING_OFF:
426  payload = "off";
427  break;
428  case CLIMATE_SWING_BOTH:
429  payload = "both";
430  break;
432  payload = "vertical";
433  break;
435  payload = "horizontal";
436  break;
437  }
438  if (!this->publish(this->get_swing_mode_state_topic(), payload))
439  success = false;
440  }
441 
442  return success;
443 }
444 
445 } // namespace mqtt
446 } // namespace esphome
447 
448 #endif
449 #endif // USE_MQTT
constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC
Definition: mqtt_const.h:56
value_type const & value() const
Definition: optional.h:89
constexpr const char *const MQTT_MIN_TEMP
Definition: mqtt_const.h:113
float current_humidity
The current humidity of the climate device, as reported from the integration.
Definition: climate.h:182
ClimateSwingMode swing_mode
The active swing mode of the climate device.
Definition: climate.h:202
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals)
Create a string from a value and an accuracy in decimals.
Definition: helpers.cpp:415
constexpr const char *const MQTT_FAN_MODE_COMMAND_TOPIC
Definition: mqtt_const.h:82
constexpr const char *const MQTT_SWING_MODE_COMMAND_TOPIC
Definition: mqtt_const.h:229
constexpr const char *const MQTT_FAN_MODE_STATE_TOPIC
Definition: mqtt_const.h:84
float target_temperature
The target temperature of the climate device.
Definition: climate.h:186
constexpr const char *const MQTT_CURRENT_HUMIDITY_TOPIC
Definition: mqtt_const.h:53
constexpr const char *const MQTT_ACTION_TOPIC
Definition: mqtt_const.h:13
constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TOPIC
Definition: mqtt_const.h:235
bool state_topic
If the state topic should be included. Defaults to true.
constexpr const char *const MQTT_TEMPERATURE_COMMAND_TOPIC
Definition: mqtt_const.h:238
constexpr const char *const MQTT_TEMPERATURE_STATE_TOPIC
Definition: mqtt_const.h:248
ClimateMode mode
The active mode of the climate device.
Definition: climate.h:173
float target_temperature_high
The maximum target temperature of the climate device, for climate devices with split target temperatu...
Definition: climate.h:191
float current_temperature
The current temperature of the climate device, as reported from the integration.
Definition: climate.h:179
mopeka_std_values val[4]
void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override
constexpr const char *const MQTT_MAX_TEMP
Definition: mqtt_const.h:109
constexpr const char *const MQTT_MODE_STATE_TOPIC
Definition: mqtt_const.h:118
bool has_value() const
Definition: optional.h:87
constexpr const char *const MQTT_PRESET_MODE_COMMAND_TOPIC
Definition: mqtt_const.h:180
float target_humidity
The target humidity of the climate device.
Definition: climate.h:196
MQTTClimateComponent(climate::Climate *device)
bool command_topic
If the command topic should be included. Default to true.
bool publish(const std::string &topic, const std::string &payload)
Send a MQTT message.
optional< std::string > custom_fan_mode
The active custom fan mode of the climate device.
Definition: climate.h:205
constexpr const char *const MQTT_TEMPERATURE_LOW_COMMAND_TOPIC
Definition: mqtt_const.h:244
constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TOPIC
Definition: mqtt_const.h:240
constexpr const char *const MQTT_TEMPERATURE_UNIT
Definition: mqtt_const.h:249
constexpr const char *const MQTT_CURRENT_TEMPERATURE_STEP
Definition: mqtt_const.h:54
optional< ClimatePreset > preset
The active preset of the climate device.
Definition: climate.h:208
virtual const EntityBase * get_entity() const =0
Gets the Entity served by this MQTT component.
state command command command command command command state state state MQTT_COMPONENT_CUSTOM_TOPIC(preset, command) protected bool publish_state_()
constexpr const char *const MQTT_MIN_HUMIDITY
Definition: mqtt_const.h:111
Simple Helper struct used for Home Assistant MQTT send_discovery().
constexpr const char *const MQTT_MAX_HUMIDITY
Definition: mqtt_const.h:107
ClimateTraits get_traits()
Get the traits of this climate device with all overrides applied.
Definition: climate.cpp:440
ClimateFanMode fan_mode
Definition: climate.h:573
optional< std::string > custom_preset
The active custom preset mode of the climate device.
Definition: climate.h:211
optional< ClimateFanMode > fan_mode
The active fan mode of the climate device.
Definition: climate.h:199
constexpr const char *const MQTT_MODES
Definition: mqtt_const.h:119
constexpr const char *const MQTT_SWING_MODE_STATE_TOPIC
Definition: mqtt_const.h:231
constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TOPIC
Definition: mqtt_const.h:233
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
constexpr const char *const MQTT_PRESET_MODE_STATE_TOPIC
Definition: mqtt_const.h:181
constexpr const char *const MQTT_TARGET_TEMPERATURE_STEP
Definition: mqtt_const.h:236
constexpr const char *const MQTT_TEMPERATURE_HIGH_STATE_TOPIC
Definition: mqtt_const.h:242
std::string component_type() const override
constexpr const char *const MQTT_MODE_COMMAND_TOPIC
Definition: mqtt_const.h:116
float target_temperature_low
The minimum target temperature of the climate device, for climate devices with split target temperatu...
Definition: climate.h:189
constexpr const char *const MQTT_TEMPERATURE_LOW_STATE_TOPIC
Definition: mqtt_const.h:246
ClimatePreset preset
Definition: climate.h:578
ClimateAction action
The active state of the climate device.
Definition: climate.h:176
ClimateDevice - This is the base class for all climate integrations.
Definition: climate.h:168