mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'more_flexible_template' into integration
This commit is contained in:
		
							
								
								
									
										2
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							| @@ -62,7 +62,7 @@ jobs: | ||||
|         run: git diff | ||||
|       - if: failure() | ||||
|         name: Archive artifacts | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         with: | ||||
|           name: generated-proto-files | ||||
|           path: | | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -849,7 +849,7 @@ jobs: | ||||
|           fi | ||||
|  | ||||
|       - name: Upload memory analysis JSON | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         with: | ||||
|           name: memory-analysis-target | ||||
|           path: memory-analysis-target.json | ||||
| @@ -913,7 +913,7 @@ jobs: | ||||
|             --platform "$platform" | ||||
|  | ||||
|       - name: Upload memory analysis JSON | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         with: | ||||
|           name: memory-analysis-pr | ||||
|           path: memory-analysis-pr.json | ||||
| @@ -943,13 +943,13 @@ jobs: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Download target analysis JSON | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | ||||
|         with: | ||||
|           name: memory-analysis-target | ||||
|           path: ./memory-analysis | ||||
|         continue-on-error: true | ||||
|       - name: Download PR analysis JSON | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | ||||
|         with: | ||||
|           name: memory-analysis-pr | ||||
|           path: ./memory-analysis | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							| @@ -58,7 +58,7 @@ jobs: | ||||
|  | ||||
|       # Initializes the CodeQL tools for scanning. | ||||
|       - name: Initialize CodeQL | ||||
|         uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 | ||||
|         uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 | ||||
|         with: | ||||
|           languages: ${{ matrix.language }} | ||||
|           build-mode: ${{ matrix.build-mode }} | ||||
| @@ -86,6 +86,6 @@ jobs: | ||||
|           exit 1 | ||||
|  | ||||
|       - name: Perform CodeQL Analysis | ||||
|         uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 | ||||
|         uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 | ||||
|         with: | ||||
|           category: "/language:${{matrix.language}}" | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -138,7 +138,7 @@ jobs: | ||||
|       #     version: ${{ needs.init.outputs.tag }} | ||||
|  | ||||
|       - name: Upload digests | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         with: | ||||
|           name: digests-${{ matrix.platform.arch }} | ||||
|           path: /tmp/digests | ||||
| @@ -171,7 +171,7 @@ jobs: | ||||
|       - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|  | ||||
|       - name: Download digests | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | ||||
|         with: | ||||
|           pattern: digests-* | ||||
|           path: /tmp/digests | ||||
|   | ||||
| @@ -11,7 +11,7 @@ ci: | ||||
| repos: | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     # Ruff version. | ||||
|     rev: v0.14.1 | ||||
|     rev: v0.14.2 | ||||
|     hooks: | ||||
|       # Run the linter. | ||||
|       - id: ruff | ||||
|   | ||||
| @@ -172,12 +172,6 @@ def alarm_control_panel_schema( | ||||
|     return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel) | ||||
| ALARM_CONTROL_PANEL_SCHEMA.add_extra( | ||||
|     cv.deprecated_schema_constant("alarm_control_panel") | ||||
| ) | ||||
|  | ||||
| ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( | ||||
|     { | ||||
|         cv.GenerateID(): cv.use_id(AlarmControlPanel), | ||||
|   | ||||
| @@ -548,11 +548,6 @@ def binary_sensor_schema( | ||||
|     return _BINARY_SENSOR_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| BINARY_SENSOR_SCHEMA = binary_sensor_schema() | ||||
| BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor")) | ||||
|  | ||||
|  | ||||
| async def setup_binary_sensor_core_(var, config): | ||||
|     await setup_entity(var, config, "binary_sensor") | ||||
|  | ||||
|   | ||||
| @@ -84,11 +84,6 @@ def button_schema( | ||||
|     return _BUTTON_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| BUTTON_SCHEMA = button_schema(Button) | ||||
| BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button")) | ||||
|  | ||||
|  | ||||
| async def setup_button_core_(var, config): | ||||
|     await setup_entity(var, config, "button") | ||||
|  | ||||
|   | ||||
| @@ -270,11 +270,6 @@ def climate_schema( | ||||
|     return _CLIMATE_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| CLIMATE_SCHEMA = climate_schema(Climate) | ||||
| CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate")) | ||||
|  | ||||
|  | ||||
| async def setup_climate_core_(var, config): | ||||
|     await setup_entity(var, config, "climate") | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,9 @@ | ||||
| import logging | ||||
|  | ||||
| from esphome import core | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import climate, remote_base, sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT | ||||
| from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT | ||||
| from esphome.cpp_generator import MockObjClass | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
| @@ -52,26 +51,6 @@ def climate_ir_with_receiver_schema( | ||||
|     ) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| def deprecated_schema_constant(config): | ||||
|     type: str = "unknown" | ||||
|     if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID): | ||||
|         type = str(id.type).split("::", maxsplit=1)[0] | ||||
|     _LOGGER.warning( | ||||
|         "Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " | ||||
|         "Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. " | ||||
|         "If you are seeing this, report an issue to the external_component author and ask them to update it. " | ||||
|         "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. " | ||||
|         "Component using this schema: %s", | ||||
|         type, | ||||
|     ) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR) | ||||
| CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant) | ||||
|  | ||||
|  | ||||
| async def register_climate_ir(var, config): | ||||
|     await cg.register_component(var, config) | ||||
|     await remote_base.register_transmittable(var, config) | ||||
|   | ||||
| @@ -151,11 +151,6 @@ def cover_schema( | ||||
|     return _COVER_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| COVER_SCHEMA = cover_schema(Cover) | ||||
| COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover")) | ||||
|  | ||||
|  | ||||
| async def setup_cover_core_(var, config): | ||||
|     await setup_entity(var, config, "cover") | ||||
|  | ||||
|   | ||||
| @@ -85,11 +85,6 @@ def event_schema( | ||||
|     return _EVENT_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| EVENT_SCHEMA = event_schema() | ||||
| EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event")) | ||||
|  | ||||
|  | ||||
| async def setup_event_core_(var, config, *, event_types: list[str]): | ||||
|     await setup_entity(var, config, "event") | ||||
|  | ||||
|   | ||||
| @@ -189,10 +189,6 @@ def fan_schema( | ||||
|     return _FAN_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| FAN_SCHEMA = fan_schema(Fan) | ||||
| FAN_SCHEMA.add_extra(cv.deprecated_schema_constant("fan")) | ||||
|  | ||||
| _PRESET_MODES_SCHEMA = cv.All( | ||||
|     cv.ensure_list(cv.string_strict), | ||||
|     cv.Length(min=1), | ||||
|   | ||||
| @@ -91,11 +91,6 @@ def lock_schema( | ||||
|     return _LOCK_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| LOCK_SCHEMA = lock_schema() | ||||
| LOCK_SCHEMA.add_extra(cv.deprecated_schema_constant("lock")) | ||||
|  | ||||
|  | ||||
| async def _setup_lock_core(var, config): | ||||
|     await setup_entity(var, config, "lock") | ||||
|  | ||||
|   | ||||
| @@ -5,6 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i | ||||
| """ | ||||
|  | ||||
| import logging | ||||
| from typing import TYPE_CHECKING, Any | ||||
|  | ||||
| from esphome import codegen as cg, config_validation as cv | ||||
| from esphome.const import CONF_ITEMS | ||||
| @@ -12,6 +13,7 @@ from esphome.core import ID, Lambda | ||||
| from esphome.cpp_generator import LambdaExpression, MockObj | ||||
| from esphome.cpp_types import uint32 | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
| from esphome.types import Expression, SafeExpType | ||||
|  | ||||
| from .helpers import requires_component | ||||
|  | ||||
| @@ -42,7 +44,13 @@ def static_cast(type, value): | ||||
| def call_lambda(lamb: LambdaExpression): | ||||
|     expr = lamb.content.strip() | ||||
|     if expr.startswith("return") and expr.endswith(";"): | ||||
|         return expr[6:][:-1].strip() | ||||
|         return expr[6:-1].strip() | ||||
|     # If lambda has parameters, call it with those parameter names | ||||
|     # Parameter names come from hardcoded component code (like "x", "it", "event") | ||||
|     # not from user input, so they're safe to use directly | ||||
|     if lamb.parameters and lamb.parameters.parameters: | ||||
|         param_names = ", ".join(str(param.id) for param in lamb.parameters.parameters) | ||||
|         return f"{lamb}({param_names})" | ||||
|     return f"{lamb}()" | ||||
|  | ||||
|  | ||||
| @@ -65,10 +73,20 @@ class LValidator: | ||||
|             return cv.returning_lambda(value) | ||||
|         return self.validator(value) | ||||
|  | ||||
|     async def process(self, value, args=()): | ||||
|     async def process( | ||||
|         self, value: Any, args: list[tuple[SafeExpType, str]] | None = None | ||||
|     ) -> Expression: | ||||
|         if value is None: | ||||
|             return None | ||||
|         if isinstance(value, Lambda): | ||||
|             # Local import to avoid circular import | ||||
|             from .lvcode import CodeContext, LambdaContext | ||||
|  | ||||
|             if TYPE_CHECKING: | ||||
|                 # CodeContext does not have get_automation_parameters | ||||
|                 # so we need to assert the type here | ||||
|                 assert isinstance(CodeContext.code_context, LambdaContext) | ||||
|             args = args or CodeContext.code_context.get_automation_parameters() | ||||
|             return cg.RawExpression( | ||||
|                 call_lambda( | ||||
|                     await cg.process_lambda(value, args, return_type=self.rtype) | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| from typing import TYPE_CHECKING, Any | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import image | ||||
| from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw | ||||
| @@ -17,6 +19,7 @@ from esphome.cpp_generator import MockObj | ||||
| from esphome.cpp_types import ESPTime, int32, uint32 | ||||
| from esphome.helpers import cpp_string_escape | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
| from esphome.types import Expression, SafeExpType | ||||
|  | ||||
| from . import types as ty | ||||
| from .defines import ( | ||||
| @@ -388,11 +391,23 @@ class TextValidator(LValidator): | ||||
|             return value | ||||
|         return super().__call__(value) | ||||
|  | ||||
|     async def process(self, value, args=()): | ||||
|     async def process( | ||||
|         self, value: Any, args: list[tuple[SafeExpType, str]] | None = None | ||||
|     ) -> Expression: | ||||
|         # Local import to avoid circular import at module level | ||||
|  | ||||
|         from .lvcode import CodeContext, LambdaContext | ||||
|  | ||||
|         if TYPE_CHECKING: | ||||
|             # CodeContext does not have get_automation_parameters | ||||
|             # so we need to assert the type here | ||||
|             assert isinstance(CodeContext.code_context, LambdaContext) | ||||
|         args = args or CodeContext.code_context.get_automation_parameters() | ||||
|  | ||||
|         if isinstance(value, dict): | ||||
|             if format_str := value.get(CONF_FORMAT): | ||||
|                 args = [str(x) for x in value[CONF_ARGS]] | ||||
|                 arg_expr = cg.RawExpression(",".join(args)) | ||||
|                 str_args = [str(x) for x in value[CONF_ARGS]] | ||||
|                 arg_expr = cg.RawExpression(",".join(str_args)) | ||||
|                 format_str = cpp_string_escape(format_str) | ||||
|                 return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") | ||||
|             if time_format := value.get(CONF_TIME_FORMAT): | ||||
|   | ||||
| @@ -164,6 +164,9 @@ class LambdaContext(CodeContext): | ||||
|             code_text.append(text) | ||||
|         return code_text | ||||
|  | ||||
|     def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]: | ||||
|         return self.parameters | ||||
|  | ||||
|     async def __aenter__(self): | ||||
|         await super().__aenter__() | ||||
|         add_line_marks(self.where) | ||||
| @@ -178,9 +181,8 @@ class LvContext(LambdaContext): | ||||
|  | ||||
|     added_lambda_count = 0 | ||||
|  | ||||
|     def __init__(self, args=None): | ||||
|         self.args = args or LVGL_COMP_ARG | ||||
|         super().__init__(parameters=self.args) | ||||
|     def __init__(self): | ||||
|         super().__init__(parameters=LVGL_COMP_ARG) | ||||
|  | ||||
|     async def __aexit__(self, exc_type, exc_val, exc_tb): | ||||
|         await super().__aexit__(exc_type, exc_val, exc_tb) | ||||
| @@ -189,6 +191,11 @@ class LvContext(LambdaContext): | ||||
|         cg.add(expression) | ||||
|         return expression | ||||
|  | ||||
|     def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]: | ||||
|         # When generating automations, we don't want the `lv_component` parameter to be passed | ||||
|         # to the lambda. | ||||
|         return [] | ||||
|  | ||||
|     def __call__(self, *args): | ||||
|         return self.add(*args) | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,6 @@ from ..defines import CONF_WIDGET | ||||
| from ..lvcode import ( | ||||
|     API_EVENT, | ||||
|     EVENT_ARG, | ||||
|     LVGL_COMP_ARG, | ||||
|     UPDATE_EVENT, | ||||
|     LambdaContext, | ||||
|     LvContext, | ||||
| @@ -30,7 +29,7 @@ async def to_code(config): | ||||
|     await wait_for_widgets() | ||||
|     async with LambdaContext(EVENT_ARG) as lamb: | ||||
|         lv_add(sensor.publish_state(widget.get_value())) | ||||
|     async with LvContext(LVGL_COMP_ARG): | ||||
|     async with LvContext(): | ||||
|         lv_add( | ||||
|             lvgl_static.add_event_cb( | ||||
|                 widget.obj, | ||||
|   | ||||
| @@ -192,10 +192,6 @@ def media_player_schema( | ||||
|     return _MEDIA_PLAYER_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer) | ||||
| MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player")) | ||||
|  | ||||
| MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id( | ||||
|     cv.Schema( | ||||
|         { | ||||
|   | ||||
| @@ -238,11 +238,6 @@ def number_schema( | ||||
|     return _NUMBER_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| NUMBER_SCHEMA = number_schema(Number) | ||||
| NUMBER_SCHEMA.add_extra(cv.deprecated_schema_constant("number")) | ||||
|  | ||||
|  | ||||
| async def setup_number_core_( | ||||
|     var, config, *, min_value: float, max_value: float, step: float | ||||
| ): | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import logging | ||||
|  | ||||
| from esphome import automation, pins | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import esp32, esp32_rmt, remote_base | ||||
| @@ -18,9 +20,12 @@ from esphome.const import ( | ||||
| ) | ||||
| from esphome.core import CORE | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| AUTO_LOAD = ["remote_base"] | ||||
|  | ||||
| CONF_EOT_LEVEL = "eot_level" | ||||
| CONF_NON_BLOCKING = "non_blocking" | ||||
| CONF_ON_TRANSMIT = "on_transmit" | ||||
| CONF_ON_COMPLETE = "on_complete" | ||||
| CONF_TRANSMITTER_ID = remote_base.CONF_TRANSMITTER_ID | ||||
| @@ -65,11 +70,25 @@ CONFIG_SCHEMA = cv.Schema( | ||||
|             esp32_c6=48, | ||||
|             esp32_h2=48, | ||||
|         ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), | ||||
|         cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), | ||||
|         cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), | ||||
|         cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
|  | ||||
| def _validate_non_blocking(config): | ||||
|     if CORE.is_esp32 and CONF_NON_BLOCKING not in config: | ||||
|         _LOGGER.warning( | ||||
|             "'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n" | ||||
|             "The default behavior changed in 2025.11.0; previously blocking mode was used.\n" | ||||
|             "To silence this warning, explicitly set 'non_blocking: true' (or 'false')." | ||||
|         ) | ||||
|         config[CONF_NON_BLOCKING] = True | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = _validate_non_blocking | ||||
|  | ||||
| DIGITAL_WRITE_ACTION_SCHEMA = cv.maybe_simple_value( | ||||
|     { | ||||
|         cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterComponent), | ||||
| @@ -95,6 +114,7 @@ async def to_code(config): | ||||
|     if CORE.is_esp32: | ||||
|         var = cg.new_Pvariable(config[CONF_ID], pin) | ||||
|         cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) | ||||
|         cg.add(var.set_non_blocking(config[CONF_NON_BLOCKING])) | ||||
|         if CONF_CLOCK_RESOLUTION in config: | ||||
|             cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) | ||||
|         if CONF_USE_DMA in config: | ||||
|   | ||||
| @@ -54,6 +54,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, | ||||
| #if defined(USE_ESP32) | ||||
|   void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } | ||||
|   void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; } | ||||
|   void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; } | ||||
| #endif | ||||
|  | ||||
|   Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; }; | ||||
| @@ -74,6 +75,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   void configure_rmt_(); | ||||
|   void wait_for_rmt_(); | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
|   RemoteTransmitterComponentStore store_{}; | ||||
| @@ -90,6 +92,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, | ||||
|   esp_err_t error_code_{ESP_OK}; | ||||
|   std::string error_string_{""}; | ||||
|   bool inverted_{false}; | ||||
|   bool non_blocking_{false}; | ||||
| #endif | ||||
|   uint8_t carrier_duty_percent_; | ||||
|  | ||||
|   | ||||
| @@ -196,12 +196,29 @@ void RemoteTransmitterComponent::configure_rmt_() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::wait_for_rmt_() { | ||||
|   esp_err_t error = rmt_tx_wait_all_done(this->channel_, -1); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error)); | ||||
|     this->status_set_warning(); | ||||
|   } | ||||
|  | ||||
|   this->complete_trigger_->trigger(); | ||||
| } | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
| void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { | ||||
|   uint64_t total_duration = 0; | ||||
|  | ||||
|   if (this->is_failed()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // if the timeout was cancelled, block until the tx is complete | ||||
|   if (this->non_blocking_ && this->cancel_timeout("complete")) { | ||||
|     this->wait_for_rmt_(); | ||||
|   } | ||||
|  | ||||
|   if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) { | ||||
|     this->current_carrier_frequency_ = this->temp_.get_carrier_frequency(); | ||||
|     this->configure_rmt_(); | ||||
| @@ -212,6 +229,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|  | ||||
|   // encode any delay at the start of the buffer to simplify the encoder callback | ||||
|   // this will be skipped the first time around | ||||
|   total_duration += send_wait * (send_times - 1); | ||||
|   send_wait = this->from_microseconds_(static_cast<uint32_t>(send_wait)); | ||||
|   while (send_wait > 0) { | ||||
|     int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
| @@ -229,6 +247,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|     if (!level) { | ||||
|       value = -value; | ||||
|     } | ||||
|     total_duration += value * send_times; | ||||
|     value = this->from_microseconds_(static_cast<uint32_t>(value)); | ||||
|     while (value > 0) { | ||||
|       int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
| @@ -260,13 +279,12 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|   } else { | ||||
|     this->status_clear_warning(); | ||||
|   } | ||||
|   error = rmt_tx_wait_all_done(this->channel_, -1); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error)); | ||||
|     this->status_set_warning(); | ||||
|   } | ||||
|  | ||||
|   this->complete_trigger_->trigger(); | ||||
|   if (this->non_blocking_) { | ||||
|     this->set_timeout("complete", total_duration / 1000, [this]() { this->wait_for_rmt_(); }); | ||||
|   } else { | ||||
|     this->wait_for_rmt_(); | ||||
|   } | ||||
| } | ||||
| #else | ||||
| void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { | ||||
|   | ||||
| @@ -86,11 +86,6 @@ def select_schema( | ||||
|     return _SELECT_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| SELECT_SCHEMA = select_schema(Select) | ||||
| SELECT_SCHEMA.add_extra(cv.deprecated_schema_constant("select")) | ||||
|  | ||||
|  | ||||
| async def setup_select_core_(var, config, *, options: list[str]): | ||||
|     await setup_entity(var, config, "select") | ||||
|  | ||||
|   | ||||
| @@ -369,11 +369,6 @@ def sensor_schema( | ||||
|     return _SENSOR_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| SENSOR_SCHEMA = sensor_schema() | ||||
| SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("sensor")) | ||||
|  | ||||
|  | ||||
| @FILTER_REGISTRY.register("offset", OffsetFilter, cv.templatable(cv.float_)) | ||||
| async def offset_filter_to_code(config, filter_id): | ||||
|     template_ = await cg.templatable(config, [], float) | ||||
|   | ||||
| @@ -139,11 +139,6 @@ def switch_schema( | ||||
|     return _SWITCH_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| SWITCH_SCHEMA = switch_schema(Switch) | ||||
| SWITCH_SCHEMA.add_extra(cv.deprecated_schema_constant("switch")) | ||||
|  | ||||
|  | ||||
| async def setup_switch_core_(var, config): | ||||
|     await setup_entity(var, config, "switch") | ||||
|  | ||||
|   | ||||
| @@ -25,6 +25,20 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, | ||||
|   this->sensor_data_.push_back(sd); | ||||
|   this->sensor_map_[sensor].store_index = this->next_store_index_++; | ||||
| }; | ||||
|  | ||||
| static const LogString *sensor_type_to_string(AlarmSensorType type) { | ||||
|   switch (type) { | ||||
|     case ALARM_SENSOR_TYPE_INSTANT: | ||||
|       return LOG_STR("instant"); | ||||
|     case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER: | ||||
|       return LOG_STR("delayed_follower"); | ||||
|     case ALARM_SENSOR_TYPE_INSTANT_ALWAYS: | ||||
|       return LOG_STR("instant_always"); | ||||
|     case ALARM_SENSOR_TYPE_DELAYED: | ||||
|     default: | ||||
|       return LOG_STR("delayed"); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
|  | ||||
| void TemplateAlarmControlPanel::dump_config() { | ||||
| @@ -46,35 +60,20 @@ void TemplateAlarmControlPanel::dump_config() { | ||||
|                 "  Supported Features: %" PRIu32, | ||||
|                 (this->pending_time_ / 1000), (this->trigger_time_ / 1000), this->get_supported_features()); | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   for (auto sensor_info : this->sensor_map_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Binary Sensor:"); | ||||
|   for (auto const &[sensor, info] : this->sensor_map_) { | ||||
|     ESP_LOGCONFIG(TAG, | ||||
|                   "  Binary Sensor:\n" | ||||
|                   "    Name: %s\n" | ||||
|                   "    Type: %s\n" | ||||
|                   "    Armed home bypass: %s\n" | ||||
|                   "    Armed night bypass: %s\n" | ||||
|                   "    Auto bypass: %s\n" | ||||
|                   "    Chime mode: %s", | ||||
|                   sensor_info.first->get_name().c_str(), | ||||
|                   TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME), | ||||
|                   TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT), | ||||
|                   TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO), | ||||
|                   TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME)); | ||||
|     const char *sensor_type; | ||||
|     switch (sensor_info.second.type) { | ||||
|       case ALARM_SENSOR_TYPE_INSTANT: | ||||
|         sensor_type = "instant"; | ||||
|         break; | ||||
|       case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER: | ||||
|         sensor_type = "delayed_follower"; | ||||
|         break; | ||||
|       case ALARM_SENSOR_TYPE_INSTANT_ALWAYS: | ||||
|         sensor_type = "instant_always"; | ||||
|         break; | ||||
|       case ALARM_SENSOR_TYPE_DELAYED: | ||||
|       default: | ||||
|         sensor_type = "delayed"; | ||||
|     } | ||||
|     ESP_LOGCONFIG(TAG, "    Sensor type: %s", sensor_type); | ||||
|                   sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(info.type)), | ||||
|                   TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME), | ||||
|                   TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT), | ||||
|                   TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO), | ||||
|                   TRUEFALSE(info.flags & BINARY_SENSOR_MODE_CHIME)); | ||||
|   } | ||||
| #endif | ||||
| } | ||||
| @@ -123,39 +122,37 @@ void TemplateAlarmControlPanel::loop() { | ||||
|   bool instant_sensor_faulted = false; | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   // Test all of the sensors in the list regardless of the alarm panel state | ||||
|   for (auto sensor_info : this->sensor_map_) { | ||||
|   // Test all of the sensors regardless of the alarm panel state | ||||
|   for (auto const &[sensor, info] : this->sensor_map_) { | ||||
|     // Check for chime zones | ||||
|     if ((sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME)) { | ||||
|     if (info.flags & BINARY_SENSOR_MODE_CHIME) { | ||||
|       // Look for the transition from closed to open | ||||
|       if ((!this->sensor_data_[sensor_info.second.store_index].last_chime_state) && (sensor_info.first->state)) { | ||||
|       if ((!this->sensor_data_[info.store_index].last_chime_state) && (sensor->state)) { | ||||
|         // Must be disarmed to chime | ||||
|         if (this->current_state_ == ACP_STATE_DISARMED) { | ||||
|           this->chime_callback_.call(); | ||||
|         } | ||||
|       } | ||||
|       // Record the sensor state change | ||||
|       this->sensor_data_[sensor_info.second.store_index].last_chime_state = sensor_info.first->state; | ||||
|       this->sensor_data_[info.store_index].last_chime_state = sensor->state; | ||||
|     } | ||||
|     // Check for faulted sensors | ||||
|     if (sensor_info.first->state) {  // Sensor triggered? | ||||
|     if (sensor->state) { | ||||
|       // Skip if auto bypassed | ||||
|       if (std::count(this->bypassed_sensor_indicies_.begin(), this->bypassed_sensor_indicies_.end(), | ||||
|                      sensor_info.second.store_index) == 1) { | ||||
|                      info.store_index) == 1) { | ||||
|         continue; | ||||
|       } | ||||
|       // Skip if bypass armed home | ||||
|       if (this->current_state_ == ACP_STATE_ARMED_HOME && | ||||
|           (sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) { | ||||
|       if ((this->current_state_ == ACP_STATE_ARMED_HOME) && (info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) { | ||||
|         continue; | ||||
|       } | ||||
|       // Skip if bypass armed night | ||||
|       if (this->current_state_ == ACP_STATE_ARMED_NIGHT && | ||||
|           (sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) { | ||||
|       if ((this->current_state_ == ACP_STATE_ARMED_NIGHT) && (info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) { | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       switch (sensor_info.second.type) { | ||||
|       switch (info.type) { | ||||
|         case ALARM_SENSOR_TYPE_INSTANT_ALWAYS: | ||||
|           next_state = ACP_STATE_TRIGGERED; | ||||
|           [[fallthrough]]; | ||||
| @@ -247,11 +244,11 @@ void TemplateAlarmControlPanel::arm_(optional<std::string> code, alarm_control_p | ||||
|  | ||||
| void TemplateAlarmControlPanel::bypass_before_arming() { | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   for (auto sensor_info : this->sensor_map_) { | ||||
|   for (auto const &[sensor, info] : this->sensor_map_) { | ||||
|     // Check for faulted bypass_auto sensors and remove them from monitoring | ||||
|     if ((sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor_info.first->state)) { | ||||
|       ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor_info.first->get_name().c_str()); | ||||
|       this->bypassed_sensor_indicies_.push_back(sensor_info.second.store_index); | ||||
|     if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) { | ||||
|       ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str()); | ||||
|       this->bypassed_sensor_indicies_.push_back(info.store_index); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|   | ||||
| @@ -6,17 +6,21 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.binary_sensor"; | ||||
|  | ||||
| void TemplateBinarySensor::setup() { this->loop(); } | ||||
| void TemplateBinarySensor::setup() { | ||||
|   if (!this->f_.has_value()) { | ||||
|     this->disable_loop(); | ||||
|   } else { | ||||
|     this->loop(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TemplateBinarySensor::loop() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   auto s = (*this->f_)(); | ||||
|   auto s = this->f_(); | ||||
|   if (s.has_value()) { | ||||
|     this->publish_state(*s); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TemplateBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Template Binary Sensor", this); } | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/template_lambda.h" | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -8,7 +9,10 @@ namespace template_ { | ||||
|  | ||||
| class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { | ||||
|  public: | ||||
|   void set_template(optional<bool> (*f)()) { this->f_ = f; } | ||||
|   template<typename F> void set_template(F &&f) { | ||||
|     this->f_.set(std::forward<F>(f)); | ||||
|     this->enable_loop(); | ||||
|   } | ||||
|  | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
| @@ -17,7 +21,7 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|  protected: | ||||
|   optional<optional<bool> (*)()> f_; | ||||
|   TemplateLambda<bool> f_; | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -33,28 +33,27 @@ void TemplateCover::setup() { | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|   if (!this->state_f_.has_value() && !this->tilt_f_.has_value()) | ||||
|     this->disable_loop(); | ||||
| } | ||||
| void TemplateCover::loop() { | ||||
|   bool changed = false; | ||||
|  | ||||
|   if (this->state_f_.has_value()) { | ||||
|     auto s = (*this->state_f_)(); | ||||
|     if (s.has_value()) { | ||||
|       auto pos = clamp(*s, 0.0f, 1.0f); | ||||
|       if (pos != this->position) { | ||||
|         this->position = pos; | ||||
|         changed = true; | ||||
|       } | ||||
|   auto s = this->state_f_(); | ||||
|   if (s.has_value()) { | ||||
|     auto pos = clamp(*s, 0.0f, 1.0f); | ||||
|     if (pos != this->position) { | ||||
|       this->position = pos; | ||||
|       changed = true; | ||||
|     } | ||||
|   } | ||||
|   if (this->tilt_f_.has_value()) { | ||||
|     auto s = (*this->tilt_f_)(); | ||||
|     if (s.has_value()) { | ||||
|       auto tilt = clamp(*s, 0.0f, 1.0f); | ||||
|       if (tilt != this->tilt) { | ||||
|         this->tilt = tilt; | ||||
|         changed = true; | ||||
|       } | ||||
|  | ||||
|   auto tilt = this->tilt_f_(); | ||||
|   if (tilt.has_value()) { | ||||
|     auto tilt_val = clamp(*tilt, 0.0f, 1.0f); | ||||
|     if (tilt_val != this->tilt) { | ||||
|       this->tilt = tilt_val; | ||||
|       changed = true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -63,7 +62,6 @@ void TemplateCover::loop() { | ||||
| } | ||||
| void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
| void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } | ||||
| void TemplateCover::set_state_lambda(optional<float> (*f)()) { this->state_f_ = f; } | ||||
| float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
| Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } | ||||
| Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } | ||||
| @@ -124,7 +122,6 @@ CoverTraits TemplateCover::get_traits() { | ||||
| } | ||||
| Trigger<float> *TemplateCover::get_position_trigger() const { return this->position_trigger_; } | ||||
| Trigger<float> *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } | ||||
| void TemplateCover::set_tilt_lambda(optional<float> (*tilt_f)()) { this->tilt_f_ = tilt_f; } | ||||
| void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } | ||||
| void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } | ||||
| void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/template_lambda.h" | ||||
| #include "esphome/components/cover/cover.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -17,7 +18,14 @@ class TemplateCover : public cover::Cover, public Component { | ||||
|  public: | ||||
|   TemplateCover(); | ||||
|  | ||||
|   void set_state_lambda(optional<float> (*f)()); | ||||
|   template<typename F> void set_state_lambda(F &&f) { | ||||
|     this->state_f_.set(std::forward<F>(f)); | ||||
|     this->enable_loop(); | ||||
|   } | ||||
|   template<typename F> void set_tilt_lambda(F &&f) { | ||||
|     this->tilt_f_.set(std::forward<F>(f)); | ||||
|     this->enable_loop(); | ||||
|   } | ||||
|   Trigger<> *get_open_trigger() const; | ||||
|   Trigger<> *get_close_trigger() const; | ||||
|   Trigger<> *get_stop_trigger() const; | ||||
| @@ -26,7 +34,6 @@ class TemplateCover : public cover::Cover, public Component { | ||||
|   Trigger<float> *get_tilt_trigger() const; | ||||
|   void set_optimistic(bool optimistic); | ||||
|   void set_assumed_state(bool assumed_state); | ||||
|   void set_tilt_lambda(optional<float> (*tilt_f)()); | ||||
|   void set_has_stop(bool has_stop); | ||||
|   void set_has_position(bool has_position); | ||||
|   void set_has_tilt(bool has_tilt); | ||||
| @@ -45,8 +52,8 @@ class TemplateCover : public cover::Cover, public Component { | ||||
|   void stop_prev_trigger_(); | ||||
|  | ||||
|   TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; | ||||
|   optional<optional<float> (*)()> state_f_; | ||||
|   optional<optional<float> (*)()> tilt_f_; | ||||
|   TemplateLambda<float> state_f_; | ||||
|   TemplateLambda<float> tilt_f_; | ||||
|   bool assumed_state_{false}; | ||||
|   bool optimistic_{false}; | ||||
|   Trigger<> *open_trigger_; | ||||
|   | ||||
| @@ -40,14 +40,13 @@ void TemplateDate::update() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   auto val = (*this->f_)(); | ||||
|   if (!val.has_value()) | ||||
|     return; | ||||
|  | ||||
|   this->year_ = val->year; | ||||
|   this->month_ = val->month; | ||||
|   this->day_ = val->day_of_month; | ||||
|   this->publish_state(); | ||||
|   auto val = this->f_(); | ||||
|   if (val.has_value()) { | ||||
|     this->year_ = val->year; | ||||
|     this->month_ = val->month; | ||||
|     this->day_ = val->day_of_month; | ||||
|     this->publish_state(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TemplateDate::control(const datetime::DateCall &call) { | ||||
|   | ||||
| @@ -9,13 +9,14 @@ | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/preferences.h" | ||||
| #include "esphome/core/time.h" | ||||
| #include "esphome/core/template_lambda.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateDate : public datetime::DateEntity, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(optional<ESPTime> (*f)()) { this->f_ = f; } | ||||
|   template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); } | ||||
|  | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
| @@ -35,7 +36,7 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent { | ||||
|   ESPTime initial_value_{}; | ||||
|   bool restore_value_{false}; | ||||
|   Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>(); | ||||
|   optional<optional<ESPTime> (*)()> f_; | ||||
|   TemplateLambda<ESPTime> f_; | ||||
|  | ||||
|   ESPPreferenceObject pref_; | ||||
| }; | ||||
|   | ||||
| @@ -43,17 +43,16 @@ void TemplateDateTime::update() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   auto val = (*this->f_)(); | ||||
|   if (!val.has_value()) | ||||
|     return; | ||||
|  | ||||
|   this->year_ = val->year; | ||||
|   this->month_ = val->month; | ||||
|   this->day_ = val->day_of_month; | ||||
|   this->hour_ = val->hour; | ||||
|   this->minute_ = val->minute; | ||||
|   this->second_ = val->second; | ||||
|   this->publish_state(); | ||||
|   auto val = this->f_(); | ||||
|   if (val.has_value()) { | ||||
|     this->year_ = val->year; | ||||
|     this->month_ = val->month; | ||||
|     this->day_ = val->day_of_month; | ||||
|     this->hour_ = val->hour; | ||||
|     this->minute_ = val->minute; | ||||
|     this->second_ = val->second; | ||||
|     this->publish_state(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TemplateDateTime::control(const datetime::DateTimeCall &call) { | ||||
|   | ||||
| @@ -9,13 +9,14 @@ | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/preferences.h" | ||||
| #include "esphome/core/time.h" | ||||
| #include "esphome/core/template_lambda.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(optional<ESPTime> (*f)()) { this->f_ = f; } | ||||
|   template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); } | ||||
|  | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
| @@ -35,7 +36,7 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen | ||||
|   ESPTime initial_value_{}; | ||||
|   bool restore_value_{false}; | ||||
|   Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>(); | ||||
|   optional<optional<ESPTime> (*)()> f_; | ||||
|   TemplateLambda<ESPTime> f_; | ||||
|  | ||||
|   ESPPreferenceObject pref_; | ||||
| }; | ||||
|   | ||||
| @@ -40,14 +40,13 @@ void TemplateTime::update() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   auto val = (*this->f_)(); | ||||
|   if (!val.has_value()) | ||||
|     return; | ||||
|  | ||||
|   this->hour_ = val->hour; | ||||
|   this->minute_ = val->minute; | ||||
|   this->second_ = val->second; | ||||
|   this->publish_state(); | ||||
|   auto val = this->f_(); | ||||
|   if (val.has_value()) { | ||||
|     this->hour_ = val->hour; | ||||
|     this->minute_ = val->minute; | ||||
|     this->second_ = val->second; | ||||
|     this->publish_state(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TemplateTime::control(const datetime::TimeCall &call) { | ||||
|   | ||||
| @@ -9,13 +9,14 @@ | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/preferences.h" | ||||
| #include "esphome/core/time.h" | ||||
| #include "esphome/core/template_lambda.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateTime : public datetime::TimeEntity, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(optional<ESPTime> (*f)()) { this->f_ = f; } | ||||
|   template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); } | ||||
|  | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
| @@ -35,7 +36,7 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent { | ||||
|   ESPTime initial_value_{}; | ||||
|   bool restore_value_{false}; | ||||
|   Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>(); | ||||
|   optional<optional<ESPTime> (*)()> f_; | ||||
|   TemplateLambda<ESPTime> f_; | ||||
|  | ||||
|   ESPPreferenceObject pref_; | ||||
| }; | ||||
|   | ||||
| @@ -12,13 +12,10 @@ TemplateLock::TemplateLock() | ||||
|     : lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {} | ||||
|  | ||||
| void TemplateLock::loop() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|   auto val = (*this->f_)(); | ||||
|   if (!val.has_value()) | ||||
|     return; | ||||
|  | ||||
|   this->publish_state(*val); | ||||
|   auto val = this->f_(); | ||||
|   if (val.has_value()) { | ||||
|     this->publish_state(*val); | ||||
|   } | ||||
| } | ||||
| void TemplateLock::control(const lock::LockCall &call) { | ||||
|   if (this->prev_trigger_ != nullptr) { | ||||
| @@ -45,7 +42,6 @@ void TemplateLock::open_latch() { | ||||
|   this->open_trigger_->trigger(); | ||||
| } | ||||
| void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
| void TemplateLock::set_state_lambda(optional<lock::LockState> (*f)()) { this->f_ = f; } | ||||
| float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
| Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } | ||||
| Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/template_lambda.h" | ||||
| #include "esphome/components/lock/lock.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -13,7 +14,10 @@ class TemplateLock : public lock::Lock, public Component { | ||||
|  | ||||
|   void dump_config() override; | ||||
|  | ||||
|   void set_state_lambda(optional<lock::LockState> (*f)()); | ||||
|   template<typename F> void set_state_lambda(F &&f) { | ||||
|     this->f_.set(std::forward<F>(f)); | ||||
|     this->enable_loop(); | ||||
|   } | ||||
|   Trigger<> *get_lock_trigger() const; | ||||
|   Trigger<> *get_unlock_trigger() const; | ||||
|   Trigger<> *get_open_trigger() const; | ||||
| @@ -26,7 +30,7 @@ class TemplateLock : public lock::Lock, public Component { | ||||
|   void control(const lock::LockCall &call) override; | ||||
|   void open_latch() override; | ||||
|  | ||||
|   optional<optional<lock::LockState> (*)()> f_; | ||||
|   TemplateLambda<lock::LockState> f_; | ||||
|   bool optimistic_{false}; | ||||
|   Trigger<> *lock_trigger_; | ||||
|   Trigger<> *unlock_trigger_; | ||||
|   | ||||
| @@ -30,11 +30,10 @@ void TemplateNumber::update() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   auto val = (*this->f_)(); | ||||
|   if (!val.has_value()) | ||||
|     return; | ||||
|  | ||||
|   this->publish_state(*val); | ||||
|   auto val = this->f_(); | ||||
|   if (val.has_value()) { | ||||
|     this->publish_state(*val); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TemplateNumber::control(float value) { | ||||
|   | ||||
| @@ -4,13 +4,14 @@ | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/preferences.h" | ||||
| #include "esphome/core/template_lambda.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateNumber : public number::Number, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(optional<float> (*f)()) { this->f_ = f; } | ||||
|   template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); } | ||||
|  | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
| @@ -28,7 +29,7 @@ class TemplateNumber : public number::Number, public PollingComponent { | ||||
|   float initial_value_{NAN}; | ||||
|   bool restore_value_{false}; | ||||
|   Trigger<float> *set_trigger_ = new Trigger<float>(); | ||||
|   optional<optional<float> (*)()> f_; | ||||
|   TemplateLambda<float> f_; | ||||
|  | ||||
|   ESPPreferenceObject pref_; | ||||
| }; | ||||
|   | ||||
| @@ -31,16 +31,14 @@ void TemplateSelect::update() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   auto val = (*this->f_)(); | ||||
|   if (!val.has_value()) | ||||
|     return; | ||||
|  | ||||
|   if (!this->has_option(*val)) { | ||||
|     ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); | ||||
|     return; | ||||
|   auto val = this->f_(); | ||||
|   if (val.has_value()) { | ||||
|     if (!this->has_option(*val)) { | ||||
|       ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); | ||||
|       return; | ||||
|     } | ||||
|     this->publish_state(*val); | ||||
|   } | ||||
|  | ||||
|   this->publish_state(*val); | ||||
| } | ||||
|  | ||||
| void TemplateSelect::control(const std::string &value) { | ||||
|   | ||||
| @@ -4,13 +4,14 @@ | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/preferences.h" | ||||
| #include "esphome/core/template_lambda.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateSelect : public select::Select, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(optional<std::string> (*f)()) { this->f_ = f; } | ||||
|   template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); } | ||||
|  | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
| @@ -28,7 +29,7 @@ class TemplateSelect : public select::Select, public PollingComponent { | ||||
|   size_t initial_option_index_{0}; | ||||
|   bool restore_value_ = false; | ||||
|   Trigger<std::string> *set_trigger_ = new Trigger<std::string>(); | ||||
|   optional<optional<std::string> (*)()> f_; | ||||
|   TemplateLambda<std::string> f_; | ||||
|  | ||||
|   ESPPreferenceObject pref_; | ||||
| }; | ||||
|   | ||||
| @@ -11,13 +11,14 @@ void TemplateSensor::update() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   auto val = (*this->f_)(); | ||||
|   auto val = this->f_(); | ||||
|   if (val.has_value()) { | ||||
|     this->publish_state(*val); | ||||
|   } | ||||
| } | ||||
|  | ||||
| float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
| void TemplateSensor::set_template(optional<float> (*f)()) { this->f_ = f; } | ||||
|  | ||||
| void TemplateSensor::dump_config() { | ||||
|   LOG_SENSOR("", "Template Sensor", this); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/template_lambda.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -8,7 +9,7 @@ namespace template_ { | ||||
|  | ||||
| class TemplateSensor : public sensor::Sensor, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(optional<float> (*f)()); | ||||
|   template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); } | ||||
|  | ||||
|   void update() override; | ||||
|  | ||||
| @@ -17,7 +18,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent { | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|  protected: | ||||
|   optional<optional<float> (*)()> f_; | ||||
|   TemplateLambda<float> f_; | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -9,13 +9,10 @@ static const char *const TAG = "template.switch"; | ||||
| TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} | ||||
|  | ||||
| void TemplateSwitch::loop() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|   auto s = (*this->f_)(); | ||||
|   if (!s.has_value()) | ||||
|     return; | ||||
|  | ||||
|   this->publish_state(*s); | ||||
|   auto s = this->f_(); | ||||
|   if (s.has_value()) { | ||||
|     this->publish_state(*s); | ||||
|   } | ||||
| } | ||||
| void TemplateSwitch::write_state(bool state) { | ||||
|   if (this->prev_trigger_ != nullptr) { | ||||
| @@ -35,11 +32,13 @@ void TemplateSwitch::write_state(bool state) { | ||||
| } | ||||
| void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
| bool TemplateSwitch::assumed_state() { return this->assumed_state_; } | ||||
| void TemplateSwitch::set_state_lambda(optional<bool> (*f)()) { this->f_ = f; } | ||||
| float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } | ||||
| Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } | ||||
| Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } | ||||
| void TemplateSwitch::setup() { | ||||
|   if (!this->f_.has_value()) | ||||
|     this->disable_loop(); | ||||
|  | ||||
|   optional<bool> initial_state = this->get_initial_state_with_restore_mode(); | ||||
|  | ||||
|   if (initial_state.has_value()) { | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/template_lambda.h" | ||||
| #include "esphome/components/switch/switch.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -14,7 +15,10 @@ class TemplateSwitch : public switch_::Switch, public Component { | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|  | ||||
|   void set_state_lambda(optional<bool> (*f)()); | ||||
|   template<typename F> void set_state_lambda(F &&f) { | ||||
|     this->f_.set(std::forward<F>(f)); | ||||
|     this->enable_loop(); | ||||
|   } | ||||
|   Trigger<> *get_turn_on_trigger() const; | ||||
|   Trigger<> *get_turn_off_trigger() const; | ||||
|   void set_optimistic(bool optimistic); | ||||
| @@ -28,7 +32,7 @@ class TemplateSwitch : public switch_::Switch, public Component { | ||||
|  | ||||
|   void write_state(bool state) override; | ||||
|  | ||||
|   optional<optional<bool> (*)()> f_; | ||||
|   TemplateLambda<bool> f_; | ||||
|   bool optimistic_{false}; | ||||
|   bool assumed_state_{false}; | ||||
|   Trigger<> *turn_on_trigger_; | ||||
|   | ||||
| @@ -7,10 +7,8 @@ namespace template_ { | ||||
| static const char *const TAG = "template.text"; | ||||
|  | ||||
| void TemplateText::setup() { | ||||
|   if (!(this->f_ == nullptr)) { | ||||
|     if (this->f_.has_value()) | ||||
|       return; | ||||
|   } | ||||
|   if (this->f_.has_value()) | ||||
|     return; | ||||
|   std::string value = this->initial_value_; | ||||
|   if (!this->pref_) { | ||||
|     ESP_LOGD(TAG, "State from initial: %s", value.c_str()); | ||||
| @@ -26,17 +24,13 @@ void TemplateText::setup() { | ||||
| } | ||||
|  | ||||
| void TemplateText::update() { | ||||
|   if (this->f_ == nullptr) | ||||
|     return; | ||||
|  | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   auto val = (*this->f_)(); | ||||
|   if (!val.has_value()) | ||||
|     return; | ||||
|  | ||||
|   this->publish_state(*val); | ||||
|   auto val = this->f_(); | ||||
|   if (val.has_value()) { | ||||
|     this->publish_state(*val); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TemplateText::control(const std::string &value) { | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/preferences.h" | ||||
| #include "esphome/core/template_lambda.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
| @@ -61,7 +62,7 @@ template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase { | ||||
|  | ||||
| class TemplateText : public text::Text, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(optional<std::string> (*f)()) { this->f_ = f; } | ||||
|   template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); } | ||||
|  | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
| @@ -78,7 +79,7 @@ class TemplateText : public text::Text, public PollingComponent { | ||||
|   bool optimistic_ = false; | ||||
|   std::string initial_value_; | ||||
|   Trigger<std::string> *set_trigger_ = new Trigger<std::string>(); | ||||
|   optional<optional<std::string> (*)()> f_{nullptr}; | ||||
|   TemplateLambda<std::string> f_{}; | ||||
|  | ||||
|   TemplateTextSaverBase *pref_ = nullptr; | ||||
| }; | ||||
|   | ||||
| @@ -10,13 +10,14 @@ void TemplateTextSensor::update() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   auto val = (*this->f_)(); | ||||
|   auto val = this->f_(); | ||||
|   if (val.has_value()) { | ||||
|     this->publish_state(*val); | ||||
|   } | ||||
| } | ||||
|  | ||||
| float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
| void TemplateTextSensor::set_template(optional<std::string> (*f)()) { this->f_ = f; } | ||||
|  | ||||
| void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); } | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/template_lambda.h" | ||||
| #include "esphome/components/text_sensor/text_sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -9,7 +10,7 @@ namespace template_ { | ||||
|  | ||||
| class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(optional<std::string> (*f)()); | ||||
|   template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); } | ||||
|  | ||||
|   void update() override; | ||||
|  | ||||
| @@ -18,7 +19,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
|   optional<optional<std::string> (*)()> f_{}; | ||||
|   TemplateLambda<std::string> f_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -33,19 +33,19 @@ void TemplateValve::setup() { | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|   if (!this->state_f_.has_value()) | ||||
|     this->disable_loop(); | ||||
| } | ||||
|  | ||||
| void TemplateValve::loop() { | ||||
|   bool changed = false; | ||||
|  | ||||
|   if (this->state_f_.has_value()) { | ||||
|     auto s = (*this->state_f_)(); | ||||
|     if (s.has_value()) { | ||||
|       auto pos = clamp(*s, 0.0f, 1.0f); | ||||
|       if (pos != this->position) { | ||||
|         this->position = pos; | ||||
|         changed = true; | ||||
|       } | ||||
|   auto s = this->state_f_(); | ||||
|   if (s.has_value()) { | ||||
|     auto pos = clamp(*s, 0.0f, 1.0f); | ||||
|     if (pos != this->position) { | ||||
|       this->position = pos; | ||||
|       changed = true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -55,7 +55,6 @@ void TemplateValve::loop() { | ||||
|  | ||||
| void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
| void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } | ||||
| void TemplateValve::set_state_lambda(optional<float> (*f)()) { this->state_f_ = f; } | ||||
| float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
|  | ||||
| Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/template_lambda.h" | ||||
| #include "esphome/components/valve/valve.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -17,7 +18,10 @@ class TemplateValve : public valve::Valve, public Component { | ||||
|  public: | ||||
|   TemplateValve(); | ||||
|  | ||||
|   void set_state_lambda(optional<float> (*f)()); | ||||
|   template<typename F> void set_state_lambda(F &&f) { | ||||
|     this->state_f_.set(std::forward<F>(f)); | ||||
|     this->enable_loop(); | ||||
|   } | ||||
|   Trigger<> *get_open_trigger() const; | ||||
|   Trigger<> *get_close_trigger() const; | ||||
|   Trigger<> *get_stop_trigger() const; | ||||
| @@ -42,7 +46,7 @@ class TemplateValve : public valve::Valve, public Component { | ||||
|   void stop_prev_trigger_(); | ||||
|  | ||||
|   TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE}; | ||||
|   optional<optional<float> (*)()> state_f_; | ||||
|   TemplateLambda<float> state_f_; | ||||
|   bool assumed_state_{false}; | ||||
|   bool optimistic_{false}; | ||||
|   Trigger<> *open_trigger_; | ||||
|   | ||||
| @@ -84,11 +84,6 @@ def text_schema( | ||||
|     return _TEXT_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| TEXT_SCHEMA = text_schema() | ||||
| TEXT_SCHEMA.add_extra(cv.deprecated_schema_constant("text")) | ||||
|  | ||||
|  | ||||
| async def setup_text_core_( | ||||
|     var, | ||||
|     config, | ||||
|   | ||||
| @@ -193,11 +193,6 @@ def text_sensor_schema( | ||||
|     return _TEXT_SENSOR_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| TEXT_SENSOR_SCHEMA = text_sensor_schema() | ||||
| TEXT_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("text_sensor")) | ||||
|  | ||||
|  | ||||
| async def build_filters(config): | ||||
|     return await cg.build_registry_list(FILTER_REGISTRY, config) | ||||
|  | ||||
|   | ||||
| @@ -84,11 +84,6 @@ def update_schema( | ||||
|     return _UPDATE_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| UPDATE_SCHEMA = update_schema() | ||||
| UPDATE_SCHEMA.add_extra(cv.deprecated_schema_constant("update")) | ||||
|  | ||||
|  | ||||
| async def setup_update_core_(var, config): | ||||
|     await setup_entity(var, config, "update") | ||||
|  | ||||
|   | ||||
| @@ -129,11 +129,6 @@ def valve_schema( | ||||
|     return _VALVE_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| VALVE_SCHEMA = valve_schema() | ||||
| VALVE_SCHEMA.add_extra(cv.deprecated_schema_constant("valve")) | ||||
|  | ||||
|  | ||||
| async def _setup_valve_core(var, config): | ||||
|     await setup_entity(var, config, "valve") | ||||
|  | ||||
|   | ||||
| @@ -319,6 +319,9 @@ def iter_ids(config, path=None): | ||||
|             yield from iter_ids(item, path + [i]) | ||||
|     elif isinstance(config, dict): | ||||
|         for key, value in config.items(): | ||||
|             if len(path) == 0 and key == CONF_SUBSTITUTIONS: | ||||
|                 # Ignore IDs in substitution definitions. | ||||
|                 continue | ||||
|             if isinstance(key, core.ID): | ||||
|                 yield key, path | ||||
|             yield from iter_ids(value, path + [key]) | ||||
|   | ||||
| @@ -2195,26 +2195,3 @@ def rename_key(old_key, new_key): | ||||
|         return config | ||||
|  | ||||
|     return validator | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| def deprecated_schema_constant(entity_type: str): | ||||
|     def validator(config): | ||||
|         type: str = "unknown" | ||||
|         if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID): | ||||
|             type = str(id.type).split("::", maxsplit=1)[0] | ||||
|         _LOGGER.warning( | ||||
|             "Using `%s.%s_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. " | ||||
|             "Please use `%s.%s_schema(...)` instead. " | ||||
|             "If you are seeing this, report an issue to the external_component author and ask them to update it. " | ||||
|             "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. " | ||||
|             "Component using this schema: %s", | ||||
|             entity_type, | ||||
|             entity_type.upper(), | ||||
|             entity_type, | ||||
|             entity_type, | ||||
|             type, | ||||
|         ) | ||||
|         return config | ||||
|  | ||||
|     return validator | ||||
|   | ||||
							
								
								
									
										120
									
								
								esphome/core/template_lambda.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								esphome/core/template_lambda.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <functional> | ||||
| #include <concepts> | ||||
| #include "esphome/core/optional.h" | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| /** Helper class for template platforms that stores either a stateless lambda (function pointer) | ||||
|  * or a stateful lambda (std::function pointer). | ||||
|  * | ||||
|  * This provides backward compatibility with PR #11555 while maintaining the optimization: | ||||
|  * - Stateless lambdas (no capture) → function pointer (4 bytes on ESP32) | ||||
|  * - Stateful lambdas (with capture) → pointer to std::function (4 bytes on ESP32) | ||||
|  * Total size: enum (1 byte) + union (4 bytes) + padding = 8 bytes (same as PR #11555) | ||||
|  * | ||||
|  * Both lambda types must return optional<T> (as YAML codegen does) to support the pattern: | ||||
|  *   return {};  // Don't publish a value | ||||
|  *   return 42.0;  // Publish this value | ||||
|  * | ||||
|  * operator() returns optional<T>, returning nullopt when no lambda is set (type == NONE). | ||||
|  * This eliminates redundant "is lambda set" checks by reusing optional's discriminator. | ||||
|  * | ||||
|  * @tparam T The return type (e.g., float for TemplateLambda<optional<float>>) | ||||
|  * @tparam Args Optional arguments for the lambda | ||||
|  */ | ||||
| template<typename T, typename... Args> class TemplateLambda { | ||||
|  public: | ||||
|   TemplateLambda() : type_(NONE) {} | ||||
|  | ||||
|   // For stateless lambdas: use function pointer | ||||
|   template<typename F> | ||||
|   requires std::invocable<F, Args...> && std::convertible_to < F, optional<T>(*) | ||||
|   (Args...) > void set(F f) { | ||||
|     this->reset(); | ||||
|     this->type_ = STATELESS_LAMBDA; | ||||
|     this->stateless_f_ = f;  // Implicit conversion to function pointer | ||||
|   } | ||||
|  | ||||
|   // For stateful lambdas: use std::function pointer | ||||
|   template<typename F> | ||||
|   requires std::invocable<F, Args...> && | ||||
|       (!std::convertible_to<F, optional<T> (*)(Args...)>) &&std::convertible_to<std::invoke_result_t<F, Args...>, | ||||
|                                                                                 optional<T>> void set(F &&f) { | ||||
|     this->reset(); | ||||
|     this->type_ = LAMBDA; | ||||
|     this->f_ = new std::function<optional<T>(Args...)>(std::forward<F>(f)); | ||||
|   } | ||||
|  | ||||
|   ~TemplateLambda() { this->reset(); } | ||||
|  | ||||
|   // Copy constructor | ||||
|   TemplateLambda(const TemplateLambda &) = delete; | ||||
|   TemplateLambda &operator=(const TemplateLambda &) = delete; | ||||
|  | ||||
|   // Move constructor | ||||
|   TemplateLambda(TemplateLambda &&other) noexcept : type_(other.type_) { | ||||
|     if (type_ == LAMBDA) { | ||||
|       this->f_ = other.f_; | ||||
|       other.f_ = nullptr; | ||||
|     } else if (type_ == STATELESS_LAMBDA) { | ||||
|       this->stateless_f_ = other.stateless_f_; | ||||
|     } | ||||
|     other.type_ = NONE; | ||||
|   } | ||||
|  | ||||
|   TemplateLambda &operator=(TemplateLambda &&other) noexcept { | ||||
|     if (this != &other) { | ||||
|       this->reset(); | ||||
|       this->type_ = other.type_; | ||||
|       if (type_ == LAMBDA) { | ||||
|         this->f_ = other.f_; | ||||
|         other.f_ = nullptr; | ||||
|       } else if (type_ == STATELESS_LAMBDA) { | ||||
|         this->stateless_f_ = other.stateless_f_; | ||||
|       } | ||||
|       other.type_ = NONE; | ||||
|     } | ||||
|     return *this; | ||||
|   } | ||||
|  | ||||
|   bool has_value() const { return this->type_ != NONE; } | ||||
|  | ||||
|   // Returns optional<T>, returning nullopt if no lambda is set | ||||
|   optional<T> operator()(Args... args) { | ||||
|     switch (this->type_) { | ||||
|       case STATELESS_LAMBDA: | ||||
|         return this->stateless_f_(args...);  // Direct function pointer call | ||||
|       case LAMBDA: | ||||
|         return (*this->f_)(args...);  // std::function call via pointer | ||||
|       case NONE: | ||||
|       default: | ||||
|         return nullopt;  // No lambda set | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   optional<T> call(Args... args) { return (*this)(args...); } | ||||
|  | ||||
|  protected: | ||||
|   void reset() { | ||||
|     if (this->type_ == LAMBDA) { | ||||
|       delete this->f_; | ||||
|       this->f_ = nullptr; | ||||
|     } | ||||
|     this->type_ = NONE; | ||||
|   } | ||||
|  | ||||
|   enum : uint8_t { | ||||
|     NONE, | ||||
|     STATELESS_LAMBDA, | ||||
|     LAMBDA, | ||||
|   } type_; | ||||
|  | ||||
|   union { | ||||
|     optional<T> (*stateless_f_)(Args...);     // Function pointer (4 bytes on ESP32) | ||||
|     std::function<optional<T>(Args...)> *f_;  // Pointer to std::function (4 bytes on ESP32) | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| }  // namespace esphome | ||||
| @@ -1,6 +1,6 @@ | ||||
| pylint==4.0.2 | ||||
| flake8==7.3.0  # also change in .pre-commit-config.yaml when updating | ||||
| ruff==0.14.1  # also change in .pre-commit-config.yaml when updating | ||||
| ruff==0.14.2  # also change in .pre-commit-config.yaml when updating | ||||
| pyupgrade==3.21.0  # also change in .pre-commit-config.yaml when updating | ||||
| pre-commit | ||||
|  | ||||
|   | ||||
| @@ -300,7 +300,7 @@ def fix_remote_receiver(): | ||||
|     remote_receiver_schema["CONFIG_SCHEMA"] = { | ||||
|         "type": "schema", | ||||
|         "schema": { | ||||
|             "extends": ["binary_sensor.BINARY_SENSOR_SCHEMA", "core.COMPONENT_SCHEMA"], | ||||
|             "extends": ["binary_sensor._BINARY_SENSOR_SCHEMA", "core.COMPONENT_SCHEMA"], | ||||
|             "config_vars": output["remote_base"].pop("binary"), | ||||
|         }, | ||||
|     } | ||||
|   | ||||
| @@ -52,6 +52,19 @@ number: | ||||
|     widget: spinbox_id | ||||
|     id: lvgl_spinbox_number | ||||
|     name: LVGL Spinbox Number | ||||
|   - platform: template | ||||
|     id: test_brightness | ||||
|     name: "Test Brightness" | ||||
|     min_value: 0 | ||||
|     max_value: 255 | ||||
|     step: 1 | ||||
|     optimistic: true | ||||
|     # Test lambda in automation accessing x parameter directly | ||||
|     # This is a real-world pattern from user configs | ||||
|     on_value: | ||||
|       - lambda: !lambda |- | ||||
|           // Direct use of x parameter in automation | ||||
|           ESP_LOGD("test", "Brightness: %.0f", x); | ||||
|  | ||||
| light: | ||||
|   - platform: lvgl | ||||
| @@ -110,3 +123,21 @@ text: | ||||
|     platform: lvgl | ||||
|     widget: hello_label | ||||
|     mode: text | ||||
|  | ||||
| text_sensor: | ||||
|   - platform: template | ||||
|     id: test_text_sensor | ||||
|     name: "Test Text Sensor" | ||||
|     # Test nested lambdas in LVGL actions can access automation parameters | ||||
|     on_value: | ||||
|       - lvgl.label.update: | ||||
|           id: hello_label | ||||
|           text: !lambda return x.c_str(); | ||||
|       - lvgl.label.update: | ||||
|           id: hello_label | ||||
|           text: !lambda |- | ||||
|             // Test complex lambda with conditionals accessing x parameter | ||||
|             if (x == "*") { | ||||
|               return "WILDCARD"; | ||||
|             } | ||||
|             return x.c_str(); | ||||
|   | ||||
| @@ -257,7 +257,30 @@ lvgl: | ||||
|             text: "Hello shiny day" | ||||
|             text_color: 0xFFFFFF | ||||
|             align: bottom_mid | ||||
|         - label: | ||||
|             id: setup_lambda_label | ||||
|             # Test lambda in widget property during setup (LvContext) | ||||
|             # Should NOT receive lv_component parameter | ||||
|             text: !lambda |- | ||||
|               char buf[32]; | ||||
|               snprintf(buf, sizeof(buf), "Setup: %d", 42); | ||||
|               return std::string(buf); | ||||
|             align: top_mid | ||||
|             text_font: space16 | ||||
|         - label: | ||||
|             id: chip_info_label | ||||
|             # Test complex setup lambda (real-world pattern) | ||||
|             # Should NOT receive lv_component parameter | ||||
|             text: !lambda |- | ||||
|               // Test conditional compilation and string formatting | ||||
|               char buf[64]; | ||||
|               #ifdef USE_ESP_IDF | ||||
|               snprintf(buf, sizeof(buf), "IDF: v%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR); | ||||
|               #else | ||||
|               snprintf(buf, sizeof(buf), "Arduino"); | ||||
|               #endif | ||||
|               return std::string(buf); | ||||
|             align: top_left | ||||
|         - obj: | ||||
|             align: center | ||||
|             arc_opa: COVER | ||||
|   | ||||
| @@ -2,6 +2,7 @@ remote_transmitter: | ||||
|   - id: xmitr | ||||
|     pin: ${pin} | ||||
|     carrier_duty_percent: 50% | ||||
|     non_blocking: true | ||||
|     clock_resolution: ${clock_resolution} | ||||
|     rmt_symbols: ${rmt_symbols} | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,27 @@ esphome: | ||||
|         id: template_sens | ||||
|         state: !lambda "return 42.0;" | ||||
|  | ||||
|     # Test C++ API: set_template() with stateless lambda (no captures) | ||||
|     - lambda: |- | ||||
|         id(template_sens).set_template([]() -> esphome::optional<float> { | ||||
|           return 123.0f; | ||||
|         }); | ||||
|  | ||||
|     # Test C++ API: set_template() with stateful lambda (with captures) | ||||
|     # This is the regression test for issue #11555 | ||||
|     - lambda: |- | ||||
|         float captured_value = 456.0f; | ||||
|         id(template_sens).set_template([captured_value]() -> esphome::optional<float> { | ||||
|           return captured_value; | ||||
|         }); | ||||
|  | ||||
|     # Test C++ API: set_template() with more complex capture | ||||
|     - lambda: |- | ||||
|         auto sensor_id = id(template_sens); | ||||
|         id(template_number).set_template([sensor_id]() -> esphome::optional<float> { | ||||
|           return sensor_id->state * 2.0f; | ||||
|         }); | ||||
|  | ||||
|     - datetime.date.set: | ||||
|         id: test_date | ||||
|         date: | ||||
| @@ -215,6 +236,7 @@ cover: | ||||
|  | ||||
| number: | ||||
|   - platform: template | ||||
|     id: template_number | ||||
|     name: "Template number" | ||||
|     optimistic: true | ||||
|     min_value: 0 | ||||
|   | ||||
| @@ -261,6 +261,17 @@ def test_device_duplicate_id( | ||||
|     assert "ID duplicate_device redefined!" in captured.out | ||||
|  | ||||
|  | ||||
| def test_substitution_with_id( | ||||
|     yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str] | ||||
| ) -> None: | ||||
|     """Test that a ids coming from substitutions do not cause false positive ID redefinition.""" | ||||
|     load_config_from_fixture( | ||||
|         yaml_file, "id_collision_with_substitution.yaml", FIXTURES_DIR | ||||
|     ) | ||||
|     captured = capsys.readouterr() | ||||
|     assert "ID some_switch_id redefined!" not in captured.out | ||||
|  | ||||
|  | ||||
| def test_add_platform_defines_priority() -> None: | ||||
|     """Test that _add_platform_defines runs after globals. | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,12 @@ | ||||
| esphome: | ||||
|   name: test | ||||
|  | ||||
| host: | ||||
|  | ||||
| substitutions: | ||||
|   support_switches: | ||||
|     - platform: gpio | ||||
|       id: some_switch_id | ||||
|       pin: 12 | ||||
|  | ||||
| switch: $support_switches | ||||
		Reference in New Issue
	
	Block a user