[tcp_uart] Add a TCP client that implements a UART (#19884)

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Bascht74
2026-10-02 08:00:40 -05:00
committed by GitHub
co-authored by pre-commit-ci-lite[bot] J. Nick Koston
parent d970e9292e
commit 4724aea42e
12 changed files with 301 additions and 0 deletions
+1
View File
@@ -560,6 +560,7 @@ esphome/components/tc74/* @sethgirvan
esphome/components/tca9548a/* @andreashergert1984
esphome/components/tca9555/* @mobrembski
esphome/components/tcl112/* @glmnet
esphome/components/tcp_uart/* @Bascht74
esphome/components/tee501/* @Stock-M
esphome/components/teleinfo/* @0hax
esphome/components/tem3200/* @bakerkj
+67
View File
@@ -0,0 +1,67 @@
import esphome.codegen as cg
from esphome.components import binary_sensor, socket, uart
from esphome.components.const import (
CONF_DATA_BITS,
CONF_HOST,
CONF_PARITY,
CONF_STOP_BITS,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_BAUD_RATE,
CONF_ID,
CONF_PORT,
DEVICE_CLASS_CONNECTIVITY,
ENTITY_CATEGORY_DIAGNOSTIC,
)
from esphome.types import ConfigType
CODEOWNERS = ["@Bascht74"]
DEPENDENCIES = ["network"]
AUTO_LOAD = ["uart", "binary_sensor", "socket"]
MULTI_CONF = True
tcp_uart_ns = cg.esphome_ns.namespace("tcp_uart")
TcpUart = tcp_uart_ns.class_("TcpUart", uart.UARTComponent, cg.Component)
CONF_RECONNECT_INTERVAL = "reconnect_interval"
CONF_CONNECTED = "connected"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(TcpUart),
cv.Required(CONF_HOST): cv.string,
cv.Required(CONF_PORT): cv.port,
cv.Optional(CONF_BAUD_RATE, default=9600): cv.int_range(min=1),
cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8),
cv.Optional(CONF_PARITY, default="NONE"): cv.enum(
uart.UART_PARITY_OPTIONS, upper=True
),
cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True),
cv.Optional(
CONF_RECONNECT_INTERVAL, default="5s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_CONNECTED): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
).extend(cv.COMPONENT_SCHEMA),
socket.consume_sockets(1, "tcp_uart"),
)
async def to_code(config: ConfigType) -> None:
socket.require_tcp_client_link()
var = cg.new_Pvariable(config[CONF_ID], config[CONF_HOST], config[CONF_PORT])
await cg.register_component(var, config)
cg.add(var.set_reconnect_interval(config[CONF_RECONNECT_INTERVAL]))
# The socket is not clocked. These only satisfy UARTComponent and a consumer check.
cg.add(var.set_baud_rate(config[CONF_BAUD_RATE]))
cg.add(var.set_data_bits(config[CONF_DATA_BITS]))
cg.add(var.set_stop_bits(config[CONF_STOP_BITS]))
cg.add(var.set_parity(config[CONF_PARITY]))
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_CONNECTED, var.set_connected_sensor)
+131
View File
@@ -0,0 +1,131 @@
#include "tcp_uart.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include <cinttypes>
#include <cstring>
namespace esphome::tcp_uart {
static const char *const TAG = "tcp_uart";
static constexpr uint32_t DROP_LOG_INTERVAL_MS = 5000;
void TcpUart::setup() {
this->link_.begin(TAG);
if (this->connected_sensor_ != nullptr) {
this->connected_sensor_->publish_state(false);
}
}
void TcpUart::dump_config() {
ESP_LOGCONFIG(TAG,
"TCP UART:\n"
" Host: %s:%u\n"
" Reconnect Interval: %" PRIu32 "ms",
this->link_.host(), this->link_.port(), this->link_.reconnect_interval());
LOG_BINARY_SENSOR(" ", "Connected", this->connected_sensor_);
}
void TcpUart::sync_link_() {
bool up = this->link_.connected();
this->link_was_up_ = up;
if (!up) {
this->rx_start_ = this->rx_end_ = 0;
this->tx_len_ = 0;
}
if (this->connected_sensor_ != nullptr) {
this->connected_sensor_->publish_state(up);
}
}
void TcpUart::read_socket_() {
if (this->rx_start_ != 0) {
this->rx_end_ -= this->rx_start_;
std::memmove(this->rx_, this->rx_ + this->rx_start_, this->rx_end_);
this->rx_start_ = 0;
}
size_t room = RX_BUFFER_SIZE - this->rx_end_;
if (room == 0) {
// Only a read that filled all free space gets here, so rx_pending_ is already set.
return;
}
ssize_t count = this->link_.read(this->rx_ + this->rx_end_, room);
if (count <= 0) {
// A dropped link (-1) is cleaned up by sync_link_() on the next loop.
if (count == 0) {
this->rx_pending_ = false;
}
return;
}
this->rx_end_ += static_cast<uint16_t>(count);
this->rx_pending_ = static_cast<size_t>(count) == room;
}
void TcpUart::flush_tx_() {
ssize_t sent = this->link_.write(this->tx_, this->tx_len_);
if (sent > 0) {
this->tx_len_ -= static_cast<uint16_t>(sent);
std::memmove(this->tx_, this->tx_ + sent, this->tx_len_);
}
}
void TcpUart::loop() {
this->link_.poll();
if (this->link_.connected() != this->link_was_up_) {
this->sync_link_();
}
if (!this->link_was_up_) {
return;
}
if (this->rx_pending_ || this->link_.ready()) {
this->read_socket_();
}
if (this->tx_len_ != 0) {
this->flush_tx_();
}
}
void TcpUart::write_array(const uint8_t *data, size_t len) {
size_t room = this->link_.connected() ? sizeof(this->tx_) - this->tx_len_ : 0;
if (len > room) {
uint32_t now = App.get_loop_component_start_time();
if (this->last_drop_log_ms_ == 0 || now - this->last_drop_log_ms_ >= DROP_LOG_INTERVAL_MS) {
ESP_LOGW(TAG, "%s, dropped %u bytes",
this->link_.connected() ? LOG_STR_LITERAL("TX buffer full") : LOG_STR_LITERAL("Not connected"),
static_cast<unsigned>(len - room));
this->last_drop_log_ms_ = now;
}
len = room;
}
std::memcpy(this->tx_ + this->tx_len_, data, len);
this->tx_len_ += static_cast<uint16_t>(len);
}
bool TcpUart::peek_byte(uint8_t *data) {
if (this->rx_start_ == this->rx_end_) {
return false;
}
*data = this->rx_[this->rx_start_];
return true;
}
bool TcpUart::read_array(uint8_t *data, size_t len) {
if (this->available() < len) {
return false;
}
std::memcpy(data, this->rx_ + this->rx_start_, len);
this->rx_start_ += static_cast<uint16_t>(len);
return true;
}
uart::UARTFlushResult TcpUart::flush() {
this->flush_tx_();
if (this->tx_len_ == 0) {
return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS;
}
return uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT;
}
} // namespace esphome::tcp_uart
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/socket/tcp_client_link.h"
#include "esphome/components/uart/uart_component.h"
#include "esphome/core/component.h"
#include <cstdint>
namespace esphome::tcp_uart {
/// TCP client presented as a UART. Bytes are copied unchanged.
class TcpUart : public uart::UARTComponent, public Component {
public:
TcpUart(const char *host, uint16_t port) {
this->link_.set_host(host);
this->link_.set_port(port);
this->rx_buffer_size_ = RX_BUFFER_SIZE;
}
void set_reconnect_interval(uint32_t ms) { this->link_.set_reconnect_interval(ms); }
void set_connected_sensor(binary_sensor::BinarySensor *sensor) { this->connected_sensor_ = sensor; }
void setup() override;
void loop() override;
void dump_config() override;
void on_shutdown() override { this->link_.close(); }
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void write_array(const uint8_t *data, size_t len) override;
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
size_t available() override { return static_cast<size_t>(this->rx_end_ - this->rx_start_); }
// Same room write_array() grants, so consumers can apply backpressure.
size_t available_for_write() override { return this->link_.connected() ? sizeof(this->tx_) - this->tx_len_ : 0; }
uart::UARTFlushResult flush() override;
bool is_connected() override { return this->link_.connected(); }
#if defined(USE_ESP8266) || defined(USE_ESP32)
void load_settings(bool dump_config) override {}
#endif
protected:
void check_logger_conflict() override {}
void sync_link_();
void read_socket_();
void flush_tx_();
static constexpr size_t RX_BUFFER_SIZE = 1024;
static constexpr size_t TX_BUFFER_SIZE = 1024;
socket::TcpClientLink link_;
binary_sensor::BinarySensor *connected_sensor_{nullptr};
uint32_t last_drop_log_ms_{0};
uint16_t tx_len_{0};
// rx_[rx_start_, rx_end_) holds unread bytes; read_socket_() compacts to the front.
uint16_t rx_start_{0};
uint16_t rx_end_{0};
// The link state loop() saw last; edges clear the buffers and publish the sensor.
bool link_was_up_{false};
// A read stopped before EAGAIN. ready() stays false until new data arrives.
bool rx_pending_{false};
uint8_t rx_[RX_BUFFER_SIZE]{};
uint8_t tx_[TX_BUFFER_SIZE]{};
};
} // namespace esphome::tcp_uart
+20
View File
@@ -0,0 +1,20 @@
wifi:
ssid: MySSID
password: password1
tcp_uart:
- id: tcp_uart_1
host: 192.0.2.10
port: 502
reconnect_interval: 10s
connected:
name: TCP UART Connected
interval:
- interval: 60s
then:
- lambda: |-
uint8_t byte;
if (id(tcp_uart_1).available() && id(tcp_uart_1).read_byte(&byte)) {
id(tcp_uart_1).write_byte(byte);
}
@@ -0,0 +1,2 @@
packages:
tcp_uart: !include common.yaml
@@ -0,0 +1,2 @@
packages:
tcp_uart: !include common.yaml
@@ -0,0 +1,2 @@
packages:
tcp_uart: !include common.yaml
+4
View File
@@ -0,0 +1,4 @@
packages:
tcp_uart: !include common.yaml
wifi: !remove
@@ -0,0 +1,2 @@
packages:
tcp_uart: !include common.yaml
@@ -0,0 +1,2 @@
packages:
tcp_uart: !include common.yaml
@@ -0,0 +1,2 @@
packages:
tcp_uart: !include common.yaml