diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index b222e7a63fb..0ef4c1f1820 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -334,11 +334,6 @@ message DeviceInfoResponse { // all-zeros PSK, so the api encryption key can be provisioned without being // sent in plaintext (protects against passive sniffing, not active MITM) bool api_encryption_provisionable = 26 [(field_ifdef) = "USE_API_NOISE"]; - - // Whether this firmware embeds its YAML configuration for recovery via - // `get_yaml`. Clients use this to skip the request entirely when the - // device cannot answer it instead of waiting for a timeout. - bool has_store_yaml = 27 [(field_ifdef) = "USE_STORE_YAML"]; } // ==================== DEVICE CAPABILITIES ==================== @@ -387,6 +382,12 @@ message ZWaveProxyCapabilities { uint32 home_id = 2; } +message StoreYamlCapabilities { + // Whether this firmware embeds its YAML configuration for recovery via + // `get_yaml`, so clients can skip the request instead of waiting for a timeout + bool supported = 1; +} + message DeviceCapabilitiesResponse { option (id) = 150; option (source) = SOURCE_SERVER; @@ -396,6 +397,7 @@ message DeviceCapabilitiesResponse { ZWaveProxyCapabilities zwave_proxy = 3 [(field_ifdef) = "USE_ZWAVE_PROXY"]; repeated SerialProxyInfo serial_proxies = 4 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"]; + StoreYamlCapabilities store_yaml = 5 [(field_ifdef) = "USE_STORE_YAML"]; } message ListEntitiesRequest { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 02309e5bc1b..20b1302259d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -2002,12 +2002,6 @@ bool APIConnection::send_device_info_response_() { #ifdef USE_DEEP_SLEEP resp.has_deep_sleep = deep_sleep::global_has_deep_sleep; #endif -#ifdef USE_STORE_YAML - // Compile-time knowledge: codegen always embeds a non-empty blob when the - // component is compiled in. Deriving this from the runtime pointer could - // report false to a client connecting before store_yaml's setup() ran. - resp.has_store_yaml = true; -#endif #ifdef ESPHOME_PROJECT_NAME #ifdef USE_ESP8266 static const char PROJECT_NAME_PROGMEM[] PROGMEM = ESPHOME_PROJECT_NAME; @@ -2112,6 +2106,12 @@ bool APIConnection::send_device_capabilities_response_() { info.port_type = proxy->get_port_type(); info.configured_line_states = proxy->get_configured_modem_pins(); } +#endif +#ifdef USE_STORE_YAML + // Compile-time knowledge: codegen always embeds a non-empty blob when the + // component is compiled in. Deriving this from the runtime pointer could + // report false to a client connecting before store_yaml's setup() ran. + resp.store_yaml.supported = true; #endif return this->send_message(resp); } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index a3189a0a896..4180fe10296 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -175,9 +175,6 @@ uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_ #endif #ifdef USE_API_NOISE ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 26, this->api_encryption_provisionable); -#endif -#ifdef USE_STORE_YAML - ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 27, this->has_store_yaml); #endif return pos; } @@ -243,9 +240,6 @@ uint32_t DeviceInfoResponse::calculate_size() const { #endif #ifdef USE_API_NOISE size += ProtoSize::calc_bool(2, this->api_encryption_provisionable); -#endif -#ifdef USE_STORE_YAML - size += ProtoSize::calc_bool(2, this->has_store_yaml); #endif return size; } @@ -289,6 +283,18 @@ uint32_t ZWaveProxyCapabilities::calculate_size() const { return size; } #endif +#ifdef USE_STORE_YAML +uint8_t *StoreYamlCapabilities::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { + uint8_t *__restrict__ pos = buffer.get_pos(); + ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 1, this->supported); + return pos; +} +uint32_t StoreYamlCapabilities::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_bool(1, this->supported); + return size; +} +#endif uint8_t *DeviceCapabilitiesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *__restrict__ pos = buffer.get_pos(); #ifdef USE_BLUETOOTH_PROXY @@ -304,6 +310,9 @@ uint8_t *DeviceCapabilitiesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCOD for (const auto &it : this->serial_proxies) { ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 4, it); } +#endif +#ifdef USE_STORE_YAML + ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 5, this->store_yaml); #endif return pos; } @@ -322,6 +331,9 @@ uint32_t DeviceCapabilitiesResponse::calculate_size() const { for (const auto &it : this->serial_proxies) { size += ProtoSize::calc_message_force(1, it.calculate_size()); } +#endif +#ifdef USE_STORE_YAML + size += ProtoSize::calc_message(1, this->store_yaml.calculate_size()); #endif return size; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index b8c8e5cb776..f2fa31c3534 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -554,7 +554,7 @@ class SerialProxyInfo final : public ProtoMessage { class DeviceInfoResponse final : public ProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 10; - static constexpr uint16_t ESTIMATED_SIZE = 315; + static constexpr uint16_t ESTIMATED_SIZE = 312; #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("device_info_response"); } #endif @@ -612,9 +612,6 @@ class DeviceInfoResponse final : public ProtoMessage { #endif #ifdef USE_API_NOISE bool api_encryption_provisionable{false}; -#endif -#ifdef USE_STORE_YAML - bool has_store_yaml{false}; #endif uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; @@ -665,10 +662,23 @@ class ZWaveProxyCapabilities final : public ProtoMessage { protected: }; #endif +#ifdef USE_STORE_YAML +class StoreYamlCapabilities final : public ProtoMessage { + public: + bool supported{false}; + 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 class DeviceCapabilitiesResponse final : public ProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 150; - static constexpr uint8_t ESTIMATED_SIZE = 102; + static constexpr uint8_t ESTIMATED_SIZE = 119; #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("device_capabilities_response"); } #endif @@ -683,6 +693,9 @@ class DeviceCapabilitiesResponse final : public ProtoMessage { #endif #ifdef USE_SERIAL_PROXY std::array serial_proxies{}; +#endif +#ifdef USE_STORE_YAML + StoreYamlCapabilities store_yaml{}; #endif uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 52baec32fd0..2a5e13200eb 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1020,9 +1020,6 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { #endif #ifdef USE_API_NOISE dump_field(out, ESPHOME_PSTR("api_encryption_provisionable"), this->api_encryption_provisionable); -#endif -#ifdef USE_STORE_YAML - dump_field(out, ESPHOME_PSTR("has_store_yaml"), this->has_store_yaml); #endif return out.c_str(); } @@ -1049,6 +1046,13 @@ const char *ZWaveProxyCapabilities::dump_to(DumpBuffer &out) const { return out.c_str(); } #endif +#ifdef USE_STORE_YAML +const char *StoreYamlCapabilities::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("StoreYamlCapabilities")); + dump_field(out, ESPHOME_PSTR("supported"), this->supported); + return out.c_str(); +} +#endif const char *DeviceCapabilitiesResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, ESPHOME_PSTR("DeviceCapabilitiesResponse")); #ifdef USE_BLUETOOTH_PROXY @@ -1072,6 +1076,11 @@ const char *DeviceCapabilitiesResponse::dump_to(DumpBuffer &out) const { it.dump_to(out); out.append("\n"); } +#endif +#ifdef USE_STORE_YAML + out.append(2, ' ').append_p(ESPHOME_PSTR("store_yaml")).append(": "); + this->store_yaml.dump_to(out); + out.append("\n"); #endif return out.c_str(); } diff --git a/tests/integration/test_store_yaml_recovery.py b/tests/integration/test_store_yaml_recovery.py index 270fb6e5a5f..0f60415cd7c 100644 --- a/tests/integration/test_store_yaml_recovery.py +++ b/tests/integration/test_store_yaml_recovery.py @@ -28,6 +28,8 @@ from .types import RunCompiledFunction # Message IDs from esphome/components/api/api.proto. HELLO_REQUEST = 1 HELLO_RESPONSE = 2 +DEVICE_CAPABILITIES_REQUEST = 149 +DEVICE_CAPABILITIES_RESPONSE = 150 GET_YAML_REQUEST = 154 GET_YAML_RESPONSE = 155 @@ -90,6 +92,30 @@ def _parse_get_yaml_response(payload: bytes) -> tuple[bytes, bool, int, str]: return data, done, total_size, encoding +def _parse_store_yaml_supported(payload: bytes) -> bool: + """Read `store_yaml.supported` (field 5, field 1) from `DeviceCapabilitiesResponse`.""" + pos = 0 + while pos < len(payload): + tag, pos = _read_varint(payload, pos) + wire_type = tag & 0x07 + if wire_type == 0: + _, pos = _read_varint(payload, pos) + continue + assert wire_type == 2, f"unexpected wire type {wire_type}" + length, pos = _read_varint(payload, pos) + chunk = payload[pos : pos + length] + pos += length + if tag >> 3 != 5: + continue + sub_pos = 0 + while sub_pos < len(chunk): + sub_tag, sub_pos = _read_varint(chunk, sub_pos) + value, sub_pos = _read_varint(chunk, sub_pos) + if sub_tag == 0x08: + return bool(value) + return False + + async def _read_varint_from(reader: asyncio.StreamReader) -> int: """Read a protobuf varint byte-by-byte from a stream.""" result = 0 @@ -159,6 +185,16 @@ async def test_store_yaml_recovery( msg_type, _ = await asyncio.wait_for(client.recv(), timeout=5.0) assert msg_type == HELLO_RESPONSE, f"expected HelloResponse, got {msg_type}" + # Clients learn the device can answer get_yaml from its capabilities. + await client.send(DEVICE_CAPABILITIES_REQUEST, b"") + while True: + msg_type, payload = await asyncio.wait_for(client.recv(), timeout=5.0) + if msg_type == DEVICE_CAPABILITIES_RESPONSE: + break + assert _parse_store_yaml_supported(payload), ( + "expected DeviceCapabilitiesResponse to report store_yaml.supported" + ) + # The actual request under test. await client.send(GET_YAML_REQUEST, b"")