From c5c42c433849e938472f2d8afa5e69868b507f0d Mon Sep 17 00:00:00 2001 From: Brandon Davidson Date: Thu, 14 Nov 2019 04:43:44 -0800 Subject: [PATCH] Tuya: Fix init sequence and handle wifi test command (#820) * Handle WiFi test command Also rename commands to match Tuya protocol docs * Fix init sequence and product info check * Fix clang-format suggestions * Additional changes based on code review * Fix temp command buffer scope * Let the interval timer fire the first heatbeat * Fix init steps; add logging * Lint * Remove setup_priority override * Add delay to dump_config * Refactor dump sequence * Fix verbose logging * Fix lints * Don't bother suppressing duplicate config dumps * nolint Co-authored-by: Otto Winter --- esphome/components/tuya/tuya.cpp | 93 +++++++++++++++++++++----------- esphome/components/tuya/tuya.h | 24 ++++++--- 2 files changed, 80 insertions(+), 37 deletions(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index cb796644c8..b21df81d1e 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -8,7 +8,6 @@ namespace tuya { static const char *TAG = "tuya"; void Tuya::setup() { - this->send_empty_command_(TuyaCommandType::MCU_CONF); this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); } @@ -22,8 +21,12 @@ void Tuya::loop() { void Tuya::dump_config() { ESP_LOGCONFIG(TAG, "Tuya:"); - if ((gpio_status_ != -1) || (gpio_reset_ != -1)) - ESP_LOGCONFIG(TAG, " GPIO MCU configuration not supported!"); + if (this->init_state_ != TuyaInitState::INIT_DONE) { + ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u", // NOLINT + this->init_state_); + ESP_LOGCONFIG(TAG, " If no further output is received, confirm that this is a supported Tuya device."); + return; + } for (auto &info : this->datapoints_) { if (info.type == TuyaDatapointType::BOOLEAN) ESP_LOGCONFIG(TAG, " Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool)); @@ -36,9 +39,11 @@ void Tuya::dump_config() { else ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id); } - if (this->datapoints_.empty()) { - ESP_LOGCONFIG(TAG, " Received no datapoints! Please make sure this is a supported Tuya device."); + if ((this->gpio_status_ != -1) || (this->gpio_reset_ != -1)) { + ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d (not supported)", this->gpio_status_, + this->gpio_reset_); } + ESP_LOGCONFIG(TAG, " Product: '%s'", this->product_.c_str()); this->check_uart_settings(9600); } @@ -89,8 +94,8 @@ bool Tuya::validate_message_() { // valid message const uint8_t *message_data = data + 6; - ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s]", command, version, - hexencode(message_data, length).c_str()); + ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version, // NOLINT + hexencode(message_data, length).c_str(), this->init_state_); this->handle_command_(command, version, message_data, length); // return false to reset rx buffer @@ -105,41 +110,58 @@ void Tuya::handle_char_(uint8_t c) { } void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) { - uint8_t c; switch ((TuyaCommandType) command) { case TuyaCommandType::HEARTBEAT: ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]); if (buffer[0] == 0) { ESP_LOGI(TAG, "MCU restarted"); - this->send_empty_command_(TuyaCommandType::QUERY_STATE); + this->init_state_ = TuyaInitState::INIT_HEARTBEAT; + } + if (this->init_state_ == TuyaInitState::INIT_HEARTBEAT) { + this->init_state_ = TuyaInitState::INIT_PRODUCT; + this->send_empty_command_(TuyaCommandType::PRODUCT_QUERY); } break; - case TuyaCommandType::QUERY_PRODUCT: { - // check it is a valid string - bool valid = false; + case TuyaCommandType::PRODUCT_QUERY: { + // check it is a valid string made up of printable characters + bool valid = true; for (int i = 0; i < len; i++) { - if (buffer[i] == 0x00) { - valid = true; + if (!std::isprint(buffer[i])) { + valid = false; break; } } if (valid) { - ESP_LOGD(TAG, "Tuya Product Code: %s", reinterpret_cast(buffer)); + this->product_ = std::string(reinterpret_cast(buffer), len); + } else { + this->product_ = R"({"p":"INVALID"})"; + } + if (this->init_state_ == TuyaInitState::INIT_PRODUCT) { + this->init_state_ = TuyaInitState::INIT_CONF; + this->send_empty_command_(TuyaCommandType::CONF_QUERY); } break; } - case TuyaCommandType::MCU_CONF: + case TuyaCommandType::CONF_QUERY: { if (len >= 2) { gpio_status_ = buffer[0]; gpio_reset_ = buffer[1]; } - // set wifi state LED to off or on depending on the MCU firmware - // but it shouldn't be blinking - c = 0x3; - this->send_command_(TuyaCommandType::WIFI_STATE, &c, 1); - this->send_empty_command_(TuyaCommandType::QUERY_STATE); + if (this->init_state_ == TuyaInitState::INIT_CONF) { + // If we were following the spec to the letter we would send + // state updates until connected to both WiFi and API/MQTT. + // Instead we just claim to be connected immediately and move on. + uint8_t c[] = {0x04}; + this->init_state_ = TuyaInitState::INIT_WIFI; + this->send_command_(TuyaCommandType::WIFI_STATE, c, 1); + } break; + } case TuyaCommandType::WIFI_STATE: + if (this->init_state_ == TuyaInitState::INIT_WIFI) { + this->init_state_ = TuyaInitState::INIT_DATAPOINT; + this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY); + } break; case TuyaCommandType::WIFI_RESET: ESP_LOGE(TAG, "TUYA_CMD_WIFI_RESET is not handled"); @@ -147,14 +169,22 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff case TuyaCommandType::WIFI_SELECT: ESP_LOGE(TAG, "TUYA_CMD_WIFI_SELECT is not handled"); break; - case TuyaCommandType::SET_DATAPOINT: + case TuyaCommandType::DATAPOINT_DELIVER: break; - case TuyaCommandType::STATE: { + case TuyaCommandType::DATAPOINT_REPORT: + if (this->init_state_ == TuyaInitState::INIT_DATAPOINT) { + this->init_state_ = TuyaInitState::INIT_DONE; + this->set_timeout("datapoint_dump", 1000, [this] { this->dump_config(); }); + } this->handle_datapoint_(buffer, len); break; - } - case TuyaCommandType::QUERY_STATE: + case TuyaCommandType::DATAPOINT_QUERY: break; + case TuyaCommandType::WIFI_TEST: { + uint8_t c[] = {0x00, 0x00}; + this->send_command_(TuyaCommandType::WIFI_TEST, c, 2); + break; + } default: ESP_LOGE(TAG, "invalid command (%02x) received", command); } @@ -214,8 +244,6 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { } if (!found) { this->datapoints_.push_back(datapoint); - // New datapoint found, reprint dump_config after a delay. - this->set_timeout("datapoint_dump", 100, [this] { this->dump_config(); }); } // Run through listeners @@ -227,9 +255,12 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { void Tuya::send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len) { uint8_t len_hi = len >> 8; uint8_t len_lo = len >> 0; - this->write_array({0x55, 0xAA, - 0x00, // version - (uint8_t) command, len_hi, len_lo}); + uint8_t version = 0; + + ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version, // NOLINT + hexencode(buffer, len).c_str(), this->init_state_); + + this->write_array({0x55, 0xAA, version, (uint8_t) command, len_hi, len_lo}); if (len != 0) this->write_array(buffer, len); @@ -278,7 +309,7 @@ void Tuya::set_datapoint_value(TuyaDatapoint datapoint) { buffer.push_back(data.size() >> 8); buffer.push_back(data.size() >> 0); buffer.insert(buffer.end(), data.begin(), data.end()); - this->send_command_(TuyaCommandType::SET_DATAPOINT, buffer.data(), buffer.size()); + this->send_command_(TuyaCommandType::DATAPOINT_DELIVER, buffer.data(), buffer.size()); } void Tuya::register_listener(uint8_t datapoint_id, const std::function &func) { diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 6bc6d92da0..2fc9a16d44 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -34,19 +34,29 @@ struct TuyaDatapointListener { enum class TuyaCommandType : uint8_t { HEARTBEAT = 0x00, - QUERY_PRODUCT = 0x01, - MCU_CONF = 0x02, + PRODUCT_QUERY = 0x01, + CONF_QUERY = 0x02, WIFI_STATE = 0x03, WIFI_RESET = 0x04, WIFI_SELECT = 0x05, - SET_DATAPOINT = 0x06, - STATE = 0x07, - QUERY_STATE = 0x08, + DATAPOINT_DELIVER = 0x06, + DATAPOINT_REPORT = 0x07, + DATAPOINT_QUERY = 0x08, + WIFI_TEST = 0x0E, +}; + +enum class TuyaInitState : uint8_t { + INIT_HEARTBEAT = 0x00, + INIT_PRODUCT, + INIT_CONF, + INIT_WIFI, + INIT_DATAPOINT, + INIT_DONE, }; class Tuya : public Component, public uart::UARTDevice { public: - float get_setup_priority() const override { return setup_priority::HARDWARE; } + float get_setup_priority() const override { return setup_priority::LATE; } void setup() override; void loop() override; void dump_config() override; @@ -62,8 +72,10 @@ class Tuya : public Component, public uart::UARTDevice { void send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len); void send_empty_command_(TuyaCommandType command) { this->send_command_(command, nullptr, 0); } + TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; int gpio_status_ = -1; int gpio_reset_ = -1; + std::string product_ = ""; std::vector listeners_; std::vector datapoints_; std::vector rx_message_;