ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
mitsubishi_cn105.cpp
Go to the documentation of this file.
1#include "mitsubishi_cn105.h"
2
3#include <algorithm>
4#include <array>
5#include <cmath>
7
9
10ESPHOME_LOG_TAG(TAG, "mitsubishi_cn105.driver");
11
12static constexpr uint32_t RESPONSE_TIMEOUT_MS = 2000;
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_TELEMETRY = 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 uint8_t checksum(const uint8_t *bytes, size_t length) {
33 return static_cast<uint8_t>(0xFC - sum_bytes(bytes, length));
34}
35
36template<std::size_t PayloadSize>
37static constexpr auto make_packet(uint8_t type, const std::array<uint8_t, PayloadSize> &payload) {
38 const size_t full_len = PayloadSize + HEADER_LEN + 1;
39 std::array<uint8_t, full_len> packet{PREAMBLE, type, HEADER_BYTE_1, HEADER_BYTE_2, static_cast<uint8_t>(PayloadSize)};
40 std::copy_n(payload.begin(), PayloadSize, packet.begin() + HEADER_LEN);
41 packet.back() = checksum(packet.data(), packet.size() - 1);
42 return packet;
43}
44
45static constexpr auto CONNECT_PACKET = make_packet(PACKET_TYPE_CONNECT_REQUEST, CONNECT_REQUEST_PAYLOAD);
46
48
50 switch (this->state_) {
52 // Defer the next request to a later loop iteration; some units might not respond if a request is sent
53 // immediately after a response. See https://github.com/esphome/esphome/issues/18099. No minimum RX-to-TX delay
54 // is enforced.
56 return false;
57
59 if (this->pending_updates_.any()) {
63 return false;
64 }
65 if (this->has_timed_out_(this->update_interval_ms_)) {
67 return false;
68 }
69 break;
70
74 if (this->has_timed_out_(RESPONSE_TIMEOUT_MS)) {
76 return false;
77 }
78 break;
79
80 default:
81 break;
82 }
83
84 return this->frame_parser_.read_and_parse(this->device_, [this](uint8_t type, const uint8_t *payload, size_t len) {
85 return this->process_rx_packet_(type, payload, len);
86 });
87}
88
90 if (should_transition(this->state_, new_state)) {
91 ESP_LOGV(TAG, "Did transition: %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
92 LOG_STR_ARG(state_to_string(new_state)));
93 this->state_ = new_state;
94 this->did_transition_(new_state);
95 } else {
96 ESP_LOGV(TAG, "Ignoring unexpected transition %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
97 LOG_STR_ARG(state_to_string(new_state)));
98 }
99}
100
102 switch (to) {
104 return from == State::NOT_CONNECTED || from == State::READ_TIMEOUT;
105
106 case State::CONNECTED:
107 return from == State::CONNECTING;
108
111
113 return from == State::UPDATING_STATUS;
114
116 return from == State::CONNECTED || from == State::STATUS_UPDATED;
117
119 return from == State::STATUS_UPDATED || from == State::SETTINGS_APPLIED;
120
123
126
128 return from == State::APPLYING_SETTINGS;
129
131 return from == State::UPDATING_STATUS || from == State::APPLYING_SETTINGS || from == State::CONNECTING;
132
134 return false;
135 }
136 return false;
137}
138
140 switch (to) {
142 this->send_packet_(CONNECT_PACKET);
143 break;
144
145 case State::CONNECTED:
146 this->current_status_msg_type_ = STATUS_MSG_SETTINGS;
148 break;
149
151 this->update_status_();
152 break;
153
155 // When present, pending settings are applied from WAITING_FOR_SCHEDULED_STATUS_UPDATE during the next update(),
156 // deferring transmission to a later loop iteration; some units might not respond if a request is sent
157 // immediately after a response, causing the request to time out.
158 const bool should_apply_pending_settings = this->pending_updates_.any() && this->is_status_initialized();
159 if (!should_apply_pending_settings && this->current_status_msg_type_ == STATUS_MSG_SETTINGS &&
161 this->current_status_msg_type_ = STATUS_MSG_TELEMETRY;
163 } else {
165 }
166 break;
167 }
168
172 this->current_status_msg_type_ = STATUS_MSG_SETTINGS;
174 break;
175
177 this->apply_settings_();
178 break;
179
182 break;
183
185 this->frame_parser_.reset();
188 break;
189
193 break;
194 }
195}
196
198 if (!this->is_telemetry_polling_enabled()) {
199 return false;
200 }
201
202 if (!this->last_telemetry_update_ms_.has_value()) {
203 return true;
204 }
205
207}
208
209void MitsubishiCN105::send_packet_(std::span<const uint8_t> packet) {
210 FrameParser::dump_buffer_vv("TX", packet.data(), packet.size());
211 this->device_.write_array(packet.data(), packet.size());
213}
214
216 std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload{this->current_status_msg_type_};
217 this->send_packet_(make_packet(PACKET_TYPE_STATUS_REQUEST, payload));
218}
219
220bool MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) {
221 switch (type) {
222 case PACKET_TYPE_CONNECT_RESPONSE:
224 return false;
225
226 case PACKET_TYPE_STATUS_RESPONSE:
227 return this->process_status_packet_(payload, len);
228
229 case PACKET_TYPE_WRITE_SETTINGS_RESPONSE:
231 return false;
232
233 default:
234 ESP_LOGVV(TAG, "RX unknown packet type 0x%02X", type);
235 return false;
236 }
237}
238
239bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len) {
240 if (len == 0) {
241 ESP_LOGVV(TAG, "RX status packet too short");
242 return false;
243 }
244
245 const auto previous = this->status_;
246 const auto msg_type = payload[0];
247 if (!this->parse_status_payload_(msg_type, payload + 1, len - 1)) {
248 return false;
249 }
250
251 if (msg_type == this->current_status_msg_type_) {
253 }
254
255 bool changed =
256 previous.power_on != this->status_.power_on || previous.mode != this->status_.mode ||
257 previous.fan_mode != this->status_.fan_mode || previous.target_temperature != this->status_.target_temperature ||
258 previous.vane_mode != this->status_.vane_mode || previous.wide_vane_mode != this->status_.wide_vane_mode;
259
260 if (this->is_telemetry_polling_enabled()) {
261 changed |= previous.room_temperature != this->status_.room_temperature;
262 }
263
264 return changed && this->is_status_initialized();
265}
266
267bool MitsubishiCN105::parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len) {
268 Property::Decoder decoder{std::span{payload, len}, this->property_context_, this->pending_updates_};
269 switch (msg_type) {
270 case STATUS_MSG_SETTINGS:
271 if (!decoder.decode_settings(this->status_)) {
272 ESP_LOGVV(TAG, "RX settings payload too short");
273 return false;
274 }
275 return true;
276
277 case STATUS_MSG_TELEMETRY:
278 if (!decoder.decode_room_temperature(this->status_)) {
279 ESP_LOGVV(TAG, "RX telemetry payload too short");
280 return false;
281 }
283 return true;
284
285 default:
286 ESP_LOGVV(TAG, "RX unsupported status msg type 0x%02X", msg_type);
287 return false;
288 }
289}
290
292 if (std::isnan(temperature)) {
293 ESP_LOGD(TAG, "Ignoring NaN remote temperature");
294 return;
295 }
297 ESP_LOGD(TAG, "Ignoring out-of-range remote temperature: %.1f", temperature);
298 return;
299 }
300 this->set_remote_temperature_half_deg_(static_cast<uint8_t>(std::round(temperature * 2.0f)));
301}
302
306
307void MitsubishiCN105::set_remote_temperature_half_deg_(uint8_t temperature_half_deg) {
308 this->remote_temperature_half_deg_ = temperature_half_deg;
310}
311
312void MitsubishiCN105::set_power(bool power_on) {
313 this->status_.power_on = power_on;
315}
316
319 ESP_LOGD(TAG, "Setting temperature out-of-range: %.1f", target_temperature);
320 return;
321 }
324}
325
328 ESP_LOGD(TAG, "Ignoring invalid mode: %u", static_cast<uint8_t>(mode));
329 }
330}
331
334 ESP_LOGD(TAG, "Ignoring invalid fan mode: %u", static_cast<uint8_t>(fan_mode));
335 }
336}
337
339 if (!Property::VaneMode::validate_and_set(vane_mode, this->status_, this->pending_updates_)) {
340 ESP_LOGD(TAG, "Ignoring invalid vane mode: %u", static_cast<uint8_t>(vane_mode));
341 }
342}
343
345 if (!Property::WideVaneMode::validate_and_set(wide_vane_mode, this->status_, this->pending_updates_)) {
346 ESP_LOGD(TAG, "Ignoring invalid wide vane mode: %u", static_cast<uint8_t>(wide_vane_mode));
347 }
348}
349
351 std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload{};
352 Property::Encoder encoder{payload.data(), this->property_context_, this->pending_updates_};
353
354 // Apply all other pending settings first; handle REMOTE_TEMPERATURE last
356 encoder.encode_remote_temperature(this->remote_temperature_half_deg_);
357 } else {
358 encoder.encode_settings(this->status_);
359 }
360
361 this->send_packet_(make_packet(PACKET_TYPE_WRITE_SETTINGS_REQUEST, payload));
362}
363
365 switch (state) {
367 return LOG_STR("Not connected");
369 return LOG_STR("Connecting");
370 case State::CONNECTED:
371 return LOG_STR("Connected");
373 return LOG_STR("UpdatingStatus");
375 return LOG_STR("StatusUpdated");
377 return LOG_STR("DeferredStatusRequest");
379 return LOG_STR("ScheduleNextStatusUpdate");
381 return LOG_STR("WaitingForScheduledStatusUpdate");
383 return LOG_STR("ApplyingSettings");
385 return LOG_STR("SettingsApplied");
387 return LOG_STR("ReadTimeout");
388 }
389 return LOG_STR("Unknown");
390}
391
392template<typename Callback>
394 uint8_t watchdog = 64;
395 while (device.available() > 0 && watchdog-- > 0) {
396 uint8_t &value = this->read_buffer_[this->read_pos_];
397 if (!device.read_byte(&value)) {
398 ESP_LOGW(TAG, "UART read failed while data available");
399 return false;
400 }
401
402 switch (++this->read_pos_) {
403 case 1:
404 if (value != PREAMBLE) {
405 this->reset_and_dump_buffer_("RX ignoring preamble");
406 }
407 continue;
408
409 case 2:
410 continue;
411
412 case 3:
413 if (value != HEADER_BYTE_1) {
414 this->reset_and_dump_buffer_("RX invalid: header 1 mismatch");
415 }
416 continue;
417
418 case 4:
419 if (value != HEADER_BYTE_2) {
420 this->reset_and_dump_buffer_("RX invalid: header 2 mismatch");
421 }
422 continue;
423
424 case HEADER_LEN:
425 static_assert(READ_BUFFER_SIZE > HEADER_LEN);
426 if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) {
427 this->reset_and_dump_buffer_("RX invalid: payload too large");
428 }
429 continue;
430
431 default:
432 break;
433 }
434
435 const size_t len_without_checksum = HEADER_LEN + static_cast<size_t>(this->read_buffer_[HEADER_LEN - 1]);
436 if (this->read_pos_ <= len_without_checksum) {
437 continue;
438 }
439
440 if (checksum(this->read_buffer_, len_without_checksum) != value) {
441 this->reset_and_dump_buffer_("RX invalid: checksum mismatch");
442 continue;
443 }
444
445 dump_buffer_vv("RX", this->read_buffer_, this->read_pos_);
446 const bool processed =
447 callback(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, len_without_checksum - HEADER_LEN);
448 this->read_pos_ = 0;
449 return processed;
450 }
451
452 return false;
453}
454
456 dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_);
457 this->read_pos_ = 0;
458}
459
460void MitsubishiCN105::FrameParser::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) {
461#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
462 char buf[format_hex_pretty_size(READ_BUFFER_SIZE)];
463 ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len));
464#endif
465}
466
467} // 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)
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
std::optional< uint32_t > last_telemetry_update_ms_
void send_packet_(std::span< const uint8_t > packet)
static const LogString * state_to_string(State state)
void set_target_temperature(float target_temperature)
bool parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len)
bool read_byte(uint8_t *data)
Definition uart.h:36
void write_array(const uint8_t *data, size_t len)
Definition uart.h:27
float target_temperature
Definition climate.h:0
ClimateFanMode fan_mode
Definition climate.h:3
uint8_t type
bool state
Definition fan.h:2
ESPHOME_LOG_TAG(TAG, "mitsubishi_cn105.driver")
constexpr uint8_t sum_bytes(const uint8_t *data, size_t len, uint8_t sum=0)
Add up the bytes of data with size len, truncated to 8 bits: the checksum many serial and IR protocol...
Definition helpers.h:886
const void size_t len
Definition hal.h:68
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:498
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:1533
static void uint32_t
Lightweight type-erased callback (8 bytes on 32-bit) that avoids std::function overhead.
Definition helpers.h:1730
static bool validate_and_set(Value value, Status &status, Mask &mask)
uint16_t temperature
Definition sun_gtil2.cpp:12
uint16_t length
Definition tt21100.cpp:0