mirror of
https://github.com/nuttytree/ESPHome-Devices.git
synced 2024-08-30 18:12:19 +00:00
Misc work on Tuya related changes
This commit is contained in:
parent
88c2e6d0e5
commit
4b6c87b941
3
.gitignore
vendored
3
.gitignore
vendored
@ -8,3 +8,6 @@
|
||||
/build/
|
||||
**/platformio.ini
|
||||
/secrets.yaml
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
39
components/tuya/__init__.py
Normal file
39
components/tuya/__init__.py
Normal file
@ -0,0 +1,39 @@
|
||||
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, CONF_TIME_ID
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS = "ignore_mcu_update_on_datapoints"
|
||||
|
||||
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),
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
cv.Optional(CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS): cv.ensure_list(
|
||||
cv.uint8_t
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
if CONF_TIME_ID in config:
|
||||
time_ = await cg.get_variable(config[CONF_TIME_ID])
|
||||
cg.add(var.set_time_id(time_))
|
||||
if CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS in config:
|
||||
for dp in config[CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS]:
|
||||
cg.add(var.add_ignore_mcu_update_on_datapoints(dp))
|
513
components/tuya/tuya.cpp
Normal file
513
components/tuya/tuya.cpp
Normal file
@ -0,0 +1,513 @@
|
||||
#include "tuya.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace tuya {
|
||||
|
||||
static const char *TAG = "tuya";
|
||||
static const int COMMAND_DELAY = 50;
|
||||
static const int RECEIVE_TIMEOUT = 300;
|
||||
|
||||
void Tuya::setup() {
|
||||
this->set_interval("heartbeat", 10000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
|
||||
}
|
||||
|
||||
void Tuya::loop() {
|
||||
while (this->available()) {
|
||||
uint8_t c;
|
||||
this->read_byte(&c);
|
||||
this->handle_char_(c);
|
||||
}
|
||||
process_command_queue_();
|
||||
}
|
||||
|
||||
void Tuya::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Tuya:");
|
||||
if (this->init_state_ != TuyaInitState::INIT_DONE) {
|
||||
ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u",
|
||||
static_cast<uint8_t>(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_) {
|
||||
if (info.type == TuyaDatapointType::RAW)
|
||||
ESP_LOGCONFIG(TAG, " Datapoint %u: raw (value: %s)", info.id, hexencode(info.value_raw).c_str());
|
||||
else if (info.type == TuyaDatapointType::BOOLEAN)
|
||||
ESP_LOGCONFIG(TAG, " Datapoint %u: switch (value: %s)", info.id, ONOFF(info.value_bool));
|
||||
else if (info.type == TuyaDatapointType::INTEGER)
|
||||
ESP_LOGCONFIG(TAG, " Datapoint %u: int value (value: %d)", info.id, info.value_int);
|
||||
else if (info.type == TuyaDatapointType::STRING)
|
||||
ESP_LOGCONFIG(TAG, " Datapoint %u: string value (value: %s)", info.id, info.value_string.c_str());
|
||||
else if (info.type == TuyaDatapointType::ENUM)
|
||||
ESP_LOGCONFIG(TAG, " Datapoint %u: enum (value: %d)", info.id, info.value_enum);
|
||||
else if (info.type == TuyaDatapointType::BITMASK)
|
||||
ESP_LOGCONFIG(TAG, " Datapoint %u: bitmask (value: %x)", info.id, info.value_bitmask);
|
||||
else
|
||||
ESP_LOGCONFIG(TAG, " Datapoint %u: unknown", info.id);
|
||||
}
|
||||
if ((this->gpio_status_ != -1) || (this->gpio_reset_ != -1)) {
|
||||
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);
|
||||
}
|
||||
|
||||
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] INIT_STATE=%u", command, version,
|
||||
hexencode(message_data, length).c_str(), static_cast<uint8_t>(this->init_state_));
|
||||
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) {
|
||||
TuyaCommandType command_type = (TuyaCommandType) command;
|
||||
|
||||
if(this->expected_response_.has_value() && this->expected_response_ == command_type)
|
||||
{
|
||||
this->expected_response_.reset();
|
||||
}
|
||||
|
||||
switch (command_type) {
|
||||
case TuyaCommandType::HEARTBEAT:
|
||||
ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]);
|
||||
this->protocol_version_ = version;
|
||||
if (buffer[0] == 0) {
|
||||
ESP_LOGI(TAG, "MCU restarted");
|
||||
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;
|
||||
case TuyaCommandType::PRODUCT_QUERY: {
|
||||
// check it is a valid string made up of printable characters
|
||||
bool valid = true;
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (!std::isprint(buffer[i])) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (valid) {
|
||||
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;
|
||||
}
|
||||
case TuyaCommandType::CONF_QUERY: {
|
||||
if (len >= 2) {
|
||||
this->gpio_status_ = buffer[0];
|
||||
this->gpio_reset_ = buffer[1];
|
||||
}
|
||||
if (this->init_state_ == TuyaInitState::INIT_CONF) {
|
||||
// If mcu returned status gpio, then we can ommit sending wifi state
|
||||
if (this->gpio_status_ != -1) {
|
||||
this->init_state_ = TuyaInitState::INIT_DATAPOINT;
|
||||
this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY);
|
||||
} else {
|
||||
this->init_state_ = TuyaInitState::INIT_WIFI;
|
||||
this->set_interval("wifi", 1000, [this] { this->send_wifi_status_(); });
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
break;
|
||||
case TuyaCommandType::WIFI_RESET:
|
||||
ESP_LOGE(TAG, "WIFI_RESET is not handled");
|
||||
break;
|
||||
case TuyaCommandType::WIFI_SELECT:
|
||||
ESP_LOGE(TAG, "WIFI_SELECT is not handled");
|
||||
break;
|
||||
case TuyaCommandType::DATAPOINT_DELIVER:
|
||||
break;
|
||||
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);
|
||||
break;
|
||||
case TuyaCommandType::DATAPOINT_QUERY:
|
||||
break;
|
||||
case TuyaCommandType::WIFI_TEST:
|
||||
this->send_command_(TuyaCommand{.cmd = TuyaCommandType::WIFI_TEST, .payload = std::vector<uint8_t>{0x00, 0x00}});
|
||||
break;
|
||||
case TuyaCommandType::LOCAL_TIME_QUERY:
|
||||
#ifdef USE_TIME
|
||||
if (this->time_id_.has_value()) {
|
||||
this->send_local_time_();
|
||||
auto time_id = *this->time_id_;
|
||||
time_id->add_on_time_sync_callback([this] { this->send_local_time_(); });
|
||||
} else {
|
||||
ESP_LOGW(TAG, "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 (0x%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;
|
||||
|
||||
// Drop update if datapoint is in ignore_mcu_datapoint_update list
|
||||
for (uint8_t i : this->ignore_mcu_update_on_datapoints_) {
|
||||
if (datapoint.id == i) {
|
||||
ESP_LOGV(TAG, "Datapoint %u found in ignore_mcu_update_on_datapoints list, dropping MCU update", datapoint.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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, "Datapoint %u is not expected size", datapoint.id);
|
||||
return;
|
||||
}
|
||||
datapoint.len = data_len;
|
||||
|
||||
switch (datapoint.type) {
|
||||
case TuyaDatapointType::RAW:
|
||||
datapoint.value_raw = std::vector<uint8_t>(data, data + data_len);
|
||||
ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, hexencode(datapoint.value_raw).c_str());
|
||||
break;
|
||||
case TuyaDatapointType::BOOLEAN:
|
||||
if (data_len != 1) {
|
||||
ESP_LOGW(TAG, "Datapoint %u has bad boolean len %zu", datapoint.id, data_len);
|
||||
return;
|
||||
}
|
||||
datapoint.value_bool = data[0];
|
||||
ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, ONOFF(datapoint.value_bool));
|
||||
break;
|
||||
case TuyaDatapointType::INTEGER:
|
||||
if (data_len != 4) {
|
||||
ESP_LOGW(TAG, "Datapoint %u has bad integer len %zu", datapoint.id, data_len);
|
||||
return;
|
||||
}
|
||||
datapoint.value_uint = encode_uint32(data[0], data[1], data[2], data[3]);
|
||||
ESP_LOGD(TAG, "Datapoint %u update to %d", datapoint.id, datapoint.value_int);
|
||||
break;
|
||||
case TuyaDatapointType::STRING:
|
||||
datapoint.value_string = std::string(reinterpret_cast<const char *>(data), data_len);
|
||||
ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, datapoint.value_string.c_str());
|
||||
break;
|
||||
case TuyaDatapointType::ENUM:
|
||||
if (data_len != 1) {
|
||||
ESP_LOGW(TAG, "Datapoint %u has bad enum len %zu", datapoint.id, data_len);
|
||||
return;
|
||||
}
|
||||
datapoint.value_enum = data[0];
|
||||
ESP_LOGD(TAG, "Datapoint %u update to %d", datapoint.id, datapoint.value_enum);
|
||||
break;
|
||||
case TuyaDatapointType::BITMASK:
|
||||
switch (data_len) {
|
||||
case 1:
|
||||
datapoint.value_bitmask = encode_uint32(0, 0, 0, data[0]);
|
||||
break;
|
||||
case 2:
|
||||
datapoint.value_bitmask = encode_uint32(0, 0, data[0], data[1]);
|
||||
break;
|
||||
case 4:
|
||||
datapoint.value_bitmask = encode_uint32(data[0], data[1], data[2], data[3]);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Datapoint %u has bad bitmask len %zu", datapoint.id, data_len);
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Datapoint %u update to %#08X", datapoint.id, datapoint.value_bitmask);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Datapoint %u has unknown type %#02hhX", datapoint.id, datapoint.type);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Run through listeners
|
||||
for (auto &listener : this->listeners_)
|
||||
if (listener.datapoint_id == datapoint.id)
|
||||
listener.on_datapoint(datapoint);
|
||||
}
|
||||
|
||||
void Tuya::send_raw_command_(TuyaCommand command) {
|
||||
uint8_t len_hi = (uint8_t)(command.payload.size() >> 8);
|
||||
uint8_t len_lo = (uint8_t)(command.payload.size() & 0xFF);
|
||||
uint8_t version = 0;
|
||||
|
||||
this->last_command_timestamp_ = millis();
|
||||
switch (command.cmd)
|
||||
{
|
||||
case TuyaCommandType::HEARTBEAT:
|
||||
this->expected_response_ = TuyaCommandType::HEARTBEAT;
|
||||
break;
|
||||
case TuyaCommandType::PRODUCT_QUERY:
|
||||
this->expected_response_ = TuyaCommandType::PRODUCT_QUERY;
|
||||
break;
|
||||
case TuyaCommandType::CONF_QUERY:
|
||||
this->expected_response_ = TuyaCommandType::CONF_QUERY;
|
||||
break;
|
||||
case TuyaCommandType::DATAPOINT_DELIVER:
|
||||
this->expected_response_ = TuyaCommandType::DATAPOINT_REPORT;
|
||||
break;
|
||||
case TuyaCommandType::DATAPOINT_QUERY:
|
||||
this->expected_response_ = TuyaCommandType::DATAPOINT_REPORT;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", static_cast<uint8_t>(command.cmd),
|
||||
version, hexencode(command.payload).c_str(), static_cast<uint8_t>(this->init_state_));
|
||||
|
||||
this->write_array({0x55, 0xAA, version, (uint8_t) command.cmd, len_hi, len_lo});
|
||||
if (!command.payload.empty())
|
||||
this->write_array(command.payload.data(), command.payload.size());
|
||||
|
||||
uint8_t checksum = 0x55 + 0xAA + (uint8_t) command.cmd + len_hi + len_lo;
|
||||
for (auto &data : command.payload)
|
||||
checksum += data;
|
||||
this->write_byte(checksum);
|
||||
}
|
||||
|
||||
void Tuya::process_command_queue_() {
|
||||
uint32_t delay = millis() - this->last_command_timestamp_;
|
||||
|
||||
if(this->expected_response_.has_value() && delay > RECEIVE_TIMEOUT)
|
||||
{
|
||||
this->expected_response_.reset();
|
||||
}
|
||||
|
||||
// Left check of delay since last command in case theres ever a command sent by calling send_raw_command_ directly
|
||||
if (delay > COMMAND_DELAY && !this->command_queue_.empty() && this->rx_message_.empty() && !this->expected_response_.has_value()) {
|
||||
this->send_raw_command_(command_queue_.front());
|
||||
this->command_queue_.erase(command_queue_.begin());
|
||||
}
|
||||
}
|
||||
|
||||
void Tuya::send_command_(TuyaCommand command) {
|
||||
command_queue_.push_back(command);
|
||||
process_command_queue_();
|
||||
}
|
||||
|
||||
void Tuya::send_empty_command_(TuyaCommandType command) {
|
||||
send_command_(TuyaCommand{.cmd = command, .payload = std::vector<uint8_t>{0x04}});
|
||||
}
|
||||
|
||||
void Tuya::send_wifi_status_() {
|
||||
uint8_t status = 0x02;
|
||||
if (network_is_connected()) {
|
||||
status = 0x03;
|
||||
|
||||
// Protocol version 3 also supports specifying when connected to "the cloud"
|
||||
if (this->protocol_version_ >= 0x03) {
|
||||
if (remote_is_connected()) {
|
||||
status = 0x04;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (status == this->wifi_status_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Sending WiFi Status");
|
||||
this->wifi_status_ = status;
|
||||
this->send_command_(TuyaCommand{.cmd = TuyaCommandType::WIFI_STATE, .payload = std::vector<uint8_t>{status}});
|
||||
}
|
||||
|
||||
#ifdef USE_TIME
|
||||
void Tuya::send_local_time_() {
|
||||
std::vector<uint8_t> payload;
|
||||
auto time_id = *this->time_id_;
|
||||
time::ESPTime now = time_id->now();
|
||||
if (now.is_valid()) {
|
||||
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;
|
||||
}
|
||||
ESP_LOGD(TAG, "Sending local time");
|
||||
payload = std::vector<uint8_t>{0x01, year, month, day_of_month, hour, minute, second, day_of_week};
|
||||
} else {
|
||||
// By spec we need to notify MCU that the time was not obtained if this is a response to a query
|
||||
ESP_LOGW(TAG, "Sending missing local time");
|
||||
payload = std::vector<uint8_t>{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
}
|
||||
this->send_command_(TuyaCommand{.cmd = TuyaCommandType::LOCAL_TIME_QUERY, .payload = payload});
|
||||
}
|
||||
#endif
|
||||
|
||||
void Tuya::set_datapoint_value(uint8_t datapoint_id, uint32_t value) {
|
||||
ESP_LOGD(TAG, "Setting datapoint %u to %u", datapoint_id, value);
|
||||
optional<TuyaDatapoint> datapoint = this->get_datapoint_(datapoint_id);
|
||||
if (!datapoint.has_value()) {
|
||||
ESP_LOGE(TAG, "Attempt to set unknown datapoint %u", datapoint_id);
|
||||
return;
|
||||
}
|
||||
if (datapoint->value_uint == value) {
|
||||
ESP_LOGV(TAG, "Not sending unchanged value");
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> data;
|
||||
switch (datapoint->len) {
|
||||
case 4:
|
||||
data.push_back(value >> 24);
|
||||
data.push_back(value >> 16);
|
||||
case 2:
|
||||
data.push_back(value >> 8);
|
||||
case 1:
|
||||
data.push_back(value >> 0);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Unexpected datapoint length %zu", datapoint->len);
|
||||
return;
|
||||
}
|
||||
this->send_datapoint_command_(datapoint->id, datapoint->type, data);
|
||||
}
|
||||
|
||||
void Tuya::set_datapoint_value(uint8_t datapoint_id, std::string value) {
|
||||
ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, value.c_str());
|
||||
optional<TuyaDatapoint> datapoint = this->get_datapoint_(datapoint_id);
|
||||
if (!datapoint.has_value()) {
|
||||
ESP_LOGE(TAG, "Attempt to set unknown datapoint %u", datapoint_id);
|
||||
}
|
||||
if (datapoint->value_string == value) {
|
||||
ESP_LOGV(TAG, "Not sending unchanged value");
|
||||
return;
|
||||
}
|
||||
std::vector<uint8_t> data;
|
||||
for (char const &c : value) {
|
||||
data.push_back(c);
|
||||
}
|
||||
this->send_datapoint_command_(datapoint->id, datapoint->type, data);
|
||||
}
|
||||
|
||||
optional<TuyaDatapoint> Tuya::get_datapoint_(uint8_t datapoint_id) {
|
||||
for (auto &datapoint : this->datapoints_)
|
||||
if (datapoint.id == datapoint_id)
|
||||
return datapoint;
|
||||
return {};
|
||||
}
|
||||
|
||||
void Tuya::send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector<uint8_t> data) {
|
||||
std::vector<uint8_t> buffer;
|
||||
buffer.push_back(datapoint_id);
|
||||
buffer.push_back(static_cast<uint8_t>(datapoint_type));
|
||||
buffer.push_back(data.size() >> 8);
|
||||
buffer.push_back(data.size() >> 0);
|
||||
buffer.insert(buffer.end(), data.begin(), data.end());
|
||||
|
||||
this->send_command_(TuyaCommand{.cmd = TuyaCommandType::DATAPOINT_DELIVER, .payload = buffer});
|
||||
}
|
||||
|
||||
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
|
121
components/tuya/tuya.h
Normal file
121
components/tuya/tuya.h
Normal file
@ -0,0 +1,121 @@
|
||||
#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 {
|
||||
|
||||
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;
|
||||
size_t len;
|
||||
union {
|
||||
bool value_bool;
|
||||
int value_int;
|
||||
uint32_t value_uint;
|
||||
uint8_t value_enum;
|
||||
uint32_t value_bitmask;
|
||||
};
|
||||
std::string value_string;
|
||||
std::vector<uint8_t> value_raw;
|
||||
};
|
||||
|
||||
struct TuyaDatapointListener {
|
||||
uint8_t datapoint_id;
|
||||
std::function<void(TuyaDatapoint)> on_datapoint;
|
||||
};
|
||||
|
||||
enum class TuyaCommandType : uint8_t {
|
||||
HEARTBEAT = 0x00,
|
||||
PRODUCT_QUERY = 0x01,
|
||||
CONF_QUERY = 0x02,
|
||||
WIFI_STATE = 0x03,
|
||||
WIFI_RESET = 0x04,
|
||||
WIFI_SELECT = 0x05,
|
||||
DATAPOINT_DELIVER = 0x06,
|
||||
DATAPOINT_REPORT = 0x07,
|
||||
DATAPOINT_QUERY = 0x08,
|
||||
WIFI_TEST = 0x0E,
|
||||
LOCAL_TIME_QUERY = 0x1C,
|
||||
};
|
||||
|
||||
enum class TuyaInitState : uint8_t {
|
||||
INIT_HEARTBEAT = 0x00,
|
||||
INIT_PRODUCT,
|
||||
INIT_CONF,
|
||||
INIT_WIFI,
|
||||
INIT_DATAPOINT,
|
||||
INIT_DONE,
|
||||
};
|
||||
|
||||
struct TuyaCommand {
|
||||
TuyaCommandType cmd;
|
||||
std::vector<uint8_t> payload;
|
||||
};
|
||||
|
||||
class Tuya : public Component, public uart::UARTDevice {
|
||||
public:
|
||||
float get_setup_priority() const override { return setup_priority::LATE; }
|
||||
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(uint8_t datapoint_id, uint32_t value);
|
||||
void set_datapoint_value(uint8_t datapoint_id, std::string value);
|
||||
#ifdef USE_TIME
|
||||
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
|
||||
#endif
|
||||
void add_ignore_mcu_update_on_datapoints(uint8_t ignore_mcu_update_on_datapoints) {
|
||||
this->ignore_mcu_update_on_datapoints_.push_back(ignore_mcu_update_on_datapoints);
|
||||
}
|
||||
|
||||
protected:
|
||||
void handle_char_(uint8_t c);
|
||||
void handle_datapoint_(const uint8_t *buffer, size_t len);
|
||||
optional<TuyaDatapoint> get_datapoint_(uint8_t datapoint_id);
|
||||
bool validate_message_();
|
||||
|
||||
void handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len);
|
||||
void send_raw_command_(TuyaCommand command);
|
||||
void process_command_queue_();
|
||||
void send_command_(TuyaCommand command);
|
||||
void send_empty_command_(TuyaCommandType command);
|
||||
void send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector<uint8_t> data);
|
||||
void send_wifi_status_();
|
||||
|
||||
#ifdef USE_TIME
|
||||
void send_local_time_();
|
||||
optional<time::RealTimeClock *> time_id_{};
|
||||
#endif
|
||||
TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT;
|
||||
uint8_t protocol_version_ = -1;
|
||||
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_;
|
||||
std::vector<uint8_t> rx_message_;
|
||||
std::vector<uint8_t> ignore_mcu_update_on_datapoints_{};
|
||||
std::vector<TuyaCommand> command_queue_;
|
||||
optional<TuyaCommandType> expected_response_{};
|
||||
uint8_t wifi_status_ = -1;
|
||||
};
|
||||
|
||||
} // namespace tuya
|
||||
} // namespace esphome
|
0
components/tuya_light_plus/__init__.py
Normal file
0
components/tuya_light_plus/__init__.py
Normal file
88
components/tuya_light_plus/light.py
Normal file
88
components/tuya_light_plus/light.py
Normal file
@ -0,0 +1,88 @@
|
||||
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,
|
||||
CONF_GAMMA_CORRECT,
|
||||
CONF_DEFAULT_TRANSITION_LENGTH,
|
||||
CONF_SWITCH_DATAPOINT,
|
||||
CONF_COLD_WHITE_COLOR_TEMPERATURE,
|
||||
CONF_WARM_WHITE_COLOR_TEMPERATURE,
|
||||
)
|
||||
from esphome.components.tuya import CONF_TUYA_ID, Tuya
|
||||
|
||||
DEPENDENCIES = ["tuya"]
|
||||
|
||||
CONF_DIMMER_DATAPOINT = "dimmer_datapoint"
|
||||
CONF_MIN_VALUE_DATAPOINT = "min_value_datapoint"
|
||||
CONF_COLOR_TEMPERATURE_DATAPOINT = "color_temperature_datapoint"
|
||||
CONF_COLOR_TEMPERATURE_MAX_VALUE = "color_temperature_max_value"
|
||||
|
||||
tuya_ns = cg.esphome_ns.namespace("tuya")
|
||||
TuyaLight = tuya_ns.class_("TuyaLightPlus", light.LightOutput, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
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.Optional(CONF_DIMMER_DATAPOINT): cv.uint8_t,
|
||||
cv.Optional(CONF_MIN_VALUE_DATAPOINT): cv.uint8_t,
|
||||
cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
|
||||
cv.Inclusive(
|
||||
CONF_COLOR_TEMPERATURE_DATAPOINT, "color_temperature"
|
||||
): cv.uint8_t,
|
||||
cv.Optional(CONF_MIN_VALUE): cv.int_,
|
||||
cv.Optional(CONF_MAX_VALUE): cv.int_,
|
||||
cv.Optional(CONF_COLOR_TEMPERATURE_MAX_VALUE): cv.int_,
|
||||
cv.Inclusive(
|
||||
CONF_COLD_WHITE_COLOR_TEMPERATURE, "color_temperature"
|
||||
): cv.color_temperature,
|
||||
cv.Inclusive(
|
||||
CONF_WARM_WHITE_COLOR_TEMPERATURE, "color_temperature"
|
||||
): cv.color_temperature,
|
||||
# Change the default gamma_correct and default transition length settings.
|
||||
# The Tuya MCU handles transitions and gamma correction on its own.
|
||||
cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float,
|
||||
cv.Optional(
|
||||
CONF_DEFAULT_TRANSITION_LENGTH, default="0s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.has_at_least_one_key(CONF_DIMMER_DATAPOINT, CONF_SWITCH_DATAPOINT),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||
await cg.register_component(var, config)
|
||||
await light.register_light(var, config)
|
||||
|
||||
if CONF_DIMMER_DATAPOINT in config:
|
||||
cg.add(var.set_dimmer_id(config[CONF_DIMMER_DATAPOINT]))
|
||||
if CONF_MIN_VALUE_DATAPOINT in config:
|
||||
cg.add(var.set_min_value_datapoint_id(config[CONF_MIN_VALUE_DATAPOINT]))
|
||||
if CONF_SWITCH_DATAPOINT in config:
|
||||
cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT]))
|
||||
if CONF_COLOR_TEMPERATURE_DATAPOINT in config:
|
||||
cg.add(var.set_color_temperature_id(config[CONF_COLOR_TEMPERATURE_DATAPOINT]))
|
||||
cg.add(
|
||||
var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])
|
||||
)
|
||||
cg.add(
|
||||
var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])
|
||||
)
|
||||
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]))
|
||||
if CONF_COLOR_TEMPERATURE_MAX_VALUE in config:
|
||||
cg.add(
|
||||
var.set_color_temperature_max_value(
|
||||
config[CONF_COLOR_TEMPERATURE_MAX_VALUE]
|
||||
)
|
||||
)
|
||||
paren = await cg.get_variable(config[CONF_TUYA_ID])
|
||||
cg.add(var.set_tuya_parent(paren))
|
93
components/tuya_light_plus/tuya_light_plus.cpp
Normal file
93
components/tuya_light_plus/tuya_light_plus.cpp
Normal file
@ -0,0 +1,93 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "tuya_light_plus.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace tuya {
|
||||
|
||||
static const char *TAG = "tuya.light_plus";
|
||||
|
||||
void TuyaLightPlus::setup() {
|
||||
if (this->color_temperature_id_.has_value()) {
|
||||
this->parent_->register_listener(*this->color_temperature_id_, [this](TuyaDatapoint datapoint) {
|
||||
auto call = this->state_->make_call();
|
||||
call.set_color_temperature(this->cold_white_temperature_ +
|
||||
(this->warm_white_temperature_ - this->cold_white_temperature_) *
|
||||
(float(datapoint.value_uint) / float(this->color_temperature_max_value_)));
|
||||
call.perform();
|
||||
});
|
||||
}
|
||||
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();
|
||||
});
|
||||
}
|
||||
if (min_value_datapoint_id_.has_value()) {
|
||||
parent_->set_datapoint_value(*this->min_value_datapoint_id_, this->min_value_);
|
||||
}
|
||||
}
|
||||
|
||||
void TuyaLightPlus::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 TuyaLightPlus::get_traits() {
|
||||
auto traits = light::LightTraits();
|
||||
traits.set_supports_brightness(this->dimmer_id_.has_value());
|
||||
traits.set_supports_color_temperature(this->color_temperature_id_.has_value());
|
||||
if (this->color_temperature_id_.has_value()) {
|
||||
traits.set_min_mireds(this->cold_white_temperature_);
|
||||
traits.set_max_mireds(this->warm_white_temperature_);
|
||||
}
|
||||
return traits;
|
||||
}
|
||||
|
||||
void TuyaLightPlus::setup_state(light::LightState *state) { state_ = state; }
|
||||
|
||||
void TuyaLightPlus::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()) {
|
||||
parent_->set_datapoint_value(*this->switch_id_, false);
|
||||
} else if (dimmer_id_.has_value()) {
|
||||
parent_->set_datapoint_value(*this->dimmer_id_, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->color_temperature_id_.has_value()) {
|
||||
uint32_t color_temp_int =
|
||||
static_cast<uint32_t>(this->color_temperature_max_value_ *
|
||||
(state->current_values.get_color_temperature() - this->cold_white_temperature_) /
|
||||
(this->warm_white_temperature_ - this->cold_white_temperature_));
|
||||
parent_->set_datapoint_value(*this->color_temperature_id_, color_temp_int);
|
||||
}
|
||||
|
||||
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()) {
|
||||
parent_->set_datapoint_value(*this->dimmer_id_, brightness_int);
|
||||
}
|
||||
if (this->switch_id_.has_value()) {
|
||||
parent_->set_datapoint_value(*this->switch_id_, true);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace tuya
|
||||
} // namespace esphome
|
54
components/tuya_light_plus/tuya_light_plus.h
Normal file
54
components/tuya_light_plus/tuya_light_plus.h
Normal file
@ -0,0 +1,54 @@
|
||||
#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 TuyaLightPlus : 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_min_value_datapoint_id(uint8_t min_value_datapoint_id) {
|
||||
this->min_value_datapoint_id_ = min_value_datapoint_id;
|
||||
}
|
||||
void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; }
|
||||
void set_color_temperature_id(uint8_t color_temperature_id) { this->color_temperature_id_ = color_temperature_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; }
|
||||
void set_color_temperature_max_value(uint32_t color_temperature_max_value) {
|
||||
this->color_temperature_max_value_ = color_temperature_max_value;
|
||||
}
|
||||
void set_cold_white_temperature(float cold_white_temperature) {
|
||||
this->cold_white_temperature_ = cold_white_temperature;
|
||||
}
|
||||
void set_warm_white_temperature(float warm_white_temperature) {
|
||||
this->warm_white_temperature_ = warm_white_temperature;
|
||||
}
|
||||
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> min_value_datapoint_id_{};
|
||||
optional<uint8_t> switch_id_{};
|
||||
optional<uint8_t> color_temperature_id_{};
|
||||
uint32_t min_value_ = 0;
|
||||
uint32_t max_value_ = 255;
|
||||
uint32_t color_temperature_max_value_ = 255;
|
||||
float cold_white_temperature_;
|
||||
float warm_white_temperature_;
|
||||
light::LightState *state_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace tuya
|
||||
} // namespace esphome
|
@ -4,6 +4,8 @@ using namespace esphome;
|
||||
|
||||
#define DOUBLE_TAP_TIMEOUT 300
|
||||
|
||||
static const char* TAG = "NuttyTuyaLight";
|
||||
|
||||
class TuyaLightPlus : public Component, public light::LightOutput, public api::CustomAPIDevice
|
||||
{
|
||||
public:
|
||||
@ -235,14 +237,21 @@ void TuyaLightPlus::on_day_night_changed(std::string state)
|
||||
|
||||
void TuyaLightPlus::handle_tuya_datapoint(tuya::TuyaDatapoint datapoint)
|
||||
{
|
||||
ESP_LOGD(TAG, "Received Datapoint:");
|
||||
if (datapoint.id == *this->switch_id_)
|
||||
{
|
||||
ESP_LOGD(TAG, " Type: Switch");
|
||||
|
||||
// Light turned on
|
||||
if (datapoint.value_bool)
|
||||
{
|
||||
ESP_LOGD(TAG, " State: On");
|
||||
|
||||
// Turned on with the physical button
|
||||
if (!this->tuya_state_)
|
||||
{
|
||||
ESP_LOGD(TAG, "Turned on at device");
|
||||
|
||||
if (this->has_double_tap_while_on_)
|
||||
{
|
||||
// We are in a double tap while on timeout period so this is a double tap
|
||||
@ -281,18 +290,25 @@ void TuyaLightPlus::handle_tuya_datapoint(tuya::TuyaDatapoint datapoint)
|
||||
}
|
||||
|
||||
// We got through all the double tap logic and the light is still on so update the Tuya state
|
||||
ESP_LOGD(TAG, "Updating Tuya state to on");
|
||||
this->tuya_state_ = true;
|
||||
}
|
||||
|
||||
// Set the brightness to the correct level (it currently is at 0)
|
||||
this->set_tuya_level(this->brightness_to_tuya_level(this->state_->current_values.get_brightness()));
|
||||
else
|
||||
{
|
||||
// Set the brightness to the correct level (it currently is at 0)
|
||||
this->set_tuya_level(this->brightness_to_tuya_level(this->state_->current_values.get_brightness()));
|
||||
}
|
||||
}
|
||||
// Light turned off
|
||||
else
|
||||
{
|
||||
ESP_LOGD(TAG, " State: Off");
|
||||
|
||||
// Turned off with physical button
|
||||
if (this->tuya_state_)
|
||||
{
|
||||
ESP_LOGD(TAG, "Turned off at device");
|
||||
|
||||
if (has_double_tap_while_on_)
|
||||
{
|
||||
// Start the double tap while on timeout
|
||||
@ -300,21 +316,28 @@ void TuyaLightPlus::handle_tuya_datapoint(tuya::TuyaDatapoint datapoint)
|
||||
}
|
||||
|
||||
// Update the Tuya state
|
||||
ESP_LOGD(TAG, "Updating Tuya state to off");
|
||||
this->tuya_state_ = false;
|
||||
}
|
||||
|
||||
// Set the Tuya level to 0 to prevent flashes during double taps
|
||||
ESP_LOGD(TAG, "Updating Tuya level to 0");
|
||||
this->set_tuya_level(0);
|
||||
|
||||
// Set the current brightness to the default so that it will turn on at the default brightness
|
||||
ESP_LOGD(TAG, "Updating brightness state to default");
|
||||
this->state_->current_values.set_brightness(this->default_brightness_);
|
||||
}
|
||||
|
||||
// Update the current values state
|
||||
ESP_LOGD(TAG, "Updating state to new value");
|
||||
this->state_->current_values.set_state(this->tuya_state_);
|
||||
}
|
||||
else if (datapoint.id == *this->dimmer_id_)
|
||||
{
|
||||
ESP_LOGD(TAG, " Type: Brightness");
|
||||
ESP_LOGD(TAG, " Value: %u", datapoint.value_uint);
|
||||
|
||||
// Only react to dimmer level changes if the light is on
|
||||
if(this->tuya_state_)
|
||||
{
|
||||
@ -326,13 +349,16 @@ void TuyaLightPlus::handle_tuya_datapoint(tuya::TuyaDatapoint datapoint)
|
||||
this->state_changed_at_ = millis();
|
||||
|
||||
// If the remote values do not reflect the current values update and publish the values
|
||||
if (this->state_->current_values.get_state() != this->state_->remote_values.get_state())
|
||||
if (this->state_->current_values.get_state() != this->state_->remote_values.get_state()
|
||||
|| this->state_->current_values.get_brightness() != this->state_->remote_values.get_brightness())
|
||||
{
|
||||
ESP_LOGD(TAG, "Publishing new state");
|
||||
this->state_->remote_values = this->state_->current_values;
|
||||
this->state_->publish_state();
|
||||
}
|
||||
|
||||
// Update any linked lights
|
||||
ESP_LOGD(TAG, "Updating linked lights");
|
||||
this->update_linked_lights();
|
||||
}
|
||||
|
||||
@ -340,24 +366,12 @@ void TuyaLightPlus::set_tuya_state(bool state)
|
||||
{
|
||||
this->tuya_state_ = state;
|
||||
|
||||
// In version 1.19.0 the code below needs to change to:
|
||||
// this->parent_->set_datapoint_value(*this->switch_id_, state);
|
||||
tuya::TuyaDatapoint datapoint{};
|
||||
datapoint.id = *this->switch_id_;
|
||||
datapoint.type = tuya::TuyaDatapointType::BOOLEAN;
|
||||
datapoint.value_bool = state;
|
||||
this->parent_->set_datapoint_value(datapoint);
|
||||
this->parent_->set_datapoint_value(*this->switch_id_, state);
|
||||
}
|
||||
|
||||
void TuyaLightPlus::set_tuya_level(uint32_t level)
|
||||
{
|
||||
// In version 1.19.0 the code below needs to change to:
|
||||
// this->parent_->set_datapoint_value(*this->dimmer_id_, std::max(level, this->min_value_));
|
||||
tuya::TuyaDatapoint datapoint{};
|
||||
datapoint.id = *this->dimmer_id_;
|
||||
datapoint.type = tuya::TuyaDatapointType::INTEGER;
|
||||
datapoint.value_uint = std::max(level, this->min_value_);
|
||||
this->parent_->set_datapoint_value(datapoint);
|
||||
this->parent_->set_datapoint_value(*this->dimmer_id_, std::max(level, this->min_value_));
|
||||
}
|
||||
|
||||
void TuyaLightPlus::update_linked_lights()
|
||||
|
16
devices/test_light.yaml
Normal file
16
devices/test_light.yaml
Normal file
@ -0,0 +1,16 @@
|
||||
substitutions:
|
||||
device_id: computer_light
|
||||
device_name: Computer Light
|
||||
ip_address: !secret computer_light_ip
|
||||
ota_pwd: !secret computer_light_ota_pwd
|
||||
api_pwd: !secret computer_light_api_pwd
|
||||
ap_wifi_pwd: !secret computer_light_ap_wifi_pwd
|
||||
day_brightness: "1"
|
||||
night_brightness: ".03"
|
||||
day_auto_off_minutes: "0"
|
||||
night_auto_off_minutes: "15"
|
||||
linked_lights: ""
|
||||
double_tap_while_off_stays_on: "true"
|
||||
|
||||
packages:
|
||||
feit_dimmer: !include ../packages/feit_dimmer_test.yaml
|
@ -1,6 +1,7 @@
|
||||
substitutions:
|
||||
platform: ESP8266
|
||||
board: esp01_1m
|
||||
log_level: verbose
|
||||
|
||||
esphome:
|
||||
includes:
|
||||
@ -10,11 +11,20 @@ esphome:
|
||||
then:
|
||||
- script.execute: startup
|
||||
|
||||
external_components:
|
||||
# - source: github://nuttytree/esphome@more-tuya-reliability-improvements
|
||||
- source:
|
||||
type: local
|
||||
path: ../components
|
||||
components: [ tuya ]
|
||||
|
||||
packages:
|
||||
base: !include device_base.yaml
|
||||
logger: !include logger/logger_no_serial.yaml
|
||||
uart: !include uart/tuya.yaml
|
||||
|
||||
tuya:
|
||||
|
||||
light:
|
||||
- platform: custom
|
||||
lambda: |-
|
||||
@ -39,5 +49,3 @@ light:
|
||||
name: ${device_name}
|
||||
gamma_correct: 1.0
|
||||
default_transition_length: 0s
|
||||
|
||||
tuya:
|
||||
|
@ -17,6 +17,8 @@ fan:
|
||||
output: tuya_fan_output
|
||||
name: ${device_name}
|
||||
|
||||
tuya:
|
||||
|
||||
output:
|
||||
- platform: custom
|
||||
type: binary
|
||||
@ -31,5 +33,3 @@ output:
|
||||
return {TuyaFanOutput};
|
||||
outputs:
|
||||
id: tuya_fan_output
|
||||
|
||||
tuya:
|
||||
|
26
packages/feit_dimmer_test.yaml
Normal file
26
packages/feit_dimmer_test.yaml
Normal file
@ -0,0 +1,26 @@
|
||||
substitutions:
|
||||
platform: ESP8266
|
||||
board: esp01_1m
|
||||
log_level: verbose
|
||||
|
||||
external_components:
|
||||
# - source: github://nuttytree/esphome@more-tuya-reliability-improvements
|
||||
- source:
|
||||
type: local
|
||||
path: ../components
|
||||
components: [ tuya, tuya_light_plus ]
|
||||
|
||||
packages:
|
||||
base: !include device_base.yaml
|
||||
logger: !include logger/logger_no_serial.yaml
|
||||
uart: !include uart/tuya.yaml
|
||||
|
||||
tuya:
|
||||
|
||||
light:
|
||||
- platform: tuya_light_plus
|
||||
id: tuya_light
|
||||
name: ${device_name}
|
||||
switch_datapoint: 1
|
||||
dimmer_datapoint: 2
|
||||
max_value: 1000
|
Loading…
Reference in New Issue
Block a user