mirror of
https://github.com/esphome/esphome.git
synced 2024-11-26 08:55:22 +01:00
Add new component for Tuya dimmers (#743)
* Add new component for Tuya dimmers * Update code * Class naming * Log output * Fixes * Lint * Format * Fix test * log setting datapoint values * remove in_setup_ and fix datapoint handling Co-authored-by: Samuel Sieb <samuel@sieb.net> Co-authored-by: Otto Winter <otto@otto-winter.com>
This commit is contained in:
parent
af15a4e710
commit
96ff9a162c
9 changed files with 571 additions and 17 deletions
|
@ -1,6 +1,7 @@
|
||||||
#include "esp32_ble_tracker.h"
|
#include "esp32_ble_tracker.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
#ifdef ARDUINO_ARCH_ESP32
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
|
|
||||||
|
@ -202,20 +203,8 @@ void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string hexencode(const std::string &raw_data) {
|
std::string hexencode_string(const std::string &raw_data) {
|
||||||
char buf[20];
|
return hexencode(reinterpret_cast<const uint8_t *>(raw_data.c_str()), raw_data.size());
|
||||||
std::string res;
|
|
||||||
for (size_t i = 0; i < raw_data.size(); i++) {
|
|
||||||
if (i + 1 != raw_data.size()) {
|
|
||||||
sprintf(buf, "0x%02X.", static_cast<uint8_t>(raw_data[i]));
|
|
||||||
} else {
|
|
||||||
sprintf(buf, "0x%02X ", static_cast<uint8_t>(raw_data[i]));
|
|
||||||
}
|
|
||||||
res += buf;
|
|
||||||
}
|
|
||||||
sprintf(buf, "(%zu)", raw_data.size());
|
|
||||||
res += buf;
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ESPBTUUID::ESPBTUUID() : uuid_() {}
|
ESPBTUUID::ESPBTUUID() : uuid_() {}
|
||||||
|
@ -327,15 +316,15 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e
|
||||||
for (auto uuid : this->service_uuids_) {
|
for (auto uuid : this->service_uuids_) {
|
||||||
ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str());
|
ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str());
|
||||||
}
|
}
|
||||||
ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode(this->manufacturer_data_).c_str());
|
ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode_string(this->manufacturer_data_).c_str());
|
||||||
ESP_LOGVV(TAG, " Service data: %s", hexencode(this->service_data_).c_str());
|
ESP_LOGVV(TAG, " Service data: %s", hexencode_string(this->service_data_).c_str());
|
||||||
|
|
||||||
if (this->service_data_uuid_.has_value()) {
|
if (this->service_data_uuid_.has_value()) {
|
||||||
ESP_LOGVV(TAG, " Service Data UUID: %s", this->service_data_uuid_->to_string().c_str());
|
ESP_LOGVV(TAG, " Service Data UUID: %s", this->service_data_uuid_->to_string().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGVV(TAG, "Adv data: %s",
|
ESP_LOGVV(TAG, "Adv data: %s",
|
||||||
hexencode(std::string(reinterpret_cast<const char *>(param.ble_adv), param.adv_data_len)).c_str());
|
hexencode_string(std::string(reinterpret_cast<const char *>(param.ble_adv), param.adv_data_len)).c_str());
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) {
|
void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) {
|
||||||
|
|
20
esphome/components/tuya/__init__.py
Normal file
20
esphome/components/tuya/__init__.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.components import uart
|
||||||
|
from esphome.const import CONF_ID
|
||||||
|
|
||||||
|
DEPENDENCIES = ['uart']
|
||||||
|
|
||||||
|
tuya_ns = cg.esphome_ns.namespace('tuya')
|
||||||
|
Tuya = tuya_ns.class_('Tuya', cg.Component, uart.UARTDevice)
|
||||||
|
|
||||||
|
CONF_TUYA_ID = 'tuya_id'
|
||||||
|
CONFIG_SCHEMA = cv.Schema({
|
||||||
|
cv.GenerateID(): cv.declare_id(Tuya),
|
||||||
|
}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
yield cg.register_component(var, config)
|
||||||
|
yield uart.register_uart_device(var, config)
|
38
esphome/components/tuya/light/__init__.py
Normal file
38
esphome/components/tuya/light/__init__.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
from esphome.components import light
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.const import CONF_OUTPUT_ID, CONF_MIN_VALUE, CONF_MAX_VALUE
|
||||||
|
from .. import tuya_ns, CONF_TUYA_ID, Tuya
|
||||||
|
|
||||||
|
DEPENDENCIES = ['tuya']
|
||||||
|
|
||||||
|
CONF_DIMMER_DATAPOINT = "dimmer_datapoint"
|
||||||
|
CONF_SWITCH_DATAPOINT = "switch_datapoint"
|
||||||
|
|
||||||
|
TuyaLight = tuya_ns.class_('TuyaLight', light.LightOutput, cg.Component)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({
|
||||||
|
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaLight),
|
||||||
|
cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya),
|
||||||
|
cv.Required(CONF_DIMMER_DATAPOINT): cv.uint8_t,
|
||||||
|
cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
|
||||||
|
cv.Optional(CONF_MIN_VALUE): cv.int_,
|
||||||
|
cv.Optional(CONF_MAX_VALUE): cv.int_,
|
||||||
|
}).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||||
|
yield cg.register_component(var, config)
|
||||||
|
yield light.register_light(var, config)
|
||||||
|
|
||||||
|
if CONF_DIMMER_DATAPOINT in config:
|
||||||
|
cg.add(var.set_dimmer_id(config[CONF_DIMMER_DATAPOINT]))
|
||||||
|
if CONF_SWITCH_DATAPOINT in config:
|
||||||
|
cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT]))
|
||||||
|
if CONF_MIN_VALUE in config:
|
||||||
|
cg.add(var.set_min_value(config[CONF_MIN_VALUE]))
|
||||||
|
if CONF_MAX_VALUE in config:
|
||||||
|
cg.add(var.set_max_value(config[CONF_MAX_VALUE]))
|
||||||
|
paren = yield cg.get_variable(config[CONF_TUYA_ID])
|
||||||
|
cg.add(var.set_tuya_parent(paren))
|
85
esphome/components/tuya/light/tuya_light.cpp
Normal file
85
esphome/components/tuya/light/tuya_light.cpp
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "tuya_light.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace tuya {
|
||||||
|
|
||||||
|
static const char *TAG = "tuya.light";
|
||||||
|
|
||||||
|
void TuyaLight::setup() {
|
||||||
|
if (this->dimmer_id_.has_value()) {
|
||||||
|
this->parent_->register_listener(*this->dimmer_id_, [this](TuyaDatapoint datapoint) {
|
||||||
|
auto call = this->state_->make_call();
|
||||||
|
call.set_brightness(float(datapoint.value_uint) / this->max_value_);
|
||||||
|
call.perform();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (switch_id_.has_value()) {
|
||||||
|
this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) {
|
||||||
|
auto call = this->state_->make_call();
|
||||||
|
call.set_state(datapoint.value_bool);
|
||||||
|
call.perform();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TuyaLight::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Tuya Dimmer:");
|
||||||
|
if (this->dimmer_id_.has_value())
|
||||||
|
ESP_LOGCONFIG(TAG, " Dimmer has datapoint ID %u", *this->dimmer_id_);
|
||||||
|
if (this->switch_id_.has_value())
|
||||||
|
ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
light::LightTraits TuyaLight::get_traits() {
|
||||||
|
auto traits = light::LightTraits();
|
||||||
|
traits.set_supports_brightness(this->dimmer_id_.has_value());
|
||||||
|
return traits;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TuyaLight::setup_state(light::LightState *state) { state_ = state; }
|
||||||
|
|
||||||
|
void TuyaLight::write_state(light::LightState *state) {
|
||||||
|
float brightness;
|
||||||
|
state->current_values_as_brightness(&brightness);
|
||||||
|
|
||||||
|
if (brightness == 0.0f) {
|
||||||
|
// turning off, first try via switch (if exists), then dimmer
|
||||||
|
if (switch_id_.has_value()) {
|
||||||
|
TuyaDatapoint datapoint{};
|
||||||
|
datapoint.id = *this->switch_id_;
|
||||||
|
datapoint.type = TuyaDatapointType::BOOLEAN;
|
||||||
|
datapoint.value_bool = false;
|
||||||
|
|
||||||
|
parent_->set_datapoint_value(datapoint);
|
||||||
|
} else if (dimmer_id_.has_value()) {
|
||||||
|
TuyaDatapoint datapoint{};
|
||||||
|
datapoint.id = *this->dimmer_id_;
|
||||||
|
datapoint.type = TuyaDatapointType::INTEGER;
|
||||||
|
datapoint.value_int = 0;
|
||||||
|
parent_->set_datapoint_value(datapoint);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto brightness_int = static_cast<uint32_t>(brightness * this->max_value_);
|
||||||
|
brightness_int = std::max(brightness_int, this->min_value_);
|
||||||
|
|
||||||
|
if (this->dimmer_id_.has_value()) {
|
||||||
|
TuyaDatapoint datapoint{};
|
||||||
|
datapoint.id = *this->dimmer_id_;
|
||||||
|
datapoint.type = TuyaDatapointType::INTEGER;
|
||||||
|
datapoint.value_int = brightness_int;
|
||||||
|
parent_->set_datapoint_value(datapoint);
|
||||||
|
}
|
||||||
|
if (this->switch_id_.has_value()) {
|
||||||
|
TuyaDatapoint datapoint{};
|
||||||
|
datapoint.id = *this->switch_id_;
|
||||||
|
datapoint.type = TuyaDatapointType::BOOLEAN;
|
||||||
|
datapoint.value_bool = true;
|
||||||
|
parent_->set_datapoint_value(datapoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tuya
|
||||||
|
} // namespace esphome
|
36
esphome/components/tuya/light/tuya_light.h
Normal file
36
esphome/components/tuya/light/tuya_light.h
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/tuya/tuya.h"
|
||||||
|
#include "esphome/components/light/light_output.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace tuya {
|
||||||
|
|
||||||
|
class TuyaLight : public Component, public light::LightOutput {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void dump_config() override;
|
||||||
|
void set_dimmer_id(uint8_t dimmer_id) { this->dimmer_id_ = dimmer_id; }
|
||||||
|
void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; }
|
||||||
|
void set_tuya_parent(Tuya *parent) { this->parent_ = parent; }
|
||||||
|
void set_min_value(uint32_t min_value) { min_value_ = min_value; }
|
||||||
|
void set_max_value(uint32_t max_value) { max_value_ = max_value; }
|
||||||
|
light::LightTraits get_traits() override;
|
||||||
|
void setup_state(light::LightState *state) override;
|
||||||
|
void write_state(light::LightState *state) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void update_dimmer_(uint32_t value);
|
||||||
|
void update_switch_(uint32_t value);
|
||||||
|
|
||||||
|
Tuya *parent_;
|
||||||
|
optional<uint8_t> dimmer_id_{};
|
||||||
|
optional<uint8_t> switch_id_{};
|
||||||
|
uint32_t min_value_ = 0;
|
||||||
|
uint32_t max_value_ = 255;
|
||||||
|
light::LightState *state_{nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace tuya
|
||||||
|
} // namespace esphome
|
294
esphome/components/tuya/tuya.cpp
Normal file
294
esphome/components/tuya/tuya.cpp
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
#include "tuya.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
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); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tuya::loop() {
|
||||||
|
while (this->available()) {
|
||||||
|
uint8_t c;
|
||||||
|
this->read_byte(&c);
|
||||||
|
this->handle_char_(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tuya::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Tuya:");
|
||||||
|
if ((gpio_status_ != -1) || (gpio_reset_ != -1))
|
||||||
|
ESP_LOGCONFIG(TAG, " GPIO MCU configuration not supported!");
|
||||||
|
for (auto &info : this->datapoints_) {
|
||||||
|
if (info.type == TuyaDatapointType::BOOLEAN)
|
||||||
|
ESP_LOGCONFIG(TAG, " Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool));
|
||||||
|
else if (info.type == TuyaDatapointType::INTEGER)
|
||||||
|
ESP_LOGCONFIG(TAG, " Datapoint %d: int value (value: %d)", info.id, info.value_int);
|
||||||
|
else if (info.type == TuyaDatapointType::ENUM)
|
||||||
|
ESP_LOGCONFIG(TAG, " Datapoint %d: enum (value: %d)", info.id, info.value_enum);
|
||||||
|
else if (info.type == TuyaDatapointType::BITMASK)
|
||||||
|
ESP_LOGCONFIG(TAG, " Datapoint %d: bitmask (value: %x)", info.id, info.value_bitmask);
|
||||||
|
else
|
||||||
|
ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Tuya::validate_message_() {
|
||||||
|
uint32_t at = this->rx_message_.size() - 1;
|
||||||
|
auto *data = &this->rx_message_[0];
|
||||||
|
uint8_t new_byte = data[at];
|
||||||
|
|
||||||
|
// Byte 0: HEADER1 (always 0x55)
|
||||||
|
if (at == 0)
|
||||||
|
return new_byte == 0x55;
|
||||||
|
// Byte 1: HEADER2 (always 0xAA)
|
||||||
|
if (at == 1)
|
||||||
|
return new_byte == 0xAA;
|
||||||
|
|
||||||
|
// Byte 2: VERSION
|
||||||
|
// no validation for the following fields:
|
||||||
|
uint8_t version = data[2];
|
||||||
|
if (at == 2)
|
||||||
|
return true;
|
||||||
|
// Byte 3: COMMAND
|
||||||
|
uint8_t command = data[3];
|
||||||
|
if (at == 3)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Byte 4: LENGTH1
|
||||||
|
// Byte 5: LENGTH2
|
||||||
|
if (at <= 5)
|
||||||
|
// no validation for these fields
|
||||||
|
return true;
|
||||||
|
|
||||||
|
uint16_t length = (uint16_t(data[4]) << 8) | (uint16_t(data[5]));
|
||||||
|
|
||||||
|
// wait until all data is read
|
||||||
|
if (at - 6 < length)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Byte 6+LEN: CHECKSUM - sum of all bytes (including header) modulo 256
|
||||||
|
uint8_t rx_checksum = new_byte;
|
||||||
|
uint8_t calc_checksum = 0;
|
||||||
|
for (uint32_t i = 0; i < 6 + length; i++)
|
||||||
|
calc_checksum += data[i];
|
||||||
|
|
||||||
|
if (rx_checksum != calc_checksum) {
|
||||||
|
ESP_LOGW(TAG, "Tuya Received invalid message checksum %02X!=%02X", rx_checksum, calc_checksum);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
this->handle_command_(command, version, message_data, length);
|
||||||
|
|
||||||
|
// return false to reset rx buffer
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tuya::handle_char_(uint8_t c) {
|
||||||
|
this->rx_message_.push_back(c);
|
||||||
|
if (!this->validate_message_()) {
|
||||||
|
this->rx_message_.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TuyaCommandType::QUERY_PRODUCT: {
|
||||||
|
// check it is a valid string
|
||||||
|
bool valid = false;
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
if (buffer[i] == 0x00) {
|
||||||
|
valid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (valid) {
|
||||||
|
ESP_LOGD(TAG, "Tuya Product Code: %s", reinterpret_cast<const char *>(buffer));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TuyaCommandType::MCU_CONF:
|
||||||
|
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);
|
||||||
|
break;
|
||||||
|
case TuyaCommandType::WIFI_STATE:
|
||||||
|
break;
|
||||||
|
case TuyaCommandType::WIFI_RESET:
|
||||||
|
ESP_LOGE(TAG, "TUYA_CMD_WIFI_RESET is not handled");
|
||||||
|
break;
|
||||||
|
case TuyaCommandType::WIFI_SELECT:
|
||||||
|
ESP_LOGE(TAG, "TUYA_CMD_WIFI_SELECT is not handled");
|
||||||
|
break;
|
||||||
|
case TuyaCommandType::SET_DATAPOINT:
|
||||||
|
break;
|
||||||
|
case TuyaCommandType::STATE: {
|
||||||
|
this->handle_datapoint_(buffer, len);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TuyaCommandType::QUERY_STATE:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE(TAG, "invalid command (%02x) received", command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) {
|
||||||
|
if (len < 2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TuyaDatapoint datapoint{};
|
||||||
|
datapoint.id = buffer[0];
|
||||||
|
datapoint.type = (TuyaDatapointType) buffer[1];
|
||||||
|
datapoint.value_uint = 0;
|
||||||
|
|
||||||
|
size_t data_size = (buffer[2] << 8) + buffer[3];
|
||||||
|
const uint8_t *data = buffer + 4;
|
||||||
|
size_t data_len = len - 4;
|
||||||
|
if (data_size != data_len) {
|
||||||
|
ESP_LOGW(TAG, "invalid datapoint update");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (datapoint.type) {
|
||||||
|
case TuyaDatapointType::BOOLEAN:
|
||||||
|
if (data_len != 1)
|
||||||
|
return;
|
||||||
|
datapoint.value_bool = data[0];
|
||||||
|
break;
|
||||||
|
case TuyaDatapointType::INTEGER:
|
||||||
|
if (data_len != 4)
|
||||||
|
return;
|
||||||
|
datapoint.value_uint =
|
||||||
|
(uint32_t(data[0]) << 24) | (uint32_t(data[1]) << 16) | (uint32_t(data[2]) << 8) | (uint32_t(data[3]) << 0);
|
||||||
|
break;
|
||||||
|
case TuyaDatapointType::ENUM:
|
||||||
|
if (data_len != 1)
|
||||||
|
return;
|
||||||
|
datapoint.value_enum = data[0];
|
||||||
|
break;
|
||||||
|
case TuyaDatapointType::BITMASK:
|
||||||
|
if (data_len != 2)
|
||||||
|
return;
|
||||||
|
datapoint.value_bitmask = (uint16_t(data[0]) << 8) | (uint16_t(data[1]) << 0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGV(TAG, "Datapoint %u update to %u", datapoint.id, datapoint.value_uint);
|
||||||
|
|
||||||
|
// Update internal datapoints
|
||||||
|
bool found = false;
|
||||||
|
for (auto &other : this->datapoints_) {
|
||||||
|
if (other.id == datapoint.id) {
|
||||||
|
other = datapoint;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
for (auto &listener : this->listeners_)
|
||||||
|
if (listener.datapoint_id == datapoint.id)
|
||||||
|
listener.on_datapoint(datapoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
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});
|
||||||
|
if (len != 0)
|
||||||
|
this->write_array(buffer, len);
|
||||||
|
|
||||||
|
uint8_t checksum = 0x55 + 0xAA + (uint8_t) command + len_hi + len_lo;
|
||||||
|
for (int i = 0; i < len; i++)
|
||||||
|
checksum += buffer[i];
|
||||||
|
this->write_byte(checksum);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tuya::set_datapoint_value(TuyaDatapoint datapoint) {
|
||||||
|
std::vector<uint8_t> buffer;
|
||||||
|
ESP_LOGV(TAG, "Datapoint %u set to %u", datapoint.id, datapoint.value_uint);
|
||||||
|
for (auto &other : this->datapoints_) {
|
||||||
|
if (other.id == datapoint.id) {
|
||||||
|
if (other.value_uint == datapoint.value_uint) {
|
||||||
|
ESP_LOGV(TAG, "Not sending unchanged value");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer.push_back(datapoint.id);
|
||||||
|
buffer.push_back(static_cast<uint8_t>(datapoint.type));
|
||||||
|
|
||||||
|
std::vector<uint8_t> data;
|
||||||
|
switch (datapoint.type) {
|
||||||
|
case TuyaDatapointType::BOOLEAN:
|
||||||
|
data.push_back(datapoint.value_bool);
|
||||||
|
break;
|
||||||
|
case TuyaDatapointType::INTEGER:
|
||||||
|
data.push_back(datapoint.value_uint >> 24);
|
||||||
|
data.push_back(datapoint.value_uint >> 16);
|
||||||
|
data.push_back(datapoint.value_uint >> 8);
|
||||||
|
data.push_back(datapoint.value_uint >> 0);
|
||||||
|
break;
|
||||||
|
case TuyaDatapointType::ENUM:
|
||||||
|
data.push_back(datapoint.value_enum);
|
||||||
|
break;
|
||||||
|
case TuyaDatapointType::BITMASK:
|
||||||
|
data.push_back(datapoint.value_bitmask >> 8);
|
||||||
|
data.push_back(datapoint.value_bitmask >> 0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tuya::register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func) {
|
||||||
|
auto listener = TuyaDatapointListener{
|
||||||
|
.datapoint_id = datapoint_id,
|
||||||
|
.on_datapoint = func,
|
||||||
|
};
|
||||||
|
this->listeners_.push_back(listener);
|
||||||
|
|
||||||
|
// Run through existing datapoints
|
||||||
|
for (auto &datapoint : this->datapoints_)
|
||||||
|
if (datapoint.id == datapoint_id)
|
||||||
|
func(datapoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace tuya
|
||||||
|
} // namespace esphome
|
73
esphome/components/tuya/tuya.h
Normal file
73
esphome/components/tuya/tuya.h
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/uart/uart.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace tuya {
|
||||||
|
|
||||||
|
enum class TuyaDatapointType : uint8_t {
|
||||||
|
RAW = 0x00, // variable length
|
||||||
|
BOOLEAN = 0x01, // 1 byte (0/1)
|
||||||
|
INTEGER = 0x02, // 4 byte
|
||||||
|
STRING = 0x03, // variable length
|
||||||
|
ENUM = 0x04, // 1 byte
|
||||||
|
BITMASK = 0x05, // 2 bytes
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TuyaDatapoint {
|
||||||
|
uint8_t id;
|
||||||
|
TuyaDatapointType type;
|
||||||
|
union {
|
||||||
|
bool value_bool;
|
||||||
|
int value_int;
|
||||||
|
uint32_t value_uint;
|
||||||
|
uint8_t value_enum;
|
||||||
|
uint16_t value_bitmask;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TuyaDatapointListener {
|
||||||
|
uint8_t datapoint_id;
|
||||||
|
std::function<void(TuyaDatapoint)> on_datapoint;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class TuyaCommandType : uint8_t {
|
||||||
|
HEARTBEAT = 0x00,
|
||||||
|
QUERY_PRODUCT = 0x01,
|
||||||
|
MCU_CONF = 0x02,
|
||||||
|
WIFI_STATE = 0x03,
|
||||||
|
WIFI_RESET = 0x04,
|
||||||
|
WIFI_SELECT = 0x05,
|
||||||
|
SET_DATAPOINT = 0x06,
|
||||||
|
STATE = 0x07,
|
||||||
|
QUERY_STATE = 0x08,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Tuya : public Component, public uart::UARTDevice {
|
||||||
|
public:
|
||||||
|
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||||
|
void setup() override;
|
||||||
|
void loop() override;
|
||||||
|
void dump_config() override;
|
||||||
|
void register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func);
|
||||||
|
void set_datapoint_value(TuyaDatapoint datapoint);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void handle_char_(uint8_t c);
|
||||||
|
void handle_datapoint_(const uint8_t *buffer, size_t len);
|
||||||
|
bool validate_message_();
|
||||||
|
|
||||||
|
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); }
|
||||||
|
|
||||||
|
int gpio_status_ = -1;
|
||||||
|
int gpio_reset_ = -1;
|
||||||
|
std::vector<TuyaDatapointListener> listeners_;
|
||||||
|
std::vector<TuyaDatapoint> datapoints_;
|
||||||
|
std::vector<uint8_t> rx_message_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace tuya
|
||||||
|
} // namespace esphome
|
|
@ -314,4 +314,20 @@ std::array<uint8_t, 2> decode_uint16(uint16_t value) {
|
||||||
return {msb, lsb};
|
return {msb, lsb};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string hexencode(const uint8_t *data, uint32_t len) {
|
||||||
|
char buf[20];
|
||||||
|
std::string res;
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
if (i + 1 != len) {
|
||||||
|
sprintf(buf, "%02X.", data[i]);
|
||||||
|
} else {
|
||||||
|
sprintf(buf, "%02X ", data[i]);
|
||||||
|
}
|
||||||
|
res += buf;
|
||||||
|
}
|
||||||
|
sprintf(buf, "(%u)", len);
|
||||||
|
res += buf;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
|
@ -156,6 +156,9 @@ enum ParseOnOffState {
|
||||||
|
|
||||||
ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr);
|
ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr);
|
||||||
|
|
||||||
|
// Encode raw data to a human-readable string (for debugging)
|
||||||
|
std::string hexencode(const uint8_t *data, uint32_t len);
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971
|
// https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971
|
||||||
template<int...> struct seq {}; // NOLINT
|
template<int...> struct seq {}; // NOLINT
|
||||||
template<int N, int... S> struct gens : gens<N - 1, N - 1, S...> {}; // NOLINT
|
template<int N, int... S> struct gens : gens<N - 1, N - 1, S...> {}; // NOLINT
|
||||||
|
|
Loading…
Reference in a new issue