Compare commits

..
Author SHA1 Message Date
dependabot[bot] f980c39e31 Bump click from 8.3.3 to 8.5.0
Bumps [click](https://github.com/pallets/click) from 8.3.3 to 8.5.0.
- [Release notes](https://github.com/pallets/click/releases)
- [Changelog](https://github.com/pallets/click/blob/main/CHANGES.md)
- [Commits](https://github.com/pallets/click/compare/8.3.3...8.5.0)

---
updated-dependencies:
- dependency-name: click
  dependency-version: 8.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-10 15:01:54 +00:00
26 changed files with 58 additions and 603 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.6
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.5
RUN \
platformio settings set enable_telemetry No \
+4 -33
View File
@@ -77,7 +77,6 @@ service APIConnection {
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
}
@@ -2727,8 +2726,7 @@ enum SerialProxyParity {
SERIAL_PROXY_PARITY_ODD = 2;
}
// Configure UART parameters for a serial proxy instance. Only the subscribed client may
// configure the port; others are refused with PORT_IN_USE (since API 1.17).
// Configure UART parameters for a serial proxy instance
message SerialProxyConfigureRequest {
option (id) = 138;
option (source) = SOURCE_CLIENT;
@@ -2754,8 +2752,7 @@ message SerialProxyDataReceived {
bytes data = 2; // Raw data received from the serial device
}
// Write data to a serial device. Only the subscribed client may write; writes from
// others are ignored (since API 1.17).
// Write data to a serial device
message SerialProxyWriteRequest {
option (id) = 140;
option (source) = SOURCE_CLIENT;
@@ -2766,8 +2763,7 @@ message SerialProxyWriteRequest {
bytes data = 2; // Raw data to write to the serial device
}
// Set modem control pin states (RTS and DTR). Only the subscribed client may set them;
// others are refused with PORT_IN_USE (since API 1.17).
// Set modem control pin states (RTS and DTR)
message SerialProxySetModemPinsRequest {
option (id) = 141;
option (source) = SOURCE_CLIENT;
@@ -2806,7 +2802,6 @@ enum SerialProxyRequestType {
// error the device answers with INVALID_ARGUMENT.
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5; // Acknowledges a SerialProxySetModeRequest (since API 1.17)
}
enum SerialProxyStatus {
@@ -2819,8 +2814,7 @@ enum SerialProxyStatus {
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value
}
// Generic request message for simple serial proxy operations. FLUSH requires an active
// subscription; it is refused with PORT_IN_USE otherwise (since API 1.17).
// Generic request message for simple serial proxy operations
message SerialProxyRequest {
option (id) = 144;
option (source) = SOURCE_CLIENT;
@@ -2844,29 +2838,6 @@ message SerialProxyRequestResponse {
string error_message = 4; // Additional detail on failure (optional)
}
// How a port treats the bytes passing through it. RAW is a plain byte pipe; PROTOCOL
// activates the port's protocol-aware tap (if one is configured), letting it observe
// traffic and inject protocol bytes such as acknowledgements. Which protocol the tap
// speaks is a property of the device configuration, discoverable from the tap
// component's own API surface. A client that is about to flash firmware selects RAW
// first, which definitively disables that injection.
enum SerialProxyMode {
SERIAL_PROXY_MODE_RAW = 0;
SERIAL_PROXY_MODE_PROTOCOL = 1;
}
// Only the subscribed client may change the mode; any other caller -- including one that
// never subscribed -- is refused with PORT_IN_USE. PROTOCOL is refused with NOT_SUPPORTED
// when the port has no protocol-aware tap configured.
message SerialProxySetModeRequest {
option (id) = 152;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1;
SerialProxyMode mode = 2;
}
// ==================== BLUETOOTH CONNECTION PARAMS ====================
message BluetoothSetConnectionParamsRequest {
option (id) = 145;
+1 -15
View File
@@ -1661,7 +1661,6 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
break;
case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
// Response-only discriminators; never valid in a request
ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
@@ -1674,19 +1673,6 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
send_serial_proxy_ack(this, msg.instance, msg.type, status);
}
void APIConnection::on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
return;
}
serial_proxy::SerialProxyResult result = proxies[msg.instance]->set_mode_from_client(this, msg.mode);
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
serial_proxy_result_to_status(result));
}
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
if (!this->send_message(msg)) {
ESP_LOGV(TAG, "Serial proxy data dropped, TCP buffer full");
@@ -1813,7 +1799,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
HelloResponse resp;
resp.api_version_major = 1;
resp.api_version_minor = 17;
resp.api_version_minor = 16;
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
resp.server_info = ESPHOME_VERSION_REF;
resp.name = StringRef(App.get_name());
-1
View File
@@ -244,7 +244,6 @@ class APIConnection final : public APIServerConnectionBase {
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg);
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg);
void on_serial_proxy_request(const SerialProxyRequest &msg);
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg);
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
#endif
-13
View File
@@ -4253,19 +4253,6 @@ uint32_t SerialProxyRequestResponse::calculate_size() const {
size += ProtoSize::calc_length(1, this->error_message.size());
return size;
}
bool SerialProxySetModeRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
switch (field_id) {
case 1:
this->instance = value;
break;
case 2:
this->mode = static_cast<enums::SerialProxyMode>(value);
break;
default:
return false;
}
return true;
}
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
-21
View File
@@ -356,7 +356,6 @@ enum SerialProxyRequestType : uint32_t {
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2,
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3,
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4,
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5,
};
enum SerialProxyStatus : uint32_t {
SERIAL_PROXY_STATUS_OK = 0,
@@ -367,10 +366,6 @@ enum SerialProxyStatus : uint32_t {
SERIAL_PROXY_STATUS_PORT_IN_USE = 5,
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6,
};
enum SerialProxyMode : uint32_t {
SERIAL_PROXY_MODE_RAW = 0,
SERIAL_PROXY_MODE_PROTOCOL = 1,
};
#endif
} // namespace enums
@@ -3408,22 +3403,6 @@ class SerialProxyRequestResponse final : public ProtoMessage {
protected:
};
class SerialProxySetModeRequest final : public ProtoDecodableMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 152;
static constexpr uint8_t ESTIMATED_SIZE = 6;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("serial_proxy_set_mode_request"); }
#endif
uint32_t instance{0};
enums::SerialProxyMode mode{};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage {
-18
View File
@@ -854,8 +854,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_CONFIGURE");
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS");
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODE");
default:
return ESPHOME_PSTR("UNKNOWN");
}
@@ -880,16 +878,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
return ESPHOME_PSTR("UNKNOWN");
}
}
template<> const char *proto_enum_to_string<enums::SerialProxyMode>(enums::SerialProxyMode value) {
switch (value) {
case enums::SERIAL_PROXY_MODE_RAW:
return ESPHOME_PSTR("SERIAL_PROXY_MODE_RAW");
case enums::SERIAL_PROXY_MODE_PROTOCOL:
return ESPHOME_PSTR("SERIAL_PROXY_MODE_PROTOCOL");
default:
return ESPHOME_PSTR("UNKNOWN");
}
}
#endif
const char *HelloRequest::dump_to(DumpBuffer &out) const {
@@ -2817,12 +2805,6 @@ const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("error_message"), this->error_message);
return out.c_str();
}
const char *SerialProxySetModeRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxySetModeRequest"));
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::SerialProxyMode>(this->mode));
return out.c_str();
}
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
@@ -712,17 +712,6 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
this->on_device_capabilities_request();
break;
}
#ifdef USE_SERIAL_PROXY
case SerialProxySetModeRequest::MESSAGE_TYPE: {
SerialProxySetModeRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_set_mode_request"), msg);
#endif
this->on_serial_proxy_set_mode_request(msg);
break;
}
#endif
default:
break;
}
-3
View File
@@ -235,9 +235,6 @@ class APIServerConnectionBase {
void on_serial_proxy_request(const SerialProxyRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
#endif
+2 -3
View File
@@ -153,14 +153,13 @@ bool ES7210::configure_mic_gain_() {
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, regv));
// Configure mic 3
// MIC3 uses the ADC3/4 and MIC3/4 clock domains (bits 2 and 4), not the MIC1/2 domains.
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, regv));
// Configure mic 4
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, regv));
-32
View File
@@ -6,17 +6,12 @@ from esphome.components import esp32, network, psram, socket, wifi
import esphome.config_validation as cv
from esphome.const import (
CONF_BUFFER_SIZE,
CONF_ESPHOME,
CONF_FORMAT,
CONF_HEIGHT,
CONF_ID,
CONF_MODEL,
CONF_NAME,
CONF_PROJECT,
CONF_SAMPLE_RATE,
CONF_SOURCE,
CONF_TASK_STACK_IN_PSRAM,
CONF_VERSION,
CONF_WIDTH,
)
from esphome.core import CORE, ID
@@ -32,14 +27,6 @@ DOMAIN = "sendspin"
CONF_DISPLAY_OFFSET = "display_offset"
CONF_SENDSPIN_ID = "sendspin_id"
CONF_FIRMWARE_VERSION = "firmware_version"
CONF_MANUFACTURER = "manufacturer"
# An empty device information string would be sent to the server as an empty value rather than
# falling back, so reject it instead of silently substituting the fallback. The 127 byte cap keeps
# the length prefix of a protobuf string field to a single byte, matching `esphome: project:`.
DEVICE_INFO_STRING = cv.All(cv.string_strict, cv.Length(min=1), cv.ByteLength(max=127))
CONF_INITIAL_STATIC_DELAY = "initial_static_delay"
CONF_FIXED_DELAY = "fixed_delay"
CONF_DECODE_MEMORY = "decode_memory"
@@ -211,9 +198,6 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(SendspinHub),
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
cv.Optional(CONF_MANUFACTURER): DEVICE_INFO_STRING,
cv.Optional(CONF_MODEL): DEVICE_INFO_STRING,
cv.Optional(CONF_FIRMWARE_VERSION): DEVICE_INFO_STRING,
}
),
cv.only_on_esp32,
@@ -264,22 +248,6 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_task_stack_in_psram(True))
psram.request_external_task_stack()
# Device information for the server's client/hello message. Falls back to the project
# information, which is written as `manufacturer.model`. Anything still unset keeps the
# default the hub itself applies: the ESPHome name and version.
project = CORE.config[CONF_ESPHOME].get(CONF_PROJECT, {})
project_manufacturer, _, project_model = project.get(CONF_NAME, "").partition(".")
for value, setter in (
(config.get(CONF_MANUFACTURER) or project_manufacturer, var.set_manufacturer),
(config.get(CONF_MODEL) or project_model, var.set_model),
(
config.get(CONF_FIRMWARE_VERSION) or project.get(CONF_VERSION),
var.set_firmware_version,
),
):
if value:
cg.add(setter(value))
# sendspin-cpp library
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.7.2")
+4 -12
View File
@@ -76,12 +76,8 @@ void SendspinHub::dump_config() {
ESP_LOGCONFIG(TAG,
"Sendspin Hub:\n"
" Client ID: %s\n"
" Manufacturer: %s\n"
" Model: %s\n"
" Firmware version: %s\n"
" Task stack in PSRAM: %s",
get_client_id_into_buffer(mac_buf), this->manufacturer_, this->get_product_name_(),
this->firmware_version_, YESNO(this->task_stack_in_psram_));
get_client_id_into_buffer(mac_buf), YESNO(this->task_stack_in_psram_));
#ifdef USE_SENDSPIN_ARTWORK
// Slot indices come from the order the image platform entries were declared, so the log is the
@@ -131,19 +127,15 @@ const char *SendspinHub::get_client_id_into_buffer(std::span<char, MAC_ADDRESS_P
return get_mac_address_pretty_into_buffer(buf);
}
const char *SendspinHub::get_product_name_() const {
return this->model_ != nullptr ? this->model_ : App.get_name().c_str();
}
sendspin::SendspinClientConfig SendspinHub::build_client_config_() {
sendspin::SendspinClientConfig config;
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
config.client_id = SendspinHub::get_client_id_into_buffer(mac_buf);
config.name = App.get_friendly_name();
config.product_name = this->get_product_name_();
config.manufacturer = this->manufacturer_;
config.software_version = this->firmware_version_;
config.product_name = App.get_name();
config.manufacturer = "ESPHome";
config.software_version = ESPHOME_VERSION;
config.httpd_psram_stack = this->task_stack_in_psram_;
return config;
@@ -8,7 +8,6 @@
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/core/version.h"
#include <sendspin/client.h>
#include <sendspin/config.h>
@@ -126,15 +125,6 @@ class SendspinHub final : public Component,
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
/// @brief Sets the device information reported to the server in the `client/hello` message.
///
/// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the
/// lifetime of the hub. Only called for values the configuration overrides; anything left alone
/// keeps the default described on the member below.
void set_manufacturer(const char *manufacturer) { this->manufacturer_ = manufacturer; }
void set_model(const char *model) { this->model_ = model; }
void set_firmware_version(const char *firmware_version) { this->firmware_version_ = firmware_version; }
// --- Sendspin role specific methods ---
#ifdef USE_SENDSPIN_ARTWORK
@@ -197,9 +187,6 @@ class SendspinHub final : public Component,
/// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info.
sendspin::SendspinClientConfig build_client_config_();
/// @brief Returns the product name reported to the server: the configured model, or the device name.
const char *get_product_name_() const;
/// @brief Writes the active network interface's MAC into @p buf and returns its data pointer.
/// Uses the ethernet MAC if ethernet is configured, otherwise the base MAC (used by wifi).
static const char *get_client_id_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
@@ -281,12 +268,6 @@ class SendspinHub final : public Component,
CallbackManager<void(const sendspin::GroupUpdateObject &)> group_update_callbacks_{};
bool task_stack_in_psram_{false};
// Device information sent in the `client/hello` message. Defaults apply when neither the
// sendspin configuration nor the project information supplies a value.
const char *manufacturer_{"ESPHome"};
const char *model_{nullptr}; // nullptr reports the device name instead
const char *firmware_version_{ESPHOME_VERSION};
};
/// @brief Base class for all sendspin subcomponents.
@@ -30,7 +30,6 @@ MULTI_CONF = True
serial_proxy_ns = cg.esphome_ns.namespace("serial_proxy")
SerialProxy = serial_proxy_ns.class_("SerialProxy", cg.Component, uart.UARTDevice)
SerialProxyTap = serial_proxy_ns.class_("SerialProxyTap")
api_enums_ns = cg.esphome_ns.namespace("api").namespace("enums")
SerialProxyPortType = api_enums_ns.enum("SerialProxyPortType")
+25 -163
View File
@@ -29,57 +29,26 @@ void SerialProxy::setup() {
#ifdef USE_API
// instance_index_ is fixed at registration time; pre-set it so loop() only needs to update data
this->outgoing_msg_.instance = this->instance_index_;
#endif
#ifdef USE_SERIAL_PROXY_TAP
// A tap sets itself up before this runs (its setup priority is higher), so it may
// already be waiting on the port -- a boot-time handshake with the device, say. Leaving
// the loop enabled is what lets that finish; without it the tap would stall until a
// client happened to subscribe.
if (this->tap_ != nullptr && this->tap_->tap_needs_port()) {
return;
}
#endif
// No subscriber at startup; disable loop until a client subscribes
this->disable_loop();
}
#ifdef USE_SERIAL_PROXY_TAP
void SerialProxy::reset_mode_() {
// The mode belongs to a session, not to the port. Carrying a departed client's choice
// over to the next one would inject protocol bytes into a stream that never asked for
// them -- a firmware upload, or any client built before this request existed and so
// unable to turn it off. Guessing RAW is the safe direction: a client that wanted
// protocol handling and did not ask for it merely sends its own acknowledgements.
if (this->mode_ == api::enums::SERIAL_PROXY_MODE_RAW) {
return;
}
ESP_LOGD(TAG, "Session ended, returning serial proxy [%" PRIu32 "] to RAW mode", this->instance_index_);
this->mode_ = api::enums::SERIAL_PROXY_MODE_RAW;
}
#endif
void SerialProxy::loop() {
#ifdef USE_API
// Detect subscriber disconnect
if (this->api_connection_ != nullptr && (this->api_connection_->is_marked_for_removal() ||
!this->api_connection_->is_connection_setup() || !api_is_connected())) {
ESP_LOGW(TAG, "Subscriber disconnected");
this->api_connection_ = nullptr;
this->reset_mode_();
}
// With no subscriber there is normally nothing to do, but a tap may still need the port
// read -- it does its protocol work precisely while nobody else is listening.
// Safety check — loop should only run when subscribed, but guard against races
if (this->api_connection_ == nullptr) [[unlikely]] {
#ifdef USE_SERIAL_PROXY_TAP
if (this->tap_ == nullptr || !this->tap_->tap_needs_port()) {
this->disable_loop();
return;
}
#else
this->disable_loop();
return;
#endif
}
// Detect subscriber disconnect
if (this->api_connection_->is_marked_for_removal() || !this->api_connection_->is_connection_setup() ||
!api_is_connected()) {
ESP_LOGW(TAG, "Subscriber disconnected");
this->api_connection_ = nullptr;
this->disable_loop();
return;
}
// Read available data from UART and forward to subscribed client
@@ -100,54 +69,11 @@ void __attribute__((noinline)) SerialProxy::read_and_send_(size_t available) {
if (!this->read_array(buffer, to_read))
return;
#ifdef USE_SERIAL_PROXY_TAP
// Before forwarding, so a tap that answers the device (an acknowledgement, say) is not
// waiting on the network round trip to a subscriber that may not even exist.
if (this->tap_observing_()) {
this->tap_->on_device_rx(buffer, to_read);
}
#endif
if (this->api_connection_ == nullptr) {
return;
}
this->outgoing_msg_.set_data(buffer, to_read);
this->api_connection_->send_serial_proxy_data(this->outgoing_msg_);
}
#endif
#ifdef USE_SERIAL_PROXY_TAP
bool SerialProxy::tap_observing_() const {
if (this->tap_ == nullptr) {
return false;
}
// With no subscriber, a tap doing its own protocol work (the boot-time handshake with
// the device, say) is served regardless of mode -- nobody has chosen one yet. Once a
// subscriber holds the port, the mode alone decides, so RAW stays inert.
if (this->api_connection_ == nullptr && this->tap_->tap_needs_port()) {
return true;
}
// Otherwise the mode decides. RAW must be inert: a client that flips to RAW before
// flashing firmware is entitled to a byte pipe with nothing injecting protocol bytes
// into it, and "the tap turned out not to recognise the stream" is not good enough.
return this->mode_ == api::enums::SERIAL_PROXY_MODE_PROTOCOL;
}
void SerialProxy::tap_pump() {
#ifdef USE_API
// Nothing would consume the bytes; leave them in the FIFO
if (!this->tap_observing_() && this->api_connection_ == nullptr) {
return;
}
const size_t available = this->available();
if (available > 0) {
this->read_and_send_(available);
}
#endif
}
#endif
void SerialProxy::dump_config() {
ESP_LOGCONFIG(TAG,
"Serial Proxy [%" PRIu32 "]:\n"
@@ -166,9 +92,8 @@ void SerialProxy::dump_config() {
SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control,
uint8_t parity, uint8_t stop_bits, uint8_t data_size) {
#ifdef USE_API
if (!this->is_subscriber_(api_connection)) {
ESP_LOGW(TAG, "Ignoring configure request from client without port subscription [%" PRIu32 "]",
this->instance_index_);
if (this->port_claimed_by_other_(api_connection)) {
ESP_LOGW(TAG, "Ignoring configure request from client without port access [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
#endif
@@ -234,80 +159,24 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
SerialProxyResult SerialProxy::set_mode_from_client(api::APIConnection *api_connection,
api::enums::SerialProxyMode mode) {
#ifdef USE_API
// Only the live subscriber may change the mode, so the mode cannot outlive a session
if (!this->is_subscriber_(api_connection)) {
ESP_LOGW(TAG, "Ignoring mode request from client without port subscription [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
#endif
// Values come from a remote client
if (mode != api::enums::SERIAL_PROXY_MODE_RAW && mode != api::enums::SERIAL_PROXY_MODE_PROTOCOL) {
ESP_LOGW(TAG, "Invalid mode: %" PRIu32, static_cast<uint32_t>(mode));
return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT;
}
// PROTOCOL on a port with no tap would be a silent no-op; refuse so the client knows
#ifdef USE_SERIAL_PROXY_TAP
const bool has_tap = this->tap_ != nullptr;
#else
const bool has_tap = false;
#endif
if (mode == api::enums::SERIAL_PROXY_MODE_PROTOCOL && !has_tap) {
ESP_LOGW(TAG, "No tap on serial proxy [%" PRIu32 "]; PROTOCOL mode unavailable", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
}
ESP_LOGD(TAG, "Serial proxy [%" PRIu32 "] mode set to %s", this->instance_index_,
mode == api::enums::SERIAL_PROXY_MODE_PROTOCOL ? LOG_STR_LITERAL("PROTOCOL") : LOG_STR_LITERAL("RAW"));
#ifdef USE_SERIAL_PROXY_TAP
const bool leaving_protocol_mode =
this->mode_ != api::enums::SERIAL_PROXY_MODE_RAW && mode == api::enums::SERIAL_PROXY_MODE_RAW;
this->mode_ = mode;
// Only for an explicit client request, not for reset_mode_() at the end of a session:
// an ordinary disconnect says nothing about the device, whereas a client deliberately
// asking for raw bytes usually precedes changing what the device is.
if (leaving_protocol_mode && this->tap_ != nullptr) {
this->tap_->on_protocol_disabled();
}
#endif
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
void SerialProxy::write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {
#ifdef USE_API
// Bytes from anyone but the live subscriber would interleave with the subscriber's
// traffic -- or with an active tap's -- on the wire
if (!this->is_subscriber_(api_connection)) {
if (this->api_connection_ != nullptr) {
ESP_LOGW(TAG, "Ignoring write from client that does not hold serial proxy [%" PRIu32 "]", this->instance_index_);
} else {
// A legacy client streaming writes without subscribing would flood WARN, one per
// request; writes are the only high-rate, unacknowledged operation, so keep this
// visible without drowning the log
ESP_LOGV(TAG, "Ignoring write from client without port subscription [%" PRIu32 "]", this->instance_index_);
}
// Bytes from a client other than the live subscriber would interleave with the
// subscriber's traffic on the wire
if (this->port_claimed_by_other_(api_connection)) {
ESP_LOGW(TAG, "Ignoring write from client without port access [%" PRIu32 "]", this->instance_index_);
return;
}
#endif
if (data == nullptr || len == 0)
return;
this->write_array(data, len);
#ifdef USE_SERIAL_PROXY_TAP
// After the write, so the tap observes the same ordering the device does
if (this->tap_observing_()) {
this->tap_->on_client_tx(data, len);
}
#endif
}
SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {
#ifdef USE_API
if (!this->is_subscriber_(api_connection)) {
ESP_LOGW(TAG, "Ignoring modem pin request from client without port subscription [%" PRIu32 "]",
this->instance_index_);
if (this->port_claimed_by_other_(api_connection)) {
ESP_LOGW(TAG, "Ignoring modem pin request from client without port access [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
#endif
@@ -341,8 +210,8 @@ uint32_t SerialProxy::get_modem_pins() const {
SerialProxyResult SerialProxy::flush_port(api::APIConnection *api_connection) {
#ifdef USE_API
// Flushing stalls the port, so it gets the same ownership check as writes
if (!this->is_subscriber_(api_connection)) {
ESP_LOGW(TAG, "Ignoring flush from client without port subscription [%" PRIu32 "]", this->instance_index_);
if (this->port_claimed_by_other_(api_connection)) {
ESP_LOGW(TAG, "Ignoring flush from client without port access [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
#endif
@@ -361,6 +230,11 @@ SerialProxyResult SerialProxy::flush_port(api::APIConnection *api_connection) {
}
#ifdef USE_API
bool SerialProxy::port_claimed_by_other_(api::APIConnection *api_connection) const {
return this->api_connection_ != nullptr && this->api_connection_ != api_connection &&
this->api_connection_->is_connection_setup();
}
SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_connection,
api::enums::SerialProxyRequestType type) {
switch (type) {
@@ -378,10 +252,6 @@ SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_conn
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription");
// End the dead client's session before starting the new one, so its mode
// cannot leak into a session that never asked for it
this->api_connection_ = nullptr;
this->reset_mode_();
}
this->api_connection_ = api_connection;
this->enable_loop();
@@ -394,15 +264,7 @@ SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_conn
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
this->api_connection_ = nullptr;
this->reset_mode_();
#ifdef USE_SERIAL_PROXY_TAP
// Keep the loop alive for a tap that still needs the port (mirrors loop())
if (this->tap_ == nullptr || !this->tap_->tap_needs_port()) {
this->disable_loop();
}
#else
this->disable_loop();
#endif
ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
default:
+3 -100
View File
@@ -26,7 +26,6 @@ class APIConnection;
namespace enums {
enum SerialProxyPortType : uint32_t;
enum SerialProxyRequestType : uint32_t;
enum SerialProxyMode : uint32_t;
} // namespace enums
} // namespace esphome::api
@@ -53,36 +52,6 @@ enum class SerialProxyResult : uint8_t {
/// Maximum bytes to read from UART in a single loop iteration
inline constexpr size_t SERIAL_PROXY_MAX_READ_SIZE = 256;
#ifdef USE_SERIAL_PROXY_TAP
/// Observes a port's traffic without owning it, and may inject bytes of its own.
///
/// This exists so protocol-aware behaviour can be layered onto a plain byte pipe without
/// the pipe knowing anything about the protocol: the tap is compiled in only when some
/// component asks for one, so a proxy carrying an RS485 meter pays nothing for it.
///
/// A tap is an observer, never a gatekeeper -- it cannot suppress or alter the bytes
/// flowing in either direction, so a misbehaving tap cannot corrupt the stream.
class SerialProxyTap {
public:
/// Bytes read from the device, before they are forwarded to any subscriber.
virtual void on_device_rx(const uint8_t *data, size_t len) = 0;
/// Bytes a subscriber sent towards the device, after they have been written.
virtual void on_client_tx(const uint8_t *data, size_t len) = 0;
/// True when the port must keep reading even with no subscriber attached, so a tap can
/// do its own protocol work while nobody is listening. Honoured only while no
/// subscriber holds the port; with one attached, the port mode alone decides.
virtual bool tap_needs_port() const = 0;
/// A client explicitly turned protocol handling off for this port. Distinct from the
/// automatic reset when a session ends: this one means a client intends to do something
/// else with the device -- reflash it, most likely -- so anything the tap believes about
/// it should be treated as suspect.
virtual void on_protocol_disabled() = 0;
};
#endif
class SerialProxy final : public uart::UARTDevice, public Component {
public:
void setup() override;
@@ -108,9 +77,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Get the port type
api::enums::SerialProxyPortType get_port_type() const { return this->port_type_; }
/// Handle a mode change requested by an API client
SerialProxyResult set_mode_from_client(api::APIConnection *api_connection, api::enums::SerialProxyMode mode);
/// Configure UART parameters and apply them
/// @param api_connection The API connection requesting the change
/// @param baudrate Baud rate in bits per second
@@ -155,67 +121,13 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Set the DTR GPIO pin (from YAML configuration)
void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; }
#ifdef USE_SERIAL_PROXY_TAP
/// Attach a traffic observer. At most one, set once at setup time.
void set_tap(SerialProxyTap *tap) { this->tap_ = tap; }
/// Write bytes originating from the tap rather than from a client. Bypasses the
/// subscriber ownership check, but only while the tap is being served bytes -- so a
/// port in RAW mode with a subscriber attached stays inert. Returns false when the
/// bytes were dropped for that reason.
bool write_from_tap(const uint8_t *data, size_t len) {
if (!this->tap_observing_()) {
return false;
}
this->write_array(data, len);
return true;
}
/// Whether the tap is currently being served bytes. Can flip false with no callback
/// (a subscriber attaching in RAW mode, say), so a tap should check before starting
/// protocol work and when a reply seems overdue.
bool tap_is_observed() const { return this->tap_observing_(); }
/// Resume reading after a tap's needs change. loop() disables itself when there is
/// neither a subscriber nor a tap that wants the port, so a tap starting fresh work
/// must ask for it back. Must be called from the main loop.
void tap_request_port() { this->enable_loop(); }
/// Whether the underlying device is present. On a USB UART this tracks enumeration, so
/// a tap can notice the device being unplugged and plugged back in.
bool is_device_connected() const { return this->parent_->is_connected(); }
/// Run one read-and-dispatch cycle immediately. Lets a tap make progress before the
/// main loop is running -- during setup, for instance, while a component is still
/// blocking on can_proceed(). Must not be called from on_device_rx() or
/// on_client_tx(): each nested cycle costs a 256-byte stack frame.
void tap_pump();
#endif
protected:
#ifdef USE_API
/// Read from UART, hand the bytes to any tap, and forward them to a subscriber
/// (slow path with a 256-byte stack buffer)
/// Read from UART and send to API client (slow path with 256-byte stack buffer)
void read_and_send_(size_t available);
/// True when the given connection is the live subscriber. Every port operation
/// (write, configure, modem pins, flush, mode) requires this, so an unsubscribed
/// client can never share the wire with the subscriber or an active tap.
bool is_subscriber_(api::APIConnection *api_connection) const { return this->api_connection_ == api_connection; }
#endif
#ifdef USE_SERIAL_PROXY_TAP
/// Return the port to RAW when a subscriber goes away, so the mode never outlives it
void reset_mode_();
#else
/// Without a tap, PROTOCOL is refused, so the mode is fixed at RAW and there is
/// nothing to reset
void reset_mode_() {}
#endif
#ifdef USE_SERIAL_PROXY_TAP
/// True when the tap should be shown the traffic passing through this port
bool tap_observing_() const;
/// True when a live subscriber other than the given connection holds the port
bool port_claimed_by_other_(api::APIConnection *api_connection) const;
#endif
/// Instance index for identifying this proxy in API messages
@@ -235,11 +147,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Port type
api::enums::SerialProxyPortType port_type_{};
#ifdef USE_SERIAL_PROXY_TAP
/// How the bytes passing through are treated; zero is SERIAL_PROXY_MODE_RAW
api::enums::SerialProxyMode mode_{};
#endif
/// Optional GPIO pins for modem control
GPIOPin *rts_pin_{nullptr};
GPIOPin *dtr_pin_{nullptr};
@@ -247,10 +154,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Current modem pin states
bool rts_state_{false};
bool dtr_state_{false};
#ifdef USE_SERIAL_PROXY_TAP
SerialProxyTap *tap_{nullptr};
#endif
};
} // namespace esphome::serial_proxy
@@ -1,5 +1,7 @@
"""Speaker Media Player Setup."""
import logging
from esphome import automation
import esphome.codegen as cg
from esphome.components import (
@@ -31,6 +33,9 @@ from esphome.const import (
CONF_TASK_STACK_IN_PSRAM,
)
_LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["audio"]
DEPENDENCIES = ["network"]
@@ -39,7 +44,7 @@ DOMAIN = "media_player"
CONF_ANNOUNCEMENT = "announcement"
CONF_ANNOUNCEMENT_PIPELINE = "announcement_pipeline"
CONF_CODEC_SUPPORT_ENABLED = "codec_support_enabled" # Remove before 2027.4.0
CONF_CODEC_SUPPORT_ENABLED = "codec_support_enabled" # Remove before 2026.10.0
CONF_ENQUEUE = "enqueue"
CONF_MEDIA_FILE = "media_file"
CONF_MEDIA_PIPELINE = "media_pipeline"
@@ -98,6 +103,15 @@ def _validate_repeated_speaker(config):
def _final_validate(config):
# Remove before 2026.10.0
if CONF_CODEC_SUPPORT_ENABLED in config:
_LOGGER.warning(
"'%s' is deprecated and will be removed in 2026.10.0. "
"Codec support is now automatically determined from the pipeline "
"'format' setting. Set format to 'NONE' to enable all codecs.",
CONF_CODEC_SUPPORT_ENABLED,
)
# Request codecs based on pipeline formats. Codecs needed by local files are
# already requested during CONFIG_SCHEMA validation (via audio_files_schema).
media_player.request_codecs_for_format_configs(
@@ -137,12 +151,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range(
min=4000, max=4000000
),
# Removed in 2026.10.0 - kept to provide helpful error message
cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.invalid(
"The 'codec_support_enabled' option has been removed in ESPHome 2026.10.0.\n"
"Codec support is now determined from the pipeline 'format' setting.\n"
"Set 'format: NONE' on the pipeline to enable all codecs."
),
# Remove before 2026.10.0
cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.Any(cv.boolean, cv.string),
cv.Optional(CONF_FILES): audio_file.audio_files_schema(),
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage,
-1
View File
@@ -181,7 +181,6 @@
#define USE_SENSOR
#define USE_SENSOR_FILTER
#define USE_SERIAL_PROXY
#define USE_SERIAL_PROXY_TAP
#define USE_SETUP_PRIORITY_OVERRIDE
#define USE_STATUS_LED
#define USE_STATUS_SENSOR
+1 -1
View File
@@ -11,7 +11,7 @@ tzdata>=2026.3 # from time
pyserial==3.5
platformio==6.1.19
esptool==5.4.0
click==8.3.3
click==8.5.0
aioesphomeapi==46.4.0
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
zeroconf==0.151.3
@@ -40,9 +40,6 @@ class SerialProxy {
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
void write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {}
SerialProxyResult set_mode_from_client(api::APIConnection *api_connection, api::enums::SerialProxyMode mode) {
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
SerialProxyResult set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
@@ -1,12 +0,0 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ap:
sendspin:
@@ -1,18 +0,0 @@
esphome:
name: test
project:
name: project_manufacturer.project_model
version: 9.9.9
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ap:
sendspin:
manufacturer: Explicit Manufacturer
model: Explicit Model
firmware_version: 1.2.3
@@ -1,15 +0,0 @@
esphome:
name: test
project:
name: project_manufacturer.project_model
version: 9.9.9
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ap:
sendspin:
@@ -1,83 +0,0 @@
"""Tests for the device information the sendspin hub reports to the server."""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome import config_validation as cv
from esphome.components.sendspin import (
CONF_FIRMWARE_VERSION,
CONF_MANUFACTURER,
CONFIG_SCHEMA,
)
from esphome.const import CONF_MODEL, PlatformFramework
from tests.component_tests.types import SetCoreConfigCallable
def test_explicit_device_info_wins_over_project(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Configured values take precedence over the project information."""
main_cpp = generate_main(component_config_path("device_info_explicit.yaml"))
assert 'set_manufacturer("Explicit Manufacturer")' in main_cpp
assert 'set_model("Explicit Model")' in main_cpp
assert 'set_firmware_version("1.2.3")' in main_cpp
def test_project_supplies_device_info(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Without configured values, the project name splits into manufacturer and model."""
main_cpp = generate_main(component_config_path("device_info_project.yaml"))
assert 'set_manufacturer("project_manufacturer")' in main_cpp
assert 'set_model("project_model")' in main_cpp
assert 'set_firmware_version("9.9.9")' in main_cpp
def test_no_device_info_leaves_hub_defaults(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""With neither source, nothing is emitted and the hub keeps its own defaults."""
main_cpp = generate_main(component_config_path("device_info_default.yaml"))
assert "set_manufacturer(" not in main_cpp
assert "set_model(" not in main_cpp
assert "set_firmware_version(" not in main_cpp
@pytest.mark.parametrize(
"conf_key", [CONF_MANUFACTURER, CONF_MODEL, CONF_FIRMWARE_VERSION]
)
def test_empty_device_info_rejected(
set_core_config: SetCoreConfigCallable, conf_key: str
) -> None:
"""An empty string would be sent to the server as an empty value, so it is not accepted."""
set_core_config(PlatformFramework.ESP32_IDF)
with pytest.raises(cv.Invalid):
CONFIG_SCHEMA({conf_key: ""})
@pytest.mark.parametrize(
"conf_key", [CONF_MANUFACTURER, CONF_MODEL, CONF_FIRMWARE_VERSION]
)
def test_device_info_capped_at_127_bytes(
set_core_config: SetCoreConfigCallable, conf_key: str
) -> None:
"""The cap is in bytes so the protobuf length prefix stays a single byte."""
set_core_config(PlatformFramework.ESP32_IDF)
CONFIG_SCHEMA({conf_key: "a" * 127})
with pytest.raises(cv.Invalid):
CONFIG_SCHEMA({conf_key: "a" * 128})
# 64 two-byte characters is 128 bytes.
with pytest.raises(cv.Invalid):
CONFIG_SCHEMA({conf_key: "é" * 64})
@@ -4,6 +4,3 @@ psram:
sendspin:
id: sendspin_hub_id
task_stack_in_psram: true
manufacturer: Test Manufacturer
model: Test Model
firmware_version: 1.2.3
@@ -1,14 +0,0 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
# Compile the tap code paths; no tap is attached, so this exercises the
# null-tap branches that a normal build never defines.
esphome:
platformio_options:
build_flags:
- "-DUSE_SERIAL_PROXY_TAP"
packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
serial_proxy: !include common.yaml