mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Fix compilation error when using string lambdas with homeassistant services (#9543)
This commit is contained in:
		
				
					committed by
					
						 Jesse Hills
						Jesse Hills
					
				
			
			
				
	
			
			
			
						parent
						
							cd987feb5b
						
					
				
				
					commit
					efcad565ee
				
			| @@ -11,6 +11,15 @@ namespace esphome { | |||||||
| namespace api { | namespace api { | ||||||
|  |  | ||||||
| template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> { | template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> { | ||||||
|  |  private: | ||||||
|  |   // Helper to convert value to string - handles the case where value is already a string | ||||||
|  |   template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); } | ||||||
|  |  | ||||||
|  |   // Overloads for string types - needed because std::to_string doesn't support them | ||||||
|  |   static std::string value_to_string(const char *val) { return std::string(val); }  // For lambdas returning .c_str() | ||||||
|  |   static std::string value_to_string(const std::string &val) { return val; } | ||||||
|  |   static std::string value_to_string(std::string &&val) { return std::move(val); } | ||||||
|  |  | ||||||
|  public: |  public: | ||||||
|   TemplatableStringValue() : TemplatableValue<std::string, X...>() {} |   TemplatableStringValue() : TemplatableValue<std::string, X...>() {} | ||||||
|  |  | ||||||
| @@ -19,7 +28,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s | |||||||
|  |  | ||||||
|   template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> |   template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> | ||||||
|   TemplatableStringValue(F f) |   TemplatableStringValue(F f) | ||||||
|       : TemplatableValue<std::string, X...>([f](X... x) -> std::string { return to_string(f(x...)); }) {} |       : TemplatableValue<std::string, X...>([f](X... x) -> std::string { return value_to_string(f(x...)); }) {} | ||||||
| }; | }; | ||||||
|  |  | ||||||
| template<typename... Ts> class TemplatableKeyValuePair { | template<typename... Ts> class TemplatableKeyValuePair { | ||||||
|   | |||||||
							
								
								
									
										64
									
								
								tests/integration/fixtures/api_string_lambda.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								tests/integration/fixtures/api_string_lambda.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | esphome: | ||||||
|  |   name: api-string-lambda-test | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   actions: | ||||||
|  |     # Service that tests string lambda functionality | ||||||
|  |     - action: test_string_lambda | ||||||
|  |       variables: | ||||||
|  |         input_string: string | ||||||
|  |       then: | ||||||
|  |         # Log the input to verify service was called | ||||||
|  |         - logger.log: | ||||||
|  |             format: "Service called with string: %s" | ||||||
|  |             args: [input_string.c_str()] | ||||||
|  |  | ||||||
|  |         # This is the key test - using a lambda that returns x.c_str() | ||||||
|  |         # where x is already a string. This would fail to compile in 2025.7.0b5 | ||||||
|  |         # with "no matching function for call to 'to_string(std::string)'" | ||||||
|  |         # This is the exact case from issue #9539 | ||||||
|  |         - homeassistant.tag_scanned: !lambda 'return input_string.c_str();' | ||||||
|  |  | ||||||
|  |         # Also test with homeassistant.event to verify our fix works with data fields | ||||||
|  |         - homeassistant.event: | ||||||
|  |             event: esphome.test_string_lambda | ||||||
|  |             data: | ||||||
|  |               value: !lambda 'return input_string.c_str();' | ||||||
|  |  | ||||||
|  |     # Service that tests int lambda functionality | ||||||
|  |     - action: test_int_lambda | ||||||
|  |       variables: | ||||||
|  |         input_number: int | ||||||
|  |       then: | ||||||
|  |         # Log the input to verify service was called | ||||||
|  |         - logger.log: | ||||||
|  |             format: "Service called with int: %d" | ||||||
|  |             args: [input_number] | ||||||
|  |  | ||||||
|  |         # Test that int lambdas still work correctly with to_string | ||||||
|  |         # The TemplatableStringValue should automatically convert int to string | ||||||
|  |         - homeassistant.event: | ||||||
|  |             event: esphome.test_int_lambda | ||||||
|  |             data: | ||||||
|  |               value: !lambda 'return input_number;' | ||||||
|  |  | ||||||
|  |     # Service that tests float lambda functionality | ||||||
|  |     - action: test_float_lambda | ||||||
|  |       variables: | ||||||
|  |         input_float: float | ||||||
|  |       then: | ||||||
|  |         # Log the input to verify service was called | ||||||
|  |         - logger.log: | ||||||
|  |             format: "Service called with float: %.2f" | ||||||
|  |             args: [input_float] | ||||||
|  |  | ||||||
|  |         # Test that float lambdas still work correctly with to_string | ||||||
|  |         # The TemplatableStringValue should automatically convert float to string | ||||||
|  |         - homeassistant.event: | ||||||
|  |             event: esphome.test_float_lambda | ||||||
|  |             data: | ||||||
|  |               value: !lambda 'return input_float;' | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: DEBUG | ||||||
							
								
								
									
										85
									
								
								tests/integration/test_api_string_lambda.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								tests/integration/test_api_string_lambda.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | """Integration test for TemplatableStringValue with string lambdas.""" | ||||||
|  |  | ||||||
|  | from __future__ import annotations | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_api_string_lambda( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test TemplatableStringValue works with lambdas that return different types.""" | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |  | ||||||
|  |     # Track log messages for all three service calls | ||||||
|  |     string_called_future = loop.create_future() | ||||||
|  |     int_called_future = loop.create_future() | ||||||
|  |     float_called_future = loop.create_future() | ||||||
|  |  | ||||||
|  |     # Patterns to match in logs - confirms the lambdas compiled and executed | ||||||
|  |     string_pattern = re.compile(r"Service called with string: STRING_FROM_LAMBDA") | ||||||
|  |     int_pattern = re.compile(r"Service called with int: 42") | ||||||
|  |     float_pattern = re.compile(r"Service called with float: 3\.14") | ||||||
|  |  | ||||||
|  |     def check_output(line: str) -> None: | ||||||
|  |         """Check log output for expected messages.""" | ||||||
|  |         if not string_called_future.done() and string_pattern.search(line): | ||||||
|  |             string_called_future.set_result(True) | ||||||
|  |         if not int_called_future.done() and int_pattern.search(line): | ||||||
|  |             int_called_future.set_result(True) | ||||||
|  |         if not float_called_future.done() and float_pattern.search(line): | ||||||
|  |             float_called_future.set_result(True) | ||||||
|  |  | ||||||
|  |     # Run with log monitoring | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=check_output), | ||||||
|  |         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-string-lambda-test" | ||||||
|  |  | ||||||
|  |         # List services to find our test services | ||||||
|  |         _, services = await client.list_entities_services() | ||||||
|  |  | ||||||
|  |         # Find all test services | ||||||
|  |         string_service = next( | ||||||
|  |             (s for s in services if s.name == "test_string_lambda"), None | ||||||
|  |         ) | ||||||
|  |         assert string_service is not None, "test_string_lambda service not found" | ||||||
|  |  | ||||||
|  |         int_service = next((s for s in services if s.name == "test_int_lambda"), None) | ||||||
|  |         assert int_service is not None, "test_int_lambda service not found" | ||||||
|  |  | ||||||
|  |         float_service = next( | ||||||
|  |             (s for s in services if s.name == "test_float_lambda"), None | ||||||
|  |         ) | ||||||
|  |         assert float_service is not None, "test_float_lambda service not found" | ||||||
|  |  | ||||||
|  |         # Execute all three services to test different lambda return types | ||||||
|  |         client.execute_service(string_service, {"input_string": "STRING_FROM_LAMBDA"}) | ||||||
|  |         client.execute_service(int_service, {"input_number": 42}) | ||||||
|  |         client.execute_service(float_service, {"input_float": 3.14}) | ||||||
|  |  | ||||||
|  |         # Wait for all service log messages | ||||||
|  |         # This confirms the lambdas compiled successfully and executed | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for( | ||||||
|  |                 asyncio.gather( | ||||||
|  |                     string_called_future, int_called_future, float_called_future | ||||||
|  |                 ), | ||||||
|  |                 timeout=5.0, | ||||||
|  |             ) | ||||||
|  |         except TimeoutError: | ||||||
|  |             pytest.fail( | ||||||
|  |                 "One or more service log messages not received - lambda may have failed to compile or execute" | ||||||
|  |             ) | ||||||
		Reference in New Issue
	
	Block a user