ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
tuya.cpp
Go to the documentation of this file.
1#include "tuya.h"
2#include "esphome/core/gpio.h"
4#include "esphome/core/log.h"
5#include "esphome/core/util.h"
6
7#ifdef USE_NETWORK
9#endif
10
11#ifdef USE_WIFI
13#endif
14
15#ifdef USE_CAPTIVE_PORTAL
17#endif
18
19namespace esphome::tuya {
20
21ESPHOME_LOG_TAG(TAG, "tuya");
22
23static constexpr uint32_t HEARTBEAT_INTERVAL_ID = 0;
24static constexpr uint32_t WIFI_STATUS_INTERVAL_ID = 1;
25static constexpr uint32_t DATAPOINT_DUMP_TIMEOUT_ID = 2;
26static constexpr uint32_t FORCE_TIME_SYNC_INTERVAL_ID = 3;
27static const int COMMAND_DELAY = 10;
28static const int RECEIVE_TIMEOUT = 300;
29static const int MAX_RETRIES = 5;
30// Max bytes to log for datapoint values (larger values are truncated)
31static constexpr size_t MAX_DATAPOINT_LOG_BYTES = 16;
32static constexpr uint32_t FORCE_TIME_SYNC_CHECK_INTERVAL = 1000;
33
34static bool network_is_connected() {
35#ifdef USE_NETWORK
36 return network::is_connected();
37#else
38 return false;
39#endif
40}
41
43 this->set_interval(HEARTBEAT_INTERVAL_ID, 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
44#ifdef USE_TIME
45 if (this->force_time_sync_) {
47 this->set_interval(FORCE_TIME_SYNC_INTERVAL_ID, FORCE_TIME_SYNC_CHECK_INTERVAL,
48 [this] { this->check_force_time_sync_(); });
49 }
50#endif
51 if (this->status_pin_ != nullptr) {
52 this->status_pin_->digital_write(false);
53 }
54}
55
56void Tuya::loop() {
57 // Read all available bytes in batches to reduce UART call overhead.
58 size_t avail = this->available();
59 uint8_t buf[64];
60 while (avail > 0) {
61 size_t to_read = std::min(avail, sizeof(buf));
62 if (!this->read_array(buf, to_read)) {
63 break;
64 }
65 avail -= to_read;
66
67 for (size_t i = 0; i < to_read; i++) {
68 this->handle_char_(buf[i]);
69 }
70 }
72}
73
75 ESP_LOGCONFIG(TAG, "Tuya:");
77 if (this->init_failed_) {
78 ESP_LOGCONFIG(TAG, " Initialization failed. Current init_state: %u", static_cast<uint8_t>(this->init_state_));
79 } else {
80 ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u",
81 static_cast<uint8_t>(this->init_state_));
82 }
83 ESP_LOGCONFIG(TAG, " If no further output is received, confirm that this is a supported Tuya device.");
84 return;
85 }
86 for (auto &info : this->datapoints_) {
87 if (info.type == TuyaDatapointType::RAW) {
88 char hex_buf[format_hex_pretty_size(MAX_DATAPOINT_LOG_BYTES)];
89 ESP_LOGCONFIG(TAG, " Datapoint %u: raw (value: %s)", info.id,
90 format_hex_pretty_to(hex_buf, info.value_raw.data(), info.value_raw.size()));
91 } else if (info.type == TuyaDatapointType::BOOLEAN) {
92 ESP_LOGCONFIG(TAG, " Datapoint %u: switch (value: %s)", info.id, ONOFF(info.value_bool));
93 } else if (info.type == TuyaDatapointType::INTEGER) {
94 ESP_LOGCONFIG(TAG, " Datapoint %u: int value (value: %d)", info.id, info.value_int);
95 } else if (info.type == TuyaDatapointType::STRING) {
96 ESP_LOGCONFIG(TAG, " Datapoint %u: string value (value: %s)", info.id, info.value_string.c_str());
97 } else if (info.type == TuyaDatapointType::ENUM) {
98 ESP_LOGCONFIG(TAG, " Datapoint %u: enum (value: %d)", info.id, info.value_enum);
99 } else if (info.type == TuyaDatapointType::BITMASK) {
100 ESP_LOGCONFIG(TAG, " Datapoint %u: bitmask (value: %" PRIx32 ")", info.id, info.value_bitmask);
101 } else {
102 ESP_LOGCONFIG(TAG, " Datapoint %u: unknown", info.id);
103 }
104 }
105 if ((this->status_pin_reported_ != -1) || (this->reset_pin_reported_ != -1)) {
106 ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d", this->status_pin_reported_,
107 this->reset_pin_reported_);
108 }
109 LOG_PIN(" Status Pin: ", this->status_pin_);
110 ESP_LOGCONFIG(TAG, " Product: '%s'",
111 this->product_invalid_ ? LOG_STR_LITERAL(R"({"p":"INVALID"})")
112 : (this->product_ ? this->product_.get() : LOG_STR_LITERAL("")));
113#ifdef USE_TIME
114 if (this->time_id_ != nullptr) {
115 ESP_LOGCONFIG(TAG, " Force time sync: %s", YESNO(this->force_time_sync_));
116 }
117#endif
118}
119
121 uint32_t at = this->rx_message_.size() - 1;
122 auto *data = &this->rx_message_[0];
123 uint8_t new_byte = data[at];
124
125 // Byte 0: HEADER1 (always 0x55)
126 if (at == 0)
127 return new_byte == 0x55;
128 // Byte 1: HEADER2 (always 0xAA)
129 if (at == 1)
130 return new_byte == 0xAA;
131
132 // Byte 2: VERSION
133 // no validation for the following fields:
134 uint8_t version = data[2];
135 if (at == 2)
136 return true;
137 // Byte 3: COMMAND
138 uint8_t command = data[3];
139 if (at == 3)
140 return true;
141
142 // Byte 4: LENGTH1
143 // Byte 5: LENGTH2
144 if (at <= 5) {
145 // no validation for these fields
146 return true;
147 }
148
149 uint16_t length = (uint16_t(data[4]) << 8) | (uint16_t(data[5]));
150
151 // wait until all data is read
152 if (at - 6 < length)
153 return true;
154
155 // Byte 6+LEN: CHECKSUM - sum of all bytes (including header) modulo 256
156 uint8_t rx_checksum = new_byte;
157 const uint8_t calc_checksum = sum_bytes(data, 6 + length);
158
159 if (rx_checksum != calc_checksum) {
160 ESP_LOGW(TAG, "Tuya Received invalid message checksum %02X!=%02X", rx_checksum, calc_checksum);
161 return false;
162 }
163
164 // valid message
165 const uint8_t *message_data = data + 6;
166#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
167 char hex_buf[format_hex_pretty_size(MAX_DATAPOINT_LOG_BYTES)];
168 ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version,
169 format_hex_pretty_to(hex_buf, message_data, length), static_cast<uint8_t>(this->init_state_));
170#endif
171 this->handle_command_(command, version, message_data, length);
172
173 // return false to reset rx buffer
174 return false;
175}
176
177void Tuya::handle_char_(uint8_t c) {
178 this->rx_message_.push_back(c);
179 if (!this->validate_message_()) {
180 this->rx_message_.clear();
181 } else {
183 }
184}
185
186void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) {
187 TuyaCommandType command_type = (TuyaCommandType) command;
188
189 if (this->expected_response_.has_value() && this->expected_response_ == command_type) {
190 this->expected_response_.reset();
191 this->command_queue_.erase(command_queue_.begin());
192 this->init_retries_ = 0;
193 }
194
195 switch (command_type) {
197 ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]);
198 this->protocol_version_ = version;
199 if (buffer[0] == 0) {
200 ESP_LOGI(TAG, "MCU restarted");
202 }
206 }
207 break;
209 // check it is a valid string made up of printable characters
210 bool valid = true;
211 for (size_t i = 0; i < len; i++) {
212 if (!std::isprint(buffer[i])) {
213 valid = false;
214 break;
215 }
216 }
217 if (valid) {
218 this->product_ = std::make_unique<char[]>(len + 1);
219 memcpy(this->product_.get(), buffer, len);
220 } else {
221 this->product_.reset();
222 }
223 this->product_invalid_ = !valid;
227 }
228 break;
229 }
231 if (len >= 2) {
232 this->status_pin_reported_ = buffer[0];
233 this->reset_pin_reported_ = buffer[1];
234 }
236 // If mcu returned status gpio, then we can omit sending wifi state
237 if (this->status_pin_reported_ != -1) {
240 if (this->status_pin_ != nullptr) {
241 if (this->status_pin_->get_pin() != this->status_pin_reported_) {
242 ESP_LOGW(TAG, "Supplied status_pin does not equal the reported pin %i. Using supplied pin anyway.",
244 }
245 ESP_LOGV(TAG, "Configured status pin %i", this->status_pin_->get_pin());
246 this->set_interval(WIFI_STATUS_INTERVAL_ID, 1000, [this] { this->set_status_pin_(); });
247 } else {
248 ESP_LOGW(TAG, "MCU reported status_pin %i but no status_pin was configured; running in limited mode.",
250 }
251 } else {
253 ESP_LOGV(TAG, "Configured WIFI_STATE periodic send");
254 this->set_interval(WIFI_STATUS_INTERVAL_ID, 1000, [this] { this->send_wifi_status_(); });
255 }
256 }
257 break;
258 }
263 }
264 break;
267 const bool is_select = (len >= 1);
268 // Send WIFI_SELECT ACK
271 ack.payload.clear();
272 this->send_command_(ack);
273 // Establish pairing mode for correct first WIFI_STATE byte, EZ (0x00) default
274 uint8_t first = 0x00;
275 const char *mode_str = "EZ";
276 if (is_select && buffer[0] == 0x01) {
277 first = 0x01;
278 mode_str = "AP";
279 }
280 // Send WIFI_STATE response, MCU exits pairing mode
281 TuyaCommand st;
283 st.payload.resize(1);
284 st.payload[0] = first;
285 this->send_command_(st);
286 st.payload[0] = 0x02;
287 this->send_command_(st);
288 st.payload[0] = 0x03;
289 this->send_command_(st);
290 st.payload[0] = 0x04;
291 this->send_command_(st);
292 ESP_LOGI(TAG, "%s received (%s), replied with WIFI_STATE confirming connection established",
293 is_select ? LOG_STR_LITERAL("WIFI_SELECT") : LOG_STR_LITERAL("WIFI_RESET"), mode_str);
294 break;
295 }
297 break;
302 this->set_timeout(DATAPOINT_DUMP_TIMEOUT_ID, 1000, [this] { this->dump_config(); });
303 this->initialized_callback_.call();
304 }
305 this->handle_datapoints_(buffer, len);
306
307 if (command_type == TuyaCommandType::DATAPOINT_REPORT_SYNC) {
308 this->send_command_(
309 TuyaCommand{.cmd = TuyaCommandType::DATAPOINT_REPORT_ACK, .payload = std::vector<uint8_t>{0x01}});
310 }
311 break;
313 break;
315 this->send_command_(TuyaCommand{.cmd = TuyaCommandType::WIFI_TEST, .payload = std::vector<uint8_t>{0x00, 0x00}});
316 break;
318 this->send_command_(
319 TuyaCommand{.cmd = TuyaCommandType::WIFI_RSSI, .payload = std::vector<uint8_t>{get_wifi_rssi_()}});
320 break;
322#ifdef USE_TIME
323 if (this->time_id_ != nullptr) {
324 this->send_local_time_();
326 } else
327#endif
328 {
329 ESP_LOGW(TAG, "LOCAL_TIME_QUERY is not handled because time is not configured");
330 }
331 break;
333#ifdef USE_TIME
334 if (this->time_id_ != nullptr) {
335 this->send_gmt_time_();
336
338 // tuya mcu supports time, so we let them know when our time changed
339 this->time_id_->add_on_time_sync_callback([this] { this->send_gmt_time_(); });
341 }
342 } else
343#endif
344 {
345 ESP_LOGW(TAG, "GMT_TIME_QUERY is not handled because time is not configured");
346 }
347 break;
349 this->send_command_(
350 TuyaCommand{.cmd = TuyaCommandType::VACUUM_MAP_UPLOAD, .payload = std::vector<uint8_t>{0x01}});
351 ESP_LOGW(TAG, "Vacuum map upload requested, responding that it is not enabled.");
352 break;
354 uint8_t wifi_status = this->get_wifi_status_code_();
355
356 this->send_command_(
357 TuyaCommand{.cmd = TuyaCommandType::GET_NETWORK_STATUS, .payload = std::vector<uint8_t>{wifi_status}});
358 ESP_LOGV(TAG, "Network status requested, reported as %i", wifi_status);
359 break;
360 }
362 uint8_t subcommand = buffer[0];
363 switch ((TuyaExtendedServicesCommandType) subcommand) {
365 this->send_command_(
367 .payload = std::vector<uint8_t>{
368 static_cast<uint8_t>(TuyaExtendedServicesCommandType::RESET_NOTIFICATION), 0x00}});
369 ESP_LOGV(TAG, "Reset status notification enabled");
370 break;
371 }
373 ESP_LOGE(TAG, "EXTENDED_SERVICES::MODULE_RESET is not handled");
374 break;
375 }
377 ESP_LOGE(TAG, "EXTENDED_SERVICES::UPDATE_IN_PROGRESS is not handled");
378 break;
379 }
380 default:
381 ESP_LOGE(TAG, "Invalid extended services subcommand (0x%02X) received", subcommand);
382 }
383 break;
384 }
385 default:
386 ESP_LOGE(TAG, "Invalid command (0x%02X) received", command);
387 }
388}
389
390void Tuya::handle_datapoints_(const uint8_t *buffer, size_t len) {
391 while (len >= 4) {
392 TuyaDatapoint datapoint{};
393 datapoint.id = buffer[0];
394 datapoint.type = (TuyaDatapointType) buffer[1];
395 datapoint.value_uint = 0;
396
397 size_t data_size = (buffer[2] << 8) + buffer[3];
398 const uint8_t *data = buffer + 4;
399 size_t data_len = len - 4;
400 if (data_size > data_len) {
401 ESP_LOGW(TAG, "Datapoint %u is truncated and cannot be parsed (%zu > %zu)", datapoint.id, data_size, data_len);
402 return;
403 }
404
405 datapoint.len = data_size;
406
407 switch (datapoint.type) {
409 datapoint.value_raw = std::vector<uint8_t>(data, data + data_size);
410 {
411 char hex_buf[format_hex_pretty_size(MAX_DATAPOINT_LOG_BYTES)];
412 ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id,
413 format_hex_pretty_to(hex_buf, datapoint.value_raw.data(), datapoint.value_raw.size()));
414 }
415 break;
417 if (data_size != 1) {
418 ESP_LOGW(TAG, "Datapoint %u has bad boolean len %zu", datapoint.id, data_size);
419 return;
420 }
421 datapoint.value_bool = data[0];
422 ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, ONOFF(datapoint.value_bool));
423 break;
425 if (data_size != 4) {
426 ESP_LOGW(TAG, "Datapoint %u has bad integer len %zu", datapoint.id, data_size);
427 return;
428 }
429 datapoint.value_uint = encode_uint32(data[0], data[1], data[2], data[3]);
430 ESP_LOGD(TAG, "Datapoint %u update to %d", datapoint.id, datapoint.value_int);
431 break;
433 datapoint.value_string = std::string(reinterpret_cast<const char *>(data), data_size);
434 ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, datapoint.value_string.c_str());
435 break;
437 if (data_size != 1) {
438 ESP_LOGW(TAG, "Datapoint %u has bad enum len %zu", datapoint.id, data_size);
439 return;
440 }
441 datapoint.value_enum = data[0];
442 ESP_LOGD(TAG, "Datapoint %u update to %d", datapoint.id, datapoint.value_enum);
443 break;
445 switch (data_size) {
446 case 1:
447 datapoint.value_bitmask = encode_uint32(0, 0, 0, data[0]);
448 break;
449 case 2:
450 datapoint.value_bitmask = encode_uint32(0, 0, data[0], data[1]);
451 break;
452 case 4:
453 datapoint.value_bitmask = encode_uint32(data[0], data[1], data[2], data[3]);
454 break;
455 default:
456 ESP_LOGW(TAG, "Datapoint %u has bad bitmask len %zu", datapoint.id, data_size);
457 return;
458 }
459 ESP_LOGD(TAG, "Datapoint %u update to %#08" PRIX32, datapoint.id, datapoint.value_bitmask);
460 break;
461 default:
462 ESP_LOGW(TAG, "Datapoint %u has unknown type %#02hhX", datapoint.id, static_cast<uint8_t>(datapoint.type));
463 return;
464 }
465
466 len -= data_size + 4;
467 buffer = data + data_size;
468
469 // drop update if datapoint is in ignore_mcu_datapoint_update list
470 bool skip = false;
471 for (auto i : this->ignore_mcu_update_on_datapoints_) {
472 if (datapoint.id == i) {
473 ESP_LOGV(TAG, "Datapoint %u found in ignore_mcu_update_on_datapoints list, dropping MCU update", datapoint.id);
474 skip = true;
475 break;
476 }
477 }
478 if (skip)
479 continue;
480
481 // Update internal datapoints
482 bool found = false;
483 for (auto &other : this->datapoints_) {
484 if (other.id == datapoint.id) {
485 other = datapoint;
486 found = true;
487 }
488 }
489 if (!found) {
490 this->datapoints_.push_back(datapoint);
491 }
492
493 // Run through listeners
494 for (auto &listener : this->listeners_) {
495 if (listener.datapoint_id == datapoint.id)
496 listener.on_datapoint.call(datapoint);
497 }
498 }
499}
500
502 uint8_t len_hi = (uint8_t) (command.payload.size() >> 8);
503 uint8_t len_lo = (uint8_t) (command.payload.size() & 0xFF);
504 uint8_t version = 0;
505
507 switch (command.cmd) {
510 break;
513 break;
516 break;
520 break;
521 default:
522 break;
523 }
524
525#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
526 char hex_buf[format_hex_pretty_size(MAX_DATAPOINT_LOG_BYTES)];
527 ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", static_cast<uint8_t>(command.cmd),
528 version, format_hex_pretty_to(hex_buf, command.payload.data(), command.payload.size()),
529 static_cast<uint8_t>(this->init_state_));
530#endif
531
532 this->write_array({0x55, 0xAA, version, (uint8_t) command.cmd, len_hi, len_lo});
533 if (!command.payload.empty())
534 this->write_array(command.payload.data(), command.payload.size());
535
536 const uint8_t header_sum = 0x55 + 0xAA + (uint8_t) command.cmd + len_hi + len_lo;
537 this->write_byte(sum_bytes(command.payload.data(), command.payload.size(), header_sum));
538}
539
541 uint32_t now = millis();
543
544 if (now - this->last_rx_char_timestamp_ > RECEIVE_TIMEOUT) {
545 this->rx_message_.clear();
546 }
547
548 if (this->expected_response_.has_value() && delay > RECEIVE_TIMEOUT) {
549 this->expected_response_.reset();
551 if (++this->init_retries_ >= MAX_RETRIES) {
552 this->init_failed_ = true;
553 ESP_LOGE(TAG, "Initialization failed at init_state %u", static_cast<uint8_t>(this->init_state_));
554 this->command_queue_.erase(command_queue_.begin());
555 this->init_retries_ = 0;
556 }
557 } else {
558 this->command_queue_.erase(command_queue_.begin());
559 }
560 }
561
562 // Left check of delay since last command in case there's ever a command sent by calling send_raw_command_ directly
563 if (delay > COMMAND_DELAY && !this->command_queue_.empty() && this->rx_message_.empty() &&
564 !this->expected_response_.has_value()) {
565 this->send_raw_command_(command_queue_.front());
566 if (!this->expected_response_.has_value())
567 this->command_queue_.erase(command_queue_.begin());
568 }
569}
570
571void Tuya::send_command_(const TuyaCommand &command) {
572 command_queue_.push_back(command);
574}
575
577 send_command_(TuyaCommand{.cmd = command, .payload = std::vector<uint8_t>{}});
578}
579
581 bool is_network_ready = network_is_connected() && remote_is_connected();
582 this->status_pin_->digital_write(is_network_ready);
583}
584
586 uint8_t status = 0x02;
587
588 if (network_is_connected()) {
589 status = 0x03;
590
591 // Protocol version 3 also supports specifying when connected to "the cloud"
592 if (this->protocol_version_ >= 0x03 && remote_is_connected()) {
593 status = 0x04;
594 }
595 } else {
596#ifdef USE_CAPTIVE_PORTAL
598 status = 0x01;
599 }
600#endif
601 };
602
603 return status;
604}
605
607#ifdef USE_WIFI
608 if (wifi::global_wifi_component != nullptr)
610#endif
611
612 return 0;
613}
614
616 uint8_t status = this->get_wifi_status_code_();
617
618 if (status == this->wifi_status_) {
619 return;
620 }
621
622 ESP_LOGD(TAG, "Sending WiFi Status");
623 this->wifi_status_ = status;
624 this->send_command_(TuyaCommand{.cmd = TuyaCommandType::WIFI_STATE, .payload = std::vector<uint8_t>{status}});
625}
626
627#ifdef USE_TIME
630 return;
631 }
632 this->time_id_->add_on_time_sync_callback([this] { this->send_local_time_(); });
634}
635
637 ESPTime now = this->time_id_->now();
638 if (!now.is_valid() || now.minute == this->last_force_time_sync_minute_)
639 return;
641 this->send_local_time_(true);
642}
643
644void Tuya::send_local_time_(bool periodic) {
645 std::vector<uint8_t> payload;
646 ESPTime now = this->time_id_->now();
647 if (now.is_valid()) {
648 uint8_t year = now.year - 2000;
649 uint8_t month = now.month;
650 uint8_t day_of_month = now.day_of_month;
651 uint8_t hour = now.hour;
652 uint8_t minute = now.minute;
653 uint8_t second = now.second;
654 // Tuya days starts from Monday, esphome uses Sunday as day 1
655 uint8_t day_of_week = now.day_of_week - 1;
656 if (day_of_week == 0) {
657 day_of_week = 7;
658 }
659 // The forced send repeats every minute, so it stays out of the debug log
660 if (periodic) {
661 ESP_LOGV(TAG, "Sending local time");
662 } else {
663 ESP_LOGD(TAG, "Sending local time");
664 }
665 payload = std::vector<uint8_t>{0x01, year, month, day_of_month, hour, minute, second, day_of_week};
666 } else {
667 // By spec we need to notify MCU that the time was not obtained if this is a response to a query
668 ESP_LOGW(TAG, "Sending missing local time");
669 payload = std::vector<uint8_t>{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
670 }
671 this->send_command_(TuyaCommand{.cmd = TuyaCommandType::LOCAL_TIME_QUERY, .payload = payload});
672}
674 std::vector<uint8_t> payload;
675 ESPTime now = this->time_id_->utcnow();
676 if (now.is_valid()) {
677 uint8_t year = now.year - 2000;
678 uint8_t month = now.month;
679 uint8_t day_of_month = now.day_of_month;
680 uint8_t hour = now.hour;
681 uint8_t minute = now.minute;
682 uint8_t second = now.second;
683 ESP_LOGD(TAG, "Sending gmt time");
684 payload = std::vector<uint8_t>{0x01, year, month, day_of_month, hour, minute, second};
685 } else {
686 // By spec we need to notify MCU that the time was not obtained if this is a response to a query
687 ESP_LOGW(TAG, "Sending missing gmt time");
688 payload = std::vector<uint8_t>{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
689 }
690 this->send_command_(TuyaCommand{.cmd = TuyaCommandType::GMT_TIME_QUERY, .payload = payload});
691}
692#endif
693
694void Tuya::set_raw_datapoint_value(uint8_t datapoint_id, const std::vector<uint8_t> &value) {
695 this->set_raw_datapoint_value_(datapoint_id, value, false);
696}
697
698void Tuya::set_boolean_datapoint_value(uint8_t datapoint_id, bool value) {
699 this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::BOOLEAN, value, 1, false);
700}
701
702void Tuya::set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value) {
703 this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::INTEGER, value, 4, false);
704}
705
706void Tuya::set_string_datapoint_value(uint8_t datapoint_id, const std::string &value) {
707 this->set_string_datapoint_value_(datapoint_id, value, false);
708}
709
710void Tuya::set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value) {
711 this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::ENUM, value, 1, false);
712}
713
714void Tuya::set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length) {
715 this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::BITMASK, value, length, false);
716}
717
718void Tuya::force_set_raw_datapoint_value(uint8_t datapoint_id, const std::vector<uint8_t> &value) {
719 this->set_raw_datapoint_value_(datapoint_id, value, true);
720}
721
722void Tuya::force_set_boolean_datapoint_value(uint8_t datapoint_id, bool value) {
723 this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::BOOLEAN, value, 1, true);
724}
725
726void Tuya::force_set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value) {
727 this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::INTEGER, value, 4, true);
728}
729
730void Tuya::force_set_string_datapoint_value(uint8_t datapoint_id, const std::string &value) {
731 this->set_string_datapoint_value_(datapoint_id, value, true);
732}
733
734void Tuya::force_set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value) {
735 this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::ENUM, value, 1, true);
736}
737
738void Tuya::force_set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length) {
739 this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::BITMASK, value, length, true);
740}
741
742optional<TuyaDatapoint> Tuya::get_datapoint_(uint8_t datapoint_id) {
743 for (auto &datapoint : this->datapoints_) {
744 if (datapoint.id == datapoint_id)
745 return datapoint;
746 }
747 return {};
748}
749
750void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, const uint32_t value,
751 uint8_t length, bool forced) {
752 ESP_LOGD(TAG, "Setting datapoint %u to %" PRIu32, datapoint_id, value);
753 optional<TuyaDatapoint> datapoint = this->get_datapoint_(datapoint_id);
754 if (!datapoint.has_value()) {
755 ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id);
756 } else if (datapoint->type != datapoint_type) {
757 ESP_LOGE(TAG, "Attempt to set datapoint %u with incorrect type", datapoint_id);
758 return;
759 } else if (!forced && datapoint->value_uint == value) {
760 ESP_LOGV(TAG, "Not sending unchanged value");
761 return;
762 }
763
764 std::vector<uint8_t> data;
765 switch (length) {
766 case 4:
767 data.push_back(value >> 24);
768 data.push_back(value >> 16);
769 [[fallthrough]];
770 case 2:
771 data.push_back(value >> 8);
772 [[fallthrough]];
773 case 1:
774 data.push_back(value >> 0);
775 break;
776 default:
777 ESP_LOGE(TAG, "Unexpected datapoint length %u", length);
778 return;
779 }
780 this->send_datapoint_command_(datapoint_id, datapoint_type, data);
781}
782
783void Tuya::set_raw_datapoint_value_(uint8_t datapoint_id, const std::vector<uint8_t> &value, bool forced) {
784 char hex_buf[format_hex_pretty_size(MAX_DATAPOINT_LOG_BYTES)];
785 ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, format_hex_pretty_to(hex_buf, value.data(), value.size()));
786 optional<TuyaDatapoint> datapoint = this->get_datapoint_(datapoint_id);
787 if (!datapoint.has_value()) {
788 ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id);
789 } else if (datapoint->type != TuyaDatapointType::RAW) {
790 ESP_LOGE(TAG, "Attempt to set datapoint %u with incorrect type", datapoint_id);
791 return;
792 } else if (!forced && datapoint->value_raw == value) {
793 ESP_LOGV(TAG, "Not sending unchanged value");
794 return;
795 }
796 this->send_datapoint_command_(datapoint_id, TuyaDatapointType::RAW, value);
797}
798
799void Tuya::set_string_datapoint_value_(uint8_t datapoint_id, const std::string &value, bool forced) {
800 ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, value.c_str());
801 optional<TuyaDatapoint> datapoint = this->get_datapoint_(datapoint_id);
802 if (!datapoint.has_value()) {
803 ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id);
804 } else if (datapoint->type != TuyaDatapointType::STRING) {
805 ESP_LOGE(TAG, "Attempt to set datapoint %u with incorrect type", datapoint_id);
806 return;
807 } else if (!forced && datapoint->value_string == value) {
808 ESP_LOGV(TAG, "Not sending unchanged value");
809 return;
810 }
811 std::vector<uint8_t> data;
812 for (char const &c : value) {
813 data.push_back(c);
814 }
815 this->send_datapoint_command_(datapoint_id, TuyaDatapointType::STRING, data);
816}
817
818void Tuya::send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector<uint8_t> data) {
819 std::vector<uint8_t> buffer;
820 buffer.push_back(datapoint_id);
821 buffer.push_back(static_cast<uint8_t>(datapoint_type));
822 buffer.push_back(data.size() >> 8);
823 buffer.push_back(data.size() >> 0);
824 buffer.insert(buffer.end(), data.begin(), data.end());
825
827}
828
829void Tuya::register_listener_(uint8_t datapoint_id, Callback<void(const TuyaDatapoint &)> func) {
830 this->listeners_.push_back(TuyaDatapointListener{
831 .datapoint_id = datapoint_id,
832 .on_datapoint = func,
833 });
834
835 // Run through existing datapoints
836 for (auto &datapoint : this->datapoints_) {
837 if (datapoint.id == datapoint_id)
838 func.call(datapoint);
839 }
840}
841
843 if (this->type != expected) {
844 ESP_LOGW(TAG, "Tuya sensor %u expected datapoint type %#02hhX but got %#02hhX", this->id,
845 static_cast<uint8_t>(expected), static_cast<uint8_t>(this->type));
846 }
847 return *this;
848}
849
851
852} // namespace esphome::tuya
uint8_t status
Definition bl0942.h:8
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
void set_interval(const char *name, uint32_t interval, std::function< void()> &&f)
Set an interval function with a const char* name.
Definition component.cpp:88
void digital_write(bool value) override
Definition gpio.cpp:208
ESPTime now()
Get the time in the currently defined timezone.
ESPTime utcnow()
Get the time without any time zone or DST corrections.
void add_on_time_sync_callback(F &&callback)
void send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector< uint8_t > data)
Definition tuya.cpp:818
void set_string_datapoint_value(uint8_t datapoint_id, const std::string &value)
Definition tuya.cpp:706
optional< TuyaDatapoint > get_datapoint_(uint8_t datapoint_id)
Definition tuya.cpp:742
void send_empty_command_(TuyaCommandType command)
Definition tuya.cpp:576
void setup() override
Definition tuya.cpp:42
uint8_t get_wifi_rssi_()
Definition tuya.cpp:606
uint8_t get_wifi_status_code_()
Definition tuya.cpp:585
void dump_config() override
Definition tuya.cpp:74
time::RealTimeClock * time_id_
Definition tuya.h:153
CallbackManager< void()> initialized_callback_
Definition tuya.h:177
void set_boolean_datapoint_value(uint8_t datapoint_id, bool value)
Definition tuya.cpp:698
bool force_time_sync_
Definition tuya.h:154
TuyaInitState init_state_
Definition tuya.h:159
int status_pin_reported_
Definition tuya.h:164
bool product_invalid_
Definition tuya.h:176
void set_raw_datapoint_value_(uint8_t datapoint_id, const std::vector< uint8_t > &value, bool forced)
Definition tuya.cpp:783
bool validate_message_()
Definition tuya.cpp:120
void check_force_time_sync_()
Definition tuya.cpp:636
InternalGPIOPin * status_pin_
Definition tuya.h:163
void force_set_raw_datapoint_value(uint8_t datapoint_id, const std::vector< uint8_t > &value)
Definition tuya.cpp:718
uint8_t protocol_version_
Definition tuya.h:162
void set_string_datapoint_value_(uint8_t datapoint_id, const std::string &value, bool forced)
Definition tuya.cpp:799
void force_set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value)
Definition tuya.cpp:726
uint32_t last_rx_char_timestamp_
Definition tuya.h:167
void process_command_queue_()
Definition tuya.cpp:540
void send_local_time_(bool periodic=false)
Definition tuya.cpp:644
void send_gmt_time_()
Definition tuya.cpp:673
std::unique_ptr< char[]> product_
Definition tuya.h:168
uint32_t last_command_timestamp_
Definition tuya.h:166
std::vector< TuyaDatapointListener > listeners_
Definition tuya.h:169
std::vector< TuyaCommand > command_queue_
Definition tuya.h:173
std::vector< TuyaDatapoint > datapoints_
Definition tuya.h:170
std::vector< uint8_t > ignore_mcu_update_on_datapoints_
Definition tuya.h:172
std::vector< uint8_t > rx_message_
Definition tuya.h:171
void handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len)
Definition tuya.cpp:186
void set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value)
Definition tuya.cpp:710
void set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, uint32_t value, uint8_t length, bool forced)
Definition tuya.cpp:750
TuyaInitState get_init_state()
Definition tuya.cpp:850
uint8_t last_force_time_sync_minute_
Definition tuya.h:155
void set_status_pin_()
Definition tuya.cpp:580
void handle_char_(uint8_t c)
Definition tuya.cpp:177
void force_set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length)
Definition tuya.cpp:738
void handle_datapoints_(const uint8_t *buffer, size_t len)
Definition tuya.cpp:390
void send_wifi_status_()
Definition tuya.cpp:615
void send_command_(const TuyaCommand &command)
Definition tuya.cpp:571
void force_set_boolean_datapoint_value(uint8_t datapoint_id, bool value)
Definition tuya.cpp:722
void set_raw_datapoint_value(uint8_t datapoint_id, const std::vector< uint8_t > &value)
Definition tuya.cpp:694
optional< TuyaCommandType > expected_response_
Definition tuya.h:174
void send_raw_command_(TuyaCommand command)
Definition tuya.cpp:501
void force_set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value)
Definition tuya.cpp:734
void force_set_string_datapoint_value(uint8_t datapoint_id, const std::string &value)
Definition tuya.cpp:730
bool time_sync_callback_registered_
Definition tuya.h:156
void set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length)
Definition tuya.cpp:714
int reset_pin_reported_
Definition tuya.h:165
uint8_t wifi_status_
Definition tuya.h:175
void register_listener_(uint8_t datapoint_id, Callback< void(const TuyaDatapoint &)> func)
Definition tuya.cpp:829
void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value)
Definition tuya.cpp:702
bool gmt_time_sync_callback_registered_
Definition tuya.h:157
void loop() override
Definition tuya.cpp:56
void register_local_time_sync_callback_()
Definition tuya.cpp:628
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:43
void write_byte(uint8_t data)
Definition uart.h:22
void write_array(const uint8_t *data, size_t len)
Definition uart.h:30
uint8_t month
Definition date_entity.h:1
uint16_t year
Definition date_entity.h:0
uint8_t second
uint8_t minute
uint8_t hour
uint32_t buffer
Definition gif.cpp:50
CaptivePortal * global_captive_portal
ESPHOME_LOG_TAG(TAG, "cover")
ESPHOME_ALWAYS_INLINE bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition util.h:36
TuyaExtendedServicesCommandType
Definition tuya.h:72
TuyaCommandType
Definition tuya.h:51
TuyaDatapointType
Definition tuya.h:19
WiFiComponent * global_wifi_component
bool remote_is_connected()
Return whether the node has any form of "remote" connection via the API or to an MQTT broker.
Definition util.cpp:13
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:894
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:1609
constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4)
Encode a 32-bit value given four bytes in most to least significant byte order.
Definition helpers.h:1006
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
bool valid
static void uint32_t
Lightweight type-erased callback (8 bytes on 32-bit) that avoids std::function overhead.
Definition helpers.h:1806
A more user-friendly version of struct tm from time.h.
Definition time.h:23
uint8_t minute
minutes after the hour [0-59]
Definition time.h:32
uint8_t second
seconds after the minute [0-60]
Definition time.h:30
uint8_t hour
hours since midnight [0-23]
Definition time.h:34
bool is_valid(bool check_day_of_week=true, bool check_day_of_year=true) const
Check if this ESPTime is valid (year >= 2019 and the requested fields are in range).
Definition time.h:82
uint8_t day_of_month
day of the month [1-31]
Definition time.h:38
uint16_t year
year
Definition time.h:44
uint8_t month
month; january=1 [1-12]
Definition time.h:42
uint8_t day_of_week
day of the week; sunday=1 [1-7]
Definition time.h:36
TuyaCommandType cmd
Definition tuya.h:88
std::vector< uint8_t > payload
Definition tuya.h:89
const TuyaDatapoint & expect_type(TuyaDatapointType expected) const
Log a warning when this datapoint is not of type expected, and return it.
Definition tuya.cpp:842
TuyaDatapointType type
Definition tuya.h:30
uint8_t ack
uint16_t length
Definition tt21100.cpp:0