mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-27 13:13:50 +00:00 
			
		
		
		
	Merge branch 'guard_custom_services' into integration
This commit is contained in:
		| @@ -24,8 +24,9 @@ from esphome.const import ( | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_VARIABLES, | ||||
| ) | ||||
| from esphome.core import coroutine_with_priority | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
|  | ||||
| DOMAIN = "api" | ||||
| DEPENDENCIES = ["network"] | ||||
| AUTO_LOAD = ["socket"] | ||||
| CODEOWNERS = ["@OttoWinter"] | ||||
| @@ -51,6 +52,7 @@ SERVICE_ARG_NATIVE_TYPES = { | ||||
| } | ||||
| CONF_ENCRYPTION = "encryption" | ||||
| CONF_BATCH_DELAY = "batch_delay" | ||||
| CONF_CUSTOM_SERVICES = "custom_services" | ||||
|  | ||||
|  | ||||
| def validate_encryption_key(value): | ||||
| @@ -115,6 +117,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|                 cv.positive_time_period_milliseconds, | ||||
|                 cv.Range(max=cv.TimePeriod(milliseconds=65535)), | ||||
|             ), | ||||
|             cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean, | ||||
|             cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( | ||||
|                 single=True | ||||
|             ), | ||||
| @@ -139,8 +142,11 @@ async def to_code(config): | ||||
|     cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) | ||||
|     cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) | ||||
|  | ||||
|     # Set USE_API_SERVICES if any services are enabled | ||||
|     if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: | ||||
|         cg.add_define("USE_API_SERVICES") | ||||
|  | ||||
|     if actions := config.get(CONF_ACTIONS, []): | ||||
|         cg.add_define("USE_API_YAML_SERVICES") | ||||
|         for conf in actions: | ||||
|             template_args = [] | ||||
|             func_args = [] | ||||
| @@ -317,7 +323,10 @@ async def api_connected_to_code(config, condition_id, template_arg, args): | ||||
|  | ||||
|  | ||||
| def FILTER_SOURCE_FILES() -> list[str]: | ||||
|     """Filter out api_pb2_dump.cpp when proto message dumping is not enabled.""" | ||||
|     """Filter out api_pb2_dump.cpp when proto message dumping is not enabled | ||||
|     and user_services.cpp when no services are defined.""" | ||||
|     files_to_filter = [] | ||||
|  | ||||
|     # api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined | ||||
|     # This is a particularly large file that still needs to be opened and read | ||||
|     # all the way to the end even when ifdef'd out | ||||
| @@ -325,6 +334,11 @@ def FILTER_SOURCE_FILES() -> list[str]: | ||||
|     # HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set, | ||||
|     # which happens when the logger level is VERY_VERBOSE | ||||
|     if get_logger_level() != "VERY_VERBOSE": | ||||
|         return ["api_pb2_dump.cpp"] | ||||
|         files_to_filter.append("api_pb2_dump.cpp") | ||||
|  | ||||
|     return [] | ||||
|     # user_services.cpp is only needed when services are defined | ||||
|     config = CORE.config.get(DOMAIN, {}) | ||||
|     if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]: | ||||
|         files_to_filter.append("user_services.cpp") | ||||
|  | ||||
|     return files_to_filter | ||||
|   | ||||
| @@ -807,18 +807,21 @@ enum ServiceArgType { | ||||
|   SERVICE_ARG_TYPE_STRING_ARRAY = 7; | ||||
| } | ||||
| message ListEntitiesServicesArgument { | ||||
|   option (ifdef) = "USE_API_SERVICES"; | ||||
|   string name = 1; | ||||
|   ServiceArgType type = 2; | ||||
| } | ||||
| message ListEntitiesServicesResponse { | ||||
|   option (id) = 41; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_API_SERVICES"; | ||||
|  | ||||
|   string name = 1; | ||||
|   fixed32 key = 2; | ||||
|   repeated ListEntitiesServicesArgument args = 3; | ||||
| } | ||||
| message ExecuteServiceArgument { | ||||
|   option (ifdef) = "USE_API_SERVICES"; | ||||
|   bool bool_ = 1; | ||||
|   int32 legacy_int = 2; | ||||
|   float float_ = 3; | ||||
| @@ -834,6 +837,7 @@ message ExecuteServiceRequest { | ||||
|   option (id) = 42; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
|   option (no_delay) = true; | ||||
|   option (ifdef) = "USE_API_SERVICES"; | ||||
|  | ||||
|   fixed32 key = 1; | ||||
|   repeated ExecuteServiceArgument args = 2; | ||||
|   | ||||
| @@ -1567,6 +1567,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes | ||||
|     } | ||||
|   } | ||||
| } | ||||
| #ifdef USE_API_SERVICES | ||||
| void APIConnection::execute_service(const ExecuteServiceRequest &msg) { | ||||
|   bool found = false; | ||||
|   for (auto *service : this->parent_->get_user_services()) { | ||||
| @@ -1578,6 +1579,7 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { | ||||
|     ESP_LOGV(TAG, "Could not find service"); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_API_NOISE | ||||
| NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) { | ||||
|   psk_t psk{}; | ||||
|   | ||||
| @@ -195,7 +195,9 @@ class APIConnection : public APIServerConnection { | ||||
|     // TODO | ||||
|     return {}; | ||||
|   } | ||||
| #ifdef USE_API_SERVICES | ||||
|   void execute_service(const ExecuteServiceRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_API_NOISE | ||||
|   NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override; | ||||
| #endif | ||||
|   | ||||
| @@ -2051,6 +2051,7 @@ void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixe | ||||
| void GetTimeResponse::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0); | ||||
| } | ||||
| #ifdef USE_API_SERVICES | ||||
| bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
| @@ -2245,6 +2246,7 @@ void ExecuteServiceRequest::calculate_size(uint32_t &total_size) const { | ||||
|   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); | ||||
|   ProtoSize::add_repeated_message(total_size, 1, this->args); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_CAMERA | ||||
| bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|   | ||||
| @@ -82,6 +82,7 @@ enum LogLevel : uint32_t { | ||||
|   LOG_LEVEL_VERBOSE = 6, | ||||
|   LOG_LEVEL_VERY_VERBOSE = 7, | ||||
| }; | ||||
| #ifdef USE_API_SERVICES | ||||
| enum ServiceArgType : uint32_t { | ||||
|   SERVICE_ARG_TYPE_BOOL = 0, | ||||
|   SERVICE_ARG_TYPE_INT = 1, | ||||
| @@ -92,6 +93,7 @@ enum ServiceArgType : uint32_t { | ||||
|   SERVICE_ARG_TYPE_FLOAT_ARRAY = 6, | ||||
|   SERVICE_ARG_TYPE_STRING_ARRAY = 7, | ||||
| }; | ||||
| #endif | ||||
| #ifdef USE_CLIMATE | ||||
| enum ClimateMode : uint32_t { | ||||
|   CLIMATE_MODE_OFF = 0, | ||||
| @@ -1203,6 +1205,7 @@ class GetTimeResponse : public ProtoMessage { | ||||
|  protected: | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
| }; | ||||
| #ifdef USE_API_SERVICES | ||||
| class ListEntitiesServicesArgument : public ProtoMessage { | ||||
|  public: | ||||
|   std::string name{}; | ||||
| @@ -1278,6 +1281,7 @@ class ExecuteServiceRequest : public ProtoMessage { | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
| }; | ||||
| #endif | ||||
| #ifdef USE_CAMERA | ||||
| class ListEntitiesCameraResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   | ||||
| @@ -162,6 +162,7 @@ template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel val | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| } | ||||
| #ifdef USE_API_SERVICES | ||||
| template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) { | ||||
|   switch (value) { | ||||
|     case enums::SERVICE_ARG_TYPE_BOOL: | ||||
| @@ -184,6 +185,7 @@ template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::Servic | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_CLIMATE | ||||
| template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) { | ||||
|   switch (value) { | ||||
| @@ -1811,6 +1813,7 @@ void GetTimeResponse::dump_to(std::string &out) const { | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #ifdef USE_API_SERVICES | ||||
| void ListEntitiesServicesArgument::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("ListEntitiesServicesArgument {\n"); | ||||
| @@ -1910,6 +1913,7 @@ void ExecuteServiceRequest::dump_to(std::string &out) const { | ||||
|   } | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_CAMERA | ||||
| void ListEntitiesCameraResponse::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   | ||||
| @@ -195,6 +195,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       this->on_home_assistant_state_response(msg); | ||||
|       break; | ||||
|     } | ||||
| #ifdef USE_API_SERVICES | ||||
|     case 42: { | ||||
|       ExecuteServiceRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| @@ -204,6 +205,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       this->on_execute_service_request(msg); | ||||
|       break; | ||||
|     } | ||||
| #endif | ||||
| #ifdef USE_CAMERA | ||||
|     case 45: { | ||||
|       CameraImageRequest msg; | ||||
| @@ -660,11 +662,13 @@ void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { | ||||
|     } | ||||
|   } | ||||
| } | ||||
| #ifdef USE_API_SERVICES | ||||
| void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->execute_service(msg); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_API_NOISE | ||||
| void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { | ||||
|   if (this->check_authenticated_()) { | ||||
|   | ||||
| @@ -69,7 +69,9 @@ class APIServerConnectionBase : public ProtoService { | ||||
|   virtual void on_get_time_request(const GetTimeRequest &value){}; | ||||
|   virtual void on_get_time_response(const GetTimeResponse &value){}; | ||||
|  | ||||
| #ifdef USE_API_SERVICES | ||||
|   virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_CAMERA | ||||
|   virtual void on_camera_image_request(const CameraImageRequest &value){}; | ||||
| @@ -216,7 +218,9 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
|   virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; | ||||
|   virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; | ||||
|   virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; | ||||
| #ifdef USE_API_SERVICES | ||||
|   virtual void execute_service(const ExecuteServiceRequest &msg) = 0; | ||||
| #endif | ||||
| #ifdef USE_API_NOISE | ||||
|   virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0; | ||||
| #endif | ||||
| @@ -333,7 +337,9 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
|   void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; | ||||
|   void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; | ||||
|   void on_get_time_request(const GetTimeRequest &msg) override; | ||||
| #ifdef USE_API_SERVICES | ||||
|   void on_execute_service_request(const ExecuteServiceRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_API_NOISE | ||||
|   void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; | ||||
| #endif | ||||
|   | ||||
| @@ -24,14 +24,6 @@ static const char *const TAG = "api"; | ||||
| // APIServer | ||||
| APIServer *global_api_server = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| #ifndef USE_API_YAML_SERVICES | ||||
| // Global empty vector to avoid guard variables (saves 8 bytes) | ||||
| // This is initialized at program startup before any threads | ||||
| static const std::vector<UserServiceDescriptor *> empty_user_services{}; | ||||
|  | ||||
| const std::vector<UserServiceDescriptor *> &get_empty_user_services_instance() { return empty_user_services; } | ||||
| #endif | ||||
|  | ||||
| APIServer::APIServer() { | ||||
|   global_api_server = this; | ||||
|   // Pre-allocate shared write buffer | ||||
|   | ||||
| @@ -12,7 +12,9 @@ | ||||
| #include "esphome/core/log.h" | ||||
| #include "list_entities.h" | ||||
| #include "subscribe_state.h" | ||||
| #ifdef USE_API_SERVICES | ||||
| #include "user_services.h" | ||||
| #endif | ||||
|  | ||||
| #include <vector> | ||||
|  | ||||
| @@ -25,11 +27,6 @@ struct SavedNoisePsk { | ||||
| } PACKED;  // NOLINT | ||||
| #endif | ||||
|  | ||||
| #ifndef USE_API_YAML_SERVICES | ||||
| // Forward declaration of helper function | ||||
| const std::vector<UserServiceDescriptor *> &get_empty_user_services_instance(); | ||||
| #endif | ||||
|  | ||||
| class APIServer : public Component, public Controller { | ||||
|  public: | ||||
|   APIServer(); | ||||
| @@ -111,18 +108,9 @@ class APIServer : public Component, public Controller { | ||||
|   void on_media_player_update(media_player::MediaPlayer *obj) override; | ||||
| #endif | ||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call); | ||||
|   void register_user_service(UserServiceDescriptor *descriptor) { | ||||
| #ifdef USE_API_YAML_SERVICES | ||||
|     // Vector is pre-allocated when services are defined in YAML | ||||
|     this->user_services_.push_back(descriptor); | ||||
| #else | ||||
|     // Lazy allocate vector on first use for CustomAPIDevice | ||||
|     if (!this->user_services_) { | ||||
|       this->user_services_ = std::make_unique<std::vector<UserServiceDescriptor *>>(); | ||||
|     } | ||||
|     this->user_services_->push_back(descriptor); | ||||
| #ifdef USE_API_SERVICES | ||||
|   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } | ||||
| #endif | ||||
|   } | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
|   void request_time(); | ||||
| #endif | ||||
| @@ -151,17 +139,9 @@ class APIServer : public Component, public Controller { | ||||
|   void get_home_assistant_state(std::string entity_id, optional<std::string> attribute, | ||||
|                                 std::function<void(std::string)> f); | ||||
|   const std::vector<HomeAssistantStateSubscription> &get_state_subs() const; | ||||
|   const std::vector<UserServiceDescriptor *> &get_user_services() const { | ||||
| #ifdef USE_API_YAML_SERVICES | ||||
|     return this->user_services_; | ||||
| #else | ||||
|     if (this->user_services_) { | ||||
|       return *this->user_services_; | ||||
|     } | ||||
|     // Return reference to global empty instance (no guard needed) | ||||
|     return get_empty_user_services_instance(); | ||||
| #ifdef USE_API_SERVICES | ||||
|   const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; } | ||||
| #endif | ||||
|   } | ||||
|  | ||||
| #ifdef USE_API_CLIENT_CONNECTED_TRIGGER | ||||
|   Trigger<std::string, std::string> *get_client_connected_trigger() const { return this->client_connected_trigger_; } | ||||
| @@ -193,14 +173,8 @@ class APIServer : public Component, public Controller { | ||||
| #endif | ||||
|   std::vector<uint8_t> shared_write_buffer_;  // Shared proto write buffer for all connections | ||||
|   std::vector<HomeAssistantStateSubscription> state_subs_; | ||||
| #ifdef USE_API_YAML_SERVICES | ||||
|   // When services are defined in YAML, we know at compile time that services will be registered | ||||
| #ifdef USE_API_SERVICES | ||||
|   std::vector<UserServiceDescriptor *> user_services_; | ||||
| #else | ||||
|   // Services can still be registered at runtime by CustomAPIDevice components even when not | ||||
|   // defined in YAML. Using unique_ptr allows lazy allocation, saving 12 bytes in the common | ||||
|   // case where no services (YAML or custom) are used. | ||||
|   std::unique_ptr<std::vector<UserServiceDescriptor *>> user_services_; | ||||
| #endif | ||||
|  | ||||
|   // Group smaller types together | ||||
|   | ||||
| @@ -3,10 +3,13 @@ | ||||
| #include <map> | ||||
| #include "api_server.h" | ||||
| #ifdef USE_API | ||||
| #ifdef USE_API_SERVICES | ||||
| #include "user_services.h" | ||||
| #endif | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| #ifdef USE_API_SERVICES | ||||
| template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> { | ||||
|  public: | ||||
|   CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj, | ||||
| @@ -19,6 +22,7 @@ template<typename T, typename... Ts> class CustomAPIDeviceService : public UserS | ||||
|   T *obj_; | ||||
|   void (T::*callback_)(Ts...); | ||||
| }; | ||||
| #endif  // USE_API_SERVICES | ||||
|  | ||||
| class CustomAPIDevice { | ||||
|  public: | ||||
| @@ -46,12 +50,14 @@ class CustomAPIDevice { | ||||
|    * @param name The name of the service to register. | ||||
|    * @param arg_names The name of the arguments for the service, must match the arguments of the function. | ||||
|    */ | ||||
| #ifdef USE_API_SERVICES | ||||
|   template<typename T, typename... Ts> | ||||
|   void register_service(void (T::*callback)(Ts...), const std::string &name, | ||||
|                         const std::array<std::string, sizeof...(Ts)> &arg_names) { | ||||
|     auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback);  // NOLINT | ||||
|     global_api_server->register_user_service(service); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   /** Register a custom native API service that will show up in Home Assistant. | ||||
|    * | ||||
| @@ -71,10 +77,12 @@ class CustomAPIDevice { | ||||
|    * @param callback The member function to call when the service is triggered. | ||||
|    * @param name The name of the arguments for the service, must match the arguments of the function. | ||||
|    */ | ||||
| #ifdef USE_API_SERVICES | ||||
|   template<typename T> void register_service(void (T::*callback)(), const std::string &name) { | ||||
|     auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback);  // NOLINT | ||||
|     global_api_server->register_user_service(service); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   /** Subscribe to the state (or attribute state) of an entity from Home Assistant. | ||||
|    * | ||||
|   | ||||
| @@ -83,10 +83,12 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done( | ||||
|  | ||||
| ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} | ||||
|  | ||||
| #ifdef USE_API_SERVICES | ||||
| bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { | ||||
|   auto resp = service->encode_list_service_response(); | ||||
|   return this->client_->send_message(resp); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -44,7 +44,9 @@ class ListEntitiesIterator : public ComponentIterator { | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   bool on_text_sensor(text_sensor::TextSensor *entity) override; | ||||
| #endif | ||||
| #ifdef USE_API_SERVICES | ||||
|   bool on_service(UserServiceDescriptor *service) override; | ||||
| #endif | ||||
| #ifdef USE_CAMERA | ||||
|   bool on_camera(camera::Camera *entity) override; | ||||
| #endif | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
| #include "esphome/core/automation.h" | ||||
| #include "api_pb2.h" | ||||
|  | ||||
| #ifdef USE_API_SERVICES | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| @@ -73,3 +74,4 @@ template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts... | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
| #endif  // USE_API_SERVICES | ||||
|   | ||||
| @@ -4,6 +4,8 @@ | ||||
|  | ||||
| #ifdef USE_API | ||||
| #include "esphome/components/api/api_server.h" | ||||
| #endif | ||||
| #ifdef USE_API_SERVICES | ||||
| #include "esphome/components/api/user_services.h" | ||||
| #endif | ||||
|  | ||||
| @@ -148,7 +150,7 @@ void ComponentIterator::advance() { | ||||
|       } | ||||
|       break; | ||||
| #endif | ||||
| #ifdef USE_API | ||||
| #ifdef USE_API_SERVICES | ||||
|     case IteratorState ::SERVICE: | ||||
|       if (this->at_ >= api::global_api_server->get_user_services().size()) { | ||||
|         advance_platform = true; | ||||
| @@ -383,7 +385,7 @@ void ComponentIterator::advance() { | ||||
| } | ||||
| bool ComponentIterator::on_end() { return true; } | ||||
| bool ComponentIterator::on_begin() { return true; } | ||||
| #ifdef USE_API | ||||
| #ifdef USE_API_SERVICES | ||||
| bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; } | ||||
| #endif | ||||
| #ifdef USE_CAMERA | ||||
|   | ||||
| @@ -10,7 +10,7 @@ | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| #ifdef USE_API | ||||
| #ifdef USE_API_SERVICES | ||||
| namespace api { | ||||
| class UserServiceDescriptor; | ||||
| }  // namespace api | ||||
| @@ -45,7 +45,7 @@ class ComponentIterator { | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; | ||||
| #endif | ||||
| #ifdef USE_API | ||||
| #ifdef USE_API_SERVICES | ||||
|   virtual bool on_service(api::UserServiceDescriptor *service); | ||||
| #endif | ||||
| #ifdef USE_CAMERA | ||||
| @@ -122,7 +122,7 @@ class ComponentIterator { | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|     TEXT_SENSOR, | ||||
| #endif | ||||
| #ifdef USE_API | ||||
| #ifdef USE_API_SERVICES | ||||
|     SERVICE, | ||||
| #endif | ||||
| #ifdef USE_CAMERA | ||||
|   | ||||
| @@ -108,7 +108,7 @@ | ||||
| #define USE_API_CLIENT_DISCONNECTED_TRIGGER | ||||
| #define USE_API_NOISE | ||||
| #define USE_API_PLAINTEXT | ||||
| #define USE_API_YAML_SERVICES | ||||
| #define USE_API_SERVICES | ||||
| #define USE_MD5 | ||||
| #define USE_MQTT | ||||
| #define USE_NETWORK | ||||
|   | ||||
							
								
								
									
										24
									
								
								tests/integration/fixtures/api_custom_services.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								tests/integration/fixtures/api_custom_services.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| esphome: | ||||
|   name: api-custom-services-test | ||||
| host: | ||||
|  | ||||
| # This is required for CustomAPIDevice to work | ||||
| api: | ||||
|   custom_services: true | ||||
|   # Also test that YAML services still work | ||||
|   actions: | ||||
|     - action: test_yaml_service | ||||
|       then: | ||||
|         - logger.log: "YAML service called" | ||||
|  | ||||
| logger: | ||||
|   level: DEBUG | ||||
|  | ||||
| # External component that uses CustomAPIDevice | ||||
| external_components: | ||||
|   - source: | ||||
|       type: local | ||||
|       path: EXTERNAL_COMPONENT_PATH | ||||
|     components: [custom_api_device_component] | ||||
|  | ||||
| custom_api_device_component: | ||||
| @@ -0,0 +1,19 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID | ||||
|  | ||||
| custom_api_device_component_ns = cg.esphome_ns.namespace("custom_api_device_component") | ||||
| CustomAPIDeviceComponent = custom_api_device_component_ns.class_( | ||||
|     "CustomAPIDeviceComponent", cg.Component | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(CustomAPIDeviceComponent), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
| @@ -0,0 +1,52 @@ | ||||
| #include "custom_api_device_component.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef USE_API | ||||
| namespace esphome { | ||||
| namespace custom_api_device_component { | ||||
|  | ||||
| static const char *const TAG = "custom_api"; | ||||
|  | ||||
| void CustomAPIDeviceComponent::setup() { | ||||
|   // Register services using CustomAPIDevice | ||||
|   register_service(&CustomAPIDeviceComponent::on_test_service, "custom_test_service"); | ||||
|  | ||||
|   register_service(&CustomAPIDeviceComponent::on_service_with_args, "custom_service_with_args", | ||||
|                    {"arg_string", "arg_int", "arg_bool", "arg_float"}); | ||||
|  | ||||
|   // Test array types | ||||
|   register_service(&CustomAPIDeviceComponent::on_service_with_arrays, "custom_service_with_arrays", | ||||
|                    {"bool_array", "int_array", "float_array", "string_array"}); | ||||
| } | ||||
|  | ||||
| void CustomAPIDeviceComponent::on_test_service() { ESP_LOGI(TAG, "Custom test service called!"); } | ||||
|  | ||||
| void CustomAPIDeviceComponent::on_service_with_args(std::string arg_string, int32_t arg_int, bool arg_bool, | ||||
|                                                     float arg_float) { | ||||
|   ESP_LOGI(TAG, "Custom service called with: %s, %d, %d, %.2f", arg_string.c_str(), arg_int, arg_bool, arg_float); | ||||
| } | ||||
|  | ||||
| void CustomAPIDeviceComponent::on_service_with_arrays(std::vector<bool> bool_array, std::vector<int32_t> int_array, | ||||
|                                                       std::vector<float> float_array, | ||||
|                                                       std::vector<std::string> string_array) { | ||||
|   ESP_LOGI(TAG, "Array service called with %zu bools, %zu ints, %zu floats, %zu strings", bool_array.size(), | ||||
|            int_array.size(), float_array.size(), string_array.size()); | ||||
|  | ||||
|   // Log first element of each array if not empty | ||||
|   if (!bool_array.empty()) { | ||||
|     ESP_LOGI(TAG, "First bool: %d", bool_array[0]); | ||||
|   } | ||||
|   if (!int_array.empty()) { | ||||
|     ESP_LOGI(TAG, "First int: %d", int_array[0]); | ||||
|   } | ||||
|   if (!float_array.empty()) { | ||||
|     ESP_LOGI(TAG, "First float: %.2f", float_array[0]); | ||||
|   } | ||||
|   if (!string_array.empty()) { | ||||
|     ESP_LOGI(TAG, "First string: %s", string_array[0].c_str()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace custom_api_device_component | ||||
| }  // namespace esphome | ||||
| #endif  // USE_API | ||||
| @@ -0,0 +1,28 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <string> | ||||
| #include <vector> | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/api/custom_api_device.h" | ||||
|  | ||||
| #ifdef USE_API | ||||
| namespace esphome { | ||||
| namespace custom_api_device_component { | ||||
|  | ||||
| using namespace api; | ||||
|  | ||||
| class CustomAPIDeviceComponent : public Component, public CustomAPIDevice { | ||||
|  public: | ||||
|   void setup() override; | ||||
|  | ||||
|   void on_test_service(); | ||||
|  | ||||
|   void on_service_with_args(std::string arg_string, int32_t arg_int, bool arg_bool, float arg_float); | ||||
|  | ||||
|   void on_service_with_arrays(std::vector<bool> bool_array, std::vector<int32_t> int_array, | ||||
|                               std::vector<float> float_array, std::vector<std::string> string_array); | ||||
| }; | ||||
|  | ||||
| }  // namespace custom_api_device_component | ||||
| }  // namespace esphome | ||||
| #endif  // USE_API | ||||
							
								
								
									
										144
									
								
								tests/integration/test_api_custom_services.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								tests/integration/test_api_custom_services.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| """Integration test for API custom services using CustomAPIDevice.""" | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
| from pathlib import Path | ||||
| import re | ||||
|  | ||||
| from aioesphomeapi import UserService, UserServiceArgType | ||||
| import pytest | ||||
|  | ||||
| from .types import APIClientConnectedFactory, RunCompiledFunction | ||||
|  | ||||
|  | ||||
| @pytest.mark.asyncio | ||||
| async def test_api_custom_services( | ||||
|     yaml_config: str, | ||||
|     run_compiled: RunCompiledFunction, | ||||
|     api_client_connected: APIClientConnectedFactory, | ||||
| ) -> None: | ||||
|     """Test CustomAPIDevice services work correctly with custom_services: true.""" | ||||
|     # Get the path to the external components directory | ||||
|     external_components_path = str( | ||||
|         Path(__file__).parent / "fixtures" / "external_components" | ||||
|     ) | ||||
|  | ||||
|     # Replace the placeholder in the YAML config with the actual path | ||||
|     yaml_config = yaml_config.replace( | ||||
|         "EXTERNAL_COMPONENT_PATH", external_components_path | ||||
|     ) | ||||
|  | ||||
|     loop = asyncio.get_running_loop() | ||||
|  | ||||
|     # Track log messages | ||||
|     yaml_service_future = loop.create_future() | ||||
|     custom_service_future = loop.create_future() | ||||
|     custom_args_future = loop.create_future() | ||||
|     custom_arrays_future = loop.create_future() | ||||
|  | ||||
|     # Patterns to match in logs | ||||
|     yaml_service_pattern = re.compile(r"YAML service called") | ||||
|     custom_service_pattern = re.compile(r"Custom test service called!") | ||||
|     custom_args_pattern = re.compile( | ||||
|         r"Custom service called with: test_string, 456, 1, 78\.90" | ||||
|     ) | ||||
|     custom_arrays_pattern = re.compile( | ||||
|         r"Array service called with 2 bools, 3 ints, 2 floats, 2 strings" | ||||
|     ) | ||||
|  | ||||
|     def check_output(line: str) -> None: | ||||
|         """Check log output for expected messages.""" | ||||
|         if not yaml_service_future.done() and yaml_service_pattern.search(line): | ||||
|             yaml_service_future.set_result(True) | ||||
|         elif not custom_service_future.done() and custom_service_pattern.search(line): | ||||
|             custom_service_future.set_result(True) | ||||
|         elif not custom_args_future.done() and custom_args_pattern.search(line): | ||||
|             custom_args_future.set_result(True) | ||||
|         elif not custom_arrays_future.done() and custom_arrays_pattern.search(line): | ||||
|             custom_arrays_future.set_result(True) | ||||
|  | ||||
|     # Run with log monitoring | ||||
|     async with run_compiled(yaml_config, line_callback=check_output): | ||||
|         async with api_client_connected() as client: | ||||
|             # Verify device info | ||||
|             device_info = await client.device_info() | ||||
|             assert device_info is not None | ||||
|             assert device_info.name == "api-custom-services-test" | ||||
|  | ||||
|             # List services | ||||
|             _, services = await client.list_entities_services() | ||||
|  | ||||
|             # Should have 4 services: 1 YAML + 3 CustomAPIDevice | ||||
|             assert len(services) == 4, f"Expected 4 services, found {len(services)}" | ||||
|  | ||||
|             # Find our services | ||||
|             yaml_service: UserService | None = None | ||||
|             custom_service: UserService | None = None | ||||
|             custom_args_service: UserService | None = None | ||||
|             custom_arrays_service: UserService | None = None | ||||
|  | ||||
|             for service in services: | ||||
|                 if service.name == "test_yaml_service": | ||||
|                     yaml_service = service | ||||
|                 elif service.name == "custom_test_service": | ||||
|                     custom_service = service | ||||
|                 elif service.name == "custom_service_with_args": | ||||
|                     custom_args_service = service | ||||
|                 elif service.name == "custom_service_with_arrays": | ||||
|                     custom_arrays_service = service | ||||
|  | ||||
|             assert yaml_service is not None, "test_yaml_service not found" | ||||
|             assert custom_service is not None, "custom_test_service not found" | ||||
|             assert custom_args_service is not None, "custom_service_with_args not found" | ||||
|             assert custom_arrays_service is not None, ( | ||||
|                 "custom_service_with_arrays not found" | ||||
|             ) | ||||
|  | ||||
|             # Test YAML service | ||||
|             client.execute_service(yaml_service, {}) | ||||
|             await asyncio.wait_for(yaml_service_future, timeout=5.0) | ||||
|  | ||||
|             # Test simple CustomAPIDevice service | ||||
|             client.execute_service(custom_service, {}) | ||||
|             await asyncio.wait_for(custom_service_future, timeout=5.0) | ||||
|  | ||||
|             # Verify custom_args_service arguments | ||||
|             assert len(custom_args_service.args) == 4 | ||||
|             arg_types = {arg.name: arg.type for arg in custom_args_service.args} | ||||
|             assert arg_types["arg_string"] == UserServiceArgType.STRING | ||||
|             assert arg_types["arg_int"] == UserServiceArgType.INT | ||||
|             assert arg_types["arg_bool"] == UserServiceArgType.BOOL | ||||
|             assert arg_types["arg_float"] == UserServiceArgType.FLOAT | ||||
|  | ||||
|             # Test CustomAPIDevice service with arguments | ||||
|             client.execute_service( | ||||
|                 custom_args_service, | ||||
|                 { | ||||
|                     "arg_string": "test_string", | ||||
|                     "arg_int": 456, | ||||
|                     "arg_bool": True, | ||||
|                     "arg_float": 78.9, | ||||
|                 }, | ||||
|             ) | ||||
|             await asyncio.wait_for(custom_args_future, timeout=5.0) | ||||
|  | ||||
|             # Verify array service arguments | ||||
|             assert len(custom_arrays_service.args) == 4 | ||||
|             array_arg_types = {arg.name: arg.type for arg in custom_arrays_service.args} | ||||
|             assert array_arg_types["bool_array"] == UserServiceArgType.BOOL_ARRAY | ||||
|             assert array_arg_types["int_array"] == UserServiceArgType.INT_ARRAY | ||||
|             assert array_arg_types["float_array"] == UserServiceArgType.FLOAT_ARRAY | ||||
|             assert array_arg_types["string_array"] == UserServiceArgType.STRING_ARRAY | ||||
|  | ||||
|             # Test CustomAPIDevice service with arrays | ||||
|             client.execute_service( | ||||
|                 custom_arrays_service, | ||||
|                 { | ||||
|                     "bool_array": [True, False], | ||||
|                     "int_array": [1, 2, 3], | ||||
|                     "float_array": [1.1, 2.2], | ||||
|                     "string_array": ["hello", "world"], | ||||
|                 }, | ||||
|             ) | ||||
|             await asyncio.wait_for(custom_arrays_future, timeout=5.0) | ||||
		Reference in New Issue
	
	Block a user