ESPHome 2026.6.0-dev
Loading...
Searching...
No Matches
mitsubishi_cn105.cpp
Go to the documentation of this file.
1#include <algorithm>
2#include <array>
3#include <cmath>
4#include <numeric>
5#include "mitsubishi_cn105.h"
6
8
9static const char *const TAG = "mitsubishi_cn105.driver";
10
11static constexpr uint32_t RESPONSE_TIMEOUT_MS = 2000;
12
13static constexpr uint8_t TARGET_TEMPERATURE_ENC_A_OFFSET = 31;
14
15static constexpr size_t REQUEST_PAYLOAD_LEN = 0x10;
16static constexpr size_t HEADER_LEN = 5;
17static constexpr uint8_t PREAMBLE = 0xFC;
18static constexpr uint8_t HEADER_BYTE_1 = 0x01;
19static constexpr uint8_t HEADER_BYTE_2 = 0x30;
20
21static constexpr uint8_t PACKET_TYPE_CONNECT_REQUEST = 0x5A;
22static constexpr uint8_t PACKET_TYPE_CONNECT_RESPONSE = 0x7A;
23static constexpr std::array<uint8_t, 2> CONNECT_REQUEST_PAYLOAD = {0xCA, 0x01};
24
25static constexpr uint8_t PACKET_TYPE_STATUS_REQUEST = 0x42;
26static constexpr uint8_t PACKET_TYPE_STATUS_RESPONSE = 0x62;
27static constexpr uint8_t STATUS_MSG_SETTINGS = 0x02;
28static constexpr uint8_t STATUS_MSG_ROOM_TEMP = 0x03;
29
30static constexpr uint8_t PACKET_TYPE_WRITE_SETTINGS_REQUEST = 0x41;
31static constexpr uint8_t PACKET_TYPE_WRITE_SETTINGS_RESPONSE = 0x61;
32
33template<auto Unknown, size_t N> struct LookupMap {
34 using value_type = decltype(Unknown);
35 static constexpr auto UNKNOWN_VALUE = Unknown;
36 const std::array<value_type, N> table;
37
38 constexpr value_type lookup(uint8_t raw) const { return (raw < N) ? this->table[raw] : UNKNOWN_VALUE; }
39
40 constexpr bool reverse_lookup(value_type value, uint8_t &out) const {
41 static_assert(N <= std::numeric_limits<uint8_t>::max());
42 if (value == UNKNOWN_VALUE) {
43 return false;
44 }
45 for (uint8_t i = 0; i < static_cast<uint8_t>(N); ++i) {
46 if (this->table[i] == value) {
47 out = i;
48 return true;
49 }
50 }
51 return false;
52 }
53
54 constexpr bool is_valid(value_type value) const {
55 uint8_t raw;
56 return reverse_lookup(value, raw);
57 }
58};
59
60template<auto Unknown, class T, std::size_t N> static constexpr auto make_map(const T (&values)[N]) {
61 return LookupMap<Unknown, N>{std::to_array(values)};
62}
63
64static constexpr auto PROTOCOL_MODE_MAP = make_map<MitsubishiCN105::Mode::UNKNOWN>({
74});
75
76static constexpr auto PROTOCOL_FAN_MODE_MAP = make_map<MitsubishiCN105::FanMode::UNKNOWN>({
84});
85
86static constexpr auto PROTOCOL_VANE_MODE_MAP = make_map<MitsubishiCN105::VaneMode::UNKNOWN>({
95});
96
97static constexpr auto PROTOCOL_WIDE_VANE_MODE_MAP = make_map<MitsubishiCN105::WideVaneMode::UNKNOWN>({
111});
112
113static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) {
114 return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0}));
115}
116
117template<std::size_t PayloadSize>
118static constexpr auto make_packet(uint8_t type, const std::array<uint8_t, PayloadSize> &payload) {
119 const size_t full_len = PayloadSize + HEADER_LEN + 1;
120 std::array<uint8_t, full_len> packet{PREAMBLE, type, HEADER_BYTE_1, HEADER_BYTE_2, static_cast<uint8_t>(PayloadSize)};
121 std::copy_n(payload.begin(), PayloadSize, packet.begin() + HEADER_LEN);
122 packet.back() = checksum(packet.data(), packet.size() - 1);
123 return packet;
124}
125
126static constexpr float decode_temperature(int temp_a, int temp_b, int delta) {
127 return temp_b != 0 ? (temp_b - 128) / 2.0f : delta + temp_a;
128}
129
130static constexpr auto CONNECT_PACKET = make_packet(PACKET_TYPE_CONNECT_REQUEST, CONNECT_REQUEST_PAYLOAD);
131
133
135 switch (this->state_) {
137 if (this->pending_updates_.any()) {
141 return false;
142 }
143 if (this->has_timed_out_(this->update_interval_ms_)) {
145 return false;
146 }
147 break;
148
152 if (this->has_timed_out_(RESPONSE_TIMEOUT_MS)) {
154 return false;
155 }
156 break;
157
158 default:
159 break;
160 }
161
162 return this->frame_parser_.read_and_parse(this->device_, [this](uint8_t type, const uint8_t *payload, size_t len) {
163 return this->process_rx_packet_(type, payload, len);
164 });
165}
166
168 if (should_transition(this->state_, new_state)) {
169 ESP_LOGV(TAG, "Did transition: %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
170 LOG_STR_ARG(state_to_string(new_state)));
171 this->state_ = new_state;
172 this->did_transition_(new_state);
173 } else {
174 ESP_LOGV(TAG, "Ignoring unexpected transition %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
175 LOG_STR_ARG(state_to_string(new_state)));
176 }
177}
178
180 switch (to) {
182 return from == State::NOT_CONNECTED || from == State::READ_TIMEOUT;
183
184 case State::CONNECTED:
185 return from == State::CONNECTING;
186
188 return from == State::CONNECTED || from == State::STATUS_UPDATED ||
190
192 return from == State::UPDATING_STATUS;
193
195 return from == State::STATUS_UPDATED || from == State::SETTINGS_APPLIED;
196
199
202
204 return from == State::APPLYING_SETTINGS;
205
207 return from == State::UPDATING_STATUS || from == State::APPLYING_SETTINGS || from == State::CONNECTING;
208
209 default:
210 return false;
211 }
212}
213
215 switch (to) {
217 this->send_packet_(CONNECT_PACKET);
218 break;
219
220 case State::CONNECTED:
221 this->current_status_msg_type_ = STATUS_MSG_SETTINGS;
223 break;
224
226 this->update_status_();
227 break;
228
230 if (this->pending_updates_.any() && this->is_status_initialized()) {
232 } else if (this->current_status_msg_type_ == STATUS_MSG_SETTINGS && this->should_request_room_temperature_()) {
233 this->current_status_msg_type_ = STATUS_MSG_ROOM_TEMP;
235 } else {
237 }
238 break;
239 }
240
244 this->current_status_msg_type_ = STATUS_MSG_SETTINGS;
246 break;
247
249 this->apply_settings_();
250 break;
251
254 break;
255
257 this->frame_parser_.reset();
260 break;
261
262 default:
263 break;
264 }
265}
266
268 if (!this->is_room_temperature_enabled()) {
269 return false;
270 }
271
272 if (!this->last_room_temperature_update_ms_.has_value()) {
273 return true;
274 }
275
277}
278
279void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) {
280 FrameParser::dump_buffer_vv("TX", packet, len);
281 this->device_.write_array(packet, len);
283}
284
286 std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload = {this->current_status_msg_type_};
287 this->send_packet_(make_packet(PACKET_TYPE_STATUS_REQUEST, payload));
288}
289
290bool MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) {
291 switch (type) {
292 case PACKET_TYPE_CONNECT_RESPONSE:
294 return false;
295
296 case PACKET_TYPE_STATUS_RESPONSE:
297 return this->process_status_packet_(payload, len);
298
299 case PACKET_TYPE_WRITE_SETTINGS_RESPONSE:
301 return false;
302
303 default:
304 ESP_LOGVV(TAG, "RX unknown packet type 0x%02X", type);
305 return false;
306 }
307}
308
309bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len) {
310 if (len == 0) {
311 ESP_LOGVV(TAG, "RX status packet too short");
312 return false;
313 }
314
315 const auto previous = this->status_;
316 const auto msg_type = payload[0];
317 if (!this->parse_status_payload_(msg_type, payload + 1, len - 1)) {
318 return false;
319 }
320
321 if (msg_type == this->current_status_msg_type_) {
323 }
324
325 bool changed =
326 previous.power_on != this->status_.power_on || previous.mode != this->status_.mode ||
327 previous.fan_mode != this->status_.fan_mode || previous.target_temperature != this->status_.target_temperature ||
328 previous.vane_mode != this->status_.vane_mode || previous.wide_vane_mode != this->status_.wide_vane_mode;
329
330 if (this->is_room_temperature_enabled()) {
331 changed |= previous.room_temperature != this->status_.room_temperature;
332 }
333
334 return changed && this->is_status_initialized();
335}
336
337bool MitsubishiCN105::parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len) {
338 switch (msg_type) {
339 case STATUS_MSG_SETTINGS:
340 return this->parse_status_settings_(payload, len);
341
342 case STATUS_MSG_ROOM_TEMP:
343 return this->parse_status_room_temperature_(payload, len);
344
345 default:
346 ESP_LOGVV(TAG, "RX unsupported status msg type 0x%02X", msg_type);
347 return false;
348 }
349}
350
351bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len) {
352 if (len <= 10) {
353 ESP_LOGVV(TAG, "RX settings payload too short");
354 return false;
355 }
356
358 this->status_.power_on = payload[2] != 0;
359 }
360
361 this->use_temperature_encoding_b_ = payload[10] != 0;
363 this->status_.target_temperature = decode_temperature(-payload[4], payload[10], TARGET_TEMPERATURE_ENC_A_OFFSET);
364 }
365
367 const bool i_see = payload[3] > 0x08;
368 this->status_.mode = PROTOCOL_MODE_MAP.lookup(payload[3] - (i_see ? 0x08 : 0));
369 }
370
372 this->status_.fan_mode = PROTOCOL_FAN_MODE_MAP.lookup(payload[5]);
373 }
374
376 this->status_.vane_mode = PROTOCOL_VANE_MODE_MAP.lookup(payload[6]);
377 }
378
379 this->set_wide_vane_high_bit_ = (payload[9] & 0xF0) == 0x80;
381 this->status_.wide_vane_mode = PROTOCOL_WIDE_VANE_MODE_MAP.lookup(payload[9] & 0x0F);
382 }
383
384 return true;
385}
386
387bool MitsubishiCN105::parse_status_room_temperature_(const uint8_t *payload, size_t len) {
388 if (len <= 5) {
389 ESP_LOGVV(TAG, "RX room temperature payload too short");
390 return false;
391 }
392
393 this->status_.room_temperature = decode_temperature(payload[2], payload[5], 10);
395
396 return true;
397}
398
400 if (std::isnan(temperature)) {
401 ESP_LOGD(TAG, "Ignoring NaN remote temperature");
402 return;
403 }
405 ESP_LOGD(TAG, "Ignoring out-of-range remote temperature: %.1f", temperature);
406 return;
407 }
408 this->set_remote_temperature_half_deg_(static_cast<uint8_t>(std::round(temperature * 2.0f)));
409}
410
414
415void MitsubishiCN105::set_remote_temperature_half_deg_(uint8_t temperature_half_deg) {
416 this->remote_temperature_half_deg_ = temperature_half_deg;
418}
419
420void MitsubishiCN105::set_power(bool power_on) {
421 this->status_.power_on = power_on;
423}
424
427 ESP_LOGD(TAG, "Setting temperature out-of-range: %.1f", target_temperature);
428 return;
429 }
432}
433
435 if (!PROTOCOL_MODE_MAP.is_valid(mode)) {
436 ESP_LOGD(TAG, "Setting invalid mode: %u", static_cast<uint8_t>(mode));
437 return;
438 }
439 this->status_.mode = mode;
441}
442
444 if (!PROTOCOL_FAN_MODE_MAP.is_valid(fan_mode)) {
445 ESP_LOGD(TAG, "Setting invalid fan mode: %u", static_cast<uint8_t>(fan_mode));
446 return;
447 }
448 this->status_.fan_mode = fan_mode;
450}
451
453 if (!PROTOCOL_VANE_MODE_MAP.is_valid(vane_mode)) {
454 ESP_LOGD(TAG, "Setting invalid vane mode: %u", static_cast<uint8_t>(vane_mode));
455 return;
456 }
457 this->status_.vane_mode = vane_mode;
459}
460
462 if (!PROTOCOL_WIDE_VANE_MODE_MAP.is_valid(wide_vane_mode)) {
463 ESP_LOGD(TAG, "Setting invalid wide vane mode: %u", static_cast<uint8_t>(wide_vane_mode));
464 return;
465 }
466 this->status_.wide_vane_mode = wide_vane_mode;
468}
469
471 std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload{};
472
473 // Apply all other pending settings first; handle REMOTE_TEMPERATURE last
475 payload[0] = 0x07;
477 payload[3] = 0x80;
478 } else {
479 payload[1] = 0x01;
480 payload[2] = static_cast<uint8_t>(this->remote_temperature_half_deg_ - 16);
481 payload[3] = static_cast<uint8_t>(this->remote_temperature_half_deg_ + 128);
482 }
484 } else {
485 payload[0] = 0x01;
487 payload[1] |= 0x01;
488 payload[3] = this->status_.power_on ? 0x01 : 0x00;
489 }
490
492 payload[1] |= 0x04;
493 if (this->use_temperature_encoding_b_) {
494 payload[14] = static_cast<uint8_t>(std::round(this->status_.target_temperature * 2.0f) + 128);
495 } else {
496 payload[5] =
497 static_cast<uint8_t>(TARGET_TEMPERATURE_ENC_A_OFFSET - std::round(this->status_.target_temperature));
498 }
499 }
500
502 PROTOCOL_MODE_MAP.reverse_lookup(this->status_.mode, payload[4])) {
503 payload[1] |= 0x02;
504 }
505
507 PROTOCOL_FAN_MODE_MAP.reverse_lookup(this->status_.fan_mode, payload[6])) {
508 payload[1] |= 0x08;
509 }
510
512 PROTOCOL_VANE_MODE_MAP.reverse_lookup(this->status_.vane_mode, payload[7])) {
513 payload[1] |= 0x10;
514 }
515
517 PROTOCOL_WIDE_VANE_MODE_MAP.reverse_lookup(this->status_.wide_vane_mode, payload[13])) {
518 payload[2] |= 0x01;
519 if (this->set_wide_vane_high_bit_) {
520 payload[13] |= 0x80;
521 }
522 }
523
526 }
527
528 this->send_packet_(make_packet(PACKET_TYPE_WRITE_SETTINGS_REQUEST, payload));
529}
530
532 switch (state) {
534 return LOG_STR("Not connected");
536 return LOG_STR("Connecting");
537 case State::CONNECTED:
538 return LOG_STR("Connected");
540 return LOG_STR("UpdatingStatus");
542 return LOG_STR("StatusUpdated");
544 return LOG_STR("ScheduleNextStatusUpdate");
546 return LOG_STR("WaitingForScheduledStatusUpdate");
548 return LOG_STR("ApplyingSettings");
550 return LOG_STR("SettingsApplied");
552 return LOG_STR("ReadTimeout");
553 }
554 return LOG_STR("Unknown");
555}
556
557template<typename Callback>
559 uint8_t watchdog = 64;
560 while (device.available() > 0 && watchdog-- > 0) {
561 uint8_t &value = this->read_buffer_[this->read_pos_];
562 if (!device.read_byte(&value)) {
563 ESP_LOGW(TAG, "UART read failed while data available");
564 return false;
565 }
566
567 switch (++this->read_pos_) {
568 case 1:
569 if (value != PREAMBLE) {
570 this->reset_and_dump_buffer_("RX ignoring preamble");
571 }
572 continue;
573
574 case 2:
575 continue;
576
577 case 3:
578 if (value != HEADER_BYTE_1) {
579 this->reset_and_dump_buffer_("RX invalid: header 1 mismatch");
580 }
581 continue;
582
583 case 4:
584 if (value != HEADER_BYTE_2) {
585 this->reset_and_dump_buffer_("RX invalid: header 2 mismatch");
586 }
587 continue;
588
589 case HEADER_LEN:
590 static_assert(READ_BUFFER_SIZE > HEADER_LEN);
591 if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) {
592 this->reset_and_dump_buffer_("RX invalid: payload too large");
593 }
594 continue;
595
596 default:
597 break;
598 }
599
600 const size_t len_without_checksum = HEADER_LEN + static_cast<size_t>(this->read_buffer_[HEADER_LEN - 1]);
601 if (this->read_pos_ <= len_without_checksum) {
602 continue;
603 }
604
605 if (checksum(this->read_buffer_, len_without_checksum) != value) {
606 this->reset_and_dump_buffer_("RX invalid: checksum mismatch");
607 continue;
608 }
609
610 dump_buffer_vv("RX", this->read_buffer_, this->read_pos_);
611 const bool processed =
612 callback(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, len_without_checksum - HEADER_LEN);
613 this->read_pos_ = 0;
614 return processed;
615 }
616
617 return false;
618}
619
621 dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_);
622 this->read_pos_ = 0;
623}
624
625void MitsubishiCN105::FrameParser::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) {
626#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
627 char buf[format_hex_pretty_size(READ_BUFFER_SIZE)];
628 ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len));
629#endif
630}
631
632} // namespace esphome::mitsubishi_cn105
BedjetMode mode
BedJet operating mode.
uint8_t checksum
Definition bl0906.h:3
uint8_t raw[35]
Definition bl0939.h:0
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)
void set_remote_temperature_half_deg_(uint8_t temperature_half_deg)
bool process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len)
bool has_timed_out_(uint32_t timeout) const
static constexpr uint8_t REMOTE_TEMPERATURE_DISABLED
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
const void size_t len
Definition hal.h:64
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:341
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:1386
static void uint32_t
Lightweight type-erased callback (8 bytes on 32-bit) that avoids std::function overhead.
Definition helpers.h:1624
uint16_t temperature
Definition sun_gtil2.cpp:12
uint16_t length
Definition tt21100.cpp:0