mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +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_TRIGGER_ID, | ||||||
|     CONF_VARIABLES, |     CONF_VARIABLES, | ||||||
| ) | ) | ||||||
| from esphome.core import coroutine_with_priority | from esphome.core import CORE, coroutine_with_priority | ||||||
|  |  | ||||||
|  | DOMAIN = "api" | ||||||
| DEPENDENCIES = ["network"] | DEPENDENCIES = ["network"] | ||||||
| AUTO_LOAD = ["socket"] | AUTO_LOAD = ["socket"] | ||||||
| CODEOWNERS = ["@OttoWinter"] | CODEOWNERS = ["@OttoWinter"] | ||||||
| @@ -51,6 +52,7 @@ SERVICE_ARG_NATIVE_TYPES = { | |||||||
| } | } | ||||||
| CONF_ENCRYPTION = "encryption" | CONF_ENCRYPTION = "encryption" | ||||||
| CONF_BATCH_DELAY = "batch_delay" | CONF_BATCH_DELAY = "batch_delay" | ||||||
|  | CONF_CUSTOM_SERVICES = "custom_services" | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_encryption_key(value): | def validate_encryption_key(value): | ||||||
| @@ -115,6 +117,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|                 cv.positive_time_period_milliseconds, |                 cv.positive_time_period_milliseconds, | ||||||
|                 cv.Range(max=cv.TimePeriod(milliseconds=65535)), |                 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( |             cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( | ||||||
|                 single=True |                 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_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) | ||||||
|     cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) |     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, []): |     if actions := config.get(CONF_ACTIONS, []): | ||||||
|         cg.add_define("USE_API_YAML_SERVICES") |  | ||||||
|         for conf in actions: |         for conf in actions: | ||||||
|             template_args = [] |             template_args = [] | ||||||
|             func_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]: | 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 |     # 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 |     # 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 |     # 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, |     # HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set, | ||||||
|     # which happens when the logger level is VERY_VERBOSE |     # which happens when the logger level is VERY_VERBOSE | ||||||
|     if get_logger_level() != "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; |   SERVICE_ARG_TYPE_STRING_ARRAY = 7; | ||||||
| } | } | ||||||
| message ListEntitiesServicesArgument { | message ListEntitiesServicesArgument { | ||||||
|  |   option (ifdef) = "USE_API_SERVICES"; | ||||||
|   string name = 1; |   string name = 1; | ||||||
|   ServiceArgType type = 2; |   ServiceArgType type = 2; | ||||||
| } | } | ||||||
| message ListEntitiesServicesResponse { | message ListEntitiesServicesResponse { | ||||||
|   option (id) = 41; |   option (id) = 41; | ||||||
|   option (source) = SOURCE_SERVER; |   option (source) = SOURCE_SERVER; | ||||||
|  |   option (ifdef) = "USE_API_SERVICES"; | ||||||
|  |  | ||||||
|   string name = 1; |   string name = 1; | ||||||
|   fixed32 key = 2; |   fixed32 key = 2; | ||||||
|   repeated ListEntitiesServicesArgument args = 3; |   repeated ListEntitiesServicesArgument args = 3; | ||||||
| } | } | ||||||
| message ExecuteServiceArgument { | message ExecuteServiceArgument { | ||||||
|  |   option (ifdef) = "USE_API_SERVICES"; | ||||||
|   bool bool_ = 1; |   bool bool_ = 1; | ||||||
|   int32 legacy_int = 2; |   int32 legacy_int = 2; | ||||||
|   float float_ = 3; |   float float_ = 3; | ||||||
| @@ -834,6 +837,7 @@ message ExecuteServiceRequest { | |||||||
|   option (id) = 42; |   option (id) = 42; | ||||||
|   option (source) = SOURCE_CLIENT; |   option (source) = SOURCE_CLIENT; | ||||||
|   option (no_delay) = true; |   option (no_delay) = true; | ||||||
|  |   option (ifdef) = "USE_API_SERVICES"; | ||||||
|  |  | ||||||
|   fixed32 key = 1; |   fixed32 key = 1; | ||||||
|   repeated ExecuteServiceArgument args = 2; |   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) { | void APIConnection::execute_service(const ExecuteServiceRequest &msg) { | ||||||
|   bool found = false; |   bool found = false; | ||||||
|   for (auto *service : this->parent_->get_user_services()) { |   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"); |     ESP_LOGV(TAG, "Could not find service"); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | #endif | ||||||
| #ifdef USE_API_NOISE | #ifdef USE_API_NOISE | ||||||
| NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) { | NoiseEncryptionSetKeyResponse APIConnection::noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) { | ||||||
|   psk_t psk{}; |   psk_t psk{}; | ||||||
|   | |||||||
| @@ -195,7 +195,9 @@ class APIConnection : public APIServerConnection { | |||||||
|     // TODO |     // TODO | ||||||
|     return {}; |     return {}; | ||||||
|   } |   } | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
|   void execute_service(const ExecuteServiceRequest &msg) override; |   void execute_service(const ExecuteServiceRequest &msg) override; | ||||||
|  | #endif | ||||||
| #ifdef USE_API_NOISE | #ifdef USE_API_NOISE | ||||||
|   NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override; |   NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override; | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -2046,6 +2046,7 @@ void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixe | |||||||
| void GetTimeResponse::calculate_size(uint32_t &total_size) const { | void GetTimeResponse::calculate_size(uint32_t &total_size) const { | ||||||
|   ProtoSize::add_fixed_field<4>(total_size, 1, this->epoch_seconds != 0); |   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) { | bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||||
|   switch (field_id) { |   switch (field_id) { | ||||||
|     case 2: { |     case 2: { | ||||||
| @@ -2238,6 +2239,7 @@ void ExecuteServiceRequest::calculate_size(uint32_t &total_size) const { | |||||||
|   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); |   ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0); | ||||||
|   ProtoSize::add_repeated_message(total_size, 1, this->args); |   ProtoSize::add_repeated_message(total_size, 1, this->args); | ||||||
| } | } | ||||||
|  | #endif | ||||||
| #ifdef USE_CAMERA | #ifdef USE_CAMERA | ||||||
| bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||||
|   switch (field_id) { |   switch (field_id) { | ||||||
|   | |||||||
| @@ -82,6 +82,7 @@ enum LogLevel : uint32_t { | |||||||
|   LOG_LEVEL_VERBOSE = 6, |   LOG_LEVEL_VERBOSE = 6, | ||||||
|   LOG_LEVEL_VERY_VERBOSE = 7, |   LOG_LEVEL_VERY_VERBOSE = 7, | ||||||
| }; | }; | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
| enum ServiceArgType : uint32_t { | enum ServiceArgType : uint32_t { | ||||||
|   SERVICE_ARG_TYPE_BOOL = 0, |   SERVICE_ARG_TYPE_BOOL = 0, | ||||||
|   SERVICE_ARG_TYPE_INT = 1, |   SERVICE_ARG_TYPE_INT = 1, | ||||||
| @@ -92,6 +93,7 @@ enum ServiceArgType : uint32_t { | |||||||
|   SERVICE_ARG_TYPE_FLOAT_ARRAY = 6, |   SERVICE_ARG_TYPE_FLOAT_ARRAY = 6, | ||||||
|   SERVICE_ARG_TYPE_STRING_ARRAY = 7, |   SERVICE_ARG_TYPE_STRING_ARRAY = 7, | ||||||
| }; | }; | ||||||
|  | #endif | ||||||
| #ifdef USE_CLIMATE | #ifdef USE_CLIMATE | ||||||
| enum ClimateMode : uint32_t { | enum ClimateMode : uint32_t { | ||||||
|   CLIMATE_MODE_OFF = 0, |   CLIMATE_MODE_OFF = 0, | ||||||
| @@ -1203,6 +1205,7 @@ class GetTimeResponse : public ProtoMessage { | |||||||
|  protected: |  protected: | ||||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; |   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||||
| }; | }; | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
| class ListEntitiesServicesArgument : public ProtoMessage { | class ListEntitiesServicesArgument : public ProtoMessage { | ||||||
|  public: |  public: | ||||||
|   std::string name{}; |   std::string name{}; | ||||||
| @@ -1278,6 +1281,7 @@ class ExecuteServiceRequest : public ProtoMessage { | |||||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; |   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; |   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||||
| }; | }; | ||||||
|  | #endif | ||||||
| #ifdef USE_CAMERA | #ifdef USE_CAMERA | ||||||
| class ListEntitiesCameraResponse : public InfoResponseProtoMessage { | class ListEntitiesCameraResponse : public InfoResponseProtoMessage { | ||||||
|  public: |  public: | ||||||
|   | |||||||
| @@ -162,6 +162,7 @@ template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel val | |||||||
|       return "UNKNOWN"; |       return "UNKNOWN"; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
| template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) { | template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) { | ||||||
|   switch (value) { |   switch (value) { | ||||||
|     case enums::SERVICE_ARG_TYPE_BOOL: |     case enums::SERVICE_ARG_TYPE_BOOL: | ||||||
| @@ -184,6 +185,7 @@ template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::Servic | |||||||
|       return "UNKNOWN"; |       return "UNKNOWN"; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | #endif | ||||||
| #ifdef USE_CLIMATE | #ifdef USE_CLIMATE | ||||||
| template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) { | template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) { | ||||||
|   switch (value) { |   switch (value) { | ||||||
| @@ -1811,6 +1813,7 @@ void GetTimeResponse::dump_to(std::string &out) const { | |||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|   out.append("}"); |   out.append("}"); | ||||||
| } | } | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
| void ListEntitiesServicesArgument::dump_to(std::string &out) const { | void ListEntitiesServicesArgument::dump_to(std::string &out) const { | ||||||
|   __attribute__((unused)) char buffer[64]; |   __attribute__((unused)) char buffer[64]; | ||||||
|   out.append("ListEntitiesServicesArgument {\n"); |   out.append("ListEntitiesServicesArgument {\n"); | ||||||
| @@ -1910,6 +1913,7 @@ void ExecuteServiceRequest::dump_to(std::string &out) const { | |||||||
|   } |   } | ||||||
|   out.append("}"); |   out.append("}"); | ||||||
| } | } | ||||||
|  | #endif | ||||||
| #ifdef USE_CAMERA | #ifdef USE_CAMERA | ||||||
| void ListEntitiesCameraResponse::dump_to(std::string &out) const { | void ListEntitiesCameraResponse::dump_to(std::string &out) const { | ||||||
|   __attribute__((unused)) char buffer[64]; |   __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); |       this->on_home_assistant_state_response(msg); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
|     case 42: { |     case 42: { | ||||||
|       ExecuteServiceRequest msg; |       ExecuteServiceRequest msg; | ||||||
|       msg.decode(msg_data, msg_size); |       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); |       this->on_execute_service_request(msg); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|  | #endif | ||||||
| #ifdef USE_CAMERA | #ifdef USE_CAMERA | ||||||
|     case 45: { |     case 45: { | ||||||
|       CameraImageRequest msg; |       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) { | void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { | ||||||
|   if (this->check_authenticated_()) { |   if (this->check_authenticated_()) { | ||||||
|     this->execute_service(msg); |     this->execute_service(msg); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | #endif | ||||||
| #ifdef USE_API_NOISE | #ifdef USE_API_NOISE | ||||||
| void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { | void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { | ||||||
|   if (this->check_authenticated_()) { |   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_request(const GetTimeRequest &value){}; | ||||||
|   virtual void on_get_time_response(const GetTimeResponse &value){}; |   virtual void on_get_time_response(const GetTimeResponse &value){}; | ||||||
|  |  | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
|   virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; |   virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; | ||||||
|  | #endif | ||||||
|  |  | ||||||
| #ifdef USE_CAMERA | #ifdef USE_CAMERA | ||||||
|   virtual void on_camera_image_request(const CameraImageRequest &value){}; |   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_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; | ||||||
|   virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; |   virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; | ||||||
|   virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; |   virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
|   virtual void execute_service(const ExecuteServiceRequest &msg) = 0; |   virtual void execute_service(const ExecuteServiceRequest &msg) = 0; | ||||||
|  | #endif | ||||||
| #ifdef USE_API_NOISE | #ifdef USE_API_NOISE | ||||||
|   virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0; |   virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0; | ||||||
| #endif | #endif | ||||||
| @@ -333,7 +337,9 @@ class APIServerConnection : public APIServerConnectionBase { | |||||||
|   void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; |   void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; | ||||||
|   void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; |   void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; | ||||||
|   void on_get_time_request(const GetTimeRequest &msg) override; |   void on_get_time_request(const GetTimeRequest &msg) override; | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
|   void on_execute_service_request(const ExecuteServiceRequest &msg) override; |   void on_execute_service_request(const ExecuteServiceRequest &msg) override; | ||||||
|  | #endif | ||||||
| #ifdef USE_API_NOISE | #ifdef USE_API_NOISE | ||||||
|   void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; |   void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -24,14 +24,6 @@ static const char *const TAG = "api"; | |||||||
| // APIServer | // APIServer | ||||||
| APIServer *global_api_server = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | 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() { | APIServer::APIServer() { | ||||||
|   global_api_server = this; |   global_api_server = this; | ||||||
|   // Pre-allocate shared write buffer |   // Pre-allocate shared write buffer | ||||||
|   | |||||||
| @@ -12,7 +12,9 @@ | |||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include "list_entities.h" | #include "list_entities.h" | ||||||
| #include "subscribe_state.h" | #include "subscribe_state.h" | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
| #include "user_services.h" | #include "user_services.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
| #include <vector> | #include <vector> | ||||||
|  |  | ||||||
| @@ -25,11 +27,6 @@ struct SavedNoisePsk { | |||||||
| } PACKED;  // NOLINT | } PACKED;  // NOLINT | ||||||
| #endif | #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 { | class APIServer : public Component, public Controller { | ||||||
|  public: |  public: | ||||||
|   APIServer(); |   APIServer(); | ||||||
| @@ -111,18 +108,9 @@ class APIServer : public Component, public Controller { | |||||||
|   void on_media_player_update(media_player::MediaPlayer *obj) override; |   void on_media_player_update(media_player::MediaPlayer *obj) override; | ||||||
| #endif | #endif | ||||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call); |   void send_homeassistant_service_call(const HomeassistantServiceResponse &call); | ||||||
|   void register_user_service(UserServiceDescriptor *descriptor) { | #ifdef USE_API_SERVICES | ||||||
| #ifdef USE_API_YAML_SERVICES |   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } | ||||||
|     // 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); |  | ||||||
| #endif | #endif | ||||||
|   } |  | ||||||
| #ifdef USE_HOMEASSISTANT_TIME | #ifdef USE_HOMEASSISTANT_TIME | ||||||
|   void request_time(); |   void request_time(); | ||||||
| #endif | #endif | ||||||
| @@ -151,17 +139,9 @@ class APIServer : public Component, public Controller { | |||||||
|   void get_home_assistant_state(std::string entity_id, optional<std::string> attribute, |   void get_home_assistant_state(std::string entity_id, optional<std::string> attribute, | ||||||
|                                 std::function<void(std::string)> f); |                                 std::function<void(std::string)> f); | ||||||
|   const std::vector<HomeAssistantStateSubscription> &get_state_subs() const; |   const std::vector<HomeAssistantStateSubscription> &get_state_subs() const; | ||||||
|   const std::vector<UserServiceDescriptor *> &get_user_services() const { | #ifdef USE_API_SERVICES | ||||||
| #ifdef USE_API_YAML_SERVICES |   const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_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(); |  | ||||||
| #endif | #endif | ||||||
|   } |  | ||||||
|  |  | ||||||
| #ifdef USE_API_CLIENT_CONNECTED_TRIGGER | #ifdef USE_API_CLIENT_CONNECTED_TRIGGER | ||||||
|   Trigger<std::string, std::string> *get_client_connected_trigger() const { return this->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 | #endif | ||||||
|   std::vector<uint8_t> shared_write_buffer_;  // Shared proto write buffer for all connections |   std::vector<uint8_t> shared_write_buffer_;  // Shared proto write buffer for all connections | ||||||
|   std::vector<HomeAssistantStateSubscription> state_subs_; |   std::vector<HomeAssistantStateSubscription> state_subs_; | ||||||
| #ifdef USE_API_YAML_SERVICES | #ifdef USE_API_SERVICES | ||||||
|   // When services are defined in YAML, we know at compile time that services will be registered |  | ||||||
|   std::vector<UserServiceDescriptor *> user_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 | #endif | ||||||
|  |  | ||||||
|   // Group smaller types together |   // Group smaller types together | ||||||
|   | |||||||
| @@ -3,10 +3,13 @@ | |||||||
| #include <map> | #include <map> | ||||||
| #include "api_server.h" | #include "api_server.h" | ||||||
| #ifdef USE_API | #ifdef USE_API | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
| #include "user_services.h" | #include "user_services.h" | ||||||
|  | #endif | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace api { | namespace api { | ||||||
|  |  | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
| template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> { | template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> { | ||||||
|  public: |  public: | ||||||
|   CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj, |   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_; |   T *obj_; | ||||||
|   void (T::*callback_)(Ts...); |   void (T::*callback_)(Ts...); | ||||||
| }; | }; | ||||||
|  | #endif  // USE_API_SERVICES | ||||||
|  |  | ||||||
| class CustomAPIDevice { | class CustomAPIDevice { | ||||||
|  public: |  public: | ||||||
| @@ -46,12 +50,14 @@ class CustomAPIDevice { | |||||||
|    * @param name The name of the service to register. |    * @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. |    * @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> |   template<typename T, typename... Ts> | ||||||
|   void register_service(void (T::*callback)(Ts...), const std::string &name, |   void register_service(void (T::*callback)(Ts...), const std::string &name, | ||||||
|                         const std::array<std::string, sizeof...(Ts)> &arg_names) { |                         const std::array<std::string, sizeof...(Ts)> &arg_names) { | ||||||
|     auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback);  // NOLINT |     auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback);  // NOLINT | ||||||
|     global_api_server->register_user_service(service); |     global_api_server->register_user_service(service); | ||||||
|   } |   } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|   /** Register a custom native API service that will show up in Home Assistant. |   /** 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 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. |    * @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) { |   template<typename T> void register_service(void (T::*callback)(), const std::string &name) { | ||||||
|     auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback);  // NOLINT |     auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback);  // NOLINT | ||||||
|     global_api_server->register_user_service(service); |     global_api_server->register_user_service(service); | ||||||
|   } |   } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|   /** Subscribe to the state (or attribute state) of an entity from Home Assistant. |   /** 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) {} | ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} | ||||||
|  |  | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
| bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { | bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { | ||||||
|   auto resp = service->encode_list_service_response(); |   auto resp = service->encode_list_service_response(); | ||||||
|   return this->client_->send_message(resp); |   return this->client_->send_message(resp); | ||||||
| } | } | ||||||
|  | #endif | ||||||
|  |  | ||||||
| }  // namespace api | }  // namespace api | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -44,7 +44,9 @@ class ListEntitiesIterator : public ComponentIterator { | |||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|   bool on_text_sensor(text_sensor::TextSensor *entity) override; |   bool on_text_sensor(text_sensor::TextSensor *entity) override; | ||||||
| #endif | #endif | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
|   bool on_service(UserServiceDescriptor *service) override; |   bool on_service(UserServiceDescriptor *service) override; | ||||||
|  | #endif | ||||||
| #ifdef USE_CAMERA | #ifdef USE_CAMERA | ||||||
|   bool on_camera(camera::Camera *entity) override; |   bool on_camera(camera::Camera *entity) override; | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ | |||||||
| #include "esphome/core/automation.h" | #include "esphome/core/automation.h" | ||||||
| #include "api_pb2.h" | #include "api_pb2.h" | ||||||
|  |  | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace api { | namespace api { | ||||||
|  |  | ||||||
| @@ -73,3 +74,4 @@ template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts... | |||||||
|  |  | ||||||
| }  // namespace api | }  // namespace api | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|  | #endif  // USE_API_SERVICES | ||||||
|   | |||||||
| @@ -4,6 +4,8 @@ | |||||||
|  |  | ||||||
| #ifdef USE_API | #ifdef USE_API | ||||||
| #include "esphome/components/api/api_server.h" | #include "esphome/components/api/api_server.h" | ||||||
|  | #endif | ||||||
|  | #ifdef USE_API_SERVICES | ||||||
| #include "esphome/components/api/user_services.h" | #include "esphome/components/api/user_services.h" | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| @@ -148,7 +150,7 @@ void ComponentIterator::advance() { | |||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
| #ifdef USE_API | #ifdef USE_API_SERVICES | ||||||
|     case IteratorState ::SERVICE: |     case IteratorState ::SERVICE: | ||||||
|       if (this->at_ >= api::global_api_server->get_user_services().size()) { |       if (this->at_ >= api::global_api_server->get_user_services().size()) { | ||||||
|         advance_platform = true; |         advance_platform = true; | ||||||
| @@ -383,7 +385,7 @@ void ComponentIterator::advance() { | |||||||
| } | } | ||||||
| bool ComponentIterator::on_end() { return true; } | bool ComponentIterator::on_end() { return true; } | ||||||
| bool ComponentIterator::on_begin() { return true; } | bool ComponentIterator::on_begin() { return true; } | ||||||
| #ifdef USE_API | #ifdef USE_API_SERVICES | ||||||
| bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; } | bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; } | ||||||
| #endif | #endif | ||||||
| #ifdef USE_CAMERA | #ifdef USE_CAMERA | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ | |||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
|  |  | ||||||
| #ifdef USE_API | #ifdef USE_API_SERVICES | ||||||
| namespace api { | namespace api { | ||||||
| class UserServiceDescriptor; | class UserServiceDescriptor; | ||||||
| }  // namespace api | }  // namespace api | ||||||
| @@ -45,7 +45,7 @@ class ComponentIterator { | |||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|   virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; |   virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; | ||||||
| #endif | #endif | ||||||
| #ifdef USE_API | #ifdef USE_API_SERVICES | ||||||
|   virtual bool on_service(api::UserServiceDescriptor *service); |   virtual bool on_service(api::UserServiceDescriptor *service); | ||||||
| #endif | #endif | ||||||
| #ifdef USE_CAMERA | #ifdef USE_CAMERA | ||||||
| @@ -122,7 +122,7 @@ class ComponentIterator { | |||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|     TEXT_SENSOR, |     TEXT_SENSOR, | ||||||
| #endif | #endif | ||||||
| #ifdef USE_API | #ifdef USE_API_SERVICES | ||||||
|     SERVICE, |     SERVICE, | ||||||
| #endif | #endif | ||||||
| #ifdef USE_CAMERA | #ifdef USE_CAMERA | ||||||
|   | |||||||
| @@ -108,7 +108,7 @@ | |||||||
| #define USE_API_CLIENT_DISCONNECTED_TRIGGER | #define USE_API_CLIENT_DISCONNECTED_TRIGGER | ||||||
| #define USE_API_NOISE | #define USE_API_NOISE | ||||||
| #define USE_API_PLAINTEXT | #define USE_API_PLAINTEXT | ||||||
| #define USE_API_YAML_SERVICES | #define USE_API_SERVICES | ||||||
| #define USE_MD5 | #define USE_MD5 | ||||||
| #define USE_MQTT | #define USE_MQTT | ||||||
| #define USE_NETWORK | #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(const 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(const 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