From 6c6da8a3cd2ab8dd41c6cd20cbed8884a30a7906 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Feb 2026 12:45:24 -0600 Subject: [PATCH] [api] Skip class generation for empty SOURCE_CLIENT protobuf messages (#13880) --- esphome/components/api/api_pb2.h | 91 ---------------------- esphome/components/api/api_pb2_dump.cpp | 28 ------- esphome/components/api/api_pb2_service.cpp | 16 ++-- script/api_protobuf/api_protobuf.py | 53 ++++++++++--- 4 files changed, 52 insertions(+), 136 deletions(-) diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index cf6c65f285..15819da172 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -440,19 +440,6 @@ class PingResponse final : public ProtoMessage { protected: }; -class DeviceInfoRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 9; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "device_info_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; #ifdef USE_AREAS class AreaInfo final : public ProtoMessage { public: @@ -546,19 +533,6 @@ class DeviceInfoResponse final : public ProtoMessage { protected: }; -class ListEntitiesRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 11; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "list_entities_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; class ListEntitiesDoneResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 19; @@ -572,19 +546,6 @@ class ListEntitiesDoneResponse final : public ProtoMessage { protected: }; -class SubscribeStatesRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 20; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_states_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; #ifdef USE_BINARY_SENSOR class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { public: @@ -1037,19 +998,6 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage { }; #endif #ifdef USE_API_HOMEASSISTANT_SERVICES -class SubscribeHomeassistantServicesRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 34; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_homeassistant_services_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; class HomeassistantServiceMap final : public ProtoMessage { public: StringRef key{}; @@ -1117,19 +1065,6 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage { }; #endif #ifdef USE_API_HOMEASSISTANT_STATES -class SubscribeHomeAssistantStatesRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 38; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_home_assistant_states_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; class SubscribeHomeAssistantStateResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 39; @@ -2160,19 +2095,6 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage { protected: }; -class SubscribeBluetoothConnectionsFreeRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 80; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "subscribe_bluetooth_connections_free_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; class BluetoothConnectionsFreeResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 81; @@ -2279,19 +2201,6 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage { protected: }; -class UnsubscribeBluetoothLEAdvertisementsRequest final : public ProtoMessage { - public: - static constexpr uint8_t MESSAGE_TYPE = 87; - static constexpr uint8_t ESTIMATED_SIZE = 0; -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *message_name() const override { return "unsubscribe_bluetooth_le_advertisements_request"; } -#endif -#ifdef HAS_PROTO_MESSAGE_DUMP - const char *dump_to(DumpBuffer &out) const override; -#endif - - protected: -}; class BluetoothDeviceClearCacheResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 88; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index e9db36ae21..f1e3bdcafe 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -764,10 +764,6 @@ const char *PingResponse::dump_to(DumpBuffer &out) const { out.append("PingResponse {}"); return out.c_str(); } -const char *DeviceInfoRequest::dump_to(DumpBuffer &out) const { - out.append("DeviceInfoRequest {}"); - return out.c_str(); -} #ifdef USE_AREAS const char *AreaInfo::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "AreaInfo"); @@ -848,18 +844,10 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { #endif return out.c_str(); } -const char *ListEntitiesRequest::dump_to(DumpBuffer &out) const { - out.append("ListEntitiesRequest {}"); - return out.c_str(); -} const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const { out.append("ListEntitiesDoneResponse {}"); return out.c_str(); } -const char *SubscribeStatesRequest::dump_to(DumpBuffer &out) const { - out.append("SubscribeStatesRequest {}"); - return out.c_str(); -} #ifdef USE_BINARY_SENSOR const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse"); @@ -1191,10 +1179,6 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const { } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES -const char *SubscribeHomeassistantServicesRequest::dump_to(DumpBuffer &out) const { - out.append("SubscribeHomeassistantServicesRequest {}"); - return out.c_str(); -} const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "HomeassistantServiceMap"); dump_field(out, "key", this->key); @@ -1245,10 +1229,6 @@ const char *HomeassistantActionResponse::dump_to(DumpBuffer &out) const { } #endif #ifdef USE_API_HOMEASSISTANT_STATES -const char *SubscribeHomeAssistantStatesRequest::dump_to(DumpBuffer &out) const { - out.append("SubscribeHomeAssistantStatesRequest {}"); - return out.c_str(); -} const char *SubscribeHomeAssistantStateResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse"); dump_field(out, "entity_id", this->entity_id); @@ -1924,10 +1904,6 @@ const char *BluetoothGATTNotifyDataResponse::dump_to(DumpBuffer &out) const { dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); return out.c_str(); } -const char *SubscribeBluetoothConnectionsFreeRequest::dump_to(DumpBuffer &out) const { - out.append("SubscribeBluetoothConnectionsFreeRequest {}"); - return out.c_str(); -} const char *BluetoothConnectionsFreeResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse"); dump_field(out, "free", this->free); @@ -1970,10 +1946,6 @@ const char *BluetoothDeviceUnpairingResponse::dump_to(DumpBuffer &out) const { dump_field(out, "error", this->error); return out.c_str(); } -const char *UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(DumpBuffer &out) const { - out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}"); - return out.c_str(); -} const char *BluetoothDeviceClearCacheResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse"); dump_field(out, "address", this->address); diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 2d15deb90d..f9151ae3b4 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -27,7 +27,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, case DisconnectRequest::MESSAGE_TYPE: // No setup required case PingRequest::MESSAGE_TYPE: // No setup required break; - case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only + case 9 /* DeviceInfoRequest is empty */: // Connection setup only if (!this->check_connection_setup_()) { return; } @@ -76,21 +76,21 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_ping_response(); break; } - case DeviceInfoRequest::MESSAGE_TYPE: { + case 9 /* DeviceInfoRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_receive_message_(LOG_STR("on_device_info_request")); #endif this->on_device_info_request(); break; } - case ListEntitiesRequest::MESSAGE_TYPE: { + case 11 /* ListEntitiesRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_receive_message_(LOG_STR("on_list_entities_request")); #endif this->on_list_entities_request(); break; } - case SubscribeStatesRequest::MESSAGE_TYPE: { + case 20 /* SubscribeStatesRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_receive_message_(LOG_STR("on_subscribe_states_request")); #endif @@ -151,7 +151,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_API_HOMEASSISTANT_SERVICES - case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: { + case 34 /* SubscribeHomeassistantServicesRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request")); #endif @@ -169,7 +169,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, break; } #ifdef USE_API_HOMEASSISTANT_STATES - case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: { + case 38 /* SubscribeHomeAssistantStatesRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request")); #endif @@ -376,7 +376,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: { + case 80 /* SubscribeBluetoothConnectionsFreeRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request")); #endif @@ -385,7 +385,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, } #endif #ifdef USE_BLUETOOTH_PROXY - case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: { + case 87 /* UnsubscribeBluetoothLEAdvertisementsRequest is empty */: { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request")); #endif diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index ece0b5692f..4fbee49dae 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -2277,6 +2277,12 @@ ifdefs: dict[str, str] = {} # Track messages with no fields (empty messages) for parameter elision EMPTY_MESSAGES: set[str] = set() +# Track empty SOURCE_CLIENT messages that don't need class generation +# These messages have no fields and are only received (never sent), so the +# class definition (vtable, dump_to, message_name, ESTIMATED_SIZE) is dead code +# that the compiler compiles but the linker strips away. +SKIP_CLASS_GENERATION: set[str] = set() + def get_opt( desc: descriptor.DescriptorProto, @@ -2527,7 +2533,11 @@ def build_service_message_type( case += "#endif\n" case += f"this->{func}({'msg' if not is_empty else ''});\n" case += "break;" - RECEIVE_CASES[id_] = (case, ifdef, mt.name) + if mt.name in SKIP_CLASS_GENERATION: + case_label = f"{id_} /* {mt.name} is empty */" + else: + case_label = f"{mt.name}::MESSAGE_TYPE" + RECEIVE_CASES[id_] = (case, ifdef, case_label) # Only close ifdef if we opened it if ifdef is not None: @@ -2723,6 +2733,19 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint mt = file.message_type + # Identify empty SOURCE_CLIENT messages that don't need class generation + for m in mt: + if m.options.deprecated: + continue + if not m.options.HasExtension(pb.id): + continue + source = message_source_map.get(m.name) + if source != SOURCE_CLIENT: + continue + has_fields = any(not field.options.deprecated for field in m.field) + if not has_fields: + SKIP_CLASS_GENERATION.add(m.name) + # Collect messages by base class base_class_groups = collect_messages_by_base_class(mt) @@ -2755,6 +2778,10 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint if m.name not in used_messages and not m.options.HasExtension(pb.id): continue + # Skip class generation for empty SOURCE_CLIENT messages + if m.name in SKIP_CLASS_GENERATION: + continue + s, c, dc = build_message_type(m, base_class_fields, message_source_map) msg_ifdef = message_ifdef_map.get(m.name) @@ -2901,10 +2928,18 @@ static const char *const TAG = "api.service"; no_conn_ids: set[int] = set() conn_only_ids: set[int] = set() - for id_, (_, _, case_msg_name) in cases: - if case_msg_name in message_auth_map: - needs_auth = message_auth_map[case_msg_name] - needs_conn = message_conn_map[case_msg_name] + # Build a reverse lookup from message id to message name for auth lookups + id_to_msg_name: dict[int, str] = {} + for mt in file.message_type: + id_ = get_opt(mt, pb.id) + if id_ is not None and not mt.options.deprecated: + id_to_msg_name[id_] = mt.name + + for id_, (_, _, case_label) in cases: + msg_name = id_to_msg_name.get(id_, "") + if msg_name in message_auth_map: + needs_auth = message_auth_map[msg_name] + needs_conn = message_conn_map[msg_name] if not needs_conn: no_conn_ids.add(id_) @@ -2915,10 +2950,10 @@ static const char *const TAG = "api.service"; def generate_cases(ids: set[int], comment: str) -> str: result = "" for id_ in sorted(ids): - _, ifdef, msg_name = RECEIVE_CASES[id_] + _, ifdef, case_label = RECEIVE_CASES[id_] if ifdef: result += f"#ifdef {ifdef}\n" - result += f" case {msg_name}::MESSAGE_TYPE: {comment}\n" + result += f" case {case_label}: {comment}\n" if ifdef: result += "#endif\n" return result @@ -2958,11 +2993,11 @@ static const char *const TAG = "api.service"; # Dispatch switch out += " switch (msg_type) {\n" - for i, (case, ifdef, message_name) in cases: + for i, (case, ifdef, case_label) in cases: if ifdef is not None: out += f"#ifdef {ifdef}\n" - c = f" case {message_name}::MESSAGE_TYPE: {{\n" + c = f" case {case_label}: {{\n" c += indent(case, " ") + "\n" c += " }" out += c + "\n"