mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:18:45 +00:00 
			
		
		
		
	Compare commits
	
		
			6 Commits
		
	
	
		
			light-addr
			...
			platformio
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 2560ffb25f | ||
|   | c064bb7299 | ||
|   | 20f336ee6e | ||
|   | ebde648305 | ||
|   | 3182e6caa9 | ||
|   | 1106350114 | 
| @@ -1 +1 @@ | |||||||
| 3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c | d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248 | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -53,7 +53,6 @@ jobs: | |||||||
|               'new-target-platform', |               'new-target-platform', | ||||||
|               'merging-to-release', |               'merging-to-release', | ||||||
|               'merging-to-beta', |               'merging-to-beta', | ||||||
|               'chained-pr', |  | ||||||
|               'core', |               'core', | ||||||
|               'small-pr', |               'small-pr', | ||||||
|               'dashboard', |               'dashboard', | ||||||
| @@ -141,8 +140,6 @@ jobs: | |||||||
|                 labels.add('merging-to-release'); |                 labels.add('merging-to-release'); | ||||||
|               } else if (baseRef === 'beta') { |               } else if (baseRef === 'beta') { | ||||||
|                 labels.add('merging-to-beta'); |                 labels.add('merging-to-beta'); | ||||||
|               } else if (baseRef !== 'dev') { |  | ||||||
|                 labels.add('chained-pr'); |  | ||||||
|               } |               } | ||||||
|  |  | ||||||
|               return labels; |               return labels; | ||||||
| @@ -531,8 +528,8 @@ jobs: | |||||||
|             const apiData = await fetchApiData(); |             const apiData = await fetchApiData(); | ||||||
|             const baseRef = context.payload.pull_request.base.ref; |             const baseRef = context.payload.pull_request.base.ref; | ||||||
|  |  | ||||||
|             // Early exit for release and beta branches only |             // Early exit for non-dev branches | ||||||
|             if (baseRef === 'release' || baseRef === 'beta') { |             if (baseRef !== 'dev') { | ||||||
|               const branchLabels = await detectMergeBranch(); |               const branchLabels = await detectMergeBranch(); | ||||||
|               const finalLabels = Array.from(branchLabels); |               const finalLabels = Array.from(branchLabels); | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										232
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										232
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -170,16 +170,12 @@ jobs: | |||||||
|     outputs: |     outputs: | ||||||
|       integration-tests: ${{ steps.determine.outputs.integration-tests }} |       integration-tests: ${{ steps.determine.outputs.integration-tests }} | ||||||
|       clang-tidy: ${{ steps.determine.outputs.clang-tidy }} |       clang-tidy: ${{ steps.determine.outputs.clang-tidy }} | ||||||
|       clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }} |  | ||||||
|       python-linters: ${{ steps.determine.outputs.python-linters }} |       python-linters: ${{ steps.determine.outputs.python-linters }} | ||||||
|       changed-components: ${{ steps.determine.outputs.changed-components }} |       changed-components: ${{ steps.determine.outputs.changed-components }} | ||||||
|       changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }} |       changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }} | ||||||
|       directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-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 }} |       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 }} |       memory_impact: ${{ steps.determine.outputs.memory-impact }} | ||||||
|       cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }} |  | ||||||
|       cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }} |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||||
| @@ -204,16 +200,12 @@ jobs: | |||||||
|           # Extract individual fields |           # Extract individual fields | ||||||
|           echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT |           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=$(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 "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT | ||||||
|           echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $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 "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 "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 "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 |           echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT | ||||||
|           echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT |  | ||||||
|           echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT |  | ||||||
|  |  | ||||||
|   integration-tests: |   integration-tests: | ||||||
|     name: Run integration tests |     name: Run integration tests | ||||||
| @@ -251,34 +243,7 @@ jobs: | |||||||
|           . venv/bin/activate |           . venv/bin/activate | ||||||
|           pytest -vv --no-cov --tb=native -n auto tests/integration/ |           pytest -vv --no-cov --tb=native -n auto tests/integration/ | ||||||
|  |  | ||||||
|   cpp-unit-tests: |   clang-tidy: | ||||||
|     name: Run C++ unit tests |  | ||||||
|     runs-on: ubuntu-24.04 |  | ||||||
|     needs: |  | ||||||
|       - common |  | ||||||
|       - determine-jobs |  | ||||||
|     if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]') |  | ||||||
|     steps: |  | ||||||
|       - name: Check out code from GitHub |  | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |  | ||||||
|  |  | ||||||
|       - name: Restore Python |  | ||||||
|         uses: ./.github/actions/restore-python |  | ||||||
|         with: |  | ||||||
|           python-version: ${{ env.DEFAULT_PYTHON }} |  | ||||||
|           cache-key: ${{ needs.common.outputs.cache-key }} |  | ||||||
|  |  | ||||||
|       - name: Run cpp_unit_test.py |  | ||||||
|         run: | |  | ||||||
|           . venv/bin/activate |  | ||||||
|           if [ "${{ needs.determine-jobs.outputs.cpp-unit-tests-run-all }}" = "true" ]; then |  | ||||||
|             script/cpp_unit_test.py --all |  | ||||||
|           else |  | ||||||
|             ARGS=$(echo '${{ needs.determine-jobs.outputs.cpp-unit-tests-components }}' | jq -r '.[] | @sh' | xargs) |  | ||||||
|             script/cpp_unit_test.py $ARGS |  | ||||||
|           fi |  | ||||||
|  |  | ||||||
|   clang-tidy-single: |  | ||||||
|     name: ${{ matrix.name }} |     name: ${{ matrix.name }} | ||||||
|     runs-on: ubuntu-24.04 |     runs-on: ubuntu-24.04 | ||||||
|     needs: |     needs: | ||||||
| @@ -296,6 +261,22 @@ jobs: | |||||||
|             name: Run script/clang-tidy for ESP8266 |             name: Run script/clang-tidy for ESP8266 | ||||||
|             options: --environment esp8266-arduino-tidy --grep USE_ESP8266 |             options: --environment esp8266-arduino-tidy --grep USE_ESP8266 | ||||||
|             pio_cache_key: tidyesp8266 |             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 |           - id: clang-tidy | ||||||
|             name: Run script/clang-tidy for ESP32 IDF |             name: Run script/clang-tidy for ESP32 IDF | ||||||
|             options: --environment esp32-idf-tidy --grep USE_ESP_IDF |             options: --environment esp32-idf-tidy --grep USE_ESP_IDF | ||||||
| @@ -376,166 +357,6 @@ jobs: | |||||||
|         # yamllint disable-line rule:line-length |         # yamllint disable-line rule:line-length | ||||||
|         if: always() |         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: |   test-build-components-splitter: | ||||||
|     name: Split components for intelligent grouping (40 weighted per batch) |     name: Split components for intelligent grouping (40 weighted per batch) | ||||||
|     runs-on: ubuntu-24.04 |     runs-on: ubuntu-24.04 | ||||||
| @@ -611,6 +432,17 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           python-version: ${{ env.DEFAULT_PYTHON }} |           python-version: ${{ env.DEFAULT_PYTHON }} | ||||||
|           cache-key: ${{ needs.common.outputs.cache-key }} |           cache-key: ${{ needs.common.outputs.cache-key }} | ||||||
|  |  | ||||||
|  |       # Cache PlatformIO packages to speed up test builds | ||||||
|  |       # Note: Caches are repository-scoped, PRs from forks cannot restore from the main repo cache | ||||||
|  |       - name: Cache PlatformIO | ||||||
|  |         uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | ||||||
|  |         with: | ||||||
|  |           path: ~/.platformio | ||||||
|  |           key: platformio-test-${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-${{ hashFiles('platformio.ini') }} | ||||||
|  |           restore-keys: | | ||||||
|  |             platformio-test-${{ runner.os }}-${{ env.DEFAULT_PYTHON }}- | ||||||
|  |  | ||||||
|       - name: Validate and compile components with intelligent grouping |       - name: Validate and compile components with intelligent grouping | ||||||
|         run: | |         run: | | ||||||
|           . venv/bin/activate |           . venv/bin/activate | ||||||
| @@ -943,13 +775,13 @@ jobs: | |||||||
|           python-version: ${{ env.DEFAULT_PYTHON }} |           python-version: ${{ env.DEFAULT_PYTHON }} | ||||||
|           cache-key: ${{ needs.common.outputs.cache-key }} |           cache-key: ${{ needs.common.outputs.cache-key }} | ||||||
|       - name: Download target analysis JSON |       - name: Download target analysis JSON | ||||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 |         uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 | ||||||
|         with: |         with: | ||||||
|           name: memory-analysis-target |           name: memory-analysis-target | ||||||
|           path: ./memory-analysis |           path: ./memory-analysis | ||||||
|         continue-on-error: true |         continue-on-error: true | ||||||
|       - name: Download PR analysis JSON |       - name: Download PR analysis JSON | ||||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 |         uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 | ||||||
|         with: |         with: | ||||||
|           name: memory-analysis-pr |           name: memory-analysis-pr | ||||||
|           path: ./memory-analysis |           path: ./memory-analysis | ||||||
| @@ -976,9 +808,7 @@ jobs: | |||||||
|       - pylint |       - pylint | ||||||
|       - pytest |       - pytest | ||||||
|       - integration-tests |       - integration-tests | ||||||
|       - clang-tidy-single |       - clang-tidy | ||||||
|       - clang-tidy-nosplit |  | ||||||
|       - clang-tidy-split |  | ||||||
|       - determine-jobs |       - determine-jobs | ||||||
|       - test-build-components-splitter |       - test-build-components-splitter | ||||||
|       - test-build-components-split |       - test-build-components-split | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							| @@ -14,7 +14,6 @@ jobs: | |||||||
|         label: |         label: | ||||||
|           - needs-docs |           - needs-docs | ||||||
|           - merge-after-release |           - merge-after-release | ||||||
|           - chained-pr |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Check for ${{ matrix.label }} label |       - name: Check for ${{ matrix.label }} label | ||||||
|         uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 |         uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | ||||||
|   | |||||||
| @@ -161,7 +161,6 @@ esphome/components/esp32_rmt_led_strip/* @jesserockz | |||||||
| esphome/components/esp8266/* @esphome/core | esphome/components/esp8266/* @esphome/core | ||||||
| esphome/components/esp_ldo/* @clydebarrow | esphome/components/esp_ldo/* @clydebarrow | ||||||
| esphome/components/espnow/* @jesserockz | esphome/components/espnow/* @jesserockz | ||||||
| esphome/components/espnow/packet_transport/* @EasilyBoredEngineer |  | ||||||
| esphome/components/ethernet_info/* @gtjadsonsantos | esphome/components/ethernet_info/* @gtjadsonsantos | ||||||
| esphome/components/event/* @nohat | esphome/components/event/* @nohat | ||||||
| esphome/components/exposure_notifications/* @OttoWinter | esphome/components/exposure_notifications/* @OttoWinter | ||||||
|   | |||||||
| @@ -231,22 +231,9 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): | |||||||
|                 api_component = (name, mem) |                 api_component = (name, mem) | ||||||
|                 break |                 break | ||||||
|  |  | ||||||
|         # Also include wifi_stack and other important system components if they exist |         # Combine all components to analyze: top ESPHome + all external + API if not already included | ||||||
|         system_components_to_include = [ |         components_to_analyze = list(top_esphome_components) + list( | ||||||
|             # Empty list - we've finished debugging symbol categorization |             top_external_components | ||||||
|             # Add component names here if you need to debug their symbols |  | ||||||
|         ] |  | ||||||
|         system_components = [ |  | ||||||
|             (name, mem) |  | ||||||
|             for name, mem in components |  | ||||||
|             if name in system_components_to_include |  | ||||||
|         ] |  | ||||||
|  |  | ||||||
|         # Combine all components to analyze: top ESPHome + all external + API if not already included + system components |  | ||||||
|         components_to_analyze = ( |  | ||||||
|             list(top_esphome_components) |  | ||||||
|             + list(top_external_components) |  | ||||||
|             + system_components |  | ||||||
|         ) |         ) | ||||||
|         if api_component and api_component not in components_to_analyze: |         if api_component and api_component not in components_to_analyze: | ||||||
|             components_to_analyze.append(api_component) |             components_to_analyze.append(api_component) | ||||||
|   | |||||||
| @@ -127,39 +127,40 @@ SYMBOL_PATTERNS = { | |||||||
|         "tryget_socket_unconn", |         "tryget_socket_unconn", | ||||||
|         "cs_create_ctrl_sock", |         "cs_create_ctrl_sock", | ||||||
|         "netbuf_alloc", |         "netbuf_alloc", | ||||||
|         "tcp_",  # TCP protocol functions |  | ||||||
|         "udp_",  # UDP protocol functions |  | ||||||
|         "lwip_",  # LwIP stack functions |  | ||||||
|         "eagle_lwip",  # ESP-specific LwIP functions |  | ||||||
|         "new_linkoutput",  # Link output function |  | ||||||
|         "acd_",  # Address Conflict Detection (ACD) |  | ||||||
|         "eth_",  # Ethernet functions |  | ||||||
|         "mac_enable_bb",  # MAC baseband enable |  | ||||||
|         "reassemble_and_dispatch",  # Packet reassembly |  | ||||||
|     ], |     ], | ||||||
|     # dhcp must come before libc to avoid "dhcp_select" matching "select" pattern |  | ||||||
|     "dhcp": ["dhcp", "handle_dhcp"], |  | ||||||
|     "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], |     "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], | ||||||
|     # Order matters! More specific categories must come before general ones. |     "wifi_stack": [ | ||||||
|     # mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern |         "ieee80211", | ||||||
|     "mdns_lib": ["mdns"], |         "hostap", | ||||||
|     # memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols |         "sta_", | ||||||
|     "memory_mgmt": [ |         "ap_", | ||||||
|         "mem_", |         "scan_", | ||||||
|         "memory_", |         "wifi_", | ||||||
|         "tlsf_", |         "wpa_", | ||||||
|         "memp_", |         "wps_", | ||||||
|         "pbuf_", |         "esp_wifi", | ||||||
|         "pbuf_alloc", |         "cnx_", | ||||||
|         "pbuf_copy_partial_pbuf", |         "wpa3_", | ||||||
|         "esp_mmu_map", |         "sae_", | ||||||
|         "mmu_hal_", |         "wDev_", | ||||||
|         "s_do_mapping",  # Memory mapping function, not WiFi |         "ic_", | ||||||
|         "hash_map_",  # Hash map data structure |         "mac_", | ||||||
|         "umm_assimilate",  # UMM malloc assimilation |         "esf_buf", | ||||||
|  |         "gWpaSm", | ||||||
|  |         "sm_WPA", | ||||||
|  |         "eapol_", | ||||||
|  |         "owe_", | ||||||
|  |         "wifiLowLevelInit", | ||||||
|  |         "s_do_mapping", | ||||||
|  |         "gScanStruct", | ||||||
|  |         "ppSearchTxframe", | ||||||
|  |         "ppMapWaitTxq", | ||||||
|  |         "ppFillAMPDUBar", | ||||||
|  |         "ppCheckTxConnTrafficIdle", | ||||||
|  |         "ppCalTkipMic", | ||||||
|     ], |     ], | ||||||
|     # Bluetooth categories must come BEFORE wifi_stack to avoid misclassification |     "bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"], | ||||||
|     # Many BLE symbols contain patterns like "ble_" that would otherwise match wifi patterns |     "wifi_bt_coex": ["coex"], | ||||||
|     "bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"], |     "bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"], | ||||||
|     "bluedroid_bt": [ |     "bluedroid_bt": [ | ||||||
|         "bluedroid", |         "bluedroid", | ||||||
| @@ -206,61 +207,6 @@ SYMBOL_PATTERNS = { | |||||||
|         "copy_extra_byte_in_db", |         "copy_extra_byte_in_db", | ||||||
|         "parse_read_local_supported_commands_response", |         "parse_read_local_supported_commands_response", | ||||||
|     ], |     ], | ||||||
|     "bluetooth": [ |  | ||||||
|         "bt_", |  | ||||||
|         "_ble_",  # More specific than "ble_" to avoid matching "able_", "enable_", "disable_" |  | ||||||
|         "l2c_", |  | ||||||
|         "l2ble_",  # L2CAP for BLE |  | ||||||
|         "gatt_", |  | ||||||
|         "gap_", |  | ||||||
|         "hci_", |  | ||||||
|         "btsnd_hcic_",  # Bluetooth HCI command send functions |  | ||||||
|         "BT_init", |  | ||||||
|         "BT_tx_",  # Bluetooth transmit functions |  | ||||||
|         "esp_ble_",  # Catch esp_ble_* functions |  | ||||||
|     ], |  | ||||||
|     "bluetooth_ll": [ |  | ||||||
|         "llm_",  # Link layer manager |  | ||||||
|         "llc_",  # Link layer control |  | ||||||
|         "lld_",  # Link layer driver |  | ||||||
|         "ld_acl_",  # Link layer ACL (Asynchronous Connection-Oriented) |  | ||||||
|         "llcp_",  # Link layer control protocol |  | ||||||
|         "lmp_",  # Link manager protocol |  | ||||||
|     ], |  | ||||||
|     "wifi_bt_coex": ["coex"], |  | ||||||
|     "wifi_stack": [ |  | ||||||
|         "ieee80211", |  | ||||||
|         "hostap", |  | ||||||
|         "sta_", |  | ||||||
|         "wifi_ap_",  # More specific than "ap_" to avoid matching "cap_", "map_" |  | ||||||
|         "wifi_scan_",  # More specific than "scan_" to avoid matching "_scan_" in other contexts |  | ||||||
|         "wifi_", |  | ||||||
|         "wpa_", |  | ||||||
|         "wps_", |  | ||||||
|         "esp_wifi", |  | ||||||
|         "cnx_", |  | ||||||
|         "wpa3_", |  | ||||||
|         "sae_", |  | ||||||
|         "wDev_", |  | ||||||
|         "ic_mac_",  # More specific than "mac_" to avoid matching emac_ |  | ||||||
|         "esf_buf", |  | ||||||
|         "gWpaSm", |  | ||||||
|         "sm_WPA", |  | ||||||
|         "eapol_", |  | ||||||
|         "owe_", |  | ||||||
|         "wifiLowLevelInit", |  | ||||||
|         # Removed "s_do_mapping" - this is memory management, not WiFi |  | ||||||
|         "gScanStruct", |  | ||||||
|         "ppSearchTxframe", |  | ||||||
|         "ppMapWaitTxq", |  | ||||||
|         "ppFillAMPDUBar", |  | ||||||
|         "ppCheckTxConnTrafficIdle", |  | ||||||
|         "ppCalTkipMic", |  | ||||||
|         "phy_force_wifi", |  | ||||||
|         "phy_unforce_wifi", |  | ||||||
|         "write_wifi_chan", |  | ||||||
|         "wifi_track_pll", |  | ||||||
|     ], |  | ||||||
|     "crypto_math": [ |     "crypto_math": [ | ||||||
|         "ecp_", |         "ecp_", | ||||||
|         "bignum_", |         "bignum_", | ||||||
| @@ -285,36 +231,13 @@ SYMBOL_PATTERNS = { | |||||||
|         "p_256_init_curve", |         "p_256_init_curve", | ||||||
|         "shift_sub_rows", |         "shift_sub_rows", | ||||||
|         "rshift", |         "rshift", | ||||||
|         "rijndaelEncrypt",  # AES Rijndael encryption |  | ||||||
|     ], |  | ||||||
|     # System and Arduino core functions must come before libc |  | ||||||
|     "esp_system": [ |  | ||||||
|         "system_",  # ESP system functions |  | ||||||
|         "postmortem_",  # Postmortem reporting |  | ||||||
|     ], |  | ||||||
|     "arduino_core": [ |  | ||||||
|         "pinMode", |  | ||||||
|         "resetPins", |  | ||||||
|         "millis", |  | ||||||
|         "micros", |  | ||||||
|         "delay(",  # More specific - Arduino delay function with parenthesis |  | ||||||
|         "delayMicroseconds", |  | ||||||
|         "digitalWrite", |  | ||||||
|         "digitalRead", |  | ||||||
|     ], |  | ||||||
|     "sntp": ["sntp_", "sntp_recv"], |  | ||||||
|     "scheduler": [ |  | ||||||
|         "run_scheduled_", |  | ||||||
|         "compute_scheduled_", |  | ||||||
|         "event_TaskQueue", |  | ||||||
|     ], |     ], | ||||||
|     "hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"], |     "hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"], | ||||||
|     "libc": [ |     "libc": [ | ||||||
|         "printf", |         "printf", | ||||||
|         "scanf", |         "scanf", | ||||||
|         "malloc", |         "malloc", | ||||||
|         "_free",  # More specific than "free" to match _free, __free_r, etc. but not arbitrary "free" substring |         "free", | ||||||
|         "umm_free",  # UMM malloc free function |  | ||||||
|         "memcpy", |         "memcpy", | ||||||
|         "memset", |         "memset", | ||||||
|         "strcpy", |         "strcpy", | ||||||
| @@ -336,7 +259,7 @@ SYMBOL_PATTERNS = { | |||||||
|         "_setenv_r", |         "_setenv_r", | ||||||
|         "_tzset_unlocked_r", |         "_tzset_unlocked_r", | ||||||
|         "__tzcalc_limits", |         "__tzcalc_limits", | ||||||
|         "_select",  # More specific than "select" to avoid matching "dhcp_select", etc. |         "select", | ||||||
|         "scalbnf", |         "scalbnf", | ||||||
|         "strtof", |         "strtof", | ||||||
|         "strtof_l", |         "strtof_l", | ||||||
| @@ -393,24 +316,8 @@ SYMBOL_PATTERNS = { | |||||||
|         "CSWTCH$", |         "CSWTCH$", | ||||||
|         "dst$", |         "dst$", | ||||||
|         "sulp", |         "sulp", | ||||||
|         "_strtol_l",  # String to long with locale |  | ||||||
|         "__cvt",  # Convert |  | ||||||
|         "__utoa",  # Unsigned to ASCII |  | ||||||
|         "__global_locale",  # Global locale |  | ||||||
|         "_ctype_",  # Character type |  | ||||||
|         "impure_data",  # Impure data |  | ||||||
|     ], |  | ||||||
|     "string_ops": [ |  | ||||||
|         "strcmp", |  | ||||||
|         "strncmp", |  | ||||||
|         "strchr", |  | ||||||
|         "strstr", |  | ||||||
|         "strtok", |  | ||||||
|         "strdup", |  | ||||||
|         "strncasecmp_P",  # String compare (case insensitive, from program memory) |  | ||||||
|         "strnlen_P",  # String length (from program memory) |  | ||||||
|         "strncat_P",  # String concatenate (from program memory) |  | ||||||
|     ], |     ], | ||||||
|  |     "string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"], | ||||||
|     "memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"], |     "memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"], | ||||||
|     "file_io": [ |     "file_io": [ | ||||||
|         "fread", |         "fread", | ||||||
| @@ -431,26 +338,10 @@ SYMBOL_PATTERNS = { | |||||||
|         "vsscanf", |         "vsscanf", | ||||||
|     ], |     ], | ||||||
|     "cpp_anonymous": ["_GLOBAL__N_", "n$"], |     "cpp_anonymous": ["_GLOBAL__N_", "n$"], | ||||||
|     # Plain C patterns only - C++ symbols will be categorized via DEMANGLED_PATTERNS |     "cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality", "_Z16"], | ||||||
|     "nvs": ["nvs_"],  # Plain C NVS functions |     "exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"], | ||||||
|     "ota": ["ota_", "OTA", "esp_ota", "app_desc"], |  | ||||||
|     # cpp_runtime: Removed _ZN, _ZL to let DEMANGLED_PATTERNS categorize C++ symbols properly |  | ||||||
|     # Only keep patterns that are truly runtime-specific and not categorizable by namespace |  | ||||||
|     "cpp_runtime": ["__cxx", "_ZSt", "__gxx_personality", "_Z16"], |  | ||||||
|     "exception_handling": [ |  | ||||||
|         "__cxa_", |  | ||||||
|         "_Unwind_", |  | ||||||
|         "__gcc_personality", |  | ||||||
|         "uw_frame_state", |  | ||||||
|         "search_object",  # Search for exception handling object |  | ||||||
|         "get_cie_encoding",  # Get CIE encoding |  | ||||||
|         "add_fdes",  # Add frame description entries |  | ||||||
|         "fde_unencoded_compare",  # Compare FDEs |  | ||||||
|         "fde_mixed_encoding_compare",  # Compare mixed encoding FDEs |  | ||||||
|         "frame_downheap",  # Frame heap operations |  | ||||||
|         "frame_heapsort",  # Frame heap sorting |  | ||||||
|     ], |  | ||||||
|     "static_init": ["_GLOBAL__sub_I_"], |     "static_init": ["_GLOBAL__sub_I_"], | ||||||
|  |     "mdns_lib": ["mdns"], | ||||||
|     "phy_radio": [ |     "phy_radio": [ | ||||||
|         "phy_", |         "phy_", | ||||||
|         "rf_", |         "rf_", | ||||||
| @@ -503,47 +394,10 @@ SYMBOL_PATTERNS = { | |||||||
|         "txcal_debuge_mode", |         "txcal_debuge_mode", | ||||||
|         "ant_wifitx_cfg", |         "ant_wifitx_cfg", | ||||||
|         "reg_init_begin", |         "reg_init_begin", | ||||||
|         "tx_cap_init",  # TX capacitance init |  | ||||||
|         "ram_set_txcap",  # RAM TX capacitance setting |  | ||||||
|         "tx_atten_",  # TX attenuation |  | ||||||
|         "txiq_",  # TX I/Q calibration |  | ||||||
|         "ram_cal_",  # RAM calibration |  | ||||||
|         "ram_rxiq_",  # RAM RX I/Q |  | ||||||
|         "readvdd33",  # Read VDD33 |  | ||||||
|         "test_tout",  # Test timeout |  | ||||||
|         "tsen_meas",  # Temperature sensor measurement |  | ||||||
|         "bbpll_cal",  # Baseband PLL calibration |  | ||||||
|         "set_cal_",  # Set calibration |  | ||||||
|         "set_rfanagain_",  # Set RF analog gain |  | ||||||
|         "set_txdc_",  # Set TX DC |  | ||||||
|         "get_vdd33_",  # Get VDD33 |  | ||||||
|         "gen_rx_gain_table",  # Generate RX gain table |  | ||||||
|         "ram_ana_inf_gating_en",  # RAM analog interface gating enable |  | ||||||
|         "tx_cont_en",  # TX continuous enable |  | ||||||
|         "tx_delay_cfg",  # TX delay configuration |  | ||||||
|         "tx_gain_table_set",  # TX gain table set |  | ||||||
|         "check_and_reset_hw_deadlock",  # Hardware deadlock check |  | ||||||
|         "s_config",  # System/hardware config |  | ||||||
|         "chan14_mic_cfg",  # Channel 14 MIC config |  | ||||||
|     ], |  | ||||||
|     "wifi_phy_pp": [ |  | ||||||
|         "pp_", |  | ||||||
|         "ppT", |  | ||||||
|         "ppR", |  | ||||||
|         "ppP", |  | ||||||
|         "ppInstall", |  | ||||||
|         "ppCalTxAMPDULength", |  | ||||||
|         "ppCheckTx",  # Packet processor TX check |  | ||||||
|         "ppCal",  # Packet processor calibration |  | ||||||
|         "HdlAllBuffedEb",  # Handle buffered EB |  | ||||||
|     ], |     ], | ||||||
|  |     "wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"], | ||||||
|     "wifi_lmac": ["lmac"], |     "wifi_lmac": ["lmac"], | ||||||
|     "wifi_device": [ |     "wifi_device": ["wdev", "wDev_"], | ||||||
|         "wdev", |  | ||||||
|         "wDev_", |  | ||||||
|         "ic_set_sta",  # Set station mode |  | ||||||
|         "ic_set_vif",  # Set virtual interface |  | ||||||
|     ], |  | ||||||
|     "power_mgmt": [ |     "power_mgmt": [ | ||||||
|         "pm_", |         "pm_", | ||||||
|         "sleep", |         "sleep", | ||||||
| @@ -552,7 +406,15 @@ SYMBOL_PATTERNS = { | |||||||
|         "deep_sleep", |         "deep_sleep", | ||||||
|         "power_down", |         "power_down", | ||||||
|         "g_pm", |         "g_pm", | ||||||
|         "pmc",  # Power Management Controller |     ], | ||||||
|  |     "memory_mgmt": [ | ||||||
|  |         "mem_", | ||||||
|  |         "memory_", | ||||||
|  |         "tlsf_", | ||||||
|  |         "memp_", | ||||||
|  |         "pbuf_", | ||||||
|  |         "pbuf_alloc", | ||||||
|  |         "pbuf_copy_partial_pbuf", | ||||||
|     ], |     ], | ||||||
|     "hal_layer": ["hal_"], |     "hal_layer": ["hal_"], | ||||||
|     "clock_mgmt": [ |     "clock_mgmt": [ | ||||||
| @@ -577,6 +439,7 @@ SYMBOL_PATTERNS = { | |||||||
|     "error_handling": ["panic", "abort", "assert", "error_", "fault"], |     "error_handling": ["panic", "abort", "assert", "error_", "fault"], | ||||||
|     "authentication": ["auth"], |     "authentication": ["auth"], | ||||||
|     "ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"], |     "ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"], | ||||||
|  |     "dhcp": ["dhcp", "handle_dhcp"], | ||||||
|     "ethernet_phy": [ |     "ethernet_phy": [ | ||||||
|         "emac_", |         "emac_", | ||||||
|         "eth_phy_", |         "eth_phy_", | ||||||
| @@ -755,15 +618,7 @@ SYMBOL_PATTERNS = { | |||||||
|         "ampdu_dispatch_upto", |         "ampdu_dispatch_upto", | ||||||
|     ], |     ], | ||||||
|     "ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"], |     "ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"], | ||||||
|     "rate_control": [ |     "rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"], | ||||||
|         "rssi_margin", |  | ||||||
|         "rcGetSched", |  | ||||||
|         "get_rate_fcc_index", |  | ||||||
|         "rcGetRate",  # Get rate |  | ||||||
|         "rc_get_",  # Rate control getters |  | ||||||
|         "rc_set_",  # Rate control setters |  | ||||||
|         "rc_enable_",  # Rate control enable functions |  | ||||||
|     ], |  | ||||||
|     "nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"], |     "nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"], | ||||||
|     "channel_mgmt": ["chm_init", "chm_set_current_channel"], |     "channel_mgmt": ["chm_init", "chm_set_current_channel"], | ||||||
|     "trace": ["trc_init", "trc_onAmpduOp"], |     "trace": ["trc_init", "trc_onAmpduOp"], | ||||||
| @@ -944,18 +799,31 @@ SYMBOL_PATTERNS = { | |||||||
|         "supports_interlaced_inquiry_scan", |         "supports_interlaced_inquiry_scan", | ||||||
|         "supports_reading_remote_extended_features", |         "supports_reading_remote_extended_features", | ||||||
|     ], |     ], | ||||||
|  |     "bluetooth_ll": [ | ||||||
|  |         "lld_pdu_", | ||||||
|  |         "ld_acl_", | ||||||
|  |         "lld_stop_ind_handler", | ||||||
|  |         "lld_evt_winsize_change", | ||||||
|  |         "config_lld_evt_funcs_reset", | ||||||
|  |         "config_lld_funcs_reset", | ||||||
|  |         "config_llm_funcs_reset", | ||||||
|  |         "llm_set_long_adv_data", | ||||||
|  |         "lld_retry_tx_prog", | ||||||
|  |         "llc_link_sup_to_ind_handler", | ||||||
|  |         "config_llc_funcs_reset", | ||||||
|  |         "lld_evt_rxwin_compute", | ||||||
|  |         "config_btdm_funcs_reset", | ||||||
|  |         "config_ea_funcs_reset", | ||||||
|  |         "llc_defalut_state_tab_reset", | ||||||
|  |         "config_rwip_funcs_reset", | ||||||
|  |         "ke_lmp_rx_flooding_detect", | ||||||
|  |     ], | ||||||
| } | } | ||||||
|  |  | ||||||
| # Demangled patterns: patterns found in demangled C++ names | # Demangled patterns: patterns found in demangled C++ names | ||||||
| DEMANGLED_PATTERNS = { | DEMANGLED_PATTERNS = { | ||||||
|     "gpio_driver": ["GPIO"], |     "gpio_driver": ["GPIO"], | ||||||
|     "uart_driver": ["UART"], |     "uart_driver": ["UART"], | ||||||
|     # mdns_lib must come before network_stack to avoid "udp" matching "_udpReadBuffer" in MDNSResponder |  | ||||||
|     "mdns_lib": [ |  | ||||||
|         "MDNSResponder", |  | ||||||
|         "MDNSImplementation", |  | ||||||
|         "MDNS", |  | ||||||
|     ], |  | ||||||
|     "network_stack": [ |     "network_stack": [ | ||||||
|         "lwip", |         "lwip", | ||||||
|         "tcp", |         "tcp", | ||||||
| @@ -968,24 +836,6 @@ DEMANGLED_PATTERNS = { | |||||||
|         "ethernet", |         "ethernet", | ||||||
|         "ppp", |         "ppp", | ||||||
|         "slip", |         "slip", | ||||||
|         "UdpContext",  # UDP context class |  | ||||||
|         "DhcpServer",  # DHCP server class |  | ||||||
|     ], |  | ||||||
|     "arduino_core": [ |  | ||||||
|         "String::",  # Arduino String class |  | ||||||
|         "Print::",  # Arduino Print class |  | ||||||
|         "HardwareSerial::",  # Serial class |  | ||||||
|         "IPAddress::",  # IP address class |  | ||||||
|         "EspClass::",  # ESP class |  | ||||||
|         "experimental::_SPI",  # Experimental SPI |  | ||||||
|     ], |  | ||||||
|     "ota": [ |  | ||||||
|         "UpdaterClass", |  | ||||||
|         "Updater::", |  | ||||||
|     ], |  | ||||||
|     "wifi": [ |  | ||||||
|         "ESP8266WiFi", |  | ||||||
|         "WiFi::", |  | ||||||
|     ], |     ], | ||||||
|     "wifi_stack": ["NetworkInterface"], |     "wifi_stack": ["NetworkInterface"], | ||||||
|     "nimble_bt": [ |     "nimble_bt": [ | ||||||
| @@ -1004,6 +854,7 @@ DEMANGLED_PATTERNS = { | |||||||
|     "rtti": ["__type_info", "__class_type_info"], |     "rtti": ["__type_info", "__class_type_info"], | ||||||
|     "web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"], |     "web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"], | ||||||
|     "async_tcp": ["AsyncClient", "AsyncServer"], |     "async_tcp": ["AsyncClient", "AsyncServer"], | ||||||
|  |     "mdns_lib": ["mdns"], | ||||||
|     "json_lib": [ |     "json_lib": [ | ||||||
|         "ArduinoJson", |         "ArduinoJson", | ||||||
|         "JsonDocument", |         "JsonDocument", | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode | |||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   climate::ClimateTraits traits() override { |   climate::ClimateTraits traits() override { | ||||||
|     auto traits = climate::ClimateTraits(); |     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_supported_modes({climate::CLIMATE_MODE_OFF, climate::ClimateMode::CLIMATE_MODE_HEAT}); | ||||||
|     traits.set_visual_min_temperature(25.0); |     traits.set_visual_min_temperature(25.0); | ||||||
|     traits.set_visual_max_temperature(100.0); |     traits.set_visual_max_temperature(100.0); | ||||||
|   | |||||||
| @@ -155,17 +155,6 @@ def _validate_api_config(config: ConfigType) -> ConfigType: | |||||||
|     return config |     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( | CONFIG_SCHEMA = cv.All( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
|         { |         { | ||||||
| @@ -233,7 +222,6 @@ CONFIG_SCHEMA = cv.All( | |||||||
|     ).extend(cv.COMPONENT_SCHEMA), |     ).extend(cv.COMPONENT_SCHEMA), | ||||||
|     cv.rename_key(CONF_SERVICES, CONF_ACTIONS), |     cv.rename_key(CONF_SERVICES, CONF_ACTIONS), | ||||||
|     _validate_api_config, |     _validate_api_config, | ||||||
|     _consume_api_sockets, |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | // TEST | ||||||
| #include "api_connection.h" | #include "api_connection.h" | ||||||
| #ifdef USE_API | #ifdef USE_API | ||||||
| #ifdef USE_API_NOISE | #ifdef USE_API_NOISE | ||||||
| @@ -1572,13 +1573,7 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption | |||||||
|   resp.success = false; |   resp.success = false; | ||||||
|  |  | ||||||
|   psk_t psk{}; |   psk_t psk{}; | ||||||
|   if (msg.key.empty()) { |   if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { | ||||||
|     if (this->parent_->clear_noise_psk(true)) { |  | ||||||
|       resp.success = true; |  | ||||||
|     } else { |  | ||||||
|       ESP_LOGW(TAG, "Failed to clear encryption key"); |  | ||||||
|     } |  | ||||||
|   } else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { |  | ||||||
|     ESP_LOGW(TAG, "Invalid encryption key length"); |     ESP_LOGW(TAG, "Invalid encryption key length"); | ||||||
|   } else if (!this->parent_->save_noise_psk(psk, true)) { |   } else if (!this->parent_->save_noise_psk(psk, true)) { | ||||||
|     ESP_LOGW(TAG, "Failed to save encryption key"); |     ESP_LOGW(TAG, "Failed to save encryption key"); | ||||||
|   | |||||||
| @@ -468,31 +468,6 @@ uint16_t APIServer::get_port() const { return this->port_; } | |||||||
| void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } | void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } | ||||||
|  |  | ||||||
| #ifdef USE_API_NOISE | #ifdef USE_API_NOISE | ||||||
| bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, |  | ||||||
|                                   const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) { |  | ||||||
|   if (!this->noise_pref_.save(&new_psk)) { |  | ||||||
|     ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg)); |  | ||||||
|     return false; |  | ||||||
|   } |  | ||||||
|   // ensure it's written immediately |  | ||||||
|   if (!global_preferences->sync()) { |  | ||||||
|     ESP_LOGW(TAG, "Failed to sync preferences"); |  | ||||||
|     return false; |  | ||||||
|   } |  | ||||||
|   ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg)); |  | ||||||
|   if (make_active) { |  | ||||||
|     this->set_timeout(100, [this, active_psk]() { |  | ||||||
|       ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); |  | ||||||
|       this->set_noise_psk(active_psk); |  | ||||||
|       for (auto &c : this->clients_) { |  | ||||||
|         DisconnectRequest req; |  | ||||||
|         c->send_message(req, DisconnectRequest::MESSAGE_TYPE); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   return true; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool APIServer::save_noise_psk(psk_t psk, bool make_active) { | bool APIServer::save_noise_psk(psk_t psk, bool make_active) { | ||||||
| #ifdef USE_API_NOISE_PSK_FROM_YAML | #ifdef USE_API_NOISE_PSK_FROM_YAML | ||||||
|   // When PSK is set from YAML, this function should never be called |   // When PSK is set from YAML, this function should never be called | ||||||
| @@ -507,21 +482,27 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   SavedNoisePsk new_saved_psk{psk}; |   SavedNoisePsk new_saved_psk{psk}; | ||||||
|   return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk, |   if (!this->noise_pref_.save(&new_saved_psk)) { | ||||||
|                                  make_active); |     ESP_LOGW(TAG, "Failed to save Noise PSK"); | ||||||
| #endif |     return false; | ||||||
| } |   } | ||||||
| bool APIServer::clear_noise_psk(bool make_active) { |   // ensure it's written immediately | ||||||
| #ifdef USE_API_NOISE_PSK_FROM_YAML |   if (!global_preferences->sync()) { | ||||||
|   // When PSK is set from YAML, this function should never be called |     ESP_LOGW(TAG, "Failed to sync preferences"); | ||||||
|   // but if it is, reject the change |     return false; | ||||||
|   ESP_LOGW(TAG, "Key set in YAML"); |   } | ||||||
|   return false; |   ESP_LOGD(TAG, "Noise PSK saved"); | ||||||
| #else |   if (make_active) { | ||||||
|   SavedNoisePsk empty_psk{}; |     this->set_timeout(100, [this, psk]() { | ||||||
|   psk_t empty{}; |       ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); | ||||||
|   return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty, |       this->set_noise_psk(psk); | ||||||
|                                  make_active); |       for (auto &c : this->clients_) { | ||||||
|  |         DisconnectRequest req; | ||||||
|  |         c->send_message(req, DisconnectRequest::MESSAGE_TYPE); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |   return true; | ||||||
| #endif | #endif | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -53,7 +53,6 @@ class APIServer : public Component, public Controller { | |||||||
|  |  | ||||||
| #ifdef USE_API_NOISE | #ifdef USE_API_NOISE | ||||||
|   bool save_noise_psk(psk_t psk, bool make_active = true); |   bool save_noise_psk(psk_t psk, bool make_active = true); | ||||||
|   bool clear_noise_psk(bool make_active = true); |  | ||||||
|   void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); } |   void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); } | ||||||
|   std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; } |   std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; } | ||||||
| #endif  // USE_API_NOISE | #endif  // USE_API_NOISE | ||||||
| @@ -175,10 +174,6 @@ class APIServer : public Component, public Controller { | |||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   void schedule_reboot_timeout_(); |   void schedule_reboot_timeout_(); | ||||||
| #ifdef USE_API_NOISE |  | ||||||
|   bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, |  | ||||||
|                          const psk_t &active_psk, bool make_active); |  | ||||||
| #endif  // USE_API_NOISE |  | ||||||
|   // Pointers and pointer-like types first (4 bytes each) |   // Pointers and pointer-like types first (4 bytes each) | ||||||
|   std::unique_ptr<socket::Socket> socket_ = nullptr; |   std::unique_ptr<socket::Socket> socket_ = nullptr; | ||||||
| #ifdef USE_API_CLIENT_CONNECTED_TRIGGER | #ifdef USE_API_CLIENT_CONNECTED_TRIGGER | ||||||
|   | |||||||
| @@ -6,9 +6,6 @@ namespace bang_bang { | |||||||
|  |  | ||||||
| static const char *const TAG = "bang_bang.climate"; | 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() { | void BangBangClimate::setup() { | ||||||
|   this->sensor_->add_on_state_callback([this](float state) { |   this->sensor_->add_on_state_callback([this](float state) { | ||||||
|     this->current_temperature = state; |     this->current_temperature = state; | ||||||
| @@ -34,63 +31,53 @@ void BangBangClimate::setup() { | |||||||
|     restore->to_call(this).perform(); |     restore->to_call(this).perform(); | ||||||
|   } else { |   } else { | ||||||
|     // restore from defaults, change_away handles those for us |     // 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; |       this->mode = climate::CLIMATE_MODE_HEAT_COOL; | ||||||
|     } else if (this->supports_cool_) { |     } else if (supports_cool_) { | ||||||
|       this->mode = climate::CLIMATE_MODE_COOL; |       this->mode = climate::CLIMATE_MODE_COOL; | ||||||
|     } else if (this->supports_heat_) { |     } else if (supports_heat_) { | ||||||
|       this->mode = climate::CLIMATE_MODE_HEAT; |       this->mode = climate::CLIMATE_MODE_HEAT; | ||||||
|     } |     } | ||||||
|     this->change_away_(false); |     this->change_away_(false); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void BangBangClimate::control(const climate::ClimateCall &call) { | void BangBangClimate::control(const climate::ClimateCall &call) { | ||||||
|   if (call.get_mode().has_value()) { |   if (call.get_mode().has_value()) | ||||||
|     this->mode = *call.get_mode(); |     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(); |     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(); |     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->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); | ||||||
|   } |  | ||||||
|  |  | ||||||
|   this->compute_state_(); |   this->compute_state_(); | ||||||
|   this->publish_state(); |   this->publish_state(); | ||||||
| } | } | ||||||
|  |  | ||||||
| climate::ClimateTraits BangBangClimate::traits() { | climate::ClimateTraits BangBangClimate::traits() { | ||||||
|   auto traits = climate::ClimateTraits(); |   auto traits = climate::ClimateTraits(); | ||||||
|   traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | |   traits.set_supports_current_temperature(true); | ||||||
|                            climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION); |   if (this->humidity_sensor_ != nullptr) | ||||||
|   if (this->humidity_sensor_ != nullptr) { |     traits.set_supports_current_humidity(true); | ||||||
|     traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY); |  | ||||||
|   } |  | ||||||
|   traits.set_supported_modes({ |   traits.set_supported_modes({ | ||||||
|       climate::CLIMATE_MODE_OFF, |       climate::CLIMATE_MODE_OFF, | ||||||
|   }); |   }); | ||||||
|   if (this->supports_cool_) { |   if (supports_cool_) | ||||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_COOL); |     traits.add_supported_mode(climate::CLIMATE_MODE_COOL); | ||||||
|   } |   if (supports_heat_) | ||||||
|   if (this->supports_heat_) { |  | ||||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); |     traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); | ||||||
|   } |   if (supports_cool_ && supports_heat_) | ||||||
|   if (this->supports_cool_ && this->supports_heat_) { |  | ||||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); |     traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); | ||||||
|   } |   traits.set_supports_two_point_target_temperature(true); | ||||||
|   if (this->supports_away_) { |   if (supports_away_) { | ||||||
|     traits.set_supported_presets({ |     traits.set_supported_presets({ | ||||||
|         climate::CLIMATE_PRESET_HOME, |         climate::CLIMATE_PRESET_HOME, | ||||||
|         climate::CLIMATE_PRESET_AWAY, |         climate::CLIMATE_PRESET_AWAY, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |   traits.set_supports_action(true); | ||||||
|   return traits; |   return traits; | ||||||
| } | } | ||||||
|  |  | ||||||
| void BangBangClimate::compute_state_() { | void BangBangClimate::compute_state_() { | ||||||
|   if (this->mode == climate::CLIMATE_MODE_OFF) { |   if (this->mode == climate::CLIMATE_MODE_OFF) { | ||||||
|     this->switch_to_action_(climate::CLIMATE_ACTION_OFF); |     this->switch_to_action_(climate::CLIMATE_ACTION_OFF); | ||||||
| @@ -135,7 +122,6 @@ void BangBangClimate::compute_state_() { | |||||||
|  |  | ||||||
|   this->switch_to_action_(target_action); |   this->switch_to_action_(target_action); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BangBangClimate::switch_to_action_(climate::ClimateAction action) { | void BangBangClimate::switch_to_action_(climate::ClimateAction action) { | ||||||
|   if (action == this->action) { |   if (action == this->action) { | ||||||
|     // already in target mode |     // already in target mode | ||||||
| @@ -180,7 +166,6 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { | |||||||
|   this->prev_trigger_ = trig; |   this->prev_trigger_ = trig; | ||||||
|   this->publish_state(); |   this->publish_state(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BangBangClimate::change_away_(bool away) { | void BangBangClimate::change_away_(bool away) { | ||||||
|   if (!away) { |   if (!away) { | ||||||
|     this->target_temperature_low = this->normal_config_.default_temperature_low; |     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; |   this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME; | ||||||
| } | } | ||||||
|  |  | ||||||
| void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) { | void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) { | ||||||
|   this->normal_config_ = normal_config; |   this->normal_config_ = normal_config; | ||||||
| } | } | ||||||
|  |  | ||||||
| void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) { | void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) { | ||||||
|   this->supports_away_ = true; |   this->supports_away_ = true; | ||||||
|   this->away_config_ = away_config; |   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_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } | ||||||
| void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_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_idle_trigger() const { return this->idle_trigger_; } | ||||||
| Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_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; } | 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::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } | ||||||
|  |  | ||||||
| void BangBangClimate::dump_config() { | void BangBangClimate::dump_config() { | ||||||
|   LOG_CLIMATE("", "Bang Bang Climate", this); |   LOG_CLIMATE("", "Bang Bang Climate", this); | ||||||
|   ESP_LOGCONFIG(TAG, |   ESP_LOGCONFIG(TAG, | ||||||
|   | |||||||
| @@ -25,15 +25,14 @@ class BangBangClimate : public climate::Climate, public Component { | |||||||
|  |  | ||||||
|   void set_sensor(sensor::Sensor *sensor); |   void set_sensor(sensor::Sensor *sensor); | ||||||
|   void set_humidity_sensor(sensor::Sensor *humidity_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); |   void set_supports_cool(bool supports_cool); | ||||||
|  |   Trigger<> *get_heat_trigger() const; | ||||||
|   void set_supports_heat(bool supports_heat); |   void set_supports_heat(bool supports_heat); | ||||||
|   void set_normal_config(const BangBangClimateTargetTempConfig &normal_config); |   void set_normal_config(const BangBangClimateTargetTempConfig &normal_config); | ||||||
|   void set_away_config(const BangBangClimateTargetTempConfig &away_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: |  protected: | ||||||
|   /// Override control to change settings of the climate device. |   /// Override control to change settings of the climate device. | ||||||
|   void control(const climate::ClimateCall &call) override; |   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. |    * 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. |   /** 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. |   /** 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 |    * 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. |    * (blinds open) is possible. | ||||||
|    */ |    */ | ||||||
|   Trigger<> *heat_trigger_{nullptr}; |   Trigger<> *heat_trigger_{nullptr}; | ||||||
|  |   bool supports_heat_{false}; | ||||||
|   /** A reference to the trigger that was previously active. |   /** A reference to the trigger that was previously active. | ||||||
|    * |    * | ||||||
|    * This is so that the previous trigger can be stopped before enabling a new one. |    * This is so that the previous trigger can be stopped before enabling a new one. | ||||||
|    */ |    */ | ||||||
|   Trigger<> *prev_trigger_{nullptr}; |   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_{}; |   BangBangClimateTargetTempConfig normal_config_{}; | ||||||
|  |   bool supports_away_{false}; | ||||||
|   BangBangClimateTargetTempConfig away_config_{}; |   BangBangClimateTargetTempConfig away_config_{}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -33,7 +33,8 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli | |||||||
|  |  | ||||||
|   climate::ClimateTraits traits() override { |   climate::ClimateTraits traits() override { | ||||||
|     auto traits = climate::ClimateTraits(); |     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({ |     traits.set_supported_modes({ | ||||||
|         climate::CLIMATE_MODE_OFF, |         climate::CLIMATE_MODE_OFF, | ||||||
|         climate::CLIMATE_MODE_HEAT, |         climate::CLIMATE_MODE_HEAT, | ||||||
|   | |||||||
| @@ -264,31 +264,20 @@ async def delayed_off_filter_to_code(config, filter_id): | |||||||
|     ), |     ), | ||||||
| ) | ) | ||||||
| async def autorepeat_filter_to_code(config, filter_id): | async def autorepeat_filter_to_code(config, filter_id): | ||||||
|  |     timings = [] | ||||||
|     if len(config) > 0: |     if len(config) > 0: | ||||||
|         timings = [ |         timings.extend( | ||||||
|             cg.StructInitializer( |             (conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON]) | ||||||
|                 cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"), |  | ||||||
|                 ("delay", conf[CONF_DELAY]), |  | ||||||
|                 ("time_off", conf[CONF_TIME_OFF]), |  | ||||||
|                 ("time_on", conf[CONF_TIME_ON]), |  | ||||||
|             ) |  | ||||||
|             for conf in config |             for conf in config | ||||||
|         ] |         ) | ||||||
|     else: |     else: | ||||||
|         timings = [ |         timings.append( | ||||||
|             cg.StructInitializer( |             ( | ||||||
|                 cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"), |                 cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds, | ||||||
|                 ("delay", cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds), |                 cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds, | ||||||
|                 ( |                 cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds, | ||||||
|                     "time_off", |  | ||||||
|                     cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds, |  | ||||||
|                 ), |  | ||||||
|                 ( |  | ||||||
|                     "time_on", |  | ||||||
|                     cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds, |  | ||||||
|                 ), |  | ||||||
|             ) |             ) | ||||||
|         ] |         ) | ||||||
|     var = cg.new_Pvariable(filter_id, timings) |     var = cg.new_Pvariable(filter_id, timings) | ||||||
|     await cg.register_component(var, {}) |     await cg.register_component(var, {}) | ||||||
|     return var |     return var | ||||||
|   | |||||||
| @@ -2,11 +2,11 @@ | |||||||
|  |  | ||||||
| #include <cinttypes> | #include <cinttypes> | ||||||
| #include <utility> | #include <utility> | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/automation.h" | #include "esphome/core/automation.h" | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
| #include "esphome/core/helpers.h" |  | ||||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | #include "esphome/components/binary_sensor/binary_sensor.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| @@ -92,8 +92,8 @@ class DoubleClickTrigger : public Trigger<> { | |||||||
|  |  | ||||||
| class MultiClickTrigger : public Trigger<>, public Component { | class MultiClickTrigger : public Trigger<>, public Component { | ||||||
|  public: |  public: | ||||||
|   explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing) |   explicit MultiClickTrigger(BinarySensor *parent, std::vector<MultiClickTriggerEvent> timing) | ||||||
|       : parent_(parent), timing_(timing) {} |       : parent_(parent), timing_(std::move(timing)) {} | ||||||
|  |  | ||||||
|   void setup() override { |   void setup() override { | ||||||
|     this->last_state_ = this->parent_->get_state_default(false); |     this->last_state_ = this->parent_->get_state_default(false); | ||||||
| @@ -115,7 +115,7 @@ class MultiClickTrigger : public Trigger<>, public Component { | |||||||
|   void trigger_(); |   void trigger_(); | ||||||
|  |  | ||||||
|   BinarySensor *parent_; |   BinarySensor *parent_; | ||||||
|   FixedVector<MultiClickTriggerEvent> timing_; |   std::vector<MultiClickTriggerEvent> timing_; | ||||||
|   uint32_t invalid_cooldown_{1000}; |   uint32_t invalid_cooldown_{1000}; | ||||||
|   optional<size_t> at_index_{}; |   optional<size_t> at_index_{}; | ||||||
|   bool last_state_{false}; |   bool last_state_{false}; | ||||||
|   | |||||||
| @@ -51,7 +51,7 @@ void BinarySensor::add_filter(Filter *filter) { | |||||||
|     last_filter->next_ = 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) { |   for (Filter *filter : filters) { | ||||||
|     this->add_filter(filter); |     this->add_filter(filter); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ | |||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
| #include "esphome/components/binary_sensor/filter.h" | #include "esphome/components/binary_sensor/filter.h" | ||||||
|  |  | ||||||
| #include <initializer_list> | #include <vector> | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
|  |  | ||||||
| @@ -48,7 +48,7 @@ class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceCl | |||||||
|   void publish_initial_state(bool new_state); |   void publish_initial_state(bool new_state); | ||||||
|  |  | ||||||
|   void add_filter(Filter *filter); |   void add_filter(Filter *filter); | ||||||
|   void add_filters(std::initializer_list<Filter *> filters); |   void add_filters(const std::vector<Filter *> &filters); | ||||||
|  |  | ||||||
|   // ========== INTERNAL METHODS ========== |   // ========== INTERNAL METHODS ========== | ||||||
|   // (In most use cases you won't need these) |   // (In most use cases you won't need these) | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| #include "filter.h" | #include "filter.h" | ||||||
|  |  | ||||||
| #include "binary_sensor.h" | #include "binary_sensor.h" | ||||||
|  | #include <utility> | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
|  |  | ||||||
| @@ -67,7 +68,7 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD | |||||||
|  |  | ||||||
| optional<bool> InvertFilter::new_value(bool value) { return !value; } | optional<bool> InvertFilter::new_value(bool value) { return !value; } | ||||||
|  |  | ||||||
| AutorepeatFilter::AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) : timings_(timings) {} | AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {} | ||||||
|  |  | ||||||
| optional<bool> AutorepeatFilter::new_value(bool value) { | optional<bool> AutorepeatFilter::new_value(bool value) { | ||||||
|   if (value) { |   if (value) { | ||||||
|   | |||||||
| @@ -4,6 +4,8 @@ | |||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
|  |  | ||||||
| namespace binary_sensor { | namespace binary_sensor { | ||||||
| @@ -80,6 +82,11 @@ class InvertFilter : public Filter { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| struct AutorepeatFilterTiming { | struct AutorepeatFilterTiming { | ||||||
|  |   AutorepeatFilterTiming(uint32_t delay, uint32_t off, uint32_t on) { | ||||||
|  |     this->delay = delay; | ||||||
|  |     this->time_off = off; | ||||||
|  |     this->time_on = on; | ||||||
|  |   } | ||||||
|   uint32_t delay; |   uint32_t delay; | ||||||
|   uint32_t time_off; |   uint32_t time_off; | ||||||
|   uint32_t time_on; |   uint32_t time_on; | ||||||
| @@ -87,7 +94,7 @@ struct AutorepeatFilterTiming { | |||||||
|  |  | ||||||
| class AutorepeatFilter : public Filter, public Component { | class AutorepeatFilter : public Filter, public Component { | ||||||
|  public: |  public: | ||||||
|   explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings); |   explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings); | ||||||
|  |  | ||||||
|   optional<bool> new_value(bool value) override; |   optional<bool> new_value(bool value) override; | ||||||
|  |  | ||||||
| @@ -97,7 +104,7 @@ class AutorepeatFilter : public Filter, public Component { | |||||||
|   void next_timing_(); |   void next_timing_(); | ||||||
|   void next_value_(bool val); |   void next_value_(bool val); | ||||||
|  |  | ||||||
|   FixedVector<AutorepeatFilterTiming> timings_; |   std::vector<AutorepeatFilterTiming> timings_; | ||||||
|   uint8_t active_timing_{0}; |   uint8_t active_timing_{0}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,42 +6,6 @@ namespace climate { | |||||||
|  |  | ||||||
| static const char *const TAG = "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() { | void ClimateCall::perform() { | ||||||
|   this->parent_->control_callback_.call(*this); |   this->parent_->control_callback_.call(*this); | ||||||
|   ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); |   ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); | ||||||
| @@ -86,46 +50,47 @@ void ClimateCall::perform() { | |||||||
|   } |   } | ||||||
|   this->parent_->control(*this); |   this->parent_->control(*this); | ||||||
| } | } | ||||||
|  |  | ||||||
| void ClimateCall::validate_() { | void ClimateCall::validate_() { | ||||||
|   auto traits = this->parent_->get_traits(); |   auto traits = this->parent_->get_traits(); | ||||||
|   if (this->mode_.has_value()) { |   if (this->mode_.has_value()) { | ||||||
|     auto mode = *this->mode_; |     auto mode = *this->mode_; | ||||||
|     if (!traits.supports_mode(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(); |       this->mode_.reset(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   if (this->custom_fan_mode_.has_value()) { |   if (this->custom_fan_mode_.has_value()) { | ||||||
|     auto custom_fan_mode = *this->custom_fan_mode_; |     auto custom_fan_mode = *this->custom_fan_mode_; | ||||||
|     if (!traits.supports_custom_fan_mode(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(); |       this->custom_fan_mode_.reset(); | ||||||
|     } |     } | ||||||
|   } else if (this->fan_mode_.has_value()) { |   } else if (this->fan_mode_.has_value()) { | ||||||
|     auto fan_mode = *this->fan_mode_; |     auto fan_mode = *this->fan_mode_; | ||||||
|     if (!traits.supports_fan_mode(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(); |       this->fan_mode_.reset(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   if (this->custom_preset_.has_value()) { |   if (this->custom_preset_.has_value()) { | ||||||
|     auto custom_preset = *this->custom_preset_; |     auto custom_preset = *this->custom_preset_; | ||||||
|     if (!traits.supports_custom_preset(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(); |       this->custom_preset_.reset(); | ||||||
|     } |     } | ||||||
|   } else if (this->preset_.has_value()) { |   } else if (this->preset_.has_value()) { | ||||||
|     auto preset = *this->preset_; |     auto preset = *this->preset_; | ||||||
|     if (!traits.supports_preset(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(); |       this->preset_.reset(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   if (this->swing_mode_.has_value()) { |   if (this->swing_mode_.has_value()) { | ||||||
|     auto swing_mode = *this->swing_mode_; |     auto swing_mode = *this->swing_mode_; | ||||||
|     if (!traits.supports_swing_mode(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(); |       this->swing_mode_.reset(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -134,127 +99,159 @@ void ClimateCall::validate_() { | |||||||
|     if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | |     if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | | ||||||
|                                  CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { |                                  CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { | ||||||
|       ESP_LOGW(TAG, "  Cannot set target temperature for climate device " |       ESP_LOGW(TAG, "  Cannot set target temperature for climate device " | ||||||
|                     "with two-point target temperature"); |                     "with two-point target temperature!"); | ||||||
|       this->target_temperature_.reset(); |       this->target_temperature_.reset(); | ||||||
|     } else if (std::isnan(target)) { |     } 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(); |       this->target_temperature_.reset(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) { |   if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) { | ||||||
|     if (!traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | |     if (!traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | | ||||||
|                                   CLIMATE_REQUIRES_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_low_.reset(); | ||||||
|       this->target_temperature_high_.reset(); |       this->target_temperature_high_.reset(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   if (this->target_temperature_low_.has_value() && std::isnan(*this->target_temperature_low_)) { |   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(); |     this->target_temperature_low_.reset(); | ||||||
|   } |   } | ||||||
|   if (this->target_temperature_high_.has_value() && std::isnan(*this->target_temperature_high_)) { |   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(); |     this->target_temperature_high_.reset(); | ||||||
|   } |   } | ||||||
|   if (this->target_temperature_low_.has_value() && this->target_temperature_high_.has_value()) { |   if (this->target_temperature_low_.has_value() && this->target_temperature_high_.has_value()) { | ||||||
|     float low = *this->target_temperature_low_; |     float low = *this->target_temperature_low_; | ||||||
|     float high = *this->target_temperature_high_; |     float high = *this->target_temperature_high_; | ||||||
|     if (low > 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_low_.reset(); | ||||||
|       this->target_temperature_high_.reset(); |       this->target_temperature_high_.reset(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_mode(ClimateMode mode) { | ClimateCall &ClimateCall::set_mode(ClimateMode mode) { | ||||||
|   this->mode_ = mode; |   this->mode_ = mode; | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_mode(const std::string &mode) { | ClimateCall &ClimateCall::set_mode(const std::string &mode) { | ||||||
|   for (const auto &mode_entry : CLIMATE_MODES_BY_STR) { |   if (str_equals_case_insensitive(mode, "OFF")) { | ||||||
|     if (str_equals_case_insensitive(mode, mode_entry.str)) { |     this->set_mode(CLIMATE_MODE_OFF); | ||||||
|       this->set_mode(static_cast<ClimateMode>(mode_entry.value)); |   } else if (str_equals_case_insensitive(mode, "AUTO")) { | ||||||
|       return *this; |     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; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { | ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { | ||||||
|   this->fan_mode_ = fan_mode; |   this->fan_mode_ = fan_mode; | ||||||
|   this->custom_fan_mode_.reset(); |   this->custom_fan_mode_.reset(); | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { | 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, "ON")) { | ||||||
|     if (str_equals_case_insensitive(fan_mode, mode_entry.str)) { |     this->set_fan_mode(CLIMATE_FAN_ON); | ||||||
|       this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value)); |   } else if (str_equals_case_insensitive(fan_mode, "OFF")) { | ||||||
|       return *this; |     this->set_fan_mode(CLIMATE_FAN_OFF); | ||||||
|     } |   } else if (str_equals_case_insensitive(fan_mode, "AUTO")) { | ||||||
|   } |     this->set_fan_mode(CLIMATE_FAN_AUTO); | ||||||
|   if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { |   } else if (str_equals_case_insensitive(fan_mode, "LOW")) { | ||||||
|     this->custom_fan_mode_ = fan_mode; |     this->set_fan_mode(CLIMATE_FAN_LOW); | ||||||
|     this->fan_mode_.reset(); |   } 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 { |   } 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; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) { | ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) { | ||||||
|   if (fan_mode.has_value()) { |   if (fan_mode.has_value()) { | ||||||
|     this->set_fan_mode(fan_mode.value()); |     this->set_fan_mode(fan_mode.value()); | ||||||
|   } |   } | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_preset(ClimatePreset preset) { | ClimateCall &ClimateCall::set_preset(ClimatePreset preset) { | ||||||
|   this->preset_ = preset; |   this->preset_ = preset; | ||||||
|   this->custom_preset_.reset(); |   this->custom_preset_.reset(); | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_preset(const std::string &preset) { | ClimateCall &ClimateCall::set_preset(const std::string &preset) { | ||||||
|   for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) { |   if (str_equals_case_insensitive(preset, "ECO")) { | ||||||
|     if (str_equals_case_insensitive(preset, preset_entry.str)) { |     this->set_preset(CLIMATE_PRESET_ECO); | ||||||
|       this->set_preset(static_cast<ClimatePreset>(preset_entry.value)); |   } else if (str_equals_case_insensitive(preset, "AWAY")) { | ||||||
|       return *this; |     this->set_preset(CLIMATE_PRESET_AWAY); | ||||||
|     } |   } else if (str_equals_case_insensitive(preset, "BOOST")) { | ||||||
|   } |     this->set_preset(CLIMATE_PRESET_BOOST); | ||||||
|   if (this->parent_->get_traits().supports_custom_preset(preset)) { |   } else if (str_equals_case_insensitive(preset, "COMFORT")) { | ||||||
|     this->custom_preset_ = preset; |     this->set_preset(CLIMATE_PRESET_COMFORT); | ||||||
|     this->preset_.reset(); |   } 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 { |   } 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; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_preset(optional<std::string> preset) { | ClimateCall &ClimateCall::set_preset(optional<std::string> preset) { | ||||||
|   if (preset.has_value()) { |   if (preset.has_value()) { | ||||||
|     this->set_preset(preset.value()); |     this->set_preset(preset.value()); | ||||||
|   } |   } | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) { | ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) { | ||||||
|   this->swing_mode_ = swing_mode; |   this->swing_mode_ = swing_mode; | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) { | 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, "OFF")) { | ||||||
|     if (str_equals_case_insensitive(swing_mode, mode_entry.str)) { |     this->set_swing_mode(CLIMATE_SWING_OFF); | ||||||
|       this->set_swing_mode(static_cast<ClimateSwingMode>(mode_entry.value)); |   } else if (str_equals_case_insensitive(swing_mode, "BOTH")) { | ||||||
|       return *this; |     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; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -262,71 +259,59 @@ ClimateCall &ClimateCall::set_target_temperature(float target_temperature) { | |||||||
|   this->target_temperature_ = target_temperature; |   this->target_temperature_ = target_temperature; | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_target_temperature_low(float target_temperature_low) { | ClimateCall &ClimateCall::set_target_temperature_low(float target_temperature_low) { | ||||||
|   this->target_temperature_low_ = target_temperature_low; |   this->target_temperature_low_ = target_temperature_low; | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_high) { | ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_high) { | ||||||
|   this->target_temperature_high_ = target_temperature_high; |   this->target_temperature_high_ = target_temperature_high; | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_target_humidity(float target_humidity) { | ClimateCall &ClimateCall::set_target_humidity(float target_humidity) { | ||||||
|   this->target_humidity_ = target_humidity; |   this->target_humidity_ = target_humidity; | ||||||
|   return *this; |   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() 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_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_temperature_high() const { return this->target_temperature_high_; } | ||||||
| const optional<float> &ClimateCall::get_target_humidity() const { return this->target_humidity_; } | 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<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<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<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) { | ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) { | ||||||
|   this->target_temperature_high_ = target_temperature_high; |   this->target_temperature_high_ = target_temperature_high; | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_target_temperature_low(optional<float> target_temperature_low) { | ClimateCall &ClimateCall::set_target_temperature_low(optional<float> target_temperature_low) { | ||||||
|   this->target_temperature_low_ = target_temperature_low; |   this->target_temperature_low_ = target_temperature_low; | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperature) { | ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperature) { | ||||||
|   this->target_temperature_ = target_temperature; |   this->target_temperature_ = target_temperature; | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_target_humidity(optional<float> target_humidity) { | ClimateCall &ClimateCall::set_target_humidity(optional<float> target_humidity) { | ||||||
|   this->target_humidity_ = target_humidity; |   this->target_humidity_ = target_humidity; | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) { | ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) { | ||||||
|   this->mode_ = mode; |   this->mode_ = mode; | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) { | ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) { | ||||||
|   this->fan_mode_ = fan_mode; |   this->fan_mode_ = fan_mode; | ||||||
|   this->custom_fan_mode_.reset(); |   this->custom_fan_mode_.reset(); | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) { | ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) { | ||||||
|   this->preset_ = preset; |   this->preset_ = preset; | ||||||
|   this->custom_preset_.reset(); |   this->custom_preset_.reset(); | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode) { | ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode) { | ||||||
|   this->swing_mode_ = swing_mode; |   this->swing_mode_ = swing_mode; | ||||||
|   return *this; |   return *this; | ||||||
| @@ -351,7 +336,6 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() { | |||||||
|     return {}; |     return {}; | ||||||
|   return recovered; |   return recovered; | ||||||
| } | } | ||||||
|  |  | ||||||
| void Climate::save_state_() { | void Climate::save_state_() { | ||||||
| #if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ | #if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ | ||||||
|     !defined(CLANG_TIDY) |     !defined(CLANG_TIDY) | ||||||
| @@ -385,14 +369,12 @@ void Climate::save_state_() { | |||||||
|   if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { |   if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { | ||||||
|     state.uses_custom_fan_mode = true; |     state.uses_custom_fan_mode = true; | ||||||
|     const auto &supported = traits.get_supported_custom_fan_modes(); |     const auto &supported = traits.get_supported_custom_fan_modes(); | ||||||
|     // std::set has consistent order (lexicographic for strings) |     std::vector<std::string> vec{supported.begin(), supported.end()}; | ||||||
|     size_t i = 0; |     for (size_t i = 0; i < vec.size(); i++) { | ||||||
|     for (const auto &mode : supported) { |       if (vec[i] == custom_fan_mode) { | ||||||
|       if (mode == custom_fan_mode) { |  | ||||||
|         state.custom_fan_mode = i; |         state.custom_fan_mode = i; | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|       i++; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   if (traits.get_supports_presets() && preset.has_value()) { |   if (traits.get_supports_presets() && preset.has_value()) { | ||||||
| @@ -402,14 +384,12 @@ void Climate::save_state_() { | |||||||
|   if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { |   if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { | ||||||
|     state.uses_custom_preset = true; |     state.uses_custom_preset = true; | ||||||
|     const auto &supported = traits.get_supported_custom_presets(); |     const auto &supported = traits.get_supported_custom_presets(); | ||||||
|     // std::set has consistent order (lexicographic for strings) |     std::vector<std::string> vec{supported.begin(), supported.end()}; | ||||||
|     size_t i = 0; |     for (size_t i = 0; i < vec.size(); i++) { | ||||||
|     for (const auto &preset : supported) { |       if (vec[i] == custom_preset) { | ||||||
|       if (preset == custom_preset) { |  | ||||||
|         state.custom_preset = i; |         state.custom_preset = i; | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|       i++; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   if (traits.get_supports_swing_modes()) { |   if (traits.get_supports_swing_modes()) { | ||||||
| @@ -418,7 +398,6 @@ void Climate::save_state_() { | |||||||
|  |  | ||||||
|   this->rtc_.save(&state); |   this->rtc_.save(&state); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Climate::publish_state() { | void Climate::publish_state() { | ||||||
|   ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str()); |   ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str()); | ||||||
|   auto traits = this->get_traits(); |   auto traits = this->get_traits(); | ||||||
| @@ -490,20 +469,16 @@ ClimateTraits Climate::get_traits() { | |||||||
| void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) { | void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) { | ||||||
|   this->visual_min_temperature_override_ = 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) { | void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) { | ||||||
|   this->visual_max_temperature_override_ = visual_max_temperature_override; |   this->visual_max_temperature_override_ = visual_max_temperature_override; | ||||||
| } | } | ||||||
|  |  | ||||||
| void Climate::set_visual_temperature_step_override(float target, float current) { | void Climate::set_visual_temperature_step_override(float target, float current) { | ||||||
|   this->visual_target_temperature_step_override_ = target; |   this->visual_target_temperature_step_override_ = target; | ||||||
|   this->visual_current_temperature_step_override_ = current; |   this->visual_current_temperature_step_override_ = current; | ||||||
| } | } | ||||||
|  |  | ||||||
| void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) { | void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) { | ||||||
|   this->visual_min_humidity_override_ = 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) { | void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) { | ||||||
|   this->visual_max_humidity_override_ = visual_max_humidity_override; |   this->visual_max_humidity_override_ = visual_max_humidity_override; | ||||||
| } | } | ||||||
| @@ -535,7 +510,6 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { | |||||||
|   } |   } | ||||||
|   return call; |   return call; | ||||||
| } | } | ||||||
|  |  | ||||||
| void ClimateDeviceRestoreState::apply(Climate *climate) { | void ClimateDeviceRestoreState::apply(Climate *climate) { | ||||||
|   auto traits = climate->get_traits(); |   auto traits = climate->get_traits(); | ||||||
|   climate->mode = this->mode; |   climate->mode = this->mode; | ||||||
| @@ -553,34 +527,22 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { | |||||||
|     climate->fan_mode = this->fan_mode; |     climate->fan_mode = this->fan_mode; | ||||||
|   } |   } | ||||||
|   if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) { |   if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) { | ||||||
|     // std::set has consistent order (lexicographic for strings) |     // std::set has consistent order (lexicographic for strings), so this is ok | ||||||
|     const auto &modes = traits.get_supported_custom_fan_modes(); |     const auto &modes = traits.get_supported_custom_fan_modes(); | ||||||
|     if (custom_fan_mode < modes.size()) { |     std::vector<std::string> modes_vec{modes.begin(), modes.end()}; | ||||||
|       size_t i = 0; |     if (custom_fan_mode < modes_vec.size()) { | ||||||
|       for (const auto &mode : modes) { |       climate->custom_fan_mode = modes_vec[this->custom_fan_mode]; | ||||||
|         if (i == this->custom_fan_mode) { |  | ||||||
|           climate->custom_fan_mode = mode; |  | ||||||
|           break; |  | ||||||
|         } |  | ||||||
|         i++; |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   if (traits.get_supports_presets() && !this->uses_custom_preset) { |   if (traits.get_supports_presets() && !this->uses_custom_preset) { | ||||||
|     climate->preset = this->preset; |     climate->preset = this->preset; | ||||||
|   } |   } | ||||||
|   if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) { |   if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) { | ||||||
|     // std::set has consistent order (lexicographic for strings) |     // std::set has consistent order (lexicographic for strings), so this is ok | ||||||
|     const auto &presets = traits.get_supported_custom_presets(); |     const auto &presets = traits.get_supported_custom_presets(); | ||||||
|     if (custom_preset < presets.size()) { |     std::vector<std::string> presets_vec{presets.begin(), presets.end()}; | ||||||
|       size_t i = 0; |     if (custom_preset < presets_vec.size()) { | ||||||
|       for (const auto &preset : presets) { |       climate->custom_preset = presets_vec[this->custom_preset]; | ||||||
|         if (i == this->custom_preset) { |  | ||||||
|           climate->custom_preset = preset; |  | ||||||
|           break; |  | ||||||
|         } |  | ||||||
|         i++; |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   if (traits.get_supports_swing_modes()) { |   if (traits.get_supports_swing_modes()) { | ||||||
| @@ -617,68 +579,68 @@ void Climate::dump_traits_(const char *tag) { | |||||||
|   auto traits = this->get_traits(); |   auto traits = this->get_traits(); | ||||||
|   ESP_LOGCONFIG(tag, "ClimateTraits:"); |   ESP_LOGCONFIG(tag, "ClimateTraits:"); | ||||||
|   ESP_LOGCONFIG(tag, |   ESP_LOGCONFIG(tag, | ||||||
|                 "  Visual settings:\n" |                 "  [x] Visual settings:\n" | ||||||
|                 "  - Min temperature: %.1f\n" |                 "      - Min temperature: %.1f\n" | ||||||
|                 "  - Max temperature: %.1f\n" |                 "      - Max temperature: %.1f\n" | ||||||
|                 "  - Temperature step:\n" |                 "      - Temperature step:\n" | ||||||
|                 "      Target: %.1f", |                 "          Target: %.1f", | ||||||
|                 traits.get_visual_min_temperature(), traits.get_visual_max_temperature(), |                 traits.get_visual_min_temperature(), traits.get_visual_max_temperature(), | ||||||
|                 traits.get_visual_target_temperature_step()); |                 traits.get_visual_target_temperature_step()); | ||||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { |   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 | |   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY | | ||||||
|                                climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { |                                climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) { | ||||||
|     ESP_LOGCONFIG(tag, |     ESP_LOGCONFIG(tag, | ||||||
|                   "  - Min humidity: %.0f\n" |                   "      - Min humidity: %.0f\n" | ||||||
|                   "  - Max humidity: %.0f", |                   "      - Max humidity: %.0f", | ||||||
|                   traits.get_visual_min_humidity(), traits.get_visual_max_humidity()); |                   traits.get_visual_min_humidity(), traits.get_visual_max_humidity()); | ||||||
|   } |   } | ||||||
|   if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | |   if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | | ||||||
|                                CLIMATE_REQUIRES_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)) { |   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)) { |   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)) { |   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)) { |   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()) { |   if (!traits.get_supported_modes().empty()) { | ||||||
|     ESP_LOGCONFIG(tag, "  Supported modes:"); |     ESP_LOGCONFIG(tag, "  [x] Supported modes:"); | ||||||
|     for (ClimateMode m : traits.get_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()) { |   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()) |     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()) { |   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()) |     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()) { |   if (!traits.get_supported_presets().empty()) { | ||||||
|     ESP_LOGCONFIG(tag, "  Supported presets:"); |     ESP_LOGCONFIG(tag, "  [x] Supported presets:"); | ||||||
|     for (ClimatePreset p : traits.get_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()) { |   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()) |     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()) { |   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()) |     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(); |   void perform(); | ||||||
|  |  | ||||||
|  |   const optional<ClimateMode> &get_mode() const; | ||||||
|   const optional<float> &get_target_temperature() const; |   const optional<float> &get_target_temperature() const; | ||||||
|   const optional<float> &get_target_temperature_low() const; |   const optional<float> &get_target_temperature_low() const; | ||||||
|   const optional<float> &get_target_temperature_high() const; |   const optional<float> &get_target_temperature_high() const; | ||||||
|   const optional<float> &get_target_humidity() const; |   const optional<float> &get_target_humidity() const; | ||||||
|  |  | ||||||
|   const optional<ClimateMode> &get_mode() const; |  | ||||||
|   const optional<ClimateFanMode> &get_fan_mode() const; |   const optional<ClimateFanMode> &get_fan_mode() const; | ||||||
|   const optional<ClimateSwingMode> &get_swing_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<std::string> &get_custom_fan_mode() const; | ||||||
|  |   const optional<ClimatePreset> &get_preset() const; | ||||||
|   const optional<std::string> &get_custom_preset() const; |   const optional<std::string> &get_custom_preset() const; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   void validate_(); |   void validate_(); | ||||||
|  |  | ||||||
|   Climate *const parent_; |   Climate *const parent_; | ||||||
|  |   optional<ClimateMode> mode_; | ||||||
|   optional<float> target_temperature_; |   optional<float> target_temperature_; | ||||||
|   optional<float> target_temperature_low_; |   optional<float> target_temperature_low_; | ||||||
|   optional<float> target_temperature_high_; |   optional<float> target_temperature_high_; | ||||||
|   optional<float> target_humidity_; |   optional<float> target_humidity_; | ||||||
|   optional<ClimateMode> mode_; |  | ||||||
|   optional<ClimateFanMode> fan_mode_; |   optional<ClimateFanMode> fan_mode_; | ||||||
|   optional<ClimateSwingMode> swing_mode_; |   optional<ClimateSwingMode> swing_mode_; | ||||||
|   optional<ClimatePreset> preset_; |  | ||||||
|   optional<std::string> custom_fan_mode_; |   optional<std::string> custom_fan_mode_; | ||||||
|  |   optional<ClimatePreset> preset_; | ||||||
|   optional<std::string> custom_preset_; |   optional<std::string> custom_preset_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -170,6 +169,47 @@ class Climate : public EntityBase { | |||||||
|  public: |  public: | ||||||
|   Climate() {} |   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 |   /** 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. |    * (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_min_humidity_override(float visual_min_humidity_override); | ||||||
|   void set_visual_max_humidity_override(float visual_max_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: |  protected: | ||||||
|   friend ClimateCall; |   friend ClimateCall; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,10 +8,7 @@ static const char *const TAG = "climate_ir"; | |||||||
|  |  | ||||||
| climate::ClimateTraits ClimateIR::traits() { | climate::ClimateTraits ClimateIR::traits() { | ||||||
|   auto traits = climate::ClimateTraits(); |   auto traits = climate::ClimateTraits(); | ||||||
|   if (this->sensor_ != nullptr) { |   traits.set_supports_current_temperature(this->sensor_ != nullptr); | ||||||
|     traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); |   traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); | ||||||
|   if (this->supports_cool_) |   if (this->supports_cool_) | ||||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_COOL); |     traits.add_supported_mode(climate::CLIMATE_MODE_COOL); | ||||||
| @@ -22,6 +19,7 @@ climate::ClimateTraits ClimateIR::traits() { | |||||||
|   if (this->supports_fan_only_) |   if (this->supports_fan_only_) | ||||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_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_min_temperature(this->minimum_temperature_); | ||||||
|   traits.set_visual_max_temperature(this->maximum_temperature_); |   traits.set_visual_max_temperature(this->maximum_temperature_); | ||||||
|   traits.set_visual_temperature_step(this->temperature_step_); |   traits.set_visual_temperature_step(this->temperature_step_); | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| #include "cover.h" | #include "cover.h" | ||||||
| #include <strings.h> |  | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  | #include <strings.h> | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace cover { | namespace cover { | ||||||
| @@ -144,7 +144,21 @@ CoverCall &CoverCall::set_stop(bool stop) { | |||||||
| bool CoverCall::get_stop() const { return this->stop_; } | bool CoverCall::get_stop() const { return this->stop_; } | ||||||
|  |  | ||||||
| CoverCall Cover::make_call() { return {this}; } | 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::add_on_state_callback(std::function<void()> &&f) { this->state_callback_.add(std::move(f)); } | ||||||
| void Cover::publish_state(bool save) { | void Cover::publish_state(bool save) { | ||||||
|   this->position = clamp(this->position, 0.0f, 1.0f); |   this->position = clamp(this->position, 0.0f, 1.0f); | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ | |||||||
| #include "esphome/core/entity_base.h" | #include "esphome/core/entity_base.h" | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
| #include "esphome/core/preferences.h" | #include "esphome/core/preferences.h" | ||||||
|  |  | ||||||
| #include "cover_traits.h" | #include "cover_traits.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| @@ -126,6 +125,25 @@ class Cover : public EntityBase, public EntityBase_DeviceClass { | |||||||
|  |  | ||||||
|   /// Construct a new cover call used to control the cover. |   /// Construct a new cover call used to control the cover. | ||||||
|   CoverCall make_call(); |   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); |   void add_on_state_callback(std::function<void()> &&f); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -241,7 +241,9 @@ uint8_t DaikinArcClimate::humidity_() { | |||||||
|  |  | ||||||
| climate::ClimateTraits DaikinArcClimate::traits() { | climate::ClimateTraits DaikinArcClimate::traits() { | ||||||
|   climate::ClimateTraits traits = climate_ir::ClimateIR::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_min_humidity(38); | ||||||
|   traits.set_visual_max_humidity(52); |   traits.set_visual_max_humidity(52); | ||||||
|   return traits; |   return traits; | ||||||
|   | |||||||
| @@ -82,14 +82,16 @@ class DemoClimate : public climate::Climate, public Component { | |||||||
|     climate::ClimateTraits traits{}; |     climate::ClimateTraits traits{}; | ||||||
|     switch (type_) { |     switch (type_) { | ||||||
|       case DemoClimateType::TYPE_1: |       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({ |         traits.set_supported_modes({ | ||||||
|             climate::CLIMATE_MODE_OFF, |             climate::CLIMATE_MODE_OFF, | ||||||
|             climate::CLIMATE_MODE_HEAT, |             climate::CLIMATE_MODE_HEAT, | ||||||
|         }); |         }); | ||||||
|  |         traits.set_supports_action(true); | ||||||
|         traits.set_visual_temperature_step(0.5); |         traits.set_visual_temperature_step(0.5); | ||||||
|         break; |         break; | ||||||
|       case DemoClimateType::TYPE_2: |       case DemoClimateType::TYPE_2: | ||||||
|  |         traits.set_supports_current_temperature(false); | ||||||
|         traits.set_supported_modes({ |         traits.set_supported_modes({ | ||||||
|             climate::CLIMATE_MODE_OFF, |             climate::CLIMATE_MODE_OFF, | ||||||
|             climate::CLIMATE_MODE_HEAT, |             climate::CLIMATE_MODE_HEAT, | ||||||
| @@ -98,7 +100,7 @@ class DemoClimate : public climate::Climate, public Component { | |||||||
|             climate::CLIMATE_MODE_DRY, |             climate::CLIMATE_MODE_DRY, | ||||||
|             climate::CLIMATE_MODE_FAN_ONLY, |             climate::CLIMATE_MODE_FAN_ONLY, | ||||||
|         }); |         }); | ||||||
|         traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); |         traits.set_supports_action(true); | ||||||
|         traits.set_supported_fan_modes({ |         traits.set_supported_fan_modes({ | ||||||
|             climate::CLIMATE_FAN_ON, |             climate::CLIMATE_FAN_ON, | ||||||
|             climate::CLIMATE_FAN_OFF, |             climate::CLIMATE_FAN_OFF, | ||||||
| @@ -121,8 +123,8 @@ class DemoClimate : public climate::Climate, public Component { | |||||||
|         traits.set_supported_custom_presets({"My Preset"}); |         traits.set_supported_custom_presets({"My Preset"}); | ||||||
|         break; |         break; | ||||||
|       case DemoClimateType::TYPE_3: |       case DemoClimateType::TYPE_3: | ||||||
|         traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | |         traits.set_supports_current_temperature(true); | ||||||
|                                  climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE); |         traits.set_supports_two_point_target_temperature(true); | ||||||
|         traits.set_supported_modes({ |         traits.set_supported_modes({ | ||||||
|             climate::CLIMATE_MODE_OFF, |             climate::CLIMATE_MODE_OFF, | ||||||
|             climate::CLIMATE_MODE_COOL, |             climate::CLIMATE_MODE_COOL, | ||||||
|   | |||||||
| @@ -103,7 +103,7 @@ bool EPaperBase::is_idle_() { | |||||||
|   if (this->busy_pin_ == nullptr) { |   if (this->busy_pin_ == nullptr) { | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|   return this->busy_pin_->digital_read(); |   return !this->busy_pin_->digital_read(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void EPaperBase::reset() { | void EPaperBase::reset() { | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import contextlib |  | ||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
| import itertools | import itertools | ||||||
| import logging | import logging | ||||||
| @@ -103,10 +102,6 @@ COMPILER_OPTIMIZATIONS = { | |||||||
|     "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE", |     "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 = [ | ARDUINO_ALLOWED_VARIANTS = [ | ||||||
|     VARIANT_ESP32, |     VARIANT_ESP32, | ||||||
|     VARIANT_ESP32C3, |     VARIANT_ESP32C3, | ||||||
| @@ -550,32 +545,6 @@ CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" | |||||||
| CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking" | CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking" | ||||||
| CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety" | CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety" | ||||||
| CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram" | CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram" | ||||||
| CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios" |  | ||||||
| CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" |  | ||||||
| CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" |  | ||||||
|  |  | ||||||
| # VFS requirement tracking |  | ||||||
| # Components that need VFS features can call require_vfs_select() or require_vfs_dir() |  | ||||||
| KEY_VFS_SELECT_REQUIRED = "vfs_select_required" |  | ||||||
| KEY_VFS_DIR_REQUIRED = "vfs_dir_required" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def require_vfs_select() -> None: |  | ||||||
|     """Mark that VFS select support is required by a component. |  | ||||||
|  |  | ||||||
|     Call this from components that use esp_vfs_eventfd or other VFS select features. |  | ||||||
|     This prevents CONFIG_VFS_SUPPORT_SELECT from being disabled. |  | ||||||
|     """ |  | ||||||
|     CORE.data[KEY_VFS_SELECT_REQUIRED] = True |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def require_vfs_dir() -> None: |  | ||||||
|     """Mark that VFS directory support is required by a component. |  | ||||||
|  |  | ||||||
|     Call this from components that use directory functions (opendir, readdir, mkdir, etc.). |  | ||||||
|     This prevents CONFIG_VFS_SUPPORT_DIR from being disabled. |  | ||||||
|     """ |  | ||||||
|     CORE.data[KEY_VFS_DIR_REQUIRED] = True |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def _validate_idf_component(config: ConfigType) -> ConfigType: | def _validate_idf_component(config: ConfigType) -> ConfigType: | ||||||
| @@ -641,13 +610,6 @@ FRAMEWORK_SCHEMA = cv.All( | |||||||
|                     cv.Optional( |                     cv.Optional( | ||||||
|                         CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True |                         CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True | ||||||
|                     ): cv.boolean, |                     ): cv.boolean, | ||||||
|                     cv.Optional( |  | ||||||
|                         CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True |  | ||||||
|                     ): cv.boolean, |  | ||||||
|                     cv.Optional( |  | ||||||
|                         CONF_DISABLE_VFS_SUPPORT_SELECT, default=True |  | ||||||
|                     ): cv.boolean, |  | ||||||
|                     cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, |  | ||||||
|                     cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, |                     cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, | ||||||
|                 } |                 } | ||||||
|             ), |             ), | ||||||
| @@ -784,72 +746,6 @@ CONFIG_SCHEMA = cv.All( | |||||||
| FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) | 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): | async def to_code(config): | ||||||
|     cg.add_platformio_option("board", config[CONF_BOARD]) |     cg.add_platformio_option("board", config[CONF_BOARD]) | ||||||
|     cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) |     cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) | ||||||
| @@ -883,16 +779,6 @@ async def to_code(config): | |||||||
|         Path(__file__).parent / "post_build.py.script", |         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: |     if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF: | ||||||
|         cg.add_platformio_option("framework", "espidf") |         cg.add_platformio_option("framework", "espidf") | ||||||
|         cg.add_build_flag("-DUSE_ESP_IDF") |         cg.add_build_flag("-DUSE_ESP_IDF") | ||||||
| @@ -970,9 +856,6 @@ async def to_code(config): | |||||||
|         add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) |         add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) | ||||||
|     if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): |     if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): | ||||||
|         add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) |         add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) | ||||||
|  |  | ||||||
|     _configure_lwip_max_sockets(conf) |  | ||||||
|  |  | ||||||
|     if advanced.get(CONF_EXECUTE_FROM_PSRAM, False): |     if advanced.get(CONF_EXECUTE_FROM_PSRAM, False): | ||||||
|         add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) |         add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) | ||||||
|         add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) |         add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) | ||||||
| @@ -995,43 +878,6 @@ async def to_code(config): | |||||||
|     if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True): |     if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True): | ||||||
|         add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) |         add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) | ||||||
|  |  | ||||||
|     # Disable VFS support for termios (terminal I/O functions) |  | ||||||
|     # ESPHome doesn't use termios functions on ESP32 (only used in host UART driver). |  | ||||||
|     # Saves approximately 1.8KB of flash when disabled (default). |  | ||||||
|     add_idf_sdkconfig_option( |  | ||||||
|         "CONFIG_VFS_SUPPORT_TERMIOS", |  | ||||||
|         not advanced.get(CONF_DISABLE_VFS_SUPPORT_TERMIOS, True), |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     # Disable VFS support for select() with file descriptors |  | ||||||
|     # ESPHome only uses select() with sockets via lwip_select(), which still works. |  | ||||||
|     # VFS select is only needed for UART/eventfd file descriptors. |  | ||||||
|     # Components that need it (e.g., openthread) call require_vfs_select(). |  | ||||||
|     # Saves approximately 2.7KB of flash when disabled (default). |  | ||||||
|     if CORE.data.get(KEY_VFS_SELECT_REQUIRED, False): |  | ||||||
|         # Component requires VFS select - force enable regardless of user setting |  | ||||||
|         add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_SELECT", True) |  | ||||||
|     else: |  | ||||||
|         # No component needs it - allow user to control (default: disabled) |  | ||||||
|         add_idf_sdkconfig_option( |  | ||||||
|             "CONFIG_VFS_SUPPORT_SELECT", |  | ||||||
|             not advanced.get(CONF_DISABLE_VFS_SUPPORT_SELECT, True), |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     # Disable VFS support for directory functions (opendir, readdir, mkdir, etc.) |  | ||||||
|     # ESPHome doesn't use directory functions on ESP32. |  | ||||||
|     # Components that need it (e.g., storage components) call require_vfs_dir(). |  | ||||||
|     # Saves approximately 0.5KB+ of flash when disabled (default). |  | ||||||
|     if CORE.data.get(KEY_VFS_DIR_REQUIRED, False): |  | ||||||
|         # Component requires VFS directory support - force enable regardless of user setting |  | ||||||
|         add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_DIR", True) |  | ||||||
|     else: |  | ||||||
|         # No component needs it - allow user to control (default: disabled) |  | ||||||
|         add_idf_sdkconfig_option( |  | ||||||
|             "CONFIG_VFS_SUPPORT_DIR", |  | ||||||
|             not advanced.get(CONF_DISABLE_VFS_SUPPORT_DIR, True), |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     cg.add_platformio_option("board_build.partitions", "partitions.csv") |     cg.add_platformio_option("board_build.partitions", "partitions.csv") | ||||||
|     if CONF_PARTITIONS in config: |     if CONF_PARTITIONS in config: | ||||||
|         add_extra_build_file( |         add_extra_build_file( | ||||||
|   | |||||||
| @@ -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) |  | ||||||
| @@ -1,7 +1,6 @@ | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import CONF_ID, CONF_MODE, CONF_PORT | from esphome.const import CONF_ID, CONF_MODE, CONF_PORT | ||||||
| from esphome.types import ConfigType |  | ||||||
|  |  | ||||||
| CODEOWNERS = ["@ayufan"] | CODEOWNERS = ["@ayufan"] | ||||||
| AUTO_LOAD = ["camera"] | AUTO_LOAD = ["camera"] | ||||||
| @@ -14,27 +13,13 @@ Mode = esp32_camera_web_server_ns.enum("Mode") | |||||||
|  |  | ||||||
| MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT} | MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT} | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
| def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType: |     { | ||||||
|     """Register socket needs for camera web server.""" |         cv.GenerateID(): cv.declare_id(CameraWebServer), | ||||||
|     from esphome.components import socket |         cv.Required(CONF_PORT): cv.port, | ||||||
|  |         cv.Required(CONF_MODE): cv.enum(MODES, upper=True), | ||||||
|     # Each camera web server instance needs 1 listening socket + 2 client connections |     }, | ||||||
|     sockets_needed = 3 | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|     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, |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|   | |||||||
| @@ -95,7 +95,7 @@ async def to_code(config): | |||||||
|     if framework_ver >= cv.Version(5, 5, 0): |     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/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/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: |     else: | ||||||
|         esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0") |         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") |         esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") | ||||||
|   | |||||||
| @@ -1,11 +1,11 @@ | |||||||
| from esphome import automation | from esphome import automation | ||||||
| import esphome.codegen as cg | 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 | from esphome.components.esp32_ble import BTLoggers | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID | 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"] | CODEOWNERS = ["@jesserockz"] | ||||||
| DEPENDENCIES = ["wifi", "esp32"] | DEPENDENCIES = ["wifi", "esp32"] | ||||||
|  |  | ||||||
| @@ -20,7 +20,6 @@ CONF_ON_STOP = "on_stop" | |||||||
| CONF_STATUS_INDICATOR = "status_indicator" | CONF_STATUS_INDICATOR = "status_indicator" | ||||||
| CONF_WIFI_TIMEOUT = "wifi_timeout" | CONF_WIFI_TIMEOUT = "wifi_timeout" | ||||||
|  |  | ||||||
|  |  | ||||||
| improv_ns = cg.esphome_ns.namespace("improv") | improv_ns = cg.esphome_ns.namespace("improv") | ||||||
| Error = improv_ns.enum("Error") | Error = improv_ns.enum("Error") | ||||||
| State = improv_ns.enum("State") | State = improv_ns.enum("State") | ||||||
| @@ -44,63 +43,55 @@ ESP32ImprovStoppedTrigger = esp32_improv_ns.class_( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = ( | CONFIG_SCHEMA = cv.Schema( | ||||||
|     cv.Schema( |     { | ||||||
|         { |         cv.GenerateID(): cv.declare_id(ESP32ImprovComponent), | ||||||
|             cv.GenerateID(): cv.declare_id(ESP32ImprovComponent), |         cv.Required(CONF_AUTHORIZER): cv.Any( | ||||||
|             cv.Required(CONF_AUTHORIZER): cv.Any( |             cv.none, cv.use_id(binary_sensor.BinarySensor) | ||||||
|                 cv.none, cv.use_id(binary_sensor.BinarySensor) |         ), | ||||||
|             ), |         cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput), | ||||||
|             cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput), |         cv.Optional( | ||||||
|             cv.Optional( |             CONF_IDENTIFY_DURATION, default="10s" | ||||||
|                 CONF_IDENTIFY_DURATION, default="10s" |         ): cv.positive_time_period_milliseconds, | ||||||
|             ): cv.positive_time_period_milliseconds, |         cv.Optional( | ||||||
|             cv.Optional( |             CONF_AUTHORIZED_DURATION, default="1min" | ||||||
|                 CONF_AUTHORIZED_DURATION, default="1min" |         ): cv.positive_time_period_milliseconds, | ||||||
|             ): cv.positive_time_period_milliseconds, |         cv.Optional( | ||||||
|             cv.Optional( |             CONF_WIFI_TIMEOUT, default="1min" | ||||||
|                 CONF_WIFI_TIMEOUT, default="1min" |         ): cv.positive_time_period_milliseconds, | ||||||
|             ): cv.positive_time_period_milliseconds, |         cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation( | ||||||
|             cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation( |             { | ||||||
|                 { |                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( |                     ESP32ImprovProvisionedTrigger | ||||||
|                         ESP32ImprovProvisionedTrigger |                 ), | ||||||
|                     ), |             } | ||||||
|                 } |         ), | ||||||
|             ), |         cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation( | ||||||
|             cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation( |             { | ||||||
|                 { |                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( |                     ESP32ImprovProvisioningTrigger | ||||||
|                         ESP32ImprovProvisioningTrigger |                 ), | ||||||
|                     ), |             } | ||||||
|                 } |         ), | ||||||
|             ), |         cv.Optional(CONF_ON_START): automation.validate_automation( | ||||||
|             cv.Optional(CONF_ON_START): automation.validate_automation( |             { | ||||||
|                 { |                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStartTrigger), | ||||||
|                     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_STATE): automation.validate_automation( |             } | ||||||
|                 { |         ), | ||||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( |         cv.Optional(CONF_ON_STOP): automation.validate_automation( | ||||||
|                         ESP32ImprovStateTrigger |             { | ||||||
|                     ), |                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||||
|                 } |                     ESP32ImprovStoppedTrigger | ||||||
|             ), |                 ), | ||||||
|             cv.Optional(CONF_ON_STOP): automation.validate_automation( |             } | ||||||
|                 { |         ), | ||||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( |     } | ||||||
|                         ESP32ImprovStoppedTrigger | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|                     ), |  | ||||||
|                 } |  | ||||||
|             ), |  | ||||||
|         } |  | ||||||
|     ) |  | ||||||
|     .extend(improv_base.IMPROV_SCHEMA) |  | ||||||
|     .extend(cv.COMPONENT_SCHEMA) |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
| @@ -111,8 +102,7 @@ async def to_code(config): | |||||||
|     await cg.register_component(var, config) |     await cg.register_component(var, config) | ||||||
|  |  | ||||||
|     cg.add_define("USE_IMPROV") |     cg.add_define("USE_IMPROV") | ||||||
|  |     cg.add_library("improv/Improv", "1.2.4") | ||||||
|     await improv_base.setup_improv_core(var, config, "esp32_improv") |  | ||||||
|  |  | ||||||
|     cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION])) |     cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION])) | ||||||
|     cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION])) |     cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION])) | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| #include "esp32_improv_component.h" | #include "esp32_improv_component.h" | ||||||
|  |  | ||||||
| #include "esphome/components/bytebuffer/bytebuffer.h" |  | ||||||
| #include "esphome/components/esp32_ble/ble.h" | #include "esphome/components/esp32_ble/ble.h" | ||||||
| #include "esphome/components/esp32_ble_server/ble_2902.h" | #include "esphome/components/esp32_ble_server/ble_2902.h" | ||||||
| #include "esphome/core/application.h" | #include "esphome/core/application.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/components/bytebuffer/bytebuffer.h" | ||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
|  |  | ||||||
| @@ -384,34 +384,17 @@ void ESP32ImprovComponent::check_wifi_connection_() { | |||||||
|     this->connecting_sta_ = {}; |     this->connecting_sta_ = {}; | ||||||
|     this->cancel_timeout("wifi-connect-timeout"); |     this->cancel_timeout("wifi-connect-timeout"); | ||||||
|  |  | ||||||
|     // Build URL list with minimal allocations |     std::vector<std::string> urls = {ESPHOME_MY_LINK}; | ||||||
|     // Maximum 3 URLs: custom next_url + ESPHOME_MY_LINK + webserver URL |  | ||||||
|     std::string url_strings[3]; |  | ||||||
|     size_t url_count = 0; |  | ||||||
|  |  | ||||||
| #ifdef USE_ESP32_IMPROV_NEXT_URL |  | ||||||
|     // 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); |  | ||||||
|     } |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
|     // Add default URLs for backward compatibility |  | ||||||
|     url_strings[url_count++] = ESPHOME_MY_LINK; |  | ||||||
| #ifdef USE_WEBSERVER | #ifdef USE_WEBSERVER | ||||||
|     for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { |     for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { | ||||||
|       if (ip.is_ip4()) { |       if (ip.is_ip4()) { | ||||||
|         char url_buffer[64]; |         std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT); | ||||||
|         snprintf(url_buffer, sizeof(url_buffer), "http://%s:%d", ip.str().c_str(), USE_WEBSERVER_PORT); |         urls.push_back(webserver_url); | ||||||
|         url_strings[url_count++] = url_buffer; |  | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| #endif | #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, urls); | ||||||
|     std::vector<uint8_t> data = improv::build_rpc_response( |  | ||||||
|         improv::WIFI_SETTINGS, std::vector<std::string>(url_strings, url_strings + url_count)); |  | ||||||
|     this->send_response_(data); |     this->send_response_(data); | ||||||
|   } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) { |   } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) { | ||||||
|     ESP_LOGD(TAG, "WiFi provisioned externally"); |     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_characteristic.h" | ||||||
| #include "esphome/components/esp32_ble_server/ble_server.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" | #include "esphome/components/wifi/wifi_component.h" | ||||||
|  |  | ||||||
| #ifdef USE_ESP32_IMPROV_STATE_CALLBACK | #ifdef USE_ESP32_IMPROV_STATE_CALLBACK | ||||||
| @@ -33,7 +32,7 @@ namespace esp32_improv { | |||||||
|  |  | ||||||
| using namespace esp32_ble_server; | using namespace esp32_ble_server; | ||||||
|  |  | ||||||
| class ESP32ImprovComponent : public Component, public improv_base::ImprovBase { | class ESP32ImprovComponent : public Component { | ||||||
|  public: |  public: | ||||||
|   ESP32ImprovComponent(); |   ESP32ImprovComponent(); | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   | |||||||
| @@ -190,9 +190,7 @@ async def to_code(config): | |||||||
|     cg.add_define("ESPHOME_VARIANT", "ESP8266") |     cg.add_define("ESPHOME_VARIANT", "ESP8266") | ||||||
|     cg.add_define(ThreadModel.SINGLE) |     cg.add_define(ThreadModel.SINGLE) | ||||||
|  |  | ||||||
|     cg.add_platformio_option( |     cg.add_platformio_option("extra_scripts", ["pre:iram_fix.py", "post:post_build.py"]) | ||||||
|         "extra_scripts", ["pre:testing_mode.py", "post:post_build.py"] |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     conf = config[CONF_FRAMEWORK] |     conf = config[CONF_FRAMEWORK] | ||||||
|     cg.add_platformio_option("framework", "arduino") |     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;` |     # For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;` | ||||||
|     cg.add_build_flag("-DNEW_OOM_ABORT") |     cg.add_build_flag("-DNEW_OOM_ABORT") | ||||||
|  |  | ||||||
|     # In testing mode, fake larger memory to allow linking grouped component tests |     # In testing mode, fake a larger IRAM to allow linking grouped component tests | ||||||
|     # Real ESP8266 hardware only has 32KB IRAM and ~80KB RAM, but for CI testing |     # Real ESP8266 hardware only has 32KB IRAM, but for CI testing we pretend it has 2MB | ||||||
|     # we pretend it has much larger memory to test that components compile together |     # This is done via a pre-build script that generates a custom linker script | ||||||
|     if CORE.testing_mode: |     if CORE.testing_mode: | ||||||
|         cg.add_build_flag("-DESPHOME_TESTING_MODE") |         cg.add_build_flag("-DESPHOME_TESTING_MODE") | ||||||
|  |  | ||||||
| @@ -273,8 +271,8 @@ def copy_files(): | |||||||
|         post_build_file, |         post_build_file, | ||||||
|         CORE.relative_build_path("post_build.py"), |         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( |     copy_file_if_changed( | ||||||
|         testing_mode_file, |         iram_fix_file, | ||||||
|         CORE.relative_build_path("testing_mode.py"), |         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: | CONFIG_SCHEMA = ( | ||||||
|     """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( |  | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
|         { |         { | ||||||
|             cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent), |             cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent), | ||||||
| @@ -139,8 +130,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     .extend(BASE_OTA_SCHEMA) |     .extend(BASE_OTA_SCHEMA) | ||||||
|     .extend(cv.COMPONENT_SCHEMA), |     .extend(cv.COMPONENT_SCHEMA) | ||||||
|     _consume_ota_sockets, |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate | 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); |   TEMPLATABLE_VALUE(std::vector<uint8_t>, data); | ||||||
|  |  | ||||||
|  public: |  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); |     this->sent_.add_actions(actions); | ||||||
|     if (this->flags_.wait_for_sent) { |     if (this->flags_.wait_for_sent) { | ||||||
|       this->sent_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); })); |       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); |     this->error_.add_actions(actions); | ||||||
|     if (this->flags_.wait_for_sent) { |     if (this->flags_.wait_for_sent) { | ||||||
|       this->error_.add_action(new LambdaAction<Ts...>([this](Ts... x) { |       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 |  | ||||||
| @@ -8,19 +8,12 @@ namespace event { | |||||||
| static const char *const TAG = "event"; | static const char *const TAG = "event"; | ||||||
|  |  | ||||||
| void Event::trigger(const std::string &event_type) { | void Event::trigger(const std::string &event_type) { | ||||||
|   // Linear search - faster than std::set for small datasets (1-5 items typical) |   auto found = types_.find(event_type); | ||||||
|   const std::string *found = nullptr; |   if (found == types_.end()) { | ||||||
|   for (const auto &type : this->types_) { |  | ||||||
|     if (type == event_type) { |  | ||||||
|       found = &type; |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   if (found == nullptr) { |  | ||||||
|     ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str()); |     ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str()); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   last_event_type = found; |   last_event_type = &(*found); | ||||||
|   ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str()); |   ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str()); | ||||||
|   this->event_callback_.call(event_type); |   this->event_callback_.call(event_type); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
|  | #include <set> | ||||||
| #include <string> | #include <string> | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| @@ -25,13 +26,13 @@ class Event : public EntityBase, public EntityBase_DeviceClass { | |||||||
|   const std::string *last_event_type; |   const std::string *last_event_type; | ||||||
|  |  | ||||||
|   void trigger(const std::string &event_type); |   void trigger(const std::string &event_type); | ||||||
|   void set_event_types(const std::initializer_list<std::string> &event_types) { this->types_ = event_types; } |   void set_event_types(const std::set<std::string> &event_types) { this->types_ = event_types; } | ||||||
|   const FixedVector<std::string> &get_event_types() const { return this->types_; } |   std::set<std::string> get_event_types() const { return this->types_; } | ||||||
|   void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback); |   void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   CallbackManager<void(const std::string &event_type)> event_callback_; |   CallbackManager<void(const std::string &event_type)> event_callback_; | ||||||
|   FixedVector<std::string> types_; |   std::set<std::string> types_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace event | }  // namespace event | ||||||
|   | |||||||
| @@ -38,6 +38,7 @@ IS_PLATFORM_COMPONENT = True | |||||||
|  |  | ||||||
| fan_ns = cg.esphome_ns.namespace("fan") | fan_ns = cg.esphome_ns.namespace("fan") | ||||||
| Fan = fan_ns.class_("Fan", cg.EntityBase) | Fan = fan_ns.class_("Fan", cg.EntityBase) | ||||||
|  | FanState = fan_ns.class_("Fan", Fan, cg.Component) | ||||||
|  |  | ||||||
| FanDirection = fan_ns.enum("FanDirection", is_class=True) | FanDirection = fan_ns.enum("FanDirection", is_class=True) | ||||||
| FAN_DIRECTION_ENUM = { | FAN_DIRECTION_ENUM = { | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include "esphome/core/automation.h" |  | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "fan.h" | #include "esphome/core/automation.h" | ||||||
|  | #include "fan_state.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace fan { | 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 | ||||||
| @@ -67,7 +67,7 @@ void GPIOSwitch::write_state(bool state) { | |||||||
|   this->pin_->digital_write(state); |   this->pin_->digital_write(state); | ||||||
|   this->publish_state(state); |   this->publish_state(state); | ||||||
| } | } | ||||||
| void GPIOSwitch::set_interlock(const std::initializer_list<Switch *> &interlock) { this->interlock_ = interlock; } | void GPIOSwitch::set_interlock(const std::vector<Switch *> &interlock) { this->interlock_ = interlock; } | ||||||
|  |  | ||||||
| }  // namespace gpio | }  // namespace gpio | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -2,9 +2,10 @@ | |||||||
|  |  | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
| #include "esphome/core/helpers.h" |  | ||||||
| #include "esphome/components/switch/switch.h" | #include "esphome/components/switch/switch.h" | ||||||
|  |  | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace gpio { | namespace gpio { | ||||||
|  |  | ||||||
| @@ -18,14 +19,14 @@ class GPIOSwitch : public switch_::Switch, public Component { | |||||||
|  |  | ||||||
|   void setup() override; |   void setup() override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void set_interlock(const std::initializer_list<Switch *> &interlock); |   void set_interlock(const std::vector<Switch *> &interlock); | ||||||
|   void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; } |   void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   void write_state(bool state) override; |   void write_state(bool state) override; | ||||||
|  |  | ||||||
|   GPIOPin *pin_; |   GPIOPin *pin_; | ||||||
|   FixedVector<Switch *> interlock_; |   std::vector<Switch *> interlock_; | ||||||
|   uint32_t interlock_wait_time_{0}; |   uint32_t interlock_wait_time_{0}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -65,7 +65,7 @@ HaierClimateBase::HaierClimateBase() | |||||||
|       {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}); |       {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, |   this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, | ||||||
|                                            climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL}); |                                            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() {} | HaierClimateBase::~HaierClimateBase() {} | ||||||
|   | |||||||
| @@ -16,8 +16,7 @@ void HDC1080Component::setup() { | |||||||
|  |  | ||||||
|   // if configuration fails - there is a problem |   // if configuration fails - there is a problem | ||||||
|   if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) { |   if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) { | ||||||
|     ESP_LOGW(TAG, "Failed to configure HDC1080"); |     this->mark_failed(); | ||||||
|     this->status_set_warning(); |  | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,8 +3,6 @@ import re | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import __version__ | from esphome.const import __version__ | ||||||
| from esphome.cpp_generator import MockObj |  | ||||||
| from esphome.types import ConfigType |  | ||||||
|  |  | ||||||
| CODEOWNERS = ["@esphome/core"] | CODEOWNERS = ["@esphome/core"] | ||||||
|  |  | ||||||
| @@ -37,9 +35,7 @@ def _process_next_url(url: str): | |||||||
|     return url |     return url | ||||||
|  |  | ||||||
|  |  | ||||||
| async def setup_improv_core(var: MockObj, config: ConfigType, component: str): | async def setup_improv_core(var, config): | ||||||
|     if next_url := config.get(CONF_NEXT_URL): |     if CONF_NEXT_URL in config: | ||||||
|         cg.add(var.set_next_url(_process_next_url(next_url))) |         cg.add(var.set_next_url(_process_next_url(config[CONF_NEXT_URL]))) | ||||||
|         cg.add_define(f"USE_{component.upper()}_NEXT_URL") |  | ||||||
|  |  | ||||||
|     cg.add_library("improv/Improv", "1.2.4") |     cg.add_library("improv/Improv", "1.2.4") | ||||||
|   | |||||||
| @@ -2,50 +2,36 @@ | |||||||
|  |  | ||||||
| #include "esphome/components/network/util.h" | #include "esphome/components/network/util.h" | ||||||
| #include "esphome/core/application.h" | #include "esphome/core/application.h" | ||||||
| #include "esphome/core/defines.h" |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace improv_base { | namespace improv_base { | ||||||
|  |  | ||||||
| #if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) |  | ||||||
| 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_() { | std::string ImprovBase::get_formatted_next_url_() { | ||||||
|   if (this->next_url_.empty()) { |   if (this->next_url_.empty()) { | ||||||
|     return ""; |     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_; |   // Ip address | ||||||
|  |   pos = this->next_url_.find("{{ip_address}}"); | ||||||
|   // Replace all occurrences of {{device_name}} |   if (pos != std::string::npos) { | ||||||
|   replace_all_in_place(formatted_url, DEVICE_NAME_PLACEHOLDER, DEVICE_NAME_PLACEHOLDER_LEN, App.get_name()); |     for (auto &ip : network::get_ip_addresses()) { | ||||||
|  |       if (ip.is_ip4()) { | ||||||
|   // Replace all occurrences of {{ip_address}} |         std::string ipa = ip.str(); | ||||||
|   for (auto &ip : network::get_ip_addresses()) { |         copy.replace(pos, 14, ipa); | ||||||
|     if (ip.is_ip4()) { |         break; | ||||||
|       replace_all_in_place(formatted_url, IP_ADDRESS_PLACEHOLDER, IP_ADDRESS_PLACEHOLDER_LEN, ip.str()); |       } | ||||||
|       break; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Note: {{esphome_version}} is replaced at code generation time in Python |   return copy; | ||||||
|  |  | ||||||
|   return formatted_url; |  | ||||||
| } | } | ||||||
| #endif |  | ||||||
|  |  | ||||||
| }  // namespace improv_base | }  // namespace improv_base | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -1,22 +1,17 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include <string> | #include <string> | ||||||
| #include "esphome/core/defines.h" |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace improv_base { | namespace improv_base { | ||||||
|  |  | ||||||
| class ImprovBase { | class ImprovBase { | ||||||
|  public: |  public: | ||||||
| #if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) |  | ||||||
|   void set_next_url(const std::string &next_url) { this->next_url_ = next_url; } |   void set_next_url(const std::string &next_url) { this->next_url_ = next_url; } | ||||||
| #endif |  | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
| #if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) |  | ||||||
|   std::string get_formatted_next_url_(); |   std::string get_formatted_next_url_(); | ||||||
|   std::string next_url_; |   std::string next_url_; | ||||||
| #endif |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace improv_base | }  // namespace improv_base | ||||||
|   | |||||||
| @@ -43,4 +43,4 @@ FINAL_VALIDATE_SCHEMA = validate_logger | |||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|     await cg.register_component(var, config) |     await cg.register_component(var, config) | ||||||
|     await improv_base.setup_improv_core(var, config, "improv_serial") |     await improv_base.setup_improv_core(var, config) | ||||||
|   | |||||||
| @@ -146,11 +146,9 @@ void ImprovSerialComponent::loop() { | |||||||
|  |  | ||||||
| std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { | std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { | ||||||
|   std::vector<std::string> urls; |   std::vector<std::string> urls; | ||||||
| #ifdef USE_IMPROV_SERIAL_NEXT_URL |  | ||||||
|   if (!this->next_url_.empty()) { |   if (!this->next_url_.empty()) { | ||||||
|     urls.push_back(this->get_formatted_next_url_()); |     urls.push_back(this->get_formatted_next_url_()); | ||||||
|   } |   } | ||||||
| #endif |  | ||||||
| #ifdef USE_WEBSERVER | #ifdef USE_WEBSERVER | ||||||
|   for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { |   for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { | ||||||
|     if (ip.is_ip4()) { |     if (ip.is_ip4()) { | ||||||
|   | |||||||
| @@ -61,12 +61,8 @@ void AddressableLightTransformer::start() { | |||||||
|   this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state()); |   this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state()); | ||||||
| } | } | ||||||
|  |  | ||||||
| inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) { |  | ||||||
|   return uint8_t(int32_t(a) - (((int32_t(a) - int32_t(b)) * scale) / 256)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| optional<LightColorValues> AddressableLightTransformer::apply() { | optional<LightColorValues> AddressableLightTransformer::apply() { | ||||||
|   float smoothed_progress = LightTransformer::smoothed_progress(this->get_progress_()); |   float smoothed_progress = LightTransitionTransformer::smoothed_progress(this->get_progress_()); | ||||||
|  |  | ||||||
|   // When running an output-buffer modifying effect, don't try to transition individual LEDs, but instead just fade the |   // When running an output-buffer modifying effect, don't try to transition individual LEDs, but instead just fade the | ||||||
|   // LightColorValues. write_state() then picks up the change in brightness, and the color change is picked up by the |   // LightColorValues. write_state() then picks up the change in brightness, and the color change is picked up by the | ||||||
| @@ -78,37 +74,38 @@ optional<LightColorValues> AddressableLightTransformer::apply() { | |||||||
|   // all LEDs, we use the current state of each LED as the start. |   // all LEDs, we use the current state of each LED as the start. | ||||||
|  |  | ||||||
|   // We can't use a direct lerp smoothing here though - that would require creating a copy of the original |   // We can't use a direct lerp smoothing here though - that would require creating a copy of the original | ||||||
|   // state of each LED at the start of the transition. Instead, we "fake" the look of lerp by calculating |   // state of each LED at the start of the transition. | ||||||
|   // the delta between the current state and the target state, assuming that the delta represents the rest |   // Instead, we "fake" the look of the LERP by using an exponential average over time and using | ||||||
|   // of the transition that was to be applied as of the previous transition step, and scaling the delta for |   // dynamically-calculated alpha values to match the look. | ||||||
|   // what should be left after the current transition step. In this manner, the delta decays to zero as the |  | ||||||
|   // transition progresses. |  | ||||||
|   // |  | ||||||
|   // Here's an example of how the algorithm progresses in discrete steps: |  | ||||||
|   // |  | ||||||
|   // At time = 0.00, 0% complete, 100% remaining, 100% will remain after this step, so the scale is 100% / 100% = 100%. |  | ||||||
|   // At time = 0.10, 0% complete, 100% remaining, 90% will remain after this step, so the scale is 90% / 100% = 90%. |  | ||||||
|   // At time = 0.20, 10% complete, 90% remaining, 80% will remain after this step, so the scale is 80% / 90% = 88.9%. |  | ||||||
|   // At time = 0.50, 20% complete, 80% remaining, 50% will remain after this step, so the scale is 50% / 80% = 62.5%. |  | ||||||
|   // At time = 0.90, 50% complete, 50% remaining, 10% will remain after this step, so the scale is 10% / 50% = 20%. |  | ||||||
|   // At time = 0.91, 90% complete, 10% remaining, 9% will remain after this step, so the scale is 9% / 10% = 90%. |  | ||||||
|   // At time = 1.00, 91% complete, 9% remaining, 0% will remain after this step, so the scale is 0% / 9% = 0%. |  | ||||||
|   // |  | ||||||
|   // Because the color values are quantized to 8 bit resolution after each step, the transition may appear |  | ||||||
|   // non-linear when applying small deltas. |  | ||||||
|  |  | ||||||
|   if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) { |   float denom = (1.0f - smoothed_progress); | ||||||
|     int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f)); |   float alpha = denom == 0.0f ? 1.0f : (smoothed_progress - this->last_transition_progress_) / denom; | ||||||
|     for (auto led : this->light_) { |  | ||||||
|       led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale), |   // We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length | ||||||
|                    subtract_scaled_difference(this->target_color_.green, led.get_green(), scale), |   // We solve this by accumulating the fractional part of the alpha over time. | ||||||
|                    subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale), |   float alpha255 = alpha * 255.0f; | ||||||
|                    subtract_scaled_difference(this->target_color_.white, led.get_white(), scale)); |   float alpha255int = floorf(alpha255); | ||||||
|     } |   float alpha255remainder = alpha255 - alpha255int; | ||||||
|     this->last_transition_progress_ = smoothed_progress; |  | ||||||
|     this->light_.schedule_show(); |   this->accumulated_alpha_ += alpha255remainder; | ||||||
|  |   float alpha_add = floorf(this->accumulated_alpha_); | ||||||
|  |   this->accumulated_alpha_ -= alpha_add; | ||||||
|  |  | ||||||
|  |   alpha255 += alpha_add; | ||||||
|  |   alpha255 = clamp(alpha255, 0.0f, 255.0f); | ||||||
|  |   auto alpha8 = static_cast<uint8_t>(alpha255); | ||||||
|  |  | ||||||
|  |   if (alpha8 != 0) { | ||||||
|  |     uint8_t inv_alpha8 = 255 - alpha8; | ||||||
|  |     Color add = this->target_color_ * alpha8; | ||||||
|  |  | ||||||
|  |     for (auto led : this->light_) | ||||||
|  |       led.set(add + led.get() * inv_alpha8); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   this->last_transition_progress_ = smoothed_progress; | ||||||
|  |   this->light_.schedule_show(); | ||||||
|  |  | ||||||
|   return {}; |   return {}; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ | |||||||
| #include "esphome/core/defines.h" | #include "esphome/core/defines.h" | ||||||
| #include "light_output.h" | #include "light_output.h" | ||||||
| #include "light_state.h" | #include "light_state.h" | ||||||
| #include "light_transformer.h" | #include "transformers.h" | ||||||
|  |  | ||||||
| #ifdef USE_POWER_SUPPLY | #ifdef USE_POWER_SUPPLY | ||||||
| #include "esphome/components/power_supply/power_supply.h" | #include "esphome/components/power_supply/power_supply.h" | ||||||
| @@ -103,7 +103,7 @@ class AddressableLight : public LightOutput, public Component { | |||||||
|   bool effect_active_{false}; |   bool effect_active_{false}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class AddressableLightTransformer : public LightTransformer { | class AddressableLightTransformer : public LightTransitionTransformer { | ||||||
|  public: |  public: | ||||||
|   AddressableLightTransformer(AddressableLight &light) : light_(light) {} |   AddressableLightTransformer(AddressableLight &light) : light_(light) {} | ||||||
|  |  | ||||||
| @@ -113,6 +113,7 @@ class AddressableLightTransformer : public LightTransformer { | |||||||
|  protected: |  protected: | ||||||
|   AddressableLight &light_; |   AddressableLight &light_; | ||||||
|   float last_transition_progress_{0.0f}; |   float last_transition_progress_{0.0f}; | ||||||
|  |   float accumulated_alpha_{0.0f}; | ||||||
|   Color target_color_{}; |   Color target_color_{}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include <utility> | #include <utility> | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/helpers.h" |  | ||||||
| #include "esphome/components/light/light_state.h" | #include "esphome/components/light/light_state.h" | ||||||
| #include "esphome/components/light/addressable_light.h" | #include "esphome/components/light/addressable_light.h" | ||||||
|  |  | ||||||
| @@ -113,7 +113,7 @@ struct AddressableColorWipeEffectColor { | |||||||
| class AddressableColorWipeEffect : public AddressableLightEffect { | class AddressableColorWipeEffect : public AddressableLightEffect { | ||||||
|  public: |  public: | ||||||
|   explicit AddressableColorWipeEffect(const std::string &name) : AddressableLightEffect(name) {} |   explicit AddressableColorWipeEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||||
|   void set_colors(const std::initializer_list<AddressableColorWipeEffectColor> &colors) { this->colors_ = colors; } |   void set_colors(const std::vector<AddressableColorWipeEffectColor> &colors) { this->colors_ = colors; } | ||||||
|   void set_add_led_interval(uint32_t add_led_interval) { this->add_led_interval_ = add_led_interval; } |   void set_add_led_interval(uint32_t add_led_interval) { this->add_led_interval_ = add_led_interval; } | ||||||
|   void set_reverse(bool reverse) { this->reverse_ = reverse; } |   void set_reverse(bool reverse) { this->reverse_ = reverse; } | ||||||
|   void apply(AddressableLight &it, const Color ¤t_color) override { |   void apply(AddressableLight &it, const Color ¤t_color) override { | ||||||
| @@ -155,7 +155,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   FixedVector<AddressableColorWipeEffectColor> colors_; |   std::vector<AddressableColorWipeEffectColor> colors_; | ||||||
|   size_t at_color_{0}; |   size_t at_color_{0}; | ||||||
|   uint32_t last_add_{0}; |   uint32_t last_add_{0}; | ||||||
|   uint32_t add_led_interval_{}; |   uint32_t add_led_interval_{}; | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include <utility> | #include <utility> | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
| #include "esphome/core/automation.h" | #include "esphome/core/automation.h" | ||||||
| #include "esphome/core/helpers.h" |  | ||||||
| #include "light_effect.h" | #include "light_effect.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| @@ -188,10 +188,10 @@ class StrobeLightEffect : public LightEffect { | |||||||
|     this->last_switch_ = now; |     this->last_switch_ = now; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void set_colors(const std::initializer_list<StrobeLightEffectColor> &colors) { this->colors_ = colors; } |   void set_colors(const std::vector<StrobeLightEffectColor> &colors) { this->colors_ = colors; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   FixedVector<StrobeLightEffectColor> colors_; |   std::vector<StrobeLightEffectColor> colors_; | ||||||
|   uint32_t last_switch_{0}; |   uint32_t last_switch_{0}; | ||||||
|   size_t at_color_{0}; |   size_t at_color_{0}; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -17,19 +17,19 @@ class ESPColorCorrection { | |||||||
|                  this->color_correct_blue(color.blue), this->color_correct_white(color.white)); |                  this->color_correct_blue(color.blue), this->color_correct_white(color.white)); | ||||||
|   } |   } | ||||||
|   inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { |   inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { | ||||||
|     uint8_t res = esp_scale8_twice(red, this->max_brightness_.red, this->local_brightness_); |     uint8_t res = esp_scale8(esp_scale8(red, this->max_brightness_.red), this->local_brightness_); | ||||||
|     return this->gamma_table_[res]; |     return this->gamma_table_[res]; | ||||||
|   } |   } | ||||||
|   inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { |   inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { | ||||||
|     uint8_t res = esp_scale8_twice(green, this->max_brightness_.green, this->local_brightness_); |     uint8_t res = esp_scale8(esp_scale8(green, this->max_brightness_.green), this->local_brightness_); | ||||||
|     return this->gamma_table_[res]; |     return this->gamma_table_[res]; | ||||||
|   } |   } | ||||||
|   inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { |   inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { | ||||||
|     uint8_t res = esp_scale8_twice(blue, this->max_brightness_.blue, this->local_brightness_); |     uint8_t res = esp_scale8(esp_scale8(blue, this->max_brightness_.blue), this->local_brightness_); | ||||||
|     return this->gamma_table_[res]; |     return this->gamma_table_[res]; | ||||||
|   } |   } | ||||||
|   inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { |   inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { | ||||||
|     uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_); |     uint8_t res = esp_scale8(esp_scale8(white, this->max_brightness_.white), this->local_brightness_); | ||||||
|     return this->gamma_table_[res]; |     return this->gamma_table_[res]; | ||||||
|   } |   } | ||||||
|   inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE { |   inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE { | ||||||
|   | |||||||
| @@ -38,10 +38,6 @@ class LightTransformer { | |||||||
|   const LightColorValues &get_target_values() const { return this->target_values_; } |   const LightColorValues &get_target_values() const { return this->target_values_; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   // This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like |  | ||||||
|   // transition from 0 to 1 on x = [0, 1] |  | ||||||
|   static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } |  | ||||||
|  |  | ||||||
|   /// The progress of this transition, on a scale of 0 to 1. |   /// The progress of this transition, on a scale of 0 to 1. | ||||||
|   float get_progress_() { |   float get_progress_() { | ||||||
|     uint32_t now = esphome::millis(); |     uint32_t now = esphome::millis(); | ||||||
|   | |||||||
| @@ -50,11 +50,15 @@ class LightTransitionTransformer : public LightTransformer { | |||||||
|     if (this->changing_color_mode_) |     if (this->changing_color_mode_) | ||||||
|       p = p < 0.5f ? p * 2 : (p - 0.5) * 2; |       p = p < 0.5f ? p * 2 : (p - 0.5) * 2; | ||||||
|  |  | ||||||
|     float v = LightTransformer::smoothed_progress(p); |     float v = LightTransitionTransformer::smoothed_progress(p); | ||||||
|     return LightColorValues::lerp(start, end, v); |     return LightColorValues::lerp(start, end, v); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|  |   // This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like | ||||||
|  |   // transition from 0 to 1 on x = [0, 1] | ||||||
|  |   static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } | ||||||
|  |  | ||||||
|   LightColorValues end_values_{}; |   LightColorValues end_values_{}; | ||||||
|   LightColorValues intermediate_values_{}; |   LightColorValues intermediate_values_{}; | ||||||
|   bool changing_color_mode_{false}; |   bool changing_color_mode_{false}; | ||||||
|   | |||||||
| @@ -13,7 +13,6 @@ from esphome.const import ( | |||||||
| ) | ) | ||||||
| from esphome.core import CORE, Lambda, coroutine_with_priority | from esphome.core import CORE, Lambda, coroutine_with_priority | ||||||
| from esphome.coroutine import CoroPriority | from esphome.coroutine import CoroPriority | ||||||
| from esphome.types import ConfigType |  | ||||||
|  |  | ||||||
| CODEOWNERS = ["@esphome/core"] | CODEOWNERS = ["@esphome/core"] | ||||||
| DEPENDENCIES = ["network"] | 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( | CONFIG_SCHEMA = cv.All( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
|         { |         { | ||||||
| @@ -69,7 +55,6 @@ CONFIG_SCHEMA = cv.All( | |||||||
|         } |         } | ||||||
|     ), |     ), | ||||||
|     _remove_id_if_disabled, |     _remove_id_if_disabled, | ||||||
|     _consume_mdns_sockets, |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -77,7 +77,7 @@ void AirConditioner::control(const ClimateCall &call) { | |||||||
|  |  | ||||||
| ClimateTraits AirConditioner::traits() { | ClimateTraits AirConditioner::traits() { | ||||||
|   auto traits = ClimateTraits(); |   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_min_temperature(17); | ||||||
|   traits.set_visual_max_temperature(30); |   traits.set_visual_max_temperature(30); | ||||||
|   traits.set_visual_temperature_step(0.5); |   traits.set_visual_temperature_step(0.5); | ||||||
|   | |||||||
| @@ -30,19 +30,6 @@ wave_4_3 = DriverChip( | |||||||
|         "blue": [14, 38, 18, 17, 10], |         "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( | wave_4_3.extend( | ||||||
|     "ESP32-S3-TOUCH-LCD-7-800X480", |     "ESP32-S3-TOUCH-LCD-7-800X480", | ||||||
|     enable_pin=[{"ch422g": None, "number": 2}, {"ch422g": None, "number": 6}], |     enable_pin=[{"ch422g": None, "number": 2}, {"ch422g": None, "number": 6}], | ||||||
|   | |||||||
| @@ -52,9 +52,8 @@ const uint8_t MITSUBISHI_BYTE16 = 0x00; | |||||||
|  |  | ||||||
| climate::ClimateTraits MitsubishiClimate::traits() { | climate::ClimateTraits MitsubishiClimate::traits() { | ||||||
|   auto traits = climate::ClimateTraits(); |   auto traits = climate::ClimateTraits(); | ||||||
|   if (this->sensor_ != nullptr) { |   traits.set_supports_current_temperature(this->sensor_ != nullptr); | ||||||
|     traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); |   traits.set_supports_action(false); | ||||||
|   } |  | ||||||
|   traits.set_visual_min_temperature(MITSUBISHI_TEMP_MIN); |   traits.set_visual_min_temperature(MITSUBISHI_TEMP_MIN); | ||||||
|   traits.set_visual_max_temperature(MITSUBISHI_TEMP_MAX); |   traits.set_visual_max_temperature(MITSUBISHI_TEMP_MAX); | ||||||
|   traits.set_visual_temperature_step(1.0f); |   traits.set_visual_temperature_step(1.0f); | ||||||
|   | |||||||
| @@ -58,7 +58,6 @@ from esphome.const import ( | |||||||
|     PlatformFramework, |     PlatformFramework, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, CoroPriority, coroutine_with_priority | from esphome.core import CORE, CoroPriority, coroutine_with_priority | ||||||
| from esphome.types import ConfigType |  | ||||||
|  |  | ||||||
| DEPENDENCIES = ["network"] | DEPENDENCIES = ["network"] | ||||||
|  |  | ||||||
| @@ -211,15 +210,6 @@ def validate_fingerprint(value): | |||||||
|     return 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( | CONFIG_SCHEMA = cv.All( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
|         { |         { | ||||||
| @@ -316,7 +306,6 @@ CONFIG_SCHEMA = cv.All( | |||||||
|     ), |     ), | ||||||
|     validate_config, |     validate_config, | ||||||
|     cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]), |     cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]), | ||||||
|     _consume_mqtt_sockets, |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
| #ifdef USE_MQTT | #ifdef USE_MQTT | ||||||
| #ifdef USE_FAN | #ifdef USE_FAN | ||||||
|  |  | ||||||
| #include "esphome/components/fan/fan.h" | #include "esphome/components/fan/fan_state.h" | ||||||
| #include "mqtt_component.h" | #include "mqtt_component.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
|   | |||||||
| @@ -69,12 +69,6 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery | |||||||
|   if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) |   if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) | ||||||
|     root["brightness"] = true; |     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()) { |   if (this->state_->supports_effects()) { | ||||||
|     root["effect"] = true; |     root["effect"] = true; | ||||||
|     JsonArray effect_list = root[MQTT_EFFECT_LIST].to<JsonArray>(); |     JsonArray effect_list = root[MQTT_EFFECT_LIST].to<JsonArray>(); | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ from esphome.components.esp32 import ( | |||||||
|     VARIANT_ESP32H2, |     VARIANT_ESP32H2, | ||||||
|     add_idf_sdkconfig_option, |     add_idf_sdkconfig_option, | ||||||
|     only_on_variant, |     only_on_variant, | ||||||
|     require_vfs_select, |  | ||||||
| ) | ) | ||||||
| from esphome.components.mdns import MDNSComponent, enable_mdns_storage | from esphome.components.mdns import MDNSComponent, enable_mdns_storage | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| @@ -107,14 +106,6 @@ _CONNECTION_SCHEMA = cv.Schema( | |||||||
|     } |     } | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def _require_vfs_select(config): |  | ||||||
|     """Register VFS select requirement during config validation.""" |  | ||||||
|     # OpenThread uses esp_vfs_eventfd which requires VFS select support |  | ||||||
|     require_vfs_select() |  | ||||||
|     return config |  | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = cv.All( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
|         { |         { | ||||||
| @@ -131,7 +122,6 @@ CONFIG_SCHEMA = cv.All( | |||||||
|     cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), |     cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), | ||||||
|     cv.only_with_esp_idf, |     cv.only_with_esp_idf, | ||||||
|     only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]), |     only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]), | ||||||
|     _require_vfs_select, |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -54,10 +54,11 @@ void PIDClimate::control(const climate::ClimateCall &call) { | |||||||
| } | } | ||||||
| climate::ClimateTraits PIDClimate::traits() { | climate::ClimateTraits PIDClimate::traits() { | ||||||
|   auto traits = climate::ClimateTraits(); |   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) |   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}); |   traits.set_supported_modes({climate::CLIMATE_MODE_OFF}); | ||||||
|   if (supports_cool_()) |   if (supports_cool_()) | ||||||
| @@ -67,6 +68,7 @@ climate::ClimateTraits PIDClimate::traits() { | |||||||
|   if (supports_heat_() && supports_cool_()) |   if (supports_heat_() && supports_cool_()) | ||||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); |     traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); | ||||||
|  |  | ||||||
|  |   traits.set_supports_action(true); | ||||||
|   return traits; |   return traits; | ||||||
| } | } | ||||||
| void PIDClimate::dump_config() { | 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_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_DC_DC_OVER_CURRENT = "fault_dc_dc_over_current" | ||||||
| CONF_FAULT_CODE = "fault_code" | 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 = ( | CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START = ( | ||||||
|     "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_WARNING_BATTERY_TOO_LOW_TO_CHARGE, | ||||||
|     CONF_FAULT_DC_DC_OVER_CURRENT, |     CONF_FAULT_DC_DC_OVER_CURRENT, | ||||||
|     CONF_FAULT_CODE, |     CONF_FAULT_CODE, | ||||||
|     CONF_WARNING_LOW_PV_ENERGY, |     CONF_WARNUNG_LOW_PV_ENERGY, | ||||||
|     CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START, |     CONF_WARNING_HIGH_AC_INPUT_DURING_BUS_SOFT_START, | ||||||
|     CONF_WARNING_BATTERY_EQUALIZATION, |     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()) { |   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); |     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 { |   } else { | ||||||
|     ESP_LOGD(TAG, "Will not write: %s as it is not in list of allowed values", tmp); |     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/components/uart/uart.h" | ||||||
| #include "esphome/core/automation.h" | #include "esphome/core/automation.h" | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/helpers.h" |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace pipsolar { | namespace pipsolar { | ||||||
| @@ -29,17 +28,10 @@ struct PollingCommand { | |||||||
|   bool needs_update; |   bool needs_update; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| struct QFLAGValues { | #define PIPSOLAR_VALUED_ENTITY_(type, name, polling_command, value_type) \ | ||||||
|   esphome::optional<bool> silence_buzzer_open_buzzer; |  protected: \ | ||||||
|   esphome::optional<bool> overload_bypass_function; |   value_type value_##name##_; \ | ||||||
|   esphome::optional<bool> lcd_escape_to_default; |   PIPSOLAR_ENTITY_(type, name, polling_command) | ||||||
|   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_ENTITY_(type, name, polling_command) \ | #define PIPSOLAR_ENTITY_(type, name, polling_command) \ | ||||||
|  protected: \ |  protected: \ | ||||||
| @@ -51,123 +43,126 @@ struct QFLAGValues { | |||||||
|     this->add_polling_command_(#polling_command, POLLING_##polling_command); \ |     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_SWITCH(name, polling_command) PIPSOLAR_ENTITY_(switch_::Switch, name, polling_command) | ||||||
| #define PIPSOLAR_BINARY_SENSOR(name, polling_command) \ | #define PIPSOLAR_BINARY_SENSOR(name, polling_command, value_type) \ | ||||||
|   PIPSOLAR_ENTITY_(binary_sensor::BinarySensor, name, polling_command) |   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) | #define PIPSOLAR_TEXT_SENSOR(name, polling_command) PIPSOLAR_ENTITY_(text_sensor::TextSensor, name, polling_command) | ||||||
|  |  | ||||||
| class Pipsolar : public uart::UARTDevice, public PollingComponent { | class Pipsolar : public uart::UARTDevice, public PollingComponent { | ||||||
|   // QPIGS values |   // QPIGS values | ||||||
|   PIPSOLAR_SENSOR(grid_voltage, QPIGS) |   PIPSOLAR_SENSOR(grid_voltage, QPIGS, float) | ||||||
|   PIPSOLAR_SENSOR(grid_frequency, QPIGS) |   PIPSOLAR_SENSOR(grid_frequency, QPIGS, float) | ||||||
|   PIPSOLAR_SENSOR(ac_output_voltage, QPIGS) |   PIPSOLAR_SENSOR(ac_output_voltage, QPIGS, float) | ||||||
|   PIPSOLAR_SENSOR(ac_output_frequency, QPIGS) |   PIPSOLAR_SENSOR(ac_output_frequency, QPIGS, float) | ||||||
|   PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS) |   PIPSOLAR_SENSOR(ac_output_apparent_power, QPIGS, int) | ||||||
|   PIPSOLAR_SENSOR(ac_output_active_power, QPIGS) |   PIPSOLAR_SENSOR(ac_output_active_power, QPIGS, int) | ||||||
|   PIPSOLAR_SENSOR(output_load_percent, QPIGS) |   PIPSOLAR_SENSOR(output_load_percent, QPIGS, int) | ||||||
|   PIPSOLAR_SENSOR(bus_voltage, QPIGS) |   PIPSOLAR_SENSOR(bus_voltage, QPIGS, int) | ||||||
|   PIPSOLAR_SENSOR(battery_voltage, QPIGS) |   PIPSOLAR_SENSOR(battery_voltage, QPIGS, float) | ||||||
|   PIPSOLAR_SENSOR(battery_charging_current, QPIGS) |   PIPSOLAR_SENSOR(battery_charging_current, QPIGS, int) | ||||||
|   PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS) |   PIPSOLAR_SENSOR(battery_capacity_percent, QPIGS, int) | ||||||
|   PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS) |   PIPSOLAR_SENSOR(inverter_heat_sink_temperature, QPIGS, int) | ||||||
|   PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS) |   PIPSOLAR_SENSOR(pv_input_current_for_battery, QPIGS, float) | ||||||
|   PIPSOLAR_SENSOR(pv_input_voltage, QPIGS) |   PIPSOLAR_SENSOR(pv_input_voltage, QPIGS, float) | ||||||
|   PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS) |   PIPSOLAR_SENSOR(battery_voltage_scc, QPIGS, float) | ||||||
|   PIPSOLAR_SENSOR(battery_discharge_current, QPIGS) |   PIPSOLAR_SENSOR(battery_discharge_current, QPIGS, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS) |   PIPSOLAR_BINARY_SENSOR(add_sbu_priority_version, QPIGS, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS) |   PIPSOLAR_BINARY_SENSOR(configuration_status, QPIGS, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS) |   PIPSOLAR_BINARY_SENSOR(scc_firmware_version, QPIGS, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(load_status, QPIGS) |   PIPSOLAR_BINARY_SENSOR(load_status, QPIGS, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS) |   PIPSOLAR_BINARY_SENSOR(battery_voltage_to_steady_while_charging, QPIGS, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS) |   PIPSOLAR_BINARY_SENSOR(charging_status, QPIGS, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS) |   PIPSOLAR_BINARY_SENSOR(scc_charging_status, QPIGS, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS) |   PIPSOLAR_BINARY_SENSOR(ac_charging_status, QPIGS, int) | ||||||
|   PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS)  //.1 scale |   PIPSOLAR_SENSOR(battery_voltage_offset_for_fans_on, QPIGS, int)  //.1 scale | ||||||
|   PIPSOLAR_SENSOR(eeprom_version, QPIGS) |   PIPSOLAR_SENSOR(eeprom_version, QPIGS, int) | ||||||
|   PIPSOLAR_SENSOR(pv_charging_power, QPIGS) |   PIPSOLAR_SENSOR(pv_charging_power, QPIGS, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS) |   PIPSOLAR_BINARY_SENSOR(charging_to_floating_mode, QPIGS, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS) |   PIPSOLAR_BINARY_SENSOR(switch_on, QPIGS, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS) |   PIPSOLAR_BINARY_SENSOR(dustproof_installed, QPIGS, int) | ||||||
|  |  | ||||||
|   // QPIRI values |   // QPIRI values | ||||||
|   PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI) |   PIPSOLAR_SENSOR(grid_rating_voltage, QPIRI, float) | ||||||
|   PIPSOLAR_SENSOR(grid_rating_current, QPIRI) |   PIPSOLAR_SENSOR(grid_rating_current, QPIRI, float) | ||||||
|   PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI) |   PIPSOLAR_SENSOR(ac_output_rating_voltage, QPIRI, float) | ||||||
|   PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI) |   PIPSOLAR_SENSOR(ac_output_rating_frequency, QPIRI, float) | ||||||
|   PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI) |   PIPSOLAR_SENSOR(ac_output_rating_current, QPIRI, float) | ||||||
|   PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI) |   PIPSOLAR_SENSOR(ac_output_rating_apparent_power, QPIRI, int) | ||||||
|   PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI) |   PIPSOLAR_SENSOR(ac_output_rating_active_power, QPIRI, int) | ||||||
|   PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI) |   PIPSOLAR_SENSOR(battery_rating_voltage, QPIRI, float) | ||||||
|   PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI) |   PIPSOLAR_SENSOR(battery_recharge_voltage, QPIRI, float) | ||||||
|   PIPSOLAR_SENSOR(battery_under_voltage, QPIRI) |   PIPSOLAR_SENSOR(battery_under_voltage, QPIRI, float) | ||||||
|   PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI) |   PIPSOLAR_SENSOR(battery_bulk_voltage, QPIRI, float) | ||||||
|   PIPSOLAR_SENSOR(battery_float_voltage, QPIRI) |   PIPSOLAR_SENSOR(battery_float_voltage, QPIRI, float) | ||||||
|   PIPSOLAR_SENSOR(battery_type, QPIRI) |   PIPSOLAR_SENSOR(battery_type, QPIRI, int) | ||||||
|   PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI) |   PIPSOLAR_SENSOR(current_max_ac_charging_current, QPIRI, int) | ||||||
|   PIPSOLAR_SENSOR(current_max_charging_current, QPIRI) |   PIPSOLAR_SENSOR(current_max_charging_current, QPIRI, int) | ||||||
|   PIPSOLAR_SENSOR(input_voltage_range, QPIRI) |   PIPSOLAR_SENSOR(input_voltage_range, QPIRI, int) | ||||||
|   PIPSOLAR_SENSOR(output_source_priority, QPIRI) |   PIPSOLAR_SENSOR(output_source_priority, QPIRI, int) | ||||||
|   PIPSOLAR_SENSOR(charger_source_priority, QPIRI) |   PIPSOLAR_SENSOR(charger_source_priority, QPIRI, int) | ||||||
|   PIPSOLAR_SENSOR(parallel_max_num, QPIRI) |   PIPSOLAR_SENSOR(parallel_max_num, QPIRI, int) | ||||||
|   PIPSOLAR_SENSOR(machine_type, QPIRI) |   PIPSOLAR_SENSOR(machine_type, QPIRI, int) | ||||||
|   PIPSOLAR_SENSOR(topology, QPIRI) |   PIPSOLAR_SENSOR(topology, QPIRI, int) | ||||||
|   PIPSOLAR_SENSOR(output_mode, QPIRI) |   PIPSOLAR_SENSOR(output_mode, QPIRI, int) | ||||||
|   PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI) |   PIPSOLAR_SENSOR(battery_redischarge_voltage, QPIRI, float) | ||||||
|   PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI) |   PIPSOLAR_SENSOR(pv_ok_condition_for_parallel, QPIRI, int) | ||||||
|   PIPSOLAR_SENSOR(pv_power_balance, QPIRI) |   PIPSOLAR_SENSOR(pv_power_balance, QPIRI, int) | ||||||
|  |  | ||||||
|   // QMOD values |   // QMOD values | ||||||
|   PIPSOLAR_TEXT_SENSOR(device_mode, QMOD) |   PIPSOLAR_VALUED_TEXT_SENSOR(device_mode, QMOD, char) | ||||||
|  |  | ||||||
|   // QFLAG values |   // QFLAG values | ||||||
|   PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG) |   PIPSOLAR_BINARY_SENSOR(silence_buzzer_open_buzzer, QFLAG, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG) |   PIPSOLAR_BINARY_SENSOR(overload_bypass_function, QFLAG, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG) |   PIPSOLAR_BINARY_SENSOR(lcd_escape_to_default, QFLAG, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG) |   PIPSOLAR_BINARY_SENSOR(overload_restart_function, QFLAG, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG) |   PIPSOLAR_BINARY_SENSOR(over_temperature_restart_function, QFLAG, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG) |   PIPSOLAR_BINARY_SENSOR(backlight_on, QFLAG, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG) |   PIPSOLAR_BINARY_SENSOR(alarm_on_when_primary_source_interrupt, QFLAG, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG) |   PIPSOLAR_BINARY_SENSOR(fault_code_record, QFLAG, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG) |   PIPSOLAR_BINARY_SENSOR(power_saving, QFLAG, int) | ||||||
|  |  | ||||||
|   // QPIWS values |   // QPIWS values | ||||||
|   PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warnings_present, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS) |   PIPSOLAR_BINARY_SENSOR(faults_present, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_power_loss, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_inverter_fault, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_bus_over, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_bus_under, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_bus_soft_fail, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_line_fail, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_opvshort, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_low, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_inverter_voltage_too_high, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_over_temperature, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_fan_lock, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_battery_voltage_high, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_battery_low_alarm, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_battery_under_shutdown, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_battery_derating, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_over_load, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_eeprom_failed, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_inverter_over_current, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_inverter_soft_failed, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_self_test_failed, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_op_dc_voltage_over, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_battery_open, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_current_sensor_failed, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_battery_short, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_power_limit, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_pv_voltage_high, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_mppt_overload, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_mppt_overload, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_battery_too_low_to_charge, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_dc_dc_over_current, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS) |   PIPSOLAR_BINARY_SENSOR(fault_code, QPIWS, int) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_low_pv_energy, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warnung_low_pv_energy, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_high_ac_input_during_bus_soft_start, QPIWS, bool) | ||||||
|   PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS) |   PIPSOLAR_BINARY_SENSOR(warning_battery_equalization, QPIWS, bool) | ||||||
|  |  | ||||||
|   PIPSOLAR_TEXT_SENSOR(last_qpigs, QPIGS) |   PIPSOLAR_TEXT_SENSOR(last_qpigs, QPIGS) | ||||||
|   PIPSOLAR_TEXT_SENSOR(last_qpiri, QPIRI) |   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_ok_condition_for_parallel_switch, QPIRI) | ||||||
|   PIPSOLAR_SWITCH(pv_power_balance_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 setup() override; | ||||||
|   void loop() override; |   void loop() override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void update() override; |   void update() override; | ||||||
|  |  | ||||||
|  protected: |  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_QUEUE_LENGTH = 10; | ||||||
|   static const size_t COMMAND_TIMEOUT = 5000; |   static const size_t COMMAND_TIMEOUT = 5000; | ||||||
|   static const size_t POLLING_COMMANDS_MAX = 15; |   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); |   uint16_t pipsolar_crc_(uint8_t *msg, uint8_t len); | ||||||
|   bool send_next_command_(); |   bool send_next_command_(); | ||||||
|   bool send_next_poll_(); |   bool send_next_poll_(); | ||||||
|  |   void queue_command_(const char *command, uint8_t length); | ||||||
|   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); |  | ||||||
|  |  | ||||||
|   std::string command_queue_[COMMAND_QUEUE_LENGTH]; |   std::string command_queue_[COMMAND_QUEUE_LENGTH]; | ||||||
|   uint8_t command_queue_position_ = 0; |   uint8_t command_queue_position_ = 0; | ||||||
|   uint8_t read_buffer_[PIPSOLAR_READ_BUFFER_LENGTH]; |   uint8_t read_buffer_[PIPSOLAR_READ_BUFFER_LENGTH]; | ||||||
| @@ -237,10 +213,11 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { | |||||||
|     STATE_POLL_COMPLETE = 3, |     STATE_POLL_COMPLETE = 3, | ||||||
|     STATE_COMMAND_COMPLETE = 4, |     STATE_COMMAND_COMPLETE = 4, | ||||||
|     STATE_POLL_CHECKED = 5, |     STATE_POLL_CHECKED = 5, | ||||||
|  |     STATE_POLL_DECODED = 6, | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   uint8_t last_polling_command_ = 0; |   uint8_t last_polling_command_ = 0; | ||||||
|   PollingCommand enabled_polling_commands_[POLLING_COMMANDS_MAX]; |   PollingCommand used_polling_commands_[POLLING_COMMANDS_MAX]; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace pipsolar | }  // namespace pipsolar | ||||||
|   | |||||||
| @@ -11,11 +11,11 @@ void PipsolarSwitch::dump_config() { LOG_SWITCH("", "Pipsolar Switch", this); } | |||||||
| void PipsolarSwitch::write_state(bool state) { | void PipsolarSwitch::write_state(bool state) { | ||||||
|   if (state) { |   if (state) { | ||||||
|     if (!this->on_command_.empty()) { |     if (!this->on_command_.empty()) { | ||||||
|       this->parent_->queue_command(this->on_command_); |       this->parent_->switch_command(this->on_command_); | ||||||
|     } |     } | ||||||
|   } else { |   } else { | ||||||
|     if (!this->off_command_.empty()) { |     if (!this->off_command_.empty()) { | ||||||
|       this->parent_->queue_command(this->off_command_); |       this->parent_->switch_command(this->off_command_); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -28,8 +28,6 @@ from esphome.const import ( | |||||||
|     CONF_ON_RAW_VALUE, |     CONF_ON_RAW_VALUE, | ||||||
|     CONF_ON_VALUE, |     CONF_ON_VALUE, | ||||||
|     CONF_ON_VALUE_RANGE, |     CONF_ON_VALUE_RANGE, | ||||||
|     CONF_OPTIMISTIC, |  | ||||||
|     CONF_PERIOD, |  | ||||||
|     CONF_QUANTILE, |     CONF_QUANTILE, | ||||||
|     CONF_SEND_EVERY, |     CONF_SEND_EVERY, | ||||||
|     CONF_SEND_FIRST_AT, |     CONF_SEND_FIRST_AT, | ||||||
| @@ -263,12 +261,9 @@ ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Com | |||||||
| LambdaFilter = sensor_ns.class_("LambdaFilter", Filter) | LambdaFilter = sensor_ns.class_("LambdaFilter", Filter) | ||||||
| OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) | OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) | ||||||
| MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) | MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) | ||||||
| ValueListFilter = sensor_ns.class_("ValueListFilter", Filter) | FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", Filter) | ||||||
| FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", ValueListFilter) |  | ||||||
| ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter) | ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter) | ||||||
| ThrottleWithPriorityFilter = sensor_ns.class_( | ThrottleWithPriorityFilter = sensor_ns.class_("ThrottleWithPriorityFilter", Filter) | ||||||
|     "ThrottleWithPriorityFilter", ValueListFilter |  | ||||||
| ) |  | ||||||
| TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component) | TimeoutFilter = sensor_ns.class_("TimeoutFilter", Filter, cg.Component) | ||||||
| DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) | DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) | ||||||
| HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component) | HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter, cg.Component) | ||||||
| @@ -646,29 +641,10 @@ async def throttle_with_priority_filter_to_code(config, filter_id): | |||||||
|     return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) |     return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) | ||||||
|  |  | ||||||
|  |  | ||||||
| HEARTBEAT_SCHEMA = cv.Schema( |  | ||||||
|     { |  | ||||||
|         cv.Required(CONF_PERIOD): cv.positive_time_period_milliseconds, |  | ||||||
|         cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, |  | ||||||
|     } |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @FILTER_REGISTRY.register( | @FILTER_REGISTRY.register( | ||||||
|     "heartbeat", |     "heartbeat", HeartbeatFilter, cv.positive_time_period_milliseconds | ||||||
|     HeartbeatFilter, |  | ||||||
|     cv.Any( |  | ||||||
|         cv.positive_time_period_milliseconds, |  | ||||||
|         HEARTBEAT_SCHEMA, |  | ||||||
|     ), |  | ||||||
| ) | ) | ||||||
| async def heartbeat_filter_to_code(config, filter_id): | async def heartbeat_filter_to_code(config, filter_id): | ||||||
|     if isinstance(config, dict): |  | ||||||
|         var = cg.new_Pvariable(filter_id, config[CONF_PERIOD]) |  | ||||||
|         await cg.register_component(var, {}) |  | ||||||
|         cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) |  | ||||||
|         return var |  | ||||||
|  |  | ||||||
|     var = cg.new_Pvariable(filter_id, config) |     var = cg.new_Pvariable(filter_id, config) | ||||||
|     await cg.register_component(var, {}) |     await cg.register_component(var, {}) | ||||||
|     return var |     return var | ||||||
|   | |||||||
| @@ -228,40 +228,27 @@ MultiplyFilter::MultiplyFilter(TemplatableValue<float> multiplier) : multiplier_ | |||||||
|  |  | ||||||
| optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); } | 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::FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out) | FilterOutValueFilter::FilterOutValueFilter(std::vector<TemplatableValue<float>> values_to_filter_out) | ||||||
|     : ValueListFilter(values_to_filter_out) {} |     : values_to_filter_out_(std::move(values_to_filter_out)) {} | ||||||
|  |  | ||||||
| optional<float> FilterOutValueFilter::new_value(float value) { | optional<float> FilterOutValueFilter::new_value(float value) { | ||||||
|   if (this->value_matches_any_(value)) |   int8_t accuracy = this->parent_->get_accuracy_decimals(); | ||||||
|     return {};   // Filter out |   float accuracy_mult = powf(10.0f, accuracy); | ||||||
|   return value;  // Pass through |   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 | // ThrottleFilter | ||||||
| @@ -276,15 +263,33 @@ optional<float> ThrottleFilter::new_value(float value) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // ThrottleWithPriorityFilter | // ThrottleWithPriorityFilter | ||||||
| ThrottleWithPriorityFilter::ThrottleWithPriorityFilter( | ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, | ||||||
|     uint32_t min_time_between_inputs, std::initializer_list<TemplatableValue<float>> prioritized_values) |                                                        std::vector<TemplatableValue<float>> prioritized_values) | ||||||
|     : ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {} |     : min_time_between_inputs_(min_time_between_inputs), prioritized_values_(std::move(prioritized_values)) {} | ||||||
|  |  | ||||||
| optional<float> ThrottleWithPriorityFilter::new_value(float value) { | 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(); |   const uint32_t now = App.get_loop_component_start_time(); | ||||||
|   // Allow value through if: no previous input, time expired, or is prioritized |   // First, determine if the new value is one of the prioritized values | ||||||
|   if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ || |   for (auto prioritized_value : this->prioritized_values_) { | ||||||
|       this->value_matches_any_(value)) { |     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; |     this->last_input_ = now; | ||||||
|     return value; |     return value; | ||||||
|   } |   } | ||||||
| @@ -313,7 +318,7 @@ optional<float> DeltaFilter::new_value(float value) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // OrFilter | // OrFilter | ||||||
| OrFilter::OrFilter(std::initializer_list<Filter *> filters) : filters_(filters), phi_(this) {} | OrFilter::OrFilter(std::vector<Filter *> filters) : filters_(std::move(filters)), phi_(this) {} | ||||||
| OrFilter::PhiNode::PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {} | OrFilter::PhiNode::PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {} | ||||||
|  |  | ||||||
| optional<float> OrFilter::PhiNode::new_value(float value) { | optional<float> OrFilter::PhiNode::new_value(float value) { | ||||||
| @@ -326,14 +331,14 @@ optional<float> OrFilter::PhiNode::new_value(float value) { | |||||||
| } | } | ||||||
| optional<float> OrFilter::new_value(float value) { | optional<float> OrFilter::new_value(float value) { | ||||||
|   this->has_value_ = false; |   this->has_value_ = false; | ||||||
|   for (auto *filter : this->filters_) |   for (Filter *filter : this->filters_) | ||||||
|     filter->input(value); |     filter->input(value); | ||||||
|  |  | ||||||
|   return {}; |   return {}; | ||||||
| } | } | ||||||
| void OrFilter::initialize(Sensor *parent, Filter *next) { | void OrFilter::initialize(Sensor *parent, Filter *next) { | ||||||
|   Filter::initialize(parent, next); |   Filter::initialize(parent, next); | ||||||
|   for (auto *filter : this->filters_) { |   for (Filter *filter : this->filters_) { | ||||||
|     filter->initialize(parent, &this->phi_); |     filter->initialize(parent, &this->phi_); | ||||||
|   } |   } | ||||||
|   this->phi_.initialize(parent, nullptr); |   this->phi_.initialize(parent, nullptr); | ||||||
| @@ -372,12 +377,8 @@ optional<float> HeartbeatFilter::new_value(float value) { | |||||||
|   this->last_input_ = value; |   this->last_input_ = value; | ||||||
|   this->has_value_ = true; |   this->has_value_ = true; | ||||||
|  |  | ||||||
|   if (this->optimistic_) { |  | ||||||
|     return value; |  | ||||||
|   } |  | ||||||
|   return {}; |   return {}; | ||||||
| } | } | ||||||
|  |  | ||||||
| void HeartbeatFilter::setup() { | void HeartbeatFilter::setup() { | ||||||
|   this->set_interval("heartbeat", this->time_period_, [this]() { |   this->set_interval("heartbeat", this->time_period_, [this]() { | ||||||
|     ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_), |     ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_), | ||||||
| @@ -388,27 +389,20 @@ void HeartbeatFilter::setup() { | |||||||
|     this->output(this->last_input_); |     this->output(this->last_input_); | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } | float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||||
|  |  | ||||||
| CalibrateLinearFilter::CalibrateLinearFilter(std::initializer_list<std::array<float, 3>> linear_functions) |  | ||||||
|     : linear_functions_(linear_functions) {} |  | ||||||
|  |  | ||||||
| optional<float> CalibrateLinearFilter::new_value(float value) { | optional<float> CalibrateLinearFilter::new_value(float value) { | ||||||
|   for (const auto &f : this->linear_functions_) { |   for (std::array<float, 3> f : this->linear_functions_) { | ||||||
|     if (!std::isfinite(f[2]) || value < f[2]) |     if (!std::isfinite(f[2]) || value < f[2]) | ||||||
|       return (value * f[0]) + f[1]; |       return (value * f[0]) + f[1]; | ||||||
|   } |   } | ||||||
|   return NAN; |   return NAN; | ||||||
| } | } | ||||||
|  |  | ||||||
| CalibratePolynomialFilter::CalibratePolynomialFilter(std::initializer_list<float> coefficients) |  | ||||||
|     : coefficients_(coefficients) {} |  | ||||||
|  |  | ||||||
| optional<float> CalibratePolynomialFilter::new_value(float value) { | optional<float> CalibratePolynomialFilter::new_value(float value) { | ||||||
|   float res = 0.0f; |   float res = 0.0f; | ||||||
|   float x = 1.0f; |   float x = 1.0f; | ||||||
|   for (const auto &coefficient : this->coefficients_) { |   for (float coefficient : this->coefficients_) { | ||||||
|     res += x * coefficient; |     res += x * coefficient; | ||||||
|     x *= value; |     x *= value; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -317,28 +317,15 @@ class MultiplyFilter : public Filter { | |||||||
|   TemplatableValue<float> multiplier_; |   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`. | /// 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: |  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; |   optional<float> new_value(float value) override; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   std::vector<TemplatableValue<float>> values_to_filter_out_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class ThrottleFilter : public Filter { | class ThrottleFilter : public Filter { | ||||||
| @@ -353,16 +340,17 @@ class ThrottleFilter : public Filter { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| /// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`. | /// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`. | ||||||
| class ThrottleWithPriorityFilter : public ValueListFilter { | class ThrottleWithPriorityFilter : public Filter { | ||||||
|  public: |  public: | ||||||
|   explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, |   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; |   optional<float> new_value(float value) override; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   uint32_t last_input_{0}; |   uint32_t last_input_{0}; | ||||||
|   uint32_t min_time_between_inputs_; |   uint32_t min_time_between_inputs_; | ||||||
|  |   std::vector<TemplatableValue<float>> prioritized_values_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class TimeoutFilter : public Filter, public Component { | class TimeoutFilter : public Filter, public Component { | ||||||
| @@ -396,16 +384,15 @@ class HeartbeatFilter : public Filter, public Component { | |||||||
|   explicit HeartbeatFilter(uint32_t time_period); |   explicit HeartbeatFilter(uint32_t time_period); | ||||||
|  |  | ||||||
|   void setup() override; |   void setup() override; | ||||||
|   optional<float> new_value(float value) override; |  | ||||||
|   float get_setup_priority() const override; |  | ||||||
|  |  | ||||||
|   void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } |   optional<float> new_value(float value) override; | ||||||
|  |  | ||||||
|  |   float get_setup_priority() const override; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   uint32_t time_period_; |   uint32_t time_period_; | ||||||
|   float last_input_; |   float last_input_; | ||||||
|   bool has_value_{false}; |   bool has_value_{false}; | ||||||
|   bool optimistic_{false}; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class DeltaFilter : public Filter { | class DeltaFilter : public Filter { | ||||||
| @@ -423,7 +410,7 @@ class DeltaFilter : public Filter { | |||||||
|  |  | ||||||
| class OrFilter : public Filter { | class OrFilter : public Filter { | ||||||
|  public: |  public: | ||||||
|   explicit OrFilter(std::initializer_list<Filter *> filters); |   explicit OrFilter(std::vector<Filter *> filters); | ||||||
|  |  | ||||||
|   void initialize(Sensor *parent, Filter *next) override; |   void initialize(Sensor *parent, Filter *next) override; | ||||||
|  |  | ||||||
| @@ -439,27 +426,28 @@ class OrFilter : public Filter { | |||||||
|     OrFilter *or_parent_; |     OrFilter *or_parent_; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   FixedVector<Filter *> filters_; |   std::vector<Filter *> filters_; | ||||||
|   PhiNode phi_; |   PhiNode phi_; | ||||||
|   bool has_value_{false}; |   bool has_value_{false}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class CalibrateLinearFilter : public Filter { | class CalibrateLinearFilter : public Filter { | ||||||
|  public: |  public: | ||||||
|   explicit CalibrateLinearFilter(std::initializer_list<std::array<float, 3>> linear_functions); |   CalibrateLinearFilter(std::vector<std::array<float, 3>> linear_functions) | ||||||
|  |       : linear_functions_(std::move(linear_functions)) {} | ||||||
|   optional<float> new_value(float value) override; |   optional<float> new_value(float value) override; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   FixedVector<std::array<float, 3>> linear_functions_; |   std::vector<std::array<float, 3>> linear_functions_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class CalibratePolynomialFilter : public Filter { | class CalibratePolynomialFilter : public Filter { | ||||||
|  public: |  public: | ||||||
|   explicit CalibratePolynomialFilter(std::initializer_list<float> coefficients); |   CalibratePolynomialFilter(std::vector<float> coefficients) : coefficients_(std::move(coefficients)) {} | ||||||
|   optional<float> new_value(float value) override; |   optional<float> new_value(float value) override; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   FixedVector<float> coefficients_; |   std::vector<float> coefficients_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class ClampFilter : public Filter { | class ClampFilter : public Filter { | ||||||
|   | |||||||
| @@ -107,12 +107,12 @@ void Sensor::add_filter(Filter *filter) { | |||||||
|   } |   } | ||||||
|   filter->initialize(this, nullptr); |   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) { |   for (Filter *filter : filters) { | ||||||
|     this->add_filter(filter); |     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->clear_filters(); | ||||||
|   this->add_filters(filters); |   this->add_filters(filters); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ | |||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include "esphome/components/sensor/filter.h" | #include "esphome/components/sensor/filter.h" | ||||||
|  |  | ||||||
| #include <initializer_list> | #include <vector> | ||||||
| #include <memory> | #include <memory> | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| @@ -77,10 +77,10 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa | |||||||
|    *   SlidingWindowMovingAverageFilter(15, 15), // average over last 15 values |    *   SlidingWindowMovingAverageFilter(15, 15), // average over last 15 values | ||||||
|    * }); |    * }); | ||||||
|    */ |    */ | ||||||
|   void add_filters(std::initializer_list<Filter *> filters); |   void add_filters(const std::vector<Filter *> &filters); | ||||||
|  |  | ||||||
|   /// Clear the filters and replace them by filters. |   /// Clear the filters and replace them by filters. | ||||||
|   void set_filters(std::initializer_list<Filter *> filters); |   void set_filters(const std::vector<Filter *> &filters); | ||||||
|  |  | ||||||
|   /// Clear the entire filter chain. |   /// Clear the entire filter chain. | ||||||
|   void clear_filters(); |   void clear_filters(); | ||||||
|   | |||||||
| @@ -1,5 +1,3 @@ | |||||||
| from collections.abc import Callable, MutableMapping |  | ||||||
|  |  | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.core import CORE | from esphome.core import CORE | ||||||
| @@ -11,32 +9,6 @@ IMPLEMENTATION_LWIP_TCP = "lwip_tcp" | |||||||
| IMPLEMENTATION_LWIP_SOCKETS = "lwip_sockets" | IMPLEMENTATION_LWIP_SOCKETS = "lwip_sockets" | ||||||
| IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" | IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" | ||||||
|  |  | ||||||
| # Socket tracking infrastructure |  | ||||||
| # Components register their socket needs and platforms read this to configure appropriately |  | ||||||
| KEY_SOCKET_CONSUMERS = "socket_consumers" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def consume_sockets( |  | ||||||
|     value: int, consumer: str |  | ||||||
| ) -> Callable[[MutableMapping], MutableMapping]: |  | ||||||
|     """Register socket usage for a component. |  | ||||||
|  |  | ||||||
|     Args: |  | ||||||
|         value: Number of sockets needed by the component |  | ||||||
|         consumer: Name of the component consuming the sockets |  | ||||||
|  |  | ||||||
|     Returns: |  | ||||||
|         A validator function that records the socket usage |  | ||||||
|     """ |  | ||||||
|  |  | ||||||
|     def _consume_sockets(config: MutableMapping) -> MutableMapping: |  | ||||||
|         consumers: dict[str, int] = CORE.data.setdefault(KEY_SOCKET_CONSUMERS, {}) |  | ||||||
|         consumers[consumer] = consumers.get(consumer, 0) + value |  | ||||||
|         return config |  | ||||||
|  |  | ||||||
|     return _consume_sockets |  | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.Schema( | CONFIG_SCHEMA = cv.Schema( | ||||||
|     { |     { | ||||||
|         cv.SplitDefault( |         cv.SplitDefault( | ||||||
|   | |||||||
| @@ -110,28 +110,17 @@ def validate_mapping(value): | |||||||
|     "substitute", SubstituteFilter, cv.ensure_list(validate_mapping) |     "substitute", SubstituteFilter, cv.ensure_list(validate_mapping) | ||||||
| ) | ) | ||||||
| async def substitute_filter_to_code(config, filter_id): | async def substitute_filter_to_code(config, filter_id): | ||||||
|     substitutions = [ |     from_strings = [conf[CONF_FROM] for conf in config] | ||||||
|         cg.StructInitializer( |     to_strings = [conf[CONF_TO] for conf in config] | ||||||
|             cg.MockObj("Substitution", "esphome::text_sensor::"), |     return cg.new_Pvariable(filter_id, from_strings, to_strings) | ||||||
|             ("from", conf[CONF_FROM]), |  | ||||||
|             ("to", conf[CONF_TO]), |  | ||||||
|         ) |  | ||||||
|         for conf in config |  | ||||||
|     ] |  | ||||||
|     return cg.new_Pvariable(filter_id, substitutions) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @FILTER_REGISTRY.register("map", MapFilter, cv.ensure_list(validate_mapping)) | @FILTER_REGISTRY.register("map", MapFilter, cv.ensure_list(validate_mapping)) | ||||||
| async def map_filter_to_code(config, filter_id): | async def map_filter_to_code(config, filter_id): | ||||||
|     mappings = [ |     map_ = cg.std_ns.class_("map").template(cg.std_string, cg.std_string) | ||||||
|         cg.StructInitializer( |     return cg.new_Pvariable( | ||||||
|             cg.MockObj("Substitution", "esphome::text_sensor::"), |         filter_id, map_([(item[CONF_FROM], item[CONF_TO]) for item in config]) | ||||||
|             ("from", conf[CONF_FROM]), |     ) | ||||||
|             ("to", conf[CONF_TO]), |  | ||||||
|         ) |  | ||||||
|         for conf in config |  | ||||||
|     ] |  | ||||||
|     return cg.new_Pvariable(filter_id, mappings) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") | validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") | ||||||
|   | |||||||
| @@ -62,27 +62,19 @@ optional<std::string> AppendFilter::new_value(std::string value) { return value | |||||||
| optional<std::string> PrependFilter::new_value(std::string value) { return this->prefix_ + value; } | optional<std::string> PrependFilter::new_value(std::string value) { return this->prefix_ + value; } | ||||||
|  |  | ||||||
| // Substitute | // Substitute | ||||||
| SubstituteFilter::SubstituteFilter(const std::initializer_list<Substitution> &substitutions) |  | ||||||
|     : substitutions_(substitutions) {} |  | ||||||
|  |  | ||||||
| optional<std::string> SubstituteFilter::new_value(std::string value) { | optional<std::string> SubstituteFilter::new_value(std::string value) { | ||||||
|   std::size_t pos; |   std::size_t pos; | ||||||
|   for (const auto &sub : this->substitutions_) { |   for (size_t i = 0; i < this->from_strings_.size(); i++) { | ||||||
|     while ((pos = value.find(sub.from)) != std::string::npos) |     while ((pos = value.find(this->from_strings_[i])) != std::string::npos) | ||||||
|       value.replace(pos, sub.from.size(), sub.to); |       value.replace(pos, this->from_strings_[i].size(), this->to_strings_[i]); | ||||||
|   } |   } | ||||||
|   return value; |   return value; | ||||||
| } | } | ||||||
|  |  | ||||||
| // Map | // Map | ||||||
| MapFilter::MapFilter(const std::initializer_list<Substitution> &mappings) : mappings_(mappings) {} |  | ||||||
|  |  | ||||||
| optional<std::string> MapFilter::new_value(std::string value) { | optional<std::string> MapFilter::new_value(std::string value) { | ||||||
|   for (const auto &mapping : this->mappings_) { |   auto item = mappings_.find(value); | ||||||
|     if (mapping.from == value) |   return item == mappings_.end() ? value : item->second; | ||||||
|       return mapping.to; |  | ||||||
|   } |  | ||||||
|   return value;  // Pass through if no match |  | ||||||
| } | } | ||||||
|  |  | ||||||
| }  // namespace text_sensor | }  // namespace text_sensor | ||||||
|   | |||||||
| @@ -2,6 +2,10 @@ | |||||||
|  |  | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
|  | #include <queue> | ||||||
|  | #include <utility> | ||||||
|  | #include <map> | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace text_sensor { | namespace text_sensor { | ||||||
| @@ -94,52 +98,26 @@ class PrependFilter : public Filter { | |||||||
|   std::string prefix_; |   std::string prefix_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| struct Substitution { |  | ||||||
|   std::string from; |  | ||||||
|   std::string to; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| /// A simple filter that replaces a substring with another substring | /// A simple filter that replaces a substring with another substring | ||||||
| class SubstituteFilter : public Filter { | class SubstituteFilter : public Filter { | ||||||
|  public: |  public: | ||||||
|   explicit SubstituteFilter(const std::initializer_list<Substitution> &substitutions); |   SubstituteFilter(std::vector<std::string> from_strings, std::vector<std::string> to_strings) | ||||||
|  |       : from_strings_(std::move(from_strings)), to_strings_(std::move(to_strings)) {} | ||||||
|   optional<std::string> new_value(std::string value) override; |   optional<std::string> new_value(std::string value) override; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   FixedVector<Substitution> substitutions_; |   std::vector<std::string> from_strings_; | ||||||
|  |   std::vector<std::string> to_strings_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** A filter that maps values from one set to another | /// A filter that maps values from one set to another | ||||||
|  * |  | ||||||
|  * Uses linear search instead of std::map for typical small datasets (2-20 mappings). |  | ||||||
|  * Linear search on contiguous memory is faster than red-black tree lookups when: |  | ||||||
|  * - Dataset is small (< ~30 items) |  | ||||||
|  * - Memory is contiguous (cache-friendly, better CPU cache utilization) |  | ||||||
|  * - No pointer chasing overhead (tree node traversal) |  | ||||||
|  * - String comparison cost dominates lookup time |  | ||||||
|  * |  | ||||||
|  * Benchmark results (see benchmark_map_filter.cpp): |  | ||||||
|  * - 2 mappings:  Linear 1.26x faster than std::map |  | ||||||
|  * - 5 mappings:  Linear 2.25x faster than std::map |  | ||||||
|  * - 10 mappings: Linear 1.83x faster than std::map |  | ||||||
|  * - 20 mappings: Linear 1.59x faster than std::map |  | ||||||
|  * - 30 mappings: Linear 1.09x faster than std::map |  | ||||||
|  * - 40 mappings: std::map 1.27x faster than Linear (break-even) |  | ||||||
|  * |  | ||||||
|  * Benefits over std::map: |  | ||||||
|  * - ~2KB smaller flash (no red-black tree code) |  | ||||||
|  * - ~24-32 bytes less RAM per mapping (no tree node overhead) |  | ||||||
|  * - Faster for typical ESPHome usage (2-10 mappings common, 20+ rare) |  | ||||||
|  * |  | ||||||
|  * Break-even point: ~35-40 mappings, but ESPHome configs rarely exceed 20 |  | ||||||
|  */ |  | ||||||
| class MapFilter : public Filter { | class MapFilter : public Filter { | ||||||
|  public: |  public: | ||||||
|   explicit MapFilter(const std::initializer_list<Substitution> &mappings); |   MapFilter(std::map<std::string, std::string> mappings) : mappings_(std::move(mappings)) {} | ||||||
|   optional<std::string> new_value(std::string value) override; |   optional<std::string> new_value(std::string value) override; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   FixedVector<Substitution> mappings_; |   std::map<std::string, std::string> mappings_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace text_sensor | }  // namespace text_sensor | ||||||
|   | |||||||
| @@ -51,12 +51,12 @@ void TextSensor::add_filter(Filter *filter) { | |||||||
|   } |   } | ||||||
|   filter->initialize(this, nullptr); |   filter->initialize(this, nullptr); | ||||||
| } | } | ||||||
| void TextSensor::add_filters(std::initializer_list<Filter *> filters) { | void TextSensor::add_filters(const std::vector<Filter *> &filters) { | ||||||
|   for (Filter *filter : filters) { |   for (Filter *filter : filters) { | ||||||
|     this->add_filter(filter); |     this->add_filter(filter); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| void TextSensor::set_filters(std::initializer_list<Filter *> filters) { | void TextSensor::set_filters(const std::vector<Filter *> &filters) { | ||||||
|   this->clear_filters(); |   this->clear_filters(); | ||||||
|   this->add_filters(filters); |   this->add_filters(filters); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
| #include "esphome/components/text_sensor/filter.h" | #include "esphome/components/text_sensor/filter.h" | ||||||
|  |  | ||||||
| #include <initializer_list> | #include <vector> | ||||||
| #include <memory> | #include <memory> | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| @@ -37,10 +37,10 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { | |||||||
|   void add_filter(Filter *filter); |   void add_filter(Filter *filter); | ||||||
|  |  | ||||||
|   /// Add a list of vectors to the back of the filter chain. |   /// Add a list of vectors to the back of the filter chain. | ||||||
|   void add_filters(std::initializer_list<Filter *> filters); |   void add_filters(const std::vector<Filter *> &filters); | ||||||
|  |  | ||||||
|   /// Clear the filters and replace them by filters. |   /// Clear the filters and replace them by filters. | ||||||
|   void set_filters(std::initializer_list<Filter *> filters); |   void set_filters(const std::vector<Filter *> &filters); | ||||||
|  |  | ||||||
|   /// Clear the entire filter chain. |   /// Clear the entire filter chain. | ||||||
|   void clear_filters(); |   void clear_filters(); | ||||||
|   | |||||||
| @@ -283,11 +283,8 @@ void TuyaClimate::control_fan_mode_(const climate::ClimateCall &call) { | |||||||
|  |  | ||||||
| climate::ClimateTraits TuyaClimate::traits() { | climate::ClimateTraits TuyaClimate::traits() { | ||||||
|   auto traits = climate::ClimateTraits(); |   auto traits = climate::ClimateTraits(); | ||||||
|   traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); |   traits.set_supports_action(true); | ||||||
|   if (this->current_temperature_id_.has_value()) { |   traits.set_supports_current_temperature(this->current_temperature_id_.has_value()); | ||||||
|     traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (supports_heat_) |   if (supports_heat_) | ||||||
|     traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); |     traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); | ||||||
|   if (supports_cool_) |   if (supports_cool_) | ||||||
|   | |||||||
| @@ -56,13 +56,6 @@ uint32_t ESP8266UartComponent::get_config() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void ESP8266UartComponent::setup() { | void ESP8266UartComponent::setup() { | ||||||
|   if (this->rx_pin_) { |  | ||||||
|     this->rx_pin_->setup(); |  | ||||||
|   } |  | ||||||
|   if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { |  | ||||||
|     this->tx_pin_->setup(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Use Arduino HardwareSerial UARTs if all used pins match the ones |   // Use Arduino HardwareSerial UARTs if all used pins match the ones | ||||||
|   // preconfigured by the platform. For example if RX disabled but TX pin |   // preconfigured by the platform. For example if RX disabled but TX pin | ||||||
|   // is 1 we still want to use Serial. |   // is 1 we still want to use Serial. | ||||||
|   | |||||||
| @@ -6,9 +6,6 @@ | |||||||
| #include "esphome/core/defines.h" | #include "esphome/core/defines.h" | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include "esphome/core/gpio.h" |  | ||||||
| #include "driver/gpio.h" |  | ||||||
| #include "soc/gpio_num.h" |  | ||||||
|  |  | ||||||
| #ifdef USE_LOGGER | #ifdef USE_LOGGER | ||||||
| #include "esphome/components/logger/logger.h" | #include "esphome/components/logger/logger.h" | ||||||
| @@ -107,13 +104,6 @@ void IDFUARTComponent::load_settings(bool dump_config) { | |||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (this->rx_pin_) { |  | ||||||
|     this->rx_pin_->setup(); |  | ||||||
|   } |  | ||||||
|   if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { |  | ||||||
|     this->tx_pin_->setup(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; |   int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1; | ||||||
|   int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; |   int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1; | ||||||
|   int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; |   int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1; | ||||||
|   | |||||||
| @@ -46,13 +46,6 @@ uint16_t LibreTinyUARTComponent::get_config() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void LibreTinyUARTComponent::setup() { | void LibreTinyUARTComponent::setup() { | ||||||
|   if (this->rx_pin_) { |  | ||||||
|     this->rx_pin_->setup(); |  | ||||||
|   } |  | ||||||
|   if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { |  | ||||||
|     this->tx_pin_->setup(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   int8_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin(); |   int8_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin(); | ||||||
|   int8_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin(); |   int8_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin(); | ||||||
|   bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted(); |   bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted(); | ||||||
|   | |||||||
| @@ -52,13 +52,6 @@ uint16_t RP2040UartComponent::get_config() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void RP2040UartComponent::setup() { | void RP2040UartComponent::setup() { | ||||||
|   if (this->rx_pin_) { |  | ||||||
|     this->rx_pin_->setup(); |  | ||||||
|   } |  | ||||||
|   if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) { |  | ||||||
|     this->tx_pin_->setup(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   uint16_t config = get_config(); |   uint16_t config = get_config(); | ||||||
|  |  | ||||||
|   constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28}); |   constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28}); | ||||||
|   | |||||||
| @@ -17,12 +17,6 @@ UponorSmatrixDevice = uponor_smatrix_ns.class_( | |||||||
|     "UponorSmatrixDevice", cg.Parented.template(UponorSmatrixComponent) |     "UponorSmatrixDevice", cg.Parented.template(UponorSmatrixComponent) | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| device_address = cv.All( |  | ||||||
|     cv.hex_int, |  | ||||||
|     cv.Range(min=0x1000000, max=0xFFFFFFFF, msg="Expected a 32 bit device address"), |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| CONF_UPONOR_SMATRIX_ID = "uponor_smatrix_id" | CONF_UPONOR_SMATRIX_ID = "uponor_smatrix_id" | ||||||
| CONF_TIME_DEVICE_ADDRESS = "time_device_address" | CONF_TIME_DEVICE_ADDRESS = "time_device_address" | ||||||
|  |  | ||||||
| @@ -30,12 +24,9 @@ CONFIG_SCHEMA = ( | |||||||
|     cv.Schema( |     cv.Schema( | ||||||
|         { |         { | ||||||
|             cv.GenerateID(): cv.declare_id(UponorSmatrixComponent), |             cv.GenerateID(): cv.declare_id(UponorSmatrixComponent), | ||||||
|             cv.Optional(CONF_ADDRESS): cv.invalid( |             cv.Optional(CONF_ADDRESS): cv.hex_uint16_t, | ||||||
|                 f"The '{CONF_ADDRESS}' option has been removed. " |  | ||||||
|                 "Use full 32 bit addresses in the device definitions instead." |  | ||||||
|             ), |  | ||||||
|             cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), |             cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), | ||||||
|             cv.Optional(CONF_TIME_DEVICE_ADDRESS): device_address, |             cv.Optional(CONF_TIME_DEVICE_ADDRESS): cv.hex_uint16_t, | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     .extend(cv.COMPONENT_SCHEMA) |     .extend(cv.COMPONENT_SCHEMA) | ||||||
| @@ -56,7 +47,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( | |||||||
| UPONOR_SMATRIX_DEVICE_SCHEMA = cv.Schema( | UPONOR_SMATRIX_DEVICE_SCHEMA = cv.Schema( | ||||||
|     { |     { | ||||||
|         cv.GenerateID(CONF_UPONOR_SMATRIX_ID): cv.use_id(UponorSmatrixComponent), |         cv.GenerateID(CONF_UPONOR_SMATRIX_ID): cv.use_id(UponorSmatrixComponent), | ||||||
|         cv.Required(CONF_ADDRESS): device_address, |         cv.Required(CONF_ADDRESS): cv.hex_uint16_t, | ||||||
|     } |     } | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -67,15 +58,17 @@ async def to_code(config): | |||||||
|     await cg.register_component(var, config) |     await cg.register_component(var, config) | ||||||
|     await uart.register_uart_device(var, config) |     await uart.register_uart_device(var, config) | ||||||
|  |  | ||||||
|  |     if address := config.get(CONF_ADDRESS): | ||||||
|  |         cg.add(var.set_system_address(address)) | ||||||
|     if time_id := config.get(CONF_TIME_ID): |     if time_id := config.get(CONF_TIME_ID): | ||||||
|         time_ = await cg.get_variable(time_id) |         time_ = await cg.get_variable(time_id) | ||||||
|         cg.add(var.set_time_id(time_)) |         cg.add(var.set_time_id(time_)) | ||||||
|         if time_device_address := config.get(CONF_TIME_DEVICE_ADDRESS): |     if time_device_address := config.get(CONF_TIME_DEVICE_ADDRESS): | ||||||
|             cg.add(var.set_time_device_address(time_device_address)) |         cg.add(var.set_time_device_address(time_device_address)) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def register_uponor_smatrix_device(var, config): | async def register_uponor_smatrix_device(var, config): | ||||||
|     parent = await cg.get_variable(config[CONF_UPONOR_SMATRIX_ID]) |     parent = await cg.get_variable(config[CONF_UPONOR_SMATRIX_ID]) | ||||||
|     cg.add(var.set_parent(parent)) |     cg.add(var.set_parent(parent)) | ||||||
|     cg.add(var.set_address(config[CONF_ADDRESS])) |     cg.add(var.set_device_address(config[CONF_ADDRESS])) | ||||||
|     cg.add(parent.register_device(var)) |     cg.add(parent.register_device(var)) | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ static const char *const TAG = "uponor_smatrix.climate"; | |||||||
|  |  | ||||||
| void UponorSmatrixClimate::dump_config() { | void UponorSmatrixClimate::dump_config() { | ||||||
|   LOG_CLIMATE("", "Uponor Smatrix Climate", this); |   LOG_CLIMATE("", "Uponor Smatrix Climate", this); | ||||||
|   ESP_LOGCONFIG(TAG, "  Device address: 0x%08X", this->address_); |   ESP_LOGCONFIG(TAG, "  Device address: 0x%04X", this->address_); | ||||||
| } | } | ||||||
|  |  | ||||||
| void UponorSmatrixClimate::loop() { | void UponorSmatrixClimate::loop() { | ||||||
| @@ -30,9 +30,10 @@ void UponorSmatrixClimate::loop() { | |||||||
|  |  | ||||||
| climate::ClimateTraits UponorSmatrixClimate::traits() { | climate::ClimateTraits UponorSmatrixClimate::traits() { | ||||||
|   auto traits = climate::ClimateTraits(); |   auto traits = climate::ClimateTraits(); | ||||||
|   traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY | |   traits.set_supports_current_temperature(true); | ||||||
|                            climate::CLIMATE_SUPPORTS_ACTION); |   traits.set_supports_current_humidity(true); | ||||||
|   traits.set_supported_modes({climate::CLIMATE_MODE_HEAT}); |   traits.set_supported_modes({climate::CLIMATE_MODE_HEAT}); | ||||||
|  |   traits.set_supports_action(true); | ||||||
|   traits.set_supported_presets({climate::CLIMATE_PRESET_ECO}); |   traits.set_supported_presets({climate::CLIMATE_PRESET_ECO}); | ||||||
|   traits.set_visual_min_temperature(this->min_temperature_); |   traits.set_visual_min_temperature(this->min_temperature_); | ||||||
|   traits.set_visual_max_temperature(this->max_temperature_); |   traits.set_visual_max_temperature(this->max_temperature_); | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ static const char *const TAG = "uponor_smatrix.sensor"; | |||||||
| void UponorSmatrixSensor::dump_config() { | void UponorSmatrixSensor::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, |   ESP_LOGCONFIG(TAG, | ||||||
|                 "Uponor Smatrix Sensor\n" |                 "Uponor Smatrix Sensor\n" | ||||||
|                 "  Device address: 0x%08X", |                 "  Device address: 0x%04X", | ||||||
|                 this->address_); |                 this->address_); | ||||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); |   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||||
|   LOG_SENSOR("  ", "External Temperature", this->external_temperature_sensor_); |   LOG_SENSOR("  ", "External Temperature", this->external_temperature_sensor_); | ||||||
|   | |||||||
| @@ -18,10 +18,11 @@ void UponorSmatrixComponent::setup() { | |||||||
|  |  | ||||||
| void UponorSmatrixComponent::dump_config() { | void UponorSmatrixComponent::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, "Uponor Smatrix"); |   ESP_LOGCONFIG(TAG, "Uponor Smatrix"); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  System address: 0x%04X", this->address_); | ||||||
| #ifdef USE_TIME | #ifdef USE_TIME | ||||||
|   if (this->time_id_ != nullptr) { |   if (this->time_id_ != nullptr) { | ||||||
|     ESP_LOGCONFIG(TAG, "  Time synchronization: YES"); |     ESP_LOGCONFIG(TAG, "  Time synchronization: YES"); | ||||||
|     ESP_LOGCONFIG(TAG, "  Time master device address: 0x%08X", this->time_device_address_); |     ESP_LOGCONFIG(TAG, "  Time master device address: 0x%04X", this->time_device_address_); | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| @@ -30,7 +31,7 @@ void UponorSmatrixComponent::dump_config() { | |||||||
|   if (!this->unknown_devices_.empty()) { |   if (!this->unknown_devices_.empty()) { | ||||||
|     ESP_LOGCONFIG(TAG, "  Detected unknown device addresses:"); |     ESP_LOGCONFIG(TAG, "  Detected unknown device addresses:"); | ||||||
|     for (auto device_address : this->unknown_devices_) { |     for (auto device_address : this->unknown_devices_) { | ||||||
|       ESP_LOGCONFIG(TAG, "    0x%08X", device_address); |       ESP_LOGCONFIG(TAG, "    0x%04X", device_address); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -88,7 +89,8 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { | |||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   uint32_t device_address = encode_uint32(packet[0], packet[1], packet[2], packet[3]); |   uint16_t system_address = encode_uint16(packet[0], packet[1]); | ||||||
|  |   uint16_t device_address = encode_uint16(packet[2], packet[3]); | ||||||
|   uint16_t crc = encode_uint16(packet[packet_len - 1], packet[packet_len - 2]); |   uint16_t crc = encode_uint16(packet[packet_len - 1], packet[packet_len - 2]); | ||||||
|  |  | ||||||
|   uint16_t computed_crc = crc16(packet, packet_len - 2); |   uint16_t computed_crc = crc16(packet, packet_len - 2); | ||||||
| @@ -97,14 +99,24 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { | |||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   ESP_LOGV(TAG, "Received packet: addr=%08X, data=%s, crc=%04X", device_address, |   ESP_LOGV(TAG, "Received packet: sys=%04X, dev=%04X, data=%s, crc=%04X", system_address, device_address, | ||||||
|            format_hex(&packet[4], packet_len - 6).c_str(), crc); |            format_hex(&packet[4], packet_len - 6).c_str(), crc); | ||||||
|  |  | ||||||
|  |   // Detect or check system address | ||||||
|  |   if (this->address_ == 0) { | ||||||
|  |     ESP_LOGI(TAG, "Using detected system address 0x%04X", system_address); | ||||||
|  |     this->address_ = system_address; | ||||||
|  |   } else if (this->address_ != system_address) { | ||||||
|  |     // This should never happen except if the system address was set or detected incorrectly, so warn the user. | ||||||
|  |     ESP_LOGW(TAG, "Received packet from unknown system address 0x%04X", system_address); | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // Handle packet |   // Handle packet | ||||||
|   size_t data_len = (packet_len - 6) / 3; |   size_t data_len = (packet_len - 6) / 3; | ||||||
|   if (data_len == 0) { |   if (data_len == 0) { | ||||||
|     if (packet[4] == UPONOR_ID_REQUEST) |     if (packet[4] == UPONOR_ID_REQUEST) | ||||||
|       ESP_LOGVV(TAG, "Ignoring request packet for device 0x%08X", device_address); |       ESP_LOGVV(TAG, "Ignoring request packet for device 0x%04X", device_address); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -129,7 +141,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { | |||||||
|       if (data[i].id == UPONOR_ID_DATETIME1) |       if (data[i].id == UPONOR_ID_DATETIME1) | ||||||
|         found_time = true; |         found_time = true; | ||||||
|       if (found_temperature && found_time) { |       if (found_temperature && found_time) { | ||||||
|         ESP_LOGI(TAG, "Using detected time device address 0x%08X", device_address); |         ESP_LOGI(TAG, "Using detected time device address 0x%04X", device_address); | ||||||
|         this->time_device_address_ = device_address; |         this->time_device_address_ = device_address; | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
| @@ -148,7 +160,7 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { | |||||||
|  |  | ||||||
|   // Log unknown device addresses |   // Log unknown device addresses | ||||||
|   if (!found && !this->unknown_devices_.count(device_address)) { |   if (!found && !this->unknown_devices_.count(device_address)) { | ||||||
|     ESP_LOGI(TAG, "Received packet for unknown device address 0x%08X ", device_address); |     ESP_LOGI(TAG, "Received packet for unknown device address 0x%04X ", device_address); | ||||||
|     this->unknown_devices_.insert(device_address); |     this->unknown_devices_.insert(device_address); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -156,16 +168,16 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { | |||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
|  |  | ||||||
| bool UponorSmatrixComponent::send(uint32_t device_address, const UponorSmatrixData *data, size_t data_len) { | bool UponorSmatrixComponent::send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len) { | ||||||
|   if (device_address == 0 || data == nullptr || data_len == 0) |   if (this->address_ == 0 || device_address == 0 || data == nullptr || data_len == 0) | ||||||
|     return false; |     return false; | ||||||
|  |  | ||||||
|   // Assemble packet for send queue. All fields are big-endian except for the little-endian checksum. |   // Assemble packet for send queue. All fields are big-endian except for the little-endian checksum. | ||||||
|   std::vector<uint8_t> packet; |   std::vector<uint8_t> packet; | ||||||
|   packet.reserve(6 + 3 * data_len); |   packet.reserve(6 + 3 * data_len); | ||||||
|  |  | ||||||
|   packet.push_back(device_address >> 24); |   packet.push_back(this->address_ >> 8); | ||||||
|   packet.push_back(device_address >> 16); |   packet.push_back(this->address_ >> 0); | ||||||
|   packet.push_back(device_address >> 8); |   packet.push_back(device_address >> 8); | ||||||
|   packet.push_back(device_address >> 0); |   packet.push_back(device_address >> 0); | ||||||
|  |  | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user