mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	This commit is contained in:
		
				
					committed by
					
						 Jesse Hills
						Jesse Hills
					
				
			
			
				
	
			
			
			
						parent
						
							3c864b2bca
						
					
				
				
					commit
					42947bcf56
				
			
							
								
								
									
										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,53 @@ | ||||
| #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!"); } | ||||
|  | ||||
| // NOLINTNEXTLINE(performance-unnecessary-value-param) | ||||
| 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: %s", bool_array[0] ? "true" : "false"); | ||||
|   } | ||||
|   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,29 @@ | ||||
| #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(); | ||||
|  | ||||
|   // NOLINTNEXTLINE(performance-unnecessary-value-param) | ||||
|   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