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:
Yaroslav 2020-11-11 00:31:28 +02:00 committed by GitHub
parent eb5c4b7c4f
commit 9fed7cab5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 92 additions and 11 deletions

View file

@ -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_))

View file

@ -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);
}

View file

@ -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<void(TuyaDatapoint)> &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::RealTimeClock *> 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<TuyaDatapointListener> listeners_;
std::vector<TuyaDatapoint> datapoints_;

View file

@ -49,7 +49,12 @@ web_server:
username: admin
password: admin
time:
- platform: sntp
id: sntp_time
tuya:
time_id: sntp_time
sensor:
- platform: homeassistant