ESPHome 2026.9.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>
6#include <numeric>
8
10
11static const char *const TAG = "mitsubishi_cn105.driver";
12
13static constexpr uint32_t RESPONSE_TIMEOUT_MS = 2000;
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_TELEMETRY = 0x03;
29
30static constexpr uint8_t PACKET_TYPE_WRITE_SETTINGS_REQUEST = 0x41;
31static constexpr uint8_t PACKET_TYPE_WRITE_SETTINGS_RESPONSE = 0x61;
32
33static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) {
34 return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0}));
35}
36
37template<std::size_t PayloadSize>
38static constexpr auto make_packet(uint8_t type, const std::array<uint8_t, PayloadSize> &payload) {
39 const size_t full_len = PayloadSize + HEADER_LEN + 1;
40 std::array<uint8_t, full_len> packet{PREAMBLE, type, HEADER_BYTE_1, HEADER_BYTE_2, static_cast<uint8_t>(PayloadSize)};
41 std::copy_n(payload.begin(), PayloadSize, packet.begin() + HEADER_LEN);
42 packet.back() = checksum(packet.data(), packet.size() - 1);
43 return packet;
44}
45
46static constexpr auto CONNECT_PACKET = make_packet(PACKET_TYPE_CONNECT_REQUEST, CONNECT_REQUEST_PAYLOAD);
47
49
51 switch (this->state_) {
53 // Defer the next request to a later loop iteration; some units might not respond if a request is sent
54 // immediately after a response. See https://github.com/esphome/esphome/issues/18099. No minimum RX-to-TX delay
55 // is enforced.
57 return false;
58
60 if (this->pending_updates_.any()) {
64 return false;
65 }
66 if (this->has_timed_out_(this->update_interval_ms_)) {
68 return false;
69 }
70 break;
71
75 if (this->has_timed_out_(RESPONSE_TIMEOUT_MS)) {
77 return false;
78 }
79 break;
80
81 default:
82 break;
83 }
84
85 return this->frame_parser_.read_and_parse(this->device_, [this](uint8_t type, const uint8_t *payload, size_t len) {
86 return this->process_rx_packet_(type, payload, len);
87 });
88}
89
91 if (should_transition(this->state_, new_state)) {
92 ESP_LOGV(TAG, "Did transition: %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
93 LOG_STR_ARG(state_to_string(new_state)));
94 this->state_ = new_state;
95 this->did_transition_(new_state);
96 } else {
97 ESP_LOGV(TAG, "Ignoring unexpected transition %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
98 LOG_STR_ARG(state_to_string(new_state)));
99 }
100}
101
103 switch (to) {
105 return from == State::NOT_CONNECTED || from == State::READ_TIMEOUT;
106
107 case State::CONNECTED:
108 return from == State::CONNECTING;
109
112
114 return from == State::UPDATING_STATUS;
115
117 return from == State::CONNECTED || from == State::STATUS_UPDATED;
118
120 return from == State::STATUS_UPDATED || from == State::SETTINGS_APPLIED;
121
124
127
129 return from == State::APPLYING_SETTINGS;
130
132 return from == State::UPDATING_STATUS || from == State::APPLYING_SETTINGS || from == State::CONNECTING;
133
135 return false;
136 }
137 return false;
138}
139
141 switch (to) {
143 this->send_packet_(CONNECT_PACKET);
144 break;
145
146 case State::CONNECTED:
147 this->current_status_msg_type_ = STATUS_MSG_SETTINGS;
149 break;
150
152 this->update_status_();
153 break;
154
156 // When present, pending settings are applied from WAITING_FOR_SCHEDULED_STATUS_UPDATE during the next update(),
157 // deferring transmission to a later loop iteration; some units might not respond if a request is sent
158 // immediately after a response, causing the request to time out.
159 const bool should_apply_pending_settings = this->pending_updates_.any() && this->is_status_initialized();
160 if (!should_apply_pending_settings && this->current_status_msg_type_ == STATUS_MSG_SETTINGS &&
162 this->current_status_msg_type_ = STATUS_MSG_TELEMETRY;
164 } else {
166 }
167 break;
168 }
169
173 this->current_status_msg_type_ = STATUS_MSG_SETTINGS;
175 break;
176
178 this->apply_settings_();
179 break;
180
183 break;
184
186 this->frame_parser_.reset();
189 break;
190
194 break;
195 }
196}
197
199 if (!this->is_telemetry_polling_enabled()) {
200 return false;
201 }
202
203 if (!this->last_telemetry_update_ms_.has_value()) {
204 return true;
205 }
206
208}
209
210void MitsubishiCN105::send_packet_(std::span<const uint8_t> packet) {
211 FrameParser::dump_buffer_vv("TX", packet.data(), packet.size());
212 this->device_.write_array(packet.data(), packet.size());
214}
215
217 std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload{this->current_status_msg_type_};
218 this->send_packet_(make_packet(PACKET_TYPE_STATUS_REQUEST, payload));
219}
220
221bool MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) {
222 switch (type) {
223 case PACKET_TYPE_CONNECT_RESPONSE:
225 return false;
226
227 case PACKET_TYPE_STATUS_RESPONSE:
228 return this->process_status_packet_(payload, len);
229
230 case PACKET_TYPE_WRITE_SETTINGS_RESPONSE:
232 return false;
233
234 default:
235 ESP_LOGVV(TAG, "RX unknown packet type 0x%02X", type);
236 return false;
237 }
238}
239
240bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len) {
241 if (len == 0) {
242 ESP_LOGVV(TAG, "RX status packet too short");
243 return false;
244 }
245
246 const auto previous = this->status_;
247 const auto msg_type = payload[0];
248 if (!this->parse_status_payload_(msg_type, payload + 1, len - 1)) {
249 return false;
250 }
251
252 if (msg_type == this->current_status_msg_type_) {
254 }
255
256 bool changed =
257 previous.power_on != this->status_.power_on || previous.mode != this->status_.mode ||
258 previous.fan_mode != this->status_.fan_mode || previous.target_temperature != this->status_.target_temperature ||
259 previous.vane_mode != this->status_.vane_mode || previous.wide_vane_mode != this->status_.wide_vane_mode;
260
261 if (this->is_telemetry_polling_enabled()) {
262 changed |= previous.room_temperature != this->status_.room_temperature;
263 }
264
265 return changed && this->is_status_initialized();
266}
267
268bool MitsubishiCN105::parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len) {
269 Property::Decoder decoder{std::span{payload, len}, this->property_context_, this->pending_updates_};
270 switch (msg_type) {
271 case STATUS_MSG_SETTINGS:
272 if (!decoder.decode_settings(this->status_)) {
273 ESP_LOGVV(TAG, "RX settings payload too short");
274 return false;
275 }
276 return true;
277
278 case STATUS_MSG_TELEMETRY:
279 if (!decoder.decode_room_temperature(this->status_)) {
280 ESP_LOGVV(TAG, "RX telemetry payload too short");
281 return false;
282 }
284 return true;
285
286 default:
287 ESP_LOGVV(TAG, "RX unsupported status msg type 0x%02X", msg_type);
288 return false;
289 }
290}
291
293 if (std::isnan(temperature)) {
294 ESP_LOGD(TAG, "Ignoring NaN remote temperature");
295 return;
296 }
298 ESP_LOGD(TAG, "Ignoring out-of-range remote temperature: %.1f", temperature);
299 return;
300 }
301 this->set_remote_temperature_half_deg_(static_cast<uint8_t>(std::round(temperature * 2.0f)));
302}
303
307
308void MitsubishiCN105::set_remote_temperature_half_deg_(uint8_t temperature_half_deg) {
309 this->remote_temperature_half_deg_ = temperature_half_deg;
311}
312
313void MitsubishiCN105::set_power(bool power_on) {
314 this->status_.power_on = power_on;
316}
317
320 ESP_LOGD(TAG, "Setting temperature out-of-range: %.1f", target_temperature);
321 return;
322 }
325}
326
329 ESP_LOGD(TAG, "Ignoring invalid mode: %u", static_cast<uint8_t>(mode));
330 }
331}
332
335 ESP_LOGD(TAG, "Ignoring invalid fan mode: %u", static_cast<uint8_t>(fan_mode));
336 }
337}
338
340 if (!Property::VaneMode::validate_and_set(vane_mode, this->status_, this->pending_updates_)) {
341 ESP_LOGD(TAG, "Ignoring invalid vane mode: %u", static_cast<uint8_t>(vane_mode));
342 }
343}
344
346 if (!Property::WideVaneMode::validate_and_set(wide_vane_mode, this->status_, this->pending_updates_)) {
347 ESP_LOGD(TAG, "Ignoring invalid wide vane mode: %u", static_cast<uint8_t>(wide_vane_mode));
348 }
349}
350
352 std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload{};
353 Property::Encoder encoder{payload.data(), this->property_context_, this->pending_updates_};
354
355 // Apply all other pending settings first; handle REMOTE_TEMPERATURE last
357 encoder.encode_remote_temperature(this->remote_temperature_half_deg_);
358 } else {
359 encoder.encode_settings(this->status_);
360 }
361
362 this->send_packet_(make_packet(PACKET_TYPE_WRITE_SETTINGS_REQUEST, payload));
363}
364
366 switch (state) {
368 return LOG_STR("Not connected");
370 return LOG_STR("Connecting");
371 case State::CONNECTED:
372 return LOG_STR("Connected");
374 return LOG_STR("UpdatingStatus");
376 return LOG_STR("StatusUpdated");
378 return LOG_STR("DeferredStatusRequest");
380 return LOG_STR("ScheduleNextStatusUpdate");
382 return LOG_STR("WaitingForScheduledStatusUpdate");
384 return LOG_STR("ApplyingSettings");
386 return LOG_STR("SettingsApplied");
388 return LOG_STR("ReadTimeout");
389 }
390 return LOG_STR("Unknown");
391}
392
393template<typename Callback>
395 uint8_t watchdog = 64;
396 while (device.available() > 0 && watchdog-- > 0) {
397 uint8_t &value = this->read_buffer_[this->read_pos_];
398 if (!device.read_byte(&value)) {
399 ESP_LOGW(TAG, "UART read failed while data available");
400 return false;
401 }
402
403 switch (++this->read_pos_) {
404 case 1:
405 if (value != PREAMBLE) {
406 this->reset_and_dump_buffer_("RX ignoring preamble");
407 }
408 continue;
409
410 case 2:
411 continue;
412
413 case 3:
414 if (value != HEADER_BYTE_1) {
415 this->reset_and_dump_buffer_("RX invalid: header 1 mismatch");
416 }
417 continue;
418
419 case 4:
420 if (value != HEADER_BYTE_2) {
421 this->reset_and_dump_buffer_("RX invalid: header 2 mismatch");
422 }
423 continue;
424
425 case HEADER_LEN:
426 static_assert(READ_BUFFER_SIZE > HEADER_LEN);
427 if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) {
428 this->reset_and_dump_buffer_("RX invalid: payload too large");
429 }
430 continue;
431
432 default:
433 break;
434 }
435
436 const size_t len_without_checksum = HEADER_LEN + static_cast<size_t>(this->read_buffer_[HEADER_LEN - 1]);
437 if (this->read_pos_ <= len_without_checksum) {
438 continue;
439 }
440
441 if (checksum(this->read_buffer_, len_without_checksum) != value) {
442 this->reset_and_dump_buffer_("RX invalid: checksum mismatch");
443 continue;
444 }
445
446 dump_buffer_vv("RX", this->read_buffer_, this->read_pos_);
447 const bool processed =
448 callback(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, len_without_checksum - HEADER_LEN);
449 this->read_pos_ = 0;
450 return processed;
451 }
452
453 return false;
454}
455
457 dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_);
458 this->read_pos_ = 0;
459}
460
461void MitsubishiCN105::FrameParser::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) {
462#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
463 char buf[format_hex_pretty_size(READ_BUFFER_SIZE)];
464 ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len));
465#endif
466}
467
468} // 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: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:425
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:1438
static void uint32_t
Lightweight type-erased callback (8 bytes on 32-bit) that avoids std::function overhead.
Definition helpers.h:1673
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