mirror of
https://github.com/esphome/esphome.git
synced 2024-11-30 10:44:13 +01: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:
parent
531428b8b0
commit
c5c42c4338
2 changed files with 80 additions and 37 deletions
|
@ -8,7 +8,6 @@ namespace tuya {
|
||||||
static const char *TAG = "tuya";
|
static const char *TAG = "tuya";
|
||||||
|
|
||||||
void Tuya::setup() {
|
void Tuya::setup() {
|
||||||
this->send_empty_command_(TuyaCommandType::MCU_CONF);
|
|
||||||
this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
|
this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,8 +21,12 @@ void Tuya::loop() {
|
||||||
|
|
||||||
void Tuya::dump_config() {
|
void Tuya::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "Tuya:");
|
ESP_LOGCONFIG(TAG, "Tuya:");
|
||||||
if ((gpio_status_ != -1) || (gpio_reset_ != -1))
|
if (this->init_state_ != TuyaInitState::INIT_DONE) {
|
||||||
ESP_LOGCONFIG(TAG, " GPIO MCU configuration not supported!");
|
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_) {
|
for (auto &info : this->datapoints_) {
|
||||||
if (info.type == TuyaDatapointType::BOOLEAN)
|
if (info.type == TuyaDatapointType::BOOLEAN)
|
||||||
ESP_LOGCONFIG(TAG, " Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool));
|
ESP_LOGCONFIG(TAG, " Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool));
|
||||||
|
@ -36,9 +39,11 @@ void Tuya::dump_config() {
|
||||||
else
|
else
|
||||||
ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id);
|
ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id);
|
||||||
}
|
}
|
||||||
if (this->datapoints_.empty()) {
|
if ((this->gpio_status_ != -1) || (this->gpio_reset_ != -1)) {
|
||||||
ESP_LOGCONFIG(TAG, " Received no datapoints! Please make sure this is a supported Tuya device.");
|
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);
|
this->check_uart_settings(9600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,8 +94,8 @@ bool Tuya::validate_message_() {
|
||||||
|
|
||||||
// valid message
|
// valid message
|
||||||
const uint8_t *message_data = data + 6;
|
const uint8_t *message_data = data + 6;
|
||||||
ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s]", command, version,
|
ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version, // NOLINT
|
||||||
hexencode(message_data, length).c_str());
|
hexencode(message_data, length).c_str(), this->init_state_);
|
||||||
this->handle_command_(command, version, message_data, length);
|
this->handle_command_(command, version, message_data, length);
|
||||||
|
|
||||||
// return false to reset rx buffer
|
// 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) {
|
void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) {
|
||||||
uint8_t c;
|
|
||||||
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]);
|
||||||
if (buffer[0] == 0) {
|
if (buffer[0] == 0) {
|
||||||
ESP_LOGI(TAG, "MCU restarted");
|
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;
|
break;
|
||||||
case TuyaCommandType::QUERY_PRODUCT: {
|
case TuyaCommandType::PRODUCT_QUERY: {
|
||||||
// check it is a valid string
|
// check it is a valid string made up of printable characters
|
||||||
bool valid = false;
|
bool valid = true;
|
||||||
for (int i = 0; i < len; i++) {
|
for (int i = 0; i < len; i++) {
|
||||||
if (buffer[i] == 0x00) {
|
if (!std::isprint(buffer[i])) {
|
||||||
valid = true;
|
valid = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (valid) {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case TuyaCommandType::MCU_CONF:
|
case TuyaCommandType::CONF_QUERY: {
|
||||||
if (len >= 2) {
|
if (len >= 2) {
|
||||||
gpio_status_ = buffer[0];
|
gpio_status_ = buffer[0];
|
||||||
gpio_reset_ = buffer[1];
|
gpio_reset_ = buffer[1];
|
||||||
}
|
}
|
||||||
// set wifi state LED to off or on depending on the MCU firmware
|
if (this->init_state_ == TuyaInitState::INIT_CONF) {
|
||||||
// but it shouldn't be blinking
|
// If we were following the spec to the letter we would send
|
||||||
c = 0x3;
|
// state updates until connected to both WiFi and API/MQTT.
|
||||||
this->send_command_(TuyaCommandType::WIFI_STATE, &c, 1);
|
// Instead we just claim to be connected immediately and move on.
|
||||||
this->send_empty_command_(TuyaCommandType::QUERY_STATE);
|
uint8_t c[] = {0x04};
|
||||||
|
this->init_state_ = TuyaInitState::INIT_WIFI;
|
||||||
|
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) {
|
||||||
|
this->init_state_ = TuyaInitState::INIT_DATAPOINT;
|
||||||
|
this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case TuyaCommandType::WIFI_RESET:
|
case TuyaCommandType::WIFI_RESET:
|
||||||
ESP_LOGE(TAG, "TUYA_CMD_WIFI_RESET is not handled");
|
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:
|
case TuyaCommandType::WIFI_SELECT:
|
||||||
ESP_LOGE(TAG, "TUYA_CMD_WIFI_SELECT is not handled");
|
ESP_LOGE(TAG, "TUYA_CMD_WIFI_SELECT is not handled");
|
||||||
break;
|
break;
|
||||||
case TuyaCommandType::SET_DATAPOINT:
|
case TuyaCommandType::DATAPOINT_DELIVER:
|
||||||
break;
|
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);
|
this->handle_datapoint_(buffer, len);
|
||||||
break;
|
break;
|
||||||
}
|
case TuyaCommandType::DATAPOINT_QUERY:
|
||||||
case TuyaCommandType::QUERY_STATE:
|
|
||||||
break;
|
break;
|
||||||
|
case TuyaCommandType::WIFI_TEST: {
|
||||||
|
uint8_t c[] = {0x00, 0x00};
|
||||||
|
this->send_command_(TuyaCommandType::WIFI_TEST, c, 2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
ESP_LOGE(TAG, "invalid command (%02x) received", command);
|
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) {
|
if (!found) {
|
||||||
this->datapoints_.push_back(datapoint);
|
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
|
// 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) {
|
void Tuya::send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len) {
|
||||||
uint8_t len_hi = len >> 8;
|
uint8_t len_hi = len >> 8;
|
||||||
uint8_t len_lo = len >> 0;
|
uint8_t len_lo = len >> 0;
|
||||||
this->write_array({0x55, 0xAA,
|
uint8_t version = 0;
|
||||||
0x00, // version
|
|
||||||
(uint8_t) command, len_hi, len_lo});
|
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)
|
if (len != 0)
|
||||||
this->write_array(buffer, len);
|
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() >> 8);
|
||||||
buffer.push_back(data.size() >> 0);
|
buffer.push_back(data.size() >> 0);
|
||||||
buffer.insert(buffer.end(), data.begin(), data.end());
|
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) {
|
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 {
|
enum class TuyaCommandType : uint8_t {
|
||||||
HEARTBEAT = 0x00,
|
HEARTBEAT = 0x00,
|
||||||
QUERY_PRODUCT = 0x01,
|
PRODUCT_QUERY = 0x01,
|
||||||
MCU_CONF = 0x02,
|
CONF_QUERY = 0x02,
|
||||||
WIFI_STATE = 0x03,
|
WIFI_STATE = 0x03,
|
||||||
WIFI_RESET = 0x04,
|
WIFI_RESET = 0x04,
|
||||||
WIFI_SELECT = 0x05,
|
WIFI_SELECT = 0x05,
|
||||||
SET_DATAPOINT = 0x06,
|
DATAPOINT_DELIVER = 0x06,
|
||||||
STATE = 0x07,
|
DATAPOINT_REPORT = 0x07,
|
||||||
QUERY_STATE = 0x08,
|
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 {
|
class Tuya : public Component, public uart::UARTDevice {
|
||||||
public:
|
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 setup() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
void dump_config() 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_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); }
|
||||||
|
|
||||||
|
TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT;
|
||||||
int gpio_status_ = -1;
|
int gpio_status_ = -1;
|
||||||
int gpio_reset_ = -1;
|
int gpio_reset_ = -1;
|
||||||
|
std::string product_ = "";
|
||||||
std::vector<TuyaDatapointListener> listeners_;
|
std::vector<TuyaDatapointListener> listeners_;
|
||||||
std::vector<TuyaDatapoint> datapoints_;
|
std::vector<TuyaDatapoint> datapoints_;
|
||||||
std::vector<uint8_t> rx_message_;
|
std::vector<uint8_t> rx_message_;
|
||||||
|
|
Loading…
Reference in a new issue