mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 05:58:42 +00:00 
			
		
		
		
	Compare commits
	
		
			10 Commits
		
	
	
		
			memory_api
			...
			select_opt
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | bfd115d25c | ||
|   | bfacb8d363 | ||
|   | d955479145 | ||
|   | 5a3251a693 | ||
|   | ceea861cc4 | ||
|   | cc6ee15e99 | ||
|   | 65c88e3ec4 | ||
|   | 7cd787075f | ||
|   | 88c7811e99 | ||
|   | a966ac7255 | 
							
								
								
									
										15
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -416,7 +416,7 @@ jobs: | ||||
|             } | ||||
|  | ||||
|             // Generate review messages | ||||
|             function generateReviewMessages(finalLabels, originalLabelCount) { | ||||
|             function generateReviewMessages(finalLabels) { | ||||
|               const messages = []; | ||||
|               const prAuthor = context.payload.pull_request.user.login; | ||||
|  | ||||
| @@ -430,15 +430,15 @@ jobs: | ||||
|                   .reduce((sum, file) => sum + (file.deletions || 0), 0); | ||||
|                 const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions); | ||||
|  | ||||
|                 const tooManyLabels = originalLabelCount > MAX_LABELS; | ||||
|                 const tooManyLabels = finalLabels.length > MAX_LABELS; | ||||
|                 const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD; | ||||
|  | ||||
|                 let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`; | ||||
|  | ||||
|                 if (tooManyLabels && tooManyChanges) { | ||||
|                   message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`; | ||||
|                   message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`; | ||||
|                 } else if (tooManyLabels) { | ||||
|                   message += `This PR affects ${originalLabelCount} different components/areas.`; | ||||
|                   message += `This PR affects ${finalLabels.length} different components/areas.`; | ||||
|                 } else { | ||||
|                   message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`; | ||||
|                 } | ||||
| @@ -466,8 +466,8 @@ jobs: | ||||
|             } | ||||
|  | ||||
|             // Handle reviews | ||||
|             async function handleReviews(finalLabels, originalLabelCount) { | ||||
|               const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount); | ||||
|             async function handleReviews(finalLabels) { | ||||
|               const reviewMessages = generateReviewMessages(finalLabels); | ||||
|               const hasReviewableLabels = finalLabels.some(label => | ||||
|                 ['too-big', 'needs-codeowners'].includes(label) | ||||
|               ); | ||||
| @@ -627,7 +627,6 @@ jobs: | ||||
|  | ||||
|             // Handle too many labels (only for non-mega PRs) | ||||
|             const tooManyLabels = finalLabels.length > MAX_LABELS; | ||||
|             const originalLabelCount = finalLabels.length; | ||||
|  | ||||
|             if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) { | ||||
|               finalLabels = ['too-big']; | ||||
| @@ -636,7 +635,7 @@ jobs: | ||||
|             console.log('Computed labels:', finalLabels.join(', ')); | ||||
|  | ||||
|             // Handle reviews | ||||
|             await handleReviews(finalLabels, originalLabelCount); | ||||
|             await handleReviews(finalLabels); | ||||
|  | ||||
|             // Apply labels | ||||
|             if (finalLabels.length > 0) { | ||||
|   | ||||
							
								
								
									
										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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         with: | ||||
|           name: generated-proto-files | ||||
|           path: | | ||||
|   | ||||
							
								
								
									
										62
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										62
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -114,7 +114,7 @@ jobs: | ||||
|       matrix: | ||||
|         python-version: | ||||
|           - "3.11" | ||||
|           - "3.13" | ||||
|           - "3.14" | ||||
|         os: | ||||
|           - ubuntu-latest | ||||
|           - macOS-latest | ||||
| @@ -123,9 +123,9 @@ jobs: | ||||
|           # Minimize CI resource usage | ||||
|           # by only running the Python version | ||||
|           # version used for docker images on Windows and macOS | ||||
|           - python-version: "3.13" | ||||
|           - python-version: "3.14" | ||||
|             os: windows-latest | ||||
|           - python-version: "3.13" | ||||
|           - python-version: "3.14" | ||||
|             os: macOS-latest | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     needs: | ||||
| @@ -180,7 +180,6 @@ jobs: | ||||
|       memory_impact: ${{ steps.determine.outputs.memory-impact }} | ||||
|       cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }} | ||||
|       cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }} | ||||
|       component-test-batches: ${{ steps.determine.outputs.component-test-batches }} | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
| @@ -215,7 +214,6 @@ jobs: | ||||
|           echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT | ||||
|           echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT | ||||
|           echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT | ||||
|           echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT | ||||
|  | ||||
|   integration-tests: | ||||
|     name: Run integration tests | ||||
| @@ -460,7 +458,7 @@ jobs: | ||||
|       GH_TOKEN: ${{ github.token }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       max-parallel: 2 | ||||
|       max-parallel: 1 | ||||
|       matrix: | ||||
|         include: | ||||
|           - id: clang-tidy | ||||
| @@ -538,18 +536,59 @@ jobs: | ||||
|         run: script/ci-suggest-changes | ||||
|         if: always() | ||||
|  | ||||
|   test-build-components-splitter: | ||||
|     name: Split components for intelligent grouping (40 weighted per batch) | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - determine-jobs | ||||
|     if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 | ||||
|     outputs: | ||||
|       matrix: ${{ steps.split.outputs.components }} | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Split components intelligently based on bus configurations | ||||
|         id: split | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|  | ||||
|           # Use intelligent splitter that groups components with same bus configs | ||||
|           components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}' | ||||
|  | ||||
|           # Only isolate directly changed components when targeting dev branch | ||||
|           # For beta/release branches, group everything for faster CI | ||||
|           if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then | ||||
|             directly_changed='[]' | ||||
|             echo "Target branch: ${{ github.base_ref }} - grouping all components" | ||||
|           else | ||||
|             directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}' | ||||
|             echo "Target branch: ${{ github.base_ref }} - isolating directly changed components" | ||||
|           fi | ||||
|  | ||||
|           echo "Splitting components intelligently..." | ||||
|           output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github) | ||||
|  | ||||
|           echo "$output" >> $GITHUB_OUTPUT | ||||
|  | ||||
|   test-build-components-split: | ||||
|     name: Test components batch (${{ matrix.components }}) | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - determine-jobs | ||||
|       - test-build-components-splitter | ||||
|     if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }} | ||||
|       matrix: | ||||
|         components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }} | ||||
|         components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }} | ||||
|     steps: | ||||
|       - name: Show disk space | ||||
|         run: | | ||||
| @@ -810,7 +849,7 @@ jobs: | ||||
|           fi | ||||
|  | ||||
|       - name: Upload memory analysis JSON | ||||
|         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         with: | ||||
|           name: memory-analysis-target | ||||
|           path: memory-analysis-target.json | ||||
| @@ -874,7 +913,7 @@ jobs: | ||||
|             --platform "$platform" | ||||
|  | ||||
|       - name: Upload memory analysis JSON | ||||
|         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         with: | ||||
|           name: memory-analysis-pr | ||||
|           path: memory-analysis-pr.json | ||||
| @@ -904,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@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         with: | ||||
|           name: memory-analysis-target | ||||
|           path: ./memory-analysis | ||||
|         continue-on-error: true | ||||
|       - name: Download PR analysis JSON | ||||
|         uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         with: | ||||
|           name: memory-analysis-pr | ||||
|           path: ./memory-analysis | ||||
| @@ -941,6 +980,7 @@ jobs: | ||||
|       - clang-tidy-nosplit | ||||
|       - clang-tidy-split | ||||
|       - determine-jobs | ||||
|       - test-build-components-splitter | ||||
|       - test-build-components-split | ||||
|       - pre-commit-ci-lite | ||||
|       - memory-impact-target-branch | ||||
|   | ||||
							
								
								
									
										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@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 | ||||
