15#ifdef USE_CAPTIVE_PORTAL
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;
31static constexpr size_t MAX_DATAPOINT_LOG_BYTES = 16;
32static constexpr uint32_t FORCE_TIME_SYNC_CHECK_INTERVAL = 1000;
34static bool network_is_connected() {
47 this->
set_interval(FORCE_TIME_SYNC_INTERVAL_ID, FORCE_TIME_SYNC_CHECK_INTERVAL,
61 size_t to_read = std::min(avail,
sizeof(buf));
67 for (
size_t i = 0; i < to_read; i++) {
75 ESP_LOGCONFIG(TAG,
"Tuya:");
78 ESP_LOGCONFIG(TAG,
" Initialization failed. Current init_state: %u",
static_cast<uint8_t
>(this->
init_state_));
80 ESP_LOGCONFIG(TAG,
" Configuration will be reported when setup is complete. Current init_state: %u",
83 ESP_LOGCONFIG(TAG,
" If no further output is received, confirm that this is a supported Tuya device.");
89 ESP_LOGCONFIG(TAG,
" Datapoint %u: raw (value: %s)", info.id,
92 ESP_LOGCONFIG(TAG,
" Datapoint %u: switch (value: %s)", info.id, ONOFF(info.value_bool));
94 ESP_LOGCONFIG(TAG,
" Datapoint %u: int value (value: %d)", info.id, info.value_int);
96 ESP_LOGCONFIG(TAG,
" Datapoint %u: string value (value: %s)", info.id, info.value_string.c_str());
98 ESP_LOGCONFIG(TAG,
" Datapoint %u: enum (value: %d)", info.id, info.value_enum);
100 ESP_LOGCONFIG(TAG,
" Datapoint %u: bitmask (value: %" PRIx32
")", info.id, info.value_bitmask);
102 ESP_LOGCONFIG(TAG,
" Datapoint %u: unknown", info.id);
106 ESP_LOGCONFIG(TAG,
" GPIO Configuration: status: pin %d, reset: pin %d", this->
status_pin_reported_,
110 ESP_LOGCONFIG(TAG,
" Product: '%s'",
123 uint8_t new_byte = data[at];
127 return new_byte == 0x55;
130 return new_byte == 0xAA;
134 uint8_t version = data[2];
138 uint8_t command = data[3];
149 uint16_t
length = (uint16_t(data[4]) << 8) | (uint16_t(data[5]));
156 uint8_t rx_checksum = new_byte;
159 if (rx_checksum != calc_checksum) {
160 ESP_LOGW(TAG,
"Tuya Received invalid message checksum %02X!=%02X", rx_checksum, calc_checksum);
165 const uint8_t *message_data = data + 6;
166#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
168 ESP_LOGV(TAG,
"Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version,
189 if (this->
expected_response_.has_value() && this->expected_response_ == command_type) {
195 switch (command_type) {
197 ESP_LOGV(TAG,
"MCU Heartbeat (0x%02X)",
buffer[0]);
200 ESP_LOGI(TAG,
"MCU restarted");
211 for (
size_t i = 0; i <
len; i++) {
212 if (!std::isprint(
buffer[i])) {
218 this->
product_ = std::make_unique<char[]>(
len + 1);
242 ESP_LOGW(TAG,
"Supplied status_pin does not equal the reported pin %i. Using supplied pin anyway.",
248 ESP_LOGW(TAG,
"MCU reported status_pin %i but no status_pin was configured; running in limited mode.",
253 ESP_LOGV(TAG,
"Configured WIFI_STATE periodic send");
267 const bool is_select = (
len >= 1);
274 uint8_t first = 0x00;
275 const char *mode_str =
"EZ";
276 if (is_select &&
buffer[0] == 0x01) {
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);
329 ESP_LOGW(TAG,
"LOCAL_TIME_QUERY is not handled because time is not configured");
345 ESP_LOGW(TAG,
"GMT_TIME_QUERY is not handled because time is not configured");
351 ESP_LOGW(TAG,
"Vacuum map upload requested, responding that it is not enabled.");
358 ESP_LOGV(TAG,
"Network status requested, reported as %i", wifi_status);
362 uint8_t subcommand =
buffer[0];
367 .payload = std::vector<uint8_t>{
369 ESP_LOGV(TAG,
"Reset status notification enabled");
373 ESP_LOGE(TAG,
"EXTENDED_SERVICES::MODULE_RESET is not handled");
377 ESP_LOGE(TAG,
"EXTENDED_SERVICES::UPDATE_IN_PROGRESS is not handled");
381 ESP_LOGE(TAG,
"Invalid extended services subcommand (0x%02X) received", subcommand);
386 ESP_LOGE(TAG,
"Invalid command (0x%02X) received", command);
395 datapoint.value_uint = 0;
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);
405 datapoint.len = data_size;
407 switch (datapoint.type) {
409 datapoint.value_raw = std::vector<uint8_t>(data, data + data_size);
412 ESP_LOGD(TAG,
"Datapoint %u update to %s", datapoint.id,
417 if (data_size != 1) {
418 ESP_LOGW(TAG,
"Datapoint %u has bad boolean len %zu", datapoint.id, data_size);
421 datapoint.value_bool = data[0];
422 ESP_LOGD(TAG,
"Datapoint %u update to %s", datapoint.id, ONOFF(datapoint.value_bool));
425 if (data_size != 4) {
426 ESP_LOGW(TAG,
"Datapoint %u has bad integer len %zu", datapoint.id, data_size);
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);
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());
437 if (data_size != 1) {
438 ESP_LOGW(TAG,
"Datapoint %u has bad enum len %zu", datapoint.id, data_size);
441 datapoint.value_enum = data[0];
442 ESP_LOGD(TAG,
"Datapoint %u update to %d", datapoint.id, datapoint.value_enum);
450 datapoint.value_bitmask =
encode_uint32(0, 0, data[0], data[1]);
453 datapoint.value_bitmask =
encode_uint32(data[0], data[1], data[2], data[3]);
456 ESP_LOGW(TAG,
"Datapoint %u has bad bitmask len %zu", datapoint.id, data_size);
459 ESP_LOGD(TAG,
"Datapoint %u update to %#08" PRIX32, datapoint.id, datapoint.value_bitmask);
462 ESP_LOGW(TAG,
"Datapoint %u has unknown type %#02hhX", datapoint.id,
static_cast<uint8_t
>(datapoint.type));
466 len -= data_size + 4;
467 buffer = data + data_size;
472 if (datapoint.id == i) {
473 ESP_LOGV(TAG,
"Datapoint %u found in ignore_mcu_update_on_datapoints list, dropping MCU update", datapoint.id);
484 if (other.id == datapoint.id) {
490 this->datapoints_.push_back(datapoint);
495 if (listener.datapoint_id == datapoint.id)
496 listener.on_datapoint.call(datapoint);
502 uint8_t len_hi = (uint8_t) (command.
payload.size() >> 8);
503 uint8_t len_lo = (uint8_t) (command.
payload.size() & 0xFF);
507 switch (command.
cmd) {
525#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
527 ESP_LOGV(TAG,
"Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u",
static_cast<uint8_t
>(command.
cmd),
529 static_cast<uint8_t
>(this->init_state_));
532 this->
write_array({0x55, 0xAA, version, (uint8_t) command.
cmd, len_hi, len_lo});
536 const uint8_t header_sum = 0x55 + 0xAA + (uint8_t) command.
cmd + len_hi + len_lo;
553 ESP_LOGE(TAG,
"Initialization failed at init_state %u",
static_cast<uint8_t
>(this->
init_state_));
563 if (delay > COMMAND_DELAY && !this->
command_queue_.empty() && this->rx_message_.empty() &&
564 !this->expected_response_.has_value()) {
588 if (network_is_connected()) {
596#ifdef USE_CAPTIVE_PORTAL
622 ESP_LOGD(TAG,
"Sending WiFi Status");
638 if (!now.
is_valid() || now.
minute == this->last_force_time_sync_minute_)
645 std::vector<uint8_t> payload;
656 if (day_of_week == 0) {
661 ESP_LOGV(TAG,
"Sending local time");
663 ESP_LOGD(TAG,
"Sending local time");
668 ESP_LOGW(TAG,
"Sending missing local time");
669 payload = std::vector<uint8_t>{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
674 std::vector<uint8_t> payload;
683 ESP_LOGD(TAG,
"Sending gmt time");
687 ESP_LOGW(TAG,
"Sending missing gmt time");
688 payload = std::vector<uint8_t>{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
744 if (datapoint.id == datapoint_id)
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);
759 }
else if (!forced && datapoint->value_uint == value) {
760 ESP_LOGV(TAG,
"Not sending unchanged value");
764 std::vector<uint8_t> data;
767 data.push_back(value >> 24);
768 data.push_back(value >> 16);
771 data.push_back(value >> 8);
774 data.push_back(value >> 0);
777 ESP_LOGE(TAG,
"Unexpected datapoint length %u",
length);
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);
790 ESP_LOGE(TAG,
"Attempt to set datapoint %u with incorrect type", datapoint_id);
792 }
else if (!forced && datapoint->value_raw == value) {
793 ESP_LOGV(TAG,
"Not sending unchanged value");
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);
805 ESP_LOGE(TAG,
"Attempt to set datapoint %u with incorrect type", datapoint_id);
807 }
else if (!forced && datapoint->value_string == value) {
808 ESP_LOGV(TAG,
"Not sending unchanged value");
811 std::vector<uint8_t> data;
812 for (
char const &c : value) {
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);
831 .datapoint_id = datapoint_id,
832 .on_datapoint = func,
837 if (datapoint.id == datapoint_id)
838 func.call(datapoint);
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));
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.
void digital_write(bool value) override
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)
void check_force_time_sync_()
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_()
void send_local_time_(bool periodic=false)
std::unique_ptr< char[]> product_
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()
uint8_t last_force_time_sync_minute_
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 register_listener_(uint8_t datapoint_id, Callback< void(const TuyaDatapoint &)> func)
void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value)
bool gmt_time_sync_callback_registered_
void register_local_time_sync_callback_()
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_LOG_TAG(TAG, "cover")
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.
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...
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()
Lightweight type-erased callback (8 bytes on 32-bit) that avoids std::function overhead.
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
const TuyaDatapoint & expect_type(TuyaDatapointType expected) const
Log a warning when this datapoint is not of type expected, and return it.