Updates needed for ESPHome 2021.8.0 (#20)

* Begin making updates needed for ESPHome v2021.8.0

* Wrap up changes needed for ESPHome 2021.8.0

Co-authored-by: Chris Nussbaum <chris.nussbaum@protolabs.com>
This commit is contained in:
Chris Nussbaum 2021-08-19 08:08:10 -05:00 committed by GitHub
parent 5ab19cc690
commit 18bb3a8e5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 57 additions and 953 deletions

View File

@ -21,9 +21,6 @@ This an enhanced version of the standard [gpio switch](https://esphome.io/compon
### TREO LED Pool Light
This is a custom light component that works with [TREO LED Pool Lights](https://www.srsmith.com/en-us/products/pool-lighting/treo-led-pool-light/) and exposes the different colors as "effects" so thay can be selected from Home Assistant. More details on how to use this component are available [here](./components/treo_led_pool_light/README.md).
### Tuya
This is a copy of the standard Tuya component with a couple of fixes/tweaks that are still getting hashed out in the main branch of ESPHome that are needed to get my custom Tuya Light Plus component to work reliably. Hopefully I can remove this component soon.
### Tuya Dimmer as Fan
This a modified version of the [Tuya fan](https://esphome.io/components/fan/tuya.html) component I use with [Feit Dimmers](https://www.amazon.com/gp/product/B07SXDFH38/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&psc=1) (but it will likely work with other Tuya dimmers) to control bathroom fans and adds several features. I created this component because I couldn't find a regular on/off switch with the same look and feel as the Feit dimmers so I decided to use the Feit dimmers but use this component to prevent "dimming" the fan. More details on features and how to use this component are available [here](./components/tuya_dimmer_as_fan/README.md).

View File

@ -5,8 +5,7 @@ namespace binary_power {
light::LightTraits BinaryLightWithPower::get_traits() {
auto traits = light::LightTraits();
// TODO: Enable this with the 1.21.x version of ESPHome
// traits.set_supported_color_modes({light::ColorMode::ON_OFF});
traits.set_supported_color_modes({light::ColorMode::ON_OFF});
return traits;
}

View File

@ -23,11 +23,11 @@ CONFIG_SCHEMA = light.BINARY_LIGHT_SCHEMA.extend(
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(LightWithPower),
cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
cv.Optional(CONF_POWER): sensor.sensor_schema(
unit_of_measurement_=UNIT_WATT,
accuracy_decimals_=1,
device_class_=DEVICE_CLASS_POWER,
state_class_=STATE_CLASS_MEASUREMENT,
icon_=ICON_POWER,
unit_of_measurement=UNIT_WATT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
icon=ICON_POWER,
).extend(
{
cv.Optional(CONF_LIGHT_WATTAGE): cv.positive_float,

View File

@ -43,11 +43,11 @@ CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend(
CONF_INTERLOCK_WAIT_TIME, default="0ms"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_POWER): sensor.sensor_schema(
unit_of_measurement_=UNIT_WATT,
accuracy_decimals_=1,
device_class_=DEVICE_CLASS_POWER,
state_class_=STATE_CLASS_MEASUREMENT,
icon_=ICON_POWER,
unit_of_measurement=UNIT_WATT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
icon=ICON_POWER,
).extend(
{
cv.Optional(CONF_DEVICE_WATTAGE): cv.positive_float,

View File

@ -1,65 +0,0 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor, time
from esphome.const import (
CONF_ID,
CONF_TIME_ID,
DEVICE_CLASS_ENERGY,
LAST_RESET_TYPE_AUTO,
STATE_CLASS_MEASUREMENT,
)
DEPENDENCIES = ["time"]
CONF_POWER_ID = "power_id"
CONF_MIN_SAVE_INTERVAL = "min_save_interval"
CONF_TOTAL_DAILY_ENERGY_METHOD = "method"
total_daily_energy_ns = cg.esphome_ns.namespace("total_daily_energy")
TotalDailyEnergyMethod = total_daily_energy_ns.enum("TotalDailyEnergyMethod")
TOTAL_DAILY_ENERGY_METHODS = {
"trapezoid": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID,
"left": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_LEFT,
"right": TotalDailyEnergyMethod.TOTAL_DAILY_ENERGY_METHOD_RIGHT,
}
TotalDailyEnergy = total_daily_energy_ns.class_(
"TotalDailyEnergy", sensor.Sensor, cg.Component
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
accuracy_decimals_=0,
device_class_=DEVICE_CLASS_ENERGY,
state_class_=STATE_CLASS_MEASUREMENT,
last_reset_type_=LAST_RESET_TYPE_AUTO,
icon_="",
unit_of_measurement_="",
)
.extend(
{
cv.GenerateID(): cv.declare_id(TotalDailyEnergy),
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Required(CONF_POWER_ID): cv.use_id(sensor.Sensor),
cv.Optional(
CONF_MIN_SAVE_INTERVAL, default="0s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_TOTAL_DAILY_ENERGY_METHOD, default="left"): cv.enum(
TOTAL_DAILY_ENERGY_METHODS, lower=True
),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
sens = await cg.get_variable(config[CONF_POWER_ID])
cg.add(var.set_parent(sens))
time_ = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_time(time_))
cg.add(var.set_min_save_interval(config[CONF_MIN_SAVE_INTERVAL]))
cg.add(var.set_method(config[CONF_TOTAL_DAILY_ENERGY_METHOD]))

View File

@ -1,79 +0,0 @@
#include "total_daily_energy.h"
#include "esphome/core/log.h"
namespace esphome {
namespace total_daily_energy {
static const char *const TAG = "total_daily_energy";
void TotalDailyEnergy::setup() {
this->pref_ = global_preferences.make_preference<float>(this->get_object_id_hash());
float recovered;
if (this->pref_.load(&recovered)) {
this->publish_state_and_save(recovered);
} else {
this->publish_state_and_save(0);
}
this->last_update_ = millis();
this->last_save_ = this->last_update_;
this->parent_->add_on_state_callback([this](float state) { this->process_new_state_(state); });
}
void TotalDailyEnergy::dump_config() { LOG_SENSOR("", "Total Daily Energy", this); }
void TotalDailyEnergy::loop() {
auto t = this->time_->now();
if (!t.is_valid())
return;
if (this->last_day_of_year_ == 0) {
this->last_day_of_year_ = t.day_of_year;
return;
}
if (t.day_of_year != this->last_day_of_year_) {
this->last_day_of_year_ = t.day_of_year;
this->total_energy_ = 0;
this->publish_state_and_save(0);
}
}
void TotalDailyEnergy::publish_state_and_save(float state) {
this->total_energy_ = state;
this->publish_state(state);
const uint32_t now = millis();
if (now - this->last_save_ < this->min_save_interval_) {
return;
}
this->last_save_ = now;
this->pref_.save(&state);
}
void TotalDailyEnergy::process_new_state_(float state) {
if (isnan(state))
return;
const uint32_t now = millis();
const float old_state = this->last_power_state_;
const float new_state = state;
float delta_hours = (now - this->last_update_) / 1000.0f / 60.0f / 60.0f;
float delta_energy = 0.0f;
switch (this->method_) {
case TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID:
delta_energy = delta_hours * (old_state + new_state) / 2.0;
break;
case TOTAL_DAILY_ENERGY_METHOD_LEFT:
delta_energy = delta_hours * old_state;
break;
case TOTAL_DAILY_ENERGY_METHOD_RIGHT:
delta_energy = delta_hours * new_state;
break;
}
this->last_power_state_ = new_state;
this->last_update_ = now;
this->publish_state_and_save(this->total_energy_ + delta_energy);
}
} // namespace total_daily_energy
} // namespace esphome

View File

@ -1,49 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/time/real_time_clock.h"
namespace esphome {
namespace total_daily_energy {
enum TotalDailyEnergyMethod {
TOTAL_DAILY_ENERGY_METHOD_TRAPEZOID = 0,
TOTAL_DAILY_ENERGY_METHOD_LEFT,
TOTAL_DAILY_ENERGY_METHOD_RIGHT,
};
class TotalDailyEnergy : public sensor::Sensor, public Component {
public:
void set_min_save_interval(uint32_t min_interval) { this->min_save_interval_ = min_interval; }
void set_time(time::RealTimeClock *time) { time_ = time; }
void set_parent(Sensor *parent) { parent_ = parent; }
void set_method(TotalDailyEnergyMethod method) { method_ = method; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
std::string unit_of_measurement() override { return this->parent_->get_unit_of_measurement() + "h"; }
std::string icon() override { return this->parent_->get_icon(); }
int8_t accuracy_decimals() override { return this->parent_->get_accuracy_decimals() + 2; }
void loop() override;
void publish_state_and_save(float state);
protected:
void process_new_state_(float state);
ESPPreferenceObject pref_;
time::RealTimeClock *time_;
Sensor *parent_;
TotalDailyEnergyMethod method_;
uint16_t last_day_of_year_{};
uint32_t last_update_{0};
uint32_t last_save_{0};
uint32_t min_save_interval_{0};
float total_energy_{0.0f};
float last_power_state_{0.0f};
};
} // namespace total_daily_energy
} // namespace esphome

View File

@ -26,11 +26,11 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TreoLight),
cv.Required(CONF_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_POWER): sensor.sensor_schema(
unit_of_measurement_=UNIT_WATT,
accuracy_decimals_=1,
device_class_=DEVICE_CLASS_POWER,
state_class_=STATE_CLASS_MEASUREMENT,
icon_=ICON_POWER,
unit_of_measurement=UNIT_WATT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
icon=ICON_POWER,
).extend(
{
cv.Optional(CONF_LIGHT_WATTAGE): cv.positive_float,

View File

@ -11,6 +11,7 @@ static const uint32_t RESTORE_COLOR_VERSION = 0x7B715952UL;
LightTraits TreoLedPoolLight::get_traits() {
auto traits = light::LightTraits();
traits.set_supported_color_modes({light::ColorMode::ON_OFF});
return traits;
}

View File

@ -1,39 +0,0 @@
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))

View File

@ -1,548 +0,0 @@
#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", 15000, [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>{}});
}
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_raw_datapoint_value(uint8_t datapoint_id, const std::vector<uint8_t> &value) {
ESP_LOGD(TAG, "Setting datapoint %u to %s", datapoint_id, hexencode(value).c_str());
optional<TuyaDatapoint> datapoint = this->get_datapoint_(datapoint_id);
if (!datapoint.has_value()) {
ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id);
} else if (datapoint->type != TuyaDatapointType::RAW) {
ESP_LOGE(TAG, "Attempt to set datapoint %u with incorrect type", datapoint_id);
return;
} else if (datapoint->value_raw == value) {
ESP_LOGV(TAG, "Not sending unchanged value");
return;
}
this->send_datapoint_command_(datapoint_id, TuyaDatapointType::RAW, value);
}
void Tuya::set_boolean_datapoint_value(uint8_t datapoint_id, bool value) {
this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::BOOLEAN, value, 1);
}
void Tuya::set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value) {
this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::INTEGER, value, 4);
}
void Tuya::set_string_datapoint_value(uint8_t datapoint_id, const 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_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id);
} else if (datapoint->type != TuyaDatapointType::STRING) {
ESP_LOGE(TAG, "Attempt to set datapoint %u with incorrect type", datapoint_id);
return;
} else 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);
}
void Tuya::set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value) {
this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::ENUM, value, 1);
}
void Tuya::set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length) {
this->set_numeric_datapoint_value_(datapoint_id, TuyaDatapointType::BITMASK, value, length);
}
optional<TuyaDatapoint> Tuya::get_datapoint_(uint8_t datapoint_id) {
for (auto &datapoint : this->datapoints_)
if (datapoint.id == datapoint_id)
return datapoint;
return {};
}
void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, const uint32_t value,
uint8_t length) {
ESP_LOGD(TAG, "Setting datapoint %u to %u", datapoint_id, value);
optional<TuyaDatapoint> datapoint = this->get_datapoint_(datapoint_id);
if (!datapoint.has_value()) {
ESP_LOGW(TAG, "Setting unknown datapoint %u", datapoint_id);
} else if (datapoint->type != datapoint_type) {
ESP_LOGE(TAG, "Attempt to set datapoint %u with incorrect type", datapoint_id);
return;
} else if (datapoint->value_uint == value) {
ESP_LOGV(TAG, "Not sending unchanged value");
return;
}
std::vector<uint8_t> data;
switch (length) {
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 %u", length);
return;
}
this->send_datapoint_command_(datapoint_id, datapoint_type, data);
}
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