|         uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 | ||||
|         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@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 | ||||
|         uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 | ||||
|         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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         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@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.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.2 | ||||
|     rev: v0.14.1 | ||||
|     hooks: | ||||
|       # Run the linter. | ||||
|       - id: ruff | ||||
|   | ||||
| @@ -201,7 +201,6 @@ esphome/components/havells_solar/* @sourabhjaiswal | ||||
| esphome/components/hbridge/fan/* @WeekendWarrior | ||||
| esphome/components/hbridge/light/* @DotNetDann | ||||
| esphome/components/hbridge/switch/* @dwmw2 | ||||
| esphome/components/hdc2010/* @optimusprimespace @ssieb | ||||
| esphome/components/he60r/* @clydebarrow | ||||
| esphome/components/heatpumpir/* @rob-deutsch | ||||
| esphome/components/hitachi_ac424/* @sourabhjaiswal | ||||
|   | ||||
| @@ -731,13 +731,6 @@ def command_vscode(args: ArgsProtocol) -> int | None: | ||||
|  | ||||
|  | ||||
| def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None: | ||||
|     # Set memory analysis options in config | ||||
|     if args.analyze_memory: | ||||
|         config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True | ||||
|  | ||||
|     if args.memory_report: | ||||
|         config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report | ||||
|  | ||||
|     exit_code = write_cpp(config) | ||||
|     if exit_code != 0: | ||||
|         return exit_code | ||||
| @@ -1199,17 +1192,6 @@ def parse_args(argv): | ||||
|         help="Only generate source code, do not compile.", | ||||
|         action="store_true", | ||||
|     ) | ||||
|     parser_compile.add_argument( | ||||
|         "--analyze-memory", | ||||
|         help="Analyze and display memory usage by component after compilation.", | ||||
|         action="store_true", | ||||
|     ) | ||||
|     parser_compile.add_argument( | ||||
|         "--memory-report", | ||||
|         help="Save memory analysis report to a file (supports .json or .txt).", | ||||
|         type=str, | ||||
|         metavar="FILE", | ||||
|     ) | ||||
|  | ||||
|     parser_upload = subparsers.add_parser( | ||||
|         "upload", | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| """CLI interface for memory analysis with report generation.""" | ||||
|  | ||||
| from collections import defaultdict | ||||
| import json | ||||
| import sys | ||||
|  | ||||
| from . import ( | ||||
| @@ -284,28 +283,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): | ||||
|  | ||||
|         return "\n".join(lines) | ||||
|  | ||||
|     def to_json(self) -> str: | ||||
|         """Export analysis results as JSON.""" | ||||
|         data = { | ||||
|             "components": { | ||||
|                 name: { | ||||
|                     "text": mem.text_size, | ||||
|                     "rodata": mem.rodata_size, | ||||
|                     "data": mem.data_size, | ||||
|                     "bss": mem.bss_size, | ||||
|                     "flash_total": mem.flash_total, | ||||
|                     "ram_total": mem.ram_total, | ||||
|                     "symbol_count": mem.symbol_count, | ||||
|                 } | ||||
|                 for name, mem in self.components.items() | ||||
|             }, | ||||
|             "totals": { | ||||
|                 "flash": sum(c.flash_total for c in self.components.values()), | ||||
|                 "ram": sum(c.ram_total for c in self.components.values()), | ||||
|             }, | ||||
|         } | ||||
|         return json.dumps(data, indent=2) | ||||
|  | ||||
|     def dump_uncategorized_symbols(self, output_file: str | None = None) -> None: | ||||
|         """Dump uncategorized symbols for analysis.""" | ||||
|         # Sort by size descending | ||||
|   | ||||
| @@ -16,12 +16,7 @@ from esphome.const import ( | ||||
|     CONF_UPDATE_INTERVAL, | ||||
| ) | ||||
| from esphome.core import ID | ||||
| from esphome.cpp_generator import ( | ||||
|     LambdaExpression, | ||||
|     MockObj, | ||||
|     MockObjClass, | ||||
|     TemplateArgsType, | ||||
| ) | ||||
| from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
| from esphome.types import ConfigType | ||||
| from esphome.util import Registry | ||||
| @@ -92,7 +87,6 @@ def validate_potentially_or_condition(value): | ||||
|  | ||||
| DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) | ||||
| LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) | ||||
| StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action) | ||||
| IfAction = cg.esphome_ns.class_("IfAction", Action) | ||||
| WhileAction = cg.esphome_ns.class_("WhileAction", Action) | ||||
| RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) | ||||
| @@ -103,40 +97,9 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action) | ||||
| Automation = cg.esphome_ns.class_("Automation") | ||||
|  | ||||
| LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) | ||||
| StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition) | ||||
| ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) | ||||
|  | ||||
|  | ||||
| def new_lambda_pvariable( | ||||
|     id_obj: ID, | ||||
|     lambda_expr: LambdaExpression, | ||||
|     stateless_class: MockObjClass, | ||||
|     template_arg: cg.TemplateArguments | None = None, | ||||
| ) -> MockObj: | ||||
|     """Create Pvariable for lambda, using stateless class if applicable. | ||||
|  | ||||
|     Combines ID selection and Pvariable creation in one call. For stateless | ||||
|     lambdas (empty capture), uses function pointer instead of std::function. | ||||
|  | ||||
|     Args: | ||||
|         id_obj: The ID object (action_id, condition_id, or filter_id) | ||||
|         lambda_expr: The lambda expression object | ||||
|         stateless_class: The stateless class to use for stateless lambdas | ||||
|         template_arg: Optional template arguments (for actions/conditions) | ||||
|  | ||||
|     Returns: | ||||
|         The created Pvariable | ||||
|     """ | ||||
|     # For stateless lambdas, use function pointer instead of std::function | ||||
|     if lambda_expr.capture == "": | ||||
|         id_obj = id_obj.copy() | ||||
|         id_obj.type = stateless_class | ||||
|  | ||||
|     if template_arg is not None: | ||||
|         return cg.new_Pvariable(id_obj, template_arg, lambda_expr) | ||||
|     return cg.new_Pvariable(id_obj, lambda_expr) | ||||
|  | ||||
|  | ||||
| def validate_automation(extra_schema=None, extra_validators=None, single=False): | ||||
|     if extra_schema is None: | ||||
|         extra_schema = {} | ||||
| @@ -277,9 +240,7 @@ async def lambda_condition_to_code( | ||||
|     args: TemplateArgsType, | ||||
| ) -> MockObj: | ||||
|     lambda_ = await cg.process_lambda(config, args, return_type=bool) | ||||
|     return new_lambda_pvariable( | ||||
|         condition_id, lambda_, StatelessLambdaCondition, template_arg | ||||
|     ) | ||||
|     return cg.new_Pvariable(condition_id, template_arg, lambda_) | ||||
|  | ||||
|  | ||||
| @register_condition( | ||||
| @@ -445,7 +406,7 @@ async def lambda_action_to_code( | ||||
|     args: TemplateArgsType, | ||||
| ) -> MockObj: | ||||
|     lambda_ = await cg.process_lambda(config, args, return_type=cg.void) | ||||
|     return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg) | ||||
|     return cg.new_Pvariable(action_id, template_arg, lambda_) | ||||
|  | ||||
|  | ||||
| @register_action( | ||||
|   | ||||
| @@ -62,7 +62,6 @@ from esphome.cpp_types import (  # noqa: F401 | ||||
|     EntityBase, | ||||
|     EntityCategory, | ||||
|     ESPTime, | ||||
|     FixedVector, | ||||
|     GPIOPin, | ||||
|     InternalGPIOPin, | ||||
|     JsonObject, | ||||
|   | ||||
| @@ -9,7 +9,7 @@ static const char *const TAG = "adalight_light_effect"; | ||||
| static const uint32_t ADALIGHT_ACK_INTERVAL = 1000; | ||||
| static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000; | ||||
|  | ||||
| AdalightLightEffect::AdalightLightEffect(const char *name) : AddressableLightEffect(name) {} | ||||
| AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|  | ||||
| void AdalightLightEffect::start() { | ||||
|   AddressableLightEffect::start(); | ||||
|   | ||||
| @@ -11,7 +11,7 @@ namespace adalight { | ||||
|  | ||||
| class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice { | ||||
|  public: | ||||
|   AdalightLightEffect(const char *name); | ||||
|   AdalightLightEffect(const std::string &name); | ||||
|  | ||||
|   void start() override; | ||||
|   void stop() override; | ||||
|   | ||||
| @@ -172,6 +172,12 @@ 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), | ||||
|   | ||||
| @@ -71,12 +71,10 @@ SERVICE_ARG_NATIVE_TYPES = { | ||||
|     "int": cg.int32, | ||||
|     "float": float, | ||||
|     "string": cg.std_string, | ||||
|     "bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"), | ||||
|     "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), | ||||
|     "float[]": cg.FixedVector.template(float).operator("const").operator("ref"), | ||||
|     "string[]": cg.FixedVector.template(cg.std_string) | ||||
|     .operator("const") | ||||
|     .operator("ref"), | ||||
|     "bool[]": cg.std_vector.template(bool), | ||||
|     "int[]": cg.std_vector.template(cg.int32), | ||||
|     "float[]": cg.std_vector.template(float), | ||||
|     "string[]": cg.std_vector.template(cg.std_string), | ||||
| } | ||||
| CONF_ENCRYPTION = "encryption" | ||||
| CONF_BATCH_DELAY = "batch_delay" | ||||
| @@ -260,10 +258,6 @@ async def to_code(config): | ||||
|     if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: | ||||
|         cg.add_define("USE_API_SERVICES") | ||||
|  | ||||
|     # Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration | ||||
|     if config[CONF_CUSTOM_SERVICES]: | ||||
|         cg.add_define("USE_API_CUSTOM_SERVICES") | ||||
|  | ||||
|     if config[CONF_HOMEASSISTANT_SERVICES]: | ||||
|         cg.add_define("USE_API_HOMEASSISTANT_SERVICES") | ||||
|  | ||||
| @@ -271,8 +265,6 @@ async def to_code(config): | ||||
|         cg.add_define("USE_API_HOMEASSISTANT_STATES") | ||||
|  | ||||
|     if actions := config.get(CONF_ACTIONS, []): | ||||
|         # Collect all triggers first, then register all at once with initializer_list | ||||
|         triggers: list[cg.Pvariable] = [] | ||||
|         for conf in actions: | ||||
|             template_args = [] | ||||
|             func_args = [] | ||||
| @@ -286,10 +278,8 @@ async def to_code(config): | ||||
|             trigger = cg.new_Pvariable( | ||||
|                 conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names | ||||
|             ) | ||||
|             triggers.append(trigger) | ||||
|             cg.add(var.register_user_service(trigger)) | ||||
|             await automation.build_automation(trigger, func_args, conf) | ||||
|         # Register all services at once - single allocation, no reallocations | ||||
|         cg.add(var.initialize_user_services(triggers)) | ||||
|  | ||||
|     if CONF_ON_CLIENT_CONNECTED in config: | ||||
|         cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER") | ||||
|   | ||||
| @@ -425,7 +425,7 @@ message ListEntitiesFanResponse { | ||||
|   bool disabled_by_default = 9; | ||||
|   string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; | ||||
|   EntityCategory entity_category = 11; | ||||
|   repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"]; | ||||
|   repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"]; | ||||
|   uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; | ||||
| } | ||||
| // Deprecated in API version 1.6 - only used in deprecated fields | ||||
| @@ -989,7 +989,7 @@ message ListEntitiesClimateResponse { | ||||
|  | ||||
|   bool supports_current_temperature = 5; // Deprecated: use feature_flags | ||||
|   bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags | ||||
|   repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"]; | ||||
|   repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"]; | ||||
|   float visual_min_temperature = 8; | ||||
|   float visual_max_temperature = 9; | ||||
|   float visual_target_temperature_step = 10; | ||||
| @@ -998,11 +998,11 @@ message ListEntitiesClimateResponse { | ||||
|   // Deprecated in API version 1.5 | ||||
|   bool legacy_supports_away = 11 [deprecated=true]; | ||||
|   bool supports_action = 12; // Deprecated: use feature_flags | ||||
|   repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"]; | ||||
|   repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"]; | ||||
|   repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::vector"]; | ||||
|   repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; | ||||
|   repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"]; | ||||
|   repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"]; | ||||
|   repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"]; | ||||
|   repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"]; | ||||
|   repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set<climate::ClimatePreset>"]; | ||||
|   repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"]; | ||||
|   bool disabled_by_default = 18; | ||||
|   string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; | ||||
|   EntityCategory entity_category = 20; | ||||
| @@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse { | ||||
|   reserved 4; // Deprecated: was string unique_id | ||||
|  | ||||
|   string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; | ||||
|   repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"]; | ||||
|   repeated string options = 6 [(container_pointer) = "FixedVector"]; | ||||
|   bool disabled_by_default = 7; | ||||
|   EntityCategory entity_category = 8; | ||||
|   uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; | ||||
|   | ||||
| @@ -423,7 +423,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con | ||||
|   msg.supports_speed = traits.supports_speed(); | ||||
|   msg.supports_direction = traits.supports_direction(); | ||||
|   msg.supported_speed_count = traits.supported_speed_count(); | ||||
|   msg.supported_preset_modes = &traits.supported_preset_modes(); | ||||
|   msg.supported_preset_modes = &traits.supported_preset_modes_for_api_(); | ||||
|   return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
| void APIConnection::fan_command(const FanCommandRequest &msg) { | ||||
| @@ -486,7 +486,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c | ||||
|   if (light->supports_effects()) { | ||||
|     msg.effects.emplace_back("None"); | ||||
|     for (auto *effect : light->get_effects()) { | ||||
|       msg.effects.emplace_back(effect->get_name()); | ||||
|       msg.effects.push_back(effect->get_name()); | ||||
|     } | ||||
|   } | ||||
|   return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, | ||||
| @@ -669,18 +669,18 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection | ||||
|   msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); | ||||
|   // Current feature flags and other supported parameters | ||||
|   msg.feature_flags = traits.get_feature_flags(); | ||||
|   msg.supported_modes = &traits.get_supported_modes(); | ||||
|   msg.supported_modes = &traits.get_supported_modes_for_api_(); | ||||
|   msg.visual_min_temperature = traits.get_visual_min_temperature(); | ||||
|   msg.visual_max_temperature = traits.get_visual_max_temperature(); | ||||
|   msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); | ||||
|   msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); | ||||
|   msg.visual_min_humidity = traits.get_visual_min_humidity(); | ||||
|   msg.visual_max_humidity = traits.get_visual_max_humidity(); | ||||
|   msg.supported_fan_modes = &traits.get_supported_fan_modes(); | ||||
|   msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes(); | ||||
|   msg.supported_presets = &traits.get_supported_presets(); | ||||
|   msg.supported_custom_presets = &traits.get_supported_custom_presets(); | ||||
|   msg.supported_swing_modes = &traits.get_supported_swing_modes(); | ||||
|   msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_(); | ||||
|   msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_(); | ||||
|   msg.supported_presets = &traits.get_supported_presets_for_api_(); | ||||
|   msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_(); | ||||
|   msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_(); | ||||
|   return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, | ||||
|                                      is_single); | ||||
| } | ||||
| @@ -1572,13 +1572,7 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption | ||||
|   resp.success = false; | ||||
|  | ||||
|   psk_t psk{}; | ||||
|   if (msg.key.empty()) { | ||||
|     if (this->parent_->clear_noise_psk(true)) { | ||||
|       resp.success = true; | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Failed to clear encryption key"); | ||||
|     } | ||||
|   } else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { | ||||
|   if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { | ||||
|     ESP_LOGW(TAG, "Invalid encryption key length"); | ||||
|   } else if (!this->parent_->save_noise_psk(psk, true)) { | ||||
|     ESP_LOGW(TAG, "Failed to save encryption key"); | ||||
|   | ||||
| @@ -142,11 +142,6 @@ APIError APINoiseFrameHelper::loop() { | ||||
|  * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. | ||||
|  */ | ||||
| APIError APINoiseFrameHelper::try_read_frame_() { | ||||
|   // Clear buffer when starting a new frame (rx_buf_len_ == 0 means not resuming after WOULD_BLOCK) | ||||
|   if (this->rx_buf_len_ == 0) { | ||||
|     this->rx_buf_.clear(); | ||||
|   } | ||||
|  | ||||
|   // read header | ||||
|   if (rx_header_buf_len_ < 3) { | ||||
|     // no header information yet | ||||
|   | ||||
| @@ -54,11 +54,6 @@ APIError APIPlaintextFrameHelper::loop() { | ||||
|  * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. | ||||
|  */ | ||||
| APIError APIPlaintextFrameHelper::try_read_frame_() { | ||||
|   // Clear buffer when starting a new frame (rx_buf_len_ == 0 means not resuming after WOULD_BLOCK) | ||||
|   if (this->rx_buf_len_ == 0) { | ||||
|     this->rx_buf_.clear(); | ||||
|   } | ||||
|  | ||||
|   // read header | ||||
|   while (!rx_header_parsed_) { | ||||
|     // Now that we know when the socket is ready, we can read up to 3 bytes | ||||
|   | ||||
| @@ -355,8 +355,8 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(10, this->icon_ref_); | ||||
| #endif | ||||
|   buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category)); | ||||
|   for (const char *it : *this->supported_preset_modes) { | ||||
|     buffer.encode_string(12, it, strlen(it), true); | ||||
|   for (const auto &it : *this->supported_preset_modes) { | ||||
|     buffer.encode_string(12, it, true); | ||||
|   } | ||||
| #ifdef USE_DEVICES | ||||
|   buffer.encode_uint32(13, this->device_id); | ||||
| @@ -376,8 +376,8 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const { | ||||
| #endif | ||||
|   size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); | ||||
|   if (!this->supported_preset_modes->empty()) { | ||||
|     for (const char *it : *this->supported_preset_modes) { | ||||
|       size.add_length_force(1, strlen(it)); | ||||
|     for (const auto &it : *this->supported_preset_modes) { | ||||
|       size.add_length_force(1, it.size()); | ||||
|     } | ||||
|   } | ||||
| #ifdef USE_DEVICES | ||||
| @@ -1475,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { | ||||
| #ifdef USE_ENTITY_ICON | ||||
|   buffer.encode_string(5, this->icon_ref_); | ||||
| #endif | ||||
|   for (const char *it : *this->options) { | ||||
|     buffer.encode_string(6, it, strlen(it), true); | ||||
|   for (const auto &it : *this->options) { | ||||
|     buffer.encode_string(6, it, true); | ||||
|   } | ||||
|   buffer.encode_bool(7, this->disabled_by_default); | ||||
|   buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category)); | ||||
| @@ -1492,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const { | ||||
|   size.add_length(1, this->icon_ref_.size()); | ||||
| #endif | ||||
|   if (!this->options->empty()) { | ||||
|     for (const char *it : *this->options) { | ||||
|       size.add_length_force(1, strlen(it)); | ||||
|     for (const auto &it : *this->options) { | ||||
|       size.add_length_force(1, it.size()); | ||||
|     } | ||||
|   } | ||||
|   size.add_bool(1, this->disabled_by_default); | ||||
|   | ||||
| @@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { | ||||
|   bool supports_speed{false}; | ||||
|   bool supports_direction{false}; | ||||
|   int32_t supported_speed_count{0}; | ||||
|   const std::vector<const char *> *supported_preset_modes{}; | ||||
|   const std::set<std::string> *supported_preset_modes{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(ProtoSize &size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1377,16 +1377,16 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { | ||||
| #endif | ||||
|   bool supports_current_temperature{false}; | ||||
|   bool supports_two_point_target_temperature{false}; | ||||
|   const climate::ClimateModeMask *supported_modes{}; | ||||
|   const std::set<climate::ClimateMode> *supported_modes{}; | ||||
|   float visual_min_temperature{0.0f}; | ||||
|   float visual_max_temperature{0.0f}; | ||||
|   float visual_target_temperature_step{0.0f}; | ||||
|   bool supports_action{false}; | ||||
|   const climate::ClimateFanModeMask *supported_fan_modes{}; | ||||
|   const climate::ClimateSwingModeMask *supported_swing_modes{}; | ||||
|   const std::vector<std::string> *supported_custom_fan_modes{}; | ||||
|   const climate::ClimatePresetMask *supported_presets{}; | ||||
|   const std::vector<std::string> *supported_custom_presets{}; | ||||
|   const std::set<climate::ClimateFanMode> *supported_fan_modes{}; | ||||
|   const std::set<climate::ClimateSwingMode> *supported_swing_modes{}; | ||||
|   const std::set<std::string> *supported_custom_fan_modes{}; | ||||
|   const std::set<climate::ClimatePreset> *supported_presets{}; | ||||
|   const std::set<std::string> *supported_custom_presets{}; | ||||
|   float visual_current_temperature_step{0.0f}; | ||||
|   bool supports_current_humidity{false}; | ||||
|   bool supports_target_humidity{false}; | ||||
| @@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "list_entities_select_response"; } | ||||
| #endif | ||||
|   const FixedVector<const char *> *options{}; | ||||
|   const FixedVector<std::string> *options{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(ProtoSize &size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   | ||||
| @@ -88,12 +88,6 @@ static void dump_field(std::string &out, const char *field_name, StringRef value | ||||
|   out.append("\n"); | ||||
| } | ||||
|  | ||||
| static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) { | ||||
|   append_field_prefix(out, field_name, indent); | ||||
|   out.append("'").append(value).append("'"); | ||||
|   out.append("\n"); | ||||
| } | ||||
|  | ||||
| template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { | ||||
|   append_field_prefix(out, field_name, indent); | ||||
|   out.append(proto_enum_to_string<T>(value)); | ||||
|   | ||||
| @@ -468,31 +468,6 @@ uint16_t APIServer::get_port() const { return this->port_; } | ||||
| void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } | ||||
|  | ||||
| #ifdef USE_API_NOISE | ||||
| bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, | ||||
|                                   const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) { | ||||
|   if (!this->noise_pref_.save(&new_psk)) { | ||||
|     ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg)); | ||||
|     return false; | ||||
|   } | ||||
|   // ensure it's written immediately | ||||
|   if (!global_preferences->sync()) { | ||||
|     ESP_LOGW(TAG, "Failed to sync preferences"); | ||||
|     return false; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg)); | ||||
|   if (make_active) { | ||||
|     this->set_timeout(100, [this, active_psk]() { | ||||
|       ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); | ||||
|       this->set_noise_psk(active_psk); | ||||
|       for (auto &c : this->clients_) { | ||||
|         DisconnectRequest req; | ||||
|         c->send_message(req, DisconnectRequest::MESSAGE_TYPE); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| bool APIServer::save_noise_psk(psk_t psk, bool make_active) { | ||||
| #ifdef USE_API_NOISE_PSK_FROM_YAML | ||||
|   // When PSK is set from YAML, this function should never be called | ||||
| @@ -507,21 +482,27 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { | ||||
|   } | ||||
|  | ||||
|   SavedNoisePsk new_saved_psk{psk}; | ||||
|   return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk, | ||||
|                                  make_active); | ||||
| #endif | ||||
| } | ||||
| bool APIServer::clear_noise_psk(bool make_active) { | ||||
| #ifdef USE_API_NOISE_PSK_FROM_YAML | ||||
|   // When PSK is set from YAML, this function should never be called | ||||
|   // but if it is, reject the change | ||||
|   ESP_LOGW(TAG, "Key set in YAML"); | ||||
|   if (!this->noise_pref_.save(&new_saved_psk)) { | ||||
|     ESP_LOGW(TAG, "Failed to save Noise PSK"); | ||||
|     return false; | ||||
| #else | ||||
|   SavedNoisePsk empty_psk{}; | ||||
|   psk_t empty{}; | ||||
|   return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty, | ||||
|                                  make_active); | ||||
|   } | ||||
|   // ensure it's written immediately | ||||
|   if (!global_preferences->sync()) { | ||||
|     ESP_LOGW(TAG, "Failed to sync preferences"); | ||||
|     return false; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "Noise PSK saved"); | ||||
|   if (make_active) { | ||||
|     this->set_timeout(100, [this, psk]() { | ||||
|       ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); | ||||
|       this->set_noise_psk(psk); | ||||
|       for (auto &c : this->clients_) { | ||||
|         DisconnectRequest req; | ||||
|         c->send_message(req, DisconnectRequest::MESSAGE_TYPE); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   return true; | ||||
| #endif | ||||
| } | ||||
| #endif | ||||
|   | ||||
| @@ -53,7 +53,6 @@ class APIServer : public Component, public Controller { | ||||
|  | ||||
| #ifdef USE_API_NOISE | ||||
|   bool save_noise_psk(psk_t psk, bool make_active = true); | ||||
|   bool clear_noise_psk(bool make_active = true); | ||||
|   void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); } | ||||
|   std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; } | ||||
| #endif  // USE_API_NOISE | ||||
| @@ -125,14 +124,8 @@ class APIServer : public Component, public Controller { | ||||
| #endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES | ||||
| #endif  // USE_API_HOMEASSISTANT_SERVICES | ||||
| #ifdef USE_API_SERVICES | ||||
|   void initialize_user_services(std::initializer_list<UserServiceDescriptor *> services) { | ||||
|     this->user_services_.assign(services); | ||||
|   } | ||||
| #ifdef USE_API_CUSTOM_SERVICES | ||||
|   // Only compile push_back method when custom_services: true (external components) | ||||
|   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } | ||||
| #endif | ||||
| #endif | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
|   void request_time(); | ||||
| #endif | ||||
| @@ -181,10 +174,6 @@ class APIServer : public Component, public Controller { | ||||
|  | ||||
|  protected: | ||||
|   void schedule_reboot_timeout_(); | ||||
| #ifdef USE_API_NOISE | ||||
|   bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, | ||||
|                          const psk_t &active_psk, bool make_active); | ||||
| #endif  // USE_API_NOISE | ||||
|   // Pointers and pointer-like types first (4 bytes each) | ||||
|   std::unique_ptr<socket::Socket> socket_ = nullptr; | ||||
| #ifdef USE_API_CLIENT_CONNECTED_TRIGGER | ||||
|   | ||||
| @@ -53,14 +53,8 @@ class CustomAPIDevice { | ||||
|   template<typename T, typename... Ts> | ||||
|   void register_service(void (T::*callback)(Ts...), const std::string &name, | ||||
|                         const std::array<std::string, sizeof...(Ts)> &arg_names) { | ||||
| #ifdef USE_API_CUSTOM_SERVICES | ||||
|     auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback);  // NOLINT | ||||
|     global_api_server->register_user_service(service); | ||||
| #else | ||||
|     static_assert( | ||||
|         sizeof(T) == 0, | ||||
|         "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); | ||||
| #endif | ||||
|   } | ||||
| #else | ||||
|   template<typename T, typename... Ts> | ||||
| @@ -92,14 +86,8 @@ class CustomAPIDevice { | ||||
|    */ | ||||
| #ifdef USE_API_SERVICES | ||||
|   template<typename T> void register_service(void (T::*callback)(), const std::string &name) { | ||||
| #ifdef USE_API_CUSTOM_SERVICES | ||||
|     auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback);  // NOLINT | ||||
|     global_api_server->register_user_service(service); | ||||
| #else | ||||
|     static_assert( | ||||
|         sizeof(T) == 0, | ||||
|         "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration"); | ||||
| #endif | ||||
|   } | ||||
| #else | ||||
|   template<typename T> void register_service(void (T::*callback)(), const std::string &name) { | ||||
|   | ||||
| @@ -11,58 +11,23 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument & | ||||
| } | ||||
| template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; } | ||||
| template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; } | ||||
|  | ||||
| // Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve | ||||
| template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) { | ||||
|   std::vector<bool> result; | ||||
|   result.reserve(arg.bool_array.size()); | ||||
|   result.insert(result.end(), arg.bool_array.begin(), arg.bool_array.end()); | ||||
|   return result; | ||||
|   return std::vector<bool>(arg.bool_array.begin(), arg.bool_array.end()); | ||||
| } | ||||
| template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) { | ||||
|   std::vector<int32_t> result; | ||||
|   result.reserve(arg.int_array.size()); | ||||
|   result.insert(result.end(), arg.int_array.begin(), arg.int_array.end()); | ||||
|   return result; | ||||
|   return std::vector<int32_t>(arg.int_array.begin(), arg.int_array.end()); | ||||
| } | ||||
| template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) { | ||||
|   std::vector<float> result; | ||||
|   result.reserve(arg.float_array.size()); | ||||
|   result.insert(result.end(), arg.float_array.begin(), arg.float_array.end()); | ||||
|   return result; | ||||
|   return std::vector<float>(arg.float_array.begin(), arg.float_array.end()); | ||||
| } | ||||
| template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) { | ||||
|   std::vector<std::string> result; | ||||
|   result.reserve(arg.string_array.size()); | ||||
|   result.insert(result.end(), arg.string_array.begin(), arg.string_array.end()); | ||||
|   return result; | ||||
| } | ||||
|  | ||||
| // New FixedVector const reference versions for YAML-generated services - zero-copy | ||||
| template<> | ||||
| const FixedVector<bool> &get_execute_arg_value<const FixedVector<bool> &>(const ExecuteServiceArgument &arg) { | ||||
|   return arg.bool_array; | ||||
| } | ||||
| template<> | ||||
| const FixedVector<int32_t> &get_execute_arg_value<const FixedVector<int32_t> &>(const ExecuteServiceArgument &arg) { | ||||
|   return arg.int_array; | ||||
| } | ||||
| template<> | ||||
| const FixedVector<float> &get_execute_arg_value<const FixedVector<float> &>(const ExecuteServiceArgument &arg) { | ||||
|   return arg.float_array; | ||||
| } | ||||
| template<> | ||||
| const FixedVector<std::string> &get_execute_arg_value<const FixedVector<std::string> &>( | ||||
|     const ExecuteServiceArgument &arg) { | ||||
|   return arg.string_array; | ||||
|   return std::vector<std::string>(arg.string_array.begin(), arg.string_array.end()); | ||||
| } | ||||
|  | ||||
| template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; } | ||||
| template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; } | ||||
| template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; } | ||||
| template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; } | ||||
|  | ||||
| // Legacy std::vector versions for external components using custom_api_device.h | ||||
| template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } | ||||
| template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() { | ||||
|   return enums::SERVICE_ARG_TYPE_INT_ARRAY; | ||||
| @@ -74,18 +39,4 @@ template<> enums::ServiceArgType to_service_arg_type<std::vector<std::string>>() | ||||
|   return enums::SERVICE_ARG_TYPE_STRING_ARRAY; | ||||
| } | ||||
|  | ||||
| // New FixedVector const reference versions for YAML-generated services | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<bool> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; | ||||
| } | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<int32_t> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_INT_ARRAY; | ||||
| } | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<float> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY; | ||||
| } | ||||
| template<> enums::ServiceArgType to_service_arg_type<const FixedVector<std::string> &>() { | ||||
|   return enums::SERVICE_ARG_TYPE_STRING_ARRAY; | ||||
| } | ||||
|  | ||||
| }  // namespace esphome::api | ||||
|   | ||||
| @@ -99,8 +99,9 @@ enum BedjetCommand : uint8_t { | ||||
|  | ||||
| static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; | ||||
|  | ||||
| static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; | ||||
| static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; | ||||
| static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; | ||||
| static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_; | ||||
|  | ||||
| }  // namespace bedjet | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -43,7 +43,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli | ||||
|     }); | ||||
|  | ||||
|     // It would be better if we had a slider for the fan modes. | ||||
|     traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES); | ||||
|     traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET); | ||||
|     traits.set_supported_presets({ | ||||
|         // If we support NONE, then have to decide what happens if the user switches to it (turn off?) | ||||
|         // climate::CLIMATE_PRESET_NONE, | ||||
|   | ||||
| @@ -155,7 +155,6 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon | ||||
| InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) | ||||
| AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) | ||||
| LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) | ||||
| StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter) | ||||
| SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) | ||||
|  | ||||
| _LOGGER = getLogger(__name__) | ||||
| @@ -300,7 +299,7 @@ async def lambda_filter_to_code(config, filter_id): | ||||
|     lambda_ = await cg.process_lambda( | ||||
|         config, [(bool, "x")], return_type=cg.optional.template(bool) | ||||
|     ) | ||||
|     return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) | ||||
|     return cg.new_Pvariable(filter_id, lambda_) | ||||
|  | ||||
|  | ||||
| @register_filter( | ||||
| @@ -548,6 +547,11 @@ 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") | ||||
|  | ||||
|   | ||||
| @@ -2,11 +2,11 @@ | ||||
|  | ||||
| #include <cinttypes> | ||||
| #include <utility> | ||||
| #include <vector> | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -92,8 +92,8 @@ class DoubleClickTrigger : public Trigger<> { | ||||
|  | ||||
| class MultiClickTrigger : public Trigger<>, public Component { | ||||
|  public: | ||||
|   explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing) | ||||
|       : parent_(parent), timing_(timing) {} | ||||
|   explicit MultiClickTrigger(BinarySensor *parent, std::vector<MultiClickTriggerEvent> timing) | ||||
|       : parent_(parent), timing_(std::move(timing)) {} | ||||
|  | ||||
|   void setup() override { | ||||
|     this->last_state_ = this->parent_->get_state_default(false); | ||||
| @@ -115,7 +115,7 @@ class MultiClickTrigger : public Trigger<>, public Component { | ||||
|   void trigger_(); | ||||
|  | ||||
|   BinarySensor *parent_; | ||||
|   FixedVector<MultiClickTriggerEvent> timing_; | ||||
|   std::vector<MultiClickTriggerEvent> timing_; | ||||
|   uint32_t invalid_cooldown_{1000}; | ||||
|   optional<size_t> at_index_{}; | ||||
|   bool last_state_{false}; | ||||
|   | ||||
| @@ -111,21 +111,6 @@ class LambdaFilter : public Filter { | ||||
|   std::function<optional<bool>(bool)> f_; | ||||
| }; | ||||
|  | ||||
| /** Optimized lambda filter for stateless lambdas (no capture). | ||||
|  * | ||||
|  * Uses function pointer instead of std::function to reduce memory overhead. | ||||
|  * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). | ||||
|  */ | ||||
| class StatelessLambdaFilter : public Filter { | ||||
|  public: | ||||
|   explicit StatelessLambdaFilter(optional<bool> (*f)(bool)) : f_(f) {} | ||||
|  | ||||
|   optional<bool> new_value(bool value) override { return this->f_(value); } | ||||
|  | ||||
|  protected: | ||||
|   optional<bool> (*f_)(bool); | ||||
| }; | ||||
|  | ||||
| class SettleFilter : public Filter, public Component { | ||||
|  public: | ||||
|   optional<bool> new_value(bool value) override; | ||||
|   | ||||
| @@ -96,11 +96,8 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ | ||||
|   BLEClientWriteAction(BLEClient *ble_client) { | ||||
|     ble_client->register_ble_node(this); | ||||
|     ble_client_ = ble_client; | ||||
|     this->construct_simple_value_(); | ||||
|   } | ||||
|  | ||||
|   ~BLEClientWriteAction() { this->destroy_simple_value_(); } | ||||
|  | ||||
|   void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } | ||||
|   void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } | ||||
|   void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } | ||||
| @@ -109,18 +106,14 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ | ||||
|   void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } | ||||
|   void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } | ||||
|  | ||||
|   void set_value_template(std::vector<uint8_t> (*func)(Ts...)) { | ||||
|     this->destroy_simple_value_(); | ||||
|     this->value_.template_func = func; | ||||
|     this->has_simple_value_ = false; | ||||
|   void set_value_template(std::function<std::vector<uint8_t>(Ts...)> func) { | ||||
|     this->value_template_ = std::move(func); | ||||
|     has_simple_value_ = false; | ||||
|   } | ||||
|  | ||||
|   void set_value_simple(const std::vector<uint8_t> &value) { | ||||
|     if (!this->has_simple_value_) { | ||||
|       this->construct_simple_value_(); | ||||
|     } | ||||
|     this->value_.simple = value; | ||||
|     this->has_simple_value_ = true; | ||||
|     this->value_simple_ = value; | ||||
|     has_simple_value_ = true; | ||||
|   } | ||||
|  | ||||
|   void play(Ts... x) override {} | ||||
| @@ -128,7 +121,7 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ | ||||
|   void play_complex(Ts... x) override { | ||||
|     this->num_running_++; | ||||
|     this->var_ = std::make_tuple(x...); | ||||
|     auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...); | ||||
|     auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...); | ||||
|     // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. | ||||
|     if (!write(value)) | ||||
|       this->play_next_(x...); | ||||
| @@ -201,22 +194,10 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ | ||||
|   } | ||||
|  | ||||
|  private: | ||||
|   void construct_simple_value_() { new (&this->value_.simple) std::vector<uint8_t>(); } | ||||
|  | ||||
|   void destroy_simple_value_() { | ||||
|     if (this->has_simple_value_) { | ||||
|       this->value_.simple.~vector(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   BLEClient *ble_client_; | ||||
|   bool has_simple_value_ = true; | ||||
|   union Value { | ||||
|     std::vector<uint8_t> simple; | ||||
|     std::vector<uint8_t> (*template_func)(Ts...); | ||||
|     Value() {}   // trivial constructor | ||||
|     ~Value() {}  // trivial destructor - we manage lifetime via discriminator | ||||
|   } value_; | ||||
|   std::vector<uint8_t> value_simple_; | ||||
|   std::function<std::vector<uint8_t>(Ts...)> value_template_{}; | ||||
|   espbt::ESPBTUUID service_uuid_; | ||||
|   espbt::ESPBTUUID char_uuid_; | ||||
|   std::tuple<Ts...> var_{}; | ||||
| @@ -232,9 +213,9 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts... | ||||
|   void play(Ts... x) override { | ||||
|     uint32_t passkey; | ||||
|     if (has_simple_value_) { | ||||
|       passkey = this->value_.simple; | ||||
|       passkey = this->value_simple_; | ||||
|     } else { | ||||
|       passkey = this->value_.template_func(x...); | ||||
|       passkey = this->value_template_(x...); | ||||
|     } | ||||
|     if (passkey > 999999) | ||||
|       return; | ||||
| @@ -243,23 +224,21 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts... | ||||
|     esp_ble_passkey_reply(remote_bda, true, passkey); | ||||
|   } | ||||
|  | ||||
|   void set_value_template(uint32_t (*func)(Ts...)) { | ||||
|     this->value_.template_func = func; | ||||
|     this->has_simple_value_ = false; | ||||
|   void set_value_template(std::function<uint32_t(Ts...)> func) { | ||||
|     this->value_template_ = std::move(func); | ||||
|     has_simple_value_ = false; | ||||
|   } | ||||
|  | ||||
|   void set_value_simple(const uint32_t &value) { | ||||
|     this->value_.simple = value; | ||||
|     this->has_simple_value_ = true; | ||||
|     this->value_simple_ = value; | ||||
|     has_simple_value_ = true; | ||||
|   } | ||||
|  | ||||
|  private: | ||||
|   BLEClient *parent_{nullptr}; | ||||
|   bool has_simple_value_ = true; | ||||
|   union { | ||||
|     uint32_t simple; | ||||
|     uint32_t (*template_func)(Ts...); | ||||
|   } value_{.simple = 0}; | ||||
|   uint32_t value_simple_{0}; | ||||
|   std::function<uint32_t(Ts...)> value_template_{}; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> { | ||||
| @@ -270,29 +249,27 @@ template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Ac | ||||
|     esp_bd_addr_t remote_bda; | ||||
|     memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); | ||||
|     if (has_simple_value_) { | ||||
|       esp_ble_confirm_reply(remote_bda, this->value_.simple); | ||||
|       esp_ble_confirm_reply(remote_bda, this->value_simple_); | ||||
|     } else { | ||||
|       esp_ble_confirm_reply(remote_bda, this->value_.template_func(x...)); | ||||
|       esp_ble_confirm_reply(remote_bda, this->value_template_(x...)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void set_value_template(bool (*func)(Ts...)) { | ||||
|     this->value_.template_func = func; | ||||
|     this->has_simple_value_ = false; | ||||
|   void set_value_template(std::function<bool(Ts...)> func) { | ||||
|     this->value_template_ = std::move(func); | ||||
|     has_simple_value_ = false; | ||||
|   } | ||||
|  | ||||
|   void set_value_simple(const bool &value) { | ||||
|     this->value_.simple = value; | ||||
|     this->has_simple_value_ = true; | ||||
|     this->value_simple_ = value; | ||||
|     has_simple_value_ = true; | ||||
|   } | ||||
|  | ||||
|  private: | ||||
|   BLEClient *parent_{nullptr}; | ||||
|   bool has_simple_value_ = true; | ||||
|   union { | ||||
|     bool simple; | ||||
|     bool (*template_func)(Ts...); | ||||
|   } value_{.simple = false}; | ||||
|   bool value_simple_{false}; | ||||
|   std::function<bool(Ts...)> value_template_{}; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> { | ||||
|   | ||||
| @@ -117,9 +117,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga | ||||
| } | ||||
|  | ||||
| float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) { | ||||
|   if (this->has_data_to_value_) { | ||||
|   if (this->data_to_value_func_.has_value()) { | ||||
|     std::vector<uint8_t> data(value, value + value_len); | ||||
|     return this->data_to_value_func_(data); | ||||
|     return (*this->data_to_value_func_)(data); | ||||
|   } else { | ||||
|     return value[0]; | ||||
|   } | ||||
|   | ||||
| @@ -15,6 +15,8 @@ namespace ble_client { | ||||
|  | ||||
| namespace espbt = esphome::esp32_ble_tracker; | ||||
|  | ||||
| using data_to_value_t = std::function<float(std::vector<uint8_t>)>; | ||||
|  | ||||
| class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode { | ||||
|  public: | ||||
|   void loop() override; | ||||
| @@ -31,17 +33,13 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie | ||||
|   void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } | ||||
|   void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } | ||||
|   void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } | ||||
|   void set_data_to_value(float (*lambda)(const std::vector<uint8_t> &)) { | ||||
|     this->data_to_value_func_ = lambda; | ||||
|     this->has_data_to_value_ = true; | ||||
|   } | ||||
|   void set_data_to_value(data_to_value_t &&lambda) { this->data_to_value_func_ = lambda; } | ||||
|   void set_enable_notify(bool notify) { this->notify_ = notify; } | ||||
|   uint16_t handle; | ||||
|  | ||||
|  protected: | ||||
|   float parse_data_(uint8_t *value, uint16_t value_len); | ||||
|   bool has_data_to_value_{false}; | ||||
|   float (*data_to_value_func_)(const std::vector<uint8_t> &){}; | ||||
|   optional<data_to_value_t> data_to_value_func_{}; | ||||
|   bool notify_; | ||||
|   espbt::ESPBTUUID service_uuid_; | ||||
|   espbt::ESPBTUUID char_uuid_; | ||||
|   | ||||
| @@ -84,6 +84,11 @@ 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,6 +270,11 @@ 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") | ||||
|  | ||||
|   | ||||
| @@ -385,7 +385,7 @@ void Climate::save_state_() { | ||||
|   if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { | ||||
|     state.uses_custom_fan_mode = true; | ||||
|     const auto &supported = traits.get_supported_custom_fan_modes(); | ||||
|     // std::vector maintains insertion order | ||||
|     // std::set has consistent order (lexicographic for strings) | ||||
|     size_t i = 0; | ||||
|     for (const auto &mode : supported) { | ||||
|       if (mode == custom_fan_mode) { | ||||
| @@ -402,7 +402,7 @@ void Climate::save_state_() { | ||||
|   if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { | ||||
|     state.uses_custom_preset = true; | ||||
|     const auto &supported = traits.get_supported_custom_presets(); | ||||
|     // std::vector maintains insertion order | ||||
|     // std::set has consistent order (lexicographic for strings) | ||||
|     size_t i = 0; | ||||
|     for (const auto &preset : supported) { | ||||
|       if (preset == custom_preset) { | ||||
| @@ -524,23 +524,13 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { | ||||
|     call.set_target_humidity(this->target_humidity); | ||||
|   } | ||||
|   if (this->uses_custom_fan_mode) { | ||||
|     if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { | ||||
|       call.fan_mode_.reset(); | ||||
|       call.custom_fan_mode_ = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode); | ||||
|     } | ||||
|   } else if (traits.supports_fan_mode(this->fan_mode)) { | ||||
|   if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { | ||||
|     call.set_fan_mode(this->fan_mode); | ||||
|   } | ||||
|   if (this->uses_custom_preset) { | ||||
|     if (this->custom_preset < traits.get_supported_custom_presets().size()) { | ||||
|       call.preset_.reset(); | ||||
|       call.custom_preset_ = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset); | ||||
|     } | ||||
|   } else if (traits.supports_preset(this->preset)) { | ||||
|   if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { | ||||
|     call.set_preset(this->preset); | ||||
|   } | ||||
|   if (traits.supports_swing_mode(this->swing_mode)) { | ||||
|   if (traits.get_supports_swing_modes()) { | ||||
|     call.set_swing_mode(this->swing_mode); | ||||
|   } | ||||
|   return call; | ||||
| @@ -559,25 +549,41 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { | ||||
|     climate->target_humidity = this->target_humidity; | ||||
|   } | ||||
|   if (this->uses_custom_fan_mode) { | ||||
|     if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { | ||||
|       climate->fan_mode.reset(); | ||||
|       climate->custom_fan_mode = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode); | ||||
|     } | ||||
|   } else if (traits.supports_fan_mode(this->fan_mode)) { | ||||
|   if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) { | ||||
|     climate->fan_mode = this->fan_mode; | ||||
|     climate->custom_fan_mode.reset(); | ||||
|   } | ||||
|   if (this->uses_custom_preset) { | ||||
|     if (this->custom_preset < traits.get_supported_custom_presets().size()) { | ||||
|       climate->preset.reset(); | ||||
|       climate->custom_preset = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset); | ||||
|   if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) { | ||||
|     // std::set has consistent order (lexicographic for strings) | ||||
|     const auto &modes = traits.get_supported_custom_fan_modes(); | ||||
|     if (custom_fan_mode < modes.size()) { | ||||
|       size_t i = 0; | ||||
|       for (const auto &mode : modes) { | ||||
|         if (i == this->custom_fan_mode) { | ||||
|           climate->custom_fan_mode = mode; | ||||
|           break; | ||||
|         } | ||||
|   } else if (traits.supports_preset(this->preset)) { | ||||
|         i++; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   if (traits.get_supports_presets() && !this->uses_custom_preset) { | ||||
|     climate->preset = this->preset; | ||||
|     climate->custom_preset.reset(); | ||||
|   } | ||||
|   if (traits.supports_swing_mode(this->swing_mode)) { | ||||
|   if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) { | ||||
|     // std::set has consistent order (lexicographic for strings) | ||||
|     const auto &presets = traits.get_supported_custom_presets(); | ||||
|     if (custom_preset < presets.size()) { | ||||
|       size_t i = 0; | ||||
|       for (const auto &preset : presets) { | ||||
|         if (i == this->custom_preset) { | ||||
|           climate->custom_preset = preset; | ||||
|           break; | ||||
|         } | ||||
|         i++; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   if (traits.get_supports_swing_modes()) { | ||||
|     climate->swing_mode = this->swing_mode; | ||||
|   } | ||||
|   climate->publish_state(); | ||||
|   | ||||
| @@ -33,7 +33,6 @@ class Climate; | ||||
| class ClimateCall { | ||||
|  public: | ||||
|   explicit ClimateCall(Climate *parent) : parent_(parent) {} | ||||
|   friend struct ClimateDeviceRestoreState; | ||||
|  | ||||
|   /// Set the mode of the climate device. | ||||
|   ClimateCall &set_mode(ClimateMode mode); | ||||
|   | ||||
| @@ -7,7 +7,6 @@ namespace esphome { | ||||
| namespace climate { | ||||
|  | ||||
| /// Enum for all modes a climate device can be in. | ||||
| /// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value | ||||
| enum ClimateMode : uint8_t { | ||||
|   /// The climate device is off | ||||
|   CLIMATE_MODE_OFF = 0, | ||||
| @@ -25,7 +24,7 @@ enum ClimateMode : uint8_t { | ||||
|    * For example, the target temperature can be adjusted based on a schedule, or learned behavior. | ||||
|    * The target temperature can't be adjusted when in this mode. | ||||
|    */ | ||||
|   CLIMATE_MODE_AUTO = 6  // Update ClimateModeMask in climate_traits.h if adding values after this | ||||
|   CLIMATE_MODE_AUTO = 6 | ||||
| }; | ||||
|  | ||||
| /// Enum for the current action of the climate device. Values match those of ClimateMode. | ||||
| @@ -44,7 +43,6 @@ enum ClimateAction : uint8_t { | ||||
|   CLIMATE_ACTION_FAN = 6, | ||||
| }; | ||||
|  | ||||
| /// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value | ||||
| enum ClimateFanMode : uint8_t { | ||||
|   /// The fan mode is set to On | ||||
|   CLIMATE_FAN_ON = 0, | ||||
| @@ -65,11 +63,10 @@ enum ClimateFanMode : uint8_t { | ||||
|   /// The fan mode is set to Diffuse | ||||
|   CLIMATE_FAN_DIFFUSE = 8, | ||||
|   /// The fan mode is set to Quiet | ||||
|   CLIMATE_FAN_QUIET = 9,  // Update ClimateFanModeMask in climate_traits.h if adding values after this | ||||
|   CLIMATE_FAN_QUIET = 9, | ||||
| }; | ||||
|  | ||||
| /// Enum for all modes a climate swing can be in | ||||
| /// NOTE: If adding values, update ClimateSwingModeMask in climate_traits.h to use the new last value | ||||
| enum ClimateSwingMode : uint8_t { | ||||
|   /// The swing mode is set to Off | ||||
|   CLIMATE_SWING_OFF = 0, | ||||
| @@ -78,11 +75,10 @@ enum ClimateSwingMode : uint8_t { | ||||
|   /// The fan mode is set to Vertical | ||||
|   CLIMATE_SWING_VERTICAL = 2, | ||||
|   /// The fan mode is set to Horizontal | ||||
|   CLIMATE_SWING_HORIZONTAL = 3,  // Update ClimateSwingModeMask in climate_traits.h if adding values after this | ||||
|   CLIMATE_SWING_HORIZONTAL = 3, | ||||
| }; | ||||
|  | ||||
| /// Enum for all preset modes | ||||
| /// NOTE: If adding values, update ClimatePresetMask in climate_traits.h to use the new last value | ||||
| enum ClimatePreset : uint8_t { | ||||
|   /// No preset is active | ||||
|   CLIMATE_PRESET_NONE = 0, | ||||
| @@ -99,7 +95,7 @@ enum ClimatePreset : uint8_t { | ||||
|   /// Device is prepared for sleep | ||||
|   CLIMATE_PRESET_SLEEP = 6, | ||||
|   /// Device is reacting to activity (e.g., movement sensors) | ||||
|   CLIMATE_PRESET_ACTIVITY = 7,  // Update ClimatePresetMask in climate_traits.h if adding values after this | ||||
|   CLIMATE_PRESET_ACTIVITY = 7, | ||||
| }; | ||||
|  | ||||
| enum ClimateFeature : uint32_t { | ||||
|   | ||||
| @@ -1,33 +1,19 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <vector> | ||||
| #include <set> | ||||
| #include "climate_mode.h" | ||||
| #include "esphome/core/finite_set_mask.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| #ifdef USE_API | ||||
| namespace api { | ||||
| class APIConnection; | ||||
| }  // namespace api | ||||
| #endif | ||||
|  | ||||
| namespace climate { | ||||
|  | ||||
| // Type aliases for climate enum bitmasks | ||||
| // These replace std::set<EnumType> to eliminate red-black tree overhead | ||||
| // For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position) | ||||
| // Bitmask size is automatically calculated from the last enum value | ||||
| using ClimateModeMask = FiniteSetMask<ClimateMode, DefaultBitPolicy<ClimateMode, CLIMATE_MODE_AUTO + 1>>; | ||||
| using ClimateFanModeMask = FiniteSetMask<ClimateFanMode, DefaultBitPolicy<ClimateFanMode, CLIMATE_FAN_QUIET + 1>>; | ||||
| using ClimateSwingModeMask = | ||||
|     FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>; | ||||
| using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>; | ||||
|  | ||||
| // Lightweight linear search for small vectors (1-20 items) | ||||
| // Avoids std::find template overhead | ||||
| template<typename T> inline bool vector_contains(const std::vector<T> &vec, const T &value) { | ||||
|   for (const auto &item : vec) { | ||||
|     if (item == value) | ||||
|       return true; | ||||
|   } | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| /** This class contains all static data for climate devices. | ||||
|  * | ||||
|  * All climate devices must support these features: | ||||
| @@ -121,60 +107,48 @@ class ClimateTraits { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } | ||||
|   void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); } | ||||
|   void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } | ||||
|   bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); } | ||||
|   const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; } | ||||
|   const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; } | ||||
|  | ||||
|   void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } | ||||
|   void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); } | ||||
|   void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } | ||||
|   void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); } | ||||
|   void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); } | ||||
|   bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } | ||||
|   bool get_supports_fan_modes() const { | ||||
|     return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); | ||||
|   } | ||||
|   const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; } | ||||
|   const std::set<ClimateFanMode> &get_supported_fan_modes() const { return this->supported_fan_modes_; } | ||||
|  | ||||
|   void set_supported_custom_fan_modes(std::vector<std::string> supported_custom_fan_modes) { | ||||
|   void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) { | ||||
|     this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); | ||||
|   } | ||||
|   void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) { | ||||
|     this->supported_custom_fan_modes_ = modes; | ||||
|   } | ||||
|   template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) { | ||||
|     this->supported_custom_fan_modes_.assign(modes, modes + N); | ||||
|   } | ||||
|   const std::vector<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } | ||||
|   const std::set<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } | ||||
|   bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { | ||||
|     return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); | ||||
|     return this->supported_custom_fan_modes_.count(custom_fan_mode); | ||||
|   } | ||||
|  | ||||
|   void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } | ||||
|   void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); } | ||||
|   void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } | ||||
|   void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); } | ||||
|   void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.insert(preset); } | ||||
|   bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); } | ||||
|   bool get_supports_presets() const { return !this->supported_presets_.empty(); } | ||||
|   const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } | ||||
|   const std::set<climate::ClimatePreset> &get_supported_presets() const { return this->supported_presets_; } | ||||
|  | ||||
|   void set_supported_custom_presets(std::vector<std::string> supported_custom_presets) { | ||||
|   void set_supported_custom_presets(std::set<std::string> supported_custom_presets) { | ||||
|     this->supported_custom_presets_ = std::move(supported_custom_presets); | ||||
|   } | ||||
|   void set_supported_custom_presets(std::initializer_list<std::string> presets) { | ||||
|     this->supported_custom_presets_ = presets; | ||||
|   } | ||||
|   template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) { | ||||
|     this->supported_custom_presets_.assign(presets, presets + N); | ||||
|   } | ||||
|   const std::vector<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; } | ||||
|   const std::set<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; } | ||||
|   bool supports_custom_preset(const std::string &custom_preset) const { | ||||
|     return vector_contains(this->supported_custom_presets_, custom_preset); | ||||
|     return this->supported_custom_presets_.count(custom_preset); | ||||
|   } | ||||
|  | ||||
|   void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } | ||||
|   void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); } | ||||
|   void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } | ||||
|   bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); } | ||||
|   bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); } | ||||
|   const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; } | ||||
|   const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; } | ||||
|  | ||||
|   float get_visual_min_temperature() const { return this->visual_min_temperature_; } | ||||
|   void set_visual_min_temperature(float visual_min_temperature) { | ||||
| @@ -205,6 +179,23 @@ class ClimateTraits { | ||||
|   void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; } | ||||
|  | ||||
|  protected: | ||||
| #ifdef USE_API | ||||
|   // The API connection is a friend class to access internal methods | ||||
|   friend class api::APIConnection; | ||||
|   // These methods return references to internal data structures. | ||||
|   // They are used by the API to avoid copying data when encoding messages. | ||||
|   // Warning: Do not use these methods outside of the API connection code. | ||||
|   // They return references to internal data that can be invalidated. | ||||
|   const std::set<ClimateMode> &get_supported_modes_for_api_() const { return this->supported_modes_; } | ||||
|   const std::set<ClimateFanMode> &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; } | ||||
|   const std::set<std::string> &get_supported_custom_fan_modes_for_api_() const { | ||||
|     return this->supported_custom_fan_modes_; | ||||
|   } | ||||
|   const std::set<climate::ClimatePreset> &get_supported_presets_for_api_() const { return this->supported_presets_; } | ||||
|   const std::set<std::string> &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; } | ||||
|   const std::set<ClimateSwingMode> &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; } | ||||
| #endif | ||||
|  | ||||
|   void set_mode_support_(climate::ClimateMode mode, bool supported) { | ||||
|     if (supported) { | ||||
|       this->supported_modes_.insert(mode); | ||||
| @@ -235,12 +226,12 @@ class ClimateTraits { | ||||
|   float visual_min_humidity_{30}; | ||||
|   float visual_max_humidity_{99}; | ||||
|  | ||||
|   climate::ClimateModeMask supported_modes_{climate::CLIMATE_MODE_OFF}; | ||||
|   climate::ClimateFanModeMask supported_fan_modes_; | ||||
|   climate::ClimateSwingModeMask supported_swing_modes_; | ||||
|   climate::ClimatePresetMask supported_presets_; | ||||
|   std::vector<std::string> supported_custom_fan_modes_; | ||||
|   std::vector<std::string> supported_custom_presets_; | ||||
|   std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF}; | ||||
|   std::set<climate::ClimateFanMode> supported_fan_modes_; | ||||
|   std::set<climate::ClimateSwingMode> supported_swing_modes_; | ||||
|   std::set<climate::ClimatePreset> supported_presets_; | ||||
|   std::set<std::string> supported_custom_fan_modes_; | ||||
|   std::set<std::string> supported_custom_presets_; | ||||
| }; | ||||
|  | ||||
| }  // namespace climate | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| 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_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT | ||||
| from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT | ||||
| from esphome.cpp_generator import MockObjClass | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
| @@ -51,6 +52,26 @@ 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) | ||||
|   | ||||
| @@ -24,18 +24,16 @@ class ClimateIR : public Component, | ||||
|                   public remote_base::RemoteTransmittable { | ||||
|  public: | ||||
|   ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, | ||||
|             bool supports_dry = false, bool supports_fan_only = false, | ||||
|             climate::ClimateFanModeMask fan_modes = climate::ClimateFanModeMask(), | ||||
|             climate::ClimateSwingModeMask swing_modes = climate::ClimateSwingModeMask(), | ||||
|             climate::ClimatePresetMask presets = climate::ClimatePresetMask()) { | ||||
|             bool supports_dry = false, bool supports_fan_only = false, std::set<climate::ClimateFanMode> fan_modes = {}, | ||||
|             std::set<climate::ClimateSwingMode> swing_modes = {}, std::set<climate::ClimatePreset> presets = {}) { | ||||
|     this->minimum_temperature_ = minimum_temperature; | ||||
|     this->maximum_temperature_ = maximum_temperature; | ||||
|     this->temperature_step_ = temperature_step; | ||||
|     this->supports_dry_ = supports_dry; | ||||
|     this->supports_fan_only_ = supports_fan_only; | ||||
|     this->fan_modes_ = fan_modes; | ||||
|     this->swing_modes_ = swing_modes; | ||||
|     this->presets_ = presets; | ||||
|     this->fan_modes_ = std::move(fan_modes); | ||||
|     this->swing_modes_ = std::move(swing_modes); | ||||
|     this->presets_ = std::move(presets); | ||||
|   } | ||||
|  | ||||
|   void setup() override; | ||||
| @@ -62,9 +60,9 @@ class ClimateIR : public Component, | ||||
|   bool supports_heat_{true}; | ||||
|   bool supports_dry_{false}; | ||||
|   bool supports_fan_only_{false}; | ||||
|   climate::ClimateFanModeMask fan_modes_{}; | ||||
|   climate::ClimateSwingModeMask swing_modes_{}; | ||||
|   climate::ClimatePresetMask presets_{}; | ||||
|   std::set<climate::ClimateFanMode> fan_modes_ = {}; | ||||
|   std::set<climate::ClimateSwingMode> swing_modes_ = {}; | ||||
|   std::set<climate::ClimatePreset> presets_ = {}; | ||||
|  | ||||
|   sensor::Sensor *sensor_{nullptr}; | ||||
| }; | ||||
|   | ||||
| @@ -9,7 +9,7 @@ static const char *const TAG = "copy.select"; | ||||
| void CopySelect::setup() { | ||||
|   source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); | ||||
|  | ||||
|   traits.set_options(source_->traits.get_options()); | ||||
|   this->traits.copy_options(source_->traits.get_options()); | ||||
|  | ||||
|   if (source_->has_state()) | ||||
|     this->publish_state(source_->state); | ||||
|   | ||||
| @@ -151,6 +151,11 @@ 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") | ||||
|  | ||||
|   | ||||
| @@ -3,8 +3,6 @@ | ||||
| #include "e131_addressable_light_effect.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #include <algorithm> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace e131 { | ||||
|  | ||||
| @@ -78,14 +76,14 @@ void E131Component::loop() { | ||||
| } | ||||
|  | ||||
| void E131Component::add_effect(E131AddressableLightEffect *light_effect) { | ||||
|   if (std::find(light_effects_.begin(), light_effects_.end(), light_effect) != light_effects_.end()) { | ||||
|   if (light_effects_.count(light_effect)) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), | ||||
|            light_effect->get_last_universe()); | ||||
|   ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(), | ||||
|            light_effect->get_first_universe(), light_effect->get_last_universe()); | ||||
|  | ||||
|   light_effects_.push_back(light_effect); | ||||
|   light_effects_.insert(light_effect); | ||||
|  | ||||
|   for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { | ||||
|     join_(universe); | ||||
| @@ -93,17 +91,14 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) { | ||||
| } | ||||
|  | ||||
| void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { | ||||
|   auto it = std::find(light_effects_.begin(), light_effects_.end(), light_effect); | ||||
|   if (it == light_effects_.end()) { | ||||
|   if (!light_effects_.count(light_effect)) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), | ||||
|            light_effect->get_last_universe()); | ||||
|   ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(), | ||||
|            light_effect->get_first_universe(), light_effect->get_last_universe()); | ||||
|  | ||||
|   // Swap with last element and pop for O(1) removal (order doesn't matter) | ||||
|   *it = light_effects_.back(); | ||||
|   light_effects_.pop_back(); | ||||
|   light_effects_.erase(light_effect); | ||||
|  | ||||
|   for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { | ||||
|     leave_(universe); | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
| #include <cinttypes> | ||||
| #include <map> | ||||
| #include <memory> | ||||
| #include <set> | ||||
| #include <vector> | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -46,8 +47,9 @@ class E131Component : public esphome::Component { | ||||
|  | ||||
|   E131ListenMethod listen_method_{E131_MULTICAST}; | ||||
|   std::unique_ptr<socket::Socket> socket_; | ||||
|   std::vector<E131AddressableLightEffect *> light_effects_; | ||||
|   std::set<E131AddressableLightEffect *> light_effects_; | ||||
|   std::map<int, int> universe_consumers_; | ||||
|   std::map<int, E131Packet> universe_packets_; | ||||
| }; | ||||
|  | ||||
| }  // namespace e131 | ||||
|   | ||||
| @@ -9,7 +9,7 @@ namespace e131 { | ||||
| static const char *const TAG = "e131_addressable_light_effect"; | ||||
| static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1); | ||||
|  | ||||
| E131AddressableLightEffect::E131AddressableLightEffect(const char *name) : AddressableLightEffect(name) {} | ||||
| E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|  | ||||
| int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; } | ||||
|  | ||||
| @@ -58,8 +58,8 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet | ||||
|       std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1)); | ||||
|   auto *input_data = packet.values + 1; | ||||
|  | ||||
|   ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name(), universe, output_offset, | ||||
|            output_end); | ||||
|   ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name().c_str(), universe, | ||||
|            output_offset, output_end); | ||||
|  | ||||
|   switch (channels_) { | ||||
|     case E131_MONO: | ||||
|   | ||||
| @@ -13,7 +13,7 @@ enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 }; | ||||
|  | ||||
| class E131AddressableLightEffect : public light::AddressableLightEffect { | ||||
|  public: | ||||
|   E131AddressableLightEffect(const char *name); | ||||
|   E131AddressableLightEffect(const std::string &name); | ||||
|  | ||||
|   void start() override; | ||||
|   void stop() override; | ||||
|   | ||||
| @@ -304,13 +304,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: | ||||
| def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: | ||||
|     # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to | ||||
|     # a PIO platformio/framework-espidf value | ||||
|     if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1): | ||||
|         ext = "tar.xz" | ||||
|     else: | ||||
|         ext = "zip" | ||||
|     if release: | ||||
|         return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}" | ||||
|     return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}" | ||||
|         return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip" | ||||
|     return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip" | ||||
|  | ||||
|  | ||||
| def _is_framework_url(source: str) -> str: | ||||
| @@ -359,7 +355,6 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { | ||||
| ESP_IDF_PLATFORM_VERSION_LOOKUP = { | ||||
|     cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"), | ||||
|     cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"), | ||||
|     cv.Version(5, 4, 3): cv.Version(55, 3, 32), | ||||
|     cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"), | ||||
|     cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"), | ||||
|     cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"), | ||||
| @@ -882,11 +877,6 @@ async def to_code(config): | ||||
|     for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"): | ||||
|         os.environ.pop(clean_var, None) | ||||
|  | ||||
|     # Set the location of the IDF component manager cache | ||||
|     os.environ["IDF_COMPONENT_CACHE_PATH"] = str( | ||||
|         CORE.relative_internal_path(".espressif") | ||||
|     ) | ||||
|  | ||||
|     add_extra_script( | ||||
|         "post", | ||||
|         "post_build.py", | ||||
|   | ||||
| @@ -41,12 +41,12 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { | ||||
|   // - 1 byte padding for alignment | ||||
|   // - 4 bytes for vtable pointer | ||||
|   uint8_t pin_;        // GPIO pin number (0-255, actual max ~54 on ESP32) | ||||
|   gpio::Flags flags_{};  // GPIO flags (1 byte) | ||||
|   gpio::Flags flags_;  // GPIO flags (1 byte) | ||||
|   struct PinFlags { | ||||
|     uint8_t inverted : 1;        // Invert pin logic (1 bit) | ||||
|     uint8_t drive_strength : 2;  // Drive strength 0-3 (2 bits) | ||||
|     uint8_t reserved : 5;        // Reserved for future use (5 bits) | ||||
|   } pin_flags_{};                // Total: 1 byte | ||||
|   } pin_flags_;                  // Total: 1 byte | ||||
|   // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|   static bool isr_service_installed; | ||||
| }; | ||||
|   | ||||
| @@ -223,10 +223,7 @@ async def esp32_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}"))) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     if CONF_DRIVE_STRENGTH in config: | ||||
|         cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH])) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|   | ||||
| @@ -76,10 +76,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) { | ||||
| } | ||||
|  | ||||
| void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) { | ||||
|   this->advertising_set_manufacturer_data(std::span<const uint8_t>(data)); | ||||
| } | ||||
|  | ||||
| void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) { | ||||
|   this->advertising_init_(); | ||||
|   this->advertising_->set_manufacturer_data(data); | ||||
|   this->advertising_start(); | ||||
|   | ||||
| @@ -118,7 +118,6 @@ class ESP32BLE : public Component { | ||||
|   void advertising_start(); | ||||
|   void advertising_set_service_data(const std::vector<uint8_t> &data); | ||||
|   void advertising_set_manufacturer_data(const std::vector<uint8_t> &data); | ||||
|   void advertising_set_manufacturer_data(std::span<const uint8_t> data); | ||||
|   void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; } | ||||
|   void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name); | ||||
|   void advertising_add_service_uuid(ESPBTUUID uuid); | ||||
|   | ||||
| @@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) { | ||||
| } | ||||
|  | ||||
| void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) { | ||||
|   this->set_manufacturer_data(std::span<const uint8_t>(data)); | ||||
| } | ||||
|  | ||||
| void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) { | ||||
|   delete[] this->advertising_data_.p_manufacturer_data; | ||||
|   this->advertising_data_.p_manufacturer_data = nullptr; | ||||
|   this->advertising_data_.manufacturer_len = data.size(); | ||||
|   | ||||
| @@ -37,7 +37,6 @@ class BLEAdvertising { | ||||
|   void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; } | ||||
|   void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; } | ||||
|   void set_manufacturer_data(const std::vector<uint8_t> &data); | ||||
|   void set_manufacturer_data(std::span<const uint8_t> data); | ||||
|   void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; } | ||||
|   void set_service_data(const std::vector<uint8_t> &data); | ||||
|   void set_service_data(std::span<const uint8_t> data); | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| #include "esp32_ble_beacon.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
|   | ||||
| @@ -461,9 +461,7 @@ async def parse_value(value_config, args): | ||||
|     if isinstance(value, str): | ||||
|         value = list(value.encode(value_config[CONF_STRING_ENCODING])) | ||||
|     if isinstance(value, list): | ||||
|         # Generate initializer list {1, 2, 3} instead of std::vector<uint8_t>({1, 2, 3}) | ||||
|         # This calls the set_value(std::initializer_list<uint8_t>) overload | ||||
|         return cg.ArrayInitializer(*value) | ||||
|         return cg.std_vector.template(cg.uint8)(value) | ||||
|     val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})") | ||||
|     return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS]) | ||||
|  | ||||
|   | ||||
| @@ -35,18 +35,13 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) | ||||
|  | ||||
| void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } | ||||
|  | ||||
| void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) { | ||||
| void BLECharacteristic::set_value(const std::vector<uint8_t> &buffer) { | ||||
|   xSemaphoreTake(this->set_value_lock_, 0L); | ||||
|   this->value_ = std::move(buffer); | ||||
|   this->value_ = buffer; | ||||
|   xSemaphoreGive(this->set_value_lock_); | ||||
| } | ||||
|  | ||||
| void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) { | ||||
|   this->set_value(std::vector<uint8_t>(data));  // Delegate to move overload | ||||
| } | ||||
|  | ||||
| void BLECharacteristic::set_value(const std::string &buffer) { | ||||
|   this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end()));  // Delegate to move overload | ||||
|   this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end())); | ||||
| } | ||||
|  | ||||
| void BLECharacteristic::notify() { | ||||
|   | ||||
| @@ -33,8 +33,7 @@ class BLECharacteristic { | ||||
|   ~BLECharacteristic(); | ||||
|  | ||||
|   void set_value(ByteBuffer buffer); | ||||
|   void set_value(std::vector<uint8_t> &&buffer); | ||||
|   void set_value(std::initializer_list<uint8_t> data); | ||||
|   void set_value(const std::vector<uint8_t> &buffer); | ||||
|   void set_value(const std::string &buffer); | ||||
|  | ||||
|   void set_broadcast_property(bool value); | ||||
|   | ||||
| @@ -46,17 +46,15 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) { | ||||
|   this->state_ = CREATING; | ||||
| } | ||||
|  | ||||
| void BLEDescriptor::set_value(std::vector<uint8_t> &&buffer) { this->set_value_impl_(buffer.data(), buffer.size()); } | ||||
| void BLEDescriptor::set_value(std::vector<uint8_t> buffer) { | ||||
|   size_t length = buffer.size(); | ||||
|  | ||||
| void BLEDescriptor::set_value(std::initializer_list<uint8_t> data) { this->set_value_impl_(data.begin(), data.size()); } | ||||
|  | ||||
| void BLEDescriptor::set_value_impl_(const uint8_t *data, size_t length) { | ||||
|   if (length > this->value_.attr_max_len) { | ||||
|     ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len); | ||||
|     return; | ||||
|   } | ||||
|   this->value_.attr_len = length; | ||||
|   memcpy(this->value_.attr_value, data, length); | ||||
|   memcpy(this->value_.attr_value, buffer.data(), length); | ||||
| } | ||||
|  | ||||
| void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, | ||||
|   | ||||
| @@ -27,8 +27,7 @@ class BLEDescriptor { | ||||
|   void do_create(BLECharacteristic *characteristic); | ||||
|   ESPBTUUID get_uuid() const { return this->uuid_; } | ||||
|  | ||||
|   void set_value(std::vector<uint8_t> &&buffer); | ||||
|   void set_value(std::initializer_list<uint8_t> data); | ||||
|   void set_value(std::vector<uint8_t> buffer); | ||||
|   void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } | ||||
|  | ||||
|   void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); | ||||
| @@ -43,8 +42,6 @@ class BLEDescriptor { | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   void set_value_impl_(const uint8_t *data, size_t length); | ||||
|  | ||||
|   BLECharacteristic *characteristic_{nullptr}; | ||||
|   ESPBTUUID uuid_; | ||||
|   uint16_t handle_{0xFFFF}; | ||||
|   | ||||
| @@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w | ||||
|   Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger =  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|       new Trigger<std::vector<uint8_t>, uint16_t>(); | ||||
|   characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) { | ||||
|     // Convert span to vector for trigger - copy is necessary because: | ||||
|     // 1. Trigger stores the data for use in automation actions that execute later | ||||
|     // 2. The span is only valid during this callback (points to temporary BLE stack data) | ||||
|     // 3. User lambdas in automations need persistent data they can access asynchronously | ||||
|     // Convert span to vector for trigger | ||||
|     on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id); | ||||
|   }); | ||||
|   return on_write_trigger; | ||||
| @@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write | ||||
|   Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger =  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|       new Trigger<std::vector<uint8_t>, uint16_t>(); | ||||
|   descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) { | ||||
|     // Convert span to vector for trigger - copy is necessary because: | ||||
|     // 1. Trigger stores the data for use in automation actions that execute later | ||||
|     // 2. The span is only valid during this callback (points to temporary BLE stack data) | ||||
|     // 3. User lambdas in automations need persistent data they can access asynchronously | ||||
|     // Convert span to vector for trigger | ||||
|     on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id); | ||||
|   }); | ||||
|   return on_write_trigger; | ||||
|   | ||||
| @@ -270,8 +270,8 @@ void ESP32ImprovComponent::set_error_(improv::Error error) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &&response) { | ||||
|   this->rpc_response_->set_value(std::move(response)); | ||||
| void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &response) { | ||||
|   this->rpc_response_->set_value(ByteBuffer::wrap(response)); | ||||
|   if (this->state_ != improv::STATE_STOPPED) | ||||
|     this->rpc_response_->notify(); | ||||
| } | ||||
| @@ -409,8 +409,10 @@ void ESP32ImprovComponent::check_wifi_connection_() { | ||||
|       } | ||||
|     } | ||||
| #endif | ||||
|     this->send_response_(improv::build_rpc_response(improv::WIFI_SETTINGS, | ||||
|                                                     std::vector<std::string>(url_strings, url_strings + url_count))); | ||||
|     // Pass to build_rpc_response using vector constructor from iterators to avoid extra copies | ||||
|     std::vector<uint8_t> data = improv::build_rpc_response( | ||||
|         improv::WIFI_SETTINGS, std::vector<std::string>(url_strings, url_strings + url_count)); | ||||
|     this->send_response_(data); | ||||
|   } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) { | ||||
|     ESP_LOGD(TAG, "WiFi provisioned externally"); | ||||
|   } | ||||
|   | ||||
| @@ -109,7 +109,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase { | ||||
|   void set_state_(improv::State state, bool update_advertising = true); | ||||
|   void set_error_(improv::Error error); | ||||
|   improv::State get_initial_state_() const; | ||||
|   void send_response_(std::vector<uint8_t> &&response); | ||||
|   void send_response_(std::vector<uint8_t> &response); | ||||
|   void process_incoming_data_(); | ||||
|   void on_wifi_connect_timeout_(); | ||||
|   void check_wifi_connection_(); | ||||
|   | ||||
| @@ -29,8 +29,8 @@ class ESP8266GPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   bool inverted_{}; | ||||
|   gpio::Flags flags_{}; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
| }; | ||||
|  | ||||
| }  // namespace esp8266 | ||||
|   | ||||
| @@ -165,10 +165,7 @@ async def esp8266_pin_to_code(config): | ||||
|     num = config[CONF_NUMBER] | ||||
|     mode = config[CONF_MODE] | ||||
|     cg.add(var.set_pin(num)) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(mode))) | ||||
|     if num < 16: | ||||
|         initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][ | ||||
|   | ||||
| @@ -281,15 +281,19 @@ void ESPHomeOTAComponent::handle_data_() { | ||||
| #endif | ||||
|  | ||||
|   // Acknowledge auth OK - 1 byte | ||||
|   this->write_byte_(ota::OTA_RESPONSE_AUTH_OK); | ||||
|   buf[0] = ota::OTA_RESPONSE_AUTH_OK; | ||||
|   this->writeall_(buf, 1); | ||||
|  | ||||
|   // Read size, 4 bytes MSB first | ||||
|   if (!this->readall_(buf, 4)) { | ||||
|     this->log_read_error_(LOG_STR("size")); | ||||
|     goto error;  // NOLINT(cppcoreguidelines-avoid-goto) | ||||
|   } | ||||
|   ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) | | ||||
|              (static_cast<size_t>(buf[2]) << 8) | buf[3]; | ||||
|   ota_size = 0; | ||||
|   for (uint8_t i = 0; i < 4; i++) { | ||||
|     ota_size <<= 8; | ||||
|     ota_size |= buf[i]; | ||||
|   } | ||||
|   ESP_LOGV(TAG, "Size is %u bytes", ota_size); | ||||
|  | ||||
|   // Now that we've passed authentication and are actually | ||||
| @@ -309,7 +313,8 @@ void ESPHomeOTAComponent::handle_data_() { | ||||
|   update_started = true; | ||||
|  | ||||
|   // Acknowledge prepare OK - 1 byte | ||||
|   this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK); | ||||
|   buf[0] = ota::OTA_RESPONSE_UPDATE_PREPARE_OK; | ||||
|   this->writeall_(buf, 1); | ||||
|  | ||||
|   // Read binary MD5, 32 bytes | ||||
|   if (!this->readall_(buf, 32)) { | ||||
| @@ -321,7 +326,8 @@ void ESPHomeOTAComponent::handle_data_() { | ||||
|   this->backend_->set_update_md5(sbuf); | ||||
|  | ||||
|   // Acknowledge MD5 OK - 1 byte | ||||
|   this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK); | ||||
|   buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK; | ||||
|   this->writeall_(buf, 1); | ||||
|  | ||||
|   while (total < ota_size) { | ||||
|     // TODO: timeout check | ||||
| @@ -348,7 +354,8 @@ void ESPHomeOTAComponent::handle_data_() { | ||||
|     total += read; | ||||
| #if USE_OTA_VERSION == 2 | ||||
|     while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) { | ||||
|       this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK); | ||||
|       buf[0] = ota::OTA_RESPONSE_CHUNK_OK; | ||||
|       this->writeall_(buf, 1); | ||||
|       size_acknowledged += OTA_BLOCK_SIZE; | ||||
|     } | ||||
| #endif | ||||
| @@ -367,7 +374,8 @@ void ESPHomeOTAComponent::handle_data_() { | ||||
|   } | ||||
|  | ||||
|   // Acknowledge receive OK - 1 byte | ||||
|   this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK); | ||||
|   buf[0] = ota::OTA_RESPONSE_RECEIVE_OK; | ||||
|   this->writeall_(buf, 1); | ||||
|  | ||||
|   error_code = this->backend_->end(); | ||||
|   if (error_code != ota::OTA_RESPONSE_OK) { | ||||
| @@ -376,7 +384,8 @@ void ESPHomeOTAComponent::handle_data_() { | ||||
|   } | ||||
|  | ||||
|   // Acknowledge Update end OK - 1 byte | ||||
|   this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK); | ||||
|   buf[0] = ota::OTA_RESPONSE_UPDATE_END_OK; | ||||
|   this->writeall_(buf, 1); | ||||
|  | ||||
|   // Read ACK | ||||
|   if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) { | ||||
| @@ -395,7 +404,8 @@ void ESPHomeOTAComponent::handle_data_() { | ||||
|   App.safe_reboot(); | ||||
|  | ||||
| error: | ||||
|   this->write_byte_(static_cast<uint8_t>(error_code)); | ||||
|   buf[0] = static_cast<uint8_t>(error_code); | ||||
|   this->writeall_(buf, 1); | ||||
|   this->cleanup_connection_(); | ||||
|  | ||||
|   if (this->backend_ != nullptr && update_started) { | ||||
|   | ||||
| @@ -53,7 +53,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent { | ||||
| #endif  // USE_OTA_PASSWORD | ||||
|   bool readall_(uint8_t *buf, size_t len); | ||||
|   bool writeall_(const uint8_t *buf, size_t len); | ||||
|   inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); } | ||||
|  | ||||
|   bool try_read_(size_t to_read, const LogString *desc); | ||||
|   bool try_write_(size_t to_write, const LogString *desc); | ||||
|   | ||||
| @@ -14,7 +14,7 @@ from esphome.components.esp32.const import ( | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
| ) | ||||
| from esphome.components.network import ip_address_literal | ||||
| from esphome.components.network import IPAddress | ||||
| from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
| @@ -32,7 +32,6 @@ from esphome.const import ( | ||||
|     CONF_MISO_PIN, | ||||
|     CONF_MODE, | ||||
|     CONF_MOSI_PIN, | ||||
|     CONF_NUMBER, | ||||
|     CONF_PAGE_ID, | ||||
|     CONF_PIN, | ||||
|     CONF_POLLING_INTERVAL, | ||||
| @@ -53,36 +52,12 @@ from esphome.core import ( | ||||
|     coroutine_with_priority, | ||||
| ) | ||||
| import esphome.final_validate as fv | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| CONFLICTS_WITH = ["wifi"] | ||||
| DEPENDENCIES = ["esp32"] | ||||
| AUTO_LOAD = ["network"] | ||||
| LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| # RMII pins that are hardcoded on ESP32 classic and cannot be changed | ||||
| # These pins are used by the internal Ethernet MAC when using RMII PHYs | ||||
| ESP32_RMII_FIXED_PINS = { | ||||
|     19: "EMAC_TXD0", | ||||
|     21: "EMAC_TX_EN", | ||||
|     22: "EMAC_TXD1", | ||||
|     25: "EMAC_RXD0", | ||||
|     26: "EMAC_RXD1", | ||||
|     27: "EMAC_RX_CRS_DV", | ||||
| } | ||||
|  | ||||
| # RMII default pins for ESP32-P4 | ||||
| # These are the default pins used by ESP-IDF and are configurable in principle, | ||||
| # but ESPHome's ethernet component currently has no way to change them | ||||
| ESP32P4_RMII_DEFAULT_PINS = { | ||||
|     34: "EMAC_TXD0", | ||||
|     35: "EMAC_TXD1", | ||||
|     28: "EMAC_RX_CRS_DV", | ||||
|     29: "EMAC_RXD0", | ||||
|     30: "EMAC_RXD1", | ||||
|     49: "EMAC_TX_EN", | ||||
| } | ||||
|  | ||||
| ethernet_ns = cg.esphome_ns.namespace("ethernet") | ||||
| PHYRegister = ethernet_ns.struct("PHYRegister") | ||||
| CONF_PHY_ADDR = "phy_addr" | ||||
| @@ -298,7 +273,7 @@ CONFIG_SCHEMA = cv.All( | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _final_validate_spi(config): | ||||
| def _final_validate(config): | ||||
|     if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: | ||||
|         return | ||||
|     if spi_configs := fv.full_config.get().get(CONF_SPI): | ||||
| @@ -317,14 +292,17 @@ def _final_validate_spi(config): | ||||
|                     ) | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = _final_validate | ||||
|  | ||||
|  | ||||
| def manual_ip(config): | ||||
|     return cg.StructInitializer( | ||||
|         ManualIP, | ||||
|         ("static_ip", ip_address_literal(config[CONF_STATIC_IP])), | ||||
|         ("gateway", ip_address_literal(config[CONF_GATEWAY])), | ||||
|         ("subnet", ip_address_literal(config[CONF_SUBNET])), | ||||
|         ("dns1", ip_address_literal(config[CONF_DNS1])), | ||||
|         ("dns2", ip_address_literal(config[CONF_DNS2])), | ||||
|         ("static_ip", IPAddress(str(config[CONF_STATIC_IP]))), | ||||
|         ("gateway", IPAddress(str(config[CONF_GATEWAY]))), | ||||
|         ("subnet", IPAddress(str(config[CONF_SUBNET]))), | ||||
|         ("dns1", IPAddress(str(config[CONF_DNS1]))), | ||||
|         ("dns2", IPAddress(str(config[CONF_DNS2]))), | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @@ -405,57 +383,3 @@ async def to_code(config): | ||||
|  | ||||
|     if CORE.using_arduino: | ||||
|         cg.add_library("WiFi", None) | ||||
|  | ||||
|  | ||||
| def _final_validate_rmii_pins(config: ConfigType) -> None: | ||||
|     """Validate that RMII pins are not used by other components.""" | ||||
|     # Only validate for RMII-based PHYs on ESP32/ESP32P4 | ||||
|     if config[CONF_TYPE] in SPI_ETHERNET_TYPES or config[CONF_TYPE] == "OPENETH": | ||||
|         return  # SPI and OPENETH don't use RMII | ||||
|  | ||||
|     variant = get_esp32_variant() | ||||
|     if variant == VARIANT_ESP32: | ||||
|         rmii_pins = ESP32_RMII_FIXED_PINS | ||||
|         is_configurable = False | ||||
|     elif variant == VARIANT_ESP32P4: | ||||
|         rmii_pins = ESP32P4_RMII_DEFAULT_PINS | ||||
|         is_configurable = True | ||||
|     else: | ||||
|         return  # No RMII validation needed for other variants | ||||
|  | ||||
|     # Check all used pins against RMII reserved pins | ||||
|     for pin_list in pins.PIN_SCHEMA_REGISTRY.pins_used.values(): | ||||
|         for pin_path, _, pin_config in pin_list: | ||||
|             pin_num = pin_config.get(CONF_NUMBER) | ||||
|             if pin_num not in rmii_pins: | ||||
|                 continue | ||||
|             # Found a conflict - show helpful error message | ||||
|             pin_function = rmii_pins[pin_num] | ||||
|             component_path = ".".join(str(p) for p in pin_path) | ||||
|             if is_configurable: | ||||
|                 error_msg = ( | ||||
|                     f"GPIO{pin_num} is used by Ethernet RMII " | ||||
|                     f"({pin_function}) with the current default " | ||||
|                     f"configuration. This conflicts with '{component_path}'. " | ||||
|                     f"Please choose a different GPIO pin for " | ||||
|                     f"'{component_path}'." | ||||
|                 ) | ||||
|             else: | ||||
|                 error_msg = ( | ||||
|                     f"GPIO{pin_num} is reserved for Ethernet RMII " | ||||
|                     f"({pin_function}) and cannot be used. This pin is " | ||||
|                     f"hardcoded by ESP-IDF and cannot be changed when using " | ||||
|                     f"RMII Ethernet PHYs. Please choose a different GPIO pin " | ||||
|                     f"for '{component_path}'." | ||||
|                 ) | ||||
|             raise cv.Invalid(error_msg, path=pin_path) | ||||
|  | ||||
|  | ||||
| def _final_validate(config: ConfigType) -> ConfigType: | ||||
|     """Final validation for Ethernet component.""" | ||||
|     _final_validate_spi(config) | ||||
|     _final_validate_rmii_pins(config) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = _final_validate | ||||
|   | ||||
| @@ -85,6 +85,11 @@ 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") | ||||
|  | ||||
|   | ||||
| @@ -8,19 +8,12 @@ namespace event { | ||||
| static const char *const TAG = "event"; | ||||
|  | ||||
| void Event::trigger(const std::string &event_type) { | ||||
|   // Linear search - faster than std::set for small datasets (1-5 items typical) | ||||
|   const std::string *found = nullptr; | ||||
|   for (const auto &type : this->types_) { | ||||
|     if (type == event_type) { | ||||
|       found = &type; | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|   if (found == nullptr) { | ||||
|   auto found = types_.find(event_type); | ||||
|   if (found == types_.end()) { | ||||
|     ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str()); | ||||
|     return; | ||||
|   } | ||||
|   last_event_type = found; | ||||
|   last_event_type = &(*found); | ||||
|   ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str()); | ||||
|   this->event_callback_.call(event_type); | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <set> | ||||
| #include <string> | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| @@ -25,13 +26,13 @@ class Event : public EntityBase, public EntityBase_DeviceClass { | ||||
|   const std::string *last_event_type; | ||||
|  | ||||
|   void trigger(const std::string &event_type); | ||||
|   void set_event_types(const std::initializer_list<std::string> &event_types) { this->types_ = event_types; } | ||||
|   const FixedVector<std::string> &get_event_types() const { return this->types_; } | ||||
|   void set_event_types(const std::set<std::string> &event_types) { this->types_ = event_types; } | ||||
|   std::set<std::string> get_event_types() const { return this->types_; } | ||||
|   void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback); | ||||
|  | ||||
|  protected: | ||||
|   CallbackManager<void(const std::string &event_type)> event_callback_; | ||||
|   FixedVector<std::string> types_; | ||||
|   std::set<std::string> types_; | ||||
| }; | ||||
|  | ||||
| }  // namespace event | ||||
|   | ||||
| @@ -189,6 +189,10 @@ 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), | ||||
|   | ||||
| @@ -51,14 +51,7 @@ void FanCall::validate_() { | ||||
|  | ||||
|   if (!this->preset_mode_.empty()) { | ||||
|     const auto &preset_modes = traits.supported_preset_modes(); | ||||
|     bool found = false; | ||||
|     for (const auto &mode : preset_modes) { | ||||
|       if (strcmp(mode, this->preset_mode_.c_str()) == 0) { | ||||
|         found = true; | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|     if (!found) { | ||||
|     if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { | ||||
|       ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str()); | ||||
|       this->preset_mode_.clear(); | ||||
|     } | ||||
| @@ -99,12 +92,11 @@ FanCall FanRestoreState::to_call(Fan &fan) { | ||||
|   call.set_speed(this->speed); | ||||
|   call.set_direction(this->direction); | ||||
|  | ||||
|   auto traits = fan.get_traits(); | ||||
|   if (traits.supports_preset_modes()) { | ||||
|   if (fan.get_traits().supports_preset_modes()) { | ||||
|     // Use stored preset index to get preset name | ||||
|     const auto &preset_modes = traits.supported_preset_modes(); | ||||
|     const auto &preset_modes = fan.get_traits().supported_preset_modes(); | ||||
|     if (this->preset_mode < preset_modes.size()) { | ||||
|       call.set_preset_mode(preset_modes[this->preset_mode]); | ||||
|       call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode)); | ||||
|     } | ||||
|   } | ||||
|   return call; | ||||
| @@ -115,12 +107,11 @@ void FanRestoreState::apply(Fan &fan) { | ||||
|   fan.speed = this->speed; | ||||
|   fan.direction = this->direction; | ||||
|  | ||||
|   auto traits = fan.get_traits(); | ||||
|   if (traits.supports_preset_modes()) { | ||||
|   if (fan.get_traits().supports_preset_modes()) { | ||||
|     // Use stored preset index to get preset name | ||||
|     const auto &preset_modes = traits.supported_preset_modes(); | ||||
|     const auto &preset_modes = fan.get_traits().supported_preset_modes(); | ||||
|     if (this->preset_mode < preset_modes.size()) { | ||||
|       fan.preset_mode = preset_modes[this->preset_mode]; | ||||
|       fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode); | ||||
|     } | ||||
|   } | ||||
|   fan.publish_state(); | ||||
| @@ -191,25 +182,18 @@ void Fan::save_state_() { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   auto traits = this->get_traits(); | ||||
|  | ||||
|   FanRestoreState state{}; | ||||
|   state.state = this->state; | ||||
|   state.oscillating = this->oscillating; | ||||
|   state.speed = this->speed; | ||||
|   state.direction = this->direction; | ||||
|  | ||||
|   if (traits.supports_preset_modes() && !this->preset_mode.empty()) { | ||||
|     const auto &preset_modes = traits.supported_preset_modes(); | ||||
|   if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { | ||||
|     const auto &preset_modes = this->get_traits().supported_preset_modes(); | ||||
|     // Store index of current preset mode | ||||
|     size_t i = 0; | ||||
|     for (const auto &mode : preset_modes) { | ||||
|       if (strcmp(mode, this->preset_mode.c_str()) == 0) { | ||||
|         state.preset_mode = i; | ||||
|         break; | ||||
|       } | ||||
|       i++; | ||||
|     } | ||||
|     auto preset_iterator = preset_modes.find(this->preset_mode); | ||||
|     if (preset_iterator != preset_modes.end()) | ||||
|       state.preset_mode = std::distance(preset_modes.begin(), preset_iterator); | ||||
|   } | ||||
|  | ||||
|   this->rtc_.save(&state); | ||||
| @@ -232,8 +216,8 @@ void Fan::dump_traits_(const char *tag, const char *prefix) { | ||||
|   } | ||||
|   if (traits.supports_preset_modes()) { | ||||
|     ESP_LOGCONFIG(tag, "%s  Supported presets:", prefix); | ||||
|     for (const char *s : traits.supported_preset_modes()) | ||||
|       ESP_LOGCONFIG(tag, "%s    - %s", prefix, s); | ||||
|     for (const std::string &s : traits.supported_preset_modes()) | ||||
|       ESP_LOGCONFIG(tag, "%s    - %s", prefix, s.c_str()); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -60,6 +60,8 @@ class FanCall { | ||||
|     this->speed_ = speed; | ||||
|     return *this; | ||||
|   } | ||||
|   ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9") | ||||
|   FanCall &set_speed(const char *legacy_speed); | ||||
|   optional<int> get_speed() const { return this->speed_; } | ||||
|   FanCall &set_direction(FanDirection direction) { | ||||
|     this->direction_ = direction; | ||||
|   | ||||
| @@ -1,10 +1,16 @@ | ||||
| #include <set> | ||||
| #include <utility> | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <vector> | ||||
| #include <initializer_list> | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| #ifdef USE_API | ||||
| namespace api { | ||||
| class APIConnection; | ||||
| }  // namespace api | ||||
| #endif | ||||
|  | ||||
| namespace fan { | ||||
|  | ||||
| class FanTraits { | ||||
| @@ -30,22 +36,27 @@ class FanTraits { | ||||
|   /// Set whether this fan supports changing direction | ||||
|   void set_direction(bool direction) { this->direction_ = direction; } | ||||
|   /// Return the preset modes supported by the fan. | ||||
|   const std::vector<const char *> &supported_preset_modes() const { return this->preset_modes_; } | ||||
|   /// Set the preset modes supported by the fan (from initializer list). | ||||
|   void set_supported_preset_modes(std::initializer_list<const char *> preset_modes) { | ||||
|     this->preset_modes_ = preset_modes; | ||||
|   } | ||||
|   /// Set the preset modes supported by the fan (from vector). | ||||
|   void set_supported_preset_modes(const std::vector<const char *> &preset_modes) { this->preset_modes_ = preset_modes; } | ||||
|   std::set<std::string> supported_preset_modes() const { return this->preset_modes_; } | ||||
|   /// Set the preset modes supported by the fan. | ||||
|   void set_supported_preset_modes(const std::set<std::string> &preset_modes) { this->preset_modes_ = preset_modes; } | ||||
|   /// Return if preset modes are supported | ||||
|   bool supports_preset_modes() const { return !this->preset_modes_.empty(); } | ||||
|  | ||||
|  protected: | ||||
| #ifdef USE_API | ||||
|   // The API connection is a friend class to access internal methods | ||||
|   friend class api::APIConnection; | ||||
|   // This method returns a reference to the internal preset modes set. | ||||
|   // It is used by the API to avoid copying data when encoding messages. | ||||
|   // Warning: Do not use this method outside of the API connection code. | ||||
|   // It returns a reference to internal data that can be invalidated. | ||||
|   const std::set<std::string> &supported_preset_modes_for_api_() const { return this->preset_modes_; } | ||||
| #endif | ||||
|   bool oscillation_{false}; | ||||
|   bool speed_{false}; | ||||
|   bool direction_{false}; | ||||
|   int speed_count_{}; | ||||
|   std::vector<const char *> preset_modes_{}; | ||||
|   std::set<std::string> preset_modes_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace fan | ||||
|   | ||||
| @@ -94,8 +94,6 @@ async def to_code(config): | ||||
|         ) | ||||
|         use_interrupt = False | ||||
|  | ||||
|     cg.add(var.set_use_interrupt(use_interrupt)) | ||||
|     if use_interrupt: | ||||
|         cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) | ||||
|     else: | ||||
|         # Only generate call when disabling interrupts (default is true) | ||||
|         cg.add(var.set_use_interrupt(use_interrupt)) | ||||
|   | ||||
| @@ -171,7 +171,7 @@ void HaierClimateBase::toggle_power() { | ||||
|       PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()}); | ||||
| } | ||||
|  | ||||
| void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask modes) { | ||||
| void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) { | ||||
|   this->traits_.set_supported_swing_modes(modes); | ||||
|   if (!modes.empty()) | ||||
|     this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); | ||||
| @@ -179,13 +179,13 @@ void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask m | ||||
|  | ||||
| void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); } | ||||
|  | ||||
| void HaierClimateBase::set_supported_modes(climate::ClimateModeMask modes) { | ||||
| void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) { | ||||
|   this->traits_.set_supported_modes(modes); | ||||
|   this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF);        // Always available | ||||
|   this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);  // Always available | ||||
| } | ||||
|  | ||||
| void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets) { | ||||
| void HaierClimateBase::set_supported_presets(const std::set<climate::ClimatePreset> &presets) { | ||||
|   this->traits_.set_supported_presets(presets); | ||||
|   if (!presets.empty()) | ||||
|     this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE); | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <chrono> | ||||
| #include <set> | ||||
| #include "esphome/components/climate/climate.h" | ||||
| #include "esphome/components/uart/uart.h" | ||||
| #include "esphome/core/automation.h" | ||||
| @@ -59,9 +60,9 @@ class HaierClimateBase : public esphome::Component, | ||||
|   void send_power_off_command(); | ||||
|   void toggle_power(); | ||||
|   void reset_protocol() { this->reset_protocol_request_ = true; }; | ||||
|   void set_supported_modes(esphome::climate::ClimateModeMask modes); | ||||
|   void set_supported_swing_modes(esphome::climate::ClimateSwingModeMask modes); | ||||
|   void set_supported_presets(esphome::climate::ClimatePresetMask presets); | ||||
|   void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes); | ||||
|   void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes); | ||||
|   void set_supported_presets(const std::set<esphome::climate::ClimatePreset> &presets); | ||||
|   bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; }; | ||||
|   size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; | ||||
|   size_t read_array(uint8_t *data, size_t len) noexcept override { | ||||
|   | ||||
| @@ -1033,9 +1033,9 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * | ||||
|   { | ||||
|     // Swing mode | ||||
|     ClimateSwingMode old_swing_mode = this->swing_mode; | ||||
|     const auto &swing_modes = traits_.get_supported_swing_modes(); | ||||
|     bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL); | ||||
|     bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL); | ||||
|     const std::set<ClimateSwingMode> &swing_modes = traits_.get_supported_swing_modes(); | ||||
|     bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end(); | ||||
|     bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end(); | ||||
|     if (horizontal_swing_supported && | ||||
|         (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) { | ||||
|       if (vertical_swing_supported && | ||||
| @@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() { | ||||
|                                                 (uint8_t) hon_protocol::DataParameters::QUIET_MODE, | ||||
|                                             quiet_mode_buf, 2); | ||||
|     } | ||||
|     if ((fast_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) { | ||||
|     if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) { | ||||
|       this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, | ||||
|                                             (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||
|                                                 (uint8_t) hon_protocol::DataParameters::FAST_MODE, | ||||
|                                             fast_mode_buf, 2); | ||||
|     } | ||||
|     if ((away_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) { | ||||
|     if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) { | ||||
|       this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL, | ||||
|                                             (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||
|                                                 (uint8_t) hon_protocol::DataParameters::TEN_DEGREE, | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <set> | ||||
|  | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/components/output/binary_output.h" | ||||
| #include "esphome/components/output/float_output.h" | ||||
| @@ -20,7 +22,7 @@ class HBridgeFan : public Component, public fan::Fan { | ||||
|   void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } | ||||
|   void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } | ||||
|   void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } | ||||
|   void set_preset_modes(std::initializer_list<const char *> presets) { preset_modes_ = presets; } | ||||
|   void set_preset_modes(const std::set<std::string> &presets) { preset_modes_ = presets; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
| @@ -36,7 +38,7 @@ class HBridgeFan : public Component, public fan::Fan { | ||||
|   int speed_count_{}; | ||||
|   DecayMode decay_mode_{DECAY_MODE_SLOW}; | ||||
|   fan::FanTraits traits_; | ||||
|   std::vector<const char *> preset_modes_{}; | ||||
|   std::set<std::string> preset_modes_{}; | ||||
|  | ||||
|   void control(const fan::FanCall &call) override; | ||||
|   void write_state_(); | ||||
|   | ||||
| @@ -1 +0,0 @@ | ||||
| CODEOWNERS = ["@optimusprimespace", "@ssieb"] | ||||
| @@ -1,111 +0,0 @@ | ||||
| #include "esphome/core/hal.h" | ||||
| #include "hdc2010.h" | ||||
| // https://github.com/vigsterkr/homebridge-hdc2010/blob/main/src/hdc2010.js | ||||
| // https://github.com/lime-labs/HDC2080-Arduino/blob/master/src/HDC2080.cpp | ||||
| namespace esphome { | ||||
| namespace hdc2010 { | ||||
|  | ||||
| static const char *const TAG = "hdc2010"; | ||||
|  | ||||
| static const uint8_t HDC2010_ADDRESS = 0x40;  // 0b1000000 or 0b1000001 from datasheet | ||||
| static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F; | ||||
| static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9; | ||||
| static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00; | ||||
| static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01; | ||||
| static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02; | ||||
| static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03; | ||||
| static const uint8_t CONFIG = 0x0E; | ||||
| static const uint8_t MEASUREMENT_CONFIG = 0x0F; | ||||
|  | ||||
| void HDC2010Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   const uint8_t data[2] = { | ||||
|       0b00000000,  // resolution 14bit for both humidity and temperature | ||||
|       0b00000000   // reserved | ||||
|   }; | ||||
|  | ||||
|   if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) { | ||||
|     ESP_LOGW(TAG, "Initial config instruction error"); | ||||
|     this->status_set_warning(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Set measurement mode to temperature and humidity | ||||
|   uint8_t config_contents; | ||||
|   this->read_register(MEASUREMENT_CONFIG, &config_contents, 1); | ||||
|   config_contents = (config_contents & 0xF9);  // Always set to TEMP_AND_HUMID mode | ||||
|   this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); | ||||
|  | ||||
|   // Set rate to manual | ||||
|   this->read_register(CONFIG, &config_contents, 1); | ||||
|   config_contents &= 0x8F; | ||||
|   this->write_bytes(CONFIG, &config_contents, 1); | ||||
|  | ||||
|   // Set temperature resolution to 14bit | ||||
|   this->read_register(CONFIG, &config_contents, 1); | ||||
|   config_contents &= 0x3F; | ||||
|   this->write_bytes(CONFIG, &config_contents, 1); | ||||
|  | ||||
|   // Set humidity resolution to 14bit | ||||
|   this->read_register(CONFIG, &config_contents, 1); | ||||
|   config_contents &= 0xCF; | ||||
|   this->write_bytes(CONFIG, &config_contents, 1); | ||||
| } | ||||
|  | ||||
| void HDC2010Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "HDC2010:"); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   if (this->is_failed()) { | ||||
|     ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); | ||||
|   } | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||
|   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_); | ||||
| } | ||||
|  | ||||
| void HDC2010Component::update() { | ||||
|   // Trigger measurement | ||||
|   uint8_t config_contents; | ||||
|   this->read_register(CONFIG, &config_contents, 1); | ||||
|   config_contents |= 0x01; | ||||
|   this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); | ||||
|  | ||||
|   // 1ms delay after triggering the sample | ||||
|   set_timeout(1, [this]() { | ||||
|     if (this->temperature_sensor_ != nullptr) { | ||||
|       float temp = this->read_temp(); | ||||
|       this->temperature_sensor_->publish_state(temp); | ||||
|       ESP_LOGD(TAG, "Temp=%.1f°C", temp); | ||||
|     } | ||||
|  | ||||
|     if (this->humidity_sensor_ != nullptr) { | ||||
|       float humidity = this->read_humidity(); | ||||
|       this->humidity_sensor_->publish_state(humidity); | ||||
|       ESP_LOGD(TAG, "Humidity=%.1f%%", humidity); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| float HDC2010Component::read_temp() { | ||||
|   uint8_t byte[2]; | ||||
|  | ||||
|   this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1); | ||||
|   this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1); | ||||
|  | ||||
|   uint16_t temp = encode_uint16(byte[1], byte[0]); | ||||
|   return (float) temp * 0.0025177f - 40.0f; | ||||
| } | ||||
|  | ||||
| float HDC2010Component::read_humidity() { | ||||
|   uint8_t byte[2]; | ||||
|  | ||||
|   this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1); | ||||
|   this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1); | ||||
|  | ||||
|   uint16_t humidity = encode_uint16(byte[1], byte[0]); | ||||
|   return (float) humidity * 0.001525879f; | ||||
| } | ||||
|  | ||||
| }  // namespace hdc2010 | ||||
| }  // namespace esphome | ||||
| @@ -1,32 +0,0 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace hdc2010 { | ||||
|  | ||||
| class HDC2010Component : public PollingComponent, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void set_temperature_sensor(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; } | ||||
|  | ||||
|   void set_humidity_sensor(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; } | ||||
|  | ||||
|   /// Setup the sensor and check for connection. | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   /// Retrieve the latest sensor values. This operation takes approximately 16ms. | ||||
|   void update() override; | ||||
|  | ||||
|   float read_temp(); | ||||
|  | ||||
|   float read_humidity(); | ||||
|  | ||||
|  protected: | ||||
|   sensor::Sensor *temperature_sensor_{nullptr}; | ||||
|   sensor::Sensor *humidity_sensor_{nullptr}; | ||||
| }; | ||||
|  | ||||
| }  // namespace hdc2010 | ||||
| }  // namespace esphome | ||||
| @@ -1,56 +0,0 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import i2c, sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_HUMIDITY, | ||||
|     CONF_ID, | ||||
|     CONF_TEMPERATURE, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_PERCENT, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["i2c"] | ||||
|  | ||||
| hdc2010_ns = cg.esphome_ns.namespace("hdc2010") | ||||
| HDC2010Component = hdc2010_ns.class_( | ||||
|     "HDC2010Component", cg.PollingComponent, i2c.I2CDevice | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(HDC2010Component), | ||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_HUMIDITY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(i2c.i2c_device_schema(0x40)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|  | ||||
|     if temperature_config := config.get(CONF_TEMPERATURE): | ||||
|         sens = await sensor.new_sensor(temperature_config) | ||||
|         cg.add(var.set_temperature_sensor(sens)) | ||||
|  | ||||
|     if humidity_config := config.get(CONF_HUMIDITY): | ||||
|         sens = await sensor.new_sensor(humidity_config) | ||||
|         cg.add(var.set_humidity_sensor(sens)) | ||||
| @@ -97,10 +97,11 @@ const float TEMP_MAX = 100;  // Celsius | ||||
| class HeatpumpIRClimate : public climate_ir::ClimateIR { | ||||
|  public: | ||||
|   HeatpumpIRClimate() | ||||
|       : climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true, | ||||
|                               {climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, | ||||
|                                climate::CLIMATE_FAN_AUTO}, | ||||
|                               {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, | ||||
|       : climate_ir::ClimateIR( | ||||
|             TEMP_MIN, TEMP_MAX, 1.0f, true, true, | ||||
|             std::set<climate::ClimateFanMode>{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, | ||||
|                                               climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_AUTO}, | ||||
|             std::set<climate::ClimateSwingMode>{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL, | ||||
|                                                 climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {} | ||||
|   void setup() override; | ||||
|   void set_protocol(Protocol protocol) { this->protocol_ = protocol; } | ||||
|   | ||||
| @@ -28,8 +28,8 @@ class HostGPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   bool inverted_{}; | ||||
|   gpio::Flags flags_{}; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
| }; | ||||
|  | ||||
| }  // namespace host | ||||
|   | ||||
| @@ -57,9 +57,6 @@ async def host_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(num)) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|     return var | ||||
|   | ||||
| @@ -12,6 +12,7 @@ from esphome.const import ( | ||||
|     CONF_ON_ERROR, | ||||
|     CONF_ON_RESPONSE, | ||||
|     CONF_TIMEOUT, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_URL, | ||||
|     CONF_WATCHDOG_TIMEOUT, | ||||
|     PLATFORM_HOST, | ||||
| @@ -215,8 +216,16 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema( | ||||
|             f"{CONF_VERIFY_SSL} has moved to the base component configuration." | ||||
|         ), | ||||
|         cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean, | ||||
|         cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(single=True), | ||||
|         cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), | ||||
|         cv.Optional(CONF_ON_RESPONSE): automation.validate_automation( | ||||
|             {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)} | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_ERROR): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||
|                     automation.Trigger.template() | ||||
|                 ) | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes, | ||||
|     } | ||||
| ) | ||||
| @@ -271,12 +280,7 @@ async def http_request_action_to_code(config, action_id, template_arg, args): | ||||
|     template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) | ||||
|     cg.add(var.set_url(template_)) | ||||
|     cg.add(var.set_method(config[CONF_METHOD])) | ||||
|  | ||||
|     capture_response = config[CONF_CAPTURE_RESPONSE] | ||||
|     if capture_response: | ||||
|         cg.add(var.set_capture_response(capture_response)) | ||||
|         cg.add_define("USE_HTTP_REQUEST_RESPONSE") | ||||
|  | ||||
|     cg.add(var.set_capture_response(config[CONF_CAPTURE_RESPONSE])) | ||||
|     cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE])) | ||||
|  | ||||
|     if CONF_BODY in config: | ||||
| @@ -299,26 +303,21 @@ async def http_request_action_to_code(config, action_id, template_arg, args): | ||||
|     for value in config.get(CONF_COLLECT_HEADERS, []): | ||||
|         cg.add(var.add_collect_header(value)) | ||||
|  | ||||
|     if response_conf := config.get(CONF_ON_RESPONSE): | ||||
|         if capture_response: | ||||
|     for conf in config.get(CONF_ON_RESPONSE, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) | ||||
|         cg.add(var.register_response_trigger(trigger)) | ||||
|         await automation.build_automation( | ||||
|                 var.get_success_trigger_with_response(), | ||||
|             trigger, | ||||
|             [ | ||||
|                 (cg.std_shared_ptr.template(HttpContainer), "response"), | ||||
|                 (cg.std_string_ref, "body"), | ||||
|                     *args, | ||||
|             ], | ||||
|                 response_conf, | ||||
|             conf, | ||||
|         ) | ||||
|         else: | ||||
|             await automation.build_automation( | ||||
|                 var.get_success_trigger(), | ||||
|                 [(cg.std_shared_ptr.template(HttpContainer), "response"), *args], | ||||
|                 response_conf, | ||||
|             ) | ||||
|  | ||||
|     if error_conf := config.get(CONF_ON_ERROR): | ||||
|         await automation.build_automation(var.get_error_trigger(), args, error_conf) | ||||
|     for conf in config.get(CONF_ON_ERROR, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) | ||||
|         cg.add(var.register_error_trigger(trigger)) | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
|  | ||||
|     return var | ||||
|  | ||||
|   | ||||
| @@ -124,7 +124,7 @@ class HttpRequestComponent : public Component { | ||||
|   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } | ||||
|  | ||||
|   void set_useragent(const char *useragent) { this->useragent_ = useragent; } | ||||
|   void set_timeout(uint32_t timeout) { this->timeout_ = timeout; } | ||||
|   void set_timeout(uint16_t timeout) { this->timeout_ = timeout; } | ||||
|   void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; } | ||||
|   uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; } | ||||
|   void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; } | ||||
| @@ -169,11 +169,11 @@ class HttpRequestComponent : public Component { | ||||
|  protected: | ||||
|   virtual std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, | ||||
|                                                  const std::string &body, const std::list<Header> &request_headers, | ||||
|                                                  const std::set<std::string> &collect_headers) = 0; | ||||
|                                                  std::set<std::string> collect_headers) = 0; | ||||
|   const char *useragent_{nullptr}; | ||||
|   bool follow_redirects_{}; | ||||
|   uint16_t redirect_limit_{}; | ||||
|   uint32_t timeout_{4500}; | ||||
|   uint16_t timeout_{4500}; | ||||
|   uint32_t watchdog_timeout_{0}; | ||||
| }; | ||||
|  | ||||
| @@ -183,9 +183,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { | ||||
|   TEMPLATABLE_VALUE(std::string, url) | ||||
|   TEMPLATABLE_VALUE(const char *, method) | ||||
|   TEMPLATABLE_VALUE(std::string, body) | ||||
| #ifdef USE_HTTP_REQUEST_RESPONSE | ||||
|   TEMPLATABLE_VALUE(bool, capture_response) | ||||
| #endif | ||||
|  | ||||
|   void add_request_header(const char *key, TemplatableValue<const char *, Ts...> value) { | ||||
|     this->request_headers_.insert({key, value}); | ||||
| @@ -197,14 +195,9 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { | ||||
|  | ||||
|   void set_json(std::function<void(Ts..., JsonObject)> json_func) { this->json_func_ = json_func; } | ||||
|  | ||||
| #ifdef USE_HTTP_REQUEST_RESPONSE | ||||
|   Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *get_success_trigger_with_response() const { | ||||
|     return this->success_trigger_with_response_; | ||||
|   } | ||||
| #endif | ||||
|   Trigger<std::shared_ptr<HttpContainer>, Ts...> *get_success_trigger() const { return this->success_trigger_; } | ||||
|   void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); } | ||||
|  | ||||
|   Trigger<Ts...> *get_error_trigger() const { return this->error_trigger_; } | ||||
|   void register_error_trigger(Trigger<> *trigger) { this->error_triggers_.push_back(trigger); } | ||||
|  | ||||
|   void set_max_response_buffer_size(size_t max_response_buffer_size) { | ||||
|     this->max_response_buffer_size_ = max_response_buffer_size; | ||||
| @@ -235,20 +228,17 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { | ||||
|     auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers, | ||||
|                                           this->collect_headers_); | ||||
|  | ||||
|     auto captured_args = std::make_tuple(x...); | ||||
|  | ||||
|     if (container == nullptr) { | ||||
|       std::apply([this](Ts... captured_args_inner) { this->error_trigger_->trigger(captured_args_inner...); }, | ||||
|                  captured_args); | ||||
|       for (auto *trigger : this->error_triggers_) | ||||
|         trigger->trigger(); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     size_t content_length = container->content_length; | ||||
|     size_t max_length = std::min(content_length, this->max_response_buffer_size_); | ||||
|  | ||||
| #ifdef USE_HTTP_REQUEST_RESPONSE | ||||
|     if (this->capture_response_.value(x...)) { | ||||
|     std::string response_body; | ||||
|     if (this->capture_response_.value(x...)) { | ||||
|       RAMAllocator<uint8_t> allocator; | ||||
|       uint8_t *buf = allocator.allocate(max_length); | ||||
|       if (buf != nullptr) { | ||||
| @@ -263,17 +253,19 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { | ||||
|         response_body.assign((char *) buf, read_index); | ||||
|         allocator.deallocate(buf, max_length); | ||||
|       } | ||||
|       std::apply( | ||||
|           [this, &container, &response_body](Ts... captured_args_inner) { | ||||
|             this->success_trigger_with_response_->trigger(container, response_body, captured_args_inner...); | ||||
|           }, | ||||
|           captured_args); | ||||
|     } else | ||||
| #endif | ||||
|     { | ||||
|       std::apply([this, &container]( | ||||
|                      Ts... captured_args_inner) { this->success_trigger_->trigger(container, captured_args_inner...); }, | ||||
|                  captured_args); | ||||
|     } | ||||
|  | ||||
|     if (this->response_triggers_.size() == 1) { | ||||
|       // if there is only one trigger, no need to copy the response body | ||||
|       this->response_triggers_[0]->process(container, response_body); | ||||
|     } else { | ||||
|       for (auto *trigger : this->response_triggers_) { | ||||
|         // with multiple triggers, pass a copy of the response body to each | ||||
|         // one so that modifications made in one trigger are not visible to | ||||
|         // the others | ||||
|         auto response_body_copy = std::string(response_body); | ||||
|         trigger->process(container, response_body_copy); | ||||
|       } | ||||
|     } | ||||
|     container->end(); | ||||
|   } | ||||
| @@ -291,13 +283,8 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { | ||||
|   std::set<std::string> collect_headers_{"content-type", "content-length"}; | ||||
|   std::map<const char *, TemplatableValue<std::string, Ts...>> json_{}; | ||||
|   std::function<void(Ts..., JsonObject)> json_func_{nullptr}; | ||||
| #ifdef USE_HTTP_REQUEST_RESPONSE | ||||
|   Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *success_trigger_with_response_ = | ||||
|       new Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...>(); | ||||
| #endif | ||||
|   Trigger<std::shared_ptr<HttpContainer>, Ts...> *success_trigger_ = | ||||
|       new Trigger<std::shared_ptr<HttpContainer>, Ts...>(); | ||||
|   Trigger<Ts...> *error_trigger_ = new Trigger<Ts...>(); | ||||
|   std::vector<HttpRequestResponseTrigger *> response_triggers_{}; | ||||
|   std::vector<Trigger<> *> error_triggers_{}; | ||||
|  | ||||
|   size_t max_response_buffer_size_{SIZE_MAX}; | ||||
| }; | ||||
|   | ||||
| @@ -17,7 +17,7 @@ static const char *const TAG = "http_request.arduino"; | ||||
| std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method, | ||||
|                                                            const std::string &body, | ||||
|                                                            const std::list<Header> &request_headers, | ||||
|                                                            const std::set<std::string> &collect_headers) { | ||||
|                                                            std::set<std::string> collect_headers) { | ||||
|   if (!network::is_connected()) { | ||||
|     this->status_momentary_error("failed", 1000); | ||||
|     ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); | ||||
|   | ||||
| @@ -33,7 +33,7 @@ class HttpRequestArduino : public HttpRequestComponent { | ||||
|  protected: | ||||
|   std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body, | ||||
|                                          const std::list<Header> &request_headers, | ||||
|                                          const std::set<std::string> &collect_headers) override; | ||||
|                                          std::set<std::string> collect_headers) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace http_request | ||||
|   | ||||
| @@ -20,7 +20,7 @@ static const char *const TAG = "http_request.host"; | ||||
| std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url, const std::string &method, | ||||
|                                                         const std::string &body, | ||||
|                                                         const std::list<Header> &request_headers, | ||||
|                                                         const std::set<std::string> &response_headers) { | ||||
|                                                         std::set<std::string> response_headers) { | ||||
|   if (!network::is_connected()) { | ||||
|     this->status_momentary_error("failed", 1000); | ||||
|     ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); | ||||
|   | ||||
| @@ -20,7 +20,7 @@ class HttpRequestHost : public HttpRequestComponent { | ||||
|  public: | ||||
|   std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body, | ||||
|                                          const std::list<Header> &request_headers, | ||||
|                                          const std::set<std::string> &response_headers) override; | ||||
|                                          std::set<std::string> response_headers) override; | ||||
|   void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; } | ||||
|  | ||||
|  protected: | ||||
|   | ||||
| @@ -55,7 +55,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { | ||||
| std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, const std::string &method, | ||||
|                                                        const std::string &body, | ||||
|                                                        const std::list<Header> &request_headers, | ||||
|                                                        const std::set<std::string> &collect_headers) { | ||||
|                                                        std::set<std::string> collect_headers) { | ||||
|   if (!network::is_connected()) { | ||||
|     this->status_momentary_error("failed", 1000); | ||||
|     ESP_LOGE(TAG, "HTTP Request failed; Not connected to network"); | ||||
|   | ||||
| @@ -39,7 +39,7 @@ class HttpRequestIDF : public HttpRequestComponent { | ||||
|  protected: | ||||
|   std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body, | ||||
|                                          const std::list<Header> &request_headers, | ||||
|                                          const std::set<std::string> &collect_headers) override; | ||||
|                                          std::set<std::string> collect_headers) override; | ||||
|   // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE | ||||
|   uint16_t buffer_size_rx_{}; | ||||
|   uint16_t buffer_size_tx_{}; | ||||
|   | ||||
| @@ -107,7 +107,7 @@ void IDFI2CBus::dump_config() { | ||||
|         if (s.second) { | ||||
|           ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first); | ||||
|         } else { | ||||
|           ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first); | ||||
|           ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -125,7 +125,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { | ||||
|  | ||||
|       case IMAGE_TYPE_RGB: | ||||
| #if LV_COLOR_DEPTH == 32 | ||||
|         switch (this->transparency_) { | ||||
|         switch (this->transparent_) { | ||||
|           case TRANSPARENCY_ALPHA_CHANNEL: | ||||
|             this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; | ||||
|             break; | ||||
| @@ -156,8 +156,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { | ||||
|             break; | ||||
|         } | ||||
| #else | ||||
|         this->dsc_.header.cf = | ||||
|             this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565; | ||||
|         this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565; | ||||
| #endif | ||||
|         break; | ||||
|     } | ||||
|   | ||||
| @@ -28,38 +28,6 @@ void ImprovSerialComponent::setup() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::loop() { | ||||
|   if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { | ||||
|     this->last_read_byte_ = 0; | ||||
|     this->rx_buffer_.clear(); | ||||
|     ESP_LOGV(TAG, "Timeout"); | ||||
|   } | ||||
|  | ||||
|   auto byte = this->read_byte_(); | ||||
|   while (byte.has_value()) { | ||||
|     if (this->parse_improv_serial_byte_(byte.value())) { | ||||
|       this->last_read_byte_ = millis(); | ||||
|     } else { | ||||
|       this->last_read_byte_ = 0; | ||||
|       this->rx_buffer_.clear(); | ||||
|     } | ||||
|     byte = this->read_byte_(); | ||||
|   } | ||||
|  | ||||
|   if (this->state_ == improv::STATE_PROVISIONING) { | ||||
|     if (wifi::global_wifi_component->is_connected()) { | ||||
|       wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), | ||||
|                                                  this->connecting_sta_.get_password()); | ||||
|       this->connecting_sta_ = {}; | ||||
|       this->cancel_timeout("wifi-connect-timeout"); | ||||
|       this->set_state_(improv::STATE_PROVISIONED); | ||||
|  | ||||
|       std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); | ||||
|       this->send_response_(url); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); } | ||||
|  | ||||
| optional<uint8_t> ImprovSerialComponent::read_byte_() { | ||||
| @@ -110,28 +78,8 @@ optional<uint8_t> ImprovSerialComponent::read_byte_() { | ||||
|   return byte; | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) { | ||||
|   // First, set length field | ||||
|   this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1; | ||||
|  | ||||
|   const bool there_is_data = data != nullptr && size > 0; | ||||
|   // If there_is_data, checksum must not include our optional data byte | ||||
|   const uint8_t header_checksum_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE - 2; | ||||
|   // Only transmit the full buffer length if there is no data (only state/error byte is provided in this case) | ||||
|   const uint8_t header_tx_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE; | ||||
|   // Calculate checksum for message | ||||
|   uint8_t checksum = 0; | ||||
|   for (uint8_t i = 0; i < header_checksum_len; i++) { | ||||
|     checksum += this->tx_header_[i]; | ||||
|   } | ||||
|   if (there_is_data) { | ||||
|     // Include data in checksum | ||||
|     for (size_t i = 0; i < size; i++) { | ||||
|       checksum += data[i]; | ||||
|     } | ||||
|   } | ||||
|   this->tx_header_[TX_CHECKSUM_IDX] = checksum; | ||||
|  | ||||
| void ImprovSerialComponent::write_data_(std::vector<uint8_t> &data) { | ||||
|   data.push_back('\n'); | ||||
| #ifdef USE_ESP32 | ||||
|   switch (logger::global_logger->get_uart()) { | ||||
|     case logger::UART_SELECTION_UART0: | ||||
| @@ -139,45 +87,63 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) | ||||
| #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ | ||||
|     !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) | ||||
|     case logger::UART_SELECTION_UART2: | ||||
| #endif | ||||
|       uart_write_bytes(this->uart_num_, this->tx_header_, header_tx_len); | ||||
|       if (there_is_data) { | ||||
|         uart_write_bytes(this->uart_num_, data, size); | ||||
|         uart_write_bytes(this->uart_num_, &this->tx_header_[TX_CHECKSUM_IDX], 2);  // Footer: checksum and newline | ||||
|       } | ||||
| #endif  // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 | ||||
|       uart_write_bytes(this->uart_num_, data.data(), data.size()); | ||||
|       break; | ||||
| #if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC) | ||||
|     case logger::UART_SELECTION_USB_CDC: | ||||
|       esp_usb_console_write_buf((const char *) this->tx_header_, header_tx_len); | ||||
|       if (there_is_data) { | ||||
|         esp_usb_console_write_buf((const char *) data, size); | ||||
|         esp_usb_console_write_buf((const char *) &this->tx_header_[TX_CHECKSUM_IDX], | ||||
|                                   2);  // Footer: checksum and newline | ||||
|       } | ||||
|     case logger::UART_SELECTION_USB_CDC: { | ||||
|       const char *msg = (char *) data.data(); | ||||
|       esp_usb_console_write_buf(msg, data.size()); | ||||
|       break; | ||||
| #endif | ||||
|     } | ||||
| #endif  // USE_LOGGER_USB_CDC | ||||
| #ifdef USE_LOGGER_USB_SERIAL_JTAG | ||||
|     case logger::UART_SELECTION_USB_SERIAL_JTAG: | ||||
|       usb_serial_jtag_write_bytes((const char *) this->tx_header_, header_tx_len, 20 / portTICK_PERIOD_MS); | ||||
|       if (there_is_data) { | ||||
|         usb_serial_jtag_write_bytes((const char *) data, size, 20 / portTICK_PERIOD_MS); | ||||
|         usb_serial_jtag_write_bytes((const char *) &this->tx_header_[TX_CHECKSUM_IDX], 2, | ||||
|                                     20 / portTICK_PERIOD_MS);  // Footer: checksum and newline | ||||
|       } | ||||
|       usb_serial_jtag_write_bytes((char *) data.data(), data.size(), 20 / portTICK_PERIOD_MS); | ||||
|       delay(10); | ||||
|       usb_serial_jtag_ll_txfifo_flush();  // fixes for issue in IDF 4.4.7 | ||||
|       break; | ||||
| #endif | ||||
| #endif  // USE_LOGGER_USB_SERIAL_JTAG | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| #elif defined(USE_ARDUINO) | ||||
|   this->hw_serial_->write(this->tx_header_, header_tx_len); | ||||
|   if (there_is_data) { | ||||
|     this->hw_serial_->write(data, size); | ||||
|     this->hw_serial_->write(&this->tx_header_[TX_CHECKSUM_IDX], 2);  // Footer: checksum and newline | ||||
|   } | ||||
|   this->hw_serial_->write(data.data(), data.size()); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::loop() { | ||||
|   if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { | ||||
|     this->last_read_byte_ = 0; | ||||
|     this->rx_buffer_.clear(); | ||||
|     ESP_LOGV(TAG, "Improv Serial timeout"); | ||||
|   } | ||||
|  | ||||
|   auto byte = this->read_byte_(); | ||||
|   while (byte.has_value()) { | ||||
|     if (this->parse_improv_serial_byte_(byte.value())) { | ||||
|       this->last_read_byte_ = millis(); | ||||
|     } else { | ||||
|       this->last_read_byte_ = 0; | ||||
|       this->rx_buffer_.clear(); | ||||
|     } | ||||
|     byte = this->read_byte_(); | ||||
|   } | ||||
|  | ||||
|   if (this->state_ == improv::STATE_PROVISIONING) { | ||||
|     if (wifi::global_wifi_component->is_connected()) { | ||||
|       wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), | ||||
|                                                  this->connecting_sta_.get_password()); | ||||
|       this->connecting_sta_ = {}; | ||||
|       this->cancel_timeout("wifi-connect-timeout"); | ||||
|       this->set_state_(improv::STATE_PROVISIONED); | ||||
|  | ||||
|       std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); | ||||
|       this->send_response_(url); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { | ||||
|   std::vector<std::string> urls; | ||||
| #ifdef USE_IMPROV_SERIAL_NEXT_URL | ||||
| @@ -211,13 +177,13 @@ std::vector<uint8_t> ImprovSerialComponent::build_version_info_() { | ||||
| bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) { | ||||
|   size_t at = this->rx_buffer_.size(); | ||||
|   this->rx_buffer_.push_back(byte); | ||||
|   ESP_LOGV(TAG, "Byte: 0x%02X", byte); | ||||
|   ESP_LOGV(TAG, "Improv Serial byte: 0x%02X", byte); | ||||
|   const uint8_t *raw = &this->rx_buffer_[0]; | ||||
|  | ||||
|   return improv::parse_improv_serial_byte( | ||||
|       at, byte, raw, [this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); }, | ||||
|       [this](improv::Error error) -> void { | ||||
|         ESP_LOGW(TAG, "Error decoding payload"); | ||||
|         ESP_LOGW(TAG, "Error decoding Improv payload"); | ||||
|         this->set_error_(error); | ||||
|       }); | ||||
| } | ||||
| @@ -233,7 +199,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command | ||||
|       wifi::global_wifi_component->set_sta(sta); | ||||
|       wifi::global_wifi_component->start_connecting(sta, false); | ||||
|       this->set_state_(improv::STATE_PROVISIONING); | ||||
|       ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), | ||||
|       ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), | ||||
|                command.password.c_str()); | ||||
|  | ||||
|       auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this); | ||||
| @@ -274,7 +240,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command | ||||
|       return true; | ||||
|     } | ||||
|     default: { | ||||
|       ESP_LOGW(TAG, "Unknown payload"); | ||||
|       ESP_LOGW(TAG, "Unknown Improv payload"); | ||||
|       this->set_error_(improv::ERROR_UNKNOWN_RPC); | ||||
|       return false; | ||||
|     } | ||||
| @@ -283,26 +249,57 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command | ||||
|  | ||||
| void ImprovSerialComponent::set_state_(improv::State state) { | ||||
|   this->state_ = state; | ||||
|   this->tx_header_[TX_TYPE_IDX] = TYPE_CURRENT_STATE; | ||||
|   this->tx_header_[TX_DATA_IDX] = state; | ||||
|   this->write_data_(); | ||||
|  | ||||
|   std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'}; | ||||
|   data.resize(11); | ||||
|   data[6] = IMPROV_SERIAL_VERSION; | ||||
|   data[7] = TYPE_CURRENT_STATE; | ||||
|   data[8] = 1; | ||||
|   data[9] = state; | ||||
|  | ||||
|   uint8_t checksum = 0x00; | ||||
|   for (uint8_t d : data) | ||||
|     checksum += d; | ||||
|   data[10] = checksum; | ||||
|  | ||||
|   this->write_data_(data); | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::set_error_(improv::Error error) { | ||||
|   this->tx_header_[TX_TYPE_IDX] = TYPE_ERROR_STATE; | ||||
|   this->tx_header_[TX_DATA_IDX] = error; | ||||
|   this->write_data_(); | ||||
|   std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'}; | ||||
|   data.resize(11); | ||||
|   data[6] = IMPROV_SERIAL_VERSION; | ||||
|   data[7] = TYPE_ERROR_STATE; | ||||
|   data[8] = 1; | ||||
|   data[9] = error; | ||||
|  | ||||
|   uint8_t checksum = 0x00; | ||||
|   for (uint8_t d : data) | ||||
|     checksum += d; | ||||
|   data[10] = checksum; | ||||
|   this->write_data_(data); | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::send_response_(std::vector<uint8_t> &response) { | ||||
|   this->tx_header_[TX_TYPE_IDX] = TYPE_RPC_RESPONSE; | ||||
|   this->write_data_(response.data(), response.size()); | ||||
|   std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'}; | ||||
|   data.resize(9); | ||||
|   data[6] = IMPROV_SERIAL_VERSION; | ||||
|   data[7] = TYPE_RPC_RESPONSE; | ||||
|   data[8] = response.size(); | ||||
|   data.insert(data.end(), response.begin(), response.end()); | ||||
|  | ||||
|   uint8_t checksum = 0x00; | ||||
|   for (uint8_t d : data) | ||||
|     checksum += d; | ||||
|   data.push_back(checksum); | ||||
|  | ||||
|   this->write_data_(data); | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::on_wifi_connect_timeout_() { | ||||
|   this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); | ||||
|   this->set_state_(improv::STATE_AUTHORIZED); | ||||
|   ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network"); | ||||
|   ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); | ||||
|   wifi::global_wifi_component->clear_sta(); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -26,16 +26,6 @@ | ||||
| namespace esphome { | ||||
| namespace improv_serial { | ||||
|  | ||||
| // TX buffer layout constants | ||||
| static constexpr uint8_t TX_HEADER_SIZE = 6;  // Bytes 0-5 = "IMPROV" | ||||
| static constexpr uint8_t TX_VERSION_IDX = 6; | ||||
| static constexpr uint8_t TX_TYPE_IDX = 7; | ||||
| static constexpr uint8_t TX_LENGTH_IDX = 8; | ||||
| static constexpr uint8_t TX_DATA_IDX = 9;  // For state/error messages only | ||||
| static constexpr uint8_t TX_CHECKSUM_IDX = 10; | ||||
| static constexpr uint8_t TX_NEWLINE_IDX = 11; | ||||
| static constexpr uint8_t TX_BUFFER_SIZE = 12; | ||||
|  | ||||
| enum ImprovSerialType : uint8_t { | ||||
|   TYPE_CURRENT_STATE = 0x01, | ||||
|   TYPE_ERROR_STATE = 0x02, | ||||
| @@ -67,22 +57,7 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase { | ||||
|   std::vector<uint8_t> build_version_info_(); | ||||
|  | ||||
|   optional<uint8_t> read_byte_(); | ||||
|   void write_data_(const uint8_t *data = nullptr, size_t size = 0); | ||||
|  | ||||
|   uint8_t tx_header_[TX_BUFFER_SIZE] = { | ||||
|       'I',                    // 0: Header | ||||
|       'M',                    // 1: Header | ||||
|       'P',                    // 2: Header | ||||
|       'R',                    // 3: Header | ||||
|       'O',                    // 4: Header | ||||
|       'V',                    // 5: Header | ||||
|       IMPROV_SERIAL_VERSION,  // 6: Version | ||||
|       0,                      // 7: ImprovSerialType | ||||
|       0,                      // 8: Length | ||||
|       0,                      // 9...X: Data (here, one byte reserved for state/error) | ||||
|       0,                      // X + 10: Checksum | ||||
|       '\n', | ||||
|   }; | ||||
|   void write_data_(std::vector<uint8_t> &data); | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   uart_port_t uart_num_; | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user