[store_yaml] Report store_yaml support in DeviceCapabilitiesResponse instead of DeviceInfoResponse

This commit is contained in:
J. Nick Koston
2026-09-14 15:29:10 -05:00
parent e47133171f
commit 2f3b573752
6 changed files with 97 additions and 25 deletions
+7 -5
View File
@@ -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 {
+6 -6
View File
@@ -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);
}
+18 -6
View File
@@ -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;
}
+18 -5
View File
@@ -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<SerialProxyInfo, SERIAL_PROXY_COUNT> 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;
+12 -3
View File
@@ -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();
}
@@ -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"")