[tuya] Build on_datapoint_update with build_trigger_callback (#20453)

This commit is contained in:
J. Nick Koston
2026-10-09 15:42:43 -10:00
committed by GitHub
parent bc44e8b550
commit fdc163a30d
6 changed files with 87 additions and 190 deletions
+22 -63
View File
@@ -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))
-65
View File
@@ -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<uint8_t>(expected), static_cast<uint8_t>(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
-53
View File
@@ -1,53 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "tuya.h"
#include <vector>
namespace esphome::tuya {
class TuyaDatapointUpdateTrigger final : public Trigger<TuyaDatapoint> {
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<std::vector<uint8_t>> {
public:
explicit TuyaRawDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
};
class TuyaBoolDatapointUpdateTrigger final : public Trigger<bool> {
public:
explicit TuyaBoolDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
};
class TuyaIntDatapointUpdateTrigger final : public Trigger<int> {
public:
explicit TuyaIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
};
class TuyaUIntDatapointUpdateTrigger final : public Trigger<uint32_t> {
public:
explicit TuyaUIntDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
};
class TuyaStringDatapointUpdateTrigger final : public Trigger<std::string> {
public:
explicit TuyaStringDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
};
class TuyaEnumDatapointUpdateTrigger final : public Trigger<uint8_t> {
public:
explicit TuyaEnumDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
};
class TuyaBitmaskDatapointUpdateTrigger final : public Trigger<uint32_t> {
public:
explicit TuyaBitmaskDatapointUpdateTrigger(Tuya *parent, uint8_t sensor_id);
};
} // namespace esphome::tuya
+13 -6
View File
@@ -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<void(TuyaDatapoint)> &func) {
auto listener = TuyaDatapointListener{
void Tuya::register_listener_(uint8_t datapoint_id, Callback<void(const TuyaDatapoint &)> 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<uint8_t>(expected), static_cast<uint8_t>(this->type));
}
return *this;
}
TuyaInitState Tuya::get_init_state() { return this->init_state_; }
} // namespace esphome::tuya
+8 -2
View File
@@ -37,11 +37,14 @@ struct TuyaDatapoint {
};
std::string value_string;
std::vector<uint8_t> 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<void(TuyaDatapoint)> on_datapoint;
Callback<void(const TuyaDatapoint &)> 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<void(TuyaDatapoint)> &func);
template<typename F> void register_listener(uint8_t datapoint_id, F &&func) {
this->register_listener_(datapoint_id, Callback<void(const TuyaDatapoint &)>::create(std::forward<F>(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);
@@ -117,6 +122,7 @@ class Tuya final : public Component, public uart::UARTDevice {
}
protected:
void register_listener_(uint8_t datapoint_id, Callback<void(const TuyaDatapoint &)> func);
void handle_char_(uint8_t c);
void handle_datapoints_(const uint8_t *buffer, size_t len);
optional<TuyaDatapoint> get_datapoint_(uint8_t datapoint_id);
+44 -1
View File
@@ -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