mirror of
https://github.com/esphome/esphome.git
synced 2026-10-03 09:46:03 +00:00
[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:
co-authored by
pre-commit-ci-lite[bot]
J. Nick Koston
parent
d970e9292e
commit
4724aea42e
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user