Add Event Component to UART (#11765)

Co-authored-by: J. Nick Koston <nick+github@koston.org>
This commit is contained in:
eoasmxd
2025-12-23 06:19:48 +08:00
committed by GitHub
parent af0d4d2c2c
commit 1b31253287
6 changed files with 186 additions and 0 deletions

View File

@@ -519,6 +519,7 @@ esphome/components/tuya/switch/* @jesserockz
esphome/components/tuya/text_sensor/* @dentra
esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb
esphome/components/uart/event/* @eoasmxd
esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli

View File

@@ -0,0 +1,90 @@
import esphome.codegen as cg
from esphome.components import event, uart
import esphome.config_validation as cv
from esphome.const import CONF_EVENT_TYPES, CONF_ID
from esphome.core import ID
from esphome.types import ConfigType
from .. import uart_ns
CODEOWNERS = ["@eoasmxd"]
DEPENDENCIES = ["uart"]
UARTEvent = uart_ns.class_("UARTEvent", event.Event, uart.UARTDevice, cg.Component)
def validate_event_types(value) -> list[tuple[str, str | list[int]]]:
if not isinstance(value, list):
raise cv.Invalid("Event type must be a list of key-value mappings.")
processed: list[tuple[str, str | list[int]]] = []
for item in value:
if not isinstance(item, dict):
raise cv.Invalid(f"Event type item must be a mapping (dictionary): {item}")
if len(item) != 1:
raise cv.Invalid(
f"Event type item must be a single key-value mapping: {item}"
)
# Get the single key-value pair
event_name, match_data = next(iter(item.items()))
if not isinstance(event_name, str):
raise cv.Invalid(f"Event name (key) must be a string: {event_name}")
try:
# Try to validate as list of hex bytes
match_data_bin = cv.ensure_list(cv.hex_uint8_t)(match_data)
processed.append((event_name, match_data_bin))
continue
except cv.Invalid:
pass # Not binary, try string
try:
# Try to validate as string
match_data_str = cv.string_strict(match_data)
processed.append((event_name, match_data_str))
continue
except cv.Invalid:
pass # Not string either
# If neither validation passed
raise cv.Invalid(
f"Event match data for '{event_name}' must be a string or a list of hex bytes. Invalid data: {match_data}"
)
if not processed:
raise cv.Invalid("event_types must contain at least one event mapping.")
return processed
CONFIG_SCHEMA = (
event.event_schema(UARTEvent)
.extend(
{
cv.Required(CONF_EVENT_TYPES): validate_event_types,
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
event_names = [item[0] for item in config[CONF_EVENT_TYPES]]
var = await event.new_event(config, event_types=event_names)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
for i, (event_name, match_data) in enumerate(config[CONF_EVENT_TYPES]):
if isinstance(match_data, str):
match_data = [ord(c) for c in match_data]
match_data_var_id = ID(
f"match_data_{config[CONF_ID]}_{i}", is_declaration=True, type=cg.uint8
)
match_data_var = cg.static_const_array(
match_data_var_id, cg.ArrayInitializer(*match_data)
)
cg.add(var.add_event_matcher(event_name, match_data_var, len(match_data)))

View File

@@ -0,0 +1,48 @@
#include "uart_event.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome::uart {
static const char *const TAG = "uart.event";
void UARTEvent::setup() {}
void UARTEvent::dump_config() { LOG_EVENT("", "UART Event", this); }
void UARTEvent::loop() { this->read_data_(); }
void UARTEvent::add_event_matcher(const char *event_name, const uint8_t *match_data, size_t match_data_len) {
this->matchers_.push_back({event_name, match_data, match_data_len});
if (match_data_len > this->max_matcher_len_) {
this->max_matcher_len_ = match_data_len;
}
}
void UARTEvent::read_data_() {
while (this->available()) {
uint8_t data;
this->read_byte(&data);
this->buffer_.push_back(data);
bool match_found = false;
for (const auto &matcher : this->matchers_) {
if (this->buffer_.size() < matcher.data_len) {
continue;
}
if (std::equal(matcher.data, matcher.data + matcher.data_len, this->buffer_.end() - matcher.data_len)) {
this->trigger(matcher.event_name);
this->buffer_.clear();
match_found = true;
break;
}
}
if (!match_found && this->max_matcher_len_ > 0 && this->buffer_.size() > this->max_matcher_len_) {
this->buffer_.erase(this->buffer_.begin());
}
}
}
} // namespace esphome::uart

View File

@@ -0,0 +1,31 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/event/event.h"
#include "esphome/components/uart/uart.h"
#include <vector>
namespace esphome::uart {
class UARTEvent : public event::Event, public UARTDevice, public Component {
public:
void setup() override;
void loop() override;
void dump_config() override;
void add_event_matcher(const char *event_name, const uint8_t *match_data, size_t match_data_len);
protected:
struct EventMatcher {
const char *event_name;
const uint8_t *data;
size_t data_len;
};
void read_data_();
std::vector<EventMatcher> matchers_;
std::vector<uint8_t> buffer_;
size_t max_matcher_len_ = 0;
};
} // namespace esphome::uart

View File

@@ -75,3 +75,11 @@ button:
- uart.write: !lambda |-
std::string cmd = "VALUE=" + str_sprintf("%.0f", id(test_number).state) + "\r\n";
return std::vector<uint8_t>(cmd.begin(), cmd.end());
event:
- platform: uart
uart_id: uart_uart
name: "UART Event"
event_types:
- "string_event_A": "*A#"
- "bytes_event_B": [0x2A, 0x42, 0x23]

View File

@@ -31,3 +31,11 @@ button:
name: "UART Button"
uart_id: uart_uart
data: [0xFF, 0xEE]
event:
- platform: uart
uart_id: uart_uart
name: "UART Event"
event_types:
- "string_event_A": "*A#"
- "bytes_event_B": [0x2A, 0x42, 0x23]