mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	| @@ -9,7 +9,7 @@ This document provides essential context for AI models interacting with this pro | |||||||
|  |  | ||||||
| ## 2. Core Technologies & Stack | ## 2. Core Technologies & Stack | ||||||
|  |  | ||||||
| *   **Languages:** Python (>=3.10), C++ (gnu++20) | *   **Languages:** Python (>=3.11), C++ (gnu++20) | ||||||
| *   **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF. | *   **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF. | ||||||
| *   **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative. | *   **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative. | ||||||
| *   **Configuration:** YAML. | *   **Configuration:** YAML. | ||||||
| @@ -38,7 +38,7 @@ This document provides essential context for AI models interacting with this pro | |||||||
|     5.  **Dashboard** (`esphome/dashboard/`): A web-based interface for device configuration, management, and OTA updates. |     5.  **Dashboard** (`esphome/dashboard/`): A web-based interface for device configuration, management, and OTA updates. | ||||||
|  |  | ||||||
| *   **Platform Support:** | *   **Platform Support:** | ||||||
|     1.  **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (S2, S3, C3, etc.) and both IDF and Arduino frameworks. |     1.  **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (Original, C2, C3, C5, C6, H2, P4, S2, S3) with ESP-IDF framework. Arduino framework supports only a subset of the variants (Original, C3, S2, S3). | ||||||
|     2.  **ESP8266** (`components/esp8266/`): Espressif ESP8266. Arduino framework only, with memory constraints. |     2.  **ESP8266** (`components/esp8266/`): Espressif ESP8266. Arduino framework only, with memory constraints. | ||||||
|     3.  **RP2040** (`components/rp2040/`): Raspberry Pi Pico/RP2040. Arduino framework with PIO (Programmable I/O) support. |     3.  **RP2040** (`components/rp2040/`): Raspberry Pi Pico/RP2040. Arduino framework with PIO (Programmable I/O) support. | ||||||
|     4.  **LibreTiny** (`components/libretiny/`): Realtek and Beken chips. Supports multiple chip families and auto-generated components. |     4.  **LibreTiny** (`components/libretiny/`): Realtek and Beken chips. Supports multiple chip families and auto-generated components. | ||||||
| @@ -60,7 +60,7 @@ This document provides essential context for AI models interacting with this pro | |||||||
|         ├── __init__.py          # Component configuration schema and code generation |         ├── __init__.py          # Component configuration schema and code generation | ||||||
|         ├── [component].h        # C++ header file (if needed) |         ├── [component].h        # C++ header file (if needed) | ||||||
|         ├── [component].cpp      # C++ implementation (if needed) |         ├── [component].cpp      # C++ implementation (if needed) | ||||||
|         └── [platform]/         # Platform-specific implementations |         └── [platform]/          # Platform-specific implementations | ||||||
|             ├── __init__.py      # Platform-specific configuration |             ├── __init__.py      # Platform-specific configuration | ||||||
|             ├── [platform].h     # Platform C++ header |             ├── [platform].h     # Platform C++ header | ||||||
|             └── [platform].cpp   # Platform C++ implementation |             └── [platform].cpp   # Platform C++ implementation | ||||||
| @@ -150,7 +150,8 @@ This document provides essential context for AI models interacting with this pro | |||||||
| *   **Configuration Validation:** | *   **Configuration Validation:** | ||||||
|     *   **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`. |     *   **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`. | ||||||
|     *   **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`. |     *   **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`. | ||||||
|     *   **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `cv.only_with_arduino`. |     *   **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `esp32.only_on_variant(...)`, `cv.only_on_esp32`, `cv.only_on_esp8266`, `cv.only_on_rp2040`. | ||||||
|  |     *   **Framework-Specific:** `cv.only_with_framework(...)`, `cv.only_with_arduino`, `cv.only_with_esp_idf`. | ||||||
|     *   **Schema Extensions:** |     *   **Schema Extensions:** | ||||||
|         ```python |         ```python | ||||||
|         CONFIG_SCHEMA = cv.Schema({ ... }) |         CONFIG_SCHEMA = cv.Schema({ ... }) | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| 6af8b429b94191fe8e239fcb3b73f7982d0266cb5b05ffbc81edaeac1bc8c273 | 4368db58e8f884aff245996b1e8b644cc0796c0bb2fa706d5740d40b823d3ac9 | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ runs: | |||||||
|   steps: |   steps: | ||||||
|     - name: Set up Python ${{ inputs.python-version }} |     - name: Set up Python ${{ inputs.python-version }} | ||||||
|       id: python |       id: python | ||||||
|       uses: actions/setup-python@v5.6.0 |       uses: actions/setup-python@v6.0.0 | ||||||
|       with: |       with: | ||||||
|         python-version: ${{ inputs.python-version }} |         python-version: ${{ inputs.python-version }} | ||||||
|     - name: Restore Python virtual environment |     - name: Restore Python virtual environment | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										32
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -32,7 +32,7 @@ jobs: | |||||||
|           private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} |           private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} | ||||||
|  |  | ||||||
|       - name: Auto Label PR |       - name: Auto Label PR | ||||||
|         uses: actions/github-script@v7.0.1 |         uses: actions/github-script@v8.0.0 | ||||||
|         with: |         with: | ||||||
|           github-token: ${{ steps.generate-token.outputs.token }} |           github-token: ${{ steps.generate-token.outputs.token }} | ||||||
|           script: | |           script: | | ||||||
| @@ -105,7 +105,9 @@ jobs: | |||||||
|  |  | ||||||
|             // Calculate data from PR files |             // Calculate data from PR files | ||||||
|             const changedFiles = prFiles.map(file => file.filename); |             const changedFiles = prFiles.map(file => file.filename); | ||||||
|             const totalChanges = prFiles.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); |             const totalAdditions = prFiles.reduce((sum, file) => sum + (file.additions || 0), 0); | ||||||
|  |             const totalDeletions = prFiles.reduce((sum, file) => sum + (file.deletions || 0), 0); | ||||||
|  |             const totalChanges = totalAdditions + totalDeletions; | ||||||
|  |  | ||||||
|             console.log('Current labels:', currentLabels.join(', ')); |             console.log('Current labels:', currentLabels.join(', ')); | ||||||
|             console.log('Changed files:', changedFiles.length); |             console.log('Changed files:', changedFiles.length); | ||||||
| @@ -231,16 +233,21 @@ jobs: | |||||||
|             // Strategy: PR size detection |             // Strategy: PR size detection | ||||||
|             async function detectPRSize() { |             async function detectPRSize() { | ||||||
|               const labels = new Set(); |               const labels = new Set(); | ||||||
|               const testChanges = prFiles |  | ||||||
|                 .filter(file => file.filename.startsWith('tests/')) |  | ||||||
|                 .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); |  | ||||||
|  |  | ||||||
|               const nonTestChanges = totalChanges - testChanges; |  | ||||||
|  |  | ||||||
|               if (totalChanges <= SMALL_PR_THRESHOLD) { |               if (totalChanges <= SMALL_PR_THRESHOLD) { | ||||||
|                 labels.add('small-pr'); |                 labels.add('small-pr'); | ||||||
|  |                 return labels; | ||||||
|               } |               } | ||||||
|  |  | ||||||
|  |               const testAdditions = prFiles | ||||||
|  |                 .filter(file => file.filename.startsWith('tests/')) | ||||||
|  |                 .reduce((sum, file) => sum + (file.additions || 0), 0); | ||||||
|  |               const testDeletions = prFiles | ||||||
|  |                 .filter(file => file.filename.startsWith('tests/')) | ||||||
|  |                 .reduce((sum, file) => sum + (file.deletions || 0), 0); | ||||||
|  |  | ||||||
|  |               const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions); | ||||||
|  |  | ||||||
|               // Don't add too-big if mega-pr label is already present |               // Don't add too-big if mega-pr label is already present | ||||||
|               if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) { |               if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) { | ||||||
|                 labels.add('too-big'); |                 labels.add('too-big'); | ||||||
| @@ -375,7 +382,7 @@ jobs: | |||||||
|               const labels = new Set(); |               const labels = new Set(); | ||||||
|  |  | ||||||
|               // Check for missing tests |               // Check for missing tests | ||||||
|               if ((allLabels.has('new-component') || allLabels.has('new-platform')) && !allLabels.has('has-tests')) { |               if ((allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) && !allLabels.has('has-tests')) { | ||||||
|                 labels.add('needs-tests'); |                 labels.add('needs-tests'); | ||||||
|               } |               } | ||||||
|  |  | ||||||
| @@ -412,10 +419,13 @@ jobs: | |||||||
|  |  | ||||||
|               // Too big message |               // Too big message | ||||||
|               if (finalLabels.includes('too-big')) { |               if (finalLabels.includes('too-big')) { | ||||||
|                 const testChanges = prFiles |                 const testAdditions = prFiles | ||||||
|                   .filter(file => file.filename.startsWith('tests/')) |                   .filter(file => file.filename.startsWith('tests/')) | ||||||
|                   .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); |                   .reduce((sum, file) => sum + (file.additions || 0), 0); | ||||||
|                 const nonTestChanges = totalChanges - testChanges; |                 const testDeletions = prFiles | ||||||
|  |                   .filter(file => file.filename.startsWith('tests/')) | ||||||
|  |                   .reduce((sum, file) => sum + (file.deletions || 0), 0); | ||||||
|  |                 const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions); | ||||||
|  |  | ||||||
|                 const tooManyLabels = finalLabels.length > MAX_LABELS; |                 const tooManyLabels = finalLabels.length > MAX_LABELS; | ||||||
|                 const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD; |                 const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD; | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							| @@ -23,7 +23,7 @@ jobs: | |||||||
|       - name: Checkout |       - name: Checkout | ||||||
|         uses: actions/checkout@v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|       - name: Set up Python |       - name: Set up Python | ||||||
|         uses: actions/setup-python@v5.6.0 |         uses: actions/setup-python@v6.0.0 | ||||||
|         with: |         with: | ||||||
|           python-version: "3.11" |           python-version: "3.11" | ||||||
|  |  | ||||||
| @@ -47,7 +47,7 @@ jobs: | |||||||
|           fi |           fi | ||||||
|       - if: failure() |       - if: failure() | ||||||
|         name: Review PR |         name: Review PR | ||||||
|         uses: actions/github-script@v7.0.1 |         uses: actions/github-script@v8.0.0 | ||||||
|         with: |         with: | ||||||
|           script: | |           script: | | ||||||
|             await github.rest.pulls.createReview({ |             await github.rest.pulls.createReview({ | ||||||
| @@ -70,7 +70,7 @@ jobs: | |||||||
|             esphome/components/api/api_pb2_service.* |             esphome/components/api/api_pb2_service.* | ||||||
|       - if: success() |       - if: success() | ||||||
|         name: Dismiss review |         name: Dismiss review | ||||||
|         uses: actions/github-script@v7.0.1 |         uses: actions/github-script@v8.0.0 | ||||||
|         with: |         with: | ||||||
|           script: | |           script: | | ||||||
|             let reviews = await github.rest.pulls.listReviews({ |             let reviews = await github.rest.pulls.listReviews({ | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								.github/workflows/ci-clang-tidy-hash.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/ci-clang-tidy-hash.yml
									
									
									
									
										vendored
									
									
								
							| @@ -23,7 +23,7 @@ jobs: | |||||||
|         uses: actions/checkout@v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|  |  | ||||||
|       - name: Set up Python |       - name: Set up Python | ||||||
|         uses: actions/setup-python@v5.6.0 |         uses: actions/setup-python@v6.0.0 | ||||||
|         with: |         with: | ||||||
|           python-version: "3.11" |           python-version: "3.11" | ||||||
|  |  | ||||||
| @@ -41,7 +41,7 @@ jobs: | |||||||
|  |  | ||||||
|       - if: failure() |       - if: failure() | ||||||
|         name: Request changes |         name: Request changes | ||||||
|         uses: actions/github-script@v7.0.1 |         uses: actions/github-script@v8.0.0 | ||||||
|         with: |         with: | ||||||
|           script: | |           script: | | ||||||
|             await github.rest.pulls.createReview({ |             await github.rest.pulls.createReview({ | ||||||
| @@ -54,7 +54,7 @@ jobs: | |||||||
|  |  | ||||||
|       - if: success() |       - if: success() | ||||||
|         name: Dismiss review |         name: Dismiss review | ||||||
|         uses: actions/github-script@v7.0.1 |         uses: actions/github-script@v8.0.0 | ||||||
|         with: |         with: | ||||||
|           script: | |           script: | | ||||||
|             let reviews = await github.rest.pulls.listReviews({ |             let reviews = await github.rest.pulls.listReviews({ | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							| @@ -45,7 +45,7 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v5.0.0 |       - uses: actions/checkout@v5.0.0 | ||||||
|       - name: Set up Python |       - name: Set up Python | ||||||
|         uses: actions/setup-python@v5.6.0 |         uses: actions/setup-python@v6.0.0 | ||||||
|         with: |         with: | ||||||
|           python-version: "3.11" |           python-version: "3.11" | ||||||
|       - name: Set up Docker Buildx |       - name: Set up Docker Buildx | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -42,7 +42,7 @@ jobs: | |||||||
|         run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT |         run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT | ||||||
|       - name: Set up Python ${{ env.DEFAULT_PYTHON }} |       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||||
|         id: python |         id: python | ||||||
|         uses: actions/setup-python@v5.6.0 |         uses: actions/setup-python@v6.0.0 | ||||||
|         with: |         with: | ||||||
|           python-version: ${{ env.DEFAULT_PYTHON }} |           python-version: ${{ env.DEFAULT_PYTHON }} | ||||||
|       - name: Restore Python virtual environment |       - name: Restore Python virtual environment | ||||||
| @@ -156,7 +156,7 @@ jobs: | |||||||
|           . venv/bin/activate |           . venv/bin/activate | ||||||
|           pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ |           pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ | ||||||
|       - name: Upload coverage to Codecov |       - name: Upload coverage to Codecov | ||||||
|         uses: codecov/codecov-action@v5.4.3 |         uses: codecov/codecov-action@v5.5.1 | ||||||
|         with: |         with: | ||||||
|           token: ${{ secrets.CODECOV_TOKEN }} |           token: ${{ secrets.CODECOV_TOKEN }} | ||||||
|       - name: Save Python virtual environment cache |       - name: Save Python virtual environment cache | ||||||
| @@ -217,7 +217,7 @@ jobs: | |||||||
|         uses: actions/checkout@v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|       - name: Set up Python 3.13 |       - name: Set up Python 3.13 | ||||||
|         id: python |         id: python | ||||||
|         uses: actions/setup-python@v5.6.0 |         uses: actions/setup-python@v6.0.0 | ||||||
|         with: |         with: | ||||||
|           python-version: "3.13" |           python-version: "3.13" | ||||||
|       - name: Restore Python virtual environment |       - name: Restore Python virtual environment | ||||||
|   | |||||||
| @@ -25,7 +25,7 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Request reviews from component codeowners |       - name: Request reviews from component codeowners | ||||||
|         uses: actions/github-script@v7.0.1 |         uses: actions/github-script@v8.0.0 | ||||||
|         with: |         with: | ||||||
|           script: | |           script: | | ||||||
|             const owner = context.repo.owner; |             const owner = context.repo.owner; | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/external-component-bot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/external-component-bot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -15,7 +15,7 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Add external component comment |       - name: Add external component comment | ||||||
|         uses: actions/github-script@v7.0.1 |         uses: actions/github-script@v8.0.0 | ||||||
|         with: |         with: | ||||||
|           github-token: ${{ secrets.GITHUB_TOKEN }} |           github-token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|           script: | |           script: | | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/issue-codeowner-notify.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/issue-codeowner-notify.yml
									
									
									
									
										vendored
									
									
								
							| @@ -19,7 +19,7 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Notify codeowners for component issues |       - name: Notify codeowners for component issues | ||||||
|         uses: actions/github-script@v7.0.1 |         uses: actions/github-script@v8.0.0 | ||||||
|         with: |         with: | ||||||
|           script: | |           script: | | ||||||
|             const owner = context.repo.owner; |             const owner = context.repo.owner; | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								.github/workflows/needs-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								.github/workflows/needs-docs.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,24 +0,0 @@ | |||||||
| name: Needs Docs |  | ||||||
|  |  | ||||||
| on: |  | ||||||
|   pull_request: |  | ||||||
|     types: [labeled, unlabeled] |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   check: |  | ||||||
|     name: Check |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - name: Check for needs-docs label |  | ||||||
|         uses: actions/github-script@v7.0.1 |  | ||||||
|         with: |  | ||||||
|           script: | |  | ||||||
|             const { data: labels } = await github.rest.issues.listLabelsOnIssue({ |  | ||||||
|               owner: context.repo.owner, |  | ||||||
|               repo: context.repo.repo, |  | ||||||
|               issue_number: context.issue.number |  | ||||||
|             }); |  | ||||||
|             const needsDocs = labels.find(label => label.name === 'needs-docs'); |  | ||||||
|             if (needsDocs) { |  | ||||||
|               core.setFailed('Pull request needs docs'); |  | ||||||
|             } |  | ||||||
							
								
								
									
										10
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -62,7 +62,7 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v5.0.0 |       - uses: actions/checkout@v5.0.0 | ||||||
|       - name: Set up Python |       - name: Set up Python | ||||||
|         uses: actions/setup-python@v5.6.0 |         uses: actions/setup-python@v6.0.0 | ||||||
|         with: |         with: | ||||||
|           python-version: "3.x" |           python-version: "3.x" | ||||||
|       - name: Build |       - name: Build | ||||||
| @@ -70,7 +70,7 @@ jobs: | |||||||
|           pip3 install build |           pip3 install build | ||||||
|           python3 -m build |           python3 -m build | ||||||
|       - name: Publish |       - name: Publish | ||||||
|         uses: pypa/gh-action-pypi-publish@v1.12.4 |         uses: pypa/gh-action-pypi-publish@v1.13.0 | ||||||
|         with: |         with: | ||||||
|           skip-existing: true |           skip-existing: true | ||||||
|  |  | ||||||
| @@ -94,7 +94,7 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v5.0.0 |       - uses: actions/checkout@v5.0.0 | ||||||
|       - name: Set up Python |       - name: Set up Python | ||||||
|         uses: actions/setup-python@v5.6.0 |         uses: actions/setup-python@v6.0.0 | ||||||
|         with: |         with: | ||||||
|           python-version: "3.11" |           python-version: "3.11" | ||||||
|  |  | ||||||
| @@ -220,7 +220,7 @@ jobs: | |||||||
|       - deploy-manifest |       - deploy-manifest | ||||||
|     steps: |     steps: | ||||||
|       - name: Trigger Workflow |       - name: Trigger Workflow | ||||||
|         uses: actions/github-script@v7.0.1 |         uses: actions/github-script@v8.0.0 | ||||||
|         with: |         with: | ||||||
|           github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} |           github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} | ||||||
|           script: | |           script: | | ||||||
| @@ -246,7 +246,7 @@ jobs: | |||||||
|     environment: ${{ needs.init.outputs.deploy_env }} |     environment: ${{ needs.init.outputs.deploy_env }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Trigger Workflow |       - name: Trigger Workflow | ||||||
|         uses: actions/github-script@v7.0.1 |         uses: actions/github-script@v8.0.0 | ||||||
|         with: |         with: | ||||||
|           github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }} |           github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }} | ||||||
|           script: | |           script: | | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ jobs: | |||||||
|   stale: |   stale: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/stale@v9.1.0 |       - uses: actions/stale@v10.0.0 | ||||||
|         with: |         with: | ||||||
|           days-before-pr-stale: 90 |           days-before-pr-stale: 90 | ||||||
|           days-before-pr-close: 7 |           days-before-pr-close: 7 | ||||||
| @@ -37,7 +37,7 @@ jobs: | |||||||
|   close-issues: |   close-issues: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/stale@v9.1.0 |       - uses: actions/stale@v10.0.0 | ||||||
|         with: |         with: | ||||||
|           days-before-pr-stale: -1 |           days-before-pr-stale: -1 | ||||||
|           days-before-pr-close: -1 |           days-before-pr-close: -1 | ||||||
|   | |||||||
							
								
								
									
										30
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | name: Status check labels | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   pull_request: | ||||||
|  |     types: [labeled, unlabeled] | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   check: | ||||||
|  |     name: Check ${{ matrix.label }} | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         label: | ||||||
|  |           - needs-docs | ||||||
|  |           - merge-after-release | ||||||
|  |     steps: | ||||||
|  |       - name: Check for ${{ matrix.label }} label | ||||||
|  |         uses: actions/github-script@v8.0.0 | ||||||
|  |         with: | ||||||
|  |           script: | | ||||||
|  |             const { data: labels } = await github.rest.issues.listLabelsOnIssue({ | ||||||
|  |               owner: context.repo.owner, | ||||||
|  |               repo: context.repo.repo, | ||||||
|  |               issue_number: context.issue.number | ||||||
|  |             }); | ||||||
|  |             const hasLabel = labels.find(label => label.name === '${{ matrix.label }}'); | ||||||
|  |             if (hasLabel) { | ||||||
|  |               core.setFailed('Pull request cannot be merged, it is labeled as ${{ matrix.label }}'); | ||||||
|  |             } | ||||||
							
								
								
									
										2
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							| @@ -22,7 +22,7 @@ jobs: | |||||||
|           path: lib/home-assistant |           path: lib/home-assistant | ||||||
|  |  | ||||||
|       - name: Setup Python |       - name: Setup Python | ||||||
|         uses: actions/setup-python@v5.6.0 |         uses: actions/setup-python@v6.0.0 | ||||||
|         with: |         with: | ||||||
|           python-version: 3.13 |           python-version: 3.13 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ ci: | |||||||
| repos: | repos: | ||||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit |   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||||
|     # Ruff version. |     # Ruff version. | ||||||
|     rev: v0.12.8 |     rev: v0.12.12 | ||||||
|     hooks: |     hooks: | ||||||
|       # Run the linter. |       # Run the linter. | ||||||
|       - id: ruff |       - id: ruff | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								CODEOWNERS
									
									
									
									
									
								
							| @@ -66,7 +66,7 @@ esphome/components/binary_sensor/* @esphome/core | |||||||
| esphome/components/bk72xx/* @kuba2k2 | esphome/components/bk72xx/* @kuba2k2 | ||||||
| esphome/components/bl0906/* @athom-tech @jesserockz @tarontop | esphome/components/bl0906/* @athom-tech @jesserockz @tarontop | ||||||
| esphome/components/bl0939/* @ziceva | esphome/components/bl0939/* @ziceva | ||||||
| esphome/components/bl0940/* @tobias- | esphome/components/bl0940/* @dan-s-github @tobias- | ||||||
| esphome/components/bl0942/* @dbuezas @dwmw2 | esphome/components/bl0942/* @dbuezas @dwmw2 | ||||||
| esphome/components/ble_client/* @buxtronix @clydebarrow | esphome/components/ble_client/* @buxtronix @clydebarrow | ||||||
| esphome/components/bluetooth_proxy/* @bdraco @jesserockz | esphome/components/bluetooth_proxy/* @bdraco @jesserockz | ||||||
| @@ -88,7 +88,8 @@ esphome/components/bp1658cj/* @Cossid | |||||||
| esphome/components/bp5758d/* @Cossid | esphome/components/bp5758d/* @Cossid | ||||||
| esphome/components/button/* @esphome/core | esphome/components/button/* @esphome/core | ||||||
| esphome/components/bytebuffer/* @clydebarrow | esphome/components/bytebuffer/* @clydebarrow | ||||||
| esphome/components/camera/* @DT-art1 @bdraco | esphome/components/camera/* @bdraco @DT-art1 | ||||||
|  | esphome/components/camera_encoder/* @DT-art1 | ||||||
| esphome/components/canbus/* @danielschramm @mvturnho | esphome/components/canbus/* @danielschramm @mvturnho | ||||||
| esphome/components/cap1188/* @mreditor97 | esphome/components/cap1188/* @mreditor97 | ||||||
| esphome/components/captive_portal/* @esphome/core | esphome/components/captive_portal/* @esphome/core | ||||||
| @@ -144,9 +145,9 @@ esphome/components/es8156/* @kbx81 | |||||||
| esphome/components/es8311/* @kahrendt @kroimon | esphome/components/es8311/* @kahrendt @kroimon | ||||||
| esphome/components/es8388/* @P4uLT | esphome/components/es8388/* @P4uLT | ||||||
| esphome/components/esp32/* @esphome/core | esphome/components/esp32/* @esphome/core | ||||||
| esphome/components/esp32_ble/* @Rapsssito @bdraco @jesserockz | esphome/components/esp32_ble/* @bdraco @jesserockz @Rapsssito | ||||||
| esphome/components/esp32_ble_client/* @bdraco @jesserockz | esphome/components/esp32_ble_client/* @bdraco @jesserockz | ||||||
| esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz | esphome/components/esp32_ble_server/* @clydebarrow @jesserockz @Rapsssito | ||||||
| esphome/components/esp32_ble_tracker/* @bdraco | esphome/components/esp32_ble_tracker/* @bdraco | ||||||
| esphome/components/esp32_camera_web_server/* @ayufan | esphome/components/esp32_camera_web_server/* @ayufan | ||||||
| esphome/components/esp32_can/* @Sympatron | esphome/components/esp32_can/* @Sympatron | ||||||
| @@ -166,7 +167,7 @@ esphome/components/ezo_pmp/* @carlos-sarmiento | |||||||
| esphome/components/factory_reset/* @anatoly-savchenkov | esphome/components/factory_reset/* @anatoly-savchenkov | ||||||
| esphome/components/fastled_base/* @OttoWinter | esphome/components/fastled_base/* @OttoWinter | ||||||
| esphome/components/feedback/* @ianchi | esphome/components/feedback/* @ianchi | ||||||
| esphome/components/fingerprint_grow/* @OnFreund @alexborro @loongyh | esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund | ||||||
| esphome/components/font/* @clydebarrow @esphome/core | esphome/components/font/* @clydebarrow @esphome/core | ||||||
| esphome/components/fs3000/* @kahrendt | esphome/components/fs3000/* @kahrendt | ||||||
| esphome/components/ft5x06/* @clydebarrow | esphome/components/ft5x06/* @clydebarrow | ||||||
| @@ -202,7 +203,7 @@ esphome/components/heatpumpir/* @rob-deutsch | |||||||
| esphome/components/hitachi_ac424/* @sourabhjaiswal | esphome/components/hitachi_ac424/* @sourabhjaiswal | ||||||
| esphome/components/hm3301/* @freekode | esphome/components/hm3301/* @freekode | ||||||
| esphome/components/hmac_md5/* @dwmw2 | esphome/components/hmac_md5/* @dwmw2 | ||||||
| esphome/components/homeassistant/* @OttoWinter @esphome/core | esphome/components/homeassistant/* @esphome/core @OttoWinter | ||||||
| esphome/components/homeassistant/number/* @landonr | esphome/components/homeassistant/number/* @landonr | ||||||
| esphome/components/homeassistant/switch/* @Links2004 | esphome/components/homeassistant/switch/* @Links2004 | ||||||
| esphome/components/honeywell_hih_i2c/* @Benichou34 | esphome/components/honeywell_hih_i2c/* @Benichou34 | ||||||
| @@ -227,13 +228,13 @@ esphome/components/iaqcore/* @yozik04 | |||||||
| esphome/components/ili9xxx/* @clydebarrow @nielsnl68 | esphome/components/ili9xxx/* @clydebarrow @nielsnl68 | ||||||
| esphome/components/improv_base/* @esphome/core | esphome/components/improv_base/* @esphome/core | ||||||
| esphome/components/improv_serial/* @esphome/core | esphome/components/improv_serial/* @esphome/core | ||||||
| esphome/components/ina226/* @Sergio303 @latonita | esphome/components/ina226/* @latonita @Sergio303 | ||||||
| esphome/components/ina260/* @mreditor97 | esphome/components/ina260/* @mreditor97 | ||||||
| esphome/components/ina2xx_base/* @latonita | esphome/components/ina2xx_base/* @latonita | ||||||
| esphome/components/ina2xx_i2c/* @latonita | esphome/components/ina2xx_i2c/* @latonita | ||||||
| esphome/components/ina2xx_spi/* @latonita | esphome/components/ina2xx_spi/* @latonita | ||||||
| esphome/components/inkbird_ibsth1_mini/* @fkirill | esphome/components/inkbird_ibsth1_mini/* @fkirill | ||||||
| esphome/components/inkplate6/* @jesserockz | esphome/components/inkplate/* @jesserockz @JosipKuci | ||||||
| esphome/components/integration/* @OttoWinter | esphome/components/integration/* @OttoWinter | ||||||
| esphome/components/internal_temperature/* @Mat931 | esphome/components/internal_temperature/* @Mat931 | ||||||
| esphome/components/interval/* @esphome/core | esphome/components/interval/* @esphome/core | ||||||
| @@ -276,8 +277,8 @@ esphome/components/max7219digit/* @rspaargaren | |||||||
| esphome/components/max9611/* @mckaymatthew | esphome/components/max9611/* @mckaymatthew | ||||||
| esphome/components/mcp23008/* @jesserockz | esphome/components/mcp23008/* @jesserockz | ||||||
| esphome/components/mcp23017/* @jesserockz | esphome/components/mcp23017/* @jesserockz | ||||||
| esphome/components/mcp23s08/* @SenexCrenshaw @jesserockz | esphome/components/mcp23s08/* @jesserockz @SenexCrenshaw | ||||||
| esphome/components/mcp23s17/* @SenexCrenshaw @jesserockz | esphome/components/mcp23s17/* @jesserockz @SenexCrenshaw | ||||||
| esphome/components/mcp23x08_base/* @jesserockz | esphome/components/mcp23x08_base/* @jesserockz | ||||||
| esphome/components/mcp23x17_base/* @jesserockz | esphome/components/mcp23x17_base/* @jesserockz | ||||||
| esphome/components/mcp23xxx_base/* @jesserockz | esphome/components/mcp23xxx_base/* @jesserockz | ||||||
| @@ -298,6 +299,7 @@ esphome/components/mics_4514/* @jesserockz | |||||||
| esphome/components/midea/* @dudanov | esphome/components/midea/* @dudanov | ||||||
| esphome/components/midea_ir/* @dudanov | esphome/components/midea_ir/* @dudanov | ||||||
| esphome/components/mipi_dsi/* @clydebarrow | esphome/components/mipi_dsi/* @clydebarrow | ||||||
|  | esphome/components/mipi_rgb/* @clydebarrow | ||||||
| esphome/components/mipi_spi/* @clydebarrow | esphome/components/mipi_spi/* @clydebarrow | ||||||
| esphome/components/mitsubishi/* @RubyBailey | esphome/components/mitsubishi/* @RubyBailey | ||||||
| esphome/components/mixer/speaker/* @kahrendt | esphome/components/mixer/speaker/* @kahrendt | ||||||
| @@ -341,7 +343,7 @@ esphome/components/ota/* @esphome/core | |||||||
| esphome/components/output/* @esphome/core | esphome/components/output/* @esphome/core | ||||||
| esphome/components/packet_transport/* @clydebarrow | esphome/components/packet_transport/* @clydebarrow | ||||||
| esphome/components/pca6416a/* @Mat931 | esphome/components/pca6416a/* @Mat931 | ||||||
| esphome/components/pca9554/* @clydebarrow @hwstar | esphome/components/pca9554/* @bdraco @clydebarrow @hwstar | ||||||
| esphome/components/pcf85063/* @brogon | esphome/components/pcf85063/* @brogon | ||||||
| esphome/components/pcf8563/* @KoenBreeman | esphome/components/pcf8563/* @KoenBreeman | ||||||
| esphome/components/pi4ioe5v6408/* @jesserockz | esphome/components/pi4ioe5v6408/* @jesserockz | ||||||
| @@ -352,9 +354,9 @@ esphome/components/pm2005/* @andrewjswan | |||||||
| esphome/components/pmsa003i/* @sjtrny | esphome/components/pmsa003i/* @sjtrny | ||||||
| esphome/components/pmsx003/* @ximex | esphome/components/pmsx003/* @ximex | ||||||
| esphome/components/pmwcs3/* @SeByDocKy | esphome/components/pmwcs3/* @SeByDocKy | ||||||
| esphome/components/pn532/* @OttoWinter @jesserockz | esphome/components/pn532/* @jesserockz @OttoWinter | ||||||
| esphome/components/pn532_i2c/* @OttoWinter @jesserockz | esphome/components/pn532_i2c/* @jesserockz @OttoWinter | ||||||
| esphome/components/pn532_spi/* @OttoWinter @jesserockz | esphome/components/pn532_spi/* @jesserockz @OttoWinter | ||||||
| esphome/components/pn7150/* @jesserockz @kbx81 | esphome/components/pn7150/* @jesserockz @kbx81 | ||||||
| esphome/components/pn7150_i2c/* @jesserockz @kbx81 | esphome/components/pn7150_i2c/* @jesserockz @kbx81 | ||||||
| esphome/components/pn7160/* @jesserockz @kbx81 | esphome/components/pn7160/* @jesserockz @kbx81 | ||||||
| @@ -363,7 +365,7 @@ esphome/components/pn7160_spi/* @jesserockz @kbx81 | |||||||
| esphome/components/power_supply/* @esphome/core | esphome/components/power_supply/* @esphome/core | ||||||
| esphome/components/preferences/* @esphome/core | esphome/components/preferences/* @esphome/core | ||||||
| esphome/components/psram/* @esphome/core | esphome/components/psram/* @esphome/core | ||||||
| esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter | esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston | ||||||
| esphome/components/pvvx_mithermometer/* @pasiz | esphome/components/pvvx_mithermometer/* @pasiz | ||||||
| esphome/components/pylontech/* @functionpointer | esphome/components/pylontech/* @functionpointer | ||||||
| esphome/components/qmp6988/* @andrewpc | esphome/components/qmp6988/* @andrewpc | ||||||
| @@ -404,7 +406,7 @@ esphome/components/sensirion_common/* @martgras | |||||||
| esphome/components/sensor/* @esphome/core | esphome/components/sensor/* @esphome/core | ||||||
| esphome/components/sfa30/* @ghsensdev | esphome/components/sfa30/* @ghsensdev | ||||||
| esphome/components/sgp40/* @SenexCrenshaw | esphome/components/sgp40/* @SenexCrenshaw | ||||||
| esphome/components/sgp4x/* @SenexCrenshaw @martgras | esphome/components/sgp4x/* @martgras @SenexCrenshaw | ||||||
| esphome/components/shelly_dimmer/* @edge90 @rnauber | esphome/components/shelly_dimmer/* @edge90 @rnauber | ||||||
| esphome/components/sht3xd/* @mrtoy-me | esphome/components/sht3xd/* @mrtoy-me | ||||||
| esphome/components/sht4x/* @sjtrny | esphome/components/sht4x/* @sjtrny | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							| @@ -48,7 +48,7 @@ PROJECT_NAME           = ESPHome | |||||||
| # could be handy for archiving the generated documentation or if some version | # could be handy for archiving the generated documentation or if some version | ||||||
| # control system is used. | # control system is used. | ||||||
|  |  | ||||||
| PROJECT_NUMBER         = 2025.8.4 | PROJECT_NUMBER         = 2025.9.0 | ||||||
|  |  | ||||||
| # Using the PROJECT_BRIEF tag one can provide an optional one line description | # Using the PROJECT_BRIEF tag one can provide an optional one line description | ||||||
| # for a project that appears at the top of each page and should give viewer a | # for a project that appears at the top of each page and should give viewer a | ||||||
|   | |||||||
| @@ -15,9 +15,11 @@ import argcomplete | |||||||
|  |  | ||||||
| from esphome import const, writer, yaml_util | from esphome import const, writer, yaml_util | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
|  | from esphome.components.mqtt import CONF_DISCOVER_IP | ||||||
| from esphome.config import iter_component_configs, read_config, strip_default_ids | from esphome.config import iter_component_configs, read_config, strip_default_ids | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     ALLOWED_NAME_CHARS, |     ALLOWED_NAME_CHARS, | ||||||
|  |     CONF_API, | ||||||
|     CONF_BAUD_RATE, |     CONF_BAUD_RATE, | ||||||
|     CONF_BROKER, |     CONF_BROKER, | ||||||
|     CONF_DEASSERT_RTS_DTR, |     CONF_DEASSERT_RTS_DTR, | ||||||
| @@ -43,6 +45,7 @@ from esphome.const import ( | |||||||
|     SECRETS_FILES, |     SECRETS_FILES, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, EsphomeError, coroutine | from esphome.core import CORE, EsphomeError, coroutine | ||||||
|  | from esphome.enum import StrEnum | ||||||
| from esphome.helpers import get_bool_env, indent, is_ip_address | from esphome.helpers import get_bool_env, indent, is_ip_address | ||||||
| from esphome.log import AnsiFore, color, setup_log | from esphome.log import AnsiFore, color, setup_log | ||||||
| from esphome.types import ConfigType | from esphome.types import ConfigType | ||||||
| @@ -106,13 +109,15 @@ def choose_prompt(options, purpose: str = None): | |||||||
|     return options[opt - 1][1] |     return options[opt - 1][1] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Purpose(StrEnum): | ||||||
|  |     UPLOADING = "uploading" | ||||||
|  |     LOGGING = "logging" | ||||||
|  |  | ||||||
|  |  | ||||||
| def choose_upload_log_host( | def choose_upload_log_host( | ||||||
|     default: list[str] | str | None, |     default: list[str] | str | None, | ||||||
|     check_default: str | None, |     check_default: str | None, | ||||||
|     show_ota: bool, |     purpose: Purpose, | ||||||
|     show_mqtt: bool, |  | ||||||
|     show_api: bool, |  | ||||||
|     purpose: str | None = None, |  | ||||||
| ) -> list[str]: | ) -> list[str]: | ||||||
|     # Convert to list for uniform handling |     # Convert to list for uniform handling | ||||||
|     defaults = [default] if isinstance(default, str) else default or [] |     defaults = [default] if isinstance(default, str) else default or [] | ||||||
| @@ -132,13 +137,30 @@ def choose_upload_log_host( | |||||||
|                 ] |                 ] | ||||||
|                 resolved.append(choose_prompt(options, purpose=purpose)) |                 resolved.append(choose_prompt(options, purpose=purpose)) | ||||||
|             elif device == "OTA": |             elif device == "OTA": | ||||||
|                 if CORE.address and ( |                 # ensure IP adresses are used first | ||||||
|                     (show_ota and "ota" in CORE.config) |                 if is_ip_address(CORE.address) and ( | ||||||
|                     or (show_api and "api" in CORE.config) |                     (purpose == Purpose.LOGGING and has_api()) | ||||||
|  |                     or (purpose == Purpose.UPLOADING and has_ota()) | ||||||
|                 ): |                 ): | ||||||
|                     resolved.append(CORE.address) |                     resolved.append(CORE.address) | ||||||
|                 elif show_mqtt and has_mqtt_logging(): |  | ||||||
|                     resolved.append("MQTT") |                 if purpose == Purpose.LOGGING: | ||||||
|  |                     if has_api() and has_mqtt_ip_lookup(): | ||||||
|  |                         resolved.append("MQTTIP") | ||||||
|  |  | ||||||
|  |                     if has_mqtt_logging(): | ||||||
|  |                         resolved.append("MQTT") | ||||||
|  |  | ||||||
|  |                     if has_api() and has_non_ip_address(): | ||||||
|  |                         resolved.append(CORE.address) | ||||||
|  |  | ||||||
|  |                 elif purpose == Purpose.UPLOADING: | ||||||
|  |                     if has_ota() and has_mqtt_ip_lookup(): | ||||||
|  |                         resolved.append("MQTTIP") | ||||||
|  |  | ||||||
|  |                     if has_ota() and has_non_ip_address(): | ||||||
|  |                         resolved.append(CORE.address) | ||||||
|  |  | ||||||
|             else: |             else: | ||||||
|                 resolved.append(device) |                 resolved.append(device) | ||||||
|         if not resolved: |         if not resolved: | ||||||
| @@ -149,39 +171,111 @@ def choose_upload_log_host( | |||||||
|     options = [ |     options = [ | ||||||
|         (f"{port.path} ({port.description})", port.path) for port in get_serial_ports() |         (f"{port.path} ({port.description})", port.path) for port in get_serial_ports() | ||||||
|     ] |     ] | ||||||
|     if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config): |  | ||||||
|         options.append((f"Over The Air ({CORE.address})", CORE.address)) |     if purpose == Purpose.LOGGING: | ||||||
|     if show_mqtt and has_mqtt_logging(): |         if has_mqtt_logging(): | ||||||
|         mqtt_config = CORE.config[CONF_MQTT] |             mqtt_config = CORE.config[CONF_MQTT] | ||||||
|         options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT")) |             options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT")) | ||||||
|  |  | ||||||
|  |         if has_api(): | ||||||
|  |             if has_resolvable_address(): | ||||||
|  |                 options.append((f"Over The Air ({CORE.address})", CORE.address)) | ||||||
|  |             if has_mqtt_ip_lookup(): | ||||||
|  |                 options.append(("Over The Air (MQTT IP lookup)", "MQTTIP")) | ||||||
|  |  | ||||||
|  |     elif purpose == Purpose.UPLOADING and has_ota(): | ||||||
|  |         if has_resolvable_address(): | ||||||
|  |             options.append((f"Over The Air ({CORE.address})", CORE.address)) | ||||||
|  |         if has_mqtt_ip_lookup(): | ||||||
|  |             options.append(("Over The Air (MQTT IP lookup)", "MQTTIP")) | ||||||
|  |  | ||||||
|     if check_default is not None and check_default in [opt[1] for opt in options]: |     if check_default is not None and check_default in [opt[1] for opt in options]: | ||||||
|         return [check_default] |         return [check_default] | ||||||
|     return [choose_prompt(options, purpose=purpose)] |     return [choose_prompt(options, purpose=purpose)] | ||||||
|  |  | ||||||
|  |  | ||||||
| def mqtt_logging_enabled(mqtt_config): | def has_mqtt_logging() -> bool: | ||||||
|  |     """Check if MQTT logging is available.""" | ||||||
|  |     if CONF_MQTT not in CORE.config: | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |     mqtt_config = CORE.config[CONF_MQTT] | ||||||
|  |  | ||||||
|  |     # enabled by default | ||||||
|  |     if CONF_LOG_TOPIC not in mqtt_config: | ||||||
|  |         return True | ||||||
|  |  | ||||||
|     log_topic = mqtt_config[CONF_LOG_TOPIC] |     log_topic = mqtt_config[CONF_LOG_TOPIC] | ||||||
|     if log_topic is None: |     if log_topic is None: | ||||||
|         return False |         return False | ||||||
|  |  | ||||||
|     if CONF_TOPIC not in log_topic: |     if CONF_TOPIC not in log_topic: | ||||||
|         return False |         return False | ||||||
|     return log_topic.get(CONF_LEVEL, None) != "NONE" |  | ||||||
|  |     return log_topic[CONF_LEVEL] != "NONE" | ||||||
|  |  | ||||||
|  |  | ||||||
| def has_mqtt_logging() -> bool: | def has_mqtt() -> bool: | ||||||
|     """Check if MQTT logging is available.""" |     """Check if MQTT is available.""" | ||||||
|     return (mqtt_config := CORE.config.get(CONF_MQTT)) and mqtt_logging_enabled( |     return CONF_MQTT in CORE.config | ||||||
|         mqtt_config |  | ||||||
|     ) |  | ||||||
|  | def has_api() -> bool: | ||||||
|  |     """Check if API is available.""" | ||||||
|  |     return CONF_API in CORE.config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def has_ota() -> bool: | ||||||
|  |     """Check if OTA is available.""" | ||||||
|  |     return CONF_OTA in CORE.config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def has_mqtt_ip_lookup() -> bool: | ||||||
|  |     """Check if MQTT is available and IP lookup is supported.""" | ||||||
|  |     if CONF_MQTT not in CORE.config: | ||||||
|  |         return False | ||||||
|  |     # Default Enabled | ||||||
|  |     if CONF_DISCOVER_IP not in CORE.config[CONF_MQTT]: | ||||||
|  |         return True | ||||||
|  |     return CORE.config[CONF_MQTT][CONF_DISCOVER_IP] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def has_mdns() -> bool: | ||||||
|  |     """Check if MDNS is available.""" | ||||||
|  |     return CONF_MDNS not in CORE.config or not CORE.config[CONF_MDNS][CONF_DISABLED] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def has_non_ip_address() -> bool: | ||||||
|  |     """Check if CORE.address is set and is not an IP address.""" | ||||||
|  |     return CORE.address is not None and not is_ip_address(CORE.address) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def has_ip_address() -> bool: | ||||||
|  |     """Check if CORE.address is a valid IP address.""" | ||||||
|  |     return CORE.address is not None and is_ip_address(CORE.address) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def has_resolvable_address() -> bool: | ||||||
|  |     """Check if CORE.address is resolvable (via mDNS or is an IP address).""" | ||||||
|  |     return has_mdns() or has_ip_address() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str): | ||||||
|  |     from esphome import mqtt | ||||||
|  |  | ||||||
|  |     return mqtt.get_esphome_device_ip(config, username, password, client_id) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _PORT_TO_PORT_TYPE = { | ||||||
|  |     "MQTT": "MQTT", | ||||||
|  |     "MQTTIP": "MQTTIP", | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_port_type(port: str) -> str: | def get_port_type(port: str) -> str: | ||||||
|     if port.startswith("/") or port.startswith("COM"): |     if port.startswith("/") or port.startswith("COM"): | ||||||
|         return "SERIAL" |         return "SERIAL" | ||||||
|     if port == "MQTT": |     return _PORT_TO_PORT_TYPE.get(port, "NETWORK") | ||||||
|         return "MQTT" |  | ||||||
|     return "NETWORK" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def run_miniterm(config: ConfigType, port: str, args) -> int: | def run_miniterm(config: ConfigType, port: str, args) -> int: | ||||||
| @@ -226,7 +320,9 @@ def run_miniterm(config: ConfigType, port: str, args) -> int: | |||||||
|                         .replace(b"\n", b"") |                         .replace(b"\n", b"") | ||||||
|                         .decode("utf8", "backslashreplace") |                         .decode("utf8", "backslashreplace") | ||||||
|                     ) |                     ) | ||||||
|                     time_str = datetime.now().time().strftime("[%H:%M:%S]") |                     time_ = datetime.now() | ||||||
|  |                     nanoseconds = time_.microsecond // 1000 | ||||||
|  |                     time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]" | ||||||
|                     safe_print(parser.parse_line(line, time_str)) |                     safe_print(parser.parse_line(line, time_str)) | ||||||
|  |  | ||||||
|                     backtrace_state = platformio_api.process_stacktrace( |                     backtrace_state = platformio_api.process_stacktrace( | ||||||
| @@ -396,27 +492,29 @@ def check_permissions(port: str): | |||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | str: | def upload_program( | ||||||
|  |     config: ConfigType, args: ArgsProtocol, devices: list[str] | ||||||
|  | ) -> tuple[int, str | None]: | ||||||
|  |     host = devices[0] | ||||||
|     try: |     try: | ||||||
|         module = importlib.import_module("esphome.components." + CORE.target_platform) |         module = importlib.import_module("esphome.components." + CORE.target_platform) | ||||||
|         if getattr(module, "upload_program")(config, args, host): |         if getattr(module, "upload_program")(config, args, host): | ||||||
|             return 0 |             return 0, host | ||||||
|     except AttributeError: |     except AttributeError: | ||||||
|         pass |         pass | ||||||
|  |  | ||||||
|     if get_port_type(host) == "SERIAL": |     if get_port_type(host) == "SERIAL": | ||||||
|         check_permissions(host) |         check_permissions(host) | ||||||
|  |  | ||||||
|  |         exit_code = 1 | ||||||
|         if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266): |         if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266): | ||||||
|             file = getattr(args, "file", None) |             file = getattr(args, "file", None) | ||||||
|             return upload_using_esptool(config, host, file, args.upload_speed) |             exit_code = upload_using_esptool(config, host, file, args.upload_speed) | ||||||
|  |         elif CORE.target_platform == PLATFORM_RP2040 or CORE.is_libretiny: | ||||||
|  |             exit_code = upload_using_platformio(config, host) | ||||||
|  |         # else: Unknown target platform, exit_code remains 1 | ||||||
|  |  | ||||||
|         if CORE.target_platform in (PLATFORM_RP2040): |         return exit_code, host if exit_code == 0 else None | ||||||
|             return upload_using_platformio(config, host) |  | ||||||
|  |  | ||||||
|         if CORE.is_libretiny: |  | ||||||
|             return upload_using_platformio(config, host) |  | ||||||
|  |  | ||||||
|         return 1  # Unknown target platform |  | ||||||
|  |  | ||||||
|     ota_conf = {} |     ota_conf = {} | ||||||
|     for ota_item in config.get(CONF_OTA, []): |     for ota_item in config.get(CONF_OTA, []): | ||||||
| @@ -433,31 +531,23 @@ def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | s | |||||||
|  |  | ||||||
|     remote_port = int(ota_conf[CONF_PORT]) |     remote_port = int(ota_conf[CONF_PORT]) | ||||||
|     password = ota_conf.get(CONF_PASSWORD, "") |     password = ota_conf.get(CONF_PASSWORD, "") | ||||||
|  |     binary = args.file if getattr(args, "file", None) is not None else CORE.firmware_bin | ||||||
|  |  | ||||||
|     # Check if we should use MQTT for address resolution |     # MQTT address resolution | ||||||
|     # This happens when no device was specified, or the current host is "MQTT"/"OTA" |     if get_port_type(host) in ("MQTT", "MQTTIP"): | ||||||
|     devices: list[str] = args.device or [] |         devices = mqtt_get_ip(config, args.username, args.password, args.client_id) | ||||||
|     if ( |  | ||||||
|         CONF_MQTT in config  # pylint: disable=too-many-boolean-expressions |  | ||||||
|         and (not devices or host in ("MQTT", "OTA")) |  | ||||||
|         and ( |  | ||||||
|             ((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address)) |  | ||||||
|             or get_port_type(host) == "MQTT" |  | ||||||
|         ) |  | ||||||
|     ): |  | ||||||
|         from esphome import mqtt |  | ||||||
|  |  | ||||||
|         host = mqtt.get_esphome_device_ip( |     return espota2.run_ota(devices, remote_port, password, binary) | ||||||
|             config, args.username, args.password, args.client_id |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     if getattr(args, "file", None) is not None: |  | ||||||
|         return espota2.run_ota(host, remote_port, password, args.file) |  | ||||||
|  |  | ||||||
|     return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None: | def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None: | ||||||
|  |     try: | ||||||
|  |         module = importlib.import_module("esphome.components." + CORE.target_platform) | ||||||
|  |         if getattr(module, "show_logs")(config, args, devices): | ||||||
|  |             return 0 | ||||||
|  |     except AttributeError: | ||||||
|  |         pass | ||||||
|  |  | ||||||
|     if "logger" not in config: |     if "logger" not in config: | ||||||
|         raise EsphomeError("Logger is not configured!") |         raise EsphomeError("Logger is not configured!") | ||||||
|  |  | ||||||
| @@ -466,20 +556,28 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | |||||||
|     if get_port_type(port) == "SERIAL": |     if get_port_type(port) == "SERIAL": | ||||||
|         check_permissions(port) |         check_permissions(port) | ||||||
|         return run_miniterm(config, port, args) |         return run_miniterm(config, port, args) | ||||||
|     if get_port_type(port) == "NETWORK" and "api" in config: |  | ||||||
|         addresses_to_use = devices |  | ||||||
|         if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config: |  | ||||||
|             from esphome import mqtt |  | ||||||
|  |  | ||||||
|             mqtt_address = mqtt.get_esphome_device_ip( |     port_type = get_port_type(port) | ||||||
|  |  | ||||||
|  |     # Check if we should use API for logging | ||||||
|  |     if has_api(): | ||||||
|  |         addresses_to_use: list[str] | None = None | ||||||
|  |  | ||||||
|  |         if port_type == "NETWORK" and (has_mdns() or is_ip_address(port)): | ||||||
|  |             addresses_to_use = devices | ||||||
|  |         elif port_type in ("NETWORK", "MQTT", "MQTTIP") and has_mqtt_ip_lookup(): | ||||||
|  |             # Only use MQTT IP lookup if the first condition didn't match | ||||||
|  |             # (for MQTT/MQTTIP types, or for NETWORK when mdns/ip check fails) | ||||||
|  |             addresses_to_use = mqtt_get_ip( | ||||||
|                 config, args.username, args.password, args.client_id |                 config, args.username, args.password, args.client_id | ||||||
|             )[0] |             ) | ||||||
|             addresses_to_use = [mqtt_address] |  | ||||||
|  |  | ||||||
|         from esphome.components.api.client import run_logs |         if addresses_to_use is not None: | ||||||
|  |             from esphome.components.api.client import run_logs | ||||||
|  |  | ||||||
|         return run_logs(config, addresses_to_use) |             return run_logs(config, addresses_to_use) | ||||||
|     if get_port_type(port) in ("NETWORK", "MQTT") and "mqtt" in config: |  | ||||||
|  |     if port_type in ("NETWORK", "MQTT") and has_mqtt_logging(): | ||||||
|         from esphome import mqtt |         from esphome import mqtt | ||||||
|  |  | ||||||
|         return mqtt.show_logs( |         return mqtt.show_logs( | ||||||
| @@ -545,23 +643,14 @@ def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None: | |||||||
|     devices = choose_upload_log_host( |     devices = choose_upload_log_host( | ||||||
|         default=args.device, |         default=args.device, | ||||||
|         check_default=None, |         check_default=None, | ||||||
|         show_ota=True, |         purpose=Purpose.UPLOADING, | ||||||
|         show_mqtt=False, |  | ||||||
|         show_api=False, |  | ||||||
|         purpose="uploading", |  | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     # Try each device until one succeeds |     exit_code, _ = upload_program(config, args, devices) | ||||||
|     exit_code = 1 |     if exit_code == 0: | ||||||
|     for device in devices: |         _LOGGER.info("Successfully uploaded program.") | ||||||
|         _LOGGER.info("Uploading to %s", device) |     else: | ||||||
|         exit_code = upload_program(config, args, device) |         _LOGGER.warning("Failed to upload to %s", devices) | ||||||
|         if exit_code == 0: |  | ||||||
|             _LOGGER.info("Successfully uploaded program.") |  | ||||||
|             return 0 |  | ||||||
|         if len(devices) > 1: |  | ||||||
|             _LOGGER.warning("Failed to upload to %s", device) |  | ||||||
|  |  | ||||||
|     return exit_code |     return exit_code | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -579,10 +668,7 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None: | |||||||
|     devices = choose_upload_log_host( |     devices = choose_upload_log_host( | ||||||
|         default=args.device, |         default=args.device, | ||||||
|         check_default=None, |         check_default=None, | ||||||
|         show_ota=False, |         purpose=Purpose.LOGGING, | ||||||
|         show_mqtt=True, |  | ||||||
|         show_api=True, |  | ||||||
|         purpose="logging", |  | ||||||
|     ) |     ) | ||||||
|     return show_logs(config, args, devices) |     return show_logs(config, args, devices) | ||||||
|  |  | ||||||
| @@ -608,25 +694,14 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: | |||||||
|     devices = choose_upload_log_host( |     devices = choose_upload_log_host( | ||||||
|         default=args.device, |         default=args.device, | ||||||
|         check_default=None, |         check_default=None, | ||||||
|         show_ota=True, |         purpose=Purpose.UPLOADING, | ||||||
|         show_mqtt=False, |  | ||||||
|         show_api=True, |  | ||||||
|         purpose="uploading", |  | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     # Try each device for upload until one succeeds |     exit_code, successful_device = upload_program(config, args, devices) | ||||||
|     successful_device: str | None = None |     if exit_code == 0: | ||||||
|     for device in devices: |         _LOGGER.info("Successfully uploaded program.") | ||||||
|         _LOGGER.info("Uploading to %s", device) |     else: | ||||||
|         exit_code = upload_program(config, args, device) |         _LOGGER.warning("Failed to upload to %s", devices) | ||||||
|         if exit_code == 0: |  | ||||||
|             _LOGGER.info("Successfully uploaded program.") |  | ||||||
|             successful_device = device |  | ||||||
|             break |  | ||||||
|         if len(devices) > 1: |  | ||||||
|             _LOGGER.warning("Failed to upload to %s", device) |  | ||||||
|  |  | ||||||
|     if successful_device is None: |  | ||||||
|         return exit_code |         return exit_code | ||||||
|  |  | ||||||
|     if args.no_logs: |     if args.no_logs: | ||||||
| @@ -636,10 +711,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: | |||||||
|     devices = choose_upload_log_host( |     devices = choose_upload_log_host( | ||||||
|         default=successful_device, |         default=successful_device, | ||||||
|         check_default=successful_device, |         check_default=successful_device, | ||||||
|         show_ota=False, |         purpose=Purpose.LOGGING, | ||||||
|         show_mqtt=True, |  | ||||||
|         show_api=True, |  | ||||||
|         purpose="logging", |  | ||||||
|     ) |     ) | ||||||
|     return show_logs(config, args, devices) |     return show_logs(config, args, devices) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -61,11 +61,10 @@ void AbsoluteHumidityComponent::loop() { | |||||||
|       ESP_LOGW(TAG, "No valid state from temperature sensor!"); |       ESP_LOGW(TAG, "No valid state from temperature sensor!"); | ||||||
|     } |     } | ||||||
|     if (no_humidity) { |     if (no_humidity) { | ||||||
|       ESP_LOGW(TAG, "No valid state from temperature sensor!"); |       ESP_LOGW(TAG, "No valid state from humidity sensor!"); | ||||||
|     } |     } | ||||||
|     ESP_LOGW(TAG, "Unable to calculate absolute humidity."); |  | ||||||
|     this->publish_state(NAN); |     this->publish_state(NAN); | ||||||
|     this->status_set_warning(); |     this->status_set_warning(LOG_STR("Unable to calculate absolute humidity.")); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -87,9 +86,8 @@ void AbsoluteHumidityComponent::loop() { | |||||||
|       es = es_wobus(temperature_c); |       es = es_wobus(temperature_c); | ||||||
|       break; |       break; | ||||||
|     default: |     default: | ||||||
|       ESP_LOGE(TAG, "Invalid saturation vapor pressure equation selection!"); |  | ||||||
|       this->publish_state(NAN); |       this->publish_state(NAN); | ||||||
|       this->status_set_error(); |       this->status_set_error("Invalid saturation vapor pressure equation selection!"); | ||||||
|       return; |       return; | ||||||
|   } |   } | ||||||
|   ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es); |   ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es); | ||||||
|   | |||||||
| @@ -11,15 +11,8 @@ from esphome.components.esp32.const import ( | |||||||
|     VARIANT_ESP32S2, |     VARIANT_ESP32S2, | ||||||
|     VARIANT_ESP32S3, |     VARIANT_ESP32S3, | ||||||
| ) | ) | ||||||
| from esphome.config_helpers import filter_source_files_from_platform |  | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 | ||||||
|     CONF_ANALOG, |  | ||||||
|     CONF_INPUT, |  | ||||||
|     CONF_NUMBER, |  | ||||||
|     PLATFORM_ESP8266, |  | ||||||
|     PlatformFramework, |  | ||||||
| ) |  | ||||||
| from esphome.core import CORE | from esphome.core import CORE | ||||||
|  |  | ||||||
| CODEOWNERS = ["@esphome/core"] | CODEOWNERS = ["@esphome/core"] | ||||||
| @@ -273,21 +266,3 @@ def validate_adc_pin(value): | |||||||
|         )(value) |         )(value) | ||||||
|  |  | ||||||
|     raise NotImplementedError |     raise NotImplementedError | ||||||
|  |  | ||||||
|  |  | ||||||
| FILTER_SOURCE_FILES = filter_source_files_from_platform( |  | ||||||
|     { |  | ||||||
|         "adc_sensor_esp32.cpp": { |  | ||||||
|             PlatformFramework.ESP32_ARDUINO, |  | ||||||
|             PlatformFramework.ESP32_IDF, |  | ||||||
|         }, |  | ||||||
|         "adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, |  | ||||||
|         "adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, |  | ||||||
|         "adc_sensor_libretiny.cpp": { |  | ||||||
|             PlatformFramework.BK72XX_ARDUINO, |  | ||||||
|             PlatformFramework.RTL87XX_ARDUINO, |  | ||||||
|             PlatformFramework.LN882X_ARDUINO, |  | ||||||
|         }, |  | ||||||
|         "adc_sensor_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, |  | ||||||
|     } |  | ||||||
| ) |  | ||||||
|   | |||||||
| @@ -241,6 +241,8 @@ float ADCSensor::sample_autorange_() { | |||||||
|     cali_config.bitwidth = ADC_BITWIDTH_DEFAULT; |     cali_config.bitwidth = ADC_BITWIDTH_DEFAULT; | ||||||
|  |  | ||||||
|     err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); |     err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); | ||||||
|  |     ESP_LOGVV(TAG, "Autorange atten=%d: Calibration handle creation %s (err=%d)", atten, | ||||||
|  |               (err == ESP_OK) ? "SUCCESS" : "FAILED", err); | ||||||
| #else | #else | ||||||
|     adc_cali_line_fitting_config_t cali_config = { |     adc_cali_line_fitting_config_t cali_config = { | ||||||
|       .unit_id = this->adc_unit_, |       .unit_id = this->adc_unit_, | ||||||
| @@ -251,10 +253,14 @@ float ADCSensor::sample_autorange_() { | |||||||
| #endif | #endif | ||||||
|     }; |     }; | ||||||
|     err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); |     err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); | ||||||
|  |     ESP_LOGVV(TAG, "Autorange atten=%d: Calibration handle creation %s (err=%d)", atten, | ||||||
|  |               (err == ESP_OK) ? "SUCCESS" : "FAILED", err); | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|     int raw; |     int raw; | ||||||
|     err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw); |     err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw); | ||||||
|  |     ESP_LOGVV(TAG, "Autorange atten=%d: Raw ADC read %s, value=%d (err=%d)", atten, | ||||||
|  |               (err == ESP_OK) ? "SUCCESS" : "FAILED", raw, err); | ||||||
|  |  | ||||||
|     if (err != ESP_OK) { |     if (err != ESP_OK) { | ||||||
|       ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); |       ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); | ||||||
| @@ -275,8 +281,10 @@ float ADCSensor::sample_autorange_() { | |||||||
|       err = adc_cali_raw_to_voltage(handle, raw, &voltage_mv); |       err = adc_cali_raw_to_voltage(handle, raw, &voltage_mv); | ||||||
|       if (err == ESP_OK) { |       if (err == ESP_OK) { | ||||||
|         voltage = voltage_mv / 1000.0f; |         voltage = voltage_mv / 1000.0f; | ||||||
|  |         ESP_LOGVV(TAG, "Autorange atten=%d: CALIBRATED - raw=%d -> %dmV -> %.6fV", atten, raw, voltage_mv, voltage); | ||||||
|       } else { |       } else { | ||||||
|         voltage = raw * 3.3f / 4095.0f; |         voltage = raw * 3.3f / 4095.0f; | ||||||
|  |         ESP_LOGVV(TAG, "Autorange atten=%d: UNCALIBRATED FALLBACK - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage); | ||||||
|       } |       } | ||||||
|       // Clean up calibration handle |       // Clean up calibration handle | ||||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | ||||||
| @@ -287,6 +295,7 @@ float ADCSensor::sample_autorange_() { | |||||||
| #endif | #endif | ||||||
|     } else { |     } else { | ||||||
|       voltage = raw * 3.3f / 4095.0f; |       voltage = raw * 3.3f / 4095.0f; | ||||||
|  |       ESP_LOGVV(TAG, "Autorange atten=%d: NO CALIBRATION - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return {raw, voltage}; |     return {raw, voltage}; | ||||||
| @@ -324,18 +333,32 @@ float ADCSensor::sample_autorange_() { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   const int adc_half = 2048; |   const int adc_half = 2048; | ||||||
|   uint32_t c12 = std::min(raw12, adc_half); |   const uint32_t c12 = std::min(raw12, adc_half); | ||||||
|   uint32_t c6 = adc_half - std::abs(raw6 - adc_half); |  | ||||||
|   uint32_t c2 = adc_half - std::abs(raw2 - adc_half); |   const int32_t c6_signed = adc_half - std::abs(raw6 - adc_half); | ||||||
|   uint32_t c0 = std::min(4095 - raw0, adc_half); |   const uint32_t c6 = (c6_signed > 0) ? c6_signed : 0;  // Clamp to prevent underflow | ||||||
|   uint32_t csum = c12 + c6 + c2 + c0; |  | ||||||
|  |   const int32_t c2_signed = adc_half - std::abs(raw2 - adc_half); | ||||||
|  |   const uint32_t c2 = (c2_signed > 0) ? c2_signed : 0;  // Clamp to prevent underflow | ||||||
|  |  | ||||||
|  |   const uint32_t c0 = std::min(4095 - raw0, adc_half); | ||||||
|  |   const uint32_t csum = c12 + c6 + c2 + c0; | ||||||
|  |  | ||||||
|  |   ESP_LOGVV(TAG, "Autorange summary:"); | ||||||
|  |   ESP_LOGVV(TAG, "  Raw readings: 12db=%d, 6db=%d, 2.5db=%d, 0db=%d", raw12, raw6, raw2, raw0); | ||||||
|  |   ESP_LOGVV(TAG, "  Voltages: 12db=%.6f, 6db=%.6f, 2.5db=%.6f, 0db=%.6f", mv12, mv6, mv2, mv0); | ||||||
|  |   ESP_LOGVV(TAG, "  Coefficients: c12=%u, c6=%u, c2=%u, c0=%u, sum=%u", c12, c6, c2, c0, csum); | ||||||
|  |  | ||||||
|   if (csum == 0) { |   if (csum == 0) { | ||||||
|     ESP_LOGE(TAG, "Invalid weight sum in autorange calculation"); |     ESP_LOGE(TAG, "Invalid weight sum in autorange calculation"); | ||||||
|     return NAN; |     return NAN; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; |   const float final_result = (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; | ||||||
|  |   ESP_LOGV(TAG, "Autorange final: (%.6f*%u + %.6f*%u + %.6f*%u + %.6f*%u)/%u = %.6fV", mv12, c12, mv6, c6, mv2, c2, mv0, | ||||||
|  |            c0, csum, final_result); | ||||||
|  |  | ||||||
|  |   return final_result; | ||||||
| } | } | ||||||
|  |  | ||||||
| }  // namespace adc | }  // namespace adc | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ from esphome.components.zephyr import ( | |||||||
|     zephyr_add_prj_conf, |     zephyr_add_prj_conf, | ||||||
|     zephyr_add_user, |     zephyr_add_user, | ||||||
| ) | ) | ||||||
|  | from esphome.config_helpers import filter_source_files_from_platform | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_ATTENUATION, |     CONF_ATTENUATION, | ||||||
| @@ -20,6 +21,7 @@ from esphome.const import ( | |||||||
|     PLATFORM_NRF52, |     PLATFORM_NRF52, | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     UNIT_VOLT, |     UNIT_VOLT, | ||||||
|  |     PlatformFramework, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE | from esphome.core import CORE | ||||||
|  |  | ||||||
| @@ -174,3 +176,21 @@ async def to_code(config): | |||||||
| }}; | }}; | ||||||
| """ | """ | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FILTER_SOURCE_FILES = filter_source_files_from_platform( | ||||||
|  |     { | ||||||
|  |         "adc_sensor_esp32.cpp": { | ||||||
|  |             PlatformFramework.ESP32_ARDUINO, | ||||||
|  |             PlatformFramework.ESP32_IDF, | ||||||
|  |         }, | ||||||
|  |         "adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, | ||||||
|  |         "adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO}, | ||||||
|  |         "adc_sensor_libretiny.cpp": { | ||||||
|  |             PlatformFramework.BK72XX_ARDUINO, | ||||||
|  |             PlatformFramework.RTL87XX_ARDUINO, | ||||||
|  |             PlatformFramework.LN882X_ARDUINO, | ||||||
|  |         }, | ||||||
|  |         "adc_sensor_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, | ||||||
|  |     } | ||||||
|  | ) | ||||||
|   | |||||||
| @@ -113,7 +113,7 @@ void ADE7880::update() { | |||||||
|   if (this->channel_a_ != nullptr) { |   if (this->channel_a_ != nullptr) { | ||||||
|     auto *chan = this->channel_a_; |     auto *chan = this->channel_a_; | ||||||
|     this->update_sensor_from_s24zp_register16_(chan->current, AIRMS, [](float val) { return val / 100000.0f; }); |     this->update_sensor_from_s24zp_register16_(chan->current, AIRMS, [](float val) { return val / 100000.0f; }); | ||||||
|     this->update_sensor_from_s24zp_register16_(chan->voltage, BVRMS, [](float val) { return val / 10000.0f; }); |     this->update_sensor_from_s24zp_register16_(chan->voltage, AVRMS, [](float val) { return val / 10000.0f; }); | ||||||
|     this->update_sensor_from_s24zp_register16_(chan->active_power, AWATT, [](float val) { return val / 100.0f; }); |     this->update_sensor_from_s24zp_register16_(chan->active_power, AWATT, [](float val) { return val / 100.0f; }); | ||||||
|     this->update_sensor_from_s24zp_register16_(chan->apparent_power, AVA, [](float val) { return val / 100.0f; }); |     this->update_sensor_from_s24zp_register16_(chan->apparent_power, AVA, [](float val) { return val / 100.0f; }); | ||||||
|     this->update_sensor_from_s16_register16_(chan->power_factor, APF, |     this->update_sensor_from_s16_register16_(chan->power_factor, APF, | ||||||
|   | |||||||
| @@ -89,7 +89,7 @@ void AGS10Component::dump_config() { | |||||||
| bool AGS10Component::new_i2c_address(uint8_t newaddress) { | bool AGS10Component::new_i2c_address(uint8_t newaddress) { | ||||||
|   uint8_t rev_newaddress = ~newaddress; |   uint8_t rev_newaddress = ~newaddress; | ||||||
|   std::array<uint8_t, 5> data{newaddress, rev_newaddress, newaddress, rev_newaddress, 0}; |   std::array<uint8_t, 5> data{newaddress, rev_newaddress, newaddress, rev_newaddress, 0}; | ||||||
|   data[4] = calc_crc8_(data, 4); |   data[4] = crc8(data.data(), 4, 0xFF, 0x31, true); | ||||||
|   if (!this->write_bytes(REG_ADDRESS, data)) { |   if (!this->write_bytes(REG_ADDRESS, data)) { | ||||||
|     this->error_code_ = COMMUNICATION_FAILED; |     this->error_code_ = COMMUNICATION_FAILED; | ||||||
|     this->status_set_warning(); |     this->status_set_warning(); | ||||||
| @@ -109,7 +109,7 @@ bool AGS10Component::set_zero_point_with_current_resistance() { return this->set | |||||||
|  |  | ||||||
| bool AGS10Component::set_zero_point_with(uint16_t value) { | bool AGS10Component::set_zero_point_with(uint16_t value) { | ||||||
|   std::array<uint8_t, 5> data{0x00, 0x0C, (uint8_t) ((value >> 8) & 0xFF), (uint8_t) (value & 0xFF), 0}; |   std::array<uint8_t, 5> data{0x00, 0x0C, (uint8_t) ((value >> 8) & 0xFF), (uint8_t) (value & 0xFF), 0}; | ||||||
|   data[4] = calc_crc8_(data, 4); |   data[4] = crc8(data.data(), 4, 0xFF, 0x31, true); | ||||||
|   if (!this->write_bytes(REG_CALIBRATION, data)) { |   if (!this->write_bytes(REG_CALIBRATION, data)) { | ||||||
|     this->error_code_ = COMMUNICATION_FAILED; |     this->error_code_ = COMMUNICATION_FAILED; | ||||||
|     this->status_set_warning(); |     this->status_set_warning(); | ||||||
| @@ -184,7 +184,7 @@ template<size_t N> optional<std::array<uint8_t, N>> AGS10Component::read_and_che | |||||||
|   auto res = *data; |   auto res = *data; | ||||||
|   auto crc_byte = res[len]; |   auto crc_byte = res[len]; | ||||||
|  |  | ||||||
|   if (crc_byte != calc_crc8_(res, len)) { |   if (crc_byte != crc8(res.data(), len, 0xFF, 0x31, true)) { | ||||||
|     this->error_code_ = CRC_CHECK_FAILED; |     this->error_code_ = CRC_CHECK_FAILED; | ||||||
|     ESP_LOGE(TAG, "Reading AGS10 version failed: crc error!"); |     ESP_LOGE(TAG, "Reading AGS10 version failed: crc error!"); | ||||||
|     return optional<std::array<uint8_t, N>>(); |     return optional<std::array<uint8_t, N>>(); | ||||||
| @@ -192,20 +192,5 @@ template<size_t N> optional<std::array<uint8_t, N>> AGS10Component::read_and_che | |||||||
|  |  | ||||||
|   return data; |   return data; | ||||||
| } | } | ||||||
|  |  | ||||||
| template<size_t N> uint8_t AGS10Component::calc_crc8_(std::array<uint8_t, N> dat, uint8_t num) { |  | ||||||
|   uint8_t i, byte1, crc = 0xFF; |  | ||||||
|   for (byte1 = 0; byte1 < num; byte1++) { |  | ||||||
|     crc ^= (dat[byte1]); |  | ||||||
|     for (i = 0; i < 8; i++) { |  | ||||||
|       if (crc & 0x80) { |  | ||||||
|         crc = (crc << 1) ^ 0x31; |  | ||||||
|       } else { |  | ||||||
|         crc = (crc << 1); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   return crc; |  | ||||||
| } |  | ||||||
| }  // namespace ags10 | }  // namespace ags10 | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/components/i2c/i2c.h" | ||||||
|  | #include "esphome/components/sensor/sensor.h" | ||||||
| #include "esphome/core/automation.h" | #include "esphome/core/automation.h" | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/components/sensor/sensor.h" |  | ||||||
| #include "esphome/components/i2c/i2c.h" |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace ags10 { | namespace ags10 { | ||||||
| @@ -99,16 +99,6 @@ class AGS10Component : public PollingComponent, public i2c::I2CDevice { | |||||||
|    * Read, checks and returns data from the sensor. |    * Read, checks and returns data from the sensor. | ||||||
|    */ |    */ | ||||||
|   template<size_t N> optional<std::array<uint8_t, N>> read_and_check_(uint8_t a_register); |   template<size_t N> optional<std::array<uint8_t, N>> read_and_check_(uint8_t a_register); | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Calculates CRC8 value. |  | ||||||
|    * |  | ||||||
|    * CRC8 calculation, initial value: 0xFF, polynomial: 0x31 (x8+ x5+ x4+1) |  | ||||||
|    * |  | ||||||
|    * @param[in] dat the data buffer |  | ||||||
|    * @param num number of bytes in the buffer |  | ||||||
|    */ |  | ||||||
|   template<size_t N> uint8_t calc_crc8_(std::array<uint8_t, N> dat, uint8_t num); |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| template<typename... Ts> class AGS10NewI2cAddressAction : public Action<Ts...>, public Parented<AGS10Component> { | template<typename... Ts> class AGS10NewI2cAddressAction : public Action<Ts...>, public Parented<AGS10Component> { | ||||||
|   | |||||||
| @@ -96,7 +96,7 @@ void AHT10Component::read_data_() { | |||||||
|     ESP_LOGD(TAG, "Read attempt %d at %ums", this->read_count_, (unsigned) (millis() - this->start_time_)); |     ESP_LOGD(TAG, "Read attempt %d at %ums", this->read_count_, (unsigned) (millis() - this->start_time_)); | ||||||
|   } |   } | ||||||
|   if (this->read(data, 6) != i2c::ERROR_OK) { |   if (this->read(data, 6) != i2c::ERROR_OK) { | ||||||
|     this->status_set_warning("Read failed, will retry"); |     this->status_set_warning(LOG_STR("Read failed, will retry")); | ||||||
|     this->restart_read_(); |     this->restart_read_(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| @@ -113,7 +113,7 @@ void AHT10Component::read_data_() { | |||||||
|     } else { |     } else { | ||||||
|       ESP_LOGD(TAG, "Invalid humidity, retrying"); |       ESP_LOGD(TAG, "Invalid humidity, retrying"); | ||||||
|       if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { |       if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { | ||||||
|         this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); |         this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); | ||||||
|       } |       } | ||||||
|       this->restart_read_(); |       this->restart_read_(); | ||||||
|       return; |       return; | ||||||
| @@ -144,7 +144,7 @@ void AHT10Component::update() { | |||||||
|     return; |     return; | ||||||
|   this->start_time_ = millis(); |   this->start_time_ = millis(); | ||||||
|   if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { |   if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { | ||||||
|     this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); |     this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   this->restart_read_(); |   this->restart_read_(); | ||||||
|   | |||||||
| @@ -18,6 +18,6 @@ CONFIG_SCHEMA = cv.Schema( | |||||||
| ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) | ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
| def to_code(config): | async def to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|     yield esp32_ble_tracker.register_ble_device(var, config) |     await esp32_ble_tracker.register_ble_device(var, config) | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ from esphome.const import ( | |||||||
|     CONF_TRIGGER_ID, |     CONF_TRIGGER_ID, | ||||||
|     CONF_WEB_SERVER, |     CONF_WEB_SERVER, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, CoroPriority, coroutine_with_priority | ||||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||||
| from esphome.cpp_generator import MockObjClass | from esphome.cpp_generator import MockObjClass | ||||||
|  |  | ||||||
| @@ -345,6 +345,6 @@ async def alarm_control_panel_is_armed_to_code( | |||||||
|     return cg.new_Pvariable(condition_id, template_arg, paren) |     return cg.new_Pvariable(condition_id, template_arg, paren) | ||||||
|  |  | ||||||
|  |  | ||||||
| @coroutine_with_priority(100.0) | @coroutine_with_priority(CoroPriority.CORE) | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     cg.add_global(alarm_control_panel_ns.using) |     cg.add_global(alarm_control_panel_ns.using) | ||||||
|   | |||||||
| @@ -29,22 +29,6 @@ namespace am2315c { | |||||||
|  |  | ||||||
| static const char *const TAG = "am2315c"; | static const char *const TAG = "am2315c"; | ||||||
|  |  | ||||||
| uint8_t AM2315C::crc8_(uint8_t *data, uint8_t len) { |  | ||||||
|   uint8_t crc = 0xFF; |  | ||||||
|   while (len--) { |  | ||||||
|     crc ^= *data++; |  | ||||||
|     for (uint8_t i = 0; i < 8; i++) { |  | ||||||
|       if (crc & 0x80) { |  | ||||||
|         crc <<= 1; |  | ||||||
|         crc ^= 0x31; |  | ||||||
|       } else { |  | ||||||
|         crc <<= 1; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   return crc; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool AM2315C::reset_register_(uint8_t reg) { | bool AM2315C::reset_register_(uint8_t reg) { | ||||||
|   //  code based on demo code sent by www.aosong.com |   //  code based on demo code sent by www.aosong.com | ||||||
|   //  no further documentation. |   //  no further documentation. | ||||||
| @@ -86,7 +70,7 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) { | |||||||
|   humidity = raw * 9.5367431640625e-5; |   humidity = raw * 9.5367431640625e-5; | ||||||
|   raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]; |   raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]; | ||||||
|   temperature = raw * 1.9073486328125e-4 - 50; |   temperature = raw * 1.9073486328125e-4 - 50; | ||||||
|   return this->crc8_(data, 6) == data[6]; |   return crc8(data, 6, 0xFF, 0x31, true) == data[6]; | ||||||
| } | } | ||||||
|  |  | ||||||
| void AM2315C::setup() { | void AM2315C::setup() { | ||||||
|   | |||||||
| @@ -21,9 +21,9 @@ | |||||||
| // SOFTWARE. | // SOFTWARE. | ||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" |  | ||||||
| #include "esphome/components/sensor/sensor.h" |  | ||||||
| #include "esphome/components/i2c/i2c.h" | #include "esphome/components/i2c/i2c.h" | ||||||
|  | #include "esphome/components/sensor/sensor.h" | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace am2315c { | namespace am2315c { | ||||||
| @@ -39,7 +39,6 @@ class AM2315C : public PollingComponent, public i2c::I2CDevice { | |||||||
|   void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } |   void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   uint8_t crc8_(uint8_t *data, uint8_t len); |  | ||||||
|   bool convert_(uint8_t *data, float &humidity, float &temperature); |   bool convert_(uint8_t *data, float &humidity, float &temperature); | ||||||
|   bool reset_register_(uint8_t reg); |   bool reset_register_(uint8_t reg); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ from esphome.const import ( | |||||||
|     CONF_TRIGGER_ID, |     CONF_TRIGGER_ID, | ||||||
|     CONF_VARIABLES, |     CONF_VARIABLES, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, CoroPriority, coroutine_with_priority | ||||||
|  |  | ||||||
| DOMAIN = "api" | DOMAIN = "api" | ||||||
| DEPENDENCIES = ["network"] | DEPENDENCIES = ["network"] | ||||||
| @@ -134,7 +134,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @coroutine_with_priority(40.0) | @coroutine_with_priority(CoroPriority.WEB) | ||||||
| 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) | ||||||
|   | |||||||
| @@ -27,9 +27,6 @@ service APIConnection { | |||||||
|   rpc subscribe_logs (SubscribeLogsRequest) returns (void) {} |   rpc subscribe_logs (SubscribeLogsRequest) returns (void) {} | ||||||
|   rpc subscribe_homeassistant_services (SubscribeHomeassistantServicesRequest) returns (void) {} |   rpc subscribe_homeassistant_services (SubscribeHomeassistantServicesRequest) returns (void) {} | ||||||
|   rpc subscribe_home_assistant_states (SubscribeHomeAssistantStatesRequest) returns (void) {} |   rpc subscribe_home_assistant_states (SubscribeHomeAssistantStatesRequest) returns (void) {} | ||||||
|   rpc get_time (GetTimeRequest) returns (GetTimeResponse) { |  | ||||||
|     option (needs_authentication) = false; |  | ||||||
|   } |  | ||||||
|   rpc execute_service (ExecuteServiceRequest) returns (void) {} |   rpc execute_service (ExecuteServiceRequest) returns (void) {} | ||||||
|   rpc noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {} |   rpc noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {} | ||||||
|  |  | ||||||
| @@ -809,15 +806,16 @@ message HomeAssistantStateResponse { | |||||||
| // ==================== IMPORT TIME ==================== | // ==================== IMPORT TIME ==================== | ||||||
| message GetTimeRequest { | message GetTimeRequest { | ||||||
|   option (id) = 36; |   option (id) = 36; | ||||||
|   option (source) = SOURCE_BOTH; |   option (source) = SOURCE_SERVER; | ||||||
| } | } | ||||||
|  |  | ||||||
| message GetTimeResponse { | message GetTimeResponse { | ||||||
|   option (id) = 37; |   option (id) = 37; | ||||||
|   option (source) = SOURCE_BOTH; |   option (source) = SOURCE_CLIENT; | ||||||
|   option (no_delay) = true; |   option (no_delay) = true; | ||||||
|  |  | ||||||
|   fixed32 epoch_seconds = 1; |   fixed32 epoch_seconds = 1; | ||||||
|  |   string timezone = 2; | ||||||
| } | } | ||||||
|  |  | ||||||
| // ==================== USER-DEFINES SERVICES ==================== | // ==================== USER-DEFINES SERVICES ==================== | ||||||
| @@ -1712,6 +1710,7 @@ message BluetoothScannerStateResponse { | |||||||
|  |  | ||||||
|   BluetoothScannerState state = 1; |   BluetoothScannerState state = 1; | ||||||
|   BluetoothScannerMode mode = 2; |   BluetoothScannerMode mode = 2; | ||||||
|  |   BluetoothScannerMode configured_mode = 3; | ||||||
| } | } | ||||||
|  |  | ||||||
| message BluetoothScannerSetModeRequest { | message BluetoothScannerSetModeRequest { | ||||||
|   | |||||||
| @@ -42,6 +42,8 @@ static constexpr uint8_t MAX_PING_RETRIES = 60; | |||||||
| static constexpr uint16_t PING_RETRY_INTERVAL = 1000; | static constexpr uint16_t PING_RETRY_INTERVAL = 1000; | ||||||
| static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; | static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; | ||||||
|  |  | ||||||
|  | static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); | ||||||
|  |  | ||||||
| static const char *const TAG = "api.connection"; | static const char *const TAG = "api.connection"; | ||||||
| #ifdef USE_CAMERA | #ifdef USE_CAMERA | ||||||
| static const int CAMERA_STOP_STREAM = 5000; | static const int CAMERA_STOP_STREAM = 5000; | ||||||
| @@ -112,7 +114,7 @@ void APIConnection::start() { | |||||||
|   APIError err = this->helper_->init(); |   APIError err = this->helper_->init(); | ||||||
|   if (err != APIError::OK) { |   if (err != APIError::OK) { | ||||||
|     on_fatal_error(); |     on_fatal_error(); | ||||||
|     this->log_warning_("Helper init failed", err); |     this->log_warning_(LOG_STR("Helper init failed"), err); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   this->client_info_.peername = helper_->getpeername(); |   this->client_info_.peername = helper_->getpeername(); | ||||||
| @@ -159,7 +161,7 @@ void APIConnection::loop() { | |||||||
|         break; |         break; | ||||||
|       } else if (err != APIError::OK) { |       } else if (err != APIError::OK) { | ||||||
|         on_fatal_error(); |         on_fatal_error(); | ||||||
|         this->log_warning_("Reading failed", err); |         this->log_warning_(LOG_STR("Reading failed"), err); | ||||||
|         return; |         return; | ||||||
|       } else { |       } else { | ||||||
|         this->last_traffic_ = now; |         this->last_traffic_ = now; | ||||||
| @@ -289,16 +291,26 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess | |||||||
|     return 0;  // Doesn't fit |     return 0;  // Doesn't fit | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Allocate buffer space - pass payload size, allocation functions add header/footer space |  | ||||||
|   ProtoWriteBuffer buffer = is_single ? conn->allocate_single_message_buffer(calculated_size) |  | ||||||
|                                       : conn->allocate_batch_message_buffer(calculated_size); |  | ||||||
|  |  | ||||||
|   // Get buffer size after allocation (which includes header padding) |   // Get buffer size after allocation (which includes header padding) | ||||||
|   std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref(); |   std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref(); | ||||||
|   size_t size_before_encode = shared_buf.size(); |  | ||||||
|  |   if (is_single || conn->flags_.batch_first_message) { | ||||||
|  |     // Single message or first batch message | ||||||
|  |     conn->prepare_first_message_buffer(shared_buf, header_padding, total_calculated_size); | ||||||
|  |     if (conn->flags_.batch_first_message) { | ||||||
|  |       conn->flags_.batch_first_message = false; | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     // Batch message second or later | ||||||
|  |     // Add padding for previous message footer + this message header | ||||||
|  |     size_t current_size = shared_buf.size(); | ||||||
|  |     shared_buf.reserve(current_size + total_calculated_size); | ||||||
|  |     shared_buf.resize(current_size + footer_size + header_padding); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // Encode directly into buffer |   // Encode directly into buffer | ||||||
|   msg.encode(buffer); |   size_t size_before_encode = shared_buf.size(); | ||||||
|  |   msg.encode({&shared_buf}); | ||||||
|  |  | ||||||
|   // Calculate actual encoded size (not including header that was already added) |   // Calculate actual encoded size (not including header that was already added) | ||||||
|   size_t actual_payload_size = shared_buf.size() - size_before_encode; |   size_t actual_payload_size = shared_buf.size() - size_before_encode; | ||||||
| @@ -1060,17 +1072,17 @@ void APIConnection::camera_image(const CameraImageRequest &msg) { | |||||||
|  |  | ||||||
| #ifdef USE_HOMEASSISTANT_TIME | #ifdef USE_HOMEASSISTANT_TIME | ||||||
| void APIConnection::on_get_time_response(const GetTimeResponse &value) { | void APIConnection::on_get_time_response(const GetTimeResponse &value) { | ||||||
|   if (homeassistant::global_homeassistant_time != nullptr) |   if (homeassistant::global_homeassistant_time != nullptr) { | ||||||
|     homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); |     homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); | ||||||
|  | #ifdef USE_TIME_TIMEZONE | ||||||
|  |     if (!value.timezone.empty() && value.timezone != homeassistant::global_homeassistant_time->get_timezone()) { | ||||||
|  |       homeassistant::global_homeassistant_time->set_timezone(value.timezone); | ||||||
|  |     } | ||||||
|  | #endif | ||||||
|  |   } | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| bool APIConnection::send_get_time_response(const GetTimeRequest &msg) { |  | ||||||
|   GetTimeResponse resp; |  | ||||||
|   resp.epoch_seconds = ::time(nullptr); |  | ||||||
|   return this->send_message(resp, GetTimeResponse::MESSAGE_TYPE); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
| void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { | void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { | ||||||
|   bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); |   bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); | ||||||
| @@ -1360,9 +1372,8 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) { | |||||||
|   HelloResponse resp; |   HelloResponse resp; | ||||||
|   resp.api_version_major = 1; |   resp.api_version_major = 1; | ||||||
|   resp.api_version_minor = 12; |   resp.api_version_minor = 12; | ||||||
|   // Temporary string for concatenation - will be valid during send_message call |   // Send only the version string - the client only logs this for debugging and doesn't use it otherwise | ||||||
|   std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; |   resp.set_server_info(ESPHOME_VERSION_REF); | ||||||
|   resp.set_server_info(StringRef(server_info)); |  | ||||||
|   resp.set_name(StringRef(App.get_name())); |   resp.set_name(StringRef(App.get_name())); | ||||||
|  |  | ||||||
| #ifdef USE_API_PASSWORD | #ifdef USE_API_PASSWORD | ||||||
| @@ -1409,8 +1420,6 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { | |||||||
|   std::string mac_address = get_mac_address_pretty(); |   std::string mac_address = get_mac_address_pretty(); | ||||||
|   resp.set_mac_address(StringRef(mac_address)); |   resp.set_mac_address(StringRef(mac_address)); | ||||||
|  |  | ||||||
|   // Compile-time StringRef constants |  | ||||||
|   static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); |  | ||||||
|   resp.set_esphome_version(ESPHOME_VERSION_REF); |   resp.set_esphome_version(ESPHOME_VERSION_REF); | ||||||
|  |  | ||||||
|   resp.set_compilation_time(App.get_compilation_time_ref()); |   resp.set_compilation_time(App.get_compilation_time_ref()); | ||||||
| @@ -1555,7 +1564,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { | |||||||
|     return false; |     return false; | ||||||
|   if (err != APIError::OK) { |   if (err != APIError::OK) { | ||||||
|     on_fatal_error(); |     on_fatal_error(); | ||||||
|     this->log_warning_("Packet write failed", err); |     this->log_warning_(LOG_STR("Packet write failed"), err); | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|   // Do not set last_traffic_ on send |   // Do not set last_traffic_ on send | ||||||
| @@ -1616,14 +1625,6 @@ bool APIConnection::schedule_batch_() { | |||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
|  |  | ||||||
| ProtoWriteBuffer APIConnection::allocate_single_message_buffer(uint16_t size) { return this->create_buffer(size); } |  | ||||||
|  |  | ||||||
| ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) { |  | ||||||
|   ProtoWriteBuffer result = this->prepare_message_buffer(size, this->flags_.batch_first_message); |  | ||||||
|   this->flags_.batch_first_message = false; |  | ||||||
|   return result; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void APIConnection::process_batch_() { | void APIConnection::process_batch_() { | ||||||
|   // Ensure PacketInfo remains trivially destructible for our placement new approach |   // Ensure PacketInfo remains trivially destructible for our placement new approach | ||||||
|   static_assert(std::is_trivially_destructible<PacketInfo>::value, |   static_assert(std::is_trivially_destructible<PacketInfo>::value, | ||||||
| @@ -1731,7 +1732,7 @@ void APIConnection::process_batch_() { | |||||||
|     } |     } | ||||||
|     remaining_size -= payload_size; |     remaining_size -= payload_size; | ||||||
|     // Calculate where the next message's header padding will start |     // Calculate where the next message's header padding will start | ||||||
|     // Current buffer size + footer space (that prepare_message_buffer will add for this message) |     // Current buffer size + footer space for this message | ||||||
|     current_offset = shared_buf.size() + footer_size; |     current_offset = shared_buf.size() + footer_size; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -1750,7 +1751,7 @@ void APIConnection::process_batch_() { | |||||||
|                                                        std::span<const PacketInfo>(packet_info, packet_count)); |                                                        std::span<const PacketInfo>(packet_info, packet_count)); | ||||||
|   if (err != APIError::OK && err != APIError::WOULD_BLOCK) { |   if (err != APIError::OK && err != APIError::WOULD_BLOCK) { | ||||||
|     on_fatal_error(); |     on_fatal_error(); | ||||||
|     this->log_warning_("Batch write failed", err); |     this->log_warning_(LOG_STR("Batch write failed"), err); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
| @@ -1828,11 +1829,14 @@ void APIConnection::process_state_subscriptions_() { | |||||||
| } | } | ||||||
| #endif  // USE_API_HOMEASSISTANT_STATES | #endif  // USE_API_HOMEASSISTANT_STATES | ||||||
|  |  | ||||||
| void APIConnection::log_warning_(const char *message, APIError err) { | void APIConnection::log_warning_(const LogString *message, APIError err) { | ||||||
|   ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), message, api_error_to_str(err), errno); |   ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), LOG_STR_ARG(message), | ||||||
|  |            LOG_STR_ARG(api_error_to_logstr(err)), errno); | ||||||
| } | } | ||||||
|  |  | ||||||
| void APIConnection::log_socket_operation_failed_(APIError err) { this->log_warning_("Socket operation failed", err); } | void APIConnection::log_socket_operation_failed_(APIError err) { | ||||||
|  |   this->log_warning_(LOG_STR("Socket operation failed"), err); | ||||||
|  | } | ||||||
|  |  | ||||||
| }  // namespace esphome::api | }  // namespace esphome::api | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ static constexpr size_t MAX_PACKETS_PER_BATCH = 64;  // ESP32 has 8KB+ stack, HO | |||||||
| static constexpr size_t MAX_PACKETS_PER_BATCH = 32;  // ESP8266/RP2040/etc have smaller stacks | static constexpr size_t MAX_PACKETS_PER_BATCH = 32;  // ESP8266/RP2040/etc have smaller stacks | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| class APIConnection : public APIServerConnection { | class APIConnection final : public APIServerConnection { | ||||||
|  public: |  public: | ||||||
|   friend class APIServer; |   friend class APIServer; | ||||||
|   friend class ListEntitiesIterator; |   friend class ListEntitiesIterator; | ||||||
| @@ -219,7 +219,6 @@ class APIConnection : public APIServerConnection { | |||||||
| #ifdef USE_API_HOMEASSISTANT_STATES | #ifdef USE_API_HOMEASSISTANT_STATES | ||||||
|   void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; |   void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; | ||||||
| #endif | #endif | ||||||
|   bool send_get_time_response(const GetTimeRequest &msg) override; |  | ||||||
| #ifdef USE_API_SERVICES | #ifdef USE_API_SERVICES | ||||||
|   void execute_service(const ExecuteServiceRequest &msg) override; |   void execute_service(const ExecuteServiceRequest &msg) override; | ||||||
| #endif | #endif | ||||||
| @@ -252,44 +251,21 @@ class APIConnection : public APIServerConnection { | |||||||
|  |  | ||||||
|     // Get header padding size - used for both reserve and insert |     // Get header padding size - used for both reserve and insert | ||||||
|     uint8_t header_padding = this->helper_->frame_header_padding(); |     uint8_t header_padding = this->helper_->frame_header_padding(); | ||||||
|  |  | ||||||
|     // Get shared buffer from parent server |     // Get shared buffer from parent server | ||||||
|     std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref(); |     std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref(); | ||||||
|  |     this->prepare_first_message_buffer(shared_buf, header_padding, | ||||||
|  |                                        reserve_size + header_padding + this->helper_->frame_footer_size()); | ||||||
|  |     return {&shared_buf}; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) { | ||||||
|     shared_buf.clear(); |     shared_buf.clear(); | ||||||
|     // Reserve space for header padding + message + footer |     // Reserve space for header padding + message + footer | ||||||
|     // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) |     // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) | ||||||
|     // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext) |     // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext) | ||||||
|     shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_size()); |     shared_buf.reserve(total_size); | ||||||
|     // Resize to add header padding so message encoding starts at the correct position |     // Resize to add header padding so message encoding starts at the correct position | ||||||
|     shared_buf.resize(header_padding); |     shared_buf.resize(header_padding); | ||||||
|     return {&shared_buf}; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Prepare buffer for next message in batch |  | ||||||
|   ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) { |  | ||||||
|     // Get reference to shared buffer (it maintains state between batch messages) |  | ||||||
|     std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref(); |  | ||||||
|  |  | ||||||
|     if (is_first_message) { |  | ||||||
|       shared_buf.clear(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     size_t current_size = shared_buf.size(); |  | ||||||
|  |  | ||||||
|     // Calculate padding to add: |  | ||||||
|     // - First message: just header padding |  | ||||||
|     // - Subsequent messages: footer for previous message + header padding for this message |  | ||||||
|     size_t padding_to_add = is_first_message |  | ||||||
|                                 ? this->helper_->frame_header_padding() |  | ||||||
|                                 : this->helper_->frame_header_padding() + this->helper_->frame_footer_size(); |  | ||||||
|  |  | ||||||
|     // Reserve space for padding + message |  | ||||||
|     shared_buf.reserve(current_size + padding_to_add + message_size); |  | ||||||
|  |  | ||||||
|     // Resize to add the padding bytes |  | ||||||
|     shared_buf.resize(current_size + padding_to_add); |  | ||||||
|  |  | ||||||
|     return {&shared_buf}; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   bool try_to_clear_buffer(bool log_out_of_space); |   bool try_to_clear_buffer(bool log_out_of_space); | ||||||
| @@ -297,10 +273,6 @@ class APIConnection : public APIServerConnection { | |||||||
|  |  | ||||||
|   std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); } |   std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); } | ||||||
|  |  | ||||||
|   // Buffer allocator methods for batch processing |  | ||||||
|   ProtoWriteBuffer allocate_single_message_buffer(uint16_t size); |  | ||||||
|   ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); |  | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   // Helper function to handle authentication completion |   // Helper function to handle authentication completion | ||||||
|   void complete_authentication_(); |   void complete_authentication_(); | ||||||
| @@ -328,9 +300,17 @@ class APIConnection : public APIServerConnection { | |||||||
|                                               APIConnection *conn, uint32_t remaining_size, bool is_single) { |                                               APIConnection *conn, uint32_t remaining_size, bool is_single) { | ||||||
|     // Set common fields that are shared by all entity types |     // Set common fields that are shared by all entity types | ||||||
|     msg.key = entity->get_object_id_hash(); |     msg.key = entity->get_object_id_hash(); | ||||||
|     // IMPORTANT: get_object_id() may return a temporary std::string |     // Try to use static reference first to avoid allocation | ||||||
|     std::string object_id = entity->get_object_id(); |     StringRef static_ref = entity->get_object_id_ref_for_api_(); | ||||||
|     msg.set_object_id(StringRef(object_id)); |     // Store dynamic string outside the if-else to maintain lifetime | ||||||
|  |     std::string object_id; | ||||||
|  |     if (!static_ref.empty()) { | ||||||
|  |       msg.set_object_id(static_ref); | ||||||
|  |     } else { | ||||||
|  |       // Dynamic case - need to allocate | ||||||
|  |       object_id = entity->get_object_id(); | ||||||
|  |       msg.set_object_id(StringRef(object_id)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (entity->has_own_name()) { |     if (entity->has_own_name()) { | ||||||
|       msg.set_name(entity->get_name()); |       msg.set_name(entity->get_name()); | ||||||
| @@ -751,7 +731,7 @@ class APIConnection : public APIServerConnection { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Helper function to log API errors with errno |   // Helper function to log API errors with errno | ||||||
|   void log_warning_(const char *message, APIError err); |   void log_warning_(const LogString *message, APIError err); | ||||||
|   // Specific helper for duplicated error message |   // Specific helper for duplicated error message | ||||||
|   void log_socket_operation_failed_(APIError err); |   void log_socket_operation_failed_(APIError err); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -23,59 +23,59 @@ static const char *const TAG = "api.frame_helper"; | |||||||
| #define LOG_PACKET_SENDING(data, len) ((void) 0) | #define LOG_PACKET_SENDING(data, len) ((void) 0) | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| const char *api_error_to_str(APIError err) { | const LogString *api_error_to_logstr(APIError err) { | ||||||
|   // not using switch to ensure compiler doesn't try to build a big table out of it |   // not using switch to ensure compiler doesn't try to build a big table out of it | ||||||
|   if (err == APIError::OK) { |   if (err == APIError::OK) { | ||||||
|     return "OK"; |     return LOG_STR("OK"); | ||||||
|   } else if (err == APIError::WOULD_BLOCK) { |   } else if (err == APIError::WOULD_BLOCK) { | ||||||
|     return "WOULD_BLOCK"; |     return LOG_STR("WOULD_BLOCK"); | ||||||
|   } else if (err == APIError::BAD_INDICATOR) { |   } else if (err == APIError::BAD_INDICATOR) { | ||||||
|     return "BAD_INDICATOR"; |     return LOG_STR("BAD_INDICATOR"); | ||||||
|   } else if (err == APIError::BAD_DATA_PACKET) { |   } else if (err == APIError::BAD_DATA_PACKET) { | ||||||
|     return "BAD_DATA_PACKET"; |     return LOG_STR("BAD_DATA_PACKET"); | ||||||
|   } else if (err == APIError::TCP_NODELAY_FAILED) { |   } else if (err == APIError::TCP_NODELAY_FAILED) { | ||||||
|     return "TCP_NODELAY_FAILED"; |     return LOG_STR("TCP_NODELAY_FAILED"); | ||||||
|   } else if (err == APIError::TCP_NONBLOCKING_FAILED) { |   } else if (err == APIError::TCP_NONBLOCKING_FAILED) { | ||||||
|     return "TCP_NONBLOCKING_FAILED"; |     return LOG_STR("TCP_NONBLOCKING_FAILED"); | ||||||
|   } else if (err == APIError::CLOSE_FAILED) { |   } else if (err == APIError::CLOSE_FAILED) { | ||||||
|     return "CLOSE_FAILED"; |     return LOG_STR("CLOSE_FAILED"); | ||||||
|   } else if (err == APIError::SHUTDOWN_FAILED) { |   } else if (err == APIError::SHUTDOWN_FAILED) { | ||||||
|     return "SHUTDOWN_FAILED"; |     return LOG_STR("SHUTDOWN_FAILED"); | ||||||
|   } else if (err == APIError::BAD_STATE) { |   } else if (err == APIError::BAD_STATE) { | ||||||
|     return "BAD_STATE"; |     return LOG_STR("BAD_STATE"); | ||||||
|   } else if (err == APIError::BAD_ARG) { |   } else if (err == APIError::BAD_ARG) { | ||||||
|     return "BAD_ARG"; |     return LOG_STR("BAD_ARG"); | ||||||
|   } else if (err == APIError::SOCKET_READ_FAILED) { |   } else if (err == APIError::SOCKET_READ_FAILED) { | ||||||
|     return "SOCKET_READ_FAILED"; |     return LOG_STR("SOCKET_READ_FAILED"); | ||||||
|   } else if (err == APIError::SOCKET_WRITE_FAILED) { |   } else if (err == APIError::SOCKET_WRITE_FAILED) { | ||||||
|     return "SOCKET_WRITE_FAILED"; |     return LOG_STR("SOCKET_WRITE_FAILED"); | ||||||
|   } else if (err == APIError::OUT_OF_MEMORY) { |   } else if (err == APIError::OUT_OF_MEMORY) { | ||||||
|     return "OUT_OF_MEMORY"; |     return LOG_STR("OUT_OF_MEMORY"); | ||||||
|   } else if (err == APIError::CONNECTION_CLOSED) { |   } else if (err == APIError::CONNECTION_CLOSED) { | ||||||
|     return "CONNECTION_CLOSED"; |     return LOG_STR("CONNECTION_CLOSED"); | ||||||
|   } |   } | ||||||
| #ifdef USE_API_NOISE | #ifdef USE_API_NOISE | ||||||
|   else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) { |   else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) { | ||||||
|     return "BAD_HANDSHAKE_PACKET_LEN"; |     return LOG_STR("BAD_HANDSHAKE_PACKET_LEN"); | ||||||
|   } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) { |   } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) { | ||||||
|     return "HANDSHAKESTATE_READ_FAILED"; |     return LOG_STR("HANDSHAKESTATE_READ_FAILED"); | ||||||
|   } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) { |   } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) { | ||||||
|     return "HANDSHAKESTATE_WRITE_FAILED"; |     return LOG_STR("HANDSHAKESTATE_WRITE_FAILED"); | ||||||
|   } else if (err == APIError::HANDSHAKESTATE_BAD_STATE) { |   } else if (err == APIError::HANDSHAKESTATE_BAD_STATE) { | ||||||
|     return "HANDSHAKESTATE_BAD_STATE"; |     return LOG_STR("HANDSHAKESTATE_BAD_STATE"); | ||||||
|   } else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) { |   } else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) { | ||||||
|     return "CIPHERSTATE_DECRYPT_FAILED"; |     return LOG_STR("CIPHERSTATE_DECRYPT_FAILED"); | ||||||
|   } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) { |   } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) { | ||||||
|     return "CIPHERSTATE_ENCRYPT_FAILED"; |     return LOG_STR("CIPHERSTATE_ENCRYPT_FAILED"); | ||||||
|   } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) { |   } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) { | ||||||
|     return "HANDSHAKESTATE_SETUP_FAILED"; |     return LOG_STR("HANDSHAKESTATE_SETUP_FAILED"); | ||||||
|   } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) { |   } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) { | ||||||
|     return "HANDSHAKESTATE_SPLIT_FAILED"; |     return LOG_STR("HANDSHAKESTATE_SPLIT_FAILED"); | ||||||
|   } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) { |   } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) { | ||||||
|     return "BAD_HANDSHAKE_ERROR_BYTE"; |     return LOG_STR("BAD_HANDSHAKE_ERROR_BYTE"); | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|   return "UNKNOWN"; |   return LOG_STR("UNKNOWN"); | ||||||
| } | } | ||||||
|  |  | ||||||
| // Default implementation for loop - handles sending buffered data | // Default implementation for loop - handles sending buffered data | ||||||
|   | |||||||
| @@ -66,7 +66,7 @@ enum class APIError : uint16_t { | |||||||
| #endif | #endif | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const char *api_error_to_str(APIError err); | const LogString *api_error_to_logstr(APIError err); | ||||||
|  |  | ||||||
| class APIFrameHelper { | class APIFrameHelper { | ||||||
|  public: |  public: | ||||||
| @@ -104,9 +104,9 @@ class APIFrameHelper { | |||||||
|   // The buffer contains all messages with appropriate padding before each |   // The buffer contains all messages with appropriate padding before each | ||||||
|   virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) = 0; |   virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) = 0; | ||||||
|   // Get the frame header padding required by this protocol |   // Get the frame header padding required by this protocol | ||||||
|   virtual uint8_t frame_header_padding() = 0; |   uint8_t frame_header_padding() const { return frame_header_padding_; } | ||||||
|   // Get the frame footer size required by this protocol |   // Get the frame footer size required by this protocol | ||||||
|   virtual uint8_t frame_footer_size() = 0; |   uint8_t frame_footer_size() const { return frame_footer_size_; } | ||||||
|   // Check if socket has data ready to read |   // Check if socket has data ready to read | ||||||
|   bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } |   bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,10 +10,18 @@ | |||||||
| #include <cstring> | #include <cstring> | ||||||
| #include <cinttypes> | #include <cinttypes> | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP8266 | ||||||
|  | #include <pgmspace.h> | ||||||
|  | #endif | ||||||
|  |  | ||||||
| namespace esphome::api { | namespace esphome::api { | ||||||
|  |  | ||||||
| static const char *const TAG = "api.noise"; | static const char *const TAG = "api.noise"; | ||||||
|  | #ifdef USE_ESP8266 | ||||||
|  | static const char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; | ||||||
|  | #else | ||||||
| static const char *const PROLOGUE_INIT = "NoiseAPIInit"; | static const char *const PROLOGUE_INIT = "NoiseAPIInit"; | ||||||
|  | #endif | ||||||
| static constexpr size_t PROLOGUE_INIT_LEN = 12;  // strlen("NoiseAPIInit") | static constexpr size_t PROLOGUE_INIT_LEN = 12;  // strlen("NoiseAPIInit") | ||||||
|  |  | ||||||
| #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) | #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) | ||||||
| @@ -27,42 +35,42 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12;  // strlen("NoiseAPIInit") | |||||||
| #endif | #endif | ||||||
|  |  | ||||||
| /// Convert a noise error code to a readable error | /// Convert a noise error code to a readable error | ||||||
| std::string noise_err_to_str(int err) { | const LogString *noise_err_to_logstr(int err) { | ||||||
|   if (err == NOISE_ERROR_NO_MEMORY) |   if (err == NOISE_ERROR_NO_MEMORY) | ||||||
|     return "NO_MEMORY"; |     return LOG_STR("NO_MEMORY"); | ||||||
|   if (err == NOISE_ERROR_UNKNOWN_ID) |   if (err == NOISE_ERROR_UNKNOWN_ID) | ||||||
|     return "UNKNOWN_ID"; |     return LOG_STR("UNKNOWN_ID"); | ||||||
|   if (err == NOISE_ERROR_UNKNOWN_NAME) |   if (err == NOISE_ERROR_UNKNOWN_NAME) | ||||||
|     return "UNKNOWN_NAME"; |     return LOG_STR("UNKNOWN_NAME"); | ||||||
|   if (err == NOISE_ERROR_MAC_FAILURE) |   if (err == NOISE_ERROR_MAC_FAILURE) | ||||||
|     return "MAC_FAILURE"; |     return LOG_STR("MAC_FAILURE"); | ||||||
|   if (err == NOISE_ERROR_NOT_APPLICABLE) |   if (err == NOISE_ERROR_NOT_APPLICABLE) | ||||||
|     return "NOT_APPLICABLE"; |     return LOG_STR("NOT_APPLICABLE"); | ||||||
|   if (err == NOISE_ERROR_SYSTEM) |   if (err == NOISE_ERROR_SYSTEM) | ||||||
|     return "SYSTEM"; |     return LOG_STR("SYSTEM"); | ||||||
|   if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) |   if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) | ||||||
|     return "REMOTE_KEY_REQUIRED"; |     return LOG_STR("REMOTE_KEY_REQUIRED"); | ||||||
|   if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) |   if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) | ||||||
|     return "LOCAL_KEY_REQUIRED"; |     return LOG_STR("LOCAL_KEY_REQUIRED"); | ||||||
|   if (err == NOISE_ERROR_PSK_REQUIRED) |   if (err == NOISE_ERROR_PSK_REQUIRED) | ||||||
|     return "PSK_REQUIRED"; |     return LOG_STR("PSK_REQUIRED"); | ||||||
|   if (err == NOISE_ERROR_INVALID_LENGTH) |   if (err == NOISE_ERROR_INVALID_LENGTH) | ||||||
|     return "INVALID_LENGTH"; |     return LOG_STR("INVALID_LENGTH"); | ||||||
|   if (err == NOISE_ERROR_INVALID_PARAM) |   if (err == NOISE_ERROR_INVALID_PARAM) | ||||||
|     return "INVALID_PARAM"; |     return LOG_STR("INVALID_PARAM"); | ||||||
|   if (err == NOISE_ERROR_INVALID_STATE) |   if (err == NOISE_ERROR_INVALID_STATE) | ||||||
|     return "INVALID_STATE"; |     return LOG_STR("INVALID_STATE"); | ||||||
|   if (err == NOISE_ERROR_INVALID_NONCE) |   if (err == NOISE_ERROR_INVALID_NONCE) | ||||||
|     return "INVALID_NONCE"; |     return LOG_STR("INVALID_NONCE"); | ||||||
|   if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) |   if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) | ||||||
|     return "INVALID_PRIVATE_KEY"; |     return LOG_STR("INVALID_PRIVATE_KEY"); | ||||||
|   if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) |   if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) | ||||||
|     return "INVALID_PUBLIC_KEY"; |     return LOG_STR("INVALID_PUBLIC_KEY"); | ||||||
|   if (err == NOISE_ERROR_INVALID_FORMAT) |   if (err == NOISE_ERROR_INVALID_FORMAT) | ||||||
|     return "INVALID_FORMAT"; |     return LOG_STR("INVALID_FORMAT"); | ||||||
|   if (err == NOISE_ERROR_INVALID_SIGNATURE) |   if (err == NOISE_ERROR_INVALID_SIGNATURE) | ||||||
|     return "INVALID_SIGNATURE"; |     return LOG_STR("INVALID_SIGNATURE"); | ||||||
|   return to_string(err); |   return LOG_STR("UNKNOWN"); | ||||||
| } | } | ||||||
|  |  | ||||||
| /// Initialize the frame helper, returns OK if successful. | /// Initialize the frame helper, returns OK if successful. | ||||||
| @@ -75,7 +83,11 @@ APIError APINoiseFrameHelper::init() { | |||||||
|   // init prologue |   // init prologue | ||||||
|   size_t old_size = prologue_.size(); |   size_t old_size = prologue_.size(); | ||||||
|   prologue_.resize(old_size + PROLOGUE_INIT_LEN); |   prologue_.resize(old_size + PROLOGUE_INIT_LEN); | ||||||
|  | #ifdef USE_ESP8266 | ||||||
|  |   memcpy_P(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); | ||||||
|  | #else | ||||||
|   std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); |   std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); | ||||||
|  | #endif | ||||||
|  |  | ||||||
|   state_ = State::CLIENT_HELLO; |   state_ = State::CLIENT_HELLO; | ||||||
|   return APIError::OK; |   return APIError::OK; | ||||||
| @@ -83,18 +95,18 @@ APIError APINoiseFrameHelper::init() { | |||||||
| // Helper for handling handshake frame errors | // Helper for handling handshake frame errors | ||||||
| APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) { | APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) { | ||||||
|   if (aerr == APIError::BAD_INDICATOR) { |   if (aerr == APIError::BAD_INDICATOR) { | ||||||
|     send_explicit_handshake_reject_("Bad indicator byte"); |     send_explicit_handshake_reject_(LOG_STR("Bad indicator byte")); | ||||||
|   } else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { |   } else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { | ||||||
|     send_explicit_handshake_reject_("Bad handshake packet len"); |     send_explicit_handshake_reject_(LOG_STR("Bad handshake packet len")); | ||||||
|   } |   } | ||||||
|   return aerr; |   return aerr; | ||||||
| } | } | ||||||
|  |  | ||||||
| // Helper for handling noise library errors | // Helper for handling noise library errors | ||||||
| APIError APINoiseFrameHelper::handle_noise_error_(int err, const char *func_name, APIError api_err) { | APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func_name, APIError api_err) { | ||||||
|   if (err != 0) { |   if (err != 0) { | ||||||
|     state_ = State::FAILED; |     state_ = State::FAILED; | ||||||
|     HELPER_LOG("%s failed: %s", func_name, noise_err_to_str(err).c_str()); |     HELPER_LOG("%s failed: %s", LOG_STR_ARG(func_name), LOG_STR_ARG(noise_err_to_logstr(err))); | ||||||
|     return api_err; |     return api_err; | ||||||
|   } |   } | ||||||
|   return APIError::OK; |   return APIError::OK; | ||||||
| @@ -279,11 +291,11 @@ APIError APINoiseFrameHelper::state_action_() { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (frame.empty()) { |       if (frame.empty()) { | ||||||
|         send_explicit_handshake_reject_("Empty handshake message"); |         send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); | ||||||
|         return APIError::BAD_HANDSHAKE_ERROR_BYTE; |         return APIError::BAD_HANDSHAKE_ERROR_BYTE; | ||||||
|       } else if (frame[0] != 0x00) { |       } else if (frame[0] != 0x00) { | ||||||
|         HELPER_LOG("Bad handshake error byte: %u", frame[0]); |         HELPER_LOG("Bad handshake error byte: %u", frame[0]); | ||||||
|         send_explicit_handshake_reject_("Bad handshake error byte"); |         send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); | ||||||
|         return APIError::BAD_HANDSHAKE_ERROR_BYTE; |         return APIError::BAD_HANDSHAKE_ERROR_BYTE; | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -293,8 +305,10 @@ APIError APINoiseFrameHelper::state_action_() { | |||||||
|       err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); |       err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); | ||||||
|       if (err != 0) { |       if (err != 0) { | ||||||
|         // Special handling for MAC failure |         // Special handling for MAC failure | ||||||
|         send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error"); |         send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure") | ||||||
|         return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED); |                                                                        : LOG_STR("Handshake error")); | ||||||
|  |         return handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"), | ||||||
|  |                                    APIError::HANDSHAKESTATE_READ_FAILED); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       aerr = check_handshake_finished_(); |       aerr = check_handshake_finished_(); | ||||||
| @@ -307,8 +321,8 @@ APIError APINoiseFrameHelper::state_action_() { | |||||||
|       noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); |       noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); | ||||||
|  |  | ||||||
|       err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); |       err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); | ||||||
|       APIError aerr_write = |       APIError aerr_write = handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"), | ||||||
|           handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED); |                                                 APIError::HANDSHAKESTATE_WRITE_FAILED); | ||||||
|       if (aerr_write != APIError::OK) |       if (aerr_write != APIError::OK) | ||||||
|         return aerr_write; |         return aerr_write; | ||||||
|       buffer[0] = 0x00;  // success |       buffer[0] = 0x00;  // success | ||||||
| @@ -331,15 +345,31 @@ APIError APINoiseFrameHelper::state_action_() { | |||||||
|   } |   } | ||||||
|   return APIError::OK; |   return APIError::OK; | ||||||
| } | } | ||||||
| void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { | void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) { | ||||||
|  | #ifdef USE_STORE_LOG_STR_IN_FLASH | ||||||
|  |   // On ESP8266 with flash strings, we need to use PROGMEM-aware functions | ||||||
|  |   size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason)); | ||||||
|   std::vector<uint8_t> data; |   std::vector<uint8_t> data; | ||||||
|   data.resize(reason.length() + 1); |   data.resize(reason_len + 1); | ||||||
|  |   data[0] = 0x01;  // failure | ||||||
|  |  | ||||||
|  |   // Copy error message from PROGMEM | ||||||
|  |   if (reason_len > 0) { | ||||||
|  |     memcpy_P(data.data() + 1, reinterpret_cast<PGM_P>(reason), reason_len); | ||||||
|  |   } | ||||||
|  | #else | ||||||
|  |   // Normal memory access | ||||||
|  |   const char *reason_str = LOG_STR_ARG(reason); | ||||||
|  |   size_t reason_len = strlen(reason_str); | ||||||
|  |   std::vector<uint8_t> data; | ||||||
|  |   data.resize(reason_len + 1); | ||||||
|   data[0] = 0x01;  // failure |   data[0] = 0x01;  // failure | ||||||
|  |  | ||||||
|   // Copy error message in bulk |   // Copy error message in bulk | ||||||
|   if (!reason.empty()) { |   if (reason_len > 0) { | ||||||
|     std::memcpy(data.data() + 1, reason.c_str(), reason.length()); |     std::memcpy(data.data() + 1, reason_str, reason_len); | ||||||
|   } |   } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|   // temporarily remove failed state |   // temporarily remove failed state | ||||||
|   auto orig_state = state_; |   auto orig_state = state_; | ||||||
| @@ -368,7 +398,8 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { | |||||||
|   noise_buffer_init(mbuf); |   noise_buffer_init(mbuf); | ||||||
|   noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size()); |   noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size()); | ||||||
|   err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); |   err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); | ||||||
|   APIError decrypt_err = handle_noise_error_(err, "noise_cipherstate_decrypt", APIError::CIPHERSTATE_DECRYPT_FAILED); |   APIError decrypt_err = | ||||||
|  |       handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED); | ||||||
|   if (decrypt_err != APIError::OK) |   if (decrypt_err != APIError::OK) | ||||||
|     return decrypt_err; |     return decrypt_err; | ||||||
|  |  | ||||||
| @@ -450,7 +481,8 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st | |||||||
|                            4 + packet.payload_size + frame_footer_size_); |                            4 + packet.payload_size + frame_footer_size_); | ||||||
|  |  | ||||||
|     int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); |     int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); | ||||||
|     APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED); |     APIError aerr = | ||||||
|  |         handle_noise_error_(err, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED); | ||||||
|     if (aerr != APIError::OK) |     if (aerr != APIError::OK) | ||||||
|       return aerr; |       return aerr; | ||||||
|  |  | ||||||
| @@ -504,25 +536,27 @@ APIError APINoiseFrameHelper::init_handshake_() { | |||||||
|   nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; |   nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; | ||||||
|  |  | ||||||
|   err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); |   err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); | ||||||
|   APIError aerr = handle_noise_error_(err, "noise_handshakestate_new_by_id", APIError::HANDSHAKESTATE_SETUP_FAILED); |   APIError aerr = | ||||||
|  |       handle_noise_error_(err, LOG_STR("noise_handshakestate_new_by_id"), APIError::HANDSHAKESTATE_SETUP_FAILED); | ||||||
|   if (aerr != APIError::OK) |   if (aerr != APIError::OK) | ||||||
|     return aerr; |     return aerr; | ||||||
|  |  | ||||||
|   const auto &psk = ctx_->get_psk(); |   const auto &psk = ctx_->get_psk(); | ||||||
|   err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); |   err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); | ||||||
|   aerr = handle_noise_error_(err, "noise_handshakestate_set_pre_shared_key", APIError::HANDSHAKESTATE_SETUP_FAILED); |   aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"), | ||||||
|  |                              APIError::HANDSHAKESTATE_SETUP_FAILED); | ||||||
|   if (aerr != APIError::OK) |   if (aerr != APIError::OK) | ||||||
|     return aerr; |     return aerr; | ||||||
|  |  | ||||||
|   err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); |   err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); | ||||||
|   aerr = handle_noise_error_(err, "noise_handshakestate_set_prologue", APIError::HANDSHAKESTATE_SETUP_FAILED); |   aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_prologue"), APIError::HANDSHAKESTATE_SETUP_FAILED); | ||||||
|   if (aerr != APIError::OK) |   if (aerr != APIError::OK) | ||||||
|     return aerr; |     return aerr; | ||||||
|   // set_prologue copies it into handshakestate, so we can get rid of it now |   // set_prologue copies it into handshakestate, so we can get rid of it now | ||||||
|   prologue_ = {}; |   prologue_ = {}; | ||||||
|  |  | ||||||
|   err = noise_handshakestate_start(handshake_); |   err = noise_handshakestate_start(handshake_); | ||||||
|   aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED); |   aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED); | ||||||
|   if (aerr != APIError::OK) |   if (aerr != APIError::OK) | ||||||
|     return aerr; |     return aerr; | ||||||
|   return APIError::OK; |   return APIError::OK; | ||||||
| @@ -540,7 +574,8 @@ APIError APINoiseFrameHelper::check_handshake_finished_() { | |||||||
|     return APIError::HANDSHAKESTATE_BAD_STATE; |     return APIError::HANDSHAKESTATE_BAD_STATE; | ||||||
|   } |   } | ||||||
|   int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); |   int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); | ||||||
|   APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED); |   APIError aerr = | ||||||
|  |       handle_noise_error_(err, LOG_STR("noise_handshakestate_split"), APIError::HANDSHAKESTATE_SPLIT_FAILED); | ||||||
|   if (aerr != APIError::OK) |   if (aerr != APIError::OK) | ||||||
|     return aerr; |     return aerr; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ | |||||||
|  |  | ||||||
| namespace esphome::api { | namespace esphome::api { | ||||||
|  |  | ||||||
| class APINoiseFrameHelper : public APIFrameHelper { | class APINoiseFrameHelper final : public APIFrameHelper { | ||||||
|  public: |  public: | ||||||
|   APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx, |   APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx, | ||||||
|                       const ClientInfo *client_info) |                       const ClientInfo *client_info) | ||||||
| @@ -25,10 +25,6 @@ class APINoiseFrameHelper : public APIFrameHelper { | |||||||
|   APIError read_packet(ReadPacketBuffer *buffer) override; |   APIError read_packet(ReadPacketBuffer *buffer) override; | ||||||
|   APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; |   APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; | ||||||
|   APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override; |   APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override; | ||||||
|   // Get the frame header padding required by this protocol |  | ||||||
|   uint8_t frame_header_padding() override { return frame_header_padding_; } |  | ||||||
|   // Get the frame footer size required by this protocol |  | ||||||
|   uint8_t frame_footer_size() override { return frame_footer_size_; } |  | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   APIError state_action_(); |   APIError state_action_(); | ||||||
| @@ -36,9 +32,9 @@ class APINoiseFrameHelper : public APIFrameHelper { | |||||||
|   APIError write_frame_(const uint8_t *data, uint16_t len); |   APIError write_frame_(const uint8_t *data, uint16_t len); | ||||||
|   APIError init_handshake_(); |   APIError init_handshake_(); | ||||||
|   APIError check_handshake_finished_(); |   APIError check_handshake_finished_(); | ||||||
|   void send_explicit_handshake_reject_(const std::string &reason); |   void send_explicit_handshake_reject_(const LogString *reason); | ||||||
|   APIError handle_handshake_frame_error_(APIError aerr); |   APIError handle_handshake_frame_error_(APIError aerr); | ||||||
|   APIError handle_noise_error_(int err, const char *func_name, APIError api_err); |   APIError handle_noise_error_(int err, const LogString *func_name, APIError api_err); | ||||||
|  |  | ||||||
|   // Pointers first (4 bytes each) |   // Pointers first (4 bytes each) | ||||||
|   NoiseHandshakeState *handshake_{nullptr}; |   NoiseHandshakeState *handshake_{nullptr}; | ||||||
|   | |||||||
| @@ -10,6 +10,10 @@ | |||||||
| #include <cstring> | #include <cstring> | ||||||
| #include <cinttypes> | #include <cinttypes> | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP8266 | ||||||
|  | #include <pgmspace.h> | ||||||
|  | #endif | ||||||
|  |  | ||||||
| namespace esphome::api { | namespace esphome::api { | ||||||
|  |  | ||||||
| static const char *const TAG = "api.plaintext"; | static const char *const TAG = "api.plaintext"; | ||||||
| @@ -197,11 +201,20 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { | |||||||
|       // We must send at least 3 bytes to be read, so we add |       // We must send at least 3 bytes to be read, so we add | ||||||
|       // a message after the indicator byte to ensures its long |       // a message after the indicator byte to ensures its long | ||||||
|       // enough and can aid in debugging. |       // enough and can aid in debugging. | ||||||
|       const char msg[] = "\x00" |       static constexpr uint8_t INDICATOR_MSG_SIZE = 19; | ||||||
|                          "Bad indicator byte"; | #ifdef USE_ESP8266 | ||||||
|  |       static const char MSG_PROGMEM[] PROGMEM = "\x00" | ||||||
|  |                                                 "Bad indicator byte"; | ||||||
|  |       char msg[INDICATOR_MSG_SIZE]; | ||||||
|  |       memcpy_P(msg, MSG_PROGMEM, INDICATOR_MSG_SIZE); | ||||||
|       iov[0].iov_base = (void *) msg; |       iov[0].iov_base = (void *) msg; | ||||||
|       iov[0].iov_len = 19; | #else | ||||||
|       this->write_raw_(iov, 1, 19); |       static const char MSG[] = "\x00" | ||||||
|  |                                 "Bad indicator byte"; | ||||||
|  |       iov[0].iov_base = (void *) MSG; | ||||||
|  | #endif | ||||||
|  |       iov[0].iov_len = INDICATOR_MSG_SIZE; | ||||||
|  |       this->write_raw_(iov, 1, INDICATOR_MSG_SIZE); | ||||||
|     } |     } | ||||||
|     return aerr; |     return aerr; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
|  |  | ||||||
| namespace esphome::api { | namespace esphome::api { | ||||||
|  |  | ||||||
| class APIPlaintextFrameHelper : public APIFrameHelper { | class APIPlaintextFrameHelper final : public APIFrameHelper { | ||||||
|  public: |  public: | ||||||
|   APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info) |   APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info) | ||||||
|       : APIFrameHelper(std::move(socket), client_info) { |       : APIFrameHelper(std::move(socket), client_info) { | ||||||
| @@ -22,9 +22,6 @@ class APIPlaintextFrameHelper : public APIFrameHelper { | |||||||
|   APIError read_packet(ReadPacketBuffer *buffer) override; |   APIError read_packet(ReadPacketBuffer *buffer) override; | ||||||
|   APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; |   APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; | ||||||
|   APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override; |   APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override; | ||||||
|   uint8_t frame_header_padding() override { return frame_header_padding_; } |  | ||||||
|   // Get the frame footer size required by this protocol |  | ||||||
|   uint8_t frame_footer_size() override { return frame_footer_size_; } |  | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   APIError try_read_frame_(std::vector<uint8_t> *frame); |   APIError try_read_frame_(std::vector<uint8_t> *frame); | ||||||
|   | |||||||
| @@ -901,6 +901,16 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel | |||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  | bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 2: | ||||||
|  |       this->timezone = value.as_string(); | ||||||
|  |       break; | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  |   return true; | ||||||
|  | } | ||||||
| bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { | bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||||
|   switch (field_id) { |   switch (field_id) { | ||||||
|     case 1: |     case 1: | ||||||
| @@ -911,8 +921,6 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { | |||||||
|   } |   } | ||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
| void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->epoch_seconds); } |  | ||||||
| void GetTimeResponse::calculate_size(ProtoSize &size) const { size.add_fixed32(1, this->epoch_seconds); } |  | ||||||
| #ifdef USE_API_SERVICES | #ifdef USE_API_SERVICES | ||||||
| void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { | void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { | ||||||
|   buffer.encode_string(1, this->name_ref_); |   buffer.encode_string(1, this->name_ref_); | ||||||
| @@ -2153,10 +2161,12 @@ void BluetoothDeviceClearCacheResponse::calculate_size(ProtoSize &size) const { | |||||||
| void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const { | void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const { | ||||||
|   buffer.encode_uint32(1, static_cast<uint32_t>(this->state)); |   buffer.encode_uint32(1, static_cast<uint32_t>(this->state)); | ||||||
|   buffer.encode_uint32(2, static_cast<uint32_t>(this->mode)); |   buffer.encode_uint32(2, static_cast<uint32_t>(this->mode)); | ||||||
|  |   buffer.encode_uint32(3, static_cast<uint32_t>(this->configured_mode)); | ||||||
| } | } | ||||||
| void BluetoothScannerStateResponse::calculate_size(ProtoSize &size) const { | void BluetoothScannerStateResponse::calculate_size(ProtoSize &size) const { | ||||||
|   size.add_uint32(1, static_cast<uint32_t>(this->state)); |   size.add_uint32(1, static_cast<uint32_t>(this->state)); | ||||||
|   size.add_uint32(1, static_cast<uint32_t>(this->mode)); |   size.add_uint32(1, static_cast<uint32_t>(this->mode)); | ||||||
|  |   size.add_uint32(1, static_cast<uint32_t>(this->configured_mode)); | ||||||
| } | } | ||||||
| bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||||
|   switch (field_id) { |   switch (field_id) { | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1110,7 +1110,11 @@ void HomeAssistantStateResponse::dump_to(std::string &out) const { | |||||||
| } | } | ||||||
| #endif | #endif | ||||||
| void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } | void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } | ||||||
| void GetTimeResponse::dump_to(std::string &out) const { dump_field(out, "epoch_seconds", this->epoch_seconds); } | void GetTimeResponse::dump_to(std::string &out) const { | ||||||
|  |   MessageDumpHelper helper(out, "GetTimeResponse"); | ||||||
|  |   dump_field(out, "epoch_seconds", this->epoch_seconds); | ||||||
|  |   dump_field(out, "timezone", this->timezone); | ||||||
|  | } | ||||||
| #ifdef USE_API_SERVICES | #ifdef USE_API_SERVICES | ||||||
| void ListEntitiesServicesArgument::dump_to(std::string &out) const { | void ListEntitiesServicesArgument::dump_to(std::string &out) const { | ||||||
|   MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); |   MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); | ||||||
| @@ -1704,6 +1708,7 @@ void BluetoothScannerStateResponse::dump_to(std::string &out) const { | |||||||
|   MessageDumpHelper helper(out, "BluetoothScannerStateResponse"); |   MessageDumpHelper helper(out, "BluetoothScannerStateResponse"); | ||||||
|   dump_field(out, "state", static_cast<enums::BluetoothScannerState>(this->state)); |   dump_field(out, "state", static_cast<enums::BluetoothScannerState>(this->state)); | ||||||
|   dump_field(out, "mode", static_cast<enums::BluetoothScannerMode>(this->mode)); |   dump_field(out, "mode", static_cast<enums::BluetoothScannerMode>(this->mode)); | ||||||
|  |   dump_field(out, "configured_mode", static_cast<enums::BluetoothScannerMode>(this->configured_mode)); | ||||||
| } | } | ||||||
| void BluetoothScannerSetModeRequest::dump_to(std::string &out) const { | void BluetoothScannerSetModeRequest::dump_to(std::string &out) const { | ||||||
|   MessageDumpHelper helper(out, "BluetoothScannerSetModeRequest"); |   MessageDumpHelper helper(out, "BluetoothScannerSetModeRequest"); | ||||||
|   | |||||||
| @@ -160,15 +160,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | |||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
| #endif | #endif | ||||||
|     case GetTimeRequest::MESSAGE_TYPE: { |  | ||||||
|       GetTimeRequest msg; |  | ||||||
|       // Empty message: no decode needed |  | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP |  | ||||||
|       ESP_LOGVV(TAG, "on_get_time_request: %s", msg.dump().c_str()); |  | ||||||
| #endif |  | ||||||
|       this->on_get_time_request(msg); |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|     case GetTimeResponse::MESSAGE_TYPE: { |     case GetTimeResponse::MESSAGE_TYPE: { | ||||||
|       GetTimeResponse msg; |       GetTimeResponse msg; | ||||||
|       msg.decode(msg_data, msg_size); |       msg.decode(msg_data, msg_size); | ||||||
| @@ -656,11 +647,6 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc | |||||||
|   } |   } | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { |  | ||||||
|   if (this->check_connection_setup_() && !this->send_get_time_response(msg)) { |  | ||||||
|     this->on_fatal_error(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| #ifdef USE_API_SERVICES | #ifdef USE_API_SERVICES | ||||||
| void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { | void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { | ||||||
|   if (this->check_authenticated_()) { |   if (this->check_authenticated_()) { | ||||||
|   | |||||||
| @@ -71,7 +71,7 @@ class APIServerConnectionBase : public ProtoService { | |||||||
| #ifdef USE_API_HOMEASSISTANT_STATES | #ifdef USE_API_HOMEASSISTANT_STATES | ||||||
|   virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; |   virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; | ||||||
| #endif | #endif | ||||||
|   virtual void on_get_time_request(const GetTimeRequest &value){}; |  | ||||||
|   virtual void on_get_time_response(const GetTimeResponse &value){}; |   virtual void on_get_time_response(const GetTimeResponse &value){}; | ||||||
|  |  | ||||||
| #ifdef USE_API_SERVICES | #ifdef USE_API_SERVICES | ||||||
| @@ -226,7 +226,6 @@ class APIServerConnection : public APIServerConnectionBase { | |||||||
| #ifdef USE_API_HOMEASSISTANT_STATES | #ifdef USE_API_HOMEASSISTANT_STATES | ||||||
|   virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; |   virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; | ||||||
| #endif | #endif | ||||||
|   virtual bool send_get_time_response(const GetTimeRequest &msg) = 0; |  | ||||||
| #ifdef USE_API_SERVICES | #ifdef USE_API_SERVICES | ||||||
|   virtual void execute_service(const ExecuteServiceRequest &msg) = 0; |   virtual void execute_service(const ExecuteServiceRequest &msg) = 0; | ||||||
| #endif | #endif | ||||||
| @@ -348,7 +347,6 @@ class APIServerConnection : public APIServerConnectionBase { | |||||||
| #ifdef USE_API_HOMEASSISTANT_STATES | #ifdef USE_API_HOMEASSISTANT_STATES | ||||||
|   void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; |   void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; | ||||||
| #endif | #endif | ||||||
|   void on_get_time_request(const GetTimeRequest &msg) override; |  | ||||||
| #ifdef USE_API_SERVICES | #ifdef USE_API_SERVICES | ||||||
|   void on_execute_service_request(const ExecuteServiceRequest &msg) override; |   void on_execute_service_request(const ExecuteServiceRequest &msg) override; | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -62,9 +62,11 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: | |||||||
|         time_ = datetime.now() |         time_ = datetime.now() | ||||||
|         message: bytes = msg.message |         message: bytes = msg.message | ||||||
|         text = message.decode("utf8", "backslashreplace") |         text = message.decode("utf8", "backslashreplace") | ||||||
|         for parsed_msg in parse_log_message( |         nanoseconds = time_.microsecond // 1000 | ||||||
|             text, f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]" |         timestamp = ( | ||||||
|         ): |             f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]" | ||||||
|  |         ) | ||||||
|  |         for parsed_msg in parse_log_message(text, timestamp): | ||||||
|             print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg) |             print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg) | ||||||
|  |  | ||||||
|     stop = await async_run(cli, on_log, name=name) |     stop = await async_run(cli, on_log, name=name) | ||||||
|   | |||||||
| @@ -8,74 +8,70 @@ namespace esphome::api { | |||||||
| static const char *const TAG = "api.proto"; | static const char *const TAG = "api.proto"; | ||||||
|  |  | ||||||
| void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { | void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { | ||||||
|   uint32_t i = 0; |   const uint8_t *ptr = buffer; | ||||||
|   bool error = false; |   const uint8_t *end = buffer + length; | ||||||
|   while (i < length) { |  | ||||||
|  |   while (ptr < end) { | ||||||
|     uint32_t consumed; |     uint32_t consumed; | ||||||
|     auto res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); |  | ||||||
|  |     // Parse field header | ||||||
|  |     auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed); | ||||||
|     if (!res.has_value()) { |     if (!res.has_value()) { | ||||||
|       ESP_LOGV(TAG, "Invalid field start at %" PRIu32, i); |       ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer)); | ||||||
|       break; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     uint32_t field_type = (res->as_uint32()) & 0b111; |     uint32_t tag = res->as_uint32(); | ||||||
|     uint32_t field_id = (res->as_uint32()) >> 3; |     uint32_t field_type = tag & 0b111; | ||||||
|     i += consumed; |     uint32_t field_id = tag >> 3; | ||||||
|  |     ptr += consumed; | ||||||
|  |  | ||||||
|     switch (field_type) { |     switch (field_type) { | ||||||
|       case 0: {  // VarInt |       case 0: {  // VarInt | ||||||
|         res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); |         res = ProtoVarInt::parse(ptr, end - ptr, &consumed); | ||||||
|         if (!res.has_value()) { |         if (!res.has_value()) { | ||||||
|           ESP_LOGV(TAG, "Invalid VarInt at %" PRIu32, i); |           ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer)); | ||||||
|           error = true; |           return; | ||||||
|           break; |  | ||||||
|         } |         } | ||||||
|         if (!this->decode_varint(field_id, *res)) { |         if (!this->decode_varint(field_id, *res)) { | ||||||
|           ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32()); |           ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32()); | ||||||
|         } |         } | ||||||
|         i += consumed; |         ptr += consumed; | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|       case 2: {  // Length-delimited |       case 2: {  // Length-delimited | ||||||
|         res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); |         res = ProtoVarInt::parse(ptr, end - ptr, &consumed); | ||||||
|         if (!res.has_value()) { |         if (!res.has_value()) { | ||||||
|           ESP_LOGV(TAG, "Invalid Length Delimited at %" PRIu32, i); |           ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer)); | ||||||
|           error = true; |           return; | ||||||
|           break; |  | ||||||
|         } |         } | ||||||
|         uint32_t field_length = res->as_uint32(); |         uint32_t field_length = res->as_uint32(); | ||||||
|         i += consumed; |         ptr += consumed; | ||||||
|         if (field_length > length - i) { |         if (ptr + field_length > end) { | ||||||
|           ESP_LOGV(TAG, "Out-of-bounds Length Delimited at %" PRIu32, i); |           ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer)); | ||||||
|           error = true; |           return; | ||||||
|           break; |  | ||||||
|         } |         } | ||||||
|         if (!this->decode_length(field_id, ProtoLengthDelimited(&buffer[i], field_length))) { |         if (!this->decode_length(field_id, ProtoLengthDelimited(ptr, field_length))) { | ||||||
|           ESP_LOGV(TAG, "Cannot decode Length Delimited field %" PRIu32 "!", field_id); |           ESP_LOGV(TAG, "Cannot decode Length Delimited field %" PRIu32 "!", field_id); | ||||||
|         } |         } | ||||||
|         i += field_length; |         ptr += field_length; | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|       case 5: {  // 32-bit |       case 5: {  // 32-bit | ||||||
|         if (length - i < 4) { |         if (ptr + 4 > end) { | ||||||
|           ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at %" PRIu32, i); |           ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer)); | ||||||
|           error = true; |           return; | ||||||
|           break; |  | ||||||
|         } |         } | ||||||
|         uint32_t val = encode_uint32(buffer[i + 3], buffer[i + 2], buffer[i + 1], buffer[i]); |         uint32_t val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]); | ||||||
|         if (!this->decode_32bit(field_id, Proto32Bit(val))) { |         if (!this->decode_32bit(field_id, Proto32Bit(val))) { | ||||||
|           ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val); |           ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val); | ||||||
|         } |         } | ||||||
|         i += 4; |         ptr += 4; | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|       default: |       default: | ||||||
|         ESP_LOGV(TAG, "Invalid field type at %" PRIu32, i); |         ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer)); | ||||||
|         error = true; |         return; | ||||||
|         break; |  | ||||||
|     } |  | ||||||
|     if (error) { |  | ||||||
|       break; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ from esphome.const import ( | |||||||
|     PLATFORM_LN882X, |     PLATFORM_LN882X, | ||||||
|     PLATFORM_RTL87XX, |     PLATFORM_RTL87XX, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, CoroPriority, coroutine_with_priority | ||||||
|  |  | ||||||
| CODEOWNERS = ["@esphome/core"] | CODEOWNERS = ["@esphome/core"] | ||||||
|  |  | ||||||
| @@ -27,7 +27,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @coroutine_with_priority(200.0) | @coroutine_with_priority(CoroPriority.NETWORK_TRANSPORT) | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     if CORE.is_esp32 or CORE.is_libretiny: |     if CORE.is_esp32 or CORE.is_libretiny: | ||||||
|         # https://github.com/ESP32Async/AsyncTCP |         # https://github.com/ESP32Async/AsyncTCP | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ from esphome.const import ( | |||||||
|     DEVICE_CLASS_ENERGY, |     DEVICE_CLASS_ENERGY, | ||||||
|     DEVICE_CLASS_POWER, |     DEVICE_CLASS_POWER, | ||||||
|     DEVICE_CLASS_POWER_FACTOR, |     DEVICE_CLASS_POWER_FACTOR, | ||||||
|  |     DEVICE_CLASS_REACTIVE_POWER, | ||||||
|     DEVICE_CLASS_VOLTAGE, |     DEVICE_CLASS_VOLTAGE, | ||||||
|     ICON_CURRENT_AC, |     ICON_CURRENT_AC, | ||||||
|     ICON_LIGHTBULB, |     ICON_LIGHTBULB, | ||||||
| @@ -78,6 +79,7 @@ CONFIG_SCHEMA = ( | |||||||
|                 unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, |                 unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, | ||||||
|                 icon=ICON_LIGHTBULB, |                 icon=ICON_LIGHTBULB, | ||||||
|                 accuracy_decimals=2, |                 accuracy_decimals=2, | ||||||
|  |                 device_class=DEVICE_CLASS_REACTIVE_POWER, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( |             cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( | ||||||
|   | |||||||
| @@ -17,10 +17,12 @@ from esphome.const import ( | |||||||
|     CONF_REACTIVE_POWER, |     CONF_REACTIVE_POWER, | ||||||
|     CONF_REVERSE_ACTIVE_ENERGY, |     CONF_REVERSE_ACTIVE_ENERGY, | ||||||
|     CONF_VOLTAGE, |     CONF_VOLTAGE, | ||||||
|  |     DEVICE_CLASS_APPARENT_POWER, | ||||||
|     DEVICE_CLASS_CURRENT, |     DEVICE_CLASS_CURRENT, | ||||||
|     DEVICE_CLASS_ENERGY, |     DEVICE_CLASS_ENERGY, | ||||||
|     DEVICE_CLASS_POWER, |     DEVICE_CLASS_POWER, | ||||||
|     DEVICE_CLASS_POWER_FACTOR, |     DEVICE_CLASS_POWER_FACTOR, | ||||||
|  |     DEVICE_CLASS_REACTIVE_POWER, | ||||||
|     DEVICE_CLASS_TEMPERATURE, |     DEVICE_CLASS_TEMPERATURE, | ||||||
|     DEVICE_CLASS_VOLTAGE, |     DEVICE_CLASS_VOLTAGE, | ||||||
|     ENTITY_CATEGORY_DIAGNOSTIC, |     ENTITY_CATEGORY_DIAGNOSTIC, | ||||||
| @@ -100,13 +102,13 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( | |||||||
|             unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, |             unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, | ||||||
|             icon=ICON_LIGHTBULB, |             icon=ICON_LIGHTBULB, | ||||||
|             accuracy_decimals=2, |             accuracy_decimals=2, | ||||||
|             device_class=DEVICE_CLASS_POWER, |             device_class=DEVICE_CLASS_REACTIVE_POWER, | ||||||
|             state_class=STATE_CLASS_MEASUREMENT, |             state_class=STATE_CLASS_MEASUREMENT, | ||||||
|         ), |         ), | ||||||
|         cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( |         cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( | ||||||
|             unit_of_measurement=UNIT_VOLT_AMPS, |             unit_of_measurement=UNIT_VOLT_AMPS, | ||||||
|             accuracy_decimals=2, |             accuracy_decimals=2, | ||||||
|             device_class=DEVICE_CLASS_POWER, |             device_class=DEVICE_CLASS_APPARENT_POWER, | ||||||
|             state_class=STATE_CLASS_MEASUREMENT, |             state_class=STATE_CLASS_MEASUREMENT, | ||||||
|         ), |         ), | ||||||
|         cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( |         cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ from esphome import automation | |||||||
| 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_MIC_GAIN | from esphome.const import CONF_ID, CONF_MIC_GAIN | ||||||
| from esphome.core import coroutine_with_priority | from esphome.core import CoroPriority, coroutine_with_priority | ||||||
|  |  | ||||||
| CODEOWNERS = ["@kbx81"] | CODEOWNERS = ["@kbx81"] | ||||||
| IS_PLATFORM_COMPONENT = True | IS_PLATFORM_COMPONENT = True | ||||||
| @@ -35,7 +35,7 @@ async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args): | |||||||
|     return var |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
| @coroutine_with_priority(100.0) | @coroutine_with_priority(CoroPriority.CORE) | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     cg.add_define("USE_AUDIO_ADC") |     cg.add_define("USE_AUDIO_ADC") | ||||||
|     cg.add_global(audio_adc_ns.using) |     cg.add_global(audio_adc_ns.using) | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ from esphome.automation import maybe_simple_id | |||||||
| 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_VOLUME | from esphome.const import CONF_ID, CONF_VOLUME | ||||||
| from esphome.core import coroutine_with_priority | from esphome.core import CoroPriority, coroutine_with_priority | ||||||
|  |  | ||||||
| CODEOWNERS = ["@kbx81"] | CODEOWNERS = ["@kbx81"] | ||||||
| IS_PLATFORM_COMPONENT = True | IS_PLATFORM_COMPONENT = True | ||||||
| @@ -51,7 +51,7 @@ async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): | |||||||
|     return var |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
| @coroutine_with_priority(100.0) | @coroutine_with_priority(CoroPriority.CORE) | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     cg.add_define("USE_AUDIO_DAC") |     cg.add_define("USE_AUDIO_DAC") | ||||||
|     cg.add_global(audio_dac_ns.using) |     cg.add_global(audio_dac_ns.using) | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ constexpr static const uint8_t AXS_READ_TOUCHPAD[11] = {0xb5, 0xab, 0xa5, 0x5a, | |||||||
|  |  | ||||||
| #define ERROR_CHECK(err) \ | #define ERROR_CHECK(err) \ | ||||||
|   if ((err) != i2c::ERROR_OK) { \ |   if ((err) != i2c::ERROR_OK) { \ | ||||||
|     this->status_set_warning("Failed to communicate"); \ |     this->status_set_warning(LOG_STR("Failed to communicate")); \ | ||||||
|     return; \ |     return; \ | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -493,7 +493,7 @@ void BedJetHub::dump_config() { | |||||||
|                 "  ble_client.app_id: %d\n" |                 "  ble_client.app_id: %d\n" | ||||||
|                 "  ble_client.conn_id: %d", |                 "  ble_client.conn_id: %d", | ||||||
|                 this->get_name().c_str(), this->parent()->app_id, this->parent()->get_conn_id()); |                 this->get_name().c_str(), this->parent()->app_id, this->parent()->get_conn_id()); | ||||||
|   LOG_UPDATE_INTERVAL(this) |   LOG_UPDATE_INTERVAL(this); | ||||||
|   ESP_LOGCONFIG(TAG, "  Child components (%d):", this->children_.size()); |   ESP_LOGCONFIG(TAG, "  Child components (%d):", this->children_.size()); | ||||||
|   for (auto *child : this->children_) { |   for (auto *child : this->children_) { | ||||||
|     ESP_LOGCONFIG(TAG, "    - %s", child->describe().c_str()); |     ESP_LOGCONFIG(TAG, "    - %s", child->describe().c_str()); | ||||||
|   | |||||||
| @@ -59,7 +59,7 @@ from esphome.const import ( | |||||||
|     DEVICE_CLASS_VIBRATION, |     DEVICE_CLASS_VIBRATION, | ||||||
|     DEVICE_CLASS_WINDOW, |     DEVICE_CLASS_WINDOW, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, CoroPriority, coroutine_with_priority | ||||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||||
| from esphome.cpp_generator import MockObjClass | from esphome.cpp_generator import MockObjClass | ||||||
| from esphome.util import Registry | from esphome.util import Registry | ||||||
| @@ -652,7 +652,7 @@ async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args) | |||||||
|     return cg.new_Pvariable(condition_id, template_arg, paren, False) |     return cg.new_Pvariable(condition_id, template_arg, paren, False) | ||||||
|  |  | ||||||
|  |  | ||||||
| @coroutine_with_priority(100.0) | @coroutine_with_priority(CoroPriority.CORE) | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     cg.add_global(binary_sensor_ns.using) |     cg.add_global(binary_sensor_ns.using) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,6 +7,19 @@ namespace binary_sensor { | |||||||
|  |  | ||||||
| static const char *const TAG = "binary_sensor"; | static const char *const TAG = "binary_sensor"; | ||||||
|  |  | ||||||
|  | // Function implementation of LOG_BINARY_SENSOR macro to reduce code size | ||||||
|  | void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj) { | ||||||
|  |   if (obj == nullptr) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); | ||||||
|  |  | ||||||
|  |   if (!obj->get_device_class_ref().empty()) { | ||||||
|  |     ESP_LOGCONFIG(tag, "%s  Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| void BinarySensor::publish_state(bool new_state) { | void BinarySensor::publish_state(bool new_state) { | ||||||
|   if (this->filter_list_ == nullptr) { |   if (this->filter_list_ == nullptr) { | ||||||
|     this->send_state_internal(new_state); |     this->send_state_internal(new_state); | ||||||
|   | |||||||
| @@ -10,13 +10,10 @@ namespace esphome { | |||||||
|  |  | ||||||
| namespace binary_sensor { | namespace binary_sensor { | ||||||
|  |  | ||||||
| #define LOG_BINARY_SENSOR(prefix, type, obj) \ | class BinarySensor; | ||||||
|   if ((obj) != nullptr) { \ | void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj); | ||||||
|     ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ |  | ||||||
|     if (!(obj)->get_device_class().empty()) { \ | #define LOG_BINARY_SENSOR(prefix, type, obj) log_binary_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj) | ||||||
|       ESP_LOGCONFIG(TAG, "%s  Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ |  | ||||||
|     } \ |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| #define SUB_BINARY_SENSOR(name) \ | #define SUB_BINARY_SENSOR(name) \ | ||||||
|  protected: \ |  protected: \ | ||||||
|   | |||||||
| @@ -1 +1,6 @@ | |||||||
| CODEOWNERS = ["@tobias-"] | import esphome.codegen as cg | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@tobias-", "@dan-s-github"] | ||||||
|  |  | ||||||
|  | CONF_BL0940_ID = "bl0940_id" | ||||||
|  | bl0940_ns = cg.esphome_ns.namespace("bl0940") | ||||||
|   | |||||||
| @@ -7,28 +7,26 @@ namespace bl0940 { | |||||||
|  |  | ||||||
| static const char *const TAG = "bl0940"; | static const char *const TAG = "bl0940"; | ||||||
|  |  | ||||||
| static const uint8_t BL0940_READ_COMMAND = 0x50;  // 0x58 according to documentation |  | ||||||
| static const uint8_t BL0940_FULL_PACKET = 0xAA; | static const uint8_t BL0940_FULL_PACKET = 0xAA; | ||||||
| static const uint8_t BL0940_PACKET_HEADER = 0x55;  // 0x58 according to documentation | static const uint8_t BL0940_PACKET_HEADER = 0x55;  // 0x58 according to en doc but 0x55 in cn doc | ||||||
|  |  | ||||||
| static const uint8_t BL0940_WRITE_COMMAND = 0xA0;  // 0xA8 according to documentation |  | ||||||
| static const uint8_t BL0940_REG_I_FAST_RMS_CTRL = 0x10; | static const uint8_t BL0940_REG_I_FAST_RMS_CTRL = 0x10; | ||||||
| static const uint8_t BL0940_REG_MODE = 0x18; | static const uint8_t BL0940_REG_MODE = 0x18; | ||||||
| static const uint8_t BL0940_REG_SOFT_RESET = 0x19; | static const uint8_t BL0940_REG_SOFT_RESET = 0x19; | ||||||
| static const uint8_t BL0940_REG_USR_WRPROT = 0x1A; | static const uint8_t BL0940_REG_USR_WRPROT = 0x1A; | ||||||
| static const uint8_t BL0940_REG_TPS_CTRL = 0x1B; | static const uint8_t BL0940_REG_TPS_CTRL = 0x1B; | ||||||
|  |  | ||||||
| const uint8_t BL0940_INIT[5][6] = { | static const uint8_t BL0940_INIT[5][5] = { | ||||||
|     // Reset to default |     // Reset to default | ||||||
|     {BL0940_WRITE_COMMAND, BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, |     {BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, | ||||||
|     // Enable User Operation Write |     // Enable User Operation Write | ||||||
|     {BL0940_WRITE_COMMAND, BL0940_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, |     {BL0940_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, | ||||||
|     // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS |     // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS | ||||||
|     {BL0940_WRITE_COMMAND, BL0940_REG_MODE, 0x00, 0x10, 0x00, 0x37}, |     {BL0940_REG_MODE, 0x00, 0x10, 0x00, 0x37}, | ||||||
|     // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS |     // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS | ||||||
|     {BL0940_WRITE_COMMAND, BL0940_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, |     {BL0940_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, | ||||||
|     // 0x181C = Half cycle, Fast RMS threshold 6172 |     // 0x181C = Half cycle, Fast RMS threshold 6172 | ||||||
|     {BL0940_WRITE_COMMAND, BL0940_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; |     {BL0940_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; | ||||||
|  |  | ||||||
| void BL0940::loop() { | void BL0940::loop() { | ||||||
|   DataPacket buffer; |   DataPacket buffer; | ||||||
| @@ -36,8 +34,8 @@ void BL0940::loop() { | |||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (read_array((uint8_t *) &buffer, sizeof(buffer))) { |   if (read_array((uint8_t *) &buffer, sizeof(buffer))) { | ||||||
|     if (validate_checksum(&buffer)) { |     if (this->validate_checksum_(&buffer)) { | ||||||
|       received_package_(&buffer); |       this->received_package_(&buffer); | ||||||
|     } |     } | ||||||
|   } else { |   } else { | ||||||
|     ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); |     ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); | ||||||
| @@ -46,35 +44,151 @@ void BL0940::loop() { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| bool BL0940::validate_checksum(const DataPacket *data) { | bool BL0940::validate_checksum_(DataPacket *data) { | ||||||
|   uint8_t checksum = BL0940_READ_COMMAND; |   uint8_t checksum = this->read_command_; | ||||||
|   // Whole package but checksum |   // Whole package but checksum | ||||||
|   for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) { |   uint8_t *raw = (uint8_t *) data; | ||||||
|     checksum += data->raw[i]; |   for (uint32_t i = 0; i < sizeof(*data) - 1; i++) { | ||||||
|  |     checksum += raw[i]; | ||||||
|   } |   } | ||||||
|   checksum ^= 0xFF; |   checksum ^= 0xFF; | ||||||
|   if (checksum != data->checksum) { |   if (checksum != data->checksum) { | ||||||
|     ESP_LOGW(TAG, "BL0940 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); |     ESP_LOGW(TAG, "Invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); | ||||||
|   } |   } | ||||||
|   return checksum == data->checksum; |   return checksum == data->checksum; | ||||||
| } | } | ||||||
|  |  | ||||||
| void BL0940::update() { | void BL0940::update() { | ||||||
|   this->flush(); |   this->flush(); | ||||||
|   this->write_byte(BL0940_READ_COMMAND); |   this->write_byte(this->read_command_); | ||||||
|   this->write_byte(BL0940_FULL_PACKET); |   this->write_byte(BL0940_FULL_PACKET); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BL0940::setup() { | void BL0940::setup() { | ||||||
|  | #ifdef USE_NUMBER | ||||||
|  |   // add calibration callbacks | ||||||
|  |   if (this->voltage_calibration_number_ != nullptr) { | ||||||
|  |     this->voltage_calibration_number_->add_on_state_callback( | ||||||
|  |         [this](float state) { this->voltage_calibration_callback_(state); }); | ||||||
|  |     if (this->voltage_calibration_number_->has_state()) { | ||||||
|  |       this->voltage_calibration_callback_(this->voltage_calibration_number_->state); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (this->current_calibration_number_ != nullptr) { | ||||||
|  |     this->current_calibration_number_->add_on_state_callback( | ||||||
|  |         [this](float state) { this->current_calibration_callback_(state); }); | ||||||
|  |     if (this->current_calibration_number_->has_state()) { | ||||||
|  |       this->current_calibration_callback_(this->current_calibration_number_->state); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (this->power_calibration_number_ != nullptr) { | ||||||
|  |     this->power_calibration_number_->add_on_state_callback( | ||||||
|  |         [this](float state) { this->power_calibration_callback_(state); }); | ||||||
|  |     if (this->power_calibration_number_->has_state()) { | ||||||
|  |       this->power_calibration_callback_(this->power_calibration_number_->state); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (this->energy_calibration_number_ != nullptr) { | ||||||
|  |     this->energy_calibration_number_->add_on_state_callback( | ||||||
|  |         [this](float state) { this->energy_calibration_callback_(state); }); | ||||||
|  |     if (this->energy_calibration_number_->has_state()) { | ||||||
|  |       this->energy_calibration_callback_(this->energy_calibration_number_->state); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |   // calculate calibrated reference values | ||||||
|  |   this->voltage_reference_cal_ = this->voltage_reference_ / this->voltage_cal_; | ||||||
|  |   this->current_reference_cal_ = this->current_reference_ / this->current_cal_; | ||||||
|  |   this->power_reference_cal_ = this->power_reference_ / this->power_cal_; | ||||||
|  |   this->energy_reference_cal_ = this->energy_reference_ / this->energy_cal_; | ||||||
|  |  | ||||||
|   for (auto *i : BL0940_INIT) { |   for (auto *i : BL0940_INIT) { | ||||||
|     this->write_array(i, 6); |     this->write_byte(this->write_command_), this->write_array(i, 5); | ||||||
|     delay(1); |     delay(1); | ||||||
|   } |   } | ||||||
|   this->flush(); |   this->flush(); | ||||||
| } | } | ||||||
|  |  | ||||||
| float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { | float BL0940::calculate_power_reference_() { | ||||||
|   auto tb = (float) (temperature.h << 8 | temperature.l); |   // calculate power reference based on voltage and current reference | ||||||
|  |   return this->voltage_reference_cal_ * this->current_reference_cal_ * 4046 / 324004 / 79931; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | float BL0940::calculate_energy_reference_() { | ||||||
|  |   // formula: 3600000 * 4046 * RL * R1 * 1000 / (1638.4 * 256) / Vref² / (R1 + R2) | ||||||
|  |   // or:  power_reference_ * 3600000 / (1638.4 * 256) | ||||||
|  |   return this->power_reference_cal_ * 3600000 / (1638.4 * 256); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | float BL0940::calculate_calibration_value_(float state) { return (100 + state) / 100; } | ||||||
|  |  | ||||||
|  | void BL0940::reset_calibration() { | ||||||
|  | #ifdef USE_NUMBER | ||||||
|  |   if (this->current_calibration_number_ != nullptr && this->current_cal_ != 1) { | ||||||
|  |     this->current_calibration_number_->make_call().set_value(0).perform(); | ||||||
|  |   } | ||||||
|  |   if (this->voltage_calibration_number_ != nullptr && this->voltage_cal_ != 1) { | ||||||
|  |     this->voltage_calibration_number_->make_call().set_value(0).perform(); | ||||||
|  |   } | ||||||
|  |   if (this->power_calibration_number_ != nullptr && this->power_cal_ != 1) { | ||||||
|  |     this->power_calibration_number_->make_call().set_value(0).perform(); | ||||||
|  |   } | ||||||
|  |   if (this->energy_calibration_number_ != nullptr && this->energy_cal_ != 1) { | ||||||
|  |     this->energy_calibration_number_->make_call().set_value(0).perform(); | ||||||
|  |   } | ||||||
|  | #endif | ||||||
|  |   ESP_LOGD(TAG, "external calibration values restored to initial state"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void BL0940::current_calibration_callback_(float state) { | ||||||
|  |   this->current_cal_ = this->calculate_calibration_value_(state); | ||||||
|  |   ESP_LOGV(TAG, "update current calibration state: %f", this->current_cal_); | ||||||
|  |   this->recalibrate_(); | ||||||
|  | } | ||||||
|  | void BL0940::voltage_calibration_callback_(float state) { | ||||||
|  |   this->voltage_cal_ = this->calculate_calibration_value_(state); | ||||||
|  |   ESP_LOGV(TAG, "update voltage calibration state: %f", this->voltage_cal_); | ||||||
|  |   this->recalibrate_(); | ||||||
|  | } | ||||||
|  | void BL0940::power_calibration_callback_(float state) { | ||||||
|  |   this->power_cal_ = this->calculate_calibration_value_(state); | ||||||
|  |   ESP_LOGV(TAG, "update power calibration state: %f", this->power_cal_); | ||||||
|  |   this->recalibrate_(); | ||||||
|  | } | ||||||
|  | void BL0940::energy_calibration_callback_(float state) { | ||||||
|  |   this->energy_cal_ = this->calculate_calibration_value_(state); | ||||||
|  |   ESP_LOGV(TAG, "update energy calibration state: %f", this->energy_cal_); | ||||||
|  |   this->recalibrate_(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void BL0940::recalibrate_() { | ||||||
|  |   ESP_LOGV(TAG, "Recalibrating reference values"); | ||||||
|  |   this->voltage_reference_cal_ = this->voltage_reference_ / this->voltage_cal_; | ||||||
|  |   this->current_reference_cal_ = this->current_reference_ / this->current_cal_; | ||||||
|  |  | ||||||
|  |   if (this->voltage_cal_ != 1 || this->current_cal_ != 1) { | ||||||
|  |     this->power_reference_ = this->calculate_power_reference_(); | ||||||
|  |   } | ||||||
|  |   this->power_reference_cal_ = this->power_reference_ / this->power_cal_; | ||||||
|  |  | ||||||
|  |   if (this->voltage_cal_ != 1 || this->current_cal_ != 1 || this->power_cal_ != 1) { | ||||||
|  |     this->energy_reference_ = this->calculate_energy_reference_(); | ||||||
|  |   } | ||||||
|  |   this->energy_reference_cal_ = this->energy_reference_ / this->energy_cal_; | ||||||
|  |  | ||||||
|  |   ESP_LOGD(TAG, | ||||||
|  |            "Recalibrated reference values:\n" | ||||||
|  |            "Voltage: %f\n, Current: %f\n, Power: %f\n, Energy: %f\n", | ||||||
|  |            this->voltage_reference_cal_, this->current_reference_cal_, this->power_reference_cal_, | ||||||
|  |            this->energy_reference_cal_); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | float BL0940::update_temp_(sensor::Sensor *sensor, uint16_le_t temperature) const { | ||||||
|  |   auto tb = (float) temperature; | ||||||
|   float converted_temp = ((float) 170 / 448) * (tb / 2 - 32) - 45; |   float converted_temp = ((float) 170 / 448) * (tb / 2 - 32) - 45; | ||||||
|   if (sensor != nullptr) { |   if (sensor != nullptr) { | ||||||
|     if (sensor->has_state() && std::abs(converted_temp - sensor->get_state()) > max_temperature_diff_) { |     if (sensor->has_state() && std::abs(converted_temp - sensor->get_state()) > max_temperature_diff_) { | ||||||
| @@ -87,33 +201,40 @@ float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { | |||||||
|   return converted_temp; |   return converted_temp; | ||||||
| } | } | ||||||
|  |  | ||||||
| void BL0940::received_package_(const DataPacket *data) const { | void BL0940::received_package_(DataPacket *data) { | ||||||
|   // Bad header |   // Bad header | ||||||
|   if (data->frame_header != BL0940_PACKET_HEADER) { |   if (data->frame_header != BL0940_PACKET_HEADER) { | ||||||
|     ESP_LOGI(TAG, "Invalid data. Header mismatch: %d", data->frame_header); |     ESP_LOGI(TAG, "Invalid data. Header mismatch: %d", data->frame_header); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_; |   // cf_cnt is only 24 bits, so track overflows | ||||||
|   float i_rms = (float) to_uint32_t(data->i_rms) / current_reference_; |   uint32_t cf_cnt = (uint24_t) data->cf_cnt; | ||||||
|   float watt = (float) to_int32_t(data->watt) / power_reference_; |   cf_cnt |= this->prev_cf_cnt_ & 0xff000000; | ||||||
|   uint32_t cf_cnt = to_uint32_t(data->cf_cnt); |   if (cf_cnt < this->prev_cf_cnt_) { | ||||||
|   float total_energy_consumption = (float) cf_cnt / energy_reference_; |     cf_cnt += 0x1000000; | ||||||
|  |   } | ||||||
|  |   this->prev_cf_cnt_ = cf_cnt; | ||||||
|  |  | ||||||
|   float tps1 = update_temp_(internal_temperature_sensor_, data->tps1); |   float v_rms = (uint24_t) data->v_rms / this->voltage_reference_cal_; | ||||||
|   float tps2 = update_temp_(external_temperature_sensor_, data->tps2); |   float i_rms = (uint24_t) data->i_rms / this->current_reference_cal_; | ||||||
|  |   float watt = (int24_t) data->watt / this->power_reference_cal_; | ||||||
|  |   float total_energy_consumption = cf_cnt / this->energy_reference_cal_; | ||||||
|  |  | ||||||
|   if (voltage_sensor_ != nullptr) { |   float tps1 = update_temp_(this->internal_temperature_sensor_, data->tps1); | ||||||
|     voltage_sensor_->publish_state(v_rms); |   float tps2 = update_temp_(this->external_temperature_sensor_, data->tps2); | ||||||
|  |  | ||||||
|  |   if (this->voltage_sensor_ != nullptr) { | ||||||
|  |     this->voltage_sensor_->publish_state(v_rms); | ||||||
|   } |   } | ||||||
|   if (current_sensor_ != nullptr) { |   if (this->current_sensor_ != nullptr) { | ||||||
|     current_sensor_->publish_state(i_rms); |     this->current_sensor_->publish_state(i_rms); | ||||||
|   } |   } | ||||||
|   if (power_sensor_ != nullptr) { |   if (this->power_sensor_ != nullptr) { | ||||||
|     power_sensor_->publish_state(watt); |     this->power_sensor_->publish_state(watt); | ||||||
|   } |   } | ||||||
|   if (energy_sensor_ != nullptr) { |   if (this->energy_sensor_ != nullptr) { | ||||||
|     energy_sensor_->publish_state(total_energy_consumption); |     this->energy_sensor_->publish_state(total_energy_consumption); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   ESP_LOGV(TAG, "BL0940: U %fV, I %fA, P %fW, Cnt %" PRId32 ", ∫P %fkWh, T1 %f°C, T2 %f°C", v_rms, i_rms, watt, cf_cnt, |   ESP_LOGV(TAG, "BL0940: U %fV, I %fA, P %fW, Cnt %" PRId32 ", ∫P %fkWh, T1 %f°C, T2 %f°C", v_rms, i_rms, watt, cf_cnt, | ||||||
| @@ -121,7 +242,27 @@ void BL0940::received_package_(const DataPacket *data) const { | |||||||
| } | } | ||||||
|  |  | ||||||
| void BL0940::dump_config() {  // NOLINT(readability-function-cognitive-complexity) | void BL0940::dump_config() {  // NOLINT(readability-function-cognitive-complexity) | ||||||
|   ESP_LOGCONFIG(TAG, "BL0940:"); |   ESP_LOGCONFIG(TAG, | ||||||
|  |                 "BL0940:\n" | ||||||
|  |                 "  LEGACY MODE: %s\n" | ||||||
|  |                 "  READ  CMD: 0x%02X\n" | ||||||
|  |                 "  WRITE CMD: 0x%02X\n" | ||||||
|  |                 "  ------------------\n" | ||||||
|  |                 "  Current reference: %f\n" | ||||||
|  |                 "  Energy reference: %f\n" | ||||||
|  |                 "  Power reference: %f\n" | ||||||
|  |                 "  Voltage reference: %f\n", | ||||||
|  |                 TRUEFALSE(this->legacy_mode_enabled_), this->read_command_, this->write_command_, | ||||||
|  |                 this->current_reference_, this->energy_reference_, this->power_reference_, this->voltage_reference_); | ||||||
|  | #ifdef USE_NUMBER | ||||||
|  |   ESP_LOGCONFIG(TAG, | ||||||
|  |                 "BL0940:\n" | ||||||
|  |                 "  Current calibration: %f\n" | ||||||
|  |                 "  Energy calibration: %f\n" | ||||||
|  |                 "  Power calibration: %f\n" | ||||||
|  |                 "  Voltage calibration: %f\n", | ||||||
|  |                 this->current_cal_, this->energy_cal_, this->power_cal_, this->voltage_cal_); | ||||||
|  | #endif | ||||||
|   LOG_SENSOR("", "Voltage", this->voltage_sensor_); |   LOG_SENSOR("", "Voltage", this->voltage_sensor_); | ||||||
|   LOG_SENSOR("", "Current", this->current_sensor_); |   LOG_SENSOR("", "Current", this->current_sensor_); | ||||||
|   LOG_SENSOR("", "Power", this->power_sensor_); |   LOG_SENSOR("", "Power", this->power_sensor_); | ||||||
| @@ -130,9 +271,5 @@ void BL0940::dump_config() {  // NOLINT(readability-function-cognitive-complexit | |||||||
|   LOG_SENSOR("", "External temperature", this->external_temperature_sensor_); |   LOG_SENSOR("", "External temperature", this->external_temperature_sensor_); | ||||||
| } | } | ||||||
|  |  | ||||||
| uint32_t BL0940::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } |  | ||||||
|  |  | ||||||
| int32_t BL0940::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } |  | ||||||
|  |  | ||||||
| }  // namespace bl0940 | }  // namespace bl0940 | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -1,66 +1,48 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/components/uart/uart.h" | #include "esphome/core/datatypes.h" | ||||||
|  | #include "esphome/core/defines.h" | ||||||
|  | #ifdef USE_BUTTON | ||||||
|  | #include "esphome/components/button/button.h" | ||||||
|  | #endif | ||||||
|  | #ifdef USE_NUMBER | ||||||
|  | #include "esphome/components/number/number.h" | ||||||
|  | #endif | ||||||
| #include "esphome/components/sensor/sensor.h" | #include "esphome/components/sensor/sensor.h" | ||||||
|  | #include "esphome/components/uart/uart.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace bl0940 { | namespace bl0940 { | ||||||
|  |  | ||||||
| static const float BL0940_PREF = 1430; |  | ||||||
| static const float BL0940_UREF = 33000; |  | ||||||
| static const float BL0940_IREF = 275000;  // 2750 from tasmota. Seems to generate values 100 times too high |  | ||||||
|  |  | ||||||
| // Measured to 297J  per click according to power consumption of 5 minutes |  | ||||||
| // Converted to kWh (3.6MJ per kwH). Used to be 256 * 1638.4 |  | ||||||
| static const float BL0940_EREF = 3.6e6 / 297; |  | ||||||
|  |  | ||||||
| struct ube24_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align) |  | ||||||
|   uint8_t l; |  | ||||||
|   uint8_t m; |  | ||||||
|   uint8_t h; |  | ||||||
| } __attribute__((packed)); |  | ||||||
|  |  | ||||||
| struct ube16_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align) |  | ||||||
|   uint8_t l; |  | ||||||
|   uint8_t h; |  | ||||||
| } __attribute__((packed)); |  | ||||||
|  |  | ||||||
| struct sbe24_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align) |  | ||||||
|   uint8_t l; |  | ||||||
|   uint8_t m; |  | ||||||
|   int8_t h; |  | ||||||
| } __attribute__((packed)); |  | ||||||
|  |  | ||||||
| // Caveat: All these values are big endian (low - middle - high) | // Caveat: All these values are big endian (low - middle - high) | ||||||
|  | struct DataPacket { | ||||||
| union DataPacket {  // NOLINT(altera-struct-pack-align) |   uint8_t frame_header;    // Packet header (0x58 in EN docs, 0x55 in CN docs and Tasmota tests) | ||||||
|   uint8_t raw[35]; |   uint24_le_t i_fast_rms;  // Fast RMS current | ||||||
|   struct { |   uint24_le_t i_rms;       // RMS current | ||||||
|     uint8_t frame_header;  // value of 0x58 according to docs. 0x55 according to Tasmota real world tests. Reality wins. |   uint24_t RESERVED0;      // Reserved | ||||||
|     ube24_t i_fast_rms;    // 0x00 |   uint24_le_t v_rms;       // RMS voltage | ||||||
|     ube24_t i_rms;         // 0x04 |   uint24_t RESERVED1;      // Reserved | ||||||
|     ube24_t RESERVED0;     // reserved |   int24_le_t watt;         // Active power (can be negative for bidirectional measurement) | ||||||
|     ube24_t v_rms;         // 0x06 |   uint24_t RESERVED2;      // Reserved | ||||||
|     ube24_t RESERVED1;     // reserved |   uint24_le_t cf_cnt;      // Energy pulse count | ||||||
|     sbe24_t watt;          // 0x08 |   uint24_t RESERVED3;      // Reserved | ||||||
|     ube24_t RESERVED2;     // reserved |   uint16_le_t tps1;        // Internal temperature sensor 1 | ||||||
|     ube24_t cf_cnt;        // 0x0A |   uint8_t RESERVED4;       // Reserved (should be 0x00) | ||||||
|     ube24_t RESERVED3;     // reserved |   uint16_le_t tps2;        // Internal temperature sensor 2 | ||||||
|     ube16_t tps1;          // 0x0c |   uint8_t RESERVED5;       // Reserved (should be 0x00) | ||||||
|     uint8_t RESERVED4;     // value of 0x00 |   uint8_t checksum;        // Packet checksum | ||||||
|     ube16_t tps2;          // 0x0c |  | ||||||
|     uint8_t RESERVED5;     // value of 0x00 |  | ||||||
|     uint8_t checksum;      // checksum |  | ||||||
|   }; |  | ||||||
| } __attribute__((packed)); | } __attribute__((packed)); | ||||||
|  |  | ||||||
| class BL0940 : public PollingComponent, public uart::UARTDevice { | class BL0940 : public PollingComponent, public uart::UARTDevice { | ||||||
|  public: |  public: | ||||||
|  |   // Sensor setters | ||||||
|   void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } |   void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } | ||||||
|   void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } |   void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } | ||||||
|   void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } |   void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } | ||||||
|   void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } |   void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } | ||||||
|  |  | ||||||
|  |   // Temperature sensor setters | ||||||
|   void set_internal_temperature_sensor(sensor::Sensor *internal_temperature_sensor) { |   void set_internal_temperature_sensor(sensor::Sensor *internal_temperature_sensor) { | ||||||
|     internal_temperature_sensor_ = internal_temperature_sensor; |     internal_temperature_sensor_ = internal_temperature_sensor; | ||||||
|   } |   } | ||||||
| @@ -68,42 +50,105 @@ class BL0940 : public PollingComponent, public uart::UARTDevice { | |||||||
|     external_temperature_sensor_ = external_temperature_sensor; |     external_temperature_sensor_ = external_temperature_sensor; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void loop() override; |   // Configuration setters | ||||||
|  |   void set_legacy_mode(bool enable) { this->legacy_mode_enabled_ = enable; } | ||||||
|  |   void set_read_command(uint8_t read_command) { this->read_command_ = read_command; } | ||||||
|  |   void set_write_command(uint8_t write_command) { this->write_command_ = write_command; } | ||||||
|  |  | ||||||
|  |   // Reference value setters (used for calibration and conversion) | ||||||
|  |   void set_current_reference(float current_ref) { this->current_reference_ = current_ref; } | ||||||
|  |   void set_energy_reference(float energy_ref) { this->energy_reference_ = energy_ref; } | ||||||
|  |   void set_power_reference(float power_ref) { this->power_reference_ = power_ref; } | ||||||
|  |   void set_voltage_reference(float voltage_ref) { this->voltage_reference_ = voltage_ref; } | ||||||
|  |  | ||||||
|  | #ifdef USE_NUMBER | ||||||
|  |   // Calibration number setters (for Home Assistant number entities) | ||||||
|  |   void set_current_calibration_number(number::Number *num) { this->current_calibration_number_ = num; } | ||||||
|  |   void set_voltage_calibration_number(number::Number *num) { this->voltage_calibration_number_ = num; } | ||||||
|  |   void set_power_calibration_number(number::Number *num) { this->power_calibration_number_ = num; } | ||||||
|  |   void set_energy_calibration_number(number::Number *num) { this->energy_calibration_number_ = num; } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_BUTTON | ||||||
|  |   // Resets all calibration values to defaults (can be triggered by a button) | ||||||
|  |   void reset_calibration(); | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |   // Core component methods | ||||||
|  |   void loop() override; | ||||||
|   void update() override; |   void update() override; | ||||||
|   void setup() override; |   void setup() override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   sensor::Sensor *voltage_sensor_{nullptr}; |   // --- Sensor pointers --- | ||||||
|   sensor::Sensor *current_sensor_{nullptr}; |   sensor::Sensor *voltage_sensor_{nullptr};               // Voltage sensor | ||||||
|   // NB This may be negative as the circuits is seemingly able to measure |   sensor::Sensor *current_sensor_{nullptr};               // Current sensor | ||||||
|   // power in both directions |   sensor::Sensor *power_sensor_{nullptr};                 // Power sensor (can be negative for bidirectional) | ||||||
|   sensor::Sensor *power_sensor_{nullptr}; |   sensor::Sensor *energy_sensor_{nullptr};                // Energy sensor | ||||||
|   sensor::Sensor *energy_sensor_{nullptr}; |   sensor::Sensor *internal_temperature_sensor_{nullptr};  // Internal temperature sensor | ||||||
|   sensor::Sensor *internal_temperature_sensor_{nullptr}; |   sensor::Sensor *external_temperature_sensor_{nullptr};  // External temperature sensor | ||||||
|   sensor::Sensor *external_temperature_sensor_{nullptr}; |  | ||||||
|  |  | ||||||
|   // Max difference between two measurements of the temperature. Used to avoid noise. | #ifdef USE_NUMBER | ||||||
|   float max_temperature_diff_{0}; |   // --- Calibration number entities (for dynamic calibration via HA UI) --- | ||||||
|   // Divide by this to turn into Watt |   number::Number *voltage_calibration_number_{nullptr}; | ||||||
|   float power_reference_ = BL0940_PREF; |   number::Number *current_calibration_number_{nullptr}; | ||||||
|   // Divide by this to turn into Volt |   number::Number *power_calibration_number_{nullptr}; | ||||||
|   float voltage_reference_ = BL0940_UREF; |   number::Number *energy_calibration_number_{nullptr}; | ||||||
|   // Divide by this to turn into Ampere | #endif | ||||||
|   float current_reference_ = BL0940_IREF; |  | ||||||
|   // Divide by this to turn into kWh |  | ||||||
|   float energy_reference_ = BL0940_EREF; |  | ||||||
|  |  | ||||||
|   float update_temp_(sensor::Sensor *sensor, ube16_t packed_temperature) const; |   // --- Internal state --- | ||||||
|  |   uint32_t prev_cf_cnt_ = 0;       // Previous energy pulse count (for energy calculation) | ||||||
|  |   float max_temperature_diff_{0};  // Max allowed temperature difference between two measurements (noise filter) | ||||||
|  |  | ||||||
|   static uint32_t to_uint32_t(ube24_t input); |   // --- Reference values for conversion --- | ||||||
|  |   float power_reference_;        // Divider for raw power to get Watts | ||||||
|  |   float power_reference_cal_;    // Calibrated power reference | ||||||
|  |   float voltage_reference_;      // Divider for raw voltage to get Volts | ||||||
|  |   float voltage_reference_cal_;  // Calibrated voltage reference | ||||||
|  |   float current_reference_;      // Divider for raw current to get Amperes | ||||||
|  |   float current_reference_cal_;  // Calibrated current reference | ||||||
|  |   float energy_reference_;       // Divider for raw energy to get kWh | ||||||
|  |   float energy_reference_cal_;   // Calibrated energy reference | ||||||
|  |  | ||||||
|   static int32_t to_int32_t(sbe24_t input); |   // --- Home Assistant calibration values (multipliers, default 1) --- | ||||||
|  |   float current_cal_{1}; | ||||||
|  |   float voltage_cal_{1}; | ||||||
|  |   float power_cal_{1}; | ||||||
|  |   float energy_cal_{1}; | ||||||
|  |  | ||||||
|   static bool validate_checksum(const DataPacket *data); |   // --- Protocol commands --- | ||||||
|  |   uint8_t read_command_; | ||||||
|  |   uint8_t write_command_; | ||||||
|  |  | ||||||
|   void received_package_(const DataPacket *data) const; |   // --- Mode flags --- | ||||||
|  |   bool legacy_mode_enabled_ = true; | ||||||
|  |  | ||||||
|  |   // --- Methods --- | ||||||
|  |   // Converts packed temperature value to float and updates the sensor | ||||||
|  |   float update_temp_(sensor::Sensor *sensor, uint16_le_t packed_temperature) const; | ||||||
|  |  | ||||||
|  |   // Validates the checksum of a received data packet | ||||||
|  |   bool validate_checksum_(DataPacket *data); | ||||||
|  |  | ||||||
|  |   // Handles a received data packet | ||||||
|  |   void received_package_(DataPacket *data); | ||||||
|  |  | ||||||
|  |   // Calculates reference values for calibration and conversion | ||||||
|  |   float calculate_energy_reference_(); | ||||||
|  |   float calculate_power_reference_(); | ||||||
|  |   float calculate_calibration_value_(float state); | ||||||
|  |  | ||||||
|  |   // Calibration update callbacks (used with number entities) | ||||||
|  |   void current_calibration_callback_(float state); | ||||||
|  |   void voltage_calibration_callback_(float state); | ||||||
|  |   void power_calibration_callback_(float state); | ||||||
|  |   void energy_calibration_callback_(float state); | ||||||
|  |   void reset_calibration_callback_(); | ||||||
|  |  | ||||||
|  |   // Recalculates all reference values after calibration changes | ||||||
|  |   void recalibrate_(); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace bl0940 | }  // namespace bl0940 | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								esphome/components/bl0940/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/bl0940/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | from esphome.components import button | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART | ||||||
|  |  | ||||||
|  | from .. import CONF_BL0940_ID, bl0940_ns | ||||||
|  | from ..sensor import BL0940 | ||||||
|  |  | ||||||
|  | CalibrationResetButton = bl0940_ns.class_( | ||||||
|  |     "CalibrationResetButton", button.Button, cg.Component | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.All( | ||||||
|  |     button.button_schema( | ||||||
|  |         CalibrationResetButton, | ||||||
|  |         entity_category=ENTITY_CATEGORY_CONFIG, | ||||||
|  |         icon=ICON_RESTART, | ||||||
|  |     ) | ||||||
|  |     .extend({cv.GenerateID(CONF_BL0940_ID): cv.use_id(BL0940)}) | ||||||
|  |     .extend(cv.COMPONENT_SCHEMA) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = await button.new_button(config) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await cg.register_parented(var, config[CONF_BL0940_ID]) | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | #include "calibration_reset_button.h" | ||||||
|  | #include "../bl0940.h" | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/core/application.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace bl0940 { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "bl0940.button.calibration_reset"; | ||||||
|  |  | ||||||
|  | void CalibrationResetButton::dump_config() { LOG_BUTTON("", "Calibration Reset Button", this); } | ||||||
|  |  | ||||||
|  | void CalibrationResetButton::press_action() { | ||||||
|  |   ESP_LOGI(TAG, "Resetting calibration defaults..."); | ||||||
|  |   this->parent_->reset_calibration(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace bl0940 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										19
									
								
								esphome/components/bl0940/button/calibration_reset_button.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								esphome/components/bl0940/button/calibration_reset_button.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/components/button/button.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace bl0940 { | ||||||
|  |  | ||||||
|  | class BL0940;  // Forward declaration of BL0940 class | ||||||
|  |  | ||||||
|  | class CalibrationResetButton : public button::Button, public Component, public Parented<BL0940> { | ||||||
|  |  public: | ||||||
|  |   void dump_config() override; | ||||||
|  |  | ||||||
|  |   void press_action() override; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace bl0940 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										94
									
								
								esphome/components/bl0940/number/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								esphome/components/bl0940/number/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | from esphome.components import number | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_MAX_VALUE, | ||||||
|  |     CONF_MIN_VALUE, | ||||||
|  |     CONF_MODE, | ||||||
|  |     CONF_RESTORE_VALUE, | ||||||
|  |     CONF_STEP, | ||||||
|  |     ENTITY_CATEGORY_CONFIG, | ||||||
|  |     UNIT_PERCENT, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | from .. import CONF_BL0940_ID, bl0940_ns | ||||||
|  | from ..sensor import BL0940 | ||||||
|  |  | ||||||
|  | # Define calibration types | ||||||
|  | CONF_CURRENT_CALIBRATION = "current_calibration" | ||||||
|  | CONF_VOLTAGE_CALIBRATION = "voltage_calibration" | ||||||
|  | CONF_POWER_CALIBRATION = "power_calibration" | ||||||
|  | CONF_ENERGY_CALIBRATION = "energy_calibration" | ||||||
|  |  | ||||||
|  | BL0940Number = bl0940_ns.class_("BL0940Number") | ||||||
|  |  | ||||||
|  | CalibrationNumber = bl0940_ns.class_( | ||||||
|  |     "CalibrationNumber", number.Number, cg.PollingComponent | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_min_max(config): | ||||||
|  |     if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: | ||||||
|  |         raise cv.Invalid("max_value must be greater than min_value") | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CALIBRATION_SCHEMA = cv.All( | ||||||
|  |     number.number_schema( | ||||||
|  |         CalibrationNumber, | ||||||
|  |         entity_category=ENTITY_CATEGORY_CONFIG, | ||||||
|  |         unit_of_measurement=UNIT_PERCENT, | ||||||
|  |     ) | ||||||
|  |     .extend( | ||||||
|  |         { | ||||||
|  |             cv.Optional(CONF_MODE, default="BOX"): cv.enum(number.NUMBER_MODES), | ||||||
|  |             cv.Optional(CONF_MAX_VALUE, default=10): cv.All( | ||||||
|  |                 cv.float_, cv.Range(max=50) | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_MIN_VALUE, default=-10): cv.All( | ||||||
|  |                 cv.float_, cv.Range(min=-50) | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_STEP, default=0.1): cv.positive_float, | ||||||
|  |             cv.Optional(CONF_RESTORE_VALUE): cv.boolean, | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     .extend(cv.COMPONENT_SCHEMA), | ||||||
|  |     validate_min_max, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # Configuration schema for BL0940 numbers | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(BL0940Number), | ||||||
|  |         cv.GenerateID(CONF_BL0940_ID): cv.use_id(BL0940), | ||||||
|  |         cv.Optional(CONF_CURRENT_CALIBRATION): CALIBRATION_SCHEMA, | ||||||
|  |         cv.Optional(CONF_VOLTAGE_CALIBRATION): CALIBRATION_SCHEMA, | ||||||
|  |         cv.Optional(CONF_POWER_CALIBRATION): CALIBRATION_SCHEMA, | ||||||
|  |         cv.Optional(CONF_ENERGY_CALIBRATION): CALIBRATION_SCHEMA, | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     # Get the BL0940 component instance | ||||||
|  |     bl0940 = await cg.get_variable(config[CONF_BL0940_ID]) | ||||||
|  |  | ||||||
|  |     # Process all calibration types | ||||||
|  |     for cal_type, setter_method in [ | ||||||
|  |         (CONF_CURRENT_CALIBRATION, "set_current_calibration_number"), | ||||||
|  |         (CONF_VOLTAGE_CALIBRATION, "set_voltage_calibration_number"), | ||||||
|  |         (CONF_POWER_CALIBRATION, "set_power_calibration_number"), | ||||||
|  |         (CONF_ENERGY_CALIBRATION, "set_energy_calibration_number"), | ||||||
|  |     ]: | ||||||
|  |         if conf := config.get(cal_type): | ||||||
|  |             var = await number.new_number( | ||||||
|  |                 conf, | ||||||
|  |                 min_value=conf.get(CONF_MIN_VALUE), | ||||||
|  |                 max_value=conf.get(CONF_MAX_VALUE), | ||||||
|  |                 step=conf.get(CONF_STEP), | ||||||
|  |             ) | ||||||
|  |             await cg.register_component(var, conf) | ||||||
|  |  | ||||||
|  |             if restore_value := config.get(CONF_RESTORE_VALUE): | ||||||
|  |                 cg.add(var.set_restore_value(restore_value)) | ||||||
|  |             cg.add(getattr(bl0940, setter_method)(var)) | ||||||
							
								
								
									
										29
									
								
								esphome/components/bl0940/number/calibration_number.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								esphome/components/bl0940/number/calibration_number.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | #include "calibration_number.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace bl0940 { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "bl0940.number"; | ||||||
|  |  | ||||||
|  | void CalibrationNumber::setup() { | ||||||
|  |   float value = 0.0f; | ||||||
|  |   if (this->restore_value_) { | ||||||
|  |     this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash()); | ||||||
|  |     if (!this->pref_.load(&value)) { | ||||||
|  |       value = 0.0f; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   this->publish_state(value); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void CalibrationNumber::control(float value) { | ||||||
|  |   this->publish_state(value); | ||||||
|  |   if (this->restore_value_) | ||||||
|  |     this->pref_.save(&value); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void CalibrationNumber::dump_config() { LOG_NUMBER("", "Calibration Number", this); } | ||||||
|  |  | ||||||
|  | }  // namespace bl0940 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										26
									
								
								esphome/components/bl0940/number/calibration_number.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								esphome/components/bl0940/number/calibration_number.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/components/number/number.h" | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/preferences.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace bl0940 { | ||||||
|  |  | ||||||
|  | class CalibrationNumber : public number::Number, public Component { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||||
|  |  | ||||||
|  |   void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void control(float value) override; | ||||||
|  |   bool restore_value_{true}; | ||||||
|  |  | ||||||
|  |   ESPPreferenceObject pref_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace bl0940 | ||||||
|  | }  // namespace esphome | ||||||
| @@ -8,6 +8,7 @@ from esphome.const import ( | |||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_INTERNAL_TEMPERATURE, |     CONF_INTERNAL_TEMPERATURE, | ||||||
|     CONF_POWER, |     CONF_POWER, | ||||||
|  |     CONF_REFERENCE_VOLTAGE, | ||||||
|     CONF_VOLTAGE, |     CONF_VOLTAGE, | ||||||
|     DEVICE_CLASS_CURRENT, |     DEVICE_CLASS_CURRENT, | ||||||
|     DEVICE_CLASS_ENERGY, |     DEVICE_CLASS_ENERGY, | ||||||
| @@ -23,12 +24,133 @@ from esphome.const import ( | |||||||
|     UNIT_WATT, |     UNIT_WATT, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | from . import bl0940_ns | ||||||
|  |  | ||||||
| DEPENDENCIES = ["uart"] | DEPENDENCIES = ["uart"] | ||||||
|  |  | ||||||
|  |  | ||||||
| bl0940_ns = cg.esphome_ns.namespace("bl0940") |  | ||||||
| BL0940 = bl0940_ns.class_("BL0940", cg.PollingComponent, uart.UARTDevice) | BL0940 = bl0940_ns.class_("BL0940", cg.PollingComponent, uart.UARTDevice) | ||||||
|  |  | ||||||
|  | CONF_LEGACY_MODE = "legacy_mode" | ||||||
|  |  | ||||||
|  | CONF_READ_COMMAND = "read_command" | ||||||
|  | CONF_WRITE_COMMAND = "write_command" | ||||||
|  |  | ||||||
|  | CONF_RESISTOR_SHUNT = "resistor_shunt" | ||||||
|  | CONF_RESISTOR_ONE = "resistor_one" | ||||||
|  | CONF_RESISTOR_TWO = "resistor_two" | ||||||
|  |  | ||||||
|  | CONF_CURRENT_REFERENCE = "current_reference" | ||||||
|  | CONF_ENERGY_REFERENCE = "energy_reference" | ||||||
|  | CONF_POWER_REFERENCE = "power_reference" | ||||||
|  | CONF_VOLTAGE_REFERENCE = "voltage_reference" | ||||||
|  |  | ||||||
|  | DEFAULT_BL0940_READ_COMMAND = 0x58 | ||||||
|  | DEFAULT_BL0940_WRITE_COMMAND = 0xA1 | ||||||
|  |  | ||||||
|  | # Values according to BL0940 application note: | ||||||
|  | # https://www.belling.com.cn/media/file_object/bel_product/BL0940/guide/BL0940_APPNote_TSSOP14_V1.04_EN.pdf | ||||||
|  | DEFAULT_BL0940_VREF = 1.218  # Vref = 1.218 | ||||||
|  | DEFAULT_BL0940_RL = 1  # RL = 1 mΩ | ||||||
|  | DEFAULT_BL0940_R1 = 0.51  # R1 = 0.51 kΩ | ||||||
|  | DEFAULT_BL0940_R2 = 1950  # R2 = 5 x 390 kΩ -> 1950 kΩ | ||||||
|  |  | ||||||
|  | # ---------------------------------------------------- | ||||||
|  | # values from initial implementation | ||||||
|  | DEFAULT_BL0940_LEGACY_READ_COMMAND = 0x50 | ||||||
|  | DEFAULT_BL0940_LEGACY_WRITE_COMMAND = 0xA0 | ||||||
|  |  | ||||||
|  | DEFAULT_BL0940_LEGACY_UREF = 33000 | ||||||
|  | DEFAULT_BL0940_LEGACY_IREF = 275000 | ||||||
|  | DEFAULT_BL0940_LEGACY_PREF = 1430 | ||||||
|  | # Measured to 297J  per click according to power consumption of 5 minutes | ||||||
|  | # Converted to kWh (3.6MJ per kwH). Used to be 256 * 1638.4 | ||||||
|  | DEFAULT_BL0940_LEGACY_EREF = 3.6e6 / 297 | ||||||
|  | # ---------------------------------------------------- | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # methods to calculate voltage and current reference values | ||||||
|  | def calculate_voltage_reference(vref, r_one, r_two): | ||||||
|  |     # formula: 79931 / Vref * (R1 * 1000) / (R1 + R2) | ||||||
|  |     return 79931 / vref * (r_one * 1000) / (r_one + r_two) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def calculate_current_reference(vref, r_shunt): | ||||||
|  |     # formula: 324004 * RL / Vref | ||||||
|  |     return 324004 * r_shunt / vref | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def calculate_power_reference(voltage_reference, current_reference): | ||||||
|  |     # calculate power reference based on voltage and current reference | ||||||
|  |     return voltage_reference * current_reference * 4046 / 324004 / 79931 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def calculate_energy_reference(power_reference): | ||||||
|  |     # formula: power_reference * 3600000 / (1638.4 * 256) | ||||||
|  |     return power_reference * 3600000 / (1638.4 * 256) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_legacy_mode(config): | ||||||
|  |     # Only allow schematic calibration options if legacy_mode is False | ||||||
|  |     if config.get(CONF_LEGACY_MODE, True): | ||||||
|  |         forbidden = [ | ||||||
|  |             CONF_REFERENCE_VOLTAGE, | ||||||
|  |             CONF_RESISTOR_SHUNT, | ||||||
|  |             CONF_RESISTOR_ONE, | ||||||
|  |             CONF_RESISTOR_TWO, | ||||||
|  |         ] | ||||||
|  |         for key in forbidden: | ||||||
|  |             if key in config: | ||||||
|  |                 raise cv.Invalid( | ||||||
|  |                     f"Option '{key}' is only allowed when legacy_mode: false" | ||||||
|  |                 ) | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def set_command_defaults(config): | ||||||
|  |     # Set defaults for read_command and write_command based on legacy_mode | ||||||
|  |     legacy = config.get(CONF_LEGACY_MODE, True) | ||||||
|  |     if legacy: | ||||||
|  |         config.setdefault(CONF_READ_COMMAND, DEFAULT_BL0940_LEGACY_READ_COMMAND) | ||||||
|  |         config.setdefault(CONF_WRITE_COMMAND, DEFAULT_BL0940_LEGACY_WRITE_COMMAND) | ||||||
|  |     else: | ||||||
|  |         config.setdefault(CONF_READ_COMMAND, DEFAULT_BL0940_READ_COMMAND) | ||||||
|  |         config.setdefault(CONF_WRITE_COMMAND, DEFAULT_BL0940_WRITE_COMMAND) | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def set_reference_values(config): | ||||||
|  |     # Set default reference values based on legacy_mode | ||||||
|  |     if config.get(CONF_LEGACY_MODE, True): | ||||||
|  |         config.setdefault(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_LEGACY_UREF) | ||||||
|  |         config.setdefault(CONF_CURRENT_REFERENCE, DEFAULT_BL0940_LEGACY_IREF) | ||||||
|  |         config.setdefault(CONF_POWER_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) | ||||||
|  |         config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) | ||||||
|  |     else: | ||||||
|  |         vref = config.get(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_VREF) | ||||||
|  |         r_one = config.get(CONF_RESISTOR_ONE, DEFAULT_BL0940_R1) | ||||||
|  |         r_two = config.get(CONF_RESISTOR_TWO, DEFAULT_BL0940_R2) | ||||||
|  |         r_shunt = config.get(CONF_RESISTOR_SHUNT, DEFAULT_BL0940_RL) | ||||||
|  |  | ||||||
|  |         config.setdefault( | ||||||
|  |             CONF_VOLTAGE_REFERENCE, calculate_voltage_reference(vref, r_one, r_two) | ||||||
|  |         ) | ||||||
|  |         config.setdefault( | ||||||
|  |             CONF_CURRENT_REFERENCE, calculate_current_reference(vref, r_shunt) | ||||||
|  |         ) | ||||||
|  |         config.setdefault( | ||||||
|  |             CONF_POWER_REFERENCE, | ||||||
|  |             calculate_power_reference( | ||||||
|  |                 config.get(CONF_VOLTAGE_REFERENCE), config.get(CONF_CURRENT_REFERENCE) | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |         config.setdefault( | ||||||
|  |             CONF_ENERGY_REFERENCE, | ||||||
|  |             calculate_energy_reference(config.get(CONF_POWER_REFERENCE)), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = ( | CONFIG_SCHEMA = ( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
|         { |         { | ||||||
| @@ -69,10 +191,24 @@ CONFIG_SCHEMA = ( | |||||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, |                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|             ), |             ), | ||||||
|  |             cv.Optional(CONF_LEGACY_MODE, default=True): cv.boolean, | ||||||
|  |             cv.Optional(CONF_READ_COMMAND): cv.hex_uint8_t, | ||||||
|  |             cv.Optional(CONF_WRITE_COMMAND): cv.hex_uint8_t, | ||||||
|  |             cv.Optional(CONF_REFERENCE_VOLTAGE): cv.float_, | ||||||
|  |             cv.Optional(CONF_RESISTOR_SHUNT): cv.float_, | ||||||
|  |             cv.Optional(CONF_RESISTOR_ONE): cv.float_, | ||||||
|  |             cv.Optional(CONF_RESISTOR_TWO): cv.float_, | ||||||
|  |             cv.Optional(CONF_CURRENT_REFERENCE): cv.float_, | ||||||
|  |             cv.Optional(CONF_ENERGY_REFERENCE): cv.float_, | ||||||
|  |             cv.Optional(CONF_POWER_REFERENCE): cv.float_, | ||||||
|  |             cv.Optional(CONF_VOLTAGE_REFERENCE): cv.float_, | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     .extend(cv.polling_component_schema("60s")) |     .extend(cv.polling_component_schema("60s")) | ||||||
|     .extend(uart.UART_DEVICE_SCHEMA) |     .extend(uart.UART_DEVICE_SCHEMA) | ||||||
|  |     .add_extra(validate_legacy_mode) | ||||||
|  |     .add_extra(set_command_defaults) | ||||||
|  |     .add_extra(set_reference_values) | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -99,3 +235,16 @@ async def to_code(config): | |||||||
|     if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE): |     if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE): | ||||||
|         sens = await sensor.new_sensor(external_temperature_config) |         sens = await sensor.new_sensor(external_temperature_config) | ||||||
|         cg.add(var.set_external_temperature_sensor(sens)) |         cg.add(var.set_external_temperature_sensor(sens)) | ||||||
|  |  | ||||||
|  |     # enable legacy mode | ||||||
|  |     cg.add(var.set_legacy_mode(config.get(CONF_LEGACY_MODE))) | ||||||
|  |  | ||||||
|  |     # Set bl0940 commands after validator has determined which defaults to use if not set | ||||||
|  |     cg.add(var.set_read_command(config.get(CONF_READ_COMMAND))) | ||||||
|  |     cg.add(var.set_write_command(config.get(CONF_WRITE_COMMAND))) | ||||||
|  |  | ||||||
|  |     # Set reference values after validator has set the values either from defaults or calculated | ||||||
|  |     cg.add(var.set_current_reference(config.get(CONF_CURRENT_REFERENCE))) | ||||||
|  |     cg.add(var.set_voltage_reference(config.get(CONF_VOLTAGE_REFERENCE))) | ||||||
|  |     cg.add(var.set_power_reference(config.get(CONF_POWER_REFERENCE))) | ||||||
|  |     cg.add(var.set_energy_reference(config.get(CONF_ENERGY_REFERENCE))) | ||||||
|   | |||||||
| @@ -149,7 +149,7 @@ void BL0942::setup() { | |||||||
|   this->write_reg_(BL0942_REG_USR_WRPROT, 0); |   this->write_reg_(BL0942_REG_USR_WRPROT, 0); | ||||||
|  |  | ||||||
|   if (this->read_reg_(BL0942_REG_MODE) != mode) |   if (this->read_reg_(BL0942_REG_MODE) != mode) | ||||||
|     this->status_set_warning("BL0942 setup failed!"); |     this->status_set_warning(LOG_STR("BL0942 setup failed!")); | ||||||
|  |  | ||||||
|   this->flush(); |   this->flush(); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def to_code(config): | async def to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|     if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): |     if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): | ||||||
|         cg.add( |         cg.add( | ||||||
| @@ -63,6 +63,6 @@ def to_code(config): | |||||||
|         ) |         ) | ||||||
|         cg.add(var.set_char_uuid128(uuid128)) |         cg.add(var.set_char_uuid128(uuid128)) | ||||||
|     cg.add(var.set_require_response(config[CONF_REQUIRE_RESPONSE])) |     cg.add(var.set_require_response(config[CONF_REQUIRE_RESPONSE])) | ||||||
|     yield output.register_output(var, config) |     await output.register_output(var, config) | ||||||
|     yield ble_client.register_ble_node(var, config) |     await ble_client.register_ble_node(var, config) | ||||||
|     yield cg.register_component(var, config) |     await cg.register_component(var, config) | ||||||
|   | |||||||
| @@ -80,7 +80,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|         cv.Schema( |         cv.Schema( | ||||||
|             { |             { | ||||||
|                 cv.GenerateID(): cv.declare_id(BluetoothProxy), |                 cv.GenerateID(): cv.declare_id(BluetoothProxy), | ||||||
|                 cv.Optional(CONF_ACTIVE, default=False): cv.boolean, |                 cv.Optional(CONF_ACTIVE, default=True): cv.boolean, | ||||||
|                 cv.SplitDefault(CONF_CACHE_SERVICES, esp32_idf=True): cv.All( |                 cv.SplitDefault(CONF_CACHE_SERVICES, esp32_idf=True): cv.All( | ||||||
|                     cv.only_with_esp_idf, cv.boolean |                     cv.only_with_esp_idf, cv.boolean | ||||||
|                 ), |                 ), | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ namespace esphome::bluetooth_proxy { | |||||||
|  |  | ||||||
| class BluetoothProxy; | class BluetoothProxy; | ||||||
|  |  | ||||||
| class BluetoothConnection : public esp32_ble_client::BLEClientBase { | class BluetoothConnection final : public esp32_ble_client::BLEClientBase { | ||||||
|  public: |  public: | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void loop() override; |   void loop() override; | ||||||
|   | |||||||
| @@ -24,6 +24,9 @@ void BluetoothProxy::setup() { | |||||||
|   this->connections_free_response_.limit = BLUETOOTH_PROXY_MAX_CONNECTIONS; |   this->connections_free_response_.limit = BLUETOOTH_PROXY_MAX_CONNECTIONS; | ||||||
|   this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS; |   this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS; | ||||||
|  |  | ||||||
|  |   // Capture the configured scan mode from YAML before any API changes | ||||||
|  |   this->configured_scan_active_ = this->parent_->get_scan_active(); | ||||||
|  |  | ||||||
|   this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { |   this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { | ||||||
|     if (this->api_connection_ != nullptr) { |     if (this->api_connection_ != nullptr) { | ||||||
|       this->send_bluetooth_scanner_state_(state); |       this->send_bluetooth_scanner_state_(state); | ||||||
| @@ -36,6 +39,9 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta | |||||||
|   resp.state = static_cast<api::enums::BluetoothScannerState>(state); |   resp.state = static_cast<api::enums::BluetoothScannerState>(state); | ||||||
|   resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE |   resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE | ||||||
|                                                : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; |                                                : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; | ||||||
|  |   resp.configured_mode = this->configured_scan_active_ | ||||||
|  |                              ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE | ||||||
|  |                              : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; | ||||||
|   this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE); |   this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -183,6 +189,12 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest | |||||||
|         this->send_device_connection(msg.address, false); |         this->send_device_connection(msg.address, false); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |       if (!msg.has_address_type) { | ||||||
|  |         ESP_LOGE(TAG, "[%d] [%s] Missing address type in connect request", connection->get_connection_index(), | ||||||
|  |                  connection->address_str().c_str()); | ||||||
|  |         this->send_device_connection(msg.address, false); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|       if (connection->state() == espbt::ClientState::CONNECTED || |       if (connection->state() == espbt::ClientState::CONNECTED || | ||||||
|           connection->state() == espbt::ClientState::ESTABLISHED) { |           connection->state() == espbt::ClientState::ESTABLISHED) { | ||||||
|         this->log_connection_request_ignored_(connection, connection->state()); |         this->log_connection_request_ignored_(connection, connection->state()); | ||||||
| @@ -209,13 +221,9 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest | |||||||
|         connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE); |         connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE); | ||||||
|         this->log_connection_info_(connection, "v3 without cache"); |         this->log_connection_info_(connection, "v3 without cache"); | ||||||
|       } |       } | ||||||
|       if (msg.has_address_type) { |       uint64_to_bd_addr(msg.address, connection->remote_bda_); | ||||||
|         uint64_to_bd_addr(msg.address, connection->remote_bda_); |       connection->set_remote_addr_type(static_cast<esp_ble_addr_type_t>(msg.address_type)); | ||||||
|         connection->set_remote_addr_type(static_cast<esp_ble_addr_type_t>(msg.address_type)); |       connection->set_state(espbt::ClientState::DISCOVERED); | ||||||
|         connection->set_state(espbt::ClientState::DISCOVERED); |  | ||||||
|       } else { |  | ||||||
|         connection->set_state(espbt::ClientState::SEARCHING); |  | ||||||
|       } |  | ||||||
|       this->send_connections_free(); |       this->send_connections_free(); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -50,7 +50,7 @@ enum BluetoothProxySubscriptionFlag : uint32_t { | |||||||
|   SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0, |   SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { | class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, public Component { | ||||||
|   friend class BluetoothConnection;  // Allow connection to update connections_free_response_ |   friend class BluetoothConnection;  // Allow connection to update connections_free_response_ | ||||||
|  public: |  public: | ||||||
|   BluetoothProxy(); |   BluetoothProxy(); | ||||||
| @@ -130,7 +130,9 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | |||||||
|  |  | ||||||
|   std::string get_bluetooth_mac_address_pretty() { |   std::string get_bluetooth_mac_address_pretty() { | ||||||
|     const uint8_t *mac = esp_bt_dev_get_address(); |     const uint8_t *mac = esp_bt_dev_get_address(); | ||||||
|     return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); |     char buf[18]; | ||||||
|  |     format_mac_addr_upper(mac, buf); | ||||||
|  |     return std::string(buf); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
| @@ -161,7 +163,8 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | |||||||
|   // Group 4: 1-byte types grouped together |   // Group 4: 1-byte types grouped together | ||||||
|   bool active_; |   bool active_; | ||||||
|   uint8_t connection_count_{0}; |   uint8_t connection_count_{0}; | ||||||
|   // 2 bytes used, 2 bytes padding |   bool configured_scan_active_{false};  // Configured scan mode from YAML | ||||||
|  |   // 3 bytes used, 1 byte padding | ||||||
| }; | }; | ||||||
|  |  | ||||||
| extern BluetoothProxy *global_bluetooth_proxy;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | extern BluetoothProxy *global_bluetooth_proxy;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ from esphome.const import ( | |||||||
|     DEVICE_CLASS_RESTART, |     DEVICE_CLASS_RESTART, | ||||||
|     DEVICE_CLASS_UPDATE, |     DEVICE_CLASS_UPDATE, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, CoroPriority, coroutine_with_priority | ||||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||||
| from esphome.cpp_generator import MockObjClass | from esphome.cpp_generator import MockObjClass | ||||||
|  |  | ||||||
| @@ -134,6 +134,6 @@ async def button_press_to_code(config, action_id, template_arg, args): | |||||||
|     return cg.new_Pvariable(action_id, template_arg, paren) |     return cg.new_Pvariable(action_id, template_arg, paren) | ||||||
|  |  | ||||||
|  |  | ||||||
| @coroutine_with_priority(100.0) | @coroutine_with_priority(CoroPriority.CORE) | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     cg.add_global(button_ns.using) |     cg.add_global(button_ns.using) | ||||||
|   | |||||||
| @@ -6,6 +6,19 @@ namespace button { | |||||||
|  |  | ||||||
| static const char *const TAG = "button"; | static const char *const TAG = "button"; | ||||||
|  |  | ||||||
|  | // Function implementation of LOG_BUTTON macro to reduce code size | ||||||
|  | void log_button(const char *tag, const char *prefix, const char *type, Button *obj) { | ||||||
|  |   if (obj == nullptr) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); | ||||||
|  |  | ||||||
|  |   if (!obj->get_icon_ref().empty()) { | ||||||
|  |     ESP_LOGCONFIG(tag, "%s  Icon: '%s'", prefix, obj->get_icon_ref().c_str()); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| void Button::press() { | void Button::press() { | ||||||
|   ESP_LOGD(TAG, "'%s' Pressed.", this->get_name().c_str()); |   ESP_LOGD(TAG, "'%s' Pressed.", this->get_name().c_str()); | ||||||
|   this->press_action(); |   this->press_action(); | ||||||
|   | |||||||
| @@ -7,13 +7,10 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace button { | namespace button { | ||||||
|  |  | ||||||
| #define LOG_BUTTON(prefix, type, obj) \ | class Button; | ||||||
|   if ((obj) != nullptr) { \ | void log_button(const char *tag, const char *prefix, const char *type, Button *obj); | ||||||
|     ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ |  | ||||||
|     if (!(obj)->get_icon().empty()) { \ | #define LOG_BUTTON(prefix, type, obj) log_button(TAG, prefix, LOG_STR_LITERAL(type), obj) | ||||||
|       ESP_LOGCONFIG(TAG, "%s  Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ |  | ||||||
|     } \ |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| #define SUB_BUTTON(name) \ | #define SUB_BUTTON(name) \ | ||||||
|  protected: \ |  protected: \ | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								esphome/components/camera/buffer.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								esphome/components/camera/buffer.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <cinttypes> | ||||||
|  | #include <cstddef> | ||||||
|  |  | ||||||
|  | namespace esphome::camera { | ||||||
|  |  | ||||||
|  | /// Interface for a generic buffer that stores image data. | ||||||
|  | class Buffer { | ||||||
|  |  public: | ||||||
|  |   /// Returns a pointer to the buffer's data. | ||||||
|  |   virtual uint8_t *get_data_buffer() = 0; | ||||||
|  |   /// Returns the length of the buffer in bytes. | ||||||
|  |   virtual size_t get_data_length() = 0; | ||||||
|  |   virtual ~Buffer() = default; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace esphome::camera | ||||||
							
								
								
									
										20
									
								
								esphome/components/camera/buffer_impl.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								esphome/components/camera/buffer_impl.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | #include "buffer_impl.h" | ||||||
|  |  | ||||||
|  | namespace esphome::camera { | ||||||
|  |  | ||||||
|  | BufferImpl::BufferImpl(size_t size) { | ||||||
|  |   this->data_ = this->allocator_.allocate(size); | ||||||
|  |   this->size_ = size; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | BufferImpl::BufferImpl(CameraImageSpec *spec) { | ||||||
|  |   this->data_ = this->allocator_.allocate(spec->bytes_per_image()); | ||||||
|  |   this->size_ = spec->bytes_per_image(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | BufferImpl::~BufferImpl() { | ||||||
|  |   if (this->data_ != nullptr) | ||||||
|  |     this->allocator_.deallocate(this->data_, this->size_); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace esphome::camera | ||||||
							
								
								
									
										26
									
								
								esphome/components/camera/buffer_impl.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								esphome/components/camera/buffer_impl.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "buffer.h" | ||||||
|  | #include "camera.h" | ||||||
|  |  | ||||||
|  | namespace esphome::camera { | ||||||
|  |  | ||||||
|  | /// Default implementation of Buffer Interface. | ||||||
|  | /// Uses a RAMAllocator for memory reservation. | ||||||
|  | class BufferImpl : public Buffer { | ||||||
|  |  public: | ||||||
|  |   explicit BufferImpl(size_t size); | ||||||
|  |   explicit BufferImpl(CameraImageSpec *spec); | ||||||
|  |   // -------- Buffer -------- | ||||||
|  |   uint8_t *get_data_buffer() override { return data_; } | ||||||
|  |   size_t get_data_length() override { return size_; } | ||||||
|  |   // ------------------------ | ||||||
|  |   ~BufferImpl() override; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   RAMAllocator<uint8_t> allocator_; | ||||||
|  |   size_t size_{}; | ||||||
|  |   uint8_t *data_{}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace esphome::camera | ||||||
| @@ -15,6 +15,26 @@ namespace camera { | |||||||
|  */ |  */ | ||||||
| enum CameraRequester : uint8_t { IDLE, API_REQUESTER, WEB_REQUESTER }; | enum CameraRequester : uint8_t { IDLE, API_REQUESTER, WEB_REQUESTER }; | ||||||
|  |  | ||||||
|  | /// Enumeration of different pixel formats. | ||||||
|  | enum PixelFormat : uint8_t { | ||||||
|  |   PIXEL_FORMAT_GRAYSCALE = 0,  ///< 8-bit grayscale. | ||||||
|  |   PIXEL_FORMAT_RGB565,         ///< 16-bit RGB (5-6-5). | ||||||
|  |   PIXEL_FORMAT_BGR888,         ///< RGB pixel data in 8-bit format, stored as B, G, R (1 byte each). | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /// Returns string name for a given PixelFormat. | ||||||
|  | inline const char *to_string(PixelFormat format) { | ||||||
|  |   switch (format) { | ||||||
|  |     case PIXEL_FORMAT_GRAYSCALE: | ||||||
|  |       return "PIXEL_FORMAT_GRAYSCALE"; | ||||||
|  |     case PIXEL_FORMAT_RGB565: | ||||||
|  |       return "PIXEL_FORMAT_RGB565"; | ||||||
|  |     case PIXEL_FORMAT_BGR888: | ||||||
|  |       return "PIXEL_FORMAT_BGR888"; | ||||||
|  |   } | ||||||
|  |   return "PIXEL_FORMAT_UNKNOWN"; | ||||||
|  | } | ||||||
|  |  | ||||||
| /** Abstract camera image base class. | /** Abstract camera image base class. | ||||||
|  *  Encapsulates the JPEG encoded data and it is shared among |  *  Encapsulates the JPEG encoded data and it is shared among | ||||||
|  *  all connected clients. |  *  all connected clients. | ||||||
| @@ -43,6 +63,29 @@ class CameraImageReader { | |||||||
|   virtual ~CameraImageReader() {} |   virtual ~CameraImageReader() {} | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | /// Specification of a caputured camera image. | ||||||
|  | /// This struct defines the format and size details for images captured | ||||||
|  | /// or processed by a camera component. | ||||||
|  | struct CameraImageSpec { | ||||||
|  |   uint16_t width; | ||||||
|  |   uint16_t height; | ||||||
|  |   PixelFormat format; | ||||||
|  |   size_t bytes_per_pixel() { | ||||||
|  |     switch (format) { | ||||||
|  |       case PIXEL_FORMAT_GRAYSCALE: | ||||||
|  |         return 1; | ||||||
|  |       case PIXEL_FORMAT_RGB565: | ||||||
|  |         return 2; | ||||||
|  |       case PIXEL_FORMAT_BGR888: | ||||||
|  |         return 3; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return 1; | ||||||
|  |   } | ||||||
|  |   size_t bytes_per_row() { return bytes_per_pixel() * width; } | ||||||
|  |   size_t bytes_per_image() { return bytes_per_pixel() * width * height; } | ||||||
|  | }; | ||||||
|  |  | ||||||
| /** Abstract camera base class. Collaborates with API. | /** Abstract camera base class. Collaborates with API. | ||||||
|  *  1) API server starts and installs callback (add_image_callback) |  *  1) API server starts and installs callback (add_image_callback) | ||||||
|  *     which is called by the camera when a new image is available. |  *     which is called by the camera when a new image is available. | ||||||
|   | |||||||
							
								
								
									
										69
									
								
								esphome/components/camera/encoder.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								esphome/components/camera/encoder.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "buffer.h" | ||||||
|  | #include "camera.h" | ||||||
|  |  | ||||||
|  | namespace esphome::camera { | ||||||
|  |  | ||||||
|  | /// Result codes from the encoder used to control camera pipeline flow. | ||||||
|  | enum EncoderError : uint8_t { | ||||||
|  |   ENCODER_ERROR_SUCCESS = 0,   ///< Encoding succeeded, continue pipeline normally. | ||||||
|  |   ENCODER_ERROR_SKIP_FRAME,    ///< Skip current frame, try again on next frame. | ||||||
|  |   ENCODER_ERROR_RETRY_FRAME,   ///< Retry current frame, after buffer growth or for incremental encoding. | ||||||
|  |   ENCODER_ERROR_CONFIGURATION  ///< Fatal config error, shut down pipeline. | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /// Converts EncoderError to string. | ||||||
|  | inline const char *to_string(EncoderError error) { | ||||||
|  |   switch (error) { | ||||||
|  |     case ENCODER_ERROR_SUCCESS: | ||||||
|  |       return "ENCODER_ERROR_SUCCESS"; | ||||||
|  |     case ENCODER_ERROR_SKIP_FRAME: | ||||||
|  |       return "ENCODER_ERROR_SKIP_FRAME"; | ||||||
|  |     case ENCODER_ERROR_RETRY_FRAME: | ||||||
|  |       return "ENCODER_ERROR_RETRY_FRAME"; | ||||||
|  |     case ENCODER_ERROR_CONFIGURATION: | ||||||
|  |       return "ENCODER_ERROR_CONFIGURATION"; | ||||||
|  |   } | ||||||
|  |   return "ENCODER_ERROR_INVALID"; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Interface for an encoder buffer supporting resizing and variable-length data. | ||||||
|  | class EncoderBuffer { | ||||||
|  |  public: | ||||||
|  |   ///  Sets logical buffer size, reallocates if needed. | ||||||
|  |   ///  @param size Required size in bytes. | ||||||
|  |   ///  @return true on success, false on allocation failure. | ||||||
|  |   virtual bool set_buffer_size(size_t size) = 0; | ||||||
|  |  | ||||||
|  |   /// Returns a pointer to the buffer data. | ||||||
|  |   virtual uint8_t *get_data() const = 0; | ||||||
|  |  | ||||||
|  |   /// Returns number of bytes currently used. | ||||||
|  |   virtual size_t get_size() const = 0; | ||||||
|  |  | ||||||
|  |   ///  Returns total allocated buffer size. | ||||||
|  |   virtual size_t get_max_size() const = 0; | ||||||
|  |  | ||||||
|  |   virtual ~EncoderBuffer() = default; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /// Interface for image encoders used in a camera pipeline. | ||||||
|  | class Encoder { | ||||||
|  |  public: | ||||||
|  |   /// Encodes pixel data from a previous camera pipeline stage. | ||||||
|  |   /// @param spec Specification of the input pixel data. | ||||||
|  |   /// @param pixels Image pixels in RGB or grayscale format, as specified in @p spec. | ||||||
|  |   /// @return EncoderError Indicating the result of the encoding operation. | ||||||
|  |   virtual EncoderError encode_pixels(CameraImageSpec *spec, Buffer *pixels) = 0; | ||||||
|  |  | ||||||
|  |   /// Returns the encoder's output buffer. | ||||||
|  |   /// @return Pointer to an EncoderBuffer containing encoded data. | ||||||
|  |   virtual EncoderBuffer *get_output_buffer() = 0; | ||||||
|  |  | ||||||
|  |   ///  Prints the encoder's configuration to the log. | ||||||
|  |   virtual void dump_config() = 0; | ||||||
|  |   virtual ~Encoder() = default; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace esphome::camera | ||||||
							
								
								
									
										62
									
								
								esphome/components/camera_encoder/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								esphome/components/camera_encoder/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | from esphome.components.esp32 import add_idf_component | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TYPE | ||||||
|  | from esphome.core import CORE | ||||||
|  | from esphome.types import ConfigType | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@DT-art1"] | ||||||
|  |  | ||||||
|  | AUTO_LOAD = ["camera"] | ||||||
|  |  | ||||||
|  | CONF_BUFFER_EXPAND_SIZE = "buffer_expand_size" | ||||||
|  | CONF_ENCODER_BUFFER_ID = "encoder_buffer_id" | ||||||
|  | CONF_QUALITY = "quality" | ||||||
|  |  | ||||||
|  | ESP32_CAMERA_ENCODER = "esp32_camera" | ||||||
|  |  | ||||||
|  | camera_ns = cg.esphome_ns.namespace("camera") | ||||||
|  | camera_encoder_ns = cg.esphome_ns.namespace("camera_encoder") | ||||||
|  |  | ||||||
|  | Encoder = camera_ns.class_("Encoder") | ||||||
|  | EncoderBufferImpl = camera_encoder_ns.class_("EncoderBufferImpl") | ||||||
|  |  | ||||||
|  | ESP32CameraJPEGEncoder = camera_encoder_ns.class_("ESP32CameraJPEGEncoder", Encoder) | ||||||
|  |  | ||||||
|  | MAX_JPEG_BUFFER_SIZE_2MB = 2 * 1024 * 1024 | ||||||
|  |  | ||||||
|  | ESP32_CAMERA_ENCODER_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(ESP32CameraJPEGEncoder), | ||||||
|  |         cv.Optional(CONF_QUALITY, default=80): cv.int_range(1, 100), | ||||||
|  |         cv.Optional(CONF_BUFFER_SIZE, default=4096): cv.int_range( | ||||||
|  |             1024, MAX_JPEG_BUFFER_SIZE_2MB | ||||||
|  |         ), | ||||||
|  |         cv.Optional(CONF_BUFFER_EXPAND_SIZE, default=1024): cv.int_range( | ||||||
|  |             0, MAX_JPEG_BUFFER_SIZE_2MB | ||||||
|  |         ), | ||||||
|  |         cv.GenerateID(CONF_ENCODER_BUFFER_ID): cv.declare_id(EncoderBufferImpl), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.typed_schema( | ||||||
|  |     { | ||||||
|  |         ESP32_CAMERA_ENCODER: ESP32_CAMERA_ENCODER_SCHEMA, | ||||||
|  |     }, | ||||||
|  |     default_type=ESP32_CAMERA_ENCODER, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config: ConfigType) -> None: | ||||||
|  |     buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID]) | ||||||
|  |     cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE])) | ||||||
|  |     if config[CONF_TYPE] == ESP32_CAMERA_ENCODER: | ||||||
|  |         if CORE.using_esp_idf: | ||||||
|  |             add_idf_component(name="espressif/esp32-camera", ref="2.1.0") | ||||||
|  |         cg.add_build_flag("-DUSE_ESP32_CAMERA_JPEG_ENCODER") | ||||||
|  |         var = cg.new_Pvariable( | ||||||
|  |             config[CONF_ID], | ||||||
|  |             config[CONF_QUALITY], | ||||||
|  |             buffer, | ||||||
|  |         ) | ||||||
|  |         cg.add(var.set_buffer_expand_size(config[CONF_BUFFER_EXPAND_SIZE])) | ||||||
							
								
								
									
										23
									
								
								esphome/components/camera_encoder/encoder_buffer_impl.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/camera_encoder/encoder_buffer_impl.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | #include "encoder_buffer_impl.h" | ||||||
|  |  | ||||||
|  | namespace esphome::camera_encoder { | ||||||
|  |  | ||||||
|  | bool EncoderBufferImpl::set_buffer_size(size_t size) { | ||||||
|  |   if (size > this->capacity_) { | ||||||
|  |     uint8_t *p = this->allocator_.reallocate(this->data_, size); | ||||||
|  |     if (p == nullptr) | ||||||
|  |       return false; | ||||||
|  |  | ||||||
|  |     this->data_ = p; | ||||||
|  |     this->capacity_ = size; | ||||||
|  |   } | ||||||
|  |   this->size_ = size; | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | EncoderBufferImpl::~EncoderBufferImpl() { | ||||||
|  |   if (this->data_ != nullptr) | ||||||
|  |     this->allocator_.deallocate(this->data_, this->capacity_); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace esphome::camera_encoder | ||||||
							
								
								
									
										25
									
								
								esphome/components/camera_encoder/encoder_buffer_impl.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								esphome/components/camera_encoder/encoder_buffer_impl.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/components/camera/encoder.h" | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
|  | namespace esphome::camera_encoder { | ||||||
|  |  | ||||||
|  | class EncoderBufferImpl : public camera::EncoderBuffer { | ||||||
|  |  public: | ||||||
|  |   // --- EncoderBuffer  --- | ||||||
|  |   bool set_buffer_size(size_t size) override; | ||||||
|  |   uint8_t *get_data() const override { return this->data_; } | ||||||
|  |   size_t get_size() const override { return this->size_; } | ||||||
|  |   size_t get_max_size() const override { return this->capacity_; } | ||||||
|  |   // ---------------------- | ||||||
|  |   ~EncoderBufferImpl() override; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   RAMAllocator<uint8_t> allocator_; | ||||||
|  |   size_t capacity_{}; | ||||||
|  |   size_t size_{}; | ||||||
|  |   uint8_t *data_{}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace esphome::camera_encoder | ||||||
| @@ -0,0 +1,82 @@ | |||||||
|  | #ifdef USE_ESP32_CAMERA_JPEG_ENCODER | ||||||
|  |  | ||||||
|  | #include "esp32_camera_jpeg_encoder.h" | ||||||
|  |  | ||||||
|  | namespace esphome::camera_encoder { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "camera_encoder"; | ||||||
|  |  | ||||||
|  | ESP32CameraJPEGEncoder::ESP32CameraJPEGEncoder(uint8_t quality, camera::EncoderBuffer *output) { | ||||||
|  |   this->quality_ = quality; | ||||||
|  |   this->output_ = output; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | camera::EncoderError ESP32CameraJPEGEncoder::encode_pixels(camera::CameraImageSpec *spec, camera::Buffer *pixels) { | ||||||
|  |   this->bytes_written_ = 0; | ||||||
|  |   this->out_of_output_memory_ = false; | ||||||
|  |   bool success = fmt2jpg_cb(pixels->get_data_buffer(), pixels->get_data_length(), spec->width, spec->height, | ||||||
|  |                             to_internal_(spec->format), this->quality_, callback_, this); | ||||||
|  |  | ||||||
|  |   if (!success) | ||||||
|  |     return camera::ENCODER_ERROR_CONFIGURATION; | ||||||
|  |  | ||||||
|  |   if (this->out_of_output_memory_) { | ||||||
|  |     if (this->buffer_expand_size_ <= 0) | ||||||
|  |       return camera::ENCODER_ERROR_SKIP_FRAME; | ||||||
|  |  | ||||||
|  |     size_t current_size = this->output_->get_max_size(); | ||||||
|  |     size_t new_size = this->output_->get_max_size() + this->buffer_expand_size_; | ||||||
|  |     if (!this->output_->set_buffer_size(new_size)) { | ||||||
|  |       ESP_LOGE(TAG, "Failed to expand output buffer."); | ||||||
|  |       this->buffer_expand_size_ = 0; | ||||||
|  |       return camera::ENCODER_ERROR_SKIP_FRAME; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     ESP_LOGD(TAG, "Output buffer expanded (%u -> %u).", current_size, this->output_->get_max_size()); | ||||||
|  |     return camera::ENCODER_ERROR_RETRY_FRAME; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   this->output_->set_buffer_size(this->bytes_written_); | ||||||
|  |   return camera::ENCODER_ERROR_SUCCESS; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ESP32CameraJPEGEncoder::dump_config() { | ||||||
|  |   ESP_LOGCONFIG(TAG, | ||||||
|  |                 "ESP32 Camera JPEG Encoder:\n" | ||||||
|  |                 "  Size: %zu\n" | ||||||
|  |                 "  Quality: %d\n" | ||||||
|  |                 "  Expand: %d\n", | ||||||
|  |                 this->output_->get_max_size(), this->quality_, this->buffer_expand_size_); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | size_t ESP32CameraJPEGEncoder::callback_(void *arg, size_t index, const void *data, size_t len) { | ||||||
|  |   ESP32CameraJPEGEncoder *that = reinterpret_cast<ESP32CameraJPEGEncoder *>(arg); | ||||||
|  |   uint8_t *buffer = that->output_->get_data(); | ||||||
|  |   size_t buffer_length = that->output_->get_max_size(); | ||||||
|  |   if (index + len > buffer_length) { | ||||||
|  |     that->out_of_output_memory_ = true; | ||||||
|  |     return 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   std::memcpy(&buffer[index], data, len); | ||||||
|  |   that->bytes_written_ += len; | ||||||
|  |   return len; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pixformat_t ESP32CameraJPEGEncoder::to_internal_(camera::PixelFormat format) { | ||||||
|  |   switch (format) { | ||||||
|  |     case camera::PIXEL_FORMAT_GRAYSCALE: | ||||||
|  |       return PIXFORMAT_GRAYSCALE; | ||||||
|  |     case camera::PIXEL_FORMAT_RGB565: | ||||||
|  |       return PIXFORMAT_RGB565; | ||||||
|  |     // Internal representation for RGB is in byte order: B, G, R | ||||||
|  |     case camera::PIXEL_FORMAT_BGR888: | ||||||
|  |       return PIXFORMAT_RGB888; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return PIXFORMAT_GRAYSCALE; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace esphome::camera_encoder | ||||||
|  |  | ||||||
|  | #endif | ||||||
| @@ -0,0 +1,39 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP32_CAMERA_JPEG_ENCODER | ||||||
|  |  | ||||||
|  | #include <esp_camera.h> | ||||||
|  |  | ||||||
|  | #include "esphome/components/camera/encoder.h" | ||||||
|  |  | ||||||
|  | namespace esphome::camera_encoder { | ||||||
|  |  | ||||||
|  | /// Encoder that uses the software-based JPEG implementation from Espressif's esp32-camera component. | ||||||
|  | class ESP32CameraJPEGEncoder : public camera::Encoder { | ||||||
|  |  public: | ||||||
|  |   /// Constructs a ESP32CameraJPEGEncoder instance. | ||||||
|  |   /// @param quality Sets the quality of the encoded image (1-100). | ||||||
|  |   /// @param output Pointer to preallocated output buffer. | ||||||
|  |   ESP32CameraJPEGEncoder(uint8_t quality, camera::EncoderBuffer *output); | ||||||
|  |   /// Sets the number of bytes to expand the output buffer on underflow during encoding. | ||||||
|  |   /// @param buffer_expand_size Number of bytes to expand the buffer. | ||||||
|  |   void set_buffer_expand_size(size_t buffer_expand_size) { this->buffer_expand_size_ = buffer_expand_size; } | ||||||
|  |   // -------- Encoder -------- | ||||||
|  |   camera::EncoderError encode_pixels(camera::CameraImageSpec *spec, camera::Buffer *pixels) override; | ||||||
|  |   camera::EncoderBuffer *get_output_buffer() override { return output_; } | ||||||
|  |   void dump_config() override; | ||||||
|  |   // ------------------------- | ||||||
|  |  protected: | ||||||
|  |   static size_t callback_(void *arg, size_t index, const void *data, size_t len); | ||||||
|  |   pixformat_t to_internal_(camera::PixelFormat format); | ||||||
|  |  | ||||||
|  |   camera::EncoderBuffer *output_{}; | ||||||
|  |   size_t buffer_expand_size_{}; | ||||||
|  |   size_t bytes_written_{}; | ||||||
|  |   uint8_t quality_{}; | ||||||
|  |   bool out_of_output_memory_{}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace esphome::camera_encoder | ||||||
|  |  | ||||||
|  | #endif | ||||||
| @@ -10,7 +10,7 @@ from esphome.const import ( | |||||||
|     PLATFORM_LN882X, |     PLATFORM_LN882X, | ||||||
|     PLATFORM_RTL87XX, |     PLATFORM_RTL87XX, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, CoroPriority, coroutine_with_priority | ||||||
|  |  | ||||||
| AUTO_LOAD = ["web_server_base", "ota.web_server"] | AUTO_LOAD = ["web_server_base", "ota.web_server"] | ||||||
| DEPENDENCIES = ["wifi"] | DEPENDENCIES = ["wifi"] | ||||||
| @@ -40,7 +40,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @coroutine_with_priority(64.0) | @coroutine_with_priority(CoroPriority.COMMUNICATION) | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) |     paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,103 +7,83 @@ namespace esphome { | |||||||
| namespace captive_portal { | namespace captive_portal { | ||||||
|  |  | ||||||
| const uint8_t INDEX_GZ[] PROGMEM = { | const uint8_t INDEX_GZ[] PROGMEM = { | ||||||
|     0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xdd, 0x58, 0x6d, 0x6f, 0xdb, 0x38, 0x12, 0xfe, 0xde, |     0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e, | ||||||
|     0x5f, 0x31, 0xa7, 0x36, 0x6b, 0x6b, 0x1b, 0x51, 0x22, 0xe5, 0xb7, 0xd8, 0x92, 0x16, 0x69, 0xae, 0x8b, 0x5d, 0xa0, |     0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36, | ||||||
|     0xdd, 0x2d, 0x90, 0x6c, 0xef, 0x43, 0x51, 0x20, 0xb4, 0x34, 0xb2, 0xd8, 0x48, 0xa4, 0x4e, 0xa4, 0x5f, 0x52, 0xc3, |     0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf, | ||||||
|     0xf7, 0xdb, 0x0f, 0x94, 0x6c, 0xc7, 0xe9, 0x35, 0x87, 0xeb, 0xe2, 0x0e, 0x87, 0xdd, 0x18, 0x21, 0x86, 0xe4, 0xcc, |     0x7e, 0xa0, 0x24, 0x7b, 0x9d, 0x45, 0x73, 0xb8, 0xb3, 0x60, 0x61, 0x38, 0xef, 0x19, 0xcd, 0x83, 0xc5, 0xdf, 0x2a, | ||||||
|     0x70, 0xe6, 0xf1, 0x0c, 0x67, 0xcc, 0xe8, 0x2f, 0x99, 0x4a, 0xcd, 0x7d, 0x8d, 0x50, 0x98, 0xaa, 0x4c, 0x22, 0x3b, |     0xc5, 0xec, 0xbe, 0x03, 0xd4, 0xd8, 0x56, 0x94, 0x85, 0x7b, 0x23, 0x41, 0xe5, 0x8a, 0x80, 0x2c, 0x8b, 0x06, 0x68, | ||||||
|     0x42, 0xc9, 0xe5, 0x22, 0x46, 0x99, 0x44, 0x05, 0xf2, 0x2c, 0x89, 0x2a, 0x34, 0x1c, 0xd2, 0x82, 0x37, 0x1a, 0x4d, |     0x55, 0x16, 0x2d, 0x58, 0x8a, 0x58, 0x43, 0xb5, 0x01, 0x4b, 0xfe, 0xbc, 0xff, 0x39, 0xb8, 0x2d, 0x0b, 0xc1, 0xe5, | ||||||
|     0xfc, 0xdb, 0xcd, 0x8f, 0xde, 0x04, 0xfc, 0x24, 0x2a, 0x85, 0xbc, 0x83, 0x06, 0xcb, 0x58, 0xa4, 0x4a, 0x42, 0xd1, |     0x1a, 0x69, 0x10, 0x84, 0x33, 0x25, 0x51, 0xa3, 0xa1, 0x26, 0x15, 0xb5, 0x34, 0xe3, 0x2d, 0x5d, 0xc1, 0x24, 0x22, | ||||||
|     0x60, 0x1e, 0x67, 0xdc, 0xf0, 0xa9, 0xa8, 0xf8, 0x02, 0x2d, 0x43, 0x2b, 0x26, 0x79, 0x85, 0xf1, 0x4a, 0xe0, 0xba, |     0x69, 0x0b, 0x64, 0xc3, 0x61, 0xdb, 0x29, 0x6d, 0x11, 0x53, 0xd2, 0x82, 0xb4, 0x04, 0x6f, 0x79, 0x65, 0x1b, 0x52, | ||||||
|     0x56, 0x8d, 0x81, 0x54, 0x49, 0x83, 0xd2, 0xc4, 0xce, 0x5a, 0x64, 0xa6, 0x88, 0x33, 0x5c, 0x89, 0x14, 0xbd, 0x76, |     0xc1, 0x86, 0x33, 0x08, 0x86, 0xc3, 0x35, 0x97, 0xdc, 0x72, 0x2a, 0x02, 0xc3, 0xa8, 0x00, 0x92, 0x5c, 0xf7, 0x06, | ||||||
|     0x72, 0x2e, 0xa4, 0x30, 0x82, 0x97, 0x9e, 0x4e, 0x79, 0x89, 0x31, 0x3d, 0x5f, 0x6a, 0x6c, 0xda, 0x09, 0x9f, 0x97, |     0xf4, 0x70, 0xa0, 0x4b, 0x01, 0x44, 0x2a, 0x5c, 0x16, 0x86, 0x69, 0xde, 0x59, 0xe4, 0x5c, 0x25, 0xad, 0xaa, 0x7a, | ||||||
|     0x18, 0x4b, 0xe5, 0xf8, 0x49, 0xa4, 0xd3, 0x46, 0xd4, 0x06, 0xac, 0xbd, 0x71, 0xa5, 0xb2, 0x65, 0x89, 0x89, 0xef, |     0x01, 0x65, 0x14, 0x51, 0x63, 0xc0, 0x9a, 0x88, 0xcb, 0x0a, 0x76, 0xe1, 0x32, 0x66, 0x2c, 0x86, 0xdb, 0xdb, 0xf0, | ||||||
|     0x73, 0xad, 0xd1, 0x68, 0x5f, 0xc8, 0x0c, 0x37, 0x64, 0x14, 0x86, 0x29, 0xe3, 0xe3, 0x9c, 0x7c, 0xd2, 0xcf, 0x32, |     0xb3, 0x79, 0x56, 0x29, 0xd6, 0xb7, 0x20, 0x6d, 0x28, 0x14, 0xa3, 0x96, 0x2b, 0x19, 0x1a, 0xa0, 0x9a, 0x35, 0x84, | ||||||
|     0x95, 0x2e, 0x2b, 0x94, 0x86, 0x94, 0x2a, 0xe5, 0x46, 0x28, 0x49, 0x34, 0xf2, 0x26, 0x2d, 0xe2, 0x38, 0x76, 0x7e, |     0x10, 0xfc, 0xa3, 0xa1, 0x1b, 0xc0, 0xdf, 0x7f, 0xef, 0x9d, 0x99, 0x56, 0x60, 0xff, 0x21, 0xc0, 0x81, 0xe6, 0xa7, | ||||||
|     0xd0, 0x7c, 0x85, 0xce, 0x77, 0xdf, 0xf5, 0x8f, 0x4c, 0x0b, 0x34, 0xaf, 0x4b, 0xb4, 0xa4, 0x7e, 0x75, 0x7f, 0xc3, |     0xfd, 0x3d, 0x5d, 0xfd, 0x4e, 0x5b, 0xf0, 0x30, 0x35, 0xbc, 0x02, 0xec, 0x7f, 0x8c, 0x3f, 0x85, 0xc6, 0xee, 0x05, | ||||||
|     0x17, 0xbf, 0xf0, 0x0a, 0xfb, 0x0e, 0xd7, 0x22, 0x43, 0xc7, 0xfd, 0x10, 0x7c, 0x24, 0xda, 0xdc, 0x97, 0x48, 0x32, |     0x84, 0x15, 0x37, 0x9d, 0xa0, 0x7b, 0x82, 0x97, 0x42, 0xb1, 0x35, 0xf6, 0xf3, 0xba, 0x97, 0xcc, 0x29, 0x47, 0xc6, | ||||||
|     0xa1, 0xeb, 0x92, 0xdf, 0xc7, 0xce, 0xbc, 0x54, 0xe9, 0x9d, 0xe3, 0xce, 0xf2, 0xa5, 0x4c, 0xad, 0x72, 0xd0, 0x7d, |     0x03, 0xff, 0x20, 0xc0, 0x22, 0x4b, 0xde, 0x51, 0xdb, 0x84, 0x2d, 0xdd, 0x79, 0x23, 0xc0, 0xa5, 0x97, 0xfe, 0xe0, | ||||||
|     0x74, 0xb7, 0x25, 0x1a, 0x30, 0xf1, 0x5b, 0x6e, 0x0a, 0x52, 0xf1, 0x4d, 0xbf, 0x23, 0x84, 0xec, 0xb3, 0xef, 0xfb, |     0xc1, 0xcb, 0x24, 0x8e, 0xfd, 0xeb, 0xe1, 0x15, 0xfb, 0x51, 0x12, 0xc7, 0xb9, 0x06, 0xdb, 0x6b, 0x89, 0xa8, 0xf7, | ||||||
|     0xf8, 0x92, 0x06, 0x81, 0x7b, 0xde, 0x0e, 0x81, 0xeb, 0xd3, 0x20, 0x98, 0x35, 0x68, 0x96, 0x8d, 0x04, 0xde, 0xbf, |     0x50, 0x74, 0xd4, 0x36, 0xa8, 0x22, 0xf8, 0x5d, 0x92, 0xa2, 0xe4, 0x75, 0x98, 0xce, 0x7f, 0x0b, 0x5f, 0xa1, 0x9b, | ||||||
|     0x8d, 0x6a, 0x6e, 0x0a, 0xc8, 0x62, 0xa7, 0xa2, 0x8c, 0x04, 0xc1, 0x04, 0xe8, 0x05, 0x61, 0x43, 0x8f, 0x52, 0x12, |     0x30, 0x9d, 0xb3, 0x57, 0xc1, 0x1c, 0x25, 0x37, 0xc1, 0x1c, 0xa5, 0x69, 0x38, 0x47, 0xf1, 0x17, 0x8c, 0x6a, 0x2e, | ||||||
|     0x7a, 0x74, 0x98, 0x8e, 0xbd, 0x21, 0xd0, 0x81, 0x37, 0x04, 0xc6, 0xc8, 0x10, 0x82, 0xcf, 0x0e, 0xe4, 0xa2, 0x2c, |     0x04, 0xc1, 0x52, 0x49, 0xc0, 0xc8, 0x58, 0xad, 0xd6, 0x40, 0x30, 0xeb, 0xb5, 0x06, 0x69, 0xdf, 0x2a, 0xa1, 0x34, | ||||||
|     0x63, 0x47, 0x2a, 0x89, 0x0e, 0x68, 0xd3, 0xa8, 0x3b, 0x8c, 0x9d, 0x74, 0xd9, 0x34, 0x28, 0xcd, 0x95, 0x2a, 0x55, |     0x8e, 0xca, 0x67, 0xff, 0x97, 0x42, 0xab, 0xa9, 0x34, 0xb5, 0xd2, 0x2d, 0xc1, 0x43, 0xf6, 0xbd, 0x17, 0x07, 0x7b, | ||||||
|     0xe3, 0xf8, 0xc9, 0x33, 0x78, 0xf4, 0xf7, 0xcd, 0x47, 0x98, 0x86, 0x4b, 0x9d, 0xab, 0xa6, 0x8a, 0x9d, 0xf6, 0x4b, |     0x44, 0xee, 0xe5, 0x5f, 0x10, 0x03, 0xa5, 0xf9, 0x8a, 0x4b, 0x82, 0x9d, 0xc6, 0x5b, 0x1c, 0x95, 0x0f, 0xfe, 0xf1, | ||||||
|     0xe9, 0xbf, 0xd8, 0x9a, 0x1d, 0xd8, 0xc1, 0x3d, 0xd9, 0xf4, 0x54, 0x23, 0x16, 0x42, 0xc6, 0x0e, 0x65, 0x40, 0x27, |     0x1c, 0x3d, 0x75, 0xd1, 0x4f, 0xf1, 0x28, 0xef, 0xe3, 0x43, 0x61, 0x36, 0x2b, 0xb4, 0x6b, 0x85, 0x34, 0x04, 0x37, | ||||||
|     0x8e, 0x9f, 0xdc, 0xba, 0xbb, 0x23, 0x26, 0xdc, 0x62, 0xb2, 0xf7, 0x52, 0xf5, 0x3f, 0xdc, 0x46, 0x7a, 0xb5, 0x80, |     0xd6, 0x76, 0x59, 0x14, 0x6d, 0xb7, 0xdb, 0x70, 0x3b, 0x0b, 0x95, 0x5e, 0x45, 0x69, 0x1c, 0xc7, 0x91, 0xd9, 0xac, | ||||||
|     0x4d, 0x55, 0x4a, 0x1d, 0x3b, 0x85, 0x31, 0xf5, 0xd4, 0xf7, 0xd7, 0xeb, 0x35, 0x59, 0x87, 0x44, 0x35, 0x0b, 0x9f, |     0x30, 0x1a, 0x0b, 0x01, 0xa7, 0x37, 0x18, 0x35, 0xc0, 0x57, 0x8d, 0x1d, 0xe0, 0xf2, 0xc5, 0x01, 0x8e, 0x85, 0xe3, | ||||||
|     0x05, 0x41, 0xe0, 0xeb, 0xd5, 0xc2, 0x81, 0x2e, 0x3e, 0x1c, 0x36, 0x70, 0xa0, 0x40, 0xb1, 0x28, 0x4c, 0x4b, 0x27, |     0x28, 0x1f, 0x3e, 0x5d, 0x58, 0xe1, 0x17, 0x56, 0xe0, 0x47, 0xea, 0xe1, 0x53, 0x98, 0x57, 0x43, 0x98, 0xaf, 0x68, | ||||||
|     0x2f, 0xb6, 0xb8, 0x8b, 0x2c, 0x47, 0x72, 0xfb, 0xf1, 0xe4, 0x14, 0x71, 0x72, 0x0a, 0xfe, 0x70, 0x82, 0x66, 0xef, |     0x8a, 0x52, 0x14, 0x0f, 0x4f, 0x1a, 0x38, 0x78, 0x3a, 0x05, 0x4f, 0x4e, 0xe8, 0xe2, 0xe4, 0xa0, 0x76, 0x11, 0xbc, | ||||||
|     0xad, 0x35, 0x6a, 0xcc, 0x19, 0x30, 0x08, 0xda, 0x0f, 0xf3, 0x2c, 0xbd, 0x9f, 0x79, 0x5f, 0xcc, 0xe0, 0x64, 0x06, |     0x3e, 0xcb, 0x26, 0x0e, 0xb3, 0x49, 0xe2, 0x47, 0x84, 0x13, 0xf8, 0x65, 0x71, 0x79, 0x0e, 0xd2, 0x0f, 0x97, 0x0c, | ||||||
|     0x0c, 0x9e, 0x01, 0xb0, 0x6a, 0xe4, 0x5d, 0x1c, 0xc5, 0xa9, 0xdd, 0x5e, 0xd1, 0xe0, 0x61, 0xc1, 0xca, 0xfc, 0x34, |     0xce, 0x5a, 0x93, 0x7c, 0x58, 0xd0, 0x39, 0x9a, 0x4f, 0x98, 0x79, 0xe0, 0xe0, 0xf3, 0x09, 0xcd, 0x37, 0x69, 0x93, | ||||||
|     0x3a, 0x9d, 0x7b, 0xec, 0xbd, 0x65, 0xb0, 0xd8, 0x1f, 0x85, 0x3c, 0x56, 0xd0, 0xf7, 0x23, 0x3e, 0x84, 0xe1, 0x7e, |     0xb4, 0xc1, 0x22, 0x98, 0xd3, 0x19, 0x9a, 0x4d, 0x8e, 0xcc, 0xd0, 0x6c, 0x93, 0x36, 0x8b, 0x0f, 0x8b, 0x4b, 0x5c, | ||||||
|     0x65, 0xe8, 0x59, 0xfa, 0x38, 0xb3, 0x27, 0xc1, 0x70, 0xc5, 0x0a, 0x5a, 0x79, 0x23, 0x6f, 0xc8, 0x43, 0x08, 0xf7, |     0x30, 0xfb, 0x72, 0x15, 0x95, 0xd8, 0xcf, 0x30, 0x7e, 0x8c, 0x5c, 0x5d, 0x46, 0x1e, 0x7e, 0x56, 0x5c, 0x7a, 0x18, | ||||||
|     0x26, 0x85, 0x10, 0xae, 0x58, 0x31, 0x7a, 0x3f, 0x3a, 0x5d, 0xf3, 0xc2, 0xcf, 0x3d, 0x0b, 0xf3, 0xd4, 0x71, 0x1e, |     0xfb, 0xc7, 0x1a, 0x2c, 0x6b, 0x3c, 0x1c, 0x31, 0x25, 0x6b, 0xbe, 0x0a, 0x3f, 0x1b, 0x25, 0xb1, 0x1f, 0xda, 0x06, | ||||||
|     0x30, 0x50, 0xa7, 0x18, 0x90, 0x4f, 0x4a, 0xc8, 0xbe, 0xe3, 0xb8, 0xbb, 0x1c, 0x4d, 0x5a, 0xf4, 0x1d, 0x3f, 0x55, |     0xa4, 0x77, 0x12, 0x75, 0x82, 0x30, 0x50, 0xbc, 0xa7, 0x14, 0xeb, 0x1f, 0xce, 0xf5, 0x6f, 0xb9, 0x15, 0x40, 0x6c, | ||||||
|     0x32, 0x17, 0x0b, 0xf2, 0x49, 0x2b, 0xe9, 0xb8, 0xc4, 0x14, 0x28, 0xfb, 0x07, 0x51, 0x2b, 0x88, 0xed, 0x4e, 0xff, |     0xe8, 0x1a, 0xf6, 0xfa, 0x8c, 0x5d, 0xaa, 0x6a, 0xff, 0x8d, 0xd6, 0x68, 0x92, 0xb1, 0x2f, 0xb8, 0x94, 0xa0, 0xef, | ||||||
|     0xcb, 0x1d, 0xe3, 0x6e, 0x8f, 0xf9, 0x61, 0x84, 0x29, 0x31, 0x36, 0xc4, 0x66, 0xf4, 0xf9, 0x71, 0x75, 0xae, 0xb2, |     0x61, 0x67, 0x09, 0x7e, 0xf7, 0xe6, 0x2d, 0x7a, 0x53, 0x55, 0x1a, 0x8c, 0xc9, 0x10, 0x7e, 0x69, 0xc3, 0x96, 0xb2, | ||||||
|     0xfb, 0x27, 0x52, 0xa7, 0xa0, 0x5d, 0xde, 0x08, 0x29, 0xb1, 0xb9, 0xc1, 0x8d, 0x89, 0x9d, 0xb7, 0x97, 0x57, 0x70, |     0xff, 0x5d, 0x57, 0xf2, 0x95, 0xae, 0x7f, 0xf2, 0x9f, 0x39, 0xfa, 0x1d, 0xec, 0x56, 0xe9, 0xf5, 0xa4, 0xcd, 0xb9, | ||||||
|     0x99, 0x65, 0x0d, 0x6a, 0x3d, 0x05, 0xe7, 0xa5, 0x21, 0x15, 0x4f, 0xff, 0x73, 0x5d, 0xf4, 0x91, 0xae, 0xbf, 0x89, |     0x96, 0xbb, 0x0e, 0xd3, 0xc4, 0x86, 0xb4, 0x33, 0xa1, 0x11, 0x9c, 0x81, 0x97, 0xf8, 0x61, 0x4b, 0xbb, 0xc7, 0xa8, | ||||||
|     0x1f, 0x05, 0xfc, 0x82, 0x66, 0xad, 0x9a, 0xbb, 0xbd, 0x36, 0x6b, 0xda, 0xcc, 0x66, 0x60, 0x13, 0x1b, 0xc2, 0x6b, |     0xe4, 0x29, 0x51, 0x0f, 0x45, 0xc5, 0x37, 0x88, 0x09, 0x6a, 0x0c, 0xc1, 0x72, 0x54, 0x85, 0xd1, 0x33, 0x34, 0xfc, | ||||||
|     0x4d, 0x74, 0x29, 0x52, 0xec, 0x53, 0x97, 0x54, 0xbc, 0x7e, 0xf0, 0x4a, 0x1e, 0x80, 0xba, 0x8d, 0x32, 0xb1, 0x82, |     0x94, 0x64, 0x82, 0xb3, 0x35, 0xc1, 0x7f, 0x31, 0x01, 0x7e, 0xda, 0xff, 0x5a, 0x79, 0x57, 0xc6, 0xf0, 0xea, 0xca, | ||||||
|     0xb4, 0xe4, 0x5a, 0xc7, 0x8e, 0xec, 0x54, 0x39, 0xb0, 0x4f, 0x1b, 0x25, 0xd3, 0x52, 0xa4, 0x77, 0xb1, 0xf3, 0x95, |     0x0f, 0x37, 0x54, 0xf4, 0x80, 0x08, 0xb2, 0x0d, 0x37, 0x8f, 0x0e, 0xe6, 0xdf, 0x14, 0xeb, 0xcc, 0xfa, 0xca, 0x0f, | ||||||
|     0x1b, 0xe2, 0xd5, 0xfd, 0xcf, 0x59, 0xbf, 0xa7, 0xb5, 0xc8, 0x7a, 0x2e, 0x59, 0xf1, 0x72, 0x89, 0x10, 0x83, 0x29, |     0x6b, 0xc5, 0x7a, 0xe3, 0xf9, 0xb8, 0x9c, 0xcc, 0x15, 0x74, 0x1c, 0x90, 0xf8, 0x39, 0x7e, 0xe2, 0x51, 0x20, 0xa0, | ||||||
|     0x84, 0x7e, 0x30, 0x70, 0xf6, 0xa4, 0x58, 0xad, 0xef, 0x7a, 0x2e, 0xc9, 0x55, 0xba, 0xd4, 0x7d, 0xd7, 0x39, 0x64, |     0xb6, 0x67, 0x3e, 0x84, 0x5e, 0x1c, 0x8c, 0x27, 0x43, 0x6d, 0x0c, 0xf7, 0x8f, 0x67, 0x64, 0x61, 0x3a, 0x2a, 0x9f, | ||||||
|     0x69, 0xc4, 0xbb, 0x3b, 0xd4, 0x79, 0xee, 0x7c, 0x61, 0x91, 0x57, 0x62, 0x6e, 0x9c, 0x87, 0x6c, 0x7e, 0xb1, 0xd5, |     0x0a, 0x3a, 0x07, 0x5d, 0xab, 0xc8, 0xd0, 0x41, 0xae, 0x5f, 0x3a, 0x2a, 0xcf, 0x06, 0x23, 0x7a, 0x02, 0x5f, 0x1c, | ||||||
|     0x7d, 0x49, 0x1a, 0xad, 0x85, 0xbb, 0x3b, 0x2e, 0x46, 0xba, 0xe6, 0xf2, 0x4b, 0x41, 0x6b, 0xa0, 0x4d, 0x1a, 0x49, |     0xb8, 0x27, 0xdd, 0x14, 0x5c, 0x9f, 0x35, 0x16, 0x51, 0xc5, 0x37, 0xe5, 0xc3, 0xd1, 0x7f, 0x8c, 0xe3, 0x5f, 0x3d, | ||||||
|     0x2c, 0x65, 0x33, 0xa7, 0xe6, 0xf2, 0x78, 0xa0, 0xcf, 0x0f, 0xe4, 0x8b, 0xad, 0xe8, 0x4b, 0x7b, 0x4b, 0xde, 0x1d, |     0xe8, 0xfd, 0x1d, 0x08, 0x60, 0x56, 0x69, 0x0f, 0x3f, 0x97, 0x60, 0xb1, 0x3f, 0x06, 0xfc, 0xcb, 0xfd, 0xbb, 0xdf, | ||||||
|     0x35, 0x46, 0x7e, 0x26, 0x56, 0xc9, 0xed, 0xce, 0x7d, 0xf0, 0xe3, 0xef, 0x4b, 0x6c, 0xee, 0xaf, 0xb1, 0xc4, 0xd4, |     0x88, 0xf2, 0xb4, 0x7f, 0xfd, 0x2d, 0x6e, 0xb7, 0x0a, 0x3e, 0x6a, 0x10, 0xff, 0x26, 0x57, 0x6e, 0x19, 0x5c, 0x7d, | ||||||
|     0xa8, 0xa6, 0xef, 0x3c, 0x97, 0x68, 0x1c, 0xb7, 0x73, 0xf8, 0xa7, 0x9b, 0xb7, 0x6f, 0x62, 0xd5, 0x6f, 0xdc, 0xf3, |     0xc2, 0x7e, 0x38, 0xc4, 0xfb, 0xf0, 0xb8, 0x11, 0x5c, 0x3b, 0xbf, 0xdc, 0xb5, 0xe2, 0xda, 0x45, 0x18, 0x2c, 0xe6, | ||||||
|     0xa7, 0xb8, 0x6d, 0xb5, 0xf8, 0xd0, 0x60, 0xf9, 0x8f, 0xb8, 0x67, 0xeb, 0x45, 0xef, 0xa3, 0xe3, 0x92, 0xd6, 0xdf, |     0xfe, 0xf1, 0xe1, 0xe8, 0x1f, 0xfd, 0xbc, 0x88, 0xc6, 0xb9, 0x5e, 0x16, 0xc3, 0x88, 0x2d, 0x7f, 0x38, 0x2c, 0xd5, | ||||||
|     0xdb, 0x87, 0xa2, 0x61, 0x13, 0xfb, 0xe5, 0xa6, 0x2a, 0xcf, 0xad, 0x87, 0xde, 0x68, 0xe8, 0xee, 0x6e, 0x77, 0xee, |     0x2e, 0x30, 0xfc, 0x0b, 0x97, 0xab, 0x8c, 0xcb, 0x06, 0x34, 0xb7, 0xc7, 0x8a, 0x6f, 0xae, 0xb9, 0xec, 0x7a, 0x7b, | ||||||
|     0xce, 0x9d, 0x45, 0x7e, 0x77, 0xef, 0x27, 0x51, 0x7b, 0x05, 0x27, 0xdf, 0x6f, 0xe7, 0x6a, 0xe3, 0x69, 0xf1, 0x59, |     0xe8, 0x68, 0x55, 0x39, 0xca, 0xbc, 0xdb, 0xe5, 0xb5, 0x92, 0xd6, 0x71, 0x42, 0x96, 0x40, 0x7b, 0x1c, 0xe9, 0xc3, | ||||||
|     0xc8, 0xc5, 0x54, 0xc8, 0x02, 0x1b, 0x61, 0x76, 0x99, 0x58, 0x9d, 0x0b, 0x59, 0x2f, 0xcd, 0xb6, 0xe6, 0x59, 0x66, |     0x44, 0xc9, 0x5e, 0xcf, 0xbf, 0x3b, 0xba, 0x82, 0x3b, 0x58, 0xd8, 0xd9, 0x80, 0x0a, 0xbe, 0x92, 0x19, 0x03, 0x69, | ||||||
|     0x77, 0x86, 0xf5, 0x66, 0x96, 0x2b, 0x69, 0x2c, 0x27, 0x4e, 0x29, 0x56, 0xbb, 0x6e, 0xbf, 0xbd, 0x5b, 0xa6, 0x17, |     0x41, 0x8f, 0x42, 0x35, 0x6d, 0xb9, 0xd8, 0x67, 0x86, 0x4a, 0x13, 0x18, 0xd0, 0xbc, 0x3e, 0x2e, 0x7b, 0x6b, 0x95, | ||||||
|     0xc3, 0xb3, 0x9d, 0x0d, 0xb8, 0xad, 0xc1, 0x8d, 0xf1, 0x78, 0x29, 0x16, 0x72, 0x9a, 0xa2, 0x34, 0xd8, 0x74, 0x42, |     0x3c, 0x2c, 0x95, 0xae, 0x40, 0x67, 0x71, 0x3e, 0x02, 0x81, 0xa6, 0x15, 0xef, 0x4d, 0x16, 0xce, 0x34, 0xb4, 0xf9, | ||||||
|     0x39, 0xaf, 0x44, 0x79, 0x3f, 0xd5, 0x5c, 0x6a, 0x4f, 0x63, 0x23, 0xf2, 0xdd, 0x7c, 0x69, 0x8c, 0x92, 0xdb, 0xb9, |     0x92, 0xb2, 0xf5, 0x4a, 0xab, 0x5e, 0x56, 0x01, 0x73, 0x93, 0x36, 0x7b, 0x9e, 0xd4, 0x74, 0x06, 0x2c, 0x9f, 0x4e, | ||||||
|     0x6a, 0x32, 0x6c, 0xa6, 0xc1, 0xac, 0x23, 0xbc, 0x86, 0x67, 0x62, 0xa9, 0xa7, 0x24, 0x6c, 0xb0, 0x9a, 0xcd, 0x79, |     0x75, 0x5d, 0xe7, 0x82, 0x4b, 0x08, 0xc6, 0x59, 0x96, 0xa5, 0xe1, 0x8d, 0x13, 0xbb, 0x70, 0x33, 0x4c, 0x1d, 0x62, | ||||||
|     0x7a, 0xb7, 0x68, 0xd4, 0x52, 0x66, 0x5e, 0x6a, 0x6f, 0xe1, 0xe9, 0x73, 0x9a, 0xf3, 0x10, 0xd3, 0xd9, 0x7e, 0x96, |     0xf4, 0x31, 0x89, 0xe3, 0xef, 0xf2, 0x53, 0x38, 0x71, 0xce, 0x7a, 0x6d, 0x94, 0xce, 0x3a, 0xc5, 0x9d, 0x9b, 0xc7, | ||||||
|     0xe7, 0xf9, 0xac, 0x14, 0x12, 0xbd, 0xee, 0x56, 0x9b, 0x32, 0x32, 0xb0, 0x62, 0x27, 0x66, 0x12, 0x66, 0x17, 0x3a, |     0x96, 0x72, 0x79, 0xe9, 0xbd, 0x2b, 0x93, 0x7c, 0x5a, 0x3f, 0x19, 0x97, 0x83, 0x99, 0x61, 0x09, 0xe5, 0x2d, 0x97, | ||||||
|     0x1b, 0x69, 0x10, 0x9c, 0xcd, 0x0e, 0xee, 0x04, 0xb3, 0x74, 0xd9, 0x68, 0xd5, 0x4c, 0x6b, 0x25, 0xac, 0x99, 0xbb, |     0xe3, 0x0e, 0xcd, 0xd2, 0x45, 0xdc, 0xed, 0x8e, 0xe1, 0x54, 0x20, 0x87, 0x13, 0x77, 0x2d, 0x60, 0x97, 0x7f, 0xee, | ||||||
|     0x8a, 0x0b, 0x79, 0x6a, 0xbd, 0x0d, 0x93, 0xd9, 0xbe, 0x3c, 0x4d, 0x85, 0x6c, 0x8f, 0x69, 0x8b, 0xd4, 0xac, 0x12, |     0x8d, 0xe5, 0xf5, 0x3e, 0x98, 0x76, 0x70, 0x66, 0x3a, 0xca, 0x20, 0x58, 0x82, 0xdd, 0x02, 0xc8, 0x7c, 0xb0, 0x11, | ||||||
|     0xb2, 0x2b, 0xb2, 0x53, 0x36, 0x0a, 0xea, 0xcd, 0x8e, 0xec, 0x03, 0x64, 0x7b, 0xe0, 0xce, 0x4b, 0xdc, 0xcc, 0x3e, |     0x70, 0x0b, 0xad, 0x99, 0xf2, 0x74, 0x56, 0x33, 0x14, 0xe8, 0xd7, 0xba, 0xfe, 0x1b, 0xb7, 0xab, 0xc5, 0x43, 0x4b, | ||||||
|     0x2d, 0xb5, 0x11, 0xf9, 0xbd, 0xb7, 0x2f, 0xd2, 0x53, 0x5d, 0xf3, 0x14, 0xbd, 0x39, 0x9a, 0x35, 0xa2, 0x9c, 0xb5, |     0xf5, 0x8a, 0xcb, 0x60, 0xa9, 0xac, 0x55, 0x6d, 0x16, 0xbc, 0xea, 0x76, 0xf9, 0x84, 0x72, 0xca, 0xb2, 0xc4, 0xb9, | ||||||
|     0x67, 0x78, 0xc2, 0x60, 0xa5, 0xf7, 0x38, 0x1d, 0xd5, 0xb4, 0x01, 0xfa, 0x58, 0xd7, 0xbf, 0xe3, 0xb6, 0xb1, 0xb8, |     0x39, 0xec, 0xd6, 0x53, 0xbe, 0x93, 0x6e, 0x87, 0x8c, 0x12, 0xbc, 0x9a, 0xf8, 0x06, 0x16, 0x14, 0x9f, 0xd3, 0x93, | ||||||
|     0xad, 0x78, 0xb3, 0x10, 0xd2, 0x9b, 0x2b, 0x63, 0x54, 0x35, 0xf5, 0xc6, 0xf5, 0x66, 0xb6, 0x5f, 0xb2, 0xca, 0xa6, |     0xcc, 0xbb, 0x1d, 0x72, 0xb8, 0x53, 0xaa, 0x6f, 0xea, 0x5b, 0x9a, 0xc4, 0x7f, 0xf1, 0x45, 0xaa, 0xba, 0x4e, 0x97, | ||||||
|     0xd4, 0x9a, 0xd9, 0xd6, 0xde, 0x03, 0xde, 0xb4, 0xde, 0x80, 0x56, 0xa5, 0xc8, 0xf6, 0x7c, 0x2d, 0x0b, 0x04, 0x47, |     0xf5, 0x39, 0x53, 0x6e, 0x4d, 0xba, 0xd6, 0x18, 0x4a, 0xab, 0x88, 0xc6, 0xdb, 0x8c, 0xab, 0x8c, 0xb2, 0x70, 0x19, | ||||||
|     0x78, 0xe8, 0xb0, 0xde, 0x80, 0x5d, 0x3b, 0x40, 0x3d, 0xc8, 0x27, 0x9c, 0x06, 0x5f, 0xf9, 0x46, 0xb2, 0x3c, 0x67, |     0x2e, 0x8b, 0x26, 0x41, 0xbc, 0x22, 0x2d, 0x65, 0xe5, 0xc5, 0xf8, 0x2a, 0xa2, 0x26, 0x39, 0x91, 0x9a, 0xa4, 0xfc, | ||||||
|     0xf3, 0xfc, 0x88, 0x94, 0x2d, 0xa1, 0x3b, 0xb1, 0x8f, 0x0a, 0x36, 0xa8, 0x37, 0xb3, 0xc3, 0x77, 0x33, 0xa8, 0x37, |     0x6a, 0x18, 0x8d, 0xb4, 0xc1, 0xfb, 0xf2, 0xad, 0x92, 0x12, 0x98, 0xe5, 0x72, 0x85, 0xac, 0x42, 0x53, 0x0a, 0xc2, | ||||||
|     0x3b, 0xd1, 0xa6, 0xc5, 0xf6, 0x44, 0x4b, 0x1b, 0xaa, 0xd3, 0x65, 0x53, 0xf6, 0x9d, 0xaf, 0x84, 0xee, 0x59, 0x78, |     0x30, 0x2c, 0x96, 0xba, 0x7c, 0x2f, 0x80, 0x1a, 0x40, 0x5b, 0xca, 0x6d, 0x58, 0x44, 0x23, 0xff, 0xd8, 0xc7, 0xbc, | ||||||
|     0xf5, 0x50, 0xe2, 0x7a, 0x4f, 0x97, 0xb8, 0x1e, 0xd8, 0xa6, 0xe8, 0x95, 0xda, 0xc4, 0xbd, 0xb6, 0xd8, 0x0c, 0x80, |     0x22, 0x12, 0x6c, 0x39, 0x35, 0x6c, 0xd1, 0xcc, 0x46, 0x03, 0x77, 0x60, 0x9d, 0x26, 0x67, 0x60, 0x56, 0x16, 0x6e, | ||||||
|     0x0d, 0x7a, 0x67, 0xe1, 0xeb, 0xb3, 0xf0, 0xea, 0xbf, 0x52, 0xbb, 0x7e, 0x77, 0xe1, 0xfa, 0x86, 0xaa, 0xf5, 0x8d, |     0xe5, 0x22, 0x3a, 0x8c, 0x34, 0x12, 0x6d, 0x79, 0xcd, 0xdd, 0x95, 0xa5, 0x2c, 0x86, 0x22, 0x77, 0x1a, 0x5c, 0x9e, | ||||||
|     0x15, 0xab, 0xf3, 0xce, 0x3a, 0x7f, 0x16, 0xbe, 0x76, 0xdc, 0x9d, 0x20, 0x5a, 0x2c, 0xe8, 0xff, 0x02, 0xda, 0x7f, |     0xc7, 0xeb, 0xd5, 0x00, 0x09, 0x90, 0x2b, 0xdb, 0x90, 0x59, 0x8a, 0x3a, 0x41, 0x19, 0x34, 0x4a, 0x54, 0xa0, 0xc9, | ||||||
|     0xc5, 0x31, 0xbc, 0xa4, 0x13, 0x72, 0x01, 0xed, 0xd0, 0x41, 0x44, 0xc2, 0x09, 0x8c, 0xaf, 0x06, 0x64, 0x40, 0xc1, |     0xdd, 0xdd, 0xaf, 0x7f, 0x2f, 0x9d, 0x33, 0x8f, 0x72, 0x9d, 0x59, 0x8f, 0x62, 0x0e, 0x98, 0xa4, 0x16, 0x37, 0xe3, | ||||||
|     0xb6, 0x43, 0x23, 0x18, 0x93, 0xc9, 0x05, 0xd0, 0x11, 0x09, 0xc7, 0x40, 0x19, 0x30, 0x4a, 0x86, 0x6f, 0x58, 0x48, |     0xa5, 0xaa, 0xa3, 0xc6, 0x6c, 0x95, 0xae, 0xbe, 0xd2, 0xf1, 0x7e, 0x42, 0x8e, 0x7a, 0x86, 0xff, 0xd0, 0x2a, 0xe5, | ||||||
|     0x46, 0x43, 0x18, 0x5f, 0xb1, 0x80, 0x84, 0x0c, 0x3a, 0xde, 0x11, 0x61, 0x0c, 0x42, 0xcb, 0x12, 0x56, 0x01, 0xb0, |     0x1d, 0xdd, 0x40, 0x11, 0x4d, 0x87, 0x22, 0x72, 0x0e, 0x8f, 0xf4, 0x66, 0xe2, 0x6b, 0x92, 0xf2, 0x8f, 0xfb, 0x37, | ||||||
|     0x34, 0x24, 0xc1, 0x18, 0x02, 0x18, 0x91, 0xe0, 0x82, 0x4c, 0x46, 0x30, 0x21, 0x63, 0x0a, 0x8c, 0x0c, 0x86, 0xa5, |     0xe8, 0xcf, 0xae, 0xa2, 0x16, 0xc6, 0xb4, 0x0d, 0x51, 0xb5, 0x60, 0x1b, 0x55, 0x91, 0xf7, 0x7f, 0xdc, 0xdd, 0x9f, | ||||||
|     0x37, 0x24, 0x14, 0x46, 0x24, 0x1c, 0xf1, 0x09, 0x19, 0x84, 0xd0, 0x0e, 0x1d, 0x1c, 0x63, 0xc2, 0x98, 0x47, 0x02, |     0x23, 0xec, 0x07, 0x26, 0x04, 0x92, 0x8d, 0xd7, 0xbb, 0x5e, 0x58, 0xde, 0x51, 0x6d, 0x07, 0xb5, 0x81, 0x9b, 0x22, | ||||||
|     0xfa, 0x26, 0x24, 0x6c, 0x0c, 0x63, 0x32, 0x18, 0x5c, 0xd2, 0x11, 0xb9, 0x18, 0x40, 0x37, 0x76, 0xf0, 0x52, 0x06, |     0xa7, 0x18, 0x06, 0x7a, 0xcd, 0x05, 0x8c, 0x61, 0x8c, 0x82, 0x25, 0x3a, 0x79, 0x75, 0xb2, 0xf6, 0xc4, 0xaf, 0x68, | ||||||
|     0xc3, 0xa7, 0x40, 0x63, 0x7f, 0x5e, 0xd0, 0x42, 0xc2, 0x28, 0x84, 0xe4, 0x62, 0xc2, 0x6d, 0x5f, 0xca, 0xa0, 0x1b, |     0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; | ||||||
|     0x3b, 0xdc, 0x28, 0x85, 0xe0, 0x77, 0x63, 0x16, 0xfe, 0x79, 0x31, 0xa3, 0x16, 0x01, 0x46, 0x06, 0xe1, 0x25, 0x0d, |  | ||||||
|     0xc9, 0x08, 0xda, 0xa1, 0x3b, 0x9b, 0x32, 0x98, 0x5c, 0x5d, 0xc0, 0x04, 0x46, 0x64, 0x34, 0x81, 0x0b, 0x18, 0x5a, |  | ||||||
|     0x74, 0x2f, 0xc8, 0x64, 0xd0, 0x09, 0x79, 0x8c, 0x7c, 0x2b, 0x8c, 0x83, 0x3f, 0x30, 0x8c, 0x4f, 0xf9, 0xf4, 0x07, |  | ||||||
|     0x76, 0xe9, 0xff, 0x71, 0x05, 0x45, 0x7e, 0xd7, 0x86, 0x45, 0x7e, 0xf7, 0x3c, 0x60, 0xbb, 0xa8, 0x24, 0xb2, 0xdd, |  | ||||||
|     0x48, 0x12, 0x15, 0x14, 0x44, 0x16, 0x57, 0x3c, 0x4d, 0x4e, 0x5a, 0xfd, 0xc8, 0x2f, 0xe8, 0x61, 0xab, 0xa0, 0xc9, |  | ||||||
|     0xa3, 0xc6, 0xbd, 0xdb, 0x6b, 0x2b, 0x7d, 0x72, 0x53, 0x20, 0xbc, 0xbe, 0x7e, 0x07, 0x6b, 0x51, 0x96, 0x20, 0xd5, |  | ||||||
|     0x1a, 0x4c, 0x73, 0x0f, 0x46, 0xd9, 0x57, 0x03, 0x89, 0xa9, 0xb1, 0xa4, 0x29, 0x10, 0xf6, 0x7d, 0x04, 0x21, 0x24, |  | ||||||
|     0x9a, 0x37, 0xc9, 0xbb, 0x12, 0xb9, 0x46, 0x58, 0x88, 0x15, 0x82, 0x30, 0xa0, 0x55, 0x85, 0x60, 0x84, 0x1d, 0x8e, |  | ||||||
|     0x82, 0x2d, 0x5f, 0xe4, 0x77, 0x87, 0x74, 0x8d, 0xb2, 0xc8, 0x62, 0x89, 0x26, 0xd9, 0x77, 0xc4, 0x51, 0x11, 0x76, |  | ||||||
|     0x56, 0x5d, 0xa3, 0x31, 0x42, 0x2e, 0xac, 0x55, 0x61, 0x12, 0xd9, 0x5f, 0xb7, 0xc0, 0xdb, 0xdf, 0x0c, 0xb1, 0xbf, |  | ||||||
|     0x16, 0xb9, 0xb0, 0x6f, 0x06, 0x49, 0xd4, 0x76, 0x91, 0x56, 0x83, 0x6d, 0x64, 0xba, 0x07, 0x8e, 0x96, 0x2a, 0x51, |  | ||||||
|     0x2e, 0x4c, 0x11, 0x87, 0x0c, 0xea, 0x92, 0xa7, 0x58, 0xa8, 0x32, 0xc3, 0x26, 0xbe, 0xbe, 0xfe, 0xf9, 0xaf, 0xf6, |  | ||||||
|     0x35, 0xc4, 0x9a, 0x70, 0x94, 0xac, 0xf5, 0x5d, 0x27, 0x68, 0x89, 0xbd, 0xdc, 0x68, 0xd0, 0xbd, 0x6b, 0xd4, 0x5c, |  | ||||||
|     0xeb, 0xb5, 0x6a, 0xb2, 0x47, 0x5a, 0xde, 0x1d, 0x16, 0xf7, 0x9a, 0xda, 0xff, 0xb6, 0x1f, 0xed, 0x84, 0xf4, 0x72, |  | ||||||
|     0x5e, 0x09, 0x93, 0x5c, 0xf3, 0x15, 0x46, 0x7e, 0xb7, 0x91, 0x44, 0xbe, 0x75, 0xa0, 0xe3, 0x2d, 0xf6, 0x32, 0x05, |  | ||||||
|     0x4d, 0x7e, 0xbd, 0xb9, 0x84, 0xdf, 0xea, 0x8c, 0x1b, 0xec, 0xb0, 0x6f, 0xbd, 0xac, 0xd0, 0x14, 0x2a, 0x8b, 0xdf, |  | ||||||
|     0xfd, 0x7a, 0x7d, 0x73, 0xf4, 0x78, 0xd9, 0x32, 0x01, 0xca, 0xb4, 0x7b, 0x6f, 0x59, 0x96, 0x46, 0xd4, 0xbc, 0x31, |  | ||||||
|     0xad, 0x5a, 0xcf, 0x66, 0xc7, 0xc1, 0xa3, 0x76, 0x3f, 0x17, 0x25, 0x76, 0x4e, 0xed, 0x05, 0xfd, 0x04, 0xbe, 0x66, |  | ||||||
|     0xe3, 0xe1, 0xec, 0x2f, 0xac, 0xf4, 0xbb, 0x00, 0xf2, 0xbb, 0x68, 0xf2, 0xdb, 0xd7, 0xa8, 0x7f, 0x02, 0x14, 0xee, |  | ||||||
|     0xbc, 0x64, 0x9d, 0x12, 0x00, 0x00}; |  | ||||||
|  |  | ||||||
| }  // namespace captive_portal | }  // namespace captive_portal | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -11,17 +11,35 @@ namespace captive_portal { | |||||||
| static const char *const TAG = "captive_portal"; | static const char *const TAG = "captive_portal"; | ||||||
|  |  | ||||||
| void CaptivePortal::handle_config(AsyncWebServerRequest *request) { | void CaptivePortal::handle_config(AsyncWebServerRequest *request) { | ||||||
|   AsyncResponseStream *stream = request->beginResponseStream("application/json"); |   AsyncResponseStream *stream = request->beginResponseStream(F("application/json")); | ||||||
|   stream->addHeader("cache-control", "public, max-age=0, must-revalidate"); |   stream->addHeader(F("cache-control"), F("public, max-age=0, must-revalidate")); | ||||||
|  | #ifdef USE_ESP8266 | ||||||
|  |   stream->print(F("{\"mac\":\"")); | ||||||
|  |   stream->print(get_mac_address_pretty().c_str()); | ||||||
|  |   stream->print(F("\",\"name\":\"")); | ||||||
|  |   stream->print(App.get_name().c_str()); | ||||||
|  |   stream->print(F("\",\"aps\":[{}")); | ||||||
|  | #else | ||||||
|   stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str()); |   stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str()); | ||||||
|  | #endif | ||||||
|  |  | ||||||
|   for (auto &scan : wifi::global_wifi_component->get_scan_result()) { |   for (auto &scan : wifi::global_wifi_component->get_scan_result()) { | ||||||
|     if (scan.get_is_hidden()) |     if (scan.get_is_hidden()) | ||||||
|       continue; |       continue; | ||||||
|  |  | ||||||
|     // Assumes no " in ssid, possible unicode isses? |       // Assumes no " in ssid, possible unicode isses? | ||||||
|  | #ifdef USE_ESP8266 | ||||||
|  |     stream->print(F(",{\"ssid\":\"")); | ||||||
|  |     stream->print(scan.get_ssid().c_str()); | ||||||
|  |     stream->print(F("\",\"rssi\":")); | ||||||
|  |     stream->print(scan.get_rssi()); | ||||||
|  |     stream->print(F(",\"lock\":")); | ||||||
|  |     stream->print(scan.get_with_auth()); | ||||||
|  |     stream->print(F("}")); | ||||||
|  | #else | ||||||
|     stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(), |     stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(), | ||||||
|                    scan.get_with_auth()); |                    scan.get_with_auth()); | ||||||
|  | #endif | ||||||
|   } |   } | ||||||
|   stream->print(F("]}")); |   stream->print(F("]}")); | ||||||
|   request->send(stream); |   request->send(stream); | ||||||
| @@ -34,7 +52,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { | |||||||
|   ESP_LOGI(TAG, "  Password=" LOG_SECRET("'%s'"), psk.c_str()); |   ESP_LOGI(TAG, "  Password=" LOG_SECRET("'%s'"), psk.c_str()); | ||||||
|   wifi::global_wifi_component->save_wifi_sta(ssid, psk); |   wifi::global_wifi_component->save_wifi_sta(ssid, psk); | ||||||
|   wifi::global_wifi_component->start_scanning(); |   wifi::global_wifi_component->start_scanning(); | ||||||
|   request->redirect("/?save"); |   request->redirect(F("/?save")); | ||||||
| } | } | ||||||
|  |  | ||||||
| void CaptivePortal::setup() { | void CaptivePortal::setup() { | ||||||
| @@ -53,18 +71,23 @@ void CaptivePortal::start() { | |||||||
|   this->dns_server_ = make_unique<DNSServer>(); |   this->dns_server_ = make_unique<DNSServer>(); | ||||||
|   this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); |   this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); | ||||||
|   network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); |   network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); | ||||||
|   this->dns_server_->start(53, "*", ip); |   this->dns_server_->start(53, F("*"), ip); | ||||||
|   // Re-enable loop() when DNS server is started |   // Re-enable loop() when DNS server is started | ||||||
|   this->enable_loop(); |   this->enable_loop(); | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { |   this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { | ||||||
|     if (!this->active_ || req->host().c_str() == wifi::global_wifi_component->wifi_soft_ap_ip().str()) { |     if (!this->active_ || req->host().c_str() == wifi::global_wifi_component->wifi_soft_ap_ip().str()) { | ||||||
|       req->send(404, "text/html", "File not found"); |       req->send(404, F("text/html"), F("File not found")); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP8266 | ||||||
|  |     String url = F("http://"); | ||||||
|  |     url += wifi::global_wifi_component->wifi_soft_ap_ip().str().c_str(); | ||||||
|  | #else | ||||||
|     auto url = "http://" + wifi::global_wifi_component->wifi_soft_ap_ip().str(); |     auto url = "http://" + wifi::global_wifi_component->wifi_soft_ap_ip().str(); | ||||||
|  | #endif | ||||||
|     req->redirect(url.c_str()); |     req->redirect(url.c_str()); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
| @@ -73,19 +96,19 @@ void CaptivePortal::start() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { | void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { | ||||||
|   if (req->url() == "/") { |   if (req->url() == F("/")) { | ||||||
| #ifndef USE_ESP8266 | #ifndef USE_ESP8266 | ||||||
|     auto *response = req->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); |     auto *response = req->beginResponse(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); | ||||||
| #else | #else | ||||||
|     auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); |     auto *response = req->beginResponse_P(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); | ||||||
| #endif | #endif | ||||||
|     response->addHeader("Content-Encoding", "gzip"); |     response->addHeader(F("Content-Encoding"), F("gzip")); | ||||||
|     req->send(response); |     req->send(response); | ||||||
|     return; |     return; | ||||||
|   } else if (req->url() == "/config.json") { |   } else if (req->url() == F("/config.json")) { | ||||||
|     this->handle_config(req); |     this->handle_config(req); | ||||||
|     return; |     return; | ||||||
|   } else if (req->url() == "/wifisave") { |   } else if (req->url() == F("/wifisave")) { | ||||||
|     this->handle_wifisave(req); |     this->handle_wifisave(req); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -45,11 +45,11 @@ class CaptivePortal : public AsyncWebHandler, public Component { | |||||||
|       return false; |       return false; | ||||||
|  |  | ||||||
|     if (request->method() == HTTP_GET) { |     if (request->method() == HTTP_GET) { | ||||||
|       if (request->url() == "/") |       if (request->url() == F("/")) | ||||||
|         return true; |         return true; | ||||||
|       if (request->url() == "/config.json") |       if (request->url() == F("/config.json")) | ||||||
|         return true; |         return true; | ||||||
|       if (request->url() == "/wifisave") |       if (request->url() == F("/wifisave")) | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -152,9 +152,9 @@ void CCS811Component::send_env_data_() { | |||||||
| void CCS811Component::dump_config() { | void CCS811Component::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, "CCS811"); |   ESP_LOGCONFIG(TAG, "CCS811"); | ||||||
|   LOG_I2C_DEVICE(this) |   LOG_I2C_DEVICE(this) | ||||||
|   LOG_UPDATE_INTERVAL(this) |   LOG_UPDATE_INTERVAL(this); | ||||||
|   LOG_SENSOR("  ", "CO2 Sensor", this->co2_) |   LOG_SENSOR("  ", "CO2 Sensor", this->co2_); | ||||||
|   LOG_SENSOR("  ", "TVOC Sensor", this->tvoc_) |   LOG_SENSOR("  ", "TVOC Sensor", this->tvoc_); | ||||||
|   LOG_TEXT_SENSOR("  ", "Firmware Version Sensor", this->version_) |   LOG_TEXT_SENSOR("  ", "Firmware Version Sensor", this->version_) | ||||||
|   if (this->baseline_) { |   if (this->baseline_) { | ||||||
|     ESP_LOGCONFIG(TAG, "  Baseline: %04X", *this->baseline_); |     ESP_LOGCONFIG(TAG, "  Baseline: %04X", *this->baseline_); | ||||||
|   | |||||||
| @@ -47,7 +47,7 @@ from esphome.const import ( | |||||||
|     CONF_VISUAL, |     CONF_VISUAL, | ||||||
|     CONF_WEB_SERVER, |     CONF_WEB_SERVER, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, CoroPriority, coroutine_with_priority | ||||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||||
| from esphome.cpp_generator import MockObjClass | from esphome.cpp_generator import MockObjClass | ||||||
|  |  | ||||||
| @@ -517,6 +517,6 @@ async def climate_control_to_code(config, action_id, template_arg, args): | |||||||
|     return var |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
| @coroutine_with_priority(100.0) | @coroutine_with_priority(CoroPriority.CORE) | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     cg.add_global(climate_ns.using) |     cg.add_global(climate_ns.using) | ||||||
|   | |||||||
| @@ -327,7 +327,7 @@ void Climate::add_on_control_callback(std::function<void(ClimateCall &)> &&callb | |||||||
| static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; | static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; | ||||||
|  |  | ||||||
| optional<ClimateDeviceRestoreState> Climate::restore_state_() { | optional<ClimateDeviceRestoreState> Climate::restore_state_() { | ||||||
|   this->rtc_ = global_preferences->make_preference<ClimateDeviceRestoreState>(this->get_object_id_hash() ^ |   this->rtc_ = global_preferences->make_preference<ClimateDeviceRestoreState>(this->get_preference_hash() ^ | ||||||
|                                                                               RESTORE_STATE_VERSION); |                                                                               RESTORE_STATE_VERSION); | ||||||
|   ClimateDeviceRestoreState recovered{}; |   ClimateDeviceRestoreState recovered{}; | ||||||
|   if (!this->rtc_.load(&recovered)) |   if (!this->rtc_.load(&recovered)) | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ from esphome.const import ( | |||||||
|     DEVICE_CLASS_SHUTTER, |     DEVICE_CLASS_SHUTTER, | ||||||
|     DEVICE_CLASS_WINDOW, |     DEVICE_CLASS_WINDOW, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, CoroPriority, coroutine_with_priority | ||||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||||
| from esphome.cpp_generator import MockObjClass | from esphome.cpp_generator import MockObjClass | ||||||
|  |  | ||||||
| @@ -228,9 +228,9 @@ async def cover_stop_to_code(config, action_id, template_arg, args): | |||||||
|  |  | ||||||
|  |  | ||||||
| @automation.register_action("cover.toggle", ToggleAction, COVER_ACTION_SCHEMA) | @automation.register_action("cover.toggle", ToggleAction, COVER_ACTION_SCHEMA) | ||||||
| def cover_toggle_to_code(config, action_id, template_arg, args): | async def cover_toggle_to_code(config, action_id, template_arg, args): | ||||||
|     paren = yield cg.get_variable(config[CONF_ID]) |     paren = await cg.get_variable(config[CONF_ID]) | ||||||
|     yield cg.new_Pvariable(action_id, template_arg, paren) |     return cg.new_Pvariable(action_id, template_arg, paren) | ||||||
|  |  | ||||||
|  |  | ||||||
| COVER_CONTROL_ACTION_SCHEMA = cv.Schema( | COVER_CONTROL_ACTION_SCHEMA = cv.Schema( | ||||||
| @@ -263,6 +263,6 @@ async def cover_control_to_code(config, action_id, template_arg, args): | |||||||
|     return var |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
| @coroutine_with_priority(100.0) | @coroutine_with_priority(CoroPriority.CORE) | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     cg.add_global(cover_ns.using) |     cg.add_global(cover_ns.using) | ||||||
|   | |||||||
| @@ -194,7 +194,7 @@ void Cover::publish_state(bool save) { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| optional<CoverRestoreState> Cover::restore_state_() { | optional<CoverRestoreState> Cover::restore_state_() { | ||||||
|   this->rtc_ = global_preferences->make_preference<CoverRestoreState>(this->get_object_id_hash()); |   this->rtc_ = global_preferences->make_preference<CoverRestoreState>(this->get_preference_hash()); | ||||||
|   CoverRestoreState recovered{}; |   CoverRestoreState recovered{}; | ||||||
|   if (!this->rtc_.load(&recovered)) |   if (!this->rtc_.load(&recovered)) | ||||||
|     return {}; |     return {}; | ||||||
|   | |||||||
| @@ -19,8 +19,8 @@ const extern float COVER_CLOSED; | |||||||
|     if (traits_.get_is_assumed_state()) { \ |     if (traits_.get_is_assumed_state()) { \ | ||||||
|       ESP_LOGCONFIG(TAG, "%s  Assumed State: YES", prefix); \ |       ESP_LOGCONFIG(TAG, "%s  Assumed State: YES", prefix); \ | ||||||
|     } \ |     } \ | ||||||
|     if (!(obj)->get_device_class().empty()) { \ |     if (!(obj)->get_device_class_ref().empty()) { \ | ||||||
|       ESP_LOGCONFIG(TAG, "%s  Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ |       ESP_LOGCONFIG(TAG, "%s  Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ | ||||||
|     } \ |     } \ | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -64,7 +64,7 @@ bool DallasTemperatureSensor::read_scratch_pad_() { | |||||||
|     } |     } | ||||||
|   } else { |   } else { | ||||||
|     ESP_LOGW(TAG, "'%s' - reading scratch pad failed bus reset", this->get_name().c_str()); |     ESP_LOGW(TAG, "'%s' - reading scratch pad failed bus reset", this->get_name().c_str()); | ||||||
|     this->status_set_warning("bus reset failed"); |     this->status_set_warning(LOG_STR("bus reset failed")); | ||||||
|   } |   } | ||||||
|   return success; |   return success; | ||||||
| } | } | ||||||
| @@ -124,7 +124,7 @@ bool DallasTemperatureSensor::check_scratch_pad_() { | |||||||
|             crc8(this->scratch_pad_, 8)); |             crc8(this->scratch_pad_, 8)); | ||||||
| #endif | #endif | ||||||
|   if (!chksum_validity) { |   if (!chksum_validity) { | ||||||
|     this->status_set_warning("scratch pad checksum invalid"); |     this->status_set_warning(LOG_STR("scratch pad checksum invalid")); | ||||||
|     ESP_LOGD(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0], |     ESP_LOGD(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0], | ||||||
|              this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4], |              this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4], | ||||||
|              this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8], |              this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8], | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ from esphome.const import ( | |||||||
|     CONF_WEB_SERVER, |     CONF_WEB_SERVER, | ||||||
|     CONF_YEAR, |     CONF_YEAR, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, CoroPriority, coroutine_with_priority | ||||||
| from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity | ||||||
| from esphome.cpp_generator import MockObjClass | from esphome.cpp_generator import MockObjClass | ||||||
|  |  | ||||||
| @@ -172,7 +172,7 @@ async def new_datetime(config, *args): | |||||||
|     return var |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
| @coroutine_with_priority(100.0) | @coroutine_with_priority(CoroPriority.CORE) | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     cg.add_global(datetime_ns.using) |     cg.add_global(datetime_ns.using) | ||||||
|  |  | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user