mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 00:51:49 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			2025.9.0b3
			...
			jesserockz
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					bc3212d981 | 
@@ -9,7 +9,7 @@ This document provides essential context for AI models interacting with this pro
 | 
			
		||||
 | 
			
		||||
## 2. Core Technologies & Stack
 | 
			
		||||
 | 
			
		||||
*   **Languages:** Python (>=3.11), C++ (gnu++20)
 | 
			
		||||
*   **Languages:** Python (>=3.10), 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 (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).
 | 
			
		||||
    1.  **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (S2, S3, C3, etc.) and both IDF and Arduino frameworks.
 | 
			
		||||
    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,8 +150,7 @@ 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"])`, `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`.
 | 
			
		||||
    *   **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `cv.only_with_arduino`.
 | 
			
		||||
    *   **Schema Extensions:**
 | 
			
		||||
        ```python
 | 
			
		||||
        CONFIG_SCHEMA = cv.Schema({ ... })
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1 @@
 | 
			
		||||
4368db58e8f884aff245996b1e8b644cc0796c0bb2fa706d5740d40b823d3ac9
 | 
			
		||||
6af8b429b94191fe8e239fcb3b73f7982d0266cb5b05ffbc81edaeac1bc8c273
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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@v6.0.0
 | 
			
		||||
      uses: actions/setup-python@v5.6.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@v8.0.0
 | 
			
		||||
        uses: actions/github-script@v7.0.1
 | 
			
		||||
        with:
 | 
			
		||||
          github-token: ${{ steps.generate-token.outputs.token }}
 | 
			
		||||
          script: |
 | 
			
		||||
@@ -105,9 +105,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
            // Calculate data from PR files
 | 
			
		||||
            const changedFiles = prFiles.map(file => file.filename);
 | 
			
		||||
            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;
 | 
			
		||||
            const totalChanges = prFiles.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
 | 
			
		||||
 | 
			
		||||
            console.log('Current labels:', currentLabels.join(', '));
 | 
			
		||||
            console.log('Changed files:', changedFiles.length);
 | 
			
		||||
@@ -233,21 +231,16 @@ 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');
 | 
			
		||||
@@ -382,7 +375,7 @@ jobs:
 | 
			
		||||
              const labels = new Set();
 | 
			
		||||
 | 
			
		||||
              // Check for missing tests
 | 
			
		||||
              if ((allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) && !allLabels.has('has-tests')) {
 | 
			
		||||
              if ((allLabels.has('new-component') || allLabels.has('new-platform')) && !allLabels.has('has-tests')) {
 | 
			
		||||
                labels.add('needs-tests');
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
@@ -419,13 +412,10 @@ jobs:
 | 
			
		||||
 | 
			
		||||
              // Too big message
 | 
			
		||||
              if (finalLabels.includes('too-big')) {
 | 
			
		||||
                const testAdditions = prFiles
 | 
			
		||||
                const testChanges = 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);
 | 
			
		||||
                  .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
 | 
			
		||||
                const nonTestChanges = totalChanges - testChanges;
 | 
			
		||||
 | 
			
		||||
                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@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.11"
 | 
			
		||||
 | 
			
		||||
@@ -47,7 +47,7 @@ jobs:
 | 
			
		||||
          fi
 | 
			
		||||
      - if: failure()
 | 
			
		||||
        name: Review PR
 | 
			
		||||
        uses: actions/github-script@v8.0.0
 | 
			
		||||
        uses: actions/github-script@v7.0.1
 | 
			
		||||
        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@v8.0.0
 | 
			
		||||
        uses: actions/github-script@v7.0.1
 | 
			
		||||
        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@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.11"
 | 
			
		||||
 | 
			
		||||
@@ -41,7 +41,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - if: failure()
 | 
			
		||||
        name: Request changes
 | 
			
		||||
        uses: actions/github-script@v8.0.0
 | 
			
		||||
        uses: actions/github-script@v7.0.1
 | 
			
		||||
        with:
 | 
			
		||||
          script: |
 | 
			
		||||
            await github.rest.pulls.createReview({
 | 
			
		||||
@@ -54,7 +54,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - if: success()
 | 
			
		||||
        name: Dismiss review
 | 
			
		||||
        uses: actions/github-script@v8.0.0
 | 
			
		||||
        uses: actions/github-script@v7.0.1
 | 
			
		||||
        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@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@v5.6.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@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@v5.6.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.5.1
 | 
			
		||||
        uses: codecov/codecov-action@v5.4.3
 | 
			
		||||
        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@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@v5.6.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@v8.0.0
 | 
			
		||||
        uses: actions/github-script@v7.0.1
 | 
			
		||||
        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@v8.0.0
 | 
			
		||||
        uses: actions/github-script@v7.0.1
 | 
			
		||||
        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@v8.0.0
 | 
			
		||||
        uses: actions/github-script@v7.0.1
 | 
			
		||||
        with:
 | 
			
		||||
          script: |
 | 
			
		||||
            const owner = context.repo.owner;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								.github/workflows/needs-docs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.github/workflows/needs-docs.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
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@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@v5.6.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.13.0
 | 
			
		||||
        uses: pypa/gh-action-pypi-publish@v1.12.4
 | 
			
		||||
        with:
 | 
			
		||||
          skip-existing: true
 | 
			
		||||
 | 
			
		||||
@@ -94,7 +94,7 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5.0.0
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.11"
 | 
			
		||||
 | 
			
		||||
@@ -220,7 +220,7 @@ jobs:
 | 
			
		||||
      - deploy-manifest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Trigger Workflow
 | 
			
		||||
        uses: actions/github-script@v8.0.0
 | 
			
		||||
        uses: actions/github-script@v7.0.1
 | 
			
		||||
        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@v8.0.0
 | 
			
		||||
        uses: actions/github-script@v7.0.1
 | 
			
		||||
        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@v10.0.0
 | 
			
		||||
      - uses: actions/stale@v9.1.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@v10.0.0
 | 
			
		||||
      - uses: actions/stale@v9.1.0
 | 
			
		||||
        with:
 | 
			
		||||
          days-before-pr-stale: -1
 | 
			
		||||
          days-before-pr-close: -1
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										30
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,30 +0,0 @@
 | 
			
		||||
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@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@v5.6.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.12
 | 
			
		||||
    rev: v0.12.8
 | 
			
		||||
    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/* @dan-s-github @tobias-
 | 
			
		||||
esphome/components/bl0940/* @tobias-
 | 
			
		||||
esphome/components/bl0942/* @dbuezas @dwmw2
 | 
			
		||||
esphome/components/ble_client/* @buxtronix @clydebarrow
 | 
			
		||||
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
 | 
			
		||||
@@ -88,8 +88,7 @@ esphome/components/bp1658cj/* @Cossid
 | 
			
		||||
esphome/components/bp5758d/* @Cossid
 | 
			
		||||
esphome/components/button/* @esphome/core
 | 
			
		||||
esphome/components/bytebuffer/* @clydebarrow
 | 
			
		||||
esphome/components/camera/* @bdraco @DT-art1
 | 
			
		||||
esphome/components/camera_encoder/* @DT-art1
 | 
			
		||||
esphome/components/camera/* @DT-art1 @bdraco
 | 
			
		||||
esphome/components/canbus/* @danielschramm @mvturnho
 | 
			
		||||
esphome/components/cap1188/* @mreditor97
 | 
			
		||||
esphome/components/captive_portal/* @esphome/core
 | 
			
		||||
@@ -145,9 +144,9 @@ esphome/components/es8156/* @kbx81
 | 
			
		||||
esphome/components/es8311/* @kahrendt @kroimon
 | 
			
		||||
esphome/components/es8388/* @P4uLT
 | 
			
		||||
esphome/components/esp32/* @esphome/core
 | 
			
		||||
esphome/components/esp32_ble/* @bdraco @jesserockz @Rapsssito
 | 
			
		||||
esphome/components/esp32_ble/* @Rapsssito @bdraco @jesserockz
 | 
			
		||||
esphome/components/esp32_ble_client/* @bdraco @jesserockz
 | 
			
		||||
esphome/components/esp32_ble_server/* @clydebarrow @jesserockz @Rapsssito
 | 
			
		||||
esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz
 | 
			
		||||
esphome/components/esp32_ble_tracker/* @bdraco
 | 
			
		||||
esphome/components/esp32_camera_web_server/* @ayufan
 | 
			
		||||
esphome/components/esp32_can/* @Sympatron
 | 
			
		||||
@@ -167,7 +166,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/* @alexborro @loongyh @OnFreund
 | 
			
		||||
esphome/components/fingerprint_grow/* @OnFreund @alexborro @loongyh
 | 
			
		||||
esphome/components/font/* @clydebarrow @esphome/core
 | 
			
		||||
esphome/components/fs3000/* @kahrendt
 | 
			
		||||
esphome/components/ft5x06/* @clydebarrow
 | 
			
		||||
@@ -203,7 +202,7 @@ esphome/components/heatpumpir/* @rob-deutsch
 | 
			
		||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
 | 
			
		||||
esphome/components/hm3301/* @freekode
 | 
			
		||||
esphome/components/hmac_md5/* @dwmw2
 | 
			
		||||
esphome/components/homeassistant/* @esphome/core @OttoWinter
 | 
			
		||||
esphome/components/homeassistant/* @OttoWinter @esphome/core
 | 
			
		||||
esphome/components/homeassistant/number/* @landonr
 | 
			
		||||
esphome/components/homeassistant/switch/* @Links2004
 | 
			
		||||
esphome/components/honeywell_hih_i2c/* @Benichou34
 | 
			
		||||
@@ -228,13 +227,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/* @latonita @Sergio303
 | 
			
		||||
esphome/components/ina226/* @Sergio303 @latonita
 | 
			
		||||
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/inkplate/* @jesserockz @JosipKuci
 | 
			
		||||
esphome/components/inkplate6/* @jesserockz
 | 
			
		||||
esphome/components/integration/* @OttoWinter
 | 
			
		||||
esphome/components/internal_temperature/* @Mat931
 | 
			
		||||
esphome/components/interval/* @esphome/core
 | 
			
		||||
@@ -277,8 +276,8 @@ esphome/components/max7219digit/* @rspaargaren
 | 
			
		||||
esphome/components/max9611/* @mckaymatthew
 | 
			
		||||
esphome/components/mcp23008/* @jesserockz
 | 
			
		||||
esphome/components/mcp23017/* @jesserockz
 | 
			
		||||
esphome/components/mcp23s08/* @jesserockz @SenexCrenshaw
 | 
			
		||||
esphome/components/mcp23s17/* @jesserockz @SenexCrenshaw
 | 
			
		||||
esphome/components/mcp23s08/* @SenexCrenshaw @jesserockz
 | 
			
		||||
esphome/components/mcp23s17/* @SenexCrenshaw @jesserockz
 | 
			
		||||
esphome/components/mcp23x08_base/* @jesserockz
 | 
			
		||||
esphome/components/mcp23x17_base/* @jesserockz
 | 
			
		||||
esphome/components/mcp23xxx_base/* @jesserockz
 | 
			
		||||
@@ -299,7 +298,6 @@ 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
 | 
			
		||||
@@ -343,7 +341,7 @@ esphome/components/ota/* @esphome/core
 | 
			
		||||
esphome/components/output/* @esphome/core
 | 
			
		||||
esphome/components/packet_transport/* @clydebarrow
 | 
			
		||||
esphome/components/pca6416a/* @Mat931
 | 
			
		||||
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
 | 
			
		||||
esphome/components/pca9554/* @clydebarrow @hwstar
 | 
			
		||||
esphome/components/pcf85063/* @brogon
 | 
			
		||||
esphome/components/pcf8563/* @KoenBreeman
 | 
			
		||||
esphome/components/pi4ioe5v6408/* @jesserockz
 | 
			
		||||
@@ -354,9 +352,9 @@ esphome/components/pm2005/* @andrewjswan
 | 
			
		||||
esphome/components/pmsa003i/* @sjtrny
 | 
			
		||||
esphome/components/pmsx003/* @ximex
 | 
			
		||||
esphome/components/pmwcs3/* @SeByDocKy
 | 
			
		||||
esphome/components/pn532/* @jesserockz @OttoWinter
 | 
			
		||||
esphome/components/pn532_i2c/* @jesserockz @OttoWinter
 | 
			
		||||
esphome/components/pn532_spi/* @jesserockz @OttoWinter
 | 
			
		||||
esphome/components/pn532/* @OttoWinter @jesserockz
 | 
			
		||||
esphome/components/pn532_i2c/* @OttoWinter @jesserockz
 | 
			
		||||
esphome/components/pn532_spi/* @OttoWinter @jesserockz
 | 
			
		||||
esphome/components/pn7150/* @jesserockz @kbx81
 | 
			
		||||
esphome/components/pn7150_i2c/* @jesserockz @kbx81
 | 
			
		||||
esphome/components/pn7160/* @jesserockz @kbx81
 | 
			
		||||
@@ -365,7 +363,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/* @cstaahl @stevebaxter @TrentHouliston
 | 
			
		||||
esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter
 | 
			
		||||
esphome/components/pvvx_mithermometer/* @pasiz
 | 
			
		||||
esphome/components/pylontech/* @functionpointer
 | 
			
		||||
esphome/components/qmp6988/* @andrewpc
 | 
			
		||||
@@ -406,7 +404,7 @@ esphome/components/sensirion_common/* @martgras
 | 
			
		||||
esphome/components/sensor/* @esphome/core
 | 
			
		||||
esphome/components/sfa30/* @ghsensdev
 | 
			
		||||
esphome/components/sgp40/* @SenexCrenshaw
 | 
			
		||||
esphome/components/sgp4x/* @martgras @SenexCrenshaw
 | 
			
		||||
esphome/components/sgp4x/* @SenexCrenshaw @martgras
 | 
			
		||||
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.9.0b3
 | 
			
		||||
PROJECT_NUMBER         = 2025.8.0-dev
 | 
			
		||||
 | 
			
		||||
# 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,11 +15,9 @@ 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,
 | 
			
		||||
@@ -45,7 +43,6 @@ 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
 | 
			
		||||
@@ -109,15 +106,13 @@ 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,
 | 
			
		||||
    purpose: Purpose,
 | 
			
		||||
    show_ota: bool,
 | 
			
		||||
    show_mqtt: bool,
 | 
			
		||||
    show_api: bool,
 | 
			
		||||
    purpose: str | None = None,
 | 
			
		||||
) -> list[str]:
 | 
			
		||||
    # Convert to list for uniform handling
 | 
			
		||||
    defaults = [default] if isinstance(default, str) else default or []
 | 
			
		||||
@@ -137,145 +132,53 @@ def choose_upload_log_host(
 | 
			
		||||
                ]
 | 
			
		||||
                resolved.append(choose_prompt(options, purpose=purpose))
 | 
			
		||||
            elif device == "OTA":
 | 
			
		||||
                # 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())
 | 
			
		||||
                if (show_ota and "ota" in CORE.config) or (
 | 
			
		||||
                    show_api and "api" in CORE.config
 | 
			
		||||
                ):
 | 
			
		||||
                    resolved.append(CORE.address)
 | 
			
		||||
 | 
			
		||||
                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)
 | 
			
		||||
 | 
			
		||||
                elif show_mqtt and has_mqtt_logging():
 | 
			
		||||
                    resolved.append("MQTT")
 | 
			
		||||
            else:
 | 
			
		||||
                resolved.append(device)
 | 
			
		||||
        if not resolved:
 | 
			
		||||
            _LOGGER.error("All specified devices: %s could not be resolved.", defaults)
 | 
			
		||||
        return resolved
 | 
			
		||||
 | 
			
		||||
    # No devices specified, show interactive chooser
 | 
			
		||||
    options = [
 | 
			
		||||
        (f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    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 (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 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 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
 | 
			
		||||
 | 
			
		||||
def mqtt_logging_enabled(mqtt_config):
 | 
			
		||||
    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[CONF_LEVEL] != "NONE"
 | 
			
		||||
    return log_topic.get(CONF_LEVEL, None) != "NONE"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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 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 get_port_type(port: str) -> str:
 | 
			
		||||
    if port.startswith("/") or port.startswith("COM"):
 | 
			
		||||
        return "SERIAL"
 | 
			
		||||
    return _PORT_TO_PORT_TYPE.get(port, "NETWORK")
 | 
			
		||||
    if port == "MQTT":
 | 
			
		||||
        return "MQTT"
 | 
			
		||||
    return "NETWORK"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_miniterm(config: ConfigType, port: str, args) -> int:
 | 
			
		||||
@@ -320,9 +223,7 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
 | 
			
		||||
                        .replace(b"\n", b"")
 | 
			
		||||
                        .decode("utf8", "backslashreplace")
 | 
			
		||||
                    )
 | 
			
		||||
                    time_ = datetime.now()
 | 
			
		||||
                    nanoseconds = time_.microsecond // 1000
 | 
			
		||||
                    time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]"
 | 
			
		||||
                    time_str = datetime.now().time().strftime("[%H:%M:%S]")
 | 
			
		||||
                    safe_print(parser.parse_line(line, time_str))
 | 
			
		||||
 | 
			
		||||
                    backtrace_state = platformio_api.process_stacktrace(
 | 
			
		||||
@@ -492,29 +393,27 @@ def check_permissions(port: str):
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upload_program(
 | 
			
		||||
    config: ConfigType, args: ArgsProtocol, devices: list[str]
 | 
			
		||||
) -> tuple[int, str | None]:
 | 
			
		||||
    host = devices[0]
 | 
			
		||||
def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | str:
 | 
			
		||||
    try:
 | 
			
		||||
        module = importlib.import_module("esphome.components." + CORE.target_platform)
 | 
			
		||||
        if getattr(module, "upload_program")(config, args, host):
 | 
			
		||||
            return 0, host
 | 
			
		||||
            return 0
 | 
			
		||||
    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)
 | 
			
		||||
            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
 | 
			
		||||
            return upload_using_esptool(config, host, file, args.upload_speed)
 | 
			
		||||
 | 
			
		||||
        return exit_code, host if exit_code == 0 else None
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
    ota_conf = {}
 | 
			
		||||
    for ota_item in config.get(CONF_OTA, []):
 | 
			
		||||
@@ -531,23 +430,31 @@ def upload_program(
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
    # MQTT address resolution
 | 
			
		||||
    if get_port_type(host) in ("MQTT", "MQTTIP"):
 | 
			
		||||
        devices = mqtt_get_ip(config, args.username, args.password, args.client_id)
 | 
			
		||||
    # 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
 | 
			
		||||
 | 
			
		||||
    return espota2.run_ota(devices, remote_port, password, binary)
 | 
			
		||||
        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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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!")
 | 
			
		||||
 | 
			
		||||
@@ -556,28 +463,20 @@ 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
 | 
			
		||||
 | 
			
		||||
    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(
 | 
			
		||||
            mqtt_address = mqtt.get_esphome_device_ip(
 | 
			
		||||
                config, args.username, args.password, args.client_id
 | 
			
		||||
            )
 | 
			
		||||
            )[0]
 | 
			
		||||
            addresses_to_use = [mqtt_address]
 | 
			
		||||
 | 
			
		||||
        if addresses_to_use is not None:
 | 
			
		||||
            from esphome.components.api.client import run_logs
 | 
			
		||||
        from esphome.components.api.client import run_logs
 | 
			
		||||
 | 
			
		||||
            return run_logs(config, addresses_to_use)
 | 
			
		||||
 | 
			
		||||
    if port_type in ("NETWORK", "MQTT") and has_mqtt_logging():
 | 
			
		||||
        return run_logs(config, addresses_to_use)
 | 
			
		||||
    if get_port_type(port) == "MQTT" and "mqtt" in config:
 | 
			
		||||
        from esphome import mqtt
 | 
			
		||||
 | 
			
		||||
        return mqtt.show_logs(
 | 
			
		||||
@@ -643,14 +542,23 @@ def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    devices = choose_upload_log_host(
 | 
			
		||||
        default=args.device,
 | 
			
		||||
        check_default=None,
 | 
			
		||||
        purpose=Purpose.UPLOADING,
 | 
			
		||||
        show_ota=True,
 | 
			
		||||
        show_mqtt=False,
 | 
			
		||||
        show_api=False,
 | 
			
		||||
        purpose="uploading",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
    # 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)
 | 
			
		||||
 | 
			
		||||
    return exit_code
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -668,7 +576,10 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    devices = choose_upload_log_host(
 | 
			
		||||
        default=args.device,
 | 
			
		||||
        check_default=None,
 | 
			
		||||
        purpose=Purpose.LOGGING,
 | 
			
		||||
        show_ota=False,
 | 
			
		||||
        show_mqtt=True,
 | 
			
		||||
        show_api=True,
 | 
			
		||||
        purpose="logging",
 | 
			
		||||
    )
 | 
			
		||||
    return show_logs(config, args, devices)
 | 
			
		||||
 | 
			
		||||
@@ -694,14 +605,25 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    devices = choose_upload_log_host(
 | 
			
		||||
        default=args.device,
 | 
			
		||||
        check_default=None,
 | 
			
		||||
        purpose=Purpose.UPLOADING,
 | 
			
		||||
        show_ota=True,
 | 
			
		||||
        show_mqtt=False,
 | 
			
		||||
        show_api=True,
 | 
			
		||||
        purpose="uploading",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
    # 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:
 | 
			
		||||
        return exit_code
 | 
			
		||||
 | 
			
		||||
    if args.no_logs:
 | 
			
		||||
@@ -711,7 +633,10 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    devices = choose_upload_log_host(
 | 
			
		||||
        default=successful_device,
 | 
			
		||||
        check_default=successful_device,
 | 
			
		||||
        purpose=Purpose.LOGGING,
 | 
			
		||||
        show_ota=False,
 | 
			
		||||
        show_mqtt=True,
 | 
			
		||||
        show_api=True,
 | 
			
		||||
        purpose="logging",
 | 
			
		||||
    )
 | 
			
		||||
    return show_logs(config, args, devices)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -61,10 +61,11 @@ void AbsoluteHumidityComponent::loop() {
 | 
			
		||||
      ESP_LOGW(TAG, "No valid state from temperature sensor!");
 | 
			
		||||
    }
 | 
			
		||||
    if (no_humidity) {
 | 
			
		||||
      ESP_LOGW(TAG, "No valid state from humidity sensor!");
 | 
			
		||||
      ESP_LOGW(TAG, "No valid state from temperature sensor!");
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGW(TAG, "Unable to calculate absolute humidity.");
 | 
			
		||||
    this->publish_state(NAN);
 | 
			
		||||
    this->status_set_warning(LOG_STR("Unable to calculate absolute humidity."));
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -86,8 +87,9 @@ 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("Invalid saturation vapor pressure equation selection!");
 | 
			
		||||
      this->status_set_error();
 | 
			
		||||
      return;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es);
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,15 @@ 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
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ANALOG,
 | 
			
		||||
    CONF_INPUT,
 | 
			
		||||
    CONF_NUMBER,
 | 
			
		||||
    PLATFORM_ESP8266,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
@@ -266,3 +273,21 @@ 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,8 +241,6 @@ 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_,
 | 
			
		||||
@@ -253,14 +251,10 @@ 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);
 | 
			
		||||
@@ -281,10 +275,8 @@ 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 || \
 | 
			
		||||
@@ -295,7 +287,6 @@ 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};
 | 
			
		||||
@@ -333,32 +324,18 @@ float ADCSensor::sample_autorange_() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const int adc_half = 2048;
 | 
			
		||||
  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);
 | 
			
		||||
  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;
 | 
			
		||||
 | 
			
		||||
  if (csum == 0) {
 | 
			
		||||
    ESP_LOGE(TAG, "Invalid weight sum in autorange calculation");
 | 
			
		||||
    return NAN;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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;
 | 
			
		||||
  return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace adc
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ 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,
 | 
			
		||||
@@ -21,7 +20,6 @@ from esphome.const import (
 | 
			
		||||
    PLATFORM_NRF52,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
@@ -176,21 +174,3 @@ 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, AVRMS, [](float val) { return val / 10000.0f; });
 | 
			
		||||
    this->update_sensor_from_s24zp_register16_(chan->voltage, BVRMS, [](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] = crc8(data.data(), 4, 0xFF, 0x31, true);
 | 
			
		||||
  data[4] = calc_crc8_(data, 4);
 | 
			
		||||
  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] = crc8(data.data(), 4, 0xFF, 0x31, true);
 | 
			
		||||
  data[4] = calc_crc8_(data, 4);
 | 
			
		||||
  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 != crc8(res.data(), len, 0xFF, 0x31, true)) {
 | 
			
		||||
  if (crc_byte != calc_crc8_(res, len)) {
 | 
			
		||||
    this->error_code_ = CRC_CHECK_FAILED;
 | 
			
		||||
    ESP_LOGE(TAG, "Reading AGS10 version failed: crc error!");
 | 
			
		||||
    return optional<std::array<uint8_t, N>>();
 | 
			
		||||
@@ -192,5 +192,20 @@ 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,6 +99,16 @@ 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(LOG_STR("Read failed, will retry"));
 | 
			
		||||
    this->status_set_warning("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(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
 | 
			
		||||
        this->status_set_warning(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(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
 | 
			
		||||
    this->status_set_warning(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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await esp32_ble_tracker.register_ble_device(var, config)
 | 
			
		||||
    yield 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, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, 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(CoroPriority.CORE)
 | 
			
		||||
@coroutine_with_priority(100.0)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add_global(alarm_control_panel_ns.using)
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,22 @@ 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.
 | 
			
		||||
@@ -70,7 +86,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 crc8(data, 6, 0xFF, 0x31, true) == data[6];
 | 
			
		||||
  return this->crc8_(data, 6) == data[6];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AM2315C::setup() {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,9 +21,9 @@
 | 
			
		||||
// SOFTWARE.
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace am2315c {
 | 
			
		||||
@@ -39,6 +39,7 @@ 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, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
 | 
			
		||||
DOMAIN = "api"
 | 
			
		||||
DEPENDENCIES = ["network"]
 | 
			
		||||
@@ -134,7 +134,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine_with_priority(CoroPriority.WEB)
 | 
			
		||||
@coroutine_with_priority(40.0)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
@@ -321,7 +321,6 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
 | 
			
		||||
    HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
 | 
			
		||||
)
 | 
			
		||||
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
 | 
			
		||||
    serv = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, serv, True)
 | 
			
		||||
    cg.add(var.set_service("esphome.tag_scanned"))
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,9 @@ 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) {}
 | 
			
		||||
 | 
			
		||||
@@ -806,16 +809,15 @@ message HomeAssistantStateResponse {
 | 
			
		||||
// ==================== IMPORT TIME ====================
 | 
			
		||||
message GetTimeRequest {
 | 
			
		||||
  option (id) = 36;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
  option (source) = SOURCE_BOTH;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message GetTimeResponse {
 | 
			
		||||
  option (id) = 37;
 | 
			
		||||
  option (source) = SOURCE_CLIENT;
 | 
			
		||||
  option (source) = SOURCE_BOTH;
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
 | 
			
		||||
  fixed32 epoch_seconds = 1;
 | 
			
		||||
  string timezone = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== USER-DEFINES SERVICES ====================
 | 
			
		||||
@@ -1710,7 +1712,6 @@ message BluetoothScannerStateResponse {
 | 
			
		||||
 | 
			
		||||
  BluetoothScannerState state = 1;
 | 
			
		||||
  BluetoothScannerMode mode = 2;
 | 
			
		||||
  BluetoothScannerMode configured_mode = 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message BluetoothScannerSetModeRequest {
 | 
			
		||||
 
 | 
			
		||||
@@ -42,8 +42,6 @@ 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;
 | 
			
		||||
@@ -114,7 +112,7 @@ void APIConnection::start() {
 | 
			
		||||
  APIError err = this->helper_->init();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    this->log_warning_(LOG_STR("Helper init failed"), err);
 | 
			
		||||
    this->log_warning_("Helper init failed", err);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->client_info_.peername = helper_->getpeername();
 | 
			
		||||
@@ -161,7 +159,7 @@ void APIConnection::loop() {
 | 
			
		||||
        break;
 | 
			
		||||
      } else if (err != APIError::OK) {
 | 
			
		||||
        on_fatal_error();
 | 
			
		||||
        this->log_warning_(LOG_STR("Reading failed"), err);
 | 
			
		||||
        this->log_warning_("Reading failed", err);
 | 
			
		||||
        return;
 | 
			
		||||
      } else {
 | 
			
		||||
        this->last_traffic_ = now;
 | 
			
		||||
@@ -291,26 +289,16 @@ 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();
 | 
			
		||||
 | 
			
		||||
  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);
 | 
			
		||||
  }
 | 
			
		||||
  size_t size_before_encode = shared_buf.size();
 | 
			
		||||
 | 
			
		||||
  // Encode directly into buffer
 | 
			
		||||
  size_t size_before_encode = shared_buf.size();
 | 
			
		||||
  msg.encode({&shared_buf});
 | 
			
		||||
  msg.encode(buffer);
 | 
			
		||||
 | 
			
		||||
  // Calculate actual encoded size (not including header that was already added)
 | 
			
		||||
  size_t actual_payload_size = shared_buf.size() - size_before_encode;
 | 
			
		||||
@@ -467,7 +455,9 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
 | 
			
		||||
  resp.cold_white = values.get_cold_white();
 | 
			
		||||
  resp.warm_white = values.get_warm_white();
 | 
			
		||||
  if (light->supports_effects()) {
 | 
			
		||||
    resp.set_effect(light->get_effect_name_ref());
 | 
			
		||||
    // get_effect_name() returns temporary std::string - must store it
 | 
			
		||||
    std::string effect_name = light->get_effect_name();
 | 
			
		||||
    resp.set_effect(StringRef(effect_name));
 | 
			
		||||
  }
 | 
			
		||||
  return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
@@ -1072,17 +1062,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);
 | 
			
		||||
@@ -1372,8 +1362,9 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
 | 
			
		||||
  HelloResponse resp;
 | 
			
		||||
  resp.api_version_major = 1;
 | 
			
		||||
  resp.api_version_minor = 12;
 | 
			
		||||
  // 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);
 | 
			
		||||
  // 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));
 | 
			
		||||
  resp.set_name(StringRef(App.get_name()));
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
@@ -1420,9 +1411,13 @@ 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());
 | 
			
		||||
  // get_compilation_time() returns temporary std::string - must store it
 | 
			
		||||
  std::string compilation_time = App.get_compilation_time();
 | 
			
		||||
  resp.set_compilation_time(StringRef(compilation_time));
 | 
			
		||||
 | 
			
		||||
  // Compile-time StringRef constants for manufacturers
 | 
			
		||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
 | 
			
		||||
@@ -1564,7 +1559,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
 | 
			
		||||
    return false;
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    this->log_warning_(LOG_STR("Packet write failed"), err);
 | 
			
		||||
    this->log_warning_("Packet write failed", err);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  // Do not set last_traffic_ on send
 | 
			
		||||
@@ -1625,6 +1620,14 @@ 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,
 | 
			
		||||
@@ -1732,7 +1735,7 @@ void APIConnection::process_batch_() {
 | 
			
		||||
    }
 | 
			
		||||
    remaining_size -= payload_size;
 | 
			
		||||
    // Calculate where the next message's header padding will start
 | 
			
		||||
    // Current buffer size + footer space for this message
 | 
			
		||||
    // Current buffer size + footer space (that prepare_message_buffer will add for this message)
 | 
			
		||||
    current_offset = shared_buf.size() + footer_size;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -1751,7 +1754,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_(LOG_STR("Batch write failed"), err);
 | 
			
		||||
    this->log_warning_("Batch write failed", err);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
@@ -1829,14 +1832,11 @@ void APIConnection::process_state_subscriptions_() {
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
 | 
			
		||||
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_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_socket_operation_failed_(APIError err) {
 | 
			
		||||
  this->log_warning_(LOG_STR("Socket operation failed"), err);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::log_socket_operation_failed_(APIError err) { this->log_warning_("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 final : public APIServerConnection {
 | 
			
		||||
class APIConnection : public APIServerConnection {
 | 
			
		||||
 public:
 | 
			
		||||
  friend class APIServer;
 | 
			
		||||
  friend class ListEntitiesIterator;
 | 
			
		||||
@@ -219,6 +219,7 @@ class APIConnection final : 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
 | 
			
		||||
@@ -251,21 +252,44 @@ class APIConnection final : 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(total_size);
 | 
			
		||||
    shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_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);
 | 
			
		||||
@@ -273,6 +297,10 @@ class APIConnection final : 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_();
 | 
			
		||||
@@ -300,17 +328,9 @@ class APIConnection final : 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();
 | 
			
		||||
    // 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));
 | 
			
		||||
    }
 | 
			
		||||
    // 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));
 | 
			
		||||
 | 
			
		||||
    if (entity->has_own_name()) {
 | 
			
		||||
      msg.set_name(entity->get_name());
 | 
			
		||||
@@ -731,7 +751,7 @@ class APIConnection final : public APIServerConnection {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Helper function to log API errors with errno
 | 
			
		||||
  void log_warning_(const LogString *message, APIError err);
 | 
			
		||||
  void log_warning_(const char *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 LogString *api_error_to_logstr(APIError err) {
 | 
			
		||||
const char *api_error_to_str(APIError err) {
 | 
			
		||||
  // not using switch to ensure compiler doesn't try to build a big table out of it
 | 
			
		||||
  if (err == APIError::OK) {
 | 
			
		||||
    return LOG_STR("OK");
 | 
			
		||||
    return "OK";
 | 
			
		||||
  } else if (err == APIError::WOULD_BLOCK) {
 | 
			
		||||
    return LOG_STR("WOULD_BLOCK");
 | 
			
		||||
    return "WOULD_BLOCK";
 | 
			
		||||
  } else if (err == APIError::BAD_INDICATOR) {
 | 
			
		||||
    return LOG_STR("BAD_INDICATOR");
 | 
			
		||||
    return "BAD_INDICATOR";
 | 
			
		||||
  } else if (err == APIError::BAD_DATA_PACKET) {
 | 
			
		||||
    return LOG_STR("BAD_DATA_PACKET");
 | 
			
		||||
    return "BAD_DATA_PACKET";
 | 
			
		||||
  } else if (err == APIError::TCP_NODELAY_FAILED) {
 | 
			
		||||
    return LOG_STR("TCP_NODELAY_FAILED");
 | 
			
		||||
    return "TCP_NODELAY_FAILED";
 | 
			
		||||
  } else if (err == APIError::TCP_NONBLOCKING_FAILED) {
 | 
			
		||||
    return LOG_STR("TCP_NONBLOCKING_FAILED");
 | 
			
		||||
    return "TCP_NONBLOCKING_FAILED";
 | 
			
		||||
  } else if (err == APIError::CLOSE_FAILED) {
 | 
			
		||||
    return LOG_STR("CLOSE_FAILED");
 | 
			
		||||
    return "CLOSE_FAILED";
 | 
			
		||||
  } else if (err == APIError::SHUTDOWN_FAILED) {
 | 
			
		||||
    return LOG_STR("SHUTDOWN_FAILED");
 | 
			
		||||
    return "SHUTDOWN_FAILED";
 | 
			
		||||
  } else if (err == APIError::BAD_STATE) {
 | 
			
		||||
    return LOG_STR("BAD_STATE");
 | 
			
		||||
    return "BAD_STATE";
 | 
			
		||||
  } else if (err == APIError::BAD_ARG) {
 | 
			
		||||
    return LOG_STR("BAD_ARG");
 | 
			
		||||
    return "BAD_ARG";
 | 
			
		||||
  } else if (err == APIError::SOCKET_READ_FAILED) {
 | 
			
		||||
    return LOG_STR("SOCKET_READ_FAILED");
 | 
			
		||||
    return "SOCKET_READ_FAILED";
 | 
			
		||||
  } else if (err == APIError::SOCKET_WRITE_FAILED) {
 | 
			
		||||
    return LOG_STR("SOCKET_WRITE_FAILED");
 | 
			
		||||
    return "SOCKET_WRITE_FAILED";
 | 
			
		||||
  } else if (err == APIError::OUT_OF_MEMORY) {
 | 
			
		||||
    return LOG_STR("OUT_OF_MEMORY");
 | 
			
		||||
    return "OUT_OF_MEMORY";
 | 
			
		||||
  } else if (err == APIError::CONNECTION_CLOSED) {
 | 
			
		||||
    return LOG_STR("CONNECTION_CLOSED");
 | 
			
		||||
    return "CONNECTION_CLOSED";
 | 
			
		||||
  }
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) {
 | 
			
		||||
    return LOG_STR("BAD_HANDSHAKE_PACKET_LEN");
 | 
			
		||||
    return "BAD_HANDSHAKE_PACKET_LEN";
 | 
			
		||||
  } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) {
 | 
			
		||||
    return LOG_STR("HANDSHAKESTATE_READ_FAILED");
 | 
			
		||||
    return "HANDSHAKESTATE_READ_FAILED";
 | 
			
		||||
  } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) {
 | 
			
		||||
    return LOG_STR("HANDSHAKESTATE_WRITE_FAILED");
 | 
			
		||||
    return "HANDSHAKESTATE_WRITE_FAILED";
 | 
			
		||||
  } else if (err == APIError::HANDSHAKESTATE_BAD_STATE) {
 | 
			
		||||
    return LOG_STR("HANDSHAKESTATE_BAD_STATE");
 | 
			
		||||
    return "HANDSHAKESTATE_BAD_STATE";
 | 
			
		||||
  } else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) {
 | 
			
		||||
    return LOG_STR("CIPHERSTATE_DECRYPT_FAILED");
 | 
			
		||||
    return "CIPHERSTATE_DECRYPT_FAILED";
 | 
			
		||||
  } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) {
 | 
			
		||||
    return LOG_STR("CIPHERSTATE_ENCRYPT_FAILED");
 | 
			
		||||
    return "CIPHERSTATE_ENCRYPT_FAILED";
 | 
			
		||||
  } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) {
 | 
			
		||||
    return LOG_STR("HANDSHAKESTATE_SETUP_FAILED");
 | 
			
		||||
    return "HANDSHAKESTATE_SETUP_FAILED";
 | 
			
		||||
  } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) {
 | 
			
		||||
    return LOG_STR("HANDSHAKESTATE_SPLIT_FAILED");
 | 
			
		||||
    return "HANDSHAKESTATE_SPLIT_FAILED";
 | 
			
		||||
  } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) {
 | 
			
		||||
    return LOG_STR("BAD_HANDSHAKE_ERROR_BYTE");
 | 
			
		||||
    return "BAD_HANDSHAKE_ERROR_BYTE";
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  return LOG_STR("UNKNOWN");
 | 
			
		||||
  return "UNKNOWN";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Default implementation for loop - handles sending buffered data
 | 
			
		||||
 
 | 
			
		||||
@@ -66,7 +66,7 @@ enum class APIError : uint16_t {
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const LogString *api_error_to_logstr(APIError err);
 | 
			
		||||
const char *api_error_to_str(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
 | 
			
		||||
  uint8_t frame_header_padding() const { return frame_header_padding_; }
 | 
			
		||||
  virtual uint8_t frame_header_padding() = 0;
 | 
			
		||||
  // Get the frame footer size required by this protocol
 | 
			
		||||
  uint8_t frame_footer_size() const { return frame_footer_size_; }
 | 
			
		||||
  virtual uint8_t frame_footer_size() = 0;
 | 
			
		||||
  // Check if socket has data ready to read
 | 
			
		||||
  bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,18 +10,10 @@
 | 
			
		||||
#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__)
 | 
			
		||||
@@ -35,42 +27,42 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12;  // strlen("NoiseAPIInit")
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
/// Convert a noise error code to a readable error
 | 
			
		||||
const LogString *noise_err_to_logstr(int err) {
 | 
			
		||||
std::string noise_err_to_str(int err) {
 | 
			
		||||
  if (err == NOISE_ERROR_NO_MEMORY)
 | 
			
		||||
    return LOG_STR("NO_MEMORY");
 | 
			
		||||
    return "NO_MEMORY";
 | 
			
		||||
  if (err == NOISE_ERROR_UNKNOWN_ID)
 | 
			
		||||
    return LOG_STR("UNKNOWN_ID");
 | 
			
		||||
    return "UNKNOWN_ID";
 | 
			
		||||
  if (err == NOISE_ERROR_UNKNOWN_NAME)
 | 
			
		||||
    return LOG_STR("UNKNOWN_NAME");
 | 
			
		||||
    return "UNKNOWN_NAME";
 | 
			
		||||
  if (err == NOISE_ERROR_MAC_FAILURE)
 | 
			
		||||
    return LOG_STR("MAC_FAILURE");
 | 
			
		||||
    return "MAC_FAILURE";
 | 
			
		||||
  if (err == NOISE_ERROR_NOT_APPLICABLE)
 | 
			
		||||
    return LOG_STR("NOT_APPLICABLE");
 | 
			
		||||
    return "NOT_APPLICABLE";
 | 
			
		||||
  if (err == NOISE_ERROR_SYSTEM)
 | 
			
		||||
    return LOG_STR("SYSTEM");
 | 
			
		||||
    return "SYSTEM";
 | 
			
		||||
  if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
 | 
			
		||||
    return LOG_STR("REMOTE_KEY_REQUIRED");
 | 
			
		||||
    return "REMOTE_KEY_REQUIRED";
 | 
			
		||||
  if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
 | 
			
		||||
    return LOG_STR("LOCAL_KEY_REQUIRED");
 | 
			
		||||
    return "LOCAL_KEY_REQUIRED";
 | 
			
		||||
  if (err == NOISE_ERROR_PSK_REQUIRED)
 | 
			
		||||
    return LOG_STR("PSK_REQUIRED");
 | 
			
		||||
    return "PSK_REQUIRED";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_LENGTH)
 | 
			
		||||
    return LOG_STR("INVALID_LENGTH");
 | 
			
		||||
    return "INVALID_LENGTH";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_PARAM)
 | 
			
		||||
    return LOG_STR("INVALID_PARAM");
 | 
			
		||||
    return "INVALID_PARAM";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_STATE)
 | 
			
		||||
    return LOG_STR("INVALID_STATE");
 | 
			
		||||
    return "INVALID_STATE";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_NONCE)
 | 
			
		||||
    return LOG_STR("INVALID_NONCE");
 | 
			
		||||
    return "INVALID_NONCE";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
 | 
			
		||||
    return LOG_STR("INVALID_PRIVATE_KEY");
 | 
			
		||||
    return "INVALID_PRIVATE_KEY";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
 | 
			
		||||
    return LOG_STR("INVALID_PUBLIC_KEY");
 | 
			
		||||
    return "INVALID_PUBLIC_KEY";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_FORMAT)
 | 
			
		||||
    return LOG_STR("INVALID_FORMAT");
 | 
			
		||||
    return "INVALID_FORMAT";
 | 
			
		||||
  if (err == NOISE_ERROR_INVALID_SIGNATURE)
 | 
			
		||||
    return LOG_STR("INVALID_SIGNATURE");
 | 
			
		||||
  return LOG_STR("UNKNOWN");
 | 
			
		||||
    return "INVALID_SIGNATURE";
 | 
			
		||||
  return to_string(err);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Initialize the frame helper, returns OK if successful.
 | 
			
		||||
@@ -83,11 +75,7 @@ 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;
 | 
			
		||||
@@ -95,18 +83,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_(LOG_STR("Bad indicator byte"));
 | 
			
		||||
    send_explicit_handshake_reject_("Bad indicator byte");
 | 
			
		||||
  } else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
 | 
			
		||||
    send_explicit_handshake_reject_(LOG_STR("Bad handshake packet len"));
 | 
			
		||||
    send_explicit_handshake_reject_("Bad handshake packet len");
 | 
			
		||||
  }
 | 
			
		||||
  return aerr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper for handling noise library errors
 | 
			
		||||
APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func_name, APIError api_err) {
 | 
			
		||||
APIError APINoiseFrameHelper::handle_noise_error_(int err, const char *func_name, APIError api_err) {
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("%s failed: %s", LOG_STR_ARG(func_name), LOG_STR_ARG(noise_err_to_logstr(err)));
 | 
			
		||||
    HELPER_LOG("%s failed: %s", func_name, noise_err_to_str(err).c_str());
 | 
			
		||||
    return api_err;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
@@ -291,11 +279,11 @@ APIError APINoiseFrameHelper::state_action_() {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (frame.empty()) {
 | 
			
		||||
        send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
 | 
			
		||||
        send_explicit_handshake_reject_("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_(LOG_STR("Bad handshake error byte"));
 | 
			
		||||
        send_explicit_handshake_reject_("Bad handshake error byte");
 | 
			
		||||
        return APIError::BAD_HANDSHAKE_ERROR_BYTE;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@@ -305,10 +293,8 @@ 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 ? LOG_STR("Handshake MAC failure")
 | 
			
		||||
                                                                       : LOG_STR("Handshake error"));
 | 
			
		||||
        return handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"),
 | 
			
		||||
                                   APIError::HANDSHAKESTATE_READ_FAILED);
 | 
			
		||||
        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);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      aerr = check_handshake_finished_();
 | 
			
		||||
@@ -321,8 +307,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, LOG_STR("noise_handshakestate_write_message"),
 | 
			
		||||
                                                APIError::HANDSHAKESTATE_WRITE_FAILED);
 | 
			
		||||
      APIError aerr_write =
 | 
			
		||||
          handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED);
 | 
			
		||||
      if (aerr_write != APIError::OK)
 | 
			
		||||
        return aerr_write;
 | 
			
		||||
      buffer[0] = 0x00;  // success
 | 
			
		||||
@@ -345,31 +331,15 @@ APIError APINoiseFrameHelper::state_action_() {
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
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));
 | 
			
		||||
void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) {
 | 
			
		||||
  std::vector<uint8_t> data;
 | 
			
		||||
  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.resize(reason.length() + 1);
 | 
			
		||||
  data[0] = 0x01;  // failure
 | 
			
		||||
 | 
			
		||||
  // Copy error message in bulk
 | 
			
		||||
  if (reason_len > 0) {
 | 
			
		||||
    std::memcpy(data.data() + 1, reason_str, reason_len);
 | 
			
		||||
  if (!reason.empty()) {
 | 
			
		||||
    std::memcpy(data.data() + 1, reason.c_str(), reason.length());
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // temporarily remove failed state
 | 
			
		||||
  auto orig_state = state_;
 | 
			
		||||
@@ -398,8 +368,7 @@ 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, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
 | 
			
		||||
  APIError decrypt_err = handle_noise_error_(err, "noise_cipherstate_decrypt", APIError::CIPHERSTATE_DECRYPT_FAILED);
 | 
			
		||||
  if (decrypt_err != APIError::OK)
 | 
			
		||||
    return decrypt_err;
 | 
			
		||||
 | 
			
		||||
@@ -481,8 +450,7 @@ 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, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED);
 | 
			
		||||
    APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED);
 | 
			
		||||
    if (aerr != APIError::OK)
 | 
			
		||||
      return aerr;
 | 
			
		||||
 | 
			
		||||
@@ -536,27 +504,25 @@ 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, LOG_STR("noise_handshakestate_new_by_id"), APIError::HANDSHAKESTATE_SETUP_FAILED);
 | 
			
		||||
  APIError aerr = handle_noise_error_(err, "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, LOG_STR("noise_handshakestate_set_pre_shared_key"),
 | 
			
		||||
                             APIError::HANDSHAKESTATE_SETUP_FAILED);
 | 
			
		||||
  aerr = handle_noise_error_(err, "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, LOG_STR("noise_handshakestate_set_prologue"), APIError::HANDSHAKESTATE_SETUP_FAILED);
 | 
			
		||||
  aerr = handle_noise_error_(err, "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, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);
 | 
			
		||||
  aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED);
 | 
			
		||||
  if (aerr != APIError::OK)
 | 
			
		||||
    return aerr;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
@@ -574,8 +540,7 @@ 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, LOG_STR("noise_handshakestate_split"), APIError::HANDSHAKESTATE_SPLIT_FAILED);
 | 
			
		||||
  APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED);
 | 
			
		||||
  if (aerr != APIError::OK)
 | 
			
		||||
    return aerr;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
 | 
			
		||||
namespace esphome::api {
 | 
			
		||||
 | 
			
		||||
class APINoiseFrameHelper final : public APIFrameHelper {
 | 
			
		||||
class APINoiseFrameHelper : public APIFrameHelper {
 | 
			
		||||
 public:
 | 
			
		||||
  APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
 | 
			
		||||
                      const ClientInfo *client_info)
 | 
			
		||||
@@ -25,6 +25,10 @@ class APINoiseFrameHelper final : 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_();
 | 
			
		||||
@@ -32,9 +36,9 @@ class APINoiseFrameHelper final : 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 LogString *reason);
 | 
			
		||||
  void send_explicit_handshake_reject_(const std::string &reason);
 | 
			
		||||
  APIError handle_handshake_frame_error_(APIError aerr);
 | 
			
		||||
  APIError handle_noise_error_(int err, const LogString *func_name, APIError api_err);
 | 
			
		||||
  APIError handle_noise_error_(int err, const char *func_name, APIError api_err);
 | 
			
		||||
 | 
			
		||||
  // Pointers first (4 bytes each)
 | 
			
		||||
  NoiseHandshakeState *handshake_{nullptr};
 | 
			
		||||
 
 | 
			
		||||
@@ -10,10 +10,6 @@
 | 
			
		||||
#include <cstring>
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
#include <pgmspace.h>
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome::api {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "api.plaintext";
 | 
			
		||||
@@ -201,20 +197,11 @@ 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.
 | 
			
		||||
      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);
 | 
			
		||||
      const char msg[] = "\x00"
 | 
			
		||||
                         "Bad indicator byte";
 | 
			
		||||
      iov[0].iov_base = (void *) msg;
 | 
			
		||||
#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);
 | 
			
		||||
      iov[0].iov_len = 19;
 | 
			
		||||
      this->write_raw_(iov, 1, 19);
 | 
			
		||||
    }
 | 
			
		||||
    return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
 | 
			
		||||
namespace esphome::api {
 | 
			
		||||
 | 
			
		||||
class APIPlaintextFrameHelper final : public APIFrameHelper {
 | 
			
		||||
class APIPlaintextFrameHelper : public APIFrameHelper {
 | 
			
		||||
 public:
 | 
			
		||||
  APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
 | 
			
		||||
      : APIFrameHelper(std::move(socket), client_info) {
 | 
			
		||||
@@ -22,6 +22,9 @@ class APIPlaintextFrameHelper final : 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,16 +901,6 @@ 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:
 | 
			
		||||
@@ -921,6 +911,8 @@ 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_);
 | 
			
		||||
@@ -2161,12 +2153,10 @@ 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,11 +1110,7 @@ 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 {
 | 
			
		||||
  MessageDumpHelper helper(out, "GetTimeResponse");
 | 
			
		||||
  dump_field(out, "epoch_seconds", this->epoch_seconds);
 | 
			
		||||
  dump_field(out, "timezone", this->timezone);
 | 
			
		||||
}
 | 
			
		||||
void GetTimeResponse::dump_to(std::string &out) const { dump_field(out, "epoch_seconds", this->epoch_seconds); }
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
void ListEntitiesServicesArgument::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "ListEntitiesServicesArgument");
 | 
			
		||||
@@ -1139,7 +1135,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const {
 | 
			
		||||
  dump_field(out, "string_", this->string_);
 | 
			
		||||
  dump_field(out, "int_", this->int_);
 | 
			
		||||
  for (const auto it : this->bool_array) {
 | 
			
		||||
    dump_field(out, "bool_array", static_cast<bool>(it), 4);
 | 
			
		||||
    dump_field(out, "bool_array", it, 4);
 | 
			
		||||
  }
 | 
			
		||||
  for (const auto &it : this->int_array) {
 | 
			
		||||
    dump_field(out, "int_array", it, 4);
 | 
			
		||||
@@ -1708,7 +1704,6 @@ 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,6 +160,15 @@ 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);
 | 
			
		||||
@@ -647,6 +656,11 @@ 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,6 +226,7 @@ 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
 | 
			
		||||
@@ -347,6 +348,7 @@ 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,11 +62,9 @@ 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")
 | 
			
		||||
        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):
 | 
			
		||||
        for parsed_msg in parse_log_message(
 | 
			
		||||
            text, f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]"
 | 
			
		||||
        ):
 | 
			
		||||
            print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg)
 | 
			
		||||
 | 
			
		||||
    stop = await async_run(cli, on_log, name=name)
 | 
			
		||||
 
 | 
			
		||||
@@ -8,70 +8,74 @@ namespace esphome::api {
 | 
			
		||||
static const char *const TAG = "api.proto";
 | 
			
		||||
 | 
			
		||||
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
 | 
			
		||||
  const uint8_t *ptr = buffer;
 | 
			
		||||
  const uint8_t *end = buffer + length;
 | 
			
		||||
 | 
			
		||||
  while (ptr < end) {
 | 
			
		||||
  uint32_t i = 0;
 | 
			
		||||
  bool error = false;
 | 
			
		||||
  while (i < length) {
 | 
			
		||||
    uint32_t consumed;
 | 
			
		||||
 | 
			
		||||
    // Parse field header
 | 
			
		||||
    auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
 | 
			
		||||
    auto res = ProtoVarInt::parse(&buffer[i], length - i, &consumed);
 | 
			
		||||
    if (!res.has_value()) {
 | 
			
		||||
      ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
 | 
			
		||||
      return;
 | 
			
		||||
      ESP_LOGV(TAG, "Invalid field start at %" PRIu32, i);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    uint32_t tag = res->as_uint32();
 | 
			
		||||
    uint32_t field_type = tag & 0b111;
 | 
			
		||||
    uint32_t field_id = tag >> 3;
 | 
			
		||||
    ptr += consumed;
 | 
			
		||||
    uint32_t field_type = (res->as_uint32()) & 0b111;
 | 
			
		||||
    uint32_t field_id = (res->as_uint32()) >> 3;
 | 
			
		||||
    i += consumed;
 | 
			
		||||
 | 
			
		||||
    switch (field_type) {
 | 
			
		||||
      case 0: {  // VarInt
 | 
			
		||||
        res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
 | 
			
		||||
        res = ProtoVarInt::parse(&buffer[i], length - i, &consumed);
 | 
			
		||||
        if (!res.has_value()) {
 | 
			
		||||
          ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
 | 
			
		||||
          return;
 | 
			
		||||
          ESP_LOGV(TAG, "Invalid VarInt at %" PRIu32, i);
 | 
			
		||||
          error = true;
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        if (!this->decode_varint(field_id, *res)) {
 | 
			
		||||
          ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32());
 | 
			
		||||
        }
 | 
			
		||||
        ptr += consumed;
 | 
			
		||||
        i += consumed;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case 2: {  // Length-delimited
 | 
			
		||||
        res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
 | 
			
		||||
        res = ProtoVarInt::parse(&buffer[i], length - i, &consumed);
 | 
			
		||||
        if (!res.has_value()) {
 | 
			
		||||
          ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
 | 
			
		||||
          return;
 | 
			
		||||
          ESP_LOGV(TAG, "Invalid Length Delimited at %" PRIu32, i);
 | 
			
		||||
          error = true;
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        uint32_t field_length = res->as_uint32();
 | 
			
		||||
        ptr += consumed;
 | 
			
		||||
        if (ptr + field_length > end) {
 | 
			
		||||
          ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
 | 
			
		||||
          return;
 | 
			
		||||
        i += consumed;
 | 
			
		||||
        if (field_length > length - i) {
 | 
			
		||||
          ESP_LOGV(TAG, "Out-of-bounds Length Delimited at %" PRIu32, i);
 | 
			
		||||
          error = true;
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        if (!this->decode_length(field_id, ProtoLengthDelimited(ptr, field_length))) {
 | 
			
		||||
        if (!this->decode_length(field_id, ProtoLengthDelimited(&buffer[i], field_length))) {
 | 
			
		||||
          ESP_LOGV(TAG, "Cannot decode Length Delimited field %" PRIu32 "!", field_id);
 | 
			
		||||
        }
 | 
			
		||||
        ptr += field_length;
 | 
			
		||||
        i += field_length;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case 5: {  // 32-bit
 | 
			
		||||
        if (ptr + 4 > end) {
 | 
			
		||||
          ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
 | 
			
		||||
          return;
 | 
			
		||||
        if (length - i < 4) {
 | 
			
		||||
          ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at %" PRIu32, i);
 | 
			
		||||
          error = true;
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        uint32_t val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
 | 
			
		||||
        uint32_t val = encode_uint32(buffer[i + 3], buffer[i + 2], buffer[i + 1], buffer[i]);
 | 
			
		||||
        if (!this->decode_32bit(field_id, Proto32Bit(val))) {
 | 
			
		||||
          ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val);
 | 
			
		||||
        }
 | 
			
		||||
        ptr += 4;
 | 
			
		||||
        i += 4;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      default:
 | 
			
		||||
        ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer));
 | 
			
		||||
        return;
 | 
			
		||||
        ESP_LOGV(TAG, "Invalid field type at %" PRIu32, i);
 | 
			
		||||
        error = true;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
    if (error) {
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ from esphome.const import (
 | 
			
		||||
    PLATFORM_LN882X,
 | 
			
		||||
    PLATFORM_RTL87XX,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
 | 
			
		||||
@@ -27,7 +27,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine_with_priority(CoroPriority.NETWORK_TRANSPORT)
 | 
			
		||||
@coroutine_with_priority(200.0)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    if CORE.is_esp32 or CORE.is_libretiny:
 | 
			
		||||
        # https://github.com/ESP32Async/AsyncTCP
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,6 @@ 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,
 | 
			
		||||
@@ -79,7 +78,6 @@ 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(
 | 
			
		||||
 
 | 
			
		||||
@@ -382,15 +382,20 @@ float ATM90E32Component::get_setup_priority() const { return setup_priority::IO;
 | 
			
		||||
// R/C registers can conly be cleared after the LastSPIData register is updated (register 78H)
 | 
			
		||||
// Peakdetect period: 05H. Bit 15:8 are PeakDet_period in ms. 7:0 are Sag_period
 | 
			
		||||
// Default is 143FH (20ms, 63ms)
 | 
			
		||||
uint16_t ATM90E32Component::read16_(uint16_t a_register) {
 | 
			
		||||
  this->enable();
 | 
			
		||||
  delay_microseconds_safe(1);  // min delay between CS low and first SCK is 200ns - 1us is plenty
 | 
			
		||||
uint16_t ATM90E32Component::read16_transaction_(uint16_t a_register) {
 | 
			
		||||
  uint8_t addrh = (1 << 7) | ((a_register >> 8) & 0x03);
 | 
			
		||||
  uint8_t addrl = (a_register & 0xFF);
 | 
			
		||||
  uint8_t data[4] = {addrh, addrl, 0x00, 0x00};
 | 
			
		||||
  this->transfer_array(data, 4);
 | 
			
		||||
  uint16_t output = encode_uint16(data[2], data[3]);
 | 
			
		||||
  ESP_LOGVV(TAG, "read16_ 0x%04" PRIX16 " output 0x%04" PRIX16, a_register, output);
 | 
			
		||||
  return output;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint16_t ATM90E32Component::read16_(uint16_t a_register) {
 | 
			
		||||
  this->enable();
 | 
			
		||||
  delay_microseconds_safe(1);  // min delay between CS low and first SCK is 200ns - 1us is plenty
 | 
			
		||||
  uint16_t output = this->read16_transaction_(a_register);
 | 
			
		||||
  delay_microseconds_safe(1);  // allow the last clock to propagate before releasing CS
 | 
			
		||||
  this->disable();
 | 
			
		||||
  delay_microseconds_safe(1);  // meet minimum CS high time before next transaction
 | 
			
		||||
@@ -398,8 +403,14 @@ uint16_t ATM90E32Component::read16_(uint16_t a_register) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) {
 | 
			
		||||
  const uint16_t val_h = this->read16_(addr_h);
 | 
			
		||||
  const uint16_t val_l = this->read16_(addr_l);
 | 
			
		||||
  this->enable();
 | 
			
		||||
  delay_microseconds_safe(1);
 | 
			
		||||
  const uint16_t val_h = this->read16_transaction_(addr_h);
 | 
			
		||||
  delay_microseconds_safe(1);
 | 
			
		||||
  const uint16_t val_l = this->read16_transaction_(addr_l);
 | 
			
		||||
  delay_microseconds_safe(1);
 | 
			
		||||
  this->disable();
 | 
			
		||||
  delay_microseconds_safe(1);
 | 
			
		||||
  const int32_t val = (val_h << 16) | val_l;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGVV(TAG,
 | 
			
		||||
 
 | 
			
		||||
@@ -140,6 +140,7 @@ class ATM90E32Component : public PollingComponent,
 | 
			
		||||
  number::Number *ref_currents_[3]{nullptr, nullptr, nullptr};
 | 
			
		||||
#endif
 | 
			
		||||
  uint16_t read16_(uint16_t a_register);
 | 
			
		||||
  uint16_t read16_transaction_(uint16_t a_register);
 | 
			
		||||
  int read32_(uint16_t addr_h, uint16_t addr_l);
 | 
			
		||||
  void write16_(uint16_t a_register, uint16_t val, bool validate = true);
 | 
			
		||||
  float get_local_phase_voltage_(uint8_t phase);
 | 
			
		||||
 
 | 
			
		||||
@@ -17,12 +17,10 @@ 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,
 | 
			
		||||
@@ -102,13 +100,13 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
 | 
			
		||||
            unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE,
 | 
			
		||||
            icon=ICON_LIGHTBULB,
 | 
			
		||||
            accuracy_decimals=2,
 | 
			
		||||
            device_class=DEVICE_CLASS_REACTIVE_POWER,
 | 
			
		||||
            device_class=DEVICE_CLASS_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_APPARENT_POWER,
 | 
			
		||||
            device_class=DEVICE_CLASS_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 CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import 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(CoroPriority.CORE)
 | 
			
		||||
@coroutine_with_priority(100.0)
 | 
			
		||||
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 CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import 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(CoroPriority.CORE)
 | 
			
		||||
@coroutine_with_priority(100.0)
 | 
			
		||||
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(LOG_STR("Failed to communicate")); \
 | 
			
		||||
    this->status_set_warning("Failed to communicate"); \
 | 
			
		||||
    return; \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -41,7 +41,7 @@ void AXS15231Touchscreen::update_touches() {
 | 
			
		||||
  i2c::ErrorCode err;
 | 
			
		||||
  uint8_t data[8]{};
 | 
			
		||||
 | 
			
		||||
  err = this->write(AXS_READ_TOUCHPAD, sizeof(AXS_READ_TOUCHPAD));
 | 
			
		||||
  err = this->write(AXS_READ_TOUCHPAD, sizeof(AXS_READ_TOUCHPAD), false);
 | 
			
		||||
  ERROR_CHECK(err);
 | 
			
		||||
  err = this->read(data, sizeof(data));
 | 
			
		||||
  ERROR_CHECK(err);
 | 
			
		||||
 
 | 
			
		||||
@@ -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, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, 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(CoroPriority.CORE)
 | 
			
		||||
@coroutine_with_priority(100.0)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add_global(binary_sensor_ns.using)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,19 +7,6 @@ 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,10 +10,13 @@ namespace esphome {
 | 
			
		||||
 | 
			
		||||
namespace binary_sensor {
 | 
			
		||||
 | 
			
		||||
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 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()); \
 | 
			
		||||
    } \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#define SUB_BINARY_SENSOR(name) \
 | 
			
		||||
 protected: \
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@tobias-", "@dan-s-github"]
 | 
			
		||||
 | 
			
		||||
CONF_BL0940_ID = "bl0940_id"
 | 
			
		||||
bl0940_ns = cg.esphome_ns.namespace("bl0940")
 | 
			
		||||
CODEOWNERS = ["@tobias-"]
 | 
			
		||||
 
 | 
			
		||||
@@ -7,26 +7,28 @@ 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 en doc but 0x55 in cn doc
 | 
			
		||||
static const uint8_t BL0940_PACKET_HEADER = 0x55;  // 0x58 according to documentation
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
static const uint8_t BL0940_INIT[5][5] = {
 | 
			
		||||
const uint8_t BL0940_INIT[5][6] = {
 | 
			
		||||
    // Reset to default
 | 
			
		||||
    {BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38},
 | 
			
		||||
    {BL0940_WRITE_COMMAND, BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38},
 | 
			
		||||
    // Enable User Operation Write
 | 
			
		||||
    {BL0940_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0},
 | 
			
		||||
    {BL0940_WRITE_COMMAND, BL0940_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0},
 | 
			
		||||
    // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS
 | 
			
		||||
    {BL0940_REG_MODE, 0x00, 0x10, 0x00, 0x37},
 | 
			
		||||
    {BL0940_WRITE_COMMAND, BL0940_REG_MODE, 0x00, 0x10, 0x00, 0x37},
 | 
			
		||||
    // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS
 | 
			
		||||
    {BL0940_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE},
 | 
			
		||||
    {BL0940_WRITE_COMMAND, BL0940_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE},
 | 
			
		||||
    // 0x181C = Half cycle, Fast RMS threshold 6172
 | 
			
		||||
    {BL0940_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}};
 | 
			
		||||
    {BL0940_WRITE_COMMAND, BL0940_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}};
 | 
			
		||||
 | 
			
		||||
void BL0940::loop() {
 | 
			
		||||
  DataPacket buffer;
 | 
			
		||||
@@ -34,8 +36,8 @@ void BL0940::loop() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (read_array((uint8_t *) &buffer, sizeof(buffer))) {
 | 
			
		||||
    if (this->validate_checksum_(&buffer)) {
 | 
			
		||||
      this->received_package_(&buffer);
 | 
			
		||||
    if (validate_checksum(&buffer)) {
 | 
			
		||||
      received_package_(&buffer);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "Junk on wire. Throwing away partial message");
 | 
			
		||||
@@ -44,151 +46,35 @@ void BL0940::loop() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BL0940::validate_checksum_(DataPacket *data) {
 | 
			
		||||
  uint8_t checksum = this->read_command_;
 | 
			
		||||
bool BL0940::validate_checksum(const DataPacket *data) {
 | 
			
		||||
  uint8_t checksum = BL0940_READ_COMMAND;
 | 
			
		||||
  // Whole package but checksum
 | 
			
		||||
  uint8_t *raw = (uint8_t *) data;
 | 
			
		||||
  for (uint32_t i = 0; i < sizeof(*data) - 1; i++) {
 | 
			
		||||
    checksum += raw[i];
 | 
			
		||||
  for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) {
 | 
			
		||||
    checksum += data->raw[i];
 | 
			
		||||
  }
 | 
			
		||||
  checksum ^= 0xFF;
 | 
			
		||||
  if (checksum != data->checksum) {
 | 
			
		||||
    ESP_LOGW(TAG, "Invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum);
 | 
			
		||||
    ESP_LOGW(TAG, "BL0940 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum);
 | 
			
		||||
  }
 | 
			
		||||
  return checksum == data->checksum;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BL0940::update() {
 | 
			
		||||
  this->flush();
 | 
			
		||||
  this->write_byte(this->read_command_);
 | 
			
		||||
  this->write_byte(BL0940_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_byte(this->write_command_), this->write_array(i, 5);
 | 
			
		||||
    this->write_array(i, 6);
 | 
			
		||||
    delay(1);
 | 
			
		||||
  }
 | 
			
		||||
  this->flush();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const {
 | 
			
		||||
  auto tb = (float) (temperature.h << 8 | temperature.l);
 | 
			
		||||
  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_) {
 | 
			
		||||
@@ -201,40 +87,33 @@ float BL0940::update_temp_(sensor::Sensor *sensor, uint16_le_t temperature) cons
 | 
			
		||||
  return converted_temp;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BL0940::received_package_(DataPacket *data) {
 | 
			
		||||
void BL0940::received_package_(const DataPacket *data) const {
 | 
			
		||||
  // Bad header
 | 
			
		||||
  if (data->frame_header != BL0940_PACKET_HEADER) {
 | 
			
		||||
    ESP_LOGI(TAG, "Invalid data. Header mismatch: %d", data->frame_header);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 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 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_;
 | 
			
		||||
 | 
			
		||||
  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_;
 | 
			
		||||
  float tps1 = update_temp_(internal_temperature_sensor_, data->tps1);
 | 
			
		||||
  float tps2 = update_temp_(external_temperature_sensor_, data->tps2);
 | 
			
		||||
 | 
			
		||||
  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 (voltage_sensor_ != nullptr) {
 | 
			
		||||
    voltage_sensor_->publish_state(v_rms);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->current_sensor_ != nullptr) {
 | 
			
		||||
    this->current_sensor_->publish_state(i_rms);
 | 
			
		||||
  if (current_sensor_ != nullptr) {
 | 
			
		||||
    current_sensor_->publish_state(i_rms);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->power_sensor_ != nullptr) {
 | 
			
		||||
    this->power_sensor_->publish_state(watt);
 | 
			
		||||
  if (power_sensor_ != nullptr) {
 | 
			
		||||
    power_sensor_->publish_state(watt);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->energy_sensor_ != nullptr) {
 | 
			
		||||
    this->energy_sensor_->publish_state(total_energy_consumption);
 | 
			
		||||
  if (energy_sensor_ != nullptr) {
 | 
			
		||||
    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,
 | 
			
		||||
@@ -242,27 +121,7 @@ void BL0940::received_package_(DataPacket *data) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BL0940::dump_config() {  // NOLINT(readability-function-cognitive-complexity)
 | 
			
		||||
  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
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "BL0940:");
 | 
			
		||||
  LOG_SENSOR("", "Voltage", this->voltage_sensor_);
 | 
			
		||||
  LOG_SENSOR("", "Current", this->current_sensor_);
 | 
			
		||||
  LOG_SENSOR("", "Power", this->power_sensor_);
 | 
			
		||||
@@ -271,5 +130,9 @@ 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,48 +1,66 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.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"
 | 
			
		||||
#include "esphome/components/sensor/sensor.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)
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
  };
 | 
			
		||||
} __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;
 | 
			
		||||
  }
 | 
			
		||||
@@ -50,105 +68,42 @@ class BL0940 : public PollingComponent, public uart::UARTDevice {
 | 
			
		||||
    external_temperature_sensor_ = external_temperature_sensor;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 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 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
 | 
			
		||||
  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};
 | 
			
		||||
 | 
			
		||||
#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
 | 
			
		||||
  // 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;
 | 
			
		||||
 | 
			
		||||
  // --- 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)
 | 
			
		||||
  float update_temp_(sensor::Sensor *sensor, ube16_t packed_temperature) const;
 | 
			
		||||
 | 
			
		||||
  // --- 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 uint32_t to_uint32_t(ube24_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 int32_t to_int32_t(sbe24_t input);
 | 
			
		||||
 | 
			
		||||
  // --- Protocol commands ---
 | 
			
		||||
  uint8_t read_command_;
 | 
			
		||||
  uint8_t write_command_;
 | 
			
		||||
  static bool validate_checksum(const DataPacket *data);
 | 
			
		||||
 | 
			
		||||
  // --- 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_();
 | 
			
		||||
  void received_package_(const DataPacket *data) const;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace bl0940
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
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])
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,94 +0,0 @@
 | 
			
		||||
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))
 | 
			
		||||
@@ -1,29 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
#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,7 +8,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_INTERNAL_TEMPERATURE,
 | 
			
		||||
    CONF_POWER,
 | 
			
		||||
    CONF_REFERENCE_VOLTAGE,
 | 
			
		||||
    CONF_VOLTAGE,
 | 
			
		||||
    DEVICE_CLASS_CURRENT,
 | 
			
		||||
    DEVICE_CLASS_ENERGY,
 | 
			
		||||
@@ -24,133 +23,12 @@ 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(
 | 
			
		||||
        {
 | 
			
		||||
@@ -191,24 +69,10 @@ 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)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -235,16 +99,3 @@ 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(LOG_STR("BL0942 setup failed!"));
 | 
			
		||||
    this->status_set_warning("BL0942 setup failed!");
 | 
			
		||||
 | 
			
		||||
  this->flush();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -286,7 +286,6 @@ async def remove_bond_to_code(config, action_id, template_arg, args):
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    # Register the loggers this component needs
 | 
			
		||||
    esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP)
 | 
			
		||||
    cg.add_define("USE_ESP32_BLE_UUID")
 | 
			
		||||
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
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 @@ async def to_code(config):
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(var.set_char_uuid128(uuid128))
 | 
			
		||||
    cg.add(var.set_require_response(config[CONF_REQUIRE_RESPONSE]))
 | 
			
		||||
    await output.register_output(var, config)
 | 
			
		||||
    await ble_client.register_ble_node(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    yield output.register_output(var, config)
 | 
			
		||||
    yield ble_client.register_ble_node(var, config)
 | 
			
		||||
    yield 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=True): cv.boolean,
 | 
			
		||||
                cv.Optional(CONF_ACTIVE, default=False): cv.boolean,
 | 
			
		||||
                cv.SplitDefault(CONF_CACHE_SERVICES, esp32_idf=True): cv.All(
 | 
			
		||||
                    cv.only_with_esp_idf, cv.boolean
 | 
			
		||||
                ),
 | 
			
		||||
 
 | 
			
		||||
@@ -12,30 +12,16 @@ namespace esphome::bluetooth_proxy {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "bluetooth_proxy.connection";
 | 
			
		||||
 | 
			
		||||
// This function is allocation-free and directly packs UUIDs into the output array
 | 
			
		||||
// using precalculated constants for the Bluetooth base UUID
 | 
			
		||||
static void fill_128bit_uuid_array(std::array<uint64_t, 2> &out, esp_bt_uuid_t uuid_source) {
 | 
			
		||||
  // Bluetooth base UUID: 00000000-0000-1000-8000-00805F9B34FB
 | 
			
		||||
  // out[0] = bytes 8-15 (big-endian)
 | 
			
		||||
  // - For 128-bit UUIDs: use bytes 8-15 as-is
 | 
			
		||||
  // - For 16/32-bit UUIDs: insert into bytes 12-15, use 0x00001000 for bytes 8-11
 | 
			
		||||
  out[0] = uuid_source.len == ESP_UUID_LEN_128
 | 
			
		||||
               ? (((uint64_t) uuid_source.uuid.uuid128[15] << 56) | ((uint64_t) uuid_source.uuid.uuid128[14] << 48) |
 | 
			
		||||
                  ((uint64_t) uuid_source.uuid.uuid128[13] << 40) | ((uint64_t) uuid_source.uuid.uuid128[12] << 32) |
 | 
			
		||||
                  ((uint64_t) uuid_source.uuid.uuid128[11] << 24) | ((uint64_t) uuid_source.uuid.uuid128[10] << 16) |
 | 
			
		||||
                  ((uint64_t) uuid_source.uuid.uuid128[9] << 8) | ((uint64_t) uuid_source.uuid.uuid128[8]))
 | 
			
		||||
               : (((uint64_t) (uuid_source.len == ESP_UUID_LEN_16 ? uuid_source.uuid.uuid16 : uuid_source.uuid.uuid32)
 | 
			
		||||
                   << 32) |
 | 
			
		||||
                  0x00001000ULL);  // Base UUID bytes 8-11
 | 
			
		||||
  // out[1] = bytes 0-7 (big-endian)
 | 
			
		||||
  // - For 128-bit UUIDs: use bytes 0-7 as-is
 | 
			
		||||
  // - For 16/32-bit UUIDs: use precalculated base UUID constant
 | 
			
		||||
  out[1] = uuid_source.len == ESP_UUID_LEN_128
 | 
			
		||||
               ? ((uint64_t) uuid_source.uuid.uuid128[7] << 56) | ((uint64_t) uuid_source.uuid.uuid128[6] << 48) |
 | 
			
		||||
                     ((uint64_t) uuid_source.uuid.uuid128[5] << 40) | ((uint64_t) uuid_source.uuid.uuid128[4] << 32) |
 | 
			
		||||
                     ((uint64_t) uuid_source.uuid.uuid128[3] << 24) | ((uint64_t) uuid_source.uuid.uuid128[2] << 16) |
 | 
			
		||||
                     ((uint64_t) uuid_source.uuid.uuid128[1] << 8) | ((uint64_t) uuid_source.uuid.uuid128[0])
 | 
			
		||||
               : 0x800000805F9B34FBULL;  // Base UUID bytes 0-7: 80-00-00-80-5F-9B-34-FB
 | 
			
		||||
  esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid();
 | 
			
		||||
  out[0] = ((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) |
 | 
			
		||||
           ((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) |
 | 
			
		||||
           ((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) |
 | 
			
		||||
           ((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]);
 | 
			
		||||
  out[1] = ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) |
 | 
			
		||||
           ((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) |
 | 
			
		||||
           ((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) |
 | 
			
		||||
           ((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper to fill UUID in the appropriate format based on client support and UUID type
 | 
			
		||||
@@ -133,7 +119,7 @@ void BluetoothConnection::loop() {
 | 
			
		||||
 | 
			
		||||
  // Check if we should disable the loop
 | 
			
		||||
  // - For V3_WITH_CACHE: Services are never sent, disable after INIT state
 | 
			
		||||
  // - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
 | 
			
		||||
  // - For other connections: Disable only after service discovery is complete
 | 
			
		||||
  //   (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
 | 
			
		||||
  if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
 | 
			
		||||
                                                   this->send_service_ == DONE_SENDING_SERVICES)) {
 | 
			
		||||
@@ -160,7 +146,10 @@ void BluetoothConnection::send_service_for_discovery_() {
 | 
			
		||||
  if (this->send_service_ >= this->service_count_) {
 | 
			
		||||
    this->send_service_ = DONE_SENDING_SERVICES;
 | 
			
		||||
    this->proxy_->send_gatt_services_done(this->address_);
 | 
			
		||||
    this->release_services();
 | 
			
		||||
    if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
 | 
			
		||||
        this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
 | 
			
		||||
      this->release_services();
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -375,19 +364,10 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
 | 
			
		||||
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
      // Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources
 | 
			
		||||
      // This prevents race condition where we mark slot as free before controller cleanup is complete
 | 
			
		||||
      ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
               param->disconnect.reason);
 | 
			
		||||
      // Send disconnection notification but don't free the slot yet
 | 
			
		||||
      this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
 | 
			
		||||
      this->reset_connection_(param->disconnect.reason);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_CLOSE_EVT: {
 | 
			
		||||
      ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
               param->close.reason);
 | 
			
		||||
      // Now the GATT connection is fully closed and controller resources are freed
 | 
			
		||||
      // Safe to mark the connection slot as available
 | 
			
		||||
      this->reset_connection_(param->close.reason);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ namespace esphome::bluetooth_proxy {
 | 
			
		||||
 | 
			
		||||
class BluetoothProxy;
 | 
			
		||||
 | 
			
		||||
class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
 | 
			
		||||
class BluetoothConnection : public esp32_ble_client::BLEClientBase {
 | 
			
		||||
 public:
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
 
 | 
			
		||||
@@ -24,9 +24,6 @@ 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);
 | 
			
		||||
@@ -39,9 +36,6 @@ 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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -189,12 +183,6 @@ 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());
 | 
			
		||||
@@ -221,9 +209,13 @@ 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");
 | 
			
		||||
      }
 | 
			
		||||
      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);
 | 
			
		||||
      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);
 | 
			
		||||
      }
 | 
			
		||||
      this->send_connections_free();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,7 @@ enum BluetoothProxySubscriptionFlag : uint32_t {
 | 
			
		||||
  SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
 | 
			
		||||
class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
 | 
			
		||||
  friend class BluetoothConnection;  // Allow connection to update connections_free_response_
 | 
			
		||||
 public:
 | 
			
		||||
  BluetoothProxy();
 | 
			
		||||
@@ -130,9 +130,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ
 | 
			
		||||
 | 
			
		||||
  std::string get_bluetooth_mac_address_pretty() {
 | 
			
		||||
    const uint8_t *mac = esp_bt_dev_get_address();
 | 
			
		||||
    char buf[18];
 | 
			
		||||
    format_mac_addr_upper(mac, buf);
 | 
			
		||||
    return std::string(buf);
 | 
			
		||||
    return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
@@ -163,8 +161,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ
 | 
			
		||||
  // Group 4: 1-byte types grouped together
 | 
			
		||||
  bool active_;
 | 
			
		||||
  uint8_t connection_count_{0};
 | 
			
		||||
  bool configured_scan_active_{false};  // Configured scan mode from YAML
 | 
			
		||||
  // 3 bytes used, 1 byte padding
 | 
			
		||||
  // 2 bytes used, 2 bytes padding
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
extern BluetoothProxy *global_bluetooth_proxy;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,6 @@
 | 
			
		||||
#include <esphome/components/sensor/sensor.h>
 | 
			
		||||
#include <esphome/core/component.h>
 | 
			
		||||
 | 
			
		||||
#define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bme280_base {
 | 
			
		||||
 | 
			
		||||
@@ -100,18 +98,18 @@ void BME280Component::setup() {
 | 
			
		||||
 | 
			
		||||
  if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
 | 
			
		||||
    this->error_code_ = COMMUNICATION_FAILED;
 | 
			
		||||
    this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (chip_id != 0x60) {
 | 
			
		||||
    this->error_code_ = WRONG_CHIP_ID;
 | 
			
		||||
    this->mark_failed(BME280_ERROR_WRONG_CHIP_ID);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Send a soft reset.
 | 
			
		||||
  if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) {
 | 
			
		||||
    this->mark_failed("Reset failed");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // Wait until the NVM data has finished loading.
 | 
			
		||||
@@ -120,12 +118,14 @@ void BME280Component::setup() {
 | 
			
		||||
  do {  // NOLINT
 | 
			
		||||
    delay(2);
 | 
			
		||||
    if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
 | 
			
		||||
      this->mark_failed("Error reading status register");
 | 
			
		||||
      ESP_LOGW(TAG, "Error reading status register.");
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  } while ((status & BME280_STATUS_IM_UPDATE) && (--retry));
 | 
			
		||||
  if (status & BME280_STATUS_IM_UPDATE) {
 | 
			
		||||
    this->mark_failed("Timeout loading NVM");
 | 
			
		||||
    ESP_LOGW(TAG, "Timeout loading NVM.");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -153,26 +153,26 @@ void BME280Component::setup() {
 | 
			
		||||
 | 
			
		||||
  uint8_t humid_control_val = 0;
 | 
			
		||||
  if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) {
 | 
			
		||||
    this->mark_failed("Read humidity control");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  humid_control_val &= ~0b00000111;
 | 
			
		||||
  humid_control_val |= this->humidity_oversampling_ & 0b111;
 | 
			
		||||
  if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) {
 | 
			
		||||
    this->mark_failed("Write humidity control");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint8_t config_register = 0;
 | 
			
		||||
  if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) {
 | 
			
		||||
    this->mark_failed("Read config");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  config_register &= ~0b11111100;
 | 
			
		||||
  config_register |= 0b101 << 5;  // 1000 ms standby time
 | 
			
		||||
  config_register |= (this->iir_filter_ & 0b111) << 2;
 | 
			
		||||
  if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) {
 | 
			
		||||
    this->mark_failed("Write config");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -183,7 +183,7 @@ void BME280Component::dump_config() {
 | 
			
		||||
      ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
      break;
 | 
			
		||||
    case WRONG_CHIP_ID:
 | 
			
		||||
      ESP_LOGE(TAG, BME280_ERROR_WRONG_CHIP_ID);
 | 
			
		||||
      ESP_LOGE(TAG, "BME280 has wrong chip ID! Is it a BME280?");
 | 
			
		||||
      break;
 | 
			
		||||
    case NONE:
 | 
			
		||||
    default:
 | 
			
		||||
@@ -223,21 +223,21 @@ void BME280Component::update() {
 | 
			
		||||
  this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() {
 | 
			
		||||
    uint8_t data[8];
 | 
			
		||||
    if (!this->read_bytes(BME280_REGISTER_MEASUREMENTS, data, 8)) {
 | 
			
		||||
      ESP_LOGW(TAG, "Error reading registers");
 | 
			
		||||
      ESP_LOGW(TAG, "Error reading registers.");
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    int32_t t_fine = 0;
 | 
			
		||||
    float const temperature = this->read_temperature_(data, &t_fine);
 | 
			
		||||
    if (std::isnan(temperature)) {
 | 
			
		||||
      ESP_LOGW(TAG, "Invalid temperature");
 | 
			
		||||
      ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values.");
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    float const pressure = this->read_pressure_(data, t_fine);
 | 
			
		||||
    float const humidity = this->read_humidity_(data, t_fine);
 | 
			
		||||
 | 
			
		||||
    ESP_LOGV(TAG, "Temperature=%.1f°C Pressure=%.1fhPa Humidity=%.1f%%", temperature, pressure, humidity);
 | 
			
		||||
    ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity);
 | 
			
		||||
    if (this->temperature_sensor_ != nullptr)
 | 
			
		||||
      this->temperature_sensor_->publish_state(temperature);
 | 
			
		||||
    if (this->pressure_sensor_ != nullptr)
 | 
			
		||||
 
 | 
			
		||||
@@ -203,7 +203,7 @@ void BMI160Component::dump_config() {
 | 
			
		||||
i2c::ErrorCode BMI160Component::read_le_int16_(uint8_t reg, int16_t *value, uint8_t len) {
 | 
			
		||||
  uint8_t raw_data[len * 2];
 | 
			
		||||
  // read using read_register because we have little-endian data, and read_bytes_16 will swap it
 | 
			
		||||
  i2c::ErrorCode err = this->read_register(reg, raw_data, len * 2);
 | 
			
		||||
  i2c::ErrorCode err = this->read_register(reg, raw_data, len * 2, true);
 | 
			
		||||
  if (err != i2c::ERROR_OK) {
 | 
			
		||||
    return err;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,6 @@
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bmp280_base {
 | 
			
		||||
 | 
			
		||||
@@ -63,25 +61,25 @@ void BMP280Component::setup() {
 | 
			
		||||
 | 
			
		||||
  // Read the chip id twice, to work around a bug where the first read is 0.
 | 
			
		||||
  // https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855
 | 
			
		||||
  if (!this->bmp_read_byte(0xD0, &chip_id)) {
 | 
			
		||||
  if (!this->read_byte(0xD0, &chip_id)) {
 | 
			
		||||
    this->error_code_ = COMMUNICATION_FAILED;
 | 
			
		||||
    this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->bmp_read_byte(0xD0, &chip_id)) {
 | 
			
		||||
  if (!this->read_byte(0xD0, &chip_id)) {
 | 
			
		||||
    this->error_code_ = COMMUNICATION_FAILED;
 | 
			
		||||
    this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (chip_id != 0x58) {
 | 
			
		||||
    this->error_code_ = WRONG_CHIP_ID;
 | 
			
		||||
    this->mark_failed(BMP280_ERROR_WRONG_CHIP_ID);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Send a soft reset.
 | 
			
		||||
  if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) {
 | 
			
		||||
    this->mark_failed("Reset failed");
 | 
			
		||||
  if (!this->write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // Wait until the NVM data has finished loading.
 | 
			
		||||
@@ -89,13 +87,15 @@ void BMP280Component::setup() {
 | 
			
		||||
  uint8_t retry = 5;
 | 
			
		||||
  do {
 | 
			
		||||
    delay(2);
 | 
			
		||||
    if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) {
 | 
			
		||||
      this->mark_failed("Error reading status register");
 | 
			
		||||
    if (!this->read_byte(BMP280_REGISTER_STATUS, &status)) {
 | 
			
		||||
      ESP_LOGW(TAG, "Error reading status register.");
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  } while ((status & BMP280_STATUS_IM_UPDATE) && (--retry));
 | 
			
		||||
  if (status & BMP280_STATUS_IM_UPDATE) {
 | 
			
		||||
    this->mark_failed("Timeout loading NVM");
 | 
			
		||||
    ESP_LOGW(TAG, "Timeout loading NVM.");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -115,15 +115,15 @@ void BMP280Component::setup() {
 | 
			
		||||
  this->calibration_.p9 = this->read_s16_le_(0x9E);
 | 
			
		||||
 | 
			
		||||
  uint8_t config_register = 0;
 | 
			
		||||
  if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) {
 | 
			
		||||
    this->mark_failed("Read config");
 | 
			
		||||
  if (!this->read_byte(BMP280_REGISTER_CONFIG, &config_register)) {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  config_register &= ~0b11111100;
 | 
			
		||||
  config_register |= 0b000 << 5;  // 0.5 ms standby time
 | 
			
		||||
  config_register |= (this->iir_filter_ & 0b111) << 2;
 | 
			
		||||
  if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) {
 | 
			
		||||
    this->mark_failed("Write config");
 | 
			
		||||
  if (!this->write_byte(BMP280_REGISTER_CONFIG, config_register)) {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -134,7 +134,7 @@ void BMP280Component::dump_config() {
 | 
			
		||||
      ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
      break;
 | 
			
		||||
    case WRONG_CHIP_ID:
 | 
			
		||||
      ESP_LOGE(TAG, BMP280_ERROR_WRONG_CHIP_ID);
 | 
			
		||||
      ESP_LOGE(TAG, "BMP280 has wrong chip ID! Is it a BME280?");
 | 
			
		||||
      break;
 | 
			
		||||
    case NONE:
 | 
			
		||||
    default:
 | 
			
		||||
@@ -159,7 +159,7 @@ void BMP280Component::update() {
 | 
			
		||||
  meas_value |= (this->temperature_oversampling_ & 0b111) << 5;
 | 
			
		||||
  meas_value |= (this->pressure_oversampling_ & 0b111) << 2;
 | 
			
		||||
  meas_value |= 0b01;  // Forced mode
 | 
			
		||||
  if (!this->bmp_write_byte(BMP280_REGISTER_CONTROL, meas_value)) {
 | 
			
		||||
  if (!this->write_byte(BMP280_REGISTER_CONTROL, meas_value)) {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
@@ -172,13 +172,13 @@ void BMP280Component::update() {
 | 
			
		||||
    int32_t t_fine = 0;
 | 
			
		||||
    float temperature = this->read_temperature_(&t_fine);
 | 
			
		||||
    if (std::isnan(temperature)) {
 | 
			
		||||
      ESP_LOGW(TAG, "Invalid temperature");
 | 
			
		||||
      ESP_LOGW(TAG, "Invalid temperature, cannot read pressure values.");
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    float pressure = this->read_pressure_(t_fine);
 | 
			
		||||
 | 
			
		||||
    ESP_LOGV(TAG, "Temperature=%.1f°C Pressure=%.1fhPa", temperature, pressure);
 | 
			
		||||
    ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa", temperature, pressure);
 | 
			
		||||
    if (this->temperature_sensor_ != nullptr)
 | 
			
		||||
      this->temperature_sensor_->publish_state(temperature);
 | 
			
		||||
    if (this->pressure_sensor_ != nullptr)
 | 
			
		||||
@@ -188,10 +188,9 @@ void BMP280Component::update() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float BMP280Component::read_temperature_(int32_t *t_fine) {
 | 
			
		||||
  uint8_t data[3]{};
 | 
			
		||||
  if (!this->bmp_read_bytes(BMP280_REGISTER_TEMPDATA, data, 3))
 | 
			
		||||
  uint8_t data[3];
 | 
			
		||||
  if (!this->read_bytes(BMP280_REGISTER_TEMPDATA, data, 3))
 | 
			
		||||
    return NAN;
 | 
			
		||||
  ESP_LOGV(TAG, "Read temperature data, raw: %02X %02X %02X", data[0], data[1], data[2]);
 | 
			
		||||
  int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF);
 | 
			
		||||
  adc >>= 4;
 | 
			
		||||
  if (adc == 0x80000) {
 | 
			
		||||
@@ -213,7 +212,7 @@ float BMP280Component::read_temperature_(int32_t *t_fine) {
 | 
			
		||||
 | 
			
		||||
float BMP280Component::read_pressure_(int32_t t_fine) {
 | 
			
		||||
  uint8_t data[3];
 | 
			
		||||
  if (!this->bmp_read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3))
 | 
			
		||||
  if (!this->read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3))
 | 
			
		||||
    return NAN;
 | 
			
		||||
  int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF);
 | 
			
		||||
  adc >>= 4;
 | 
			
		||||
@@ -259,12 +258,12 @@ void BMP280Component::set_pressure_oversampling(BMP280Oversampling pressure_over
 | 
			
		||||
void BMP280Component::set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
 | 
			
		||||
uint8_t BMP280Component::read_u8_(uint8_t a_register) {
 | 
			
		||||
  uint8_t data = 0;
 | 
			
		||||
  this->bmp_read_byte(a_register, &data);
 | 
			
		||||
  this->read_byte(a_register, &data);
 | 
			
		||||
  return data;
 | 
			
		||||
}
 | 
			
		||||
uint16_t BMP280Component::read_u16_le_(uint8_t a_register) {
 | 
			
		||||
  uint16_t data = 0;
 | 
			
		||||
  this->bmp_read_byte_16(a_register, &data);
 | 
			
		||||
  this->read_byte_16(a_register, &data);
 | 
			
		||||
  return (data >> 8) | (data << 8);
 | 
			
		||||
}
 | 
			
		||||
int16_t BMP280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); }
 | 
			
		||||
 
 | 
			
		||||
@@ -67,12 +67,12 @@ class BMP280Component : public PollingComponent {
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  virtual bool bmp_read_byte(uint8_t a_register, uint8_t *data) = 0;
 | 
			
		||||
  virtual bool bmp_write_byte(uint8_t a_register, uint8_t data) = 0;
 | 
			
		||||
  virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
 | 
			
		||||
  virtual bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) = 0;
 | 
			
		||||
  virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0;
 | 
			
		||||
  virtual bool write_byte(uint8_t a_register, uint8_t data) = 0;
 | 
			
		||||
  virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
 | 
			
		||||
  virtual bool read_byte_16(uint8_t a_register, uint16_t *data) = 0;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  /// Read the temperature value and store the calculated ambient temperature in t_fine.
 | 
			
		||||
  float read_temperature_(int32_t *t_fine);
 | 
			
		||||
  /// Read the pressure value in hPa using the provided t_fine value.
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,19 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bmp280_i2c {
 | 
			
		||||
 | 
			
		||||
bool BMP280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) {
 | 
			
		||||
  return I2CDevice::read_byte(a_register, data);
 | 
			
		||||
};
 | 
			
		||||
bool BMP280I2CComponent::write_byte(uint8_t a_register, uint8_t data) {
 | 
			
		||||
  return I2CDevice::write_byte(a_register, data);
 | 
			
		||||
};
 | 
			
		||||
bool BMP280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
 | 
			
		||||
  return I2CDevice::read_bytes(a_register, data, len);
 | 
			
		||||
};
 | 
			
		||||
bool BMP280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) {
 | 
			
		||||
  return I2CDevice::read_byte_16(a_register, data);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void BMP280I2CComponent::dump_config() {
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  BMP280Component::dump_config();
 | 
			
		||||
 
 | 
			
		||||
@@ -11,12 +11,10 @@ static const char *const TAG = "bmp280_i2c.sensor";
 | 
			
		||||
/// This class implements support for the BMP280 Temperature+Pressure i2c sensor.
 | 
			
		||||
class BMP280I2CComponent : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  bool bmp_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); }
 | 
			
		||||
  bool bmp_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); }
 | 
			
		||||
  bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
 | 
			
		||||
    return read_bytes(a_register, data, len);
 | 
			
		||||
  }
 | 
			
		||||
  bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) override { return read_byte_16(a_register, data); }
 | 
			
		||||
  bool read_byte(uint8_t a_register, uint8_t *data) override;
 | 
			
		||||
  bool write_byte(uint8_t a_register, uint8_t data) override;
 | 
			
		||||
  bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
 | 
			
		||||
  bool read_byte_16(uint8_t a_register, uint16_t *data) override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ void BMP280SPIComponent::setup() {
 | 
			
		||||
// 0x77 is transferred, for read access, the byte 0xF7 is transferred.
 | 
			
		||||
// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp280-ds001.pdf
 | 
			
		||||
 | 
			
		||||
bool BMP280SPIComponent::bmp_read_byte(uint8_t a_register, uint8_t *data) {
 | 
			
		||||
bool BMP280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) {
 | 
			
		||||
  this->enable();
 | 
			
		||||
  this->transfer_byte(set_bit(a_register, 7));
 | 
			
		||||
  *data = this->transfer_byte(0);
 | 
			
		||||
@@ -36,7 +36,7 @@ bool BMP280SPIComponent::bmp_read_byte(uint8_t a_register, uint8_t *data) {
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BMP280SPIComponent::bmp_write_byte(uint8_t a_register, uint8_t data) {
 | 
			
		||||
bool BMP280SPIComponent::write_byte(uint8_t a_register, uint8_t data) {
 | 
			
		||||
  this->enable();
 | 
			
		||||
  this->transfer_byte(clear_bit(a_register, 7));
 | 
			
		||||
  this->transfer_byte(data);
 | 
			
		||||
@@ -44,7 +44,7 @@ bool BMP280SPIComponent::bmp_write_byte(uint8_t a_register, uint8_t data) {
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BMP280SPIComponent::bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
 | 
			
		||||
bool BMP280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
 | 
			
		||||
  this->enable();
 | 
			
		||||
  this->transfer_byte(set_bit(a_register, 7));
 | 
			
		||||
  this->read_array(data, len);
 | 
			
		||||
@@ -52,7 +52,7 @@ bool BMP280SPIComponent::bmp_read_bytes(uint8_t a_register, uint8_t *data, size_
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BMP280SPIComponent::bmp_read_byte_16(uint8_t a_register, uint16_t *data) {
 | 
			
		||||
bool BMP280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) {
 | 
			
		||||
  this->enable();
 | 
			
		||||
  this->transfer_byte(set_bit(a_register, 7));
 | 
			
		||||
  ((uint8_t *) data)[1] = this->transfer_byte(0);
 | 
			
		||||
 
 | 
			
		||||
@@ -10,10 +10,10 @@ class BMP280SPIComponent : public esphome::bmp280_base::BMP280Component,
 | 
			
		||||
                           public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
 | 
			
		||||
                                                 spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_200KHZ> {
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  bool bmp_read_byte(uint8_t a_register, uint8_t *data) override;
 | 
			
		||||
  bool bmp_write_byte(uint8_t a_register, uint8_t data) override;
 | 
			
		||||
  bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
 | 
			
		||||
  bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) override;
 | 
			
		||||
  bool read_byte(uint8_t a_register, uint8_t *data) override;
 | 
			
		||||
  bool write_byte(uint8_t a_register, uint8_t data) override;
 | 
			
		||||
  bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
 | 
			
		||||
  bool read_byte_16(uint8_t a_register, uint16_t *data) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace bmp280_spi
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_RESTART,
 | 
			
		||||
    DEVICE_CLASS_UPDATE,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, 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(CoroPriority.CORE)
 | 
			
		||||
@coroutine_with_priority(100.0)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add_global(button_ns.using)
 | 
			
		||||
 
 | 
			
		||||
@@ -6,19 +6,6 @@ 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,10 +7,13 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace button {
 | 
			
		||||
 | 
			
		||||
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 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()); \
 | 
			
		||||
    } \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#define SUB_BUTTON(name) \
 | 
			
		||||
 protected: \
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
#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,26 +15,6 @@ 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.
 | 
			
		||||
@@ -63,29 +43,6 @@ 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.
 | 
			
		||||
 
 | 
			
		||||
@@ -1,69 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,62 +0,0 @@
 | 
			
		||||
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]))
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,82 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,39 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user