ESPHome 2026.4.0
Loading...
Searching...
No Matches
mitsubishi_cn105.cpp
Go to the documentation of this file.
1#include <array>
2#include <cmath>
3#include <numeric>
4#include "mitsubishi_cn105.h"
5
7
8static const char *const TAG = "mitsubishi_cn105.driver";
9
10static constexpr uint32_t WRITE_TIMEOUT_MS = 2000;
11
12static constexpr uint8_t TARGET_TEMPERATURE_ENC_A_OFFSET = 31;
13
14static constexpr size_t REQUEST_PAYLOAD_LEN = 0x10;
15static constexpr size_t HEADER_LEN = 5;
16static constexpr uint8_t PREAMBLE = 0xFC;
17static constexpr uint8_t HEADER_BYTE_1 = 0x01;
18static constexpr uint8_t HEADER_BYTE_2 = 0x30;
19
20static constexpr uint8_t PACKET_TYPE_CONNECT_REQUEST = 0x5A;
21static constexpr uint8_t PACKET_TYPE_CONNECT_RESPONSE = 0x7A;
22static constexpr std::array<uint8_t, 2> CONNECT_REQUEST_PAYLOAD = {0xCA, 0x01};
23
24static constexpr uint8_t PACKET_TYPE_STATUS_REQUEST = 0x42;
25static constexpr uint8_t PACKET_TYPE_STATUS_RESPONSE = 0x62;
26static constexpr uint8_t STATUS_MSG_SETTINGS = 0x02;
27static constexpr uint8_t STATUS_MSG_ROOM_TEMP = 0x03;
28
29static constexpr uint8_t PACKET_TYPE_WRITE_SETTINGS_REQUEST = 0x41;
30static constexpr uint8_t PACKET_TYPE_WRITE_SETTINGS_RESPONSE = 0x61;
31
32static constexpr std::array<std::optional<MitsubishiCN105::Mode>, 9> PROTOCOL_MODE_MAP = {
33 std::nullopt, // 0x00
37 std::nullopt, // 0x04
38 std::nullopt, // 0x05
39 std::nullopt, // 0x06
42};
43
44static constexpr std::array<std::optional<MitsubishiCN105::FanMode>, 7> PROTOCOL_FAN_MODE_MAP = {
49 std::nullopt, // 0x04
52};
53
54template<typename T, size_t N>
55static constexpr std::optional<T> lookup(const std::array<std::optional<T>, N> &table, uint8_t value) {
56 return (value < N) ? table[value] : std::nullopt;
57}
58
59template<typename T, size_t N>
60static constexpr bool reverse_lookup(const std::array<std::optional<T>, N> &table, T value, uint8_t &placeholder) {
61 for (size_t i = 0; i < N; ++i) {
62 const auto &table_value = table[i];
63 if (table_value.has_value() && table_value == value) {
64 placeholder = i;
65 return true;
66 }
67 }
68 return false;
69}
70
71static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) {
72 return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0}));
73}
74
75template<std::size_t PayloadSize>
76static constexpr auto make_packet(uint8_t type, const std::array<uint8_t, PayloadSize> &payload) {
77 const size_t full_len = PayloadSize + HEADER_LEN + 1;
78 std::array<uint8_t, full_len> packet{PREAMBLE, type, HEADER_BYTE_1, HEADER_BYTE_2, static_cast<uint8_t>(PayloadSize)};
79 std::copy_n(payload.begin(), PayloadSize, packet.begin() + HEADER_LEN);
80 packet.back() = checksum(packet.data(), packet.size() - 1);
81 return packet;
82}
83
84static float decode_temperature(int temp_a, int temp_b, int delta) {
85 return temp_b != 0 ? (temp_b - 128) / 2.0f : delta + temp_a;
86}
87
88static constexpr auto CONNECT_PACKET = make_packet(PACKET_TYPE_CONNECT_REQUEST, CONNECT_REQUEST_PAYLOAD);
89
91
93 if (const auto start = this->status_update_start_ms_) {
94 if (this->pending_updates_.any()) {
96 return false;
97 }
98
99 if ((get_loop_time_ms() - *start) >= this->update_interval_ms_) {
101 return false;
102 }
103 }
104
105 if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) {
106 this->write_timeout_start_ms_.reset();
107 this->frame_parser_.reset();
109 return false;
110 }
111
112 return this->frame_parser_.read_and_parse(this->device_, [this](uint8_t type, const uint8_t *payload, size_t len) {
113 return this->process_rx_packet_(type, payload, len);
114 });
115}
116
118 if (should_transition(this->state_, new_state)) {
119 ESP_LOGV(TAG, "Did transition: %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
120 LOG_STR_ARG(state_to_string(new_state)));
121 this->state_ = new_state;
122 this->did_transition_(new_state);
123 } else {
124 ESP_LOGV(TAG, "Ignoring unexpected transition %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
125 LOG_STR_ARG(state_to_string(new_state)));
126 }
127}
128
130 switch (to) {
132 return from == State::NOT_CONNECTED || from == State::READ_TIMEOUT;
133
134 case State::CONNECTED:
135 return from == State::CONNECTING;
136
138 return from == State::CONNECTED || from == State::STATUS_UPDATED ||
140
142 return from == State::UPDATING_STATUS;
143
145 return from == State::STATUS_UPDATED || from == State::SETTINGS_APPLIED;
146
149
152
154 return from == State::APPLYING_SETTINGS;
155
157 return from == State::UPDATING_STATUS || from == State::APPLYING_SETTINGS || from == State::CONNECTING;
158
159 default:
160 return false;
161 }
162}
163
165 switch (to) {
167 this->send_packet_(CONNECT_PACKET);
168 break;
169
170 case State::CONNECTED:
171 this->write_timeout_start_ms_.reset();
172 this->current_status_msg_type_ = STATUS_MSG_SETTINGS;
174 break;
175
177 this->update_status_();
178 break;
179
181 this->write_timeout_start_ms_.reset();
182 if (this->pending_updates_.any() && this->is_status_initialized()) {
184 } else if (this->current_status_msg_type_ == STATUS_MSG_SETTINGS && this->should_request_room_temperature_()) {
185 this->current_status_msg_type_ = STATUS_MSG_ROOM_TEMP;
187 } else {
189 }
190 break;
191 }
192
195 this->current_status_msg_type_ = STATUS_MSG_SETTINGS;
197 break;
198
200 this->apply_settings_();
201 this->pending_updates_.clear();
202 break;
203
205 this->write_timeout_start_ms_.reset();
207 break;
208
211 break;
212
213 default:
214 break;
215 }
216}
217
219 if (!this->is_room_temperature_enabled()) {
220 return false;
221 }
222
223 if (!this->last_room_temperature_update_ms_.has_value()) {
224 return true;
225 }
226
228}
229
230void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) {
231 FrameParser::dump_buffer_vv("TX", packet, len);
232 this->device_.write_array(packet, len);
234}
235
237 std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload = {this->current_status_msg_type_};
238 this->send_packet_(make_packet(PACKET_TYPE_STATUS_REQUEST, payload));
239}
240
245
246bool MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) {
247 switch (type) {
248 case PACKET_TYPE_CONNECT_RESPONSE:
250 return false;
251
252 case PACKET_TYPE_STATUS_RESPONSE:
253 return this->process_status_packet_(payload, len);
254
255 case PACKET_TYPE_WRITE_SETTINGS_RESPONSE:
257 return false;
258
259 default:
260 ESP_LOGVV(TAG, "RX unknown packet type 0x%02X", type);
261 return false;
262 }
263}
264
265bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len) {
266 if (len == 0) {
267 ESP_LOGVV(TAG, "RX status packet too short");
268 return false;
269 }
270
271 const auto previous = this->status_;
272 const auto msg_type = payload[0];
273 if (!this->parse_status_payload_(msg_type, payload + 1, len - 1)) {
274 return false;
275 }
276
277 if (msg_type == this->current_status_msg_type_) {
279 }
280
281 bool changed = previous.power_on != this->status_.power_on || previous.mode != this->status_.mode ||
282 previous.fan_mode != this->status_.fan_mode ||
283 previous.target_temperature != this->status_.target_temperature;
284
285 if (this->is_room_temperature_enabled()) {
286 changed |= previous.room_temperature != this->status_.room_temperature;
287 }
288
289 return changed && this->is_status_initialized();
290}
291
292bool MitsubishiCN105::parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len) {
293 switch (msg_type) {
294 case STATUS_MSG_SETTINGS:
295 return this->parse_status_settings_(payload, len);
296
297 case STATUS_MSG_ROOM_TEMP:
298 return this->parse_status_room_temperature_(payload, len);
299
300 default:
301 ESP_LOGVV(TAG, "RX unsupported status msg type 0x%02X", msg_type);
302 return false;
303 }
304}
305
306bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len) {
307 if (len <= 10) {
308 ESP_LOGVV(TAG, "RX settings payload too short");
309 return false;
310 }
311
313 this->status_.power_on = payload[2] != 0;
314 }
315
316 this->use_temperature_encoding_b_ = payload[10] != 0;
318 this->status_.target_temperature = decode_temperature(-payload[4], payload[10], TARGET_TEMPERATURE_ENC_A_OFFSET);
319 }
320
322 const bool i_see = payload[3] > 0x08;
323 this->status_.mode = lookup(PROTOCOL_MODE_MAP, payload[3] - (i_see ? 0x08 : 0)).value_or(Mode::UNKNOWN);
324 }
325
327 this->status_.fan_mode = lookup(PROTOCOL_FAN_MODE_MAP, payload[5]).value_or(FanMode::UNKNOWN);
328 }
329
330 return true;
331}
332
333bool MitsubishiCN105::parse_status_room_temperature_(const uint8_t *payload, size_t len) {
334 if (len <= 5) {
335 ESP_LOGVV(TAG, "RX room temperature payload too short");
336 return false;
337 }
338
339 this->status_.room_temperature = decode_temperature(payload[2], payload[5], 10);
341
342 return true;
343}
344
345void MitsubishiCN105::set_power(bool power_on) {
346 this->status_.power_on = power_on;
348}
349
352 ESP_LOGD(TAG, "Setting temperature out-of-range: %.1f", target_temperature);
353 return;
354 }
357}
358
360 uint8_t placeholder;
361 if (!reverse_lookup(PROTOCOL_MODE_MAP, mode, placeholder)) {
362 ESP_LOGD(TAG, "Setting invalid mode: %u", static_cast<uint8_t>(mode));
363 return;
364 }
365 this->status_.mode = mode;
367}
368
370 uint8_t placeholder;
371 if (!reverse_lookup(PROTOCOL_FAN_MODE_MAP, fan_mode, placeholder)) {
372 ESP_LOGD(TAG, "Setting invalid fan mode: %u", static_cast<uint8_t>(fan_mode));
373 return;
374 }
375 this->status_.fan_mode = fan_mode;
377}
378
380 std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload = {0x01};
381
383 payload[1] |= 0x01;
384 payload[3] = this->status_.power_on ? 0x01 : 0x00;
385 }
386
388 payload[1] |= 0x04;
389 if (this->use_temperature_encoding_b_) {
390 payload[14] = static_cast<uint8_t>(this->status_.target_temperature * 2.0f + 128.0f);
391 } else {
392 payload[5] = static_cast<uint8_t>(TARGET_TEMPERATURE_ENC_A_OFFSET - this->status_.target_temperature);
393 }
394 }
395
397 reverse_lookup(PROTOCOL_MODE_MAP, this->status_.mode, payload[4])) {
398 payload[1] |= 0x02;
399 }
400
402 reverse_lookup(PROTOCOL_FAN_MODE_MAP, this->status_.fan_mode, payload[6])) {
403 payload[1] |= 0x08;
404 }
405
406 this->send_packet_(make_packet(PACKET_TYPE_WRITE_SETTINGS_REQUEST, payload));
407}
408
410 switch (state) {
412 return LOG_STR("Not connected");
414 return LOG_STR("Connecting");
415 case State::CONNECTED:
416 return LOG_STR("Connected");
418 return LOG_STR("UpdatingStatus");
420 return LOG_STR("StatusUpdated");
422 return LOG_STR("ScheduleNextStatusUpdate");
424 return LOG_STR("WaitingForScheduledStatusUpdate");
426 return LOG_STR("ApplyingSettings");
428 return LOG_STR("SettingsApplied");
430 return LOG_STR("ReadTimeout");
431 }
432 return LOG_STR("Unknown");
433}
434
435template<typename Callback>
437 uint8_t watchdog = 64;
438 while (device.available() > 0 && watchdog-- > 0) {
439 uint8_t &value = this->read_buffer_[this->read_pos_];
440 if (!device.read_byte(&value)) {
441 ESP_LOGW(TAG, "UART read failed while data available");
442 return false;
443 }
444
445 switch (++this->read_pos_) {
446 case 1:
447 if (value != PREAMBLE) {
448 this->reset_and_dump_buffer_("RX ignoring preamble");
449 }
450 continue;
451
452 case 2:
453 continue;
454
455 case 3:
456 if (value != HEADER_BYTE_1) {
457 this->reset_and_dump_buffer_("RX invalid: header 1 mismatch");
458 }
459 continue;
460
461 case 4:
462 if (value != HEADER_BYTE_2) {
463 this->reset_and_dump_buffer_("RX invalid: header 2 mismatch");
464 }
465 continue;
466
467 case HEADER_LEN:
468 static_assert(READ_BUFFER_SIZE > HEADER_LEN);
469 if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) {
470 this->reset_and_dump_buffer_("RX invalid: payload too large");
471 }
472 continue;
473
474 default:
475 break;
476 }
477
478 const size_t len_without_checksum = HEADER_LEN + static_cast<size_t>(this->read_buffer_[HEADER_LEN - 1]);
479 if (this->read_pos_ <= len_without_checksum) {
480 continue;
481 }
482
483 if (checksum(this->read_buffer_, len_without_checksum) != value) {
484 this->reset_and_dump_buffer_("RX invalid: checksum mismatch");
485 continue;
486 }
487
488 dump_buffer_vv("RX", this->read_buffer_, this->read_pos_);
489 const bool processed =
490 callback(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, len_without_checksum - HEADER_LEN);
491 this->read_pos_ = 0;
492 return processed;
493 }
494
495 return false;
496}
497
499 dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_);
500 this->read_pos_ = 0;
501}
502
503void MitsubishiCN105::FrameParser::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) {
504#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
505 char buf[format_hex_pretty_size(READ_BUFFER_SIZE)];
506 ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len));
507#endif
508}
509
510} // namespace esphome::mitsubishi_cn105
BedjetMode mode
BedJet operating mode.
uint8_t checksum
Definition bl0906.h:3
bool read_and_parse(uart::UARTDevice &device, Callback &&callback)
static void dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len)
std::optional< uint32_t > last_room_temperature_update_ms_
bool process_status_packet_(const uint8_t *payload, size_t len)
static bool should_transition(State from, State to)
bool process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len)
std::optional< uint32_t > status_update_start_ms_
std::optional< uint32_t > write_timeout_start_ms_
static const LogString * state_to_string(State state)
void set_target_temperature(float target_temperature)
bool parse_status_settings_(const uint8_t *payload, size_t len)
void send_packet_(const uint8_t *packet, size_t len)
bool parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len)
bool parse_status_room_temperature_(const uint8_t *payload, size_t len)
bool read_byte(uint8_t *data)
Definition uart.h:34
void write_array(const uint8_t *data, size_t len)
Definition uart.h:26
float target_temperature
Definition climate.h:0
ClimateFanMode fan_mode
Definition climate.h:3
uint16_t type
bool state
Definition fan.h:2
std::string size_t len
Definition helpers.h:1045
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:392
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1353
static void uint32_t
Lightweight type-erased callback (8 bytes on 32-bit) that avoids std::function overhead.
Definition helpers.h:1761
uint16_t length
Definition tt21100.cpp:0