mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	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 <otto@otto-winter.com>
This commit is contained in:
		
				
					committed by
					
						 Otto Winter
						Otto Winter
					
				
			
			
				
	
			
			
			
						parent
						
							531428b8b0
						
					
				
				
					commit
					c5c42c4338
				
			| @@ -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<const char *>(buffer)); | ||||
|         this->product_ = std::string(reinterpret_cast<const char *>(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<void(TuyaDatapoint)> &func) { | ||||
|   | ||||
| @@ -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<TuyaDatapointListener> listeners_; | ||||
|   std::vector<TuyaDatapoint> datapoints_; | ||||
|   std::vector<uint8_t> rx_message_; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user