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