mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 09:01:49 +00:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			automation
			...
			20251001-t
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					820a897c1b | ||
| 
						 | 
					2523d83255 | ||
| 
						 | 
					aba0ad1210 | ||
| 
						 | 
					68d57b6bc0 | 
							
								
								
									
										111
									
								
								.github/workflows/ci-memory-impact-comment.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										111
									
								
								.github/workflows/ci-memory-impact-comment.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,111 +0,0 @@
 | 
			
		||||
---
 | 
			
		||||
name: Memory Impact Comment (Forks)
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  workflow_run:
 | 
			
		||||
    workflows: ["CI"]
 | 
			
		||||
    types: [completed]
 | 
			
		||||
 | 
			
		||||
permissions:
 | 
			
		||||
  contents: read
 | 
			
		||||
  pull-requests: write
 | 
			
		||||
  actions: read
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  memory-impact-comment:
 | 
			
		||||
    name: Post memory impact comment (fork PRs only)
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    # Only run for PRs from forks that had successful CI runs
 | 
			
		||||
    if: >
 | 
			
		||||
      github.event.workflow_run.event == 'pull_request' &&
 | 
			
		||||
      github.event.workflow_run.conclusion == 'success' &&
 | 
			
		||||
      github.event.workflow_run.head_repository.full_name != github.repository
 | 
			
		||||
    env:
 | 
			
		||||
      GH_TOKEN: ${{ github.token }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Get PR details
 | 
			
		||||
        id: pr
 | 
			
		||||
        run: |
 | 
			
		||||
          # Get PR details by searching for PR with matching head SHA
 | 
			
		||||
          # The workflow_run.pull_requests field is often empty for forks
 | 
			
		||||
          # Use paginate to handle repos with many open PRs
 | 
			
		||||
          head_sha="${{ github.event.workflow_run.head_sha }}"
 | 
			
		||||
          pr_data=$(gh api --paginate "/repos/${{ github.repository }}/pulls" \
 | 
			
		||||
            --jq ".[] | select(.head.sha == \"$head_sha\") | {number: .number, base_ref: .base.ref}" \
 | 
			
		||||
            | head -n 1)
 | 
			
		||||
 | 
			
		||||
          if [ -z "$pr_data" ]; then
 | 
			
		||||
            echo "No PR found for SHA $head_sha, skipping"
 | 
			
		||||
            echo "skip=true" >> "$GITHUB_OUTPUT"
 | 
			
		||||
            exit 0
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          pr_number=$(echo "$pr_data" | jq -r '.number')
 | 
			
		||||
          base_ref=$(echo "$pr_data" | jq -r '.base_ref')
 | 
			
		||||
 | 
			
		||||
          echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
 | 
			
		||||
          echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT"
 | 
			
		||||
          echo "Found PR #$pr_number targeting base branch: $base_ref"
 | 
			
		||||
 | 
			
		||||
      - name: Check out code from base repository
 | 
			
		||||
        if: steps.pr.outputs.skip != 'true'
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          # Always check out from the base repository (esphome/esphome), never from forks
 | 
			
		||||
          # Use the PR's target branch to ensure we run trusted code from the main repo
 | 
			
		||||
          repository: ${{ github.repository }}
 | 
			
		||||
          ref: ${{ steps.pr.outputs.base_ref }}
 | 
			
		||||
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        if: steps.pr.outputs.skip != 'true'
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.11"
 | 
			
		||||
          cache-key: ${{ hashFiles('.cache-key') }}
 | 
			
		||||
 | 
			
		||||
      - name: Download memory analysis artifacts
 | 
			
		||||
        if: steps.pr.outputs.skip != 'true'
 | 
			
		||||
        run: |
 | 
			
		||||
          run_id="${{ github.event.workflow_run.id }}"
 | 
			
		||||
          echo "Downloading artifacts from workflow run $run_id"
 | 
			
		||||
 | 
			
		||||
          mkdir -p memory-analysis
 | 
			
		||||
 | 
			
		||||
          # Download target analysis artifact
 | 
			
		||||
          if gh run download --name "memory-analysis-target" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then
 | 
			
		||||
            echo "Downloaded memory-analysis-target artifact."
 | 
			
		||||
          else
 | 
			
		||||
            echo "No memory-analysis-target artifact found."
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          # Download PR analysis artifact
 | 
			
		||||
          if gh run download --name "memory-analysis-pr" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then
 | 
			
		||||
            echo "Downloaded memory-analysis-pr artifact."
 | 
			
		||||
          else
 | 
			
		||||
            echo "No memory-analysis-pr artifact found."
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Check if artifacts exist
 | 
			
		||||
        id: check
 | 
			
		||||
        if: steps.pr.outputs.skip != 'true'
 | 
			
		||||
        run: |
 | 
			
		||||
          if [ -f ./memory-analysis/memory-analysis-target.json ] && [ -f ./memory-analysis/memory-analysis-pr.json ]; then
 | 
			
		||||
            echo "found=true" >> "$GITHUB_OUTPUT"
 | 
			
		||||
          else
 | 
			
		||||
            echo "found=false" >> "$GITHUB_OUTPUT"
 | 
			
		||||
            echo "Memory analysis artifacts not found, skipping comment"
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Post or update PR comment
 | 
			
		||||
        if: steps.pr.outputs.skip != 'true' && steps.check.outputs.found == 'true'
 | 
			
		||||
        env:
 | 
			
		||||
          PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          # Pass PR number and JSON file paths directly to Python script
 | 
			
		||||
          # Let Python parse the JSON to avoid shell injection risks
 | 
			
		||||
          # The script will validate and sanitize all inputs
 | 
			
		||||
          python script/ci_memory_impact_comment.py \
 | 
			
		||||
            --pr-number "$PR_NUMBER" \
 | 
			
		||||
            --target-json ./memory-analysis/memory-analysis-target.json \
 | 
			
		||||
            --pr-json ./memory-analysis/memory-analysis-pr.json
 | 
			
		||||
							
								
								
									
										255
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										255
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -170,13 +170,11 @@ jobs:
 | 
			
		||||
    outputs:
 | 
			
		||||
      integration-tests: ${{ steps.determine.outputs.integration-tests }}
 | 
			
		||||
      clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
 | 
			
		||||
      clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
 | 
			
		||||
      python-linters: ${{ steps.determine.outputs.python-linters }}
 | 
			
		||||
      changed-components: ${{ steps.determine.outputs.changed-components }}
 | 
			
		||||
      changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
 | 
			
		||||
      directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
 | 
			
		||||
      component-test-count: ${{ steps.determine.outputs.component-test-count }}
 | 
			
		||||
      changed-cpp-file-count: ${{ steps.determine.outputs.changed-cpp-file-count }}
 | 
			
		||||
      memory_impact: ${{ steps.determine.outputs.memory-impact }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
@@ -202,13 +200,11 @@ jobs:
 | 
			
		||||
          # Extract individual fields
 | 
			
		||||
          echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "changed-cpp-file-count=$(echo "$output" | jq -r '.changed_cpp_file_count')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
  integration-tests:
 | 
			
		||||
@@ -247,7 +243,7 @@ jobs:
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          pytest -vv --no-cov --tb=native -n auto tests/integration/
 | 
			
		||||
 | 
			
		||||
  clang-tidy-single:
 | 
			
		||||
  clang-tidy:
 | 
			
		||||
    name: ${{ matrix.name }}
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
@@ -265,6 +261,22 @@ jobs:
 | 
			
		||||
            name: Run script/clang-tidy for ESP8266
 | 
			
		||||
            options: --environment esp8266-arduino-tidy --grep USE_ESP8266
 | 
			
		||||
            pio_cache_key: tidyesp8266
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 1/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
 | 
			
		||||
            pio_cache_key: tidyesp32
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 2/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
 | 
			
		||||
            pio_cache_key: tidyesp32
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 3/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
 | 
			
		||||
            pio_cache_key: tidyesp32
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 4/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
 | 
			
		||||
            pio_cache_key: tidyesp32
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 IDF
 | 
			
		||||
            options: --environment esp32-idf-tidy --grep USE_ESP_IDF
 | 
			
		||||
@@ -345,166 +357,6 @@ jobs:
 | 
			
		||||
        # yamllint disable-line rule:line-length
 | 
			
		||||
        if: always()
 | 
			
		||||
 | 
			
		||||
  clang-tidy-nosplit:
 | 
			
		||||
    name: Run script/clang-tidy for ESP32 Arduino
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
    if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit'
 | 
			
		||||
    env:
 | 
			
		||||
      GH_TOKEN: ${{ github.token }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          # Need history for HEAD~1 to work for checking changed files
 | 
			
		||||
          fetch-depth: 2
 | 
			
		||||
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref == 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref != 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/gcc.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
 | 
			
		||||
 | 
			
		||||
      - name: Check if full clang-tidy scan needed
 | 
			
		||||
        id: check_full_scan
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          if python script/clang_tidy_hash.py --check; then
 | 
			
		||||
            echo "full_scan=true" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "reason=hash_changed" >> $GITHUB_OUTPUT
 | 
			
		||||
          else
 | 
			
		||||
            echo "full_scan=false" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "reason=normal" >> $GITHUB_OUTPUT
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Run clang-tidy
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
 | 
			
		||||
            echo "Running FULL clang-tidy scan (hash changed)"
 | 
			
		||||
            script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy
 | 
			
		||||
          else
 | 
			
		||||
            echo "Running clang-tidy on changed files only"
 | 
			
		||||
            script/clang-tidy --all-headers --fix --changed --environment esp32-arduino-tidy
 | 
			
		||||
          fi
 | 
			
		||||
        env:
 | 
			
		||||
          # Also cache libdeps, store them in a ~/.platformio subfolder
 | 
			
		||||
          PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
 | 
			
		||||
 | 
			
		||||
      - name: Suggested changes
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
        if: always()
 | 
			
		||||
 | 
			
		||||
  clang-tidy-split:
 | 
			
		||||
    name: ${{ matrix.name }}
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
    if: needs.determine-jobs.outputs.clang-tidy-mode == 'split'
 | 
			
		||||
    env:
 | 
			
		||||
      GH_TOKEN: ${{ github.token }}
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      max-parallel: 1
 | 
			
		||||
      matrix:
 | 
			
		||||
        include:
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 1/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 2/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 3/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 4/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          # Need history for HEAD~1 to work for checking changed files
 | 
			
		||||
          fetch-depth: 2
 | 
			
		||||
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref == 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref != 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/gcc.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
 | 
			
		||||
 | 
			
		||||
      - name: Check if full clang-tidy scan needed
 | 
			
		||||
        id: check_full_scan
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          if python script/clang_tidy_hash.py --check; then
 | 
			
		||||
            echo "full_scan=true" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "reason=hash_changed" >> $GITHUB_OUTPUT
 | 
			
		||||
          else
 | 
			
		||||
            echo "full_scan=false" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "reason=normal" >> $GITHUB_OUTPUT
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Run clang-tidy
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
 | 
			
		||||
            echo "Running FULL clang-tidy scan (hash changed)"
 | 
			
		||||
            script/clang-tidy --all-headers --fix ${{ matrix.options }}
 | 
			
		||||
          else
 | 
			
		||||
            echo "Running clang-tidy on changed files only"
 | 
			
		||||
            script/clang-tidy --all-headers --fix --changed ${{ matrix.options }}
 | 
			
		||||
          fi
 | 
			
		||||
        env:
 | 
			
		||||
          # Also cache libdeps, store them in a ~/.platformio subfolder
 | 
			
		||||
          PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
 | 
			
		||||
 | 
			
		||||
      - name: Suggested changes
 | 
			
		||||
        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
 | 
			
		||||
@@ -789,12 +641,6 @@ jobs:
 | 
			
		||||
              --output-env \
 | 
			
		||||
              --output-json memory-analysis-target.json
 | 
			
		||||
 | 
			
		||||
          # Add metadata to JSON before caching
 | 
			
		||||
          python script/ci_add_metadata_to_json.py \
 | 
			
		||||
            --json-file memory-analysis-target.json \
 | 
			
		||||
            --components "$components" \
 | 
			
		||||
            --platform "$platform"
 | 
			
		||||
 | 
			
		||||
      - name: Save memory analysis to cache
 | 
			
		||||
        if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
 | 
			
		||||
        uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
@@ -874,13 +720,6 @@ jobs:
 | 
			
		||||
            python script/ci_memory_impact_extract.py \
 | 
			
		||||
              --output-env \
 | 
			
		||||
              --output-json memory-analysis-pr.json
 | 
			
		||||
 | 
			
		||||
          # Add metadata to JSON (components and platform are in shell variables above)
 | 
			
		||||
          python script/ci_add_metadata_to_json.py \
 | 
			
		||||
            --json-file memory-analysis-pr.json \
 | 
			
		||||
            --components "$components" \
 | 
			
		||||
            --platform "$platform"
 | 
			
		||||
 | 
			
		||||
      - name: Upload memory analysis JSON
 | 
			
		||||
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
 | 
			
		||||
        with:
 | 
			
		||||
@@ -897,12 +736,10 @@ jobs:
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
      - memory-impact-target-branch
 | 
			
		||||
      - memory-impact-pr-branch
 | 
			
		||||
    if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true'
 | 
			
		||||
    if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true'
 | 
			
		||||
    permissions:
 | 
			
		||||
      contents: read
 | 
			
		||||
      pull-requests: write
 | 
			
		||||
    env:
 | 
			
		||||
      GH_TOKEN: ${{ github.token }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
@@ -912,29 +749,65 @@ jobs:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Download target analysis JSON
 | 
			
		||||
        uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
 | 
			
		||||
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          name: memory-analysis-target
 | 
			
		||||
          path: ./memory-analysis
 | 
			
		||||
        continue-on-error: true
 | 
			
		||||
      - name: Download PR analysis JSON
 | 
			
		||||
        uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
 | 
			
		||||
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          name: memory-analysis-pr
 | 
			
		||||
          path: ./memory-analysis
 | 
			
		||||
        continue-on-error: true
 | 
			
		||||
      - name: Post or update PR comment
 | 
			
		||||
        env:
 | 
			
		||||
          PR_NUMBER: ${{ github.event.pull_request.number }}
 | 
			
		||||
          GH_TOKEN: ${{ github.token }}
 | 
			
		||||
          COMPONENTS: ${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}
 | 
			
		||||
          PLATFORM: ${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}
 | 
			
		||||
          TARGET_RAM: ${{ needs.memory-impact-target-branch.outputs.ram_usage }}
 | 
			
		||||
          TARGET_FLASH: ${{ needs.memory-impact-target-branch.outputs.flash_usage }}
 | 
			
		||||
          PR_RAM: ${{ needs.memory-impact-pr-branch.outputs.ram_usage }}
 | 
			
		||||
          PR_FLASH: ${{ needs.memory-impact-pr-branch.outputs.flash_usage }}
 | 
			
		||||
          TARGET_CACHE_HIT: ${{ needs.memory-impact-target-branch.outputs.cache_hit }}
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
 | 
			
		||||
          # Pass JSON file paths directly to Python script
 | 
			
		||||
          # All data is extracted from JSON files for security
 | 
			
		||||
          # Check if analysis JSON files exist
 | 
			
		||||
          target_json_arg=""
 | 
			
		||||
          pr_json_arg=""
 | 
			
		||||
 | 
			
		||||
          if [ -f ./memory-analysis/memory-analysis-target.json ]; then
 | 
			
		||||
            echo "Found target analysis JSON"
 | 
			
		||||
            target_json_arg="--target-json ./memory-analysis/memory-analysis-target.json"
 | 
			
		||||
          else
 | 
			
		||||
            echo "No target analysis JSON found"
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          if [ -f ./memory-analysis/memory-analysis-pr.json ]; then
 | 
			
		||||
            echo "Found PR analysis JSON"
 | 
			
		||||
            pr_json_arg="--pr-json ./memory-analysis/memory-analysis-pr.json"
 | 
			
		||||
          else
 | 
			
		||||
            echo "No PR analysis JSON found"
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          # Add cache flag if target was cached
 | 
			
		||||
          cache_flag=""
 | 
			
		||||
          if [ "$TARGET_CACHE_HIT" == "true" ]; then
 | 
			
		||||
            cache_flag="--target-cache-hit"
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          python script/ci_memory_impact_comment.py \
 | 
			
		||||
            --pr-number "$PR_NUMBER" \
 | 
			
		||||
            --target-json ./memory-analysis/memory-analysis-target.json \
 | 
			
		||||
            --pr-json ./memory-analysis/memory-analysis-pr.json
 | 
			
		||||
            --pr-number "${{ github.event.pull_request.number }}" \
 | 
			
		||||
            --components "$COMPONENTS" \
 | 
			
		||||
            --platform "$PLATFORM" \
 | 
			
		||||
            --target-ram "$TARGET_RAM" \
 | 
			
		||||
            --target-flash "$TARGET_FLASH" \
 | 
			
		||||
            --pr-ram "$PR_RAM" \
 | 
			
		||||
            --pr-flash "$PR_FLASH" \
 | 
			
		||||
            $target_json_arg \
 | 
			
		||||
            $pr_json_arg \
 | 
			
		||||
            $cache_flag
 | 
			
		||||
 | 
			
		||||
  ci-status:
 | 
			
		||||
    name: CI Status
 | 
			
		||||
@@ -945,9 +818,7 @@ jobs:
 | 
			
		||||
      - pylint
 | 
			
		||||
      - pytest
 | 
			
		||||
      - integration-tests
 | 
			
		||||
      - clang-tidy-single
 | 
			
		||||
      - clang-tidy-nosplit
 | 
			
		||||
      - clang-tidy-split
 | 
			
		||||
      - clang-tidy
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
      - test-build-components-splitter
 | 
			
		||||
      - test-build-components-split
 | 
			
		||||
 
 | 
			
		||||
@@ -70,7 +70,6 @@ esphome/components/bl0939/* @ziceva
 | 
			
		||||
esphome/components/bl0940/* @dan-s-github @tobias-
 | 
			
		||||
esphome/components/bl0942/* @dbuezas @dwmw2
 | 
			
		||||
esphome/components/ble_client/* @buxtronix @clydebarrow
 | 
			
		||||
esphome/components/ble_nus/* @tomaszduda23
 | 
			
		||||
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
 | 
			
		||||
esphome/components/bme280_base/* @esphome/core
 | 
			
		||||
esphome/components/bme280_spi/* @apbodrov
 | 
			
		||||
@@ -161,7 +160,6 @@ esphome/components/esp32_rmt_led_strip/* @jesserockz
 | 
			
		||||
esphome/components/esp8266/* @esphome/core
 | 
			
		||||
esphome/components/esp_ldo/* @clydebarrow
 | 
			
		||||
esphome/components/espnow/* @jesserockz
 | 
			
		||||
esphome/components/espnow/packet_transport/* @EasilyBoredEngineer
 | 
			
		||||
esphome/components/ethernet_info/* @gtjadsonsantos
 | 
			
		||||
esphome/components/event/* @nohat
 | 
			
		||||
esphome/components/exposure_notifications/* @OttoWinter
 | 
			
		||||
 
 | 
			
		||||
@@ -62,40 +62,6 @@ from esphome.util import (
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
# Special non-component keys that appear in configs
 | 
			
		||||
_NON_COMPONENT_KEYS = frozenset(
 | 
			
		||||
    {
 | 
			
		||||
        CONF_ESPHOME,
 | 
			
		||||
        "substitutions",
 | 
			
		||||
        "packages",
 | 
			
		||||
        "globals",
 | 
			
		||||
        "external_components",
 | 
			
		||||
        "<<",
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def detect_external_components(config: ConfigType) -> set[str]:
 | 
			
		||||
    """Detect external/custom components in the configuration.
 | 
			
		||||
 | 
			
		||||
    External components are those that appear in the config but are not
 | 
			
		||||
    part of ESPHome's built-in components and are not special config keys.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        config: The ESPHome configuration dictionary
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        A set of external component names
 | 
			
		||||
    """
 | 
			
		||||
    from esphome.analyze_memory.helpers import get_esphome_components
 | 
			
		||||
 | 
			
		||||
    builtin_components = get_esphome_components()
 | 
			
		||||
    return {
 | 
			
		||||
        key
 | 
			
		||||
        for key in config
 | 
			
		||||
        if key not in builtin_components and key not in _NON_COMPONENT_KEYS
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ArgsProtocol(Protocol):
 | 
			
		||||
    device: list[str] | None
 | 
			
		||||
@@ -219,9 +185,7 @@ def choose_upload_log_host(
 | 
			
		||||
            else:
 | 
			
		||||
                resolved.append(device)
 | 
			
		||||
        if not resolved:
 | 
			
		||||
            raise EsphomeError(
 | 
			
		||||
                f"All specified devices {defaults} could not be resolved. Is the device connected to the network?"
 | 
			
		||||
            )
 | 
			
		||||
            _LOGGER.error("All specified devices: %s could not be resolved.", defaults)
 | 
			
		||||
        return resolved
 | 
			
		||||
 | 
			
		||||
    # No devices specified, show interactive chooser
 | 
			
		||||
@@ -731,13 +695,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
 | 
			
		||||
@@ -933,54 +890,6 @@ def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
 | 
			
		||||
    """Analyze memory usage by component.
 | 
			
		||||
 | 
			
		||||
    This command compiles the configuration and performs memory analysis.
 | 
			
		||||
    Compilation is fast if sources haven't changed (just relinking).
 | 
			
		||||
    """
 | 
			
		||||
    from esphome import platformio_api
 | 
			
		||||
    from esphome.analyze_memory.cli import MemoryAnalyzerCLI
 | 
			
		||||
 | 
			
		||||
    # Always compile to ensure fresh data (fast if no changes - just relinks)
 | 
			
		||||
    exit_code = write_cpp(config)
 | 
			
		||||
    if exit_code != 0:
 | 
			
		||||
        return exit_code
 | 
			
		||||
    exit_code = compile_program(args, config)
 | 
			
		||||
    if exit_code != 0:
 | 
			
		||||
        return exit_code
 | 
			
		||||
    _LOGGER.info("Successfully compiled program.")
 | 
			
		||||
 | 
			
		||||
    # Get idedata for analysis
 | 
			
		||||
    idedata = platformio_api.get_idedata(config)
 | 
			
		||||
    if idedata is None:
 | 
			
		||||
        _LOGGER.error("Failed to get IDE data for memory analysis")
 | 
			
		||||
        return 1
 | 
			
		||||
 | 
			
		||||
    firmware_elf = Path(idedata.firmware_elf_path)
 | 
			
		||||
 | 
			
		||||
    # Extract external components from config
 | 
			
		||||
    external_components = detect_external_components(config)
 | 
			
		||||
    _LOGGER.debug("Detected external components: %s", external_components)
 | 
			
		||||
 | 
			
		||||
    # Perform memory analysis
 | 
			
		||||
    _LOGGER.info("Analyzing memory usage...")
 | 
			
		||||
    analyzer = MemoryAnalyzerCLI(
 | 
			
		||||
        str(firmware_elf),
 | 
			
		||||
        idedata.objdump_path,
 | 
			
		||||
        idedata.readelf_path,
 | 
			
		||||
        external_components,
 | 
			
		||||
    )
 | 
			
		||||
    analyzer.analyze()
 | 
			
		||||
 | 
			
		||||
    # Generate and display report
 | 
			
		||||
    report = analyzer.generate_report()
 | 
			
		||||
    print()
 | 
			
		||||
    print(report)
 | 
			
		||||
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    new_name = args.name
 | 
			
		||||
    for c in new_name:
 | 
			
		||||
@@ -1096,7 +1005,6 @@ POST_CONFIG_ACTIONS = {
 | 
			
		||||
    "idedata": command_idedata,
 | 
			
		||||
    "rename": command_rename,
 | 
			
		||||
    "discover": command_discover,
 | 
			
		||||
    "analyze-memory": command_analyze_memory,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SIMPLE_CONFIG_ACTIONS = [
 | 
			
		||||
@@ -1199,17 +1107,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",
 | 
			
		||||
@@ -1393,14 +1290,6 @@ def parse_args(argv):
 | 
			
		||||
    )
 | 
			
		||||
    parser_rename.add_argument("name", help="The new name for the device.", type=str)
 | 
			
		||||
 | 
			
		||||
    parser_analyze_memory = subparsers.add_parser(
 | 
			
		||||
        "analyze-memory",
 | 
			
		||||
        help="Analyze memory usage by component.",
 | 
			
		||||
    )
 | 
			
		||||
    parser_analyze_memory.add_argument(
 | 
			
		||||
        "configuration", help="Your YAML configuration file(s).", nargs="+"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Keep backward compatibility with the old command line format of
 | 
			
		||||
    # esphome <config> <command>.
 | 
			
		||||
    #
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
"""CLI interface for memory analysis with report generation."""
 | 
			
		||||
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
import json
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
from . import (
 | 
			
		||||
@@ -271,28 +270,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
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  climate::ClimateTraits traits() override {
 | 
			
		||||
    auto traits = climate::ClimateTraits();
 | 
			
		||||
    traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
 | 
			
		||||
    traits.set_supports_current_temperature(true);
 | 
			
		||||
    traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::ClimateMode::CLIMATE_MODE_HEAT});
 | 
			
		||||
    traits.set_visual_min_temperature(25.0);
 | 
			
		||||
    traits.set_visual_max_temperature(100.0);
 | 
			
		||||
 
 | 
			
		||||
@@ -155,17 +155,6 @@ def _validate_api_config(config: ConfigType) -> ConfigType:
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _consume_api_sockets(config: ConfigType) -> ConfigType:
 | 
			
		||||
    """Register socket needs for API component."""
 | 
			
		||||
    from esphome.components import socket
 | 
			
		||||
 | 
			
		||||
    # API needs 1 listening socket + typically 3 concurrent client connections
 | 
			
		||||
    # (not max_connections, which is the upper limit rarely reached)
 | 
			
		||||
    sockets_needed = 1 + 3
 | 
			
		||||
    socket.consume_sockets(sockets_needed, "api")(config)
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
@@ -233,7 +222,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
 | 
			
		||||
    _validate_api_config,
 | 
			
		||||
    _consume_api_sockets,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -506,7 +506,7 @@ message ListEntitiesLightResponse {
 | 
			
		||||
  string name = 3;
 | 
			
		||||
  reserved 4; // Deprecated: was string unique_id
 | 
			
		||||
 | 
			
		||||
  repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"];
 | 
			
		||||
  repeated ColorMode supported_color_modes = 12 [(container_pointer) = "std::set<light::ColorMode>"];
 | 
			
		||||
  // next four supports_* are for legacy clients, newer clients should use color modes
 | 
			
		||||
  // Deprecated in API version 1.6
 | 
			
		||||
  bool legacy_supports_brightness = 5 [deprecated=true];
 | 
			
		||||
 
 | 
			
		||||
@@ -453,6 +453,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
 | 
			
		||||
                                             bool is_single) {
 | 
			
		||||
  auto *light = static_cast<light::LightState *>(entity);
 | 
			
		||||
  LightStateResponse resp;
 | 
			
		||||
  auto traits = light->get_traits();
 | 
			
		||||
  auto values = light->remote_values;
 | 
			
		||||
  auto color_mode = values.get_color_mode();
 | 
			
		||||
  resp.state = values.is_on();
 | 
			
		||||
@@ -476,8 +477,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
 | 
			
		||||
  auto *light = static_cast<light::LightState *>(entity);
 | 
			
		||||
  ListEntitiesLightResponse msg;
 | 
			
		||||
  auto traits = light->get_traits();
 | 
			
		||||
  // Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
 | 
			
		||||
  msg.supported_color_modes = &traits.get_supported_color_modes();
 | 
			
		||||
  msg.supported_color_modes = &traits.get_supported_color_modes_for_api_();
 | 
			
		||||
  if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
 | 
			
		||||
      traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
 | 
			
		||||
    msg.min_mireds = traits.get_min_mireds();
 | 
			
		||||
@@ -661,12 +661,11 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
 | 
			
		||||
  ListEntitiesClimateResponse msg;
 | 
			
		||||
  auto traits = climate->get_traits();
 | 
			
		||||
  // Flags set for backward compatibility, deprecated in 2025.11.0
 | 
			
		||||
  msg.supports_current_temperature = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
 | 
			
		||||
  msg.supports_current_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
 | 
			
		||||
  msg.supports_two_point_target_temperature = traits.has_feature_flags(
 | 
			
		||||
      climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
 | 
			
		||||
  msg.supports_target_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
 | 
			
		||||
  msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
 | 
			
		||||
  msg.supports_current_temperature = traits.get_supports_current_temperature();
 | 
			
		||||
  msg.supports_current_humidity = traits.get_supports_current_humidity();
 | 
			
		||||
  msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
 | 
			
		||||
  msg.supports_target_humidity = traits.get_supports_target_humidity();
 | 
			
		||||
  msg.supports_action = traits.get_supports_action();
 | 
			
		||||
  // Current feature flags and other supported parameters
 | 
			
		||||
  msg.feature_flags = traits.get_feature_flags();
 | 
			
		||||
  msg.supported_modes = &traits.get_supported_modes_for_api_();
 | 
			
		||||
@@ -1082,8 +1081,13 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
 | 
			
		||||
    homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
 | 
			
		||||
#ifdef USE_TIME_TIMEZONE
 | 
			
		||||
    if (value.timezone_len > 0) {
 | 
			
		||||
      homeassistant::global_homeassistant_time->set_timezone(reinterpret_cast<const char *>(value.timezone),
 | 
			
		||||
                                                             value.timezone_len);
 | 
			
		||||
      const std::string ¤t_tz = homeassistant::global_homeassistant_time->get_timezone();
 | 
			
		||||
      // Compare without allocating a string
 | 
			
		||||
      if (current_tz.length() != value.timezone_len ||
 | 
			
		||||
          memcmp(current_tz.c_str(), value.timezone, value.timezone_len) != 0) {
 | 
			
		||||
        homeassistant::global_homeassistant_time->set_timezone(
 | 
			
		||||
            std::string(reinterpret_cast<const char *>(value.timezone), value.timezone_len));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -70,14 +70,4 @@ extend google.protobuf.FieldOptions {
 | 
			
		||||
    // init(size) before adding elements. This eliminates std::vector template overhead
 | 
			
		||||
    // and is ideal when the exact size is known before populating the array.
 | 
			
		||||
    optional bool fixed_vector = 50013 [default=false];
 | 
			
		||||
 | 
			
		||||
    // container_pointer_no_template: Use a non-template container type for repeated fields
 | 
			
		||||
    // Similar to container_pointer, but for containers that don't take template parameters.
 | 
			
		||||
    // The container type is used as-is without appending element type.
 | 
			
		||||
    // The container must have:
 | 
			
		||||
    // - begin() and end() methods returning iterators
 | 
			
		||||
    // - empty() method
 | 
			
		||||
    // Example: [(container_pointer_no_template) = "light::ColorModeMask"]
 | 
			
		||||
    //   generates: const light::ColorModeMask *supported_color_modes{};
 | 
			
		||||
    optional string container_pointer_no_template = 50014;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -790,7 +790,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "list_entities_light_response"; }
 | 
			
		||||
#endif
 | 
			
		||||
  const light::ColorModeMask *supported_color_modes{};
 | 
			
		||||
  const std::set<light::ColorMode> *supported_color_modes{};
 | 
			
		||||
  float min_mireds{0.0f};
 | 
			
		||||
  float max_mireds{0.0f};
 | 
			
		||||
  std::vector<std::string> effects{};
 | 
			
		||||
 
 | 
			
		||||
@@ -6,9 +6,6 @@ namespace bang_bang {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "bang_bang.climate";
 | 
			
		||||
 | 
			
		||||
BangBangClimate::BangBangClimate()
 | 
			
		||||
    : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::setup() {
 | 
			
		||||
  this->sensor_->add_on_state_callback([this](float state) {
 | 
			
		||||
    this->current_temperature = state;
 | 
			
		||||
@@ -34,63 +31,53 @@ void BangBangClimate::setup() {
 | 
			
		||||
    restore->to_call(this).perform();
 | 
			
		||||
  } else {
 | 
			
		||||
    // restore from defaults, change_away handles those for us
 | 
			
		||||
    if (this->supports_cool_ && this->supports_heat_) {
 | 
			
		||||
    if (supports_cool_ && supports_heat_) {
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
    } else if (this->supports_cool_) {
 | 
			
		||||
    } else if (supports_cool_) {
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_COOL;
 | 
			
		||||
    } else if (this->supports_heat_) {
 | 
			
		||||
    } else if (supports_heat_) {
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT;
 | 
			
		||||
    }
 | 
			
		||||
    this->change_away_(false);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::control(const climate::ClimateCall &call) {
 | 
			
		||||
  if (call.get_mode().has_value()) {
 | 
			
		||||
  if (call.get_mode().has_value())
 | 
			
		||||
    this->mode = *call.get_mode();
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_target_temperature_low().has_value()) {
 | 
			
		||||
  if (call.get_target_temperature_low().has_value())
 | 
			
		||||
    this->target_temperature_low = *call.get_target_temperature_low();
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_target_temperature_high().has_value()) {
 | 
			
		||||
  if (call.get_target_temperature_high().has_value())
 | 
			
		||||
    this->target_temperature_high = *call.get_target_temperature_high();
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_preset().has_value()) {
 | 
			
		||||
  if (call.get_preset().has_value())
 | 
			
		||||
    this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->compute_state_();
 | 
			
		||||
  this->publish_state();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
climate::ClimateTraits BangBangClimate::traits() {
 | 
			
		||||
  auto traits = climate::ClimateTraits();
 | 
			
		||||
  traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
 | 
			
		||||
                           climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr) {
 | 
			
		||||
    traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
 | 
			
		||||
  }
 | 
			
		||||
  traits.set_supports_current_temperature(true);
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr)
 | 
			
		||||
    traits.set_supports_current_humidity(true);
 | 
			
		||||
  traits.set_supported_modes({
 | 
			
		||||
      climate::CLIMATE_MODE_OFF,
 | 
			
		||||
  });
 | 
			
		||||
  if (this->supports_cool_) {
 | 
			
		||||
  if (supports_cool_)
 | 
			
		||||
    traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->supports_heat_) {
 | 
			
		||||
  if (supports_heat_)
 | 
			
		||||
    traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->supports_cool_ && this->supports_heat_) {
 | 
			
		||||
  if (supports_cool_ && supports_heat_)
 | 
			
		||||
    traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->supports_away_) {
 | 
			
		||||
  traits.set_supports_two_point_target_temperature(true);
 | 
			
		||||
  if (supports_away_) {
 | 
			
		||||
    traits.set_supported_presets({
 | 
			
		||||
        climate::CLIMATE_PRESET_HOME,
 | 
			
		||||
        climate::CLIMATE_PRESET_AWAY,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  traits.set_supports_action(true);
 | 
			
		||||
  return traits;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::compute_state_() {
 | 
			
		||||
  if (this->mode == climate::CLIMATE_MODE_OFF) {
 | 
			
		||||
    this->switch_to_action_(climate::CLIMATE_ACTION_OFF);
 | 
			
		||||
@@ -135,7 +122,6 @@ void BangBangClimate::compute_state_() {
 | 
			
		||||
 | 
			
		||||
  this->switch_to_action_(target_action);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
 | 
			
		||||
  if (action == this->action) {
 | 
			
		||||
    // already in target mode
 | 
			
		||||
@@ -180,7 +166,6 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
 | 
			
		||||
  this->prev_trigger_ = trig;
 | 
			
		||||
  this->publish_state();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::change_away_(bool away) {
 | 
			
		||||
  if (!away) {
 | 
			
		||||
    this->target_temperature_low = this->normal_config_.default_temperature_low;
 | 
			
		||||
@@ -191,26 +176,22 @@ void BangBangClimate::change_away_(bool away) {
 | 
			
		||||
  }
 | 
			
		||||
  this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) {
 | 
			
		||||
  this->normal_config_ = normal_config;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) {
 | 
			
		||||
  this->supports_away_ = true;
 | 
			
		||||
  this->away_config_ = away_config;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
BangBangClimate::BangBangClimate()
 | 
			
		||||
    : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
 | 
			
		||||
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
 | 
			
		||||
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
 | 
			
		||||
 | 
			
		||||
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
 | 
			
		||||
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
 | 
			
		||||
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
 | 
			
		||||
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
 | 
			
		||||
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::dump_config() {
 | 
			
		||||
  LOG_CLIMATE("", "Bang Bang Climate", this);
 | 
			
		||||
  ESP_LOGCONFIG(TAG,
 | 
			
		||||
 
 | 
			
		||||
@@ -25,15 +25,14 @@ class BangBangClimate : public climate::Climate, public Component {
 | 
			
		||||
 | 
			
		||||
  void set_sensor(sensor::Sensor *sensor);
 | 
			
		||||
  void set_humidity_sensor(sensor::Sensor *humidity_sensor);
 | 
			
		||||
  Trigger<> *get_idle_trigger() const;
 | 
			
		||||
  Trigger<> *get_cool_trigger() const;
 | 
			
		||||
  void set_supports_cool(bool supports_cool);
 | 
			
		||||
  Trigger<> *get_heat_trigger() const;
 | 
			
		||||
  void set_supports_heat(bool supports_heat);
 | 
			
		||||
  void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
 | 
			
		||||
  void set_away_config(const BangBangClimateTargetTempConfig &away_config);
 | 
			
		||||
 | 
			
		||||
  Trigger<> *get_idle_trigger() const;
 | 
			
		||||
  Trigger<> *get_cool_trigger() const;
 | 
			
		||||
  Trigger<> *get_heat_trigger() const;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  /// Override control to change settings of the climate device.
 | 
			
		||||
  void control(const climate::ClimateCall &call) override;
 | 
			
		||||
@@ -57,10 +56,16 @@ class BangBangClimate : public climate::Climate, public Component {
 | 
			
		||||
   *
 | 
			
		||||
   * In idle mode, the controller is assumed to have both heating and cooling disabled.
 | 
			
		||||
   */
 | 
			
		||||
  Trigger<> *idle_trigger_{nullptr};
 | 
			
		||||
  Trigger<> *idle_trigger_;
 | 
			
		||||
  /** The trigger to call when the controller should switch to cooling mode.
 | 
			
		||||
   */
 | 
			
		||||
  Trigger<> *cool_trigger_{nullptr};
 | 
			
		||||
  Trigger<> *cool_trigger_;
 | 
			
		||||
  /** Whether the controller supports cooling.
 | 
			
		||||
   *
 | 
			
		||||
   * A false value for this attribute means that the controller has no cooling action
 | 
			
		||||
   * (for example a thermostat, where only heating and not-heating is possible).
 | 
			
		||||
   */
 | 
			
		||||
  bool supports_cool_{false};
 | 
			
		||||
  /** The trigger to call when the controller should switch to heating mode.
 | 
			
		||||
   *
 | 
			
		||||
   * A null value for this attribute means that the controller has no heating action
 | 
			
		||||
@@ -68,23 +73,15 @@ class BangBangClimate : public climate::Climate, public Component {
 | 
			
		||||
   * (blinds open) is possible.
 | 
			
		||||
   */
 | 
			
		||||
  Trigger<> *heat_trigger_{nullptr};
 | 
			
		||||
  bool supports_heat_{false};
 | 
			
		||||
  /** A reference to the trigger that was previously active.
 | 
			
		||||
   *
 | 
			
		||||
   * This is so that the previous trigger can be stopped before enabling a new one.
 | 
			
		||||
   */
 | 
			
		||||
  Trigger<> *prev_trigger_{nullptr};
 | 
			
		||||
 | 
			
		||||
  /** Whether the controller supports cooling/heating
 | 
			
		||||
   *
 | 
			
		||||
   * A false value for this attribute means that the controller has no respective action
 | 
			
		||||
   * (for example a thermostat, where only heating and not-heating is possible).
 | 
			
		||||
   */
 | 
			
		||||
  bool supports_cool_{false};
 | 
			
		||||
  bool supports_heat_{false};
 | 
			
		||||
 | 
			
		||||
  bool supports_away_{false};
 | 
			
		||||
 | 
			
		||||
  BangBangClimateTargetTempConfig normal_config_{};
 | 
			
		||||
  bool supports_away_{false};
 | 
			
		||||
  BangBangClimateTargetTempConfig away_config_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,8 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
 | 
			
		||||
 | 
			
		||||
  climate::ClimateTraits traits() override {
 | 
			
		||||
    auto traits = climate::ClimateTraits();
 | 
			
		||||
    traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION | climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
 | 
			
		||||
    traits.set_supports_action(true);
 | 
			
		||||
    traits.set_supports_current_temperature(true);
 | 
			
		||||
    traits.set_supported_modes({
 | 
			
		||||
        climate::CLIMATE_MODE_OFF,
 | 
			
		||||
        climate::CLIMATE_MODE_HEAT,
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ void BinarySensor::add_filter(Filter *filter) {
 | 
			
		||||
    last_filter->next_ = filter;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
 | 
			
		||||
void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
 | 
			
		||||
  for (Filter *filter : filters) {
 | 
			
		||||
    this->add_filter(filter);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/components/binary_sensor/filter.h"
 | 
			
		||||
 | 
			
		||||
#include <initializer_list>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
@@ -48,7 +48,7 @@ class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceCl
 | 
			
		||||
  void publish_initial_state(bool new_state);
 | 
			
		||||
 | 
			
		||||
  void add_filter(Filter *filter);
 | 
			
		||||
  void add_filters(std::initializer_list<Filter *> filters);
 | 
			
		||||
  void add_filters(const std::vector<Filter *> &filters);
 | 
			
		||||
 | 
			
		||||
  // ========== INTERNAL METHODS ==========
 | 
			
		||||
  // (In most use cases you won't need these)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,29 +0,0 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components.zephyr import zephyr_add_prj_conf
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_LOGS, CONF_TYPE
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["zephyr_ble_server"]
 | 
			
		||||
CODEOWNERS = ["@tomaszduda23"]
 | 
			
		||||
 | 
			
		||||
ble_nus_ns = cg.esphome_ns.namespace("ble_nus")
 | 
			
		||||
BLENUS = ble_nus_ns.class_("BLENUS", cg.Component)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BLENUS),
 | 
			
		||||
            cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of(
 | 
			
		||||
                *[CONF_LOGS], lower=True
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.only_with_framework("zephyr"),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    zephyr_add_prj_conf("BT_NUS", True)
 | 
			
		||||
    cg.add(var.set_expose_log(config[CONF_TYPE] == CONF_LOGS))
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
@@ -1,157 +0,0 @@
 | 
			
		||||
#ifdef USE_ZEPHYR
 | 
			
		||||
#include "ble_nus.h"
 | 
			
		||||
#include <zephyr/kernel.h>
 | 
			
		||||
#include <bluetooth/services/nus.h>
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
#include "esphome/components/logger/logger.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#endif
 | 
			
		||||
#include <zephyr/sys/ring_buffer.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome::ble_nus {
 | 
			
		||||
 | 
			
		||||
constexpr size_t BLE_TX_BUF_SIZE = 2048;
 | 
			
		||||
 | 
			
		||||
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
BLENUS *global_ble_nus;
 | 
			
		||||
RING_BUF_DECLARE(global_ble_tx_ring_buf, BLE_TX_BUF_SIZE);
 | 
			
		||||
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "ble_nus";
 | 
			
		||||
 | 
			
		||||
size_t BLENUS::write_array(const uint8_t *data, size_t len) {
 | 
			
		||||
  if (atomic_get(&this->tx_status_) == TX_DISABLED) {
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
  return ring_buf_put(&global_ble_tx_ring_buf, data, len);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::connected(bt_conn *conn, uint8_t err) {
 | 
			
		||||
  if (err == 0) {
 | 
			
		||||
    global_ble_nus->conn_.store(bt_conn_ref(conn));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::disconnected(bt_conn *conn, uint8_t reason) {
 | 
			
		||||
  if (global_ble_nus->conn_) {
 | 
			
		||||
    bt_conn_unref(global_ble_nus->conn_.load());
 | 
			
		||||
    // Connection array is global static.
 | 
			
		||||
    // Reference can be kept even if disconnected.
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::tx_callback(bt_conn *conn) {
 | 
			
		||||
  atomic_cas(&global_ble_nus->tx_status_, TX_BUSY, TX_ENABLED);
 | 
			
		||||
  ESP_LOGVV(TAG, "Sent operation completed");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::send_enabled_callback(bt_nus_send_status status) {
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case BT_NUS_SEND_STATUS_ENABLED:
 | 
			
		||||
      atomic_set(&global_ble_nus->tx_status_, TX_ENABLED);
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
      if (global_ble_nus->expose_log_) {
 | 
			
		||||
        App.schedule_dump_config();
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      ESP_LOGD(TAG, "NUS notification has been enabled");
 | 
			
		||||
      break;
 | 
			
		||||
    case BT_NUS_SEND_STATUS_DISABLED:
 | 
			
		||||
      atomic_set(&global_ble_nus->tx_status_, TX_DISABLED);
 | 
			
		||||
      ESP_LOGD(TAG, "NUS notification has been disabled");
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::rx_callback(bt_conn *conn, const uint8_t *const data, uint16_t len) {
 | 
			
		||||
  ESP_LOGD(TAG, "Received %d bytes.", len);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::setup() {
 | 
			
		||||
  bt_nus_cb callbacks = {
 | 
			
		||||
      .received = rx_callback,
 | 
			
		||||
      .sent = tx_callback,
 | 
			
		||||
      .send_enabled = send_enabled_callback,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  bt_nus_init(&callbacks);
 | 
			
		||||
 | 
			
		||||
  static bt_conn_cb conn_callbacks = {
 | 
			
		||||
      .connected = BLENUS::connected,
 | 
			
		||||
      .disconnected = BLENUS::disconnected,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  bt_conn_cb_register(&conn_callbacks);
 | 
			
		||||
 | 
			
		||||
  global_ble_nus = this;
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
  if (logger::global_logger != nullptr && this->expose_log_) {
 | 
			
		||||
    logger::global_logger->add_on_log_callback(
 | 
			
		||||
        [this](int level, const char *tag, const char *message, size_t message_len) {
 | 
			
		||||
          this->write_array(reinterpret_cast<const uint8_t *>(message), message_len);
 | 
			
		||||
          const char c = '\n';
 | 
			
		||||
          this->write_array(reinterpret_cast<const uint8_t *>(&c), 1);
 | 
			
		||||
        });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "ble nus:");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  log: %s", YESNO(this->expose_log_));
 | 
			
		||||
  uint32_t mtu = 0;
 | 
			
		||||
  bt_conn *conn = this->conn_.load();
 | 
			
		||||
  if (conn) {
 | 
			
		||||
    mtu = bt_nus_get_mtu(conn);
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  MTU: %u", mtu);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::loop() {
 | 
			
		||||
  if (ring_buf_is_empty(&global_ble_tx_ring_buf)) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!atomic_cas(&this->tx_status_, TX_ENABLED, TX_BUSY)) {
 | 
			
		||||
    if (atomic_get(&this->tx_status_) == TX_DISABLED) {
 | 
			
		||||
      ring_buf_reset(&global_ble_tx_ring_buf);
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bt_conn *conn = this->conn_.load();
 | 
			
		||||
  if (conn) {
 | 
			
		||||
    conn = bt_conn_ref(conn);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (nullptr == conn) {
 | 
			
		||||
    atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint32_t req_len = bt_nus_get_mtu(conn);
 | 
			
		||||
 | 
			
		||||
  uint8_t *buf;
 | 
			
		||||
  uint32_t size = ring_buf_get_claim(&global_ble_tx_ring_buf, &buf, req_len);
 | 
			
		||||
 | 
			
		||||
  int err, err2;
 | 
			
		||||
 | 
			
		||||
  err = bt_nus_send(conn, buf, size);
 | 
			
		||||
  err2 = ring_buf_get_finish(&global_ble_tx_ring_buf, size);
 | 
			
		||||
  if (err2) {
 | 
			
		||||
    // It should no happen.
 | 
			
		||||
    ESP_LOGE(TAG, "Size %u exceeds valid bytes in the ring buffer (%d error)", size, err2);
 | 
			
		||||
  }
 | 
			
		||||
  if (err == 0) {
 | 
			
		||||
    ESP_LOGVV(TAG, "Sent %d bytes", size);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGE(TAG, "Failed to send %d bytes (%d error)", size, err);
 | 
			
		||||
    atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED);
 | 
			
		||||
  }
 | 
			
		||||
  bt_conn_unref(conn);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::ble_nus
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1,37 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#ifdef USE_ZEPHYR
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include <shell/shell_bt_nus.h>
 | 
			
		||||
#include <atomic>
 | 
			
		||||
 | 
			
		||||
namespace esphome::ble_nus {
 | 
			
		||||
 | 
			
		||||
class BLENUS : public Component {
 | 
			
		||||
  enum TxStatus {
 | 
			
		||||
    TX_DISABLED,
 | 
			
		||||
    TX_ENABLED,
 | 
			
		||||
    TX_BUSY,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  size_t write_array(const uint8_t *data, size_t len);
 | 
			
		||||
  void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  static void send_enabled_callback(bt_nus_send_status status);
 | 
			
		||||
  static void tx_callback(bt_conn *conn);
 | 
			
		||||
  static void rx_callback(bt_conn *conn, const uint8_t *data, uint16_t len);
 | 
			
		||||
  static void connected(bt_conn *conn, uint8_t err);
 | 
			
		||||
  static void disconnected(bt_conn *conn, uint8_t reason);
 | 
			
		||||
 | 
			
		||||
  std::atomic<bt_conn *> conn_ = nullptr;
 | 
			
		||||
  bool expose_log_ = false;
 | 
			
		||||
  atomic_t tx_status_ = ATOMIC_INIT(TX_DISABLED);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::ble_nus
 | 
			
		||||
#endif
 | 
			
		||||
@@ -155,12 +155,16 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par
 | 
			
		||||
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
 | 
			
		||||
  for (uint8_t i = 0; i < this->connection_count_; i++) {
 | 
			
		||||
    auto *connection = this->connections_[i];
 | 
			
		||||
    uint64_t conn_addr = connection->get_address();
 | 
			
		||||
 | 
			
		||||
    if (conn_addr == address)
 | 
			
		||||
    if (connection->get_address() == address)
 | 
			
		||||
      return connection;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    if (reserve && conn_addr == 0) {
 | 
			
		||||
  if (!reserve)
 | 
			
		||||
    return nullptr;
 | 
			
		||||
 | 
			
		||||
  for (uint8_t i = 0; i < this->connection_count_; i++) {
 | 
			
		||||
    auto *connection = this->connections_[i];
 | 
			
		||||
    if (connection->get_address() == 0) {
 | 
			
		||||
      connection->send_service_ = INIT_SENDING_SERVICES;
 | 
			
		||||
      connection->set_address(address);
 | 
			
		||||
      // All connections must start at INIT
 | 
			
		||||
@@ -171,6 +175,7 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
 | 
			
		||||
      return connection;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return nullptr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BME680BSECComponent),
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta,
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
 | 
			
		||||
            cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum(
 | 
			
		||||
                IAQ_MODE_OPTIONS, upper=True
 | 
			
		||||
            ),
 | 
			
		||||
 
 | 
			
		||||
@@ -139,7 +139,7 @@ CONFIG_SCHEMA_BASE = (
 | 
			
		||||
            cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum(
 | 
			
		||||
                VOLTAGE_OPTIONS, upper=True
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta,
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
                CONF_STATE_SAVE_INTERVAL, default="6hours"
 | 
			
		||||
            ): cv.positive_time_period_minutes,
 | 
			
		||||
 
 | 
			
		||||
@@ -6,42 +6,6 @@ namespace climate {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "climate";
 | 
			
		||||
 | 
			
		||||
// Memory-efficient lookup tables
 | 
			
		||||
struct StringToUint8 {
 | 
			
		||||
  const char *str;
 | 
			
		||||
  const uint8_t value;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr StringToUint8 CLIMATE_MODES_BY_STR[] = {
 | 
			
		||||
    {"OFF", CLIMATE_MODE_OFF},
 | 
			
		||||
    {"AUTO", CLIMATE_MODE_AUTO},
 | 
			
		||||
    {"COOL", CLIMATE_MODE_COOL},
 | 
			
		||||
    {"HEAT", CLIMATE_MODE_HEAT},
 | 
			
		||||
    {"FAN_ONLY", CLIMATE_MODE_FAN_ONLY},
 | 
			
		||||
    {"DRY", CLIMATE_MODE_DRY},
 | 
			
		||||
    {"HEAT_COOL", CLIMATE_MODE_HEAT_COOL},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr StringToUint8 CLIMATE_FAN_MODES_BY_STR[] = {
 | 
			
		||||
    {"ON", CLIMATE_FAN_ON},         {"OFF", CLIMATE_FAN_OFF},       {"AUTO", CLIMATE_FAN_AUTO},
 | 
			
		||||
    {"LOW", CLIMATE_FAN_LOW},       {"MEDIUM", CLIMATE_FAN_MEDIUM}, {"HIGH", CLIMATE_FAN_HIGH},
 | 
			
		||||
    {"MIDDLE", CLIMATE_FAN_MIDDLE}, {"FOCUS", CLIMATE_FAN_FOCUS},   {"DIFFUSE", CLIMATE_FAN_DIFFUSE},
 | 
			
		||||
    {"QUIET", CLIMATE_FAN_QUIET},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr StringToUint8 CLIMATE_PRESETS_BY_STR[] = {
 | 
			
		||||
    {"ECO", CLIMATE_PRESET_ECO},           {"AWAY", CLIMATE_PRESET_AWAY}, {"BOOST", CLIMATE_PRESET_BOOST},
 | 
			
		||||
    {"COMFORT", CLIMATE_PRESET_COMFORT},   {"HOME", CLIMATE_PRESET_HOME}, {"SLEEP", CLIMATE_PRESET_SLEEP},
 | 
			
		||||
    {"ACTIVITY", CLIMATE_PRESET_ACTIVITY}, {"NONE", CLIMATE_PRESET_NONE},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr StringToUint8 CLIMATE_SWING_MODES_BY_STR[] = {
 | 
			
		||||
    {"OFF", CLIMATE_SWING_OFF},
 | 
			
		||||
    {"BOTH", CLIMATE_SWING_BOTH},
 | 
			
		||||
    {"VERTICAL", CLIMATE_SWING_VERTICAL},
 | 
			
		||||
    {"HORIZONTAL", CLIMATE_SWING_HORIZONTAL},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void ClimateCall::perform() {
 | 
			
		||||
  this->parent_->control_callback_.call(*this);
 | 
			
		||||
  ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
 | 
			
		||||
@@ -86,46 +50,47 @@ void ClimateCall::perform() {
 | 
			
		||||
  }
 | 
			
		||||
  this->parent_->control(*this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ClimateCall::validate_() {
 | 
			
		||||
  auto traits = this->parent_->get_traits();
 | 
			
		||||
  if (this->mode_.has_value()) {
 | 
			
		||||
    auto mode = *this->mode_;
 | 
			
		||||
    if (!traits.supports_mode(mode)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Mode %s not supported", LOG_STR_ARG(climate_mode_to_string(mode)));
 | 
			
		||||
      ESP_LOGW(TAG, "  Mode %s is not supported by this device!", LOG_STR_ARG(climate_mode_to_string(mode)));
 | 
			
		||||
      this->mode_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->custom_fan_mode_.has_value()) {
 | 
			
		||||
    auto custom_fan_mode = *this->custom_fan_mode_;
 | 
			
		||||
    if (!traits.supports_custom_fan_mode(custom_fan_mode)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Fan Mode %s not supported", custom_fan_mode.c_str());
 | 
			
		||||
      ESP_LOGW(TAG, "  Fan Mode %s is not supported by this device!", custom_fan_mode.c_str());
 | 
			
		||||
      this->custom_fan_mode_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  } else if (this->fan_mode_.has_value()) {
 | 
			
		||||
    auto fan_mode = *this->fan_mode_;
 | 
			
		||||
    if (!traits.supports_fan_mode(fan_mode)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Fan Mode %s not supported", LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
 | 
			
		||||
      ESP_LOGW(TAG, "  Fan Mode %s is not supported by this device!",
 | 
			
		||||
               LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
 | 
			
		||||
      this->fan_mode_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->custom_preset_.has_value()) {
 | 
			
		||||
    auto custom_preset = *this->custom_preset_;
 | 
			
		||||
    if (!traits.supports_custom_preset(custom_preset)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Preset %s not supported", custom_preset.c_str());
 | 
			
		||||
      ESP_LOGW(TAG, "  Preset %s is not supported by this device!", custom_preset.c_str());
 | 
			
		||||
      this->custom_preset_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  } else if (this->preset_.has_value()) {
 | 
			
		||||
    auto preset = *this->preset_;
 | 
			
		||||
    if (!traits.supports_preset(preset)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Preset %s not supported", LOG_STR_ARG(climate_preset_to_string(preset)));
 | 
			
		||||
      ESP_LOGW(TAG, "  Preset %s is not supported by this device!", LOG_STR_ARG(climate_preset_to_string(preset)));
 | 
			
		||||
      this->preset_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->swing_mode_.has_value()) {
 | 
			
		||||
    auto swing_mode = *this->swing_mode_;
 | 
			
		||||
    if (!traits.supports_swing_mode(swing_mode)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Swing Mode %s not supported", LOG_STR_ARG(climate_swing_mode_to_string(swing_mode)));
 | 
			
		||||
      ESP_LOGW(TAG, "  Swing Mode %s is not supported by this device!",
 | 
			
		||||
               LOG_STR_ARG(climate_swing_mode_to_string(swing_mode)));
 | 
			
		||||
      this->swing_mode_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -134,127 +99,159 @@ void ClimateCall::validate_() {
 | 
			
		||||
    if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
 | 
			
		||||
                                 CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Cannot set target temperature for climate device "
 | 
			
		||||
                    "with two-point target temperature");
 | 
			
		||||
                    "with two-point target temperature!");
 | 
			
		||||
      this->target_temperature_.reset();
 | 
			
		||||
    } else if (std::isnan(target)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Target temperature must not be NAN");
 | 
			
		||||
      ESP_LOGW(TAG, "  Target temperature must not be NAN!");
 | 
			
		||||
      this->target_temperature_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) {
 | 
			
		||||
    if (!traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
 | 
			
		||||
                                  CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Cannot set low/high target temperature");
 | 
			
		||||
      ESP_LOGW(TAG, "  Cannot set low/high target temperature for this device!");
 | 
			
		||||
      this->target_temperature_low_.reset();
 | 
			
		||||
      this->target_temperature_high_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->target_temperature_low_.has_value() && std::isnan(*this->target_temperature_low_)) {
 | 
			
		||||
    ESP_LOGW(TAG, "  Target temperature low must not be NAN");
 | 
			
		||||
    ESP_LOGW(TAG, "  Target temperature low must not be NAN!");
 | 
			
		||||
    this->target_temperature_low_.reset();
 | 
			
		||||
  }
 | 
			
		||||
  if (this->target_temperature_high_.has_value() && std::isnan(*this->target_temperature_high_)) {
 | 
			
		||||
    ESP_LOGW(TAG, "  Target temperature high must not be NAN");
 | 
			
		||||
    ESP_LOGW(TAG, "  Target temperature low must not be NAN!");
 | 
			
		||||
    this->target_temperature_high_.reset();
 | 
			
		||||
  }
 | 
			
		||||
  if (this->target_temperature_low_.has_value() && this->target_temperature_high_.has_value()) {
 | 
			
		||||
    float low = *this->target_temperature_low_;
 | 
			
		||||
    float high = *this->target_temperature_high_;
 | 
			
		||||
    if (low > high) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Target temperature low %.2f must be less than target temperature high %.2f", low, high);
 | 
			
		||||
      ESP_LOGW(TAG, "  Target temperature low %.2f must be smaller than target temperature high %.2f!", low, high);
 | 
			
		||||
      this->target_temperature_low_.reset();
 | 
			
		||||
      this->target_temperature_high_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_mode(ClimateMode mode) {
 | 
			
		||||
  this->mode_ = mode;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_mode(const std::string &mode) {
 | 
			
		||||
  for (const auto &mode_entry : CLIMATE_MODES_BY_STR) {
 | 
			
		||||
    if (str_equals_case_insensitive(mode, mode_entry.str)) {
 | 
			
		||||
      this->set_mode(static_cast<ClimateMode>(mode_entry.value));
 | 
			
		||||
      return *this;
 | 
			
		||||
    }
 | 
			
		||||
  if (str_equals_case_insensitive(mode, "OFF")) {
 | 
			
		||||
    this->set_mode(CLIMATE_MODE_OFF);
 | 
			
		||||
  } else if (str_equals_case_insensitive(mode, "AUTO")) {
 | 
			
		||||
    this->set_mode(CLIMATE_MODE_AUTO);
 | 
			
		||||
  } else if (str_equals_case_insensitive(mode, "COOL")) {
 | 
			
		||||
    this->set_mode(CLIMATE_MODE_COOL);
 | 
			
		||||
  } else if (str_equals_case_insensitive(mode, "HEAT")) {
 | 
			
		||||
    this->set_mode(CLIMATE_MODE_HEAT);
 | 
			
		||||
  } else if (str_equals_case_insensitive(mode, "FAN_ONLY")) {
 | 
			
		||||
    this->set_mode(CLIMATE_MODE_FAN_ONLY);
 | 
			
		||||
  } else if (str_equals_case_insensitive(mode, "DRY")) {
 | 
			
		||||
    this->set_mode(CLIMATE_MODE_DRY);
 | 
			
		||||
  } else if (str_equals_case_insensitive(mode, "HEAT_COOL")) {
 | 
			
		||||
    this->set_mode(CLIMATE_MODE_HEAT_COOL);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
 | 
			
		||||
  this->fan_mode_ = fan_mode;
 | 
			
		||||
  this->custom_fan_mode_.reset();
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
 | 
			
		||||
  for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
 | 
			
		||||
    if (str_equals_case_insensitive(fan_mode, mode_entry.str)) {
 | 
			
		||||
      this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
 | 
			
		||||
      return *this;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
 | 
			
		||||
    this->custom_fan_mode_ = fan_mode;
 | 
			
		||||
    this->fan_mode_.reset();
 | 
			
		||||
  if (str_equals_case_insensitive(fan_mode, "ON")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_ON);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "OFF")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_OFF);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "AUTO")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_AUTO);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "LOW")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_LOW);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "MEDIUM")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_MEDIUM);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "HIGH")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_HIGH);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "MIDDLE")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_MIDDLE);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "FOCUS")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_FOCUS);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_DIFFUSE);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "QUIET")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_QUIET);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
 | 
			
		||||
    if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
 | 
			
		||||
      this->custom_fan_mode_ = fan_mode;
 | 
			
		||||
      this->fan_mode_.reset();
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
 | 
			
		||||
  if (fan_mode.has_value()) {
 | 
			
		||||
    this->set_fan_mode(fan_mode.value());
 | 
			
		||||
  }
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
 | 
			
		||||
  this->preset_ = preset;
 | 
			
		||||
  this->custom_preset_.reset();
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_preset(const std::string &preset) {
 | 
			
		||||
  for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
 | 
			
		||||
    if (str_equals_case_insensitive(preset, preset_entry.str)) {
 | 
			
		||||
      this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
 | 
			
		||||
      return *this;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->parent_->get_traits().supports_custom_preset(preset)) {
 | 
			
		||||
    this->custom_preset_ = preset;
 | 
			
		||||
    this->preset_.reset();
 | 
			
		||||
  if (str_equals_case_insensitive(preset, "ECO")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_ECO);
 | 
			
		||||
  } else if (str_equals_case_insensitive(preset, "AWAY")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_AWAY);
 | 
			
		||||
  } else if (str_equals_case_insensitive(preset, "BOOST")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_BOOST);
 | 
			
		||||
  } else if (str_equals_case_insensitive(preset, "COMFORT")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_COMFORT);
 | 
			
		||||
  } else if (str_equals_case_insensitive(preset, "HOME")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_HOME);
 | 
			
		||||
  } else if (str_equals_case_insensitive(preset, "SLEEP")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_SLEEP);
 | 
			
		||||
  } else if (str_equals_case_insensitive(preset, "ACTIVITY")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_ACTIVITY);
 | 
			
		||||
  } else if (str_equals_case_insensitive(preset, "NONE")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_NONE);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
 | 
			
		||||
    if (this->parent_->get_traits().supports_custom_preset(preset)) {
 | 
			
		||||
      this->custom_preset_ = preset;
 | 
			
		||||
      this->preset_.reset();
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
 | 
			
		||||
  if (preset.has_value()) {
 | 
			
		||||
    this->set_preset(preset.value());
 | 
			
		||||
  }
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) {
 | 
			
		||||
  this->swing_mode_ = swing_mode;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) {
 | 
			
		||||
  for (const auto &mode_entry : CLIMATE_SWING_MODES_BY_STR) {
 | 
			
		||||
    if (str_equals_case_insensitive(swing_mode, mode_entry.str)) {
 | 
			
		||||
      this->set_swing_mode(static_cast<ClimateSwingMode>(mode_entry.value));
 | 
			
		||||
      return *this;
 | 
			
		||||
    }
 | 
			
		||||
  if (str_equals_case_insensitive(swing_mode, "OFF")) {
 | 
			
		||||
    this->set_swing_mode(CLIMATE_SWING_OFF);
 | 
			
		||||
  } else if (str_equals_case_insensitive(swing_mode, "BOTH")) {
 | 
			
		||||
    this->set_swing_mode(CLIMATE_SWING_BOTH);
 | 
			
		||||
  } else if (str_equals_case_insensitive(swing_mode, "VERTICAL")) {
 | 
			
		||||
    this->set_swing_mode(CLIMATE_SWING_VERTICAL);
 | 
			
		||||
  } else if (str_equals_case_insensitive(swing_mode, "HORIZONTAL")) {
 | 
			
		||||
    this->set_swing_mode(CLIMATE_SWING_HORIZONTAL);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str());
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str());
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -262,71 +259,59 @@ ClimateCall &ClimateCall::set_target_temperature(float target_temperature) {
 | 
			
		||||
  this->target_temperature_ = target_temperature;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_target_temperature_low(float target_temperature_low) {
 | 
			
		||||
  this->target_temperature_low_ = target_temperature_low;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_high) {
 | 
			
		||||
  this->target_temperature_high_ = target_temperature_high;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_target_humidity(float target_humidity) {
 | 
			
		||||
  this->target_humidity_ = target_humidity;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; }
 | 
			
		||||
const optional<float> &ClimateCall::get_target_temperature() const { return this->target_temperature_; }
 | 
			
		||||
const optional<float> &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; }
 | 
			
		||||
const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; }
 | 
			
		||||
const optional<float> &ClimateCall::get_target_humidity() const { return this->target_humidity_; }
 | 
			
		||||
 | 
			
		||||
const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; }
 | 
			
		||||
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
 | 
			
		||||
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
 | 
			
		||||
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
 | 
			
		||||
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
 | 
			
		||||
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
 | 
			
		||||
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
 | 
			
		||||
 | 
			
		||||
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
 | 
			
		||||
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
 | 
			
		||||
  this->target_temperature_high_ = target_temperature_high;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_target_temperature_low(optional<float> target_temperature_low) {
 | 
			
		||||
  this->target_temperature_low_ = target_temperature_low;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperature) {
 | 
			
		||||
  this->target_temperature_ = target_temperature;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_target_humidity(optional<float> target_humidity) {
 | 
			
		||||
  this->target_humidity_ = target_humidity;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
 | 
			
		||||
  this->mode_ = mode;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
 | 
			
		||||
  this->fan_mode_ = fan_mode;
 | 
			
		||||
  this->custom_fan_mode_.reset();
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
 | 
			
		||||
  this->preset_ = preset;
 | 
			
		||||
  this->custom_preset_.reset();
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode) {
 | 
			
		||||
  this->swing_mode_ = swing_mode;
 | 
			
		||||
  return *this;
 | 
			
		||||
@@ -351,7 +336,6 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
 | 
			
		||||
    return {};
 | 
			
		||||
  return recovered;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Climate::save_state_() {
 | 
			
		||||
#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
 | 
			
		||||
    !defined(CLANG_TIDY)
 | 
			
		||||
@@ -414,7 +398,6 @@ void Climate::save_state_() {
 | 
			
		||||
 | 
			
		||||
  this->rtc_.save(&state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Climate::publish_state() {
 | 
			
		||||
  ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str());
 | 
			
		||||
  auto traits = this->get_traits();
 | 
			
		||||
@@ -486,20 +469,16 @@ ClimateTraits Climate::get_traits() {
 | 
			
		||||
void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) {
 | 
			
		||||
  this->visual_min_temperature_override_ = visual_min_temperature_override;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) {
 | 
			
		||||
  this->visual_max_temperature_override_ = visual_max_temperature_override;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Climate::set_visual_temperature_step_override(float target, float current) {
 | 
			
		||||
  this->visual_target_temperature_step_override_ = target;
 | 
			
		||||
  this->visual_current_temperature_step_override_ = current;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) {
 | 
			
		||||
  this->visual_min_humidity_override_ = visual_min_humidity_override;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) {
 | 
			
		||||
  this->visual_max_humidity_override_ = visual_max_humidity_override;
 | 
			
		||||
}
 | 
			
		||||
@@ -531,7 +510,6 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
 | 
			
		||||
  }
 | 
			
		||||
  return call;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ClimateDeviceRestoreState::apply(Climate *climate) {
 | 
			
		||||
  auto traits = climate->get_traits();
 | 
			
		||||
  climate->mode = this->mode;
 | 
			
		||||
@@ -601,68 +579,68 @@ void Climate::dump_traits_(const char *tag) {
 | 
			
		||||
  auto traits = this->get_traits();
 | 
			
		||||
  ESP_LOGCONFIG(tag, "ClimateTraits:");
 | 
			
		||||
  ESP_LOGCONFIG(tag,
 | 
			
		||||
                "  Visual settings:\n"
 | 
			
		||||
                "  - Min temperature: %.1f\n"
 | 
			
		||||
                "  - Max temperature: %.1f\n"
 | 
			
		||||
                "  - Temperature step:\n"
 | 
			
		||||
                "      Target: %.1f",
 | 
			
		||||
                "  [x] Visual settings:\n"
 | 
			
		||||
                "      - Min temperature: %.1f\n"
 | 
			
		||||
                "      - Max temperature: %.1f\n"
 | 
			
		||||
                "      - Temperature step:\n"
 | 
			
		||||
                "          Target: %.1f",
 | 
			
		||||
                traits.get_visual_min_temperature(), traits.get_visual_max_temperature(),
 | 
			
		||||
                traits.get_visual_target_temperature_step());
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "      Current: %.1f", traits.get_visual_current_temperature_step());
 | 
			
		||||
    ESP_LOGCONFIG(tag, "          Current: %.1f", traits.get_visual_current_temperature_step());
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY |
 | 
			
		||||
                               climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag,
 | 
			
		||||
                  "  - Min humidity: %.0f\n"
 | 
			
		||||
                  "  - Max humidity: %.0f",
 | 
			
		||||
                  "      - Min humidity: %.0f\n"
 | 
			
		||||
                  "      - Max humidity: %.0f",
 | 
			
		||||
                  traits.get_visual_min_humidity(), traits.get_visual_max_humidity());
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
 | 
			
		||||
                               CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supports two-point target temperature");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports two-point target temperature");
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supports current temperature");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports current temperature");
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supports target humidity");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports target humidity");
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supports current humidity");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports current humidity");
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supports action");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports action");
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_modes().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supported modes:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported modes:");
 | 
			
		||||
    for (ClimateMode m : traits.get_supported_modes())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "  - %s", LOG_STR_ARG(climate_mode_to_string(m)));
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", LOG_STR_ARG(climate_mode_to_string(m)));
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_fan_modes().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supported fan modes:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported fan modes:");
 | 
			
		||||
    for (ClimateFanMode m : traits.get_supported_fan_modes())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "  - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_custom_fan_modes().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supported custom fan modes:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported custom fan modes:");
 | 
			
		||||
    for (const std::string &s : traits.get_supported_custom_fan_modes())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "  - %s", s.c_str());
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", s.c_str());
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_presets().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supported presets:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported presets:");
 | 
			
		||||
    for (ClimatePreset p : traits.get_supported_presets())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "  - %s", LOG_STR_ARG(climate_preset_to_string(p)));
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", LOG_STR_ARG(climate_preset_to_string(p)));
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_custom_presets().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supported custom presets:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported custom presets:");
 | 
			
		||||
    for (const std::string &s : traits.get_supported_custom_presets())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "  - %s", s.c_str());
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", s.c_str());
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_swing_modes().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supported swing modes:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported swing modes:");
 | 
			
		||||
    for (ClimateSwingMode m : traits.get_supported_swing_modes())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "  - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -93,31 +93,30 @@ class ClimateCall {
 | 
			
		||||
 | 
			
		||||
  void perform();
 | 
			
		||||
 | 
			
		||||
  const optional<ClimateMode> &get_mode() const;
 | 
			
		||||
  const optional<float> &get_target_temperature() const;
 | 
			
		||||
  const optional<float> &get_target_temperature_low() const;
 | 
			
		||||
  const optional<float> &get_target_temperature_high() const;
 | 
			
		||||
  const optional<float> &get_target_humidity() const;
 | 
			
		||||
 | 
			
		||||
  const optional<ClimateMode> &get_mode() const;
 | 
			
		||||
  const optional<ClimateFanMode> &get_fan_mode() const;
 | 
			
		||||
  const optional<ClimateSwingMode> &get_swing_mode() const;
 | 
			
		||||
  const optional<ClimatePreset> &get_preset() const;
 | 
			
		||||
  const optional<std::string> &get_custom_fan_mode() const;
 | 
			
		||||
  const optional<ClimatePreset> &get_preset() const;
 | 
			
		||||
  const optional<std::string> &get_custom_preset() const;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void validate_();
 | 
			
		||||
 | 
			
		||||
  Climate *const parent_;
 | 
			
		||||
  optional<ClimateMode> mode_;
 | 
			
		||||
  optional<float> target_temperature_;
 | 
			
		||||
  optional<float> target_temperature_low_;
 | 
			
		||||
  optional<float> target_temperature_high_;
 | 
			
		||||
  optional<float> target_humidity_;
 | 
			
		||||
  optional<ClimateMode> mode_;
 | 
			
		||||
  optional<ClimateFanMode> fan_mode_;
 | 
			
		||||
  optional<ClimateSwingMode> swing_mode_;
 | 
			
		||||
  optional<ClimatePreset> preset_;
 | 
			
		||||
  optional<std::string> custom_fan_mode_;
 | 
			
		||||
  optional<ClimatePreset> preset_;
 | 
			
		||||
  optional<std::string> custom_preset_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -170,6 +169,47 @@ class Climate : public EntityBase {
 | 
			
		||||
 public:
 | 
			
		||||
  Climate() {}
 | 
			
		||||
 | 
			
		||||
  /// The active mode of the climate device.
 | 
			
		||||
  ClimateMode mode{CLIMATE_MODE_OFF};
 | 
			
		||||
 | 
			
		||||
  /// The active state of the climate device.
 | 
			
		||||
  ClimateAction action{CLIMATE_ACTION_OFF};
 | 
			
		||||
 | 
			
		||||
  /// The current temperature of the climate device, as reported from the integration.
 | 
			
		||||
  float current_temperature{NAN};
 | 
			
		||||
 | 
			
		||||
  /// The current humidity of the climate device, as reported from the integration.
 | 
			
		||||
  float current_humidity{NAN};
 | 
			
		||||
 | 
			
		||||
  union {
 | 
			
		||||
    /// The target temperature of the climate device.
 | 
			
		||||
    float target_temperature;
 | 
			
		||||
    struct {
 | 
			
		||||
      /// The minimum target temperature of the climate device, for climate devices with split target temperature.
 | 
			
		||||
      float target_temperature_low{NAN};
 | 
			
		||||
      /// The maximum target temperature of the climate device, for climate devices with split target temperature.
 | 
			
		||||
      float target_temperature_high{NAN};
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /// The target humidity of the climate device.
 | 
			
		||||
  float target_humidity;
 | 
			
		||||
 | 
			
		||||
  /// The active fan mode of the climate device.
 | 
			
		||||
  optional<ClimateFanMode> fan_mode;
 | 
			
		||||
 | 
			
		||||
  /// The active swing mode of the climate device.
 | 
			
		||||
  ClimateSwingMode swing_mode;
 | 
			
		||||
 | 
			
		||||
  /// The active custom fan mode of the climate device.
 | 
			
		||||
  optional<std::string> custom_fan_mode;
 | 
			
		||||
 | 
			
		||||
  /// The active preset of the climate device.
 | 
			
		||||
  optional<ClimatePreset> preset;
 | 
			
		||||
 | 
			
		||||
  /// The active custom preset mode of the climate device.
 | 
			
		||||
  optional<std::string> custom_preset;
 | 
			
		||||
 | 
			
		||||
  /** Add a callback for the climate device state, each time the state of the climate device is updated
 | 
			
		||||
   * (using publish_state), this callback will be called.
 | 
			
		||||
   *
 | 
			
		||||
@@ -211,47 +251,6 @@ class Climate : public EntityBase {
 | 
			
		||||
  void set_visual_min_humidity_override(float visual_min_humidity_override);
 | 
			
		||||
  void set_visual_max_humidity_override(float visual_max_humidity_override);
 | 
			
		||||
 | 
			
		||||
  /// The current temperature of the climate device, as reported from the integration.
 | 
			
		||||
  float current_temperature{NAN};
 | 
			
		||||
 | 
			
		||||
  /// The current humidity of the climate device, as reported from the integration.
 | 
			
		||||
  float current_humidity{NAN};
 | 
			
		||||
 | 
			
		||||
  union {
 | 
			
		||||
    /// The target temperature of the climate device.
 | 
			
		||||
    float target_temperature;
 | 
			
		||||
    struct {
 | 
			
		||||
      /// The minimum target temperature of the climate device, for climate devices with split target temperature.
 | 
			
		||||
      float target_temperature_low{NAN};
 | 
			
		||||
      /// The maximum target temperature of the climate device, for climate devices with split target temperature.
 | 
			
		||||
      float target_temperature_high{NAN};
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /// The target humidity of the climate device.
 | 
			
		||||
  float target_humidity;
 | 
			
		||||
 | 
			
		||||
  /// The active fan mode of the climate device.
 | 
			
		||||
  optional<ClimateFanMode> fan_mode;
 | 
			
		||||
 | 
			
		||||
  /// The active preset of the climate device.
 | 
			
		||||
  optional<ClimatePreset> preset;
 | 
			
		||||
 | 
			
		||||
  /// The active custom fan mode of the climate device.
 | 
			
		||||
  optional<std::string> custom_fan_mode;
 | 
			
		||||
 | 
			
		||||
  /// The active custom preset mode of the climate device.
 | 
			
		||||
  optional<std::string> custom_preset;
 | 
			
		||||
 | 
			
		||||
  /// The active mode of the climate device.
 | 
			
		||||
  ClimateMode mode{CLIMATE_MODE_OFF};
 | 
			
		||||
 | 
			
		||||
  /// The active state of the climate device.
 | 
			
		||||
  ClimateAction action{CLIMATE_ACTION_OFF};
 | 
			
		||||
 | 
			
		||||
  /// The active swing mode of the climate device.
 | 
			
		||||
  ClimateSwingMode swing_mode{CLIMATE_SWING_OFF};
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  friend ClimateCall;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <set>
 | 
			
		||||
#include "climate_mode.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "climate_mode.h"
 | 
			
		||||
#include <set>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
@@ -109,12 +109,44 @@ class ClimateTraits {
 | 
			
		||||
 | 
			
		||||
  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); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_cool_mode(bool supports_cool_mode) { set_mode_support_(CLIMATE_MODE_COOL, supports_cool_mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_heat_mode(bool supports_heat_mode) { set_mode_support_(CLIMATE_MODE_HEAT, supports_heat_mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_heat_cool_mode(bool supported) { set_mode_support_(CLIMATE_MODE_HEAT_COOL, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_only_mode(bool supports_fan_only_mode) {
 | 
			
		||||
    set_mode_support_(CLIMATE_MODE_FAN_ONLY, supports_fan_only_mode);
 | 
			
		||||
  }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); }
 | 
			
		||||
  bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
 | 
			
		||||
  const std::set<ClimateMode> &get_supported_modes() const { return this->supported_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_.insert(mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_off(bool supported) { set_fan_mode_support_(CLIMATE_FAN_OFF, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_auto(bool supported) { set_fan_mode_support_(CLIMATE_FAN_AUTO, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_low(bool supported) { set_fan_mode_support_(CLIMATE_FAN_LOW, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_medium(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MEDIUM, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_high(bool supported) { set_fan_mode_support_(CLIMATE_FAN_HIGH, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_middle(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MIDDLE, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); }
 | 
			
		||||
  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();
 | 
			
		||||
@@ -146,6 +178,16 @@ class ClimateTraits {
 | 
			
		||||
 | 
			
		||||
  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); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_swing_mode_both(bool supported) { set_swing_mode_support_(CLIMATE_SWING_BOTH, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_swing_mode_vertical(bool supported) { set_swing_mode_support_(CLIMATE_SWING_VERTICAL, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_swing_mode_horizontal(bool supported) {
 | 
			
		||||
    set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported);
 | 
			
		||||
  }
 | 
			
		||||
  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 std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; }
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,7 @@ static const char *const TAG = "climate_ir";
 | 
			
		||||
 | 
			
		||||
climate::ClimateTraits ClimateIR::traits() {
 | 
			
		||||
  auto traits = climate::ClimateTraits();
 | 
			
		||||
  if (this->sensor_ != nullptr) {
 | 
			
		||||
    traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  traits.set_supports_current_temperature(this->sensor_ != nullptr);
 | 
			
		||||
  traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL});
 | 
			
		||||
  if (this->supports_cool_)
 | 
			
		||||
    traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
 | 
			
		||||
@@ -22,6 +19,7 @@ climate::ClimateTraits ClimateIR::traits() {
 | 
			
		||||
  if (this->supports_fan_only_)
 | 
			
		||||
    traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY);
 | 
			
		||||
 | 
			
		||||
  traits.set_supports_two_point_target_temperature(false);
 | 
			
		||||
  traits.set_visual_min_temperature(this->minimum_temperature_);
 | 
			
		||||
  traits.set_visual_max_temperature(this->maximum_temperature_);
 | 
			
		||||
  traits.set_visual_temperature_step(this->temperature_step_);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#include "cover.h"
 | 
			
		||||
#include <strings.h>
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <strings.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace cover {
 | 
			
		||||
@@ -144,7 +144,21 @@ CoverCall &CoverCall::set_stop(bool stop) {
 | 
			
		||||
bool CoverCall::get_stop() const { return this->stop_; }
 | 
			
		||||
 | 
			
		||||
CoverCall Cover::make_call() { return {this}; }
 | 
			
		||||
 | 
			
		||||
void Cover::open() {
 | 
			
		||||
  auto call = this->make_call();
 | 
			
		||||
  call.set_command_open();
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
void Cover::close() {
 | 
			
		||||
  auto call = this->make_call();
 | 
			
		||||
  call.set_command_close();
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
void Cover::stop() {
 | 
			
		||||
  auto call = this->make_call();
 | 
			
		||||
  call.set_command_stop();
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
void Cover::add_on_state_callback(std::function<void()> &&f) { this->state_callback_.add(std::move(f)); }
 | 
			
		||||
void Cover::publish_state(bool save) {
 | 
			
		||||
  this->position = clamp(this->position, 0.0f, 1.0f);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
 | 
			
		||||
#include "cover_traits.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -126,6 +125,25 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
 | 
			
		||||
 | 
			
		||||
  /// Construct a new cover call used to control the cover.
 | 
			
		||||
  CoverCall make_call();
 | 
			
		||||
  /** Open the cover.
 | 
			
		||||
   *
 | 
			
		||||
   * This is a legacy method and may be removed later, please use `.make_call()` instead.
 | 
			
		||||
   */
 | 
			
		||||
  ESPDEPRECATED("open() is deprecated, use make_call().set_command_open().perform() instead.", "2021.9")
 | 
			
		||||
  void open();
 | 
			
		||||
  /** Close the cover.
 | 
			
		||||
   *
 | 
			
		||||
   * This is a legacy method and may be removed later, please use `.make_call()` instead.
 | 
			
		||||
   */
 | 
			
		||||
  ESPDEPRECATED("close() is deprecated, use make_call().set_command_close().perform() instead.", "2021.9")
 | 
			
		||||
  void close();
 | 
			
		||||
  /** Stop the cover.
 | 
			
		||||
   *
 | 
			
		||||
   * This is a legacy method and may be removed later, please use `.make_call()` instead.
 | 
			
		||||
   * As per solution from issue #2885 the call should include perform()
 | 
			
		||||
   */
 | 
			
		||||
  ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop().perform() instead.", "2021.9")
 | 
			
		||||
  void stop();
 | 
			
		||||
 | 
			
		||||
  void add_on_state_callback(std::function<void()> &&f);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -241,7 +241,9 @@ uint8_t DaikinArcClimate::humidity_() {
 | 
			
		||||
 | 
			
		||||
climate::ClimateTraits DaikinArcClimate::traits() {
 | 
			
		||||
  climate::ClimateTraits traits = climate_ir::ClimateIR::traits();
 | 
			
		||||
  traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
 | 
			
		||||
  traits.set_supports_current_temperature(true);
 | 
			
		||||
  traits.set_supports_current_humidity(false);
 | 
			
		||||
  traits.set_supports_target_humidity(true);
 | 
			
		||||
  traits.set_visual_min_humidity(38);
 | 
			
		||||
  traits.set_visual_max_humidity(52);
 | 
			
		||||
  return traits;
 | 
			
		||||
 
 | 
			
		||||
@@ -82,14 +82,16 @@ class DemoClimate : public climate::Climate, public Component {
 | 
			
		||||
    climate::ClimateTraits traits{};
 | 
			
		||||
    switch (type_) {
 | 
			
		||||
      case DemoClimateType::TYPE_1:
 | 
			
		||||
        traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
 | 
			
		||||
        traits.set_supports_current_temperature(true);
 | 
			
		||||
        traits.set_supported_modes({
 | 
			
		||||
            climate::CLIMATE_MODE_OFF,
 | 
			
		||||
            climate::CLIMATE_MODE_HEAT,
 | 
			
		||||
        });
 | 
			
		||||
        traits.set_supports_action(true);
 | 
			
		||||
        traits.set_visual_temperature_step(0.5);
 | 
			
		||||
        break;
 | 
			
		||||
      case DemoClimateType::TYPE_2:
 | 
			
		||||
        traits.set_supports_current_temperature(false);
 | 
			
		||||
        traits.set_supported_modes({
 | 
			
		||||
            climate::CLIMATE_MODE_OFF,
 | 
			
		||||
            climate::CLIMATE_MODE_HEAT,
 | 
			
		||||
@@ -98,7 +100,7 @@ class DemoClimate : public climate::Climate, public Component {
 | 
			
		||||
            climate::CLIMATE_MODE_DRY,
 | 
			
		||||
            climate::CLIMATE_MODE_FAN_ONLY,
 | 
			
		||||
        });
 | 
			
		||||
        traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
 | 
			
		||||
        traits.set_supports_action(true);
 | 
			
		||||
        traits.set_supported_fan_modes({
 | 
			
		||||
            climate::CLIMATE_FAN_ON,
 | 
			
		||||
            climate::CLIMATE_FAN_OFF,
 | 
			
		||||
@@ -121,8 +123,8 @@ class DemoClimate : public climate::Climate, public Component {
 | 
			
		||||
        traits.set_supported_custom_presets({"My Preset"});
 | 
			
		||||
        break;
 | 
			
		||||
      case DemoClimateType::TYPE_3:
 | 
			
		||||
        traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
 | 
			
		||||
                                 climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE);
 | 
			
		||||
        traits.set_supports_current_temperature(true);
 | 
			
		||||
        traits.set_supports_two_point_target_temperature(true);
 | 
			
		||||
        traits.set_supported_modes({
 | 
			
		||||
            climate::CLIMATE_MODE_OFF,
 | 
			
		||||
            climate::CLIMATE_MODE_COOL,
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,7 @@ bool EPaperBase::is_idle_() {
 | 
			
		||||
  if (this->busy_pin_ == nullptr) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  return this->busy_pin_->digital_read();
 | 
			
		||||
  return !this->busy_pin_->digital_read();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::reset() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import contextlib
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
import itertools
 | 
			
		||||
import logging
 | 
			
		||||
@@ -103,10 +102,6 @@ COMPILER_OPTIMIZATIONS = {
 | 
			
		||||
    "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Socket limit configuration for ESP-IDF
 | 
			
		||||
# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10
 | 
			
		||||
DEFAULT_MAX_SOCKETS = 10  # ESP-IDF default
 | 
			
		||||
 | 
			
		||||
ARDUINO_ALLOWED_VARIANTS = [
 | 
			
		||||
    VARIANT_ESP32,
 | 
			
		||||
    VARIANT_ESP32C3,
 | 
			
		||||
@@ -751,72 +746,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _configure_lwip_max_sockets(conf: dict) -> None:
 | 
			
		||||
    """Calculate and set CONFIG_LWIP_MAX_SOCKETS based on component needs.
 | 
			
		||||
 | 
			
		||||
    Socket component tracks consumer needs via consume_sockets() called during config validation.
 | 
			
		||||
    This function runs in to_code() after all components have registered their socket needs.
 | 
			
		||||
    User-provided sdkconfig_options take precedence.
 | 
			
		||||
    """
 | 
			
		||||
    from esphome.components.socket import KEY_SOCKET_CONSUMERS
 | 
			
		||||
 | 
			
		||||
    # Check if user manually specified CONFIG_LWIP_MAX_SOCKETS
 | 
			
		||||
    user_max_sockets = conf.get(CONF_SDKCONFIG_OPTIONS, {}).get(
 | 
			
		||||
        "CONFIG_LWIP_MAX_SOCKETS"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {})
 | 
			
		||||
    total_sockets = sum(socket_consumers.values())
 | 
			
		||||
 | 
			
		||||
    # Early return if no sockets registered and no user override
 | 
			
		||||
    if total_sockets == 0 and user_max_sockets is None:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    components_list = ", ".join(
 | 
			
		||||
        f"{name}={count}" for name, count in sorted(socket_consumers.items())
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # User specified their own value - respect it but warn if insufficient
 | 
			
		||||
    if user_max_sockets is not None:
 | 
			
		||||
        _LOGGER.info(
 | 
			
		||||
            "Using user-provided CONFIG_LWIP_MAX_SOCKETS: %s",
 | 
			
		||||
            user_max_sockets,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Warn if user's value is less than what components need
 | 
			
		||||
        if total_sockets > 0:
 | 
			
		||||
            user_sockets_int = 0
 | 
			
		||||
            with contextlib.suppress(ValueError, TypeError):
 | 
			
		||||
                user_sockets_int = int(user_max_sockets)
 | 
			
		||||
 | 
			
		||||
            if user_sockets_int < total_sockets:
 | 
			
		||||
                _LOGGER.warning(
 | 
			
		||||
                    "CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration "
 | 
			
		||||
                    "needs %d sockets (registered: %s). You may experience socket "
 | 
			
		||||
                    "exhaustion errors. Consider increasing to at least %d.",
 | 
			
		||||
                    user_sockets_int,
 | 
			
		||||
                    total_sockets,
 | 
			
		||||
                    components_list,
 | 
			
		||||
                    total_sockets,
 | 
			
		||||
                )
 | 
			
		||||
        # User's value already added via sdkconfig_options processing
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Auto-calculate based on component needs
 | 
			
		||||
    # Use at least the ESP-IDF default (10), or the total needed by components
 | 
			
		||||
    max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets)
 | 
			
		||||
 | 
			
		||||
    log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG
 | 
			
		||||
    _LOGGER.log(
 | 
			
		||||
        log_level,
 | 
			
		||||
        "Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)",
 | 
			
		||||
        max_sockets,
 | 
			
		||||
        components_list,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add_platformio_option("board", config[CONF_BOARD])
 | 
			
		||||
    cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
 | 
			
		||||
@@ -850,16 +779,6 @@ async def to_code(config):
 | 
			
		||||
        Path(__file__).parent / "post_build.py.script",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # In testing mode, add IRAM fix script to allow linking grouped component tests
 | 
			
		||||
    # Similar to ESP8266's approach but for ESP-IDF
 | 
			
		||||
    if CORE.testing_mode:
 | 
			
		||||
        cg.add_build_flag("-DESPHOME_TESTING_MODE")
 | 
			
		||||
        add_extra_script(
 | 
			
		||||
            "pre",
 | 
			
		||||
            "iram_fix.py",
 | 
			
		||||
            Path(__file__).parent / "iram_fix.py.script",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
 | 
			
		||||
        cg.add_platformio_option("framework", "espidf")
 | 
			
		||||
        cg.add_build_flag("-DUSE_ESP_IDF")
 | 
			
		||||
@@ -886,7 +805,6 @@ async def to_code(config):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
 | 
			
		||||
 | 
			
		||||
    cg.add_build_flag("-Wno-nonnull-compare")
 | 
			
		||||
 | 
			
		||||
@@ -937,9 +855,6 @@ async def to_code(config):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
 | 
			
		||||
    if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
 | 
			
		||||
 | 
			
		||||
    _configure_lwip_max_sockets(conf)
 | 
			
		||||
 | 
			
		||||
    if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@
 | 
			
		||||
#include <freertos/FreeRTOS.h>
 | 
			
		||||
#include <freertos/task.h>
 | 
			
		||||
#include <esp_idf_version.h>
 | 
			
		||||
#include <esp_ota_ops.h>
 | 
			
		||||
#include <esp_task_wdt.h>
 | 
			
		||||
#include <esp_timer.h>
 | 
			
		||||
#include <soc/rtc.h>
 | 
			
		||||
@@ -53,16 +52,6 @@ void arch_init() {
 | 
			
		||||
  disableCore1WDT();
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // If the bootloader was compiled with CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE the current
 | 
			
		||||
  // partition will get rolled back unless it is marked as valid.
 | 
			
		||||
  esp_ota_img_states_t state;
 | 
			
		||||
  const esp_partition_t *running = esp_ota_get_running_partition();
 | 
			
		||||
  if (esp_ota_get_state_partition(running, &state) == ESP_OK) {
 | 
			
		||||
    if (state == ESP_OTA_IMG_PENDING_VERIFY) {
 | 
			
		||||
      esp_ota_mark_app_valid_cancel_rollback();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,71 +0,0 @@
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
# pylint: disable=E0602
 | 
			
		||||
Import("env")  # noqa
 | 
			
		||||
 | 
			
		||||
# IRAM size for testing mode (2MB - large enough to accommodate grouped tests)
 | 
			
		||||
TESTING_IRAM_SIZE = 0x200000
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def patch_idf_linker_script(source, target, env):
 | 
			
		||||
    """Patch ESP-IDF linker script to increase IRAM size for testing mode."""
 | 
			
		||||
    # Check if we're in testing mode by looking for the define
 | 
			
		||||
    build_flags = env.get("BUILD_FLAGS", [])
 | 
			
		||||
    testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
 | 
			
		||||
 | 
			
		||||
    if not testing_mode:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # For ESP-IDF, the linker scripts are generated in the build directory
 | 
			
		||||
    build_dir = env.subst("$BUILD_DIR")
 | 
			
		||||
 | 
			
		||||
    # The memory.ld file is directly in the build directory
 | 
			
		||||
    memory_ld = os.path.join(build_dir, "memory.ld")
 | 
			
		||||
 | 
			
		||||
    if not os.path.exists(memory_ld):
 | 
			
		||||
        print(f"ESPHome: Warning - could not find linker script at {memory_ld}")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        with open(memory_ld, "r") as f:
 | 
			
		||||
            content = f.read()
 | 
			
		||||
    except OSError as e:
 | 
			
		||||
        print(f"ESPHome: Error reading linker script: {e}")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Check if this file contains iram0_0_seg
 | 
			
		||||
    if 'iram0_0_seg' not in content:
 | 
			
		||||
        print(f"ESPHome: Warning - iram0_0_seg not found in {memory_ld}")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Look for iram0_0_seg definition and increase its length
 | 
			
		||||
    # ESP-IDF format can be:
 | 
			
		||||
    #   iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0
 | 
			
		||||
    # or more complex with nested parentheses:
 | 
			
		||||
    #   iram0_0_seg (RX) : org = (0x40370000 + 0x4000), len = (((0x403CB700 - (0x40378000 - 0x3FC88000)) - 0x3FC88000) + 0x8000 - 0x4000)
 | 
			
		||||
    # We want to change len to TESTING_IRAM_SIZE for testing
 | 
			
		||||
 | 
			
		||||
    # Use a more robust approach: find the line and manually parse it
 | 
			
		||||
    lines = content.split('\n')
 | 
			
		||||
    for i, line in enumerate(lines):
 | 
			
		||||
        if 'iram0_0_seg' in line and 'len' in line:
 | 
			
		||||
            # Find the position of "len = " and replace everything after it until the end of the statement
 | 
			
		||||
            match = re.search(r'(iram0_0_seg\s*\([^)]*\)\s*:\s*org\s*=\s*(?:\([^)]+\)|0x[0-9a-fA-F]+)\s*,\s*len\s*=\s*)(.+?)(\s*)$', line)
 | 
			
		||||
            if match:
 | 
			
		||||
                lines[i] = f"{match.group(1)}{TESTING_IRAM_SIZE:#x}{match.group(3)}"
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
    updated = '\n'.join(lines)
 | 
			
		||||
 | 
			
		||||
    if updated != content:
 | 
			
		||||
        with open(memory_ld, "w") as f:
 | 
			
		||||
            f.write(updated)
 | 
			
		||||
        print(f"ESPHome: Patched IRAM size to {TESTING_IRAM_SIZE:#x} in {memory_ld} for testing mode")
 | 
			
		||||
    else:
 | 
			
		||||
        print(f"ESPHome: Warning - could not patch iram0_0_seg in {memory_ld}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Hook into the build process before linking
 | 
			
		||||
# For ESP-IDF, we need to run this after the linker scripts are generated
 | 
			
		||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_idf_linker_script)
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,12 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
 | 
			
		||||
      this->address_str_ = "";
 | 
			
		||||
    } else {
 | 
			
		||||
      char buf[18];
 | 
			
		||||
      format_mac_addr_upper(this->remote_bda_, buf);
 | 
			
		||||
      uint8_t mac[6] = {
 | 
			
		||||
          (uint8_t) ((this->address_ >> 40) & 0xff), (uint8_t) ((this->address_ >> 32) & 0xff),
 | 
			
		||||
          (uint8_t) ((this->address_ >> 24) & 0xff), (uint8_t) ((this->address_ >> 16) & 0xff),
 | 
			
		||||
          (uint8_t) ((this->address_ >> 8) & 0xff),  (uint8_t) ((this->address_ >> 0) & 0xff),
 | 
			
		||||
      };
 | 
			
		||||
      format_mac_addr_upper(mac, buf);
 | 
			
		||||
      this->address_str_ = buf;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_MODE, CONF_PORT
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@ayufan"]
 | 
			
		||||
AUTO_LOAD = ["camera"]
 | 
			
		||||
@@ -14,27 +13,13 @@ Mode = esp32_camera_web_server_ns.enum("Mode")
 | 
			
		||||
 | 
			
		||||
MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType:
 | 
			
		||||
    """Register socket needs for camera web server."""
 | 
			
		||||
    from esphome.components import socket
 | 
			
		||||
 | 
			
		||||
    # Each camera web server instance needs 1 listening socket + 2 client connections
 | 
			
		||||
    sockets_needed = 3
 | 
			
		||||
    socket.consume_sockets(sockets_needed, "esp32_camera_web_server")(config)
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(CameraWebServer),
 | 
			
		||||
            cv.Required(CONF_PORT): cv.port,
 | 
			
		||||
            cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
 | 
			
		||||
        },
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    _consume_camera_web_server_sockets,
 | 
			
		||||
)
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(CameraWebServer),
 | 
			
		||||
        cv.Required(CONF_PORT): cv.port,
 | 
			
		||||
        cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
 | 
			
		||||
    },
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
 
 | 
			
		||||
@@ -95,7 +95,7 @@ async def to_code(config):
 | 
			
		||||
    if framework_ver >= cv.Version(5, 5, 0):
 | 
			
		||||
        esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.1.5")
 | 
			
		||||
        esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.3")
 | 
			
		||||
        esp32.add_idf_component(name="espressif/esp_hosted", ref="2.6.1")
 | 
			
		||||
        esp32.add_idf_component(name="espressif/esp_hosted", ref="2.5.11")
 | 
			
		||||
    else:
 | 
			
		||||
        esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
 | 
			
		||||
        esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
from esphome import automation
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import binary_sensor, esp32_ble, improv_base, output
 | 
			
		||||
from esphome.components import binary_sensor, esp32_ble, output
 | 
			
		||||
from esphome.components.esp32_ble import BTLoggers
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["esp32_ble_server", "improv_base"]
 | 
			
		||||
AUTO_LOAD = ["esp32_ble_server"]
 | 
			
		||||
CODEOWNERS = ["@jesserockz"]
 | 
			
		||||
DEPENDENCIES = ["wifi", "esp32"]
 | 
			
		||||
 | 
			
		||||
@@ -20,7 +20,6 @@ CONF_ON_STOP = "on_stop"
 | 
			
		||||
CONF_STATUS_INDICATOR = "status_indicator"
 | 
			
		||||
CONF_WIFI_TIMEOUT = "wifi_timeout"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
improv_ns = cg.esphome_ns.namespace("improv")
 | 
			
		||||
Error = improv_ns.enum("Error")
 | 
			
		||||
State = improv_ns.enum("State")
 | 
			
		||||
@@ -44,63 +43,55 @@ ESP32ImprovStoppedTrigger = esp32_improv_ns.class_(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
 | 
			
		||||
            cv.Required(CONF_AUTHORIZER): cv.Any(
 | 
			
		||||
                cv.none, cv.use_id(binary_sensor.BinarySensor)
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput),
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
                CONF_IDENTIFY_DURATION, default="10s"
 | 
			
		||||
            ): cv.positive_time_period_milliseconds,
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
                CONF_AUTHORIZED_DURATION, default="1min"
 | 
			
		||||
            ): cv.positive_time_period_milliseconds,
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
                CONF_WIFI_TIMEOUT, default="1min"
 | 
			
		||||
            ): cv.positive_time_period_milliseconds,
 | 
			
		||||
            cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                        ESP32ImprovProvisionedTrigger
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                        ESP32ImprovProvisioningTrigger
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ON_START): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                        ESP32ImprovStartTrigger
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ON_STATE): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                        ESP32ImprovStateTrigger
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ON_STOP): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                        ESP32ImprovStoppedTrigger
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(improv_base.IMPROV_SCHEMA)
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
 | 
			
		||||
        cv.Required(CONF_AUTHORIZER): cv.Any(
 | 
			
		||||
            cv.none, cv.use_id(binary_sensor.BinarySensor)
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput),
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            CONF_IDENTIFY_DURATION, default="10s"
 | 
			
		||||
        ): cv.positive_time_period_milliseconds,
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            CONF_AUTHORIZED_DURATION, default="1min"
 | 
			
		||||
        ): cv.positive_time_period_milliseconds,
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            CONF_WIFI_TIMEOUT, default="1min"
 | 
			
		||||
        ): cv.positive_time_period_milliseconds,
 | 
			
		||||
        cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                    ESP32ImprovProvisionedTrigger
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                    ESP32ImprovProvisioningTrigger
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_START): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStartTrigger),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_STATE): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStateTrigger),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_STOP): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                    ESP32ImprovStoppedTrigger
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
@@ -111,8 +102,7 @@ async def to_code(config):
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_IMPROV")
 | 
			
		||||
 | 
			
		||||
    await improv_base.setup_improv_core(var, config)
 | 
			
		||||
    cg.add_library("improv/Improv", "1.2.4")
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION]))
 | 
			
		||||
    cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION]))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
#include "esp32_improv_component.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/bytebuffer/bytebuffer.h"
 | 
			
		||||
#include "esphome/components/esp32_ble/ble.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_server/ble_2902.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/components/bytebuffer/bytebuffer.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
@@ -384,32 +384,17 @@ void ESP32ImprovComponent::check_wifi_connection_() {
 | 
			
		||||
    this->connecting_sta_ = {};
 | 
			
		||||
    this->cancel_timeout("wifi-connect-timeout");
 | 
			
		||||
 | 
			
		||||
    // Build URL list with minimal allocations
 | 
			
		||||
    // Maximum 3 URLs: custom next_url + ESPHOME_MY_LINK + webserver URL
 | 
			
		||||
    std::string url_strings[3];
 | 
			
		||||
    size_t url_count = 0;
 | 
			
		||||
 | 
			
		||||
    // Add next_url if configured (should be first per Improv BLE spec)
 | 
			
		||||
    std::string next_url = this->get_formatted_next_url_();
 | 
			
		||||
    if (!next_url.empty()) {
 | 
			
		||||
      url_strings[url_count++] = std::move(next_url);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add default URLs for backward compatibility
 | 
			
		||||
    url_strings[url_count++] = ESPHOME_MY_LINK;
 | 
			
		||||
    std::vector<std::string> urls = {ESPHOME_MY_LINK};
 | 
			
		||||
#ifdef USE_WEBSERVER
 | 
			
		||||
    for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
 | 
			
		||||
      if (ip.is_ip4()) {
 | 
			
		||||
        char url_buffer[64];
 | 
			
		||||
        snprintf(url_buffer, sizeof(url_buffer), "http://%s:%d", ip.str().c_str(), USE_WEBSERVER_PORT);
 | 
			
		||||
        url_strings[url_count++] = url_buffer;
 | 
			
		||||
        std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
 | 
			
		||||
        urls.push_back(webserver_url);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    // 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));
 | 
			
		||||
    std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
 | 
			
		||||
    this->send_response_(data);
 | 
			
		||||
  } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
 | 
			
		||||
    ESP_LOGD(TAG, "WiFi provisioned externally");
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/esp32_ble_server/ble_characteristic.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_server/ble_server.h"
 | 
			
		||||
#include "esphome/components/improv_base/improv_base.h"
 | 
			
		||||
#include "esphome/components/wifi/wifi_component.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
 | 
			
		||||
@@ -33,7 +32,7 @@ namespace esp32_improv {
 | 
			
		||||
 | 
			
		||||
using namespace esp32_ble_server;
 | 
			
		||||
 | 
			
		||||
class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
 | 
			
		||||
class ESP32ImprovComponent : public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  ESP32ImprovComponent();
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 
 | 
			
		||||
@@ -190,9 +190,7 @@ async def to_code(config):
 | 
			
		||||
    cg.add_define("ESPHOME_VARIANT", "ESP8266")
 | 
			
		||||
    cg.add_define(ThreadModel.SINGLE)
 | 
			
		||||
 | 
			
		||||
    cg.add_platformio_option(
 | 
			
		||||
        "extra_scripts", ["pre:testing_mode.py", "post:post_build.py"]
 | 
			
		||||
    )
 | 
			
		||||
    cg.add_platformio_option("extra_scripts", ["pre:iram_fix.py", "post:post_build.py"])
 | 
			
		||||
 | 
			
		||||
    conf = config[CONF_FRAMEWORK]
 | 
			
		||||
    cg.add_platformio_option("framework", "arduino")
 | 
			
		||||
@@ -232,9 +230,9 @@ async def to_code(config):
 | 
			
		||||
    # For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
 | 
			
		||||
    cg.add_build_flag("-DNEW_OOM_ABORT")
 | 
			
		||||
 | 
			
		||||
    # In testing mode, fake larger memory to allow linking grouped component tests
 | 
			
		||||
    # Real ESP8266 hardware only has 32KB IRAM and ~80KB RAM, but for CI testing
 | 
			
		||||
    # we pretend it has much larger memory to test that components compile together
 | 
			
		||||
    # In testing mode, fake a larger IRAM to allow linking grouped component tests
 | 
			
		||||
    # Real ESP8266 hardware only has 32KB IRAM, but for CI testing we pretend it has 2MB
 | 
			
		||||
    # This is done via a pre-build script that generates a custom linker script
 | 
			
		||||
    if CORE.testing_mode:
 | 
			
		||||
        cg.add_build_flag("-DESPHOME_TESTING_MODE")
 | 
			
		||||
 | 
			
		||||
@@ -273,8 +271,8 @@ def copy_files():
 | 
			
		||||
        post_build_file,
 | 
			
		||||
        CORE.relative_build_path("post_build.py"),
 | 
			
		||||
    )
 | 
			
		||||
    testing_mode_file = dir / "testing_mode.py.script"
 | 
			
		||||
    iram_fix_file = dir / "iram_fix.py.script"
 | 
			
		||||
    copy_file_if_changed(
 | 
			
		||||
        testing_mode_file,
 | 
			
		||||
        CORE.relative_build_path("testing_mode.py"),
 | 
			
		||||
        iram_fix_file,
 | 
			
		||||
        CORE.relative_build_path("iram_fix.py"),
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								esphome/components/esp8266/iram_fix.py.script
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								esphome/components/esp8266/iram_fix.py.script
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
# pylint: disable=E0602
 | 
			
		||||
Import("env")  # noqa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def patch_linker_script_after_preprocess(source, target, env):
 | 
			
		||||
    """Patch the local linker script after PlatformIO preprocesses it."""
 | 
			
		||||
    # Check if we're in testing mode by looking for the define
 | 
			
		||||
    build_flags = env.get("BUILD_FLAGS", [])
 | 
			
		||||
    testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
 | 
			
		||||
 | 
			
		||||
    if not testing_mode:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Get the local linker script path
 | 
			
		||||
    build_dir = env.subst("$BUILD_DIR")
 | 
			
		||||
    local_ld = os.path.join(build_dir, "ld", "local.eagle.app.v6.common.ld")
 | 
			
		||||
 | 
			
		||||
    if not os.path.exists(local_ld):
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Read the linker script
 | 
			
		||||
    with open(local_ld, "r") as f:
 | 
			
		||||
        content = f.read()
 | 
			
		||||
 | 
			
		||||
    # Replace IRAM size from 0x8000 (32KB) to 0x200000 (2MB)
 | 
			
		||||
    # The line looks like: iram1_0_seg : org = 0x40100000, len = 0x8000
 | 
			
		||||
    updated = re.sub(
 | 
			
		||||
        r"(iram1_0_seg\s*:\s*org\s*=\s*0x40100000\s*,\s*len\s*=\s*)0x8000",
 | 
			
		||||
        r"\g<1>0x200000",
 | 
			
		||||
        content,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if updated != content:
 | 
			
		||||
        with open(local_ld, "w") as f:
 | 
			
		||||
            f.write(updated)
 | 
			
		||||
        print("ESPHome: Patched IRAM size to 2MB for testing mode")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Hook into the build process right before linking
 | 
			
		||||
# This runs after PlatformIO has already preprocessed the linker scripts
 | 
			
		||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_linker_script_after_preprocess)
 | 
			
		||||
@@ -1,166 +0,0 @@
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
# pylint: disable=E0602
 | 
			
		||||
Import("env")  # noqa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Memory sizes for testing mode (allow larger builds for CI component grouping)
 | 
			
		||||
TESTING_IRAM_SIZE = "0x200000"  # 2MB
 | 
			
		||||
TESTING_DRAM_SIZE = "0x200000"  # 2MB
 | 
			
		||||
TESTING_FLASH_SIZE = "0x2000000"  # 32MB
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def patch_segment_size(content, segment_name, new_size, label):
 | 
			
		||||
    """Patch a memory segment's length in linker script.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        content: Linker script content
 | 
			
		||||
        segment_name: Name of the segment (e.g., 'iram1_0_seg')
 | 
			
		||||
        new_size: New size as hex string (e.g., '0x200000')
 | 
			
		||||
        label: Human-readable label for logging (e.g., 'IRAM')
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        Tuple of (patched_content, was_patched)
 | 
			
		||||
    """
 | 
			
		||||
    # Match: segment_name : org = 0x..., len = 0x...
 | 
			
		||||
    pattern = rf"({segment_name}\s*:\s*org\s*=\s*0x[0-9a-fA-F]+\s*,\s*len\s*=\s*)0x[0-9a-fA-F]+"
 | 
			
		||||
    new_content = re.sub(pattern, rf"\g<1>{new_size}", content)
 | 
			
		||||
    return new_content, new_content != content
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def apply_memory_patches(content):
 | 
			
		||||
    """Apply IRAM, DRAM, and Flash patches to linker script content.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        content: Linker script content as string
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        Patched content as string
 | 
			
		||||
    """
 | 
			
		||||
    patches_applied = []
 | 
			
		||||
 | 
			
		||||
    # Patch IRAM (for larger code in IRAM)
 | 
			
		||||
    content, patched = patch_segment_size(content, "iram1_0_seg", TESTING_IRAM_SIZE, "IRAM")
 | 
			
		||||
    if patched:
 | 
			
		||||
        patches_applied.append("IRAM")
 | 
			
		||||
 | 
			
		||||
    # Patch DRAM (for larger BSS/data sections)
 | 
			
		||||
    content, patched = patch_segment_size(content, "dram0_0_seg", TESTING_DRAM_SIZE, "DRAM")
 | 
			
		||||
    if patched:
 | 
			
		||||
        patches_applied.append("DRAM")
 | 
			
		||||
 | 
			
		||||
    # Patch Flash (for larger code sections)
 | 
			
		||||
    content, patched = patch_segment_size(content, "irom0_0_seg", TESTING_FLASH_SIZE, "Flash")
 | 
			
		||||
    if patched:
 | 
			
		||||
        patches_applied.append("Flash")
 | 
			
		||||
 | 
			
		||||
    if patches_applied:
 | 
			
		||||
        iram_mb = int(TESTING_IRAM_SIZE, 16) // (1024 * 1024)
 | 
			
		||||
        dram_mb = int(TESTING_DRAM_SIZE, 16) // (1024 * 1024)
 | 
			
		||||
        flash_mb = int(TESTING_FLASH_SIZE, 16) // (1024 * 1024)
 | 
			
		||||
        print(f"  Patched memory segments: {', '.join(patches_applied)} (IRAM/DRAM: {iram_mb}MB, Flash: {flash_mb}MB)")
 | 
			
		||||
 | 
			
		||||
    return content
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def patch_linker_script_file(filepath, description):
 | 
			
		||||
    """Patch a linker script file in the build directory with enlarged memory segments.
 | 
			
		||||
 | 
			
		||||
    This function modifies linker scripts in the build directory only (never SDK files).
 | 
			
		||||
    It patches IRAM, DRAM, and Flash segments to allow larger builds in testing mode.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        filepath: Path to the linker script file in the build directory
 | 
			
		||||
        description: Human-readable description for logging
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        True if the file was patched, False if already patched or not found
 | 
			
		||||
    """
 | 
			
		||||
    if not os.path.exists(filepath):
 | 
			
		||||
        print(f"ESPHome: {description} not found at {filepath}")
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    print(f"ESPHome: Patching {description}...")
 | 
			
		||||
    with open(filepath, "r") as f:
 | 
			
		||||
        content = f.read()
 | 
			
		||||
 | 
			
		||||
    patched_content = apply_memory_patches(content)
 | 
			
		||||
 | 
			
		||||
    if patched_content != content:
 | 
			
		||||
        with open(filepath, "w") as f:
 | 
			
		||||
            f.write(patched_content)
 | 
			
		||||
        print(f"ESPHome: Successfully patched {description}")
 | 
			
		||||
        return True
 | 
			
		||||
    else:
 | 
			
		||||
        print(f"ESPHome: {description} already patched or no changes needed")
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def patch_local_linker_script(source, target, env):
 | 
			
		||||
    """Patch the local.eagle.app.v6.common.ld in build directory.
 | 
			
		||||
 | 
			
		||||
    This patches the preprocessed linker script that PlatformIO creates in the build
 | 
			
		||||
    directory, enlarging IRAM, DRAM, and Flash segments for testing mode.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        source: SCons source nodes
 | 
			
		||||
        target: SCons target nodes
 | 
			
		||||
        env: SCons environment
 | 
			
		||||
    """
 | 
			
		||||
    # Check if we're in testing mode
 | 
			
		||||
    build_flags = env.get("BUILD_FLAGS", [])
 | 
			
		||||
    testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
 | 
			
		||||
 | 
			
		||||
    if not testing_mode:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Patch the local linker script if it exists
 | 
			
		||||
    build_dir = env.subst("$BUILD_DIR")
 | 
			
		||||
    ld_dir = os.path.join(build_dir, "ld")
 | 
			
		||||
    if os.path.exists(ld_dir):
 | 
			
		||||
        local_ld = os.path.join(ld_dir, "local.eagle.app.v6.common.ld")
 | 
			
		||||
        if os.path.exists(local_ld):
 | 
			
		||||
            patch_linker_script_file(local_ld, "local.eagle.app.v6.common.ld")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Check if we're in testing mode
 | 
			
		||||
build_flags = env.get("BUILD_FLAGS", [])
 | 
			
		||||
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
 | 
			
		||||
 | 
			
		||||
if testing_mode:
 | 
			
		||||
    # Create a custom linker script in the build directory with patched memory limits
 | 
			
		||||
    # This allows larger IRAM/DRAM/Flash for CI component grouping tests
 | 
			
		||||
    build_dir = env.subst("$BUILD_DIR")
 | 
			
		||||
    ldscript = env.GetProjectOption("board_build.ldscript", "")
 | 
			
		||||
    assert ldscript, "No linker script configured in board_build.ldscript"
 | 
			
		||||
 | 
			
		||||
    framework_dir = env.PioPlatform().get_package_dir("framework-arduinoespressif8266")
 | 
			
		||||
    assert framework_dir is not None, "Could not find framework-arduinoespressif8266 package"
 | 
			
		||||
 | 
			
		||||
    # Read the original SDK linker script (read-only, SDK is never modified)
 | 
			
		||||
    sdk_ld = os.path.join(framework_dir, "tools", "sdk", "ld", ldscript)
 | 
			
		||||
    # Create a custom version in the build directory (isolated, temporary)
 | 
			
		||||
    custom_ld = os.path.join(build_dir, f"testing_{ldscript}")
 | 
			
		||||
 | 
			
		||||
    if os.path.exists(sdk_ld) and not os.path.exists(custom_ld):
 | 
			
		||||
        # Read the SDK linker script
 | 
			
		||||
        with open(sdk_ld, "r") as f:
 | 
			
		||||
            content = f.read()
 | 
			
		||||
 | 
			
		||||
        # Apply memory patches (IRAM: 2MB, DRAM: 2MB, Flash: 32MB)
 | 
			
		||||
        patched_content = apply_memory_patches(content)
 | 
			
		||||
 | 
			
		||||
        # Write the patched linker script to the build directory
 | 
			
		||||
        with open(custom_ld, "w") as f:
 | 
			
		||||
            f.write(patched_content)
 | 
			
		||||
 | 
			
		||||
        print(f"ESPHome: Created custom linker script: {custom_ld}")
 | 
			
		||||
 | 
			
		||||
    # Tell the linker to use our custom script from the build directory
 | 
			
		||||
    assert os.path.exists(custom_ld), f"Custom linker script not found: {custom_ld}"
 | 
			
		||||
    env.Replace(LDSCRIPT_PATH=custom_ld)
 | 
			
		||||
    print(f"ESPHome: Using custom linker script with patched memory limits")
 | 
			
		||||
 | 
			
		||||
    # Also patch local.eagle.app.v6.common.ld after PlatformIO creates it
 | 
			
		||||
    env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_local_linker_script)
 | 
			
		||||
@@ -103,16 +103,7 @@ def ota_esphome_final_validate(config):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
 | 
			
		||||
    """Register socket needs for OTA component."""
 | 
			
		||||
    from esphome.components import socket
 | 
			
		||||
 | 
			
		||||
    # OTA needs 1 listening socket (client connections are temporary during updates)
 | 
			
		||||
    socket.consume_sockets(1, "ota")(config)
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
 | 
			
		||||
@@ -139,8 +130,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(BASE_OTA_SCHEMA)
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    _consume_ota_sockets,
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
 | 
			
		||||
 
 | 
			
		||||
@@ -14,13 +14,13 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
 | 
			
		||||
  TEMPLATABLE_VALUE(std::vector<uint8_t>, data);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  void add_on_sent(const std::initializer_list<Action<Ts...> *> &actions) {
 | 
			
		||||
  void add_on_sent(const std::vector<Action<Ts...> *> &actions) {
 | 
			
		||||
    this->sent_.add_actions(actions);
 | 
			
		||||
    if (this->flags_.wait_for_sent) {
 | 
			
		||||
      this->sent_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); }));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  void add_on_error(const std::initializer_list<Action<Ts...> *> &actions) {
 | 
			
		||||
  void add_on_error(const std::vector<Action<Ts...> *> &actions) {
 | 
			
		||||
    this->error_.add_actions(actions);
 | 
			
		||||
    if (this->flags_.wait_for_sent) {
 | 
			
		||||
      this->error_.add_action(new LambdaAction<Ts...>([this](Ts... x) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,39 +0,0 @@
 | 
			
		||||
"""ESP-NOW transport platform for packet_transport component."""
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components.packet_transport import (
 | 
			
		||||
    PacketTransport,
 | 
			
		||||
    new_packet_transport,
 | 
			
		||||
    transport_schema,
 | 
			
		||||
)
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.core import HexInt
 | 
			
		||||
from esphome.cpp_types import PollingComponent
 | 
			
		||||
 | 
			
		||||
from .. import ESPNowComponent, espnow_ns
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@EasilyBoredEngineer"]
 | 
			
		||||
DEPENDENCIES = ["espnow"]
 | 
			
		||||
 | 
			
		||||
ESPNowTransport = espnow_ns.class_("ESPNowTransport", PacketTransport, PollingComponent)
 | 
			
		||||
 | 
			
		||||
CONF_ESPNOW_ID = "espnow_id"
 | 
			
		||||
CONF_PEER_ADDRESS = "peer_address"
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = transport_schema(ESPNowTransport).extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(CONF_ESPNOW_ID): cv.use_id(ESPNowComponent),
 | 
			
		||||
        cv.Optional(CONF_PEER_ADDRESS, default="FF:FF:FF:FF:FF:FF"): cv.mac_address,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    """Set up the ESP-NOW transport component."""
 | 
			
		||||
    var, _ = await new_packet_transport(config)
 | 
			
		||||
 | 
			
		||||
    await cg.register_parented(var, config[CONF_ESPNOW_ID])
 | 
			
		||||
 | 
			
		||||
    # Set peer address - convert MAC to parts array like ESP-NOW does
 | 
			
		||||
    mac = config[CONF_PEER_ADDRESS]
 | 
			
		||||
    cg.add(var.set_peer_address([HexInt(x) for x in mac.parts]))
 | 
			
		||||
@@ -1,97 +0,0 @@
 | 
			
		||||
#include "espnow_transport.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace espnow {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "espnow.transport";
 | 
			
		||||
 | 
			
		||||
bool ESPNowTransport::should_send() { return this->parent_ != nullptr && !this->parent_->is_failed(); }
 | 
			
		||||
 | 
			
		||||
void ESPNowTransport::setup() {
 | 
			
		||||
  packet_transport::PacketTransport::setup();
 | 
			
		||||
 | 
			
		||||
  if (this->parent_ == nullptr) {
 | 
			
		||||
    ESP_LOGE(TAG, "ESPNow component not set");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "Registering ESP-NOW handlers");
 | 
			
		||||
  ESP_LOGI(TAG, "Peer address: %02X:%02X:%02X:%02X:%02X:%02X", this->peer_address_[0], this->peer_address_[1],
 | 
			
		||||
           this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], this->peer_address_[5]);
 | 
			
		||||
 | 
			
		||||
  // Register received handler
 | 
			
		||||
  this->parent_->register_received_handler(static_cast<ESPNowReceivedPacketHandler *>(this));
 | 
			
		||||
 | 
			
		||||
  // Register broadcasted handler
 | 
			
		||||
  this->parent_->register_broadcasted_handler(static_cast<ESPNowBroadcastedHandler *>(this));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPNowTransport::update() {
 | 
			
		||||
  packet_transport::PacketTransport::update();
 | 
			
		||||
  this->updated_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
 | 
			
		||||
  if (this->parent_ == nullptr) {
 | 
			
		||||
    ESP_LOGE(TAG, "ESPNow component not set");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (buf.empty()) {
 | 
			
		||||
    ESP_LOGW(TAG, "Attempted to send empty packet");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (buf.size() > ESP_NOW_MAX_DATA_LEN) {
 | 
			
		||||
    ESP_LOGE(TAG, "Packet too large: %zu bytes (max %d)", buf.size(), ESP_NOW_MAX_DATA_LEN);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Send to configured peer address
 | 
			
		||||
  this->parent_->send(this->peer_address_.data(), buf.data(), buf.size(), [](esp_err_t err) {
 | 
			
		||||
    if (err != ESP_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "Send failed: %d", err);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ESPNowTransport::on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
 | 
			
		||||
  ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
 | 
			
		||||
           info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
 | 
			
		||||
 | 
			
		||||
  if (data == nullptr || size == 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Received empty or null packet");
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->packet_buffer_.resize(size);
 | 
			
		||||
  memcpy(this->packet_buffer_.data(), data, size);
 | 
			
		||||
  this->process_(this->packet_buffer_);
 | 
			
		||||
  return false;  // Allow other handlers to run
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ESPNowTransport::on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
 | 
			
		||||
  ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
 | 
			
		||||
           info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
 | 
			
		||||
 | 
			
		||||
  if (data == nullptr || size == 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Received empty or null broadcast packet");
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->packet_buffer_.resize(size);
 | 
			
		||||
  memcpy(this->packet_buffer_.data(), data, size);
 | 
			
		||||
  this->process_(this->packet_buffer_);
 | 
			
		||||
  return false;  // Allow other handlers to run
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace espnow
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
@@ -1,44 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "../espnow_component.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/packet_transport/packet_transport.h"
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace espnow {
 | 
			
		||||
 | 
			
		||||
class ESPNowTransport : public packet_transport::PacketTransport,
 | 
			
		||||
                        public Parented<ESPNowComponent>,
 | 
			
		||||
                        public ESPNowReceivedPacketHandler,
 | 
			
		||||
                        public ESPNowBroadcastedHandler {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
 | 
			
		||||
 | 
			
		||||
  void set_peer_address(peer_address_t address) {
 | 
			
		||||
    memcpy(this->peer_address_.data(), address.data(), ESP_NOW_ETH_ALEN);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // ESPNow handler interface
 | 
			
		||||
  bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
 | 
			
		||||
  bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void send_packet(const std::vector<uint8_t> &buf) const override;
 | 
			
		||||
  size_t get_max_packet_size() override { return ESP_NOW_MAX_DATA_LEN; }
 | 
			
		||||
  bool should_send() override;
 | 
			
		||||
 | 
			
		||||
  peer_address_t peer_address_{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
 | 
			
		||||
  std::vector<uint8_t> packet_buffer_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace espnow
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
@@ -38,6 +38,7 @@ IS_PLATFORM_COMPONENT = True
 | 
			
		||||
 | 
			
		||||
fan_ns = cg.esphome_ns.namespace("fan")
 | 
			
		||||
Fan = fan_ns.class_("Fan", cg.EntityBase)
 | 
			
		||||
FanState = fan_ns.class_("Fan", Fan, cg.Component)
 | 
			
		||||
 | 
			
		||||
FanDirection = fan_ns.enum("FanDirection", is_class=True)
 | 
			
		||||
FAN_DIRECTION_ENUM = {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "fan.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "fan_state.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace fan {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								esphome/components/fan/fan_state.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								esphome/components/fan/fan_state.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
#include "fan_state.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace fan {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "fan";
 | 
			
		||||
 | 
			
		||||
void FanState::setup() {
 | 
			
		||||
  auto restore = this->restore_state_();
 | 
			
		||||
  if (restore)
 | 
			
		||||
    restore->to_call(*this).perform();
 | 
			
		||||
}
 | 
			
		||||
float FanState::get_setup_priority() const { return setup_priority::DATA - 1.0f; }
 | 
			
		||||
 | 
			
		||||
}  // namespace fan
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										34
									
								
								esphome/components/fan/fan_state.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								esphome/components/fan/fan_state.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "fan.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace fan {
 | 
			
		||||
 | 
			
		||||
enum ESPDEPRECATED("LegacyFanDirection members are deprecated, use FanDirection instead.",
 | 
			
		||||
                   "2022.2") LegacyFanDirection {
 | 
			
		||||
  FAN_DIRECTION_FORWARD = 0,
 | 
			
		||||
  FAN_DIRECTION_REVERSE = 1
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ESPDEPRECATED("FanState is deprecated, use Fan instead.", "2022.2") FanState : public Fan, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  FanState() = default;
 | 
			
		||||
 | 
			
		||||
  /// Get the traits of this fan.
 | 
			
		||||
  FanTraits get_traits() override { return this->traits_; }
 | 
			
		||||
  /// Set the traits of this fan (i.e. what features it supports).
 | 
			
		||||
  void set_traits(const FanTraits &traits) { this->traits_ = traits; }
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(const FanCall &call) override { this->publish_state(); }
 | 
			
		||||
 | 
			
		||||
  FanTraits traits_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace fan
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -65,7 +65,7 @@ HaierClimateBase::HaierClimateBase()
 | 
			
		||||
      {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH});
 | 
			
		||||
  this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH,
 | 
			
		||||
                                           climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
 | 
			
		||||
  this->traits_.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
 | 
			
		||||
  this->traits_.set_supports_current_temperature(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
HaierClimateBase::~HaierClimateBase() {}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,8 +16,7 @@ void HDC1080Component::setup() {
 | 
			
		||||
 | 
			
		||||
  // if configuration fails - there is a problem
 | 
			
		||||
  if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "Failed to configure HDC1080");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,42 +6,31 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace improv_base {
 | 
			
		||||
 | 
			
		||||
static constexpr const char DEVICE_NAME_PLACEHOLDER[] = "{{device_name}}";
 | 
			
		||||
static constexpr size_t DEVICE_NAME_PLACEHOLDER_LEN = sizeof(DEVICE_NAME_PLACEHOLDER) - 1;
 | 
			
		||||
static constexpr const char IP_ADDRESS_PLACEHOLDER[] = "{{ip_address}}";
 | 
			
		||||
static constexpr size_t IP_ADDRESS_PLACEHOLDER_LEN = sizeof(IP_ADDRESS_PLACEHOLDER) - 1;
 | 
			
		||||
 | 
			
		||||
static void replace_all_in_place(std::string &str, const char *placeholder, size_t placeholder_len,
 | 
			
		||||
                                 const std::string &replacement) {
 | 
			
		||||
  size_t pos = 0;
 | 
			
		||||
  const size_t replacement_len = replacement.length();
 | 
			
		||||
  while ((pos = str.find(placeholder, pos)) != std::string::npos) {
 | 
			
		||||
    str.replace(pos, placeholder_len, replacement);
 | 
			
		||||
    pos += replacement_len;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string ImprovBase::get_formatted_next_url_() {
 | 
			
		||||
  if (this->next_url_.empty()) {
 | 
			
		||||
    return "";
 | 
			
		||||
  }
 | 
			
		||||
  std::string copy = this->next_url_;
 | 
			
		||||
  // Device name
 | 
			
		||||
  std::size_t pos = this->next_url_.find("{{device_name}}");
 | 
			
		||||
  if (pos != std::string::npos) {
 | 
			
		||||
    const std::string &device_name = App.get_name();
 | 
			
		||||
    copy.replace(pos, 15, device_name);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::string formatted_url = this->next_url_;
 | 
			
		||||
 | 
			
		||||
  // Replace all occurrences of {{device_name}}
 | 
			
		||||
  replace_all_in_place(formatted_url, DEVICE_NAME_PLACEHOLDER, DEVICE_NAME_PLACEHOLDER_LEN, App.get_name());
 | 
			
		||||
 | 
			
		||||
  // Replace all occurrences of {{ip_address}}
 | 
			
		||||
  for (auto &ip : network::get_ip_addresses()) {
 | 
			
		||||
    if (ip.is_ip4()) {
 | 
			
		||||
      replace_all_in_place(formatted_url, IP_ADDRESS_PLACEHOLDER, IP_ADDRESS_PLACEHOLDER_LEN, ip.str());
 | 
			
		||||
      break;
 | 
			
		||||
  // Ip address
 | 
			
		||||
  pos = this->next_url_.find("{{ip_address}}");
 | 
			
		||||
  if (pos != std::string::npos) {
 | 
			
		||||
    for (auto &ip : network::get_ip_addresses()) {
 | 
			
		||||
      if (ip.is_ip4()) {
 | 
			
		||||
        std::string ipa = ip.str();
 | 
			
		||||
        copy.replace(pos, 14, ipa);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Note: {{esphome_version}} is replaced at code generation time in Python
 | 
			
		||||
 | 
			
		||||
  return formatted_url;
 | 
			
		||||
  return copy;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace improv_base
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
 | 
			
		||||
  auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); };
 | 
			
		||||
 | 
			
		||||
  this->waiting_ = false;
 | 
			
		||||
  ESP_LOGV(TAG, "Data: %s", format_hex_pretty(data).c_str());
 | 
			
		||||
  ESP_LOGV(TAG, "Data: %s", hexencode(data).c_str());
 | 
			
		||||
 | 
			
		||||
  float value = (float) get_16bit(0);
 | 
			
		||||
  for (int i = 0; i < data[3]; i++)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/color.h"
 | 
			
		||||
#include "esp_color_correction.h"
 | 
			
		||||
#include "esp_color_view.h"
 | 
			
		||||
#include "esp_range_view.h"
 | 
			
		||||
#include "esphome/core/color.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "light_output.h"
 | 
			
		||||
#include "light_state.h"
 | 
			
		||||
#include "transformers.h"
 | 
			
		||||
@@ -17,6 +17,8 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace light {
 | 
			
		||||
 | 
			
		||||
using ESPColor ESPDEPRECATED("esphome::light::ESPColor is deprecated, use esphome::Color instead.", "v1.21") = Color;
 | 
			
		||||
 | 
			
		||||
/// Convert the color information from a `LightColorValues` object to a `Color` object (does not apply brightness).
 | 
			
		||||
Color color_from_light_color_values(LightColorValues val);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -104,200 +104,5 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) {
 | 
			
		||||
  return static_cast<ColorMode>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Type alias for raw color mode bitmask values
 | 
			
		||||
using color_mode_bitmask_t = uint16_t;
 | 
			
		||||
 | 
			
		||||
// Constants for ColorMode count and bit range
 | 
			
		||||
static constexpr int COLOR_MODE_COUNT = 10;                             // UNKNOWN through RGB_COLD_WARM_WHITE
 | 
			
		||||
static constexpr int MAX_BIT_INDEX = sizeof(color_mode_bitmask_t) * 8;  // Number of bits in bitmask type
 | 
			
		||||
 | 
			
		||||
// Compile-time array of all ColorMode values in declaration order
 | 
			
		||||
// Bit positions (0-9) map directly to enum declaration order
 | 
			
		||||
static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = {
 | 
			
		||||
    ColorMode::UNKNOWN,                // bit 0
 | 
			
		||||
    ColorMode::ON_OFF,                 // bit 1
 | 
			
		||||
    ColorMode::BRIGHTNESS,             // bit 2
 | 
			
		||||
    ColorMode::WHITE,                  // bit 3
 | 
			
		||||
    ColorMode::COLOR_TEMPERATURE,      // bit 4
 | 
			
		||||
    ColorMode::COLD_WARM_WHITE,        // bit 5
 | 
			
		||||
    ColorMode::RGB,                    // bit 6
 | 
			
		||||
    ColorMode::RGB_WHITE,              // bit 7
 | 
			
		||||
    ColorMode::RGB_COLOR_TEMPERATURE,  // bit 8
 | 
			
		||||
    ColorMode::RGB_COLD_WARM_WHITE,    // bit 9
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Map ColorMode enum values to bit positions (0-9)
 | 
			
		||||
/// Bit positions follow the enum declaration order
 | 
			
		||||
static constexpr int mode_to_bit(ColorMode mode) {
 | 
			
		||||
  // Linear search through COLOR_MODES array
 | 
			
		||||
  // Compiler optimizes this to efficient code since array is constexpr
 | 
			
		||||
  for (int i = 0; i < COLOR_MODE_COUNT; ++i) {
 | 
			
		||||
    if (COLOR_MODES[i] == mode)
 | 
			
		||||
      return i;
 | 
			
		||||
  }
 | 
			
		||||
  return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Map bit positions (0-9) to ColorMode enum values
 | 
			
		||||
/// Bit positions follow the enum declaration order
 | 
			
		||||
static constexpr ColorMode bit_to_mode(int bit) {
 | 
			
		||||
  // Direct lookup in COLOR_MODES array
 | 
			
		||||
  return (bit >= 0 && bit < COLOR_MODE_COUNT) ? COLOR_MODES[bit] : ColorMode::UNKNOWN;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Helper to compute capability bitmask at compile time
 | 
			
		||||
static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability capability) {
 | 
			
		||||
  color_mode_bitmask_t mask = 0;
 | 
			
		||||
  uint8_t cap_bit = static_cast<uint8_t>(capability);
 | 
			
		||||
 | 
			
		||||
  // Check each ColorMode to see if it has this capability
 | 
			
		||||
  for (int bit = 0; bit < COLOR_MODE_COUNT; ++bit) {
 | 
			
		||||
    uint8_t mode_val = static_cast<uint8_t>(bit_to_mode(bit));
 | 
			
		||||
    if ((mode_val & cap_bit) != 0) {
 | 
			
		||||
      mask |= (1 << bit);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return mask;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Number of ColorCapability enum values
 | 
			
		||||
static constexpr int COLOR_CAPABILITY_COUNT = 6;
 | 
			
		||||
 | 
			
		||||
/// Compile-time lookup table mapping ColorCapability to bitmask
 | 
			
		||||
/// This array is computed at compile time using constexpr
 | 
			
		||||
static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = {
 | 
			
		||||
    compute_capability_bitmask(ColorCapability::ON_OFF),             // 1 << 0
 | 
			
		||||
    compute_capability_bitmask(ColorCapability::BRIGHTNESS),         // 1 << 1
 | 
			
		||||
    compute_capability_bitmask(ColorCapability::WHITE),              // 1 << 2
 | 
			
		||||
    compute_capability_bitmask(ColorCapability::COLOR_TEMPERATURE),  // 1 << 3
 | 
			
		||||
    compute_capability_bitmask(ColorCapability::COLD_WARM_WHITE),    // 1 << 4
 | 
			
		||||
    compute_capability_bitmask(ColorCapability::RGB),                // 1 << 5
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Bitmask for storing a set of ColorMode values efficiently.
 | 
			
		||||
/// Replaces std::set<ColorMode> to eliminate red-black tree overhead (~586 bytes).
 | 
			
		||||
class ColorModeMask {
 | 
			
		||||
 public:
 | 
			
		||||
  constexpr ColorModeMask() = default;
 | 
			
		||||
 | 
			
		||||
  /// Support initializer list syntax: {ColorMode::RGB, ColorMode::WHITE}
 | 
			
		||||
  constexpr ColorModeMask(std::initializer_list<ColorMode> modes) {
 | 
			
		||||
    for (auto mode : modes) {
 | 
			
		||||
      this->add(mode);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); }
 | 
			
		||||
 | 
			
		||||
  /// Add multiple modes at once using initializer list
 | 
			
		||||
  constexpr void add(std::initializer_list<ColorMode> modes) {
 | 
			
		||||
    for (auto mode : modes) {
 | 
			
		||||
      this->add(mode);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; }
 | 
			
		||||
 | 
			
		||||
  constexpr size_t size() const {
 | 
			
		||||
    // Count set bits using Brian Kernighan's algorithm
 | 
			
		||||
    // More efficient for sparse bitmasks (typical case: 2-4 modes out of 10)
 | 
			
		||||
    uint16_t n = this->mask_;
 | 
			
		||||
    size_t count = 0;
 | 
			
		||||
    while (n) {
 | 
			
		||||
      n &= n - 1;  // Clear the least significant set bit
 | 
			
		||||
      count++;
 | 
			
		||||
    }
 | 
			
		||||
    return count;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constexpr bool empty() const { return this->mask_ == 0; }
 | 
			
		||||
 | 
			
		||||
  /// Iterator support for API encoding
 | 
			
		||||
  class Iterator {
 | 
			
		||||
   public:
 | 
			
		||||
    using iterator_category = std::forward_iterator_tag;
 | 
			
		||||
    using value_type = ColorMode;
 | 
			
		||||
    using difference_type = std::ptrdiff_t;
 | 
			
		||||
    using pointer = const ColorMode *;
 | 
			
		||||
    using reference = ColorMode;
 | 
			
		||||
 | 
			
		||||
    constexpr Iterator(color_mode_bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); }
 | 
			
		||||
 | 
			
		||||
    constexpr ColorMode operator*() const { return bit_to_mode(bit_); }
 | 
			
		||||
 | 
			
		||||
    constexpr Iterator &operator++() {
 | 
			
		||||
      ++bit_;
 | 
			
		||||
      advance_to_next_set_bit_();
 | 
			
		||||
      return *this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; }
 | 
			
		||||
 | 
			
		||||
    constexpr bool operator!=(const Iterator &other) const { return !(*this == other); }
 | 
			
		||||
 | 
			
		||||
   private:
 | 
			
		||||
    constexpr void advance_to_next_set_bit_() { bit_ = ColorModeMask::find_next_set_bit(mask_, bit_); }
 | 
			
		||||
 | 
			
		||||
    color_mode_bitmask_t mask_;
 | 
			
		||||
    int bit_;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  constexpr Iterator begin() const { return Iterator(mask_, 0); }
 | 
			
		||||
  constexpr Iterator end() const { return Iterator(mask_, MAX_BIT_INDEX); }
 | 
			
		||||
 | 
			
		||||
  /// Get the raw bitmask value for API encoding
 | 
			
		||||
  constexpr color_mode_bitmask_t get_mask() const { return this->mask_; }
 | 
			
		||||
 | 
			
		||||
  /// Find the next set bit in a bitmask starting from a given position
 | 
			
		||||
  /// Returns the bit position, or MAX_BIT_INDEX if no more bits are set
 | 
			
		||||
  static constexpr int find_next_set_bit(color_mode_bitmask_t mask, int start_bit) {
 | 
			
		||||
    int bit = start_bit;
 | 
			
		||||
    while (bit < MAX_BIT_INDEX && !(mask & (1 << bit))) {
 | 
			
		||||
      ++bit;
 | 
			
		||||
    }
 | 
			
		||||
    return bit;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Find the first set bit in a bitmask and return the corresponding ColorMode
 | 
			
		||||
  /// Used for optimizing compute_color_mode_() intersection logic
 | 
			
		||||
  static constexpr ColorMode first_mode_from_mask(color_mode_bitmask_t mask) {
 | 
			
		||||
    return bit_to_mode(find_next_set_bit(mask, 0));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Check if a ColorMode is present in a raw bitmask value
 | 
			
		||||
  /// Useful for checking intersection results without creating a temporary ColorModeMask
 | 
			
		||||
  static constexpr bool mask_contains(color_mode_bitmask_t mask, ColorMode mode) {
 | 
			
		||||
    return (mask & (1 << mode_to_bit(mode))) != 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Check if any mode in the bitmask has a specific capability
 | 
			
		||||
  /// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB)
 | 
			
		||||
  bool has_capability(ColorCapability capability) const {
 | 
			
		||||
    // Lookup the pre-computed bitmask for this capability and check intersection with our mask
 | 
			
		||||
    // ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5
 | 
			
		||||
    // We need to convert the power-of-2 value to an index
 | 
			
		||||
    uint8_t cap_val = static_cast<uint8_t>(capability);
 | 
			
		||||
#if defined(__GNUC__) || defined(__clang__)
 | 
			
		||||
    // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n))
 | 
			
		||||
    int index = __builtin_ctz(cap_val);
 | 
			
		||||
#else
 | 
			
		||||
    // Fallback for compilers without __builtin_ctz
 | 
			
		||||
    int index = 0;
 | 
			
		||||
    while (cap_val > 1) {
 | 
			
		||||
      cap_val >>= 1;
 | 
			
		||||
      ++index;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    return (this->mask_ & CAPABILITY_BITMASKS[index]) != 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  // Using uint16_t instead of uint32_t for more efficient iteration (fewer bits to scan).
 | 
			
		||||
  // Currently only 10 ColorMode values exist, so 16 bits is sufficient.
 | 
			
		||||
  // Can be changed to uint32_t if more than 16 color modes are needed in the future.
 | 
			
		||||
  // Note: Due to struct padding, uint16_t and uint32_t result in same LightTraits size (12 bytes).
 | 
			
		||||
  color_mode_bitmask_t mask_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace light
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -406,7 +406,7 @@ void LightCall::transform_parameters_() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
ColorMode LightCall::compute_color_mode_() {
 | 
			
		||||
  const auto &supported_modes = this->parent_->get_traits().get_supported_color_modes();
 | 
			
		||||
  auto supported_modes = this->parent_->get_traits().get_supported_color_modes();
 | 
			
		||||
  int supported_count = supported_modes.size();
 | 
			
		||||
 | 
			
		||||
  // Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown.
 | 
			
		||||
@@ -425,19 +425,20 @@ ColorMode LightCall::compute_color_mode_() {
 | 
			
		||||
  // If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to
 | 
			
		||||
  // pre-colormode clients and automations, but also for the MQTT API, where HA doesn't let us know which color mode
 | 
			
		||||
  // was used for some reason.
 | 
			
		||||
  // Compute intersection of suitable and supported modes using bitwise AND
 | 
			
		||||
  color_mode_bitmask_t intersection = this->get_suitable_color_modes_mask_() & supported_modes.get_mask();
 | 
			
		||||
  std::set<ColorMode> suitable_modes = this->get_suitable_color_modes_();
 | 
			
		||||
 | 
			
		||||
  // Don't change if the current mode is in the intersection (suitable AND supported)
 | 
			
		||||
  if (ColorModeMask::mask_contains(intersection, current_mode)) {
 | 
			
		||||
  // Don't change if the current mode is suitable.
 | 
			
		||||
  if (suitable_modes.count(current_mode) > 0) {
 | 
			
		||||
    ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(),
 | 
			
		||||
             LOG_STR_ARG(color_mode_to_human(current_mode)));
 | 
			
		||||
    return current_mode;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Use the preferred suitable mode.
 | 
			
		||||
  if (intersection != 0) {
 | 
			
		||||
    ColorMode mode = ColorModeMask::first_mode_from_mask(intersection);
 | 
			
		||||
  for (auto mode : suitable_modes) {
 | 
			
		||||
    if (supported_modes.count(mode) == 0)
 | 
			
		||||
      continue;
 | 
			
		||||
 | 
			
		||||
    ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(),
 | 
			
		||||
             LOG_STR_ARG(color_mode_to_human(mode)));
 | 
			
		||||
    return mode;
 | 
			
		||||
@@ -450,7 +451,7 @@ ColorMode LightCall::compute_color_mode_() {
 | 
			
		||||
           LOG_STR_ARG(color_mode_to_human(color_mode)));
 | 
			
		||||
  return color_mode;
 | 
			
		||||
}
 | 
			
		||||
color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
 | 
			
		||||
std::set<ColorMode> LightCall::get_suitable_color_modes_() {
 | 
			
		||||
  bool has_white = this->has_white() && this->white_ > 0.0f;
 | 
			
		||||
  bool has_ct = this->has_color_temperature();
 | 
			
		||||
  bool has_cwww =
 | 
			
		||||
@@ -458,44 +459,36 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
 | 
			
		||||
  bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) ||
 | 
			
		||||
                 (this->has_red() || this->has_green() || this->has_blue());
 | 
			
		||||
 | 
			
		||||
  // Build key from flags: [rgb][cwww][ct][white]
 | 
			
		||||
// Build key from flags: [rgb][cwww][ct][white]
 | 
			
		||||
#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3)
 | 
			
		||||
 | 
			
		||||
  uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb);
 | 
			
		||||
 | 
			
		||||
  switch (key) {
 | 
			
		||||
    case KEY(true, false, false, false):  // white only
 | 
			
		||||
      return ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
 | 
			
		||||
                            ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE})
 | 
			
		||||
          .get_mask();
 | 
			
		||||
      return {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
 | 
			
		||||
              ColorMode::RGB_COLD_WARM_WHITE};
 | 
			
		||||
    case KEY(false, true, false, false):  // ct only
 | 
			
		||||
      return ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
 | 
			
		||||
                            ColorMode::RGB_COLD_WARM_WHITE})
 | 
			
		||||
          .get_mask();
 | 
			
		||||
      return {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
 | 
			
		||||
              ColorMode::RGB_COLD_WARM_WHITE};
 | 
			
		||||
    case KEY(true, true, false, false):  // white + ct
 | 
			
		||||
      return ColorModeMask(
 | 
			
		||||
                 {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
 | 
			
		||||
          .get_mask();
 | 
			
		||||
      return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
 | 
			
		||||
    case KEY(false, false, true, false):  // cwww only
 | 
			
		||||
      return ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
 | 
			
		||||
      return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE};
 | 
			
		||||
    case KEY(false, false, false, false):  // none
 | 
			
		||||
      return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE,
 | 
			
		||||
                            ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE})
 | 
			
		||||
          .get_mask();
 | 
			
		||||
      return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB,
 | 
			
		||||
              ColorMode::WHITE,     ColorMode::COLOR_TEMPERATURE,     ColorMode::COLD_WARM_WHITE};
 | 
			
		||||
    case KEY(true, false, false, true):  // rgb + white
 | 
			
		||||
      return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
 | 
			
		||||
          .get_mask();
 | 
			
		||||
      return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
 | 
			
		||||
    case KEY(false, true, false, true):  // rgb + ct
 | 
			
		||||
    case KEY(true, true, false, true):   // rgb + white + ct
 | 
			
		||||
      return ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
 | 
			
		||||
      return {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
 | 
			
		||||
    case KEY(false, false, true, true):  // rgb + cwww
 | 
			
		||||
      return ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
 | 
			
		||||
      return {ColorMode::RGB_COLD_WARM_WHITE};
 | 
			
		||||
    case KEY(false, false, false, true):  // rgb only
 | 
			
		||||
      return ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
 | 
			
		||||
                            ColorMode::RGB_COLD_WARM_WHITE})
 | 
			
		||||
          .get_mask();
 | 
			
		||||
      return {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
 | 
			
		||||
    default:
 | 
			
		||||
      return 0;  // conflicting flags
 | 
			
		||||
      return {};  // conflicting flags
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#undef KEY
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "light_color_values.h"
 | 
			
		||||
#include <set>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
@@ -185,8 +186,8 @@ class LightCall {
 | 
			
		||||
 | 
			
		||||
  //// Compute the color mode that should be used for this call.
 | 
			
		||||
  ColorMode compute_color_mode_();
 | 
			
		||||
  /// Get potential color modes bitmask for this light call.
 | 
			
		||||
  color_mode_bitmask_t get_suitable_color_modes_mask_();
 | 
			
		||||
  /// Get potential color modes for this light call.
 | 
			
		||||
  std::set<ColorMode> get_suitable_color_modes_();
 | 
			
		||||
  /// Some color modes also can be set using non-native parameters, transform those calls.
 | 
			
		||||
  void transform_parameters_();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto values = state.remote_values;
 | 
			
		||||
  auto traits = state.get_output()->get_traits();
 | 
			
		||||
 | 
			
		||||
  const auto color_mode = values.get_color_mode();
 | 
			
		||||
  const char *mode_str = get_color_mode_json_str(color_mode);
 | 
			
		||||
 
 | 
			
		||||
@@ -191,9 +191,11 @@ void LightState::current_values_as_brightness(float *brightness) {
 | 
			
		||||
  this->current_values.as_brightness(brightness, this->gamma_correct_);
 | 
			
		||||
}
 | 
			
		||||
void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) {
 | 
			
		||||
  auto traits = this->get_traits();
 | 
			
		||||
  this->current_values.as_rgb(red, green, blue, this->gamma_correct_, false);
 | 
			
		||||
}
 | 
			
		||||
void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) {
 | 
			
		||||
  auto traits = this->get_traits();
 | 
			
		||||
  this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, false);
 | 
			
		||||
}
 | 
			
		||||
void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
 | 
			
		||||
@@ -207,6 +209,7 @@ void LightState::current_values_as_rgbct(float *red, float *green, float *blue,
 | 
			
		||||
                                white_brightness, this->gamma_correct_);
 | 
			
		||||
}
 | 
			
		||||
void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) {
 | 
			
		||||
  auto traits = this->get_traits();
 | 
			
		||||
  this->current_values.as_cwww(cold_white, warm_white, this->gamma_correct_, constant_brightness);
 | 
			
		||||
}
 | 
			
		||||
void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "color_mode.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "color_mode.h"
 | 
			
		||||
#include <set>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
@@ -18,17 +19,38 @@ class LightTraits {
 | 
			
		||||
 public:
 | 
			
		||||
  LightTraits() = default;
 | 
			
		||||
 | 
			
		||||
  const ColorModeMask &get_supported_color_modes() const { return this->supported_color_modes_; }
 | 
			
		||||
  void set_supported_color_modes(ColorModeMask supported_color_modes) {
 | 
			
		||||
    this->supported_color_modes_ = supported_color_modes;
 | 
			
		||||
  }
 | 
			
		||||
  void set_supported_color_modes(std::initializer_list<ColorMode> modes) {
 | 
			
		||||
    this->supported_color_modes_ = ColorModeMask(modes);
 | 
			
		||||
  const std::set<ColorMode> &get_supported_color_modes() const { return this->supported_color_modes_; }
 | 
			
		||||
  void set_supported_color_modes(std::set<ColorMode> supported_color_modes) {
 | 
			
		||||
    this->supported_color_modes_ = std::move(supported_color_modes);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); }
 | 
			
		||||
  bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode); }
 | 
			
		||||
  bool supports_color_capability(ColorCapability color_capability) const {
 | 
			
		||||
    return this->supported_color_modes_.has_capability(color_capability);
 | 
			
		||||
    for (auto mode : this->supported_color_modes_) {
 | 
			
		||||
      if (mode & color_capability)
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESPDEPRECATED("get_supports_brightness() is deprecated, use color modes instead.", "v1.21")
 | 
			
		||||
  bool get_supports_brightness() const { return this->supports_color_capability(ColorCapability::BRIGHTNESS); }
 | 
			
		||||
  ESPDEPRECATED("get_supports_rgb() is deprecated, use color modes instead.", "v1.21")
 | 
			
		||||
  bool get_supports_rgb() const { return this->supports_color_capability(ColorCapability::RGB); }
 | 
			
		||||
  ESPDEPRECATED("get_supports_rgb_white_value() is deprecated, use color modes instead.", "v1.21")
 | 
			
		||||
  bool get_supports_rgb_white_value() const {
 | 
			
		||||
    return this->supports_color_mode(ColorMode::RGB_WHITE) ||
 | 
			
		||||
           this->supports_color_mode(ColorMode::RGB_COLOR_TEMPERATURE);
 | 
			
		||||
  }
 | 
			
		||||
  ESPDEPRECATED("get_supports_color_temperature() is deprecated, use color modes instead.", "v1.21")
 | 
			
		||||
  bool get_supports_color_temperature() const {
 | 
			
		||||
    return this->supports_color_capability(ColorCapability::COLOR_TEMPERATURE);
 | 
			
		||||
  }
 | 
			
		||||
  ESPDEPRECATED("get_supports_color_interlock() is deprecated, use color modes instead.", "v1.21")
 | 
			
		||||
  bool get_supports_color_interlock() const {
 | 
			
		||||
    return this->supports_color_mode(ColorMode::RGB) &&
 | 
			
		||||
           (this->supports_color_mode(ColorMode::WHITE) || this->supports_color_mode(ColorMode::COLD_WARM_WHITE) ||
 | 
			
		||||
            this->supports_color_mode(ColorMode::COLOR_TEMPERATURE));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  float get_min_mireds() const { return this->min_mireds_; }
 | 
			
		||||
@@ -37,9 +59,19 @@ class LightTraits {
 | 
			
		||||
  void set_max_mireds(float max_mireds) { this->max_mireds_ = max_mireds; }
 | 
			
		||||
 | 
			
		||||
 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 color 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<ColorMode> &get_supported_color_modes_for_api_() const { return this->supported_color_modes_; }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  std::set<ColorMode> supported_color_modes_{};
 | 
			
		||||
  float min_mireds_{0};
 | 
			
		||||
  float max_mireds_{0};
 | 
			
		||||
  ColorModeMask supported_color_modes_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace light
 | 
			
		||||
 
 | 
			
		||||
@@ -68,9 +68,6 @@ static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
 | 
			
		||||
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
 | 
			
		||||
static constexpr uint16_t MAX_HEADER_SIZE = 128;
 | 
			
		||||
 | 
			
		||||
// "0x" + 2 hex digits per byte + '\0'
 | 
			
		||||
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
 | 
			
		||||
/** Enum for logging UART selection
 | 
			
		||||
 *
 | 
			
		||||
@@ -180,11 +177,8 @@ class Logger : public Component {
 | 
			
		||||
  inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format,
 | 
			
		||||
                                                        va_list args, char *buffer, uint16_t *buffer_at,
 | 
			
		||||
                                                        uint16_t buffer_size) {
 | 
			
		||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
 | 
			
		||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
 | 
			
		||||
    this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size);
 | 
			
		||||
#elif defined(USE_ZEPHYR)
 | 
			
		||||
    char buff[MAX_POINTER_REPRESENTATION];
 | 
			
		||||
    this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(buff), buffer, buffer_at, buffer_size);
 | 
			
		||||
#else
 | 
			
		||||
    this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size);
 | 
			
		||||
#endif
 | 
			
		||||
@@ -283,11 +277,7 @@ class Logger : public Component {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
 | 
			
		||||
  const char *HOT get_thread_name_(
 | 
			
		||||
#ifdef USE_ZEPHYR
 | 
			
		||||
      char *buff
 | 
			
		||||
#endif
 | 
			
		||||
  ) {
 | 
			
		||||
  const char *HOT get_thread_name_() {
 | 
			
		||||
#ifdef USE_ZEPHYR
 | 
			
		||||
    k_tid_t current_task = k_current_get();
 | 
			
		||||
#else
 | 
			
		||||
@@ -301,13 +291,7 @@ class Logger : public Component {
 | 
			
		||||
#elif defined(USE_LIBRETINY)
 | 
			
		||||
      return pcTaskGetTaskName(current_task);
 | 
			
		||||
#elif defined(USE_ZEPHYR)
 | 
			
		||||
      const char *name = k_thread_name_get(current_task);
 | 
			
		||||
      if (name) {
 | 
			
		||||
        // zephyr print task names only if debug component is present
 | 
			
		||||
        return name;
 | 
			
		||||
      }
 | 
			
		||||
      std::snprintf(buff, MAX_POINTER_REPRESENTATION, "%p", current_task);
 | 
			
		||||
      return buff;
 | 
			
		||||
      return k_thread_name_get(current_task);
 | 
			
		||||
#endif
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
 | 
			
		||||
    this->update_reg_(pin, false, iodir);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
 | 
			
		||||
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
 | 
			
		||||
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
 | 
			
		||||
  if (this->is_failed())
 | 
			
		||||
    return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,6 @@ from esphome.const import (
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, Lambda, coroutine_with_priority
 | 
			
		||||
from esphome.coroutine import CoroPriority
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
DEPENDENCIES = ["network"]
 | 
			
		||||
@@ -47,19 +46,6 @@ SERVICE_SCHEMA = cv.Schema(
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _consume_mdns_sockets(config: ConfigType) -> ConfigType:
 | 
			
		||||
    """Register socket needs for mDNS component."""
 | 
			
		||||
    if config.get(CONF_DISABLED):
 | 
			
		||||
        return config
 | 
			
		||||
 | 
			
		||||
    from esphome.components import socket
 | 
			
		||||
 | 
			
		||||
    # mDNS needs 2 sockets (IPv4 + IPv6 multicast)
 | 
			
		||||
    socket.consume_sockets(2, "mdns")(config)
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
@@ -69,7 +55,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
    _remove_id_if_disabled,
 | 
			
		||||
    _consume_mdns_sockets,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -31,17 +31,18 @@ void MDNSComponent::setup() {
 | 
			
		||||
  mdns_instance_name_set(this->hostname_.c_str());
 | 
			
		||||
 | 
			
		||||
  for (const auto &service : services) {
 | 
			
		||||
    auto txt_records = std::make_unique<mdns_txt_item_t[]>(service.txt_records.size());
 | 
			
		||||
    for (size_t i = 0; i < service.txt_records.size(); i++) {
 | 
			
		||||
      const auto &record = service.txt_records[i];
 | 
			
		||||
    std::vector<mdns_txt_item_t> txt_records;
 | 
			
		||||
    for (const auto &record : service.txt_records) {
 | 
			
		||||
      mdns_txt_item_t it{};
 | 
			
		||||
      // key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_
 | 
			
		||||
      // Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies
 | 
			
		||||
      txt_records[i].key = MDNS_STR_ARG(record.key);
 | 
			
		||||
      txt_records[i].value = MDNS_STR_ARG(record.value);
 | 
			
		||||
      it.key = MDNS_STR_ARG(record.key);
 | 
			
		||||
      it.value = MDNS_STR_ARG(record.value);
 | 
			
		||||
      txt_records.push_back(it);
 | 
			
		||||
    }
 | 
			
		||||
    uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
 | 
			
		||||
    err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port,
 | 
			
		||||
                           txt_records.get(), service.txt_records.size());
 | 
			
		||||
                           txt_records.data(), txt_records.size());
 | 
			
		||||
 | 
			
		||||
    if (err != ESP_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err));
 | 
			
		||||
 
 | 
			
		||||
@@ -77,7 +77,7 @@ void AirConditioner::control(const ClimateCall &call) {
 | 
			
		||||
 | 
			
		||||
ClimateTraits AirConditioner::traits() {
 | 
			
		||||
  auto traits = ClimateTraits();
 | 
			
		||||
  traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
 | 
			
		||||
  traits.set_supports_current_temperature(true);
 | 
			
		||||
  traits.set_visual_min_temperature(17);
 | 
			
		||||
  traits.set_visual_max_temperature(30);
 | 
			
		||||
  traits.set_visual_temperature_step(0.5);
 | 
			
		||||
 
 | 
			
		||||
@@ -30,19 +30,6 @@ wave_4_3 = DriverChip(
 | 
			
		||||
        "blue": [14, 38, 18, 17, 10],
 | 
			
		||||
    },
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
wave_4_3.extend(
 | 
			
		||||
    "WAVESHARE-5-1024X600",
 | 
			
		||||
    width=1024,
 | 
			
		||||
    height=600,
 | 
			
		||||
    hsync_back_porch=145,
 | 
			
		||||
    hsync_front_porch=170,
 | 
			
		||||
    hsync_pulse_width=30,
 | 
			
		||||
    vsync_back_porch=23,
 | 
			
		||||
    vsync_front_porch=12,
 | 
			
		||||
    vsync_pulse_width=2,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
wave_4_3.extend(
 | 
			
		||||
    "ESP32-S3-TOUCH-LCD-7-800X480",
 | 
			
		||||
    enable_pin=[{"ch422g": None, "number": 2}, {"ch422g": None, "number": 6}],
 | 
			
		||||
 
 | 
			
		||||
@@ -52,9 +52,8 @@ const uint8_t MITSUBISHI_BYTE16 = 0x00;
 | 
			
		||||
 | 
			
		||||
climate::ClimateTraits MitsubishiClimate::traits() {
 | 
			
		||||
  auto traits = climate::ClimateTraits();
 | 
			
		||||
  if (this->sensor_ != nullptr) {
 | 
			
		||||
    traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
 | 
			
		||||
  }
 | 
			
		||||
  traits.set_supports_current_temperature(this->sensor_ != nullptr);
 | 
			
		||||
  traits.set_supports_action(false);
 | 
			
		||||
  traits.set_visual_min_temperature(MITSUBISHI_TEMP_MIN);
 | 
			
		||||
  traits.set_visual_max_temperature(MITSUBISHI_TEMP_MAX);
 | 
			
		||||
  traits.set_visual_temperature_step(1.0f);
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,6 @@ from esphome.const import (
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["network"]
 | 
			
		||||
 | 
			
		||||
@@ -211,15 +210,6 @@ def validate_fingerprint(value):
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _consume_mqtt_sockets(config: ConfigType) -> ConfigType:
 | 
			
		||||
    """Register socket needs for MQTT component."""
 | 
			
		||||
    from esphome.components import socket
 | 
			
		||||
 | 
			
		||||
    # MQTT needs 1 socket for the broker connection
 | 
			
		||||
    socket.consume_sockets(1, "mqtt")(config)
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
@@ -316,7 +306,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    ),
 | 
			
		||||
    validate_config,
 | 
			
		||||
    cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]),
 | 
			
		||||
    _consume_mqtt_sockets,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -140,8 +140,11 @@ void MQTTClientComponent::send_device_info_() {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
        root[api::global_api_server->get_noise_ctx()->has_psk() ? "api_encryption" : "api_encryption_supported"] =
 | 
			
		||||
            "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
 | 
			
		||||
        if (api::global_api_server->get_noise_ctx()->has_psk()) {
 | 
			
		||||
          root["api_encryption"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
 | 
			
		||||
        } else {
 | 
			
		||||
          root["api_encryption_supported"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
 | 
			
		||||
        }
 | 
			
		||||
#endif
 | 
			
		||||
      },
 | 
			
		||||
      2, this->discovery_info_.retain);
 | 
			
		||||
 
 | 
			
		||||
@@ -17,11 +17,11 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
 | 
			
		||||
  // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
 | 
			
		||||
  auto traits = this->device_->get_traits();
 | 
			
		||||
  // current_temperature_topic
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
 | 
			
		||||
  if (traits.get_supports_current_temperature()) {
 | 
			
		||||
    root[MQTT_CURRENT_TEMPERATURE_TOPIC] = this->get_current_temperature_state_topic();
 | 
			
		||||
  }
 | 
			
		||||
  // current_humidity_topic
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
 | 
			
		||||
  if (traits.get_supports_current_humidity()) {
 | 
			
		||||
    root[MQTT_CURRENT_HUMIDITY_TOPIC] = this->get_current_humidity_state_topic();
 | 
			
		||||
  }
 | 
			
		||||
  // mode_command_topic
 | 
			
		||||
@@ -45,8 +45,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
 | 
			
		||||
  if (traits.supports_mode(CLIMATE_MODE_HEAT_COOL))
 | 
			
		||||
    modes.add("heat_cool");
 | 
			
		||||
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
 | 
			
		||||
                               climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
 | 
			
		||||
  if (traits.get_supports_two_point_target_temperature()) {
 | 
			
		||||
    // temperature_low_command_topic
 | 
			
		||||
    root[MQTT_TEMPERATURE_LOW_COMMAND_TOPIC] = this->get_target_temperature_low_command_topic();
 | 
			
		||||
    // temperature_low_state_topic
 | 
			
		||||
@@ -62,7 +61,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
 | 
			
		||||
    root[MQTT_TEMPERATURE_STATE_TOPIC] = this->get_target_temperature_state_topic();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
 | 
			
		||||
  if (traits.get_supports_target_humidity()) {
 | 
			
		||||
    // target_humidity_command_topic
 | 
			
		||||
    root[MQTT_TARGET_HUMIDITY_COMMAND_TOPIC] = this->get_target_humidity_command_topic();
 | 
			
		||||
    // target_humidity_state_topic
 | 
			
		||||
@@ -110,7 +109,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
 | 
			
		||||
      presets.add(preset);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
 | 
			
		||||
  if (traits.get_supports_action()) {
 | 
			
		||||
    // action_topic
 | 
			
		||||
    root[MQTT_ACTION_TOPIC] = this->get_action_state_topic();
 | 
			
		||||
  }
 | 
			
		||||
@@ -175,8 +174,7 @@ void MQTTClimateComponent::setup() {
 | 
			
		||||
    call.perform();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
 | 
			
		||||
                               climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
 | 
			
		||||
  if (traits.get_supports_two_point_target_temperature()) {
 | 
			
		||||
    this->subscribe(this->get_target_temperature_low_command_topic(),
 | 
			
		||||
                    [this](const std::string &topic, const std::string &payload) {
 | 
			
		||||
                      auto val = parse_number<float>(payload);
 | 
			
		||||
@@ -213,7 +211,7 @@ void MQTTClimateComponent::setup() {
 | 
			
		||||
                    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
 | 
			
		||||
  if (traits.get_supports_target_humidity()) {
 | 
			
		||||
    this->subscribe(this->get_target_humidity_command_topic(),
 | 
			
		||||
                    [this](const std::string &topic, const std::string &payload) {
 | 
			
		||||
                      auto val = parse_number<float>(payload);
 | 
			
		||||
@@ -292,14 +290,12 @@ bool MQTTClimateComponent::publish_state_() {
 | 
			
		||||
    success = false;
 | 
			
		||||
  int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
 | 
			
		||||
  int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) &&
 | 
			
		||||
      !std::isnan(this->device_->current_temperature)) {
 | 
			
		||||
  if (traits.get_supports_current_temperature() && !std::isnan(this->device_->current_temperature)) {
 | 
			
		||||
    std::string payload = value_accuracy_to_string(this->device_->current_temperature, current_accuracy);
 | 
			
		||||
    if (!this->publish(this->get_current_temperature_state_topic(), payload))
 | 
			
		||||
      success = false;
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
 | 
			
		||||
                               climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
 | 
			
		||||
  if (traits.get_supports_two_point_target_temperature()) {
 | 
			
		||||
    std::string payload = value_accuracy_to_string(this->device_->target_temperature_low, target_accuracy);
 | 
			
		||||
    if (!this->publish(this->get_target_temperature_low_state_topic(), payload))
 | 
			
		||||
      success = false;
 | 
			
		||||
@@ -312,14 +308,12 @@ bool MQTTClimateComponent::publish_state_() {
 | 
			
		||||
      success = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) &&
 | 
			
		||||
      !std::isnan(this->device_->current_humidity)) {
 | 
			
		||||
  if (traits.get_supports_current_humidity() && !std::isnan(this->device_->current_humidity)) {
 | 
			
		||||
    std::string payload = value_accuracy_to_string(this->device_->current_humidity, 0);
 | 
			
		||||
    if (!this->publish(this->get_current_humidity_state_topic(), payload))
 | 
			
		||||
      success = false;
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) &&
 | 
			
		||||
      !std::isnan(this->device_->target_humidity)) {
 | 
			
		||||
  if (traits.get_supports_target_humidity() && !std::isnan(this->device_->target_humidity)) {
 | 
			
		||||
    std::string payload = value_accuracy_to_string(this->device_->target_humidity, 0);
 | 
			
		||||
    if (!this->publish(this->get_target_humidity_state_topic(), payload))
 | 
			
		||||
      success = false;
 | 
			
		||||
@@ -363,7 +357,7 @@ bool MQTTClimateComponent::publish_state_() {
 | 
			
		||||
      success = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
 | 
			
		||||
  if (traits.get_supports_action()) {
 | 
			
		||||
    const char *payload;
 | 
			
		||||
    switch (this->device_->action) {
 | 
			
		||||
      case CLIMATE_ACTION_OFF:
 | 
			
		||||
 
 | 
			
		||||
@@ -85,20 +85,24 @@ bool MQTTComponent::send_discovery_() {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Fields from EntityBase
 | 
			
		||||
        root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name() : "";
 | 
			
		||||
 | 
			
		||||
        if (this->get_entity()->has_own_name()) {
 | 
			
		||||
          root[MQTT_NAME] = this->friendly_name();
 | 
			
		||||
        } else {
 | 
			
		||||
          root[MQTT_NAME] = "";
 | 
			
		||||
        }
 | 
			
		||||
        if (this->is_disabled_by_default())
 | 
			
		||||
          root[MQTT_ENABLED_BY_DEFAULT] = false;
 | 
			
		||||
        if (!this->get_icon().empty())
 | 
			
		||||
          root[MQTT_ICON] = this->get_icon();
 | 
			
		||||
 | 
			
		||||
        const auto entity_category = this->get_entity()->get_entity_category();
 | 
			
		||||
        switch (entity_category) {
 | 
			
		||||
        switch (this->get_entity()->get_entity_category()) {
 | 
			
		||||
          case ENTITY_CATEGORY_NONE:
 | 
			
		||||
            break;
 | 
			
		||||
          case ENTITY_CATEGORY_CONFIG:
 | 
			
		||||
            root[MQTT_ENTITY_CATEGORY] = "config";
 | 
			
		||||
            break;
 | 
			
		||||
          case ENTITY_CATEGORY_DIAGNOSTIC:
 | 
			
		||||
            root[MQTT_ENTITY_CATEGORY] = entity_category == ENTITY_CATEGORY_CONFIG ? "config" : "diagnostic";
 | 
			
		||||
            root[MQTT_ENTITY_CATEGORY] = "diagnostic";
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -109,14 +113,20 @@ bool MQTTComponent::send_discovery_() {
 | 
			
		||||
        if (this->command_retain_)
 | 
			
		||||
          root[MQTT_COMMAND_RETAIN] = true;
 | 
			
		||||
 | 
			
		||||
        const Availability &avail =
 | 
			
		||||
            this->availability_ == nullptr ? global_mqtt_client->get_availability() : *this->availability_;
 | 
			
		||||
        if (!avail.topic.empty()) {
 | 
			
		||||
          root[MQTT_AVAILABILITY_TOPIC] = avail.topic;
 | 
			
		||||
          if (avail.payload_available != "online")
 | 
			
		||||
            root[MQTT_PAYLOAD_AVAILABLE] = avail.payload_available;
 | 
			
		||||
          if (avail.payload_not_available != "offline")
 | 
			
		||||
            root[MQTT_PAYLOAD_NOT_AVAILABLE] = avail.payload_not_available;
 | 
			
		||||
        if (this->availability_ == nullptr) {
 | 
			
		||||
          if (!global_mqtt_client->get_availability().topic.empty()) {
 | 
			
		||||
            root[MQTT_AVAILABILITY_TOPIC] = global_mqtt_client->get_availability().topic;
 | 
			
		||||
            if (global_mqtt_client->get_availability().payload_available != "online")
 | 
			
		||||
              root[MQTT_PAYLOAD_AVAILABLE] = global_mqtt_client->get_availability().payload_available;
 | 
			
		||||
            if (global_mqtt_client->get_availability().payload_not_available != "offline")
 | 
			
		||||
              root[MQTT_PAYLOAD_NOT_AVAILABLE] = global_mqtt_client->get_availability().payload_not_available;
 | 
			
		||||
          }
 | 
			
		||||
        } else if (!this->availability_->topic.empty()) {
 | 
			
		||||
          root[MQTT_AVAILABILITY_TOPIC] = this->availability_->topic;
 | 
			
		||||
          if (this->availability_->payload_available != "online")
 | 
			
		||||
            root[MQTT_PAYLOAD_AVAILABLE] = this->availability_->payload_available;
 | 
			
		||||
          if (this->availability_->payload_not_available != "offline")
 | 
			
		||||
            root[MQTT_PAYLOAD_NOT_AVAILABLE] = this->availability_->payload_not_available;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
 | 
			
		||||
@@ -135,8 +145,10 @@ bool MQTTComponent::send_discovery_() {
 | 
			
		||||
        if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR)
 | 
			
		||||
          root[MQTT_OBJECT_ID] = node_name + "_" + this->get_default_object_id_();
 | 
			
		||||
 | 
			
		||||
        const std::string &friendly_name_ref = App.get_friendly_name();
 | 
			
		||||
        const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref;
 | 
			
		||||
        std::string node_friendly_name = App.get_friendly_name();
 | 
			
		||||
        if (node_friendly_name.empty()) {
 | 
			
		||||
          node_friendly_name = node_name;
 | 
			
		||||
        }
 | 
			
		||||
        std::string node_area = App.get_area();
 | 
			
		||||
 | 
			
		||||
        JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
 | 
			
		||||
@@ -146,9 +158,13 @@ bool MQTTComponent::send_discovery_() {
 | 
			
		||||
#ifdef ESPHOME_PROJECT_NAME
 | 
			
		||||
        device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")";
 | 
			
		||||
        const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.');
 | 
			
		||||
        device_info[MQTT_DEVICE_MODEL] = model == nullptr ? ESPHOME_BOARD : model + 1;
 | 
			
		||||
        device_info[MQTT_DEVICE_MANUFACTURER] =
 | 
			
		||||
            model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
 | 
			
		||||
        if (model == nullptr) {  // must never happen but check anyway
 | 
			
		||||
          device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
 | 
			
		||||
          device_info[MQTT_DEVICE_MANUFACTURER] = ESPHOME_PROJECT_NAME;
 | 
			
		||||
        } else {
 | 
			
		||||
          device_info[MQTT_DEVICE_MODEL] = model + 1;
 | 
			
		||||
          device_info[MQTT_DEVICE_MANUFACTURER] = std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
 | 
			
		||||
        }
 | 
			
		||||
#else
 | 
			
		||||
        device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_VERSION " (" + App.get_compilation_time() + ")";
 | 
			
		||||
        device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
#ifdef USE_MQTT
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/fan/fan.h"
 | 
			
		||||
#include "esphome/components/fan/fan_state.h"
 | 
			
		||||
#include "mqtt_component.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 
 | 
			
		||||
@@ -69,12 +69,6 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
 | 
			
		||||
  if (traits.supports_color_capability(ColorCapability::BRIGHTNESS))
 | 
			
		||||
    root["brightness"] = true;
 | 
			
		||||
 | 
			
		||||
  if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) ||
 | 
			
		||||
      traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) {
 | 
			
		||||
    root[MQTT_MIN_MIREDS] = traits.get_min_mireds();
 | 
			
		||||
    root[MQTT_MAX_MIREDS] = traits.get_max_mireds();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->state_->supports_effects()) {
 | 
			
		||||
    root["effect"] = true;
 | 
			
		||||
    JsonArray effect_list = root[MQTT_EFFECT_LIST].to<JsonArray>();
 | 
			
		||||
 
 | 
			
		||||
@@ -1291,6 +1291,9 @@ void Nextion::check_pending_waveform_() {
 | 
			
		||||
 | 
			
		||||
void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; }
 | 
			
		||||
 | 
			
		||||
ESPDEPRECATED("set_wait_for_ack(bool) deprecated, no effect", "v1.20")
 | 
			
		||||
void Nextion::set_wait_for_ack(bool wait_for_ack) { ESP_LOGE(TAG, "Deprecated"); }
 | 
			
		||||
 | 
			
		||||
bool Nextion::is_updating() { return this->connection_state_.is_updating_; }
 | 
			
		||||
 | 
			
		||||
}  // namespace nextion
 | 
			
		||||
 
 | 
			
		||||
@@ -54,10 +54,11 @@ void PIDClimate::control(const climate::ClimateCall &call) {
 | 
			
		||||
}
 | 
			
		||||
climate::ClimateTraits PIDClimate::traits() {
 | 
			
		||||
  auto traits = climate::ClimateTraits();
 | 
			
		||||
  traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
 | 
			
		||||
  traits.set_supports_current_temperature(true);
 | 
			
		||||
  traits.set_supports_two_point_target_temperature(false);
 | 
			
		||||
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr)
 | 
			
		||||
    traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
 | 
			
		||||
    traits.set_supports_current_humidity(true);
 | 
			
		||||
 | 
			
		||||
  traits.set_supported_modes({climate::CLIMATE_MODE_OFF});
 | 
			
		||||
  if (supports_cool_())
 | 
			
		||||
@@ -67,6 +68,7 @@ climate::ClimateTraits PIDClimate::traits() {
 | 
			
		||||
  if (supports_heat_() && supports_cool_())
 | 
			
		||||
    traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
 | 
			
		||||
 | 
			
		||||
  traits.set_supports_action(true);
 | 
			
		||||
  return traits;
 | 
			
		||||
}
 | 
			
		||||
void PIDClimate::dump_config() {
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,7 @@ CONF_WARNING_MPPT_OVERLOAD = "warning_mppt_overload"
 | 
			
		||||
CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE = "warning_battery_too_low_to_charge"
 | 
			
		||||
CONF_FAULT_DC_DC_OVER_CURRENT = "fault_dc_dc_over_current"
 | 
			
		||||
CONF_FAULT_CODE = "fault_code"
 | 
			
		||||
CONF_WARNING_LOW_PV_ENERGY = "warning_low_pv_energy"
 | 
			
		||||
CONF_WARNUNG_LOW_PV_ENERGY = "warnung_low_pv_energy"
 | 
			
		||||
CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START = (
 | 
			
		||||
    "warning_high_ac_input_during_bus_soft_start"
 | 
			
		||||
)
 | 
			
		||||
@@ -122,7 +122,7 @@ TYPES = [
 | 
			
		||||
    CONF_WARNING_BATTERY_TOO_LOW_TO_CHARGE,
 | 
			
		||||
    CONF_FAULT_DC_DC_OVER_CURRENT,
 | 
			
		||||
    CONF_FAULT_CODE,
 | 
			
		||||
    CONF_WARNING_LOW_PV_ENERGY,
 | 
			
		||||
    CONF_WARNUNG_LOW_PV_ENERGY,
 | 
			
		||||
    CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START,
 | 
			
		||||
    CONF_WARNING_BATTERY_EQUALIZATION,
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ void PipsolarOutput::write_state(float state) {
 | 
			
		||||
 | 
			
		||||
  if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) {
 | 
			
		||||
    ESP_LOGD(TAG, "Will write: %s out of value %f / %02.0f", tmp, state, state);
 | 
			
		||||
    this->parent_->queue_command(std::string(tmp));
 | 
			
		||||
    this->parent_->switch_command(std::string(tmp));
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGD(TAG, "Will not write: %s as it is not in list of allowed values", tmp);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -7,7 +7,6 @@
 | 
			
		||||
#include "esphome/components/uart/uart.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace pipsolar {
 | 
			
		||||
@@ -29,17 +28,10 @@ struct PollingCommand {
 | 
			
		||||
  bool needs_update;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct QFLAGValues {
 | 
			
		||||
  esphome::optional<bool> silence_buzzer_open_buzzer;
 | 
			
		||||
  esphome::optional<bool> overload_bypass_function;
 | 
			
		||||
  esphome::optional<bool> lcd_escape_to_default;
 | 
			
		||||
  esphome::optional<bool> overload_restart_function;
 | 
			
		||||
  esphome::optional<bool> over_temperature_restart_function;
 | 
			
		||||
  esphome::optional<bool> backlight_on;
 | 
			
		||||
  esphome::optional<bool> alarm_on_when_primary_source_interrupt;
 | 
			
		||||
  esphome::optional<bool> fault_code_record;
 | 
			
		||||
  esphome::optional<bool> power_saving;
 | 
			
		||||
};
 | 
			
		||||
#define PIPSOLAR_VALUED_ENTITY_(type, name, polling_command, value_type) \
 | 
			
		||||
 protected: \
 | 
			
		||||
  value_type value_##name##_; \
 | 
			
		||||
  PIPSOLAR_ENTITY_(type, name, polling_command)
 | 
			
		||||
 | 
			
		||||
#define PIPSOLAR_ENTITY_(type, name, polling_command) \
 | 
			
		||||
 protected: \
 | 
			
		||||
@@ -51,123 +43,126 @@ struct QFLAGValues {
 | 
			
		||||
    this->add_polling_command_(#polling_command, POLLING_##polling_command); \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#define PIPSOLAR_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(sensor::Sensor, name, polling_command)
 | 
			
		||||
#define PIPSOLAR_SENSOR(name, polling_command, value_type) \
 | 
			
		||||
  PIPSOLAR_VALUED_ENTITY_(sensor::Sensor, name, polling_command, value_type)
 | 
			
		||||
#define PIPSOLAR_SWITCH(name, polling_command) PIPSOLAR_ENTITY_(switch_::Switch, name, polling_command)
 | 
			
		||||
#define PIPSOLAR_BINARY_SENSOR(name, polling_command) \
 | 
			
		||||
  PIPSOLAR_ENTITY_(binary_sensor::BinarySensor, name, polling_command)
 | 
			
		||||
#define PIPSOLAR_BINARY_SENSOR(name, polling_command, value_type) \
 | 
			
		||||
  PIPSOLAR_VALUED_ENTITY_(binary_sensor::BinarySensor, name, polling_command, value_type)
 | 
			
		||||
#define PIPSOLAR_VALUED_TEXT_SENSOR(name, polling_command, value_type) \
 | 
			
		||||
  PIPSOLAR_VALUED_ENTITY_(text_sensor::TextSensor, name, polling_command, value_type)
 | 
			
		||||
#define PIPSOLAR_TEXT_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(text_sensor::TextSensor, name, polling_command)
 | 
			
		||||
 | 
			
		||||
class Pipsolar : public uart::UARTDevice, public PollingComponent {
 | 
			
		||||
  // QPIGS values
 | 
			
		||||
  PIPSOLAR_SENSOR(grid_voltage, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(grid_frequency, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_voltage, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_frequency, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_active_power, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(output_load_percent, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(bus_voltage, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_voltage, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_charging_current, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(pv_input_voltage, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_discharge_current, QPIGS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(load_status, QPIGS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS)  //.1 scale
 | 
			
		||||
  PIPSOLAR_SENSOR(eeprom_version, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(pv_charging_power, QPIGS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS)
 | 
			
		||||
  PIPSOLAR_SENSOR(grid_voltage, QPIGS, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(grid_frequency, QPIGS, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_voltage, QPIGS, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_frequency, QPIGS, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_active_power, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(output_load_percent, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(bus_voltage, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_voltage, QPIGS, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_charging_current, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(pv_input_voltage, QPIGS, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_discharge_current, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(load_status, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS, int)  //.1 scale
 | 
			
		||||
  PIPSOLAR_SENSOR(eeprom_version, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(pv_charging_power, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS, int)
 | 
			
		||||
 | 
			
		||||
  // QPIRI values
 | 
			
		||||
  PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(grid_rating_current, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_under_voltage, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_float_voltage, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_type, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(current_max_charging_current, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(input_voltage_range, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(output_source_priority, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(charger_source_priority, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(parallel_max_num, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(machine_type, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(topology, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(output_mode, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(pv_power_balance, QPIRI)
 | 
			
		||||
  PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(grid_rating_current, QPIRI, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_under_voltage, QPIRI, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_float_voltage, QPIRI, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_type, QPIRI, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(current_max_charging_current, QPIRI, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(input_voltage_range, QPIRI, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(output_source_priority, QPIRI, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(charger_source_priority, QPIRI, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(parallel_max_num, QPIRI, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(machine_type, QPIRI, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(topology, QPIRI, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(output_mode, QPIRI, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI, float)
 | 
			
		||||
  PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI, int)
 | 
			
		||||
  PIPSOLAR_SENSOR(pv_power_balance, QPIRI, int)
 | 
			
		||||
 | 
			
		||||
  // QMOD values
 | 
			
		||||
  PIPSOLAR_TEXT_SENSOR(device_mode, QMOD)
 | 
			
		||||
  PIPSOLAR_VALUED_TEXT_SENSOR(device_mode, QMOD, char)
 | 
			
		||||
 | 
			
		||||
  // QFLAG values
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG, int)
 | 
			
		||||
 | 
			
		||||
  // QPIWS values
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_low_pv_energy, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS, int)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warnung_low_pv_energy, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS, bool)
 | 
			
		||||
  PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS, bool)
 | 
			
		||||
 | 
			
		||||
  PIPSOLAR_TEXT_SENSOR(last_qpigs, QPIGS)
 | 
			
		||||
  PIPSOLAR_TEXT_SENSOR(last_qpiri, QPIRI)
 | 
			
		||||
@@ -185,14 +180,14 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
 | 
			
		||||
  PIPSOLAR_SWITCH(pv_ok_condition_for_parallel_switch, QPIRI)
 | 
			
		||||
  PIPSOLAR_SWITCH(pv_power_balance_switch, QPIRI)
 | 
			
		||||
 | 
			
		||||
  void queue_command(const std::string &command);
 | 
			
		||||
  void switch_command(const std::string &command);
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 128;  // maximum supported answer length
 | 
			
		||||
  static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 110;  // maximum supported answer length
 | 
			
		||||
  static const size_t COMMAND_QUEUE_LENGTH = 10;
 | 
			
		||||
  static const size_t COMMAND_TIMEOUT = 5000;
 | 
			
		||||
  static const size_t POLLING_COMMANDS_MAX = 15;
 | 
			
		||||
@@ -203,26 +198,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
 | 
			
		||||
  uint16_t pipsolar_crc_(uint8_t *msg, uint8_t len);
 | 
			
		||||
  bool send_next_command_();
 | 
			
		||||
  bool send_next_poll_();
 | 
			
		||||
 | 
			
		||||
  void handle_qpiri_(const char *message);
 | 
			
		||||
  void handle_qpigs_(const char *message);
 | 
			
		||||
  void handle_qmod_(const char *message);
 | 
			
		||||
  void handle_qflag_(const char *message);
 | 
			
		||||
  void handle_qpiws_(const char *message);
 | 
			
		||||
  void handle_qt_(const char *message);
 | 
			
		||||
  void handle_qmn_(const char *message);
 | 
			
		||||
 | 
			
		||||
  void skip_start_(const char *message, size_t *pos);
 | 
			
		||||
  void skip_field_(const char *message, size_t *pos);
 | 
			
		||||
  std::string read_field_(const char *message, size_t *pos);
 | 
			
		||||
 | 
			
		||||
  void read_float_sensor_(const char *message, size_t *pos, sensor::Sensor *sensor);
 | 
			
		||||
  void read_int_sensor_(const char *message, size_t *pos, sensor::Sensor *sensor);
 | 
			
		||||
 | 
			
		||||
  void publish_binary_sensor_(esphome::optional<bool> b, binary_sensor::BinarySensor *sensor);
 | 
			
		||||
 | 
			
		||||
  esphome::optional<bool> get_bit_(std::string bits, uint8_t bit_pos);
 | 
			
		||||
 | 
			
		||||
  void queue_command_(const char *command, uint8_t length);
 | 
			
		||||
  std::string command_queue_[COMMAND_QUEUE_LENGTH];
 | 
			
		||||
  uint8_t command_queue_position_ = 0;
 | 
			
		||||
  uint8_t read_buffer_[PIPSOLAR_READ_BUFFER_LENGTH];
 | 
			
		||||
@@ -237,10 +213,11 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
 | 
			
		||||
    STATE_POLL_COMPLETE = 3,
 | 
			
		||||
    STATE_COMMAND_COMPLETE = 4,
 | 
			
		||||
    STATE_POLL_CHECKED = 5,
 | 
			
		||||
    STATE_POLL_DECODED = 6,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  uint8_t last_polling_command_ = 0;
 | 
			
		||||
  PollingCommand enabled_polling_commands_[POLLING_COMMANDS_MAX];
 | 
			
		||||
  PollingCommand used_polling_commands_[POLLING_COMMANDS_MAX];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace pipsolar
 | 
			
		||||
 
 | 
			
		||||
@@ -11,11 +11,11 @@ void PipsolarSwitch::dump_config() { LOG_SWITCH("", "Pipsolar Switch", this); }
 | 
			
		||||
void PipsolarSwitch::write_state(bool state) {
 | 
			
		||||
  if (state) {
 | 
			
		||||
    if (!this->on_command_.empty()) {
 | 
			
		||||
      this->parent_->queue_command(this->on_command_);
 | 
			
		||||
      this->parent_->switch_command(this->on_command_);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    if (!this->off_command_.empty()) {
 | 
			
		||||
      this->parent_->queue_command(this->off_command_);
 | 
			
		||||
      this->parent_->switch_command(this->off_command_);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -916,7 +916,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
 | 
			
		||||
  auto min_temp_value = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy);
 | 
			
		||||
  climate_value_row_(stream, obj, area, node, friendly_name, min_temp, min_temp_value);
 | 
			
		||||
  // now check optional traits
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
 | 
			
		||||
  if (traits.get_supports_current_temperature()) {
 | 
			
		||||
    std::string current_temp = "current_temperature";
 | 
			
		||||
    if (std::isnan(obj->current_temperature)) {
 | 
			
		||||
      climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, true);
 | 
			
		||||
@@ -927,7 +927,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
 | 
			
		||||
      climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
 | 
			
		||||
  if (traits.get_supports_current_humidity()) {
 | 
			
		||||
    std::string current_humidity = "current_humidity";
 | 
			
		||||
    if (std::isnan(obj->current_humidity)) {
 | 
			
		||||
      climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, true);
 | 
			
		||||
@@ -938,7 +938,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
 | 
			
		||||
      climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
 | 
			
		||||
  if (traits.get_supports_target_humidity()) {
 | 
			
		||||
    std::string target_humidity = "target_humidity";
 | 
			
		||||
    if (std::isnan(obj->target_humidity)) {
 | 
			
		||||
      climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, true);
 | 
			
		||||
@@ -949,8 +949,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
 | 
			
		||||
      climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
 | 
			
		||||
                               climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
 | 
			
		||||
  if (traits.get_supports_two_point_target_temperature()) {
 | 
			
		||||
    std::string target_temp_low = "target_temperature_low";
 | 
			
		||||
    auto target_temp_low_value = value_accuracy_to_string(obj->target_temperature_low, target_accuracy);
 | 
			
		||||
    climate_value_row_(stream, obj, area, node, friendly_name, target_temp_low, target_temp_low_value);
 | 
			
		||||
@@ -962,7 +961,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
 | 
			
		||||
    auto target_temp_value = value_accuracy_to_string(obj->target_temperature, target_accuracy);
 | 
			
		||||
    climate_value_row_(stream, obj, area, node, friendly_name, target_temp, target_temp_value);
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
 | 
			
		||||
  if (traits.get_supports_action()) {
 | 
			
		||||
    std::string climate_trait_category = "action";
 | 
			
		||||
    const auto *climate_trait_value = climate::climate_action_to_string(obj->action);
 | 
			
		||||
    climate_setting_row_(stream, obj, area, node, friendly_name, climate_trait_category, climate_trait_value);
 | 
			
		||||
 
 | 
			
		||||
@@ -81,7 +81,7 @@ CONFIG_SCHEMA = (
 | 
			
		||||
                cv.int_range(min=0, max=0xFFFF, max_included=False),
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION): cv.pressure,
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE_OFFSET, default="4°C"): cv.temperature_delta,
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE_OFFSET, default="4°C"): cv.temperature,
 | 
			
		||||
            cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id(
 | 
			
		||||
                sensor.Sensor
 | 
			
		||||
            ),
 | 
			
		||||
 
 | 
			
		||||
@@ -45,26 +45,13 @@ def get_script(script_id):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_max_runs(value):
 | 
			
		||||
    # Set default for queued mode to prevent unbounded queue growth
 | 
			
		||||
    if CONF_MAX_RUNS not in value and value[CONF_MODE] == CONF_QUEUED:
 | 
			
		||||
        value[CONF_MAX_RUNS] = 5
 | 
			
		||||
 | 
			
		||||
    if CONF_MAX_RUNS not in value:
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
    if value[CONF_MODE] not in [CONF_QUEUED, CONF_PARALLEL]:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "The option 'max_runs' is only valid in 'queued' and 'parallel' mode.",
 | 
			
		||||
            "The option 'max_runs' is only valid in 'queue' and 'parallel' mode.",
 | 
			
		||||
            path=[CONF_MAX_RUNS],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Queued mode must have bounded queue (min 1), parallel mode can be unlimited (0)
 | 
			
		||||
    if value[CONF_MODE] == CONF_QUEUED and value[CONF_MAX_RUNS] < 1:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "The option 'max_runs' must be at least 1 for queued mode.",
 | 
			
		||||
            path=[CONF_MAX_RUNS],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -119,7 +106,7 @@ CONFIG_SCHEMA = automation.validate_automation(
 | 
			
		||||
        cv.Optional(CONF_MODE, default=CONF_SINGLE): cv.one_of(
 | 
			
		||||
            *SCRIPT_MODES, lower=True
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_MAX_RUNS): cv.int_range(min=0, max=100),
 | 
			
		||||
        cv.Optional(CONF_MAX_RUNS): cv.positive_int,
 | 
			
		||||
        cv.Optional(CONF_PARAMETERS, default={}): cv.Schema(
 | 
			
		||||
            {
 | 
			
		||||
                validate_parameter_name: validate_parameter_type,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,10 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <tuple>
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include <queue>
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace script {
 | 
			
		||||
 | 
			
		||||
@@ -97,41 +96,23 @@ template<typename... Ts> class RestartScript : public Script<Ts...> {
 | 
			
		||||
/** A script type that queues new instances that are created.
 | 
			
		||||
 *
 | 
			
		||||
 * Only one instance of the script can be active at a time.
 | 
			
		||||
 *
 | 
			
		||||
 * Ring buffer implementation:
 | 
			
		||||
 * - num_queued_ tracks the number of queued (waiting) instances, NOT including the currently running one
 | 
			
		||||
 * - queue_front_ points to the next item to execute (read position)
 | 
			
		||||
 * - Buffer size is max_runs_ - 1 (max total instances minus the running one)
 | 
			
		||||
 * - Write position is calculated as: (queue_front_ + num_queued_) % (max_runs_ - 1)
 | 
			
		||||
 * - When an item finishes, queue_front_ advances: (queue_front_ + 1) % (max_runs_ - 1)
 | 
			
		||||
 * - First execute() runs immediately without queuing (num_queued_ stays 0)
 | 
			
		||||
 * - Subsequent executes while running are queued starting at position 0
 | 
			
		||||
 * - Maximum total instances = max_runs_ (includes 1 running + (max_runs_ - 1) queued)
 | 
			
		||||
 */
 | 
			
		||||
template<typename... Ts> class QueueingScript : public Script<Ts...>, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void execute(Ts... x) override {
 | 
			
		||||
    if (this->is_action_running() || this->num_queued_ > 0) {
 | 
			
		||||
      // num_queued_ is the number of *queued* instances (waiting, not including currently running)
 | 
			
		||||
      // max_runs_ is the maximum *total* instances (running + queued)
 | 
			
		||||
      // So we reject when num_queued_ + 1 >= max_runs_ (queued + running >= max)
 | 
			
		||||
      if (this->num_queued_ + 1 >= this->max_runs_) {
 | 
			
		||||
        this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' max instances (running + queued) reached!"),
 | 
			
		||||
    if (this->is_action_running() || this->num_runs_ > 0) {
 | 
			
		||||
      // num_runs_ is the number of *queued* instances, so total number of instances is
 | 
			
		||||
      // num_runs_ + 1
 | 
			
		||||
      if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) {
 | 
			
		||||
        this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of queued runs exceeded!"),
 | 
			
		||||
                        LOG_STR_ARG(this->name_));
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Initialize queue on first queued item (after capacity check)
 | 
			
		||||
      this->lazy_init_queue_();
 | 
			
		||||
 | 
			
		||||
      this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' queueing new instance (mode: queued)"),
 | 
			
		||||
                      LOG_STR_ARG(this->name_));
 | 
			
		||||
      // Ring buffer: write to (queue_front_ + num_queued_) % queue_capacity
 | 
			
		||||
      const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
 | 
			
		||||
      size_t write_pos = (this->queue_front_ + this->num_queued_) % queue_capacity;
 | 
			
		||||
      // Use std::make_unique to replace the unique_ptr
 | 
			
		||||
      this->var_queue_[write_pos] = std::make_unique<std::tuple<Ts...>>(x...);
 | 
			
		||||
      this->num_queued_++;
 | 
			
		||||
      this->num_runs_++;
 | 
			
		||||
      this->var_queue_.push(std::make_tuple(x...));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -141,46 +122,29 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void stop() override {
 | 
			
		||||
    // Clear all queued items to free memory immediately
 | 
			
		||||
    // Resetting the array automatically destroys all unique_ptrs and their contents
 | 
			
		||||
    this->var_queue_.reset();
 | 
			
		||||
    this->num_queued_ = 0;
 | 
			
		||||
    this->queue_front_ = 0;
 | 
			
		||||
    this->num_runs_ = 0;
 | 
			
		||||
    Script<Ts...>::stop();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void loop() override {
 | 
			
		||||
    if (this->num_queued_ != 0 && !this->is_action_running()) {
 | 
			
		||||
      // Dequeue: decrement count, move tuple out (frees slot), advance read position
 | 
			
		||||
      this->num_queued_--;
 | 
			
		||||
      const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
 | 
			
		||||
      auto tuple_ptr = std::move(this->var_queue_[this->queue_front_]);
 | 
			
		||||
      this->queue_front_ = (this->queue_front_ + 1) % queue_capacity;
 | 
			
		||||
      this->trigger_tuple_(*tuple_ptr, typename gens<sizeof...(Ts)>::type());
 | 
			
		||||
    if (this->num_runs_ != 0 && !this->is_action_running()) {
 | 
			
		||||
      this->num_runs_--;
 | 
			
		||||
      auto &vars = this->var_queue_.front();
 | 
			
		||||
      this->var_queue_.pop();
 | 
			
		||||
      this->trigger_tuple_(vars, typename gens<sizeof...(Ts)>::type());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_max_runs(int max_runs) { max_runs_ = max_runs; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  // Lazy init queue on first use - avoids setup() ordering issues and saves memory
 | 
			
		||||
  // if script is never executed during this boot cycle
 | 
			
		||||
  inline void lazy_init_queue_() {
 | 
			
		||||
    if (!this->var_queue_) {
 | 
			
		||||
      // Allocate array of max_runs_ - 1 slots for queued items (running item is separate)
 | 
			
		||||
      // unique_ptr array is zero-initialized, so all slots start as nullptr
 | 
			
		||||
      this->var_queue_ = std::make_unique<std::unique_ptr<std::tuple<Ts...>>[]>(this->max_runs_ - 1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  template<int... S> void trigger_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
 | 
			
		||||
    this->trigger(std::get<S>(tuple)...);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int num_queued_ = 0;      // Number of queued instances (not including currently running)
 | 
			
		||||
  int max_runs_ = 0;        // Maximum total instances (running + queued)
 | 
			
		||||
  size_t queue_front_ = 0;  // Ring buffer read position (next item to execute)
 | 
			
		||||
  std::unique_ptr<std::unique_ptr<std::tuple<Ts...>>[]> var_queue_;  // Ring buffer of queued parameters
 | 
			
		||||
  int num_runs_ = 0;
 | 
			
		||||
  int max_runs_ = 0;
 | 
			
		||||
  std::queue<std::tuple<Ts...>> var_queue_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** A script type that executes new instances in parallel.
 | 
			
		||||
 
 | 
			
		||||
@@ -251,9 +251,6 @@ MaxFilter = sensor_ns.class_("MaxFilter", Filter)
 | 
			
		||||
SlidingWindowMovingAverageFilter = sensor_ns.class_(
 | 
			
		||||
    "SlidingWindowMovingAverageFilter", Filter
 | 
			
		||||
)
 | 
			
		||||
StreamingMinFilter = sensor_ns.class_("StreamingMinFilter", Filter)
 | 
			
		||||
StreamingMaxFilter = sensor_ns.class_("StreamingMaxFilter", Filter)
 | 
			
		||||
StreamingMovingAverageFilter = sensor_ns.class_("StreamingMovingAverageFilter", Filter)
 | 
			
		||||
ExponentialMovingAverageFilter = sensor_ns.class_(
 | 
			
		||||
    "ExponentialMovingAverageFilter", Filter
 | 
			
		||||
)
 | 
			
		||||
@@ -261,12 +258,9 @@ ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Com
 | 
			
		||||
LambdaFilter = sensor_ns.class_("LambdaFilter", Filter)
 | 
			
		||||
OffsetFilter = sensor_ns.class_("OffsetFilter", Filter)
 | 
			
		||||
MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter)
 | 
			
		||||
ValueListFilter = sensor_ns.class_("ValueListFilter", Filter)
 | 
			
		||||
FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", ValueListFilter)
 | 
			
		||||
FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", Filter)
 | 
			
		||||
ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter)
 | 
			
		||||
ThrottleWithPriorityFilter = sensor_ns.class_(
 | 
			
		||||
    "ThrottleWithPriorityFilter", ValueListFilter
 | 
			
		||||
)
 | 
			
		||||
ThrottleWithPriorityFilter = sensor_ns.class_("ThrottleWithPriorityFilter", Filter)
 | 
			
		||||
TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component)
 | 
			
		||||
DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component)
 | 
			
		||||
HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component)
 | 
			
		||||
@@ -458,21 +452,14 @@ async def skip_initial_filter_to_code(config, filter_id):
 | 
			
		||||
    return cg.new_Pvariable(filter_id, config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@FILTER_REGISTRY.register("min", Filter, MIN_SCHEMA)
 | 
			
		||||
@FILTER_REGISTRY.register("min", MinFilter, MIN_SCHEMA)
 | 
			
		||||
async def min_filter_to_code(config, filter_id):
 | 
			
		||||
    window_size: int = config[CONF_WINDOW_SIZE]
 | 
			
		||||
    send_every: int = config[CONF_SEND_EVERY]
 | 
			
		||||
    send_first_at: int = config[CONF_SEND_FIRST_AT]
 | 
			
		||||
 | 
			
		||||
    # Optimization: Use streaming filter for batch windows (window_size == send_every)
 | 
			
		||||
    # Saves 99.98% memory for large windows (e.g., 20KB → 4 bytes for window_size=5000)
 | 
			
		||||
    if window_size == send_every:
 | 
			
		||||
        # Use streaming filter - O(1) memory instead of O(n)
 | 
			
		||||
        rhs = StreamingMinFilter.new(window_size, send_first_at)
 | 
			
		||||
        return cg.Pvariable(filter_id, rhs, StreamingMinFilter)
 | 
			
		||||
    # Use sliding window filter - maintains ring buffer
 | 
			
		||||
    rhs = MinFilter.new(window_size, send_every, send_first_at)
 | 
			
		||||
    return cg.Pvariable(filter_id, rhs, MinFilter)
 | 
			
		||||
    return cg.new_Pvariable(
 | 
			
		||||
        filter_id,
 | 
			
		||||
        config[CONF_WINDOW_SIZE],
 | 
			
		||||
        config[CONF_SEND_EVERY],
 | 
			
		||||
        config[CONF_SEND_FIRST_AT],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
MAX_SCHEMA = cv.All(
 | 
			
		||||
@@ -487,18 +474,14 @@ MAX_SCHEMA = cv.All(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@FILTER_REGISTRY.register("max", Filter, MAX_SCHEMA)
 | 
			
		||||
@FILTER_REGISTRY.register("max", MaxFilter, MAX_SCHEMA)
 | 
			
		||||
async def max_filter_to_code(config, filter_id):
 | 
			
		||||
    window_size: int = config[CONF_WINDOW_SIZE]
 | 
			
		||||
    send_every: int = config[CONF_SEND_EVERY]
 | 
			
		||||
    send_first_at: int = config[CONF_SEND_FIRST_AT]
 | 
			
		||||
 | 
			
		||||
    # Optimization: Use streaming filter for batch windows (window_size == send_every)
 | 
			
		||||
    if window_size == send_every:
 | 
			
		||||
        rhs = StreamingMaxFilter.new(window_size, send_first_at)
 | 
			
		||||
        return cg.Pvariable(filter_id, rhs, StreamingMaxFilter)
 | 
			
		||||
    rhs = MaxFilter.new(window_size, send_every, send_first_at)
 | 
			
		||||
    return cg.Pvariable(filter_id, rhs, MaxFilter)
 | 
			
		||||
    return cg.new_Pvariable(
 | 
			
		||||
        filter_id,
 | 
			
		||||
        config[CONF_WINDOW_SIZE],
 | 
			
		||||
        config[CONF_SEND_EVERY],
 | 
			
		||||
        config[CONF_SEND_FIRST_AT],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SLIDING_AVERAGE_SCHEMA = cv.All(
 | 
			
		||||
@@ -515,20 +498,16 @@ SLIDING_AVERAGE_SCHEMA = cv.All(
 | 
			
		||||
 | 
			
		||||
@FILTER_REGISTRY.register(
 | 
			
		||||
    "sliding_window_moving_average",
 | 
			
		||||
    Filter,
 | 
			
		||||
    SlidingWindowMovingAverageFilter,
 | 
			
		||||
    SLIDING_AVERAGE_SCHEMA,
 | 
			
		||||
)
 | 
			
		||||
async def sliding_window_moving_average_filter_to_code(config, filter_id):
 | 
			
		||||
    window_size: int = config[CONF_WINDOW_SIZE]
 | 
			
		||||
    send_every: int = config[CONF_SEND_EVERY]
 | 
			
		||||
    send_first_at: int = config[CONF_SEND_FIRST_AT]
 | 
			
		||||
 | 
			
		||||
    # Optimization: Use streaming filter for batch windows (window_size == send_every)
 | 
			
		||||
    if window_size == send_every:
 | 
			
		||||
        rhs = StreamingMovingAverageFilter.new(window_size, send_first_at)
 | 
			
		||||
        return cg.Pvariable(filter_id, rhs, StreamingMovingAverageFilter)
 | 
			
		||||
    rhs = SlidingWindowMovingAverageFilter.new(window_size, send_every, send_first_at)
 | 
			
		||||
    return cg.Pvariable(filter_id, rhs, SlidingWindowMovingAverageFilter)
 | 
			
		||||
    return cg.new_Pvariable(
 | 
			
		||||
        filter_id,
 | 
			
		||||
        config[CONF_WINDOW_SIZE],
 | 
			
		||||
        config[CONF_SEND_EVERY],
 | 
			
		||||
        config[CONF_SEND_FIRST_AT],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
EXPONENTIAL_AVERAGE_SCHEMA = cv.All(
 | 
			
		||||
 
 | 
			
		||||
@@ -32,76 +32,50 @@ void Filter::initialize(Sensor *parent, Filter *next) {
 | 
			
		||||
  this->next_ = next;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SlidingWindowFilter
 | 
			
		||||
SlidingWindowFilter::SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at)
 | 
			
		||||
    : window_size_(window_size), send_every_(send_every), send_at_(send_every - send_first_at) {
 | 
			
		||||
  // Allocate ring buffer once at initialization
 | 
			
		||||
  this->window_.init(window_size);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
optional<float> SlidingWindowFilter::new_value(float value) {
 | 
			
		||||
  // Add value to ring buffer
 | 
			
		||||
  if (this->window_count_ < this->window_size_) {
 | 
			
		||||
    // Buffer not yet full - just append
 | 
			
		||||
    this->window_.push_back(value);
 | 
			
		||||
    this->window_count_++;
 | 
			
		||||
  } else {
 | 
			
		||||
    // Buffer full - overwrite oldest value (ring buffer)
 | 
			
		||||
    this->window_[this->window_head_] = value;
 | 
			
		||||
    this->window_head_++;
 | 
			
		||||
    if (this->window_head_ >= this->window_size_) {
 | 
			
		||||
      this->window_head_ = 0;
 | 
			
		||||
    }
 | 
			
		||||
// MedianFilter
 | 
			
		||||
MedianFilter::MedianFilter(size_t window_size, size_t send_every, size_t send_first_at)
 | 
			
		||||
    : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {}
 | 
			
		||||
void MedianFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
 | 
			
		||||
void MedianFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
 | 
			
		||||
optional<float> MedianFilter::new_value(float value) {
 | 
			
		||||
  while (this->queue_.size() >= this->window_size_) {
 | 
			
		||||
    this->queue_.pop_front();
 | 
			
		||||
  }
 | 
			
		||||
  this->queue_.push_back(value);
 | 
			
		||||
  ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f)", this, value);
 | 
			
		||||
 | 
			
		||||
  // Check if we should send a result
 | 
			
		||||
  if (++this->send_at_ >= this->send_every_) {
 | 
			
		||||
    this->send_at_ = 0;
 | 
			
		||||
    float result = this->compute_result();
 | 
			
		||||
    ESP_LOGVV(TAG, "SlidingWindowFilter(%p)::new_value(%f) SENDING %f", this, value, result);
 | 
			
		||||
    return result;
 | 
			
		||||
 | 
			
		||||
    float median = NAN;
 | 
			
		||||
    if (!this->queue_.empty()) {
 | 
			
		||||
      // Copy queue without NaN values
 | 
			
		||||
      std::vector<float> median_queue;
 | 
			
		||||
      median_queue.reserve(this->queue_.size());
 | 
			
		||||
      for (auto v : this->queue_) {
 | 
			
		||||
        if (!std::isnan(v)) {
 | 
			
		||||
          median_queue.push_back(v);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      sort(median_queue.begin(), median_queue.end());
 | 
			
		||||
 | 
			
		||||
      size_t queue_size = median_queue.size();
 | 
			
		||||
      if (queue_size) {
 | 
			
		||||
        if (queue_size % 2) {
 | 
			
		||||
          median = median_queue[queue_size / 2];
 | 
			
		||||
        } else {
 | 
			
		||||
          median = (median_queue[queue_size / 2] + median_queue[(queue_size / 2) - 1]) / 2.0f;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f) SENDING %f", this, value, median);
 | 
			
		||||
    return median;
 | 
			
		||||
  }
 | 
			
		||||
  return {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SortedWindowFilter
 | 
			
		||||
FixedVector<float> SortedWindowFilter::get_window_values_() {
 | 
			
		||||
  // Copy window without NaN values using FixedVector (no heap allocation)
 | 
			
		||||
  // Returns unsorted values - caller will use std::nth_element for partial sorting as needed
 | 
			
		||||
  FixedVector<float> values;
 | 
			
		||||
  values.init(this->window_count_);
 | 
			
		||||
  for (size_t i = 0; i < this->window_count_; i++) {
 | 
			
		||||
    float v = this->window_[i];
 | 
			
		||||
    if (!std::isnan(v)) {
 | 
			
		||||
      values.push_back(v);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return values;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MedianFilter
 | 
			
		||||
float MedianFilter::compute_result() {
 | 
			
		||||
  FixedVector<float> values = this->get_window_values_();
 | 
			
		||||
  if (values.empty())
 | 
			
		||||
    return NAN;
 | 
			
		||||
 | 
			
		||||
  size_t size = values.size();
 | 
			
		||||
  size_t mid = size / 2;
 | 
			
		||||
 | 
			
		||||
  if (size % 2) {
 | 
			
		||||
    // Odd number of elements - use nth_element to find middle element
 | 
			
		||||
    std::nth_element(values.begin(), values.begin() + mid, values.end());
 | 
			
		||||
    return values[mid];
 | 
			
		||||
  }
 | 
			
		||||
  // Even number of elements - need both middle elements
 | 
			
		||||
  // Use nth_element to find upper middle element
 | 
			
		||||
  std::nth_element(values.begin(), values.begin() + mid, values.end());
 | 
			
		||||
  float upper = values[mid];
 | 
			
		||||
  // Find the maximum of the lower half (which is now everything before mid)
 | 
			
		||||
  float lower = *std::max_element(values.begin(), values.begin() + mid);
 | 
			
		||||
  return (lower + upper) / 2.0f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SkipInitialFilter
 | 
			
		||||
SkipInitialFilter::SkipInitialFilter(size_t num_to_ignore) : num_to_ignore_(num_to_ignore) {}
 | 
			
		||||
optional<float> SkipInitialFilter::new_value(float value) {
 | 
			
		||||
@@ -117,39 +91,136 @@ optional<float> SkipInitialFilter::new_value(float value) {
 | 
			
		||||
 | 
			
		||||
// QuantileFilter
 | 
			
		||||
QuantileFilter::QuantileFilter(size_t window_size, size_t send_every, size_t send_first_at, float quantile)
 | 
			
		||||
    : SortedWindowFilter(window_size, send_every, send_first_at), quantile_(quantile) {}
 | 
			
		||||
    : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size), quantile_(quantile) {}
 | 
			
		||||
void QuantileFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
 | 
			
		||||
void QuantileFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
 | 
			
		||||
void QuantileFilter::set_quantile(float quantile) { this->quantile_ = quantile; }
 | 
			
		||||
optional<float> QuantileFilter::new_value(float value) {
 | 
			
		||||
  while (this->queue_.size() >= this->window_size_) {
 | 
			
		||||
    this->queue_.pop_front();
 | 
			
		||||
  }
 | 
			
		||||
  this->queue_.push_back(value);
 | 
			
		||||
  ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f), quantile:%f", this, value, this->quantile_);
 | 
			
		||||
 | 
			
		||||
float QuantileFilter::compute_result() {
 | 
			
		||||
  FixedVector<float> values = this->get_window_values_();
 | 
			
		||||
  if (values.empty())
 | 
			
		||||
    return NAN;
 | 
			
		||||
  if (++this->send_at_ >= this->send_every_) {
 | 
			
		||||
    this->send_at_ = 0;
 | 
			
		||||
 | 
			
		||||
  size_t position = ceilf(values.size() * this->quantile_) - 1;
 | 
			
		||||
  ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, values.size());
 | 
			
		||||
    float result = NAN;
 | 
			
		||||
    if (!this->queue_.empty()) {
 | 
			
		||||
      // Copy queue without NaN values
 | 
			
		||||
      std::vector<float> quantile_queue;
 | 
			
		||||
      for (auto v : this->queue_) {
 | 
			
		||||
        if (!std::isnan(v)) {
 | 
			
		||||
          quantile_queue.push_back(v);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
  // Use nth_element to find the quantile element (O(n) instead of O(n log n))
 | 
			
		||||
  std::nth_element(values.begin(), values.begin() + position, values.end());
 | 
			
		||||
  return values[position];
 | 
			
		||||
      sort(quantile_queue.begin(), quantile_queue.end());
 | 
			
		||||
 | 
			
		||||
      size_t queue_size = quantile_queue.size();
 | 
			
		||||
      if (queue_size) {
 | 
			
		||||
        size_t position = ceilf(queue_size * this->quantile_) - 1;
 | 
			
		||||
        ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, queue_size);
 | 
			
		||||
        result = quantile_queue[position];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ESP_LOGVV(TAG, "QuantileFilter(%p)::new_value(%f) SENDING %f", this, value, result);
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
  return {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MinFilter
 | 
			
		||||
float MinFilter::compute_result() { return this->find_extremum_<std::less<float>>(); }
 | 
			
		||||
MinFilter::MinFilter(size_t window_size, size_t send_every, size_t send_first_at)
 | 
			
		||||
    : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {}
 | 
			
		||||
void MinFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
 | 
			
		||||
void MinFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
 | 
			
		||||
optional<float> MinFilter::new_value(float value) {
 | 
			
		||||
  while (this->queue_.size() >= this->window_size_) {
 | 
			
		||||
    this->queue_.pop_front();
 | 
			
		||||
  }
 | 
			
		||||
  this->queue_.push_back(value);
 | 
			
		||||
  ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f)", this, value);
 | 
			
		||||
 | 
			
		||||
  if (++this->send_at_ >= this->send_every_) {
 | 
			
		||||
    this->send_at_ = 0;
 | 
			
		||||
 | 
			
		||||
    float min = NAN;
 | 
			
		||||
    for (auto v : this->queue_) {
 | 
			
		||||
      if (!std::isnan(v)) {
 | 
			
		||||
        min = std::isnan(min) ? v : std::min(min, v);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f) SENDING %f", this, value, min);
 | 
			
		||||
    return min;
 | 
			
		||||
  }
 | 
			
		||||
  return {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MaxFilter
 | 
			
		||||
float MaxFilter::compute_result() { return this->find_extremum_<std::greater<float>>(); }
 | 
			
		||||
MaxFilter::MaxFilter(size_t window_size, size_t send_every, size_t send_first_at)
 | 
			
		||||
    : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {}
 | 
			
		||||
void MaxFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
 | 
			
		||||
void MaxFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
 | 
			
		||||
optional<float> MaxFilter::new_value(float value) {
 | 
			
		||||
  while (this->queue_.size() >= this->window_size_) {
 | 
			
		||||
    this->queue_.pop_front();
 | 
			
		||||
  }
 | 
			
		||||
  this->queue_.push_back(value);
 | 
			
		||||
  ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f)", this, value);
 | 
			
		||||
 | 
			
		||||
  if (++this->send_at_ >= this->send_every_) {
 | 
			
		||||
    this->send_at_ = 0;
 | 
			
		||||
 | 
			
		||||
    float max = NAN;
 | 
			
		||||
    for (auto v : this->queue_) {
 | 
			
		||||
      if (!std::isnan(v)) {
 | 
			
		||||
        max = std::isnan(max) ? v : std::max(max, v);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f) SENDING %f", this, value, max);
 | 
			
		||||
    return max;
 | 
			
		||||
  }
 | 
			
		||||
  return {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SlidingWindowMovingAverageFilter
 | 
			
		||||
float SlidingWindowMovingAverageFilter::compute_result() {
 | 
			
		||||
  float sum = 0;
 | 
			
		||||
  size_t valid_count = 0;
 | 
			
		||||
  for (size_t i = 0; i < this->window_count_; i++) {
 | 
			
		||||
    float v = this->window_[i];
 | 
			
		||||
    if (!std::isnan(v)) {
 | 
			
		||||
      sum += v;
 | 
			
		||||
      valid_count++;
 | 
			
		||||
    }
 | 
			
		||||
SlidingWindowMovingAverageFilter::SlidingWindowMovingAverageFilter(size_t window_size, size_t send_every,
 | 
			
		||||
                                                                   size_t send_first_at)
 | 
			
		||||
    : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {}
 | 
			
		||||
void SlidingWindowMovingAverageFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; }
 | 
			
		||||
void SlidingWindowMovingAverageFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; }
 | 
			
		||||
optional<float> SlidingWindowMovingAverageFilter::new_value(float value) {
 | 
			
		||||
  while (this->queue_.size() >= this->window_size_) {
 | 
			
		||||
    this->queue_.pop_front();
 | 
			
		||||
  }
 | 
			
		||||
  return valid_count ? sum / valid_count : NAN;
 | 
			
		||||
  this->queue_.push_back(value);
 | 
			
		||||
  ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f)", this, value);
 | 
			
		||||
 | 
			
		||||
  if (++this->send_at_ >= this->send_every_) {
 | 
			
		||||
    this->send_at_ = 0;
 | 
			
		||||
 | 
			
		||||
    float sum = 0;
 | 
			
		||||
    size_t valid_count = 0;
 | 
			
		||||
    for (auto v : this->queue_) {
 | 
			
		||||
      if (!std::isnan(v)) {
 | 
			
		||||
        sum += v;
 | 
			
		||||
        valid_count++;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    float average = NAN;
 | 
			
		||||
    if (valid_count) {
 | 
			
		||||
      average = sum / valid_count;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f) SENDING %f", this, value, average);
 | 
			
		||||
    return average;
 | 
			
		||||
  }
 | 
			
		||||
  return {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ExponentialMovingAverageFilter
 | 
			
		||||
@@ -228,40 +299,27 @@ MultiplyFilter::MultiplyFilter(TemplatableValue<float> multiplier) : multiplier_
 | 
			
		||||
 | 
			
		||||
optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); }
 | 
			
		||||
 | 
			
		||||
// ValueListFilter (base class)
 | 
			
		||||
ValueListFilter::ValueListFilter(std::initializer_list<TemplatableValue<float>> values) : values_(values) {}
 | 
			
		||||
 | 
			
		||||
bool ValueListFilter::value_matches_any_(float sensor_value) {
 | 
			
		||||
  int8_t accuracy = this->parent_->get_accuracy_decimals();
 | 
			
		||||
  float accuracy_mult = powf(10.0f, accuracy);
 | 
			
		||||
  float rounded_sensor = roundf(accuracy_mult * sensor_value);
 | 
			
		||||
 | 
			
		||||
  for (auto &filter_value : this->values_) {
 | 
			
		||||
    float fv = filter_value.value();
 | 
			
		||||
 | 
			
		||||
    // Handle NaN comparison
 | 
			
		||||
    if (std::isnan(fv)) {
 | 
			
		||||
      if (std::isnan(sensor_value))
 | 
			
		||||
        return true;
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Compare rounded values
 | 
			
		||||
    if (roundf(accuracy_mult * fv) == rounded_sensor)
 | 
			
		||||
      return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FilterOutValueFilter
 | 
			
		||||
FilterOutValueFilter::FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out)
 | 
			
		||||
    : ValueListFilter(values_to_filter_out) {}
 | 
			
		||||
FilterOutValueFilter::FilterOutValueFilter(std::vector<TemplatableValue<float>> values_to_filter_out)
 | 
			
		||||
    : values_to_filter_out_(std::move(values_to_filter_out)) {}
 | 
			
		||||
 | 
			
		||||
optional<float> FilterOutValueFilter::new_value(float value) {
 | 
			
		||||
  if (this->value_matches_any_(value))
 | 
			
		||||
    return {};   // Filter out
 | 
			
		||||
  return value;  // Pass through
 | 
			
		||||
  int8_t accuracy = this->parent_->get_accuracy_decimals();
 | 
			
		||||
  float accuracy_mult = powf(10.0f, accuracy);
 | 
			
		||||
  for (auto filter_value : this->values_to_filter_out_) {
 | 
			
		||||
    if (std::isnan(filter_value.value())) {
 | 
			
		||||
      if (std::isnan(value)) {
 | 
			
		||||
        return {};
 | 
			
		||||
      }
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    float rounded_filter_out = roundf(accuracy_mult * filter_value.value());
 | 
			
		||||
    float rounded_value = roundf(accuracy_mult * value);
 | 
			
		||||
    if (rounded_filter_out == rounded_value) {
 | 
			
		||||
      return {};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ThrottleFilter
 | 
			
		||||
@@ -276,15 +334,33 @@ optional<float> ThrottleFilter::new_value(float value) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ThrottleWithPriorityFilter
 | 
			
		||||
ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(
 | 
			
		||||
    uint32_t min_time_between_inputs, std::initializer_list<TemplatableValue<float>> prioritized_values)
 | 
			
		||||
    : ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {}
 | 
			
		||||
ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
 | 
			
		||||
                                                       std::vector<TemplatableValue<float>> prioritized_values)
 | 
			
		||||
    : min_time_between_inputs_(min_time_between_inputs), prioritized_values_(std::move(prioritized_values)) {}
 | 
			
		||||
 | 
			
		||||
optional<float> ThrottleWithPriorityFilter::new_value(float value) {
 | 
			
		||||
  bool is_prioritized_value = false;
 | 
			
		||||
  int8_t accuracy = this->parent_->get_accuracy_decimals();
 | 
			
		||||
  float accuracy_mult = powf(10.0f, accuracy);
 | 
			
		||||
  const uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
  // Allow value through if: no previous input, time expired, or is prioritized
 | 
			
		||||
  if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ ||
 | 
			
		||||
      this->value_matches_any_(value)) {
 | 
			
		||||
  // First, determine if the new value is one of the prioritized values
 | 
			
		||||
  for (auto prioritized_value : this->prioritized_values_) {
 | 
			
		||||
    if (std::isnan(prioritized_value.value())) {
 | 
			
		||||
      if (std::isnan(value)) {
 | 
			
		||||
        is_prioritized_value = true;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    float rounded_prioritized_value = roundf(accuracy_mult * prioritized_value.value());
 | 
			
		||||
    float rounded_value = roundf(accuracy_mult * value);
 | 
			
		||||
    if (rounded_prioritized_value == rounded_value) {
 | 
			
		||||
      is_prioritized_value = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  // Finally, determine if the new value should be throttled and pass it through if not
 | 
			
		||||
  if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ || is_prioritized_value) {
 | 
			
		||||
    this->last_input_ = now;
 | 
			
		||||
    return value;
 | 
			
		||||
  }
 | 
			
		||||
@@ -467,78 +543,5 @@ optional<float> ToNTCTemperatureFilter::new_value(float value) {
 | 
			
		||||
  return temp;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StreamingFilter (base class)
 | 
			
		||||
StreamingFilter::StreamingFilter(size_t window_size, size_t send_first_at)
 | 
			
		||||
    : window_size_(window_size), send_first_at_(send_first_at) {}
 | 
			
		||||
 | 
			
		||||
optional<float> StreamingFilter::new_value(float value) {
 | 
			
		||||
  // Process the value (child class tracks min/max/sum/etc)
 | 
			
		||||
  this->process_value(value);
 | 
			
		||||
 | 
			
		||||
  this->count_++;
 | 
			
		||||
 | 
			
		||||
  // Check if we should send (handle send_first_at for first value)
 | 
			
		||||
  bool should_send = false;
 | 
			
		||||
  if (this->first_send_ && this->count_ >= this->send_first_at_) {
 | 
			
		||||
    should_send = true;
 | 
			
		||||
    this->first_send_ = false;
 | 
			
		||||
  } else if (!this->first_send_ && this->count_ >= this->window_size_) {
 | 
			
		||||
    should_send = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (should_send) {
 | 
			
		||||
    float result = this->compute_batch_result();
 | 
			
		||||
    // Reset for next batch
 | 
			
		||||
    this->count_ = 0;
 | 
			
		||||
    this->reset_batch();
 | 
			
		||||
    ESP_LOGVV(TAG, "StreamingFilter(%p)::new_value(%f) SENDING %f", this, value, result);
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StreamingMinFilter
 | 
			
		||||
void StreamingMinFilter::process_value(float value) {
 | 
			
		||||
  // Update running minimum (ignore NaN values)
 | 
			
		||||
  if (!std::isnan(value)) {
 | 
			
		||||
    this->current_min_ = std::isnan(this->current_min_) ? value : std::min(this->current_min_, value);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float StreamingMinFilter::compute_batch_result() { return this->current_min_; }
 | 
			
		||||
 | 
			
		||||
void StreamingMinFilter::reset_batch() { this->current_min_ = NAN; }
 | 
			
		||||
 | 
			
		||||
// StreamingMaxFilter
 | 
			
		||||
void StreamingMaxFilter::process_value(float value) {
 | 
			
		||||
  // Update running maximum (ignore NaN values)
 | 
			
		||||
  if (!std::isnan(value)) {
 | 
			
		||||
    this->current_max_ = std::isnan(this->current_max_) ? value : std::max(this->current_max_, value);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float StreamingMaxFilter::compute_batch_result() { return this->current_max_; }
 | 
			
		||||
 | 
			
		||||
void StreamingMaxFilter::reset_batch() { this->current_max_ = NAN; }
 | 
			
		||||
 | 
			
		||||
// StreamingMovingAverageFilter
 | 
			
		||||
void StreamingMovingAverageFilter::process_value(float value) {
 | 
			
		||||
  // Accumulate sum (ignore NaN values)
 | 
			
		||||
  if (!std::isnan(value)) {
 | 
			
		||||
    this->sum_ += value;
 | 
			
		||||
    this->valid_count_++;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float StreamingMovingAverageFilter::compute_batch_result() {
 | 
			
		||||
  return this->valid_count_ > 0 ? this->sum_ / this->valid_count_ : NAN;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void StreamingMovingAverageFilter::reset_batch() {
 | 
			
		||||
  this->sum_ = 0.0f;
 | 
			
		||||
  this->valid_count_ = 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace sensor
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -44,75 +44,11 @@ class Filter {
 | 
			
		||||
  Sensor *parent_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Base class for filters that use a sliding window of values.
 | 
			
		||||
 *
 | 
			
		||||
 * Uses a ring buffer to efficiently maintain a fixed-size sliding window without
 | 
			
		||||
 * reallocations or pop_front() overhead. Eliminates deque fragmentation issues.
 | 
			
		||||
 */
 | 
			
		||||
class SlidingWindowFilter : public Filter {
 | 
			
		||||
 public:
 | 
			
		||||
  SlidingWindowFilter(size_t window_size, size_t send_every, size_t send_first_at);
 | 
			
		||||
 | 
			
		||||
  optional<float> new_value(float value) final;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  /// Called by new_value() to compute the filtered result from the current window
 | 
			
		||||
  virtual float compute_result() = 0;
 | 
			
		||||
 | 
			
		||||
  /// Access the sliding window values (ring buffer implementation)
 | 
			
		||||
  /// Use: for (size_t i = 0; i < window_count_; i++) { float val = window_[i]; }
 | 
			
		||||
  FixedVector<float> window_;
 | 
			
		||||
  size_t window_head_{0};   ///< Index where next value will be written
 | 
			
		||||
  size_t window_count_{0};  ///< Number of valid values in window (0 to window_size_)
 | 
			
		||||
  size_t window_size_;      ///< Maximum window size
 | 
			
		||||
  size_t send_every_;       ///< Send result every N values
 | 
			
		||||
  size_t send_at_;          ///< Counter for send_every
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Base class for Min/Max filters.
 | 
			
		||||
 *
 | 
			
		||||
 * Provides a templated helper to find extremum values efficiently.
 | 
			
		||||
 */
 | 
			
		||||
class MinMaxFilter : public SlidingWindowFilter {
 | 
			
		||||
 public:
 | 
			
		||||
  using SlidingWindowFilter::SlidingWindowFilter;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  /// Helper to find min or max value in window, skipping NaN values
 | 
			
		||||
  /// Usage: find_extremum_<std::less<float>>() for min, find_extremum_<std::greater<float>>() for max
 | 
			
		||||
  template<typename Compare> float find_extremum_() {
 | 
			
		||||
    float result = NAN;
 | 
			
		||||
    Compare comp;
 | 
			
		||||
    for (size_t i = 0; i < this->window_count_; i++) {
 | 
			
		||||
      float v = this->window_[i];
 | 
			
		||||
      if (!std::isnan(v)) {
 | 
			
		||||
        result = std::isnan(result) ? v : (comp(v, result) ? v : result);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Base class for filters that need a sorted window (Median, Quantile).
 | 
			
		||||
 *
 | 
			
		||||
 * Extends SlidingWindowFilter to provide a helper that filters out NaN values.
 | 
			
		||||
 * Derived classes use std::nth_element for efficient partial sorting.
 | 
			
		||||
 */
 | 
			
		||||
class SortedWindowFilter : public SlidingWindowFilter {
 | 
			
		||||
 public:
 | 
			
		||||
  using SlidingWindowFilter::SlidingWindowFilter;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  /// Helper to get non-NaN values from the window (not sorted - caller will use nth_element)
 | 
			
		||||
  /// Returns empty FixedVector if all values are NaN
 | 
			
		||||
  FixedVector<float> get_window_values_();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Simple quantile filter.
 | 
			
		||||
 *
 | 
			
		||||
 * Takes the quantile of the last <window_size> values and pushes it out every <send_every>.
 | 
			
		||||
 * Takes the quantile of the last <send_every> values and pushes it out every <send_every>.
 | 
			
		||||
 */
 | 
			
		||||
class QuantileFilter : public SortedWindowFilter {
 | 
			
		||||
class QuantileFilter : public Filter {
 | 
			
		||||
 public:
 | 
			
		||||
  /** Construct a QuantileFilter.
 | 
			
		||||
   *
 | 
			
		||||
@@ -125,18 +61,25 @@ class QuantileFilter : public SortedWindowFilter {
 | 
			
		||||
   */
 | 
			
		||||
  explicit QuantileFilter(size_t window_size, size_t send_every, size_t send_first_at, float quantile);
 | 
			
		||||
 | 
			
		||||
  void set_quantile(float quantile) { this->quantile_ = quantile; }
 | 
			
		||||
  optional<float> new_value(float value) override;
 | 
			
		||||
 | 
			
		||||
  void set_send_every(size_t send_every);
 | 
			
		||||
  void set_window_size(size_t window_size);
 | 
			
		||||
  void set_quantile(float quantile);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  float compute_result() override;
 | 
			
		||||
  std::deque<float> queue_;
 | 
			
		||||
  size_t send_every_;
 | 
			
		||||
  size_t send_at_;
 | 
			
		||||
  size_t window_size_;
 | 
			
		||||
  float quantile_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Simple median filter.
 | 
			
		||||
 *
 | 
			
		||||
 * Takes the median of the last <window_size> values and pushes it out every <send_every>.
 | 
			
		||||
 * Takes the median of the last <send_every> values and pushes it out every <send_every>.
 | 
			
		||||
 */
 | 
			
		||||
class MedianFilter : public SortedWindowFilter {
 | 
			
		||||
class MedianFilter : public Filter {
 | 
			
		||||
 public:
 | 
			
		||||
  /** Construct a MedianFilter.
 | 
			
		||||
   *
 | 
			
		||||
@@ -146,10 +89,18 @@ class MedianFilter : public SortedWindowFilter {
 | 
			
		||||
   *   on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
 | 
			
		||||
   *   send_every.
 | 
			
		||||
   */
 | 
			
		||||
  using SortedWindowFilter::SortedWindowFilter;
 | 
			
		||||
  explicit MedianFilter(size_t window_size, size_t send_every, size_t send_first_at);
 | 
			
		||||
 | 
			
		||||
  optional<float> new_value(float value) override;
 | 
			
		||||
 | 
			
		||||
  void set_send_every(size_t send_every);
 | 
			
		||||
  void set_window_size(size_t window_size);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  float compute_result() override;
 | 
			
		||||
  std::deque<float> queue_;
 | 
			
		||||
  size_t send_every_;
 | 
			
		||||
  size_t send_at_;
 | 
			
		||||
  size_t window_size_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Simple skip filter.
 | 
			
		||||
@@ -172,9 +123,9 @@ class SkipInitialFilter : public Filter {
 | 
			
		||||
 | 
			
		||||
/** Simple min filter.
 | 
			
		||||
 *
 | 
			
		||||
 * Takes the min of the last <window_size> values and pushes it out every <send_every>.
 | 
			
		||||
 * Takes the min of the last <send_every> values and pushes it out every <send_every>.
 | 
			
		||||
 */
 | 
			
		||||
class MinFilter : public MinMaxFilter {
 | 
			
		||||
class MinFilter : public Filter {
 | 
			
		||||
 public:
 | 
			
		||||
  /** Construct a MinFilter.
 | 
			
		||||
   *
 | 
			
		||||
@@ -184,17 +135,25 @@ class MinFilter : public MinMaxFilter {
 | 
			
		||||
   *   on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
 | 
			
		||||
   *   send_every.
 | 
			
		||||
   */
 | 
			
		||||
  using MinMaxFilter::MinMaxFilter;
 | 
			
		||||
  explicit MinFilter(size_t window_size, size_t send_every, size_t send_first_at);
 | 
			
		||||
 | 
			
		||||
  optional<float> new_value(float value) override;
 | 
			
		||||
 | 
			
		||||
  void set_send_every(size_t send_every);
 | 
			
		||||
  void set_window_size(size_t window_size);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  float compute_result() override;
 | 
			
		||||
  std::deque<float> queue_;
 | 
			
		||||
  size_t send_every_;
 | 
			
		||||
  size_t send_at_;
 | 
			
		||||
  size_t window_size_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Simple max filter.
 | 
			
		||||
 *
 | 
			
		||||
 * Takes the max of the last <window_size> values and pushes it out every <send_every>.
 | 
			
		||||
 * Takes the max of the last <send_every> values and pushes it out every <send_every>.
 | 
			
		||||
 */
 | 
			
		||||
class MaxFilter : public MinMaxFilter {
 | 
			
		||||
class MaxFilter : public Filter {
 | 
			
		||||
 public:
 | 
			
		||||
  /** Construct a MaxFilter.
 | 
			
		||||
   *
 | 
			
		||||
@@ -204,10 +163,18 @@ class MaxFilter : public MinMaxFilter {
 | 
			
		||||
   *   on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
 | 
			
		||||
   *   send_every.
 | 
			
		||||
   */
 | 
			
		||||
  using MinMaxFilter::MinMaxFilter;
 | 
			
		||||
  explicit MaxFilter(size_t window_size, size_t send_every, size_t send_first_at);
 | 
			
		||||
 | 
			
		||||
  optional<float> new_value(float value) override;
 | 
			
		||||
 | 
			
		||||
  void set_send_every(size_t send_every);
 | 
			
		||||
  void set_window_size(size_t window_size);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  float compute_result() override;
 | 
			
		||||
  std::deque<float> queue_;
 | 
			
		||||
  size_t send_every_;
 | 
			
		||||
  size_t send_at_;
 | 
			
		||||
  size_t window_size_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Simple sliding window moving average filter.
 | 
			
		||||
@@ -215,7 +182,7 @@ class MaxFilter : public MinMaxFilter {
 | 
			
		||||
 * Essentially just takes takes the average of the last window_size values and pushes them out
 | 
			
		||||
 * every send_every.
 | 
			
		||||
 */
 | 
			
		||||
class SlidingWindowMovingAverageFilter : public SlidingWindowFilter {
 | 
			
		||||
class SlidingWindowMovingAverageFilter : public Filter {
 | 
			
		||||
 public:
 | 
			
		||||
  /** Construct a SlidingWindowMovingAverageFilter.
 | 
			
		||||
   *
 | 
			
		||||
@@ -225,10 +192,18 @@ class SlidingWindowMovingAverageFilter : public SlidingWindowFilter {
 | 
			
		||||
   *   on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to
 | 
			
		||||
   *   send_every.
 | 
			
		||||
   */
 | 
			
		||||
  using SlidingWindowFilter::SlidingWindowFilter;
 | 
			
		||||
  explicit SlidingWindowMovingAverageFilter(size_t window_size, size_t send_every, size_t send_first_at);
 | 
			
		||||
 | 
			
		||||
  optional<float> new_value(float value) override;
 | 
			
		||||
 | 
			
		||||
  void set_send_every(size_t send_every);
 | 
			
		||||
  void set_window_size(size_t window_size);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  float compute_result() override;
 | 
			
		||||
  std::deque<float> queue_;
 | 
			
		||||
  size_t send_every_;
 | 
			
		||||
  size_t send_at_;
 | 
			
		||||
  size_t window_size_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Simple exponential moving average filter.
 | 
			
		||||
@@ -317,28 +292,15 @@ class MultiplyFilter : public Filter {
 | 
			
		||||
  TemplatableValue<float> multiplier_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Base class for filters that compare sensor values against a list of configured values.
 | 
			
		||||
 *
 | 
			
		||||
 * This base class provides common functionality for filters that need to check if a sensor
 | 
			
		||||
 * value matches any value in a configured list, with proper handling of NaN values and
 | 
			
		||||
 * accuracy-based rounding for comparisons.
 | 
			
		||||
 */
 | 
			
		||||
class ValueListFilter : public Filter {
 | 
			
		||||
 protected:
 | 
			
		||||
  explicit ValueListFilter(std::initializer_list<TemplatableValue<float>> values);
 | 
			
		||||
 | 
			
		||||
  /// Check if sensor value matches any configured value (with accuracy rounding)
 | 
			
		||||
  bool value_matches_any_(float sensor_value);
 | 
			
		||||
 | 
			
		||||
  FixedVector<TemplatableValue<float>> values_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`.
 | 
			
		||||
class FilterOutValueFilter : public ValueListFilter {
 | 
			
		||||
class FilterOutValueFilter : public Filter {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out);
 | 
			
		||||
  explicit FilterOutValueFilter(std::vector<TemplatableValue<float>> values_to_filter_out);
 | 
			
		||||
 | 
			
		||||
  optional<float> new_value(float value) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  std::vector<TemplatableValue<float>> values_to_filter_out_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ThrottleFilter : public Filter {
 | 
			
		||||
@@ -353,16 +315,17 @@ class ThrottleFilter : public Filter {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`.
 | 
			
		||||
class ThrottleWithPriorityFilter : public ValueListFilter {
 | 
			
		||||
class ThrottleWithPriorityFilter : public Filter {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
 | 
			
		||||
                                      std::initializer_list<TemplatableValue<float>> prioritized_values);
 | 
			
		||||
                                      std::vector<TemplatableValue<float>> prioritized_values);
 | 
			
		||||
 | 
			
		||||
  optional<float> new_value(float value) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  uint32_t last_input_{0};
 | 
			
		||||
  uint32_t min_time_between_inputs_;
 | 
			
		||||
  std::vector<TemplatableValue<float>> prioritized_values_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class TimeoutFilter : public Filter, public Component {
 | 
			
		||||
@@ -513,81 +476,5 @@ class ToNTCTemperatureFilter : public Filter {
 | 
			
		||||
  double c_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Base class for streaming filters (batch windows where window_size == send_every).
 | 
			
		||||
 *
 | 
			
		||||
 * When window_size equals send_every, we don't need a sliding window.
 | 
			
		||||
 * This base class handles the common batching logic.
 | 
			
		||||
 */
 | 
			
		||||
class StreamingFilter : public Filter {
 | 
			
		||||
 public:
 | 
			
		||||
  StreamingFilter(size_t window_size, size_t send_first_at);
 | 
			
		||||
 | 
			
		||||
  optional<float> new_value(float value) final;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  /// Called by new_value() to process each value in the batch
 | 
			
		||||
  virtual void process_value(float value) = 0;
 | 
			
		||||
 | 
			
		||||
  /// Called by new_value() to compute the result after collecting window_size values
 | 
			
		||||
  virtual float compute_batch_result() = 0;
 | 
			
		||||
 | 
			
		||||
  /// Called by new_value() to reset internal state after sending a result
 | 
			
		||||
  virtual void reset_batch() = 0;
 | 
			
		||||
 | 
			
		||||
  size_t window_size_;
 | 
			
		||||
  size_t count_{0};
 | 
			
		||||
  size_t send_first_at_;
 | 
			
		||||
  bool first_send_{true};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Streaming min filter for batch windows (window_size == send_every).
 | 
			
		||||
 *
 | 
			
		||||
 * Uses O(1) memory instead of O(n) by tracking only the minimum value.
 | 
			
		||||
 */
 | 
			
		||||
class StreamingMinFilter : public StreamingFilter {
 | 
			
		||||
 public:
 | 
			
		||||
  using StreamingFilter::StreamingFilter;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void process_value(float value) override;
 | 
			
		||||
  float compute_batch_result() override;
 | 
			
		||||
  void reset_batch() override;
 | 
			
		||||
 | 
			
		||||
  float current_min_{NAN};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Streaming max filter for batch windows (window_size == send_every).
 | 
			
		||||
 *
 | 
			
		||||
 * Uses O(1) memory instead of O(n) by tracking only the maximum value.
 | 
			
		||||
 */
 | 
			
		||||
class StreamingMaxFilter : public StreamingFilter {
 | 
			
		||||
 public:
 | 
			
		||||
  using StreamingFilter::StreamingFilter;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void process_value(float value) override;
 | 
			
		||||
  float compute_batch_result() override;
 | 
			
		||||
  void reset_batch() override;
 | 
			
		||||
 | 
			
		||||
  float current_max_{NAN};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Streaming moving average filter for batch windows (window_size == send_every).
 | 
			
		||||
 *
 | 
			
		||||
 * Uses O(1) memory instead of O(n) by tracking only sum and count.
 | 
			
		||||
 */
 | 
			
		||||
class StreamingMovingAverageFilter : public StreamingFilter {
 | 
			
		||||
 public:
 | 
			
		||||
  using StreamingFilter::StreamingFilter;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void process_value(float value) override;
 | 
			
		||||
  float compute_batch_result() override;
 | 
			
		||||
  void reset_batch() override;
 | 
			
		||||
 | 
			
		||||
  float sum_{0.0f};
 | 
			
		||||
  size_t valid_count_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace sensor
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -107,12 +107,12 @@ void Sensor::add_filter(Filter *filter) {
 | 
			
		||||
  }
 | 
			
		||||
  filter->initialize(this, nullptr);
 | 
			
		||||
}
 | 
			
		||||
void Sensor::add_filters(std::initializer_list<Filter *> filters) {
 | 
			
		||||
void Sensor::add_filters(const std::vector<Filter *> &filters) {
 | 
			
		||||
  for (Filter *filter : filters) {
 | 
			
		||||
    this->add_filter(filter);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void Sensor::set_filters(std::initializer_list<Filter *> filters) {
 | 
			
		||||
void Sensor::set_filters(const std::vector<Filter *> &filters) {
 | 
			
		||||
  this->clear_filters();
 | 
			
		||||
  this->add_filters(filters);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user