ESPHome  2024.12.2
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  default:
284  mode_s = "unknown";
285  }
286  bool success = true;
287  if (!this->publish(this->get_mode_state_topic(), mode_s))
288  success = false;
289  int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
290  int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
291  if (traits.get_supports_current_temperature() && !std::isnan(this->device_->current_temperature)) {
292  std::string payload = value_accuracy_to_string(this->device_->current_temperature, current_accuracy);
293  if (!this->publish(this->get_current_temperature_state_topic(), payload))
294  success = false;
295  }
296  if (traits.get_supports_two_point_target_temperature()) {
297  std::string payload = value_accuracy_to_string(this->device_->target_temperature_low, target_accuracy);
298  if (!this->publish(this->get_target_temperature_low_state_topic(), payload))
299  success = false;
300  payload = value_accuracy_to_string(this->device_->target_temperature_high, target_accuracy);
301  if (!this->publish(this->get_target_temperature_high_state_topic(), payload))
302  success = false;
303  } else {
304  std::string payload = value_accuracy_to_string(this->device_->target_temperature, target_accuracy);
305  if (!this->publish(this->get_target_temperature_state_topic(), payload))
306  success = false;
307  }
308 
309  if (traits.get_supports_current_humidity() && !std::isnan(this->device_->current_humidity)) {
310  std::string payload = value_accuracy_to_string(this->device_->current_humidity, 0);
311  if (!this->publish(this->get_current_humidity_state_topic(), payload))
312  success = false;
313  }
314  if (traits.get_supports_target_humidity() && !std::isnan(this->device_->target_humidity)) {
315  std::string payload = value_accuracy_to_string(this->device_->target_humidity, 0);
316  if (!this->publish(this->get_target_humidity_state_topic(), payload))
317  success = false;
318  }
319 
320  if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
321  std::string payload;
322  if (this->device_->preset.has_value()) {
323  switch (this->device_->preset.value()) {
324  case CLIMATE_PRESET_NONE:
325  payload = "none";
326  break;
327  case CLIMATE_PRESET_HOME:
328  payload = "home";
329  break;
330  case CLIMATE_PRESET_AWAY:
331  payload = "away";
332  break;
334  payload = "boost";
335  break;
337  payload = "comfort";
338  break;
339  case CLIMATE_PRESET_ECO:
340  payload = "eco";
341  break;
343  payload = "sleep";
344  break;
346  payload = "activity";
347  break;
348  default:
349  payload = "unknown";
350  }
351  }
352  if (this->device_->custom_preset.has_value())
353  payload = this->device_->custom_preset.value();
354  if (!this->publish(this->get_preset_state_topic(), payload))
355  success = false;
356  }
357 
358  if (traits.get_supports_action()) {
359  const char *payload;
360  switch (this->device_->action) {
361  case CLIMATE_ACTION_OFF:
362  payload = "off";
363  break;
365  payload = "cooling";
366  break;
368  payload = "heating";
369  break;
370  case CLIMATE_ACTION_IDLE:
371  payload = "idle";
372  break;
374  payload = "drying";
375  break;
376  case CLIMATE_ACTION_FAN:
377  payload = "fan";
378  break;
379  default:
380  payload = "unknown";
381  }
382  if (!this->publish(this->get_action_state_topic(), payload))
383  success = false;
384  }
385 
386  if (traits.get_supports_fan_modes()) {
387  std::string payload;
388  if (this->device_->fan_mode.has_value()) {
389  switch (this->device_->fan_mode.value()) {
390  case CLIMATE_FAN_ON:
391  payload = "on";
392  break;
393  case CLIMATE_FAN_OFF:
394  payload = "off";
395  break;
396  case CLIMATE_FAN_AUTO:
397  payload = "auto";
398  break;
399  case CLIMATE_FAN_LOW:
400  payload = "low";
401  break;
402  case CLIMATE_FAN_MEDIUM:
403  payload = "medium";
404  break;
405  case CLIMATE_FAN_HIGH:
406  payload = "high";
407  break;
408  case CLIMATE_FAN_MIDDLE:
409  payload = "middle";
410  break;
411  case CLIMATE_FAN_FOCUS:
412  payload = "focus";
413  break;
414  case CLIMATE_FAN_DIFFUSE:
415  payload = "diffuse";
416  break;
417  case CLIMATE_FAN_QUIET:
418  payload = "quiet";
419  break;
420  default:
421  payload = "unknown";
422  }
423  }
424  if (this->device_->custom_fan_mode.has_value())
425  payload = this->device_->custom_fan_mode.value();
426  if (!this->publish(this->get_fan_mode_state_topic(), payload))
427  success = false;
428  }
429 
430  if (traits.get_supports_swing_modes()) {
431  const char *payload;
432  switch (this->device_->swing_mode) {
433  case CLIMATE_SWING_OFF:
434  payload = "off";
435  break;
436  case CLIMATE_SWING_BOTH:
437  payload = "both";
438  break;
440  payload = "vertical";
441  break;
443  payload = "horizontal";
444  break;
445  default:
446  payload = "unknown";
447  }
448  if (!this->publish(this->get_swing_mode_state_topic(), payload))
449  success = false;
450  }
451 
452  return success;
453 }
454 
455 } // namespace mqtt
456 } // namespace esphome
457 
458 #endif
459 #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:432
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