From fdc163a30d764045ce16d0a5f7cbb88f29bbeaf3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Oct 2026 15:42:43 -1000 Subject: [PATCH] [tuya] Build on_datapoint_update with build_trigger_callback (#20453) --- esphome/components/tuya/__init__.py | 85 +++++++------------------- esphome/components/tuya/automation.cpp | 65 -------------------- esphome/components/tuya/automation.h | 53 ---------------- esphome/components/tuya/tuya.cpp | 19 ++++-- esphome/components/tuya/tuya.h | 10 ++- tests/components/tuya/common.yaml | 45 +++++++++++++- 6 files changed, 87 insertions(+), 190 deletions(-) delete mode 100644 esphome/components/tuya/automation.cpp delete mode 100644 esphome/components/tuya/automation.h diff --git a/esphome/components/tuya/__init__.py b/esphome/components/tuya/__init__.py index 6957b57e0e..bf392c7826 100644 --- a/esphome/components/tuya/__init__.py +++ b/esphome/components/tuya/__init__.py @@ -2,7 +2,8 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import time, uart import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT, CONF_TIME_ID, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT, CONF_TIME_ID +from esphome.cpp_generator import MockObj DEPENDENCIES = ["uart"] DOMAIN = "tuya" @@ -17,6 +18,8 @@ tuya_ns = cg.esphome_ns.namespace("tuya") TuyaDatapointType = tuya_ns.enum("TuyaDatapointType", is_class=True) Tuya = tuya_ns.class_("Tuya", cg.Component, uart.UARTDevice) +TuyaDatapoint = tuya_ns.struct("TuyaDatapoint") + DPTYPE_ANY = "any" DPTYPE_RAW = "raw" DPTYPE_BOOL = "bool" @@ -26,60 +29,18 @@ DPTYPE_STRING = "string" DPTYPE_ENUM = "enum" DPTYPE_BITMASK = "bitmask" +# Automation argument type, plus the expected TuyaDatapointType and the field forwarded DATAPOINT_TYPES = { - DPTYPE_ANY: tuya_ns.struct("TuyaDatapoint"), - DPTYPE_RAW: cg.std_vector.template(cg.uint8), - DPTYPE_BOOL: cg.bool_, - DPTYPE_INT: cg.int_, - DPTYPE_UINT: cg.uint32, - DPTYPE_STRING: cg.std_string, - DPTYPE_ENUM: cg.uint8, - DPTYPE_BITMASK: cg.uint32, + DPTYPE_ANY: (TuyaDatapoint, None, None), + DPTYPE_RAW: (cg.std_vector.template(cg.uint8), TuyaDatapointType.RAW, "value_raw"), + DPTYPE_BOOL: (cg.bool_, TuyaDatapointType.BOOLEAN, "value_bool"), + DPTYPE_INT: (cg.int_, TuyaDatapointType.INTEGER, "value_int"), + DPTYPE_UINT: (cg.uint32, TuyaDatapointType.INTEGER, "value_uint"), + DPTYPE_STRING: (cg.std_string, TuyaDatapointType.STRING, "value_string"), + DPTYPE_ENUM: (cg.uint8, TuyaDatapointType.ENUM, "value_enum"), + DPTYPE_BITMASK: (cg.uint32, TuyaDatapointType.BITMASK, "value_bitmask"), } -DATAPOINT_TRIGGERS = { - DPTYPE_ANY: tuya_ns.class_( - "TuyaDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_ANY]), - ), - DPTYPE_RAW: tuya_ns.class_( - "TuyaRawDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_RAW]), - ), - DPTYPE_BOOL: tuya_ns.class_( - "TuyaBoolDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_BOOL]), - ), - DPTYPE_INT: tuya_ns.class_( - "TuyaIntDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_INT]), - ), - DPTYPE_UINT: tuya_ns.class_( - "TuyaUIntDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_UINT]), - ), - DPTYPE_STRING: tuya_ns.class_( - "TuyaStringDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_STRING]), - ), - DPTYPE_ENUM: tuya_ns.class_( - "TuyaEnumDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_ENUM]), - ), - DPTYPE_BITMASK: tuya_ns.class_( - "TuyaBitmaskDatapointUpdateTrigger", - automation.Trigger.template(DATAPOINT_TYPES[DPTYPE_BITMASK]), - ), -} - - -def assign_declare_id(value): - value = value.copy() - value[CONF_TRIGGER_ID] = cv.declare_id( - DATAPOINT_TRIGGERS[value[CONF_DATAPOINT_TYPE]] - )(value[CONF_TRIGGER_ID].id) - return value - CONF_TUYA_ID = "tuya_id" CONFIG_SCHEMA = ( @@ -93,15 +54,11 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_STATUS_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_ON_DATAPOINT_UPDATE): automation.validate_automation( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - DATAPOINT_TRIGGERS[DPTYPE_ANY] - ), cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, cv.Optional(CONF_DATAPOINT_TYPE, default=DPTYPE_ANY): cv.one_of( - *DATAPOINT_TRIGGERS, lower=True + *DATAPOINT_TYPES, lower=True ), - }, - extra_validators=assign_declare_id, + } ), } ) @@ -124,9 +81,11 @@ async def to_code(config): for dp in config[CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS]: cg.add(var.add_ignore_mcu_update_on_datapoints(dp)) for conf in config.get(CONF_ON_DATAPOINT_UPDATE, []): - trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], var, conf[CONF_SENSOR_DATAPOINT] - ) - await automation.build_automation( - trigger, [(DATAPOINT_TYPES[conf[CONF_DATAPOINT_TYPE]], "x")], conf + type_, expected, field = DATAPOINT_TYPES[conf[CONF_DATAPOINT_TYPE]] + forward = None + if expected is not None: + forward = [getattr(MockObj("x", ".").expect_type(expected), field)] + callback = await automation.build_trigger_callback( + [(type_, "x")], conf, params=[(TuyaDatapoint, "x")], forward=forward ) + cg.add(var.register_listener(conf[CONF_SENSOR_DATAPOINT], callback)) diff --git a/esphome/components/tuya/automation.cpp b/esphome/components/tuya/automation.cpp deleted file mode 100644 index a7eacf8e9a..0000000000 --- a/esphome/components/tuya/automation.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "esphome/core/log.h" - -#include "automation.h" - -ESPHOME_LOG_TAG(TAG, "tuya.automation"); - -namespace esphome::tuya { - -void check_expected_datapoint(const TuyaDatapoint &dp, TuyaDatapointType expected) { - if (dp.type != expected) { - ESP_LOGW(TAG, "Tuya sensor %u expected datapoint type %#02hhX but got %#02hhX", dp.id, - static_cast(expected), static_cast(dp.type)); - } -} - -TuyaRawDatapointUpdateTrigger::TuyaRawDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { - check_expected_datapoint(dp, TuyaDatapointType::RAW); - this->trigger(dp.value_raw); - }); -} - -TuyaBoolDatapointUpdateTrigger::TuyaBoolDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { - check_expected_datapoint(dp, TuyaDatapointType::BOOLEAN); - this->trigger(dp.value_bool); - }); -} - -TuyaIntDatapointUpdateTrigger::TuyaIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { - check_expected_datapoint(dp, TuyaDatapointType::INTEGER); - this->trigger(dp.value_int); - }); -} - -TuyaUIntDatapointUpdateTrigger::TuyaUIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { - check_expected_datapoint(dp, TuyaDatapointType::INTEGER); - this->trigger(dp.value_uint); - }); -} - -TuyaStringDatapointUpdateTrigger::TuyaStringDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { - check_expected_datapoint(dp, TuyaDatapointType::STRING); - this->trigger(dp.value_string); - }); -} - -TuyaEnumDatapointUpdateTrigger::TuyaEnumDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { - check_expected_datapoint(dp, TuyaDatapointType::ENUM); - this->trigger(dp.value_enum); - }); -} - -TuyaBitmaskDatapointUpdateTrigger::TuyaBitmaskDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { - check_expected_datapoint(dp, TuyaDatapointType::BITMASK); - this->trigger(dp.value_bitmask); - }); -} - -} // namespace esphome::tuya diff --git a/esphome/components/tuya/automation.h b/esphome/components/tuya/automation.h deleted file mode 100644 index 0cd63a76be..0000000000 --- a/esphome/components/tuya/automation.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "tuya.h" - -#include - -namespace esphome::tuya { - -class TuyaDatapointUpdateTrigger final : public Trigger { - public: - explicit TuyaDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id) { - parent->register_listener(sensor_id, [this](const TuyaDatapoint &dp) { this->trigger(dp); }); - } -}; - -class TuyaRawDatapointUpdateTrigger final : public Trigger> { - public: - explicit TuyaRawDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); -}; - -class TuyaBoolDatapointUpdateTrigger final : public Trigger { - public: - explicit TuyaBoolDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); -}; - -class TuyaIntDatapointUpdateTrigger final : public Trigger { - public: - explicit TuyaIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); -}; - -class TuyaUIntDatapointUpdateTrigger final : public Trigger { - public: - explicit TuyaUIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); -}; - -class TuyaStringDatapointUpdateTrigger final : public Trigger { - public: - explicit TuyaStringDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); -}; - -class TuyaEnumDatapointUpdateTrigger final : public Trigger { - public: - explicit TuyaEnumDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); -}; - -class TuyaBitmaskDatapointUpdateTrigger final : public Trigger { - public: - explicit TuyaBitmaskDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id); -}; - -} // namespace esphome::tuya diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 784aea5c50..b23dcc946b 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -482,7 +482,7 @@ void Tuya::handle_datapoints_(const uint8_t *buffer, size_t len) { // Run through listeners for (auto &listener : this->listeners_) { if (listener.datapoint_id == datapoint.id) - listener.on_datapoint(datapoint); + listener.on_datapoint.call(datapoint); } } } @@ -796,20 +796,27 @@ void Tuya::send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datap this->send_command_(TuyaCommand{.cmd = TuyaCommandType::DATAPOINT_DELIVER, .payload = buffer}); } -void Tuya::register_listener(uint8_t datapoint_id, const std::function &func) { - auto listener = TuyaDatapointListener{ +void Tuya::register_listener_(uint8_t datapoint_id, Callback func) { + this->listeners_.push_back(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); + func.call(datapoint); } } +const TuyaDatapoint &TuyaDatapoint::expect_type(TuyaDatapointType expected) const { + if (this->type != expected) { + ESP_LOGW(TAG, "Tuya sensor %u expected datapoint type %#02hhX but got %#02hhX", this->id, + static_cast(expected), static_cast(this->type)); + } + return *this; +} + TuyaInitState Tuya::get_init_state() { return this->init_state_; } } // namespace esphome::tuya diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index b8bf4e0ab1..f074d30162 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -37,11 +37,14 @@ struct TuyaDatapoint { }; std::string value_string; std::vector value_raw; + + /// Log a warning when this datapoint is not of type `expected`, and return it. + const TuyaDatapoint &expect_type(TuyaDatapointType expected) const; }; struct TuyaDatapointListener { uint8_t datapoint_id; - std::function on_datapoint; + Callback on_datapoint; }; enum class TuyaCommandType : uint8_t { @@ -91,7 +94,9 @@ class Tuya final : public Component, public uart::UARTDevice { void setup() override; void loop() override; void dump_config() override; - void register_listener(uint8_t datapoint_id, const std::function &func); + template void register_listener(uint8_t datapoint_id, F &&func) { + this->register_listener_(datapoint_id, Callback::create(std::forward(func))); + } void set_raw_datapoint_value(uint8_t datapoint_id, const std::vector &value); void set_boolean_datapoint_value(uint8_t datapoint_id, bool value); void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value); @@ -117,6 +122,7 @@ class Tuya final : public Component, public uart::UARTDevice { } protected: + void register_listener_(uint8_t datapoint_id, Callback func); void handle_char_(uint8_t c); void handle_datapoints_(const uint8_t *buffer, size_t len); optional get_datapoint_(uint8_t datapoint_id); diff --git a/tests/components/tuya/common.yaml b/tests/components/tuya/common.yaml index f52d47e7a0..1d0fc473ac 100644 --- a/tests/components/tuya/common.yaml +++ b/tests/components/tuya/common.yaml @@ -10,7 +10,50 @@ tuya: - sensor_datapoint: 6 datapoint_type: raw then: - - logger.log: Datapoint 6 updated + - logger.log: + format: "Datapoint 6 updated, %u bytes" + args: [x.size()] + - sensor_datapoint: 1 + then: + - logger.log: + format: "Datapoint %u updated" + args: [x.id] + - sensor_datapoint: 1 + datapoint_type: bool + then: + - logger.log: + format: "Datapoint 1 is %s" + args: ['x ? "on" : "off"'] + - sensor_datapoint: 3 + datapoint_type: int + then: + - logger.log: + format: "Datapoint 3 is %d" + args: [x] + - sensor_datapoint: 3 + datapoint_type: uint + then: + - logger.log: + format: "Datapoint 3 is %u" + args: [x] + - sensor_datapoint: 7 + datapoint_type: string + then: + - logger.log: + format: "Datapoint 7 is %s" + args: [x.c_str()] + - sensor_datapoint: 8 + datapoint_type: enum + then: + - logger.log: + format: "Datapoint 8 is %u" + args: [x] + - sensor_datapoint: 9 + datapoint_type: bitmask + then: + - logger.log: + format: "Datapoint 9 is %u" + args: [x] binary_sensor: - platform: tuya