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 | ||||
|  | ||||
| *   **Languages:** Python (>=3.10), C++ (gnu++20) | ||||
| *   **Languages:** Python (>=3.11), C++ (gnu++20) | ||||
| *   **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF. | ||||
| *   **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative. | ||||
| *   **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. | ||||
|  | ||||
| *   **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. | ||||
|     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. | ||||
| @@ -60,7 +60,7 @@ This document provides essential context for AI models interacting with this pro | ||||
|         ├── __init__.py          # Component configuration schema and code generation | ||||
|         ├── [component].h        # C++ header file (if needed) | ||||
|         ├── [component].cpp      # C++ implementation (if needed) | ||||
|         └── [platform]/         # Platform-specific implementations | ||||
|         └── [platform]/          # Platform-specific implementations | ||||
|             ├── __init__.py      # Platform-specific configuration | ||||
|             ├── [platform].h     # Platform C++ header | ||||
|             └── [platform].cpp   # Platform C++ implementation | ||||
| @@ -150,7 +150,8 @@ This document provides essential context for AI models interacting with this pro | ||||
| *   **Configuration Validation:** | ||||
|     *   **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)`. | ||||
|     *   **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:** | ||||
|         ```python | ||||
|         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: | ||||
|     - name: Set up Python ${{ inputs.python-version }} | ||||
|       id: python | ||||
|       uses: actions/setup-python@v5.6.0 | ||||
|       uses: actions/setup-python@v6.0.0 | ||||
|       with: | ||||
|         python-version: ${{ inputs.python-version }} | ||||
|     - 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 }} | ||||
|  | ||||
|       - name: Auto Label PR | ||||
|         uses: actions/github-script@v7.0.1 | ||||
|         uses: actions/github-script@v8.0.0 | ||||
|         with: | ||||
|           github-token: ${{ steps.generate-token.outputs.token }} | ||||
|           script: | | ||||
| @@ -105,7 +105,9 @@ jobs: | ||||
|  | ||||
|             // Calculate data from PR files | ||||
|             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('Changed files:', changedFiles.length); | ||||
| @@ -231,16 +233,21 @@ jobs: | ||||
|             // Strategy: PR size detection | ||||
|             async function detectPRSize() { | ||||
|               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) { | ||||
|                 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 | ||||
|               if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) { | ||||
|                 labels.add('too-big'); | ||||
| @@ -375,7 +382,7 @@ jobs: | ||||
|               const labels = new Set(); | ||||
|  | ||||
|               // 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'); | ||||
|               } | ||||
|  | ||||
| @@ -412,10 +419,13 @@ jobs: | ||||
|  | ||||
|               // Too big message | ||||
|               if (finalLabels.includes('too-big')) { | ||||
|                 const testChanges = prFiles | ||||
|                 const testAdditions = prFiles | ||||
|                   .filter(file => file.filename.startsWith('tests/')) | ||||
|                   .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); | ||||
|                 const nonTestChanges = totalChanges - testChanges; | ||||
|                   .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); | ||||
|  | ||||
|                 const tooManyLabels = finalLabels.length > MAX_LABELS; | ||||
|                 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 | ||||
|         uses: actions/checkout@v5.0.0 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v5.6.0 | ||||
|         uses: actions/setup-python@v6.0.0 | ||||
|         with: | ||||
|           python-version: "3.11" | ||||
|  | ||||
| @@ -47,7 +47,7 @@ jobs: | ||||
|           fi | ||||
|       - if: failure() | ||||
|         name: Review PR | ||||
|         uses: actions/github-script@v7.0.1 | ||||
|         uses: actions/github-script@v8.0.0 | ||||
|         with: | ||||
|           script: | | ||||
|             await github.rest.pulls.createReview({ | ||||
| @@ -70,7 +70,7 @@ jobs: | ||||
|             esphome/components/api/api_pb2_service.* | ||||
|       - if: success() | ||||
|         name: Dismiss review | ||||
|         uses: actions/github-script@v7.0.1 | ||||
|         uses: actions/github-script@v8.0.0 | ||||
|         with: | ||||
|           script: | | ||||
|             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 | ||||
|  | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v5.6.0 | ||||
|         uses: actions/setup-python@v6.0.0 | ||||
|         with: | ||||
|           python-version: "3.11" | ||||
|  | ||||
| @@ -41,7 +41,7 @@ jobs: | ||||
|  | ||||
|       - if: failure() | ||||
|         name: Request changes | ||||
|         uses: actions/github-script@v7.0.1 | ||||
|         uses: actions/github-script@v8.0.0 | ||||
|         with: | ||||
|           script: | | ||||
|             await github.rest.pulls.createReview({ | ||||
| @@ -54,7 +54,7 @@ jobs: | ||||
|  | ||||
|       - if: success() | ||||
|         name: Dismiss review | ||||
|         uses: actions/github-script@v7.0.1 | ||||
|         uses: actions/github-script@v8.0.0 | ||||
|         with: | ||||
|           script: | | ||||
|             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: | ||||
|       - uses: actions/checkout@v5.0.0 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v5.6.0 | ||||
|         uses: actions/setup-python@v6.0.0 | ||||
|         with: | ||||
|           python-version: "3.11" | ||||
|       - 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 | ||||
|       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||
|         id: python | ||||
|         uses: actions/setup-python@v5.6.0 | ||||
|         uses: actions/setup-python@v6.0.0 | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|       - name: Restore Python virtual environment | ||||
| @@ -156,7 +156,7 @@ jobs: | ||||
|           . venv/bin/activate | ||||
|           pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ | ||||
|       - name: Upload coverage to Codecov | ||||
|         uses: codecov/codecov-action@v5.4.3 | ||||
|         uses: codecov/codecov-action@v5.5.1 | ||||
|         with: | ||||
|           token: ${{ secrets.CODECOV_TOKEN }} | ||||
|       - name: Save Python virtual environment cache | ||||
| @@ -217,7 +217,7 @@ jobs: | ||||
|         uses: actions/checkout@v5.0.0 | ||||
|       - name: Set up Python 3.13 | ||||
|         id: python | ||||
|         uses: actions/setup-python@v5.6.0 | ||||
|         uses: actions/setup-python@v6.0.0 | ||||
|         with: | ||||
|           python-version: "3.13" | ||||
|       - name: Restore Python virtual environment | ||||
|   | ||||
| @@ -25,7 +25,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Request reviews from component codeowners | ||||
|         uses: actions/github-script@v7.0.1 | ||||
|         uses: actions/github-script@v8.0.0 | ||||
|         with: | ||||
|           script: | | ||||
|             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 | ||||
|     steps: | ||||
|       - name: Add external component comment | ||||
|         uses: actions/github-script@v7.0.1 | ||||
|         uses: actions/github-script@v8.0.0 | ||||
|         with: | ||||
|           github-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|           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 | ||||
|     steps: | ||||
|       - name: Notify codeowners for component issues | ||||
|         uses: actions/github-script@v7.0.1 | ||||
|         uses: actions/github-script@v8.0.0 | ||||
|         with: | ||||
|           script: | | ||||
|             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: | ||||
|       - uses: actions/checkout@v5.0.0 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v5.6.0 | ||||
|         uses: actions/setup-python@v6.0.0 | ||||
|         with: | ||||
|           python-version: "3.x" | ||||
|       - name: Build | ||||
| @@ -70,7 +70,7 @@ jobs: | ||||
|           pip3 install build | ||||
|           python3 -m build | ||||
|       - name: Publish | ||||
|         uses: pypa/gh-action-pypi-publish@v1.12.4 | ||||
|         uses: pypa/gh-action-pypi-publish@v1.13.0 | ||||
|         with: | ||||
|           skip-existing: true | ||||
|  | ||||
| @@ -94,7 +94,7 @@ jobs: | ||||
|     steps: | ||||
|       - uses: actions/checkout@v5.0.0 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v5.6.0 | ||||
|         uses: actions/setup-python@v6.0.0 | ||||
|         with: | ||||
|           python-version: "3.11" | ||||
|  | ||||
| @@ -220,7 +220,7 @@ jobs: | ||||
|       - deploy-manifest | ||||
|     steps: | ||||
|       - name: Trigger Workflow | ||||
|         uses: actions/github-script@v7.0.1 | ||||
|         uses: actions/github-script@v8.0.0 | ||||
|         with: | ||||
|           github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} | ||||
|           script: | | ||||
| @@ -246,7 +246,7 @@ jobs: | ||||
|     environment: ${{ needs.init.outputs.deploy_env }} | ||||
|     steps: | ||||
|       - name: Trigger Workflow | ||||
|         uses: actions/github-script@v7.0.1 | ||||
|         uses: actions/github-script@v8.0.0 | ||||
|         with: | ||||
|           github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }} | ||||
|           script: | | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ jobs: | ||||
|   stale: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/stale@v9.1.0 | ||||
|       - uses: actions/stale@v10.0.0 | ||||
|         with: | ||||
|           days-before-pr-stale: 90 | ||||
|           days-before-pr-close: 7 | ||||
| @@ -37,7 +37,7 @@ jobs: | ||||
|   close-issues: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/stale@v9.1.0 | ||||
|       - uses: actions/stale@v10.0.0 | ||||
|         with: | ||||
|           days-before-pr-stale: -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 | ||||
|  | ||||
|       - name: Setup Python | ||||
|         uses: actions/setup-python@v5.6.0 | ||||
|         uses: actions/setup-python@v6.0.0 | ||||
|         with: | ||||
|           python-version: 3.13 | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,7 @@ ci: | ||||
| repos: | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     # Ruff version. | ||||
|     rev: v0.12.8 | ||||
|     rev: v0.12.12 | ||||
|     hooks: | ||||
|       # Run the linter. | ||||
|       - id: ruff | ||||
|   | ||||
							
								
								
									
										34
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								CODEOWNERS
									
									
									
									
									
								
							| @@ -66,7 +66,7 @@ esphome/components/binary_sensor/* @esphome/core | ||||
| esphome/components/bk72xx/* @kuba2k2 | ||||
| esphome/components/bl0906/* @athom-tech @jesserockz @tarontop | ||||
| esphome/components/bl0939/* @ziceva | ||||
| esphome/components/bl0940/* @tobias- | ||||
| esphome/components/bl0940/* @dan-s-github @tobias- | ||||
| esphome/components/bl0942/* @dbuezas @dwmw2 | ||||
| esphome/components/ble_client/* @buxtronix @clydebarrow | ||||
| esphome/components/bluetooth_proxy/* @bdraco @jesserockz | ||||
| @@ -88,7 +88,8 @@ esphome/components/bp1658cj/* @Cossid | ||||
| esphome/components/bp5758d/* @Cossid | ||||
| esphome/components/button/* @esphome/core | ||||
| 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/cap1188/* @mreditor97 | ||||
| esphome/components/captive_portal/* @esphome/core | ||||
| @@ -144,9 +145,9 @@ esphome/components/es8156/* @kbx81 | ||||
| esphome/components/es8311/* @kahrendt @kroimon | ||||
| esphome/components/es8388/* @P4uLT | ||||
| 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_server/* @Rapsssito @clydebarrow @jesserockz | ||||
| esphome/components/esp32_ble_server/* @clydebarrow @jesserockz @Rapsssito | ||||
| esphome/components/esp32_ble_tracker/* @bdraco | ||||
| esphome/components/esp32_camera_web_server/* @ayufan | ||||
| esphome/components/esp32_can/* @Sympatron | ||||
| @@ -166,7 +167,7 @@ esphome/components/ezo_pmp/* @carlos-sarmiento | ||||
| esphome/components/factory_reset/* @anatoly-savchenkov | ||||
| esphome/components/fastled_base/* @OttoWinter | ||||
| 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/fs3000/* @kahrendt | ||||
| esphome/components/ft5x06/* @clydebarrow | ||||
| @@ -202,7 +203,7 @@ esphome/components/heatpumpir/* @rob-deutsch | ||||
| esphome/components/hitachi_ac424/* @sourabhjaiswal | ||||
| esphome/components/hm3301/* @freekode | ||||
| 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/switch/* @Links2004 | ||||
| esphome/components/honeywell_hih_i2c/* @Benichou34 | ||||
| @@ -227,13 +228,13 @@ esphome/components/iaqcore/* @yozik04 | ||||
| esphome/components/ili9xxx/* @clydebarrow @nielsnl68 | ||||
| esphome/components/improv_base/* @esphome/core | ||||
| esphome/components/improv_serial/* @esphome/core | ||||
| esphome/components/ina226/* @Sergio303 @latonita | ||||
| esphome/components/ina226/* @latonita @Sergio303 | ||||
| esphome/components/ina260/* @mreditor97 | ||||
| esphome/components/ina2xx_base/* @latonita | ||||
| esphome/components/ina2xx_i2c/* @latonita | ||||
| esphome/components/ina2xx_spi/* @latonita | ||||
| esphome/components/inkbird_ibsth1_mini/* @fkirill | ||||
| esphome/components/inkplate6/* @jesserockz | ||||
| esphome/components/inkplate/* @jesserockz @JosipKuci | ||||
| esphome/components/integration/* @OttoWinter | ||||
| esphome/components/internal_temperature/* @Mat931 | ||||
| esphome/components/interval/* @esphome/core | ||||
| @@ -276,8 +277,8 @@ esphome/components/max7219digit/* @rspaargaren | ||||
| esphome/components/max9611/* @mckaymatthew | ||||
| esphome/components/mcp23008/* @jesserockz | ||||
| esphome/components/mcp23017/* @jesserockz | ||||
| esphome/components/mcp23s08/* @SenexCrenshaw @jesserockz | ||||
| esphome/components/mcp23s17/* @SenexCrenshaw @jesserockz | ||||
| esphome/components/mcp23s08/* @jesserockz @SenexCrenshaw | ||||
| esphome/components/mcp23s17/* @jesserockz @SenexCrenshaw | ||||
| esphome/components/mcp23x08_base/* @jesserockz | ||||
| esphome/components/mcp23x17_base/* @jesserockz | ||||
| esphome/components/mcp23xxx_base/* @jesserockz | ||||
| @@ -298,6 +299,7 @@ esphome/components/mics_4514/* @jesserockz | ||||
| esphome/components/midea/* @dudanov | ||||
| esphome/components/midea_ir/* @dudanov | ||||
| esphome/components/mipi_dsi/* @clydebarrow | ||||
| esphome/components/mipi_rgb/* @clydebarrow | ||||
| esphome/components/mipi_spi/* @clydebarrow | ||||
| esphome/components/mitsubishi/* @RubyBailey | ||||
| esphome/components/mixer/speaker/* @kahrendt | ||||
| @@ -341,7 +343,7 @@ esphome/components/ota/* @esphome/core | ||||
| esphome/components/output/* @esphome/core | ||||
| esphome/components/packet_transport/* @clydebarrow | ||||
| esphome/components/pca6416a/* @Mat931 | ||||
| esphome/components/pca9554/* @clydebarrow @hwstar | ||||
| esphome/components/pca9554/* @bdraco @clydebarrow @hwstar | ||||
| esphome/components/pcf85063/* @brogon | ||||
| esphome/components/pcf8563/* @KoenBreeman | ||||
| esphome/components/pi4ioe5v6408/* @jesserockz | ||||
| @@ -352,9 +354,9 @@ esphome/components/pm2005/* @andrewjswan | ||||
| esphome/components/pmsa003i/* @sjtrny | ||||
| esphome/components/pmsx003/* @ximex | ||||
| esphome/components/pmwcs3/* @SeByDocKy | ||||
| esphome/components/pn532/* @OttoWinter @jesserockz | ||||
| esphome/components/pn532_i2c/* @OttoWinter @jesserockz | ||||
| esphome/components/pn532_spi/* @OttoWinter @jesserockz | ||||
| esphome/components/pn532/* @jesserockz @OttoWinter | ||||
| esphome/components/pn532_i2c/* @jesserockz @OttoWinter | ||||
| esphome/components/pn532_spi/* @jesserockz @OttoWinter | ||||
| esphome/components/pn7150/* @jesserockz @kbx81 | ||||
| esphome/components/pn7150_i2c/* @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/preferences/* @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/pylontech/* @functionpointer | ||||
| esphome/components/qmp6988/* @andrewpc | ||||
| @@ -404,7 +406,7 @@ esphome/components/sensirion_common/* @martgras | ||||
| esphome/components/sensor/* @esphome/core | ||||
| esphome/components/sfa30/* @ghsensdev | ||||
| esphome/components/sgp40/* @SenexCrenshaw | ||||
| esphome/components/sgp4x/* @SenexCrenshaw @martgras | ||||
| esphome/components/sgp4x/* @martgras @SenexCrenshaw | ||||
| esphome/components/shelly_dimmer/* @edge90 @rnauber | ||||
| esphome/components/sht3xd/* @mrtoy-me | ||||
| 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 | ||||
| # 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 | ||||
| # 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 | ||||
| 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.const import ( | ||||
|     ALLOWED_NAME_CHARS, | ||||
|     CONF_API, | ||||
|     CONF_BAUD_RATE, | ||||
|     CONF_BROKER, | ||||
|     CONF_DEASSERT_RTS_DTR, | ||||
| @@ -43,6 +45,7 @@ from esphome.const import ( | ||||
|     SECRETS_FILES, | ||||
| ) | ||||
| 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.log import AnsiFore, color, setup_log | ||||
| from esphome.types import ConfigType | ||||
| @@ -106,13 +109,15 @@ def choose_prompt(options, purpose: str = None): | ||||
|     return options[opt - 1][1] | ||||
|  | ||||
|  | ||||
| class Purpose(StrEnum): | ||||
|     UPLOADING = "uploading" | ||||
|     LOGGING = "logging" | ||||
|  | ||||
|  | ||||
| def choose_upload_log_host( | ||||
|     default: list[str] | str | None, | ||||
|     check_default: str | None, | ||||
|     show_ota: bool, | ||||
|     show_mqtt: bool, | ||||
|     show_api: bool, | ||||
|     purpose: str | None = None, | ||||
|     purpose: Purpose, | ||||
| ) -> list[str]: | ||||
|     # Convert to list for uniform handling | ||||
|     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)) | ||||
|             elif device == "OTA": | ||||
|                 if CORE.address and ( | ||||
|                     (show_ota and "ota" in CORE.config) | ||||
|                     or (show_api and "api" in CORE.config) | ||||
|                 # ensure IP adresses are used first | ||||
|                 if is_ip_address(CORE.address) and ( | ||||
|                     (purpose == Purpose.LOGGING and has_api()) | ||||
|                     or (purpose == Purpose.UPLOADING and has_ota()) | ||||
|                 ): | ||||
|                     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: | ||||
|                 resolved.append(device) | ||||
|         if not resolved: | ||||
| @@ -149,39 +171,111 @@ def choose_upload_log_host( | ||||
|     options = [ | ||||
|         (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 show_mqtt and has_mqtt_logging(): | ||||
|         mqtt_config = CORE.config[CONF_MQTT] | ||||
|         options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT")) | ||||
|  | ||||
|     if purpose == Purpose.LOGGING: | ||||
|         if has_mqtt_logging(): | ||||
|             mqtt_config = CORE.config[CONF_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]: | ||||
|         return [check_default] | ||||
|     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] | ||||
|     if log_topic is None: | ||||
|         return False | ||||
|  | ||||
|     if CONF_TOPIC not in log_topic: | ||||
|         return False | ||||
|     return log_topic.get(CONF_LEVEL, None) != "NONE" | ||||
|  | ||||
|     return log_topic[CONF_LEVEL] != "NONE" | ||||
|  | ||||
|  | ||||
| def has_mqtt_logging() -> bool: | ||||
|     """Check if MQTT logging is available.""" | ||||
|     return (mqtt_config := CORE.config.get(CONF_MQTT)) and mqtt_logging_enabled( | ||||
|         mqtt_config | ||||
|     ) | ||||
| def has_mqtt() -> bool: | ||||
|     """Check if MQTT is available.""" | ||||
|     return CONF_MQTT in CORE.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: | ||||
|     if port.startswith("/") or port.startswith("COM"): | ||||
|         return "SERIAL" | ||||
|     if port == "MQTT": | ||||
|         return "MQTT" | ||||
|     return "NETWORK" | ||||
|     return _PORT_TO_PORT_TYPE.get(port, "NETWORK") | ||||
|  | ||||
|  | ||||
| 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"") | ||||
|                         .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)) | ||||
|  | ||||
|                     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: | ||||
|         module = importlib.import_module("esphome.components." + CORE.target_platform) | ||||
|         if getattr(module, "upload_program")(config, args, host): | ||||
|             return 0 | ||||
|             return 0, host | ||||
|     except AttributeError: | ||||
|         pass | ||||
|  | ||||
|     if get_port_type(host) == "SERIAL": | ||||
|         check_permissions(host) | ||||
|  | ||||
|         exit_code = 1 | ||||
|         if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266): | ||||
|             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 upload_using_platformio(config, host) | ||||
|  | ||||
|         if CORE.is_libretiny: | ||||
|             return upload_using_platformio(config, host) | ||||
|  | ||||
|         return 1  # Unknown target platform | ||||
|         return exit_code, host if exit_code == 0 else None | ||||
|  | ||||
|     ota_conf = {} | ||||
|     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]) | ||||
|     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 | ||||
|     # This happens when no device was specified, or the current host is "MQTT"/"OTA" | ||||
|     devices: list[str] = args.device or [] | ||||
|     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 | ||||
|     # MQTT address resolution | ||||
|     if get_port_type(host) in ("MQTT", "MQTTIP"): | ||||
|         devices = mqtt_get_ip(config, args.username, args.password, args.client_id) | ||||
|  | ||||
|         host = mqtt.get_esphome_device_ip( | ||||
|             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) | ||||
|     return espota2.run_ota(devices, remote_port, password, binary) | ||||
|  | ||||
|  | ||||
| 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: | ||||
|         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": | ||||
|         check_permissions(port) | ||||
|         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 | ||||
|             )[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) | ||||
|     if get_port_type(port) in ("NETWORK", "MQTT") and "mqtt" in config: | ||||
|             return run_logs(config, addresses_to_use) | ||||
|  | ||||
|     if port_type in ("NETWORK", "MQTT") and has_mqtt_logging(): | ||||
|         from esphome import mqtt | ||||
|  | ||||
|         return mqtt.show_logs( | ||||
| @@ -545,23 +643,14 @@ def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None: | ||||
|     devices = choose_upload_log_host( | ||||
|         default=args.device, | ||||
|         check_default=None, | ||||
|         show_ota=True, | ||||
|         show_mqtt=False, | ||||
|         show_api=False, | ||||
|         purpose="uploading", | ||||
|         purpose=Purpose.UPLOADING, | ||||
|     ) | ||||
|  | ||||
|     # Try each device until one succeeds | ||||
|     exit_code = 1 | ||||
|     for device in devices: | ||||
|         _LOGGER.info("Uploading to %s", device) | ||||
|         exit_code = upload_program(config, args, device) | ||||
|         if exit_code == 0: | ||||
|             _LOGGER.info("Successfully uploaded program.") | ||||
|             return 0 | ||||
|         if len(devices) > 1: | ||||
|             _LOGGER.warning("Failed to upload to %s", device) | ||||
|  | ||||
|     exit_code, _ = upload_program(config, args, devices) | ||||
|     if exit_code == 0: | ||||
|         _LOGGER.info("Successfully uploaded program.") | ||||
|     else: | ||||
|         _LOGGER.warning("Failed to upload to %s", devices) | ||||
|     return exit_code | ||||
|  | ||||
|  | ||||
| @@ -579,10 +668,7 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None: | ||||
|     devices = choose_upload_log_host( | ||||
|         default=args.device, | ||||
|         check_default=None, | ||||
|         show_ota=False, | ||||
|         show_mqtt=True, | ||||
|         show_api=True, | ||||
|         purpose="logging", | ||||
|         purpose=Purpose.LOGGING, | ||||
|     ) | ||||
|     return show_logs(config, args, devices) | ||||
|  | ||||
| @@ -608,25 +694,14 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: | ||||
|     devices = choose_upload_log_host( | ||||
|         default=args.device, | ||||
|         check_default=None, | ||||
|         show_ota=True, | ||||
|         show_mqtt=False, | ||||
|         show_api=True, | ||||
|         purpose="uploading", | ||||
|         purpose=Purpose.UPLOADING, | ||||
|     ) | ||||
|  | ||||
|     # Try each device for upload until one succeeds | ||||
|     successful_device: str | None = None | ||||
|     for device in devices: | ||||
|         _LOGGER.info("Uploading to %s", device) | ||||
|         exit_code = upload_program(config, args, device) | ||||
|         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: | ||||
|     exit_code, successful_device = upload_program(config, args, devices) | ||||
|     if exit_code == 0: | ||||
|         _LOGGER.info("Successfully uploaded program.") | ||||
|     else: | ||||
|         _LOGGER.warning("Failed to upload to %s", devices) | ||||
|         return exit_code | ||||
|  | ||||
|     if args.no_logs: | ||||
| @@ -636,10 +711,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: | ||||
|     devices = choose_upload_log_host( | ||||
|         default=successful_device, | ||||
|         check_default=successful_device, | ||||
|         show_ota=False, | ||||
|         show_mqtt=True, | ||||
|         show_api=True, | ||||
|         purpose="logging", | ||||
|         purpose=Purpose.LOGGING, | ||||
|     ) | ||||
|     return show_logs(config, args, devices) | ||||
|  | ||||
|   | ||||
| @@ -61,11 +61,10 @@ void AbsoluteHumidityComponent::loop() { | ||||
|       ESP_LOGW(TAG, "No valid state from temperature sensor!"); | ||||
|     } | ||||
|     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->status_set_warning(); | ||||
|     this->status_set_warning(LOG_STR("Unable to calculate absolute humidity.")); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
| @@ -87,9 +86,8 @@ void AbsoluteHumidityComponent::loop() { | ||||
|       es = es_wobus(temperature_c); | ||||
|       break; | ||||
|     default: | ||||
|       ESP_LOGE(TAG, "Invalid saturation vapor pressure equation selection!"); | ||||
|       this->publish_state(NAN); | ||||
|       this->status_set_error(); | ||||
|       this->status_set_error("Invalid saturation vapor pressure equation selection!"); | ||||
|       return; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es); | ||||
|   | ||||
| @@ -11,15 +11,8 @@ from esphome.components.esp32.const import ( | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
| ) | ||||
| from esphome.config_helpers import filter_source_files_from_platform | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ANALOG, | ||||
|     CONF_INPUT, | ||||
|     CONF_NUMBER, | ||||
|     PLATFORM_ESP8266, | ||||
|     PlatformFramework, | ||||
| ) | ||||
| from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 | ||||
| from esphome.core import CORE | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| @@ -273,21 +266,3 @@ def validate_adc_pin(value): | ||||
|         )(value) | ||||
|  | ||||
|     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; | ||||
|  | ||||
|     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 | ||||
|     adc_cali_line_fitting_config_t cali_config = { | ||||
|       .unit_id = this->adc_unit_, | ||||
| @@ -251,10 +253,14 @@ float ADCSensor::sample_autorange_() { | ||||
| #endif | ||||
|     }; | ||||
|     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 | ||||
|  | ||||
|     int 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) { | ||||
|       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); | ||||
|       if (err == ESP_OK) { | ||||
|         voltage = voltage_mv / 1000.0f; | ||||
|         ESP_LOGVV(TAG, "Autorange atten=%d: CALIBRATED - raw=%d -> %dmV -> %.6fV", atten, raw, voltage_mv, voltage); | ||||
|       } else { | ||||
|         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 | ||||
| #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ | ||||
| @@ -287,6 +295,7 @@ float ADCSensor::sample_autorange_() { | ||||
| #endif | ||||
|     } else { | ||||
|       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}; | ||||
| @@ -324,18 +333,32 @@ float ADCSensor::sample_autorange_() { | ||||
|   } | ||||
|  | ||||
|   const int adc_half = 2048; | ||||
|   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); | ||||
|   uint32_t c0 = std::min(4095 - raw0, adc_half); | ||||
|   uint32_t csum = c12 + c6 + c2 + c0; | ||||
|   const uint32_t c12 = std::min(raw12, adc_half); | ||||
|  | ||||
|   const int32_t c6_signed = adc_half - std::abs(raw6 - adc_half); | ||||
|   const uint32_t c6 = (c6_signed > 0) ? c6_signed : 0;  // Clamp to prevent underflow | ||||
|  | ||||
|   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) { | ||||
|     ESP_LOGE(TAG, "Invalid weight sum in autorange calculation"); | ||||
|     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 | ||||
|   | ||||
| @@ -9,6 +9,7 @@ from esphome.components.zephyr import ( | ||||
|     zephyr_add_prj_conf, | ||||
|     zephyr_add_user, | ||||
| ) | ||||
| from esphome.config_helpers import filter_source_files_from_platform | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ATTENUATION, | ||||
| @@ -20,6 +21,7 @@ from esphome.const import ( | ||||
|     PLATFORM_NRF52, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_VOLT, | ||||
|     PlatformFramework, | ||||
| ) | ||||
| 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) { | ||||
|     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->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->apparent_power, AVA, [](float val) { return val / 100.0f; }); | ||||
|     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) { | ||||
|   uint8_t rev_newaddress = ~newaddress; | ||||
|   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)) { | ||||
|     this->error_code_ = COMMUNICATION_FAILED; | ||||
|     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) { | ||||
|   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)) { | ||||
|     this->error_code_ = COMMUNICATION_FAILED; | ||||
|     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 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; | ||||
|     ESP_LOGE(TAG, "Reading AGS10 version failed: crc error!"); | ||||
|     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; | ||||
| } | ||||
|  | ||||
| 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 esphome | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ags10 { | ||||
| @@ -99,16 +99,6 @@ class AGS10Component : public PollingComponent, public i2c::I2CDevice { | ||||
|    * 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); | ||||
|  | ||||
|   /** | ||||
|    * 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> { | ||||
|   | ||||
| @@ -96,7 +96,7 @@ void AHT10Component::read_data_() { | ||||
|     ESP_LOGD(TAG, "Read attempt %d at %ums", this->read_count_, (unsigned) (millis() - this->start_time_)); | ||||
|   } | ||||
|   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_(); | ||||
|     return; | ||||
|   } | ||||
| @@ -113,7 +113,7 @@ void AHT10Component::read_data_() { | ||||
|     } else { | ||||
|       ESP_LOGD(TAG, "Invalid humidity, retrying"); | ||||
|       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_(); | ||||
|       return; | ||||
| @@ -144,7 +144,7 @@ void AHT10Component::update() { | ||||
|     return; | ||||
|   this->start_time_ = millis(); | ||||
|   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; | ||||
|   } | ||||
|   this->restart_read_(); | ||||
|   | ||||
| @@ -18,6 +18,6 @@ CONFIG_SCHEMA = cv.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]) | ||||
|     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_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.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) | ||||
|  | ||||
|  | ||||
| @coroutine_with_priority(100.0) | ||||
| @coroutine_with_priority(CoroPriority.CORE) | ||||
| async def to_code(config): | ||||
|     cg.add_global(alarm_control_panel_ns.using) | ||||
|   | ||||
| @@ -29,22 +29,6 @@ namespace 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) { | ||||
|   //  code based on demo code sent by www.aosong.com | ||||
|   //  no further documentation. | ||||
| @@ -86,7 +70,7 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) { | ||||
|   humidity = raw * 9.5367431640625e-5; | ||||
|   raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]; | ||||
|   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() { | ||||
|   | ||||
| @@ -21,9 +21,9 @@ | ||||
| // SOFTWARE. | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| namespace esphome { | ||||
| 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; } | ||||
|  | ||||
|  protected: | ||||
|   uint8_t crc8_(uint8_t *data, uint8_t len); | ||||
|   bool convert_(uint8_t *data, float &humidity, float &temperature); | ||||
|   bool reset_register_(uint8_t reg); | ||||
|  | ||||
|   | ||||
| @@ -24,7 +24,7 @@ from esphome.const import ( | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_VARIABLES, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.core import CORE, CoroPriority, coroutine_with_priority | ||||
|  | ||||
| DOMAIN = "api" | ||||
| 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): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|   | ||||
| @@ -27,9 +27,6 @@ service APIConnection { | ||||
|   rpc subscribe_logs (SubscribeLogsRequest) returns (void) {} | ||||
|   rpc subscribe_homeassistant_services (SubscribeHomeassistantServicesRequest) 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 noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {} | ||||
|  | ||||
| @@ -809,15 +806,16 @@ message HomeAssistantStateResponse { | ||||
| // ==================== IMPORT TIME ==================== | ||||
| message GetTimeRequest { | ||||
|   option (id) = 36; | ||||
|   option (source) = SOURCE_BOTH; | ||||
|   option (source) = SOURCE_SERVER; | ||||
| } | ||||
|  | ||||
| message GetTimeResponse { | ||||
|   option (id) = 37; | ||||
|   option (source) = SOURCE_BOTH; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
|   option (no_delay) = true; | ||||
|  | ||||
|   fixed32 epoch_seconds = 1; | ||||
|   string timezone = 2; | ||||
| } | ||||
|  | ||||
| // ==================== USER-DEFINES SERVICES ==================== | ||||
| @@ -1712,6 +1710,7 @@ message BluetoothScannerStateResponse { | ||||
|  | ||||
|   BluetoothScannerState state = 1; | ||||
|   BluetoothScannerMode mode = 2; | ||||
|   BluetoothScannerMode configured_mode = 3; | ||||
| } | ||||
|  | ||||
| message BluetoothScannerSetModeRequest { | ||||
|   | ||||
| @@ -42,6 +42,8 @@ static constexpr uint8_t MAX_PING_RETRIES = 60; | ||||
| static constexpr uint16_t PING_RETRY_INTERVAL = 1000; | ||||
| 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"; | ||||
| #ifdef USE_CAMERA | ||||
| static const int CAMERA_STOP_STREAM = 5000; | ||||
| @@ -112,7 +114,7 @@ void APIConnection::start() { | ||||
|   APIError err = this->helper_->init(); | ||||
|   if (err != APIError::OK) { | ||||
|     on_fatal_error(); | ||||
|     this->log_warning_("Helper init failed", err); | ||||
|     this->log_warning_(LOG_STR("Helper init failed"), err); | ||||
|     return; | ||||
|   } | ||||
|   this->client_info_.peername = helper_->getpeername(); | ||||
| @@ -159,7 +161,7 @@ void APIConnection::loop() { | ||||
|         break; | ||||
|       } else if (err != APIError::OK) { | ||||
|         on_fatal_error(); | ||||
|         this->log_warning_("Reading failed", err); | ||||
|         this->log_warning_(LOG_STR("Reading failed"), err); | ||||
|         return; | ||||
|       } else { | ||||
|         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 | ||||
|   } | ||||
|  | ||||
|   // 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) | ||||
|   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 | ||||
|   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) | ||||
|   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 | ||||
| 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); | ||||
| #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 | ||||
|  | ||||
| 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 | ||||
| void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { | ||||
|   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; | ||||
|   resp.api_version_major = 1; | ||||
|   resp.api_version_minor = 12; | ||||
|   // Temporary string for concatenation - will be valid during send_message call | ||||
|   std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; | ||||
|   resp.set_server_info(StringRef(server_info)); | ||||
|   // Send only the version string - the client only logs this for debugging and doesn't use it otherwise | ||||
|   resp.set_server_info(ESPHOME_VERSION_REF); | ||||
|   resp.set_name(StringRef(App.get_name())); | ||||
|  | ||||
| #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(); | ||||
|   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_compilation_time(App.get_compilation_time_ref()); | ||||
| @@ -1555,7 +1564,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { | ||||
|     return false; | ||||
|   if (err != APIError::OK) { | ||||
|     on_fatal_error(); | ||||
|     this->log_warning_("Packet write failed", err); | ||||
|     this->log_warning_(LOG_STR("Packet write failed"), err); | ||||
|     return false; | ||||
|   } | ||||
|   // Do not set last_traffic_ on send | ||||
| @@ -1616,14 +1625,6 @@ bool APIConnection::schedule_batch_() { | ||||
|   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_() { | ||||
|   // Ensure PacketInfo remains trivially destructible for our placement new approach | ||||
|   static_assert(std::is_trivially_destructible<PacketInfo>::value, | ||||
| @@ -1731,7 +1732,7 @@ void APIConnection::process_batch_() { | ||||
|     } | ||||
|     remaining_size -= payload_size; | ||||
|     // 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; | ||||
|   } | ||||
|  | ||||
| @@ -1750,7 +1751,7 @@ void APIConnection::process_batch_() { | ||||
|                                                        std::span<const PacketInfo>(packet_info, packet_count)); | ||||
|   if (err != APIError::OK && err != APIError::WOULD_BLOCK) { | ||||
|     on_fatal_error(); | ||||
|     this->log_warning_("Batch write failed", err); | ||||
|     this->log_warning_(LOG_STR("Batch write failed"), err); | ||||
|   } | ||||
|  | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1828,11 +1829,14 @@ void APIConnection::process_state_subscriptions_() { | ||||
| } | ||||
| #endif  // USE_API_HOMEASSISTANT_STATES | ||||
|  | ||||
| void APIConnection::log_warning_(const char *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); | ||||
| void APIConnection::log_warning_(const LogString *message, APIError err) { | ||||
|   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 | ||||
| #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 | ||||
| #endif | ||||
|  | ||||
| class APIConnection : public APIServerConnection { | ||||
| class APIConnection final : public APIServerConnection { | ||||
|  public: | ||||
|   friend class APIServer; | ||||
|   friend class ListEntitiesIterator; | ||||
| @@ -219,7 +219,6 @@ class APIConnection : public APIServerConnection { | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; | ||||
| #endif | ||||
|   bool send_get_time_response(const GetTimeRequest &msg) override; | ||||
| #ifdef USE_API_SERVICES | ||||
|   void execute_service(const ExecuteServiceRequest &msg) override; | ||||
| #endif | ||||
| @@ -252,44 +251,21 @@ class APIConnection : public APIServerConnection { | ||||
|  | ||||
|     // Get header padding size - used for both reserve and insert | ||||
|     uint8_t header_padding = this->helper_->frame_header_padding(); | ||||
|  | ||||
|     // Get shared buffer from parent server | ||||
|     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(); | ||||
|     // Reserve space for header padding + message + footer | ||||
|     // - Header padding: space for protocol headers (7 bytes for Noise, 6 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 | ||||
|     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); | ||||
| @@ -297,10 +273,6 @@ class APIConnection : public APIServerConnection { | ||||
|  | ||||
|   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: | ||||
|   // Helper function to handle authentication completion | ||||
|   void complete_authentication_(); | ||||
| @@ -328,9 +300,17 @@ class APIConnection : public APIServerConnection { | ||||
|                                               APIConnection *conn, uint32_t remaining_size, bool is_single) { | ||||
|     // Set common fields that are shared by all entity types | ||||
|     msg.key = entity->get_object_id_hash(); | ||||
|     // IMPORTANT: get_object_id() may return a temporary std::string | ||||
|     std::string object_id = entity->get_object_id(); | ||||
|     msg.set_object_id(StringRef(object_id)); | ||||
|     // Try to use static reference first to avoid allocation | ||||
|     StringRef static_ref = entity->get_object_id_ref_for_api_(); | ||||
|     // 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()) { | ||||
|       msg.set_name(entity->get_name()); | ||||
| @@ -751,7 +731,7 @@ class APIConnection : public APIServerConnection { | ||||
|   } | ||||
|  | ||||
|   // 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 | ||||
|   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) | ||||
| #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 | ||||
|   if (err == APIError::OK) { | ||||
|     return "OK"; | ||||
|     return LOG_STR("OK"); | ||||
|   } else if (err == APIError::WOULD_BLOCK) { | ||||
|     return "WOULD_BLOCK"; | ||||
|     return LOG_STR("WOULD_BLOCK"); | ||||
|   } else if (err == APIError::BAD_INDICATOR) { | ||||
|     return "BAD_INDICATOR"; | ||||
|     return LOG_STR("BAD_INDICATOR"); | ||||
|   } else if (err == APIError::BAD_DATA_PACKET) { | ||||
|     return "BAD_DATA_PACKET"; | ||||
|     return LOG_STR("BAD_DATA_PACKET"); | ||||
|   } else if (err == APIError::TCP_NODELAY_FAILED) { | ||||
|     return "TCP_NODELAY_FAILED"; | ||||
|     return LOG_STR("TCP_NODELAY_FAILED"); | ||||
|   } else if (err == APIError::TCP_NONBLOCKING_FAILED) { | ||||
|     return "TCP_NONBLOCKING_FAILED"; | ||||
|     return LOG_STR("TCP_NONBLOCKING_FAILED"); | ||||
|   } else if (err == APIError::CLOSE_FAILED) { | ||||
|     return "CLOSE_FAILED"; | ||||
|     return LOG_STR("CLOSE_FAILED"); | ||||
|   } else if (err == APIError::SHUTDOWN_FAILED) { | ||||
|     return "SHUTDOWN_FAILED"; | ||||
|     return LOG_STR("SHUTDOWN_FAILED"); | ||||
|   } else if (err == APIError::BAD_STATE) { | ||||
|     return "BAD_STATE"; | ||||
|     return LOG_STR("BAD_STATE"); | ||||
|   } else if (err == APIError::BAD_ARG) { | ||||
|     return "BAD_ARG"; | ||||
|     return LOG_STR("BAD_ARG"); | ||||
|   } else if (err == APIError::SOCKET_READ_FAILED) { | ||||
|     return "SOCKET_READ_FAILED"; | ||||
|     return LOG_STR("SOCKET_READ_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) { | ||||
|     return "OUT_OF_MEMORY"; | ||||
|     return LOG_STR("OUT_OF_MEMORY"); | ||||
|   } else if (err == APIError::CONNECTION_CLOSED) { | ||||
|     return "CONNECTION_CLOSED"; | ||||
|     return LOG_STR("CONNECTION_CLOSED"); | ||||
|   } | ||||
| #ifdef USE_API_NOISE | ||||
|   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) { | ||||
|     return "HANDSHAKESTATE_READ_FAILED"; | ||||
|     return LOG_STR("HANDSHAKESTATE_READ_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) { | ||||
|     return "HANDSHAKESTATE_BAD_STATE"; | ||||
|     return LOG_STR("HANDSHAKESTATE_BAD_STATE"); | ||||
|   } else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) { | ||||
|     return "CIPHERSTATE_DECRYPT_FAILED"; | ||||
|     return LOG_STR("CIPHERSTATE_DECRYPT_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) { | ||||
|     return "HANDSHAKESTATE_SETUP_FAILED"; | ||||
|     return LOG_STR("HANDSHAKESTATE_SETUP_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) { | ||||
|     return "BAD_HANDSHAKE_ERROR_BYTE"; | ||||
|     return LOG_STR("BAD_HANDSHAKE_ERROR_BYTE"); | ||||
|   } | ||||
| #endif | ||||
|   return "UNKNOWN"; | ||||
|   return LOG_STR("UNKNOWN"); | ||||
| } | ||||
|  | ||||
| // Default implementation for loop - handles sending buffered data | ||||
|   | ||||
| @@ -66,7 +66,7 @@ enum class APIError : uint16_t { | ||||
| #endif | ||||
| }; | ||||
|  | ||||
| const char *api_error_to_str(APIError err); | ||||
| const LogString *api_error_to_logstr(APIError err); | ||||
|  | ||||
| class APIFrameHelper { | ||||
|  public: | ||||
| @@ -104,9 +104,9 @@ class APIFrameHelper { | ||||
|   // The buffer contains all messages with appropriate padding before each | ||||
|   virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) = 0; | ||||
|   // 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 | ||||
|   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 | ||||
|   bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } | ||||
|  | ||||
|   | ||||
| @@ -10,10 +10,18 @@ | ||||
| #include <cstring> | ||||
| #include <cinttypes> | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
| #include <pgmspace.h> | ||||
| #endif | ||||
|  | ||||
| namespace esphome::api { | ||||
|  | ||||
| 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"; | ||||
| #endif | ||||
| 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__) | ||||
| @@ -27,42 +35,42 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12;  // strlen("NoiseAPIInit") | ||||
| #endif | ||||
|  | ||||
| /// 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) | ||||
|     return "NO_MEMORY"; | ||||
|     return LOG_STR("NO_MEMORY"); | ||||
|   if (err == NOISE_ERROR_UNKNOWN_ID) | ||||
|     return "UNKNOWN_ID"; | ||||
|     return LOG_STR("UNKNOWN_ID"); | ||||
|   if (err == NOISE_ERROR_UNKNOWN_NAME) | ||||
|     return "UNKNOWN_NAME"; | ||||
|     return LOG_STR("UNKNOWN_NAME"); | ||||
|   if (err == NOISE_ERROR_MAC_FAILURE) | ||||
|     return "MAC_FAILURE"; | ||||
|     return LOG_STR("MAC_FAILURE"); | ||||
|   if (err == NOISE_ERROR_NOT_APPLICABLE) | ||||
|     return "NOT_APPLICABLE"; | ||||
|     return LOG_STR("NOT_APPLICABLE"); | ||||
|   if (err == NOISE_ERROR_SYSTEM) | ||||
|     return "SYSTEM"; | ||||
|     return LOG_STR("SYSTEM"); | ||||
|   if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) | ||||
|     return "REMOTE_KEY_REQUIRED"; | ||||
|     return LOG_STR("REMOTE_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) | ||||
|     return "PSK_REQUIRED"; | ||||
|     return LOG_STR("PSK_REQUIRED"); | ||||
|   if (err == NOISE_ERROR_INVALID_LENGTH) | ||||
|     return "INVALID_LENGTH"; | ||||
|     return LOG_STR("INVALID_LENGTH"); | ||||
|   if (err == NOISE_ERROR_INVALID_PARAM) | ||||
|     return "INVALID_PARAM"; | ||||
|     return LOG_STR("INVALID_PARAM"); | ||||
|   if (err == NOISE_ERROR_INVALID_STATE) | ||||
|     return "INVALID_STATE"; | ||||
|     return LOG_STR("INVALID_STATE"); | ||||
|   if (err == NOISE_ERROR_INVALID_NONCE) | ||||
|     return "INVALID_NONCE"; | ||||
|     return LOG_STR("INVALID_NONCE"); | ||||
|   if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) | ||||
|     return "INVALID_PRIVATE_KEY"; | ||||
|     return LOG_STR("INVALID_PRIVATE_KEY"); | ||||
|   if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) | ||||
|     return "INVALID_PUBLIC_KEY"; | ||||
|     return LOG_STR("INVALID_PUBLIC_KEY"); | ||||
|   if (err == NOISE_ERROR_INVALID_FORMAT) | ||||
|     return "INVALID_FORMAT"; | ||||
|     return LOG_STR("INVALID_FORMAT"); | ||||
|   if (err == NOISE_ERROR_INVALID_SIGNATURE) | ||||
|     return "INVALID_SIGNATURE"; | ||||
|   return to_string(err); | ||||
|     return LOG_STR("INVALID_SIGNATURE"); | ||||
|   return LOG_STR("UNKNOWN"); | ||||
| } | ||||
|  | ||||
| /// Initialize the frame helper, returns OK if successful. | ||||
| @@ -75,7 +83,11 @@ APIError APINoiseFrameHelper::init() { | ||||
|   // init prologue | ||||
|   size_t old_size = prologue_.size(); | ||||
|   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); | ||||
| #endif | ||||
|  | ||||
|   state_ = State::CLIENT_HELLO; | ||||
|   return APIError::OK; | ||||
| @@ -83,18 +95,18 @@ APIError APINoiseFrameHelper::init() { | ||||
| // Helper for handling handshake frame errors | ||||
| APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) { | ||||
|   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) { | ||||
|     send_explicit_handshake_reject_("Bad handshake packet len"); | ||||
|     send_explicit_handshake_reject_(LOG_STR("Bad handshake packet len")); | ||||
|   } | ||||
|   return aerr; | ||||
| } | ||||
|  | ||||
| // 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) { | ||||
|     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 APIError::OK; | ||||
| @@ -279,11 +291,11 @@ APIError APINoiseFrameHelper::state_action_() { | ||||
|       } | ||||
|  | ||||
|       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; | ||||
|       } else if (frame[0] != 0x00) { | ||||
|         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; | ||||
|       } | ||||
|  | ||||
| @@ -293,8 +305,10 @@ APIError APINoiseFrameHelper::state_action_() { | ||||
|       err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); | ||||
|       if (err != 0) { | ||||
|         // Special handling for MAC failure | ||||
|         send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error"); | ||||
|         return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED); | ||||
|         send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure") | ||||
|                                                                        : LOG_STR("Handshake error")); | ||||
|         return handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"), | ||||
|                                    APIError::HANDSHAKESTATE_READ_FAILED); | ||||
|       } | ||||
|  | ||||
|       aerr = check_handshake_finished_(); | ||||
| @@ -307,8 +321,8 @@ APIError APINoiseFrameHelper::state_action_() { | ||||
|       noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); | ||||
|  | ||||
|       err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); | ||||
|       APIError aerr_write = | ||||
|           handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED); | ||||
|       APIError aerr_write = handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"), | ||||
|                                                 APIError::HANDSHAKESTATE_WRITE_FAILED); | ||||
|       if (aerr_write != APIError::OK) | ||||
|         return aerr_write; | ||||
|       buffer[0] = 0x00;  // success | ||||
| @@ -331,15 +345,31 @@ APIError APINoiseFrameHelper::state_action_() { | ||||
|   } | ||||
|   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; | ||||
|   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 | ||||
|  | ||||
|   // Copy error message in bulk | ||||
|   if (!reason.empty()) { | ||||
|     std::memcpy(data.data() + 1, reason.c_str(), reason.length()); | ||||
|   if (reason_len > 0) { | ||||
|     std::memcpy(data.data() + 1, reason_str, reason_len); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   // temporarily remove failed state | ||||
|   auto orig_state = state_; | ||||
| @@ -368,7 +398,8 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { | ||||
|   noise_buffer_init(mbuf); | ||||
|   noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size()); | ||||
|   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) | ||||
|     return decrypt_err; | ||||
|  | ||||
| @@ -450,7 +481,8 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st | ||||
|                            4 + packet.payload_size + frame_footer_size_); | ||||
|  | ||||
|     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) | ||||
|       return aerr; | ||||
|  | ||||
| @@ -504,25 +536,27 @@ APIError APINoiseFrameHelper::init_handshake_() { | ||||
|   nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; | ||||
|  | ||||
|   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) | ||||
|     return aerr; | ||||
|  | ||||
|   const auto &psk = ctx_->get_psk(); | ||||
|   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) | ||||
|     return aerr; | ||||
|  | ||||
|   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) | ||||
|     return aerr; | ||||
|   // set_prologue copies it into handshakestate, so we can get rid of it now | ||||
|   prologue_ = {}; | ||||
|  | ||||
|   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) | ||||
|     return aerr; | ||||
|   return APIError::OK; | ||||
| @@ -540,7 +574,8 @@ APIError APINoiseFrameHelper::check_handshake_finished_() { | ||||
|     return APIError::HANDSHAKESTATE_BAD_STATE; | ||||
|   } | ||||
|   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) | ||||
|     return aerr; | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,7 @@ | ||||
|  | ||||
| namespace esphome::api { | ||||
|  | ||||
| class APINoiseFrameHelper : public APIFrameHelper { | ||||
| class APINoiseFrameHelper final : public APIFrameHelper { | ||||
|  public: | ||||
|   APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx, | ||||
|                       const ClientInfo *client_info) | ||||
| @@ -25,10 +25,6 @@ class APINoiseFrameHelper : public APIFrameHelper { | ||||
|   APIError read_packet(ReadPacketBuffer *buffer) override; | ||||
|   APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) 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: | ||||
|   APIError state_action_(); | ||||
| @@ -36,9 +32,9 @@ class APINoiseFrameHelper : public APIFrameHelper { | ||||
|   APIError write_frame_(const uint8_t *data, uint16_t len); | ||||
|   APIError init_handshake_(); | ||||
|   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_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) | ||||
|   NoiseHandshakeState *handshake_{nullptr}; | ||||
|   | ||||
| @@ -10,6 +10,10 @@ | ||||
| #include <cstring> | ||||
| #include <cinttypes> | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
| #include <pgmspace.h> | ||||
| #endif | ||||
|  | ||||
| namespace esphome::api { | ||||
|  | ||||
| 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 | ||||
|       // a message after the indicator byte to ensures its long | ||||
|       // enough and can aid in debugging. | ||||
|       const char msg[] = "\x00" | ||||
|                          "Bad indicator byte"; | ||||
|       static constexpr uint8_t INDICATOR_MSG_SIZE = 19; | ||||
| #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_len = 19; | ||||
|       this->write_raw_(iov, 1, 19); | ||||
| #else | ||||
|       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; | ||||
|   } | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|  | ||||
| namespace esphome::api { | ||||
|  | ||||
| class APIPlaintextFrameHelper : public APIFrameHelper { | ||||
| class APIPlaintextFrameHelper final : public APIFrameHelper { | ||||
|  public: | ||||
|   APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info) | ||||
|       : APIFrameHelper(std::move(socket), client_info) { | ||||
| @@ -22,9 +22,6 @@ class APIPlaintextFrameHelper : public APIFrameHelper { | ||||
|   APIError read_packet(ReadPacketBuffer *buffer) override; | ||||
|   APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) 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: | ||||
|   APIError try_read_frame_(std::vector<uint8_t> *frame); | ||||
|   | ||||
| @@ -901,6 +901,16 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel | ||||
|   return true; | ||||
| } | ||||
| #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) { | ||||
|   switch (field_id) { | ||||
|     case 1: | ||||
| @@ -911,8 +921,6 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||
|   } | ||||
|   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 | ||||
| void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(1, this->name_ref_); | ||||
| @@ -2153,10 +2161,12 @@ void BluetoothDeviceClearCacheResponse::calculate_size(ProtoSize &size) const { | ||||
| void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_uint32(1, static_cast<uint32_t>(this->state)); | ||||
|   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 { | ||||
|   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->configured_mode)); | ||||
| } | ||||
| bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   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 | ||||
| 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 | ||||
| void ListEntitiesServicesArgument::dump_to(std::string &out) const { | ||||
|   MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); | ||||
| @@ -1704,6 +1708,7 @@ void BluetoothScannerStateResponse::dump_to(std::string &out) const { | ||||
|   MessageDumpHelper helper(out, "BluetoothScannerStateResponse"); | ||||
|   dump_field(out, "state", static_cast<enums::BluetoothScannerState>(this->state)); | ||||
|   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 { | ||||
|   MessageDumpHelper helper(out, "BluetoothScannerSetModeRequest"); | ||||
|   | ||||
| @@ -160,15 +160,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       break; | ||||
|     } | ||||
| #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: { | ||||
|       GetTimeResponse msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| @@ -656,11 +647,6 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc | ||||
|   } | ||||
| } | ||||
| #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 | ||||
| void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { | ||||
|   if (this->check_authenticated_()) { | ||||
|   | ||||
| @@ -71,7 +71,7 @@ class APIServerConnectionBase : public ProtoService { | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; | ||||
| #endif | ||||
|   virtual void on_get_time_request(const GetTimeRequest &value){}; | ||||
|  | ||||
|   virtual void on_get_time_response(const GetTimeResponse &value){}; | ||||
|  | ||||
| #ifdef USE_API_SERVICES | ||||
| @@ -226,7 +226,6 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; | ||||
| #endif | ||||
|   virtual bool send_get_time_response(const GetTimeRequest &msg) = 0; | ||||
| #ifdef USE_API_SERVICES | ||||
|   virtual void execute_service(const ExecuteServiceRequest &msg) = 0; | ||||
| #endif | ||||
| @@ -348,7 +347,6 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
| #ifdef USE_API_HOMEASSISTANT_STATES | ||||
|   void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; | ||||
| #endif | ||||
|   void on_get_time_request(const GetTimeRequest &msg) override; | ||||
| #ifdef USE_API_SERVICES | ||||
|   void on_execute_service_request(const ExecuteServiceRequest &msg) override; | ||||
| #endif | ||||
|   | ||||
| @@ -62,9 +62,11 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None: | ||||
|         time_ = datetime.now() | ||||
|         message: bytes = msg.message | ||||
|         text = message.decode("utf8", "backslashreplace") | ||||
|         for parsed_msg in parse_log_message( | ||||
|             text, f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]" | ||||
|         ): | ||||
|         nanoseconds = time_.microsecond // 1000 | ||||
|         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) | ||||
|  | ||||
|     stop = await async_run(cli, on_log, name=name) | ||||
|   | ||||
| @@ -8,74 +8,70 @@ namespace esphome::api { | ||||
| static const char *const TAG = "api.proto"; | ||||
|  | ||||
| void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { | ||||
|   uint32_t i = 0; | ||||
|   bool error = false; | ||||
|   while (i < length) { | ||||
|   const uint8_t *ptr = buffer; | ||||
|   const uint8_t *end = buffer + length; | ||||
|  | ||||
|   while (ptr < end) { | ||||
|     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()) { | ||||
|       ESP_LOGV(TAG, "Invalid field start at %" PRIu32, i); | ||||
|       break; | ||||
|       ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer)); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     uint32_t field_type = (res->as_uint32()) & 0b111; | ||||
|     uint32_t field_id = (res->as_uint32()) >> 3; | ||||
|     i += consumed; | ||||
|     uint32_t tag = res->as_uint32(); | ||||
|     uint32_t field_type = tag & 0b111; | ||||
|     uint32_t field_id = tag >> 3; | ||||
|     ptr += consumed; | ||||
|  | ||||
|     switch (field_type) { | ||||
|       case 0: {  // VarInt | ||||
|         res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); | ||||
|         res = ProtoVarInt::parse(ptr, end - ptr, &consumed); | ||||
|         if (!res.has_value()) { | ||||
|           ESP_LOGV(TAG, "Invalid VarInt at %" PRIu32, i); | ||||
|           error = true; | ||||
|           break; | ||||
|           ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer)); | ||||
|           return; | ||||
|         } | ||||
|         if (!this->decode_varint(field_id, *res)) { | ||||
|           ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32()); | ||||
|         } | ||||
|         i += consumed; | ||||
|         ptr += consumed; | ||||
|         break; | ||||
|       } | ||||
|       case 2: {  // Length-delimited | ||||
|         res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); | ||||
|         res = ProtoVarInt::parse(ptr, end - ptr, &consumed); | ||||
|         if (!res.has_value()) { | ||||
|           ESP_LOGV(TAG, "Invalid Length Delimited at %" PRIu32, i); | ||||
|           error = true; | ||||
|           break; | ||||
|           ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer)); | ||||
|           return; | ||||
|         } | ||||
|         uint32_t field_length = res->as_uint32(); | ||||
|         i += consumed; | ||||
|         if (field_length > length - i) { | ||||
|           ESP_LOGV(TAG, "Out-of-bounds Length Delimited at %" PRIu32, i); | ||||
|           error = true; | ||||
|           break; | ||||
|         ptr += consumed; | ||||
|         if (ptr + field_length > end) { | ||||
|           ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer)); | ||||
|           return; | ||||
|         } | ||||
|         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); | ||||
|         } | ||||
|         i += field_length; | ||||
|         ptr += field_length; | ||||
|         break; | ||||
|       } | ||||
|       case 5: {  // 32-bit | ||||
|         if (length - i < 4) { | ||||
|           ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at %" PRIu32, i); | ||||
|           error = true; | ||||
|           break; | ||||
|         if (ptr + 4 > end) { | ||||
|           ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer)); | ||||
|           return; | ||||
|         } | ||||
|         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))) { | ||||
|           ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val); | ||||
|         } | ||||
|         i += 4; | ||||
|         ptr += 4; | ||||
|         break; | ||||
|       } | ||||
|       default: | ||||
|         ESP_LOGV(TAG, "Invalid field type at %" PRIu32, i); | ||||
|         error = true; | ||||
|         break; | ||||
|     } | ||||
|     if (error) { | ||||
|       break; | ||||
|         ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer)); | ||||
|         return; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -8,7 +8,7 @@ from esphome.const import ( | ||||
|     PLATFORM_LN882X, | ||||
|     PLATFORM_RTL87XX, | ||||
| ) | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.core import CORE, CoroPriority, coroutine_with_priority | ||||
|  | ||||
| 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): | ||||
|     if CORE.is_esp32 or CORE.is_libretiny: | ||||
|         # https://github.com/ESP32Async/AsyncTCP | ||||
|   | ||||
| @@ -16,6 +16,7 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_ENERGY, | ||||
|     DEVICE_CLASS_POWER, | ||||
|     DEVICE_CLASS_POWER_FACTOR, | ||||
|     DEVICE_CLASS_REACTIVE_POWER, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     ICON_CURRENT_AC, | ||||
|     ICON_LIGHTBULB, | ||||
| @@ -78,6 +79,7 @@ CONFIG_SCHEMA = ( | ||||
|                 unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, | ||||
|                 icon=ICON_LIGHTBULB, | ||||
|                 accuracy_decimals=2, | ||||
|                 device_class=DEVICE_CLASS_REACTIVE_POWER, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( | ||||
|   | ||||
| @@ -17,10 +17,12 @@ from esphome.const import ( | ||||
|     CONF_REACTIVE_POWER, | ||||
|     CONF_REVERSE_ACTIVE_ENERGY, | ||||
|     CONF_VOLTAGE, | ||||
|     DEVICE_CLASS_APPARENT_POWER, | ||||
|     DEVICE_CLASS_CURRENT, | ||||
|     DEVICE_CLASS_ENERGY, | ||||
|     DEVICE_CLASS_POWER, | ||||
|     DEVICE_CLASS_POWER_FACTOR, | ||||
|     DEVICE_CLASS_REACTIVE_POWER, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     ENTITY_CATEGORY_DIAGNOSTIC, | ||||
| @@ -100,13 +102,13 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( | ||||
|             unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, | ||||
|             icon=ICON_LIGHTBULB, | ||||
|             accuracy_decimals=2, | ||||
|             device_class=DEVICE_CLASS_POWER, | ||||
|             device_class=DEVICE_CLASS_REACTIVE_POWER, | ||||
|             state_class=STATE_CLASS_MEASUREMENT, | ||||
|         ), | ||||
|         cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( | ||||
|             unit_of_measurement=UNIT_VOLT_AMPS, | ||||
|             accuracy_decimals=2, | ||||
|             device_class=DEVICE_CLASS_POWER, | ||||
|             device_class=DEVICE_CLASS_APPARENT_POWER, | ||||
|             state_class=STATE_CLASS_MEASUREMENT, | ||||
|         ), | ||||
|         cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( | ||||
|   | ||||
| @@ -2,7 +2,7 @@ from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| 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"] | ||||
| 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 | ||||
|  | ||||
|  | ||||
| @coroutine_with_priority(100.0) | ||||
| @coroutine_with_priority(CoroPriority.CORE) | ||||
| async def to_code(config): | ||||
|     cg.add_define("USE_AUDIO_ADC") | ||||
|     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.config_validation as cv | ||||
| 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"] | ||||
| IS_PLATFORM_COMPONENT = True | ||||
| @@ -51,7 +51,7 @@ async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @coroutine_with_priority(100.0) | ||||
| @coroutine_with_priority(CoroPriority.CORE) | ||||
| async def to_code(config): | ||||
|     cg.add_define("USE_AUDIO_DAC") | ||||
|     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) \ | ||||
|   if ((err) != i2c::ERROR_OK) { \ | ||||
|     this->status_set_warning("Failed to communicate"); \ | ||||
|     this->status_set_warning(LOG_STR("Failed to communicate")); \ | ||||
|     return; \ | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -493,7 +493,7 @@ void BedJetHub::dump_config() { | ||||
|                 "  ble_client.app_id: %d\n" | ||||
|                 "  ble_client.conn_id: %d", | ||||
|                 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()); | ||||
|   for (auto *child : this->children_) { | ||||
|     ESP_LOGCONFIG(TAG, "    - %s", child->describe().c_str()); | ||||
|   | ||||
| @@ -59,7 +59,7 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_VIBRATION, | ||||
|     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.cpp_generator import MockObjClass | ||||
| 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) | ||||
|  | ||||
|  | ||||
| @coroutine_with_priority(100.0) | ||||
| @coroutine_with_priority(CoroPriority.CORE) | ||||
| async def to_code(config): | ||||
|     cg.add_global(binary_sensor_ns.using) | ||||
|  | ||||
|   | ||||
| @@ -7,6 +7,19 @@ namespace 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) { | ||||
|   if (this->filter_list_ == nullptr) { | ||||
|     this->send_state_internal(new_state); | ||||
|   | ||||
| @@ -10,13 +10,10 @@ namespace esphome { | ||||
|  | ||||
| namespace binary_sensor { | ||||
|  | ||||
| #define LOG_BINARY_SENSOR(prefix, type, obj) \ | ||||
|   if ((obj) != nullptr) { \ | ||||
|     ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ | ||||
|     if (!(obj)->get_device_class().empty()) { \ | ||||
|       ESP_LOGCONFIG(TAG, "%s  Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ | ||||
|     } \ | ||||
|   } | ||||
| class BinarySensor; | ||||
| void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj); | ||||
|  | ||||
| #define LOG_BINARY_SENSOR(prefix, type, obj) log_binary_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj) | ||||
|  | ||||
| #define SUB_BINARY_SENSOR(name) \ | ||||
|  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 uint8_t BL0940_READ_COMMAND = 0x50;  // 0x58 according to documentation | ||||
| 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_MODE = 0x18; | ||||
| static const uint8_t BL0940_REG_SOFT_RESET = 0x19; | ||||
| static const uint8_t BL0940_REG_USR_WRPROT = 0x1A; | ||||
| 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 | ||||
|     {BL0940_WRITE_COMMAND, BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, | ||||
|     {BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, | ||||
|     // 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 | ||||
|     {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 | ||||
|     {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 | ||||
|     {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() { | ||||
|   DataPacket buffer; | ||||
| @@ -36,8 +34,8 @@ void BL0940::loop() { | ||||
|     return; | ||||
|   } | ||||
|   if (read_array((uint8_t *) &buffer, sizeof(buffer))) { | ||||
|     if (validate_checksum(&buffer)) { | ||||
|       received_package_(&buffer); | ||||
|     if (this->validate_checksum_(&buffer)) { | ||||
|       this->received_package_(&buffer); | ||||
|     } | ||||
|   } else { | ||||
|     ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); | ||||
| @@ -46,35 +44,151 @@ void BL0940::loop() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool BL0940::validate_checksum(const DataPacket *data) { | ||||
|   uint8_t checksum = BL0940_READ_COMMAND; | ||||
| bool BL0940::validate_checksum_(DataPacket *data) { | ||||
|   uint8_t checksum = this->read_command_; | ||||
|   // Whole package but checksum | ||||
|   for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) { | ||||
|     checksum += data->raw[i]; | ||||
|   uint8_t *raw = (uint8_t *) data; | ||||
|   for (uint32_t i = 0; i < sizeof(*data) - 1; i++) { | ||||
|     checksum += raw[i]; | ||||
|   } | ||||
|   checksum ^= 0xFF; | ||||
|   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; | ||||
| } | ||||
|  | ||||
| void BL0940::update() { | ||||
|   this->flush(); | ||||
|   this->write_byte(BL0940_READ_COMMAND); | ||||
|   this->write_byte(this->read_command_); | ||||
|   this->write_byte(BL0940_FULL_PACKET); | ||||
| } | ||||
|  | ||||
| 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) { | ||||
|     this->write_array(i, 6); | ||||
|     this->write_byte(this->write_command_), this->write_array(i, 5); | ||||
|     delay(1); | ||||
|   } | ||||
|   this->flush(); | ||||
| } | ||||
|  | ||||
| float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { | ||||
|   auto tb = (float) (temperature.h << 8 | temperature.l); | ||||
| float BL0940::calculate_power_reference_() { | ||||
|   // 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; | ||||
|   if (sensor != nullptr) { | ||||
|     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; | ||||
| } | ||||
|  | ||||
| void BL0940::received_package_(const DataPacket *data) const { | ||||
| void BL0940::received_package_(DataPacket *data) { | ||||
|   // Bad header | ||||
|   if (data->frame_header != BL0940_PACKET_HEADER) { | ||||
|     ESP_LOGI(TAG, "Invalid data. Header mismatch: %d", data->frame_header); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_; | ||||
|   float i_rms = (float) to_uint32_t(data->i_rms) / current_reference_; | ||||
|   float watt = (float) to_int32_t(data->watt) / power_reference_; | ||||
|   uint32_t cf_cnt = to_uint32_t(data->cf_cnt); | ||||
|   float total_energy_consumption = (float) cf_cnt / energy_reference_; | ||||
|   // cf_cnt is only 24 bits, so track overflows | ||||
|   uint32_t cf_cnt = (uint24_t) data->cf_cnt; | ||||
|   cf_cnt |= this->prev_cf_cnt_ & 0xff000000; | ||||
|   if (cf_cnt < this->prev_cf_cnt_) { | ||||
|     cf_cnt += 0x1000000; | ||||
|   } | ||||
|   this->prev_cf_cnt_ = cf_cnt; | ||||
|  | ||||
|   float tps1 = update_temp_(internal_temperature_sensor_, data->tps1); | ||||
|   float tps2 = update_temp_(external_temperature_sensor_, data->tps2); | ||||
|   float v_rms = (uint24_t) data->v_rms / this->voltage_reference_cal_; | ||||
|   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) { | ||||
|     voltage_sensor_->publish_state(v_rms); | ||||
|   float tps1 = update_temp_(this->internal_temperature_sensor_, data->tps1); | ||||
|   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) { | ||||
|     current_sensor_->publish_state(i_rms); | ||||
|   if (this->current_sensor_ != nullptr) { | ||||
|     this->current_sensor_->publish_state(i_rms); | ||||
|   } | ||||
|   if (power_sensor_ != nullptr) { | ||||
|     power_sensor_->publish_state(watt); | ||||
|   if (this->power_sensor_ != nullptr) { | ||||
|     this->power_sensor_->publish_state(watt); | ||||
|   } | ||||
|   if (energy_sensor_ != nullptr) { | ||||
|     energy_sensor_->publish_state(total_energy_consumption); | ||||
|   if (this->energy_sensor_ != nullptr) { | ||||
|     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, | ||||
| @@ -121,7 +242,27 @@ void BL0940::received_package_(const DataPacket *data) const { | ||||
| } | ||||
|  | ||||
| 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("", "Current", this->current_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_); | ||||
| } | ||||
|  | ||||
| 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 esphome | ||||
|   | ||||
| @@ -1,66 +1,48 @@ | ||||
| #pragma once | ||||
|  | ||||
| #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/uart/uart.h" | ||||
|  | ||||
| namespace esphome { | ||||
| 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) | ||||
|  | ||||
| union DataPacket {  // NOLINT(altera-struct-pack-align) | ||||
|   uint8_t raw[35]; | ||||
|   struct { | ||||
|     uint8_t frame_header;  // value of 0x58 according to docs. 0x55 according to Tasmota real world tests. Reality wins. | ||||
|     ube24_t i_fast_rms;    // 0x00 | ||||
|     ube24_t i_rms;         // 0x04 | ||||
|     ube24_t RESERVED0;     // reserved | ||||
|     ube24_t v_rms;         // 0x06 | ||||
|     ube24_t RESERVED1;     // reserved | ||||
|     sbe24_t watt;          // 0x08 | ||||
|     ube24_t RESERVED2;     // reserved | ||||
|     ube24_t cf_cnt;        // 0x0A | ||||
|     ube24_t RESERVED3;     // reserved | ||||
|     ube16_t tps1;          // 0x0c | ||||
|     uint8_t RESERVED4;     // value of 0x00 | ||||
|     ube16_t tps2;          // 0x0c | ||||
|     uint8_t RESERVED5;     // value of 0x00 | ||||
|     uint8_t checksum;      // checksum | ||||
|   }; | ||||
| struct DataPacket { | ||||
|   uint8_t frame_header;    // Packet header (0x58 in EN docs, 0x55 in CN docs and Tasmota tests) | ||||
|   uint24_le_t i_fast_rms;  // Fast RMS current | ||||
|   uint24_le_t i_rms;       // RMS current | ||||
|   uint24_t RESERVED0;      // Reserved | ||||
|   uint24_le_t v_rms;       // RMS voltage | ||||
|   uint24_t RESERVED1;      // Reserved | ||||
|   int24_le_t watt;         // Active power (can be negative for bidirectional measurement) | ||||
|   uint24_t RESERVED2;      // Reserved | ||||
|   uint24_le_t cf_cnt;      // Energy pulse count | ||||
|   uint24_t RESERVED3;      // Reserved | ||||
|   uint16_le_t tps1;        // Internal temperature sensor 1 | ||||
|   uint8_t RESERVED4;       // Reserved (should be 0x00) | ||||
|   uint16_le_t tps2;        // Internal temperature sensor 2 | ||||
|   uint8_t RESERVED5;       // Reserved (should be 0x00) | ||||
|   uint8_t checksum;        // Packet checksum | ||||
| } __attribute__((packed)); | ||||
|  | ||||
| class BL0940 : public PollingComponent, public uart::UARTDevice { | ||||
|  public: | ||||
|   // Sensor setters | ||||
|   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_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_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) { | ||||
|     internal_temperature_sensor_ = internal_temperature_sensor; | ||||
|   } | ||||
| @@ -68,42 +50,105 @@ class BL0940 : public PollingComponent, public uart::UARTDevice { | ||||
|     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 setup() override; | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
|   sensor::Sensor *voltage_sensor_{nullptr}; | ||||
|   sensor::Sensor *current_sensor_{nullptr}; | ||||
|   // NB This may be negative as the circuits is seemingly able to measure | ||||
|   // power in both directions | ||||
|   sensor::Sensor *power_sensor_{nullptr}; | ||||
|   sensor::Sensor *energy_sensor_{nullptr}; | ||||
|   sensor::Sensor *internal_temperature_sensor_{nullptr}; | ||||
|   sensor::Sensor *external_temperature_sensor_{nullptr}; | ||||
|   // --- Sensor pointers --- | ||||
|   sensor::Sensor *voltage_sensor_{nullptr};               // Voltage sensor | ||||
|   sensor::Sensor *current_sensor_{nullptr};               // Current sensor | ||||
|   sensor::Sensor *power_sensor_{nullptr};                 // Power sensor (can be negative for bidirectional) | ||||
|   sensor::Sensor *energy_sensor_{nullptr};                // Energy sensor | ||||
|   sensor::Sensor *internal_temperature_sensor_{nullptr};  // Internal temperature sensor | ||||
|   sensor::Sensor *external_temperature_sensor_{nullptr};  // External temperature sensor | ||||
|  | ||||
|   // Max difference between two measurements of the temperature. Used to avoid noise. | ||||
|   float max_temperature_diff_{0}; | ||||
|   // Divide by this to turn into Watt | ||||
|   float power_reference_ = BL0940_PREF; | ||||
|   // Divide by this to turn into Volt | ||||
|   float voltage_reference_ = BL0940_UREF; | ||||
|   // Divide by this to turn into Ampere | ||||
|   float current_reference_ = BL0940_IREF; | ||||
|   // Divide by this to turn into kWh | ||||
|   float energy_reference_ = BL0940_EREF; | ||||
| #ifdef USE_NUMBER | ||||
|   // --- Calibration number entities (for dynamic calibration via HA UI) --- | ||||
|   number::Number *voltage_calibration_number_{nullptr}; | ||||
|   number::Number *current_calibration_number_{nullptr}; | ||||
|   number::Number *power_calibration_number_{nullptr}; | ||||
|   number::Number *energy_calibration_number_{nullptr}; | ||||
| #endif | ||||
|  | ||||
|   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 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_INTERNAL_TEMPERATURE, | ||||
|     CONF_POWER, | ||||
|     CONF_REFERENCE_VOLTAGE, | ||||
|     CONF_VOLTAGE, | ||||
|     DEVICE_CLASS_CURRENT, | ||||
|     DEVICE_CLASS_ENERGY, | ||||
| @@ -23,12 +24,133 @@ from esphome.const import ( | ||||
|     UNIT_WATT, | ||||
| ) | ||||
|  | ||||
| from . import bl0940_ns | ||||
|  | ||||
| DEPENDENCIES = ["uart"] | ||||
|  | ||||
|  | ||||
| bl0940_ns = cg.esphome_ns.namespace("bl0940") | ||||
| 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 = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
| @@ -69,10 +191,24 @@ CONFIG_SCHEMA = ( | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 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(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): | ||||
|         sens = await sensor.new_sensor(external_temperature_config) | ||||
|         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); | ||||
|  | ||||
|   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(); | ||||
| } | ||||
|   | ||||
| @@ -27,7 +27,7 @@ CONFIG_SCHEMA = cv.All( | ||||
| ) | ||||
|  | ||||
|  | ||||
| def to_code(config): | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): | ||||
|         cg.add( | ||||
| @@ -63,6 +63,6 @@ def to_code(config): | ||||
|         ) | ||||
|         cg.add(var.set_char_uuid128(uuid128)) | ||||
|     cg.add(var.set_require_response(config[CONF_REQUIRE_RESPONSE])) | ||||
|     yield output.register_output(var, config) | ||||
|     yield ble_client.register_ble_node(var, config) | ||||
|     yield cg.register_component(var, config) | ||||
|     await output.register_output(var, config) | ||||
|     await ble_client.register_ble_node(var, config) | ||||
|     await cg.register_component(var, config) | ||||
|   | ||||
| @@ -80,7 +80,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|         cv.Schema( | ||||
|             { | ||||
|                 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.only_with_esp_idf, cv.boolean | ||||
|                 ), | ||||
|   | ||||
| @@ -8,7 +8,7 @@ namespace esphome::bluetooth_proxy { | ||||
|  | ||||
| class BluetoothProxy; | ||||
|  | ||||
| class BluetoothConnection : public esp32_ble_client::BLEClientBase { | ||||
| class BluetoothConnection final : public esp32_ble_client::BLEClientBase { | ||||
|  public: | ||||
|   void dump_config() override; | ||||
|   void loop() override; | ||||
|   | ||||
| @@ -24,6 +24,9 @@ void BluetoothProxy::setup() { | ||||
|   this->connections_free_response_.limit = 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) { | ||||
|     if (this->api_connection_ != nullptr) { | ||||
|       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.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE | ||||
|                                                : 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); | ||||
| } | ||||
|  | ||||
| @@ -183,6 +189,12 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest | ||||
|         this->send_device_connection(msg.address, false); | ||||
|         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 || | ||||
|           connection->state() == espbt::ClientState::ESTABLISHED) { | ||||
|         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); | ||||
|         this->log_connection_info_(connection, "v3 without cache"); | ||||
|       } | ||||
|       if (msg.has_address_type) { | ||||
|         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_state(espbt::ClientState::DISCOVERED); | ||||
|       } else { | ||||
|         connection->set_state(espbt::ClientState::SEARCHING); | ||||
|       } | ||||
|       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_state(espbt::ClientState::DISCOVERED); | ||||
|       this->send_connections_free(); | ||||
|       break; | ||||
|     } | ||||
|   | ||||
| @@ -50,7 +50,7 @@ enum BluetoothProxySubscriptionFlag : uint32_t { | ||||
|   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_ | ||||
|  public: | ||||
|   BluetoothProxy(); | ||||
| @@ -130,7 +130,9 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | ||||
|  | ||||
|   std::string get_bluetooth_mac_address_pretty() { | ||||
|     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: | ||||
| @@ -161,7 +163,8 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | ||||
|   // Group 4: 1-byte types grouped together | ||||
|   bool active_; | ||||
|   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) | ||||
|   | ||||
| @@ -17,7 +17,7 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_RESTART, | ||||
|     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.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) | ||||
|  | ||||
|  | ||||
| @coroutine_with_priority(100.0) | ||||
| @coroutine_with_priority(CoroPriority.CORE) | ||||
| async def to_code(config): | ||||
|     cg.add_global(button_ns.using) | ||||
|   | ||||
| @@ -6,6 +6,19 @@ namespace 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() { | ||||
|   ESP_LOGD(TAG, "'%s' Pressed.", this->get_name().c_str()); | ||||
|   this->press_action(); | ||||
|   | ||||
| @@ -7,13 +7,10 @@ | ||||
| namespace esphome { | ||||
| namespace button { | ||||
|  | ||||
| #define LOG_BUTTON(prefix, type, obj) \ | ||||
|   if ((obj) != nullptr) { \ | ||||
|     ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ | ||||
|     if (!(obj)->get_icon().empty()) { \ | ||||
|       ESP_LOGCONFIG(TAG, "%s  Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ | ||||
|     } \ | ||||
|   } | ||||
| class Button; | ||||
| void log_button(const char *tag, const char *prefix, const char *type, Button *obj); | ||||
|  | ||||
| #define LOG_BUTTON(prefix, type, obj) log_button(TAG, prefix, LOG_STR_LITERAL(type), obj) | ||||
|  | ||||
| #define SUB_BUTTON(name) \ | ||||
|  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 }; | ||||
|  | ||||
| /// 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. | ||||
|  *  Encapsulates the JPEG encoded data and it is shared among | ||||
|  *  all connected clients. | ||||
| @@ -43,6 +63,29 @@ class 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. | ||||
|  *  1) API server starts and installs callback (add_image_callback) | ||||
|  *     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_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"] | ||||
| 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): | ||||
|     paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) | ||||
|  | ||||
|   | ||||
| @@ -7,103 +7,83 @@ namespace esphome { | ||||
| namespace captive_portal { | ||||
|  | ||||
| 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, | ||||
|     0x5f, 0x31, 0xa7, 0x36, 0x6b, 0x6b, 0x1b, 0x51, 0x22, 0xe5, 0xb7, 0xd8, 0x92, 0x16, 0x69, 0xae, 0x8b, 0x5d, 0xa0, | ||||
|     0xdd, 0x2d, 0x90, 0x6c, 0xef, 0x43, 0x51, 0x20, 0xb4, 0x34, 0xb2, 0xd8, 0x48, 0xa4, 0x4e, 0xa4, 0x5f, 0x52, 0xc3, | ||||
|     0xf7, 0xdb, 0x0f, 0x94, 0x6c, 0xc7, 0xe9, 0x35, 0x87, 0xeb, 0xe2, 0x0e, 0x87, 0xdd, 0x18, 0x21, 0x86, 0xe4, 0xcc, | ||||
|     0x70, 0xe6, 0xf1, 0x0c, 0x67, 0xcc, 0xe8, 0x2f, 0x99, 0x4a, 0xcd, 0x7d, 0x8d, 0x50, 0x98, 0xaa, 0x4c, 0x22, 0x3b, | ||||
|     0x42, 0xc9, 0xe5, 0x22, 0x46, 0x99, 0x44, 0x05, 0xf2, 0x2c, 0x89, 0x2a, 0x34, 0x1c, 0xd2, 0x82, 0x37, 0x1a, 0x4d, | ||||
|     0xfc, 0xdb, 0xcd, 0x8f, 0xde, 0x04, 0xfc, 0x24, 0x2a, 0x85, 0xbc, 0x83, 0x06, 0xcb, 0x58, 0xa4, 0x4a, 0x42, 0xd1, | ||||
|     0x60, 0x1e, 0x67, 0xdc, 0xf0, 0xa9, 0xa8, 0xf8, 0x02, 0x2d, 0x43, 0x2b, 0x26, 0x79, 0x85, 0xf1, 0x4a, 0xe0, 0xba, | ||||
|     0x56, 0x8d, 0x81, 0x54, 0x49, 0x83, 0xd2, 0xc4, 0xce, 0x5a, 0x64, 0xa6, 0x88, 0x33, 0x5c, 0x89, 0x14, 0xbd, 0x76, | ||||
|     0x72, 0x2e, 0xa4, 0x30, 0x82, 0x97, 0x9e, 0x4e, 0x79, 0x89, 0x31, 0x3d, 0x5f, 0x6a, 0x6c, 0xda, 0x09, 0x9f, 0x97, | ||||
|     0x18, 0x4b, 0xe5, 0xf8, 0x49, 0xa4, 0xd3, 0x46, 0xd4, 0x06, 0xac, 0xbd, 0x71, 0xa5, 0xb2, 0x65, 0x89, 0x89, 0xef, | ||||
|     0x73, 0xad, 0xd1, 0x68, 0x5f, 0xc8, 0x0c, 0x37, 0x64, 0x14, 0x86, 0x29, 0xe3, 0xe3, 0x9c, 0x7c, 0xd2, 0xcf, 0x32, | ||||
|     0x95, 0x2e, 0x2b, 0x94, 0x86, 0x94, 0x2a, 0xe5, 0x46, 0x28, 0x49, 0x34, 0xf2, 0x26, 0x2d, 0xe2, 0x38, 0x76, 0x7e, | ||||
|     0xd0, 0x7c, 0x85, 0xce, 0x77, 0xdf, 0xf5, 0x8f, 0x4c, 0x0b, 0x34, 0xaf, 0x4b, 0xb4, 0xa4, 0x7e, 0x75, 0x7f, 0xc3, | ||||
|     0x17, 0xbf, 0xf0, 0x0a, 0xfb, 0x0e, 0xd7, 0x22, 0x43, 0xc7, 0xfd, 0x10, 0x7c, 0x24, 0xda, 0xdc, 0x97, 0x48, 0x32, | ||||
|     0xa1, 0xeb, 0x92, 0xdf, 0xc7, 0xce, 0xbc, 0x54, 0xe9, 0x9d, 0xe3, 0xce, 0xf2, 0xa5, 0x4c, 0xad, 0x72, 0xd0, 0x7d, | ||||
|     0x74, 0xb7, 0x25, 0x1a, 0x30, 0xf1, 0x5b, 0x6e, 0x0a, 0x52, 0xf1, 0x4d, 0xbf, 0x23, 0x84, 0xec, 0xb3, 0xef, 0xfb, | ||||
|     0xf8, 0x92, 0x06, 0x81, 0x7b, 0xde, 0x0e, 0x81, 0xeb, 0xd3, 0x20, 0x98, 0x35, 0x68, 0x96, 0x8d, 0x04, 0xde, 0xbf, | ||||
|     0x8d, 0x6a, 0x6e, 0x0a, 0xc8, 0x62, 0xa7, 0xa2, 0x8c, 0x04, 0xc1, 0x04, 0xe8, 0x05, 0x61, 0x43, 0x8f, 0x52, 0x12, | ||||
|     0x7a, 0x74, 0x98, 0x8e, 0xbd, 0x21, 0xd0, 0x81, 0x37, 0x04, 0xc6, 0xc8, 0x10, 0x82, 0xcf, 0x0e, 0xe4, 0xa2, 0x2c, | ||||
|     0x63, 0x47, 0x2a, 0x89, 0x0e, 0x68, 0xd3, 0xa8, 0x3b, 0x8c, 0x9d, 0x74, 0xd9, 0x34, 0x28, 0xcd, 0x95, 0x2a, 0x55, | ||||
|     0xe3, 0xf8, 0xc9, 0x33, 0x78, 0xf4, 0xf7, 0xcd, 0x47, 0x98, 0x86, 0x4b, 0x9d, 0xab, 0xa6, 0x8a, 0x9d, 0xf6, 0x4b, | ||||
|     0xe9, 0xbf, 0xd8, 0x9a, 0x1d, 0xd8, 0xc1, 0x3d, 0xd9, 0xf4, 0x54, 0x23, 0x16, 0x42, 0xc6, 0x0e, 0x65, 0x40, 0x27, | ||||
|     0x8e, 0x9f, 0xdc, 0xba, 0xbb, 0x23, 0x26, 0xdc, 0x62, 0xb2, 0xf7, 0x52, 0xf5, 0x3f, 0xdc, 0x46, 0x7a, 0xb5, 0x80, | ||||
|     0x4d, 0x55, 0x4a, 0x1d, 0x3b, 0x85, 0x31, 0xf5, 0xd4, 0xf7, 0xd7, 0xeb, 0x35, 0x59, 0x87, 0x44, 0x35, 0x0b, 0x9f, | ||||
|     0x05, 0x41, 0xe0, 0xeb, 0xd5, 0xc2, 0x81, 0x2e, 0x3e, 0x1c, 0x36, 0x70, 0xa0, 0x40, 0xb1, 0x28, 0x4c, 0x4b, 0x27, | ||||
|     0x2f, 0xb6, 0xb8, 0x8b, 0x2c, 0x47, 0x72, 0xfb, 0xf1, 0xe4, 0x14, 0x71, 0x72, 0x0a, 0xfe, 0x70, 0x82, 0x66, 0xef, | ||||
|     0xad, 0x35, 0x6a, 0xcc, 0x19, 0x30, 0x08, 0xda, 0x0f, 0xf3, 0x2c, 0xbd, 0x9f, 0x79, 0x5f, 0xcc, 0xe0, 0x64, 0x06, | ||||
|     0x0c, 0x9e, 0x01, 0xb0, 0x6a, 0xe4, 0x5d, 0x1c, 0xc5, 0xa9, 0xdd, 0x5e, 0xd1, 0xe0, 0x61, 0xc1, 0xca, 0xfc, 0x34, | ||||
|     0x3a, 0x9d, 0x7b, 0xec, 0xbd, 0x65, 0xb0, 0xd8, 0x1f, 0x85, 0x3c, 0x56, 0xd0, 0xf7, 0x23, 0x3e, 0x84, 0xe1, 0x7e, | ||||
|     0x65, 0xe8, 0x59, 0xfa, 0x38, 0xb3, 0x27, 0xc1, 0x70, 0xc5, 0x0a, 0x5a, 0x79, 0x23, 0x6f, 0xc8, 0x43, 0x08, 0xf7, | ||||
|     0x26, 0x85, 0x10, 0xae, 0x58, 0x31, 0x7a, 0x3f, 0x3a, 0x5d, 0xf3, 0xc2, 0xcf, 0x3d, 0x0b, 0xf3, 0xd4, 0x71, 0x1e, | ||||
|     0x30, 0x50, 0xa7, 0x18, 0x90, 0x4f, 0x4a, 0xc8, 0xbe, 0xe3, 0xb8, 0xbb, 0x1c, 0x4d, 0x5a, 0xf4, 0x1d, 0x3f, 0x55, | ||||
|     0x32, 0x17, 0x0b, 0xf2, 0x49, 0x2b, 0xe9, 0xb8, 0xc4, 0x14, 0x28, 0xfb, 0x07, 0x51, 0x2b, 0x88, 0xed, 0x4e, 0xff, | ||||
|     0xcb, 0x1d, 0xe3, 0x6e, 0x8f, 0xf9, 0x61, 0x84, 0x29, 0x31, 0x36, 0xc4, 0x66, 0xf4, 0xf9, 0x71, 0x75, 0xae, 0xb2, | ||||
|     0xfb, 0x27, 0x52, 0xa7, 0xa0, 0x5d, 0xde, 0x08, 0x29, 0xb1, 0xb9, 0xc1, 0x8d, 0x89, 0x9d, 0xb7, 0x97, 0x57, 0x70, | ||||
|     0x99, 0x65, 0x0d, 0x6a, 0x3d, 0x05, 0xe7, 0xa5, 0x21, 0x15, 0x4f, 0xff, 0x73, 0x5d, 0xf4, 0x91, 0xae, 0xbf, 0x89, | ||||
|     0x1f, 0x05, 0xfc, 0x82, 0x66, 0xad, 0x9a, 0xbb, 0xbd, 0x36, 0x6b, 0xda, 0xcc, 0x66, 0x60, 0x13, 0x1b, 0xc2, 0x6b, | ||||
|     0x4d, 0x74, 0x29, 0x52, 0xec, 0x53, 0x97, 0x54, 0xbc, 0x7e, 0xf0, 0x4a, 0x1e, 0x80, 0xba, 0x8d, 0x32, 0xb1, 0x82, | ||||
|     0xb4, 0xe4, 0x5a, 0xc7, 0x8e, 0xec, 0x54, 0x39, 0xb0, 0x4f, 0x1b, 0x25, 0xd3, 0x52, 0xa4, 0x77, 0xb1, 0xf3, 0x95, | ||||
|     0x1b, 0xe2, 0xd5, 0xfd, 0xcf, 0x59, 0xbf, 0xa7, 0xb5, 0xc8, 0x7a, 0x2e, 0x59, 0xf1, 0x72, 0x89, 0x10, 0x83, 0x29, | ||||
|     0x84, 0x7e, 0x30, 0x70, 0xf6, 0xa4, 0x58, 0xad, 0xef, 0x7a, 0x2e, 0xc9, 0x55, 0xba, 0xd4, 0x7d, 0xd7, 0x39, 0x64, | ||||
|     0x69, 0xc4, 0xbb, 0x3b, 0xd4, 0x79, 0xee, 0x7c, 0x61, 0x91, 0x57, 0x62, 0x6e, 0x9c, 0x87, 0x6c, 0x7e, 0xb1, 0xd5, | ||||
|     0x7d, 0x49, 0x1a, 0xad, 0x85, 0xbb, 0x3b, 0x2e, 0x46, 0xba, 0xe6, 0xf2, 0x4b, 0x41, 0x6b, 0xa0, 0x4d, 0x1a, 0x49, | ||||
|     0x2c, 0x65, 0x33, 0xa7, 0xe6, 0xf2, 0x78, 0xa0, 0xcf, 0x0f, 0xe4, 0x8b, 0xad, 0xe8, 0x4b, 0x7b, 0x4b, 0xde, 0x1d, | ||||
|     0x35, 0x46, 0x7e, 0x26, 0x56, 0xc9, 0xed, 0xce, 0x7d, 0xf0, 0xe3, 0xef, 0x4b, 0x6c, 0xee, 0xaf, 0xb1, 0xc4, 0xd4, | ||||
|     0xa8, 0xa6, 0xef, 0x3c, 0x97, 0x68, 0x1c, 0xb7, 0x73, 0xf8, 0xa7, 0x9b, 0xb7, 0x6f, 0x62, 0xd5, 0x6f, 0xdc, 0xf3, | ||||
|     0xa7, 0xb8, 0x6d, 0xb5, 0xf8, 0xd0, 0x60, 0xf9, 0x8f, 0xb8, 0x67, 0xeb, 0x45, 0xef, 0xa3, 0xe3, 0x92, 0xd6, 0xdf, | ||||
|     0xdb, 0x87, 0xa2, 0x61, 0x13, 0xfb, 0xe5, 0xa6, 0x2a, 0xcf, 0xad, 0x87, 0xde, 0x68, 0xe8, 0xee, 0x6e, 0x77, 0xee, | ||||
|     0xce, 0x9d, 0x45, 0x7e, 0x77, 0xef, 0x27, 0x51, 0x7b, 0x05, 0x27, 0xdf, 0x6f, 0xe7, 0x6a, 0xe3, 0x69, 0xf1, 0x59, | ||||
|     0xc8, 0xc5, 0x54, 0xc8, 0x02, 0x1b, 0x61, 0x76, 0x99, 0x58, 0x9d, 0x0b, 0x59, 0x2f, 0xcd, 0xb6, 0xe6, 0x59, 0x66, | ||||
|     0x77, 0x86, 0xf5, 0x66, 0x96, 0x2b, 0x69, 0x2c, 0x27, 0x4e, 0x29, 0x56, 0xbb, 0x6e, 0xbf, 0xbd, 0x5b, 0xa6, 0x17, | ||||
|     0xc3, 0xb3, 0x9d, 0x0d, 0xb8, 0xad, 0xc1, 0x8d, 0xf1, 0x78, 0x29, 0x16, 0x72, 0x9a, 0xa2, 0x34, 0xd8, 0x74, 0x42, | ||||
|     0x39, 0xaf, 0x44, 0x79, 0x3f, 0xd5, 0x5c, 0x6a, 0x4f, 0x63, 0x23, 0xf2, 0xdd, 0x7c, 0x69, 0x8c, 0x92, 0xdb, 0xb9, | ||||
|     0x6a, 0x32, 0x6c, 0xa6, 0xc1, 0xac, 0x23, 0xbc, 0x86, 0x67, 0x62, 0xa9, 0xa7, 0x24, 0x6c, 0xb0, 0x9a, 0xcd, 0x79, | ||||
|     0x7a, 0xb7, 0x68, 0xd4, 0x52, 0x66, 0x5e, 0x6a, 0x6f, 0xe1, 0xe9, 0x73, 0x9a, 0xf3, 0x10, 0xd3, 0xd9, 0x7e, 0x96, | ||||
|     0xe7, 0xf9, 0xac, 0x14, 0x12, 0xbd, 0xee, 0x56, 0x9b, 0x32, 0x32, 0xb0, 0x62, 0x27, 0x66, 0x12, 0x66, 0x17, 0x3a, | ||||
|     0x1b, 0x69, 0x10, 0x9c, 0xcd, 0x0e, 0xee, 0x04, 0xb3, 0x74, 0xd9, 0x68, 0xd5, 0x4c, 0x6b, 0x25, 0xac, 0x99, 0xbb, | ||||
|     0x8a, 0x0b, 0x79, 0x6a, 0xbd, 0x0d, 0x93, 0xd9, 0xbe, 0x3c, 0x4d, 0x85, 0x6c, 0x8f, 0x69, 0x8b, 0xd4, 0xac, 0x12, | ||||
|     0xb2, 0x2b, 0xb2, 0x53, 0x36, 0x0a, 0xea, 0xcd, 0x8e, 0xec, 0x03, 0x64, 0x7b, 0xe0, 0xce, 0x4b, 0xdc, 0xcc, 0x3e, | ||||
|     0x2d, 0xb5, 0x11, 0xf9, 0xbd, 0xb7, 0x2f, 0xd2, 0x53, 0x5d, 0xf3, 0x14, 0xbd, 0x39, 0x9a, 0x35, 0xa2, 0x9c, 0xb5, | ||||
|     0x67, 0x78, 0xc2, 0x60, 0xa5, 0xf7, 0x38, 0x1d, 0xd5, 0xb4, 0x01, 0xfa, 0x58, 0xd7, 0xbf, 0xe3, 0xb6, 0xb1, 0xb8, | ||||
|     0xad, 0x78, 0xb3, 0x10, 0xd2, 0x9b, 0x2b, 0x63, 0x54, 0x35, 0xf5, 0xc6, 0xf5, 0x66, 0xb6, 0x5f, 0xb2, 0xca, 0xa6, | ||||
|     0xd4, 0x9a, 0xd9, 0xd6, 0xde, 0x03, 0xde, 0xb4, 0xde, 0x80, 0x56, 0xa5, 0xc8, 0xf6, 0x7c, 0x2d, 0x0b, 0x04, 0x47, | ||||
|     0x78, 0xe8, 0xb0, 0xde, 0x80, 0x5d, 0x3b, 0x40, 0x3d, 0xc8, 0x27, 0x9c, 0x06, 0x5f, 0xf9, 0x46, 0xb2, 0x3c, 0x67, | ||||
|     0xf3, 0xfc, 0x88, 0x94, 0x2d, 0xa1, 0x3b, 0xb1, 0x8f, 0x0a, 0x36, 0xa8, 0x37, 0xb3, 0xc3, 0x77, 0x33, 0xa8, 0x37, | ||||
|     0x3b, 0xd1, 0xa6, 0xc5, 0xf6, 0x44, 0x4b, 0x1b, 0xaa, 0xd3, 0x65, 0x53, 0xf6, 0x9d, 0xaf, 0x84, 0xee, 0x59, 0x78, | ||||
|     0xf5, 0x50, 0xe2, 0x7a, 0x4f, 0x97, 0xb8, 0x1e, 0xd8, 0xa6, 0xe8, 0x95, 0xda, 0xc4, 0xbd, 0xb6, 0xd8, 0x0c, 0x80, | ||||
|     0x0d, 0x7a, 0x67, 0xe1, 0xeb, 0xb3, 0xf0, 0xea, 0xbf, 0x52, 0xbb, 0x7e, 0x77, 0xe1, 0xfa, 0x86, 0xaa, 0xf5, 0x8d, | ||||
|     0x15, 0xab, 0xf3, 0xce, 0x3a, 0x7f, 0x16, 0xbe, 0x76, 0xdc, 0x9d, 0x20, 0x5a, 0x2c, 0xe8, 0xff, 0x02, 0xda, 0x7f, | ||||
|     0xc5, 0x31, 0xbc, 0xa4, 0x13, 0x72, 0x01, 0xed, 0xd0, 0x41, 0x44, 0xc2, 0x09, 0x8c, 0xaf, 0x06, 0x64, 0x40, 0xc1, | ||||
|     0xb6, 0x43, 0x23, 0x18, 0x93, 0xc9, 0x05, 0xd0, 0x11, 0x09, 0xc7, 0x40, 0x19, 0x30, 0x4a, 0x86, 0x6f, 0x58, 0x48, | ||||
|     0x46, 0x43, 0x18, 0x5f, 0xb1, 0x80, 0x84, 0x0c, 0x3a, 0xde, 0x11, 0x61, 0x0c, 0x42, 0xcb, 0x12, 0x56, 0x01, 0xb0, | ||||
|     0x34, 0x24, 0xc1, 0x18, 0x02, 0x18, 0x91, 0xe0, 0x82, 0x4c, 0x46, 0x30, 0x21, 0x63, 0x0a, 0x8c, 0x0c, 0x86, 0xa5, | ||||
|     0x37, 0x24, 0x14, 0x46, 0x24, 0x1c, 0xf1, 0x09, 0x19, 0x84, 0xd0, 0x0e, 0x1d, 0x1c, 0x63, 0xc2, 0x98, 0x47, 0x02, | ||||
|     0xfa, 0x26, 0x24, 0x6c, 0x0c, 0x63, 0x32, 0x18, 0x5c, 0xd2, 0x11, 0xb9, 0x18, 0x40, 0x37, 0x76, 0xf0, 0x52, 0x06, | ||||
|     0xc3, 0xa7, 0x40, 0x63, 0x7f, 0x5e, 0xd0, 0x42, 0xc2, 0x28, 0x84, 0xe4, 0x62, 0xc2, 0x6d, 0x5f, 0xca, 0xa0, 0x1b, | ||||
|     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}; | ||||
|     0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e, | ||||
|     0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36, | ||||
|     0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf, | ||||
|     0x7e, 0xa0, 0x24, 0x7b, 0x9d, 0x45, 0x73, 0xb8, 0xb3, 0x60, 0x61, 0x38, 0xef, 0x19, 0xcd, 0x83, 0xc5, 0xdf, 0x2a, | ||||
|     0xc5, 0xec, 0xbe, 0x03, 0xd4, 0xd8, 0x56, 0x94, 0x85, 0x7b, 0x23, 0x41, 0xe5, 0x8a, 0x80, 0x2c, 0x8b, 0x06, 0x68, | ||||
|     0x55, 0x16, 0x2d, 0x58, 0x8a, 0x58, 0x43, 0xb5, 0x01, 0x4b, 0xfe, 0xbc, 0xff, 0x39, 0xb8, 0x2d, 0x0b, 0xc1, 0xe5, | ||||
|     0x1a, 0x69, 0x10, 0x84, 0x33, 0x25, 0x51, 0xa3, 0xa1, 0x26, 0x15, 0xb5, 0x34, 0xe3, 0x2d, 0x5d, 0xc1, 0x24, 0x22, | ||||
|     0x69, 0x0b, 0x64, 0xc3, 0x61, 0xdb, 0x29, 0x6d, 0x11, 0x53, 0xd2, 0x82, 0xb4, 0x04, 0x6f, 0x79, 0x65, 0x1b, 0x52, | ||||
|     0xc1, 0x86, 0x33, 0x08, 0x86, 0xc3, 0x35, 0x97, 0xdc, 0x72, 0x2a, 0x02, 0xc3, 0xa8, 0x00, 0x92, 0x5c, 0xf7, 0x06, | ||||
|     0xf4, 0x70, 0xa0, 0x4b, 0x01, 0x44, 0x2a, 0x5c, 0x16, 0x86, 0x69, 0xde, 0x59, 0xe4, 0x5c, 0x25, 0xad, 0xaa, 0x7a, | ||||
|     0x01, 0x65, 0x14, 0x51, 0x63, 0xc0, 0x9a, 0x88, 0xcb, 0x0a, 0x76, 0xe1, 0x32, 0x66, 0x2c, 0x86, 0xdb, 0xdb, 0xf0, | ||||
|     0xb3, 0x79, 0x56, 0x29, 0xd6, 0xb7, 0x20, 0x6d, 0x28, 0x14, 0xa3, 0x96, 0x2b, 0x19, 0x1a, 0xa0, 0x9a, 0x35, 0x84, | ||||
|     0x10, 0xfc, 0xa3, 0xa1, 0x1b, 0xc0, 0xdf, 0x7f, 0xef, 0x9d, 0x99, 0x56, 0x60, 0xff, 0x21, 0xc0, 0x81, 0xe6, 0xa7, | ||||
|     0xfd, 0x3d, 0x5d, 0xfd, 0x4e, 0x5b, 0xf0, 0x30, 0x35, 0xbc, 0x02, 0xec, 0x7f, 0x8c, 0x3f, 0x85, 0xc6, 0xee, 0x05, | ||||
|     0x84, 0x15, 0x37, 0x9d, 0xa0, 0x7b, 0x82, 0x97, 0x42, 0xb1, 0x35, 0xf6, 0xf3, 0xba, 0x97, 0xcc, 0x29, 0x47, 0xc6, | ||||
|     0x03, 0xff, 0x20, 0xc0, 0x22, 0x4b, 0xde, 0x51, 0xdb, 0x84, 0x2d, 0xdd, 0x79, 0x23, 0xc0, 0xa5, 0x97, 0xfe, 0xe0, | ||||
|     0xc1, 0xcb, 0x24, 0x8e, 0xfd, 0xeb, 0xe1, 0x15, 0xfb, 0x51, 0x12, 0xc7, 0xb9, 0x06, 0xdb, 0x6b, 0x89, 0xa8, 0xf7, | ||||
|     0x50, 0x74, 0xd4, 0x36, 0xa8, 0x22, 0xf8, 0x5d, 0x92, 0xa2, 0xe4, 0x75, 0x98, 0xce, 0x7f, 0x0b, 0x5f, 0xa1, 0x9b, | ||||
|     0x30, 0x9d, 0xb3, 0x57, 0xc1, 0x1c, 0x25, 0x37, 0xc1, 0x1c, 0xa5, 0x69, 0x38, 0x47, 0xf1, 0x17, 0x8c, 0x6a, 0x2e, | ||||
|     0x04, 0xc1, 0x52, 0x49, 0xc0, 0xc8, 0x58, 0xad, 0xd6, 0x40, 0x30, 0xeb, 0xb5, 0x06, 0x69, 0xdf, 0x2a, 0xa1, 0x34, | ||||
|     0x8e, 0xca, 0x67, 0xff, 0x97, 0x42, 0xab, 0xa9, 0x34, 0xb5, 0xd2, 0x2d, 0xc1, 0x43, 0xf6, 0xbd, 0x17, 0x07, 0x7b, | ||||
|     0x44, 0xee, 0xe5, 0x5f, 0x10, 0x03, 0xa5, 0xf9, 0x8a, 0x4b, 0x82, 0x9d, 0xc6, 0x5b, 0x1c, 0x95, 0x0f, 0xfe, 0xf1, | ||||
|     0x1c, 0x3d, 0x75, 0xd1, 0x4f, 0xf1, 0x28, 0xef, 0xe3, 0x43, 0x61, 0x36, 0x2b, 0xb4, 0x6b, 0x85, 0x34, 0x04, 0x37, | ||||
|     0xd6, 0x76, 0x59, 0x14, 0x6d, 0xb7, 0xdb, 0x70, 0x3b, 0x0b, 0x95, 0x5e, 0x45, 0x69, 0x1c, 0xc7, 0x91, 0xd9, 0xac, | ||||
|     0x30, 0x1a, 0x0b, 0x01, 0xa7, 0x37, 0x18, 0x35, 0xc0, 0x57, 0x8d, 0x1d, 0xe0, 0xf2, 0xc5, 0x01, 0x8e, 0x85, 0xe3, | ||||
|     0x28, 0x1f, 0x3e, 0x5d, 0x58, 0xe1, 0x17, 0x56, 0xe0, 0x47, 0xea, 0xe1, 0x53, 0x98, 0x57, 0x43, 0x98, 0xaf, 0x68, | ||||
|     0x8a, 0x52, 0x14, 0x0f, 0x4f, 0x1a, 0x38, 0x78, 0x3a, 0x05, 0x4f, 0x4e, 0xe8, 0xe2, 0xe4, 0xa0, 0x76, 0x11, 0xbc, | ||||
|     0x3e, 0xcb, 0x26, 0x0e, 0xb3, 0x49, 0xe2, 0x47, 0x84, 0x13, 0xf8, 0x65, 0x71, 0x79, 0x0e, 0xd2, 0x0f, 0x97, 0x0c, | ||||
|     0xce, 0x5a, 0x93, 0x7c, 0x58, 0xd0, 0x39, 0x9a, 0x4f, 0x98, 0x79, 0xe0, 0xe0, 0xf3, 0x09, 0xcd, 0x37, 0x69, 0x93, | ||||
|     0xb4, 0xc1, 0x22, 0x98, 0xd3, 0x19, 0x9a, 0x4d, 0x8e, 0xcc, 0xd0, 0x6c, 0x93, 0x36, 0x8b, 0x0f, 0x8b, 0x4b, 0x5c, | ||||
|     0x30, 0xfb, 0x72, 0x15, 0x95, 0xd8, 0xcf, 0x30, 0x7e, 0x8c, 0x5c, 0x5d, 0x46, 0x1e, 0x7e, 0x56, 0x5c, 0x7a, 0x18, | ||||
|     0xfb, 0xc7, 0x1a, 0x2c, 0x6b, 0x3c, 0x1c, 0x31, 0x25, 0x6b, 0xbe, 0x0a, 0x3f, 0x1b, 0x25, 0xb1, 0x1f, 0xda, 0x06, | ||||
|     0xa4, 0x77, 0x12, 0x75, 0x82, 0x30, 0x50, 0xbc, 0xa7, 0x14, 0xeb, 0x1f, 0xce, 0xf5, 0x6f, 0xb9, 0x15, 0x40, 0x6c, | ||||
|     0xe8, 0x1a, 0xf6, 0xfa, 0x8c, 0x5d, 0xaa, 0x6a, 0xff, 0x8d, 0xd6, 0x68, 0x92, 0xb1, 0x2f, 0xb8, 0x94, 0xa0, 0xef, | ||||
|     0x61, 0x67, 0x09, 0x7e, 0xf7, 0xe6, 0x2d, 0x7a, 0x53, 0x55, 0x1a, 0x8c, 0xc9, 0x10, 0x7e, 0x69, 0xc3, 0x96, 0xb2, | ||||
|     0xff, 0x5d, 0x57, 0xf2, 0x95, 0xae, 0x7f, 0xf2, 0x9f, 0x39, 0xfa, 0x1d, 0xec, 0x56, 0xe9, 0xf5, 0xa4, 0xcd, 0xb9, | ||||
|     0x96, 0xbb, 0x0e, 0xd3, 0xc4, 0x86, 0xb4, 0x33, 0xa1, 0x11, 0x9c, 0x81, 0x97, 0xf8, 0x61, 0x4b, 0xbb, 0xc7, 0xa8, | ||||
|     0xe4, 0x29, 0x51, 0x0f, 0x45, 0xc5, 0x37, 0x88, 0x09, 0x6a, 0x0c, 0xc1, 0x72, 0x54, 0x85, 0xd1, 0x33, 0x34, 0xfc, | ||||
|     0x94, 0x64, 0x82, 0xb3, 0x35, 0xc1, 0x7f, 0x31, 0x01, 0x7e, 0xda, 0xff, 0x5a, 0x79, 0x57, 0xc6, 0xf0, 0xea, 0xca, | ||||
|     0x0f, 0x37, 0x54, 0xf4, 0x80, 0x08, 0xb2, 0x0d, 0x37, 0x8f, 0x0e, 0xe6, 0xdf, 0x14, 0xeb, 0xcc, 0xfa, 0xca, 0x0f, | ||||
|     0x6b, 0xc5, 0x7a, 0xe3, 0xf9, 0xb8, 0x9c, 0xcc, 0x15, 0x74, 0x1c, 0x90, 0xf8, 0x39, 0x7e, 0xe2, 0x51, 0x20, 0xa0, | ||||
|     0xb6, 0x67, 0x3e, 0x84, 0x5e, 0x1c, 0x8c, 0x27, 0x43, 0x6d, 0x0c, 0xf7, 0x8f, 0x67, 0x64, 0x61, 0x3a, 0x2a, 0x9f, | ||||
|     0x0a, 0x3a, 0x07, 0x5d, 0xab, 0xc8, 0xd0, 0x41, 0xae, 0x5f, 0x3a, 0x2a, 0xcf, 0x06, 0x23, 0x7a, 0x02, 0x5f, 0x1c, | ||||
|     0xb8, 0x27, 0xdd, 0x14, 0x5c, 0x9f, 0x35, 0x16, 0x51, 0xc5, 0x37, 0xe5, 0xc3, 0xd1, 0x7f, 0x8c, 0xe3, 0x5f, 0x3d, | ||||
|     0xe8, 0xfd, 0x1d, 0x08, 0x60, 0x56, 0x69, 0x0f, 0x3f, 0x97, 0x60, 0xb1, 0x3f, 0x06, 0xfc, 0xcb, 0xfd, 0xbb, 0xdf, | ||||
|     0x88, 0xf2, 0xb4, 0x7f, 0xfd, 0x2d, 0x6e, 0xb7, 0x0a, 0x3e, 0x6a, 0x10, 0xff, 0x26, 0x57, 0x6e, 0x19, 0x5c, 0x7d, | ||||
|     0xc2, 0x7e, 0x38, 0xc4, 0xfb, 0xf0, 0xb8, 0x11, 0x5c, 0x3b, 0xbf, 0xdc, 0xb5, 0xe2, 0xda, 0x45, 0x18, 0x2c, 0xe6, | ||||
|     0xfe, 0xf1, 0xe1, 0xe8, 0x1f, 0xfd, 0xbc, 0x88, 0xc6, 0xb9, 0x5e, 0x16, 0xc3, 0x88, 0x2d, 0x7f, 0x38, 0x2c, 0xd5, | ||||
|     0x2e, 0x30, 0xfc, 0x0b, 0x97, 0xab, 0x8c, 0xcb, 0x06, 0x34, 0xb7, 0xc7, 0x8a, 0x6f, 0xae, 0xb9, 0xec, 0x7a, 0x7b, | ||||
|     0xe8, 0x68, 0x55, 0x39, 0xca, 0xbc, 0xdb, 0xe5, 0xb5, 0x92, 0xd6, 0x71, 0x42, 0x96, 0x40, 0x7b, 0x1c, 0xe9, 0xc3, | ||||
|     0x44, 0xc9, 0x5e, 0xcf, 0xbf, 0x3b, 0xba, 0x82, 0x3b, 0x58, 0xd8, 0xd9, 0x80, 0x0a, 0xbe, 0x92, 0x19, 0x03, 0x69, | ||||
|     0x41, 0x8f, 0x42, 0x35, 0x6d, 0xb9, 0xd8, 0x67, 0x86, 0x4a, 0x13, 0x18, 0xd0, 0xbc, 0x3e, 0x2e, 0x7b, 0x6b, 0x95, | ||||
|     0x3c, 0x2c, 0x95, 0xae, 0x40, 0x67, 0x71, 0x3e, 0x02, 0x81, 0xa6, 0x15, 0xef, 0x4d, 0x16, 0xce, 0x34, 0xb4, 0xf9, | ||||
|     0x92, 0xb2, 0xf5, 0x4a, 0xab, 0x5e, 0x56, 0x01, 0x73, 0x93, 0x36, 0x7b, 0x9e, 0xd4, 0x74, 0x06, 0x2c, 0x9f, 0x4e, | ||||
|     0x75, 0x5d, 0xe7, 0x82, 0x4b, 0x08, 0xc6, 0x59, 0x96, 0xa5, 0xe1, 0x8d, 0x13, 0xbb, 0x70, 0x33, 0x4c, 0x1d, 0x62, | ||||
|     0xf4, 0x31, 0x89, 0xe3, 0xef, 0xf2, 0x53, 0x38, 0x71, 0xce, 0x7a, 0x6d, 0x94, 0xce, 0x3a, 0xc5, 0x9d, 0x9b, 0xc7, | ||||
|     0x96, 0x72, 0x79, 0xe9, 0xbd, 0x2b, 0x93, 0x7c, 0x5a, 0x3f, 0x19, 0x97, 0x83, 0x99, 0x61, 0x09, 0xe5, 0x2d, 0x97, | ||||
|     0xe3, 0x0e, 0xcd, 0xd2, 0x45, 0xdc, 0xed, 0x8e, 0xe1, 0x54, 0x20, 0x87, 0x13, 0x77, 0x2d, 0x60, 0x97, 0x7f, 0xee, | ||||
|     0x8d, 0xe5, 0xf5, 0x3e, 0x98, 0x76, 0x70, 0x66, 0x3a, 0xca, 0x20, 0x58, 0x82, 0xdd, 0x02, 0xc8, 0x7c, 0xb0, 0x11, | ||||
|     0x70, 0x0b, 0xad, 0x99, 0xf2, 0x74, 0x56, 0x33, 0x14, 0xe8, 0xd7, 0xba, 0xfe, 0x1b, 0xb7, 0xab, 0xc5, 0x43, 0x4b, | ||||
|     0xf5, 0x8a, 0xcb, 0x60, 0xa9, 0xac, 0x55, 0x6d, 0x16, 0xbc, 0xea, 0x76, 0xf9, 0x84, 0x72, 0xca, 0xb2, 0xc4, 0xb9, | ||||
|     0x39, 0xec, 0xd6, 0x53, 0xbe, 0x93, 0x6e, 0x87, 0x8c, 0x12, 0xbc, 0x9a, 0xf8, 0x06, 0x16, 0x14, 0x9f, 0xd3, 0x93, | ||||
|     0xcc, 0xbb, 0x1d, 0x72, 0xb8, 0x53, 0xaa, 0x6f, 0xea, 0x5b, 0x9a, 0xc4, 0x7f, 0xf1, 0x45, 0xaa, 0xba, 0x4e, 0x97, | ||||
|     0xf5, 0x39, 0x53, 0x6e, 0x4d, 0xba, 0xd6, 0x18, 0x4a, 0xab, 0x88, 0xc6, 0xdb, 0x8c, 0xab, 0x8c, 0xb2, 0x70, 0x19, | ||||
|     0x2e, 0x8b, 0x26, 0x41, 0xbc, 0x22, 0x2d, 0x65, 0xe5, 0xc5, 0xf8, 0x2a, 0xa2, 0x26, 0x39, 0x91, 0x9a, 0xa4, 0xfc, | ||||
|     0x6a, 0x18, 0x8d, 0xb4, 0xc1, 0xfb, 0xf2, 0xad, 0x92, 0x12, 0x98, 0xe5, 0x72, 0x85, 0xac, 0x42, 0x53, 0x0a, 0xc2, | ||||
|     0x30, 0x2c, 0x96, 0xba, 0x7c, 0x2f, 0x80, 0x1a, 0x40, 0x5b, 0xca, 0x6d, 0x58, 0x44, 0x23, 0xff, 0xd8, 0xc7, 0xbc, | ||||
|     0x22, 0x12, 0x6c, 0x39, 0x35, 0x6c, 0xd1, 0xcc, 0x46, 0x03, 0x77, 0x60, 0x9d, 0x26, 0x67, 0x60, 0x56, 0x16, 0x6e, | ||||
|     0xe5, 0x22, 0x3a, 0x8c, 0x34, 0x12, 0x6d, 0x79, 0xcd, 0xdd, 0x95, 0xa5, 0x2c, 0x86, 0x22, 0x77, 0x1a, 0x5c, 0x9e, | ||||
|     0xc7, 0xeb, 0xd5, 0x00, 0x09, 0x90, 0x2b, 0xdb, 0x90, 0x59, 0x8a, 0x3a, 0x41, 0x19, 0x34, 0x4a, 0x54, 0xa0, 0xc9, | ||||
|     0xdd, 0xdd, 0xaf, 0x7f, 0x2f, 0x9d, 0x33, 0x8f, 0x72, 0x9d, 0x59, 0x8f, 0x62, 0x0e, 0x98, 0xa4, 0x16, 0x37, 0xe3, | ||||
|     0xa5, 0xaa, 0xa3, 0xc6, 0x6c, 0x95, 0xae, 0xbe, 0xd2, 0xf1, 0x7e, 0x42, 0x8e, 0x7a, 0x86, 0xff, 0xd0, 0x2a, 0xe5, | ||||
|     0x1d, 0xdd, 0x40, 0x11, 0x4d, 0x87, 0x22, 0x72, 0x0e, 0x8f, 0xf4, 0x66, 0xe2, 0x6b, 0x92, 0xf2, 0x8f, 0xfb, 0x37, | ||||
|     0xe8, 0xcf, 0xae, 0xa2, 0x16, 0xc6, 0xb4, 0x0d, 0x51, 0xb5, 0x60, 0x1b, 0x55, 0x91, 0xf7, 0x7f, 0xdc, 0xdd, 0x9f, | ||||
|     0x23, 0xec, 0x07, 0x26, 0x04, 0x92, 0x8d, 0xd7, 0xbb, 0x5e, 0x58, 0xde, 0x51, 0x6d, 0x07, 0xb5, 0x81, 0x9b, 0x22, | ||||
|     0xa7, 0x18, 0x06, 0x7a, 0xcd, 0x05, 0x8c, 0x61, 0x8c, 0x82, 0x25, 0x3a, 0x79, 0x75, 0xb2, 0xf6, 0xc4, 0xaf, 0x68, | ||||
|     0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00}; | ||||
|  | ||||
| }  // namespace captive_portal | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -11,17 +11,35 @@ namespace captive_portal { | ||||
| static const char *const TAG = "captive_portal"; | ||||
|  | ||||
| void CaptivePortal::handle_config(AsyncWebServerRequest *request) { | ||||
|   AsyncResponseStream *stream = request->beginResponseStream("application/json"); | ||||
|   stream->addHeader("cache-control", "public, max-age=0, must-revalidate"); | ||||
|   AsyncResponseStream *stream = request->beginResponseStream(F("application/json")); | ||||
|   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()); | ||||
| #endif | ||||
|  | ||||
|   for (auto &scan : wifi::global_wifi_component->get_scan_result()) { | ||||
|     if (scan.get_is_hidden()) | ||||
|       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(), | ||||
|                    scan.get_with_auth()); | ||||
| #endif | ||||
|   } | ||||
|   stream->print(F("]}")); | ||||
|   request->send(stream); | ||||
| @@ -34,7 +52,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { | ||||
|   ESP_LOGI(TAG, "  Password=" LOG_SECRET("'%s'"), psk.c_str()); | ||||
|   wifi::global_wifi_component->save_wifi_sta(ssid, psk); | ||||
|   wifi::global_wifi_component->start_scanning(); | ||||
|   request->redirect("/?save"); | ||||
|   request->redirect(F("/?save")); | ||||
| } | ||||
|  | ||||
| void CaptivePortal::setup() { | ||||
| @@ -53,18 +71,23 @@ void CaptivePortal::start() { | ||||
|   this->dns_server_ = make_unique<DNSServer>(); | ||||
|   this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); | ||||
|   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 | ||||
|   this->enable_loop(); | ||||
| #endif | ||||
|  | ||||
|   this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { | ||||
|     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; | ||||
|     } | ||||
|  | ||||
| #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(); | ||||
| #endif | ||||
|     req->redirect(url.c_str()); | ||||
|   }); | ||||
|  | ||||
| @@ -73,19 +96,19 @@ void CaptivePortal::start() { | ||||
| } | ||||
|  | ||||
| void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { | ||||
|   if (req->url() == "/") { | ||||
|   if (req->url() == F("/")) { | ||||
| #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 | ||||
|     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 | ||||
|     response->addHeader("Content-Encoding", "gzip"); | ||||
|     response->addHeader(F("Content-Encoding"), F("gzip")); | ||||
|     req->send(response); | ||||
|     return; | ||||
|   } else if (req->url() == "/config.json") { | ||||
|   } else if (req->url() == F("/config.json")) { | ||||
|     this->handle_config(req); | ||||
|     return; | ||||
|   } else if (req->url() == "/wifisave") { | ||||
|   } else if (req->url() == F("/wifisave")) { | ||||
|     this->handle_wifisave(req); | ||||
|     return; | ||||
|   } | ||||
|   | ||||
| @@ -45,11 +45,11 @@ class CaptivePortal : public AsyncWebHandler, public Component { | ||||
|       return false; | ||||
|  | ||||
|     if (request->method() == HTTP_GET) { | ||||
|       if (request->url() == "/") | ||||
|       if (request->url() == F("/")) | ||||
|         return true; | ||||
|       if (request->url() == "/config.json") | ||||
|       if (request->url() == F("/config.json")) | ||||
|         return true; | ||||
|       if (request->url() == "/wifisave") | ||||
|       if (request->url() == F("/wifisave")) | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -152,9 +152,9 @@ void CCS811Component::send_env_data_() { | ||||
| void CCS811Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "CCS811"); | ||||
|   LOG_I2C_DEVICE(this) | ||||
|   LOG_UPDATE_INTERVAL(this) | ||||
|   LOG_SENSOR("  ", "CO2 Sensor", this->co2_) | ||||
|   LOG_SENSOR("  ", "TVOC Sensor", this->tvoc_) | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|   LOG_SENSOR("  ", "CO2 Sensor", this->co2_); | ||||
|   LOG_SENSOR("  ", "TVOC Sensor", this->tvoc_); | ||||
|   LOG_TEXT_SENSOR("  ", "Firmware Version Sensor", this->version_) | ||||
|   if (this->baseline_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Baseline: %04X", *this->baseline_); | ||||
|   | ||||
| @@ -47,7 +47,7 @@ from esphome.const import ( | ||||
|     CONF_VISUAL, | ||||
|     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.cpp_generator import MockObjClass | ||||
|  | ||||
| @@ -517,6 +517,6 @@ async def climate_control_to_code(config, action_id, template_arg, args): | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @coroutine_with_priority(100.0) | ||||
| @coroutine_with_priority(CoroPriority.CORE) | ||||
| async def to_code(config): | ||||
|     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; | ||||
|  | ||||
| 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); | ||||
|   ClimateDeviceRestoreState recovered{}; | ||||
|   if (!this->rtc_.load(&recovered)) | ||||
|   | ||||
| @@ -32,7 +32,7 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_SHUTTER, | ||||
|     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.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) | ||||
| def cover_toggle_to_code(config, action_id, template_arg, args): | ||||
|     paren = yield cg.get_variable(config[CONF_ID]) | ||||
|     yield cg.new_Pvariable(action_id, template_arg, paren) | ||||
| async def cover_toggle_to_code(config, action_id, template_arg, args): | ||||
|     paren = await cg.get_variable(config[CONF_ID]) | ||||
|     return cg.new_Pvariable(action_id, template_arg, paren) | ||||
|  | ||||
|  | ||||
| COVER_CONTROL_ACTION_SCHEMA = cv.Schema( | ||||
| @@ -263,6 +263,6 @@ async def cover_control_to_code(config, action_id, template_arg, args): | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @coroutine_with_priority(100.0) | ||||
| @coroutine_with_priority(CoroPriority.CORE) | ||||
| async def to_code(config): | ||||
|     cg.add_global(cover_ns.using) | ||||
|   | ||||
| @@ -194,7 +194,7 @@ void Cover::publish_state(bool save) { | ||||
|   } | ||||
| } | ||||
| 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{}; | ||||
|   if (!this->rtc_.load(&recovered)) | ||||
|     return {}; | ||||
|   | ||||
| @@ -19,8 +19,8 @@ const extern float COVER_CLOSED; | ||||
|     if (traits_.get_is_assumed_state()) { \ | ||||
|       ESP_LOGCONFIG(TAG, "%s  Assumed State: YES", prefix); \ | ||||
|     } \ | ||||
|     if (!(obj)->get_device_class().empty()) { \ | ||||
|       ESP_LOGCONFIG(TAG, "%s  Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ | ||||
|     if (!(obj)->get_device_class_ref().empty()) { \ | ||||
|       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 { | ||||
|     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; | ||||
| } | ||||
| @@ -124,7 +124,7 @@ bool DallasTemperatureSensor::check_scratch_pad_() { | ||||
|             crc8(this->scratch_pad_, 8)); | ||||
| #endif | ||||
|   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], | ||||
|              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], | ||||
|   | ||||
| @@ -21,7 +21,7 @@ from esphome.const import ( | ||||
|     CONF_WEB_SERVER, | ||||
|     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.cpp_generator import MockObjClass | ||||
|  | ||||
| @@ -172,7 +172,7 @@ async def new_datetime(config, *args): | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @coroutine_with_priority(100.0) | ||||
| @coroutine_with_priority(CoroPriority.CORE) | ||||
| async def to_code(config): | ||||
|     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