ESPHome 2026.4.0-dev
Loading...
Searching...
No Matches
mitsubishi_cn105.cpp
Go to the documentation of this file.
1#include <array>
2#include <numeric>
3#include "mitsubishi_cn105.h"
4
6
7static const char *const TAG = "mitsubishi_cn105.driver";
8
9static constexpr uint32_t WRITE_TIMEOUT_MS = 2000;
10
11static constexpr size_t REQUEST_PAYLOAD_LEN = 0x10;
12static constexpr size_t HEADER_LEN = 5;
13static constexpr uint8_t PREAMBLE = 0xFC;
14static constexpr uint8_t HEADER_BYTE_1 = 0x01;
15static constexpr uint8_t HEADER_BYTE_2 = 0x30;
16
17static constexpr uint8_t PACKET_TYPE_CONNECT_REQUEST = 0x5A;
18static constexpr uint8_t PACKET_TYPE_CONNECT_RESPONSE = 0x7A;
19static constexpr std::array<uint8_t, 2> CONNECT_REQUEST_PAYLOAD = {0xCA, 0x01};
20
21static constexpr uint8_t PACKET_TYPE_STATUS_REQUEST = 0x42;
22static constexpr uint8_t PACKET_TYPE_STATUS_RESPONSE = 0x62;
23static constexpr uint8_t STATUS_MSG_SETTINGS = 0x02;
24static constexpr uint8_t STATUS_MSG_ROOM_TEMP = 0x03;
25static constexpr std::array<uint8_t, 2> STATUS_MSG_TYPES = {STATUS_MSG_SETTINGS, STATUS_MSG_ROOM_TEMP};
26
27static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) {
28 return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0}));
29}
30
31template<std::size_t PayloadSize>
32static constexpr auto make_packet(uint8_t type, const std::array<uint8_t, PayloadSize> &payload) {
33 const size_t full_len = PayloadSize + HEADER_LEN + 1;
34 std::array<uint8_t, full_len> packet{PREAMBLE, type, HEADER_BYTE_1, HEADER_BYTE_2, static_cast<uint8_t>(PayloadSize)};
35 std::copy_n(payload.begin(), PayloadSize, packet.begin() + HEADER_LEN);
36 packet.back() = checksum(packet.data(), packet.size() - 1);
37 return packet;
38}
39
40static float decode_temperature(int temp_a, int temp_b, int delta) {
41 return temp_b != 0 ? (temp_b - 128) / 2.0f : delta + temp_a;
42}
43
44static constexpr auto CONNECT_PACKET = make_packet(PACKET_TYPE_CONNECT_REQUEST, CONNECT_REQUEST_PAYLOAD);
45
47
49 if (const auto start = this->status_update_start_ms_;
50 start && (get_loop_time_ms() - *start) >= this->update_interval_ms_) {
52 return false;
53 }
54
55 if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) {
56 this->write_timeout_start_ms_.reset();
57 this->read_pos_ = 0;
59 return false;
60 }
61
62 return this->read_incoming_bytes_();
63}
64
66 if (should_transition(this->state_, new_state)) {
67 ESP_LOGV(TAG, "Did transition: %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
68 LOG_STR_ARG(state_to_string(new_state)));
69 this->state_ = new_state;
70 this->did_transition_(new_state);
71 } else {
72 ESP_LOGV(TAG, "Ignoring unexpected transition %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
73 LOG_STR_ARG(state_to_string(new_state)));
74 }
75}
76
78 switch (to) {
80 return from == State::NOT_CONNECTED || from == State::READ_TIMEOUT;
81
83 return from == State::CONNECTING;
84
86 return from == State::CONNECTED || from == State::STATUS_UPDATED ||
88
90 return from == State::UPDATING_STATUS;
91
93 return from == State::STATUS_UPDATED;
94
97
99 return from == State::UPDATING_STATUS || from == State::CONNECTING;
100
101 default:
102 return false;
103 }
104}
105
107 switch (to) {
109 this->send_packet_(CONNECT_PACKET);
110 break;
111
112 case State::CONNECTED:
113 this->write_timeout_start_ms_.reset();
114 this->status_msg_index_ = 0;
116 break;
117
119 this->update_status_();
120 break;
121
123 this->write_timeout_start_ms_.reset();
124 if (++this->status_msg_index_ >= STATUS_MSG_TYPES.size()) {
125 this->status_msg_index_ = 0;
126 }
127 if (this->status_msg_index_ != 0) {
129 } else {
131 }
132 break;
133 }
134
138 break;
139
142 break;
143
144 default:
145 break;
146 }
147}
148
149void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) {
150 dump_buffer_vv("TX", packet, len);
151 this->device_.write_array(packet, len);
153}
154
156 ESP_LOGV(TAG, "Requesting status update, index=%u", this->status_msg_index_);
157 std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload = {STATUS_MSG_TYPES[this->status_msg_index_]};
158 this->send_packet_(make_packet(PACKET_TYPE_STATUS_REQUEST, payload));
159}
160
165
167 uint8_t watchdog = 64;
168 while (this->device_.available() > 0 && watchdog-- > 0) {
169 uint8_t &value = this->read_buffer_[this->read_pos_];
170 if (!this->device_.read_byte(&value)) {
171 ESP_LOGW(TAG, "UART read failed while data available");
172 return false;
173 }
174
175 switch (++this->read_pos_) {
176 case 1:
177 if (value != PREAMBLE) {
178 this->reset_read_position_and_dump_buffer_("RX ignoring preamble");
179 }
180 continue;
181
182 case 2:
183 continue;
184
185 case 3:
186 if (value != HEADER_BYTE_1) {
187 this->reset_read_position_and_dump_buffer_("RX invalid: header 1 mismatch");
188 }
189 continue;
190
191 case 4:
192 if (value != HEADER_BYTE_2) {
193 this->reset_read_position_and_dump_buffer_("RX invalid: header 2 mismatch");
194 }
195 continue;
196
197 case HEADER_LEN:
198 static_assert(READ_BUFFER_SIZE > HEADER_LEN);
199 if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) {
200 this->reset_read_position_and_dump_buffer_("RX invalid: payload too large");
201 }
202 continue;
203
204 default:
205 break;
206 }
207
208 const size_t len_without_checksum = HEADER_LEN + static_cast<size_t>(this->read_buffer_[HEADER_LEN - 1]);
209 if (this->read_pos_ <= len_without_checksum) {
210 continue;
211 }
212
213 if (checksum(this->read_buffer_, len_without_checksum) != value) {
214 this->reset_read_position_and_dump_buffer_("RX invalid: checksum mismatch");
215 continue;
216 }
217
218 bool processed = this->process_rx_packet_(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN,
219 len_without_checksum - HEADER_LEN);
221 return processed;
222 }
223
224 return false;
225}
226
227bool MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) {
228 switch (type) {
229 case PACKET_TYPE_CONNECT_RESPONSE:
231 return false;
232
233 case PACKET_TYPE_STATUS_RESPONSE:
234 return this->process_status_packet_(payload, len);
235
236 default:
237 ESP_LOGVV(TAG, "RX unknown packet type 0x%02X", type);
238 return false;
239 }
240}
241
242bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len) {
243 if (len == 0) {
244 ESP_LOGVV(TAG, "RX status packet too short");
245 return false;
246 }
247
248 const auto previous = this->status_;
249 const auto msg_type = payload[0];
250 if (!this->parse_status_payload_(msg_type, payload + 1, len - 1)) {
251 return false;
252 }
253
254 if (msg_type == STATUS_MSG_TYPES[this->status_msg_index_]) {
256 }
257
258 return previous != this->status_ && this->is_status_initialized();
259}
260
261bool MitsubishiCN105::parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len) {
262 switch (msg_type) {
263 case STATUS_MSG_SETTINGS:
264 return this->parse_status_settings_(payload, len);
265
266 case STATUS_MSG_ROOM_TEMP:
267 return this->parse_status_room_temperature_(payload, len);
268
269 default:
270 ESP_LOGVV(TAG, "RX unsupported status msg type 0x%02X", msg_type);
271 return false;
272 }
273}
274
275bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len) {
276 if (len <= 10) {
277 ESP_LOGVV(TAG, "RX settings payload too short");
278 return false;
279 }
280
281 this->status_.power_on = payload[2] != 0;
282 this->status_.target_temperature = decode_temperature(-payload[4], payload[10], 31);
283
284 return true;
285}
286
287bool MitsubishiCN105::parse_status_room_temperature_(const uint8_t *payload, size_t len) {
288 if (len <= 5) {
289 ESP_LOGVV(TAG, "RX room temperature payload too short");
290 return false;
291 }
292
293 this->status_.room_temperature = decode_temperature(payload[2], payload[5], 10);
294 return true;
295}
296
298 dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_);
299 this->read_pos_ = 0;
300}
301
302void MitsubishiCN105::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) {
303#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
304 char buf[format_hex_pretty_size(READ_BUFFER_SIZE)];
305 ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len));
306#endif
307}
308
310 switch (state) {
312 return LOG_STR("Not connected");
314 return LOG_STR("Connecting");
315 case State::CONNECTED:
316 return LOG_STR("Connected");
318 return LOG_STR("UpdatingStatus");
320 return LOG_STR("StatusUpdated");
322 return LOG_STR("ScheduleNextStatusUpdate");
324 return LOG_STR("WaitingForScheduledStatusUpdate");
326 return LOG_STR("ReadTimeout");
327 }
328 return LOG_STR("Unknown");
329}
330
331} // namespace esphome::mitsubishi_cn105
uint8_t checksum
Definition bl0906.h:3
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)
void reset_read_position_and_dump_buffer_(const char *prefix)
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)
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
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:379
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
uint16_t length
Definition tt21100.cpp:0