mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Add support for Tuya MCU 0x1C (obtain local time) (#1344)
* Fix some Tuya devices not handling commands sent without delay * Also do not report WiFi status if MCU does not support it * Support Tuya MCU 0x1c command (obtain local time) * Use #ifdef USE_TIME to handle optional dependency on RTC * Rename Tuya clock config variable to time to be consistent with the codebase * Add tuya time configuration to test4
This commit is contained in:
		| @@ -1,7 +1,8 @@ | |||||||
|  | from esphome.components import time | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.components import uart | from esphome.components import uart | ||||||
| from esphome.const import CONF_ID | from esphome.const import CONF_ID, CONF_TIME_ID | ||||||
|  |  | ||||||
| DEPENDENCIES = ['uart'] | DEPENDENCIES = ['uart'] | ||||||
|  |  | ||||||
| @@ -11,6 +12,7 @@ Tuya = tuya_ns.class_('Tuya', cg.Component, uart.UARTDevice) | |||||||
| CONF_TUYA_ID = 'tuya_id' | CONF_TUYA_ID = 'tuya_id' | ||||||
| CONFIG_SCHEMA = cv.Schema({ | CONFIG_SCHEMA = cv.Schema({ | ||||||
|     cv.GenerateID(): cv.declare_id(Tuya), |     cv.GenerateID(): cv.declare_id(Tuya), | ||||||
|  |     cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), | ||||||
| }).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) | }).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -18,3 +20,6 @@ def to_code(config): | |||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|     yield cg.register_component(var, config) |     yield cg.register_component(var, config) | ||||||
|     yield uart.register_uart_device(var, config) |     yield uart.register_uart_device(var, config) | ||||||
|  |     if CONF_TIME_ID in config: | ||||||
|  |         time_ = yield cg.get_variable(config[CONF_TIME_ID]) | ||||||
|  |         cg.add(var.set_time_id(time_)) | ||||||
|   | |||||||
| @@ -6,9 +6,10 @@ namespace esphome { | |||||||
| namespace tuya { | namespace tuya { | ||||||
|  |  | ||||||
| static const char *TAG = "tuya"; | static const char *TAG = "tuya"; | ||||||
|  | static const int COMMAND_DELAY = 50; | ||||||
|  |  | ||||||
| void Tuya::setup() { | void Tuya::setup() { | ||||||
|   this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); |   this->set_interval("heartbeat", 1000, [this] { this->schedule_empty_command_(TuyaCommandType::HEARTBEAT); }); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Tuya::loop() { | void Tuya::loop() { | ||||||
| @@ -19,6 +20,15 @@ void Tuya::loop() { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void Tuya::schedule_empty_command_(TuyaCommandType command) { | ||||||
|  |   uint32_t delay = millis() - this->last_command_timestamp_; | ||||||
|  |   if (delay > COMMAND_DELAY) { | ||||||
|  |     send_empty_command_(command); | ||||||
|  |   } else { | ||||||
|  |     this->set_timeout(COMMAND_DELAY - delay, [this, command] { this->send_empty_command_(command); }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| void Tuya::dump_config() { | void Tuya::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, "Tuya:"); |   ESP_LOGCONFIG(TAG, "Tuya:"); | ||||||
|   if (this->init_state_ != TuyaInitState::INIT_DONE) { |   if (this->init_state_ != TuyaInitState::INIT_DONE) { | ||||||
| @@ -110,6 +120,7 @@ void Tuya::handle_char_(uint8_t c) { | |||||||
| } | } | ||||||
|  |  | ||||||
| void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) { | void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) { | ||||||
|  |   this->last_command_timestamp_ = millis(); | ||||||
|   switch ((TuyaCommandType) command) { |   switch ((TuyaCommandType) command) { | ||||||
|     case TuyaCommandType::HEARTBEAT: |     case TuyaCommandType::HEARTBEAT: | ||||||
|       ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]); |       ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]); | ||||||
| @@ -119,7 +130,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff | |||||||
|       } |       } | ||||||
|       if (this->init_state_ == TuyaInitState::INIT_HEARTBEAT) { |       if (this->init_state_ == TuyaInitState::INIT_HEARTBEAT) { | ||||||
|         this->init_state_ = TuyaInitState::INIT_PRODUCT; |         this->init_state_ = TuyaInitState::INIT_PRODUCT; | ||||||
|         this->send_empty_command_(TuyaCommandType::PRODUCT_QUERY); |         this->schedule_empty_command_(TuyaCommandType::PRODUCT_QUERY); | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     case TuyaCommandType::PRODUCT_QUERY: { |     case TuyaCommandType::PRODUCT_QUERY: { | ||||||
| @@ -138,7 +149,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff | |||||||
|       } |       } | ||||||
|       if (this->init_state_ == TuyaInitState::INIT_PRODUCT) { |       if (this->init_state_ == TuyaInitState::INIT_PRODUCT) { | ||||||
|         this->init_state_ = TuyaInitState::INIT_CONF; |         this->init_state_ = TuyaInitState::INIT_CONF; | ||||||
|         this->send_empty_command_(TuyaCommandType::CONF_QUERY); |         this->schedule_empty_command_(TuyaCommandType::CONF_QUERY); | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
| @@ -148,19 +159,27 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff | |||||||
|         gpio_reset_ = buffer[1]; |         gpio_reset_ = buffer[1]; | ||||||
|       } |       } | ||||||
|       if (this->init_state_ == TuyaInitState::INIT_CONF) { |       if (this->init_state_ == TuyaInitState::INIT_CONF) { | ||||||
|         // If we were following the spec to the letter we would send |         // If mcu returned status gpio, then we can ommit sending wifi state | ||||||
|         // state updates until connected to both WiFi and API/MQTT. |         if (this->gpio_status_ != 0) { | ||||||
|         // Instead we just claim to be connected immediately and move on. |           this->init_state_ = TuyaInitState::INIT_DATAPOINT; | ||||||
|         uint8_t c[] = {0x04}; |           this->schedule_empty_command_(TuyaCommandType::DATAPOINT_QUERY); | ||||||
|         this->init_state_ = TuyaInitState::INIT_WIFI; |         } else { | ||||||
|         this->send_command_(TuyaCommandType::WIFI_STATE, c, 1); |           this->init_state_ = TuyaInitState::INIT_WIFI; | ||||||
|  |           this->set_timeout(COMMAND_DELAY, [this] { | ||||||
|  |             // 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->send_command_(TuyaCommandType::WIFI_STATE, c, 1); | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case TuyaCommandType::WIFI_STATE: |     case TuyaCommandType::WIFI_STATE: | ||||||
|       if (this->init_state_ == TuyaInitState::INIT_WIFI) { |       if (this->init_state_ == TuyaInitState::INIT_WIFI) { | ||||||
|         this->init_state_ = TuyaInitState::INIT_DATAPOINT; |         this->init_state_ = TuyaInitState::INIT_DATAPOINT; | ||||||
|         this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY); |         this->schedule_empty_command_(TuyaCommandType::DATAPOINT_QUERY); | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     case TuyaCommandType::WIFI_RESET: |     case TuyaCommandType::WIFI_RESET: | ||||||
| @@ -185,6 +204,44 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff | |||||||
|       this->send_command_(TuyaCommandType::WIFI_TEST, c, 2); |       this->send_command_(TuyaCommandType::WIFI_TEST, c, 2); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|  |     case TuyaCommandType::LOCAL_TIME_QUERY: { | ||||||
|  | #ifdef USE_TIME | ||||||
|  |       if (this->time_id_.has_value()) { | ||||||
|  |         auto time_id = *this->time_id_; | ||||||
|  |         auto now = time_id->now(); | ||||||
|  |  | ||||||
|  |         if (now.is_valid()) { | ||||||
|  |           this->set_timeout(COMMAND_DELAY, [this, now] { | ||||||
|  |             uint8_t year = now.year - 2000; | ||||||
|  |             uint8_t month = now.month; | ||||||
|  |             uint8_t day_of_month = now.day_of_month; | ||||||
|  |             uint8_t hour = now.hour; | ||||||
|  |             uint8_t minute = now.minute; | ||||||
|  |             uint8_t second = now.second; | ||||||
|  |             // Tuya days starts from Monday, esphome uses Sunday as day 1 | ||||||
|  |             uint8_t day_of_week = now.day_of_week - 1; | ||||||
|  |             if (day_of_week == 0) { | ||||||
|  |               day_of_week = 7; | ||||||
|  |             } | ||||||
|  |             uint8_t c[] = {0x01, year, month, day_of_month, hour, minute, second, day_of_week}; | ||||||
|  |             this->send_command_(TuyaCommandType::LOCAL_TIME_QUERY, c, 8); | ||||||
|  |           }); | ||||||
|  |         } else { | ||||||
|  |           ESP_LOGW(TAG, "TUYA_CMD_LOCAL_TIME_QUERY is not handled because time is not valid"); | ||||||
|  |           // By spec we need to notify MCU that the time was not obtained | ||||||
|  |           this->set_timeout(COMMAND_DELAY, [this] { | ||||||
|  |             uint8_t c[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; | ||||||
|  |             this->send_command_(TuyaCommandType::LOCAL_TIME_QUERY, c, 8); | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         ESP_LOGW(TAG, "TUYA_CMD_LOCAL_TIME_QUERY is not handled because time is not configured"); | ||||||
|  |       } | ||||||
|  | #else | ||||||
|  |       ESP_LOGE(TAG, "LOCAL_TIME_QUERY is not handled"); | ||||||
|  | #endif | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|     default: |     default: | ||||||
|       ESP_LOGE(TAG, "invalid command (%02x) received", command); |       ESP_LOGE(TAG, "invalid command (%02x) received", command); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,8 +1,13 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/defines.h" | ||||||
| #include "esphome/components/uart/uart.h" | #include "esphome/components/uart/uart.h" | ||||||
|  |  | ||||||
|  | #ifdef USE_TIME | ||||||
|  | #include "esphome/components/time/real_time_clock.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace tuya { | namespace tuya { | ||||||
|  |  | ||||||
| @@ -43,6 +48,7 @@ enum class TuyaCommandType : uint8_t { | |||||||
|   DATAPOINT_REPORT = 0x07, |   DATAPOINT_REPORT = 0x07, | ||||||
|   DATAPOINT_QUERY = 0x08, |   DATAPOINT_QUERY = 0x08, | ||||||
|   WIFI_TEST = 0x0E, |   WIFI_TEST = 0x0E, | ||||||
|  |   LOCAL_TIME_QUERY = 0x1C, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum class TuyaInitState : uint8_t { | enum class TuyaInitState : uint8_t { | ||||||
| @@ -62,6 +68,9 @@ class Tuya : public Component, public uart::UARTDevice { | |||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func); |   void register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func); | ||||||
|   void set_datapoint_value(TuyaDatapoint datapoint); |   void set_datapoint_value(TuyaDatapoint datapoint); | ||||||
|  | #ifdef USE_TIME | ||||||
|  |   void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   void handle_char_(uint8_t c); |   void handle_char_(uint8_t c); | ||||||
| @@ -71,10 +80,15 @@ class Tuya : public Component, public uart::UARTDevice { | |||||||
|   void handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len); |   void handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len); | ||||||
|   void send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len); |   void send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len); | ||||||
|   void send_empty_command_(TuyaCommandType command) { this->send_command_(command, nullptr, 0); } |   void send_empty_command_(TuyaCommandType command) { this->send_command_(command, nullptr, 0); } | ||||||
|  |   void schedule_empty_command_(TuyaCommandType command); | ||||||
|  |  | ||||||
|  | #ifdef USE_TIME | ||||||
|  |   optional<time::RealTimeClock *> time_id_{}; | ||||||
|  | #endif | ||||||
|   TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; |   TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; | ||||||
|   int gpio_status_ = -1; |   int gpio_status_ = -1; | ||||||
|   int gpio_reset_ = -1; |   int gpio_reset_ = -1; | ||||||
|  |   uint32_t last_command_timestamp_ = 0; | ||||||
|   std::string product_ = ""; |   std::string product_ = ""; | ||||||
|   std::vector<TuyaDatapointListener> listeners_; |   std::vector<TuyaDatapointListener> listeners_; | ||||||
|   std::vector<TuyaDatapoint> datapoints_; |   std::vector<TuyaDatapoint> datapoints_; | ||||||
|   | |||||||
| @@ -49,7 +49,12 @@ web_server: | |||||||
|     username: admin |     username: admin | ||||||
|     password: admin |     password: admin | ||||||
|  |  | ||||||
|  | time: | ||||||
|  |   - platform: sntp | ||||||
|  |     id: sntp_time | ||||||
|  |  | ||||||
| tuya: | tuya: | ||||||
|  |   time_id: sntp_time | ||||||
|  |  | ||||||
| sensor: | sensor: | ||||||
|   - platform: homeassistant |   - platform: homeassistant | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user