From 8d8f70595b1b94687046bb0d0d28f93c4ac727a1 Mon Sep 17 00:00:00 2001 From: kbx81 Date: Wed, 12 Aug 2026 00:40:48 -0500 Subject: [PATCH] [api] Acknowledge proxy subscribe and configuration requests --- esphome/components/api/api.proto | 25 +++++- esphome/components/api/api_connection.cpp | 87 +++++++++++++++---- esphome/components/api/api_pb2.cpp | 12 +++ esphome/components/api/api_pb2.h | 25 ++++++ esphome/components/api/api_pb2_dump.cpp | 24 +++++ .../components/serial_proxy/serial_proxy.cpp | 58 +++++++------ .../components/serial_proxy/serial_proxy.h | 25 ++++-- .../components/zwave_proxy/zwave_proxy.cpp | 16 ++-- esphome/components/zwave_proxy/zwave_proxy.h | 3 +- 9 files changed, 215 insertions(+), 60 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index f1bc9b003a..dc85c27bdb 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2622,6 +2622,21 @@ message ZWaveProxyRequest { bytes data = 2; } +enum ZWaveProxyStatus { + ZWAVE_PROXY_STATUS_OK = 0; // Request completed successfully + ZWAVE_PROXY_STATUS_IN_USE = 1; // Denied: another client is already subscribed +} + +// Acknowledges a ZWaveProxyRequest (subscribe/unsubscribe). Sent since API 1.16. +message ZWaveProxyRequestResponse { + option (id) = 151; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_ZWAVE_PROXY"; + + ZWaveProxyRequestType type = 1; // Which request type this responds to + ZWaveProxyStatus status = 2; // Result status +} + // ==================== INFRARED ==================== // Note: Feature and capability flag enums are defined in // esphome/components/infrared/infrared.h @@ -2771,6 +2786,10 @@ enum SerialProxyRequestType { SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; // Subscribe to receive data from this serial proxy instance SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; // Unsubscribe from this serial proxy instance SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent) + // Values below are only valid in SerialProxyRequestResponse.type, identifying which + // operation is being acknowledged; they must not be sent in SerialProxyRequest.type. + SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest + SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest } enum SerialProxyStatus { @@ -2779,6 +2798,8 @@ enum SerialProxyStatus { SERIAL_PROXY_STATUS_ERROR = 2; // Driver or hardware error SERIAL_PROXY_STATUS_TIMEOUT = 3; // Timed out before TX completed SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4; // Request type not supported by this instance + SERIAL_PROXY_STATUS_PORT_IN_USE = 5; // Denied: another client holds the port + SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value } // Generic request message for simple serial proxy operations @@ -2791,7 +2812,9 @@ message SerialProxyRequest { SerialProxyRequestType type = 2; // Request type } -// Response to a SerialProxyRequest (e.g. flush completion or failure) +// Acknowledges a serial proxy operation (subscribe, unsubscribe, flush, +// configure, or modem pin change). Sent since API 1.16; the type field +// identifies which operation is being acknowledged. message SerialProxyRequestResponse { option (id) = 147; option (source) = SOURCE_SERVER; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d05f98d03b..2954206e90 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1384,7 +1384,17 @@ void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { } void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { - zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type); + enums::ZWaveProxyStatus status = zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type); + // Only subscribe/unsubscribe are acknowledged (other types are server-to-client notifications) + if (msg.type == enums::ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE || + msg.type == enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE) { + ZWaveProxyRequestResponse resp{}; + resp.type = msg.type; + resp.status = status; + if (!this->send_message(resp)) { + API_LOG_MSG_DROPPED(TAG, "Z-Wave proxy response"); + } + } } #endif @@ -1553,15 +1563,46 @@ void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent #endif #ifdef USE_SERIAL_PROXY +static enums::SerialProxyStatus serial_proxy_result_to_status(serial_proxy::SerialProxyResult result) { + switch (result) { + case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_OK: + return enums::SERIAL_PROXY_STATUS_OK; + case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE: + return enums::SERIAL_PROXY_STATUS_PORT_IN_USE; + case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT: + return enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT; + case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED: + return enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED; + case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_ERROR: + default: + return enums::SERIAL_PROXY_STATUS_ERROR; + } +} + +static void send_serial_proxy_ack(APIConnection *conn, uint32_t instance, enums::SerialProxyRequestType type, + enums::SerialProxyStatus status) { + SerialProxyRequestResponse resp{}; + resp.instance = instance; + resp.type = type; + resp.status = status; + if (!conn->send_message(resp)) { + API_LOG_MSG_DROPPED(TAG, "Serial proxy response"); + } +} + void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) { auto &proxies = App.get_serial_proxies(); if (msg.instance >= proxies.size()) { ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range (max %" PRIu32 ")", msg.instance, static_cast(proxies.size())); + send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE, + enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT); return; } - proxies[msg.instance]->configure(this, msg.baudrate, msg.flow_control, static_cast(msg.parity), - msg.stop_bits, msg.data_size); + serial_proxy::SerialProxyResult result = proxies[msg.instance]->configure( + this, msg.baudrate, msg.flow_control, static_cast(msg.parity), msg.stop_bits, msg.data_size); + send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE, + serial_proxy_result_to_status(result)); } void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) { @@ -1577,9 +1618,13 @@ void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetM 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_MODEM_PINS, + enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT); return; } - proxies[msg.instance]->set_modem_pins(this, msg.line_states); + serial_proxy::SerialProxyResult result = proxies[msg.instance]->set_modem_pins(this, msg.line_states); + send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS, + serial_proxy_result_to_status(result)); } void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) { @@ -1600,40 +1645,44 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &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, msg.type, enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT); return; } + auto *proxy = proxies[msg.instance]; + enums::SerialProxyStatus status; switch (msg.type) { case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: - proxies[msg.instance]->serial_proxy_request(this, msg.type); + status = serial_proxy_result_to_status(proxy->serial_proxy_request(this, msg.type)); break; - case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: { - SerialProxyRequestResponse resp{}; - resp.instance = msg.instance; - resp.type = enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH; - switch (proxies[msg.instance]->flush_port()) { + case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: + // Flushing stalls the port, so it gets the same ownership check as writes + if (proxy->port_claimed_by_other(this)) { + status = enums::SERIAL_PROXY_STATUS_PORT_IN_USE; + break; + } + switch (proxy->flush_port()) { case uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS: - resp.status = enums::SERIAL_PROXY_STATUS_OK; + status = enums::SERIAL_PROXY_STATUS_OK; break; case uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS: - resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS; + status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS; break; case uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT: - resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT; + status = enums::SERIAL_PROXY_STATUS_TIMEOUT; break; case uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED: - resp.status = enums::SERIAL_PROXY_STATUS_ERROR; + default: + status = enums::SERIAL_PROXY_STATUS_ERROR; break; } - if (!this->send_message(resp)) { - API_LOG_MSG_DROPPED(TAG, "Serial proxy response"); - } break; - } default: ESP_LOGW(TAG, "Unknown serial proxy request type: %" PRIu32, static_cast(msg.type)); + status = enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED; break; } + send_serial_proxy_ack(this, msg.instance, msg.type, status); } void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) { @@ -1760,7 +1809,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 15; + 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()); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 1b8c6b05bd..9203c3d508 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3945,6 +3945,18 @@ uint32_t ZWaveProxyRequest::calculate_size() const { size += ProtoSize::calc_length(1, this->data_len); return size; } +uint8_t *ZWaveProxyRequestResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast(this->type)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast(this->status)); + return pos; +} +uint32_t ZWaveProxyRequestResponse::calculate_size() const { + uint32_t size = 0; + size += this->type ? 2 : 0; + size += this->status ? 2 : 0; + return size; +} #endif #ifdef USE_INFRARED uint8_t *ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 8335dae1f2..90a3c2505b 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -334,6 +334,10 @@ enum ZWaveProxyRequestType : uint32_t { ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2, }; +enum ZWaveProxyStatus : uint32_t { + ZWAVE_PROXY_STATUS_OK = 0, + ZWAVE_PROXY_STATUS_IN_USE = 1, +}; #endif #ifdef USE_SERIAL_PROXY enum SerialProxyParity : uint32_t { @@ -345,6 +349,8 @@ enum SerialProxyRequestType : uint32_t { SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0, SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2, + SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3, + SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4, }; enum SerialProxyStatus : uint32_t { SERIAL_PROXY_STATUS_OK = 0, @@ -352,6 +358,8 @@ enum SerialProxyStatus : uint32_t { SERIAL_PROXY_STATUS_ERROR = 2, SERIAL_PROXY_STATUS_TIMEOUT = 3, SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4, + SERIAL_PROXY_STATUS_PORT_IN_USE = 5, + SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6, }; #endif @@ -3130,6 +3138,23 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; }; +class ZWaveProxyRequestResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 151; + static constexpr uint8_t ESTIMATED_SIZE = 4; +#ifdef HAS_PROTO_MESSAGE_DUMP + const LogString *message_name() const override { return LOG_STR("z_wave_proxy_request_response"); } +#endif + enums::ZWaveProxyRequestType type{}; + enums::ZWaveProxyStatus status{}; + uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; #endif #ifdef USE_INFRARED class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 4d5829e45d..6bfff196d2 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -816,6 +816,16 @@ template<> const char *proto_enum_to_string(enums: return ESPHOME_PSTR("UNKNOWN"); } } +template<> const char *proto_enum_to_string(enums::ZWaveProxyStatus value) { + switch (value) { + case enums::ZWAVE_PROXY_STATUS_OK: + return ESPHOME_PSTR("ZWAVE_PROXY_STATUS_OK"); + case enums::ZWAVE_PROXY_STATUS_IN_USE: + return ESPHOME_PSTR("ZWAVE_PROXY_STATUS_IN_USE"); + default: + return ESPHOME_PSTR("UNKNOWN"); + } +} #endif #ifdef USE_SERIAL_PROXY template<> const char *proto_enum_to_string(enums::SerialProxyParity value) { @@ -838,6 +848,10 @@ template<> const char *proto_enum_to_string(enums return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE"); case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_FLUSH"); + case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE: + 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"); default: return ESPHOME_PSTR("UNKNOWN"); } @@ -854,6 +868,10 @@ template<> const char *proto_enum_to_string(enums::Ser return ESPHOME_PSTR("SERIAL_PROXY_STATUS_TIMEOUT"); case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED: return ESPHOME_PSTR("SERIAL_PROXY_STATUS_NOT_SUPPORTED"); + case enums::SERIAL_PROXY_STATUS_PORT_IN_USE: + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_PORT_IN_USE"); + case enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT: + return ESPHOME_PSTR("SERIAL_PROXY_STATUS_INVALID_ARGUMENT"); default: return ESPHOME_PSTR("UNKNOWN"); } @@ -2644,6 +2662,12 @@ const char *ZWaveProxyRequest::dump_to(DumpBuffer &out) const { dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len); return out.c_str(); } +const char *ZWaveProxyRequestResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("ZWaveProxyRequestResponse")); + dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); + dump_field(out, ESPHOME_PSTR("status"), static_cast(this->status)); + return out.c_str(); +} #endif #ifdef USE_INFRARED const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const { diff --git a/esphome/components/serial_proxy/serial_proxy.cpp b/esphome/components/serial_proxy/serial_proxy.cpp index 4b3a907416..b0127114a1 100644 --- a/esphome/components/serial_proxy/serial_proxy.cpp +++ b/esphome/components/serial_proxy/serial_proxy.cpp @@ -89,12 +89,12 @@ void SerialProxy::dump_config() { this->dtr_pin_ != nullptr ? "configured" : "not configured"); } -void SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity, - uint8_t stop_bits, uint8_t data_size) { +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->port_claimed_by_other_(api_connection)) { + if (this->port_claimed_by_other(api_connection)) { ESP_LOGW(TAG, "Ignoring configure request from client without port access [%" PRIu32 "]", this->instance_index_); - return; + return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE; } #endif ESP_LOGD(TAG, @@ -105,25 +105,29 @@ void SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrat auto *uart_comp = this->parent_; if (uart_comp == nullptr) { ESP_LOGE(TAG, "UART component not available"); - return; + return SerialProxyResult::SERIAL_PROXY_RESULT_ERROR; } // Validate all parameters before applying any (values come from a remote client) if (baudrate == 0) { ESP_LOGW(TAG, "Invalid baud rate: 0"); - return; + return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT; } if (stop_bits < 1 || stop_bits > 2) { ESP_LOGW(TAG, "Invalid stop bits: %u (must be 1 or 2)", stop_bits); - return; + return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT; } if (data_size < 5 || data_size > 8) { ESP_LOGW(TAG, "Invalid data bits: %u (must be 5-8)", data_size); - return; + return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT; } if (parity > 2) { ESP_LOGW(TAG, "Invalid parity: %u (must be 0-2)", parity); - return; + return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT; + } + if (flow_control) { + ESP_LOGW(TAG, "Hardware flow control requested but is not yet supported"); + return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED; } // Apply validated parameters @@ -143,17 +147,14 @@ void SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrat #if defined(USE_ESP8266) || defined(USE_ESP32) uart_comp->load_settings(true); #endif - - if (flow_control) { - ESP_LOGW(TAG, "Hardware flow control requested but is not yet supported"); - } + 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 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)) { + if (this->port_claimed_by_other(api_connection)) { ESP_LOGW(TAG, "Ignoring write from client without port access [%" PRIu32 "]", this->instance_index_); return; } @@ -163,13 +164,17 @@ void SerialProxy::write_from_client(api::APIConnection *api_connection, const ui this->write_array(data, len); } -void SerialProxy::set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) { +SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) { #ifdef USE_API - if (this->port_claimed_by_other_(api_connection)) { + 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; + return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE; } #endif + if (this->rts_pin_ == nullptr && this->dtr_pin_ == nullptr) { + ESP_LOGW(TAG, "No modem pins configured on serial proxy [%" PRIu32 "]", this->instance_index_); + return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED; + } const bool rts = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_RTS) != 0; const bool dtr = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_DTR) != 0; ESP_LOGV(TAG, "Setting modem pins [%" PRIu32 "]: RTS=%s, DTR=%s", this->instance_index_, ONOFF(rts), ONOFF(dtr)); @@ -182,6 +187,7 @@ void SerialProxy::set_modem_pins(api::APIConnection *api_connection, uint32_t li this->dtr_state_ = dtr; this->dtr_pin_->digital_write(dtr); } + return SerialProxyResult::SERIAL_PROXY_RESULT_OK; } uint32_t SerialProxy::get_modem_pins() const { @@ -195,17 +201,18 @@ uart::UARTFlushResult SerialProxy::flush_port() { } #ifdef USE_API -bool SerialProxy::port_claimed_by_other_(api::APIConnection *api_connection) const { +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(); } -void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type) { +SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_connection, + api::enums::SerialProxyRequestType type) { switch (type) { case api::enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: if (this->api_connection_ == api_connection) { ESP_LOGV(TAG, "API connection is already subscribed to serial proxy [%" PRIu32 "]", this->instance_index_); - return; + return SerialProxyResult::SERIAL_PROXY_RESULT_OK; } if (this->api_connection_ != nullptr) { // A living subscriber keeps exclusive access. Its connection may be dead without @@ -213,26 +220,27 @@ void SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api:: // in that case let the new client take over instead of locking it out. if (this->api_connection_->is_connection_setup()) { ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); - return; + return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE; } ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription"); } this->api_connection_ = api_connection; this->enable_loop(); ESP_LOGV(TAG, "API connection subscribed to serial proxy [%" PRIu32 "]", this->instance_index_); - break; + return SerialProxyResult::SERIAL_PROXY_RESULT_OK; case api::enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + // Unsubscribe is idempotent: not being subscribed is not an error if (this->api_connection_ != api_connection) { ESP_LOGV(TAG, "API connection is not subscribed to serial proxy [%" PRIu32 "]", this->instance_index_); - return; + return SerialProxyResult::SERIAL_PROXY_RESULT_OK; } this->api_connection_ = nullptr; this->disable_loop(); ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%" PRIu32 "]", this->instance_index_); - break; + return SerialProxyResult::SERIAL_PROXY_RESULT_OK; default: ESP_LOGW(TAG, "Unknown serial proxy request type: %" PRIu32, static_cast(type)); - break; + return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED; } } #endif diff --git a/esphome/components/serial_proxy/serial_proxy.h b/esphome/components/serial_proxy/serial_proxy.h index 268c1b52be..cd32e17e47 100644 --- a/esphome/components/serial_proxy/serial_proxy.h +++ b/esphome/components/serial_proxy/serial_proxy.h @@ -38,6 +38,15 @@ enum SerialProxyLineStateFlag : uint32_t { SERIAL_PROXY_LINE_STATE_FLAG_DTR = 1 << 1, ///< DTR (Data Terminal Ready) }; +/// Result of a client-initiated operation; mapped to api::enums::SerialProxyStatus by the API layer +enum class SerialProxyResult : uint8_t { + SERIAL_PROXY_RESULT_OK, ///< Operation completed or request accepted + SERIAL_PROXY_RESULT_PORT_IN_USE, ///< Denied: another live client holds the port + SERIAL_PROXY_RESULT_INVALID_ARGUMENT, ///< A parameter value is out of range + SERIAL_PROXY_RESULT_ERROR, ///< Driver or hardware error + SERIAL_PROXY_RESULT_NOT_SUPPORTED, ///< Requested feature is not available on this instance +}; + /// Maximum bytes to read from UART in a single loop iteration inline constexpr size_t SERIAL_PROXY_MAX_READ_SIZE = 256; @@ -73,14 +82,14 @@ class SerialProxy final : public uart::UARTDevice, public Component { /// @param parity Parity setting (0=none, 1=even, 2=odd) /// @param stop_bits Number of stop bits (1 or 2) /// @param data_size Number of data bits (5-8) - void configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity, - uint8_t stop_bits, uint8_t data_size); + SerialProxyResult configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity, + uint8_t stop_bits, uint8_t data_size); /// Get the currently subscribed API connection (nullptr if none) api::APIConnection *get_api_connection() { return this->api_connection_; } /// Handle a subscribe/unsubscribe request from an API client - void serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type); + SerialProxyResult serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type); /// Write data received from an API client to the serial device /// @param api_connection The API connection sending the data @@ -89,7 +98,7 @@ class SerialProxy final : public uart::UARTDevice, public Component { void write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len); /// Set modem pin states from a bitmask of SerialProxyLineStateFlag values - void set_modem_pins(api::APIConnection *api_connection, uint32_t line_states); + SerialProxyResult set_modem_pins(api::APIConnection *api_connection, uint32_t line_states); /// Get current modem pin states as a bitmask of SerialProxyLineStateFlag values uint32_t get_modem_pins() const; @@ -97,6 +106,11 @@ class SerialProxy final : public uart::UARTDevice, public Component { /// Flush the serial port (block until all TX data is sent) uart::UARTFlushResult flush_port(); +#ifdef USE_API + /// True when a live subscriber other than the given connection holds the port + bool port_claimed_by_other(api::APIConnection *api_connection) const; +#endif + /// Set the RTS GPIO pin (from YAML configuration) void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; } @@ -107,9 +121,6 @@ class SerialProxy final : public uart::UARTDevice, public Component { #ifdef USE_API /// Read from UART and send to API client (slow path with 256-byte stack buffer) void read_and_send_(size_t available); - - /// 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 diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index 6e3f109ca1..3844fca00f 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -194,12 +194,13 @@ void ZWaveProxy::api_connection_authenticated(api::APIConnection *conn) { } } -void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type) { +api::enums::ZWaveProxyStatus ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, + api::enums::ZWaveProxyRequestType type) { switch (type) { case api::enums::ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE: if (this->api_connection_ == api_connection) { ESP_LOGV(TAG, "API connection is already subscribed"); - return; + return api::enums::ZWAVE_PROXY_STATUS_OK; } if (this->api_connection_ != nullptr) { // A living subscriber keeps exclusive access. Its connection may be dead without @@ -207,25 +208,26 @@ void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::en // in that case let the new client take over instead of locking it out. if (this->api_connection_->is_connection_setup()) { ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); - return; + return api::enums::ZWAVE_PROXY_STATUS_IN_USE; } ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription"); } this->api_connection_ = api_connection; ESP_LOGV(TAG, "API connection is now subscribed"); - break; + return api::enums::ZWAVE_PROXY_STATUS_OK; case api::enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + // Unsubscribe is idempotent: not being subscribed is not an error if (this->api_connection_ != api_connection) { ESP_LOGV(TAG, "API connection is not subscribed"); - return; + return api::enums::ZWAVE_PROXY_STATUS_OK; } this->api_connection_ = nullptr; - break; + return api::enums::ZWAVE_PROXY_STATUS_OK; default: ESP_LOGW(TAG, "Unknown request type: %" PRIu32, static_cast(type)); - break; + return api::enums::ZWAVE_PROXY_STATUS_OK; } } diff --git a/esphome/components/zwave_proxy/zwave_proxy.h b/esphome/components/zwave_proxy/zwave_proxy.h index cb60139ef8..75225d84a8 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.h +++ b/esphome/components/zwave_proxy/zwave_proxy.h @@ -60,7 +60,8 @@ class ZWaveProxy final : public uart::UARTDevice, public Component { bool can_proceed() override; void api_connection_authenticated(api::APIConnection *conn); - void zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type); + api::enums::ZWaveProxyStatus zwave_proxy_request(api::APIConnection *api_connection, + api::enums::ZWaveProxyRequestType type); api::APIConnection *get_api_connection() { return this->api_connection_; } uint32_t get_feature_flags() const { return ZWaveProxyFeature::FEATURE_ZWAVE_PROXY_ENABLED; }