diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 6ea124d155..1942ff568b 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -232,6 +232,7 @@ enum SerialProxyPortType { message SerialProxyInfo { string name = 1; // Human-readable port name SerialProxyPortType port_type = 2; // Port type (RS232, RS485) + uint32 configured_line_states = 3; // Bitmask of SerialProxyLineStateFlags this instance can drive } // DeviceInfoResponse max_data_length values: @@ -2626,6 +2627,22 @@ 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 + ZWAVE_PROXY_STATUS_NOT_SUPPORTED = 2; // Request type not supported +} + +// 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 @@ -2769,12 +2786,18 @@ message SerialProxyGetModemPinsResponse { uint32 instance = 1; // Instance index (0-based) uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags + SerialProxyStatus status = 3; // INVALID_ARGUMENT if the instance index is out of range (since API 1.16) } 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. Sending them in SerialProxyRequest.type is an + // 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 } enum SerialProxyStatus { @@ -2783,6 +2806,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 @@ -2795,7 +2820,9 @@ message SerialProxyRequest { SerialProxyRequestType type = 2; // Request type } -// Response to a SerialProxyRequest (e.g. flush completion or failure) +// Acknowledges a serial proxy operation; the type field identifies which +// operation is being acknowledged. Flush has been acknowledged since the +// message was introduced; all other acknowledgements are sent since API 1.16. 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 bb6c1695dd..05abbf0b75 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1381,7 +1381,12 @@ 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); + ZWaveProxyRequestResponse resp{}; + resp.type = msg.type; + resp.status = zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type); + if (!this->send_message(resp)) { + API_LOG_MSG_DROPPED(TAG, "Z-Wave proxy response"); + } } #endif @@ -1550,15 +1555,50 @@ 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_ASSUMED_SUCCESS: + return enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS; + 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_TIMEOUT: + return enums::SERIAL_PROXY_STATUS_TIMEOUT; + case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED: + return enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED; + case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_ERROR: + return enums::SERIAL_PROXY_STATUS_ERROR; + } + return enums::SERIAL_PROXY_STATUS_ERROR; // Unreachable; all enum values handled above +} + +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) { @@ -1574,20 +1614,30 @@ 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) { auto &proxies = App.get_serial_proxies(); - if (msg.instance >= proxies.size()) { - ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); - return; - } SerialProxyGetModemPinsResponse resp{}; resp.instance = msg.instance; - resp.line_states = proxies[msg.instance]->get_modem_pins(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); + // Pre-1.16 clients do not read the status field and would take this error + // for a successful "both pins deasserted" answer; let them time out as before + if (!this->client_supports_api_version(1, 16)) { + return; + } + resp.status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT; + } else { + resp.line_states = proxies[msg.instance]->get_modem_pins(); + } if (!this->send_message(resp)) { API_LOG_MSG_DROPPED(TAG, "Serial proxy response"); } @@ -1597,40 +1647,31 @@ 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 uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS: - resp.status = enums::SERIAL_PROXY_STATUS_OK; - break; - case uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS: - resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS; - break; - case uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT: - resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT; - break; - case uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED: - resp.status = enums::SERIAL_PROXY_STATUS_ERROR; - break; - } - if (!this->send_message(resp)) { - API_LOG_MSG_DROPPED(TAG, "Serial proxy response"); - } + case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: + status = serial_proxy_result_to_status(proxy->flush_port(this)); + break; + case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE: + case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS: + // Response-only discriminators; never valid in a request + ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast(msg.type)); + status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT; 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) { @@ -1757,7 +1798,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()); @@ -1891,6 +1932,7 @@ bool APIConnection::send_device_info_response_() { auto &info = resp.serial_proxies[serial_proxy_index++]; info.name = StringRef(proxy->get_name()); info.port_type = proxy->get_port_type(); + info.configured_line_states = proxy->get_configured_modem_pins(); } #endif #ifdef USE_API_NOISE @@ -1951,6 +1993,7 @@ bool APIConnection::send_device_capabilities_response_() { auto &info = resp.serial_proxies[serial_proxy_index++]; info.name = StringRef(proxy->get_name()); info.port_type = proxy->get_port_type(); + info.configured_line_states = proxy->get_configured_modem_pins(); } #endif return this->send_message(resp); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 33611c5ee1..b5062f9e9f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -102,12 +102,14 @@ uint8_t *SerialProxyInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PAR uint8_t *__restrict__ pos = buffer.get_pos(); ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name); ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast(this->port_type)); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->configured_line_states); return pos; } uint32_t SerialProxyInfo::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->name.size()); size += this->port_type ? 2 : 0; + size += ProtoSize::calc_uint32(1, this->configured_line_states); return size; } #endif @@ -3942,6 +3944,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 { @@ -4184,12 +4198,14 @@ uint8_t *SerialProxyGetModemPinsResponse::encode(ProtoWriteBuffer &buffer PROTO_ uint8_t *__restrict__ pos = buffer.get_pos(); ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance); ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->line_states); + ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, static_cast(this->status)); return pos; } uint32_t SerialProxyGetModemPinsResponse::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_uint32(1, this->instance); size += ProtoSize::calc_uint32(1, this->line_states); + size += this->status ? 2 : 0; return size; } bool SerialProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 13db857467..cd2f32deaf 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -334,6 +334,11 @@ 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, + ZWAVE_PROXY_STATUS_NOT_SUPPORTED = 2, +}; #endif #ifdef USE_SERIAL_PROXY enum SerialProxyParity : uint32_t { @@ -345,6 +350,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 +359,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 @@ -523,6 +532,7 @@ class SerialProxyInfo final : public ProtoMessage { public: StringRef name{}; enums::SerialProxyPortType port_type{}; + uint32_t configured_line_states{0}; uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -3130,6 +3140,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 { @@ -3314,12 +3341,13 @@ class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage { class SerialProxyGetModemPinsResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 143; - static constexpr uint8_t ESTIMATED_SIZE = 8; + static constexpr uint8_t ESTIMATED_SIZE = 10; #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("serial_proxy_get_modem_pins_response"); } #endif uint32_t instance{0}; uint32_t line_states{0}; + enums::SerialProxyStatus status{}; uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index d54215ba2e..9f53438531 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -816,6 +816,18 @@ 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"); + case enums::ZWAVE_PROXY_STATUS_NOT_SUPPORTED: + return ESPHOME_PSTR("ZWAVE_PROXY_STATUS_NOT_SUPPORTED"); + default: + return ESPHOME_PSTR("UNKNOWN"); + } +} #endif #ifdef USE_SERIAL_PROXY template<> const char *proto_enum_to_string(enums::SerialProxyParity value) { @@ -838,6 +850,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 +870,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"); } @@ -914,6 +934,7 @@ const char *SerialProxyInfo::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyInfo")); dump_field(out, ESPHOME_PSTR("name"), this->name); dump_field(out, ESPHOME_PSTR("port_type"), static_cast(this->port_type)); + dump_field(out, ESPHOME_PSTR("configured_line_states"), this->configured_line_states); return out.c_str(); } #endif @@ -2644,6 +2665,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 { @@ -2753,6 +2780,7 @@ const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetModemPinsResponse")); dump_field(out, ESPHOME_PSTR("instance"), this->instance); dump_field(out, ESPHOME_PSTR("line_states"), this->line_states); + dump_field(out, ESPHOME_PSTR("status"), static_cast(this->status)); return out.c_str(); } const char *SerialProxyRequest::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..94cefc8700 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)) { 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,10 +147,7 @@ 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) { @@ -163,13 +164,20 @@ 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)) { 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 + // Asserting a pin that is not configured must fail so the client learns the signal never + // reached the wire; deasserting an absent pin is harmless and stays allowed. Clients can + // avoid this by masking against SerialProxyInfo.configured_line_states. + if ((line_states & ~this->get_configured_modem_pins()) != 0) { + ESP_LOGW(TAG, "Requested modem pin not 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 +190,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 { @@ -189,9 +198,26 @@ uint32_t SerialProxy::get_modem_pins() const { (this->dtr_state_ ? static_cast(SERIAL_PROXY_LINE_STATE_FLAG_DTR) : 0u); } -uart::UARTFlushResult SerialProxy::flush_port() { +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->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 ESP_LOGV(TAG, "Flushing serial proxy [%" PRIu32 "]", this->instance_index_); - return this->flush(); + switch (this->flush()) { + case uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS: + return SerialProxyResult::SERIAL_PROXY_RESULT_OK; + case uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS: + return SerialProxyResult::SERIAL_PROXY_RESULT_ASSUMED_SUCCESS; + case uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT: + return SerialProxyResult::SERIAL_PROXY_RESULT_TIMEOUT; + case uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED: + return SerialProxyResult::SERIAL_PROXY_RESULT_ERROR; + } + return SerialProxyResult::SERIAL_PROXY_RESULT_ERROR; // Unreachable; all enum values handled above } #ifdef USE_API @@ -200,12 +226,13 @@ bool SerialProxy::port_claimed_by_other_(api::APIConnection *api_connection) con 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 +240,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..a0e47ee686 100644 --- a/esphome/components/serial_proxy/serial_proxy.h +++ b/esphome/components/serial_proxy/serial_proxy.h @@ -38,6 +38,17 @@ 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_ASSUMED_SUCCESS, ///< Platform cannot confirm TX drain; success assumed + 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_TIMEOUT, ///< Timed out before TX completed + 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 +84,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,13 +100,20 @@ 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; + /// Get the modem pins this instance can drive as a bitmask of SerialProxyLineStateFlag values + uint32_t get_configured_modem_pins() const { + return (this->rts_pin_ != nullptr ? static_cast(SERIAL_PROXY_LINE_STATE_FLAG_RTS) : 0u) | + (this->dtr_pin_ != nullptr ? static_cast(SERIAL_PROXY_LINE_STATE_FLAG_DTR) : 0u); + } + /// Flush the serial port (block until all TX data is sent) - uart::UARTFlushResult flush_port(); + /// @param api_connection The API connection requesting the flush + SerialProxyResult flush_port(api::APIConnection *api_connection); /// Set the RTS GPIO pin (from YAML configuration) void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; } diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index 6e3f109ca1..68750295e1 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_NOT_SUPPORTED; } } 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; } diff --git a/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h b/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h index d8b068fb36..6fc20f3350 100644 --- a/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h +++ b/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h @@ -13,17 +13,18 @@ namespace api { class APIConnection; } // namespace api -namespace uart { -enum class UARTFlushResult : uint8_t { - UART_FLUSH_RESULT_SUCCESS, - UART_FLUSH_RESULT_ASSUMED_SUCCESS, - UART_FLUSH_RESULT_TIMEOUT, - UART_FLUSH_RESULT_FAILED, -}; -} // namespace uart - namespace serial_proxy { +enum class SerialProxyResult : uint8_t { + SERIAL_PROXY_RESULT_OK, + SERIAL_PROXY_RESULT_ASSUMED_SUCCESS, + SERIAL_PROXY_RESULT_PORT_IN_USE, + SERIAL_PROXY_RESULT_INVALID_ARGUMENT, + SERIAL_PROXY_RESULT_ERROR, + SERIAL_PROXY_RESULT_TIMEOUT, + SERIAL_PROXY_RESULT_NOT_SUPPORTED, +}; + class SerialProxy { public: void set_instance_index(uint32_t index) { this->instance_index_ = index; } @@ -31,13 +32,20 @@ class SerialProxy { const char *get_name() const { return ""; } api::enums::SerialProxyPortType get_port_type() const { return {}; } api::APIConnection *get_api_connection() { return nullptr; } - void serial_proxy_request(api::APIConnection *conn, api::enums::SerialProxyRequestType type) {} - void configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity, - uint32_t stop_bits, uint32_t data_size) {} + SerialProxyResult serial_proxy_request(api::APIConnection *conn, api::enums::SerialProxyRequestType type) { + return SerialProxyResult::SERIAL_PROXY_RESULT_OK; + } + SerialProxyResult configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity, + uint8_t stop_bits, uint8_t data_size) { + return SerialProxyResult::SERIAL_PROXY_RESULT_OK; + } void write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {} - void set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {} + SerialProxyResult set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) { + return SerialProxyResult::SERIAL_PROXY_RESULT_OK; + } uint32_t get_modem_pins() const { return 0; } - uart::UARTFlushResult flush_port() { return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; } + uint32_t get_configured_modem_pins() const { return 0; } + SerialProxyResult flush_port(api::APIConnection *api_connection) { return SerialProxyResult::SERIAL_PROXY_RESULT_OK; } protected: uint32_t instance_index_{0}; diff --git a/tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h b/tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h index b4ccd8fd00..3abbf7d257 100644 --- a/tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h +++ b/tests/benchmarks/stubs/esphome/components/zwave_proxy/zwave_proxy.h @@ -15,7 +15,9 @@ namespace zwave_proxy { class ZWaveProxy { public: api::APIConnection *get_api_connection() { return nullptr; } - void zwave_proxy_request(api::APIConnection *conn, api::enums::ZWaveProxyRequestType type) {} + api::enums::ZWaveProxyStatus zwave_proxy_request(api::APIConnection *conn, api::enums::ZWaveProxyRequestType type) { + return api::enums::ZWAVE_PROXY_STATUS_OK; + } void send_frame(api::APIConnection *api_connection, const uint8_t *data, size_t length) {} void api_connection_authenticated(api::APIConnection *conn) {} uint32_t get_feature_flags() const { return 0; } diff --git a/tests/unit_tests/components/api/test_api_proto.py b/tests/unit_tests/components/api/test_api_proto.py index 35aa5ff529..31297911f5 100644 --- a/tests/unit_tests/components/api/test_api_proto.py +++ b/tests/unit_tests/components/api/test_api_proto.py @@ -263,6 +263,17 @@ def test_device_capabilities_response_has_id_150() -> None: ) +def test_z_wave_proxy_request_response_has_id_151() -> None: + body = _extract_proto_message(PROTO_TEXT, "ZWaveProxyRequestResponse") + match = re.search(r"option \(id\) = (\d+);", body) + assert match is not None, "ZWaveProxyRequestResponse is missing `option (id)`" + assert int(match.group(1)) == 151, ( + f"ZWaveProxyRequestResponse has id {match.group(1)}, expected 151. " + "Message ids are part of the wire protocol and must not change once " + "assigned." + ) + + def test_superseded_fields_are_not_marked_deprecated_in_proto() -> None: """The six superseded fields must not carry `[deprecated = true]` in api.proto, or the generator drops them and old clients stop receiving