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