ESPHome 2026.5.0-dev
Loading...
Searching...
No Matches
smartair2_climate.cpp
Go to the documentation of this file.
1#include <chrono>
4#include "smartair2_climate.h"
5#include "smartair2_packet.h"
6
7using namespace esphome::climate;
8using namespace esphome::uart;
9
10namespace esphome {
11namespace haier {
12
13static const char *const TAG = "haier.climate";
14constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
15constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5;
16constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500);
17constexpr uint8_t INIT_REQUESTS_RETRY = 2;
18constexpr std::chrono::milliseconds INIT_REQUESTS_RETRY_INTERVAL = std::chrono::milliseconds(2000);
19
21 last_status_message_ = std::unique_ptr<uint8_t[]>(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]);
22}
23
24haier_protocol::HandlerError Smartair2Climate::status_handler_(haier_protocol::FrameType request_type,
25 haier_protocol::FrameType message_type,
26 const uint8_t *data, size_t data_size) {
27 haier_protocol::HandlerError result =
28 this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type,
29 haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
30 if (result == haier_protocol::HandlerError::HANDLER_OK) {
31 result = this->process_status_message_(data, data_size);
32 if (result != haier_protocol::HandlerError::HANDLER_OK) {
33 ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result);
34 this->reset_phase_();
35 this->action_request_.reset();
36 this->force_send_control_ = false;
37 } else {
38 if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) {
39 memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl));
40 this->status_message_callback_.call((const char *) data, data_size);
41 } else {
42 ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size,
44 }
45 switch (this->protocol_phase_) {
47 ESP_LOGI(TAG, "First HVAC status received");
49 break;
51 // Do nothing, phase will be changed in process_phase
52 break;
55 break;
58 this->force_send_control_ = false;
61 break;
62 default:
63 break;
64 }
65 }
66 return result;
67 } else {
68 this->action_request_.reset();
69 this->force_send_control_ = false;
70 this->reset_phase_();
71 return result;
72 }
73}
74
76 haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data,
77 size_t data_size) {
78 if (request_type != haier_protocol::FrameType::GET_DEVICE_VERSION)
79 return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
81 return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
82 // Invalid packet is expected answer
83 if ((message_type == haier_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE) && (data_size >= 39) &&
84 ((data[37] & 0x04) != 0)) {
85 ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the hOn protocol "
86 "instead of smartAir2");
87 }
89 return haier_protocol::HandlerError::HANDLER_OK;
90}
91
93 haier_protocol::FrameType message_type) {
96 ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type,
98 ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1);
101 this->set_phase(new_phase);
102 return haier_protocol::HandlerError::HANDLER_OK;
103}
104
106 // Set handlers
107 this->haier_protocol_.set_answer_handler(
108 haier_protocol::FrameType::GET_DEVICE_VERSION,
109 [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
110 return this->get_device_version_answer_handler_(req, msg, data, size);
111 });
112 this->haier_protocol_.set_answer_handler(
113 haier_protocol::FrameType::CONTROL,
114 [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
115 return this->status_handler_(req, msg, data, size);
116 });
117 this->haier_protocol_.set_answer_handler(
118 haier_protocol::FrameType::REPORT_NETWORK_STATUS,
119 [this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
120 return this->report_network_status_answer_handler_(req, msg, data, size);
121 });
122 this->haier_protocol_.set_default_timeout_handler(
123 [this](haier_protocol::FrameType type) { return this->messages_timeout_handler_with_cycle_for_init_(type); });
124}
125
128 ESP_LOGCONFIG(TAG, " Protocol version: smartAir2");
129}
130
131void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) {
132 switch (this->protocol_phase_) {
135 // Indicate device capabilities:
136 // bit 0 - if 1 module support interactive mode
137 // bit 1 - if 1 module support controller-device mode
138 // bit 2 - if 1 module support crc
139 // bit 3 - if 1 module support multiple devices
140 // bit 4..bit 15 - not used
141 uint8_t module_capabilities[2] = {0b00000000, 0b00000111};
142 static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST(
143 haier_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities));
144 this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_, INIT_REQUESTS_RETRY, INIT_REQUESTS_RETRY_INTERVAL);
145 }
146 break;
149 break;
152 if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
153 static const haier_protocol::HaierMessage STATUS_REQUEST(haier_protocol::FrameType::CONTROL, 0x4D01);
155 this->send_message_(STATUS_REQUEST, this->use_crc_, INIT_REQUESTS_RETRY, INIT_REQUESTS_RETRY_INTERVAL);
156 } else {
157 this->send_message_(STATUS_REQUEST, this->use_crc_);
158 }
159 this->last_status_request_ = now;
160 }
161 break;
162#ifdef USE_WIFI
164 if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
165 this->send_message_(this->get_wifi_signal_message_(), this->use_crc_);
166 this->last_signal_request_ = now;
167 }
168 break;
169#else
172 break;
173#endif
176 break;
179 break;
182 break;
185 ESP_LOGI(TAG, "Sending control packet");
186 this->send_message_(get_control_message(), this->use_crc_, CONTROL_MESSAGE_RETRIES,
188 }
189 break;
191 if (this->action_request_.has_value()) {
192 if (this->action_request_.value().message.has_value()) {
193 this->send_message_(this->action_request_.value().message.value(), this->use_crc_);
194 this->action_request_.value().message.reset();
195 } else {
196 // Message already sent, reseting request and return to idle
197 this->action_request_.reset();
199 }
200 } else {
201 ESP_LOGW(TAG, "SENDING_ACTION_COMMAND phase without action request!");
203 }
204 break;
208 this->forced_request_status_ = false;
209 }
210#ifdef USE_WIFI
211 else if (this->send_wifi_signal_ &&
212 (std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() >
215#endif
216 } break;
217 default:
218 // Shouldn't get here
219 ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
222 break;
223 }
224}
225
226haier_protocol::HaierMessage Smartair2Climate::get_power_message(bool state) {
227 if (state) {
228 static haier_protocol::HaierMessage power_on_message(haier_protocol::FrameType::CONTROL, 0x4D02);
229 return power_on_message;
230 } else {
231 static haier_protocol::HaierMessage power_off_message(haier_protocol::FrameType::CONTROL, 0x4D03);
232 return power_off_message;
233 }
234}
235
236haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
237 uint8_t control_out_buffer[sizeof(smartair2_protocol::HaierPacketControl)];
238 memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(smartair2_protocol::HaierPacketControl));
240 out_data->cntrl = 0;
241 if (this->current_hvac_settings_.valid) {
242 HvacSettings &climate_control = this->current_hvac_settings_;
243 if (climate_control.mode.has_value()) {
244 switch (climate_control.mode.value()) {
245 case CLIMATE_MODE_OFF:
246 out_data->ac_power = 0;
247 break;
249 out_data->ac_power = 1;
251 out_data->fan_mode = this->other_modes_fan_speed_;
252 break;
254 out_data->ac_power = 1;
256 out_data->fan_mode = this->other_modes_fan_speed_;
257 break;
258 case CLIMATE_MODE_DRY:
259 out_data->ac_power = 1;
261 out_data->fan_mode = this->other_modes_fan_speed_;
262 break;
264 out_data->ac_power = 1;
266 out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode
267 break;
269 out_data->ac_power = 1;
271 out_data->fan_mode = this->other_modes_fan_speed_;
272 break;
273 default:
274 ESP_LOGE("Control", "Unsupported climate mode");
275 break;
276 }
277 }
278 // Set fan speed, if we are in fan mode, reject auto in fan mode
279 if (climate_control.fan_mode.has_value()) {
280 switch (climate_control.fan_mode.value()) {
281 case CLIMATE_FAN_LOW:
283 break;
286 break;
287 case CLIMATE_FAN_HIGH:
289 break;
290 case CLIMATE_FAN_AUTO:
291 if (this->mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode
293 break;
294 default:
295 ESP_LOGE("Control", "Unsupported fan mode");
296 break;
297 }
298 }
299 // Set swing mode
300 if (climate_control.swing_mode.has_value()) {
302 switch (climate_control.swing_mode.value()) {
304 out_data->swing_mode = 0;
305 break;
307 out_data->swing_mode = 1;
308 break;
310 out_data->swing_mode = 2;
311 break;
313 out_data->swing_mode = 3;
314 break;
315 }
316 } else {
317 switch (climate_control.swing_mode.value()) {
319 out_data->use_swing_bits = 0;
320 out_data->swing_mode = 0;
321 break;
323 out_data->swing_mode = 0;
324 out_data->vertical_swing = 1;
325 out_data->horizontal_swing = 0;
326 break;
328 out_data->swing_mode = 0;
329 out_data->vertical_swing = 0;
330 out_data->horizontal_swing = 1;
331 break;
333 out_data->swing_mode = 1;
334 out_data->use_swing_bits = 0;
335 out_data->vertical_swing = 0;
336 out_data->horizontal_swing = 0;
337 break;
338 }
339 }
340 }
341 if (climate_control.target_temperature.has_value()) {
342 float target_temp = climate_control.target_temperature.value();
343 out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16
344 out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0;
345 }
346 if (out_data->ac_power == 0) {
347 // If AC is off - no presets allowed
348 out_data->turbo_mode = 0;
349 out_data->quiet_mode = 0;
350 } else if (climate_control.preset.has_value()) {
351 switch (climate_control.preset.value()) {
353 out_data->ten_degree = 0;
354 out_data->turbo_mode = 0;
355 out_data->quiet_mode = 0;
356 break;
358 out_data->ten_degree = 0;
359 out_data->turbo_mode = 1;
360 out_data->quiet_mode = 0;
361 break;
363 out_data->ten_degree = 0;
364 out_data->turbo_mode = 0;
365 out_data->quiet_mode = 1;
366 break;
368 // Only allowed in heat mode
369 out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0;
370 out_data->turbo_mode = 0;
371 out_data->quiet_mode = 0;
372 break;
373 default:
374 ESP_LOGE("Control", "Unsupported preset");
375 out_data->ten_degree = 0;
376 out_data->turbo_mode = 0;
377 out_data->quiet_mode = 0;
378 break;
379 }
380 }
381 }
382 out_data->display_status = this->get_display_state() ? 0 : 1;
383 this->display_status_ = (SwitchState) ((uint8_t) this->display_status_ & 0b01);
384 out_data->health_mode = this->get_health_mode() ? 1 : 0;
385 this->health_mode_ = (SwitchState) ((uint8_t) this->health_mode_ & 0b01);
386 return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer,
388}
389
390haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
392 return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
394 memcpy(&packet, packet_buffer, size);
395 bool should_publish = false;
396 {
397 // Extra modes/presets
398 optional<ClimatePreset> old_preset = this->preset;
399 if (packet.control.turbo_mode != 0) {
401 } else if (packet.control.quiet_mode != 0) {
403 } else if (packet.control.ten_degree != 0) {
405 } else {
407 }
408 should_publish = should_publish || (!old_preset.has_value()) ||
409 (old_preset.value_or(CLIMATE_PRESET_NONE) != this->preset.value_or(CLIMATE_PRESET_NONE));
410 }
411 {
412 // Target temperature
413 float old_target_temperature = this->target_temperature;
414 this->target_temperature = packet.control.set_point + 16.0f + ((packet.control.half_degree == 1) ? 0.5f : 0.0f);
415 should_publish = should_publish || (old_target_temperature != this->target_temperature);
416 }
417 {
418 // Current temperature
419 float old_current_temperature = this->current_temperature;
421 should_publish = should_publish || (old_current_temperature != this->current_temperature);
422 }
423 {
424 // Fan mode
425 optional<ClimateFanMode> old_fan_mode = this->fan_mode;
426 // remember the fan speed we last had for climate vs fan
429 this->fan_mode_speed_ = packet.control.fan_mode;
430 } else {
432 }
433 switch (packet.control.fan_mode) {
435 // Sometimes AC reports in fan only mode that fan speed is auto
436 // but never accept this value back
439 } else {
440 should_publish = true;
441 }
442 break;
445 break;
448 break;
451 break;
452 }
453 should_publish = should_publish || (!old_fan_mode.has_value()) ||
454 (old_fan_mode.value_or(CLIMATE_FAN_ON) != this->fan_mode.value_or(CLIMATE_FAN_ON));
455 }
456 // Display status
457 // should be before "Climate mode" because it is changing this->mode
458 if (packet.control.ac_power != 0) {
459 // if AC is off display status always ON so process it only when AC is on
460 bool disp_status = packet.control.display_status == 0;
461 if (disp_status != this->get_display_state()) {
462 // Do something only if display status changed
463 if (this->mode == CLIMATE_MODE_OFF) {
464 // AC just turned on from remote need to turn off display
465 this->force_send_control_ = true;
466 } else if ((((uint8_t) this->health_mode_) & 0b10) == 0) {
467 this->display_status_ = disp_status ? SwitchState::ON : SwitchState::OFF;
468 }
469 }
470 }
471 // Health mode
472 if ((((uint8_t) this->health_mode_) & 0b10) == 0) {
473 bool old_health_mode = this->get_health_mode();
474 this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::ON : SwitchState::OFF;
475 should_publish = should_publish || (old_health_mode != this->get_health_mode());
476 }
477 {
478 // Climate mode
479 ClimateMode old_mode = this->mode;
480 if (packet.control.ac_power == 0) {
481 this->mode = CLIMATE_MODE_OFF;
482 } else {
483 // Check current hvac mode
484 switch (packet.control.ac_mode) {
486 this->mode = CLIMATE_MODE_COOL;
487 break;
489 this->mode = CLIMATE_MODE_HEAT;
490 break;
492 this->mode = CLIMATE_MODE_DRY;
493 break;
496 break;
499 break;
500 }
501 }
502 should_publish = should_publish || (old_mode != this->mode);
503 }
504 {
505 // Swing mode
506 ClimateSwingMode old_swing_mode = this->swing_mode;
508 switch (packet.control.swing_mode) {
509 case 1:
511 break;
512 case 2:
514 break;
515 case 3:
517 break;
518 default:
520 break;
521 }
522 } else {
523 if (packet.control.swing_mode == 0) {
524 if (packet.control.vertical_swing != 0) {
526 } else if (packet.control.horizontal_swing != 0) {
528 } else {
530 }
531 } else {
533 }
534 }
535 should_publish = should_publish || (old_swing_mode != this->swing_mode);
536 }
537 this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
538 if (should_publish) {
539 this->publish_state();
540 }
541 if (should_publish) {
542 ESP_LOGI(TAG, "HVAC values changed");
543 }
544 int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG;
545 esp_log_printf_(log_level, TAG, __LINE__, "HVAC Mode = 0x%X", packet.control.ac_mode);
546 esp_log_printf_(log_level, TAG, __LINE__, "Fan speed Status = 0x%X", packet.control.fan_mode);
547 esp_log_printf_(log_level, TAG, __LINE__, "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing);
548 esp_log_printf_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_swing);
549 esp_log_printf_(log_level, TAG, __LINE__, "Set Point Status = 0x%X", packet.control.set_point);
550 return haier_protocol::HandlerError::HANDLER_OK;
551}
552
554 this->use_alternative_swing_control_ = swing_control;
555}
556
557} // namespace haier
558} // namespace esphome
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
float target_temperature
The target temperature of the climate device.
Definition climate.h:274
ClimateSwingMode swing_mode
The active swing mode of the climate device.
Definition climate.h:299
float current_temperature
The current temperature of the climate device, as reported from the integration.
Definition climate.h:267
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
bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now)
CallbackManager< void(const char *, size_t)> status_message_callback_
Definition haier_base.h:176
bool is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now)
bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now)
haier_protocol::HandlerError answer_preprocess_(haier_protocol::FrameType request_message_type, haier_protocol::FrameType expected_request_message_type, haier_protocol::FrameType answer_message_type, haier_protocol::FrameType expected_answer_message_type, ProtocolPhases expected_phase)
haier_protocol::ProtocolHandler haier_protocol_
Definition haier_base.h:156
const char * phase_to_string_(ProtocolPhases phase)
std::unique_ptr< uint8_t[]> last_status_message_
Definition haier_base.h:171
void send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats=0, std::chrono::milliseconds interval=std::chrono::milliseconds::zero())
haier_protocol::HandlerError report_network_status_answer_handler_(haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size)
std::chrono::steady_clock::time_point last_valid_status_timestamp_
Definition haier_base.h:173
haier_protocol::HaierMessage get_wifi_signal_message_()
haier_protocol::HandlerError timeout_default_handler_(haier_protocol::FrameType request_type)
bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now)
esphome::optional< PendingAction > action_request_
Definition haier_base.h:158
std::chrono::steady_clock::time_point last_status_request_
Definition haier_base.h:174
virtual void set_phase(ProtocolPhases phase)
std::chrono::steady_clock::time_point last_signal_request_
Definition haier_base.h:175
haier_protocol::HandlerError status_handler_(haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size)
haier_protocol::HaierMessage get_control_message() override
haier_protocol::HandlerError get_device_version_answer_handler_(haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, size_t data_size)
haier_protocol::HandlerError messages_timeout_handler_with_cycle_for_init_(haier_protocol::FrameType message_type)
haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size)
haier_protocol::HaierMessage get_power_message(bool state) override
void set_alternative_swing_control(bool swing_control)
void process_phase(std::chrono::steady_clock::time_point now) override
uint16_t type
bool state
Definition fan.h:2
@ CLIMATE_PRESET_NONE
No preset is active.
@ CLIMATE_PRESET_COMFORT
Device is in comfort preset.
@ CLIMATE_PRESET_AWAY
Device is in away preset.
@ CLIMATE_PRESET_BOOST
Device is in boost preset.
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_FAN_MEDIUM
The fan mode is set to Medium.
@ CLIMATE_FAN_ON
The fan mode is set to On.
@ CLIMATE_FAN_AUTO
The fan mode is set to Auto.
@ CLIMATE_FAN_LOW
The fan mode is set to Low.
@ CLIMATE_FAN_HIGH
The fan mode is set to High.
constexpr uint8_t CONTROL_MESSAGE_RETRIES
constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL
constexpr uint8_t INIT_REQUESTS_RETRY
constexpr std::chrono::milliseconds INIT_REQUESTS_RETRY_INTERVAL
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
void HOT esp_log_printf_(int level, const char *tag, int line, const char *format,...)
Definition log.cpp:21
uint16_t size
Definition helpers.cpp:25
esphome::optional< esphome::climate::ClimateFanMode > fan_mode
Definition haier_base.h:136
esphome::optional< esphome::climate::ClimateSwingMode > swing_mode
Definition haier_base.h:137
esphome::optional< esphome::climate::ClimateMode > mode
Definition haier_base.h:135
esphome::optional< esphome::climate::ClimatePreset > preset
Definition haier_base.h:139
esphome::optional< float > target_temperature
Definition haier_base.h:138