View File

@ -1,127 +0,0 @@
#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_raw_datapoint_value(uint8_t datapoint_id, const std::vector<uint8_t> &value);
void set_boolean_datapoint_value(uint8_t datapoint_id, bool value);
void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value);
void set_string_datapoint_value(uint8_t datapoint_id, const std::string &value);
void set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value);
void set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length);
#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 set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, uint32_t value,
uint8_t length);
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

View File

@ -7,11 +7,11 @@ This a modified version of the [Tuya fan](https://esphome.io/components/fan/tuya
## Setup
Using the [External Components](https://esphome.io/components/external_components.html) feature in ESPHome you can add this component to your devices directly from my GitHub repo. Note currently this component requires pulling in my custom version of the Tuya component as well to prevent communication issues between the ESP8266 and the Tuya MCU.
Using the [External Components](https://esphome.io/components/external_components.html) feature in ESPHome you can add this component to your devices directly from my GitHub repo.
```yaml
external_components:
- source: github://nuttytree/esphome
components: [ tuya, tuya_dimmer_as_fan ]
components: [ tuya_dimmer_as_fan ]
```
Like the standard Tuya fan component you need to have the [UART](https://esphome.io/components/uart.html) and [Tuya](https://esphome.io/components/tuya.html) components.

View File

@ -31,11 +31,11 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_DIMMER_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_MAX_VALUE): cv.int_,
cv.Optional(CONF_POWER): sensor.sensor_schema(
unit_of_measurement_=UNIT_WATT,
accuracy_decimals_=1,
device_class_=DEVICE_CLASS_POWER,
state_class_=STATE_CLASS_MEASUREMENT,
icon_=ICON_POWER,
unit_of_measurement=UNIT_WATT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
icon=ICON_POWER,
).extend(
{
cv.Optional(CONF_FAN_WATTAGE): cv.positive_float,

View File

@ -15,11 +15,11 @@ This an enhanced version of the standard [Tuya light](https://esphome.io/compone
## Setup
Using the [External Components](https://esphome.io/components/external_components.html) feature in ESPHome you can add this component to your devices directly from my GitHub repo. Note currently this component requires pulling in my custom version of the Tuya component as well to prevent communication issues between the ESP8266 and the Tuya MCU.
Using the [External Components](https://esphome.io/components/external_components.html) feature in ESPHome you can add this component to your devices directly from my GitHub repo.
```yaml
external_components:
- source: github://nuttytree/esphome
components: [ tuya, tuya_light_plus ]
components: [ tuya_light_plus ]
```
Like the standard Tuya Light component you need to have the [UART](https://esphome.io/components/uart.html) and [Tuya](https://esphome.io/components/tuya.html) components.

View File

@ -97,11 +97,11 @@ CONFIG_SCHEMA = cv.All(
CONF_DEFAULT_TRANSITION_LENGTH, default="0s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_POWER): sensor.sensor_schema(
unit_of_measurement_=UNIT_WATT,
accuracy_decimals_=1,
device_class_=DEVICE_CLASS_POWER,
state_class_=STATE_CLASS_MEASUREMENT,
icon_=ICON_POWER,
unit_of_measurement=UNIT_WATT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
icon=ICON_POWER,
).extend(
{
cv.Optional(CONF_LIGHT_WATTAGE): cv.positive_float,

View File

@ -36,7 +36,7 @@ void TuyaLightPlus::dump_config()
light::LightTraits TuyaLightPlus::get_traits()
{
auto traits = light::LightTraits();
traits.set_supports_brightness(true);
traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS});
return traits;
}

View File

@ -15,9 +15,11 @@ external_components:
- source:
type: local
path: ../components
components: [ binary_light_with_power, total_daily_energy ]
components: [ binary_light_with_power ]
- source: github://cbpowell/ESPSense
components: [ espsense ]
- source: github://esphome/esphome@dev
components: [ total_daily_energy ]
binary_sensor:
- platform: gpio

View File

@ -12,12 +12,14 @@ packages:
device_base: !include ../packages/device_base.yaml
external_components:
- source: github://cbpowell/ESPSense
components: [ espsense ]
- source:
type: local
path: ../components
components: [ binary_light_with_power, gpio_switch_with_power, total_daily_energy ]
components: [ binary_light_with_power, gpio_switch_with_power ]
- source: github://cbpowell/ESPSense
components: [ espsense ]
- source: github://esphome/esphome@dev
components: [ total_daily_energy ]
binary_sensor:
- platform: gpio

View File

@ -15,9 +15,11 @@ external_components:
- source:
type: local
path: ../components
components: [ binary_light_with_power, total_daily_energy ]
components: [ binary_light_with_power ]
- source: github://cbpowell/ESPSense
components: [ espsense ]
- source: github://esphome/esphome@dev
components: [ total_daily_energy ]
espsense:
plugs:

View File

@ -15,9 +15,11 @@ external_components:
- source:
type: local
path: ../components
components: [ treo_led_pool_light, binary_light_with_power, total_daily_energy ]
components: [ treo_led_pool_light, binary_light_with_power ]
- source: github://cbpowell/ESPSense
components: [ espsense ]
- source: github://esphome/esphome@dev
components: [ total_daily_energy ]
binary_sensor:
- platform: gpio

View File

@ -115,11 +115,13 @@ sensor:
- platform: total_daily_energy
name: "Pool Pump Total Daily Energy"
power_id: pool_pump_power
state_class: "measurement"
method: left
min_save_interval: 10min
- platform: total_daily_energy
name: "Pool Cleaner Total Daily Energy"
power_id: pool_cleaner_power
state_class: "measurement"
method: left
min_save_interval: 10min
status_led:
pin:

View File

@ -6,9 +6,11 @@ external_components:
- source:
type: local
path: ../components
components: [ tuya, tuya_light_plus, total_daily_energy ]
components: [ tuya_light_plus ]
- source: github://cbpowell/ESPSense
components: [ espsense ]
- source: github://esphome/esphome@dev
components: [ total_daily_energy ]
espsense:
plugs:

View File

@ -6,9 +6,11 @@ external_components:
- source:
type: local
path: ../components
components: [ tuya, tuya_dimmer_as_fan, total_daily_energy ]
components: [ tuya_dimmer_as_fan ]
- source: github://cbpowell/ESPSense
components: [ espsense ]
- source: github://esphome/esphome@dev
components: [ total_daily_energy ]
packages:
base: !include device_base.yaml

View File

@ -6,7 +6,7 @@ external_components:
- source:
type: local
path: ../components
components: [ tuya, tuya_light_plus ]
components: [ tuya_light_plus ]
packages:
base: !include device_base.yaml