#include "esphome.h" using namespace esphome; #define DOUBLE_TAP_TIMEOUT 300 static const char* TAG = "NuttyTuyaLight"; class TuyaLightPlus : public Component, public light::LightOutput, public api::CustomAPIDevice { public: void setup() override; void set_dimmer_id(uint8_t dimmer_id) { this->dimmer_id_ = dimmer_id; } void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } void set_tuya_parent(tuya::Tuya *parent) { this->parent_ = parent; } void set_min_value(uint32_t min_value) { this->min_value_ = min_value; } void set_max_value(uint32_t max_value) { this->max_value_ = max_value; } light::LightTraits get_traits() override; void setup_state(light::LightState *state) override; void write_state(light::LightState *state) override; void loop() override; void set_day_default_brightness(float brightness) { this->day_default_brightness_ = brightness; } void set_night_default_brightness(float brightness) { this->night_default_brightness_ = brightness; } void set_day_auto_off_minutes(uint32_t minutes) { this->day_auto_off_time_ = minutes * 60 * 1000; } void set_night_auto_off_minutes(uint32_t minutes) { this->night_auto_off_time_ = minutes * 60 * 1000; } void set_api_server(api::APIServer *api_server) { this->api_server_ = api_server; }; void set_day_night_sensor(const std::string day_night_sensor); void set_linked_lights(const std::string linked_lights); void add_double_tap_while_on_callback(const std::function &func); void add_double_tap_while_off_callback(const std::function &func); void set_double_tap_while_off_stays_on(bool stays_on) { this->double_tap_while_off_stays_on_ = stays_on; } void add_double_tap_callback(const std::function &func); void set_default_brightness(float brightness); void set_auto_off_minutes(int minutes) { this->current_auto_off_time_ = minutes * 60 * 1000; } protected: float tuya_level_to_brightness(uint32_t level) { return static_cast(level) / static_cast(this->max_value_); } uint32_t brightness_to_tuya_level(float brightness) { return static_cast(brightness * this->max_value_); } float brightness_pct() { return static_cast(this->state_->current_values.get_brightness() * 100); } void on_day_night_changed(std::string state); void handle_tuya_datapoint(tuya::TuyaDatapoint datapoint); void set_tuya_state(bool state); void set_tuya_level(uint32_t level); void update_linked_lights(); void set_default_auto_off_time(int time) { this->default_auto_off_time_ = time; this->current_auto_off_time_ = time; } tuya::Tuya *parent_; optional dimmer_id_{}; optional switch_id_{}; uint32_t min_value_ = 0; uint32_t max_value_ = 255; light::LightState *state_{nullptr}; float day_default_brightness_ = 1; float night_default_brightness_ = .03; float default_brightness_ = 1; uint32_t day_auto_off_time_ = 0; uint32_t night_auto_off_time_ = 0; uint32_t default_auto_off_time_ = 0; uint32_t current_auto_off_time_ = 0; bool has_linked_lights_; api::APIServer *api_server_; api::HomeAssistantServiceCallAction<> *linked_lights_turn_on_action_; api::HomeAssistantServiceCallAction<> *linked_lights_turn_off_action_; bool has_double_tap_while_on_ = false; std::vector> double_tap_while_on_callbacks_; bool has_double_tap_while_off_ = false; std::vector> double_tap_while_off_callbacks_; bool double_tap_while_off_stays_on_ = true; bool tuya_state_; unsigned long state_changed_at_ = 0; unsigned long double_tap_while_on_timeout_ = 0; bool was_double_tapped_while_on_; unsigned long double_tap_while_off_timeout_ = 0; bool was_double_tapped_while_off_; }; void TuyaLightPlus::setup() { this->parent_->register_listener(*this->switch_id_, [this](tuya::TuyaDatapoint datapoint) { this->handle_tuya_datapoint(datapoint); }); this->parent_->register_listener(*this->dimmer_id_, [this](tuya::TuyaDatapoint datapoint) { this->handle_tuya_datapoint(datapoint); }); this->register_service(&TuyaLightPlus::set_auto_off_minutes, "set_auto_off_minutes", {"minutes"}); this->register_service(&TuyaLightPlus::set_default_brightness, "set_default_brightness", {"brightness"}); } light::LightTraits TuyaLightPlus::get_traits() { auto traits = light::LightTraits(); traits.set_supports_brightness(this->dimmer_id_.has_value()); return traits; } void TuyaLightPlus::setup_state(light::LightState *state) { this->state_ = state; this->tuya_state_ = this->state_->current_values.is_on(); } void TuyaLightPlus::write_state(light::LightState *state) { float brightness; state->current_values_as_brightness(&brightness); if (brightness == 0.0f) { this->set_tuya_state(false); } else if (this->tuya_state_) { // The light is already on so just set the brightness this->set_tuya_level(this->brightness_to_tuya_level(brightness)); } else { // If the light is currently off only turn it on, we will set the brightness in the datapoint handler this->set_tuya_state(true); } } void TuyaLightPlus::loop() { // Double tap while on timed out if (this->double_tap_while_on_timeout_ != 0 && millis() > this->double_tap_while_on_timeout_) { this->double_tap_while_on_timeout_ = 0; } // Handle double tap while on callbacks if (this->was_double_tapped_while_on_) { this->was_double_tapped_while_on_ = false; for (auto &callback : this->double_tap_while_on_callbacks_) { callback(); } } // Double tap while off timed out, turn the light on if (this->double_tap_while_off_timeout_ != 0 && millis() > this->double_tap_while_off_timeout_) { this->double_tap_while_off_timeout_ = 0; this->set_tuya_state(true); } // Handle double tap while off callbacks if (this->was_double_tapped_while_off_) { this->was_double_tapped_while_off_ = false; for (auto &callback : this->double_tap_while_off_callbacks_) { callback(); } } // Handle auto turn off if (this->current_auto_off_time_ != 0 && this->tuya_state_ && millis() >= this->state_changed_at_ + this->current_auto_off_time_) { this->set_tuya_state(false); } } void TuyaLightPlus::set_day_night_sensor(const std::string day_night_sensor) { if (day_night_sensor != "") { this->subscribe_homeassistant_state(&TuyaLightPlus::on_day_night_changed, day_night_sensor); } } void TuyaLightPlus::set_linked_lights(const std::string linked_lights) { if (linked_lights != "") { this->has_linked_lights_ = true; this->linked_lights_turn_on_action_ = new api::HomeAssistantServiceCallAction<>(this->api_server_, false); this->linked_lights_turn_on_action_->set_service("light.turn_on"); this->linked_lights_turn_on_action_->add_data("entity_id", linked_lights); this->linked_lights_turn_on_action_->add_variable("brightness_pct", [=]() { return this->brightness_pct(); }); this->linked_lights_turn_on_action_->add_data_template("brightness_pct", "{{ brightness_pct }}"); this->linked_lights_turn_off_action_ = new api::HomeAssistantServiceCallAction<>(this->api_server_, false); this->linked_lights_turn_off_action_->set_service("light.turn_off"); this->linked_lights_turn_off_action_->add_data("entity_id", linked_lights); } } void TuyaLightPlus::add_double_tap_while_on_callback(const std::function &func) { this->has_double_tap_while_on_ = true; this->double_tap_while_on_callbacks_.push_back(func); } void TuyaLightPlus::add_double_tap_while_off_callback(const std::function &func) { this->has_double_tap_while_off_ = true; this->double_tap_while_off_callbacks_.push_back(func); } void TuyaLightPlus::add_double_tap_callback(const std::function &func) { this->add_double_tap_while_off_callback(func); this->add_double_tap_while_on_callback(func); } void TuyaLightPlus::set_default_brightness(float brightness) { this->default_brightness_ = brightness > 1 ? 1 : brightness; // If the light is off update the brightness in the state and publish so regardless of how the light is turned on the brightness will be the default if (!this->tuya_state_) { this->state_->current_values.set_brightness(this->default_brightness_); this->state_->remote_values = this->state_->current_values; this->state_->publish_state(); } } void TuyaLightPlus::on_day_night_changed(std::string state) { if (state == "Day") { this->set_default_brightness(day_default_brightness_); this->set_default_auto_off_time(day_auto_off_time_); } else if (state == "Night") { this->set_default_brightness(night_default_brightness_); this->set_default_auto_off_time(night_auto_off_time_); } } void TuyaLightPlus::handle_tuya_datapoint(tuya::TuyaDatapoint datapoint) { ESP_LOGD(TAG, "Received Datapoint:"); if (datapoint.id == *this->switch_id_) { ESP_LOGD(TAG, " Type: Switch"); // Light turned on if (datapoint.value_bool) { ESP_LOGD(TAG, " State: On"); // Turned on with the physical button if (!this->tuya_state_) { ESP_LOGD(TAG, "Turned on at device"); if (this->has_double_tap_while_on_) { // We are in a double tap while on timeout period so this is a double tap if (this->double_tap_while_on_timeout_ != 0) { // Double tap while on always stays off otherwise it results in weird flashing behavior this->set_tuya_state(false); this->double_tap_while_on_timeout_ = 0; this->was_double_tapped_while_on_ = true; return; } } if (this->has_double_tap_while_off_) { // We are not in a double tap while off timeout period if (this->double_tap_while_off_timeout_ == 0) { // Turn the light back off and wait to see if we get a double tap this->set_tuya_state(false); this->double_tap_while_off_timeout_ = millis() + DOUBLE_TAP_TIMEOUT; return; } // We are in a double tap while off timeout period so this is a double tap else { this->double_tap_while_off_timeout_ = 0; this->was_double_tapped_while_off_ = true; // If double tap while off triggers an event but does not turn on the light then turn it back off if (!this->double_tap_while_off_stays_on_) { this->set_tuya_state(false); return; } } } // We got through all the double tap logic and the light is still on so update the Tuya state ESP_LOGD(TAG, "Updating Tuya state to on"); this->tuya_state_ = true; } else { // Set the brightness to the correct level (it currently is at 0) this->set_tuya_level(this->brightness_to_tuya_level(this->state_->current_values.get_brightness())); } } // Light turned off else { ESP_LOGD(TAG, " State: Off"); // Turned off with physical button if (this->tuya_state_) { ESP_LOGD(TAG, "Turned off at device"); if (has_double_tap_while_on_) { // Start the double tap while on timeout this->double_tap_while_on_timeout_ = millis() + DOUBLE_TAP_TIMEOUT; } // Update the Tuya state ESP_LOGD(TAG, "Updating Tuya state to off"); this->tuya_state_ = false; } // Set the Tuya level to 0 to prevent flashes during double taps ESP_LOGD(TAG, "Updating Tuya level to 0"); this->set_tuya_level(0); // Set the current brightness to the default so that it will turn on at the default brightness ESP_LOGD(TAG, "Updating brightness state to default"); this->state_->current_values.set_brightness(this->default_brightness_); } // Update the current values state ESP_LOGD(TAG, "Updating state to new value"); this->state_->current_values.set_state(this->tuya_state_); } else if (datapoint.id == *this->dimmer_id_) { ESP_LOGD(TAG, " Type: Brightness"); ESP_LOGD(TAG, " Value: %u", datapoint.value_uint); // Only react to dimmer level changes if the light is on if(this->tuya_state_) { this->state_->current_values.set_brightness(tuya_level_to_brightness(datapoint.value_uint)); } } // Update state changed at time this->state_changed_at_ = millis(); // If the remote values do not reflect the current values update and publish the values if (this->state_->current_values.get_state() != this->state_->remote_values.get_state() || this->state_->current_values.get_brightness() != this->state_->remote_values.get_brightness()) { ESP_LOGD(TAG, "Publishing new state"); this->state_->remote_values = this->state_->current_values; this->state_->publish_state(); } // Update any linked lights ESP_LOGD(TAG, "Updating linked lights"); this->update_linked_lights(); } void TuyaLightPlus::set_tuya_state(bool state) { this->tuya_state_ = state; this->parent_->set_datapoint_value(*this->switch_id_, state); } void TuyaLightPlus::set_tuya_level(uint32_t level) { this->parent_->set_datapoint_value(*this->dimmer_id_, std::max(level, this->min_value_)); } void TuyaLightPlus::update_linked_lights() { if (this->has_linked_lights_) { if (this->state_->current_values.is_on()) { this->linked_lights_turn_on_action_->play(); } else { this->linked_lights_turn_off_action_->play(); } } } TuyaLightPlus *TuyaLight;