ESPHome  2024.4.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  // temp_step
75  root["temp_step"] = traits.get_visual_target_temperature_step();
76  // temperature units are always coerced to Celsius internally
77  root[MQTT_TEMPERATURE_UNIT] = "C";
78 
79  // min_humidity
80  root[MQTT_MIN_HUMIDITY] = traits.get_visual_min_humidity();
81  // max_humidity
82  root[MQTT_MAX_HUMIDITY] = traits.get_visual_max_humidity();
83 
84  if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
85  // preset_mode_command_topic
86  root[MQTT_PRESET_MODE_COMMAND_TOPIC] = this->get_preset_command_topic();
87  // preset_mode_state_topic
88  root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic();
89  // presets
90  JsonArray presets = root.createNestedArray("preset_modes");
91  if (traits.supports_preset(CLIMATE_PRESET_HOME))
92  presets.add("home");
93  if (traits.supports_preset(CLIMATE_PRESET_AWAY))
94  presets.add("away");
95  if (traits.supports_preset(CLIMATE_PRESET_BOOST))
96  presets.add("boost");
97  if (traits.supports_preset(CLIMATE_PRESET_COMFORT))
98  presets.add("comfort");
99  if (traits.supports_preset(CLIMATE_PRESET_ECO))
100  presets.add("eco");
101  if (traits.supports_preset(CLIMATE_PRESET_SLEEP))
102  presets.add("sleep");
103  if (traits.supports_preset(CLIMATE_PRESET_ACTIVITY))
104  presets.add("activity");
105  for (const auto &preset : traits.get_supported_custom_presets())
106  presets.add(preset);
107  }
108 
109  if (traits.get_supports_action()) {
110  // action_topic
111  root[MQTT_ACTION_TOPIC] = this->get_action_state_topic();
112  }
113 
114  if (traits.get_supports_fan_modes()) {
115  // fan_mode_command_topic
116  root[MQTT_FAN_MODE_COMMAND_TOPIC] = this->get_fan_mode_command_topic();
117  // fan_mode_state_topic
118  root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic();
119  // fan_modes
120  JsonArray fan_modes = root.createNestedArray("fan_modes");
121  if (traits.supports_fan_mode(CLIMATE_FAN_ON))
122  fan_modes.add("on");
123  if (traits.supports_fan_mode(CLIMATE_FAN_OFF))
124  fan_modes.add("off");
125  if (traits.supports_fan_mode(CLIMATE_FAN_AUTO))
126  fan_modes.add("auto");
127  if (traits.supports_fan_mode(CLIMATE_FAN_LOW))
128  fan_modes.add("low");
129  if (traits.supports_fan_mode(CLIMATE_FAN_MEDIUM))
130  fan_modes.add("medium");
131  if (traits.supports_fan_mode(CLIMATE_FAN_HIGH))
132  fan_modes.add("high");
133  if (traits.supports_fan_mode(CLIMATE_FAN_MIDDLE))
134  fan_modes.add("middle");
135  if (traits.supports_fan_mode(CLIMATE_FAN_FOCUS))
136  fan_modes.add("focus");
137  if (traits.supports_fan_mode(CLIMATE_FAN_DIFFUSE))
138  fan_modes.add("diffuse");
139  if (traits.supports_fan_mode(CLIMATE_FAN_QUIET))
140  fan_modes.add("quiet");
141  for (const auto &fan_mode : traits.get_supported_custom_fan_modes())
142  fan_modes.add(fan_mode);
143  }
144 
145  if (traits.get_supports_swing_modes()) {
146  // swing_mode_command_topic
147  root[MQTT_SWING_MODE_COMMAND_TOPIC] = this->get_swing_mode_command_topic();
148  // swing_mode_state_topic
149  root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic();
150  // swing_modes
151  JsonArray swing_modes = root.createNestedArray("swing_modes");
152  if (traits.supports_swing_mode(CLIMATE_SWING_OFF))
153  swing_modes.add("off");
154  if (traits.supports_swing_mode(CLIMATE_SWING_BOTH))
155  swing_modes.add("both");
156  if (traits.supports_swing_mode(CLIMATE_SWING_VERTICAL))
157  swing_modes.add("vertical");
158  if (traits.supports_swing_mode(CLIMATE_SWING_HORIZONTAL))
159  swing_modes.add("horizontal");
160  }
161 
162  config.state_topic = false;
163  config.command_topic = false;
164 }
166  auto traits = this->device_->get_traits();
167  this->subscribe(this->get_mode_command_topic(), [this](const std::string &topic, const std::string &payload) {
168  auto call = this->device_->make_call();
169  call.set_mode(payload);
170  call.perform();
171  });
172 
173  if (traits.get_supports_two_point_target_temperature()) {
174  this->subscribe(this->get_target_temperature_low_command_topic(),
175  [this](const std::string &topic, const std::string &payload) {
176  auto val = parse_number<float>(payload);
177  if (!val.has_value()) {
178  ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
179  return;
180  }
181  auto call = this->device_->make_call();
182  call.set_target_temperature_low(*val);
183  call.perform();
184  });
185  this->subscribe(this->get_target_temperature_high_command_topic(),
186  [this](const std::string &topic, const std::string &payload) {
187  auto val = parse_number<float>(payload);
188  if (!val.has_value()) {
189  ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
190  return;
191  }
192  auto call = this->device_->make_call();
193  call.set_target_temperature_high(*val);
194  call.perform();
195  });
196  } else {
197  this->subscribe(this->get_target_temperature_command_topic(),
198  [this](const std::string &topic, const std::string &payload) {
199  auto val = parse_number<float>(payload);
200  if (!val.has_value()) {
201  ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
202  return;
203  }
204  auto call = this->device_->make_call();
205  call.set_target_temperature(*val);
206  call.perform();
207  });
208  }
209 
210  if (traits.get_supports_target_humidity()) {
211  this->subscribe(this->get_target_humidity_command_topic(),
212  [this](const std::string &topic, const std::string &payload) {
213  auto val = parse_number<float>(payload);
214  if (!val.has_value()) {
215  ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
216  return;
217  }
218  auto call = this->device_->make_call();
219  call.set_target_humidity(*val);
220  call.perform();
221  });
222  }
223 
224  if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
225  this->subscribe(this->get_preset_command_topic(), [this](const std::string &topic, const std::string &payload) {
226  auto call = this->device_->make_call();
227  call.set_preset(payload);
228  call.perform();
229  });
230  }
231 
232  if (traits.get_supports_fan_modes()) {
233  this->subscribe(this->get_fan_mode_command_topic(), [this](const std::string &topic, const std::string &payload) {
234  auto call = this->device_->make_call();
235  call.set_fan_mode(payload);
236  call.perform();
237  });
238  }
239 
240  if (traits.get_supports_swing_modes()) {
241  this->subscribe(this->get_swing_mode_command_topic(), [this](const std::string &topic, const std::string &payload) {
242  auto call = this->device_->make_call();
243  call.set_swing_mode(payload);
244  call.perform();
245  });
246  }
247 
248  this->device_->add_on_state_callback([this](Climate & /*unused*/) { this->publish_state_(); });
249 }
252 std::string MQTTClimateComponent::component_type() const { return "climate"; }
253 const EntityBase *MQTTClimateComponent::get_entity() const { return this->device_; }
254 
256  auto traits = this->device_->get_traits();
257  // mode
258  const char *mode_s = "";
259  switch (this->device_->mode) {
260  case CLIMATE_MODE_OFF:
261  mode_s = "off";
262  break;
263  case CLIMATE_MODE_AUTO:
264  mode_s = "auto";
265  break;
266  case CLIMATE_MODE_COOL:
267  mode_s = "cool";
268  break;
269  case CLIMATE_MODE_HEAT:
270  mode_s = "heat";
271  break;
273  mode_s = "fan_only";
274  break;
275  case CLIMATE_MODE_DRY:
276  mode_s = "dry";
277  break;
279  mode_s = "heat_cool";
280  break;
281  }
282  bool success = true;
283  if (!this->publish(this->get_mode_state_topic(), mode_s))
284  success = false;
285  int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
286  int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
287  if (traits.get_supports_current_temperature() && !std::isnan(this->device_->current_temperature)) {
288  std::string payload = value_accuracy_to_string(this->device_->current_temperature, current_accuracy);
289  if (!this->publish(this->get_current_temperature_state_topic(), payload))
290  success = false;
291  }
292  if (traits.get_supports_two_point_target_temperature()) {
293  std::string payload = value_accuracy_to_string(this->device_->target_temperature_low, target_accuracy);
294  if (!this->publish(this->get_target_temperature_low_state_topic(), payload))
295  success = false;
296  payload = value_accuracy_to_string(this->device_->target_temperature_high, target_accuracy);
297  if (!this->publish(this->get_target_temperature_high_state_topic(), payload))
298  success = false;
299  } else {
300  std::string payload = value_accuracy_to_string(this->device_->target_temperature, target_accuracy);
301  if (!this->publish(this->get_target_temperature_state_topic(), payload))
302  success = false;
303  }
304 
305  if (traits.get_supports_current_humidity() && !std::isnan(this->device_->current_humidity)) {
306  std::string payload = value_accuracy_to_string(this->device_->current_humidity, 0);
307  if (!this->publish(this->get_current_humidity_state_topic(), payload))
308  success = false;
309  }
310  if (traits.get_supports_target_humidity() && !std::isnan(this->device_->target_humidity)) {
311  std::string payload = value_accuracy_to_string(this->device_->target_humidity, 0);
312  if (!this->publish(this->get_target_humidity_state_topic(), payload))
313  success = false;
314  }
315 
316  if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
317  std::string payload;
318  if (this->device_->preset.has_value()) {
319  switch (this->device_->preset.value()) {
320  case CLIMATE_PRESET_NONE:
321  payload = "none";
322  break;
323  case CLIMATE_PRESET_HOME:
324  payload = "home";
325  break;
326  case CLIMATE_PRESET_AWAY:
327  payload = "away";
328  break;
330  payload = "boost";
331  break;
333  payload = "comfort";
334  break;
335  case CLIMATE_PRESET_ECO:
336  payload = "eco";
337  break;
339  payload = "sleep";
340  break;
342  payload = "activity";
343  break;
344  }
345  }
346  if (this->device_->custom_preset.has_value())
347  payload = this->device_->custom_preset.value();
348  if (!this->publish(this->get_preset_state_topic(), payload))
349  success = false;
350  }
351 
352  if (traits.get_supports_action()) {
353  const char *payload = "unknown";
354  switch (this->device_->action) {
355  case CLIMATE_ACTION_OFF:
356  payload = "off";
357  break;
359  payload = "cooling";
360  break;
362  payload = "heating";
363  break;
364  case CLIMATE_ACTION_IDLE:
365  payload = "idle";
366  break;
368  payload = "drying";
369  break;
370  case CLIMATE_ACTION_FAN:
371  payload = "fan";
372  break;
373  }
374  if (!this->publish(this->get_action_state_topic(), payload))
375  success = false;
376  }
377 
378  if (traits.get_supports_fan_modes()) {
379  std::string payload;
380  if (this->device_->fan_mode.has_value()) {
381  switch (this->device_->fan_mode.value()) {
382  case CLIMATE_FAN_ON:
383  payload = "on";
384  break;
385  case CLIMATE_FAN_OFF:
386  payload = "off";
387  break;
388  case CLIMATE_FAN_AUTO:
389  payload = "auto";
390  break;
391  case CLIMATE_FAN_LOW:
392  payload = "low";
393  break;
394  case CLIMATE_FAN_MEDIUM:
395  payload = "medium";
396  break;
397  case CLIMATE_FAN_HIGH:
398  payload = "high";
399  break;
400  case CLIMATE_FAN_MIDDLE:
401  payload = "middle";
402  break;
403  case CLIMATE_FAN_FOCUS:
404  payload = "focus";
405  break;
406  case CLIMATE_FAN_DIFFUSE:
407  payload = "diffuse";
408  break;
409  case CLIMATE_FAN_QUIET:
410  payload = "quiet";
411  break;
412  }
413  }
414  if (this->device_->custom_fan_mode.has_value())
415  payload = this->device_->custom_fan_mode.value();
416  if (!this->publish(this->get_fan_mode_state_topic(), payload))
417  success = false;
418  }
419 
420  if (traits.get_supports_swing_modes()) {
421  const char *payload = "";
422  switch (this->device_->swing_mode) {
423  case CLIMATE_SWING_OFF:
424  payload = "off";
425  break;
426  case CLIMATE_SWING_BOTH:
427  payload = "both";
428  break;
430  payload = "vertical";
431  break;
433  payload = "horizontal";
434  break;
435  }
436  if (!this->publish(this->get_swing_mode_state_topic(), payload))
437  success = false;
438  }
439 
440  return success;
441 }
442 
443 } // namespace mqtt
444 } // namespace esphome
445 
446 #endif
447 #endif // USE_MQTT
constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC
Definition: mqtt_const.h:52
value_type const & value() const
Definition: optional.h:89
constexpr const char *const MQTT_MIN_TEMP
Definition: mqtt_const.h:105
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:412
constexpr const char *const MQTT_FAN_MODE_COMMAND_TOPIC
Definition: mqtt_const.h:75
constexpr const char *const MQTT_SWING_MODE_COMMAND_TOPIC
Definition: mqtt_const.h:218
constexpr const char *const MQTT_FAN_MODE_STATE_TOPIC
Definition: mqtt_const.h:77
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:54
constexpr const char *const MQTT_ACTION_TOPIC
Definition: mqtt_const.h:12
constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TOPIC
Definition: mqtt_const.h:91
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:222
constexpr const char *const MQTT_TEMPERATURE_STATE_TOPIC
Definition: mqtt_const.h:232
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:104
constexpr const char *const MQTT_MODE_STATE_TOPIC
Definition: mqtt_const.h:108
bool has_value() const
Definition: optional.h:87
constexpr const char *const MQTT_PRESET_MODE_COMMAND_TOPIC
Definition: mqtt_const.h:167
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:228
constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TOPIC
Definition: mqtt_const.h:224
constexpr const char *const MQTT_TEMPERATURE_UNIT
Definition: mqtt_const.h:233
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:101
Simple Helper struct used for Home Assistant MQTT send_discovery().
constexpr const char *const MQTT_MAX_HUMIDITY
Definition: mqtt_const.h:100
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:110
constexpr const char *const MQTT_SWING_MODE_STATE_TOPIC
Definition: mqtt_const.h:220
constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TOPIC
Definition: mqtt_const.h:89
This is a workaround until we can figure out a way to get the tflite-micro idf component code availab...
Definition: a01nyub.cpp:7
constexpr const char *const MQTT_PRESET_MODE_STATE_TOPIC
Definition: mqtt_const.h:169
constexpr const char *const MQTT_TEMPERATURE_HIGH_STATE_TOPIC
Definition: mqtt_const.h:226
std::string component_type() const override
constexpr const char *const MQTT_MODE_COMMAND_TOPIC
Definition: mqtt_const.h:107
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:230
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