Forked from placidorevilla/tuyadimmer_lightoutput.h
Created
September 18, 2019 17:47
-
-
Save kvvoff/4ca9ded2210506a762c3abc00ae06dea to your computer and use it in GitHub Desktop.
Prototype esphome component for Tuya Dimmer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
#include "esphome.h" | |
using namespace esphome; | |
enum TuyaCmd : uint8_t { | |
HEARTBEAT = 0x00, | |
QUERY_PRODUCT = 0x01, | |
MCU_CONF = 0x02, | |
WIFI_STATE = 0x03, | |
WIFI_RESET = 0x04, | |
WIFI_SELECT = 0x05, | |
SET_DP = 0x06, | |
STATE = 0x07, | |
QUERY_STATE = 0x08, | |
}; | |
enum TuyaType : uint8_t { | |
BOOL = 0x01, | |
VALUE = 0x02, | |
}; | |
static const uint8_t DEFAULT_POWER_ID = 1; | |
static const uint8_t DEFAULT_DIMMER_ID = 2; | |
static const float DEFAULT_MAX_POWER = 0.64; | |
static const float DEFAULT_MIN_POWER = 0.21; | |
static const char *TAG = "tuyadimmerlightoutput"; | |
class TuyaDimmerLightOutput : public Component, public light::LightOutput, public uart::UARTDevice { | |
public: | |
TuyaDimmerLightOutput(uart::UARTComponent *parent, uint8_t power_id = DEFAULT_POWER_ID, | |
uint8_t dimmer_id = DEFAULT_DIMMER_ID, float max_power = DEFAULT_MAX_POWER, float min_power = DEFAULT_MIN_POWER) | |
: uart::UARTDevice(parent), | |
power_id_(power_id), | |
dimmer_id_(dimmer_id), | |
max_power_(max_power), | |
min_power_(min_power) {} | |
LightTraits get_traits() { | |
auto traits = LightTraits(); | |
traits.set_supports_brightness(true); | |
return traits; | |
} | |
void setup_state(LightState *state) override { | |
this->state_ = state; | |
this->send_cmd_(TuyaCmd::MCU_CONF); | |
} | |
void write_state(LightState *state) override { | |
if (state->current_values.is_on()) { | |
float brightness; | |
state->current_values_as_brightness(&brightness); | |
uint8_t value = ((brightness * (this->max_power_ - this->min_power_)) + this->min_power_) * 255; | |
ESP_LOGD(TAG, "Set output to %f (%d)", brightness, value); | |
this->set_dp_(this->dimmer_id_, TuyaType::VALUE, value); | |
} else { | |
this->set_dp_(this->power_id_, TuyaType::BOOL, 0); | |
} | |
} | |
void loop() { | |
const uint32_t now = millis(); | |
if (this->data_index_ != 0 && (now - this->last_transmission_ >= 500)) { | |
// last transmission too long ago. Reset RX index. | |
ESP_LOGW(TAG, "Partial packet"); | |
this->data_index_ = 0; | |
} | |
if (now - this->last_heartbeat_ >= 10000) { | |
this->last_heartbeat_ = now; | |
this->send_cmd_(TuyaCmd::HEARTBEAT); | |
} | |
if (this->available() == 0) | |
return; | |
this->last_transmission_ = now; | |
while (this->available() != 0) { | |
this->read_byte(&this->data_[this->data_index_]); | |
auto check = this->check_byte_(); | |
if (!check.has_value()) { | |
// finished | |
this->parse_data_(); | |
this->data_index_ = 0; | |
} else if (!*check) { | |
// wrong data | |
this->data_index_ = 0; | |
} else { | |
// next byte | |
this->data_index_++; | |
if (this->data_index_ == sizeof(this->data_)) { | |
ESP_LOGW(TAG, "UART buffer overflow"); | |
this->data_index_ = 0; | |
} | |
} | |
} | |
} | |
protected: | |
void send_cmd_(TuyaCmd cmd, uint8_t payload[] = nullptr, uint16_t payload_len = 0) { | |
uint8_t checksum = (0xFF + cmd + (payload_len >> 8) + (payload_len & 0xFF)); | |
this->flush(); | |
this->write_byte(0x55); // Tuya header 55AA | |
this->write_byte(0xAA); | |
this->write_byte((uint8_t) 0x00); // version 00 | |
this->write_byte(cmd); // Tuya command | |
this->write_byte(payload_len >> 8); // following data length (Hi) | |
this->write_byte(payload_len & 0xFF); // following data length (Lo) | |
for (int i = 0; i < payload_len; ++i) { | |
this->write_byte(payload[i]); | |
checksum += payload[i]; | |
} | |
this->write_byte(checksum); | |
} | |
void set_dp_(uint8_t dp_id, TuyaType type, uint32_t value_) { | |
uint16_t payload_len = 4; | |
uint8_t payload_buffer[8]; | |
uint8_t *value = (uint8_t *) &value_; | |
payload_buffer[0] = dp_id; | |
payload_buffer[1] = type; | |
switch (type) { | |
case TuyaType::BOOL: | |
payload_len += 1; | |
payload_buffer[2] = 0x00; | |
payload_buffer[3] = 0x01; | |
payload_buffer[4] = value[0]; | |
break; | |
case TuyaType::VALUE: | |
payload_len += 4; | |
payload_buffer[2] = 0x00; | |
payload_buffer[3] = 0x04; | |
payload_buffer[4] = value[3]; | |
payload_buffer[5] = value[2]; | |
payload_buffer[6] = value[1]; | |
payload_buffer[7] = value[0]; | |
break; | |
} | |
this->send_cmd_(TuyaCmd::SET_DP, payload_buffer, payload_len); | |
} | |
optional<bool> check_byte_() const { | |
uint8_t index = this->data_index_; | |
uint8_t byte = this->data_[index]; | |
uint8_t len = 0; | |
if (index == 0) { | |
return byte == 0x55; | |
} | |
if (index == 1) { | |
return byte == 0xAA; | |
} | |
if ((index >= 2) && (index <= 4)) { | |
return true; | |
} | |
if (index >= 5) { | |
if (this->data_[4] != 0) { | |
return false; | |
} | |
len = this->data_[5]; | |
} | |
if (index < (len + 6)) { | |
return true; | |
} | |
if (index == (len + 6)) { | |
uint8_t checksum = 0xFF; | |
for (int i = 3; i < index; ++i) { | |
checksum += this->data_[i]; | |
} | |
if (checksum != byte) { | |
ESP_LOGW(TAG, "Checksum doesn't match: 0x%02X!=0x%02X", byte, checksum); | |
return false; | |
} | |
} | |
return {}; | |
} | |
void dump_packet_() { | |
static char log[768]; | |
log[0] = 0; | |
for (int i = 0; i < this->data_index_; i++) | |
snprintf_P(log, sizeof(log), PSTR("%s %02X"), log, this->data_[i]); | |
ESP_LOGD(TAG, "Packet: %s", log); | |
} | |
void parse_data_() { | |
switch (this->data_[3]) { | |
case TuyaCmd::HEARTBEAT: | |
ESP_LOGD(TAG, "MCU Heartbeat (0x%02X)", this->data_[6]); | |
break; | |
case TuyaCmd::MCU_CONF: | |
ESP_LOGD(TAG, "MCU Configuration: %s", this->data_[6] == 2 ? "ESP Exclusive" : "Cooperative"); | |
// TODO: Support ESP Exclusive mode | |
this->send_cmd_(TuyaCmd::QUERY_STATE); | |
break; | |
case TuyaCmd::STATE: | |
if (this->data_[6] == this->power_id_ && this->data_[5] == 5) { | |
ESP_LOGD(TAG, "Power State: 0x%02X", this->data_[10]); | |
if (bool(this->data_[10]) != this->state_->remote_values.is_on()) { | |
this->state_->make_call().set_state(bool(this->data_[10])).perform(); | |
} | |
} else if (this->data_[6] == this->dimmer_id_ && this->data_[5] == 8) { | |
ESP_LOGD(TAG, "Dimmer Value: 0x%02X", this->data_[13]); | |
} else { | |
ESP_LOGW(TAG, "Unknown dpID: 0x%02X", this->data_[6]); | |
} | |
break; | |
case TuyaCmd::QUERY_PRODUCT: | |
case TuyaCmd::WIFI_STATE: | |
case TuyaCmd::WIFI_RESET: | |
case TuyaCmd::WIFI_SELECT: | |
default: | |
ESP_LOGW(TAG, "Received unknown command: 0x%02X", this->data_[3]); | |
this->dump_packet_(); | |
} | |
} | |
uint8_t data_[254]; | |
uint8_t data_index_{0}; | |
uint32_t last_transmission_{0}; | |
uint32_t last_heartbeat_{0}; | |
LightState *state_{nullptr}; | |
uint8_t power_id_; | |
uint8_t dimmer_id_; | |
float max_power_; | |
float min_power_; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment