332 lines
11 KiB
C++
Executable File
332 lines
11 KiB
C++
Executable File
#include "esphome.h"
|
|
|
|
static const char *TAG = "TUYACOVER";
|
|
static const uint16_t TUYA_COVER_HEADER = 0x55AA;
|
|
//static const uint16_t TUYA_COVER_VERSION = 0x03;
|
|
static const uint16_t TUYA_COVER_VERSION = 0x00;
|
|
|
|
const uint8_t tuya_cover_enable_reversing[] = TUYA_COVER_ENABLE_REVERSING;
|
|
const uint8_t tuya_cover_disable_reversing[] = TUYA_COVER_DISABLE_REVERSING;
|
|
const uint8_t tuya_cover_open[] = TUYA_COVER_OPEN;
|
|
const uint8_t tuya_cover_close[] = TUYA_COVER_CLOSE;
|
|
const uint8_t tuya_cover_stop[] = TUYA_COVER_STOP;
|
|
static uint8_t tuya_cover_pos[] = TUYA_COVER_SET_POSITION;
|
|
|
|
#define HEARTBEAT_INTERVAL_MS 10000
|
|
unsigned long previousHeartbeatMillis = 0;
|
|
|
|
struct TUYACOVERCommand
|
|
{
|
|
uint16_t header;
|
|
uint8_t version;
|
|
uint8_t command;
|
|
uint16_t length;
|
|
uint8_t value[TUYA_COVER_MAX_LEN];
|
|
uint8_t checksum;
|
|
};
|
|
|
|
struct TUYACOVERMessage
|
|
{
|
|
uint8_t dpid;
|
|
uint8_t type;
|
|
uint16_t len;
|
|
uint8_t value[TUYA_COVER_MAX_LEN - 4]; //Subtract dpid, type, len
|
|
};
|
|
|
|
enum TUYACOVERCommandType
|
|
{
|
|
TUYA_COVER_HEARTBEAT = 0x00,
|
|
TUYA_COVER_COMMAND = 0x06,
|
|
TUYA_COVER_RESPONSE = 0x07,
|
|
TUYA_COVER_QUERY_STATUS = 0x08
|
|
};
|
|
|
|
enum TUYACOVERdpidType
|
|
{
|
|
TUYA_COVER_DPID_POSITION= 0x65,
|
|
TUYA_COVER_DPID_DIRECTION = 0x64,
|
|
TUYA_COVER_DPID_UNKNOWN = 0x67,
|
|
TUYA_COVER_DPID_ERROR = 0x6E
|
|
};
|
|
|
|
// Variables
|
|
//TUYACOVERCommand command_{TUYA_COVER_HEADER, TUYA_COVER_VERSION, 0, 0, {}, 0};
|
|
uint8_t uart_buffer_[TUYA_COVER_BUFFER_LEN]{0};
|
|
|
|
// Forward declarations
|
|
bool read_command();
|
|
void write_command(TUYACOVERCommandType command, const uint8_t *value, uint16_t length);
|
|
uint8_t checksum();
|
|
|
|
/*
|
|
* Attempt to read an entire command from the serial UART into the command struct.
|
|
* Will fail early if unable to find the two-byte header in the current
|
|
* data stream. If the header is found, it will contine to read the complete
|
|
* TLV+checksum sequence off the port. If the entire sequence can be read
|
|
* and the checksum is valid, it will return true.
|
|
*/
|
|
bool read_command()
|
|
{
|
|
// Shift bytes through until we find a valid header
|
|
bool valid_header = false;
|
|
while (Serial.available() >= 1)
|
|
{
|
|
uart_buffer_[0] = uart_buffer_[1];
|
|
uart_buffer_[1] = Serial.read();
|
|
command_.header = (uart_buffer_[0] << 8) + uart_buffer_[1];
|
|
if (command_.header == TUYA_COVER_HEADER)
|
|
{
|
|
valid_header = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Read the next 4 bytes (Version, Command, Data length)
|
|
// Read n bytes (Data length)
|
|
// Read the checksum byte
|
|
if (valid_header)
|
|
{
|
|
Serial.readBytes(uart_buffer_ + TUYA_COVER_HEADER_LEN, TUYA_COVER_BUFFER_LEN - TUYA_COVER_HEADER_LEN);
|
|
command_.version = uart_buffer_[2];
|
|
command_.command = uart_buffer_[3];
|
|
command_.length = (uart_buffer_[4] << 8) + uart_buffer_[5];
|
|
ESP_LOGV(TAG, "RX: Header = 0x%04X, Version = 0x%02X, Command = 0x%02X, Data length = 0x%04X", command_.header, command_.version, command_.command, command_.length);
|
|
|
|
if (command_.length < TUYA_COVER_MAX_LEN)
|
|
{
|
|
Serial.readBytes(command_.value, command_.length);
|
|
ESP_LOGV(TAG, "RX_RAW:");
|
|
for (size_t i = 0; i < command_.length; i++)
|
|
{
|
|
ESP_LOGV(TAG, "%02d: 0x%02X", i, command_.value[i]);
|
|
}
|
|
while (Serial.available() == 0) // Dirty
|
|
{
|
|
//Wait
|
|
}
|
|
command_.checksum = Serial.read();
|
|
|
|
ESP_LOGV(TAG, "RX_CHK: 0x%02X", command_.checksum);
|
|
uint8_t calc_checksum = checksum();
|
|
if (calc_checksum == command_.checksum)
|
|
{
|
|
// Clear buffer contents to start with beginning of next command
|
|
memset(uart_buffer_, 0, TUYA_COVER_BUFFER_LEN);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
memset(uart_buffer_, 0, TUYA_COVER_BUFFER_LEN);
|
|
ESP_LOGE(TAG, "Checksum error: Read = 0x%02X != Calculated = 0x%02X", command_.checksum, calc_checksum);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
memset(uart_buffer_, 0, TUYA_COVER_BUFFER_LEN);
|
|
ESP_LOGE(TAG, "Command length exceeds limit: %d >= %d", command_.length, TUYA_COVER_MAX_LEN);
|
|
}
|
|
}
|
|
|
|
// Do not clear buffer to allow for resume in case of reading partway through header RX
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Store the given type, value, and length into the command struct and send
|
|
* it out the serial port. Automatically calculates the checksum as well.
|
|
*/
|
|
void write_command(TUYACOVERCommandType command, const uint8_t *value, uint16_t length)
|
|
{
|
|
// Copy params into command struct
|
|
command_.header = TUYA_COVER_HEADER;
|
|
command_.version = TUYA_COVER_VERSION;
|
|
command_.command = command;
|
|
command_.length = length;
|
|
ESP_LOGV(TAG, "TX: Header = 0x%04X, Version = 0x%02X, Command = 0x%02X, Data length = 0x%04X", command_.header, command_.version, command_.command, command_.length);
|
|
memcpy(&command_.value, value, length);
|
|
ESP_LOGV(TAG, "TX_RAW");
|
|
for (size_t i = 0; i < command_.length; i++)
|
|
{
|
|
ESP_LOGV(TAG, "%02d: 0x%02X", i, command_.value[i]);
|
|
}
|
|
// Copy struct values into buffer, converting longs to big-endian
|
|
uart_buffer_[0] = command_.header >> 8;
|
|
uart_buffer_[1] = command_.header & 0xFF;
|
|
uart_buffer_[2] = command_.version;
|
|
uart_buffer_[3] = command_.command;
|
|
uart_buffer_[4] = command_.length >> 8;
|
|
uart_buffer_[5] = command_.length & 0xFF;
|
|
command_.checksum = checksum();
|
|
ESP_LOGV(TAG, "TX_CHK: 0x%02X", command_.checksum);
|
|
// Send buffer out via UART
|
|
Serial.write(uart_buffer_, TUYA_COVER_BUFFER_LEN);
|
|
Serial.write(command_.value, command_.length);
|
|
Serial.write(command_.checksum);
|
|
// Clear buffer contents to avoid re-reading our own payload
|
|
memset(uart_buffer_, 0, TUYA_COVER_BUFFER_LEN);
|
|
}
|
|
|
|
/*
|
|
* Calculate checksum from current UART buffer (header+type+length) plus command value.
|
|
*/
|
|
uint8_t checksum()
|
|
{
|
|
uint8_t checksum = 0;
|
|
for (size_t i = 0; i < TUYA_COVER_BUFFER_LEN; i++)
|
|
{
|
|
checksum += uart_buffer_[i];
|
|
}
|
|
for (size_t i = 0; i < command_.length; i++)
|
|
{
|
|
checksum += command_.value[i];
|
|
}
|
|
return checksum;
|
|
}
|
|
|
|
class CustomCurtain : public Component, public Cover
|
|
{
|
|
|
|
protected:
|
|
|
|
public:
|
|
void setup() override
|
|
{
|
|
ESP_LOGI(TAG, "TUYA_COVER_COMMAND = QUERY_STATUS");
|
|
write_command(TUYA_COVER_QUERY_STATUS, 0, 0);
|
|
}
|
|
|
|
CoverTraits get_traits() override
|
|
{
|
|
auto traits = CoverTraits();
|
|
traits.set_is_assumed_state(false);
|
|
traits.set_supports_position(true);
|
|
traits.set_supports_tilt(false);
|
|
return traits;
|
|
}
|
|
|
|
void control(const CoverCall &call) override
|
|
{
|
|
if (call.get_position().has_value())
|
|
{
|
|
// Write pos (range 0-1) to cover
|
|
// Cover pos (range 0x00-0x64) (closed - open)
|
|
uint8_t pos = (100-(*call.get_position() * 100));
|
|
ESP_LOGV(TAG, "POS = %d", pos);
|
|
|
|
switch (pos)
|
|
{
|
|
case 0:
|
|
ESP_LOGI(TAG, "TUYA_COVER_COMMAND = CLOSE");
|
|
write_command(TUYA_COVER_COMMAND, tuya_cover_close, sizeof(tuya_cover_close));
|
|
break;
|
|
case 100:
|
|
ESP_LOGI(TAG, "TUYA_COVER_COMMAND = OPEN");
|
|
write_command(TUYA_COVER_COMMAND, tuya_cover_open, sizeof(tuya_cover_open));
|
|
break;
|
|
default:
|
|
tuya_cover_pos[7] = pos;
|
|
ESP_LOGI(TAG, "TUYA_COVER_COMMAND = POS = %d%%", pos);
|
|
write_command(TUYA_COVER_COMMAND, tuya_cover_pos, sizeof(tuya_cover_pos));
|
|
break;
|
|
}
|
|
// publish_state only when position is confirmed in loop()
|
|
}
|
|
if (call.get_stop())
|
|
{
|
|
// User requested cover stop
|
|
ESP_LOGI(TAG, "TUYA_COVER_COMMAND = STOP");
|
|
write_command(TUYA_COVER_COMMAND, tuya_cover_stop, sizeof(tuya_cover_stop));
|
|
}
|
|
}
|
|
|
|
void loop() override
|
|
{
|
|
unsigned long currentHeartbeatMillis = millis();
|
|
if (currentHeartbeatMillis - previousHeartbeatMillis >= HEARTBEAT_INTERVAL_MS)
|
|
{
|
|
previousHeartbeatMillis += HEARTBEAT_INTERVAL_MS;
|
|
ESP_LOGI(TAG, "TUYA_COVER_COMMAND = HEARTBEAT");
|
|
write_command(TUYA_COVER_HEARTBEAT, 0, 0);
|
|
}
|
|
|
|
bool have_message = read_command();
|
|
|
|
if(have_message && command_.command == TUYA_COVER_HEARTBEAT)
|
|
{
|
|
ESP_LOGI(TAG, "TUYA_COVER_RESPONSE = %s", (command_.value[0] == 0) ? "FIRST_HEARTBEAT" : "HEARTBEAT");
|
|
}
|
|
else if (have_message && command_.command == TUYA_COVER_RESPONSE)
|
|
{
|
|
switch (command_.value[0])
|
|
{
|
|
case TUYA_COVER_DPID_POSITION:
|
|
ESP_LOGI(TAG, "TUYA_COVER_DPID_POSITION = %d%%", command_.value[7]);
|
|
this->position = (1-((command_.value[7]) / 100.0f));
|
|
this->publish_state();
|
|
break;
|
|
case TUYA_COVER_DPID_DIRECTION:
|
|
ESP_LOGI(TAG, "TUYA_COVER_DPID_DIRECTION = 0x%02X", command_.value[4]);
|
|
break;
|
|
case TUYA_COVER_DPID_UNKNOWN:
|
|
ESP_LOGI(TAG, "TUYA_COVER_DPID_UNKNOWN ENUM = 0x%02X", command_.value[4]);
|
|
break;
|
|
case TUYA_COVER_DPID_ERROR:
|
|
ESP_LOGI(TAG, "TUYA_COVER_DPID_ERROR BITMAP = 0x%02X", command_.value[4]);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
class CustomAPI : public Component, public CustomAPIDevice
|
|
{
|
|
public:
|
|
void setup() override
|
|
{
|
|
register_service(&CustomAPI::setStatusReport, "get_status_report");
|
|
register_service(&CustomAPI::setMotorNormal, "set_motor_normal");
|
|
register_service(&CustomAPI::setMotorReversed, "set_motor_reversed");
|
|
register_service(&CustomAPI::sendCommand, "send_command", {"data"});
|
|
}
|
|
|
|
void setStatusReport()
|
|
{
|
|
ESP_LOGI(TAG, "TUYA_COVER_COMMAND = QUERY_STATUS");
|
|
write_command(TUYA_COVER_QUERY_STATUS, 0, 0);
|
|
}
|
|
|
|
void setMotorNormal()
|
|
{
|
|
ESP_LOGI(TAG, "TUYA_COVER_COMMAND = ENABLE_REVERSING");
|
|
write_command(TUYA_COVER_COMMAND, tuya_cover_enable_reversing, sizeof(tuya_cover_enable_reversing));
|
|
}
|
|
|
|
void setMotorReversed()
|
|
{
|
|
ESP_LOGI(TAG, "TUYA_COVER_COMMAND = ENABLE_REVERSING");
|
|
write_command(TUYA_COVER_COMMAND, tuya_cover_disable_reversing, sizeof(tuya_cover_disable_reversing));
|
|
}
|
|
|
|
void sendCommand(std::string data)
|
|
{
|
|
int i = 0;
|
|
uint8_t sum = 0;
|
|
while (i < data.length())
|
|
{
|
|
const char hex[2] = {data[i++], data[i++]};
|
|
uint8_t d = strtoul(hex, NULL, 16);
|
|
sum += d;
|
|
writeByte(d);
|
|
}
|
|
writeByte(sum);
|
|
}
|
|
|
|
void writeByte(uint8_t data)
|
|
{
|
|
Serial.write(data);
|
|
}
|
|
};
|