diff --git a/esphome/components/tuya/__init__.py b/esphome/components/tuya/__init__.py index 541f10f862..114099cc60 100644 --- a/esphome/components/tuya/__init__.py +++ b/esphome/components/tuya/__init__.py @@ -1,7 +1,8 @@ +from esphome.components import time import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import uart -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_TIME_ID DEPENDENCIES = ['uart'] @@ -11,6 +12,7 @@ Tuya = tuya_ns.class_('Tuya', cg.Component, uart.UARTDevice) CONF_TUYA_ID = 'tuya_id' CONFIG_SCHEMA = cv.Schema({ 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) @@ -18,3 +20,6 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(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_)) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 644babbdec..380129a61e 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -6,9 +6,10 @@ namespace esphome { namespace tuya { static const char *TAG = "tuya"; +static const int COMMAND_DELAY = 50; 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() { @@ -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() { ESP_LOGCONFIG(TAG, "Tuya:"); 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) { + this->last_command_timestamp_ = millis(); switch ((TuyaCommandType) command) { case TuyaCommandType::HEARTBEAT: 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) { this->init_state_ = TuyaInitState::INIT_PRODUCT; - this->send_empty_command_(TuyaCommandType::PRODUCT_QUERY); + this->schedule_empty_command_(TuyaCommandType::PRODUCT_QUERY); } break; 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) { this->init_state_ = TuyaInitState::INIT_CONF; - this->send_empty_command_(TuyaCommandType::CONF_QUERY); + this->schedule_empty_command_(TuyaCommandType::CONF_QUERY); } break; } @@ -148,19 +159,27 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff gpio_reset_ = buffer[1]; } 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); + // If mcu returned status gpio, then we can ommit sending wifi state + if (this->gpio_status_ != 0) { + this->init_state_ = TuyaInitState::INIT_DATAPOINT; + this->schedule_empty_command_(TuyaCommandType::DATAPOINT_QUERY); + } else { + 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; } case TuyaCommandType::WIFI_STATE: if (this->init_state_ == TuyaInitState::INIT_WIFI) { this->init_state_ = TuyaInitState::INIT_DATAPOINT; - this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY); + this->schedule_empty_command_(TuyaCommandType::DATAPOINT_QUERY); } break; 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); 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: ESP_LOGE(TAG, "invalid command (%02x) received", command); } diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 2fc9a16d44..3a66aecb1a 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -1,8 +1,13 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/components/uart/uart.h" +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + namespace esphome { namespace tuya { @@ -43,6 +48,7 @@ enum class TuyaCommandType : uint8_t { DATAPOINT_REPORT = 0x07, DATAPOINT_QUERY = 0x08, WIFI_TEST = 0x0E, + LOCAL_TIME_QUERY = 0x1C, }; enum class TuyaInitState : uint8_t { @@ -62,6 +68,9 @@ class Tuya : public Component, public uart::UARTDevice { void dump_config() override; void register_listener(uint8_t datapoint_id, const std::function &func); void set_datapoint_value(TuyaDatapoint datapoint); +#ifdef USE_TIME + void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } +#endif protected: 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 send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len); void send_empty_command_(TuyaCommandType command) { this->send_command_(command, nullptr, 0); } + void schedule_empty_command_(TuyaCommandType command); +#ifdef USE_TIME + optional time_id_{}; +#endif TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; int gpio_status_ = -1; int gpio_reset_ = -1; + uint32_t last_command_timestamp_ = 0; std::string product_ = ""; std::vector listeners_; std::vector datapoints_; diff --git a/tests/test4.yaml b/tests/test4.yaml index 51e4c6311f..768252efd2 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -49,7 +49,12 @@ web_server: username: admin password: admin +time: + - platform: sntp + id: sntp_time + tuya: + time_id: sntp_time sensor: - platform: homeassistant