15#ifdef USE_CAPTIVE_PORTAL
21static const char *
const TAG =
"tuya";
22static const int COMMAND_DELAY = 10;
23static const int RECEIVE_TIMEOUT = 300;
24static const int MAX_RETRIES = 5;
26static constexpr size_t MAX_DATAPOINT_LOG_BYTES = 16;
28static bool network_is_connected() {
48 size_t to_read = std::min(avail,
sizeof(buf));
54 for (
size_t i = 0; i < to_read; i++) {
62 ESP_LOGCONFIG(TAG,
"Tuya:");
65 ESP_LOGCONFIG(TAG,
" Initialization failed. Current init_state: %u",
static_cast<uint8_t
>(this->
init_state_));
67 ESP_LOGCONFIG(TAG,
" Configuration will be reported when setup is complete. Current init_state: %u",
70 ESP_LOGCONFIG(TAG,
" If no further output is received, confirm that this is a supported Tuya device.");
76 ESP_LOGCONFIG(TAG,
" Datapoint %u: raw (value: %s)", info.id,
79 ESP_LOGCONFIG(TAG,
" Datapoint %u: switch (value: %s)", info.id, ONOFF(info.value_bool));
81 ESP_LOGCONFIG(TAG,
" Datapoint %u: int value (value: %d)", info.id, info.value_int);
83 ESP_LOGCONFIG(TAG,
" Datapoint %u: string value (value: %s)", info.id, info.value_string.c_str());
85 ESP_LOGCONFIG(TAG,
" Datapoint %u: enum (value: %d)", info.id, info.value_enum);
87 ESP_LOGCONFIG(TAG,
" Datapoint %u: bitmask (value: %" PRIx32
")", info.id, info.value_bitmask);
89 ESP_LOGCONFIG(TAG,
" Datapoint %u: unknown", info.id);
93 ESP_LOGCONFIG(TAG,
" GPIO Configuration: status: pin %d, reset: pin %d", this->
status_pin_reported_,
97 ESP_LOGCONFIG(TAG,
" Product: '%s'", this->
product_.c_str());
103 uint8_t new_byte = data[at];
107 return new_byte == 0x55;
110 return new_byte == 0xAA;
114 uint8_t version = data[2];
118 uint8_t command = data[3];
129 uint16_t
length = (uint16_t(data[4]) << 8) | (uint16_t(data[5]));
136 uint8_t rx_checksum = new_byte;
137 uint8_t calc_checksum = 0;
139 calc_checksum += data[i];
141 if (rx_checksum != calc_checksum) {
142 ESP_LOGW(TAG,
"Tuya Received invalid message checksum %02X!=%02X", rx_checksum, calc_checksum);
147 const uint8_t *message_data = data + 6;
148#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
150 ESP_LOGV(TAG,
"Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version,
171 if (this->
expected_response_.has_value() && this->expected_response_ == command_type) {
177 switch (command_type) {
179 ESP_LOGV(TAG,
"MCU Heartbeat (0x%02X)", buffer[0]);
181 if (buffer[0] == 0) {
182 ESP_LOGI(TAG,
"MCU restarted");
193 for (
size_t i = 0; i <
len; i++) {
194 if (!std::isprint(buffer[i])) {
200 this->
product_ = std::string(
reinterpret_cast<const char *
>(buffer),
len);
202 this->
product_ = R
"({"p":"INVALID"})";
222 ESP_LOGW(TAG,
"Supplied status_pin does not equal the reported pin %i. Using supplied pin anyway.",
228 ESP_LOGW(TAG,
"MCU reported status_pin %i but no status_pin was configured; running in limited mode.",
233 ESP_LOGV(TAG,
"Configured WIFI_STATE periodic send");
247 const bool is_select = (
len >= 1);
254 uint8_t first = 0x00;
255 const char *mode_str =
"EZ";
256 if (is_select && buffer[0] == 0x01) {
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);
314 ESP_LOGW(TAG,
"LOCAL_TIME_QUERY is not handled because time is not configured");
330 ESP_LOGW(TAG,
"GMT_TIME_QUERY is not handled because time is not configured");
336 ESP_LOGW(TAG,
"Vacuum map upload requested, responding that it is not enabled.");
343 ESP_LOGV(TAG,
"Network status requested, reported as %i", wifi_status);
347 uint8_t subcommand = buffer[0];
352 .payload = std::vector<uint8_t>{
354 ESP_LOGV(TAG,
"Reset status notification enabled");
358 ESP_LOGE(TAG,
"EXTENDED_SERVICES::MODULE_RESET is not handled");
362 ESP_LOGE(TAG,
"EXTENDED_SERVICES::UPDATE_IN_PROGRESS is not handled");
366 ESP_LOGE(TAG,
"Invalid extended services subcommand (0x%02X) received", subcommand);
371 ESP_LOGE(TAG,
"Invalid command (0x%02X) received", command);
378 datapoint.
id = buffer[0];
380 datapoint.value_uint = 0;
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);
390 datapoint.len = data_size;
392 switch (datapoint.type) {
394 datapoint.value_raw = std::vector<uint8_t>(data, data + data_size);
397 ESP_LOGD(TAG,
"Datapoint %u update to %s", datapoint.id,
402 if (data_size != 1) {
403 ESP_LOGW(TAG,
"Datapoint %u has bad boolean len %zu", datapoint.id, data_size);
406 datapoint.value_bool = data[0];
407 ESP_LOGD(TAG,
"Datapoint %u update to %s", datapoint.id, ONOFF(datapoint.value_bool));
410 if (data_size != 4) {
411 ESP_LOGW(TAG,
"Datapoint %u has bad integer len %zu", datapoint.id, data_size);
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);
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());
422 if (data_size != 1) {
423 ESP_LOGW(TAG,
"Datapoint %u has bad enum len %zu", datapoint.id, data_size);
426 datapoint.value_enum = data[0];
427 ESP_LOGD(TAG,
"Datapoint %u update to %d", datapoint.id, datapoint.value_enum);
435 datapoint.value_bitmask =
encode_uint32(0, 0, data[0], data[1]);
438 datapoint.value_bitmask =
encode_uint32(data[0], data[1], data[2], data[3]);
441 ESP_LOGW(TAG,
"Datapoint %u has bad bitmask len %zu", datapoint.id, data_size);
444 ESP_LOGD(TAG,
"Datapoint %u update to %#08" PRIX32, datapoint.id, datapoint.value_bitmask);
447 ESP_LOGW(TAG,
"Datapoint %u has unknown type %#02hhX", datapoint.id,
static_cast<uint8_t
>(datapoint.type));
451 len -= data_size + 4;
452 buffer = data + data_size;
457 if (datapoint.id == i) {
458 ESP_LOGV(TAG,
"Datapoint %u found in ignore_mcu_update_on_datapoints list, dropping MCU update", datapoint.id);
469 if (other.id == datapoint.id) {
475 this->datapoints_.push_back(datapoint);
480 if (listener.datapoint_id == datapoint.id)
481 listener.on_datapoint(datapoint);
487 uint8_t len_hi = (uint8_t) (command.
payload.size() >> 8);
488 uint8_t len_lo = (uint8_t) (command.
payload.size() & 0xFF);
492 switch (command.
cmd) {
510#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
512 ESP_LOGV(TAG,
"Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u",
static_cast<uint8_t
>(command.
cmd),
514 static_cast<uint8_t
>(this->init_state_));
517 this->
write_array({0x55, 0xAA, version, (uint8_t) command.
cmd, len_hi, len_lo});
521 uint8_t
checksum = 0x55 + 0xAA + (uint8_t) command.
cmd + len_hi + len_lo;
522 for (
auto &data : command.
payload)
540 ESP_LOGE(TAG,
"Initialization failed at init_state %u",
static_cast<uint8_t
>(this->
init_state_));
550 if (delay > COMMAND_DELAY && !this->
command_queue_.empty() && this->rx_message_.empty() &&
551 !this->expected_response_.has_value()) {
575 if (network_is_connected()) {
583#ifdef USE_CAPTIVE_PORTAL
609 ESP_LOGD(TAG,
"Sending WiFi Status");
616 std::vector<uint8_t> payload;
627 if (day_of_week == 0) {
630 ESP_LOGD(TAG,
"Sending local time");
634 ESP_LOGW(TAG,
"Sending missing local time");
635 payload = std::vector<uint8_t>{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
640 std::vector<uint8_t> payload;
649 ESP_LOGD(TAG,
"Sending gmt time");
653 ESP_LOGW(TAG,
"Sending missing gmt time");
654 payload = std::vector<uint8_t>{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
710 if (datapoint.id == datapoint_id)
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);
725 }
else if (!forced && datapoint->value_uint == value) {
726 ESP_LOGV(TAG,
"Not sending unchanged value");
730 std::vector<uint8_t> data;
733 data.push_back(value >> 24);
734 data.push_back(value >> 16);
737 data.push_back(value >> 8);
740 data.push_back(value >> 0);
743 ESP_LOGE(TAG,
"Unexpected datapoint length %u",
length);
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);
756 ESP_LOGE(TAG,
"Attempt to set datapoint %u with incorrect type", datapoint_id);
758 }
else if (!forced && datapoint->value_raw == value) {
759 ESP_LOGV(TAG,
"Not sending unchanged value");
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);
771 ESP_LOGE(TAG,
"Attempt to set datapoint %u with incorrect type", datapoint_id);
773 }
else if (!forced && datapoint->value_string == value) {
774 ESP_LOGV(TAG,
"Not sending unchanged value");
777 std::vector<uint8_t> data;
778 for (
char const &c : value) {
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());
797 .datapoint_id = datapoint_id,
798 .on_datapoint = func,
804 if (datapoint.id == datapoint_id)
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
void set_interval(const char *name, uint32_t interval, std::function< void()> &&f)
Set an interval function with a const char* name.
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)
void set_string_datapoint_value(uint8_t datapoint_id, const std::string &value)
optional< TuyaDatapoint > get_datapoint_(uint8_t datapoint_id)
void send_empty_command_(TuyaCommandType command)
uint8_t get_wifi_status_code_()
void dump_config() override
time::RealTimeClock * time_id_
CallbackManager< void()> initialized_callback_
void set_boolean_datapoint_value(uint8_t datapoint_id, bool value)
TuyaInitState init_state_
void set_raw_datapoint_value_(uint8_t datapoint_id, const std::vector< uint8_t > &value, bool forced)
InternalGPIOPin * status_pin_
void force_set_raw_datapoint_value(uint8_t datapoint_id, const std::vector< uint8_t > &value)
uint8_t protocol_version_
void set_string_datapoint_value_(uint8_t datapoint_id, const std::string &value, bool forced)
void force_set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value)
uint32_t last_rx_char_timestamp_
void process_command_queue_()
uint32_t last_command_timestamp_
std::vector< TuyaDatapointListener > listeners_
std::vector< TuyaCommand > command_queue_
std::vector< TuyaDatapoint > datapoints_
std::vector< uint8_t > ignore_mcu_update_on_datapoints_
std::vector< uint8_t > rx_message_
void handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len)
void set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value)
void set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, uint32_t value, uint8_t length, bool forced)
TuyaInitState get_init_state()
void register_listener(uint8_t datapoint_id, const std::function< void(TuyaDatapoint)> &func)
void handle_char_(uint8_t c)
void force_set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length)
void handle_datapoints_(const uint8_t *buffer, size_t len)
void send_command_(const TuyaCommand &command)
void force_set_boolean_datapoint_value(uint8_t datapoint_id, bool value)
void set_raw_datapoint_value(uint8_t datapoint_id, const std::vector< uint8_t > &value)
optional< TuyaCommandType > expected_response_
void send_raw_command_(TuyaCommand command)
void force_set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value)
void force_set_string_datapoint_value(uint8_t datapoint_id, const std::string &value)
bool time_sync_callback_registered_
void set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length)
void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value)
bool gmt_time_sync_callback_registered_
optional< std::array< uint8_t, N > > read_array()
void write_byte(uint8_t data)
void write_array(const uint8_t *data, size_t len)
CaptivePortal * global_captive_portal
ESPHOME_ALWAYS_INLINE bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
TuyaExtendedServicesCommandType
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.
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).
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".
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.
void HOT delay(uint32_t ms)
uint32_t IRAM_ATTR HOT millis()
A more user-friendly version of struct tm from time.h.
uint8_t minute
minutes after the hour [0-59]
uint8_t second
seconds after the minute [0-60]
uint8_t hour
hours since midnight [0-23]
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).
uint8_t day_of_month
day of the month [1-31]
uint8_t month
month; january=1 [1-12]
uint8_t day_of_week
day of the week; sunday=1 [1-7]
std::vector< uint8_t > payload