mirror of
https://github.com/esphome/esphome.git
synced 2024-11-12 18:27:46 +01:00
Tuya improvements (#1491)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
c3938d04f3
commit
c5c24c1989
12 changed files with 321 additions and 219 deletions
|
@ -8,8 +8,8 @@ static const char *TAG = "tuya.binary_sensor";
|
||||||
|
|
||||||
void TuyaBinarySensor::setup() {
|
void TuyaBinarySensor::setup() {
|
||||||
this->parent_->register_listener(this->sensor_id_, [this](TuyaDatapoint datapoint) {
|
this->parent_->register_listener(this->sensor_id_, [this](TuyaDatapoint datapoint) {
|
||||||
|
ESP_LOGV(TAG, "MCU reported binary sensor %u is: %s", datapoint.id, ONOFF(datapoint.value_bool));
|
||||||
this->publish_state(datapoint.value_bool);
|
this->publish_state(datapoint.value_bool);
|
||||||
ESP_LOGD(TAG, "MCU reported binary sensor is: %s", ONOFF(datapoint.value_bool));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
from esphome.components import climate
|
from esphome.components import climate
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.const import CONF_ID, CONF_SWITCH_DATAPOINT
|
from esphome.const import (
|
||||||
|
CONF_ID,
|
||||||
|
CONF_SWITCH_DATAPOINT,
|
||||||
|
CONF_SUPPORTS_COOL,
|
||||||
|
CONF_SUPPORTS_HEAT,
|
||||||
|
)
|
||||||
from .. import tuya_ns, CONF_TUYA_ID, Tuya
|
from .. import tuya_ns, CONF_TUYA_ID, Tuya
|
||||||
|
|
||||||
DEPENDENCIES = ["tuya"]
|
DEPENDENCIES = ["tuya"]
|
||||||
CODEOWNERS = ["@jesserockz"]
|
CODEOWNERS = ["@jesserockz"]
|
||||||
|
|
||||||
|
CONF_ACTIVE_STATE_DATAPOINT = "active_state_datapoint"
|
||||||
|
CONF_ACTIVE_STATE_HEATING_VALUE = "active_state_heating_value"
|
||||||
|
CONF_ACTIVE_STATE_COOLING_VALUE = "active_state_cooling_value"
|
||||||
CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint"
|
CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint"
|
||||||
CONF_CURRENT_TEMPERATURE_DATAPOINT = "current_temperature_datapoint"
|
CONF_CURRENT_TEMPERATURE_DATAPOINT = "current_temperature_datapoint"
|
||||||
CONF_TEMPERATURE_MULTIPLIER = "temperature_multiplier"
|
CONF_TEMPERATURE_MULTIPLIER = "temperature_multiplier"
|
||||||
|
@ -59,12 +67,30 @@ def validate_temperature_multipliers(value):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def validate_active_state_values(value):
|
||||||
|
if CONF_ACTIVE_STATE_DATAPOINT not in value:
|
||||||
|
return value
|
||||||
|
if value[CONF_SUPPORTS_COOL] and CONF_ACTIVE_STATE_COOLING_VALUE not in value:
|
||||||
|
raise cv.Invalid(
|
||||||
|
(
|
||||||
|
f"{CONF_ACTIVE_STATE_COOLING_VALUE} required if using "
|
||||||
|
f"{CONF_ACTIVE_STATE_DATAPOINT} and device supports cooling"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
climate.CLIMATE_SCHEMA.extend(
|
climate.CLIMATE_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(TuyaClimate),
|
cv.GenerateID(): cv.declare_id(TuyaClimate),
|
||||||
cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya),
|
cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya),
|
||||||
|
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
|
||||||
|
cv.Optional(CONF_SUPPORTS_COOL, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
|
cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
|
||||||
|
cv.Optional(CONF_ACTIVE_STATE_DATAPOINT): cv.uint8_t,
|
||||||
|
cv.Optional(CONF_ACTIVE_STATE_HEATING_VALUE, default=1): cv.uint8_t,
|
||||||
|
cv.Optional(CONF_ACTIVE_STATE_COOLING_VALUE): cv.uint8_t,
|
||||||
cv.Optional(CONF_TARGET_TEMPERATURE_DATAPOINT): cv.uint8_t,
|
cv.Optional(CONF_TARGET_TEMPERATURE_DATAPOINT): cv.uint8_t,
|
||||||
cv.Optional(CONF_CURRENT_TEMPERATURE_DATAPOINT): cv.uint8_t,
|
cv.Optional(CONF_CURRENT_TEMPERATURE_DATAPOINT): cv.uint8_t,
|
||||||
cv.Optional(CONF_TEMPERATURE_MULTIPLIER): cv.positive_float,
|
cv.Optional(CONF_TEMPERATURE_MULTIPLIER): cv.positive_float,
|
||||||
|
@ -74,6 +100,7 @@ CONFIG_SCHEMA = cv.All(
|
||||||
).extend(cv.COMPONENT_SCHEMA),
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
cv.has_at_least_one_key(CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT),
|
cv.has_at_least_one_key(CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT),
|
||||||
validate_temperature_multipliers,
|
validate_temperature_multipliers,
|
||||||
|
validate_active_state_values,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -85,8 +112,20 @@ async def to_code(config):
|
||||||
paren = await cg.get_variable(config[CONF_TUYA_ID])
|
paren = await cg.get_variable(config[CONF_TUYA_ID])
|
||||||
cg.add(var.set_tuya_parent(paren))
|
cg.add(var.set_tuya_parent(paren))
|
||||||
|
|
||||||
|
cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
|
||||||
|
cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
|
||||||
if CONF_SWITCH_DATAPOINT in config:
|
if CONF_SWITCH_DATAPOINT in config:
|
||||||
cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT]))
|
cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT]))
|
||||||
|
if CONF_ACTIVE_STATE_DATAPOINT in config:
|
||||||
|
cg.add(var.set_active_state_id(config[CONF_ACTIVE_STATE_DATAPOINT]))
|
||||||
|
if CONF_ACTIVE_STATE_HEATING_VALUE in config:
|
||||||
|
cg.add(
|
||||||
|
var.set_active_state_heating_value(config[CONF_ACTIVE_STATE_HEATING_VALUE])
|
||||||
|
)
|
||||||
|
if CONF_ACTIVE_STATE_COOLING_VALUE in config:
|
||||||
|
cg.add(
|
||||||
|
var.set_active_state_cooling_value(config[CONF_ACTIVE_STATE_COOLING_VALUE])
|
||||||
|
)
|
||||||
if CONF_TARGET_TEMPERATURE_DATAPOINT in config:
|
if CONF_TARGET_TEMPERATURE_DATAPOINT in config:
|
||||||
cg.add(var.set_target_temperature_id(config[CONF_TARGET_TEMPERATURE_DATAPOINT]))
|
cg.add(var.set_target_temperature_id(config[CONF_TARGET_TEMPERATURE_DATAPOINT]))
|
||||||
if CONF_CURRENT_TEMPERATURE_DATAPOINT in config:
|
if CONF_CURRENT_TEMPERATURE_DATAPOINT in config:
|
||||||
|
|
|
@ -9,65 +9,67 @@ static const char *TAG = "tuya.climate";
|
||||||
void TuyaClimate::setup() {
|
void TuyaClimate::setup() {
|
||||||
if (this->switch_id_.has_value()) {
|
if (this->switch_id_.has_value()) {
|
||||||
this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) {
|
this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) {
|
||||||
|
ESP_LOGV(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool));
|
||||||
|
this->mode = climate::CLIMATE_MODE_OFF;
|
||||||
if (datapoint.value_bool) {
|
if (datapoint.value_bool) {
|
||||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
if (this->supports_heat_ && this->supports_cool_) {
|
||||||
} else {
|
this->mode = climate::CLIMATE_MODE_AUTO;
|
||||||
this->mode = climate::CLIMATE_MODE_OFF;
|
} else if (this->supports_heat_) {
|
||||||
|
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||||
|
} else if (this->supports_cool_) {
|
||||||
|
this->mode = climate::CLIMATE_MODE_COOL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this->compute_state_();
|
this->compute_state_();
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool));
|
});
|
||||||
|
}
|
||||||
|
if (this->active_state_id_.has_value()) {
|
||||||
|
this->parent_->register_listener(*this->active_state_id_, [this](TuyaDatapoint datapoint) {
|
||||||
|
ESP_LOGV(TAG, "MCU reported active state is: %u", datapoint.value_enum);
|
||||||
|
this->active_state_ = datapoint.value_enum;
|
||||||
|
this->compute_state_();
|
||||||
|
this->publish_state();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this->target_temperature_id_.has_value()) {
|
if (this->target_temperature_id_.has_value()) {
|
||||||
this->parent_->register_listener(*this->target_temperature_id_, [this](TuyaDatapoint datapoint) {
|
this->parent_->register_listener(*this->target_temperature_id_, [this](TuyaDatapoint datapoint) {
|
||||||
this->target_temperature = datapoint.value_int * this->target_temperature_multiplier_;
|
this->target_temperature = datapoint.value_int * this->target_temperature_multiplier_;
|
||||||
|
ESP_LOGV(TAG, "MCU reported target temperature is: %.1f", this->target_temperature);
|
||||||
this->compute_state_();
|
this->compute_state_();
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
ESP_LOGD(TAG, "MCU reported target temperature is: %.1f", this->target_temperature);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this->current_temperature_id_.has_value()) {
|
if (this->current_temperature_id_.has_value()) {
|
||||||
this->parent_->register_listener(*this->current_temperature_id_, [this](TuyaDatapoint datapoint) {
|
this->parent_->register_listener(*this->current_temperature_id_, [this](TuyaDatapoint datapoint) {
|
||||||
this->current_temperature = datapoint.value_int * this->current_temperature_multiplier_;
|
this->current_temperature = datapoint.value_int * this->current_temperature_multiplier_;
|
||||||
|
ESP_LOGV(TAG, "MCU reported current temperature is: %.1f", this->current_temperature);
|
||||||
this->compute_state_();
|
this->compute_state_();
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
ESP_LOGD(TAG, "MCU reported current temperature is: %.1f", this->current_temperature);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TuyaClimate::control(const climate::ClimateCall &call) {
|
void TuyaClimate::control(const climate::ClimateCall &call) {
|
||||||
if (call.get_mode().has_value()) {
|
if (call.get_mode().has_value()) {
|
||||||
this->mode = *call.get_mode();
|
const bool switch_state = *call.get_mode() != climate::CLIMATE_MODE_OFF;
|
||||||
|
ESP_LOGV(TAG, "Setting switch: %s", ONOFF(switch_state));
|
||||||
TuyaDatapoint datapoint{};
|
this->parent_->set_datapoint_value(*this->switch_id_, switch_state);
|
||||||
datapoint.id = *this->switch_id_;
|
|
||||||
datapoint.type = TuyaDatapointType::BOOLEAN;
|
|
||||||
datapoint.value_bool = this->mode != climate::CLIMATE_MODE_OFF;
|
|
||||||
this->parent_->set_datapoint_value(datapoint);
|
|
||||||
ESP_LOGD(TAG, "Setting switch: %s", ONOFF(datapoint.value_bool));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (call.get_target_temperature().has_value()) {
|
if (call.get_target_temperature().has_value()) {
|
||||||
this->target_temperature = *call.get_target_temperature();
|
const float target_temperature = *call.get_target_temperature();
|
||||||
|
ESP_LOGV(TAG, "Setting target temperature: %.1f", target_temperature);
|
||||||
TuyaDatapoint datapoint{};
|
this->parent_->set_datapoint_value(*this->target_temperature_id_,
|
||||||
datapoint.id = *this->target_temperature_id_;
|
(int) (target_temperature / this->target_temperature_multiplier_));
|
||||||
datapoint.type = TuyaDatapointType::INTEGER;
|
|
||||||
datapoint.value_int = (int) (this->target_temperature / this->target_temperature_multiplier_);
|
|
||||||
this->parent_->set_datapoint_value(datapoint);
|
|
||||||
ESP_LOGD(TAG, "Setting target temperature: %.1f", this->target_temperature);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this->compute_state_();
|
|
||||||
this->publish_state();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
climate::ClimateTraits TuyaClimate::traits() {
|
climate::ClimateTraits TuyaClimate::traits() {
|
||||||
auto traits = climate::ClimateTraits();
|
auto traits = climate::ClimateTraits();
|
||||||
traits.set_supports_current_temperature(this->current_temperature_id_.has_value());
|
traits.set_supports_current_temperature(this->current_temperature_id_.has_value());
|
||||||
traits.set_supports_heat_mode(true);
|
traits.set_supports_heat_mode(this->supports_heat_);
|
||||||
|
traits.set_supports_cool_mode(this->supports_cool_);
|
||||||
traits.set_supports_action(true);
|
traits.set_supports_action(true);
|
||||||
return traits;
|
return traits;
|
||||||
}
|
}
|
||||||
|
@ -76,6 +78,8 @@ void TuyaClimate::dump_config() {
|
||||||
LOG_CLIMATE("", "Tuya Climate", this);
|
LOG_CLIMATE("", "Tuya Climate", this);
|
||||||
if (this->switch_id_.has_value())
|
if (this->switch_id_.has_value())
|
||||||
ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_);
|
ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_);
|
||||||
|
if (this->active_state_id_.has_value())
|
||||||
|
ESP_LOGCONFIG(TAG, " Active state has datapoint ID %u", *this->active_state_id_);
|
||||||
if (this->target_temperature_id_.has_value())
|
if (this->target_temperature_id_.has_value())
|
||||||
ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *this->target_temperature_id_);
|
ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *this->target_temperature_id_);
|
||||||
if (this->current_temperature_id_.has_value())
|
if (this->current_temperature_id_.has_value())
|
||||||
|
@ -94,30 +98,27 @@ void TuyaClimate::compute_state_() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool too_cold = this->current_temperature < this->target_temperature - 1;
|
climate::ClimateAction target_action = climate::CLIMATE_ACTION_IDLE;
|
||||||
const bool too_hot = this->current_temperature > this->target_temperature + 1;
|
if (this->active_state_id_.has_value()) {
|
||||||
const bool on_target = this->current_temperature == this->target_temperature;
|
if (this->supports_heat_ && this->active_state_heating_value_.has_value() &&
|
||||||
|
this->active_state_ == this->active_state_heating_value_) {
|
||||||
climate::ClimateAction target_action;
|
|
||||||
if (too_cold) {
|
|
||||||
// too cold -> show as heating if possible, else idle
|
|
||||||
if (this->traits().supports_mode(climate::CLIMATE_MODE_HEAT)) {
|
|
||||||
target_action = climate::CLIMATE_ACTION_HEATING;
|
target_action = climate::CLIMATE_ACTION_HEATING;
|
||||||
} else {
|
} else if (this->supports_cool_ && this->active_state_cooling_value_.has_value() &&
|
||||||
target_action = climate::CLIMATE_ACTION_IDLE;
|
this->active_state_ == this->active_state_cooling_value_) {
|
||||||
}
|
|
||||||
} else if (too_hot) {
|
|
||||||
// too hot -> show as cooling if possible, else idle
|
|
||||||
if (this->traits().supports_mode(climate::CLIMATE_MODE_COOL)) {
|
|
||||||
target_action = climate::CLIMATE_ACTION_COOLING;
|
target_action = climate::CLIMATE_ACTION_COOLING;
|
||||||
} else {
|
|
||||||
target_action = climate::CLIMATE_ACTION_IDLE;
|
|
||||||
}
|
}
|
||||||
} else if (on_target) {
|
|
||||||
target_action = climate::CLIMATE_ACTION_IDLE;
|
|
||||||
} else {
|
} else {
|
||||||
target_action = this->action;
|
// Fallback to active state calc based on temp and hysteresis
|
||||||
|
const float temp_diff = this->target_temperature - this->current_temperature;
|
||||||
|
if (std::abs(temp_diff) > this->hysteresis_) {
|
||||||
|
if (this->supports_heat_ && temp_diff > 0) {
|
||||||
|
target_action = climate::CLIMATE_ACTION_HEATING;
|
||||||
|
} else if (this->supports_cool_ && temp_diff < 0) {
|
||||||
|
target_action = climate::CLIMATE_ACTION_COOLING;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this->switch_to_action_(target_action);
|
this->switch_to_action_(target_action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,12 @@ class TuyaClimate : public climate::Climate, public Component {
|
||||||
public:
|
public:
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
||||||
|
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
||||||
void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; }
|
void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; }
|
||||||
|
void set_active_state_id(uint8_t state_id) { this->active_state_id_ = state_id; }
|
||||||
|
void set_active_state_heating_value(uint8_t value) { this->active_state_heating_value_ = value; }
|
||||||
|
void set_active_state_cooling_value(uint8_t value) { this->active_state_cooling_value_ = value; }
|
||||||
void set_target_temperature_id(uint8_t target_temperature_id) {
|
void set_target_temperature_id(uint8_t target_temperature_id) {
|
||||||
this->target_temperature_id_ = target_temperature_id;
|
this->target_temperature_id_ = target_temperature_id;
|
||||||
}
|
}
|
||||||
|
@ -40,11 +45,18 @@ class TuyaClimate : public climate::Climate, public Component {
|
||||||
void switch_to_action_(climate::ClimateAction action);
|
void switch_to_action_(climate::ClimateAction action);
|
||||||
|
|
||||||
Tuya *parent_;
|
Tuya *parent_;
|
||||||
|
bool supports_heat_;
|
||||||
|
bool supports_cool_;
|
||||||
optional<uint8_t> switch_id_{};
|
optional<uint8_t> switch_id_{};
|
||||||
|
optional<uint8_t> active_state_id_{};
|
||||||
|
optional<uint8_t> active_state_heating_value_{};
|
||||||
|
optional<uint8_t> active_state_cooling_value_{};
|
||||||
optional<uint8_t> target_temperature_id_{};
|
optional<uint8_t> target_temperature_id_{};
|
||||||
optional<uint8_t> current_temperature_id_{};
|
optional<uint8_t> current_temperature_id_{};
|
||||||
float current_temperature_multiplier_{1.0f};
|
float current_temperature_multiplier_{1.0f};
|
||||||
float target_temperature_multiplier_{1.0f};
|
float target_temperature_multiplier_{1.0f};
|
||||||
|
float hysteresis_{1.0f};
|
||||||
|
uint8_t active_state_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace tuya
|
} // namespace tuya
|
||||||
|
|
|
@ -14,29 +14,29 @@ void TuyaFan::setup() {
|
||||||
|
|
||||||
if (this->speed_id_.has_value()) {
|
if (this->speed_id_.has_value()) {
|
||||||
this->parent_->register_listener(*this->speed_id_, [this](TuyaDatapoint datapoint) {
|
this->parent_->register_listener(*this->speed_id_, [this](TuyaDatapoint datapoint) {
|
||||||
|
ESP_LOGV(TAG, "MCU reported speed of: %d", datapoint.value_enum);
|
||||||
auto call = this->fan_->make_call();
|
auto call = this->fan_->make_call();
|
||||||
if (datapoint.value_enum < this->speed_count_)
|
if (datapoint.value_enum < this->speed_count_)
|
||||||
call.set_speed(datapoint.value_enum + 1);
|
call.set_speed(datapoint.value_enum + 1);
|
||||||
else
|
else
|
||||||
ESP_LOGCONFIG(TAG, "Speed has invalid value %d", datapoint.value_enum);
|
ESP_LOGCONFIG(TAG, "Speed has invalid value %d", datapoint.value_enum);
|
||||||
ESP_LOGD(TAG, "MCU reported speed of: %d", datapoint.value_enum);
|
|
||||||
call.perform();
|
call.perform();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this->switch_id_.has_value()) {
|
if (this->switch_id_.has_value()) {
|
||||||
this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) {
|
this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) {
|
||||||
|
ESP_LOGV(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool));
|
||||||
auto call = this->fan_->make_call();
|
auto call = this->fan_->make_call();
|
||||||
call.set_state(datapoint.value_bool);
|
call.set_state(datapoint.value_bool);
|
||||||
call.perform();
|
call.perform();
|
||||||
ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this->oscillation_id_.has_value()) {
|
if (this->oscillation_id_.has_value()) {
|
||||||
this->parent_->register_listener(*this->oscillation_id_, [this](TuyaDatapoint datapoint) {
|
this->parent_->register_listener(*this->oscillation_id_, [this](TuyaDatapoint datapoint) {
|
||||||
|
ESP_LOGV(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool));
|
||||||
auto call = this->fan_->make_call();
|
auto call = this->fan_->make_call();
|
||||||
call.set_oscillating(datapoint.value_bool);
|
call.set_oscillating(datapoint.value_bool);
|
||||||
call.perform();
|
call.perform();
|
||||||
ESP_LOGD(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this->direction_id_.has_value()) {
|
if (this->direction_id_.has_value()) {
|
||||||
|
@ -66,37 +66,21 @@ void TuyaFan::dump_config() {
|
||||||
|
|
||||||
void TuyaFan::write_state() {
|
void TuyaFan::write_state() {
|
||||||
if (this->switch_id_.has_value()) {
|
if (this->switch_id_.has_value()) {
|
||||||
TuyaDatapoint datapoint{};
|
ESP_LOGV(TAG, "Setting switch: %s", ONOFF(this->fan_->state));
|
||||||
datapoint.id = *this->switch_id_;
|
this->parent_->set_datapoint_value(*this->switch_id_, this->fan_->state);
|
||||||
datapoint.type = TuyaDatapointType::BOOLEAN;
|
|
||||||
datapoint.value_bool = this->fan_->state;
|
|
||||||
this->parent_->set_datapoint_value(datapoint);
|
|
||||||
ESP_LOGD(TAG, "Setting switch: %s", ONOFF(this->fan_->state));
|
|
||||||
}
|
}
|
||||||
if (this->oscillation_id_.has_value()) {
|
if (this->oscillation_id_.has_value()) {
|
||||||
TuyaDatapoint datapoint{};
|
ESP_LOGV(TAG, "Setting oscillating: %s", ONOFF(this->fan_->oscillating));
|
||||||
datapoint.id = *this->oscillation_id_;
|
this->parent_->set_datapoint_value(*this->oscillation_id_, this->fan_->oscillating);
|
||||||
datapoint.type = TuyaDatapointType::BOOLEAN;
|
|
||||||
datapoint.value_bool = this->fan_->oscillating;
|
|
||||||
this->parent_->set_datapoint_value(datapoint);
|
|
||||||
ESP_LOGD(TAG, "Setting oscillating: %s", ONOFF(this->fan_->oscillating));
|
|
||||||
}
|
}
|
||||||
if (this->direction_id_.has_value()) {
|
if (this->direction_id_.has_value()) {
|
||||||
TuyaDatapoint datapoint{};
|
|
||||||
datapoint.id = *this->direction_id_;
|
|
||||||
datapoint.type = TuyaDatapointType::BOOLEAN;
|
|
||||||
bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE;
|
bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE;
|
||||||
datapoint.value_bool = enable;
|
ESP_LOGV(TAG, "Setting reverse direction: %s", ONOFF(enable));
|
||||||
this->parent_->set_datapoint_value(datapoint);
|
this->parent_->set_datapoint_value(*this->direction_id_, enable);
|
||||||
ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable));
|
|
||||||
}
|
}
|
||||||
if (this->speed_id_.has_value()) {
|
if (this->speed_id_.has_value()) {
|
||||||
TuyaDatapoint datapoint{};
|
ESP_LOGV(TAG, "Setting speed: %d", this->fan_->speed);
|
||||||
datapoint.id = *this->speed_id_;
|
this->parent_->set_datapoint_value(*this->speed_id_, this->fan_->speed);
|
||||||
datapoint.type = TuyaDatapointType::ENUM;
|
|
||||||
datapoint.value_enum = this->fan_->speed - 1;
|
|
||||||
ESP_LOGD(TAG, "Setting speed: %d", datapoint.value_enum);
|
|
||||||
this->parent_->set_datapoint_value(datapoint);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,11 +31,7 @@ void TuyaLight::setup() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (min_value_datapoint_id_.has_value()) {
|
if (min_value_datapoint_id_.has_value()) {
|
||||||
TuyaDatapoint datapoint{};
|
parent_->set_datapoint_value(*this->min_value_datapoint_id_, this->min_value_);
|
||||||
datapoint.id = *this->min_value_datapoint_id_;
|
|
||||||
datapoint.type = TuyaDatapointType::INTEGER;
|
|
||||||
datapoint.value_int = this->min_value_;
|
|
||||||
parent_->set_datapoint_value(datapoint);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,49 +63,29 @@ void TuyaLight::write_state(light::LightState *state) {
|
||||||
if (brightness == 0.0f) {
|
if (brightness == 0.0f) {
|
||||||
// turning off, first try via switch (if exists), then dimmer
|
// turning off, first try via switch (if exists), then dimmer
|
||||||
if (switch_id_.has_value()) {
|
if (switch_id_.has_value()) {
|
||||||
TuyaDatapoint datapoint{};
|
parent_->set_datapoint_value(*this->switch_id_, false);
|
||||||
datapoint.id = *this->switch_id_;
|
|
||||||
datapoint.type = TuyaDatapointType::BOOLEAN;
|
|
||||||
datapoint.value_bool = false;
|
|
||||||
|
|
||||||
parent_->set_datapoint_value(datapoint);
|
|
||||||
} else if (dimmer_id_.has_value()) {
|
} else if (dimmer_id_.has_value()) {
|
||||||
TuyaDatapoint datapoint{};
|
parent_->set_datapoint_value(*this->dimmer_id_, 0);
|
||||||
datapoint.id = *this->dimmer_id_;
|
|
||||||
datapoint.type = TuyaDatapointType::INTEGER;
|
|
||||||
datapoint.value_int = 0;
|
|
||||||
parent_->set_datapoint_value(datapoint);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->color_temperature_id_.has_value()) {
|
if (this->color_temperature_id_.has_value()) {
|
||||||
TuyaDatapoint datapoint{};
|
uint32_t color_temp_int =
|
||||||
datapoint.id = *this->color_temperature_id_;
|
|
||||||
datapoint.type = TuyaDatapointType::INTEGER;
|
|
||||||
datapoint.value_int =
|
|
||||||
static_cast<uint32_t>(this->color_temperature_max_value_ *
|
static_cast<uint32_t>(this->color_temperature_max_value_ *
|
||||||
(state->current_values.get_color_temperature() - this->cold_white_temperature_) /
|
(state->current_values.get_color_temperature() - this->cold_white_temperature_) /
|
||||||
(this->warm_white_temperature_ - this->cold_white_temperature_));
|
(this->warm_white_temperature_ - this->cold_white_temperature_));
|
||||||
parent_->set_datapoint_value(datapoint);
|
parent_->set_datapoint_value(*this->color_temperature_id_, color_temp_int);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto brightness_int = static_cast<uint32_t>(brightness * this->max_value_);
|
auto brightness_int = static_cast<uint32_t>(brightness * this->max_value_);
|
||||||
brightness_int = std::max(brightness_int, this->min_value_);
|
brightness_int = std::max(brightness_int, this->min_value_);
|
||||||
|
|
||||||
if (this->dimmer_id_.has_value()) {
|
if (this->dimmer_id_.has_value()) {
|
||||||
TuyaDatapoint datapoint{};
|
parent_->set_datapoint_value(*this->dimmer_id_, brightness_int);
|
||||||
datapoint.id = *this->dimmer_id_;
|
|
||||||
datapoint.type = TuyaDatapointType::INTEGER;
|
|
||||||
datapoint.value_int = brightness_int;
|
|
||||||
parent_->set_datapoint_value(datapoint);
|
|
||||||
}
|
}
|
||||||
if (this->switch_id_.has_value()) {
|
if (this->switch_id_.has_value()) {
|
||||||
TuyaDatapoint datapoint{};
|
parent_->set_datapoint_value(*this->switch_id_, true);
|
||||||
datapoint.id = *this->switch_id_;
|
|
||||||
datapoint.type = TuyaDatapointType::BOOLEAN;
|
|
||||||
datapoint.value_bool = true;
|
|
||||||
parent_->set_datapoint_value(datapoint);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,17 +9,17 @@ static const char *TAG = "tuya.sensor";
|
||||||
void TuyaSensor::setup() {
|
void TuyaSensor::setup() {
|
||||||
this->parent_->register_listener(this->sensor_id_, [this](TuyaDatapoint datapoint) {
|
this->parent_->register_listener(this->sensor_id_, [this](TuyaDatapoint datapoint) {
|
||||||
if (datapoint.type == TuyaDatapointType::BOOLEAN) {
|
if (datapoint.type == TuyaDatapointType::BOOLEAN) {
|
||||||
|
ESP_LOGV(TAG, "MCU reported sensor %u is: %s", datapoint.id, ONOFF(datapoint.value_bool));
|
||||||
this->publish_state(datapoint.value_bool);
|
this->publish_state(datapoint.value_bool);
|
||||||
ESP_LOGD(TAG, "MCU reported sensor is: %s", ONOFF(datapoint.value_bool));
|
|
||||||
} else if (datapoint.type == TuyaDatapointType::INTEGER) {
|
} else if (datapoint.type == TuyaDatapointType::INTEGER) {
|
||||||
|
ESP_LOGV(TAG, "MCU reported sensor %u is: %d", datapoint.id, datapoint.value_int);
|
||||||
this->publish_state(datapoint.value_int);
|
this->publish_state(datapoint.value_int);
|
||||||
ESP_LOGD(TAG, "MCU reported sensor is: %d", datapoint.value_int);
|
|
||||||
} else if (datapoint.type == TuyaDatapointType::ENUM) {
|
} else if (datapoint.type == TuyaDatapointType::ENUM) {
|
||||||
|
ESP_LOGV(TAG, "MCU reported sensor %u is: %u", datapoint.id, datapoint.value_enum);
|
||||||
this->publish_state(datapoint.value_enum);
|
this->publish_state(datapoint.value_enum);
|
||||||
ESP_LOGD(TAG, "MCU reported sensor is: %d", datapoint.value_enum);
|
|
||||||
} else if (datapoint.type == TuyaDatapointType::BITMASK) {
|
} else if (datapoint.type == TuyaDatapointType::BITMASK) {
|
||||||
|
ESP_LOGV(TAG, "MCU reported sensor %u is: %x", datapoint.id, datapoint.value_bitmask);
|
||||||
this->publish_state(datapoint.value_bitmask);
|
this->publish_state(datapoint.value_bitmask);
|
||||||
ESP_LOGD(TAG, "MCU reported sensor is: %x", datapoint.value_bitmask);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,19 +8,14 @@ static const char *TAG = "tuya.switch";
|
||||||
|
|
||||||
void TuyaSwitch::setup() {
|
void TuyaSwitch::setup() {
|
||||||
this->parent_->register_listener(this->switch_id_, [this](TuyaDatapoint datapoint) {
|
this->parent_->register_listener(this->switch_id_, [this](TuyaDatapoint datapoint) {
|
||||||
|
ESP_LOGV(TAG, "MCU reported switch %u is: %s", this->switch_id_, ONOFF(datapoint.value_bool));
|
||||||
this->publish_state(datapoint.value_bool);
|
this->publish_state(datapoint.value_bool);
|
||||||
ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void TuyaSwitch::write_state(bool state) {
|
void TuyaSwitch::write_state(bool state) {
|
||||||
TuyaDatapoint datapoint{};
|
ESP_LOGV(TAG, "Setting switch %u: %s", this->switch_id_, ONOFF(state));
|
||||||
datapoint.id = this->switch_id_;
|
this->parent_->set_datapoint_value(this->switch_id_, state);
|
||||||
datapoint.type = TuyaDatapointType::BOOLEAN;
|
|
||||||
datapoint.value_bool = state;
|
|
||||||
this->parent_->set_datapoint_value(datapoint);
|
|
||||||
ESP_LOGD(TAG, "Setting switch: %s", ONOFF(state));
|
|
||||||
|
|
||||||
this->publish_state(state);
|
this->publish_state(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#include "tuya.h"
|
#include "tuya.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/util.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
@ -9,7 +10,7 @@ static const char *TAG = "tuya";
|
||||||
static const int COMMAND_DELAY = 50;
|
static const int COMMAND_DELAY = 50;
|
||||||
|
|
||||||
void Tuya::setup() {
|
void Tuya::setup() {
|
||||||
this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
|
this->set_interval("heartbeat", 10000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tuya::loop() {
|
void Tuya::loop() {
|
||||||
|
@ -31,17 +32,17 @@ void Tuya::dump_config() {
|
||||||
}
|
}
|
||||||
for (auto &info : this->datapoints_) {
|
for (auto &info : this->datapoints_) {
|
||||||
if (info.type == TuyaDatapointType::BOOLEAN)
|
if (info.type == TuyaDatapointType::BOOLEAN)
|
||||||
ESP_LOGCONFIG(TAG, " Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool));
|
ESP_LOGCONFIG(TAG, " Datapoint %u: switch (value: %s)", info.id, ONOFF(info.value_bool));
|
||||||
else if (info.type == TuyaDatapointType::INTEGER)
|
else if (info.type == TuyaDatapointType::INTEGER)
|
||||||
ESP_LOGCONFIG(TAG, " Datapoint %d: int value (value: %d)", info.id, info.value_int);
|
ESP_LOGCONFIG(TAG, " Datapoint %u: int value (value: %d)", info.id, info.value_int);
|
||||||
else if (info.type == TuyaDatapointType::STRING)
|
else if (info.type == TuyaDatapointType::STRING)
|
||||||
ESP_LOGCONFIG(TAG, " Datapoint %d: string value (value: %s)", info.id, info.value_string.c_str());
|
ESP_LOGCONFIG(TAG, " Datapoint %u: string value (value: %s)", info.id, info.value_string.c_str());
|
||||||
else if (info.type == TuyaDatapointType::ENUM)
|
else if (info.type == TuyaDatapointType::ENUM)
|
||||||
ESP_LOGCONFIG(TAG, " Datapoint %d: enum (value: %d)", info.id, info.value_enum);
|
ESP_LOGCONFIG(TAG, " Datapoint %u: enum (value: %d)", info.id, info.value_enum);
|
||||||
else if (info.type == TuyaDatapointType::BITMASK)
|
else if (info.type == TuyaDatapointType::BITMASK)
|
||||||
ESP_LOGCONFIG(TAG, " Datapoint %d: bitmask (value: %x)", info.id, info.value_bitmask);
|
ESP_LOGCONFIG(TAG, " Datapoint %u: bitmask (value: %x)", info.id, info.value_bitmask);
|
||||||
else
|
else
|
||||||
ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id);
|
ESP_LOGCONFIG(TAG, " Datapoint %u: unknown", info.id);
|
||||||
}
|
}
|
||||||
if ((this->gpio_status_ != -1) || (this->gpio_reset_ != -1)) {
|
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_,
|
ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d (not supported)", this->gpio_status_,
|
||||||
|
@ -98,8 +99,8 @@ bool Tuya::validate_message_() {
|
||||||
|
|
||||||
// valid message
|
// valid message
|
||||||
const uint8_t *message_data = data + 6;
|
const uint8_t *message_data = data + 6;
|
||||||
ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version, // NOLINT
|
ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command, version,
|
||||||
hexencode(message_data, length).c_str(), this->init_state_);
|
hexencode(message_data, length).c_str(), static_cast<uint8_t>(this->init_state_));
|
||||||
this->handle_command_(command, version, message_data, length);
|
this->handle_command_(command, version, message_data, length);
|
||||||
|
|
||||||
// return false to reset rx buffer
|
// return false to reset rx buffer
|
||||||
|
@ -117,6 +118,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
|
||||||
switch ((TuyaCommandType) command) {
|
switch ((TuyaCommandType) command) {
|
||||||
case TuyaCommandType::HEARTBEAT:
|
case TuyaCommandType::HEARTBEAT:
|
||||||
ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]);
|
ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]);
|
||||||
|
this->protocol_version_ = version;
|
||||||
if (buffer[0] == 0) {
|
if (buffer[0] == 0) {
|
||||||
ESP_LOGI(TAG, "MCU restarted");
|
ESP_LOGI(TAG, "MCU restarted");
|
||||||
this->init_state_ = TuyaInitState::INIT_HEARTBEAT;
|
this->init_state_ = TuyaInitState::INIT_HEARTBEAT;
|
||||||
|
@ -148,8 +150,8 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
|
||||||
}
|
}
|
||||||
case TuyaCommandType::CONF_QUERY: {
|
case TuyaCommandType::CONF_QUERY: {
|
||||||
if (len >= 2) {
|
if (len >= 2) {
|
||||||
gpio_status_ = buffer[0];
|
this->gpio_status_ = buffer[0];
|
||||||
gpio_reset_ = buffer[1];
|
this->gpio_reset_ = buffer[1];
|
||||||
}
|
}
|
||||||
if (this->init_state_ == TuyaInitState::INIT_CONF) {
|
if (this->init_state_ == TuyaInitState::INIT_CONF) {
|
||||||
// If mcu returned status gpio, then we can ommit sending wifi state
|
// If mcu returned status gpio, then we can ommit sending wifi state
|
||||||
|
@ -158,10 +160,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
|
||||||
this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY);
|
this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY);
|
||||||
} else {
|
} else {
|
||||||
this->init_state_ = TuyaInitState::INIT_WIFI;
|
this->init_state_ = TuyaInitState::INIT_WIFI;
|
||||||
// If we were following the spec to the letter we would send
|
this->set_interval("wifi", 1000, [this] { this->send_wifi_status_(); });
|
||||||
// state updates until connected to both WiFi and API/MQTT.
|
|
||||||
// Instead we just claim to be connected immediately and move on.
|
|
||||||
this->send_command_(TuyaCommand{.cmd = TuyaCommandType::WIFI_STATE, .payload = std::vector<uint8_t>{0x04}});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -173,10 +172,10 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TuyaCommandType::WIFI_RESET:
|
case TuyaCommandType::WIFI_RESET:
|
||||||
ESP_LOGE(TAG, "TUYA_CMD_WIFI_RESET is not handled");
|
ESP_LOGE(TAG, "WIFI_RESET is not handled");
|
||||||
break;
|
break;
|
||||||
case TuyaCommandType::WIFI_SELECT:
|
case TuyaCommandType::WIFI_SELECT:
|
||||||
ESP_LOGE(TAG, "TUYA_CMD_WIFI_SELECT is not handled");
|
ESP_LOGE(TAG, "WIFI_SELECT is not handled");
|
||||||
break;
|
break;
|
||||||
case TuyaCommandType::DATAPOINT_DELIVER:
|
case TuyaCommandType::DATAPOINT_DELIVER:
|
||||||
break;
|
break;
|
||||||
|
@ -189,48 +188,24 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
|
||||||
break;
|
break;
|
||||||
case TuyaCommandType::DATAPOINT_QUERY:
|
case TuyaCommandType::DATAPOINT_QUERY:
|
||||||
break;
|
break;
|
||||||
case TuyaCommandType::WIFI_TEST: {
|
case TuyaCommandType::WIFI_TEST:
|
||||||
this->send_command_(TuyaCommand{.cmd = TuyaCommandType::WIFI_TEST, .payload = std::vector<uint8_t>{0x00, 0x00}});
|
this->send_command_(TuyaCommand{.cmd = TuyaCommandType::WIFI_TEST, .payload = std::vector<uint8_t>{0x00, 0x00}});
|
||||||
break;
|
break;
|
||||||
}
|
case TuyaCommandType::LOCAL_TIME_QUERY:
|
||||||
case TuyaCommandType::LOCAL_TIME_QUERY: {
|
|
||||||
#ifdef USE_TIME
|
#ifdef USE_TIME
|
||||||
if (this->time_id_.has_value()) {
|
if (this->time_id_.has_value()) {
|
||||||
|
this->send_local_time_();
|
||||||
auto time_id = *this->time_id_;
|
auto time_id = *this->time_id_;
|
||||||
auto now = time_id->now();
|
time_id->add_on_time_sync_callback([this] { this->send_local_time_(); });
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
this->send_command_(TuyaCommand{
|
|
||||||
.cmd = TuyaCommandType::LOCAL_TIME_QUERY,
|
|
||||||
.payload = std::vector<uint8_t>{0x01, year, month, day_of_month, hour, minute, second, day_of_week}});
|
|
||||||
} else {
|
|
||||||
ESP_LOGW(TAG, "TUYA_CMD_LOCAL_TIME_QUERY is not handled because time is not valid");
|
|
||||||
// By spec we need to notify MCU that the time was not obtained
|
|
||||||
this->send_command_(
|
|
||||||
TuyaCommand{.cmd = TuyaCommandType::LOCAL_TIME_QUERY,
|
|
||||||
.payload = std::vector<uint8_t>{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGW(TAG, "TUYA_CMD_LOCAL_TIME_QUERY is not handled because time is not configured");
|
ESP_LOGW(TAG, "LOCAL_TIME_QUERY is not handled because time is not configured");
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
ESP_LOGE(TAG, "LOCAL_TIME_QUERY is not handled");
|
ESP_LOGE(TAG, "LOCAL_TIME_QUERY is not handled");
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
ESP_LOGE(TAG, "invalid command (%02x) received", command);
|
ESP_LOGE(TAG, "Invalid command (0x%02X) received", command);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,8 +218,8 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) {
|
||||||
datapoint.type = (TuyaDatapointType) buffer[1];
|
datapoint.type = (TuyaDatapointType) buffer[1];
|
||||||
datapoint.value_uint = 0;
|
datapoint.value_uint = 0;
|
||||||
|
|
||||||
// drop update if datapoint is in ignore_mcu_datapoint_update list
|
// Drop update if datapoint is in ignore_mcu_datapoint_update list
|
||||||
for (auto i : this->ignore_mcu_update_on_datapoints_) {
|
for (uint8_t i : this->ignore_mcu_update_on_datapoints_) {
|
||||||
if (datapoint.id == i) {
|
if (datapoint.id == i) {
|
||||||
ESP_LOGV(TAG, "Datapoint %u found in ignore_mcu_update_on_datapoints list, dropping MCU update", datapoint.id);
|
ESP_LOGV(TAG, "Datapoint %u found in ignore_mcu_update_on_datapoints list, dropping MCU update", datapoint.id);
|
||||||
return;
|
return;
|
||||||
|
@ -255,38 +230,57 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) {
|
||||||
const uint8_t *data = buffer + 4;
|
const uint8_t *data = buffer + 4;
|
||||||
size_t data_len = len - 4;
|
size_t data_len = len - 4;
|
||||||
if (data_size != data_len) {
|
if (data_size != data_len) {
|
||||||
ESP_LOGW(TAG, "invalid datapoint update");
|
ESP_LOGW(TAG, "Datapoint %u is not expected size", datapoint.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
datapoint.len = data_len;
|
||||||
|
|
||||||
switch (datapoint.type) {
|
switch (datapoint.type) {
|
||||||
case TuyaDatapointType::BOOLEAN:
|
case TuyaDatapointType::BOOLEAN:
|
||||||
if (data_len != 1)
|
if (data_len != 1) {
|
||||||
|
ESP_LOGW(TAG, "Datapoint %u has bad boolean len %zu", datapoint.id, data_len);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
datapoint.value_bool = data[0];
|
datapoint.value_bool = data[0];
|
||||||
break;
|
break;
|
||||||
case TuyaDatapointType::INTEGER:
|
case TuyaDatapointType::INTEGER:
|
||||||
if (data_len != 4)
|
if (data_len != 4) {
|
||||||
|
ESP_LOGW(TAG, "Datapoint %u has bad integer len %zu", datapoint.id, data_len);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
datapoint.value_uint = encode_uint32(data[0], data[1], data[2], data[3]);
|
datapoint.value_uint = encode_uint32(data[0], data[1], data[2], data[3]);
|
||||||
break;
|
break;
|
||||||
case TuyaDatapointType::STRING:
|
case TuyaDatapointType::STRING:
|
||||||
datapoint.value_string = std::string(reinterpret_cast<const char *>(data), data_len);
|
datapoint.value_string = std::string(reinterpret_cast<const char *>(data), data_len);
|
||||||
break;
|
break;
|
||||||
case TuyaDatapointType::ENUM:
|
case TuyaDatapointType::ENUM:
|
||||||
if (data_len != 1)
|
if (data_len != 1) {
|
||||||
|
ESP_LOGW(TAG, "Datapoint %u has bad enum len %zu", datapoint.id, data_len);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
datapoint.value_enum = data[0];
|
datapoint.value_enum = data[0];
|
||||||
break;
|
break;
|
||||||
case TuyaDatapointType::BITMASK:
|
case TuyaDatapointType::BITMASK:
|
||||||
if (data_len != 2)
|
switch (data_len) {
|
||||||
return;
|
case 1:
|
||||||
datapoint.value_bitmask = (uint16_t(data[0]) << 8) | (uint16_t(data[1]) << 0);
|
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;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
ESP_LOGW(TAG, "Datapoint %u has unknown type 0x%02hhX", datapoint.id, datapoint.type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ESP_LOGV(TAG, "Datapoint %u update to %u", datapoint.id, datapoint.value_uint);
|
ESP_LOGD(TAG, "Datapoint %u update to %u", datapoint.id, datapoint.value_uint);
|
||||||
|
|
||||||
// Update internal datapoints
|
// Update internal datapoints
|
||||||
bool found = false;
|
bool found = false;
|
||||||
|
@ -313,8 +307,8 @@ void Tuya::send_raw_command_(TuyaCommand command) {
|
||||||
|
|
||||||
this->last_command_timestamp_ = millis();
|
this->last_command_timestamp_ = millis();
|
||||||
|
|
||||||
ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", command.cmd, version, // NOLINT
|
ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", static_cast<uint8_t>(command.cmd),
|
||||||
hexencode(command.payload).c_str(), this->init_state_);
|
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});
|
this->write_array({0x55, 0xAA, version, (uint8_t) command.cmd, len_hi, len_lo});
|
||||||
if (!command.payload.empty())
|
if (!command.payload.empty())
|
||||||
|
@ -344,53 +338,113 @@ void Tuya::send_empty_command_(TuyaCommandType command) {
|
||||||
send_command_(TuyaCommand{.cmd = command, .payload = std::vector<uint8_t>{0x04}});
|
send_command_(TuyaCommand{.cmd = command, .payload = std::vector<uint8_t>{0x04}});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Tuya::set_datapoint_value(TuyaDatapoint datapoint) {
|
void Tuya::send_wifi_status_() {
|
||||||
std::vector<uint8_t> buffer;
|
uint8_t status = 0x02;
|
||||||
ESP_LOGV(TAG, "Datapoint %u set to %u", datapoint.id, datapoint.value_uint);
|
if (network_is_connected()) {
|
||||||
for (auto &other : this->datapoints_) {
|
status = 0x03;
|
||||||
if (other.id == datapoint.id) {
|
|
||||||
// String value is stored outside the union; must be checked separately.
|
// Protocol version 3 also supports specifying when connected to "the cloud"
|
||||||
if (datapoint.type == TuyaDatapointType::STRING) {
|
if (this->protocol_version_ >= 0x03) {
|
||||||
if (other.value_string == datapoint.value_string) {
|
if (remote_is_connected()) {
|
||||||
ESP_LOGV(TAG, "Not sending unchanged value");
|
status = 0x04;
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (other.value_uint == datapoint.value_uint) {
|
|
||||||
ESP_LOGV(TAG, "Not sending unchanged value");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buffer.push_back(datapoint.id);
|
|
||||||
buffer.push_back(static_cast<uint8_t>(datapoint.type));
|
|
||||||
|
|
||||||
std::vector<uint8_t> data;
|
if (status == this->wifi_status_) {
|
||||||
switch (datapoint.type) {
|
return;
|
||||||
case TuyaDatapointType::BOOLEAN:
|
|
||||||
data.push_back(datapoint.value_bool);
|
|
||||||
break;
|
|
||||||
case TuyaDatapointType::INTEGER:
|
|
||||||
data.push_back(datapoint.value_uint >> 24);
|
|
||||||
data.push_back(datapoint.value_uint >> 16);
|
|
||||||
data.push_back(datapoint.value_uint >> 8);
|
|
||||||
data.push_back(datapoint.value_uint >> 0);
|
|
||||||
break;
|
|
||||||
case TuyaDatapointType::STRING:
|
|
||||||
for (char const &c : datapoint.value_string) {
|
|
||||||
data.push_back(c);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case TuyaDatapointType::ENUM:
|
|
||||||
data.push_back(datapoint.value_enum);
|
|
||||||
break;
|
|
||||||
case TuyaDatapointType::BITMASK:
|
|
||||||
data.push_back(datapoint.value_bitmask >> 8);
|
|
||||||
data.push_back(datapoint.value_bitmask >> 0);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() >> 8);
|
||||||
buffer.push_back(data.size() >> 0);
|
buffer.push_back(data.size() >> 0);
|
||||||
buffer.insert(buffer.end(), data.begin(), data.end());
|
buffer.insert(buffer.end(), data.begin(), data.end());
|
||||||
|
|
|
@ -23,12 +23,13 @@ enum class TuyaDatapointType : uint8_t {
|
||||||
struct TuyaDatapoint {
|
struct TuyaDatapoint {
|
||||||
uint8_t id;
|
uint8_t id;
|
||||||
TuyaDatapointType type;
|
TuyaDatapointType type;
|
||||||
|
size_t len;
|
||||||
union {
|
union {
|
||||||
bool value_bool;
|
bool value_bool;
|
||||||
int value_int;
|
int value_int;
|
||||||
uint32_t value_uint;
|
uint32_t value_uint;
|
||||||
uint8_t value_enum;
|
uint8_t value_enum;
|
||||||
uint16_t value_bitmask;
|
uint32_t value_bitmask;
|
||||||
};
|
};
|
||||||
std::string value_string;
|
std::string value_string;
|
||||||
};
|
};
|
||||||
|
@ -73,7 +74,8 @@ class Tuya : public Component, public uart::UARTDevice {
|
||||||
void loop() override;
|
void loop() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
void register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func);
|
void register_listener(uint8_t datapoint_id, const std::function<void(TuyaDatapoint)> &func);
|
||||||
void set_datapoint_value(TuyaDatapoint datapoint);
|
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
|
#ifdef USE_TIME
|
||||||
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
|
void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; }
|
||||||
#endif
|
#endif
|
||||||
|
@ -84,6 +86,7 @@ class Tuya : public Component, public uart::UARTDevice {
|
||||||
protected:
|
protected:
|
||||||
void handle_char_(uint8_t c);
|
void handle_char_(uint8_t c);
|
||||||
void handle_datapoint_(const uint8_t *buffer, size_t len);
|
void handle_datapoint_(const uint8_t *buffer, size_t len);
|
||||||
|
optional<TuyaDatapoint> get_datapoint_(uint8_t datapoint_id);
|
||||||
bool validate_message_();
|
bool validate_message_();
|
||||||
|
|
||||||
void handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len);
|
void handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len);
|
||||||
|
@ -91,11 +94,15 @@ class Tuya : public Component, public uart::UARTDevice {
|
||||||
void process_command_queue_();
|
void process_command_queue_();
|
||||||
void send_command_(TuyaCommand command);
|
void send_command_(TuyaCommand command);
|
||||||
void send_empty_command_(TuyaCommandType 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
|
#ifdef USE_TIME
|
||||||
|
void send_local_time_();
|
||||||
optional<time::RealTimeClock *> time_id_{};
|
optional<time::RealTimeClock *> time_id_{};
|
||||||
#endif
|
#endif
|
||||||
TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT;
|
TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT;
|
||||||
|
uint8_t protocol_version_ = -1;
|
||||||
int gpio_status_ = -1;
|
int gpio_status_ = -1;
|
||||||
int gpio_reset_ = -1;
|
int gpio_reset_ = -1;
|
||||||
uint32_t last_command_timestamp_ = 0;
|
uint32_t last_command_timestamp_ = 0;
|
||||||
|
@ -105,6 +112,7 @@ class Tuya : public Component, public uart::UARTDevice {
|
||||||
std::vector<uint8_t> rx_message_;
|
std::vector<uint8_t> rx_message_;
|
||||||
std::vector<uint8_t> ignore_mcu_update_on_datapoints_{};
|
std::vector<uint8_t> ignore_mcu_update_on_datapoints_{};
|
||||||
std::vector<TuyaCommand> command_queue_;
|
std::vector<TuyaCommand> command_queue_;
|
||||||
|
uint8_t wifi_status_ = -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace tuya
|
} // namespace tuya
|
||||||
|
|
|
@ -16,6 +16,10 @@
|
||||||
#include "esphome/components/ethernet/ethernet_component.h"
|
#include "esphome/components/ethernet/ethernet_component.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_MQTT
|
||||||
|
#include "esphome/components/mqtt/mqtt_client.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef USE_MDNS
|
#ifdef USE_MDNS
|
||||||
#ifdef ARDUINO_ARCH_ESP32
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
#include <ESPmDNS.h>
|
#include <ESPmDNS.h>
|
||||||
|
@ -41,6 +45,26 @@ bool network_is_connected() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool api_is_connected() {
|
||||||
|
#ifdef USE_API
|
||||||
|
if (api::global_api_server != nullptr) {
|
||||||
|
return api::global_api_server->is_connected();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool mqtt_is_connected() {
|
||||||
|
#ifdef USE_MQTT
|
||||||
|
if (mqtt::global_mqtt_client != nullptr) {
|
||||||
|
return mqtt::global_mqtt_client->is_connected();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool remote_is_connected() { return api_is_connected() || mqtt_is_connected(); }
|
||||||
|
|
||||||
#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS)
|
#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS)
|
||||||
bool mdns_setup;
|
bool mdns_setup;
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -15,6 +15,15 @@ bool network_is_connected();
|
||||||
/// Get the active network hostname
|
/// Get the active network hostname
|
||||||
std::string network_get_address();
|
std::string network_get_address();
|
||||||
|
|
||||||
|
/// Return whether the node has at least one client connected to the native API
|
||||||
|
bool api_is_connected();
|
||||||
|
|
||||||
|
/// Return whether the node has an active connection to an MQTT broker
|
||||||
|
bool mqtt_is_connected();
|
||||||
|
|
||||||
|
/// Return whether the node has any form of "remote" connection via the API or to an MQTT broker
|
||||||
|
bool remote_is_connected();
|
||||||
|
|
||||||
/// Manually set up the network stack (outside of the App.setup() loop, for example in OTA safe mode)
|
/// Manually set up the network stack (outside of the App.setup() loop, for example in OTA safe mode)
|
||||||
#ifdef ARDUINO_ARCH_ESP8266
|
#ifdef ARDUINO_ARCH_ESP8266
|
||||||
void network_setup_mdns(IPAddress address, int interface);
|
void network_setup_mdns(IPAddress address, int interface);
|
||||||
|
|
Loading…
Reference in a new issue