mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-03 00:21:56 +00:00 
			
		
		
		
	Compare commits
	
		
			140 Commits
		
	
	
		
			2023.6.2
			...
			2023.7.0b3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					362a19c2e1 | ||
| 
						 | 
					f4a4956dd4 | ||
| 
						 | 
					746488cabf | ||
| 
						 | 
					4449248c6f | ||
| 
						 | 
					036e14ab7f | ||
| 
						 | 
					f840eee1b7 | ||
| 
						 | 
					553132443f | ||
| 
						 | 
					d20242f589 | ||
| 
						 | 
					68affce274 | ||
| 
						 | 
					c4b9065749 | ||
| 
						 | 
					d57a5d1793 | ||
| 
						 | 
					74e062fdb3 | ||
| 
						 | 
					6bdc0c92fe | ||
| 
						 | 
					d7945de001 | ||
| 
						 | 
					3ba2a29e54 | ||
| 
						 | 
					76b438f79c | ||
| 
						 | 
					bc14f06a07 | ||
| 
						 | 
					844cf316e2 | ||
| 
						 | 
					9344d85414 | ||
| 
						 | 
					a539197bc4 | ||
| 
						 | 
					eb859e83f8 | ||
| 
						 | 
					e4a640844c | ||
| 
						 | 
					119bbba254 | ||
| 
						 | 
					8c5978599a | ||
| 
						 | 
					bbf3d382e8 | ||
| 
						 | 
					c85f70a236 | ||
| 
						 | 
					7e52d4f5d6 | ||
| 
						 | 
					6d9dbf9e54 | ||
| 
						 | 
					ec37dece12 | ||
| 
						 | 
					e0fd8cd850 | ||
| 
						 | 
					cf65bd8ad7 | ||
| 
						 | 
					8a9352939a | ||
| 
						 | 
					6ecc1c14d2 | ||
| 
						 | 
					5f531ac9b0 | ||
| 
						 | 
					7a551081ee | ||
| 
						 | 
					74139985c9 | ||
| 
						 | 
					f3cdcc008a | ||
| 
						 | 
					a391815921 | ||
| 
						 | 
					98fd092053 | ||
| 
						 | 
					feee075122 | ||
| 
						 | 
					ddde1ee31e | ||
| 
						 | 
					c5aacdd682 | ||
| 
						 | 
					a77cf1beec | ||
| 
						 | 
					d7bfdd0efc | ||
| 
						 | 
					62aee36f82 | ||
| 
						 | 
					8ca9115dc8 | ||
| 
						 | 
					8bf8892ab3 | ||
| 
						 | 
					8739552c0b | ||
| 
						 | 
					e6834f25ed | ||
| 
						 | 
					f9fc438de8 | ||
| 
						 | 
					677b2c6618 | ||
| 
						 | 
					301a78f983 | ||
| 
						 | 
					979f014799 | ||
| 
						 | 
					a326dcaf0e | ||
| 
						 | 
					5bf2fa5c56 | ||
| 
						 | 
					fe0404a084 | ||
| 
						 | 
					22a1134f0e | ||
| 
						 | 
					fc3d558d47 | ||
| 
						 | 
					45c72f1f22 | ||
| 
						 | 
					fd9cca565b | ||
| 
						 | 
					0709367587 | ||
| 
						 | 
					98277f6ceb | ||
| 
						 | 
					8dd509ba53 | ||
| 
						 | 
					8df455f55b | ||
| 
						 | 
					36782f13bf | ||
| 
						 | 
					e823067a6b | ||
| 
						 | 
					c3ef12d580 | ||
| 
						 | 
					d64d1650e3 | ||
| 
						 | 
					a74abb8ea8 | ||
| 
						 | 
					e74ab00b3e | ||
| 
						 | 
					2e2ac53071 | ||
| 
						 | 
					87c0f48095 | ||
| 
						 | 
					25b9bde0a5 | ||
| 
						 | 
					63d3a0e8b3 | ||
| 
						 | 
					4cc0f3fd53 | ||
| 
						 | 
					5b2176562b | ||
| 
						 | 
					099dc8d1d2 | ||
| 
						 | 
					cf98c497d5 | ||
| 
						 | 
					c5eb3941b9 | ||
| 
						 | 
					0e93b8ee0d | ||
| 
						 | 
					807621402d | ||
| 
						 | 
					321155eb40 | ||
| 
						 | 
					d34c074b92 | ||
| 
						 | 
					abc8e903c1 | ||
| 
						 | 
					832ba38f1b | ||
| 
						 | 
					70de2f5278 | ||
| 
						 | 
					604d4eec79 | ||
| 
						 | 
					ac5246e21d | ||
| 
						 | 
					951157dc26 | ||
| 
						 | 
					68119ddcd4 | ||
| 
						 | 
					c82be2cd60 | ||
| 
						 | 
					9a149a7aba | ||
| 
						 | 
					108fabe18f | ||
| 
						 | 
					8ce98dd15a | ||
| 
						 | 
					9d21cccac1 | ||
| 
						 | 
					8f4abf6a63 | ||
| 
						 | 
					bd9a4ff8de | ||
| 
						 | 
					d9398a91d1 | ||
| 
						 | 
					ef84937fd6 | ||
| 
						 | 
					2a2d20a7fc | ||
| 
						 | 
					8a1c49a4ae | ||
| 
						 | 
					eb145757e5 | ||
| 
						 | 
					fc0e1a3cb9 | ||
| 
						 | 
					ec3d5fc427 | ||
| 
						 | 
					85608a8ab7 | ||
| 
						 | 
					52d7d2cae7 | ||
| 
						 | 
					f72b07eb0e | ||
| 
						 | 
					314c1c8b5c | ||
| 
						 | 
					211453df43 | ||
| 
						 | 
					1cc7428445 | ||
| 
						 | 
					e8ce7048d8 | ||
| 
						 | 
					de6c527ca4 | ||
| 
						 | 
					9e7e3708e3 | ||
| 
						 | 
					8bd9f50659 | ||
| 
						 | 
					cb5a01da29 | ||
| 
						 | 
					bfe85dd710 | ||
| 
						 | 
					24067312f6 | ||
| 
						 | 
					ee12c68b8f | ||
| 
						 | 
					b2ccd32cd7 | ||
| 
						 | 
					7ceb16cc5a | ||
| 
						 | 
					5a8b7c17da | ||
| 
						 | 
					41a618737b | ||
| 
						 | 
					67771abc9d | ||
| 
						 | 
					c151df32bc | ||
| 
						 | 
					b346ad8080 | ||
| 
						 | 
					cd57271386 | ||
| 
						 | 
					b9f20b36cb | ||
| 
						 | 
					62d2640c37 | ||
| 
						 | 
					54eb52c19a | ||
| 
						 | 
					77a7d3f24b | ||
| 
						 | 
					8c9d63f48f | ||
| 
						 | 
					5a8e93ed0a | ||
| 
						 | 
					d4099d68a7 | ||
| 
						 | 
					e1b0d86098 | ||
| 
						 | 
					1a7f121ac6 | ||
| 
						 | 
					ffa669899a | ||
| 
						 | 
					17fed954bf | ||
| 
						 | 
					467e42d8aa | ||
| 
						 | 
					a023f24a08 | ||
| 
						 | 
					27f69f5439 | 
							
								
								
									
										38
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
name: Restore Python
 | 
			
		||||
inputs:
 | 
			
		||||
  python-version:
 | 
			
		||||
    description: Python version to restore
 | 
			
		||||
    required: true
 | 
			
		||||
    type: string
 | 
			
		||||
  cache-key:
 | 
			
		||||
    description: Cache key to use
 | 
			
		||||
    required: true
 | 
			
		||||
    type: string
 | 
			
		||||
outputs:
 | 
			
		||||
  python-version:
 | 
			
		||||
    description: Python version restored
 | 
			
		||||
    value: ${{ steps.python.outputs.python-version }}
 | 
			
		||||
runs:
 | 
			
		||||
  using: "composite"
 | 
			
		||||
  steps:
 | 
			
		||||
    - name: Set up Python ${{ inputs.python-version }}
 | 
			
		||||
      id: python
 | 
			
		||||
      uses: actions/setup-python@v4.6.0
 | 
			
		||||
      with:
 | 
			
		||||
        python-version: ${{ inputs.python-version }}
 | 
			
		||||
    - name: Restore Python virtual environment
 | 
			
		||||
      id: cache-venv
 | 
			
		||||
      uses: actions/cache/restore@v3.3.1
 | 
			
		||||
      with:
 | 
			
		||||
        path: venv
 | 
			
		||||
        # yamllint disable-line rule:line-length
 | 
			
		||||
        key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }}
 | 
			
		||||
    - name: Create Python virtual environment
 | 
			
		||||
      if: steps.cache-venv.outputs.cache-hit != 'true'
 | 
			
		||||
      shell: bash
 | 
			
		||||
      run: |
 | 
			
		||||
        python -m venv venv
 | 
			
		||||
        . venv/bin/activate
 | 
			
		||||
        python --version
 | 
			
		||||
        pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
 | 
			
		||||
        pip install -e .
 | 
			
		||||
							
								
								
									
										96
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										96
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -26,10 +26,16 @@ jobs:
 | 
			
		||||
  common:
 | 
			
		||||
    name: Create common environment
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    outputs:
 | 
			
		||||
      cache-key: ${{ steps.cache-key.outputs.key }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v3.5.2
 | 
			
		||||
      - name: Generate cache-key
 | 
			
		||||
        id: cache-key
 | 
			
		||||
        run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
 | 
			
		||||
      - name: Set up Python ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
        id: python
 | 
			
		||||
        uses: actions/setup-python@v4.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
@@ -39,7 +45,7 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
          key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
 | 
			
		||||
          key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ steps.cache-key.outputs.key }}
 | 
			
		||||
      - name: Create Python virtual environment
 | 
			
		||||
        if: steps.cache-venv.outputs.cache-hit != 'true'
 | 
			
		||||
        run: |
 | 
			
		||||
@@ -66,12 +72,11 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v3.5.2
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
        uses: actions/cache/restore@v3.3.1
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
          key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Run black
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
@@ -88,12 +93,11 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v3.5.2
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
        uses: actions/cache/restore@v3.3.1
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
          key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Run flake8
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
@@ -110,12 +114,11 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v3.5.2
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
        uses: actions/cache/restore@v3.3.1
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
          key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Run pylint
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
@@ -132,12 +135,11 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v3.5.2
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
        uses: actions/cache/restore@v3.3.1
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
          key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Run pyupgrade
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
@@ -154,12 +156,11 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v3.5.2
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
        uses: actions/cache/restore@v3.3.1
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
          key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Register matcher
 | 
			
		||||
        run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json"
 | 
			
		||||
      - name: Run script/ci-custom
 | 
			
		||||
@@ -176,12 +177,11 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v3.5.2
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
        uses: actions/cache/restore@v3.3.1
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
          key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Register matcher
 | 
			
		||||
        run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
 | 
			
		||||
      - name: Run pytest
 | 
			
		||||
@@ -197,12 +197,11 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v3.5.2
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
        uses: actions/cache/restore@v3.3.1
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
          key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Install clang-format
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
@@ -237,18 +236,11 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v3.5.2
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
        uses: actions/cache/restore@v3.3.1
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
          key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        uses: actions/cache@v3.3.1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
          key: platformio-test${{ matrix.file }}-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Run esphome compile tests/test${{ matrix.file }}.yaml
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
@@ -300,13 +292,11 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v3.5.2
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
        uses: actions/cache/restore@v3.3.1
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
          key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}
 | 
			
		||||
        # Use per check platformio cache because checks use different parts
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        uses: actions/cache@v3.3.1
 | 
			
		||||
        with:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							@@ -6,14 +6,12 @@ on:
 | 
			
		||||
  schedule:
 | 
			
		||||
    - cron: '45 6 * * *'
 | 
			
		||||
 | 
			
		||||
permissions:
 | 
			
		||||
  contents: write
 | 
			
		||||
  pull-requests: write
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  sync:
 | 
			
		||||
    name: Sync Device Classes
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v3
 | 
			
		||||
@@ -38,15 +36,6 @@ jobs:
 | 
			
		||||
        run: |
 | 
			
		||||
          python ./script/sync-device_class.py
 | 
			
		||||
 | 
			
		||||
      - name: Get PR template
 | 
			
		||||
        id: pr-template-body
 | 
			
		||||
        run: |
 | 
			
		||||
          body=$(cat .github/PULL_REQUEST_TEMPLATE.md)
 | 
			
		||||
          delimiter="$(openssl rand -hex 8)"
 | 
			
		||||
          echo "body<<$delimiter" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "$body" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "$delimiter" >> $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
      - name: Commit changes
 | 
			
		||||
        uses: peter-evans/create-pull-request@v5
 | 
			
		||||
        with:
 | 
			
		||||
@@ -56,5 +45,5 @@ jobs:
 | 
			
		||||
          branch: sync/device-classes
 | 
			
		||||
          delete-branch: true
 | 
			
		||||
          title: "Synchronise Device Classes from Home Assistant"
 | 
			
		||||
          body: ${{ steps.pr-template-body.outputs.body }}
 | 
			
		||||
          body-path: .github/PULL_REQUEST_TEMPLATE.md
 | 
			
		||||
          token: ${{ secrets.DEVICE_CLASS_SYNC_TOKEN }}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ repos:
 | 
			
		||||
          - --branch=release
 | 
			
		||||
          - --branch=beta
 | 
			
		||||
  - repo: https://github.com/asottile/pyupgrade
 | 
			
		||||
    rev: v3.4.0
 | 
			
		||||
    rev: v3.7.0
 | 
			
		||||
    hooks:
 | 
			
		||||
      - id: pyupgrade
 | 
			
		||||
        args: [--py39-plus]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								CODEOWNERS
									
									
									
									
									
								
							@@ -17,10 +17,11 @@ esphome/components/adc/* @esphome/core
 | 
			
		||||
esphome/components/adc128s102/* @DeerMaximum
 | 
			
		||||
esphome/components/addressable_light/* @justfalter
 | 
			
		||||
esphome/components/airthings_ble/* @jeromelaban
 | 
			
		||||
esphome/components/airthings_wave_base/* @jeromelaban @ncareau
 | 
			
		||||
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
 | 
			
		||||
esphome/components/airthings_wave_mini/* @ncareau
 | 
			
		||||
esphome/components/airthings_wave_plus/* @jeromelaban
 | 
			
		||||
esphome/components/alarm_control_panel/* @grahambrown11
 | 
			
		||||
esphome/components/alpha3/* @jan-hofmeier
 | 
			
		||||
esphome/components/am43/* @buxtronix
 | 
			
		||||
esphome/components/am43/cover/* @buxtronix
 | 
			
		||||
esphome/components/am43/sensor/* @buxtronix
 | 
			
		||||
@@ -31,6 +32,7 @@ esphome/components/api/* @OttoWinter
 | 
			
		||||
esphome/components/as7341/* @mrgnr
 | 
			
		||||
esphome/components/async_tcp/* @OttoWinter
 | 
			
		||||
esphome/components/atc_mithermometer/* @ahpohl
 | 
			
		||||
esphome/components/atm90e26/* @danieltwagner
 | 
			
		||||
esphome/components/b_parasite/* @rbaron
 | 
			
		||||
esphome/components/ballu/* @bazuchan
 | 
			
		||||
esphome/components/bang_bang/* @OttoWinter
 | 
			
		||||
@@ -76,6 +78,7 @@ esphome/components/display_menu_base/* @numo68
 | 
			
		||||
esphome/components/dps310/* @kbx81
 | 
			
		||||
esphome/components/ds1307/* @badbadc0ffee
 | 
			
		||||
esphome/components/dsmr/* @glmnet @zuidwijk
 | 
			
		||||
esphome/components/duty_time/* @dudanov
 | 
			
		||||
esphome/components/ee895/* @Stock-M
 | 
			
		||||
esphome/components/ektf2232/* @jesserockz
 | 
			
		||||
esphome/components/ens210/* @itn3rd77
 | 
			
		||||
@@ -102,8 +105,9 @@ esphome/components/gp8403/* @jesserockz
 | 
			
		||||
esphome/components/gpio/* @esphome/core
 | 
			
		||||
esphome/components/gps/* @coogle
 | 
			
		||||
esphome/components/graph/* @synco
 | 
			
		||||
esphome/components/grove_tb6612fng/* @max246
 | 
			
		||||
esphome/components/growatt_solar/* @leeuwte
 | 
			
		||||
esphome/components/haier/* @Yarikx
 | 
			
		||||
esphome/components/haier/* @paveldn
 | 
			
		||||
esphome/components/havells_solar/* @sourabhjaiswal
 | 
			
		||||
esphome/components/hbridge/fan/* @WeekendWarrior
 | 
			
		||||
esphome/components/hbridge/light/* @DotNetDann
 | 
			
		||||
@@ -200,6 +204,7 @@ esphome/components/output/* @esphome/core
 | 
			
		||||
esphome/components/pca6416a/* @Mat931
 | 
			
		||||
esphome/components/pca9554/* @hwstar
 | 
			
		||||
esphome/components/pcf85063/* @brogon
 | 
			
		||||
esphome/components/pcf8563/* @KoenBreeman
 | 
			
		||||
esphome/components/pid/* @OttoWinter
 | 
			
		||||
esphome/components/pipsolar/* @andreashergert1984
 | 
			
		||||
esphome/components/pm1006/* @habbie
 | 
			
		||||
@@ -294,6 +299,7 @@ esphome/components/tof10120/* @wstrzalka
 | 
			
		||||
esphome/components/toshiba/* @kbx81
 | 
			
		||||
esphome/components/touchscreen/* @jesserockz
 | 
			
		||||
esphome/components/tsl2591/* @wjcarpenter
 | 
			
		||||
esphome/components/tt21100/* @kroimon
 | 
			
		||||
esphome/components/tuya/binary_sensor/* @jesserockz
 | 
			
		||||
esphome/components/tuya/climate/* @jesserockz
 | 
			
		||||
esphome/components/tuya/number/* @frankiboy1
 | 
			
		||||
@@ -310,6 +316,7 @@ esphome/components/version/* @esphome/core
 | 
			
		||||
esphome/components/voice_assistant/* @jesserockz
 | 
			
		||||
esphome/components/wake_on_lan/* @willwill2will54
 | 
			
		||||
esphome/components/web_server_base/* @OttoWinter
 | 
			
		||||
esphome/components/web_server_idf/* @dentra
 | 
			
		||||
esphome/components/whirlpool/* @glmnet
 | 
			
		||||
esphome/components/whynter/* @aeonsablaze
 | 
			
		||||
esphome/components/wiegand/* @ssieb
 | 
			
		||||
@@ -319,4 +326,6 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl
 | 
			
		||||
esphome/components/xiaomi_mhoc303/* @drug123
 | 
			
		||||
esphome/components/xiaomi_mhoc401/* @vevsvevs
 | 
			
		||||
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
 | 
			
		||||
esphome/components/xl9535/* @mreditor97
 | 
			
		||||
esphome/components/xpt2046/* @nielsnl68 @numo68
 | 
			
		||||
esphome/components/zio_ultrasonic/* @kahrendt
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ from esphome.const import (
 | 
			
		||||
    SECRETS_FILES,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, EsphomeError, coroutine
 | 
			
		||||
from esphome.helpers import indent
 | 
			
		||||
from esphome.helpers import indent, is_ip_address
 | 
			
		||||
from esphome.util import (
 | 
			
		||||
    run_external_command,
 | 
			
		||||
    run_external_process,
 | 
			
		||||
@@ -308,8 +308,10 @@ def upload_program(config, args, host):
 | 
			
		||||
    password = ota_conf.get(CONF_PASSWORD, "")
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED]
 | 
			
		||||
    ) and CONF_MQTT in config:
 | 
			
		||||
        not is_ip_address(CORE.address)
 | 
			
		||||
        and (get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED])
 | 
			
		||||
        and CONF_MQTT in config
 | 
			
		||||
    ):
 | 
			
		||||
        from esphome import mqtt
 | 
			
		||||
 | 
			
		||||
        host = mqtt.get_esphome_device_ip(
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ ATTENUATION_MODES = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
adc1_channel_t = cg.global_ns.enum("adc1_channel_t")
 | 
			
		||||
adc2_channel_t = cg.global_ns.enum("adc2_channel_t")
 | 
			
		||||
 | 
			
		||||
# From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h
 | 
			
		||||
# pin to adc1 channel mapping
 | 
			
		||||
@@ -78,6 +79,49 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
 | 
			
		||||
    # TODO: add other variants
 | 
			
		||||
    VARIANT_ESP32: {
 | 
			
		||||
        4: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
        0: adc2_channel_t.ADC2_CHANNEL_1,
 | 
			
		||||
        2: adc2_channel_t.ADC2_CHANNEL_2,
 | 
			
		||||
        15: adc2_channel_t.ADC2_CHANNEL_3,
 | 
			
		||||
        13: adc2_channel_t.ADC2_CHANNEL_4,
 | 
			
		||||
        12: adc2_channel_t.ADC2_CHANNEL_5,
 | 
			
		||||
        14: adc2_channel_t.ADC2_CHANNEL_6,
 | 
			
		||||
        27: adc2_channel_t.ADC2_CHANNEL_7,
 | 
			
		||||
        25: adc2_channel_t.ADC2_CHANNEL_8,
 | 
			
		||||
        26: adc2_channel_t.ADC2_CHANNEL_9,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32S2: {
 | 
			
		||||
        11: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
        12: adc2_channel_t.ADC2_CHANNEL_1,
 | 
			
		||||
        13: adc2_channel_t.ADC2_CHANNEL_2,
 | 
			
		||||
        14: adc2_channel_t.ADC2_CHANNEL_3,
 | 
			
		||||
        15: adc2_channel_t.ADC2_CHANNEL_4,
 | 
			
		||||
        16: adc2_channel_t.ADC2_CHANNEL_5,
 | 
			
		||||
        17: adc2_channel_t.ADC2_CHANNEL_6,
 | 
			
		||||
        18: adc2_channel_t.ADC2_CHANNEL_7,
 | 
			
		||||
        19: adc2_channel_t.ADC2_CHANNEL_8,
 | 
			
		||||
        20: adc2_channel_t.ADC2_CHANNEL_9,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32S3: {
 | 
			
		||||
        11: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
        12: adc2_channel_t.ADC2_CHANNEL_1,
 | 
			
		||||
        13: adc2_channel_t.ADC2_CHANNEL_2,
 | 
			
		||||
        14: adc2_channel_t.ADC2_CHANNEL_3,
 | 
			
		||||
        15: adc2_channel_t.ADC2_CHANNEL_4,
 | 
			
		||||
        16: adc2_channel_t.ADC2_CHANNEL_5,
 | 
			
		||||
        17: adc2_channel_t.ADC2_CHANNEL_6,
 | 
			
		||||
        18: adc2_channel_t.ADC2_CHANNEL_7,
 | 
			
		||||
        19: adc2_channel_t.ADC2_CHANNEL_8,
 | 
			
		||||
        20: adc2_channel_t.ADC2_CHANNEL_9,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32C3: {
 | 
			
		||||
        5: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_adc_pin(value):
 | 
			
		||||
    if str(value).upper() == "VCC":
 | 
			
		||||
@@ -89,11 +133,18 @@ def validate_adc_pin(value):
 | 
			
		||||
    if CORE.is_esp32:
 | 
			
		||||
        value = pins.internal_gpio_input_pin_number(value)
 | 
			
		||||
        variant = get_esp32_variant()
 | 
			
		||||
        if variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL:
 | 
			
		||||
        if (
 | 
			
		||||
            variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL
 | 
			
		||||
            and variant not in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL
 | 
			
		||||
        ):
 | 
			
		||||
            raise cv.Invalid(f"This ESP32 variant ({variant}) is not supported")
 | 
			
		||||
 | 
			
		||||
        if value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]:
 | 
			
		||||
        if (
 | 
			
		||||
            value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]
 | 
			
		||||
            and value not in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
 | 
			
		||||
        ):
 | 
			
		||||
            raise cv.Invalid(f"{variant} doesn't support ADC on this pin")
 | 
			
		||||
 | 
			
		||||
        return pins.internal_gpio_input_pin_schema(value)
 | 
			
		||||
 | 
			
		||||
    if CORE.is_esp8266:
 | 
			
		||||
@@ -104,7 +155,7 @@ def validate_adc_pin(value):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if value != 17:  # A0
 | 
			
		||||
            raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.")
 | 
			
		||||
            raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC")
 | 
			
		||||
        return pins.gpio_pin_schema(
 | 
			
		||||
            {CONF_ANALOG: True, CONF_INPUT: True}, internal=True
 | 
			
		||||
        )(value)
 | 
			
		||||
@@ -112,7 +163,7 @@ def validate_adc_pin(value):
 | 
			
		||||
    if CORE.is_rp2040:
 | 
			
		||||
        value = pins.internal_gpio_input_pin_number(value)
 | 
			
		||||
        if value not in (26, 27, 28, 29):
 | 
			
		||||
            raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC.")
 | 
			
		||||
            raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC")
 | 
			
		||||
        return pins.internal_gpio_input_pin_schema(value)
 | 
			
		||||
 | 
			
		||||
    raise NotImplementedError
 | 
			
		||||
 
 | 
			
		||||
@@ -20,20 +20,20 @@ namespace adc {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "adc";
 | 
			
		||||
 | 
			
		||||
// 13bit for S2, and 12bit for all other esp32 variants
 | 
			
		||||
// 13-bit for S2, 12-bit for all other ESP32 variants
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1);
 | 
			
		||||
 | 
			
		||||
#ifndef SOC_ADC_RTC_MAX_BITWIDTH
 | 
			
		||||
#if USE_ESP32_VARIANT_ESP32S2
 | 
			
		||||
static const int SOC_ADC_RTC_MAX_BITWIDTH = 13;
 | 
			
		||||
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13;
 | 
			
		||||
#else
 | 
			
		||||
static const int SOC_ADC_RTC_MAX_BITWIDTH = 12;
 | 
			
		||||
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12;
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1;    // 4095 (12 bit) or 8191 (13 bit)
 | 
			
		||||
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1;  // 2048 (12 bit) or 4096 (13 bit)
 | 
			
		||||
static const int32_t ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1;    // 4095 (12 bit) or 8191 (13 bit)
 | 
			
		||||
static const int32_t ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1;  // 2048 (12 bit) or 4096 (13 bit)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_RP2040
 | 
			
		||||
@@ -47,14 +47,21 @@ extern "C"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
  adc1_config_width(ADC_WIDTH_MAX_SOC_BITS);
 | 
			
		||||
  if (!autorange_) {
 | 
			
		||||
    adc1_config_channel_atten(channel_, attenuation_);
 | 
			
		||||
  if (channel1_ != ADC1_CHANNEL_MAX) {
 | 
			
		||||
    adc1_config_width(ADC_WIDTH_MAX_SOC_BITS);
 | 
			
		||||
    if (!autorange_) {
 | 
			
		||||
      adc1_config_channel_atten(channel1_, attenuation_);
 | 
			
		||||
    }
 | 
			
		||||
  } else if (channel2_ != ADC2_CHANNEL_MAX) {
 | 
			
		||||
    if (!autorange_) {
 | 
			
		||||
      adc2_config_channel_atten(channel2_, attenuation_);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // load characteristics for each attenuation
 | 
			
		||||
  for (int i = 0; i < (int) ADC_ATTEN_MAX; i++) {
 | 
			
		||||
    auto cal_value = esp_adc_cal_characterize(ADC_UNIT_1, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS,
 | 
			
		||||
  for (int32_t i = 0; i < (int32_t) ADC_ATTEN_MAX; i++) {
 | 
			
		||||
    auto adc_unit = channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2;
 | 
			
		||||
    auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS,
 | 
			
		||||
                                              1100,  // default vref
 | 
			
		||||
                                              &cal_characteristics_[i]);
 | 
			
		||||
    switch (cal_value) {
 | 
			
		||||
@@ -136,9 +143,9 @@ void ADCSensor::update() {
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
float ADCSensor::sample() {
 | 
			
		||||
#ifdef USE_ADC_SENSOR_VCC
 | 
			
		||||
  int raw = ESP.getVcc();  // NOLINT(readability-static-accessed-through-instance)
 | 
			
		||||
  int32_t raw = ESP.getVcc();  // NOLINT(readability-static-accessed-through-instance)
 | 
			
		||||
#else
 | 
			
		||||
  int raw = analogRead(this->pin_->get_pin());  // NOLINT
 | 
			
		||||
  int32_t raw = analogRead(this->pin_->get_pin());  // NOLINT
 | 
			
		||||
#endif
 | 
			
		||||
  if (output_raw_) {
 | 
			
		||||
    return raw;
 | 
			
		||||
@@ -150,29 +157,53 @@ float ADCSensor::sample() {
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
float ADCSensor::sample() {
 | 
			
		||||
  if (!autorange_) {
 | 
			
		||||
    int raw = adc1_get_raw(channel_);
 | 
			
		||||
    int32_t raw = -1;
 | 
			
		||||
    if (channel1_ != ADC1_CHANNEL_MAX) {
 | 
			
		||||
      raw = adc1_get_raw(channel1_);
 | 
			
		||||
    } else if (channel2_ != ADC2_CHANNEL_MAX) {
 | 
			
		||||
      adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (raw == -1) {
 | 
			
		||||
      return NAN;
 | 
			
		||||
    }
 | 
			
		||||
    if (output_raw_) {
 | 
			
		||||
      return raw;
 | 
			
		||||
    }
 | 
			
		||||
    uint32_t mv = esp_adc_cal_raw_to_voltage(raw, &cal_characteristics_[(int) attenuation_]);
 | 
			
		||||
    uint32_t mv = esp_adc_cal_raw_to_voltage(raw, &cal_characteristics_[(int32_t) attenuation_]);
 | 
			
		||||
    return mv / 1000.0f;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int raw11, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
 | 
			
		||||
  adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11);
 | 
			
		||||
  raw11 = adc1_get_raw(channel_);
 | 
			
		||||
  if (raw11 < ADC_MAX) {
 | 
			
		||||
    adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6);
 | 
			
		||||
    raw6 = adc1_get_raw(channel_);
 | 
			
		||||
    if (raw6 < ADC_MAX) {
 | 
			
		||||
      adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5);
 | 
			
		||||
      raw2 = adc1_get_raw(channel_);
 | 
			
		||||
      if (raw2 < ADC_MAX) {
 | 
			
		||||
        adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0);
 | 
			
		||||
        raw0 = adc1_get_raw(channel_);
 | 
			
		||||
  int32_t raw11 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
 | 
			
		||||
 | 
			
		||||
  if (channel1_ != ADC1_CHANNEL_MAX) {
 | 
			
		||||
    adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_11);
 | 
			
		||||
    raw11 = adc1_get_raw(channel1_);
 | 
			
		||||
    if (raw11 < ADC_MAX) {
 | 
			
		||||
      adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_6);
 | 
			
		||||
      raw6 = adc1_get_raw(channel1_);
 | 
			
		||||
      if (raw6 < ADC_MAX) {
 | 
			
		||||
        adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_2_5);
 | 
			
		||||
        raw2 = adc1_get_raw(channel1_);
 | 
			
		||||
        if (raw2 < ADC_MAX) {
 | 
			
		||||
          adc1_config_channel_atten(channel1_, ADC_ATTEN_DB_0);
 | 
			
		||||
          raw0 = adc1_get_raw(channel1_);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  } else if (channel2_ != ADC2_CHANNEL_MAX) {
 | 
			
		||||
    adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_11);
 | 
			
		||||
    adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw11);
 | 
			
		||||
    if (raw11 < ADC_MAX) {
 | 
			
		||||
      adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_6);
 | 
			
		||||
      adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw6);
 | 
			
		||||
      if (raw6 < ADC_MAX) {
 | 
			
		||||
        adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_2_5);
 | 
			
		||||
        adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2);
 | 
			
		||||
        if (raw2 < ADC_MAX) {
 | 
			
		||||
          adc2_config_channel_atten(channel2_, ADC_ATTEN_DB_0);
 | 
			
		||||
          adc2_get_raw(channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -181,10 +212,10 @@ float ADCSensor::sample() {
 | 
			
		||||
    return NAN;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint32_t mv11 = esp_adc_cal_raw_to_voltage(raw11, &cal_characteristics_[(int) ADC_ATTEN_DB_11]);
 | 
			
		||||
  uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &cal_characteristics_[(int) ADC_ATTEN_DB_6]);
 | 
			
		||||
  uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int) ADC_ATTEN_DB_2_5]);
 | 
			
		||||
  uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int) ADC_ATTEN_DB_0]);
 | 
			
		||||
  uint32_t mv11 = esp_adc_cal_raw_to_voltage(raw11, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_11]);
 | 
			
		||||
  uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]);
 | 
			
		||||
  uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]);
 | 
			
		||||
  uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]);
 | 
			
		||||
 | 
			
		||||
  // Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC)
 | 
			
		||||
  uint32_t c11 = std::min(raw11, ADC_HALF);
 | 
			
		||||
@@ -212,7 +243,7 @@ float ADCSensor::sample() {
 | 
			
		||||
    adc_select_input(pin - 26);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int raw = adc_read();
 | 
			
		||||
  int32_t raw = adc_read();
 | 
			
		||||
  if (this->is_temperature_) {
 | 
			
		||||
    adc_set_temp_sensor_enabled(false);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -19,16 +19,23 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
  /// Set the attenuation for this pin. Only available on the ESP32.
 | 
			
		||||
  void set_attenuation(adc_atten_t attenuation) { attenuation_ = attenuation; }
 | 
			
		||||
  void set_channel(adc1_channel_t channel) { channel_ = channel; }
 | 
			
		||||
  void set_channel1(adc1_channel_t channel) {
 | 
			
		||||
    channel1_ = channel;
 | 
			
		||||
    channel2_ = ADC2_CHANNEL_MAX;
 | 
			
		||||
  }
 | 
			
		||||
  void set_channel2(adc2_channel_t channel) {
 | 
			
		||||
    channel2_ = channel;
 | 
			
		||||
    channel1_ = ADC1_CHANNEL_MAX;
 | 
			
		||||
  }
 | 
			
		||||
  void set_autorange(bool autorange) { autorange_ = autorange; }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  /// Update adc values.
 | 
			
		||||
  /// Update ADC values
 | 
			
		||||
  void update() override;
 | 
			
		||||
  /// Setup ADc
 | 
			
		||||
  /// Setup ADC
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  /// `HARDWARE_LATE` setup priority.
 | 
			
		||||
  /// `HARDWARE_LATE` setup priority
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
 | 
			
		||||
  void set_output_raw(bool output_raw) { output_raw_ = output_raw; }
 | 
			
		||||
@@ -52,9 +59,10 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
  adc_atten_t attenuation_{ADC_ATTEN_DB_0};
 | 
			
		||||
  adc1_channel_t channel_{};
 | 
			
		||||
  adc1_channel_t channel1_{ADC1_CHANNEL_MAX};
 | 
			
		||||
  adc2_channel_t channel2_{ADC2_CHANNEL_MAX};
 | 
			
		||||
  bool autorange_{false};
 | 
			
		||||
  esp_adc_cal_characteristics_t cal_characteristics_[(int) ADC_ATTEN_MAX] = {};
 | 
			
		||||
  esp_adc_cal_characteristics_t cal_characteristics_[(int32_t) ADC_ATTEN_MAX] = {};
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.components import sensor, voltage_sampler
 | 
			
		||||
from esphome.components.esp32 import get_esp32_variant
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
@@ -8,15 +10,15 @@ from esphome.const import (
 | 
			
		||||
    CONF_NUMBER,
 | 
			
		||||
    CONF_PIN,
 | 
			
		||||
    CONF_RAW,
 | 
			
		||||
    CONF_WIFI,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
from . import (
 | 
			
		||||
    ATTENUATION_MODES,
 | 
			
		||||
    ESP32_VARIANT_ADC1_PIN_TO_CHANNEL,
 | 
			
		||||
    ESP32_VARIANT_ADC2_PIN_TO_CHANNEL,
 | 
			
		||||
    validate_adc_pin,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -25,7 +27,23 @@ AUTO_LOAD = ["voltage_sampler"]
 | 
			
		||||
 | 
			
		||||
def validate_config(config):
 | 
			
		||||
    if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto":
 | 
			
		||||
        raise cv.Invalid("Automatic attenuation cannot be used when raw output is set.")
 | 
			
		||||
        raise cv.Invalid("Automatic attenuation cannot be used when raw output is set")
 | 
			
		||||
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def final_validate_config(config):
 | 
			
		||||
    if CORE.is_esp32:
 | 
			
		||||
        variant = get_esp32_variant()
 | 
			
		||||
        if (
 | 
			
		||||
            CONF_WIFI in fv.full_config.get()
 | 
			
		||||
            and config[CONF_PIN][CONF_NUMBER]
 | 
			
		||||
            in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
 | 
			
		||||
        ):
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                f"{variant} doesn't support ADC on this pin when Wi-Fi is configured"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -55,6 +73,8 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    validate_config,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = final_validate_config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
@@ -81,5 +101,15 @@ async def to_code(config):
 | 
			
		||||
    if CORE.is_esp32:
 | 
			
		||||
        variant = get_esp32_variant()
 | 
			
		||||
        pin_num = config[CONF_PIN][CONF_NUMBER]
 | 
			
		||||
        chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num]
 | 
			
		||||
        cg.add(var.set_channel(chan))
 | 
			
		||||
        if (
 | 
			
		||||
            variant in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL
 | 
			
		||||
            and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]
 | 
			
		||||
        ):
 | 
			
		||||
            chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num]
 | 
			
		||||
            cg.add(var.set_channel1(chan))
 | 
			
		||||
        elif (
 | 
			
		||||
            variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL
 | 
			
		||||
            and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
 | 
			
		||||
        ):
 | 
			
		||||
            chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num]
 | 
			
		||||
            cg.add(var.set_channel2(chan))
 | 
			
		||||
 
 | 
			
		||||
@@ -58,6 +58,6 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    if CONF_LAMBDA in config:
 | 
			
		||||
        lambda_ = await cg.process_lambda(
 | 
			
		||||
            config[CONF_LAMBDA], [(display.DisplayBufferRef, "it")], return_type=cg.void
 | 
			
		||||
            config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(var.set_writer(lambda_))
 | 
			
		||||
 
 | 
			
		||||
@@ -3,26 +3,31 @@ import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, ble_client
 | 
			
		||||
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_PRESSURE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_HECTOPASCAL,
 | 
			
		||||
    CONF_BATTERY_VOLTAGE,
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_TVOC,
 | 
			
		||||
    CONF_PRESSURE,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    CONF_TVOC,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_PRESSURE,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
 | 
			
		||||
    ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_HECTOPASCAL,
 | 
			
		||||
    UNIT_PARTS_PER_BILLION,
 | 
			
		||||
    ICON_RADIATOR,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@ncareau", "@jeromelaban"]
 | 
			
		||||
CODEOWNERS = ["@ncareau", "@jeromelaban", "@kpfleming"]
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["ble_client"]
 | 
			
		||||
 | 
			
		||||
CONF_BATTERY_UPDATE_INTERVAL = "battery_update_interval"
 | 
			
		||||
 | 
			
		||||
airthings_wave_base_ns = cg.esphome_ns.namespace("airthings_wave_base")
 | 
			
		||||
AirthingsWaveBase = airthings_wave_base_ns.class_(
 | 
			
		||||
    "AirthingsWaveBase", cg.PollingComponent, ble_client.BLEClientNode
 | 
			
		||||
@@ -34,9 +39,9 @@ BASE_SCHEMA = (
 | 
			
		||||
        {
 | 
			
		||||
            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
@@ -52,11 +57,21 @@ BASE_SCHEMA = (
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_TVOC): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PARTS_PER_BILLION,
 | 
			
		||||
                icon=ICON_RADIATOR,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
                accuracy_decimals=3,
 | 
			
		||||
                device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
                CONF_BATTERY_UPDATE_INTERVAL,
 | 
			
		||||
                default="24h",
 | 
			
		||||
            ): cv.update_interval,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("5min"))
 | 
			
		||||
@@ -69,15 +84,20 @@ async def wave_base_to_code(var, config):
 | 
			
		||||
 | 
			
		||||
    await ble_client.register_ble_node(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_HUMIDITY in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_HUMIDITY])
 | 
			
		||||
    if config_humidity := config.get(CONF_HUMIDITY):
 | 
			
		||||
        sens = await sensor.new_sensor(config_humidity)
 | 
			
		||||
        cg.add(var.set_humidity(sens))
 | 
			
		||||
    if CONF_TEMPERATURE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
 | 
			
		||||
    if config_temperature := config.get(CONF_TEMPERATURE):
 | 
			
		||||
        sens = await sensor.new_sensor(config_temperature)
 | 
			
		||||
        cg.add(var.set_temperature(sens))
 | 
			
		||||
    if CONF_PRESSURE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_PRESSURE])
 | 
			
		||||
    if config_pressure := config.get(CONF_PRESSURE):
 | 
			
		||||
        sens = await sensor.new_sensor(config_pressure)
 | 
			
		||||
        cg.add(var.set_pressure(sens))
 | 
			
		||||
    if CONF_TVOC in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_TVOC])
 | 
			
		||||
    if config_tvoc := config.get(CONF_TVOC):
 | 
			
		||||
        sens = await sensor.new_sensor(config_tvoc)
 | 
			
		||||
        cg.add(var.set_tvoc(sens))
 | 
			
		||||
    if config_battery_voltage := config.get(CONF_BATTERY_VOLTAGE):
 | 
			
		||||
        sens = await sensor.new_sensor(config_battery_voltage)
 | 
			
		||||
        cg.add(var.set_battery_voltage(sens))
 | 
			
		||||
    if config_battery_update_interval := config.get(CONF_BATTERY_UPDATE_INTERVAL):
 | 
			
		||||
        cg.add(var.set_battery_update_interval(config_battery_update_interval))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,8 @@
 | 
			
		||||
#include "airthings_wave_base.h"
 | 
			
		||||
 | 
			
		||||
// All information related to reading battery information came from the sensors.airthings_wave
 | 
			
		||||
// project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave)
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -18,22 +21,26 @@ void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
      this->handle_ = 0;
 | 
			
		||||
      this->acp_handle_ = 0;
 | 
			
		||||
      this->cccd_handle_ = 0;
 | 
			
		||||
      ESP_LOGW(TAG, "Disconnected!");
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
      this->handle_ = 0;
 | 
			
		||||
      auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_);
 | 
			
		||||
      if (chr == nullptr) {
 | 
			
		||||
        ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(),
 | 
			
		||||
                 this->sensors_data_characteristic_uuid_.to_string().c_str());
 | 
			
		||||
        break;
 | 
			
		||||
      if (this->request_read_values_()) {
 | 
			
		||||
        if (!this->read_battery_next_update_) {
 | 
			
		||||
          this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
        } else {
 | 
			
		||||
          // delay setting node_state to ESTABLISHED until confirmation of the notify registration
 | 
			
		||||
          this->request_battery_();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      this->handle_ = chr->handle;
 | 
			
		||||
      this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED;
 | 
			
		||||
 | 
			
		||||
      this->request_read_values_();
 | 
			
		||||
      // ensure that the client will be disconnected even if no responses arrive
 | 
			
		||||
      this->set_response_timeout_();
 | 
			
		||||
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -50,15 +57,29 @@ void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case ESP_GATTC_NOTIFY_EVT: {
 | 
			
		||||
      if (param->notify.conn_id != this->parent()->get_conn_id())
 | 
			
		||||
        break;
 | 
			
		||||
      if (param->notify.handle == this->acp_handle_) {
 | 
			
		||||
        this->read_battery_(param->notify.value, param->notify.value_len);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; }
 | 
			
		||||
bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return voc <= 16383; }
 | 
			
		||||
 | 
			
		||||
void AirthingsWaveBase::update() {
 | 
			
		||||
  if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
    if (!this->parent()->enabled) {
 | 
			
		||||
      ESP_LOGW(TAG, "Reconnecting to device");
 | 
			
		||||
      this->parent()->set_enabled(true);
 | 
			
		||||
@@ -69,12 +90,119 @@ void AirthingsWaveBase::update() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirthingsWaveBase::request_read_values_() {
 | 
			
		||||
bool AirthingsWaveBase::request_read_values_() {
 | 
			
		||||
  auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_);
 | 
			
		||||
  if (chr == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(),
 | 
			
		||||
             this->sensors_data_characteristic_uuid_.to_string().c_str());
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->handle_ = chr->handle;
 | 
			
		||||
 | 
			
		||||
  auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_,
 | 
			
		||||
                                        ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  if (status) {
 | 
			
		||||
    ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->response_pending_();
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool AirthingsWaveBase::request_battery_() {
 | 
			
		||||
  uint8_t battery_command = ACCESS_CONTROL_POINT_COMMAND;
 | 
			
		||||
  uint8_t cccd_value[2] = {1, 0};
 | 
			
		||||
 | 
			
		||||
  auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->access_control_point_characteristic_uuid_);
 | 
			
		||||
  if (chr == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "No access control point characteristic found at service %s char %s",
 | 
			
		||||
             this->service_uuid_.to_string().c_str(),
 | 
			
		||||
             this->access_control_point_characteristic_uuid_.to_string().c_str());
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto *descr = this->parent()->get_descriptor(this->service_uuid_, this->access_control_point_characteristic_uuid_,
 | 
			
		||||
                                               CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID);
 | 
			
		||||
  if (descr == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "No CCC descriptor found at service %s char %s", this->service_uuid_.to_string().c_str(),
 | 
			
		||||
             this->access_control_point_characteristic_uuid_.to_string().c_str());
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto reg_status =
 | 
			
		||||
      esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), this->parent()->get_remote_bda(), chr->handle);
 | 
			
		||||
  if (reg_status) {
 | 
			
		||||
    ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", reg_status);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->acp_handle_ = chr->handle;
 | 
			
		||||
  this->cccd_handle_ = descr->handle;
 | 
			
		||||
 | 
			
		||||
  auto descr_status =
 | 
			
		||||
      esp_ble_gattc_write_char_descr(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->cccd_handle_,
 | 
			
		||||
                                     2, cccd_value, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  if (descr_status) {
 | 
			
		||||
    ESP_LOGW(TAG, "Error sending CCC descriptor write request, status=%d", descr_status);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto chr_status =
 | 
			
		||||
      esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->acp_handle_, 1,
 | 
			
		||||
                               &battery_command, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  if (chr_status) {
 | 
			
		||||
    ESP_LOGW(TAG, "Error sending read request for battery, status=%d", chr_status);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->response_pending_();
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirthingsWaveBase::read_battery_(uint8_t *raw_value, uint16_t value_len) {
 | 
			
		||||
  auto *value = (AccessControlPointResponse *) (&raw_value[2]);
 | 
			
		||||
 | 
			
		||||
  if ((value_len >= (sizeof(AccessControlPointResponse) + 2)) && (raw_value[0] == ACCESS_CONTROL_POINT_COMMAND)) {
 | 
			
		||||
    ESP_LOGD(TAG, "Battery received: %u mV", (unsigned int) value->battery);
 | 
			
		||||
 | 
			
		||||
    if (this->battery_voltage_ != nullptr) {
 | 
			
		||||
      float voltage = value->battery / 1000.0f;
 | 
			
		||||
 | 
			
		||||
      this->battery_voltage_->publish_state(voltage);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // read the battery again at the configured update interval
 | 
			
		||||
    if (this->battery_update_interval_ != this->update_interval_) {
 | 
			
		||||
      this->read_battery_next_update_ = false;
 | 
			
		||||
      this->set_timeout("battery", this->battery_update_interval_,
 | 
			
		||||
                        [this]() { this->read_battery_next_update_ = true; });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->response_received_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirthingsWaveBase::response_pending_() {
 | 
			
		||||
  this->responses_pending_++;
 | 
			
		||||
  this->set_response_timeout_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirthingsWaveBase::response_received_() {
 | 
			
		||||
  if (--this->responses_pending_ == 0) {
 | 
			
		||||
    // This instance must not stay connected
 | 
			
		||||
    // so other clients can connect to it (e.g. the
 | 
			
		||||
    // mobile app).
 | 
			
		||||
    this->parent()->set_enabled(false);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirthingsWaveBase::set_response_timeout_() {
 | 
			
		||||
  this->set_timeout("response_timeout", 30 * 1000, [this]() {
 | 
			
		||||
    this->responses_pending_ = 1;
 | 
			
		||||
    this->response_received_();
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace airthings_wave_base
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,8 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
// All information related to reading battery levels came from the sensors.airthings_wave
 | 
			
		||||
// project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave)
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
@@ -14,6 +17,11 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_wave_base {
 | 
			
		||||
 | 
			
		||||
namespace espbt = esphome::esp32_ble_tracker;
 | 
			
		||||
 | 
			
		||||
static const uint8_t ACCESS_CONTROL_POINT_COMMAND = 0x6d;
 | 
			
		||||
static const auto CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID = espbt::ESPBTUUID::from_uint16(0x2902);
 | 
			
		||||
 | 
			
		||||
class AirthingsWaveBase : public PollingComponent, public ble_client::BLEClientNode {
 | 
			
		||||
 public:
 | 
			
		||||
  AirthingsWaveBase() = default;
 | 
			
		||||
@@ -27,21 +35,53 @@ class AirthingsWaveBase : public PollingComponent, public ble_client::BLEClientN
 | 
			
		||||
  void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
 | 
			
		||||
  void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; }
 | 
			
		||||
  void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; }
 | 
			
		||||
  void set_battery_voltage(sensor::Sensor *voltage) {
 | 
			
		||||
    battery_voltage_ = voltage;
 | 
			
		||||
    this->read_battery_next_update_ = true;
 | 
			
		||||
  }
 | 
			
		||||
  void set_battery_update_interval(uint32_t interval) { battery_update_interval_ = interval; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool is_valid_voc_value_(uint16_t voc);
 | 
			
		||||
 | 
			
		||||
  virtual void read_sensors(uint8_t *value, uint16_t value_len) = 0;
 | 
			
		||||
  void request_read_values_();
 | 
			
		||||
  bool request_read_values_();
 | 
			
		||||
  virtual void read_sensors(uint8_t *raw_value, uint16_t value_len) = 0;
 | 
			
		||||
 | 
			
		||||
  sensor::Sensor *temperature_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *humidity_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *pressure_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *tvoc_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *battery_voltage_{nullptr};
 | 
			
		||||
 | 
			
		||||
  uint16_t handle_;
 | 
			
		||||
  esp32_ble_tracker::ESPBTUUID service_uuid_;
 | 
			
		||||
  esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_;
 | 
			
		||||
  espbt::ESPBTUUID service_uuid_;
 | 
			
		||||
  espbt::ESPBTUUID sensors_data_characteristic_uuid_;
 | 
			
		||||
 | 
			
		||||
  uint16_t acp_handle_{0};
 | 
			
		||||
  uint16_t cccd_handle_{0};
 | 
			
		||||
  espbt::ESPBTUUID access_control_point_characteristic_uuid_;
 | 
			
		||||
 | 
			
		||||
  uint8_t responses_pending_{0};
 | 
			
		||||
  void response_pending_();
 | 
			
		||||
  void response_received_();
 | 
			
		||||
  void set_response_timeout_();
 | 
			
		||||
 | 
			
		||||
  // default to *not* reading battery voltage from the device; the
 | 
			
		||||
  // set_* function for the battery sensor will set this to 'true'
 | 
			
		||||
  bool read_battery_next_update_{false};
 | 
			
		||||
  bool request_battery_();
 | 
			
		||||
  void read_battery_(uint8_t *raw_value, uint16_t value_len);
 | 
			
		||||
  uint32_t battery_update_interval_{};
 | 
			
		||||
 | 
			
		||||
  struct AccessControlPointResponse {
 | 
			
		||||
    uint32_t unused1;
 | 
			
		||||
    uint8_t unused2;
 | 
			
		||||
    uint8_t illuminance;
 | 
			
		||||
    uint8_t unused3[10];
 | 
			
		||||
    uint16_t unused4[4];
 | 
			
		||||
    uint16_t battery;
 | 
			
		||||
    uint16_t unused5;
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace airthings_wave_base
 | 
			
		||||
 
 | 
			
		||||
@@ -26,12 +26,9 @@ void AirthingsWaveMini::read_sensors(uint8_t *raw_value, uint16_t value_len) {
 | 
			
		||||
    if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) {
 | 
			
		||||
      this->tvoc_sensor_->publish_state(value->voc);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // This instance must not stay connected
 | 
			
		||||
    // so other clients can connect to it (e.g. the
 | 
			
		||||
    // mobile app).
 | 
			
		||||
    this->parent()->set_enabled(false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->response_received_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AirthingsWaveMini::dump_config() {
 | 
			
		||||
@@ -42,11 +39,14 @@ void AirthingsWaveMini::dump_config() {
 | 
			
		||||
  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Pressure", this->pressure_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Battery Voltage", this->battery_voltage_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AirthingsWaveMini::AirthingsWaveMini() {
 | 
			
		||||
  this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID);
 | 
			
		||||
  this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
 | 
			
		||||
  this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID);
 | 
			
		||||
  this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
 | 
			
		||||
  this->access_control_point_characteristic_uuid_ =
 | 
			
		||||
      espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace airthings_wave_mini
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,11 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_wave_mini {
 | 
			
		||||
 | 
			
		||||
namespace espbt = esphome::esp32_ble_tracker;
 | 
			
		||||
 | 
			
		||||
static const char *const SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba";
 | 
			
		||||
static const char *const CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba";
 | 
			
		||||
static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e3ef4-ade7-11e4-89d3-123b93f75cba";
 | 
			
		||||
 | 
			
		||||
class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase {
 | 
			
		||||
 public:
 | 
			
		||||
@@ -17,7 +20,7 @@ class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase {
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void read_sensors(uint8_t *value, uint16_t value_len) override;
 | 
			
		||||
  void read_sensors(uint8_t *raw_value, uint16_t value_len) override;
 | 
			
		||||
 | 
			
		||||
  struct WaveMiniReadings {
 | 
			
		||||
    uint16_t unused01;
 | 
			
		||||
 
 | 
			
		||||
@@ -43,20 +43,17 @@ void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) {
 | 
			
		||||
      if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) {
 | 
			
		||||
        this->tvoc_sensor_->publish_state(value->voc);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // This instance must not stay connected
 | 
			
		||||
      // so other clients can connect to it (e.g. the
 | 
			
		||||
      // mobile app).
 | 
			
		||||
      this->parent()->set_enabled(false);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->response_received_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 0 <= radon && radon <= 16383; }
 | 
			
		||||
bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return radon <= 16383; }
 | 
			
		||||
 | 
			
		||||
bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return 0 <= co2 && co2 <= 16383; }
 | 
			
		||||
bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return co2 <= 16383; }
 | 
			
		||||
 | 
			
		||||
void AirthingsWavePlus::dump_config() {
 | 
			
		||||
  // these really don't belong here, but there doesn't seem to be a
 | 
			
		||||
@@ -66,6 +63,7 @@ void AirthingsWavePlus::dump_config() {
 | 
			
		||||
  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Pressure", this->pressure_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Battery Voltage", this->battery_voltage_);
 | 
			
		||||
 | 
			
		||||
  LOG_SENSOR("  ", "Radon", this->radon_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Radon Long Term", this->radon_long_term_sensor_);
 | 
			
		||||
@@ -73,8 +71,10 @@ void AirthingsWavePlus::dump_config() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AirthingsWavePlus::AirthingsWavePlus() {
 | 
			
		||||
  this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID);
 | 
			
		||||
  this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
 | 
			
		||||
  this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID);
 | 
			
		||||
  this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
 | 
			
		||||
  this->access_control_point_characteristic_uuid_ =
 | 
			
		||||
      espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace airthings_wave_plus
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,11 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace airthings_wave_plus {
 | 
			
		||||
 | 
			
		||||
namespace espbt = esphome::esp32_ble_tracker;
 | 
			
		||||
 | 
			
		||||
static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba";
 | 
			
		||||
static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba";
 | 
			
		||||
static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e2d06-ade7-11e4-89d3-123b93f75cba";
 | 
			
		||||
 | 
			
		||||
class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
 | 
			
		||||
 public:
 | 
			
		||||
@@ -24,7 +27,7 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
 | 
			
		||||
  bool is_valid_radon_value_(uint16_t radon);
 | 
			
		||||
  bool is_valid_co2_value_(uint16_t co2);
 | 
			
		||||
 | 
			
		||||
  void read_sensors(uint8_t *value, uint16_t value_len) override;
 | 
			
		||||
  void read_sensors(uint8_t *raw_value, uint16_t value_len) override;
 | 
			
		||||
 | 
			
		||||
  sensor::Sensor *radon_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *radon_long_term_sensor_{nullptr};
 | 
			
		||||
 
 | 
			
		||||
@@ -53,12 +53,12 @@ async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await airthings_wave_base.wave_base_to_code(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_RADON in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_RADON])
 | 
			
		||||
    if config_radon := config.get(CONF_RADON):
 | 
			
		||||
        sens = await sensor.new_sensor(config_radon)
 | 
			
		||||
        cg.add(var.set_radon(sens))
 | 
			
		||||
    if CONF_RADON_LONG_TERM in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM])
 | 
			
		||||
    if config_radon_long_term := config.get(CONF_RADON_LONG_TERM):
 | 
			
		||||
        sens = await sensor.new_sensor(config_radon_long_term)
 | 
			
		||||
        cg.add(var.set_radon_long_term(sens))
 | 
			
		||||
    if CONF_CO2 in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_CO2])
 | 
			
		||||
    if config_co2 := config.get(CONF_CO2):
 | 
			
		||||
        sens = await sensor.new_sensor(config_co2)
 | 
			
		||||
        cg.add(var.set_co2(sens))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/alpha3/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/alpha3/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@jan-hofmeier"]
 | 
			
		||||
							
								
								
									
										189
									
								
								esphome/components/alpha3/alpha3.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								esphome/components/alpha3/alpha3.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,189 @@
 | 
			
		||||
#include "alpha3.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include <lwip/sockets.h>  //gives ntohl
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace alpha3 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "alpha3";
 | 
			
		||||
 | 
			
		||||
void Alpha3::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "ALPHA3");
 | 
			
		||||
  LOG_SENSOR(" ", "Flow", this->flow_sensor_);
 | 
			
		||||
  LOG_SENSOR(" ", "Head", this->head_sensor_);
 | 
			
		||||
  LOG_SENSOR(" ", "Power", this->power_sensor_);
 | 
			
		||||
  LOG_SENSOR(" ", "Current", this->current_sensor_);
 | 
			
		||||
  LOG_SENSOR(" ", "Speed", this->speed_sensor_);
 | 
			
		||||
  LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Alpha3::setup() {}
 | 
			
		||||
 | 
			
		||||
void Alpha3::extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset,
 | 
			
		||||
                                           int16_t value_offset, sensor::Sensor *sensor, float factor) {
 | 
			
		||||
  if (sensor == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
  // we need to handle cases where a value is split over two packets
 | 
			
		||||
  const int16_t value_length = 4;  // 32bit float
 | 
			
		||||
  // offset inside current response packet
 | 
			
		||||
  auto rel_offset = value_offset - response_offset;
 | 
			
		||||
  if (rel_offset <= -value_length)
 | 
			
		||||
    return;  // aready passed the value completly
 | 
			
		||||
  if (rel_offset >= length)
 | 
			
		||||
    return;  // value not in this packet
 | 
			
		||||
 | 
			
		||||
  auto start_offset = std::max(0, rel_offset);
 | 
			
		||||
  auto end_offset = std::min((int16_t) (rel_offset + value_length), length);
 | 
			
		||||
  auto copy_length = end_offset - start_offset;
 | 
			
		||||
  auto buffer_offset = std::max(-rel_offset, 0);
 | 
			
		||||
  std::memcpy(this->buffer_ + buffer_offset, response + start_offset, copy_length);
 | 
			
		||||
 | 
			
		||||
  if (rel_offset + value_length <= length) {
 | 
			
		||||
    // we have the whole value
 | 
			
		||||
    void *buffer = this->buffer_;                          // to prevent warnings when casting the pointer
 | 
			
		||||
    *((int32_t *) buffer) = ntohl(*((int32_t *) buffer));  // values are big endian
 | 
			
		||||
    float fvalue = *((float *) buffer);
 | 
			
		||||
    sensor->publish_state(fvalue * factor);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Alpha3::is_current_response_type_(const uint8_t *response_type) {
 | 
			
		||||
  return !std::memcmp(this->response_type_, response_type, GENI_RESPONSE_TYPE_LENGTH);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
 | 
			
		||||
  if (this->response_offset_ >= this->response_length_) {
 | 
			
		||||
    ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str());
 | 
			
		||||
    if (length < GENI_RESPONSE_HEADER_LENGTH) {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str());
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(),
 | 
			
		||||
               response[0], response[1], response[2], response[3], response[4]);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this->response_length_ = response[1] - GENI_RESPONSE_HEADER_LENGTH + 2;  // maybe 2 byte checksum
 | 
			
		||||
    this->response_offset_ = -GENI_RESPONSE_HEADER_LENGTH;
 | 
			
		||||
    std::memcpy(this->response_type_, response + 5, GENI_RESPONSE_TYPE_LENGTH);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto extract_publish_sensor_value = [response, length, this](int16_t value_offset, sensor::Sensor *sensor,
 | 
			
		||||
                                                               float factor) {
 | 
			
		||||
    this->extract_publish_sensor_value_(response, length, this->response_offset_, value_offset, sensor, factor);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) {
 | 
			
		||||
    ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str());
 | 
			
		||||
    extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F);
 | 
			
		||||
    extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F);
 | 
			
		||||
  } else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) {
 | 
			
		||||
    ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str());
 | 
			
		||||
    extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F);
 | 
			
		||||
    extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F);
 | 
			
		||||
    extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F);
 | 
			
		||||
    extract_publish_sensor_value(GENI_RESPONSE_VOLTAGE_AC_OFFSET, this->voltage_sensor_, 1.0F);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "unkown GENI response Type %d %d %d %d %d %d %d %d", this->response_type_[0], this->response_type_[1],
 | 
			
		||||
             this->response_type_[2], this->response_type_[3], this->response_type_[4], this->response_type_[5],
 | 
			
		||||
             this->response_type_[6], this->response_type_[7]);
 | 
			
		||||
  }
 | 
			
		||||
  this->response_offset_ += length;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    case ESP_GATTC_OPEN_EVT: {
 | 
			
		||||
      this->response_offset_ = 0;
 | 
			
		||||
      this->response_length_ = 0;
 | 
			
		||||
      ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str());
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_CONNECT_EVT: {
 | 
			
		||||
      if (std::memcmp(param->connect.remote_bda, this->parent_->get_remote_bda(), 6) != 0)
 | 
			
		||||
        return;
 | 
			
		||||
      auto ret = esp_ble_set_encryption(param->connect.remote_bda, ESP_BLE_SEC_ENCRYPT);
 | 
			
		||||
      if (ret) {
 | 
			
		||||
        ESP_LOGW(TAG, "esp_ble_set_encryption failed, status=%x", ret);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
      this->node_state = espbt::ClientState::IDLE;
 | 
			
		||||
      if (this->flow_sensor_ != nullptr)
 | 
			
		||||
        this->flow_sensor_->publish_state(NAN);
 | 
			
		||||
      if (this->head_sensor_ != nullptr)
 | 
			
		||||
        this->head_sensor_->publish_state(NAN);
 | 
			
		||||
      if (this->power_sensor_ != nullptr)
 | 
			
		||||
        this->power_sensor_->publish_state(NAN);
 | 
			
		||||
      if (this->current_sensor_ != nullptr)
 | 
			
		||||
        this->current_sensor_->publish_state(NAN);
 | 
			
		||||
      if (this->speed_sensor_ != nullptr)
 | 
			
		||||
        this->speed_sensor_->publish_state(NAN);
 | 
			
		||||
      if (this->speed_sensor_ != nullptr)
 | 
			
		||||
        this->voltage_sensor_->publish_state(NAN);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
      auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID);
 | 
			
		||||
      if (chr == nullptr) {
 | 
			
		||||
        ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str());
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
 | 
			
		||||
                                                      chr->handle);
 | 
			
		||||
      if (status) {
 | 
			
		||||
        ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status);
 | 
			
		||||
      }
 | 
			
		||||
      this->geni_handle_ = chr->handle;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      this->update();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_NOTIFY_EVT: {
 | 
			
		||||
      if (param->notify.handle == this->geni_handle_) {
 | 
			
		||||
        this->handle_geni_response_(param->notify.value, param->notify.value_len);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Alpha3::send_request_(uint8_t *request, size_t len) {
 | 
			
		||||
  auto status =
 | 
			
		||||
      esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len,
 | 
			
		||||
                               request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  if (status)
 | 
			
		||||
    ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Alpha3::update() {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->flow_sensor_ != nullptr || this->head_sensor_ != nullptr) {
 | 
			
		||||
    uint8_t geni_request_flow_head[] = {39, 7, 231, 248, 10, 3, 93, 1, 33, 82, 31};
 | 
			
		||||
    this->send_request_(geni_request_flow_head, sizeof(geni_request_flow_head));
 | 
			
		||||
    delay(25);  // need to wait between requests
 | 
			
		||||
  }
 | 
			
		||||
  if (this->power_sensor_ != nullptr || this->current_sensor_ != nullptr || this->speed_sensor_ != nullptr ||
 | 
			
		||||
      this->voltage_sensor_ != nullptr) {
 | 
			
		||||
    uint8_t geni_request_power[] = {39, 7, 231, 248, 10, 3, 87, 0, 69, 138, 205};
 | 
			
		||||
    this->send_request_(geni_request_power, sizeof(geni_request_power));
 | 
			
		||||
    delay(25);  // need to wait between requests
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}  // namespace alpha3
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										73
									
								
								esphome/components/alpha3/alpha3.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								esphome/components/alpha3/alpha3.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/ble_client/ble_client.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace alpha3 {
 | 
			
		||||
 | 
			
		||||
namespace espbt = esphome::esp32_ble_tracker;
 | 
			
		||||
 | 
			
		||||
static const espbt::ESPBTUUID ALPHA3_GENI_SERVICE_UUID = espbt::ESPBTUUID::from_uint16(0xfe5d);
 | 
			
		||||
static const espbt::ESPBTUUID ALPHA3_GENI_CHARACTERISTIC_UUID =
 | 
			
		||||
    espbt::ESPBTUUID::from_raw({static_cast<char>(0xa9), 0x7b, static_cast<char>(0xb8), static_cast<char>(0x85), 0x0,
 | 
			
		||||
                                0x1a, 0x28, static_cast<char>(0xaa), 0x2a, 0x43, 0x6e, 0x3, static_cast<char>(0xd1),
 | 
			
		||||
                                static_cast<char>(0xff), static_cast<char>(0x9c), static_cast<char>(0x85)});
 | 
			
		||||
static const int16_t GENI_RESPONSE_HEADER_LENGTH = 13;
 | 
			
		||||
static const size_t GENI_RESPONSE_TYPE_LENGTH = 8;
 | 
			
		||||
 | 
			
		||||
static const uint8_t GENI_RESPONSE_TYPE_FLOW_HEAD[GENI_RESPONSE_TYPE_LENGTH] = {31, 0, 1, 48, 1, 0, 0, 24};
 | 
			
		||||
static const int16_t GENI_RESPONSE_FLOW_OFFSET = 0;
 | 
			
		||||
static const int16_t GENI_RESPONSE_HEAD_OFFSET = 4;
 | 
			
		||||
 | 
			
		||||
static const uint8_t GENI_RESPONSE_TYPE_POWER[GENI_RESPONSE_TYPE_LENGTH] = {44, 0, 1, 0, 1, 0, 0, 37};
 | 
			
		||||
static const int16_t GENI_RESPONSE_VOLTAGE_AC_OFFSET = 0;
 | 
			
		||||
static const int16_t GENI_RESPONSE_VOLTAGE_DC_OFFSET = 4;
 | 
			
		||||
static const int16_t GENI_RESPONSE_CURRENT_OFFSET = 8;
 | 
			
		||||
static const int16_t GENI_RESPONSE_POWER_OFFSET = 12;
 | 
			
		||||
static const int16_t GENI_RESPONSE_MOTOR_POWER_OFFSET = 16;  // not sure
 | 
			
		||||
static const int16_t GENI_RESPONSE_MOTOR_SPEED_OFFSET = 20;
 | 
			
		||||
 | 
			
		||||
class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
  void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; }
 | 
			
		||||
  void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; }
 | 
			
		||||
  void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; }
 | 
			
		||||
  void set_current_sensor(sensor::Sensor *sensor) { this->current_sensor_ = sensor; }
 | 
			
		||||
  void set_speed_sensor(sensor::Sensor *sensor) { this->speed_sensor_ = sensor; }
 | 
			
		||||
  void set_voltage_sensor(sensor::Sensor *sensor) { this->voltage_sensor_ = sensor; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  sensor::Sensor *flow_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *head_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *power_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *current_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *speed_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *voltage_sensor_{nullptr};
 | 
			
		||||
  uint16_t geni_handle_;
 | 
			
		||||
  int16_t response_length_;
 | 
			
		||||
  int16_t response_offset_;
 | 
			
		||||
  uint8_t response_type_[GENI_RESPONSE_TYPE_LENGTH];
 | 
			
		||||
  uint8_t buffer_[4];
 | 
			
		||||
  void extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset,
 | 
			
		||||
                                     int16_t value_offset, sensor::Sensor *sensor, float factor);
 | 
			
		||||
  void handle_geni_response_(const uint8_t *response, uint16_t length);
 | 
			
		||||
  void send_request_(uint8_t *request, size_t len);
 | 
			
		||||
  bool is_current_response_type_(const uint8_t *response_type);
 | 
			
		||||
};
 | 
			
		||||
}  // namespace alpha3
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										85
									
								
								esphome/components/alpha3/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								esphome/components/alpha3/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, ble_client
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_CURRENT,
 | 
			
		||||
    CONF_FLOW,
 | 
			
		||||
    CONF_HEAD,
 | 
			
		||||
    CONF_POWER,
 | 
			
		||||
    CONF_SPEED,
 | 
			
		||||
    CONF_VOLTAGE,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    UNIT_WATT,
 | 
			
		||||
    UNIT_METER,
 | 
			
		||||
    UNIT_CUBIC_METER_PER_HOUR,
 | 
			
		||||
    UNIT_REVOLUTIONS_PER_MINUTE,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
alpha3_ns = cg.esphome_ns.namespace("alpha3")
 | 
			
		||||
Alpha3 = alpha3_ns.class_("Alpha3", ble_client.BLEClientNode, cg.PollingComponent)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(Alpha3),
 | 
			
		||||
            cv.Optional(CONF_FLOW): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_CUBIC_METER_PER_HOUR,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_HEAD): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_METER,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_POWER): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_WATT,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_CURRENT): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_SPEED): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(ble_client.BLE_CLIENT_SCHEMA)
 | 
			
		||||
    .extend(cv.polling_component_schema("15s"))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await ble_client.register_ble_node(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_FLOW in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_FLOW])
 | 
			
		||||
        cg.add(var.set_flow_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if CONF_HEAD in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_HEAD])
 | 
			
		||||
        cg.add(var.set_head_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if CONF_POWER in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_POWER])
 | 
			
		||||
        cg.add(var.set_power_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if CONF_CURRENT in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_CURRENT])
 | 
			
		||||
        cg.add(var.set_current_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if CONF_SPEED in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_SPEED])
 | 
			
		||||
        cg.add(var.set_speed_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if CONF_VOLTAGE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_VOLTAGE])
 | 
			
		||||
        cg.add(var.set_voltage_sensor(sens))
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from esphome import core
 | 
			
		||||
from esphome.components import display, font
 | 
			
		||||
from esphome import automation, core
 | 
			
		||||
from esphome.components import font
 | 
			
		||||
import esphome.components.image as espImage
 | 
			
		||||
from esphome.components.image import CONF_USE_TRANSPARENCY
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
@@ -18,14 +18,30 @@ from esphome.core import CORE, HexInt
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["image"]
 | 
			
		||||
CODEOWNERS = ["@syndlex"]
 | 
			
		||||
DEPENDENCIES = ["display"]
 | 
			
		||||
MULTI_CONF = True
 | 
			
		||||
 | 
			
		||||
CONF_LOOP = "loop"
 | 
			
		||||
CONF_START_FRAME = "start_frame"
 | 
			
		||||
CONF_END_FRAME = "end_frame"
 | 
			
		||||
CONF_FRAME = "frame"
 | 
			
		||||
 | 
			
		||||
Animation_ = display.display_ns.class_("Animation", espImage.Image_)
 | 
			
		||||
animation_ns = cg.esphome_ns.namespace("animation")
 | 
			
		||||
 | 
			
		||||
Animation_ = animation_ns.class_("Animation", espImage.Image_)
 | 
			
		||||
 | 
			
		||||
# Actions
 | 
			
		||||
NextFrameAction = animation_ns.class_(
 | 
			
		||||
    "AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_)
 | 
			
		||||
)
 | 
			
		||||
PrevFrameAction = animation_ns.class_(
 | 
			
		||||
    "AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_)
 | 
			
		||||
)
 | 
			
		||||
SetFrameAction = animation_ns.class_(
 | 
			
		||||
    "AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_cross_dependencies(config):
 | 
			
		||||
@@ -74,7 +90,35 @@ ANIMATION_SCHEMA = cv.Schema(
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA)
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@syndlex"]
 | 
			
		||||
NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(Animation_),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
PREV_FRAME_SCHEMA = automation.maybe_simple_id(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(Animation_),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
SET_FRAME_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(Animation_),
 | 
			
		||||
        cv.Required(CONF_FRAME): cv.uint16_t,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action("animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA)
 | 
			
		||||
@automation.register_action("animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA)
 | 
			
		||||
@automation.register_action("animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA)
 | 
			
		||||
async def animation_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
 | 
			
		||||
    if CONF_FRAME in config:
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_FRAME], args, cg.uint16)
 | 
			
		||||
        cg.add(var.set_frame(template_))
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,10 @@
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace display {
 | 
			
		||||
namespace animation {
 | 
			
		||||
 | 
			
		||||
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type)
 | 
			
		||||
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
 | 
			
		||||
                     image::ImageType type)
 | 
			
		||||
    : Image(data_start, width, height, type),
 | 
			
		||||
      animation_data_start_(data_start),
 | 
			
		||||
      current_frame_(0),
 | 
			
		||||
@@ -65,5 +66,5 @@ void Animation::update_data_start_() {
 | 
			
		||||
  this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace display
 | 
			
		||||
}  // namespace animation
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										67
									
								
								esphome/components/animation/animation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								esphome/components/animation/animation.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include "esphome/components/image/image.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace animation {
 | 
			
		||||
 | 
			
		||||
class Animation : public image::Image {
 | 
			
		||||
 public:
 | 
			
		||||
  Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type);
 | 
			
		||||
 | 
			
		||||
  uint32_t get_animation_frame_count() const;
 | 
			
		||||
  int get_current_frame() const;
 | 
			
		||||
  void next_frame();
 | 
			
		||||
  void prev_frame();
 | 
			
		||||
 | 
			
		||||
  /** Selects a specific frame within the animation.
 | 
			
		||||
   *
 | 
			
		||||
   * @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame.
 | 
			
		||||
   */
 | 
			
		||||
  void set_frame(int frame);
 | 
			
		||||
 | 
			
		||||
  void set_loop(uint32_t start_frame, uint32_t end_frame, int count);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void update_data_start_();
 | 
			
		||||
 | 
			
		||||
  const uint8_t *animation_data_start_;
 | 
			
		||||
  int current_frame_;
 | 
			
		||||
  uint32_t animation_frame_count_;
 | 
			
		||||
  uint32_t loop_start_frame_;
 | 
			
		||||
  uint32_t loop_end_frame_;
 | 
			
		||||
  int loop_count_;
 | 
			
		||||
  int loop_current_iteration_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  AnimationNextFrameAction(Animation *parent) : parent_(parent) {}
 | 
			
		||||
  void play(Ts... x) override { this->parent_->next_frame(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  Animation *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class AnimationPrevFrameAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  AnimationPrevFrameAction(Animation *parent) : parent_(parent) {}
 | 
			
		||||
  void play(Ts... x) override { this->parent_->prev_frame(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  Animation *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class AnimationSetFrameAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  AnimationSetFrameAction(Animation *parent) : parent_(parent) {}
 | 
			
		||||
  TEMPLATABLE_VALUE(uint16_t, frame)
 | 
			
		||||
  void play(Ts... x) override { this->parent_->set_frame(this->frame_.value(x...)); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  Animation *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace animation
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1420,6 +1420,7 @@ message VoiceAssistantRequest {
 | 
			
		||||
 | 
			
		||||
  bool start = 1;
 | 
			
		||||
  string conversation_id = 2;
 | 
			
		||||
  bool use_vad = 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message VoiceAssistantResponse {
 | 
			
		||||
 
 | 
			
		||||
@@ -907,12 +907,13 @@ BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
bool APIConnection::request_voice_assistant(bool start, const std::string &conversation_id) {
 | 
			
		||||
bool APIConnection::request_voice_assistant(bool start, const std::string &conversation_id, bool use_vad) {
 | 
			
		||||
  if (!this->voice_assistant_subscription_)
 | 
			
		||||
    return false;
 | 
			
		||||
  VoiceAssistantRequest msg;
 | 
			
		||||
  msg.start = start;
 | 
			
		||||
  msg.conversation_id = conversation_id;
 | 
			
		||||
  msg.use_vad = use_vad;
 | 
			
		||||
  return this->send_voice_assistant_request(msg);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) {
 | 
			
		||||
 
 | 
			
		||||
@@ -124,7 +124,7 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override {
 | 
			
		||||
    this->voice_assistant_subscription_ = msg.subscribe;
 | 
			
		||||
  }
 | 
			
		||||
  bool request_voice_assistant(bool start, const std::string &conversation_id);
 | 
			
		||||
  bool request_voice_assistant(bool start, const std::string &conversation_id, bool use_vad);
 | 
			
		||||
  void on_voice_assistant_response(const VoiceAssistantResponse &msg) override;
 | 
			
		||||
  void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -6348,6 +6348,10 @@ bool VoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
 | 
			
		||||
      this->start = value.as_bool();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 3: {
 | 
			
		||||
      this->use_vad = value.as_bool();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -6365,6 +6369,7 @@ bool VoiceAssistantRequest::decode_length(uint32_t field_id, ProtoLengthDelimite
 | 
			
		||||
void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_bool(1, this->start);
 | 
			
		||||
  buffer.encode_string(2, this->conversation_id);
 | 
			
		||||
  buffer.encode_bool(3, this->use_vad);
 | 
			
		||||
}
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
void VoiceAssistantRequest::dump_to(std::string &out) const {
 | 
			
		||||
@@ -6377,6 +6382,10 @@ void VoiceAssistantRequest::dump_to(std::string &out) const {
 | 
			
		||||
  out.append("  conversation_id: ");
 | 
			
		||||
  out.append("'").append(this->conversation_id).append("'");
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  use_vad: ");
 | 
			
		||||
  out.append(YESNO(this->use_vad));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
  out.append("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -1655,6 +1655,7 @@ class VoiceAssistantRequest : public ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  bool start{false};
 | 
			
		||||
  std::string conversation_id{};
 | 
			
		||||
  bool use_vad{false};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
 
 | 
			
		||||
@@ -323,16 +323,16 @@ void APIServer::on_shutdown() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
bool APIServer::start_voice_assistant(const std::string &conversation_id) {
 | 
			
		||||
bool APIServer::start_voice_assistant(const std::string &conversation_id, bool use_vad) {
 | 
			
		||||
  for (auto &c : this->clients_) {
 | 
			
		||||
    if (c->request_voice_assistant(true, conversation_id))
 | 
			
		||||
    if (c->request_voice_assistant(true, conversation_id, use_vad))
 | 
			
		||||
      return true;
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
void APIServer::stop_voice_assistant() {
 | 
			
		||||
  for (auto &c : this->clients_) {
 | 
			
		||||
    if (c->request_voice_assistant(false, ""))
 | 
			
		||||
    if (c->request_voice_assistant(false, "", false))
 | 
			
		||||
      return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -81,7 +81,7 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
  bool start_voice_assistant(const std::string &conversation_id);
 | 
			
		||||
  bool start_voice_assistant(const std::string &conversation_id, bool use_vad);
 | 
			
		||||
  void stop_voice_assistant();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,7 @@ async def async_run_logs(config, address):
 | 
			
		||||
        except APIConnectionError:
 | 
			
		||||
            cli.disconnect()
 | 
			
		||||
 | 
			
		||||
    async def on_disconnect():
 | 
			
		||||
    async def on_disconnect(expected_disconnect: bool) -> None:
 | 
			
		||||
        _LOGGER.warning("Disconnected from API")
 | 
			
		||||
 | 
			
		||||
    zc = zeroconf.Zeroconf()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/atm90e26/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/atm90e26/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@danieltwagner"]
 | 
			
		||||
							
								
								
									
										235
									
								
								esphome/components/atm90e26/atm90e26.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								esphome/components/atm90e26/atm90e26.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,235 @@
 | 
			
		||||
#include "atm90e26.h"
 | 
			
		||||
#include "atm90e26_reg.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace atm90e26 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "atm90e26";
 | 
			
		||||
 | 
			
		||||
void ATM90E26Component::update() {
 | 
			
		||||
  if (this->read16_(ATM90E26_REGISTER_FUNCEN) != 0x0030) {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->voltage_sensor_ != nullptr) {
 | 
			
		||||
    this->voltage_sensor_->publish_state(this->get_line_voltage_());
 | 
			
		||||
  }
 | 
			
		||||
  if (this->current_sensor_ != nullptr) {
 | 
			
		||||
    this->current_sensor_->publish_state(this->get_line_current_());
 | 
			
		||||
  }
 | 
			
		||||
  if (this->power_sensor_ != nullptr) {
 | 
			
		||||
    this->power_sensor_->publish_state(this->get_active_power_());
 | 
			
		||||
  }
 | 
			
		||||
  if (this->reactive_power_sensor_ != nullptr) {
 | 
			
		||||
    this->reactive_power_sensor_->publish_state(this->get_reactive_power_());
 | 
			
		||||
  }
 | 
			
		||||
  if (this->power_factor_sensor_ != nullptr) {
 | 
			
		||||
    this->power_factor_sensor_->publish_state(this->get_power_factor_());
 | 
			
		||||
  }
 | 
			
		||||
  if (this->forward_active_energy_sensor_ != nullptr) {
 | 
			
		||||
    this->forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_());
 | 
			
		||||
  }
 | 
			
		||||
  if (this->reverse_active_energy_sensor_ != nullptr) {
 | 
			
		||||
    this->reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_());
 | 
			
		||||
  }
 | 
			
		||||
  if (this->freq_sensor_ != nullptr) {
 | 
			
		||||
    this->freq_sensor_->publish_state(this->get_frequency_());
 | 
			
		||||
  }
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E26Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up ATM90E26 Component...");
 | 
			
		||||
  this->spi_setup();
 | 
			
		||||
 | 
			
		||||
  uint16_t mmode = 0x422;  // default values for everything but L/N line current gains
 | 
			
		||||
  mmode |= (gain_pga_ & 0x7) << 13;
 | 
			
		||||
  mmode |= (n_line_gain_ & 0x3) << 11;
 | 
			
		||||
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_SOFTRESET, 0x789A);  // Perform soft reset
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_FUNCEN,
 | 
			
		||||
                 0x0030);  // Voltage sag irq=1, report on warnout pin=1, energy dir change irq=0
 | 
			
		||||
  uint16_t read = this->read16_(ATM90E26_REGISTER_LASTDATA);
 | 
			
		||||
  if (read != 0x0030) {
 | 
			
		||||
    ESP_LOGW(TAG, "Could not initialize ATM90E26 IC, check SPI settings: %d", read);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // TODO: 100 * <nominal voltage, e.g. 230> * sqrt(2) * <fraction of nominal, e.g. 0.9> / (4 * gain_voltage/32768)
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_SAGTH, 0x17DD);  // Voltage sag threshhold 0x1F2F
 | 
			
		||||
 | 
			
		||||
  // Set metering calibration values
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_CALSTART, 0x5678);  // CAL Metering calibration startup command
 | 
			
		||||
 | 
			
		||||
  // Configure
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_MMODE, mmode);  // Metering Mode Configuration (see above)
 | 
			
		||||
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_PLCONSTH, (pl_const_ >> 16));   // PL Constant MSB
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_PLCONSTL, pl_const_ & 0xFFFF);  // PL Constant LSB
 | 
			
		||||
 | 
			
		||||
  // Calibrate this to be 1 pulse per Wh
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_LGAIN, gain_metering_);  // L Line Calibration Gain (active power metering)
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_LPHI, 0x0000);           // L Line Calibration Angle
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_NGAIN, 0x0000);          // N Line Calibration Gain
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_NPHI, 0x0000);           // N Line Calibration Angle
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_PSTARTTH, 0x08BD);       // Active Startup Power Threshold (default) = 2237
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_PNOLTH, 0x0000);         // Active No-Load Power Threshold
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_QSTARTTH, 0x0AEC);       // Reactive Startup Power Threshold (default) = 2796
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_QNOLTH, 0x0000);         // Reactive No-Load Power Threshold
 | 
			
		||||
 | 
			
		||||
  // Compute Checksum for the registers we set above
 | 
			
		||||
  // low byte = sum of all bytes
 | 
			
		||||
  uint16_t cs =
 | 
			
		||||
      ((mmode >> 8) + (mmode & 0xFF) + (pl_const_ >> 24) + ((pl_const_ >> 16) & 0xFF) + ((pl_const_ >> 8) & 0xFF) +
 | 
			
		||||
       (pl_const_ & 0xFF) + (gain_metering_ >> 8) + (gain_metering_ & 0xFF) + 0x08 + 0xBD + 0x0A + 0xEC) &
 | 
			
		||||
      0xFF;
 | 
			
		||||
  // high byte = XOR of all bytes
 | 
			
		||||
  cs |= ((mmode >> 8) ^ (mmode & 0xFF) ^ (pl_const_ >> 24) ^ ((pl_const_ >> 16) & 0xFF) ^ ((pl_const_ >> 8) & 0xFF) ^
 | 
			
		||||
         (pl_const_ & 0xFF) ^ (gain_metering_ >> 8) ^ (gain_metering_ & 0xFF) ^ 0x08 ^ 0xBD ^ 0x0A ^ 0xEC)
 | 
			
		||||
        << 8;
 | 
			
		||||
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_CS1, cs);
 | 
			
		||||
  ESP_LOGVV(TAG, "Set CS1 to: 0x%04X", cs);
 | 
			
		||||
 | 
			
		||||
  // Set measurement calibration values
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_ADJSTART, 0x5678);      // Measurement calibration startup command, registers 31-3A
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_UGAIN, gain_voltage_);  // Voltage RMS gain
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_IGAINL, gain_ct_);      // L line current RMS gain
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_IGAINN, 0x7530);        // N Line Current RMS Gain
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_UOFFSET, 0x0000);       // Voltage Offset
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_IOFFSETL, 0x0000);      // L Line Current Offset
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_IOFFSETN, 0x0000);      // N Line Current Offse
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_POFFSETL, 0x0000);      // L Line Active Power Offset
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_QOFFSETL, 0x0000);      // L Line Reactive Power Offset
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_POFFSETN, 0x0000);      // N Line Active Power Offset
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_QOFFSETN, 0x0000);      // N Line Reactive Power Offset
 | 
			
		||||
 | 
			
		||||
  // Compute Checksum for the registers we set above
 | 
			
		||||
  cs = ((gain_voltage_ >> 8) + (gain_voltage_ & 0xFF) + (gain_ct_ >> 8) + (gain_ct_ & 0xFF) + 0x75 + 0x30) & 0xFF;
 | 
			
		||||
  cs |= ((gain_voltage_ >> 8) ^ (gain_voltage_ & 0xFF) ^ (gain_ct_ >> 8) ^ (gain_ct_ & 0xFF) ^ 0x75 ^ 0x30) << 8;
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_CS2, cs);
 | 
			
		||||
  ESP_LOGVV(TAG, "Set CS2 to: 0x%04X", cs);
 | 
			
		||||
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_CALSTART,
 | 
			
		||||
                 0x8765);  // Checks correctness of 21-2B registers and starts normal metering if ok
 | 
			
		||||
  this->write16_(ATM90E26_REGISTER_ADJSTART,
 | 
			
		||||
                 0x8765);  // Checks correctness of 31-3A registers and starts normal measurement  if ok
 | 
			
		||||
 | 
			
		||||
  uint16_t sys_status = this->read16_(ATM90E26_REGISTER_SYSSTATUS);
 | 
			
		||||
  if (sys_status & 0xC000) {  // Checksum 1 Error
 | 
			
		||||
 | 
			
		||||
    ESP_LOGW(TAG, "Could not initialize ATM90E26 IC: CS1 was incorrect, expected: 0x%04X",
 | 
			
		||||
             this->read16_(ATM90E26_REGISTER_CS1));
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
  }
 | 
			
		||||
  if (sys_status & 0x3000) {  // Checksum 2 Error
 | 
			
		||||
    ESP_LOGW(TAG, "Could not initialize ATM90E26 IC: CS2 was incorrect, expected: 0x%04X",
 | 
			
		||||
             this->read16_(ATM90E26_REGISTER_CS2));
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E26Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG("", "ATM90E26:");
 | 
			
		||||
  LOG_PIN("  CS Pin: ", this->cs_);
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with ATM90E26 failed!");
 | 
			
		||||
  }
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
  LOG_SENSOR("  ", "Voltage A", this->voltage_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Current A", this->current_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Power A", this->power_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Reactive Power A", this->reactive_power_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "PF A", this->power_factor_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Active Forward Energy A", this->forward_active_energy_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Active Reverse Energy A", this->reverse_active_energy_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Frequency", this->freq_sensor_);
 | 
			
		||||
}
 | 
			
		||||
float ATM90E26Component::get_setup_priority() const { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
uint16_t ATM90E26Component::read16_(uint8_t a_register) {
 | 
			
		||||
  uint8_t data[2];
 | 
			
		||||
  uint16_t output;
 | 
			
		||||
 | 
			
		||||
  this->enable();
 | 
			
		||||
  delayMicroseconds(4);
 | 
			
		||||
  this->write_byte(a_register | 0x80);
 | 
			
		||||
  delayMicroseconds(4);
 | 
			
		||||
  this->read_array(data, 2);
 | 
			
		||||
  this->disable();
 | 
			
		||||
 | 
			
		||||
  output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF);
 | 
			
		||||
  ESP_LOGVV(TAG, "read16_ 0x%04X output 0x%04X", a_register, output);
 | 
			
		||||
  return output;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E26Component::write16_(uint8_t a_register, uint16_t val) {
 | 
			
		||||
  ESP_LOGVV(TAG, "write16_ 0x%04X val 0x%04X", a_register, val);
 | 
			
		||||
  this->enable();
 | 
			
		||||
  delayMicroseconds(4);
 | 
			
		||||
  this->write_byte(a_register & 0x7F);
 | 
			
		||||
  delayMicroseconds(4);
 | 
			
		||||
  this->write_byte((val >> 8) & 0xFF);
 | 
			
		||||
  this->write_byte(val & 0xFF);
 | 
			
		||||
  this->disable();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E26Component::get_line_current_() {
 | 
			
		||||
  uint16_t current = this->read16_(ATM90E26_REGISTER_IRMS);
 | 
			
		||||
  return current / 1000.0f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E26Component::get_line_voltage_() {
 | 
			
		||||
  uint16_t voltage = this->read16_(ATM90E26_REGISTER_URMS);
 | 
			
		||||
  return voltage / 100.0f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E26Component::get_active_power_() {
 | 
			
		||||
  int16_t val = this->read16_(ATM90E26_REGISTER_PMEAN);  // two's complement
 | 
			
		||||
  return (float) val;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E26Component::get_reactive_power_() {
 | 
			
		||||
  int16_t val = this->read16_(ATM90E26_REGISTER_QMEAN);  // two's complement
 | 
			
		||||
  return (float) val;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E26Component::get_power_factor_() {
 | 
			
		||||
  uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF);  // signed
 | 
			
		||||
  if (val & 0x8000) {
 | 
			
		||||
    return -(val & 0x7FF) / 1000.0f;
 | 
			
		||||
  } else {
 | 
			
		||||
    return val / 1000.0f;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E26Component::get_forward_active_energy_() {
 | 
			
		||||
  uint16_t val = this->read16_(ATM90E26_REGISTER_APENERGY);
 | 
			
		||||
  if ((UINT32_MAX - this->cumulative_forward_active_energy_) > val) {
 | 
			
		||||
    this->cumulative_forward_active_energy_ += val;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->cumulative_forward_active_energy_ = val;
 | 
			
		||||
  }
 | 
			
		||||
  // The register holds thenths of pulses, we want to output Wh
 | 
			
		||||
  return (this->cumulative_forward_active_energy_ * 100.0f / meter_constant_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E26Component::get_reverse_active_energy_() {
 | 
			
		||||
  uint16_t val = this->read16_(ATM90E26_REGISTER_ANENERGY);
 | 
			
		||||
  if (UINT32_MAX - this->cumulative_reverse_active_energy_ > val) {
 | 
			
		||||
    this->cumulative_reverse_active_energy_ += val;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->cumulative_reverse_active_energy_ = val;
 | 
			
		||||
  }
 | 
			
		||||
  return (this->cumulative_reverse_active_energy_ * 100.0f / meter_constant_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E26Component::get_frequency_() {
 | 
			
		||||
  uint16_t freq = this->read16_(ATM90E26_REGISTER_FREQ);
 | 
			
		||||
  return freq / 100.0f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace atm90e26
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										72
									
								
								esphome/components/atm90e26/atm90e26.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								esphome/components/atm90e26/atm90e26.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/spi/spi.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace atm90e26 {
 | 
			
		||||
 | 
			
		||||
class ATM90E26Component : public PollingComponent,
 | 
			
		||||
                          public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
 | 
			
		||||
                                                spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_200KHZ> {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
 | 
			
		||||
  void set_voltage_sensor(sensor::Sensor *obj) { this->voltage_sensor_ = obj; }
 | 
			
		||||
  void set_current_sensor(sensor::Sensor *obj) { this->current_sensor_ = obj; }
 | 
			
		||||
  void set_power_sensor(sensor::Sensor *obj) { this->power_sensor_ = obj; }
 | 
			
		||||
  void set_reactive_power_sensor(sensor::Sensor *obj) { this->reactive_power_sensor_ = obj; }
 | 
			
		||||
  void set_forward_active_energy_sensor(sensor::Sensor *obj) { this->forward_active_energy_sensor_ = obj; }
 | 
			
		||||
  void set_reverse_active_energy_sensor(sensor::Sensor *obj) { this->reverse_active_energy_sensor_ = obj; }
 | 
			
		||||
  void set_power_factor_sensor(sensor::Sensor *obj) { this->power_factor_sensor_ = obj; }
 | 
			
		||||
  void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; }
 | 
			
		||||
  void set_line_freq(int freq) { line_freq_ = freq; }
 | 
			
		||||
  void set_meter_constant(float val) { meter_constant_ = val; }
 | 
			
		||||
  void set_pl_const(uint32_t pl_const) { pl_const_ = pl_const; }
 | 
			
		||||
  void set_gain_metering(uint16_t gain) { this->gain_metering_ = gain; }
 | 
			
		||||
  void set_gain_voltage(uint16_t gain) { this->gain_voltage_ = gain; }
 | 
			
		||||
  void set_gain_ct(uint16_t gain) { this->gain_ct_ = gain; }
 | 
			
		||||
  void set_gain_pga(uint16_t gain) { gain_pga_ = gain; }
 | 
			
		||||
  void set_n_line_gain(uint16_t gain) { n_line_gain_ = gain; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  uint16_t read16_(uint8_t a_register);
 | 
			
		||||
  int read32_(uint8_t addr_h, uint8_t addr_l);
 | 
			
		||||
  void write16_(uint8_t a_register, uint16_t val);
 | 
			
		||||
 | 
			
		||||
  float get_line_voltage_();
 | 
			
		||||
  float get_line_current_();
 | 
			
		||||
  float get_active_power_();
 | 
			
		||||
  float get_reactive_power_();
 | 
			
		||||
  float get_power_factor_();
 | 
			
		||||
  float get_forward_active_energy_();
 | 
			
		||||
  float get_reverse_active_energy_();
 | 
			
		||||
  float get_frequency_();
 | 
			
		||||
  float get_chip_temperature_();
 | 
			
		||||
 | 
			
		||||
  sensor::Sensor *freq_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *voltage_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *current_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *power_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *reactive_power_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *power_factor_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *forward_active_energy_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *reverse_active_energy_sensor_{nullptr};
 | 
			
		||||
  uint32_t cumulative_forward_active_energy_{0};
 | 
			
		||||
  uint32_t cumulative_reverse_active_energy_{0};
 | 
			
		||||
  uint16_t gain_metering_{7481};
 | 
			
		||||
  uint16_t gain_voltage_{26400};
 | 
			
		||||
  uint16_t gain_ct_{31251};
 | 
			
		||||
  uint16_t gain_pga_{0x4};
 | 
			
		||||
  uint16_t n_line_gain_{0x2};
 | 
			
		||||
  int line_freq_{60};
 | 
			
		||||
  float meter_constant_{3200.0f};
 | 
			
		||||
  uint32_t pl_const_{1429876};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace atm90e26
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										70
									
								
								esphome/components/atm90e26/atm90e26_reg.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								esphome/components/atm90e26/atm90e26_reg.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace atm90e26 {
 | 
			
		||||
 | 
			
		||||
/* Status and Special Register */
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_SOFTRESET = 0x00;  // Software Reset
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_SYSSTATUS = 0x01;  // System Status
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_FUNCEN = 0x02;     // Function Enable
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_SAGTH = 0x03;      // Voltage Sag Threshold
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_SMALLPMOD = 0x04;  // Small-Power Mode
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_LASTDATA = 0x06;   // Last Read/Write SPI/UART Value
 | 
			
		||||
 | 
			
		||||
/* Metering Calibration and Configuration Register */
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_LSB = 0x08;       // RMS/Power 16-bit LSB
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_CALSTART = 0x20;  // Calibration Start Command
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_PLCONSTH = 0x21;  // High Word of PL_Constant
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_PLCONSTL = 0x22;  // Low Word of PL_Constant
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_LGAIN = 0x23;     // L Line Calibration Gain
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_LPHI = 0x24;      // L Line Calibration Angle
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_NGAIN = 0x25;     // N Line Calibration Gain
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_NPHI = 0x26;      // N Line Calibration Angle
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_PSTARTTH = 0x27;  // Active Startup Power Threshold
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_PNOLTH = 0x28;    // Active No-Load Power Threshold
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_QSTARTTH = 0x29;  // Reactive Startup Power Threshold
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_QNOLTH = 0x2A;    // Reactive No-Load Power Threshold
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_MMODE = 0x2B;     // Metering Mode Configuration
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_CS1 = 0x2C;       // Checksum 1
 | 
			
		||||
 | 
			
		||||
/* Measurement Calibration Register */
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_ADJSTART = 0x30;  // Measurement Calibration Start Command
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_UGAIN = 0x31;     // Voltage RMS Gain
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_IGAINL = 0x32;    // L Line Current RMS Gain
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_IGAINN = 0x33;    // N Line Current RMS Gain
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_UOFFSET = 0x34;   // Voltage Offset
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_IOFFSETL = 0x35;  // L Line Current Offset
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_IOFFSETN = 0x36;  // N Line Current Offse
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_POFFSETL = 0x37;  // L Line Active Power Offset
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_QOFFSETL = 0x38;  // L Line Reactive Power Offset
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_POFFSETN = 0x39;  // N Line Active Power Offset
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_QOFFSETN = 0x3A;  // N Line Reactive Power Offset
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_CS2 = 0x3B;       // Checksum 2
 | 
			
		||||
 | 
			
		||||
/* Energy Register */
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_APENERGY = 0x40;  // Forward Active Energy
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_ANENERGY = 0x41;  // Reverse Active Energy
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_ATENERGY = 0x42;  // Absolute Active Energy
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_RPENERGY = 0x43;  // Forward (Inductive) Reactive Energy
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_RNENERG = 0x44;   // Reverse (Capacitive) Reactive Energy
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_RTENERGY = 0x45;  // Absolute Reactive Energy
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_ENSTATUS = 0x46;  // Metering Status
 | 
			
		||||
 | 
			
		||||
/* Measurement Register */
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_IRMS = 0x48;     // L Line Current RMS
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_URMS = 0x49;     // Voltage RMS
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_PMEAN = 0x4A;    // L Line Mean Active Power
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_QMEAN = 0x4B;    // L Line Mean Reactive Power
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_FREQ = 0x4C;     // Voltage Frequency
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_POWERF = 0x4D;   // L Line Power Factor
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_PANGLE = 0x4E;   // Phase Angle between Voltage and L Line Current
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_SMEAN = 0x4F;    // L Line Mean Apparent Power
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_IRMS2 = 0x68;    // N Line Current rms
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_PMEAN2 = 0x6A;   // N Line Mean Active Power
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_QMEAN2 = 0x6B;   // N Line Mean Reactive Power
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_POWERF2 = 0x6D;  // N Line Power Factor
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_PANGLE2 = 0x6E;  // Phase Angle between Voltage and N Line Current
 | 
			
		||||
static const uint8_t ATM90E26_REGISTER_SMEAN2 = 0x6F;   // N Line Mean Apparent Power
 | 
			
		||||
 | 
			
		||||
}  // namespace atm90e26
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										157
									
								
								esphome/components/atm90e26/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								esphome/components/atm90e26/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,157 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, spi
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_REACTIVE_POWER,
 | 
			
		||||
    CONF_VOLTAGE,
 | 
			
		||||
    CONF_CURRENT,
 | 
			
		||||
    CONF_POWER,
 | 
			
		||||
    CONF_POWER_FACTOR,
 | 
			
		||||
    CONF_FREQUENCY,
 | 
			
		||||
    CONF_FORWARD_ACTIVE_ENERGY,
 | 
			
		||||
    CONF_REVERSE_ACTIVE_ENERGY,
 | 
			
		||||
    DEVICE_CLASS_CURRENT,
 | 
			
		||||
    DEVICE_CLASS_ENERGY,
 | 
			
		||||
    DEVICE_CLASS_POWER,
 | 
			
		||||
    DEVICE_CLASS_POWER_FACTOR,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    ICON_LIGHTBULB,
 | 
			
		||||
    ICON_CURRENT_AC,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
    UNIT_HERTZ,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
    UNIT_WATT,
 | 
			
		||||
    UNIT_VOLT_AMPS_REACTIVE,
 | 
			
		||||
    UNIT_WATT_HOURS,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_LINE_FREQUENCY = "line_frequency"
 | 
			
		||||
CONF_METER_CONSTANT = "meter_constant"
 | 
			
		||||
CONF_PL_CONST = "pl_const"
 | 
			
		||||
CONF_GAIN_PGA = "gain_pga"
 | 
			
		||||
CONF_GAIN_METERING = "gain_metering"
 | 
			
		||||
CONF_GAIN_VOLTAGE = "gain_voltage"
 | 
			
		||||
CONF_GAIN_CT = "gain_ct"
 | 
			
		||||
LINE_FREQS = {
 | 
			
		||||
    "50HZ": 50,
 | 
			
		||||
    "60HZ": 60,
 | 
			
		||||
}
 | 
			
		||||
PGA_GAINS = {
 | 
			
		||||
    "1X": 0x4,
 | 
			
		||||
    "4X": 0x0,
 | 
			
		||||
    "8X": 0x1,
 | 
			
		||||
    "16X": 0x2,
 | 
			
		||||
    "24X": 0x3,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
atm90e26_ns = cg.esphome_ns.namespace("atm90e26")
 | 
			
		||||
ATM90E26Component = atm90e26_ns.class_(
 | 
			
		||||
    "ATM90E26Component", cg.PollingComponent, spi.SPIDevice
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ATM90E26Component),
 | 
			
		||||
            cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_CURRENT): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_POWER): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_WATT,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE,
 | 
			
		||||
                icon=ICON_LIGHTBULB,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_POWER_FACTOR,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_WATT_HOURS,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
                state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_WATT_HOURS,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
                state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_HERTZ,
 | 
			
		||||
                icon=ICON_CURRENT_AC,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True),
 | 
			
		||||
            cv.Required(CONF_METER_CONSTANT): cv.positive_float,
 | 
			
		||||
            cv.Optional(CONF_PL_CONST, default=1429876): cv.uint32_t,
 | 
			
		||||
            cv.Optional(CONF_GAIN_METERING, default=7481): cv.uint16_t,
 | 
			
		||||
            cv.Optional(CONF_GAIN_VOLTAGE, default=26400): cv.int_range(
 | 
			
		||||
                min=0, max=32767
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_GAIN_CT, default=31251): cv.uint16_t,
 | 
			
		||||
            cv.Optional(CONF_GAIN_PGA, default="1X"): cv.enum(PGA_GAINS, upper=True),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(spi.spi_device_schema())
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await spi.register_spi_device(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_VOLTAGE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_VOLTAGE])
 | 
			
		||||
        cg.add(var.set_voltage_sensor(sens))
 | 
			
		||||
    if CONF_CURRENT in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_CURRENT])
 | 
			
		||||
        cg.add(var.set_current_sensor(sens))
 | 
			
		||||
    if CONF_POWER in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_POWER])
 | 
			
		||||
        cg.add(var.set_power_sensor(sens))
 | 
			
		||||
    if CONF_REACTIVE_POWER in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_REACTIVE_POWER])
 | 
			
		||||
        cg.add(var.set_reactive_power_sensor(sens))
 | 
			
		||||
    if CONF_POWER_FACTOR in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_POWER_FACTOR])
 | 
			
		||||
        cg.add(var.set_power_factor_sensor(sens))
 | 
			
		||||
    if CONF_FORWARD_ACTIVE_ENERGY in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_FORWARD_ACTIVE_ENERGY])
 | 
			
		||||
        cg.add(var.set_forward_active_energy_sensor(sens))
 | 
			
		||||
    if CONF_REVERSE_ACTIVE_ENERGY in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_REVERSE_ACTIVE_ENERGY])
 | 
			
		||||
        cg.add(var.set_reverse_active_energy_sensor(sens))
 | 
			
		||||
    if CONF_FREQUENCY in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_FREQUENCY])
 | 
			
		||||
        cg.add(var.set_freq_sensor(sens))
 | 
			
		||||
    cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
 | 
			
		||||
    cg.add(var.set_meter_constant(config[CONF_METER_CONSTANT]))
 | 
			
		||||
    cg.add(var.set_pl_const(config[CONF_PL_CONST]))
 | 
			
		||||
    cg.add(var.set_gain_metering(config[CONF_GAIN_METERING]))
 | 
			
		||||
    cg.add(var.set_gain_voltage(config[CONF_GAIN_VOLTAGE]))
 | 
			
		||||
    cg.add(var.set_gain_ct(config[CONF_GAIN_CT]))
 | 
			
		||||
    cg.add(var.set_gain_pga(config[CONF_GAIN_PGA]))
 | 
			
		||||
@@ -95,6 +95,14 @@ DEVICE_CLASSES = [
 | 
			
		||||
 | 
			
		||||
IS_PLATFORM_COMPONENT = True
 | 
			
		||||
 | 
			
		||||
CONF_TIME_OFF = "time_off"
 | 
			
		||||
CONF_TIME_ON = "time_on"
 | 
			
		||||
 | 
			
		||||
DEFAULT_DELAY = "1s"
 | 
			
		||||
DEFAULT_TIME_OFF = "100ms"
 | 
			
		||||
DEFAULT_TIME_ON = "900ms"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
binary_sensor_ns = cg.esphome_ns.namespace("binary_sensor")
 | 
			
		||||
BinarySensor = binary_sensor_ns.class_("BinarySensor", cg.EntityBase)
 | 
			
		||||
BinarySensorInitiallyOff = binary_sensor_ns.class_(
 | 
			
		||||
@@ -138,47 +146,75 @@ FILTER_REGISTRY = Registry()
 | 
			
		||||
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@FILTER_REGISTRY.register("invert", InvertFilter, {})
 | 
			
		||||
def register_filter(name, filter_type, schema):
 | 
			
		||||
    return FILTER_REGISTRY.register(name, filter_type, schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_filter("invert", InvertFilter, {})
 | 
			
		||||
async def invert_filter_to_code(config, filter_id):
 | 
			
		||||
    return cg.new_Pvariable(filter_id)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@FILTER_REGISTRY.register(
 | 
			
		||||
    "delayed_on_off", DelayedOnOffFilter, cv.positive_time_period_milliseconds
 | 
			
		||||
@register_filter(
 | 
			
		||||
    "delayed_on_off",
 | 
			
		||||
    DelayedOnOffFilter,
 | 
			
		||||
    cv.Any(
 | 
			
		||||
        cv.templatable(cv.positive_time_period_milliseconds),
 | 
			
		||||
        cv.Schema(
 | 
			
		||||
            {
 | 
			
		||||
                cv.Required(CONF_TIME_ON): cv.templatable(
 | 
			
		||||
                    cv.positive_time_period_milliseconds
 | 
			
		||||
                ),
 | 
			
		||||
                cv.Required(CONF_TIME_OFF): cv.templatable(
 | 
			
		||||
                    cv.positive_time_period_milliseconds
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        msg="'delayed_on_off' filter requires either a delay time to be used for both "
 | 
			
		||||
        "turn-on and turn-off delays, or two parameters 'time_on' and 'time_off' if "
 | 
			
		||||
        "different delay times are required.",
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def delayed_on_off_filter_to_code(config, filter_id):
 | 
			
		||||
    var = cg.new_Pvariable(filter_id, config)
 | 
			
		||||
    var = cg.new_Pvariable(filter_id)
 | 
			
		||||
    await cg.register_component(var, {})
 | 
			
		||||
    if isinstance(config, dict):
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_TIME_ON], [], cg.uint32)
 | 
			
		||||
        cg.add(var.set_on_delay(template_))
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_TIME_OFF], [], cg.uint32)
 | 
			
		||||
        cg.add(var.set_off_delay(template_))
 | 
			
		||||
    else:
 | 
			
		||||
        template_ = await cg.templatable(config, [], cg.uint32)
 | 
			
		||||
        cg.add(var.set_on_delay(template_))
 | 
			
		||||
        cg.add(var.set_off_delay(template_))
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@FILTER_REGISTRY.register(
 | 
			
		||||
    "delayed_on", DelayedOnFilter, cv.positive_time_period_milliseconds
 | 
			
		||||
@register_filter(
 | 
			
		||||
    "delayed_on", DelayedOnFilter, cv.templatable(cv.positive_time_period_milliseconds)
 | 
			
		||||
)
 | 
			
		||||
async def delayed_on_filter_to_code(config, filter_id):
 | 
			
		||||
    var = cg.new_Pvariable(filter_id, config)
 | 
			
		||||
    var = cg.new_Pvariable(filter_id)
 | 
			
		||||
    await cg.register_component(var, {})
 | 
			
		||||
    template_ = await cg.templatable(config, [], cg.uint32)
 | 
			
		||||
    cg.add(var.set_delay(template_))
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@FILTER_REGISTRY.register(
 | 
			
		||||
    "delayed_off", DelayedOffFilter, cv.positive_time_period_milliseconds
 | 
			
		||||
@register_filter(
 | 
			
		||||
    "delayed_off",
 | 
			
		||||
    DelayedOffFilter,
 | 
			
		||||
    cv.templatable(cv.positive_time_period_milliseconds),
 | 
			
		||||
)
 | 
			
		||||
async def delayed_off_filter_to_code(config, filter_id):
 | 
			
		||||
    var = cg.new_Pvariable(filter_id, config)
 | 
			
		||||
    var = cg.new_Pvariable(filter_id)
 | 
			
		||||
    await cg.register_component(var, {})
 | 
			
		||||
    template_ = await cg.templatable(config, [], cg.uint32)
 | 
			
		||||
    cg.add(var.set_delay(template_))
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONF_TIME_OFF = "time_off"
 | 
			
		||||
CONF_TIME_ON = "time_on"
 | 
			
		||||
 | 
			
		||||
DEFAULT_DELAY = "1s"
 | 
			
		||||
DEFAULT_TIME_OFF = "100ms"
 | 
			
		||||
DEFAULT_TIME_ON = "900ms"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@FILTER_REGISTRY.register(
 | 
			
		||||
@register_filter(
 | 
			
		||||
    "autorepeat",
 | 
			
		||||
    AutorepeatFilter,
 | 
			
		||||
    cv.All(
 | 
			
		||||
@@ -215,7 +251,7 @@ async def autorepeat_filter_to_code(config, filter_id):
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@FILTER_REGISTRY.register("lambda", LambdaFilter, cv.returning_lambda)
 | 
			
		||||
@register_filter("lambda", LambdaFilter, cv.returning_lambda)
 | 
			
		||||
async def lambda_filter_to_code(config, filter_id):
 | 
			
		||||
    lambda_ = await cg.process_lambda(
 | 
			
		||||
        config, [(bool, "x")], return_type=cg.optional.template(bool)
 | 
			
		||||
@@ -323,6 +359,18 @@ def validate_multi_click_timing(value):
 | 
			
		||||
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_click_timing(value):
 | 
			
		||||
    for v in value:
 | 
			
		||||
        min_length = v.get(CONF_MIN_LENGTH)
 | 
			
		||||
        max_length = v.get(CONF_MAX_LENGTH)
 | 
			
		||||
        if max_length < min_length:
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                f"Max length ({max_length}) must be larger than min length ({min_length})."
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(BinarySensor),
 | 
			
		||||
@@ -342,27 +390,33 @@ BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).ex
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_CLICK): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger),
 | 
			
		||||
                cv.Optional(
 | 
			
		||||
                    CONF_MIN_LENGTH, default="50ms"
 | 
			
		||||
                ): cv.positive_time_period_milliseconds,
 | 
			
		||||
                cv.Optional(
 | 
			
		||||
                    CONF_MAX_LENGTH, default="350ms"
 | 
			
		||||
                ): cv.positive_time_period_milliseconds,
 | 
			
		||||
            }
 | 
			
		||||
        cv.Optional(CONF_ON_CLICK): cv.All(
 | 
			
		||||
            automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClickTrigger),
 | 
			
		||||
                    cv.Optional(
 | 
			
		||||
                        CONF_MIN_LENGTH, default="50ms"
 | 
			
		||||
                    ): cv.positive_time_period_milliseconds,
 | 
			
		||||
                    cv.Optional(
 | 
			
		||||
                        CONF_MAX_LENGTH, default="350ms"
 | 
			
		||||
                    ): cv.positive_time_period_milliseconds,
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            validate_click_timing,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_DOUBLE_CLICK): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DoubleClickTrigger),
 | 
			
		||||
                cv.Optional(
 | 
			
		||||
                    CONF_MIN_LENGTH, default="50ms"
 | 
			
		||||
                ): cv.positive_time_period_milliseconds,
 | 
			
		||||
                cv.Optional(
 | 
			
		||||
                    CONF_MAX_LENGTH, default="350ms"
 | 
			
		||||
                ): cv.positive_time_period_milliseconds,
 | 
			
		||||
            }
 | 
			
		||||
        cv.Optional(CONF_ON_DOUBLE_CLICK): cv.All(
 | 
			
		||||
            automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DoubleClickTrigger),
 | 
			
		||||
                    cv.Optional(
 | 
			
		||||
                        CONF_MIN_LENGTH, default="50ms"
 | 
			
		||||
                    ): cv.positive_time_period_milliseconds,
 | 
			
		||||
                    cv.Optional(
 | 
			
		||||
                        CONF_MAX_LENGTH, default="350ms"
 | 
			
		||||
                    ): cv.positive_time_period_milliseconds,
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            validate_click_timing,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_MULTI_CLICK): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -26,22 +26,20 @@ void Filter::input(bool value, bool is_initial) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
DelayedOnOffFilter::DelayedOnOffFilter(uint32_t delay) : delay_(delay) {}
 | 
			
		||||
optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) {
 | 
			
		||||
  if (value) {
 | 
			
		||||
    this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(true, is_initial); });
 | 
			
		||||
    this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
 | 
			
		||||
  } else {
 | 
			
		||||
    this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); });
 | 
			
		||||
    this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
 | 
			
		||||
  }
 | 
			
		||||
  return {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
 | 
			
		||||
 | 
			
		||||
DelayedOnFilter::DelayedOnFilter(uint32_t delay) : delay_(delay) {}
 | 
			
		||||
optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
 | 
			
		||||
  if (value) {
 | 
			
		||||
    this->set_timeout("ON", this->delay_, [this, is_initial]() { this->output(true, is_initial); });
 | 
			
		||||
    this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
 | 
			
		||||
    return {};
 | 
			
		||||
  } else {
 | 
			
		||||
    this->cancel_timeout("ON");
 | 
			
		||||
@@ -51,10 +49,9 @@ optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
 | 
			
		||||
 | 
			
		||||
float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
 | 
			
		||||
 | 
			
		||||
DelayedOffFilter::DelayedOffFilter(uint32_t delay) : delay_(delay) {}
 | 
			
		||||
optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
 | 
			
		||||
  if (!value) {
 | 
			
		||||
    this->set_timeout("OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); });
 | 
			
		||||
    this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
 | 
			
		||||
    return {};
 | 
			
		||||
  } else {
 | 
			
		||||
    this->cancel_timeout("OFF");
 | 
			
		||||
@@ -114,15 +111,6 @@ LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move
 | 
			
		||||
 | 
			
		||||
optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
 | 
			
		||||
 | 
			
		||||
optional<bool> UniqueFilter::new_value(bool value, bool is_initial) {
 | 
			
		||||
  if (this->last_value_.has_value() && *this->last_value_ == value) {
 | 
			
		||||
    return {};
 | 
			
		||||
  } else {
 | 
			
		||||
    this->last_value_ = value;
 | 
			
		||||
    return value;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace binary_sensor
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
@@ -29,38 +30,40 @@ class Filter {
 | 
			
		||||
 | 
			
		||||
class DelayedOnOffFilter : public Filter, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit DelayedOnOffFilter(uint32_t delay);
 | 
			
		||||
 | 
			
		||||
  optional<bool> new_value(bool value, bool is_initial) override;
 | 
			
		||||
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
 | 
			
		||||
  template<typename T> void set_on_delay(T delay) { this->on_delay_ = delay; }
 | 
			
		||||
  template<typename T> void set_off_delay(T delay) { this->off_delay_ = delay; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  uint32_t delay_;
 | 
			
		||||
  TemplatableValue<uint32_t> on_delay_{};
 | 
			
		||||
  TemplatableValue<uint32_t> off_delay_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class DelayedOnFilter : public Filter, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit DelayedOnFilter(uint32_t delay);
 | 
			
		||||
 | 
			
		||||
  optional<bool> new_value(bool value, bool is_initial) override;
 | 
			
		||||
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
 | 
			
		||||
  template<typename T> void set_delay(T delay) { this->delay_ = delay; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  uint32_t delay_;
 | 
			
		||||
  TemplatableValue<uint32_t> delay_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class DelayedOffFilter : public Filter, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit DelayedOffFilter(uint32_t delay);
 | 
			
		||||
 | 
			
		||||
  optional<bool> new_value(bool value, bool is_initial) override;
 | 
			
		||||
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
 | 
			
		||||
  template<typename T> void set_delay(T delay) { this->delay_ = delay; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  uint32_t delay_;
 | 
			
		||||
  TemplatableValue<uint32_t> delay_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class InvertFilter : public Filter {
 | 
			
		||||
@@ -105,14 +108,6 @@ class LambdaFilter : public Filter {
 | 
			
		||||
  std::function<optional<bool>(bool)> f_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class UniqueFilter : public Filter {
 | 
			
		||||
 public:
 | 
			
		||||
  optional<bool> new_value(bool value, bool is_initial) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  optional<bool> last_value_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace binary_sensor
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -275,6 +275,10 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl
 | 
			
		||||
  return ESP_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisement_parser_type() {
 | 
			
		||||
  return this->proxy_->get_advertisement_parser_type();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace bluetooth_proxy
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
 | 
			
		||||
  bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override;
 | 
			
		||||
  void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
 | 
			
		||||
  esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
 | 
			
		||||
 | 
			
		||||
  esp_err_t read_characteristic(uint16_t handle);
 | 
			
		||||
  esp_err_t write_characteristic(uint16_t handle, const std::string &data, bool response);
 | 
			
		||||
 
 | 
			
		||||
@@ -198,6 +198,12 @@ void BluetoothProxy::loop() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() {
 | 
			
		||||
  if (this->raw_advertisements_)
 | 
			
		||||
    return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS;
 | 
			
		||||
  return esp32_ble_tracker::AdvertisementParserType::PARSED_ADVERTISEMENTS;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
 | 
			
		||||
  for (auto *connection : this->connections_) {
 | 
			
		||||
    if (connection->get_address() == address)
 | 
			
		||||
@@ -435,6 +441,7 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection
 | 
			
		||||
  }
 | 
			
		||||
  this->api_connection_ = api_connection;
 | 
			
		||||
  this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS;
 | 
			
		||||
  this->parent_->recalculate_advertisement_parser_types();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connection) {
 | 
			
		||||
@@ -444,6 +451,7 @@ void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connecti
 | 
			
		||||
  }
 | 
			
		||||
  this->api_connection_ = nullptr;
 | 
			
		||||
  this->raw_advertisements_ = false;
 | 
			
		||||
  this->parent_->recalculate_advertisement_parser_types();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) {
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
 | 
			
		||||
  bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
 | 
			
		||||
 | 
			
		||||
  void register_connection(BluetoothConnection *connection) {
 | 
			
		||||
    this->connections_.push_back(connection);
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,10 @@ from esphome.const import (
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_PRESSURE,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_PRESSURE,
 | 
			
		||||
    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
 | 
			
		||||
    DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
@@ -17,8 +19,6 @@ from esphome.const import (
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
    ICON_GAS_CYLINDER,
 | 
			
		||||
    ICON_GAUGE,
 | 
			
		||||
    ICON_THERMOMETER,
 | 
			
		||||
    ICON_WATER_PERCENT,
 | 
			
		||||
)
 | 
			
		||||
from . import (
 | 
			
		||||
    BME680BSECComponent,
 | 
			
		||||
@@ -35,7 +35,6 @@ CONF_CO2_EQUIVALENT = "co2_equivalent"
 | 
			
		||||
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
 | 
			
		||||
UNIT_IAQ = "IAQ"
 | 
			
		||||
ICON_ACCURACY = "mdi:checkbox-marked-circle-outline"
 | 
			
		||||
ICON_TEST_TUBE = "mdi:test-tube"
 | 
			
		||||
 | 
			
		||||
TYPES = [
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
@@ -53,7 +52,6 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
        cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent),
 | 
			
		||||
        cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
            icon=ICON_THERMOMETER,
 | 
			
		||||
            accuracy_decimals=1,
 | 
			
		||||
            device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
@@ -62,16 +60,14 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_HECTOPASCAL,
 | 
			
		||||
            icon=ICON_GAUGE,
 | 
			
		||||
            accuracy_decimals=1,
 | 
			
		||||
            device_class=DEVICE_CLASS_PRESSURE,
 | 
			
		||||
            device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ).extend(
 | 
			
		||||
            {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)}
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
            icon=ICON_WATER_PERCENT,
 | 
			
		||||
            accuracy_decimals=1,
 | 
			
		||||
            device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
@@ -97,14 +93,14 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_PARTS_PER_MILLION,
 | 
			
		||||
            icon=ICON_TEST_TUBE,
 | 
			
		||||
            accuracy_decimals=1,
 | 
			
		||||
            device_class=DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_PARTS_PER_MILLION,
 | 
			
		||||
            icon=ICON_TEST_TUBE,
 | 
			
		||||
            accuracy_decimals=1,
 | 
			
		||||
            device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    CONF_MQTT_ID,
 | 
			
		||||
    DEVICE_CLASS_EMPTY,
 | 
			
		||||
    DEVICE_CLASS_IDENTIFY,
 | 
			
		||||
    DEVICE_CLASS_RESTART,
 | 
			
		||||
    DEVICE_CLASS_UPDATE,
 | 
			
		||||
)
 | 
			
		||||
@@ -24,6 +25,7 @@ IS_PLATFORM_COMPONENT = True
 | 
			
		||||
 | 
			
		||||
DEVICE_CLASSES = [
 | 
			
		||||
    DEVICE_CLASS_EMPTY,
 | 
			
		||||
    DEVICE_CLASS_IDENTIFY,
 | 
			
		||||
    DEVICE_CLASS_RESTART,
 | 
			
		||||
    DEVICE_CLASS_UPDATE,
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transm
 | 
			
		||||
  if (use_extended_id) {
 | 
			
		||||
    ESP_LOGD(TAG, "send extended id=0x%08x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGD(TAG, "send extended id=0x%03x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size);
 | 
			
		||||
    ESP_LOGD(TAG, "send standard id=0x%03x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size);
 | 
			
		||||
  }
 | 
			
		||||
  if (size > CAN_MAX_DATA_LENGTH)
 | 
			
		||||
    size = CAN_MAX_DATA_LENGTH;
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.only_with_arduino,
 | 
			
		||||
    cv.only_on(["esp32", "esp8266"]),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -34,8 +33,9 @@ async def to_code(config):
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    cg.add_define("USE_CAPTIVE_PORTAL")
 | 
			
		||||
 | 
			
		||||
    if CORE.is_esp32:
 | 
			
		||||
        cg.add_library("DNSServer", None)
 | 
			
		||||
        cg.add_library("WiFi", None)
 | 
			
		||||
    if CORE.is_esp8266:
 | 
			
		||||
        cg.add_library("DNSServer", None)
 | 
			
		||||
    if CORE.using_arduino:
 | 
			
		||||
        if CORE.is_esp32:
 | 
			
		||||
            cg.add_library("DNSServer", None)
 | 
			
		||||
            cg.add_library("WiFi", None)
 | 
			
		||||
        if CORE.is_esp8266:
 | 
			
		||||
            cg.add_library("DNSServer", None)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,3 @@
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
 | 
			
		||||
#include "captive_portal.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
@@ -46,10 +44,12 @@ void CaptivePortal::start() {
 | 
			
		||||
    this->base_->add_ota_handler();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
  this->dns_server_ = make_unique<DNSServer>();
 | 
			
		||||
  this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
 | 
			
		||||
  network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
 | 
			
		||||
  this->dns_server_->start(53, "*", (uint32_t) ip);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) {
 | 
			
		||||
    if (!this->active_ || req->host().c_str() == wifi::global_wifi_component->wifi_soft_ap_ip().str()) {
 | 
			
		||||
@@ -67,7 +67,7 @@ void CaptivePortal::start() {
 | 
			
		||||
 | 
			
		||||
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
 | 
			
		||||
  if (req->url() == "/") {
 | 
			
		||||
    AsyncWebServerResponse *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
 | 
			
		||||
    auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
 | 
			
		||||
    response->addHeader("Content-Encoding", "gzip");
 | 
			
		||||
    req->send(response);
 | 
			
		||||
    return;
 | 
			
		||||
@@ -91,5 +91,3 @@ CaptivePortal *global_captive_portal = nullptr;  // NOLINT(cppcoreguidelines-avo
 | 
			
		||||
 | 
			
		||||
}  // namespace captive_portal
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ARDUINO
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
 | 
			
		||||
#include <memory>
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
#include <DNSServer.h>
 | 
			
		||||
#endif
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
@@ -18,18 +18,22 @@ class CaptivePortal : public AsyncWebHandler, public Component {
 | 
			
		||||
  CaptivePortal(web_server_base::WebServerBase *base);
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
  void loop() override {
 | 
			
		||||
    if (this->dns_server_ != nullptr)
 | 
			
		||||
      this->dns_server_->processNextRequest();
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  void start();
 | 
			
		||||
  bool is_active() const { return this->active_; }
 | 
			
		||||
  void end() {
 | 
			
		||||
    this->active_ = false;
 | 
			
		||||
    this->base_->deinit();
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
    this->dns_server_->stop();
 | 
			
		||||
    this->dns_server_ = nullptr;
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool canHandle(AsyncWebServerRequest *request) override {
 | 
			
		||||
@@ -58,12 +62,12 @@ class CaptivePortal : public AsyncWebHandler, public Component {
 | 
			
		||||
  web_server_base::WebServerBase *base_;
 | 
			
		||||
  bool initialized_{false};
 | 
			
		||||
  bool active_{false};
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
  std::unique_ptr<DNSServer> dns_server_{nullptr};
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
extern CaptivePortal *global_captive_portal;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 | 
			
		||||
}  // namespace captive_portal
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ARDUINO
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.polling_component_schema("60s")),
 | 
			
		||||
    cv.only_on(["esp32", "esp8266"]),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/version.h"
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
@@ -13,6 +14,7 @@
 | 
			
		||||
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 4
 | 
			
		||||
#include <esp32/rom/rtc.h>
 | 
			
		||||
#include <esp_chip_info.h>
 | 
			
		||||
#else
 | 
			
		||||
#include <rom/rtc.h>
 | 
			
		||||
#endif
 | 
			
		||||
@@ -20,8 +22,12 @@
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
#ifdef USE_RP2040
 | 
			
		||||
#include <Arduino.h>
 | 
			
		||||
#else
 | 
			
		||||
#include <Esp.h>
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace debug {
 | 
			
		||||
@@ -33,6 +39,8 @@ static uint32_t get_free_heap() {
 | 
			
		||||
  return ESP.getFreeHeap();  // NOLINT(readability-static-accessed-through-instance)
 | 
			
		||||
#elif defined(USE_ESP32)
 | 
			
		||||
  return heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
 | 
			
		||||
#elif defined(USE_RP2040)
 | 
			
		||||
  return rp2040.getFreeHeap();
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -61,9 +69,9 @@ void DebugComponent::dump_config() {
 | 
			
		||||
  device_info += ESPHOME_VERSION;
 | 
			
		||||
 | 
			
		||||
  this->free_heap_ = get_free_heap();
 | 
			
		||||
  ESP_LOGD(TAG, "Free Heap Size: %u bytes", this->free_heap_);
 | 
			
		||||
  ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
#if defined(USE_ARDUINO) && !defined(USE_RP2040)
 | 
			
		||||
  const char *flash_mode;
 | 
			
		||||
  switch (ESP.getFlashChipMode()) {  // NOLINT(readability-static-accessed-through-instance)
 | 
			
		||||
    case FM_QIO:
 | 
			
		||||
@@ -272,6 +280,11 @@ void DebugComponent::dump_config() {
 | 
			
		||||
  reset_reason = ESP.getResetReason().c_str();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_RP2040
 | 
			
		||||
  ESP_LOGD(TAG, "CPU Frequency: %u", rp2040.f_cpu());
 | 
			
		||||
  device_info += "CPU Frequency: " + to_string(rp2040.f_cpu());
 | 
			
		||||
#endif  // USE_RP2040
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
  if (this->device_info_ != nullptr) {
 | 
			
		||||
    if (device_info.length() > 255)
 | 
			
		||||
@@ -289,7 +302,7 @@ void DebugComponent::loop() {
 | 
			
		||||
  uint32_t new_free_heap = get_free_heap();
 | 
			
		||||
  if (new_free_heap < this->free_heap_ / 2) {
 | 
			
		||||
    this->free_heap_ = new_free_heap;
 | 
			
		||||
    ESP_LOGD(TAG, "Free Heap Size: %u bytes", this->free_heap_);
 | 
			
		||||
    ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);
 | 
			
		||||
    this->status_momentary_warning("heap", 1000);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,10 +18,11 @@ from esphome.core import coroutine_with_priority
 | 
			
		||||
IS_PLATFORM_COMPONENT = True
 | 
			
		||||
 | 
			
		||||
display_ns = cg.esphome_ns.namespace("display")
 | 
			
		||||
Display = display_ns.class_("Display")
 | 
			
		||||
DisplayBuffer = display_ns.class_("DisplayBuffer")
 | 
			
		||||
DisplayPage = display_ns.class_("DisplayPage")
 | 
			
		||||
DisplayPagePtr = DisplayPage.operator("ptr")
 | 
			
		||||
DisplayBufferRef = DisplayBuffer.operator("ref")
 | 
			
		||||
DisplayRef = Display.operator("ref")
 | 
			
		||||
DisplayPageShowAction = display_ns.class_("DisplayPageShowAction", automation.Action)
 | 
			
		||||
DisplayPageShowNextAction = display_ns.class_(
 | 
			
		||||
    "DisplayPageShowNextAction", automation.Action
 | 
			
		||||
@@ -96,7 +97,7 @@ async def setup_display_core_(var, config):
 | 
			
		||||
        pages = []
 | 
			
		||||
        for conf in config[CONF_PAGES]:
 | 
			
		||||
            lambda_ = await cg.process_lambda(
 | 
			
		||||
                conf[CONF_LAMBDA], [(DisplayBufferRef, "it")], return_type=cg.void
 | 
			
		||||
                conf[CONF_LAMBDA], [(DisplayRef, "it")], return_type=cg.void
 | 
			
		||||
            )
 | 
			
		||||
            page = cg.new_Pvariable(conf[CONF_ID], lambda_)
 | 
			
		||||
            pages.append(page)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,37 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include "image.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace display {
 | 
			
		||||
 | 
			
		||||
class Animation : public Image {
 | 
			
		||||
 public:
 | 
			
		||||
  Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type);
 | 
			
		||||
 | 
			
		||||
  uint32_t get_animation_frame_count() const;
 | 
			
		||||
  int get_current_frame() const;
 | 
			
		||||
  void next_frame();
 | 
			
		||||
  void prev_frame();
 | 
			
		||||
 | 
			
		||||
  /** Selects a specific frame within the animation.
 | 
			
		||||
   *
 | 
			
		||||
   * @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame.
 | 
			
		||||
   */
 | 
			
		||||
  void set_frame(int frame);
 | 
			
		||||
 | 
			
		||||
  void set_loop(uint32_t start_frame, uint32_t end_frame, int count);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void update_data_start_();
 | 
			
		||||
 | 
			
		||||
  const uint8_t *animation_data_start_;
 | 
			
		||||
  int current_frame_;
 | 
			
		||||
  uint32_t animation_frame_count_;
 | 
			
		||||
  uint32_t loop_start_frame_;
 | 
			
		||||
  uint32_t loop_end_frame_;
 | 
			
		||||
  int loop_count_;
 | 
			
		||||
  int loop_current_iteration_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace display
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										343
									
								
								esphome/components/display/display.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										343
									
								
								esphome/components/display/display.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,343 @@
 | 
			
		||||
#include "display.h"
 | 
			
		||||
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace display {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "display";
 | 
			
		||||
 | 
			
		||||
const Color COLOR_OFF(0, 0, 0, 0);
 | 
			
		||||
const Color COLOR_ON(255, 255, 255, 255);
 | 
			
		||||
 | 
			
		||||
void Display::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); }
 | 
			
		||||
void Display::clear() { this->fill(COLOR_OFF); }
 | 
			
		||||
void Display::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; }
 | 
			
		||||
void HOT Display::line(int x1, int y1, int x2, int y2, Color color) {
 | 
			
		||||
  const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1;
 | 
			
		||||
  const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1;
 | 
			
		||||
  int32_t err = dx + dy;
 | 
			
		||||
 | 
			
		||||
  while (true) {
 | 
			
		||||
    this->draw_pixel_at(x1, y1, color);
 | 
			
		||||
    if (x1 == x2 && y1 == y2)
 | 
			
		||||
      break;
 | 
			
		||||
    int32_t e2 = 2 * err;
 | 
			
		||||
    if (e2 >= dy) {
 | 
			
		||||
      err += dy;
 | 
			
		||||
      x1 += sx;
 | 
			
		||||
    }
 | 
			
		||||
    if (e2 <= dx) {
 | 
			
		||||
      err += dx;
 | 
			
		||||
      y1 += sy;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void HOT Display::horizontal_line(int x, int y, int width, Color color) {
 | 
			
		||||
  // Future: Could be made more efficient by manipulating buffer directly in certain rotations.
 | 
			
		||||
  for (int i = x; i < x + width; i++)
 | 
			
		||||
    this->draw_pixel_at(i, y, color);
 | 
			
		||||
}
 | 
			
		||||
void HOT Display::vertical_line(int x, int y, int height, Color color) {
 | 
			
		||||
  // Future: Could be made more efficient by manipulating buffer directly in certain rotations.
 | 
			
		||||
  for (int i = y; i < y + height; i++)
 | 
			
		||||
    this->draw_pixel_at(x, i, color);
 | 
			
		||||
}
 | 
			
		||||
void Display::rectangle(int x1, int y1, int width, int height, Color color) {
 | 
			
		||||
  this->horizontal_line(x1, y1, width, color);
 | 
			
		||||
  this->horizontal_line(x1, y1 + height - 1, width, color);
 | 
			
		||||
  this->vertical_line(x1, y1, height, color);
 | 
			
		||||
  this->vertical_line(x1 + width - 1, y1, height, color);
 | 
			
		||||
}
 | 
			
		||||
void Display::filled_rectangle(int x1, int y1, int width, int height, Color color) {
 | 
			
		||||
  // Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses.
 | 
			
		||||
  for (int i = y1; i < y1 + height; i++) {
 | 
			
		||||
    this->horizontal_line(x1, i, width, color);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void HOT Display::circle(int center_x, int center_xy, int radius, Color color) {
 | 
			
		||||
  int dx = -radius;
 | 
			
		||||
  int dy = 0;
 | 
			
		||||
  int err = 2 - 2 * radius;
 | 
			
		||||
  int e2;
 | 
			
		||||
 | 
			
		||||
  do {
 | 
			
		||||
    this->draw_pixel_at(center_x - dx, center_xy + dy, color);
 | 
			
		||||
    this->draw_pixel_at(center_x + dx, center_xy + dy, color);
 | 
			
		||||
    this->draw_pixel_at(center_x + dx, center_xy - dy, color);
 | 
			
		||||
    this->draw_pixel_at(center_x - dx, center_xy - dy, color);
 | 
			
		||||
    e2 = err;
 | 
			
		||||
    if (e2 < dy) {
 | 
			
		||||
      err += ++dy * 2 + 1;
 | 
			
		||||
      if (-dx == dy && e2 <= dx) {
 | 
			
		||||
        e2 = 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (e2 > dx) {
 | 
			
		||||
      err += ++dx * 2 + 1;
 | 
			
		||||
    }
 | 
			
		||||
  } while (dx <= 0);
 | 
			
		||||
}
 | 
			
		||||
void Display::filled_circle(int center_x, int center_y, int radius, Color color) {
 | 
			
		||||
  int dx = -int32_t(radius);
 | 
			
		||||
  int dy = 0;
 | 
			
		||||
  int err = 2 - 2 * radius;
 | 
			
		||||
  int e2;
 | 
			
		||||
 | 
			
		||||
  do {
 | 
			
		||||
    this->draw_pixel_at(center_x - dx, center_y + dy, color);
 | 
			
		||||
    this->draw_pixel_at(center_x + dx, center_y + dy, color);
 | 
			
		||||
    this->draw_pixel_at(center_x + dx, center_y - dy, color);
 | 
			
		||||
    this->draw_pixel_at(center_x - dx, center_y - dy, color);
 | 
			
		||||
    int hline_width = 2 * (-dx) + 1;
 | 
			
		||||
    this->horizontal_line(center_x + dx, center_y + dy, hline_width, color);
 | 
			
		||||
    this->horizontal_line(center_x + dx, center_y - dy, hline_width, color);
 | 
			
		||||
    e2 = err;
 | 
			
		||||
    if (e2 < dy) {
 | 
			
		||||
      err += ++dy * 2 + 1;
 | 
			
		||||
      if (-dx == dy && e2 <= dx) {
 | 
			
		||||
        e2 = 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (e2 > dx) {
 | 
			
		||||
      err += ++dx * 2 + 1;
 | 
			
		||||
    }
 | 
			
		||||
  } while (dx <= 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) {
 | 
			
		||||
  int x_start, y_start;
 | 
			
		||||
  int width, height;
 | 
			
		||||
  this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height);
 | 
			
		||||
  font->print(x_start, y_start, this, color, text);
 | 
			
		||||
}
 | 
			
		||||
void Display::vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, va_list arg) {
 | 
			
		||||
  char buffer[256];
 | 
			
		||||
  int ret = vsnprintf(buffer, sizeof(buffer), format, arg);
 | 
			
		||||
  if (ret > 0)
 | 
			
		||||
    this->print(x, y, font, color, align, buffer);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Display::image(int x, int y, BaseImage *image, Color color_on, Color color_off) {
 | 
			
		||||
  this->image(x, y, image, ImageAlign::TOP_LEFT, color_on, color_off);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Display::image(int x, int y, BaseImage *image, ImageAlign align, Color color_on, Color color_off) {
 | 
			
		||||
  auto x_align = ImageAlign(int(align) & (int(ImageAlign::HORIZONTAL_ALIGNMENT)));
 | 
			
		||||
  auto y_align = ImageAlign(int(align) & (int(ImageAlign::VERTICAL_ALIGNMENT)));
 | 
			
		||||
 | 
			
		||||
  switch (x_align) {
 | 
			
		||||
    case ImageAlign::RIGHT:
 | 
			
		||||
      x -= image->get_width();
 | 
			
		||||
      break;
 | 
			
		||||
    case ImageAlign::CENTER_HORIZONTAL:
 | 
			
		||||
      x -= image->get_width() / 2;
 | 
			
		||||
      break;
 | 
			
		||||
    case ImageAlign::LEFT:
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  switch (y_align) {
 | 
			
		||||
    case ImageAlign::BOTTOM:
 | 
			
		||||
      y -= image->get_height();
 | 
			
		||||
      break;
 | 
			
		||||
    case ImageAlign::CENTER_VERTICAL:
 | 
			
		||||
      y -= image->get_height() / 2;
 | 
			
		||||
      break;
 | 
			
		||||
    case ImageAlign::TOP:
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  image->draw(x, y, this, color_on, color_off);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_GRAPH
 | 
			
		||||
void Display::graph(int x, int y, graph::Graph *graph, Color color_on) { graph->draw(this, x, y, color_on); }
 | 
			
		||||
void Display::legend(int x, int y, graph::Graph *graph, Color color_on) { graph->draw_legend(this, x, y, color_on); }
 | 
			
		||||
#endif  // USE_GRAPH
 | 
			
		||||
 | 
			
		||||
#ifdef USE_QR_CODE
 | 
			
		||||
void Display::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, int scale) {
 | 
			
		||||
  qr_code->draw(this, x, y, color_on, scale);
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_QR_CODE
 | 
			
		||||
 | 
			
		||||
void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1,
 | 
			
		||||
                              int *width, int *height) {
 | 
			
		||||
  int x_offset, baseline;
 | 
			
		||||
  font->measure(text, width, &x_offset, &baseline, height);
 | 
			
		||||
 | 
			
		||||
  auto x_align = TextAlign(int(align) & 0x18);
 | 
			
		||||
  auto y_align = TextAlign(int(align) & 0x07);
 | 
			
		||||
 | 
			
		||||
  switch (x_align) {
 | 
			
		||||
    case TextAlign::RIGHT:
 | 
			
		||||
      *x1 = x - *width;
 | 
			
		||||
      break;
 | 
			
		||||
    case TextAlign::CENTER_HORIZONTAL:
 | 
			
		||||
      *x1 = x - (*width) / 2;
 | 
			
		||||
      break;
 | 
			
		||||
    case TextAlign::LEFT:
 | 
			
		||||
    default:
 | 
			
		||||
      // LEFT
 | 
			
		||||
      *x1 = x;
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  switch (y_align) {
 | 
			
		||||
    case TextAlign::BOTTOM:
 | 
			
		||||
      *y1 = y - *height;
 | 
			
		||||
      break;
 | 
			
		||||
    case TextAlign::BASELINE:
 | 
			
		||||
      *y1 = y - baseline;
 | 
			
		||||
      break;
 | 
			
		||||
    case TextAlign::CENTER_VERTICAL:
 | 
			
		||||
      *y1 = y - (*height) / 2;
 | 
			
		||||
      break;
 | 
			
		||||
    case TextAlign::TOP:
 | 
			
		||||
    default:
 | 
			
		||||
      *y1 = y;
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void Display::print(int x, int y, BaseFont *font, Color color, const char *text) {
 | 
			
		||||
  this->print(x, y, font, color, TextAlign::TOP_LEFT, text);
 | 
			
		||||
}
 | 
			
		||||
void Display::print(int x, int y, BaseFont *font, TextAlign align, const char *text) {
 | 
			
		||||
  this->print(x, y, font, COLOR_ON, align, text);
 | 
			
		||||
}
 | 
			
		||||
void Display::print(int x, int y, BaseFont *font, const char *text) {
 | 
			
		||||
  this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text);
 | 
			
		||||
}
 | 
			
		||||
void Display::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) {
 | 
			
		||||
  va_list arg;
 | 
			
		||||
  va_start(arg, format);
 | 
			
		||||
  this->vprintf_(x, y, font, color, align, format, arg);
 | 
			
		||||
  va_end(arg);
 | 
			
		||||
}
 | 
			
		||||
void Display::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) {
 | 
			
		||||
  va_list arg;
 | 
			
		||||
  va_start(arg, format);
 | 
			
		||||
  this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg);
 | 
			
		||||
  va_end(arg);
 | 
			
		||||
}
 | 
			
		||||
void Display::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) {
 | 
			
		||||
  va_list arg;
 | 
			
		||||
  va_start(arg, format);
 | 
			
		||||
  this->vprintf_(x, y, font, COLOR_ON, align, format, arg);
 | 
			
		||||
  va_end(arg);
 | 
			
		||||
}
 | 
			
		||||
void Display::printf(int x, int y, BaseFont *font, const char *format, ...) {
 | 
			
		||||
  va_list arg;
 | 
			
		||||
  va_start(arg, format);
 | 
			
		||||
  this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg);
 | 
			
		||||
  va_end(arg);
 | 
			
		||||
}
 | 
			
		||||
void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; }
 | 
			
		||||
void Display::set_pages(std::vector<DisplayPage *> pages) {
 | 
			
		||||
  for (auto *page : pages)
 | 
			
		||||
    page->set_parent(this);
 | 
			
		||||
 | 
			
		||||
  for (uint32_t i = 0; i < pages.size() - 1; i++) {
 | 
			
		||||
    pages[i]->set_next(pages[i + 1]);
 | 
			
		||||
    pages[i + 1]->set_prev(pages[i]);
 | 
			
		||||
  }
 | 
			
		||||
  pages[0]->set_prev(pages[pages.size() - 1]);
 | 
			
		||||
  pages[pages.size() - 1]->set_next(pages[0]);
 | 
			
		||||
  this->show_page(pages[0]);
 | 
			
		||||
}
 | 
			
		||||
void Display::show_page(DisplayPage *page) {
 | 
			
		||||
  this->previous_page_ = this->page_;
 | 
			
		||||
  this->page_ = page;
 | 
			
		||||
  if (this->previous_page_ != this->page_) {
 | 
			
		||||
    for (auto *t : on_page_change_triggers_)
 | 
			
		||||
      t->process(this->previous_page_, this->page_);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void Display::show_next_page() { this->page_->show_next(); }
 | 
			
		||||
void Display::show_prev_page() { this->page_->show_prev(); }
 | 
			
		||||
void Display::do_update_() {
 | 
			
		||||
  if (this->auto_clear_enabled_) {
 | 
			
		||||
    this->clear();
 | 
			
		||||
  }
 | 
			
		||||
  if (this->page_ != nullptr) {
 | 
			
		||||
    this->page_->get_writer()(*this);
 | 
			
		||||
  } else if (this->writer_.has_value()) {
 | 
			
		||||
    (*this->writer_)(*this);
 | 
			
		||||
  }
 | 
			
		||||
  // remove all not ended clipping regions
 | 
			
		||||
  while (is_clipping()) {
 | 
			
		||||
    end_clipping();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) {
 | 
			
		||||
  if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to))
 | 
			
		||||
    this->trigger(from, to);
 | 
			
		||||
}
 | 
			
		||||
void Display::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) {
 | 
			
		||||
  char buffer[64];
 | 
			
		||||
  size_t ret = time.strftime(buffer, sizeof(buffer), format);
 | 
			
		||||
  if (ret > 0)
 | 
			
		||||
    this->print(x, y, font, color, align, buffer);
 | 
			
		||||
}
 | 
			
		||||
void Display::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) {
 | 
			
		||||
  this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time);
 | 
			
		||||
}
 | 
			
		||||
void Display::strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) {
 | 
			
		||||
  this->strftime(x, y, font, COLOR_ON, align, format, time);
 | 
			
		||||
}
 | 
			
		||||
void Display::strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) {
 | 
			
		||||
  this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Display::start_clipping(Rect rect) {
 | 
			
		||||
  if (!this->clipping_rectangle_.empty()) {
 | 
			
		||||
    Rect r = this->clipping_rectangle_.back();
 | 
			
		||||
    rect.shrink(r);
 | 
			
		||||
  }
 | 
			
		||||
  this->clipping_rectangle_.push_back(rect);
 | 
			
		||||
}
 | 
			
		||||
void Display::end_clipping() {
 | 
			
		||||
  if (this->clipping_rectangle_.empty()) {
 | 
			
		||||
    ESP_LOGE(TAG, "clear: Clipping is not set.");
 | 
			
		||||
  } else {
 | 
			
		||||
    this->clipping_rectangle_.pop_back();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void Display::extend_clipping(Rect add_rect) {
 | 
			
		||||
  if (this->clipping_rectangle_.empty()) {
 | 
			
		||||
    ESP_LOGE(TAG, "add: Clipping is not set.");
 | 
			
		||||
  } else {
 | 
			
		||||
    this->clipping_rectangle_.back().extend(add_rect);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void Display::shrink_clipping(Rect add_rect) {
 | 
			
		||||
  if (this->clipping_rectangle_.empty()) {
 | 
			
		||||
    ESP_LOGE(TAG, "add: Clipping is not set.");
 | 
			
		||||
  } else {
 | 
			
		||||
    this->clipping_rectangle_.back().shrink(add_rect);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
Rect Display::get_clipping() {
 | 
			
		||||
  if (this->clipping_rectangle_.empty()) {
 | 
			
		||||
    return Rect();
 | 
			
		||||
  } else {
 | 
			
		||||
    return this->clipping_rectangle_.back();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
 | 
			
		||||
void DisplayPage::show() { this->parent_->show_page(this); }
 | 
			
		||||
void DisplayPage::show_next() { this->next_->show(); }
 | 
			
		||||
void DisplayPage::show_prev() { this->prev_->show(); }
 | 
			
		||||
void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; }
 | 
			
		||||
void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; }
 | 
			
		||||
void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; }
 | 
			
		||||
const display_writer_t &DisplayPage::get_writer() const { return this->writer_; }
 | 
			
		||||
 | 
			
		||||
}  // namespace display
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										566
									
								
								esphome/components/display/display.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										566
									
								
								esphome/components/display/display.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,566 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <cstdarg>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#include "rect.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/color.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/time.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_GRAPH
 | 
			
		||||
#include "esphome/components/graph/graph.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_QR_CODE
 | 
			
		||||
#include "esphome/components/qr_code/qr_code.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace display {
 | 
			
		||||
 | 
			
		||||
/** TextAlign is used to tell the display class how to position a piece of text. By default
 | 
			
		||||
 * the coordinates you enter for the print*() functions take the upper left corner of the text
 | 
			
		||||
 * as the "anchor" point. You can customize this behavior to, for example, make the coordinates
 | 
			
		||||
 * refer to the *center* of the text.
 | 
			
		||||
 *
 | 
			
		||||
 * All text alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis
 | 
			
		||||
 * these options are allowed:
 | 
			
		||||
 *
 | 
			
		||||
 * - LEFT (x-coordinate of anchor point is on left)
 | 
			
		||||
 * - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the text)
 | 
			
		||||
 * - RIGHT (x-coordinate of anchor point is on right)
 | 
			
		||||
 *
 | 
			
		||||
 * For the Y-Axis alignment these options are allowed:
 | 
			
		||||
 *
 | 
			
		||||
 * - TOP (y-coordinate of anchor is on the top of the text)
 | 
			
		||||
 * - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the text)
 | 
			
		||||
 * - BASELINE (y-coordinate of anchor is on the baseline of the text)
 | 
			
		||||
 * - BOTTOM (y-coordinate of anchor is on the bottom of the text)
 | 
			
		||||
 *
 | 
			
		||||
 * These options are then combined to create combined TextAlignment options like:
 | 
			
		||||
 * - TOP_LEFT (default)
 | 
			
		||||
 * - CENTER (anchor point is in the middle of the text bounds)
 | 
			
		||||
 * - ...
 | 
			
		||||
 */
 | 
			
		||||
enum class TextAlign {
 | 
			
		||||
  TOP = 0x00,
 | 
			
		||||
  CENTER_VERTICAL = 0x01,
 | 
			
		||||
  BASELINE = 0x02,
 | 
			
		||||
  BOTTOM = 0x04,
 | 
			
		||||
 | 
			
		||||
  LEFT = 0x00,
 | 
			
		||||
  CENTER_HORIZONTAL = 0x08,
 | 
			
		||||
  RIGHT = 0x10,
 | 
			
		||||
 | 
			
		||||
  TOP_LEFT = TOP | LEFT,
 | 
			
		||||
  TOP_CENTER = TOP | CENTER_HORIZONTAL,
 | 
			
		||||
  TOP_RIGHT = TOP | RIGHT,
 | 
			
		||||
 | 
			
		||||
  CENTER_LEFT = CENTER_VERTICAL | LEFT,
 | 
			
		||||
  CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL,
 | 
			
		||||
  CENTER_RIGHT = CENTER_VERTICAL | RIGHT,
 | 
			
		||||
 | 
			
		||||
  BASELINE_LEFT = BASELINE | LEFT,
 | 
			
		||||
  BASELINE_CENTER = BASELINE | CENTER_HORIZONTAL,
 | 
			
		||||
  BASELINE_RIGHT = BASELINE | RIGHT,
 | 
			
		||||
 | 
			
		||||
  BOTTOM_LEFT = BOTTOM | LEFT,
 | 
			
		||||
  BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL,
 | 
			
		||||
  BOTTOM_RIGHT = BOTTOM | RIGHT,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** ImageAlign is used to tell the display class how to position a image. By default
 | 
			
		||||
 * the coordinates you enter for the image() functions take the upper left corner of the image
 | 
			
		||||
 * as the "anchor" point. You can customize this behavior to, for example, make the coordinates
 | 
			
		||||
 * refer to the *center* of the image.
 | 
			
		||||
 *
 | 
			
		||||
 * All image alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis
 | 
			
		||||
 * these options are allowed:
 | 
			
		||||
 *
 | 
			
		||||
 * - LEFT (x-coordinate of anchor point is on left)
 | 
			
		||||
 * - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the image)
 | 
			
		||||
 * - RIGHT (x-coordinate of anchor point is on right)
 | 
			
		||||
 *
 | 
			
		||||
 * For the Y-Axis alignment these options are allowed:
 | 
			
		||||
 *
 | 
			
		||||
 * - TOP (y-coordinate of anchor is on the top of the image)
 | 
			
		||||
 * - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the image)
 | 
			
		||||
 * - BOTTOM (y-coordinate of anchor is on the bottom of the image)
 | 
			
		||||
 *
 | 
			
		||||
 * These options are then combined to create combined TextAlignment options like:
 | 
			
		||||
 * - TOP_LEFT (default)
 | 
			
		||||
 * - CENTER (anchor point is in the middle of the image bounds)
 | 
			
		||||
 * - ...
 | 
			
		||||
 */
 | 
			
		||||
enum class ImageAlign {
 | 
			
		||||
  TOP = 0x00,
 | 
			
		||||
  CENTER_VERTICAL = 0x01,
 | 
			
		||||
  BOTTOM = 0x02,
 | 
			
		||||
 | 
			
		||||
  LEFT = 0x00,
 | 
			
		||||
  CENTER_HORIZONTAL = 0x04,
 | 
			
		||||
  RIGHT = 0x08,
 | 
			
		||||
 | 
			
		||||
  TOP_LEFT = TOP | LEFT,
 | 
			
		||||
  TOP_CENTER = TOP | CENTER_HORIZONTAL,
 | 
			
		||||
  TOP_RIGHT = TOP | RIGHT,
 | 
			
		||||
 | 
			
		||||
  CENTER_LEFT = CENTER_VERTICAL | LEFT,
 | 
			
		||||
  CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL,
 | 
			
		||||
  CENTER_RIGHT = CENTER_VERTICAL | RIGHT,
 | 
			
		||||
 | 
			
		||||
  BOTTOM_LEFT = BOTTOM | LEFT,
 | 
			
		||||
  BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL,
 | 
			
		||||
  BOTTOM_RIGHT = BOTTOM | RIGHT,
 | 
			
		||||
 | 
			
		||||
  HORIZONTAL_ALIGNMENT = LEFT | CENTER_HORIZONTAL | RIGHT,
 | 
			
		||||
  VERTICAL_ALIGNMENT = TOP | CENTER_VERTICAL | BOTTOM
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum DisplayType {
 | 
			
		||||
  DISPLAY_TYPE_BINARY = 1,
 | 
			
		||||
  DISPLAY_TYPE_GRAYSCALE = 2,
 | 
			
		||||
  DISPLAY_TYPE_COLOR = 3,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum DisplayRotation {
 | 
			
		||||
  DISPLAY_ROTATION_0_DEGREES = 0,
 | 
			
		||||
  DISPLAY_ROTATION_90_DEGREES = 90,
 | 
			
		||||
  DISPLAY_ROTATION_180_DEGREES = 180,
 | 
			
		||||
  DISPLAY_ROTATION_270_DEGREES = 270,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class Display;
 | 
			
		||||
class DisplayPage;
 | 
			
		||||
class DisplayOnPageChangeTrigger;
 | 
			
		||||
 | 
			
		||||
using display_writer_t = std::function<void(Display &)>;
 | 
			
		||||
 | 
			
		||||
#define LOG_DISPLAY(prefix, type, obj) \
 | 
			
		||||
  if ((obj) != nullptr) { \
 | 
			
		||||
    ESP_LOGCONFIG(TAG, prefix type); \
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "%s  Rotations: %d °", prefix, (obj)->rotation_); \
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "%s  Dimensions: %dpx x %dpx", prefix, (obj)->get_width(), (obj)->get_height()); \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
/// Turn the pixel OFF.
 | 
			
		||||
extern const Color COLOR_OFF;
 | 
			
		||||
/// Turn the pixel ON.
 | 
			
		||||
extern const Color COLOR_ON;
 | 
			
		||||
 | 
			
		||||
class BaseImage {
 | 
			
		||||
 public:
 | 
			
		||||
  virtual void draw(int x, int y, Display *display, Color color_on, Color color_off) = 0;
 | 
			
		||||
  virtual int get_width() const = 0;
 | 
			
		||||
  virtual int get_height() const = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class BaseFont {
 | 
			
		||||
 public:
 | 
			
		||||
  virtual void print(int x, int y, Display *display, Color color, const char *text) = 0;
 | 
			
		||||
  virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class Display {
 | 
			
		||||
 public:
 | 
			
		||||
  /// Fill the entire screen with the given color.
 | 
			
		||||
  virtual void fill(Color color);
 | 
			
		||||
  /// Clear the entire screen by filling it with OFF pixels.
 | 
			
		||||
  void clear();
 | 
			
		||||
 | 
			
		||||
  /// Get the width of the image in pixels with rotation applied.
 | 
			
		||||
  virtual int get_width() = 0;
 | 
			
		||||
  /// Get the height of the image in pixels with rotation applied.
 | 
			
		||||
  virtual int get_height() = 0;
 | 
			
		||||
 | 
			
		||||
  /// Set a single pixel at the specified coordinates to default color.
 | 
			
		||||
  inline void draw_pixel_at(int x, int y) { this->draw_pixel_at(x, y, COLOR_ON); }
 | 
			
		||||
 | 
			
		||||
  /// Set a single pixel at the specified coordinates to the given color.
 | 
			
		||||
  virtual void draw_pixel_at(int x, int y, Color color) = 0;
 | 
			
		||||
 | 
			
		||||
  /// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color.
 | 
			
		||||
  void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /// Draw a horizontal line from the point [x,y] to [x+width,y] with the given color.
 | 
			
		||||
  void horizontal_line(int x, int y, int width, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /// Draw a vertical line from the point [x,y] to [x,y+width] with the given color.
 | 
			
		||||
  void vertical_line(int x, int y, int height, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /// Draw the outline of a rectangle with the top left point at [x1,y1] and the bottom right point at
 | 
			
		||||
  /// [x1+width,y1+height].
 | 
			
		||||
  void rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /// Fill a rectangle with the top left point at [x1,y1] and the bottom right point at [x1+width,y1+height].
 | 
			
		||||
  void filled_rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /// Draw the outline of a circle centered around [center_x,center_y] with the radius radius with the given color.
 | 
			
		||||
  void circle(int center_x, int center_xy, int radius, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /// Fill a circle centered around [center_x,center_y] with the radius radius with the given color.
 | 
			
		||||
  void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /** Print `text` with the anchor point at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param y The y coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param color The color to draw the text with.
 | 
			
		||||
   * @param align The alignment of the text.
 | 
			
		||||
   * @param text The text to draw.
 | 
			
		||||
   */
 | 
			
		||||
  void print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text);
 | 
			
		||||
 | 
			
		||||
  /** Print `text` with the top left at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param color The color to draw the text with.
 | 
			
		||||
   * @param text The text to draw.
 | 
			
		||||
   */
 | 
			
		||||
  void print(int x, int y, BaseFont *font, Color color, const char *text);
 | 
			
		||||
 | 
			
		||||
  /** Print `text` with the anchor point at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param y The y coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param align The alignment of the text.
 | 
			
		||||
   * @param text The text to draw.
 | 
			
		||||
   */
 | 
			
		||||
  void print(int x, int y, BaseFont *font, TextAlign align, const char *text);
 | 
			
		||||
 | 
			
		||||
  /** Print `text` with the top left at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param text The text to draw.
 | 
			
		||||
   */
 | 
			
		||||
  void print(int x, int y, BaseFont *font, const char *text);
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param y The y coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param color The color to draw the text with.
 | 
			
		||||
   * @param align The alignment of the text.
 | 
			
		||||
   * @param format The format to use.
 | 
			
		||||
   * @param ... The arguments to use for the text formatting.
 | 
			
		||||
   */
 | 
			
		||||
  void printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...)
 | 
			
		||||
      __attribute__((format(printf, 7, 8)));
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param color The color to draw the text with.
 | 
			
		||||
   * @param format The format to use.
 | 
			
		||||
   * @param ... The arguments to use for the text formatting.
 | 
			
		||||
   */
 | 
			
		||||
  void printf(int x, int y, BaseFont *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7)));
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param y The y coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param align The alignment of the text.
 | 
			
		||||
   * @param format The format to use.
 | 
			
		||||
   * @param ... The arguments to use for the text formatting.
 | 
			
		||||
   */
 | 
			
		||||
  void printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...)
 | 
			
		||||
      __attribute__((format(printf, 6, 7)));
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param format The format to use.
 | 
			
		||||
   * @param ... The arguments to use for the text formatting.
 | 
			
		||||
   */
 | 
			
		||||
  void printf(int x, int y, BaseFont *font, const char *format, ...) __attribute__((format(printf, 5, 6)));
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param y The y coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param color The color to draw the text with.
 | 
			
		||||
   * @param align The alignment of the text.
 | 
			
		||||
   * @param format The strftime format to use.
 | 
			
		||||
   * @param time The time to format.
 | 
			
		||||
   */
 | 
			
		||||
  void strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time)
 | 
			
		||||
      __attribute__((format(strftime, 7, 0)));
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param color The color to draw the text with.
 | 
			
		||||
   * @param format The strftime format to use.
 | 
			
		||||
   * @param time The time to format.
 | 
			
		||||
   */
 | 
			
		||||
  void strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time)
 | 
			
		||||
      __attribute__((format(strftime, 6, 0)));
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param y The y coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param align The alignment of the text.
 | 
			
		||||
   * @param format The strftime format to use.
 | 
			
		||||
   * @param time The time to format.
 | 
			
		||||
   */
 | 
			
		||||
  void strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time)
 | 
			
		||||
      __attribute__((format(strftime, 6, 0)));
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param format The strftime format to use.
 | 
			
		||||
   * @param time The time to format.
 | 
			
		||||
   */
 | 
			
		||||
  void strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0)));
 | 
			
		||||
 | 
			
		||||
  /** Draw the `image` with the top-left corner at [x,y] to the screen.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param image The image to draw.
 | 
			
		||||
   * @param color_on The color to replace in binary images for the on bits.
 | 
			
		||||
   * @param color_off The color to replace in binary images for the off bits.
 | 
			
		||||
   */
 | 
			
		||||
  void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
 | 
			
		||||
 | 
			
		||||
  /** Draw the `image` at [x,y] to the screen.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param image The image to draw.
 | 
			
		||||
   * @param align The alignment of the image.
 | 
			
		||||
   * @param color_on The color to replace in binary images for the on bits.
 | 
			
		||||
   * @param color_off The color to replace in binary images for the off bits.
 | 
			
		||||
   */
 | 
			
		||||
  void image(int x, int y, BaseImage *image, ImageAlign align, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
 | 
			
		||||
 | 
			
		||||
#ifdef USE_GRAPH
 | 
			
		||||
  /** Draw the `graph` with the top-left corner at [x,y] to the screen.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param graph The graph id to draw
 | 
			
		||||
   * @param color_on The color to replace in binary images for the on bits.
 | 
			
		||||
   */
 | 
			
		||||
  void graph(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /** Draw the `legend` for graph with the top-left corner at [x,y] to the screen.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param graph The graph id for which the legend applies to
 | 
			
		||||
   * @param graph The graph id for which the legend applies to
 | 
			
		||||
   * @param graph The graph id for which the legend applies to
 | 
			
		||||
   * @param name_font The font used for the trace name
 | 
			
		||||
   * @param value_font The font used for the trace value and units
 | 
			
		||||
   * @param color_on The color of the border
 | 
			
		||||
   */
 | 
			
		||||
  void legend(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON);
 | 
			
		||||
#endif  // USE_GRAPH
 | 
			
		||||
 | 
			
		||||
#ifdef USE_QR_CODE
 | 
			
		||||
  /** Draw the `qr_code` with the top-left corner at [x,y] to the screen.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param qr_code The qr_code to draw
 | 
			
		||||
   * @param color_on The color to replace in binary images for the on bits.
 | 
			
		||||
   */
 | 
			
		||||
  void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  /** Get the text bounds of the given string.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate to place the string at, can be 0 if only interested in dimensions.
 | 
			
		||||
   * @param y The y coordinate to place the string at, can be 0 if only interested in dimensions.
 | 
			
		||||
   * @param text The text to measure.
 | 
			
		||||
   * @param font The font to measure the text bounds with.
 | 
			
		||||
   * @param align The alignment of the text. Set to TextAlign::TOP_LEFT if only interested in dimensions.
 | 
			
		||||
   * @param x1 A pointer to store the returned x coordinate of the upper left corner in.
 | 
			
		||||
   * @param y1 A pointer to store the returned y coordinate of the upper left corner in.
 | 
			
		||||
   * @param width A pointer to store the returned text width in.
 | 
			
		||||
   * @param height A pointer to store the returned text height in.
 | 
			
		||||
   */
 | 
			
		||||
  void get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, int *width,
 | 
			
		||||
                       int *height);
 | 
			
		||||
 | 
			
		||||
  /// Internal method to set the display writer lambda.
 | 
			
		||||
  void set_writer(display_writer_t &&writer);
 | 
			
		||||
 | 
			
		||||
  void show_page(DisplayPage *page);
 | 
			
		||||
  void show_next_page();
 | 
			
		||||
  void show_prev_page();
 | 
			
		||||
 | 
			
		||||
  void set_pages(std::vector<DisplayPage *> pages);
 | 
			
		||||
 | 
			
		||||
  const DisplayPage *get_active_page() const { return this->page_; }
 | 
			
		||||
 | 
			
		||||
  void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); }
 | 
			
		||||
 | 
			
		||||
  /// Internal method to set the display rotation with.
 | 
			
		||||
  void set_rotation(DisplayRotation rotation);
 | 
			
		||||
 | 
			
		||||
  // Internal method to set display auto clearing.
 | 
			
		||||
  void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; }
 | 
			
		||||
 | 
			
		||||
  DisplayRotation get_rotation() const { return this->rotation_; }
 | 
			
		||||
 | 
			
		||||
  /** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
 | 
			
		||||
   * returns the type the display is currently configured to.
 | 
			
		||||
   */
 | 
			
		||||
  virtual DisplayType get_display_type() = 0;
 | 
			
		||||
 | 
			
		||||
  /** Set the clipping rectangle for further drawing
 | 
			
		||||
   *
 | 
			
		||||
   * @param[in]  rect:       Pointer to Rect for clipping (or NULL for entire screen)
 | 
			
		||||
   *
 | 
			
		||||
   * return true if success, false if error
 | 
			
		||||
   */
 | 
			
		||||
  void start_clipping(Rect rect);
 | 
			
		||||
  void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) {
 | 
			
		||||
    start_clipping(Rect(left, top, right - left, bottom - top));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /** Add a rectangular region to the invalidation region
 | 
			
		||||
   * - This is usually called when an element has been modified
 | 
			
		||||
   *
 | 
			
		||||
   * @param[in]  rect: Rectangle to add to the invalidation region
 | 
			
		||||
   */
 | 
			
		||||
  void extend_clipping(Rect rect);
 | 
			
		||||
  void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) {
 | 
			
		||||
    this->extend_clipping(Rect(left, top, right - left, bottom - top));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /** substract a rectangular region to the invalidation region
 | 
			
		||||
   *  - This is usually called when an element has been modified
 | 
			
		||||
   *
 | 
			
		||||
   * @param[in]  rect: Rectangle to add to the invalidation region
 | 
			
		||||
   */
 | 
			
		||||
  void shrink_clipping(Rect rect);
 | 
			
		||||
  void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
 | 
			
		||||
    this->shrink_clipping(Rect(left, top, right - left, bottom - top));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /** Reset the invalidation region
 | 
			
		||||
   */
 | 
			
		||||
  void end_clipping();
 | 
			
		||||
 | 
			
		||||
  /** Get the current the clipping rectangle
 | 
			
		||||
   *
 | 
			
		||||
   * return rect for active clipping region
 | 
			
		||||
   */
 | 
			
		||||
  Rect get_clipping();
 | 
			
		||||
 | 
			
		||||
  bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void vprintf_(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, va_list arg);
 | 
			
		||||
 | 
			
		||||
  void do_update_();
 | 
			
		||||
 | 
			
		||||
  DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES};
 | 
			
		||||
  optional<display_writer_t> writer_{};
 | 
			
		||||
  DisplayPage *page_{nullptr};
 | 
			
		||||
  DisplayPage *previous_page_{nullptr};
 | 
			
		||||
  std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
 | 
			
		||||
  bool auto_clear_enabled_{true};
 | 
			
		||||
  std::vector<Rect> clipping_rectangle_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class DisplayPage {
 | 
			
		||||
 public:
 | 
			
		||||
  DisplayPage(display_writer_t writer);
 | 
			
		||||
  void show();
 | 
			
		||||
  void show_next();
 | 
			
		||||
  void show_prev();
 | 
			
		||||
  void set_parent(Display *parent);
 | 
			
		||||
  void set_prev(DisplayPage *prev);
 | 
			
		||||
  void set_next(DisplayPage *next);
 | 
			
		||||
  const display_writer_t &get_writer() const;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  Display *parent_;
 | 
			
		||||
  display_writer_t writer_;
 | 
			
		||||
  DisplayPage *prev_{nullptr};
 | 
			
		||||
  DisplayPage *next_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class DisplayPageShowAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  TEMPLATABLE_VALUE(DisplayPage *, page)
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    auto *page = this->page_.value(x...);
 | 
			
		||||
    if (page != nullptr) {
 | 
			
		||||
      page->show();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class DisplayPageShowNextAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  DisplayPageShowNextAction(Display *buffer) : buffer_(buffer) {}
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override { this->buffer_->show_next_page(); }
 | 
			
		||||
 | 
			
		||||
  Display *buffer_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class DisplayPageShowPrevAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  DisplayPageShowPrevAction(Display *buffer) : buffer_(buffer) {}
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override { this->buffer_->show_prev_page(); }
 | 
			
		||||
 | 
			
		||||
  Display *buffer_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class DisplayIsDisplayingPageCondition : public Condition<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {}
 | 
			
		||||
 | 
			
		||||
  void set_page(DisplayPage *page) { this->page_ = page; }
 | 
			
		||||
  bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  Display *parent_;
 | 
			
		||||
  DisplayPage *page_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class DisplayOnPageChangeTrigger : public Trigger<DisplayPage *, DisplayPage *> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit DisplayOnPageChangeTrigger(Display *parent) { parent->add_on_page_change_trigger(this); }
 | 
			
		||||
  void process(DisplayPage *from, DisplayPage *to);
 | 
			
		||||
  void set_from(DisplayPage *p) { this->from_ = p; }
 | 
			
		||||
  void set_to(DisplayPage *p) { this->to_ = p; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  DisplayPage *from_{nullptr};
 | 
			
		||||
  DisplayPage *to_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace display
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,111 +1,15 @@
 | 
			
		||||
#include "display_buffer.h"
 | 
			
		||||
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/color.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include "animation.h"
 | 
			
		||||
#include "image.h"
 | 
			
		||||
#include "font.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace display {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "display";
 | 
			
		||||
 | 
			
		||||
const Color COLOR_OFF(0, 0, 0, 0);
 | 
			
		||||
const Color COLOR_ON(255, 255, 255, 255);
 | 
			
		||||
 | 
			
		||||
void Rect::expand(int16_t horizontal, int16_t vertical) {
 | 
			
		||||
  if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) {
 | 
			
		||||
    this->x = this->x - horizontal;
 | 
			
		||||
    this->y = this->y - vertical;
 | 
			
		||||
    this->w = this->w + (2 * horizontal);
 | 
			
		||||
    this->h = this->h + (2 * vertical);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Rect::extend(Rect rect) {
 | 
			
		||||
  if (!this->is_set()) {
 | 
			
		||||
    this->x = rect.x;
 | 
			
		||||
    this->y = rect.y;
 | 
			
		||||
    this->w = rect.w;
 | 
			
		||||
    this->h = rect.h;
 | 
			
		||||
  } else {
 | 
			
		||||
    if (this->x > rect.x) {
 | 
			
		||||
      this->w = this->w + (this->x - rect.x);
 | 
			
		||||
      this->x = rect.x;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->y > rect.y) {
 | 
			
		||||
      this->h = this->h + (this->y - rect.y);
 | 
			
		||||
      this->y = rect.y;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->x2() < rect.x2()) {
 | 
			
		||||
      this->w = rect.x2() - this->x;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->y2() < rect.y2()) {
 | 
			
		||||
      this->h = rect.y2() - this->y;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void Rect::shrink(Rect rect) {
 | 
			
		||||
  if (!this->inside(rect)) {
 | 
			
		||||
    (*this) = Rect();
 | 
			
		||||
  } else {
 | 
			
		||||
    if (this->x2() > rect.x2()) {
 | 
			
		||||
      this->w = rect.x2() - this->x;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->x < rect.x) {
 | 
			
		||||
      this->w = this->w + (this->x - rect.x);
 | 
			
		||||
      this->x = rect.x;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->y2() > rect.y2()) {
 | 
			
		||||
      this->h = rect.y2() - this->y;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->y < rect.y) {
 | 
			
		||||
      this->h = this->h + (this->y - rect.y);
 | 
			
		||||
      this->y = rect.y;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Rect::equal(Rect rect) {
 | 
			
		||||
  return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) {  // NOLINT
 | 
			
		||||
  if (!this->is_set()) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (absolute) {
 | 
			
		||||
    return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2()));
 | 
			
		||||
  } else {
 | 
			
		||||
    return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Rect::inside(Rect rect, bool absolute) {
 | 
			
		||||
  if (!this->is_set() || !rect.is_set()) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (absolute) {
 | 
			
		||||
    return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y));
 | 
			
		||||
  } else {
 | 
			
		||||
    return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Rect::info(const std::string &prefix) {
 | 
			
		||||
  if (this->is_set()) {
 | 
			
		||||
    ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(),
 | 
			
		||||
             this->y2());
 | 
			
		||||
  } else
 | 
			
		||||
    ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DisplayBuffer::init_internal_(uint32_t buffer_length) {
 | 
			
		||||
  ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
 | 
			
		||||
  this->buffer_ = allocator.allocate(buffer_length);
 | 
			
		||||
@@ -116,8 +20,6 @@ void DisplayBuffer::init_internal_(uint32_t buffer_length) {
 | 
			
		||||
  this->clear();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DisplayBuffer::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); }
 | 
			
		||||
void DisplayBuffer::clear() { this->fill(COLOR_OFF); }
 | 
			
		||||
int DisplayBuffer::get_width() {
 | 
			
		||||
  switch (this->rotation_) {
 | 
			
		||||
    case DISPLAY_ROTATION_90_DEGREES:
 | 
			
		||||
@@ -129,6 +31,7 @@ int DisplayBuffer::get_width() {
 | 
			
		||||
      return this->get_width_internal();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int DisplayBuffer::get_height() {
 | 
			
		||||
  switch (this->rotation_) {
 | 
			
		||||
    case DISPLAY_ROTATION_0_DEGREES:
 | 
			
		||||
@@ -140,7 +43,7 @@ int DisplayBuffer::get_height() {
 | 
			
		||||
      return this->get_width_internal();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; }
 | 
			
		||||
 | 
			
		||||
void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
 | 
			
		||||
  if (!this->get_clipping().inside(x, y))
 | 
			
		||||
    return;  // NOLINT
 | 
			
		||||
@@ -164,372 +67,6 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
 | 
			
		||||
  this->draw_absolute_pixel_internal(x, y, color);
 | 
			
		||||
  App.feed_wdt();
 | 
			
		||||
}
 | 
			
		||||
void HOT DisplayBuffer::line(int x1, int y1, int x2, int y2, Color color) {
 | 
			
		||||
  const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1;
 | 
			
		||||
  const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1;
 | 
			
		||||
  int32_t err = dx + dy;
 | 
			
		||||
 | 
			
		||||
  while (true) {
 | 
			
		||||
    this->draw_pixel_at(x1, y1, color);
 | 
			
		||||
    if (x1 == x2 && y1 == y2)
 | 
			
		||||
      break;
 | 
			
		||||
    int32_t e2 = 2 * err;
 | 
			
		||||
    if (e2 >= dy) {
 | 
			
		||||
      err += dy;
 | 
			
		||||
      x1 += sx;
 | 
			
		||||
    }
 | 
			
		||||
    if (e2 <= dx) {
 | 
			
		||||
      err += dx;
 | 
			
		||||
      y1 += sy;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void HOT DisplayBuffer::horizontal_line(int x, int y, int width, Color color) {
 | 
			
		||||
  // Future: Could be made more efficient by manipulating buffer directly in certain rotations.
 | 
			
		||||
  for (int i = x; i < x + width; i++)
 | 
			
		||||
    this->draw_pixel_at(i, y, color);
 | 
			
		||||
}
 | 
			
		||||
void HOT DisplayBuffer::vertical_line(int x, int y, int height, Color color) {
 | 
			
		||||
  // Future: Could be made more efficient by manipulating buffer directly in certain rotations.
 | 
			
		||||
  for (int i = y; i < y + height; i++)
 | 
			
		||||
    this->draw_pixel_at(x, i, color);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::rectangle(int x1, int y1, int width, int height, Color color) {
 | 
			
		||||
  this->horizontal_line(x1, y1, width, color);
 | 
			
		||||
  this->horizontal_line(x1, y1 + height - 1, width, color);
 | 
			
		||||
  this->vertical_line(x1, y1, height, color);
 | 
			
		||||
  this->vertical_line(x1 + width - 1, y1, height, color);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::filled_rectangle(int x1, int y1, int width, int height, Color color) {
 | 
			
		||||
  // Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses.
 | 
			
		||||
  for (int i = y1; i < y1 + height; i++) {
 | 
			
		||||
    this->horizontal_line(x1, i, width, color);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void HOT DisplayBuffer::circle(int center_x, int center_xy, int radius, Color color) {
 | 
			
		||||
  int dx = -radius;
 | 
			
		||||
  int dy = 0;
 | 
			
		||||
  int err = 2 - 2 * radius;
 | 
			
		||||
  int e2;
 | 
			
		||||
 | 
			
		||||
  do {
 | 
			
		||||
    this->draw_pixel_at(center_x - dx, center_xy + dy, color);
 | 
			
		||||
    this->draw_pixel_at(center_x + dx, center_xy + dy, color);
 | 
			
		||||
    this->draw_pixel_at(center_x + dx, center_xy - dy, color);
 | 
			
		||||
    this->draw_pixel_at(center_x - dx, center_xy - dy, color);
 | 
			
		||||
    e2 = err;
 | 
			
		||||
    if (e2 < dy) {
 | 
			
		||||
      err += ++dy * 2 + 1;
 | 
			
		||||
      if (-dx == dy && e2 <= dx) {
 | 
			
		||||
        e2 = 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (e2 > dx) {
 | 
			
		||||
      err += ++dx * 2 + 1;
 | 
			
		||||
    }
 | 
			
		||||
  } while (dx <= 0);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, Color color) {
 | 
			
		||||
  int dx = -int32_t(radius);
 | 
			
		||||
  int dy = 0;
 | 
			
		||||
  int err = 2 - 2 * radius;
 | 
			
		||||
  int e2;
 | 
			
		||||
 | 
			
		||||
  do {
 | 
			
		||||
    this->draw_pixel_at(center_x - dx, center_y + dy, color);
 | 
			
		||||
    this->draw_pixel_at(center_x + dx, center_y + dy, color);
 | 
			
		||||
    this->draw_pixel_at(center_x + dx, center_y - dy, color);
 | 
			
		||||
    this->draw_pixel_at(center_x - dx, center_y - dy, color);
 | 
			
		||||
    int hline_width = 2 * (-dx) + 1;
 | 
			
		||||
    this->horizontal_line(center_x + dx, center_y + dy, hline_width, color);
 | 
			
		||||
    this->horizontal_line(center_x + dx, center_y - dy, hline_width, color);
 | 
			
		||||
    e2 = err;
 | 
			
		||||
    if (e2 < dy) {
 | 
			
		||||
      err += ++dy * 2 + 1;
 | 
			
		||||
      if (-dx == dy && e2 <= dx) {
 | 
			
		||||
        e2 = 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (e2 > dx) {
 | 
			
		||||
      err += ++dx * 2 + 1;
 | 
			
		||||
    }
 | 
			
		||||
  } while (dx <= 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align, const char *text) {
 | 
			
		||||
  int x_start, y_start;
 | 
			
		||||
  int width, height;
 | 
			
		||||
  this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height);
 | 
			
		||||
 | 
			
		||||
  int i = 0;
 | 
			
		||||
  int x_at = x_start;
 | 
			
		||||
  while (text[i] != '\0') {
 | 
			
		||||
    int match_length;
 | 
			
		||||
    int glyph_n = font->match_next_glyph(text + i, &match_length);
 | 
			
		||||
    if (glyph_n < 0) {
 | 
			
		||||
      // Unknown char, skip
 | 
			
		||||
      ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
 | 
			
		||||
      if (!font->get_glyphs().empty()) {
 | 
			
		||||
        uint8_t glyph_width = font->get_glyphs()[0].glyph_data_->width;
 | 
			
		||||
        for (int glyph_x = 0; glyph_x < glyph_width; glyph_x++) {
 | 
			
		||||
          for (int glyph_y = 0; glyph_y < height; glyph_y++)
 | 
			
		||||
            this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color);
 | 
			
		||||
        }
 | 
			
		||||
        x_at += glyph_width;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      i++;
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const Glyph &glyph = font->get_glyphs()[glyph_n];
 | 
			
		||||
    int scan_x1, scan_y1, scan_width, scan_height;
 | 
			
		||||
    glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
 | 
			
		||||
 | 
			
		||||
    {
 | 
			
		||||
      const int glyph_x_max = scan_x1 + scan_width;
 | 
			
		||||
      const int glyph_y_max = scan_y1 + scan_height;
 | 
			
		||||
      for (int glyph_x = scan_x1; glyph_x < glyph_x_max; glyph_x++) {
 | 
			
		||||
        for (int glyph_y = scan_y1; glyph_y < glyph_y_max; glyph_y++) {
 | 
			
		||||
          if (glyph.get_pixel(glyph_x, glyph_y)) {
 | 
			
		||||
            this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
 | 
			
		||||
 | 
			
		||||
    i += match_length;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg) {
 | 
			
		||||
  char buffer[256];
 | 
			
		||||
  int ret = vsnprintf(buffer, sizeof(buffer), format, arg);
 | 
			
		||||
  if (ret > 0)
 | 
			
		||||
    this->print(x, y, font, color, align, buffer);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DisplayBuffer::image(int x, int y, BaseImage *image, Color color_on, Color color_off) {
 | 
			
		||||
  this->image(x, y, image, ImageAlign::TOP_LEFT, color_on, color_off);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DisplayBuffer::image(int x, int y, BaseImage *image, ImageAlign align, Color color_on, Color color_off) {
 | 
			
		||||
  auto x_align = ImageAlign(int(align) & (int(ImageAlign::HORIZONTAL_ALIGNMENT)));
 | 
			
		||||
  auto y_align = ImageAlign(int(align) & (int(ImageAlign::VERTICAL_ALIGNMENT)));
 | 
			
		||||
 | 
			
		||||
  switch (x_align) {
 | 
			
		||||
    case ImageAlign::RIGHT:
 | 
			
		||||
      x -= image->get_width();
 | 
			
		||||
      break;
 | 
			
		||||
    case ImageAlign::CENTER_HORIZONTAL:
 | 
			
		||||
      x -= image->get_width() / 2;
 | 
			
		||||
      break;
 | 
			
		||||
    case ImageAlign::LEFT:
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  switch (y_align) {
 | 
			
		||||
    case ImageAlign::BOTTOM:
 | 
			
		||||
      y -= image->get_height();
 | 
			
		||||
      break;
 | 
			
		||||
    case ImageAlign::CENTER_VERTICAL:
 | 
			
		||||
      y -= image->get_height() / 2;
 | 
			
		||||
      break;
 | 
			
		||||
    case ImageAlign::TOP:
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  image->draw(x, y, this, color_on, color_off);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_GRAPH
 | 
			
		||||
void DisplayBuffer::graph(int x, int y, graph::Graph *graph, Color color_on) { graph->draw(this, x, y, color_on); }
 | 
			
		||||
void DisplayBuffer::legend(int x, int y, graph::Graph *graph, Color color_on) {
 | 
			
		||||
  graph->draw_legend(this, x, y, color_on);
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_GRAPH
 | 
			
		||||
 | 
			
		||||
#ifdef USE_QR_CODE
 | 
			
		||||
void DisplayBuffer::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, int scale) {
 | 
			
		||||
  qr_code->draw(this, x, y, color_on, scale);
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_QR_CODE
 | 
			
		||||
 | 
			
		||||
void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1,
 | 
			
		||||
                                    int *width, int *height) {
 | 
			
		||||
  int x_offset, baseline;
 | 
			
		||||
  font->measure(text, width, &x_offset, &baseline, height);
 | 
			
		||||
 | 
			
		||||
  auto x_align = TextAlign(int(align) & 0x18);
 | 
			
		||||
  auto y_align = TextAlign(int(align) & 0x07);
 | 
			
		||||
 | 
			
		||||
  switch (x_align) {
 | 
			
		||||
    case TextAlign::RIGHT:
 | 
			
		||||
      *x1 = x - *width;
 | 
			
		||||
      break;
 | 
			
		||||
    case TextAlign::CENTER_HORIZONTAL:
 | 
			
		||||
      *x1 = x - (*width) / 2;
 | 
			
		||||
      break;
 | 
			
		||||
    case TextAlign::LEFT:
 | 
			
		||||
    default:
 | 
			
		||||
      // LEFT
 | 
			
		||||
      *x1 = x;
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  switch (y_align) {
 | 
			
		||||
    case TextAlign::BOTTOM:
 | 
			
		||||
      *y1 = y - *height;
 | 
			
		||||
      break;
 | 
			
		||||
    case TextAlign::BASELINE:
 | 
			
		||||
      *y1 = y - baseline;
 | 
			
		||||
      break;
 | 
			
		||||
    case TextAlign::CENTER_VERTICAL:
 | 
			
		||||
      *y1 = y - (*height) / 2;
 | 
			
		||||
      break;
 | 
			
		||||
    case TextAlign::TOP:
 | 
			
		||||
    default:
 | 
			
		||||
      *y1 = y;
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::print(int x, int y, Font *font, Color color, const char *text) {
 | 
			
		||||
  this->print(x, y, font, color, TextAlign::TOP_LEFT, text);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char *text) {
 | 
			
		||||
  this->print(x, y, font, COLOR_ON, align, text);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::print(int x, int y, Font *font, const char *text) {
 | 
			
		||||
  this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) {
 | 
			
		||||
  va_list arg;
 | 
			
		||||
  va_start(arg, format);
 | 
			
		||||
  this->vprintf_(x, y, font, color, align, format, arg);
 | 
			
		||||
  va_end(arg);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::printf(int x, int y, Font *font, Color color, const char *format, ...) {
 | 
			
		||||
  va_list arg;
 | 
			
		||||
  va_start(arg, format);
 | 
			
		||||
  this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg);
 | 
			
		||||
  va_end(arg);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::printf(int x, int y, Font *font, TextAlign align, const char *format, ...) {
 | 
			
		||||
  va_list arg;
 | 
			
		||||
  va_start(arg, format);
 | 
			
		||||
  this->vprintf_(x, y, font, COLOR_ON, align, format, arg);
 | 
			
		||||
  va_end(arg);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::printf(int x, int y, Font *font, const char *format, ...) {
 | 
			
		||||
  va_list arg;
 | 
			
		||||
  va_start(arg, format);
 | 
			
		||||
  this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg);
 | 
			
		||||
  va_end(arg);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::set_writer(display_writer_t &&writer) { this->writer_ = writer; }
 | 
			
		||||
void DisplayBuffer::set_pages(std::vector<DisplayPage *> pages) {
 | 
			
		||||
  for (auto *page : pages)
 | 
			
		||||
    page->set_parent(this);
 | 
			
		||||
 | 
			
		||||
  for (uint32_t i = 0; i < pages.size() - 1; i++) {
 | 
			
		||||
    pages[i]->set_next(pages[i + 1]);
 | 
			
		||||
    pages[i + 1]->set_prev(pages[i]);
 | 
			
		||||
  }
 | 
			
		||||
  pages[0]->set_prev(pages[pages.size() - 1]);
 | 
			
		||||
  pages[pages.size() - 1]->set_next(pages[0]);
 | 
			
		||||
  this->show_page(pages[0]);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::show_page(DisplayPage *page) {
 | 
			
		||||
  this->previous_page_ = this->page_;
 | 
			
		||||
  this->page_ = page;
 | 
			
		||||
  if (this->previous_page_ != this->page_) {
 | 
			
		||||
    for (auto *t : on_page_change_triggers_)
 | 
			
		||||
      t->process(this->previous_page_, this->page_);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::show_next_page() { this->page_->show_next(); }
 | 
			
		||||
void DisplayBuffer::show_prev_page() { this->page_->show_prev(); }
 | 
			
		||||
void DisplayBuffer::do_update_() {
 | 
			
		||||
  if (this->auto_clear_enabled_) {
 | 
			
		||||
    this->clear();
 | 
			
		||||
  }
 | 
			
		||||
  if (this->page_ != nullptr) {
 | 
			
		||||
    this->page_->get_writer()(*this);
 | 
			
		||||
  } else if (this->writer_.has_value()) {
 | 
			
		||||
    (*this->writer_)(*this);
 | 
			
		||||
  }
 | 
			
		||||
  // remove all not ended clipping regions
 | 
			
		||||
  while (is_clipping()) {
 | 
			
		||||
    end_clipping();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) {
 | 
			
		||||
  if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to))
 | 
			
		||||
    this->trigger(from, to);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, ESPTime time) {
 | 
			
		||||
  char buffer[64];
 | 
			
		||||
  size_t ret = time.strftime(buffer, sizeof(buffer), format);
 | 
			
		||||
  if (ret > 0)
 | 
			
		||||
    this->print(x, y, font, color, align, buffer);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, ESPTime time) {
 | 
			
		||||
  this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, ESPTime time) {
 | 
			
		||||
  this->strftime(x, y, font, COLOR_ON, align, format, time);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, ESPTime time) {
 | 
			
		||||
  this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DisplayBuffer::start_clipping(Rect rect) {
 | 
			
		||||
  if (!this->clipping_rectangle_.empty()) {
 | 
			
		||||
    Rect r = this->clipping_rectangle_.back();
 | 
			
		||||
    rect.shrink(r);
 | 
			
		||||
  }
 | 
			
		||||
  this->clipping_rectangle_.push_back(rect);
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::end_clipping() {
 | 
			
		||||
  if (this->clipping_rectangle_.empty()) {
 | 
			
		||||
    ESP_LOGE(TAG, "clear: Clipping is not set.");
 | 
			
		||||
  } else {
 | 
			
		||||
    this->clipping_rectangle_.pop_back();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::extend_clipping(Rect add_rect) {
 | 
			
		||||
  if (this->clipping_rectangle_.empty()) {
 | 
			
		||||
    ESP_LOGE(TAG, "add: Clipping is not set.");
 | 
			
		||||
  } else {
 | 
			
		||||
    this->clipping_rectangle_.back().extend(add_rect);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void DisplayBuffer::shrink_clipping(Rect add_rect) {
 | 
			
		||||
  if (this->clipping_rectangle_.empty()) {
 | 
			
		||||
    ESP_LOGE(TAG, "add: Clipping is not set.");
 | 
			
		||||
  } else {
 | 
			
		||||
    this->clipping_rectangle_.back().shrink(add_rect);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
Rect DisplayBuffer::get_clipping() {
 | 
			
		||||
  if (this->clipping_rectangle_.empty()) {
 | 
			
		||||
    return Rect();
 | 
			
		||||
  } else {
 | 
			
		||||
    return this->clipping_rectangle_.back();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
 | 
			
		||||
void DisplayPage::show() { this->parent_->show_page(this); }
 | 
			
		||||
void DisplayPage::show_next() { this->next_->show(); }
 | 
			
		||||
void DisplayPage::show_prev() { this->prev_->show(); }
 | 
			
		||||
void DisplayPage::set_parent(DisplayBuffer *parent) { this->parent_ = parent; }
 | 
			
		||||
void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; }
 | 
			
		||||
void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; }
 | 
			
		||||
const display_writer_t &DisplayPage::get_writer() const { return this->writer_; }
 | 
			
		||||
 | 
			
		||||
}  // namespace display
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -2,579 +2,35 @@
 | 
			
		||||
 | 
			
		||||
#include <cstdarg>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#include "display.h"
 | 
			
		||||
#include "display_color_utils.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/time.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_GRAPH
 | 
			
		||||
#include "esphome/components/graph/graph.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_QR_CODE
 | 
			
		||||
#include "esphome/components/qr_code/qr_code.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include "animation.h"
 | 
			
		||||
#include "font.h"
 | 
			
		||||
#include "image.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace display {
 | 
			
		||||
 | 
			
		||||
/** TextAlign is used to tell the display class how to position a piece of text. By default
 | 
			
		||||
 * the coordinates you enter for the print*() functions take the upper left corner of the text
 | 
			
		||||
 * as the "anchor" point. You can customize this behavior to, for example, make the coordinates
 | 
			
		||||
 * refer to the *center* of the text.
 | 
			
		||||
 *
 | 
			
		||||
 * All text alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis
 | 
			
		||||
 * these options are allowed:
 | 
			
		||||
 *
 | 
			
		||||
 * - LEFT (x-coordinate of anchor point is on left)
 | 
			
		||||
 * - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the text)
 | 
			
		||||
 * - RIGHT (x-coordinate of anchor point is on right)
 | 
			
		||||
 *
 | 
			
		||||
 * For the Y-Axis alignment these options are allowed:
 | 
			
		||||
 *
 | 
			
		||||
 * - TOP (y-coordinate of anchor is on the top of the text)
 | 
			
		||||
 * - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the text)
 | 
			
		||||
 * - BASELINE (y-coordinate of anchor is on the baseline of the text)
 | 
			
		||||
 * - BOTTOM (y-coordinate of anchor is on the bottom of the text)
 | 
			
		||||
 *
 | 
			
		||||
 * These options are then combined to create combined TextAlignment options like:
 | 
			
		||||
 * - TOP_LEFT (default)
 | 
			
		||||
 * - CENTER (anchor point is in the middle of the text bounds)
 | 
			
		||||
 * - ...
 | 
			
		||||
 */
 | 
			
		||||
enum class TextAlign {
 | 
			
		||||
  TOP = 0x00,
 | 
			
		||||
  CENTER_VERTICAL = 0x01,
 | 
			
		||||
  BASELINE = 0x02,
 | 
			
		||||
  BOTTOM = 0x04,
 | 
			
		||||
 | 
			
		||||
  LEFT = 0x00,
 | 
			
		||||
  CENTER_HORIZONTAL = 0x08,
 | 
			
		||||
  RIGHT = 0x10,
 | 
			
		||||
 | 
			
		||||
  TOP_LEFT = TOP | LEFT,
 | 
			
		||||
  TOP_CENTER = TOP | CENTER_HORIZONTAL,
 | 
			
		||||
  TOP_RIGHT = TOP | RIGHT,
 | 
			
		||||
 | 
			
		||||
  CENTER_LEFT = CENTER_VERTICAL | LEFT,
 | 
			
		||||
  CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL,
 | 
			
		||||
  CENTER_RIGHT = CENTER_VERTICAL | RIGHT,
 | 
			
		||||
 | 
			
		||||
  BASELINE_LEFT = BASELINE | LEFT,
 | 
			
		||||
  BASELINE_CENTER = BASELINE | CENTER_HORIZONTAL,
 | 
			
		||||
  BASELINE_RIGHT = BASELINE | RIGHT,
 | 
			
		||||
 | 
			
		||||
  BOTTOM_LEFT = BOTTOM | LEFT,
 | 
			
		||||
  BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL,
 | 
			
		||||
  BOTTOM_RIGHT = BOTTOM | RIGHT,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** ImageAlign is used to tell the display class how to position a image. By default
 | 
			
		||||
 * the coordinates you enter for the image() functions take the upper left corner of the image
 | 
			
		||||
 * as the "anchor" point. You can customize this behavior to, for example, make the coordinates
 | 
			
		||||
 * refer to the *center* of the image.
 | 
			
		||||
 *
 | 
			
		||||
 * All image alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis
 | 
			
		||||
 * these options are allowed:
 | 
			
		||||
 *
 | 
			
		||||
 * - LEFT (x-coordinate of anchor point is on left)
 | 
			
		||||
 * - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the image)
 | 
			
		||||
 * - RIGHT (x-coordinate of anchor point is on right)
 | 
			
		||||
 *
 | 
			
		||||
 * For the Y-Axis alignment these options are allowed:
 | 
			
		||||
 *
 | 
			
		||||
 * - TOP (y-coordinate of anchor is on the top of the image)
 | 
			
		||||
 * - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the image)
 | 
			
		||||
 * - BOTTOM (y-coordinate of anchor is on the bottom of the image)
 | 
			
		||||
 *
 | 
			
		||||
 * These options are then combined to create combined TextAlignment options like:
 | 
			
		||||
 * - TOP_LEFT (default)
 | 
			
		||||
 * - CENTER (anchor point is in the middle of the image bounds)
 | 
			
		||||
 * - ...
 | 
			
		||||
 */
 | 
			
		||||
enum class ImageAlign {
 | 
			
		||||
  TOP = 0x00,
 | 
			
		||||
  CENTER_VERTICAL = 0x01,
 | 
			
		||||
  BOTTOM = 0x02,
 | 
			
		||||
 | 
			
		||||
  LEFT = 0x00,
 | 
			
		||||
  CENTER_HORIZONTAL = 0x04,
 | 
			
		||||
  RIGHT = 0x08,
 | 
			
		||||
 | 
			
		||||
  TOP_LEFT = TOP | LEFT,
 | 
			
		||||
  TOP_CENTER = TOP | CENTER_HORIZONTAL,
 | 
			
		||||
  TOP_RIGHT = TOP | RIGHT,
 | 
			
		||||
 | 
			
		||||
  CENTER_LEFT = CENTER_VERTICAL | LEFT,
 | 
			
		||||
  CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL,
 | 
			
		||||
  CENTER_RIGHT = CENTER_VERTICAL | RIGHT,
 | 
			
		||||
 | 
			
		||||
  BOTTOM_LEFT = BOTTOM | LEFT,
 | 
			
		||||
  BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL,
 | 
			
		||||
  BOTTOM_RIGHT = BOTTOM | RIGHT,
 | 
			
		||||
 | 
			
		||||
  HORIZONTAL_ALIGNMENT = LEFT | CENTER_HORIZONTAL | RIGHT,
 | 
			
		||||
  VERTICAL_ALIGNMENT = TOP | CENTER_VERTICAL | BOTTOM
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum DisplayType {
 | 
			
		||||
  DISPLAY_TYPE_BINARY = 1,
 | 
			
		||||
  DISPLAY_TYPE_GRAYSCALE = 2,
 | 
			
		||||
  DISPLAY_TYPE_COLOR = 3,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum DisplayRotation {
 | 
			
		||||
  DISPLAY_ROTATION_0_DEGREES = 0,
 | 
			
		||||
  DISPLAY_ROTATION_90_DEGREES = 90,
 | 
			
		||||
  DISPLAY_ROTATION_180_DEGREES = 180,
 | 
			
		||||
  DISPLAY_ROTATION_270_DEGREES = 270,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
static const int16_t VALUE_NO_SET = 32766;
 | 
			
		||||
 | 
			
		||||
class Rect {
 | 
			
		||||
class DisplayBuffer : public Display {
 | 
			
		||||
 public:
 | 
			
		||||
  int16_t x;  ///< X coordinate of corner
 | 
			
		||||
  int16_t y;  ///< Y coordinate of corner
 | 
			
		||||
  int16_t w;  ///< Width of region
 | 
			
		||||
  int16_t h;  ///< Height of region
 | 
			
		||||
 | 
			
		||||
  Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {}  // NOLINT
 | 
			
		||||
  inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {}
 | 
			
		||||
  inline int16_t x2() { return this->x + this->w; };  ///< X coordinate of corner
 | 
			
		||||
  inline int16_t y2() { return this->y + this->h; };  ///< Y coordinate of corner
 | 
			
		||||
 | 
			
		||||
  inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); }
 | 
			
		||||
 | 
			
		||||
  void expand(int16_t horizontal, int16_t vertical);
 | 
			
		||||
 | 
			
		||||
  void extend(Rect rect);
 | 
			
		||||
  void shrink(Rect rect);
 | 
			
		||||
 | 
			
		||||
  bool inside(Rect rect, bool absolute = true);
 | 
			
		||||
  bool inside(int16_t test_x, int16_t test_y, bool absolute = true);
 | 
			
		||||
  bool equal(Rect rect);
 | 
			
		||||
  void info(const std::string &prefix = "rect info:");
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class DisplayBuffer;
 | 
			
		||||
class DisplayPage;
 | 
			
		||||
class DisplayOnPageChangeTrigger;
 | 
			
		||||
 | 
			
		||||
using display_writer_t = std::function<void(DisplayBuffer &)>;
 | 
			
		||||
 | 
			
		||||
#define LOG_DISPLAY(prefix, type, obj) \
 | 
			
		||||
  if ((obj) != nullptr) { \
 | 
			
		||||
    ESP_LOGCONFIG(TAG, prefix type); \
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "%s  Rotations: %d °", prefix, (obj)->rotation_); \
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "%s  Dimensions: %dpx x %dpx", prefix, (obj)->get_width(), (obj)->get_height()); \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
class DisplayBuffer {
 | 
			
		||||
 public:
 | 
			
		||||
  /// Fill the entire screen with the given color.
 | 
			
		||||
  virtual void fill(Color color);
 | 
			
		||||
  /// Clear the entire screen by filling it with OFF pixels.
 | 
			
		||||
  void clear();
 | 
			
		||||
 | 
			
		||||
  /// Get the width of the image in pixels with rotation applied.
 | 
			
		||||
  int get_width();
 | 
			
		||||
  int get_width() override;
 | 
			
		||||
  /// Get the height of the image in pixels with rotation applied.
 | 
			
		||||
  int get_height();
 | 
			
		||||
  int get_height() override;
 | 
			
		||||
 | 
			
		||||
  /// Set a single pixel at the specified coordinates to the given color.
 | 
			
		||||
  void draw_pixel_at(int x, int y, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color.
 | 
			
		||||
  void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /// Draw a horizontal line from the point [x,y] to [x+width,y] with the given color.
 | 
			
		||||
  void horizontal_line(int x, int y, int width, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /// Draw a vertical line from the point [x,y] to [x,y+width] with the given color.
 | 
			
		||||
  void vertical_line(int x, int y, int height, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /// Draw the outline of a rectangle with the top left point at [x1,y1] and the bottom right point at
 | 
			
		||||
  /// [x1+width,y1+height].
 | 
			
		||||
  void rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /// Fill a rectangle with the top left point at [x1,y1] and the bottom right point at [x1+width,y1+height].
 | 
			
		||||
  void filled_rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /// Draw the outline of a circle centered around [center_x,center_y] with the radius radius with the given color.
 | 
			
		||||
  void circle(int center_x, int center_xy, int radius, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /// Fill a circle centered around [center_x,center_y] with the radius radius with the given color.
 | 
			
		||||
  void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /** Print `text` with the anchor point at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param y The y coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param color The color to draw the text with.
 | 
			
		||||
   * @param align The alignment of the text.
 | 
			
		||||
   * @param text The text to draw.
 | 
			
		||||
   */
 | 
			
		||||
  void print(int x, int y, Font *font, Color color, TextAlign align, const char *text);
 | 
			
		||||
 | 
			
		||||
  /** Print `text` with the top left at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param color The color to draw the text with.
 | 
			
		||||
   * @param text The text to draw.
 | 
			
		||||
   */
 | 
			
		||||
  void print(int x, int y, Font *font, Color color, const char *text);
 | 
			
		||||
 | 
			
		||||
  /** Print `text` with the anchor point at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param y The y coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param align The alignment of the text.
 | 
			
		||||
   * @param text The text to draw.
 | 
			
		||||
   */
 | 
			
		||||
  void print(int x, int y, Font *font, TextAlign align, const char *text);
 | 
			
		||||
 | 
			
		||||
  /** Print `text` with the top left at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param text The text to draw.
 | 
			
		||||
   */
 | 
			
		||||
  void print(int x, int y, Font *font, const char *text);
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param y The y coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param color The color to draw the text with.
 | 
			
		||||
   * @param align The alignment of the text.
 | 
			
		||||
   * @param format The format to use.
 | 
			
		||||
   * @param ... The arguments to use for the text formatting.
 | 
			
		||||
   */
 | 
			
		||||
  void printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...)
 | 
			
		||||
      __attribute__((format(printf, 7, 8)));
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param color The color to draw the text with.
 | 
			
		||||
   * @param format The format to use.
 | 
			
		||||
   * @param ... The arguments to use for the text formatting.
 | 
			
		||||
   */
 | 
			
		||||
  void printf(int x, int y, Font *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7)));
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param y The y coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param align The alignment of the text.
 | 
			
		||||
   * @param format The format to use.
 | 
			
		||||
   * @param ... The arguments to use for the text formatting.
 | 
			
		||||
   */
 | 
			
		||||
  void printf(int x, int y, Font *font, TextAlign align, const char *format, ...) __attribute__((format(printf, 6, 7)));
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param format The format to use.
 | 
			
		||||
   * @param ... The arguments to use for the text formatting.
 | 
			
		||||
   */
 | 
			
		||||
  void printf(int x, int y, Font *font, const char *format, ...) __attribute__((format(printf, 5, 6)));
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param y The y coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param color The color to draw the text with.
 | 
			
		||||
   * @param align The alignment of the text.
 | 
			
		||||
   * @param format The strftime format to use.
 | 
			
		||||
   * @param time The time to format.
 | 
			
		||||
   */
 | 
			
		||||
  void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, ESPTime time)
 | 
			
		||||
      __attribute__((format(strftime, 7, 0)));
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param color The color to draw the text with.
 | 
			
		||||
   * @param format The strftime format to use.
 | 
			
		||||
   * @param time The time to format.
 | 
			
		||||
   */
 | 
			
		||||
  void strftime(int x, int y, Font *font, Color color, const char *format, ESPTime time)
 | 
			
		||||
      __attribute__((format(strftime, 6, 0)));
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param y The y coordinate of the text alignment anchor point.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param align The alignment of the text.
 | 
			
		||||
   * @param format The strftime format to use.
 | 
			
		||||
   * @param time The time to format.
 | 
			
		||||
   */
 | 
			
		||||
  void strftime(int x, int y, Font *font, TextAlign align, const char *format, ESPTime time)
 | 
			
		||||
      __attribute__((format(strftime, 6, 0)));
 | 
			
		||||
 | 
			
		||||
  /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param font The font to draw the text with.
 | 
			
		||||
   * @param format The strftime format to use.
 | 
			
		||||
   * @param time The time to format.
 | 
			
		||||
   */
 | 
			
		||||
  void strftime(int x, int y, Font *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0)));
 | 
			
		||||
 | 
			
		||||
  /** Draw the `image` with the top-left corner at [x,y] to the screen.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param image The image to draw.
 | 
			
		||||
   * @param color_on The color to replace in binary images for the on bits.
 | 
			
		||||
   * @param color_off The color to replace in binary images for the off bits.
 | 
			
		||||
   */
 | 
			
		||||
  void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
 | 
			
		||||
 | 
			
		||||
  /** Draw the `image` at [x,y] to the screen.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param image The image to draw.
 | 
			
		||||
   * @param align The alignment of the image.
 | 
			
		||||
   * @param color_on The color to replace in binary images for the on bits.
 | 
			
		||||
   * @param color_off The color to replace in binary images for the off bits.
 | 
			
		||||
   */
 | 
			
		||||
  void image(int x, int y, BaseImage *image, ImageAlign align, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
 | 
			
		||||
 | 
			
		||||
#ifdef USE_GRAPH
 | 
			
		||||
  /** Draw the `graph` with the top-left corner at [x,y] to the screen.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param graph The graph id to draw
 | 
			
		||||
   * @param color_on The color to replace in binary images for the on bits.
 | 
			
		||||
   */
 | 
			
		||||
  void graph(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /** Draw the `legend` for graph with the top-left corner at [x,y] to the screen.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param graph The graph id for which the legend applies to
 | 
			
		||||
   * @param graph The graph id for which the legend applies to
 | 
			
		||||
   * @param graph The graph id for which the legend applies to
 | 
			
		||||
   * @param name_font The font used for the trace name
 | 
			
		||||
   * @param value_font The font used for the trace value and units
 | 
			
		||||
   * @param color_on The color of the border
 | 
			
		||||
   */
 | 
			
		||||
  void legend(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON);
 | 
			
		||||
#endif  // USE_GRAPH
 | 
			
		||||
 | 
			
		||||
#ifdef USE_QR_CODE
 | 
			
		||||
  /** Draw the `qr_code` with the top-left corner at [x,y] to the screen.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the upper left corner.
 | 
			
		||||
   * @param y The y coordinate of the upper left corner.
 | 
			
		||||
   * @param qr_code The qr_code to draw
 | 
			
		||||
   * @param color_on The color to replace in binary images for the on bits.
 | 
			
		||||
   */
 | 
			
		||||
  void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  /** Get the text bounds of the given string.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate to place the string at, can be 0 if only interested in dimensions.
 | 
			
		||||
   * @param y The y coordinate to place the string at, can be 0 if only interested in dimensions.
 | 
			
		||||
   * @param text The text to measure.
 | 
			
		||||
   * @param font The font to measure the text bounds with.
 | 
			
		||||
   * @param align The alignment of the text. Set to TextAlign::TOP_LEFT if only interested in dimensions.
 | 
			
		||||
   * @param x1 A pointer to store the returned x coordinate of the upper left corner in.
 | 
			
		||||
   * @param y1 A pointer to store the returned y coordinate of the upper left corner in.
 | 
			
		||||
   * @param width A pointer to store the returned text width in.
 | 
			
		||||
   * @param height A pointer to store the returned text height in.
 | 
			
		||||
   */
 | 
			
		||||
  void get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, int *width,
 | 
			
		||||
                       int *height);
 | 
			
		||||
 | 
			
		||||
  /// Internal method to set the display writer lambda.
 | 
			
		||||
  void set_writer(display_writer_t &&writer);
 | 
			
		||||
 | 
			
		||||
  void show_page(DisplayPage *page);
 | 
			
		||||
  void show_next_page();
 | 
			
		||||
  void show_prev_page();
 | 
			
		||||
 | 
			
		||||
  void set_pages(std::vector<DisplayPage *> pages);
 | 
			
		||||
 | 
			
		||||
  const DisplayPage *get_active_page() const { return this->page_; }
 | 
			
		||||
 | 
			
		||||
  void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); }
 | 
			
		||||
 | 
			
		||||
  /// Internal method to set the display rotation with.
 | 
			
		||||
  void set_rotation(DisplayRotation rotation);
 | 
			
		||||
 | 
			
		||||
  // Internal method to set display auto clearing.
 | 
			
		||||
  void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; }
 | 
			
		||||
  void draw_pixel_at(int x, int y, Color color) override;
 | 
			
		||||
 | 
			
		||||
  virtual int get_height_internal() = 0;
 | 
			
		||||
  virtual int get_width_internal() = 0;
 | 
			
		||||
  DisplayRotation get_rotation() const { return this->rotation_; }
 | 
			
		||||
 | 
			
		||||
  /** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
 | 
			
		||||
   * returns the type the display is currently configured to.
 | 
			
		||||
   */
 | 
			
		||||
  virtual DisplayType get_display_type() = 0;
 | 
			
		||||
 | 
			
		||||
  /** Set the clipping rectangle for further drawing
 | 
			
		||||
   *
 | 
			
		||||
   * @param[in]  rect:       Pointer to Rect for clipping (or NULL for entire screen)
 | 
			
		||||
   *
 | 
			
		||||
   * return true if success, false if error
 | 
			
		||||
   */
 | 
			
		||||
  void start_clipping(Rect rect);
 | 
			
		||||
  void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) {
 | 
			
		||||
    start_clipping(Rect(left, top, right - left, bottom - top));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /** Add a rectangular region to the invalidation region
 | 
			
		||||
   * - This is usually called when an element has been modified
 | 
			
		||||
   *
 | 
			
		||||
   * @param[in]  rect: Rectangle to add to the invalidation region
 | 
			
		||||
   */
 | 
			
		||||
  void extend_clipping(Rect rect);
 | 
			
		||||
  void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) {
 | 
			
		||||
    this->extend_clipping(Rect(left, top, right - left, bottom - top));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /** substract a rectangular region to the invalidation region
 | 
			
		||||
   *  - This is usually called when an element has been modified
 | 
			
		||||
   *
 | 
			
		||||
   * @param[in]  rect: Rectangle to add to the invalidation region
 | 
			
		||||
   */
 | 
			
		||||
  void shrink_clipping(Rect rect);
 | 
			
		||||
  void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
 | 
			
		||||
    this->shrink_clipping(Rect(left, top, right - left, bottom - top));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /** Reset the invalidation region
 | 
			
		||||
   */
 | 
			
		||||
  void end_clipping();
 | 
			
		||||
 | 
			
		||||
  /** Get the current the clipping rectangle
 | 
			
		||||
   *
 | 
			
		||||
   * return rect for active clipping region
 | 
			
		||||
   */
 | 
			
		||||
  Rect get_clipping();
 | 
			
		||||
 | 
			
		||||
  bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg);
 | 
			
		||||
 | 
			
		||||
  virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0;
 | 
			
		||||
 | 
			
		||||
  void init_internal_(uint32_t buffer_length);
 | 
			
		||||
 | 
			
		||||
  void do_update_();
 | 
			
		||||
 | 
			
		||||
  uint8_t *buffer_{nullptr};
 | 
			
		||||
  DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES};
 | 
			
		||||
  optional<display_writer_t> writer_{};
 | 
			
		||||
  DisplayPage *page_{nullptr};
 | 
			
		||||
  DisplayPage *previous_page_{nullptr};
 | 
			
		||||
  std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
 | 
			
		||||
  bool auto_clear_enabled_{true};
 | 
			
		||||
  std::vector<Rect> clipping_rectangle_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class DisplayPage {
 | 
			
		||||
 public:
 | 
			
		||||
  DisplayPage(display_writer_t writer);
 | 
			
		||||
  void show();
 | 
			
		||||
  void show_next();
 | 
			
		||||
  void show_prev();
 | 
			
		||||
  void set_parent(DisplayBuffer *parent);
 | 
			
		||||
  void set_prev(DisplayPage *prev);
 | 
			
		||||
  void set_next(DisplayPage *next);
 | 
			
		||||
  const display_writer_t &get_writer() const;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  DisplayBuffer *parent_;
 | 
			
		||||
  display_writer_t writer_;
 | 
			
		||||
  DisplayPage *prev_{nullptr};
 | 
			
		||||
  DisplayPage *next_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class DisplayPageShowAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  TEMPLATABLE_VALUE(DisplayPage *, page)
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    auto *page = this->page_.value(x...);
 | 
			
		||||
    if (page != nullptr) {
 | 
			
		||||
      page->show();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class DisplayPageShowNextAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  DisplayPageShowNextAction(DisplayBuffer *buffer) : buffer_(buffer) {}
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override { this->buffer_->show_next_page(); }
 | 
			
		||||
 | 
			
		||||
  DisplayBuffer *buffer_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class DisplayPageShowPrevAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  DisplayPageShowPrevAction(DisplayBuffer *buffer) : buffer_(buffer) {}
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override { this->buffer_->show_prev_page(); }
 | 
			
		||||
 | 
			
		||||
  DisplayBuffer *buffer_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class DisplayIsDisplayingPageCondition : public Condition<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  DisplayIsDisplayingPageCondition(DisplayBuffer *parent) : parent_(parent) {}
 | 
			
		||||
 | 
			
		||||
  void set_page(DisplayPage *page) { this->page_ = page; }
 | 
			
		||||
  bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  DisplayBuffer *parent_;
 | 
			
		||||
  DisplayPage *page_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class DisplayOnPageChangeTrigger : public Trigger<DisplayPage *, DisplayPage *> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit DisplayOnPageChangeTrigger(DisplayBuffer *parent) { parent->add_on_page_change_trigger(this); }
 | 
			
		||||
  void process(DisplayPage *from, DisplayPage *to);
 | 
			
		||||
  void set_from(DisplayPage *p) { this->from_ = p; }
 | 
			
		||||
  void set_to(DisplayPage *p) { this->to_ = p; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  DisplayPage *from_{nullptr};
 | 
			
		||||
  DisplayPage *to_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace display
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										98
									
								
								esphome/components/display/rect.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								esphome/components/display/rect.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,98 @@
 | 
			
		||||
#include "rect.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace display {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "display";
 | 
			
		||||
 | 
			
		||||
void Rect::expand(int16_t horizontal, int16_t vertical) {
 | 
			
		||||
  if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) {
 | 
			
		||||
    this->x = this->x - horizontal;
 | 
			
		||||
    this->y = this->y - vertical;
 | 
			
		||||
    this->w = this->w + (2 * horizontal);
 | 
			
		||||
    this->h = this->h + (2 * vertical);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Rect::extend(Rect rect) {
 | 
			
		||||
  if (!this->is_set()) {
 | 
			
		||||
    this->x = rect.x;
 | 
			
		||||
    this->y = rect.y;
 | 
			
		||||
    this->w = rect.w;
 | 
			
		||||
    this->h = rect.h;
 | 
			
		||||
  } else {
 | 
			
		||||
    if (this->x > rect.x) {
 | 
			
		||||
      this->w = this->w + (this->x - rect.x);
 | 
			
		||||
      this->x = rect.x;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->y > rect.y) {
 | 
			
		||||
      this->h = this->h + (this->y - rect.y);
 | 
			
		||||
      this->y = rect.y;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->x2() < rect.x2()) {
 | 
			
		||||
      this->w = rect.x2() - this->x;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->y2() < rect.y2()) {
 | 
			
		||||
      this->h = rect.y2() - this->y;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void Rect::shrink(Rect rect) {
 | 
			
		||||
  if (!this->inside(rect)) {
 | 
			
		||||
    (*this) = Rect();
 | 
			
		||||
  } else {
 | 
			
		||||
    if (this->x2() > rect.x2()) {
 | 
			
		||||
      this->w = rect.x2() - this->x;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->x < rect.x) {
 | 
			
		||||
      this->w = this->w + (this->x - rect.x);
 | 
			
		||||
      this->x = rect.x;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->y2() > rect.y2()) {
 | 
			
		||||
      this->h = rect.y2() - this->y;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->y < rect.y) {
 | 
			
		||||
      this->h = this->h + (this->y - rect.y);
 | 
			
		||||
      this->y = rect.y;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Rect::equal(Rect rect) {
 | 
			
		||||
  return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) {  // NOLINT
 | 
			
		||||
  if (!this->is_set()) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (absolute) {
 | 
			
		||||
    return ((test_x >= this->x) && (test_x <= this->x2()) && (test_y >= this->y) && (test_y <= this->y2()));
 | 
			
		||||
  } else {
 | 
			
		||||
    return ((test_x >= 0) && (test_x <= this->w) && (test_y >= 0) && (test_y <= this->h));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Rect::inside(Rect rect, bool absolute) {
 | 
			
		||||
  if (!this->is_set() || !rect.is_set()) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (absolute) {
 | 
			
		||||
    return ((rect.x <= this->x2()) && (rect.x2() >= this->x) && (rect.y <= this->y2()) && (rect.y2() >= this->y));
 | 
			
		||||
  } else {
 | 
			
		||||
    return ((rect.x <= this->w) && (rect.w >= 0) && (rect.y <= this->h) && (rect.h >= 0));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Rect::info(const std::string &prefix) {
 | 
			
		||||
  if (this->is_set()) {
 | 
			
		||||
    ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(),
 | 
			
		||||
             this->y2());
 | 
			
		||||
  } else
 | 
			
		||||
    ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace display
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										36
									
								
								esphome/components/display/rect.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								esphome/components/display/rect.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace display {
 | 
			
		||||
 | 
			
		||||
static const int16_t VALUE_NO_SET = 32766;
 | 
			
		||||
 | 
			
		||||
class Rect {
 | 
			
		||||
 public:
 | 
			
		||||
  int16_t x;  ///< X coordinate of corner
 | 
			
		||||
  int16_t y;  ///< Y coordinate of corner
 | 
			
		||||
  int16_t w;  ///< Width of region
 | 
			
		||||
  int16_t h;  ///< Height of region
 | 
			
		||||
 | 
			
		||||
  Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {}  // NOLINT
 | 
			
		||||
  inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ALWAYS_INLINE : x(x), y(y), w(w), h(h) {}
 | 
			
		||||
  inline int16_t x2() { return this->x + this->w; };  ///< X coordinate of corner
 | 
			
		||||
  inline int16_t y2() { return this->y + this->h; };  ///< Y coordinate of corner
 | 
			
		||||
 | 
			
		||||
  inline bool is_set() ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); }
 | 
			
		||||
 | 
			
		||||
  void expand(int16_t horizontal, int16_t vertical);
 | 
			
		||||
 | 
			
		||||
  void extend(Rect rect);
 | 
			
		||||
  void shrink(Rect rect);
 | 
			
		||||
 | 
			
		||||
  bool inside(Rect rect, bool absolute = true);
 | 
			
		||||
  bool inside(int16_t test_x, int16_t test_y, bool absolute = true);
 | 
			
		||||
  bool equal(Rect rect);
 | 
			
		||||
  void info(const std::string &prefix = "rect info:");
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace display
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -19,6 +19,7 @@ CONF_CRC_CHECK = "crc_check"
 | 
			
		||||
CONF_DECRYPTION_KEY = "decryption_key"
 | 
			
		||||
CONF_DSMR_ID = "dsmr_id"
 | 
			
		||||
CONF_GAS_MBUS_ID = "gas_mbus_id"
 | 
			
		||||
CONF_WATER_MBUS_ID = "water_mbus_id"
 | 
			
		||||
CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length"
 | 
			
		||||
CONF_REQUEST_INTERVAL = "request_interval"
 | 
			
		||||
CONF_REQUEST_PIN = "request_pin"
 | 
			
		||||
@@ -53,6 +54,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            cv.Optional(CONF_DECRYPTION_KEY): _validate_key,
 | 
			
		||||
            cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_,
 | 
			
		||||
            cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_,
 | 
			
		||||
            cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_,
 | 
			
		||||
            cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema,
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
@@ -82,9 +84,10 @@ async def to_code(config):
 | 
			
		||||
    cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds))
 | 
			
		||||
 | 
			
		||||
    cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID]))
 | 
			
		||||
    cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
 | 
			
		||||
 | 
			
		||||
    # DSMR Parser
 | 
			
		||||
    cg.add_library("glmnet/Dsmr", "0.5")
 | 
			
		||||
    cg.add_library("glmnet/Dsmr", "0.8")
 | 
			
		||||
 | 
			
		||||
    # Crypto
 | 
			
		||||
    cg.add_library("rweather/Crypto", "0.4.0")
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_GAS,
 | 
			
		||||
    DEVICE_CLASS_POWER,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    DEVICE_CLASS_WATER,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
@@ -236,6 +237,36 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
            device_class=DEVICE_CLASS_GAS,
 | 
			
		||||
            state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional("water_delivered"): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_CUBIC_METER,
 | 
			
		||||
            accuracy_decimals=3,
 | 
			
		||||
            device_class=DEVICE_CLASS_WATER,
 | 
			
		||||
            state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            "active_energy_import_current_average_demand"
 | 
			
		||||
        ): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_KILOWATT,
 | 
			
		||||
            accuracy_decimals=3,
 | 
			
		||||
            device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            "active_energy_import_maximum_demand_running_month"
 | 
			
		||||
        ): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_KILOWATT,
 | 
			
		||||
            accuracy_decimals=3,
 | 
			
		||||
            device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            "active_energy_import_maximum_demand_last_13_months"
 | 
			
		||||
        ): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_KILOWATT,
 | 
			
		||||
            accuracy_decimals=3,
 | 
			
		||||
            device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/duty_time/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/duty_time/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@dudanov"]
 | 
			
		||||
							
								
								
									
										103
									
								
								esphome/components/duty_time/duty_time_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								esphome/components/duty_time/duty_time_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,103 @@
 | 
			
		||||
#include "duty_time_sensor.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace duty_time_sensor {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "duty_time_sensor";
 | 
			
		||||
 | 
			
		||||
void DutyTimeSensor::set_sensor(binary_sensor::BinarySensor *const sensor) {
 | 
			
		||||
  sensor->add_on_state_callback([this](bool state) { this->process_state_(state); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DutyTimeSensor::start() {
 | 
			
		||||
  if (!this->last_state_)
 | 
			
		||||
    this->process_state_(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DutyTimeSensor::stop() {
 | 
			
		||||
  if (this->last_state_)
 | 
			
		||||
    this->process_state_(false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DutyTimeSensor::update() {
 | 
			
		||||
  if (this->last_state_)
 | 
			
		||||
    this->process_state_(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DutyTimeSensor::loop() {
 | 
			
		||||
  if (this->func_ == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  const bool state = this->func_();
 | 
			
		||||
 | 
			
		||||
  if (state != this->last_state_)
 | 
			
		||||
    this->process_state_(state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DutyTimeSensor::setup() {
 | 
			
		||||
  uint32_t seconds = 0;
 | 
			
		||||
 | 
			
		||||
  if (this->restore_) {
 | 
			
		||||
    this->pref_ = global_preferences->make_preference<uint32_t>(this->get_object_id_hash());
 | 
			
		||||
    this->pref_.load(&seconds);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->set_value_(seconds);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DutyTimeSensor::set_value_(const uint32_t sec) {
 | 
			
		||||
  this->last_time_ = 0;
 | 
			
		||||
  if (this->last_state_)
 | 
			
		||||
    this->last_time_ = millis();  // last time with 0 ms correction
 | 
			
		||||
  this->publish_and_save_(sec, 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DutyTimeSensor::process_state_(const bool state) {
 | 
			
		||||
  const uint32_t now = millis();
 | 
			
		||||
 | 
			
		||||
  if (this->last_state_) {
 | 
			
		||||
    // update or falling edge
 | 
			
		||||
    const uint32_t tm = now - this->last_time_;
 | 
			
		||||
    const uint32_t ms = tm % 1000;
 | 
			
		||||
 | 
			
		||||
    this->publish_and_save_(this->total_sec_ + tm / 1000, ms);
 | 
			
		||||
    this->last_time_ = now - ms;  // store time with ms correction
 | 
			
		||||
 | 
			
		||||
    if (!state) {
 | 
			
		||||
      // falling edge
 | 
			
		||||
      this->last_time_ = ms;  // temporary store ms correction only
 | 
			
		||||
      this->last_state_ = false;
 | 
			
		||||
 | 
			
		||||
      if (this->last_duty_time_sensor_ != nullptr) {
 | 
			
		||||
        const uint32_t turn_on_ms = now - this->edge_time_;
 | 
			
		||||
        this->last_duty_time_sensor_->publish_state(turn_on_ms * 1e-3f);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  } else if (state) {
 | 
			
		||||
    // rising edge
 | 
			
		||||
    this->last_time_ = now - this->last_time_;  // store time with ms correction
 | 
			
		||||
    this->edge_time_ = now;                     // store turn-on start time
 | 
			
		||||
    this->last_state_ = true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DutyTimeSensor::publish_and_save_(const uint32_t sec, const uint32_t ms) {
 | 
			
		||||
  this->total_sec_ = sec;
 | 
			
		||||
  this->publish_state(sec + ms * 1e-3f);
 | 
			
		||||
 | 
			
		||||
  if (this->restore_)
 | 
			
		||||
    this->pref_.save(&sec);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DutyTimeSensor::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Duty Time:");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Update Interval: %dms", this->get_update_interval());
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Restore: %s", ONOFF(this->restore_));
 | 
			
		||||
  LOG_SENSOR("  ", "Duty Time Sensor:", this);
 | 
			
		||||
  LOG_SENSOR("  ", "Last Duty Time Sensor:", this->last_duty_time_sensor_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace duty_time_sensor
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										88
									
								
								esphome/components/duty_time/duty_time_sensor.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								esphome/components/duty_time/duty_time_sensor.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
#include "esphome/components/binary_sensor/binary_sensor.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace duty_time_sensor {
 | 
			
		||||
 | 
			
		||||
class DutyTimeSensor : public sensor::Sensor, public PollingComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
  void start();
 | 
			
		||||
  void stop();
 | 
			
		||||
  bool is_running() const { return this->last_state_; }
 | 
			
		||||
  void reset() { this->set_value_(0); }
 | 
			
		||||
 | 
			
		||||
  void set_lambda(std::function<bool()> &&func) { this->func_ = func; }
 | 
			
		||||
  void set_sensor(binary_sensor::BinarySensor *sensor);
 | 
			
		||||
  void set_last_duty_time_sensor(sensor::Sensor *sensor) { this->last_duty_time_sensor_ = sensor; }
 | 
			
		||||
  void set_restore(bool restore) { this->restore_ = restore; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void set_value_(uint32_t sec);
 | 
			
		||||
  void process_state_(bool state);
 | 
			
		||||
  void publish_and_save_(uint32_t sec, uint32_t ms);
 | 
			
		||||
 | 
			
		||||
  std::function<bool()> func_{nullptr};
 | 
			
		||||
  sensor::Sensor *last_duty_time_sensor_{nullptr};
 | 
			
		||||
  ESPPreferenceObject pref_;
 | 
			
		||||
 | 
			
		||||
  uint32_t total_sec_;
 | 
			
		||||
  uint32_t last_time_;
 | 
			
		||||
  uint32_t edge_time_;
 | 
			
		||||
  bool last_state_{false};
 | 
			
		||||
  bool restore_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class StartAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit StartAction(DutyTimeSensor *parent) : parent_(parent) {}
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override { this->parent_->start(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  DutyTimeSensor *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class StopAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit StopAction(DutyTimeSensor *parent) : parent_(parent) {}
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override { this->parent_->stop(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  DutyTimeSensor *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class ResetAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit ResetAction(DutyTimeSensor *parent) : parent_(parent) {}
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override { this->parent_->reset(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  DutyTimeSensor *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class RunningCondition : public Condition<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit RunningCondition(DutyTimeSensor *parent, bool state) : parent_(parent), state_(state) {}
 | 
			
		||||
 | 
			
		||||
  bool check(Ts... x) override { return this->parent_->is_running() == this->state_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  DutyTimeSensor *parent_;
 | 
			
		||||
  bool state_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace duty_time_sensor
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										121
									
								
								esphome/components/duty_time/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								esphome/components/duty_time/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.automation import (
 | 
			
		||||
    Action,
 | 
			
		||||
    Condition,
 | 
			
		||||
    maybe_simple_id,
 | 
			
		||||
    register_action,
 | 
			
		||||
    register_condition,
 | 
			
		||||
)
 | 
			
		||||
from esphome.components import binary_sensor, sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_SENSOR,
 | 
			
		||||
    CONF_RESTORE,
 | 
			
		||||
    CONF_LAMBDA,
 | 
			
		||||
    UNIT_SECOND,
 | 
			
		||||
    STATE_CLASS_TOTAL,
 | 
			
		||||
    STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
    DEVICE_CLASS_DURATION,
 | 
			
		||||
    ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_LAST_TIME = "last_time"
 | 
			
		||||
 | 
			
		||||
duty_time_sensor_ns = cg.esphome_ns.namespace("duty_time_sensor")
 | 
			
		||||
DutyTimeSensor = duty_time_sensor_ns.class_(
 | 
			
		||||
    "DutyTimeSensor", sensor.Sensor, cg.PollingComponent
 | 
			
		||||
)
 | 
			
		||||
StartAction = duty_time_sensor_ns.class_("StartAction", Action)
 | 
			
		||||
StopAction = duty_time_sensor_ns.class_("StopAction", Action)
 | 
			
		||||
ResetAction = duty_time_sensor_ns.class_("ResetAction", Action)
 | 
			
		||||
SetAction = duty_time_sensor_ns.class_("SetAction", Action)
 | 
			
		||||
RunningCondition = duty_time_sensor_ns.class_("RunningCondition", Condition)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    sensor.sensor_schema(
 | 
			
		||||
        DutyTimeSensor,
 | 
			
		||||
        unit_of_measurement=UNIT_SECOND,
 | 
			
		||||
        icon="mdi:timer-play-outline",
 | 
			
		||||
        accuracy_decimals=3,
 | 
			
		||||
        state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        device_class=DEVICE_CLASS_DURATION,
 | 
			
		||||
        entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
    )
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Optional(CONF_SENSOR): cv.use_id(binary_sensor.BinarySensor),
 | 
			
		||||
            cv.Optional(CONF_LAMBDA): cv.lambda_,
 | 
			
		||||
            cv.Optional(CONF_RESTORE, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_LAST_TIME): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_SECOND,
 | 
			
		||||
                icon="mdi:timer-marker-outline",
 | 
			
		||||
                accuracy_decimals=3,
 | 
			
		||||
                state_class=STATE_CLASS_TOTAL,
 | 
			
		||||
                device_class=DEVICE_CLASS_DURATION,
 | 
			
		||||
                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s")),
 | 
			
		||||
    cv.has_at_most_one_key(CONF_SENSOR, CONF_LAMBDA),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = await sensor.new_sensor(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    cg.add(var.set_restore(config[CONF_RESTORE]))
 | 
			
		||||
    if CONF_SENSOR in config:
 | 
			
		||||
        sens = await cg.get_variable(config[CONF_SENSOR])
 | 
			
		||||
        cg.add(var.set_sensor(sens))
 | 
			
		||||
    if CONF_LAMBDA in config:
 | 
			
		||||
        lambda_ = await cg.process_lambda(config[CONF_LAMBDA], [], return_type=cg.bool_)
 | 
			
		||||
        cg.add(var.set_lambda(lambda_))
 | 
			
		||||
    if CONF_LAST_TIME in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_LAST_TIME])
 | 
			
		||||
        cg.add(var.set_last_duty_time_sensor(sens))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# AUTOMATIONS
 | 
			
		||||
 | 
			
		||||
DUTY_TIME_ID_SCHEMA = maybe_simple_id(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_ID): cv.use_id(DutyTimeSensor),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action("sensor.duty_time.start", StartAction, DUTY_TIME_ID_SCHEMA)
 | 
			
		||||
async def sensor_runtime_start_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    return cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action("sensor.duty_time.stop", StopAction, DUTY_TIME_ID_SCHEMA)
 | 
			
		||||
async def sensor_runtime_stop_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    return cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action("sensor.duty_time.reset", ResetAction, DUTY_TIME_ID_SCHEMA)
 | 
			
		||||
async def sensor_runtime_reset_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    return cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_condition(
 | 
			
		||||
    "sensor.duty_time.is_running", RunningCondition, DUTY_TIME_ID_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
async def duty_time_is_running_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg, paren, True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_condition(
 | 
			
		||||
    "sensor.duty_time.is_not_running", RunningCondition, DUTY_TIME_ID_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
async def duty_time_is_not_running_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg, paren, False)
 | 
			
		||||
@@ -547,6 +547,8 @@ def copy_files():
 | 
			
		||||
                    CORE.relative_build_path(f"components/{name}"),
 | 
			
		||||
                    dirs_exist_ok=True,
 | 
			
		||||
                    ignore=shutil.ignore_patterns(".git", ".github"),
 | 
			
		||||
                    symlinks=True,
 | 
			
		||||
                    ignore_dangling_symlinks=True,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    dir = os.path.dirname(__file__)
 | 
			
		||||
 
 | 
			
		||||
@@ -55,3 +55,4 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    if CORE.using_esp_idf:
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
 | 
			
		||||
 
 | 
			
		||||
@@ -107,16 +107,16 @@ void ESP32BLETracker::loop() {
 | 
			
		||||
        ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up.");
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      bool bulk_parsed = false;
 | 
			
		||||
 | 
			
		||||
      for (auto *listener : this->listeners_) {
 | 
			
		||||
        bulk_parsed |= listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
 | 
			
		||||
      }
 | 
			
		||||
      for (auto *client : this->clients_) {
 | 
			
		||||
        bulk_parsed |= client->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
 | 
			
		||||
      if (this->raw_advertisements_) {
 | 
			
		||||
        for (auto *listener : this->listeners_) {
 | 
			
		||||
          listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
 | 
			
		||||
        }
 | 
			
		||||
        for (auto *client : this->clients_) {
 | 
			
		||||
          client->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!bulk_parsed) {
 | 
			
		||||
      if (this->parse_advertisements_) {
 | 
			
		||||
        for (size_t i = 0; i < index; i++) {
 | 
			
		||||
          ESPBTDevice device;
 | 
			
		||||
          device.parse_scan_rst(this->scan_result_buffer_[i]);
 | 
			
		||||
@@ -284,6 +284,32 @@ void ESP32BLETracker::end_of_scan_() {
 | 
			
		||||
void ESP32BLETracker::register_client(ESPBTClient *client) {
 | 
			
		||||
  client->app_id = ++this->app_id_;
 | 
			
		||||
  this->clients_.push_back(client);
 | 
			
		||||
  this->recalculate_advertisement_parser_types();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) {
 | 
			
		||||
  listener->set_parent(this);
 | 
			
		||||
  this->listeners_.push_back(listener);
 | 
			
		||||
  this->recalculate_advertisement_parser_types();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::recalculate_advertisement_parser_types() {
 | 
			
		||||
  this->raw_advertisements_ = false;
 | 
			
		||||
  this->parse_advertisements_ = false;
 | 
			
		||||
  for (auto *listener : this->listeners_) {
 | 
			
		||||
    if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
 | 
			
		||||
      this->parse_advertisements_ = true;
 | 
			
		||||
    } else {
 | 
			
		||||
      this->raw_advertisements_ = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  for (auto *client : this->clients_) {
 | 
			
		||||
    if (client->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
 | 
			
		||||
      this->parse_advertisements_ = true;
 | 
			
		||||
    } else {
 | 
			
		||||
      this->raw_advertisements_ = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,11 @@ using namespace esp32_ble;
 | 
			
		||||
 | 
			
		||||
using adv_data_t = std::vector<uint8_t>;
 | 
			
		||||
 | 
			
		||||
enum AdvertisementParserType {
 | 
			
		||||
  PARSED_ADVERTISEMENTS,
 | 
			
		||||
  RAW_ADVERTISEMENTS,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct ServiceData {
 | 
			
		||||
  ESPBTUUID uuid;
 | 
			
		||||
  adv_data_t data;
 | 
			
		||||
@@ -116,6 +121,9 @@ class ESPBTDeviceListener {
 | 
			
		||||
  virtual bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) {
 | 
			
		||||
    return false;
 | 
			
		||||
  };
 | 
			
		||||
  virtual AdvertisementParserType get_advertisement_parser_type() {
 | 
			
		||||
    return AdvertisementParserType::PARSED_ADVERTISEMENTS;
 | 
			
		||||
  };
 | 
			
		||||
  void set_parent(ESP32BLETracker *parent) { parent_ = parent; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
@@ -184,12 +192,9 @@ class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEv
 | 
			
		||||
 | 
			
		||||
  void loop() override;
 | 
			
		||||
 | 
			
		||||
  void register_listener(ESPBTDeviceListener *listener) {
 | 
			
		||||
    listener->set_parent(this);
 | 
			
		||||
    this->listeners_.push_back(listener);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void register_listener(ESPBTDeviceListener *listener);
 | 
			
		||||
  void register_client(ESPBTClient *client);
 | 
			
		||||
  void recalculate_advertisement_parser_types();
 | 
			
		||||
 | 
			
		||||
  void print_bt_device_info(const ESPBTDevice &device);
 | 
			
		||||
 | 
			
		||||
@@ -231,6 +236,8 @@ class ESP32BLETracker : public Component, public GAPEventHandler, public GATTcEv
 | 
			
		||||
  bool scan_continuous_;
 | 
			
		||||
  bool scan_active_;
 | 
			
		||||
  bool scanner_idle_;
 | 
			
		||||
  bool raw_advertisements_{false};
 | 
			
		||||
  bool parse_advertisements_{false};
 | 
			
		||||
  SemaphoreHandle_t scan_result_lock_;
 | 
			
		||||
  SemaphoreHandle_t scan_end_lock_;
 | 
			
		||||
  size_t scan_result_index_{0};
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,7 @@ ETHERNET_TYPES = {
 | 
			
		||||
    "IP101": EthernetType.ETHERNET_TYPE_IP101,
 | 
			
		||||
    "JL1101": EthernetType.ETHERNET_TYPE_JL1101,
 | 
			
		||||
    "KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081,
 | 
			
		||||
    "KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t")
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,11 @@
 | 
			
		||||
#include <sys/cdefs.h>
 | 
			
		||||
#include "esp_log.h"
 | 
			
		||||
#include "esp_eth.h"
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
#include "esp_eth_phy_802_3.h"
 | 
			
		||||
#else
 | 
			
		||||
#include "eth_phy_regs_struct.h"
 | 
			
		||||
#endif
 | 
			
		||||
#include "freertos/FreeRTOS.h"
 | 
			
		||||
#include "freertos/task.h"
 | 
			
		||||
#include "driver/gpio.h"
 | 
			
		||||
@@ -170,7 +174,11 @@ static esp_err_t jl1101_reset_hw(esp_eth_phy_t *phy) {
 | 
			
		||||
  return ESP_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy, eth_phy_autoneg_cmd_t cmd, bool *nego_state) {
 | 
			
		||||
#else
 | 
			
		||||
static esp_err_t jl1101_negotiate(esp_eth_phy_t *phy) {
 | 
			
		||||
#endif
 | 
			
		||||
  phy_jl1101_t *jl1101 = __containerof(phy, phy_jl1101_t, parent);
 | 
			
		||||
  esp_eth_mediator_t *eth = jl1101->eth;
 | 
			
		||||
  /* in case any link status has changed, let's assume we're in link down status */
 | 
			
		||||
@@ -285,7 +293,11 @@ static esp_err_t jl1101_init(esp_eth_phy_t *phy) {
 | 
			
		||||
  esp_eth_mediator_t *eth = jl1101->eth;
 | 
			
		||||
  // Detect PHY address
 | 
			
		||||
  if (jl1101->addr == ESP_ETH_PHY_ADDR_AUTO) {
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
    PHY_CHECK(esp_eth_phy_802_3_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err);
 | 
			
		||||
#else
 | 
			
		||||
    PHY_CHECK(esp_eth_detect_phy_addr(eth, &jl1101->addr) == ESP_OK, "Detect PHY address failed", err);
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
  /* Power on Ethernet PHY */
 | 
			
		||||
  PHY_CHECK(jl1101_pwrctl(phy, true) == ESP_OK, "power control failed", err);
 | 
			
		||||
@@ -324,7 +336,11 @@ esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config) {
 | 
			
		||||
  jl1101->parent.init = jl1101_init;
 | 
			
		||||
  jl1101->parent.deinit = jl1101_deinit;
 | 
			
		||||
  jl1101->parent.set_mediator = jl1101_set_mediator;
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
  jl1101->parent.autonego_ctrl = jl1101_negotiate;
 | 
			
		||||
#else
 | 
			
		||||
  jl1101->parent.negotiate = jl1101_negotiate;
 | 
			
		||||
#endif
 | 
			
		||||
  jl1101->parent.get_link = jl1101_get_link;
 | 
			
		||||
  jl1101->parent.pwrctl = jl1101_pwrctl;
 | 
			
		||||
  jl1101->parent.get_addr = jl1101_get_addr;
 | 
			
		||||
 
 | 
			
		||||
@@ -41,18 +41,27 @@ void EthernetComponent::setup() {
 | 
			
		||||
  this->eth_netif_ = esp_netif_new(&cfg);
 | 
			
		||||
 | 
			
		||||
  // Init MAC and PHY configs to default
 | 
			
		||||
  eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
 | 
			
		||||
  eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
 | 
			
		||||
 | 
			
		||||
  phy_config.phy_addr = this->phy_addr_;
 | 
			
		||||
  phy_config.reset_gpio_num = this->power_pin_;
 | 
			
		||||
 | 
			
		||||
  eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
  eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG();
 | 
			
		||||
  esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_;
 | 
			
		||||
  esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_;
 | 
			
		||||
  esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_;
 | 
			
		||||
  esp32_emac_config.clock_config.rmii.clock_gpio = this->clk_gpio_;
 | 
			
		||||
 | 
			
		||||
  esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config);
 | 
			
		||||
#else
 | 
			
		||||
  mac_config.smi_mdc_gpio_num = this->mdc_pin_;
 | 
			
		||||
  mac_config.smi_mdio_gpio_num = this->mdio_pin_;
 | 
			
		||||
  mac_config.clock_config.rmii.clock_mode = this->clk_mode_;
 | 
			
		||||
  mac_config.clock_config.rmii.clock_gpio = this->clk_gpio_;
 | 
			
		||||
 | 
			
		||||
  esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&mac_config);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  switch (this->type_) {
 | 
			
		||||
    case ETHERNET_TYPE_LAN8720: {
 | 
			
		||||
@@ -75,8 +84,13 @@ void EthernetComponent::setup() {
 | 
			
		||||
      this->phy_ = esp_eth_phy_new_jl1101(&phy_config);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ETHERNET_TYPE_KSZ8081: {
 | 
			
		||||
    case ETHERNET_TYPE_KSZ8081:
 | 
			
		||||
    case ETHERNET_TYPE_KSZ8081RNA: {
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
      this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config);
 | 
			
		||||
#else
 | 
			
		||||
      this->phy_ = esp_eth_phy_new_ksz8081(&phy_config);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default: {
 | 
			
		||||
@@ -89,6 +103,12 @@ void EthernetComponent::setup() {
 | 
			
		||||
  this->eth_handle_ = nullptr;
 | 
			
		||||
  err = esp_eth_driver_install(ð_config, &this->eth_handle_);
 | 
			
		||||
  ESPHL_ERROR_CHECK(err, "ETH driver install error");
 | 
			
		||||
 | 
			
		||||
  if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) {
 | 
			
		||||
    // KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide.
 | 
			
		||||
    this->ksz8081_set_clock_reference_(mac);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* attach Ethernet driver to TCP/IP stack */
 | 
			
		||||
  err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_));
 | 
			
		||||
  ESPHL_ERROR_CHECK(err, "ETH netif attach error");
 | 
			
		||||
@@ -171,6 +191,10 @@ void EthernetComponent::dump_config() {
 | 
			
		||||
      eth_type = "KSZ8081";
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case ETHERNET_TYPE_KSZ8081RNA:
 | 
			
		||||
      eth_type = "KSZ8081RNA";
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      eth_type = "Unknown";
 | 
			
		||||
      break;
 | 
			
		||||
@@ -221,13 +245,13 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
 | 
			
		||||
      return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGV(TAG, "[Ethernet event] %s (num=%d)", event_name, event);
 | 
			
		||||
  ESP_LOGV(TAG, "[Ethernet event] %s (num=%" PRId32 ")", event_name, event);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
 | 
			
		||||
                                             void *event_data) {
 | 
			
		||||
  global_eth_component->connected_ = true;
 | 
			
		||||
  ESP_LOGV(TAG, "[Ethernet event] ETH Got IP (num=%d)", event_id);
 | 
			
		||||
  ESP_LOGV(TAG, "[Ethernet event] ETH Got IP (num=%" PRId32 ")", event_id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EthernetComponent::start_connect_() {
 | 
			
		||||
@@ -372,6 +396,37 @@ bool EthernetComponent::powerdown() {
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
 | 
			
		||||
#define KSZ80XX_PC2R_REG_ADDR (0x1F)
 | 
			
		||||
 | 
			
		||||
  esp_err_t err;
 | 
			
		||||
 | 
			
		||||
  uint32_t phy_control_2;
 | 
			
		||||
  err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
 | 
			
		||||
  ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
 | 
			
		||||
  ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str());
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
   * Bit 7 is `RMII Reference Clock Select`. Default is `0`.
 | 
			
		||||
   * KSZ8081RNA:
 | 
			
		||||
   *   0 - clock input to XI (Pin 8) is 25 MHz for RMII – 25 MHz clock mode.
 | 
			
		||||
   *   1 - clock input to XI (Pin 8) is 50 MHz for RMII – 50 MHz clock mode.
 | 
			
		||||
   * KSZ8081RND:
 | 
			
		||||
   *   0 - clock input to XI (Pin 8) is 50 MHz for RMII – 50 MHz clock mode.
 | 
			
		||||
   *   1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII – 25 MHz clock mode.
 | 
			
		||||
   */
 | 
			
		||||
  if ((phy_control_2 & (1 << 7)) != (1 << 7)) {
 | 
			
		||||
    phy_control_2 |= 1 << 7;
 | 
			
		||||
    err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2);
 | 
			
		||||
    ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
 | 
			
		||||
    err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
 | 
			
		||||
    ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
 | 
			
		||||
    ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#undef KSZ80XX_PC2R_REG_ADDR
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace ethernet
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ enum EthernetType {
 | 
			
		||||
  ETHERNET_TYPE_IP101,
 | 
			
		||||
  ETHERNET_TYPE_JL1101,
 | 
			
		||||
  ETHERNET_TYPE_KSZ8081,
 | 
			
		||||
  ETHERNET_TYPE_KSZ8081RNA,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct ManualIP {
 | 
			
		||||
@@ -67,6 +68,8 @@ class EthernetComponent : public Component {
 | 
			
		||||
 | 
			
		||||
  void start_connect_();
 | 
			
		||||
  void dump_connect_params_();
 | 
			
		||||
  /// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
 | 
			
		||||
  void ksz8081_set_clock_reference_(esp_eth_mac_t *mac);
 | 
			
		||||
 | 
			
		||||
  std::string use_address_;
 | 
			
		||||
  uint8_t phy_addr_{0};
 | 
			
		||||
 
 | 
			
		||||
@@ -3,11 +3,11 @@ from pathlib import Path
 | 
			
		||||
import hashlib
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
from packaging import version
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
 | 
			
		||||
from esphome import core
 | 
			
		||||
from esphome.components import display
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.helpers import copy_file_if_changed
 | 
			
		||||
@@ -29,9 +29,11 @@ DOMAIN = "font"
 | 
			
		||||
DEPENDENCIES = ["display"]
 | 
			
		||||
MULTI_CONF = True
 | 
			
		||||
 | 
			
		||||
Font = display.display_ns.class_("Font")
 | 
			
		||||
Glyph = display.display_ns.class_("Glyph")
 | 
			
		||||
GlyphData = display.display_ns.struct("GlyphData")
 | 
			
		||||
font_ns = cg.esphome_ns.namespace("font")
 | 
			
		||||
 | 
			
		||||
Font = font_ns.class_("Font")
 | 
			
		||||
Glyph = font_ns.class_("Glyph")
 | 
			
		||||
GlyphData = font_ns.struct("GlyphData")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_glyphs(value):
 | 
			
		||||
@@ -65,13 +67,18 @@ def validate_pillow_installed(value):
 | 
			
		||||
    except ImportError as err:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "Please install the pillow python package to use this feature. "
 | 
			
		||||
            "(pip install pillow)"
 | 
			
		||||
            '(pip install pillow">4.0.0,<10.0.0")'
 | 
			
		||||
        ) from err
 | 
			
		||||
 | 
			
		||||
    if PIL.__version__[0] < "4":
 | 
			
		||||
    if version.parse(PIL.__version__) < version.parse("4.0.0"):
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "Please update your pillow installation to at least 4.0.x. "
 | 
			
		||||
            "(pip install -U pillow)"
 | 
			
		||||
            '(pip install pillow">4.0.0,<10.0.0")'
 | 
			
		||||
        )
 | 
			
		||||
    if version.parse(PIL.__version__) >= version.parse("10.0.0"):
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "Please downgrade your pillow installation to below 10.0.0. "
 | 
			
		||||
            '(pip install pillow">4.0.0,<10.0.0")'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return value
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,35 @@
 | 
			
		||||
#include "font.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/color.h"
 | 
			
		||||
#include "esphome/components/display/display_buffer.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace display {
 | 
			
		||||
namespace font {
 | 
			
		||||
 | 
			
		||||
bool Glyph::get_pixel(int x, int y) const {
 | 
			
		||||
  const int x_data = x - this->glyph_data_->offset_x;
 | 
			
		||||
  const int y_data = y - this->glyph_data_->offset_y;
 | 
			
		||||
  if (x_data < 0 || x_data >= this->glyph_data_->width || y_data < 0 || y_data >= this->glyph_data_->height)
 | 
			
		||||
    return false;
 | 
			
		||||
  const uint32_t width_8 = ((this->glyph_data_->width + 7u) / 8u) * 8u;
 | 
			
		||||
  const uint32_t pos = x_data + y_data * width_8;
 | 
			
		||||
  return progmem_read_byte(this->glyph_data_->data + (pos / 8u)) & (0x80 >> (pos % 8u));
 | 
			
		||||
static const char *const TAG = "font";
 | 
			
		||||
 | 
			
		||||
void Glyph::draw(int x_at, int y_start, display::Display *display, Color color) const {
 | 
			
		||||
  int scan_x1, scan_y1, scan_width, scan_height;
 | 
			
		||||
  this->scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
 | 
			
		||||
 | 
			
		||||
  const unsigned char *data = this->glyph_data_->data;
 | 
			
		||||
  const int max_x = x_at + scan_x1 + scan_width;
 | 
			
		||||
  const int max_y = y_start + scan_y1 + scan_height;
 | 
			
		||||
 | 
			
		||||
  for (int glyph_y = y_start + scan_y1; glyph_y < max_y; glyph_y++) {
 | 
			
		||||
    for (int glyph_x = x_at + scan_x1; glyph_x < max_x; data++, glyph_x += 8) {
 | 
			
		||||
      uint8_t pixel_data = progmem_read_byte(data);
 | 
			
		||||
      const int pixel_max_x = std::min(max_x, glyph_x + 8);
 | 
			
		||||
 | 
			
		||||
      for (int pixel_x = glyph_x; pixel_x < pixel_max_x && pixel_data; pixel_x++, pixel_data <<= 1) {
 | 
			
		||||
        if (pixel_data & 0x80) {
 | 
			
		||||
          display->draw_pixel_at(pixel_x, glyph_y, color);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
const char *Glyph::get_char() const { return this->glyph_data_->a_char; }
 | 
			
		||||
bool Glyph::compare_to(const char *str) const {
 | 
			
		||||
@@ -47,6 +64,12 @@ void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
 | 
			
		||||
  *width = this->glyph_data_->width;
 | 
			
		||||
  *height = this->glyph_data_->height;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) {
 | 
			
		||||
  glyphs_.reserve(data_nr);
 | 
			
		||||
  for (int i = 0; i < data_nr; ++i)
 | 
			
		||||
    glyphs_.emplace_back(&data[i]);
 | 
			
		||||
}
 | 
			
		||||
int Font::match_next_glyph(const char *str, int *match_length) {
 | 
			
		||||
  int lo = 0;
 | 
			
		||||
  int hi = this->glyphs_.size() - 1;
 | 
			
		||||
@@ -95,11 +118,32 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in
 | 
			
		||||
  *x_offset = min_x;
 | 
			
		||||
  *width = x - min_x;
 | 
			
		||||
}
 | 
			
		||||
Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) {
 | 
			
		||||
  glyphs_.reserve(data_nr);
 | 
			
		||||
  for (int i = 0; i < data_nr; ++i)
 | 
			
		||||
    glyphs_.emplace_back(&data[i]);
 | 
			
		||||
void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text) {
 | 
			
		||||
  int i = 0;
 | 
			
		||||
  int x_at = x_start;
 | 
			
		||||
  while (text[i] != '\0') {
 | 
			
		||||
    int match_length;
 | 
			
		||||
    int glyph_n = this->match_next_glyph(text + i, &match_length);
 | 
			
		||||
    if (glyph_n < 0) {
 | 
			
		||||
      // Unknown char, skip
 | 
			
		||||
      ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
 | 
			
		||||
      if (!this->get_glyphs().empty()) {
 | 
			
		||||
        uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->width;
 | 
			
		||||
        display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color);
 | 
			
		||||
        x_at += glyph_width;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      i++;
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const Glyph &glyph = this->get_glyphs()[glyph_n];
 | 
			
		||||
    glyph.draw(x_at, y_start, display, color);
 | 
			
		||||
    x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
 | 
			
		||||
 | 
			
		||||
    i += match_length;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace display
 | 
			
		||||
}  // namespace font
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/datatypes.h"
 | 
			
		||||
#include "esphome/core/color.h"
 | 
			
		||||
#include "esphome/components/display/display_buffer.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace display {
 | 
			
		||||
namespace font {
 | 
			
		||||
 | 
			
		||||
class DisplayBuffer;
 | 
			
		||||
class Font;
 | 
			
		||||
 | 
			
		||||
struct GlyphData {
 | 
			
		||||
@@ -21,7 +22,7 @@ class Glyph {
 | 
			
		||||
 public:
 | 
			
		||||
  Glyph(const GlyphData *data) : glyph_data_(data) {}
 | 
			
		||||
 | 
			
		||||
  bool get_pixel(int x, int y) const;
 | 
			
		||||
  void draw(int x, int y, display::Display *display, Color color) const;
 | 
			
		||||
 | 
			
		||||
  const char *get_char() const;
 | 
			
		||||
 | 
			
		||||
@@ -33,12 +34,11 @@ class Glyph {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  friend Font;
 | 
			
		||||
  friend DisplayBuffer;
 | 
			
		||||
 | 
			
		||||
  const GlyphData *glyph_data_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class Font {
 | 
			
		||||
class Font : public display::BaseFont {
 | 
			
		||||
 public:
 | 
			
		||||
  /** Construct the font with the given glyphs.
 | 
			
		||||
   *
 | 
			
		||||
@@ -50,7 +50,8 @@ class Font {
 | 
			
		||||
 | 
			
		||||
  int match_next_glyph(const char *str, int *match_length);
 | 
			
		||||
 | 
			
		||||
  void measure(const char *str, int *width, int *x_offset, int *baseline, int *height);
 | 
			
		||||
  void print(int x_start, int y_start, display::Display *display, Color color, const char *text) override;
 | 
			
		||||
  void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) override;
 | 
			
		||||
  inline int get_baseline() { return this->baseline_; }
 | 
			
		||||
  inline int get_height() { return this->height_; }
 | 
			
		||||
 | 
			
		||||
@@ -62,5 +63,5 @@ class Font {
 | 
			
		||||
  int height_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace display
 | 
			
		||||
}  // namespace font
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
#include "graph.h"
 | 
			
		||||
#include "esphome/components/display/display_buffer.h"
 | 
			
		||||
#include "esphome/components/display/display.h"
 | 
			
		||||
#include "esphome/core/color.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
@@ -56,7 +56,7 @@ void GraphTrace::init(Graph *g) {
 | 
			
		||||
  this->data_.set_update_time_ms(g->get_duration() * 1000 / g->get_width());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Graph::draw(DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color) {
 | 
			
		||||
void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color color) {
 | 
			
		||||
  /// Plot border
 | 
			
		||||
  if (this->border_) {
 | 
			
		||||
    buff->horizontal_line(x_offset, y_offset, this->width_, color);
 | 
			
		||||
@@ -303,7 +303,7 @@ void GraphLegend::init(Graph *g) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Graph::draw_legend(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color) {
 | 
			
		||||
void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color) {
 | 
			
		||||
  if (!legend_)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,10 @@
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
// forward declare DisplayBuffer
 | 
			
		||||
// forward declare Display
 | 
			
		||||
namespace display {
 | 
			
		||||
class DisplayBuffer;
 | 
			
		||||
class Font;
 | 
			
		||||
class Display;
 | 
			
		||||
class BaseFont;
 | 
			
		||||
}  // namespace display
 | 
			
		||||
 | 
			
		||||
namespace graph {
 | 
			
		||||
@@ -45,8 +45,8 @@ enum ValuePositionType {
 | 
			
		||||
class GraphLegend {
 | 
			
		||||
 public:
 | 
			
		||||
  void init(Graph *g);
 | 
			
		||||
  void set_name_font(display::Font *font) { this->font_label_ = font; }
 | 
			
		||||
  void set_value_font(display::Font *font) { this->font_value_ = font; }
 | 
			
		||||
  void set_name_font(display::BaseFont *font) { this->font_label_ = font; }
 | 
			
		||||
  void set_value_font(display::BaseFont *font) { this->font_value_ = font; }
 | 
			
		||||
  void set_width(uint32_t width) { this->width_ = width; }
 | 
			
		||||
  void set_height(uint32_t height) { this->height_ = height; }
 | 
			
		||||
  void set_border(bool val) { this->border_ = val; }
 | 
			
		||||
@@ -63,8 +63,8 @@ class GraphLegend {
 | 
			
		||||
  ValuePositionType values_{VALUE_POSITION_TYPE_AUTO};
 | 
			
		||||
  bool units_{true};
 | 
			
		||||
  DirectionType direction_{DIRECTION_TYPE_AUTO};
 | 
			
		||||
  display::Font *font_label_{nullptr};
 | 
			
		||||
  display::Font *font_value_{nullptr};
 | 
			
		||||
  display::BaseFont *font_label_{nullptr};
 | 
			
		||||
  display::BaseFont *font_value_{nullptr};
 | 
			
		||||
  // Calculated values
 | 
			
		||||
  Graph *parent_{nullptr};
 | 
			
		||||
  //                      (x0)          (xs,ys)         (xs,ys)
 | 
			
		||||
@@ -133,8 +133,8 @@ class GraphTrace {
 | 
			
		||||
 | 
			
		||||
class Graph : public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void draw(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color);
 | 
			
		||||
  void draw_legend(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color);
 | 
			
		||||
  void draw(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color);
 | 
			
		||||
  void draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color);
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::PROCESSOR; }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										152
									
								
								esphome/components/grove_tb6612fng/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								esphome/components/grove_tb6612fng/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,152 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome.components import i2c
 | 
			
		||||
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_CHANNEL,
 | 
			
		||||
    CONF_SPEED,
 | 
			
		||||
    CONF_DIRECTION,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@max246"]
 | 
			
		||||
 | 
			
		||||
grove_tb6612fng_ns = cg.esphome_ns.namespace("grove_tb6612fng")
 | 
			
		||||
GROVE_TB6612FNG = grove_tb6612fng_ns.class_(
 | 
			
		||||
    "GroveMotorDriveTB6612FNG", cg.Component, i2c.I2CDevice
 | 
			
		||||
)
 | 
			
		||||
GROVETB6612FNGMotorRunAction = grove_tb6612fng_ns.class_(
 | 
			
		||||
    "GROVETB6612FNGMotorRunAction", automation.Action
 | 
			
		||||
)
 | 
			
		||||
GROVETB6612FNGMotorBrakeAction = grove_tb6612fng_ns.class_(
 | 
			
		||||
    "GROVETB6612FNGMotorBrakeAction", automation.Action
 | 
			
		||||
)
 | 
			
		||||
GROVETB6612FNGMotorStopAction = grove_tb6612fng_ns.class_(
 | 
			
		||||
    "GROVETB6612FNGMotorStopAction", automation.Action
 | 
			
		||||
)
 | 
			
		||||
GROVETB6612FNGMotorStandbyAction = grove_tb6612fng_ns.class_(
 | 
			
		||||
    "GROVETB6612FNGMotorStandbyAction", automation.Action
 | 
			
		||||
)
 | 
			
		||||
GROVETB6612FNGMotorNoStandbyAction = grove_tb6612fng_ns.class_(
 | 
			
		||||
    "GROVETB6612FNGMotorNoStandbyAction", automation.Action
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DIRECTION_TYPE = {
 | 
			
		||||
    "FORWARD": 1,
 | 
			
		||||
    "BACKWARD": 2,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_ID): cv.declare_id(GROVE_TB6612FNG),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
    .extend(i2c.i2c_device_schema(0x14))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "grove_tb6612fng.run",
 | 
			
		||||
    GROVETB6612FNGMotorRunAction,
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG),
 | 
			
		||||
            cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)),
 | 
			
		||||
            cv.Required(CONF_SPEED): cv.templatable(cv.int_range(min=0, max=255)),
 | 
			
		||||
            cv.Required(CONF_DIRECTION): cv.enum(DIRECTION_TYPE, upper=True),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def grove_tb6612fng_run_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg)
 | 
			
		||||
    await cg.register_parented(var, config[CONF_ID])
 | 
			
		||||
 | 
			
		||||
    template_channel = await cg.templatable(config[CONF_CHANNEL], args, int)
 | 
			
		||||
    template_speed = await cg.templatable(config[CONF_SPEED], args, cg.uint16)
 | 
			
		||||
    template_speed = (
 | 
			
		||||
        template_speed if config[CONF_DIRECTION] == "FORWARD" else -template_speed
 | 
			
		||||
    )
 | 
			
		||||
    cg.add(var.set_channel(template_channel))
 | 
			
		||||
    cg.add(var.set_speed(template_speed))
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "grove_tb6612fng.break",
 | 
			
		||||
    GROVETB6612FNGMotorBrakeAction,
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG),
 | 
			
		||||
            cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def grove_tb6612fng_break_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg)
 | 
			
		||||
    await cg.register_parented(var, config[CONF_ID])
 | 
			
		||||
 | 
			
		||||
    template_channel = await cg.templatable(config[CONF_CHANNEL], args, int)
 | 
			
		||||
    cg.add(var.set_channel(template_channel))
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "grove_tb6612fng.stop",
 | 
			
		||||
    GROVETB6612FNGMotorStopAction,
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG),
 | 
			
		||||
            cv.Required(CONF_CHANNEL): cv.templatable(cv.int_range(min=0, max=1)),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def grove_tb6612fng_stop_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg)
 | 
			
		||||
    await cg.register_parented(var, config[CONF_ID])
 | 
			
		||||
 | 
			
		||||
    template_channel = await cg.templatable(config[CONF_CHANNEL], args, int)
 | 
			
		||||
    cg.add(var.set_channel(template_channel))
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "grove_tb6612fng.standby",
 | 
			
		||||
    GROVETB6612FNGMotorStandbyAction,
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def grove_tb6612fng_standby_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg)
 | 
			
		||||
    await cg.register_parented(var, config[CONF_ID])
 | 
			
		||||
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "grove_tb6612fng.no_standby",
 | 
			
		||||
    GROVETB6612FNGMotorNoStandbyAction,
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_ID): cv.use_id(GROVE_TB6612FNG),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def grove_tb6612fng_no_standby_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg)
 | 
			
		||||
    await cg.register_parented(var, config[CONF_ID])
 | 
			
		||||
 | 
			
		||||
    return var
 | 
			
		||||
							
								
								
									
										171
									
								
								esphome/components/grove_tb6612fng/grove_tb6612fng.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								esphome/components/grove_tb6612fng/grove_tb6612fng.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,171 @@
 | 
			
		||||
#include "grove_tb6612fng.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace grove_tb6612fng {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "GroveMotorDriveTB6612FNG";
 | 
			
		||||
 | 
			
		||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_BRAKE = 0x00;
 | 
			
		||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STOP = 0x01;
 | 
			
		||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_CW = 0x02;
 | 
			
		||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_CCW = 0x03;
 | 
			
		||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STANDBY = 0x04;
 | 
			
		||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_NOT_STANDBY = 0x05;
 | 
			
		||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN = 0x06;
 | 
			
		||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP = 0x07;
 | 
			
		||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_KEEP_RUN = 0x08;
 | 
			
		||||
static const uint8_t GROVE_MOTOR_DRIVER_I2C_CMD_SET_ADDR = 0x11;
 | 
			
		||||
 | 
			
		||||
void GroveMotorDriveTB6612FNG::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "GroveMotorDriveTB6612FNG:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void GroveMotorDriveTB6612FNG::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up Grove Motor Drive TB6612FNG ...");
 | 
			
		||||
  if (!this->standby()) {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool GroveMotorDriveTB6612FNG::standby() {
 | 
			
		||||
  uint8_t status = 0;
 | 
			
		||||
  if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STANDBY, &status, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "Set standby failed!");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool GroveMotorDriveTB6612FNG::not_standby() {
 | 
			
		||||
  uint8_t status = 0;
 | 
			
		||||
  if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_NOT_STANDBY, &status, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "Set not standby failed!");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void GroveMotorDriveTB6612FNG::set_i2c_addr(uint8_t addr) {
 | 
			
		||||
  if (addr == 0x00 || addr >= 0x80) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_SET_ADDR, &addr, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "Set new i2c address failed!");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->set_i2c_address(addr);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void GroveMotorDriveTB6612FNG::dc_motor_run(uint8_t channel, int16_t speed) {
 | 
			
		||||
  speed = clamp<int16_t>(speed, -255, 255);
 | 
			
		||||
 | 
			
		||||
  buffer_[0] = channel;
 | 
			
		||||
  if (speed >= 0) {
 | 
			
		||||
    buffer_[1] = speed;
 | 
			
		||||
  } else {
 | 
			
		||||
    buffer_[1] = (uint8_t) (-speed);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (speed >= 0) {
 | 
			
		||||
    if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CW, buffer_, 2) != i2c::ERROR_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "Run motor failed!");
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_CCW, buffer_, 2) != i2c::ERROR_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "Run motor failed!");
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void GroveMotorDriveTB6612FNG::dc_motor_brake(uint8_t channel) {
 | 
			
		||||
  if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_BRAKE, &channel, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "Break motor failed!");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void GroveMotorDriveTB6612FNG::dc_motor_stop(uint8_t channel) {
 | 
			
		||||
  if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STOP, &channel, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "Stop dc motor failed!");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps, uint16_t rpm) {
 | 
			
		||||
  uint8_t cw = 0;
 | 
			
		||||
  // 0.1ms_per_step
 | 
			
		||||
  uint16_t ms_per_step = 0;
 | 
			
		||||
 | 
			
		||||
  if (steps > 0) {
 | 
			
		||||
    cw = 1;
 | 
			
		||||
  }
 | 
			
		||||
  // stop
 | 
			
		||||
  else if (steps == 0) {
 | 
			
		||||
    this->stepper_stop();
 | 
			
		||||
    return;
 | 
			
		||||
  } else if (steps == INT16_MIN) {
 | 
			
		||||
    steps = INT16_MAX;
 | 
			
		||||
  } else {
 | 
			
		||||
    steps = -steps;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  rpm = clamp<uint16_t>(rpm, 1, 300);
 | 
			
		||||
 | 
			
		||||
  ms_per_step = (uint16_t) (3000.0 / (float) rpm);
 | 
			
		||||
  buffer_[0] = mode;
 | 
			
		||||
  buffer_[1] = cw;  //(cw=1) => cw; (cw=0) => ccw
 | 
			
		||||
  buffer_[2] = steps;
 | 
			
		||||
  buffer_[3] = (steps >> 8);
 | 
			
		||||
  buffer_[4] = ms_per_step;
 | 
			
		||||
  buffer_[5] = (ms_per_step >> 8);
 | 
			
		||||
 | 
			
		||||
  if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "Run stepper failed!");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void GroveMotorDriveTB6612FNG::stepper_stop() {
 | 
			
		||||
  if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_STOP, nullptr, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "Send stop stepper failed!");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void GroveMotorDriveTB6612FNG::stepper_keep_run(StepperModeTypeT mode, uint16_t rpm, bool is_cw) {
 | 
			
		||||
  // 4=>infinite ccw  5=>infinite cw
 | 
			
		||||
  uint8_t cw = (is_cw) ? 5 : 4;
 | 
			
		||||
  // 0.1ms_per_step
 | 
			
		||||
  uint16_t ms_per_step = 0;
 | 
			
		||||
 | 
			
		||||
  rpm = clamp<uint16_t>(rpm, 1, 300);
 | 
			
		||||
  ms_per_step = (uint16_t) (3000.0 / (float) rpm);
 | 
			
		||||
 | 
			
		||||
  buffer_[0] = mode;
 | 
			
		||||
  buffer_[1] = cw;  //(cw=1) => cw; (cw=0) => ccw
 | 
			
		||||
  buffer_[2] = ms_per_step;
 | 
			
		||||
  buffer_[3] = (ms_per_step >> 8);
 | 
			
		||||
 | 
			
		||||
  if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_KEEP_RUN, buffer_, 4) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "Write stepper keep run failed");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}  // namespace grove_tb6612fng
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										208
									
								
								esphome/components/grove_tb6612fng/grove_tb6612fng.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								esphome/components/grove_tb6612fng/grove_tb6612fng.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,208 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
//#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
    Grove_Motor_Driver_TB6612FNG.h
 | 
			
		||||
    A library for the Grove - Motor Driver(TB6612FNG)
 | 
			
		||||
    Copyright (c) 2018 seeed technology co., ltd.
 | 
			
		||||
    Website    : www.seeed.cc
 | 
			
		||||
    Author     : Jerry Yip
 | 
			
		||||
    Create Time: 2018-06
 | 
			
		||||
    Version    : 0.1
 | 
			
		||||
    Change Log :
 | 
			
		||||
    The MIT License (MIT)
 | 
			
		||||
    Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
    of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
    in the Software without restriction, including without limitation the rights
 | 
			
		||||
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
    copies of the Software, and to permit persons to whom the Software is
 | 
			
		||||
    furnished to do so, subject to the following conditions:
 | 
			
		||||
    The above copyright notice and this permission notice shall be included in
 | 
			
		||||
    all copies or substantial portions of the Software.
 | 
			
		||||
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
			
		||||
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
			
		||||
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
			
		||||
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
			
		||||
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 | 
			
		||||
    THE SOFTWARE.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace grove_tb6612fng {
 | 
			
		||||
 | 
			
		||||
enum MotorChannelTypeT {
 | 
			
		||||
  MOTOR_CHA = 0,
 | 
			
		||||
  MOTOR_CHB = 1,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum StepperModeTypeT {
 | 
			
		||||
  FULL_STEP = 0,
 | 
			
		||||
  WAVE_DRIVE = 1,
 | 
			
		||||
  HALF_STEP = 2,
 | 
			
		||||
  MICRO_STEPPING = 3,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class GroveMotorDriveTB6612FNG : public Component, public i2c::I2CDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
  /*************************************************************
 | 
			
		||||
      Description
 | 
			
		||||
       Enter standby mode. Normally you don't need to call this, except that
 | 
			
		||||
       you have called notStandby() before.
 | 
			
		||||
      Parameter
 | 
			
		||||
       Null.
 | 
			
		||||
      Return
 | 
			
		||||
       True/False.
 | 
			
		||||
  *************************************************************/
 | 
			
		||||
  bool standby();
 | 
			
		||||
 | 
			
		||||
  /*************************************************************
 | 
			
		||||
      Description
 | 
			
		||||
       Exit standby mode. Motor driver does't do any action at this mode.
 | 
			
		||||
      Parameter
 | 
			
		||||
       Null.
 | 
			
		||||
      Return
 | 
			
		||||
       True/False.
 | 
			
		||||
  *************************************************************/
 | 
			
		||||
  bool not_standby();
 | 
			
		||||
 | 
			
		||||
  /*************************************************************
 | 
			
		||||
      Description
 | 
			
		||||
       Set an new I2C address.
 | 
			
		||||
      Parameter
 | 
			
		||||
       addr: 0x01~0x7f
 | 
			
		||||
      Return
 | 
			
		||||
       Null.
 | 
			
		||||
  *************************************************************/
 | 
			
		||||
  void set_i2c_addr(uint8_t addr);
 | 
			
		||||
 | 
			
		||||
  /*************************************************************
 | 
			
		||||
      Description
 | 
			
		||||
       Drive a motor.
 | 
			
		||||
      Parameter
 | 
			
		||||
       chl: MOTOR_CHA or MOTOR_CHB
 | 
			
		||||
       speed: -255~255, if speed > 0, motor moves clockwise.
 | 
			
		||||
              Note that there is always a starting speed(a starting voltage) for motor.
 | 
			
		||||
              If the input voltage is 5V, the starting speed should larger than 100 or
 | 
			
		||||
              smaller than -100.
 | 
			
		||||
      Return
 | 
			
		||||
       Null.
 | 
			
		||||
  *************************************************************/
 | 
			
		||||
  void dc_motor_run(uint8_t channel, int16_t speed);
 | 
			
		||||
 | 
			
		||||
  /*************************************************************
 | 
			
		||||
      Description
 | 
			
		||||
       Brake, stop the motor immediately
 | 
			
		||||
      Parameter
 | 
			
		||||
       chl: MOTOR_CHA or MOTOR_CHB
 | 
			
		||||
      Return
 | 
			
		||||
       Null.
 | 
			
		||||
  *************************************************************/
 | 
			
		||||
  void dc_motor_brake(uint8_t channel);
 | 
			
		||||
 | 
			
		||||
  /*************************************************************
 | 
			
		||||
      Description
 | 
			
		||||
       Stop the motor slowly.
 | 
			
		||||
      Parameter
 | 
			
		||||
       chl: MOTOR_CHA or MOTOR_CHB
 | 
			
		||||
      Return
 | 
			
		||||
       Null.
 | 
			
		||||
  *************************************************************/
 | 
			
		||||
  void dc_motor_stop(uint8_t channel);
 | 
			
		||||
 | 
			
		||||
  /*************************************************************
 | 
			
		||||
      Description
 | 
			
		||||
       Drive a stepper.
 | 
			
		||||
      Parameter
 | 
			
		||||
       mode:  4 driver mode: FULL_STEP,WAVE_DRIVE, HALF_STEP, MICRO_STEPPING,
 | 
			
		||||
              for more information: https://en.wikipedia.org/wiki/Stepper_motor#/media/File:Drive.png
 | 
			
		||||
       steps: The number of steps to run, range from -32768 to 32767.
 | 
			
		||||
              When steps = 0, the stepper stops.
 | 
			
		||||
              When steps > 0, the stepper runs clockwise. When steps < 0, the stepper runs anticlockwise.
 | 
			
		||||
       rpm:   Revolutions per minute, the speed of a stepper, range from 1 to 300.
 | 
			
		||||
              Note that high rpm will lead to step lose, so rpm should not be larger than 150.
 | 
			
		||||
      Return
 | 
			
		||||
       Null.
 | 
			
		||||
  *************************************************************/
 | 
			
		||||
  void stepper_run(StepperModeTypeT mode, int16_t steps, uint16_t rpm);
 | 
			
		||||
 | 
			
		||||
  /*************************************************************
 | 
			
		||||
      Description
 | 
			
		||||
       Stop a stepper.
 | 
			
		||||
      Parameter
 | 
			
		||||
       Null.
 | 
			
		||||
      Return
 | 
			
		||||
       Null.
 | 
			
		||||
  *************************************************************/
 | 
			
		||||
  void stepper_stop();
 | 
			
		||||
 | 
			
		||||
  // keeps moving(direction same as the last move, default to clockwise)
 | 
			
		||||
  /*************************************************************
 | 
			
		||||
      Description
 | 
			
		||||
       Keep a stepper running.
 | 
			
		||||
      Parameter
 | 
			
		||||
       mode:  4 driver mode: FULL_STEP,WAVE_DRIVE, HALF_STEP, MICRO_STEPPING,
 | 
			
		||||
              for more information: https://en.wikipedia.org/wiki/Stepper_motor#/media/File:Drive.png
 | 
			
		||||
       rpm:   Revolutions per minute, the speed of a stepper, range from 1 to 300.
 | 
			
		||||
              Note that high rpm will lead to step lose, so rpm should not be larger than 150.
 | 
			
		||||
       is_cw: Set the running direction, true for clockwise and false for anti-clockwise.
 | 
			
		||||
      Return
 | 
			
		||||
       Null.
 | 
			
		||||
  *************************************************************/
 | 
			
		||||
  void stepper_keep_run(StepperModeTypeT mode, uint16_t rpm, bool is_cw);
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  uint8_t buffer_[16];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts>
 | 
			
		||||
class GROVETB6612FNGMotorRunAction : public Action<Ts...>, public Parented<GroveMotorDriveTB6612FNG> {
 | 
			
		||||
 public:
 | 
			
		||||
  TEMPLATABLE_VALUE(uint8_t, channel)
 | 
			
		||||
  TEMPLATABLE_VALUE(uint16_t, speed)
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    auto channel = this->channel_.value(x...);
 | 
			
		||||
    auto speed = this->speed_.value(x...);
 | 
			
		||||
    this->parent_->dc_motor_run(channel, speed);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts>
 | 
			
		||||
class GROVETB6612FNGMotorBrakeAction : public Action<Ts...>, public Parented<GroveMotorDriveTB6612FNG> {
 | 
			
		||||
 public:
 | 
			
		||||
  TEMPLATABLE_VALUE(uint8_t, channel)
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override { this->parent_->dc_motor_brake(this->channel_.value(x...)); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts>
 | 
			
		||||
class GROVETB6612FNGMotorStopAction : public Action<Ts...>, public Parented<GroveMotorDriveTB6612FNG> {
 | 
			
		||||
 public:
 | 
			
		||||
  TEMPLATABLE_VALUE(uint8_t, channel)
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override { this->parent_->dc_motor_stop(this->channel_.value(x...)); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts>
 | 
			
		||||
class GROVETB6612FNGMotorStandbyAction : public Action<Ts...>, public Parented<GroveMotorDriveTB6612FNG> {
 | 
			
		||||
 public:
 | 
			
		||||
  void play(Ts... x) override { this->parent_->standby(); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts>
 | 
			
		||||
class GROVETB6612FNGMotorNoStandbyAction : public Action<Ts...>, public Parented<GroveMotorDriveTB6612FNG> {
 | 
			
		||||
 public:
 | 
			
		||||
  void play(Ts... x) override { this->parent_->not_standby(); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace grove_tb6612fng
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
CODEOWNERS = ["@Yarikx"]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										130
									
								
								esphome/components/haier/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								esphome/components/haier/automation.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,130 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "haier_base.h"
 | 
			
		||||
#include "hon_climate.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace haier {
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class DisplayOnAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  DisplayOnAction(HaierClimateBase *parent) : parent_(parent) {}
 | 
			
		||||
  void play(Ts... x) { this->parent_->set_display_state(true); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  HaierClimateBase *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class DisplayOffAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  DisplayOffAction(HaierClimateBase *parent) : parent_(parent) {}
 | 
			
		||||
  void play(Ts... x) { this->parent_->set_display_state(false); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  HaierClimateBase *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class BeeperOnAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  BeeperOnAction(HonClimate *parent) : parent_(parent) {}
 | 
			
		||||
  void play(Ts... x) { this->parent_->set_beeper_state(true); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  HonClimate *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class BeeperOffAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  BeeperOffAction(HonClimate *parent) : parent_(parent) {}
 | 
			
		||||
  void play(Ts... x) { this->parent_->set_beeper_state(false); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  HonClimate *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class VerticalAirflowAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  VerticalAirflowAction(HonClimate *parent) : parent_(parent) {}
 | 
			
		||||
  TEMPLATABLE_VALUE(AirflowVerticalDirection, direction)
 | 
			
		||||
  void play(Ts... x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  HonClimate *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class HorizontalAirflowAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  HorizontalAirflowAction(HonClimate *parent) : parent_(parent) {}
 | 
			
		||||
  TEMPLATABLE_VALUE(AirflowHorizontalDirection, direction)
 | 
			
		||||
  void play(Ts... x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  HonClimate *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class HealthOnAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  HealthOnAction(HaierClimateBase *parent) : parent_(parent) {}
 | 
			
		||||
  void play(Ts... x) { this->parent_->set_health_mode(true); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  HaierClimateBase *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class HealthOffAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  HealthOffAction(HaierClimateBase *parent) : parent_(parent) {}
 | 
			
		||||
  void play(Ts... x) { this->parent_->set_health_mode(false); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  HaierClimateBase *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class StartSelfCleaningAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  StartSelfCleaningAction(HonClimate *parent) : parent_(parent) {}
 | 
			
		||||
  void play(Ts... x) { this->parent_->start_self_cleaning(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  HonClimate *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class StartSteriCleaningAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  StartSteriCleaningAction(HonClimate *parent) : parent_(parent) {}
 | 
			
		||||
  void play(Ts... x) { this->parent_->start_steri_cleaning(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  HonClimate *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class PowerOnAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  PowerOnAction(HaierClimateBase *parent) : parent_(parent) {}
 | 
			
		||||
  void play(Ts... x) { this->parent_->send_power_on_command(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  HaierClimateBase *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class PowerOffAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  PowerOffAction(HaierClimateBase *parent) : parent_(parent) {}
 | 
			
		||||
  void play(Ts... x) { this->parent_->send_power_off_command(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  HaierClimateBase *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class PowerToggleAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  PowerToggleAction(HaierClimateBase *parent) : parent_(parent) {}
 | 
			
		||||
  void play(Ts... x) { this->parent_->toggle_power(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  HaierClimateBase *parent_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace haier
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,43 +1,364 @@
 | 
			
		||||
from esphome.components import climate
 | 
			
		||||
import logging
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import uart
 | 
			
		||||
from esphome.components.climate import ClimateSwingMode
 | 
			
		||||
from esphome.const import CONF_ID, CONF_SUPPORTED_SWING_MODES
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
from esphome.components import uart, sensor, climate, logger
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_BEEPER,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_LEVEL,
 | 
			
		||||
    CONF_LOGGER,
 | 
			
		||||
    CONF_LOGS,
 | 
			
		||||
    CONF_MAX_TEMPERATURE,
 | 
			
		||||
    CONF_MIN_TEMPERATURE,
 | 
			
		||||
    CONF_PROTOCOL,
 | 
			
		||||
    CONF_SUPPORTED_MODES,
 | 
			
		||||
    CONF_SUPPORTED_SWING_MODES,
 | 
			
		||||
    CONF_VISUAL,
 | 
			
		||||
    CONF_WIFI,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    ICON_THERMOMETER,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
)
 | 
			
		||||
from esphome.components.climate import (
 | 
			
		||||
    ClimateSwingMode,
 | 
			
		||||
    ClimateMode,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["uart"]
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
PROTOCOL_MIN_TEMPERATURE = 16.0
 | 
			
		||||
PROTOCOL_MAX_TEMPERATURE = 30.0
 | 
			
		||||
PROTOCOL_TEMPERATURE_STEP = 1.0
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@paveldn"]
 | 
			
		||||
AUTO_LOAD = ["sensor"]
 | 
			
		||||
DEPENDENCIES = ["climate", "uart"]
 | 
			
		||||
CONF_WIFI_SIGNAL = "wifi_signal"
 | 
			
		||||
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
 | 
			
		||||
CONF_VERTICAL_AIRFLOW = "vertical_airflow"
 | 
			
		||||
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
PROTOCOL_HON = "HON"
 | 
			
		||||
PROTOCOL_SMARTAIR2 = "SMARTAIR2"
 | 
			
		||||
PROTOCOLS_SUPPORTED = [PROTOCOL_HON, PROTOCOL_SMARTAIR2]
 | 
			
		||||
 | 
			
		||||
haier_ns = cg.esphome_ns.namespace("haier")
 | 
			
		||||
HaierClimate = haier_ns.class_(
 | 
			
		||||
    "HaierClimate", climate.Climate, cg.PollingComponent, uart.UARTDevice
 | 
			
		||||
HaierClimateBase = haier_ns.class_(
 | 
			
		||||
    "HaierClimateBase", uart.UARTDevice, climate.Climate, cg.Component
 | 
			
		||||
)
 | 
			
		||||
HonClimate = haier_ns.class_("HonClimate", HaierClimateBase)
 | 
			
		||||
Smartair2Climate = haier_ns.class_("Smartair2Climate", HaierClimateBase)
 | 
			
		||||
 | 
			
		||||
ALLOWED_CLIMATE_SWING_MODES = {
 | 
			
		||||
    "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
 | 
			
		||||
    "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
 | 
			
		||||
    "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
 | 
			
		||||
 | 
			
		||||
AirflowVerticalDirection = haier_ns.enum("AirflowVerticalDirection")
 | 
			
		||||
AIRFLOW_VERTICAL_DIRECTION_OPTIONS = {
 | 
			
		||||
    "UP": AirflowVerticalDirection.UP,
 | 
			
		||||
    "CENTER": AirflowVerticalDirection.CENTER,
 | 
			
		||||
    "DOWN": AirflowVerticalDirection.DOWN,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True)
 | 
			
		||||
AirflowHorizontalDirection = haier_ns.enum("AirflowHorizontalDirection")
 | 
			
		||||
AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = {
 | 
			
		||||
    "LEFT": AirflowHorizontalDirection.LEFT,
 | 
			
		||||
    "CENTER": AirflowHorizontalDirection.CENTER,
 | 
			
		||||
    "RIGHT": AirflowHorizontalDirection.RIGHT,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
SUPPORTED_SWING_MODES_OPTIONS = {
 | 
			
		||||
    "OFF": ClimateSwingMode.CLIMATE_SWING_OFF,  # always available
 | 
			
		||||
    "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,  # always available
 | 
			
		||||
    "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
 | 
			
		||||
    "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SUPPORTED_CLIMATE_MODES_OPTIONS = {
 | 
			
		||||
    "OFF": ClimateMode.CLIMATE_MODE_OFF,  # always available
 | 
			
		||||
    "AUTO": ClimateMode.CLIMATE_MODE_AUTO,  # always available
 | 
			
		||||
    "COOL": ClimateMode.CLIMATE_MODE_COOL,
 | 
			
		||||
    "HEAT": ClimateMode.CLIMATE_MODE_HEAT,
 | 
			
		||||
    "DRY": ClimateMode.CLIMATE_MODE_DRY,
 | 
			
		||||
    "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_visual(config):
 | 
			
		||||
    if CONF_VISUAL in config:
 | 
			
		||||
        visual_config = config[CONF_VISUAL]
 | 
			
		||||
        if CONF_MIN_TEMPERATURE in visual_config:
 | 
			
		||||
            min_temp = visual_config[CONF_MIN_TEMPERATURE]
 | 
			
		||||
            if min_temp < PROTOCOL_MIN_TEMPERATURE:
 | 
			
		||||
                raise cv.Invalid(
 | 
			
		||||
                    f"Configured visual minimum temperature {min_temp} is lower than supported by Haier protocol is {PROTOCOL_MIN_TEMPERATURE}"
 | 
			
		||||
                )
 | 
			
		||||
        else:
 | 
			
		||||
            config[CONF_VISUAL][CONF_MIN_TEMPERATURE] = PROTOCOL_MIN_TEMPERATURE
 | 
			
		||||
        if CONF_MAX_TEMPERATURE in visual_config:
 | 
			
		||||
            max_temp = visual_config[CONF_MAX_TEMPERATURE]
 | 
			
		||||
            if max_temp > PROTOCOL_MAX_TEMPERATURE:
 | 
			
		||||
                raise cv.Invalid(
 | 
			
		||||
                    f"Configured visual maximum temperature {max_temp} is higher than supported by Haier protocol is {PROTOCOL_MAX_TEMPERATURE}"
 | 
			
		||||
                )
 | 
			
		||||
        else:
 | 
			
		||||
            config[CONF_VISUAL][CONF_MAX_TEMPERATURE] = PROTOCOL_MAX_TEMPERATURE
 | 
			
		||||
    else:
 | 
			
		||||
        config[CONF_VISUAL] = {
 | 
			
		||||
            CONF_MIN_TEMPERATURE: PROTOCOL_MIN_TEMPERATURE,
 | 
			
		||||
            CONF_MAX_TEMPERATURE: PROTOCOL_MAX_TEMPERATURE,
 | 
			
		||||
        }
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
BASE_CONFIG_SCHEMA = (
 | 
			
		||||
    climate.CLIMATE_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(HaierClimate),
 | 
			
		||||
            cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
 | 
			
		||||
                validate_swing_modes
 | 
			
		||||
            cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
 | 
			
		||||
                cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True)
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
                CONF_SUPPORTED_SWING_MODES,
 | 
			
		||||
                default=[
 | 
			
		||||
                    "OFF",
 | 
			
		||||
                    "VERTICAL",
 | 
			
		||||
                    "HORIZONTAL",
 | 
			
		||||
                    "BOTH",
 | 
			
		||||
                ],
 | 
			
		||||
            ): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("5s"))
 | 
			
		||||
    .extend(uart.UART_DEVICE_SCHEMA),
 | 
			
		||||
    .extend(uart.UART_DEVICE_SCHEMA)
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.typed_schema(
 | 
			
		||||
        {
 | 
			
		||||
            PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(): cv.declare_id(Smartair2Climate),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(): cv.declare_id(HonClimate),
 | 
			
		||||
                    cv.Optional(CONF_WIFI_SIGNAL, default=True): cv.boolean,
 | 
			
		||||
                    cv.Optional(CONF_BEEPER, default=True): cv.boolean,
 | 
			
		||||
                    cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                        unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
                        icon=ICON_THERMOMETER,
 | 
			
		||||
                        accuracy_decimals=0,
 | 
			
		||||
                        device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
        },
 | 
			
		||||
        key=CONF_PROTOCOL,
 | 
			
		||||
        default_type=PROTOCOL_SMARTAIR2,
 | 
			
		||||
        upper=True,
 | 
			
		||||
    ),
 | 
			
		||||
    validate_visual,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Actions
 | 
			
		||||
DisplayOnAction = haier_ns.class_("DisplayOnAction", automation.Action)
 | 
			
		||||
DisplayOffAction = haier_ns.class_("DisplayOffAction", automation.Action)
 | 
			
		||||
BeeperOnAction = haier_ns.class_("BeeperOnAction", automation.Action)
 | 
			
		||||
BeeperOffAction = haier_ns.class_("BeeperOffAction", automation.Action)
 | 
			
		||||
StartSelfCleaningAction = haier_ns.class_("StartSelfCleaningAction", automation.Action)
 | 
			
		||||
StartSteriCleaningAction = haier_ns.class_(
 | 
			
		||||
    "StartSteriCleaningAction", automation.Action
 | 
			
		||||
)
 | 
			
		||||
VerticalAirflowAction = haier_ns.class_("VerticalAirflowAction", automation.Action)
 | 
			
		||||
HorizontalAirflowAction = haier_ns.class_("HorizontalAirflowAction", automation.Action)
 | 
			
		||||
HealthOnAction = haier_ns.class_("HealthOnAction", automation.Action)
 | 
			
		||||
HealthOffAction = haier_ns.class_("HealthOffAction", automation.Action)
 | 
			
		||||
PowerOnAction = haier_ns.class_("PowerOnAction", automation.Action)
 | 
			
		||||
PowerOffAction = haier_ns.class_("PowerOffAction", automation.Action)
 | 
			
		||||
PowerToggleAction = haier_ns.class_("PowerToggleAction", automation.Action)
 | 
			
		||||
 | 
			
		||||
HAIER_BASE_ACTION_SCHEMA = automation.maybe_simple_id(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(HaierClimateBase),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(HonClimate),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "climate.haier.display_on", DisplayOnAction, HAIER_BASE_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "climate.haier.display_off", DisplayOffAction, HAIER_BASE_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
async def display_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "climate.haier.beeper_on", BeeperOnAction, HAIER_HON_BASE_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "climate.haier.beeper_off", BeeperOffAction, HAIER_HON_BASE_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
async def beeper_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Start self cleaning or steri-cleaning action action
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "climate.haier.start_self_cleaning",
 | 
			
		||||
    StartSelfCleaningAction,
 | 
			
		||||
    HAIER_HON_BASE_ACTION_SCHEMA,
 | 
			
		||||
)
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "climate.haier.start_steri_cleaning",
 | 
			
		||||
    StartSteriCleaningAction,
 | 
			
		||||
    HAIER_HON_BASE_ACTION_SCHEMA,
 | 
			
		||||
)
 | 
			
		||||
async def start_cleaning_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Set vertical airflow direction action
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "climate.haier.set_vertical_airflow",
 | 
			
		||||
    VerticalAirflowAction,
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.use_id(HonClimate),
 | 
			
		||||
            cv.Required(CONF_VERTICAL_AIRFLOW): cv.templatable(
 | 
			
		||||
                cv.enum(AIRFLOW_VERTICAL_DIRECTION_OPTIONS, upper=True)
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
    template_ = await cg.templatable(
 | 
			
		||||
        config[CONF_VERTICAL_AIRFLOW], args, AirflowVerticalDirection
 | 
			
		||||
    )
 | 
			
		||||
    cg.add(var.set_direction(template_))
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Set horizontal airflow direction action
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "climate.haier.set_horizontal_airflow",
 | 
			
		||||
    HorizontalAirflowAction,
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.use_id(HonClimate),
 | 
			
		||||
            cv.Required(CONF_HORIZONTAL_AIRFLOW): cv.templatable(
 | 
			
		||||
                cv.enum(AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS, upper=True)
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
    template_ = await cg.templatable(
 | 
			
		||||
        config[CONF_HORIZONTAL_AIRFLOW], args, AirflowHorizontalDirection
 | 
			
		||||
    )
 | 
			
		||||
    cg.add(var.set_direction(template_))
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "climate.haier.health_on", HealthOnAction, HAIER_BASE_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "climate.haier.health_off", HealthOffAction, HAIER_BASE_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
async def health_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "climate.haier.power_on", PowerOnAction, HAIER_BASE_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "climate.haier.power_off", PowerOffAction, HAIER_BASE_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "climate.haier.power_toggle", PowerToggleAction, HAIER_BASE_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
async def power_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _final_validate(config):
 | 
			
		||||
    full_config = fv.full_config.get()
 | 
			
		||||
    if CONF_LOGGER in full_config:
 | 
			
		||||
        _level = "NONE"
 | 
			
		||||
        logger_config = full_config[CONF_LOGGER]
 | 
			
		||||
        if CONF_LOGS in logger_config:
 | 
			
		||||
            if "haier.protocol" in logger_config[CONF_LOGS]:
 | 
			
		||||
                _level = logger_config[CONF_LOGS]["haier.protocol"]
 | 
			
		||||
            else:
 | 
			
		||||
                _level = logger_config[CONF_LEVEL]
 | 
			
		||||
        _LOGGER.info("Detected log level for Haier protocol: %s", _level)
 | 
			
		||||
        if _level not in logger.LOG_LEVEL_SEVERITY:
 | 
			
		||||
            raise cv.Invalid("Unknown log level for Haier protocol")
 | 
			
		||||
        _severity = logger.LOG_LEVEL_SEVERITY.index(_level)
 | 
			
		||||
        cg.add_build_flag(f"-DHAIER_LOG_LEVEL={_severity}")
 | 
			
		||||
    else:
 | 
			
		||||
        _LOGGER.info(
 | 
			
		||||
            "No logger component found, logging for Haier protocol is disabled"
 | 
			
		||||
        )
 | 
			
		||||
        cg.add_build_flag("-DHAIER_LOG_LEVEL=0")
 | 
			
		||||
    if (
 | 
			
		||||
        (CONF_WIFI_SIGNAL in config)
 | 
			
		||||
        and (config[CONF_WIFI_SIGNAL])
 | 
			
		||||
        and CONF_WIFI not in full_config
 | 
			
		||||
    ):
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"No WiFi configured, if you want to use haier climate without WiFi add {CONF_WIFI_SIGNAL}: false to climate configuration"
 | 
			
		||||
        )
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = _final_validate
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add(haier_ns.init_haier_protocol_logging())
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await climate.register_climate(var, config)
 | 
			
		||||
    await uart.register_uart_device(var, config)
 | 
			
		||||
    await climate.register_climate(var, config)
 | 
			
		||||
 | 
			
		||||
    if (CONF_WIFI_SIGNAL in config) and (config[CONF_WIFI_SIGNAL]):
 | 
			
		||||
        cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL]))
 | 
			
		||||
    if CONF_BEEPER in config:
 | 
			
		||||
        cg.add(var.set_beeper_state(config[CONF_BEEPER]))
 | 
			
		||||
    if CONF_OUTDOOR_TEMPERATURE in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
 | 
			
		||||
        cg.add(var.set_outdoor_temperature_sensor(sens))
 | 
			
		||||
    if CONF_SUPPORTED_MODES in config:
 | 
			
		||||
        cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
 | 
			
		||||
    if CONF_SUPPORTED_SWING_MODES in config:
 | 
			
		||||
        cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES]))
 | 
			
		||||
    # https://github.com/paveldn/HaierProtocol
 | 
			
		||||
    cg.add_library("pavlodn/HaierProtocol", "0.9.18")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,302 +0,0 @@
 | 
			
		||||
#include <cmath>
 | 
			
		||||
#include "haier.h"
 | 
			
		||||
#include "esphome/core/macros.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace haier {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "haier";
 | 
			
		||||
 | 
			
		||||
static const uint8_t TEMPERATURE = 13;
 | 
			
		||||
static const uint8_t HUMIDITY = 15;
 | 
			
		||||
 | 
			
		||||
static const uint8_t MODE = 23;
 | 
			
		||||
 | 
			
		||||
static const uint8_t FAN_SPEED = 25;
 | 
			
		||||
 | 
			
		||||
static const uint8_t SWING = 27;
 | 
			
		||||
 | 
			
		||||
static const uint8_t POWER = 29;
 | 
			
		||||
static const uint8_t POWER_MASK = 1;
 | 
			
		||||
 | 
			
		||||
static const uint8_t SET_TEMPERATURE = 35;
 | 
			
		||||
static const uint8_t DECIMAL_MASK = (1 << 5);
 | 
			
		||||
 | 
			
		||||
static const uint8_t CRC = 36;
 | 
			
		||||
 | 
			
		||||
static const uint8_t COMFORT_PRESET_MASK = (1 << 3);
 | 
			
		||||
 | 
			
		||||
static const uint8_t MIN_VALID_TEMPERATURE = 16;
 | 
			
		||||
static const uint8_t MAX_VALID_TEMPERATURE = 50;
 | 
			
		||||
static const float TEMPERATURE_STEP = 0.5f;
 | 
			
		||||
 | 
			
		||||
static const uint8_t POLL_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 1, 90};
 | 
			
		||||
static const uint8_t OFF_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 3, 92};
 | 
			
		||||
 | 
			
		||||
void HaierClimate::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Haier:");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Update interval: %u", this->get_update_interval());
 | 
			
		||||
  this->dump_traits_(TAG);
 | 
			
		||||
  this->check_uart_settings(9600);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimate::loop() {
 | 
			
		||||
  if (this->available() >= sizeof(this->data_)) {
 | 
			
		||||
    this->read_array(this->data_, sizeof(this->data_));
 | 
			
		||||
    if (this->data_[0] != 255 || this->data_[1] != 255)
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
    read_state_(this->data_, sizeof(this->data_));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimate::update() {
 | 
			
		||||
  this->write_array(POLL_REQ, sizeof(POLL_REQ));
 | 
			
		||||
  dump_message_("Poll sent", POLL_REQ, sizeof(POLL_REQ));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
climate::ClimateTraits HaierClimate::traits() {
 | 
			
		||||
  auto traits = climate::ClimateTraits();
 | 
			
		||||
 | 
			
		||||
  traits.set_visual_min_temperature(MIN_VALID_TEMPERATURE);
 | 
			
		||||
  traits.set_visual_max_temperature(MAX_VALID_TEMPERATURE);
 | 
			
		||||
  traits.set_visual_temperature_step(TEMPERATURE_STEP);
 | 
			
		||||
 | 
			
		||||
  traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL, climate::CLIMATE_MODE_COOL,
 | 
			
		||||
                              climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY});
 | 
			
		||||
 | 
			
		||||
  traits.set_supported_fan_modes({
 | 
			
		||||
      climate::CLIMATE_FAN_AUTO,
 | 
			
		||||
      climate::CLIMATE_FAN_LOW,
 | 
			
		||||
      climate::CLIMATE_FAN_MEDIUM,
 | 
			
		||||
      climate::CLIMATE_FAN_HIGH,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  traits.set_supported_swing_modes(this->supported_swing_modes_);
 | 
			
		||||
  traits.set_supports_current_temperature(true);
 | 
			
		||||
  traits.set_supports_two_point_target_temperature(false);
 | 
			
		||||
 | 
			
		||||
  traits.add_supported_preset(climate::CLIMATE_PRESET_NONE);
 | 
			
		||||
  traits.add_supported_preset(climate::CLIMATE_PRESET_COMFORT);
 | 
			
		||||
 | 
			
		||||
  return traits;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimate::read_state_(const uint8_t *data, uint8_t size) {
 | 
			
		||||
  dump_message_("Received state", data, size);
 | 
			
		||||
 | 
			
		||||
  uint8_t check = data[CRC];
 | 
			
		||||
 | 
			
		||||
  uint8_t crc = get_checksum_(data, size);
 | 
			
		||||
 | 
			
		||||
  if (check != crc) {
 | 
			
		||||
    ESP_LOGW(TAG, "Invalid checksum");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->current_temperature = data[TEMPERATURE];
 | 
			
		||||
 | 
			
		||||
  this->target_temperature = data[SET_TEMPERATURE] + MIN_VALID_TEMPERATURE;
 | 
			
		||||
 | 
			
		||||
  if (data[POWER] & DECIMAL_MASK) {
 | 
			
		||||
    this->target_temperature += 0.5f;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  switch (data[MODE]) {
 | 
			
		||||
    case MODE_SMART:
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
      break;
 | 
			
		||||
    case MODE_COOL:
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_COOL;
 | 
			
		||||
      break;
 | 
			
		||||
    case MODE_HEAT:
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT;
 | 
			
		||||
      break;
 | 
			
		||||
    case MODE_ONLY_FAN:
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_FAN_ONLY;
 | 
			
		||||
      break;
 | 
			
		||||
    case MODE_DRY:
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_DRY;
 | 
			
		||||
      break;
 | 
			
		||||
    default:  // other modes are unsupported
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  switch (data[FAN_SPEED]) {
 | 
			
		||||
    case FAN_AUTO:
 | 
			
		||||
      this->fan_mode = climate::CLIMATE_FAN_AUTO;
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case FAN_MIN:
 | 
			
		||||
      this->fan_mode = climate::CLIMATE_FAN_LOW;
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case FAN_MIDDLE:
 | 
			
		||||
      this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case FAN_MAX:
 | 
			
		||||
      this->fan_mode = climate::CLIMATE_FAN_HIGH;
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  switch (data[SWING]) {
 | 
			
		||||
    case SWING_OFF:
 | 
			
		||||
      this->swing_mode = climate::CLIMATE_SWING_OFF;
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case SWING_VERTICAL:
 | 
			
		||||
      this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case SWING_HORIZONTAL:
 | 
			
		||||
      this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case SWING_BOTH:
 | 
			
		||||
      this->swing_mode = climate::CLIMATE_SWING_BOTH;
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (data[POWER] & COMFORT_PRESET_MASK) {
 | 
			
		||||
    this->preset = climate::CLIMATE_PRESET_COMFORT;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->preset = climate::CLIMATE_PRESET_NONE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if ((data[POWER] & POWER_MASK) == 0) {
 | 
			
		||||
    this->mode = climate::CLIMATE_MODE_OFF;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->publish_state();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimate::control(const climate::ClimateCall &call) {
 | 
			
		||||
  if (call.get_mode().has_value()) {
 | 
			
		||||
    switch (call.get_mode().value()) {
 | 
			
		||||
      case climate::CLIMATE_MODE_OFF:
 | 
			
		||||
        send_data_(OFF_REQ, sizeof(OFF_REQ));
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case climate::CLIMATE_MODE_HEAT_COOL:
 | 
			
		||||
      case climate::CLIMATE_MODE_AUTO:
 | 
			
		||||
        data_[POWER] |= POWER_MASK;
 | 
			
		||||
        data_[MODE] = MODE_SMART;
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_MODE_HEAT:
 | 
			
		||||
        data_[POWER] |= POWER_MASK;
 | 
			
		||||
        data_[MODE] = MODE_HEAT;
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_MODE_COOL:
 | 
			
		||||
        data_[POWER] |= POWER_MASK;
 | 
			
		||||
        data_[MODE] = MODE_COOL;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case climate::CLIMATE_MODE_FAN_ONLY:
 | 
			
		||||
        data_[POWER] |= POWER_MASK;
 | 
			
		||||
        data_[MODE] = MODE_ONLY_FAN;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case climate::CLIMATE_MODE_DRY:
 | 
			
		||||
        data_[POWER] |= POWER_MASK;
 | 
			
		||||
        data_[MODE] = MODE_DRY;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (call.get_preset().has_value()) {
 | 
			
		||||
    if (call.get_preset().value() == climate::CLIMATE_PRESET_COMFORT) {
 | 
			
		||||
      data_[POWER] |= COMFORT_PRESET_MASK;
 | 
			
		||||
    } else {
 | 
			
		||||
      data_[POWER] &= ~COMFORT_PRESET_MASK;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (call.get_target_temperature().has_value()) {
 | 
			
		||||
    float target = call.get_target_temperature().value() - MIN_VALID_TEMPERATURE;
 | 
			
		||||
 | 
			
		||||
    data_[SET_TEMPERATURE] = (uint8_t) target;
 | 
			
		||||
 | 
			
		||||
    if ((int) target == std::lroundf(target)) {
 | 
			
		||||
      data_[POWER] &= ~DECIMAL_MASK;
 | 
			
		||||
    } else {
 | 
			
		||||
      data_[POWER] |= DECIMAL_MASK;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (call.get_fan_mode().has_value()) {
 | 
			
		||||
    switch (call.get_fan_mode().value()) {
 | 
			
		||||
      case climate::CLIMATE_FAN_AUTO:
 | 
			
		||||
        data_[FAN_SPEED] = FAN_AUTO;
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_FAN_LOW:
 | 
			
		||||
        data_[FAN_SPEED] = FAN_MIN;
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_FAN_MEDIUM:
 | 
			
		||||
        data_[FAN_SPEED] = FAN_MIDDLE;
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_FAN_HIGH:
 | 
			
		||||
        data_[FAN_SPEED] = FAN_MAX;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      default:  // other modes are unsupported
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (call.get_swing_mode().has_value()) {
 | 
			
		||||
    switch (call.get_swing_mode().value()) {
 | 
			
		||||
      case climate::CLIMATE_SWING_OFF:
 | 
			
		||||
        data_[SWING] = SWING_OFF;
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_SWING_VERTICAL:
 | 
			
		||||
        data_[SWING] = SWING_VERTICAL;
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_SWING_HORIZONTAL:
 | 
			
		||||
        data_[SWING] = SWING_HORIZONTAL;
 | 
			
		||||
        break;
 | 
			
		||||
      case climate::CLIMATE_SWING_BOTH:
 | 
			
		||||
        data_[SWING] = SWING_BOTH;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Parts of the message that must have specific values for "send" command.
 | 
			
		||||
  // The meaning of those values is unknown at the moment.
 | 
			
		||||
  data_[9] = 1;
 | 
			
		||||
  data_[10] = 77;
 | 
			
		||||
  data_[11] = 95;
 | 
			
		||||
  data_[17] = 0;
 | 
			
		||||
 | 
			
		||||
  // Compute checksum
 | 
			
		||||
  uint8_t crc = get_checksum_(data_, sizeof(data_));
 | 
			
		||||
  data_[CRC] = crc;
 | 
			
		||||
 | 
			
		||||
  send_data_(data_, sizeof(data_));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimate::send_data_(const uint8_t *message, uint8_t size) {
 | 
			
		||||
  this->write_array(message, size);
 | 
			
		||||
 | 
			
		||||
  dump_message_("Sent message", message, size);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimate::dump_message_(const char *title, const uint8_t *message, uint8_t size) {
 | 
			
		||||
  ESP_LOGV(TAG, "%s:", title);
 | 
			
		||||
  for (int i = 0; i < size; i++) {
 | 
			
		||||
    ESP_LOGV(TAG, "  byte %02d - %d", i, message[i]);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint8_t HaierClimate::get_checksum_(const uint8_t *message, size_t size) {
 | 
			
		||||
  uint8_t position = size - 1;
 | 
			
		||||
  uint8_t crc = 0;
 | 
			
		||||
 | 
			
		||||
  for (int i = 2; i < position; i++)
 | 
			
		||||
    crc += message[i];
 | 
			
		||||
 | 
			
		||||
  return crc;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace haier
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,37 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/climate/climate.h"
 | 
			
		||||
#include "esphome/components/uart/uart.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace haier {
 | 
			
		||||
 | 
			
		||||
enum Mode : uint8_t { MODE_SMART = 0, MODE_COOL = 1, MODE_HEAT = 2, MODE_ONLY_FAN = 3, MODE_DRY = 4 };
 | 
			
		||||
enum FanSpeed : uint8_t { FAN_MAX = 0, FAN_MIDDLE = 1, FAN_MIN = 2, FAN_AUTO = 3 };
 | 
			
		||||
enum SwingMode : uint8_t { SWING_OFF = 0, SWING_VERTICAL = 1, SWING_HORIZONTAL = 2, SWING_BOTH = 3 };
 | 
			
		||||
 | 
			
		||||
class HaierClimate : public climate::Climate, public uart::UARTDevice, public PollingComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void control(const climate::ClimateCall &call) override;
 | 
			
		||||
  void set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
 | 
			
		||||
    this->supported_swing_modes_ = modes;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  climate::ClimateTraits traits() override;
 | 
			
		||||
  void read_state_(const uint8_t *data, uint8_t size);
 | 
			
		||||
  void send_data_(const uint8_t *message, uint8_t size);
 | 
			
		||||
  void dump_message_(const char *title, const uint8_t *message, uint8_t size);
 | 
			
		||||
  uint8_t get_checksum_(const uint8_t *message, size_t size);
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  uint8_t data_[37];
 | 
			
		||||
  std::set<climate::ClimateSwingMode> supported_swing_modes_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace haier
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										311
									
								
								esphome/components/haier/haier_base.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										311
									
								
								esphome/components/haier/haier_base.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,311 @@
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include "esphome/components/climate/climate.h"
 | 
			
		||||
#include "esphome/components/uart/uart.h"
 | 
			
		||||
#include "haier_base.h"
 | 
			
		||||
 | 
			
		||||
using namespace esphome::climate;
 | 
			
		||||
using namespace esphome::uart;
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace haier {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "haier.climate";
 | 
			
		||||
constexpr size_t COMMUNICATION_TIMEOUT_MS = 60000;
 | 
			
		||||
constexpr size_t STATUS_REQUEST_INTERVAL_MS = 5000;
 | 
			
		||||
constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000;
 | 
			
		||||
constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000;
 | 
			
		||||
constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400;
 | 
			
		||||
constexpr size_t CONTROL_TIMEOUT_MS = 7000;
 | 
			
		||||
constexpr size_t NO_COMMAND = 0xFF;  // Indicate that there is no command supplied
 | 
			
		||||
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
// To reduce size of binary this function only available when log level is Verbose
 | 
			
		||||
const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
 | 
			
		||||
  static const char *phase_names[] = {
 | 
			
		||||
      "SENDING_INIT_1",
 | 
			
		||||
      "WAITING_ANSWER_INIT_1",
 | 
			
		||||
      "SENDING_INIT_2",
 | 
			
		||||
      "WAITING_ANSWER_INIT_2",
 | 
			
		||||
      "SENDING_FIRST_STATUS_REQUEST",
 | 
			
		||||
      "WAITING_FIRST_STATUS_ANSWER",
 | 
			
		||||
      "SENDING_ALARM_STATUS_REQUEST",
 | 
			
		||||
      "WAITING_ALARM_STATUS_ANSWER",
 | 
			
		||||
      "IDLE",
 | 
			
		||||
      "SENDING_STATUS_REQUEST",
 | 
			
		||||
      "WAITING_STATUS_ANSWER",
 | 
			
		||||
      "SENDING_UPDATE_SIGNAL_REQUEST",
 | 
			
		||||
      "WAITING_UPDATE_SIGNAL_ANSWER",
 | 
			
		||||
      "SENDING_SIGNAL_LEVEL",
 | 
			
		||||
      "WAITING_SIGNAL_LEVEL_ANSWER",
 | 
			
		||||
      "SENDING_CONTROL",
 | 
			
		||||
      "WAITING_CONTROL_ANSWER",
 | 
			
		||||
      "SENDING_POWER_ON_COMMAND",
 | 
			
		||||
      "WAITING_POWER_ON_ANSWER",
 | 
			
		||||
      "SENDING_POWER_OFF_COMMAND",
 | 
			
		||||
      "WAITING_POWER_OFF_ANSWER",
 | 
			
		||||
      "UNKNOWN"  // Should be the last!
 | 
			
		||||
  };
 | 
			
		||||
  int phase_index = (int) phase;
 | 
			
		||||
  if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0))
 | 
			
		||||
    phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES;
 | 
			
		||||
  return phase_names[phase_index];
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
HaierClimateBase::HaierClimateBase()
 | 
			
		||||
    : haier_protocol_(*this),
 | 
			
		||||
      protocol_phase_(ProtocolPhases::SENDING_INIT_1),
 | 
			
		||||
      action_request_(ActionRequest::NO_ACTION),
 | 
			
		||||
      display_status_(true),
 | 
			
		||||
      health_mode_(false),
 | 
			
		||||
      force_send_control_(false),
 | 
			
		||||
      forced_publish_(false),
 | 
			
		||||
      forced_request_status_(false),
 | 
			
		||||
      first_control_attempt_(false),
 | 
			
		||||
      reset_protocol_request_(false) {
 | 
			
		||||
  this->traits_ = climate::ClimateTraits();
 | 
			
		||||
  this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT,
 | 
			
		||||
                                     climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY,
 | 
			
		||||
                                     climate::CLIMATE_MODE_AUTO});
 | 
			
		||||
  this->traits_.set_supported_fan_modes(
 | 
			
		||||
      {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH});
 | 
			
		||||
  this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH,
 | 
			
		||||
                                           climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
 | 
			
		||||
  this->traits_.set_supports_current_temperature(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
HaierClimateBase::~HaierClimateBase() {}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::set_phase_(ProtocolPhases phase) {
 | 
			
		||||
  if (this->protocol_phase_ != phase) {
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
    ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase));
 | 
			
		||||
#else
 | 
			
		||||
    ESP_LOGV(TAG, "Phase transition: %d => %d", (int) this->protocol_phase_, (int) phase);
 | 
			
		||||
#endif
 | 
			
		||||
    this->protocol_phase_ = phase;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HaierClimateBase::check_timeout_(std::chrono::steady_clock::time_point now,
 | 
			
		||||
                                      std::chrono::steady_clock::time_point tpoint, size_t timeout) {
 | 
			
		||||
  return std::chrono::duration_cast<std::chrono::milliseconds>(now - tpoint).count() > timeout;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HaierClimateBase::is_message_interval_exceeded_(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
  return this->check_timeout_(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HaierClimateBase::is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
  return this->check_timeout_(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HaierClimateBase::is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
  return this->check_timeout_(now, this->control_request_timestamp_, CONTROL_TIMEOUT_MS);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HaierClimateBase::is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
  return this->check_timeout_(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HaierClimateBase::is_protocol_initialisation_interval_exceded_(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
  return this->check_timeout_(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HaierClimateBase::get_display_state() const { return this->display_status_; }
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::set_display_state(bool state) {
 | 
			
		||||
  if (this->display_status_ != state) {
 | 
			
		||||
    this->display_status_ = state;
 | 
			
		||||
    this->set_force_send_control_(true);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HaierClimateBase::get_health_mode() const { return this->health_mode_; }
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::set_health_mode(bool state) {
 | 
			
		||||
  if (this->health_mode_ != state) {
 | 
			
		||||
    this->health_mode_ = state;
 | 
			
		||||
    this->set_force_send_control_(true);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::send_power_on_command() { this->action_request_ = ActionRequest::TURN_POWER_ON; }
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::send_power_off_command() { this->action_request_ = ActionRequest::TURN_POWER_OFF; }
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::toggle_power() { this->action_request_ = ActionRequest::TOGGLE_POWER; }
 | 
			
		||||
void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
 | 
			
		||||
  this->traits_.set_supported_swing_modes(modes);
 | 
			
		||||
  this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF);       // Always available
 | 
			
		||||
  this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL);  // Always available
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) {
 | 
			
		||||
  this->traits_.set_supported_modes(modes);
 | 
			
		||||
  this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF);   // Always available
 | 
			
		||||
  this->traits_.add_supported_mode(climate::CLIMATE_MODE_AUTO);  // Always available
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(uint8_t request_message_type,
 | 
			
		||||
                                                                  uint8_t expected_request_message_type,
 | 
			
		||||
                                                                  uint8_t answer_message_type,
 | 
			
		||||
                                                                  uint8_t expected_answer_message_type,
 | 
			
		||||
                                                                  ProtocolPhases expected_phase) {
 | 
			
		||||
  haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK;
 | 
			
		||||
  if ((expected_request_message_type != NO_COMMAND) && (request_message_type != expected_request_message_type))
 | 
			
		||||
    result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
 | 
			
		||||
  if ((expected_answer_message_type != NO_COMMAND) && (answer_message_type != expected_answer_message_type))
 | 
			
		||||
    result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
 | 
			
		||||
  if ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_))
 | 
			
		||||
    result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
 | 
			
		||||
  if (is_message_invalid(answer_message_type))
 | 
			
		||||
    result = haier_protocol::HandlerError::INVALID_ANSWER;
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(uint8_t request_type) {
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
  ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", request_type, phase_to_string_(this->protocol_phase_));
 | 
			
		||||
#else
 | 
			
		||||
  ESP_LOGW(TAG, "Answer timeout for command %02X, phase %d", request_type, (int) this->protocol_phase_);
 | 
			
		||||
#endif
 | 
			
		||||
  if (this->protocol_phase_ > ProtocolPhases::IDLE) {
 | 
			
		||||
    this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->set_phase_(ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
  }
 | 
			
		||||
  return haier_protocol::HandlerError::HANDLER_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::setup() {
 | 
			
		||||
  ESP_LOGI(TAG, "Haier initialization...");
 | 
			
		||||
  // Set timestamp here to give AC time to boot
 | 
			
		||||
  this->last_request_timestamp_ = std::chrono::steady_clock::now();
 | 
			
		||||
  this->set_phase_(ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
  this->set_answers_handlers();
 | 
			
		||||
  this->haier_protocol_.set_default_timeout_handler(
 | 
			
		||||
      std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::dump_config() {
 | 
			
		||||
  LOG_CLIMATE("", "Haier Climate", this);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Device communication status: %s",
 | 
			
		||||
                (this->protocol_phase_ >= ProtocolPhases::IDLE) ? "established" : "none");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::loop() {
 | 
			
		||||
  std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
 | 
			
		||||
  if ((std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_valid_status_timestamp_).count() >
 | 
			
		||||
       COMMUNICATION_TIMEOUT_MS) ||
 | 
			
		||||
      (this->reset_protocol_request_)) {
 | 
			
		||||
    if (this->protocol_phase_ >= ProtocolPhases::IDLE) {
 | 
			
		||||
      // No status too long, reseting protocol
 | 
			
		||||
      if (this->reset_protocol_request_) {
 | 
			
		||||
        this->reset_protocol_request_ = false;
 | 
			
		||||
        ESP_LOGW(TAG, "Protocol reset requested");
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGW(TAG, "Communication timeout, reseting protocol");
 | 
			
		||||
      }
 | 
			
		||||
      this->last_valid_status_timestamp_ = now;
 | 
			
		||||
      this->set_force_send_control_(false);
 | 
			
		||||
      if (this->hvac_settings_.valid)
 | 
			
		||||
        this->hvac_settings_.reset();
 | 
			
		||||
      this->set_phase_(ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
      return;
 | 
			
		||||
    } else {
 | 
			
		||||
      // No need to reset protocol if we didn't pass initialization phase
 | 
			
		||||
      this->last_valid_status_timestamp_ = now;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  if ((this->protocol_phase_ == ProtocolPhases::IDLE) ||
 | 
			
		||||
      (this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) ||
 | 
			
		||||
      (this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) ||
 | 
			
		||||
      (this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL)) {
 | 
			
		||||
    // If control message or action is pending we should send it ASAP unless we are in initialisation
 | 
			
		||||
    // procedure or waiting for an answer
 | 
			
		||||
    if (this->action_request_ != ActionRequest::NO_ACTION) {
 | 
			
		||||
      this->process_pending_action();
 | 
			
		||||
    } else if (this->hvac_settings_.valid || this->force_send_control_) {
 | 
			
		||||
      ESP_LOGV(TAG, "Control packet is pending...");
 | 
			
		||||
      this->set_phase_(ProtocolPhases::SENDING_CONTROL);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  this->process_phase(now);
 | 
			
		||||
  this->haier_protocol_.loop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::process_pending_action() {
 | 
			
		||||
  ActionRequest request = this->action_request_;
 | 
			
		||||
  if (this->action_request_ == ActionRequest::TOGGLE_POWER) {
 | 
			
		||||
    request = this->mode == CLIMATE_MODE_OFF ? ActionRequest::TURN_POWER_ON : ActionRequest::TURN_POWER_OFF;
 | 
			
		||||
  }
 | 
			
		||||
  switch (request) {
 | 
			
		||||
    case ActionRequest::TURN_POWER_ON:
 | 
			
		||||
      this->set_phase_(ProtocolPhases::SENDING_POWER_ON_COMMAND);
 | 
			
		||||
      break;
 | 
			
		||||
    case ActionRequest::TURN_POWER_OFF:
 | 
			
		||||
      this->set_phase_(ProtocolPhases::SENDING_POWER_OFF_COMMAND);
 | 
			
		||||
      break;
 | 
			
		||||
    case ActionRequest::TOGGLE_POWER:
 | 
			
		||||
    case ActionRequest::NO_ACTION:
 | 
			
		||||
      // shouldn't get here, do nothing
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_);
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  this->action_request_ = ActionRequest::NO_ACTION;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateTraits HaierClimateBase::traits() { return traits_; }
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::control(const ClimateCall &call) {
 | 
			
		||||
  ESP_LOGD("Control", "Control call");
 | 
			
		||||
  if (this->protocol_phase_ < ProtocolPhases::IDLE) {
 | 
			
		||||
    ESP_LOGW(TAG, "Can't send control packet, first poll answer not received");
 | 
			
		||||
    return;  // cancel the control, we cant do it without a poll answer.
 | 
			
		||||
  }
 | 
			
		||||
  if (this->hvac_settings_.valid) {
 | 
			
		||||
    ESP_LOGW(TAG, "Overriding old valid settings before they were applied!");
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    if (call.get_mode().has_value())
 | 
			
		||||
      this->hvac_settings_.mode = call.get_mode();
 | 
			
		||||
    if (call.get_fan_mode().has_value())
 | 
			
		||||
      this->hvac_settings_.fan_mode = call.get_fan_mode();
 | 
			
		||||
    if (call.get_swing_mode().has_value())
 | 
			
		||||
      this->hvac_settings_.swing_mode = call.get_swing_mode();
 | 
			
		||||
    if (call.get_target_temperature().has_value())
 | 
			
		||||
      this->hvac_settings_.target_temperature = call.get_target_temperature();
 | 
			
		||||
    if (call.get_preset().has_value())
 | 
			
		||||
      this->hvac_settings_.preset = call.get_preset();
 | 
			
		||||
    this->hvac_settings_.valid = true;
 | 
			
		||||
  }
 | 
			
		||||
  this->first_control_attempt_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::HvacSettings::reset() {
 | 
			
		||||
  this->valid = false;
 | 
			
		||||
  this->mode.reset();
 | 
			
		||||
  this->fan_mode.reset();
 | 
			
		||||
  this->swing_mode.reset();
 | 
			
		||||
  this->target_temperature.reset();
 | 
			
		||||
  this->preset.reset();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::set_force_send_control_(bool status) {
 | 
			
		||||
  this->force_send_control_ = status;
 | 
			
		||||
  if (status) {
 | 
			
		||||
    this->first_control_attempt_ = true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc) {
 | 
			
		||||
  this->haier_protocol_.send_message(command, use_crc);
 | 
			
		||||
  this->last_request_timestamp_ = std::chrono::steady_clock::now();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace haier
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										142
									
								
								esphome/components/haier/haier_base.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								esphome/components/haier/haier_base.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <set>
 | 
			
		||||
#include "esphome/components/climate/climate.h"
 | 
			
		||||
#include "esphome/components/uart/uart.h"
 | 
			
		||||
// HaierProtocol
 | 
			
		||||
#include <protocol/haier_protocol.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace haier {
 | 
			
		||||
 | 
			
		||||
enum class ActionRequest : uint8_t {
 | 
			
		||||
  NO_ACTION = 0,
 | 
			
		||||
  TURN_POWER_ON = 1,
 | 
			
		||||
  TURN_POWER_OFF = 2,
 | 
			
		||||
  TOGGLE_POWER = 3,
 | 
			
		||||
  START_SELF_CLEAN = 4,   // only hOn
 | 
			
		||||
  START_STERI_CLEAN = 5,  // only hOn
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class HaierClimateBase : public esphome::Component,
 | 
			
		||||
                         public esphome::climate::Climate,
 | 
			
		||||
                         public esphome::uart::UARTDevice,
 | 
			
		||||
                         public haier_protocol::ProtocolStream {
 | 
			
		||||
 public:
 | 
			
		||||
  HaierClimateBase();
 | 
			
		||||
  HaierClimateBase(const HaierClimateBase &) = delete;
 | 
			
		||||
  HaierClimateBase &operator=(const HaierClimateBase &) = delete;
 | 
			
		||||
  ~HaierClimateBase();
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void control(const esphome::climate::ClimateCall &call) override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; }
 | 
			
		||||
  void set_fahrenheit(bool fahrenheit);
 | 
			
		||||
  void set_display_state(bool state);
 | 
			
		||||
  bool get_display_state() const;
 | 
			
		||||
  void set_health_mode(bool state);
 | 
			
		||||
  bool get_health_mode() const;
 | 
			
		||||
  void send_power_on_command();
 | 
			
		||||
  void send_power_off_command();
 | 
			
		||||
  void toggle_power();
 | 
			
		||||
  void reset_protocol() { this->reset_protocol_request_ = true; };
 | 
			
		||||
  void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes);
 | 
			
		||||
  void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes);
 | 
			
		||||
  size_t available() noexcept override { return esphome::uart::UARTDevice::available(); };
 | 
			
		||||
  size_t read_array(uint8_t *data, size_t len) noexcept override {
 | 
			
		||||
    return esphome::uart::UARTDevice::read_array(data, len) ? len : 0;
 | 
			
		||||
  };
 | 
			
		||||
  void write_array(const uint8_t *data, size_t len) noexcept override {
 | 
			
		||||
    esphome::uart::UARTDevice::write_array(data, len);
 | 
			
		||||
  };
 | 
			
		||||
  bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; };
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  enum class ProtocolPhases {
 | 
			
		||||
    UNKNOWN = -1,
 | 
			
		||||
    // INITIALIZATION
 | 
			
		||||
    SENDING_INIT_1 = 0,
 | 
			
		||||
    WAITING_ANSWER_INIT_1 = 1,
 | 
			
		||||
    SENDING_INIT_2 = 2,
 | 
			
		||||
    WAITING_ANSWER_INIT_2 = 3,
 | 
			
		||||
    SENDING_FIRST_STATUS_REQUEST = 4,
 | 
			
		||||
    WAITING_FIRST_STATUS_ANSWER = 5,
 | 
			
		||||
    SENDING_ALARM_STATUS_REQUEST = 6,
 | 
			
		||||
    WAITING_ALARM_STATUS_ANSWER = 7,
 | 
			
		||||
    // FUNCTIONAL STATE
 | 
			
		||||
    IDLE = 8,
 | 
			
		||||
    SENDING_STATUS_REQUEST = 9,
 | 
			
		||||
    WAITING_STATUS_ANSWER = 10,
 | 
			
		||||
    SENDING_UPDATE_SIGNAL_REQUEST = 11,
 | 
			
		||||
    WAITING_UPDATE_SIGNAL_ANSWER = 12,
 | 
			
		||||
    SENDING_SIGNAL_LEVEL = 13,
 | 
			
		||||
    WAITING_SIGNAL_LEVEL_ANSWER = 14,
 | 
			
		||||
    SENDING_CONTROL = 15,
 | 
			
		||||
    WAITING_CONTROL_ANSWER = 16,
 | 
			
		||||
    SENDING_POWER_ON_COMMAND = 17,
 | 
			
		||||
    WAITING_POWER_ON_ANSWER = 18,
 | 
			
		||||
    SENDING_POWER_OFF_COMMAND = 19,
 | 
			
		||||
    WAITING_POWER_OFF_ANSWER = 20,
 | 
			
		||||
    NUM_PROTOCOL_PHASES
 | 
			
		||||
  };
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
  const char *phase_to_string_(ProtocolPhases phase);
 | 
			
		||||
#endif
 | 
			
		||||
  virtual void set_answers_handlers() = 0;
 | 
			
		||||
  virtual void process_phase(std::chrono::steady_clock::time_point now) = 0;
 | 
			
		||||
  virtual haier_protocol::HaierMessage get_control_message() = 0;
 | 
			
		||||
  virtual bool is_message_invalid(uint8_t message_type) = 0;
 | 
			
		||||
  virtual void process_pending_action();
 | 
			
		||||
  esphome::climate::ClimateTraits traits() override;
 | 
			
		||||
  // Answers handlers
 | 
			
		||||
  haier_protocol::HandlerError answer_preprocess_(uint8_t request_message_type, uint8_t expected_request_message_type,
 | 
			
		||||
                                                  uint8_t answer_message_type, uint8_t expected_answer_message_type,
 | 
			
		||||
                                                  ProtocolPhases expected_phase);
 | 
			
		||||
  // Timeout handler
 | 
			
		||||
  haier_protocol::HandlerError timeout_default_handler_(uint8_t request_type);
 | 
			
		||||
  // Helper functions
 | 
			
		||||
  void set_force_send_control_(bool status);
 | 
			
		||||
  void send_message_(const haier_protocol::HaierMessage &command, bool use_crc);
 | 
			
		||||
  void set_phase_(ProtocolPhases phase);
 | 
			
		||||
  bool check_timeout_(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint,
 | 
			
		||||
                      size_t timeout);
 | 
			
		||||
  bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now);
 | 
			
		||||
  bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now);
 | 
			
		||||
  bool is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now);
 | 
			
		||||
  bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now);
 | 
			
		||||
  bool is_protocol_initialisation_interval_exceded_(std::chrono::steady_clock::time_point now);
 | 
			
		||||
 | 
			
		||||
  struct HvacSettings {
 | 
			
		||||
    esphome::optional<esphome::climate::ClimateMode> mode;
 | 
			
		||||
    esphome::optional<esphome::climate::ClimateFanMode> fan_mode;
 | 
			
		||||
    esphome::optional<esphome::climate::ClimateSwingMode> swing_mode;
 | 
			
		||||
    esphome::optional<float> target_temperature;
 | 
			
		||||
    esphome::optional<esphome::climate::ClimatePreset> preset;
 | 
			
		||||
    bool valid;
 | 
			
		||||
    HvacSettings() : valid(false){};
 | 
			
		||||
    void reset();
 | 
			
		||||
  };
 | 
			
		||||
  haier_protocol::ProtocolHandler haier_protocol_;
 | 
			
		||||
  ProtocolPhases protocol_phase_;
 | 
			
		||||
  ActionRequest action_request_;
 | 
			
		||||
  uint8_t fan_mode_speed_;
 | 
			
		||||
  uint8_t other_modes_fan_speed_;
 | 
			
		||||
  bool display_status_;
 | 
			
		||||
  bool health_mode_;
 | 
			
		||||
  bool force_send_control_;
 | 
			
		||||
  bool forced_publish_;
 | 
			
		||||
  bool forced_request_status_;
 | 
			
		||||
  bool first_control_attempt_;
 | 
			
		||||
  bool reset_protocol_request_;
 | 
			
		||||
  esphome::climate::ClimateTraits traits_;
 | 
			
		||||
  HvacSettings hvac_settings_;
 | 
			
		||||
  std::chrono::steady_clock::time_point last_request_timestamp_;       // For interval between messages
 | 
			
		||||
  std::chrono::steady_clock::time_point last_valid_status_timestamp_;  // For protocol timeout
 | 
			
		||||
  std::chrono::steady_clock::time_point last_status_request_;          // To request AC status
 | 
			
		||||
  std::chrono::steady_clock::time_point control_request_timestamp_;    // To send control message
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace haier
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										857
									
								
								esphome/components/haier/hon_climate.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										857
									
								
								esphome/components/haier/hon_climate.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,857 @@
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include "esphome/components/climate/climate.h"
 | 
			
		||||
#include "esphome/components/uart/uart.h"
 | 
			
		||||
#ifdef USE_WIFI
 | 
			
		||||
#include "esphome/components/wifi/wifi_component.h"
 | 
			
		||||
#endif
 | 
			
		||||
#include "hon_climate.h"
 | 
			
		||||
#include "hon_packet.h"
 | 
			
		||||
 | 
			
		||||
using namespace esphome::climate;
 | 
			
		||||
using namespace esphome::uart;
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace haier {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "haier.climate";
 | 
			
		||||
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
 | 
			
		||||
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
 | 
			
		||||
 | 
			
		||||
hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) {
 | 
			
		||||
  switch (direction) {
 | 
			
		||||
    case AirflowVerticalDirection::HEALTH_UP:
 | 
			
		||||
      return hon_protocol::VerticalSwingMode::HEALTH_UP;
 | 
			
		||||
    case AirflowVerticalDirection::MAX_UP:
 | 
			
		||||
      return hon_protocol::VerticalSwingMode::MAX_UP;
 | 
			
		||||
    case AirflowVerticalDirection::UP:
 | 
			
		||||
      return hon_protocol::VerticalSwingMode::UP;
 | 
			
		||||
    case AirflowVerticalDirection::DOWN:
 | 
			
		||||
      return hon_protocol::VerticalSwingMode::DOWN;
 | 
			
		||||
    case AirflowVerticalDirection::HEALTH_DOWN:
 | 
			
		||||
      return hon_protocol::VerticalSwingMode::HEALTH_DOWN;
 | 
			
		||||
    default:
 | 
			
		||||
      return hon_protocol::VerticalSwingMode::CENTER;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDirection direction) {
 | 
			
		||||
  switch (direction) {
 | 
			
		||||
    case AirflowHorizontalDirection::MAX_LEFT:
 | 
			
		||||
      return hon_protocol::HorizontalSwingMode::MAX_LEFT;
 | 
			
		||||
    case AirflowHorizontalDirection::LEFT:
 | 
			
		||||
      return hon_protocol::HorizontalSwingMode::LEFT;
 | 
			
		||||
    case AirflowHorizontalDirection::RIGHT:
 | 
			
		||||
      return hon_protocol::HorizontalSwingMode::RIGHT;
 | 
			
		||||
    case AirflowHorizontalDirection::MAX_RIGHT:
 | 
			
		||||
      return hon_protocol::HorizontalSwingMode::MAX_RIGHT;
 | 
			
		||||
    default:
 | 
			
		||||
      return hon_protocol::HorizontalSwingMode::CENTER;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
HonClimate::HonClimate()
 | 
			
		||||
    : last_status_message_(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]),
 | 
			
		||||
      cleaning_status_(CleaningState::NO_CLEANING),
 | 
			
		||||
      got_valid_outdoor_temp_(false),
 | 
			
		||||
      hvac_hardware_info_available_(false),
 | 
			
		||||
      hvac_functions_{false, false, false, false, false},
 | 
			
		||||
      use_crc_(hvac_functions_[2]),
 | 
			
		||||
      active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
 | 
			
		||||
      outdoor_sensor_(nullptr),
 | 
			
		||||
      send_wifi_signal_(true) {
 | 
			
		||||
  this->traits_.set_supported_presets({
 | 
			
		||||
      climate::CLIMATE_PRESET_NONE,
 | 
			
		||||
      climate::CLIMATE_PRESET_ECO,
 | 
			
		||||
      climate::CLIMATE_PRESET_BOOST,
 | 
			
		||||
      climate::CLIMATE_PRESET_SLEEP,
 | 
			
		||||
  });
 | 
			
		||||
  this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID;
 | 
			
		||||
  this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
HonClimate::~HonClimate() {}
 | 
			
		||||
 | 
			
		||||
void HonClimate::set_beeper_state(bool state) { this->beeper_status_ = state; }
 | 
			
		||||
 | 
			
		||||
bool HonClimate::get_beeper_state() const { return this->beeper_status_; }
 | 
			
		||||
 | 
			
		||||
void HonClimate::set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; }
 | 
			
		||||
 | 
			
		||||
AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this->vertical_direction_; };
 | 
			
		||||
 | 
			
		||||
void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) {
 | 
			
		||||
  if (direction > AirflowVerticalDirection::DOWN) {
 | 
			
		||||
    this->vertical_direction_ = AirflowVerticalDirection::CENTER;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->vertical_direction_ = direction;
 | 
			
		||||
  }
 | 
			
		||||
  this->set_force_send_control_(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; }
 | 
			
		||||
 | 
			
		||||
void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) {
 | 
			
		||||
  if (direction > AirflowHorizontalDirection::RIGHT) {
 | 
			
		||||
    this->horizontal_direction_ = AirflowHorizontalDirection::CENTER;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->horizontal_direction_ = direction;
 | 
			
		||||
  }
 | 
			
		||||
  this->set_force_send_control_(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string HonClimate::get_cleaning_status_text() const {
 | 
			
		||||
  switch (this->cleaning_status_) {
 | 
			
		||||
    case CleaningState::SELF_CLEAN:
 | 
			
		||||
      return "Self clean";
 | 
			
		||||
    case CleaningState::STERI_CLEAN:
 | 
			
		||||
      return "56°C Steri-Clean";
 | 
			
		||||
    default:
 | 
			
		||||
      return "No cleaning";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CleaningState HonClimate::get_cleaning_status() const { return this->cleaning_status_; }
 | 
			
		||||
 | 
			
		||||
void HonClimate::start_self_cleaning() {
 | 
			
		||||
  if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
 | 
			
		||||
    ESP_LOGI(TAG, "Sending self cleaning start request");
 | 
			
		||||
    this->action_request_ = ActionRequest::START_SELF_CLEAN;
 | 
			
		||||
    this->set_force_send_control_(true);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HonClimate::start_steri_cleaning() {
 | 
			
		||||
  if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
 | 
			
		||||
    ESP_LOGI(TAG, "Sending steri cleaning start request");
 | 
			
		||||
    this->action_request_ = ActionRequest::START_STERI_CLEAN;
 | 
			
		||||
    this->set_force_send_control_(true);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HonClimate::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; }
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type,
 | 
			
		||||
                                                                            const uint8_t *data, size_t data_size) {
 | 
			
		||||
  haier_protocol::HandlerError result = this->answer_preprocess_(
 | 
			
		||||
      request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type,
 | 
			
		||||
      (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_ANSWER_INIT_1);
 | 
			
		||||
  if (result == haier_protocol::HandlerError::HANDLER_OK) {
 | 
			
		||||
    if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) {
 | 
			
		||||
      // Wrong structure
 | 
			
		||||
      this->set_phase_(ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
      return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
 | 
			
		||||
    }
 | 
			
		||||
    // All OK
 | 
			
		||||
    hon_protocol::DeviceVersionAnswer *answr = (hon_protocol::DeviceVersionAnswer *) data;
 | 
			
		||||
    char tmp[9];
 | 
			
		||||
    tmp[8] = 0;
 | 
			
		||||
    strncpy(tmp, answr->protocol_version, 8);
 | 
			
		||||
    this->hvac_protocol_version_ = std::string(tmp);
 | 
			
		||||
    strncpy(tmp, answr->software_version, 8);
 | 
			
		||||
    this->hvac_software_version_ = std::string(tmp);
 | 
			
		||||
    strncpy(tmp, answr->hardware_version, 8);
 | 
			
		||||
    this->hvac_hardware_version_ = std::string(tmp);
 | 
			
		||||
    strncpy(tmp, answr->device_name, 8);
 | 
			
		||||
    this->hvac_device_name_ = std::string(tmp);
 | 
			
		||||
    this->hvac_functions_[0] = (answr->functions[1] & 0x01) != 0;  // interactive mode support
 | 
			
		||||
    this->hvac_functions_[1] = (answr->functions[1] & 0x02) != 0;  // controller-device mode support
 | 
			
		||||
    this->hvac_functions_[2] = (answr->functions[1] & 0x04) != 0;  // crc support
 | 
			
		||||
    this->hvac_functions_[3] = (answr->functions[1] & 0x08) != 0;  // multiple AC support
 | 
			
		||||
    this->hvac_functions_[4] = (answr->functions[1] & 0x20) != 0;  // roles support
 | 
			
		||||
    this->hvac_hardware_info_available_ = true;
 | 
			
		||||
    this->set_phase_(ProtocolPhases::SENDING_INIT_2);
 | 
			
		||||
    return result;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
 | 
			
		||||
                                                                     : ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type,
 | 
			
		||||
                                                                       const uint8_t *data, size_t data_size) {
 | 
			
		||||
  haier_protocol::HandlerError result = this->answer_preprocess_(
 | 
			
		||||
      request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID, message_type,
 | 
			
		||||
      (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_ANSWER_INIT_2);
 | 
			
		||||
  if (result == haier_protocol::HandlerError::HANDLER_OK) {
 | 
			
		||||
    this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
 | 
			
		||||
    return result;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
 | 
			
		||||
                                                                     : ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, uint8_t message_type,
 | 
			
		||||
                                                         const uint8_t *data, size_t data_size) {
 | 
			
		||||
  haier_protocol::HandlerError result =
 | 
			
		||||
      this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::CONTROL, message_type,
 | 
			
		||||
                               (uint8_t) hon_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
 | 
			
		||||
  if (result == haier_protocol::HandlerError::HANDLER_OK) {
 | 
			
		||||
    result = this->process_status_message_(data, data_size);
 | 
			
		||||
    if (result != haier_protocol::HandlerError::HANDLER_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result);
 | 
			
		||||
      this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
 | 
			
		||||
                                                                       : ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
    } else {
 | 
			
		||||
      if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) {
 | 
			
		||||
        memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl));
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size,
 | 
			
		||||
                 sizeof(hon_protocol::HaierPacketControl));
 | 
			
		||||
      }
 | 
			
		||||
      if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) {
 | 
			
		||||
        ESP_LOGI(TAG, "First HVAC status received");
 | 
			
		||||
        this->set_phase_(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
 | 
			
		||||
      } else if ((this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) ||
 | 
			
		||||
                 (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_ON_ANSWER) ||
 | 
			
		||||
                 (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_OFF_ANSWER)) {
 | 
			
		||||
        this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
      } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) {
 | 
			
		||||
        this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
        this->set_force_send_control_(false);
 | 
			
		||||
        if (this->hvac_settings_.valid)
 | 
			
		||||
          this->hvac_settings_.reset();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return result;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
 | 
			
		||||
                                                                     : ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(uint8_t request_type,
 | 
			
		||||
                                                                                    uint8_t message_type,
 | 
			
		||||
                                                                                    const uint8_t *data,
 | 
			
		||||
                                                                                    size_t data_size) {
 | 
			
		||||
  haier_protocol::HandlerError result =
 | 
			
		||||
      this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION,
 | 
			
		||||
                               message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE,
 | 
			
		||||
                               ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
 | 
			
		||||
  if (result == haier_protocol::HandlerError::HANDLER_OK) {
 | 
			
		||||
    this->set_phase_(ProtocolPhases::SENDING_SIGNAL_LEVEL);
 | 
			
		||||
    return result;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::report_network_status_answer_handler_(uint8_t request_type,
 | 
			
		||||
                                                                               uint8_t message_type,
 | 
			
		||||
                                                                               const uint8_t *data, size_t data_size) {
 | 
			
		||||
  haier_protocol::HandlerError result =
 | 
			
		||||
      this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS, message_type,
 | 
			
		||||
                               (uint8_t) hon_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
 | 
			
		||||
  this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type,
 | 
			
		||||
                                                                          const uint8_t *data, size_t data_size) {
 | 
			
		||||
  if (request_type == (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS) {
 | 
			
		||||
    if (message_type != (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) {
 | 
			
		||||
      // Unexpected answer to request
 | 
			
		||||
      this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
      return haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->protocol_phase_ != ProtocolPhases::WAITING_ALARM_STATUS_ANSWER) {
 | 
			
		||||
      // Don't expect this answer now
 | 
			
		||||
      this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
      return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
 | 
			
		||||
    }
 | 
			
		||||
    memcpy(this->active_alarms_, data + 2, 8);
 | 
			
		||||
    this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
    return haier_protocol::HandlerError::HANDLER_OK;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
    return haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HonClimate::set_answers_handlers() {
 | 
			
		||||
  // Set handlers
 | 
			
		||||
  this->haier_protocol_.set_answer_handler(
 | 
			
		||||
      (uint8_t) (hon_protocol::FrameType::GET_DEVICE_VERSION),
 | 
			
		||||
      std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
 | 
			
		||||
                std::placeholders::_3, std::placeholders::_4));
 | 
			
		||||
  this->haier_protocol_.set_answer_handler(
 | 
			
		||||
      (uint8_t) (hon_protocol::FrameType::GET_DEVICE_ID),
 | 
			
		||||
      std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
 | 
			
		||||
                std::placeholders::_3, std::placeholders::_4));
 | 
			
		||||
  this->haier_protocol_.set_answer_handler(
 | 
			
		||||
      (uint8_t) (hon_protocol::FrameType::CONTROL),
 | 
			
		||||
      std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3,
 | 
			
		||||
                std::placeholders::_4));
 | 
			
		||||
  this->haier_protocol_.set_answer_handler(
 | 
			
		||||
      (uint8_t) (hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION),
 | 
			
		||||
      std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1,
 | 
			
		||||
                std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
 | 
			
		||||
  this->haier_protocol_.set_answer_handler(
 | 
			
		||||
      (uint8_t) (hon_protocol::FrameType::GET_ALARM_STATUS),
 | 
			
		||||
      std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
 | 
			
		||||
                std::placeholders::_3, std::placeholders::_4));
 | 
			
		||||
  this->haier_protocol_.set_answer_handler(
 | 
			
		||||
      (uint8_t) (hon_protocol::FrameType::REPORT_NETWORK_STATUS),
 | 
			
		||||
      std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
 | 
			
		||||
                std::placeholders::_3, std::placeholders::_4));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HonClimate::dump_config() {
 | 
			
		||||
  HaierClimateBase::dump_config();
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Protocol version: hOn");
 | 
			
		||||
  if (this->hvac_hardware_info_available_) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Device protocol version: %s", this->hvac_protocol_version_.c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Device software version: %s", this->hvac_software_version_.c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Device hardware version: %s", this->hvac_hardware_version_.c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Device name: %s", this->hvac_device_name_.c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Device features:%s%s%s%s%s", (this->hvac_functions_[0] ? " interactive" : ""),
 | 
			
		||||
                  (this->hvac_functions_[1] ? " controller-device" : ""), (this->hvac_functions_[2] ? " crc" : ""),
 | 
			
		||||
                  (this->hvac_functions_[3] ? " multinode" : ""), (this->hvac_functions_[4] ? " role" : ""));
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
  switch (this->protocol_phase_) {
 | 
			
		||||
    case ProtocolPhases::SENDING_INIT_1:
 | 
			
		||||
      if (this->can_send_message() && this->is_protocol_initialisation_interval_exceded_(now)) {
 | 
			
		||||
        this->hvac_hardware_info_available_ = false;
 | 
			
		||||
        // Indicate device capabilities:
 | 
			
		||||
        // bit 0 - if 1 module support interactive mode
 | 
			
		||||
        // bit 1 - if 1 module support controller-device mode
 | 
			
		||||
        // bit 2 - if 1 module support crc
 | 
			
		||||
        // bit 3 - if 1 module support multiple devices
 | 
			
		||||
        // bit 4..bit 15 - not used
 | 
			
		||||
        uint8_t module_capabilities[2] = {0b00000000, 0b00000111};
 | 
			
		||||
        static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST(
 | 
			
		||||
            (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities));
 | 
			
		||||
        this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_);
 | 
			
		||||
        this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_1);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_INIT_2:
 | 
			
		||||
      if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
 | 
			
		||||
        static const haier_protocol::HaierMessage DEVICEID_REQUEST((uint8_t) hon_protocol::FrameType::GET_DEVICE_ID);
 | 
			
		||||
        this->send_message_(DEVICEID_REQUEST, this->use_crc_);
 | 
			
		||||
        this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_2);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
 | 
			
		||||
    case ProtocolPhases::SENDING_STATUS_REQUEST:
 | 
			
		||||
      if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
 | 
			
		||||
        static const haier_protocol::HaierMessage STATUS_REQUEST(
 | 
			
		||||
            (uint8_t) hon_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcomandsControl::GET_USER_DATA);
 | 
			
		||||
        this->send_message_(STATUS_REQUEST, this->use_crc_);
 | 
			
		||||
        this->last_status_request_ = now;
 | 
			
		||||
        this->set_phase_((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1));
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
#ifdef USE_WIFI
 | 
			
		||||
    case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
 | 
			
		||||
      if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
 | 
			
		||||
        static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST(
 | 
			
		||||
            (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION);
 | 
			
		||||
        this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_);
 | 
			
		||||
        this->last_signal_request_ = now;
 | 
			
		||||
        this->set_phase_(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_SIGNAL_LEVEL:
 | 
			
		||||
      if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
 | 
			
		||||
        static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00};
 | 
			
		||||
        if (wifi::global_wifi_component->is_connected()) {
 | 
			
		||||
          wifi_status_data[1] = 0;
 | 
			
		||||
          int8_t rssi = wifi::global_wifi_component->wifi_rssi();
 | 
			
		||||
          wifi_status_data[3] = uint8_t((128 + rssi) / 1.28f);
 | 
			
		||||
          ESP_LOGD(TAG, "WiFi signal is: %ddBm => %d%%", rssi, wifi_status_data[3]);
 | 
			
		||||
        } else {
 | 
			
		||||
          ESP_LOGD(TAG, "WiFi is not connected");
 | 
			
		||||
          wifi_status_data[1] = 1;
 | 
			
		||||
          wifi_status_data[3] = 0;
 | 
			
		||||
        }
 | 
			
		||||
        haier_protocol::HaierMessage wifi_status_request((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS,
 | 
			
		||||
                                                         wifi_status_data, sizeof(wifi_status_data));
 | 
			
		||||
        this->send_message_(wifi_status_request, this->use_crc_);
 | 
			
		||||
        this->set_phase_(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
 | 
			
		||||
    case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
 | 
			
		||||
      break;
 | 
			
		||||
#else
 | 
			
		||||
    case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
 | 
			
		||||
    case ProtocolPhases::SENDING_SIGNAL_LEVEL:
 | 
			
		||||
    case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
 | 
			
		||||
    case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
 | 
			
		||||
      this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
      break;
 | 
			
		||||
#endif
 | 
			
		||||
    case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
 | 
			
		||||
      if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
 | 
			
		||||
        static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(
 | 
			
		||||
            (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS);
 | 
			
		||||
        this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_);
 | 
			
		||||
        this->set_phase_(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_CONTROL:
 | 
			
		||||
      if (this->first_control_attempt_) {
 | 
			
		||||
        this->control_request_timestamp_ = now;
 | 
			
		||||
        this->first_control_attempt_ = false;
 | 
			
		||||
      }
 | 
			
		||||
      if (this->is_control_message_timeout_exceeded_(now)) {
 | 
			
		||||
        ESP_LOGW(TAG, "Sending control packet timeout!");
 | 
			
		||||
        this->set_force_send_control_(false);
 | 
			
		||||
        if (this->hvac_settings_.valid)
 | 
			
		||||
          this->hvac_settings_.reset();
 | 
			
		||||
        this->forced_request_status_ = true;
 | 
			
		||||
        this->forced_publish_ = true;
 | 
			
		||||
        this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
      } else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) {
 | 
			
		||||
        haier_protocol::HaierMessage control_message = get_control_message();
 | 
			
		||||
        this->send_message_(control_message, this->use_crc_);
 | 
			
		||||
        ESP_LOGI(TAG, "Control packet sent");
 | 
			
		||||
        this->set_phase_(ProtocolPhases::WAITING_CONTROL_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_POWER_ON_COMMAND:
 | 
			
		||||
    case ProtocolPhases::SENDING_POWER_OFF_COMMAND:
 | 
			
		||||
      if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
 | 
			
		||||
        uint8_t pwr_cmd_buf[2] = {0x00, 0x00};
 | 
			
		||||
        if (this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND)
 | 
			
		||||
          pwr_cmd_buf[1] = 0x01;
 | 
			
		||||
        haier_protocol::HaierMessage power_cmd((uint8_t) hon_protocol::FrameType::CONTROL,
 | 
			
		||||
                                               ((uint16_t) hon_protocol::SubcomandsControl::SET_SINGLE_PARAMETER) + 1,
 | 
			
		||||
                                               pwr_cmd_buf, sizeof(pwr_cmd_buf));
 | 
			
		||||
        this->send_message_(power_cmd, this->use_crc_);
 | 
			
		||||
        this->set_phase_(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND
 | 
			
		||||
                             ? ProtocolPhases::WAITING_POWER_ON_ANSWER
 | 
			
		||||
                             : ProtocolPhases::WAITING_POWER_OFF_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case ProtocolPhases::WAITING_ANSWER_INIT_1:
 | 
			
		||||
    case ProtocolPhases::WAITING_ANSWER_INIT_2:
 | 
			
		||||
    case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER:
 | 
			
		||||
    case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER:
 | 
			
		||||
    case ProtocolPhases::WAITING_STATUS_ANSWER:
 | 
			
		||||
    case ProtocolPhases::WAITING_CONTROL_ANSWER:
 | 
			
		||||
    case ProtocolPhases::WAITING_POWER_ON_ANSWER:
 | 
			
		||||
    case ProtocolPhases::WAITING_POWER_OFF_ANSWER:
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::IDLE: {
 | 
			
		||||
      if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
 | 
			
		||||
        this->set_phase_(ProtocolPhases::SENDING_STATUS_REQUEST);
 | 
			
		||||
        this->forced_request_status_ = false;
 | 
			
		||||
      }
 | 
			
		||||
#ifdef USE_WIFI
 | 
			
		||||
      else if (this->send_wifi_signal_ &&
 | 
			
		||||
               (std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() >
 | 
			
		||||
                SIGNAL_LEVEL_UPDATE_INTERVAL_MS))
 | 
			
		||||
        this->set_phase_(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
 | 
			
		||||
#endif
 | 
			
		||||
    } break;
 | 
			
		||||
    default:
 | 
			
		||||
      // Shouldn't get here
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
      ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
 | 
			
		||||
               phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_);
 | 
			
		||||
#else
 | 
			
		||||
      ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_);
 | 
			
		||||
#endif
 | 
			
		||||
      this->set_phase_(ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HaierMessage HonClimate::get_control_message() {
 | 
			
		||||
  uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
 | 
			
		||||
  memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
 | 
			
		||||
  hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
 | 
			
		||||
  bool has_hvac_settings = false;
 | 
			
		||||
  if (this->hvac_settings_.valid) {
 | 
			
		||||
    has_hvac_settings = true;
 | 
			
		||||
    HvacSettings climate_control;
 | 
			
		||||
    climate_control = this->hvac_settings_;
 | 
			
		||||
    if (climate_control.mode.has_value()) {
 | 
			
		||||
      switch (climate_control.mode.value()) {
 | 
			
		||||
        case CLIMATE_MODE_OFF:
 | 
			
		||||
          out_data->ac_power = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_MODE_AUTO:
 | 
			
		||||
          out_data->ac_power = 1;
 | 
			
		||||
          out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::AUTO;
 | 
			
		||||
          out_data->fan_mode = this->other_modes_fan_speed_;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_MODE_HEAT:
 | 
			
		||||
          out_data->ac_power = 1;
 | 
			
		||||
          out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::HEAT;
 | 
			
		||||
          out_data->fan_mode = this->other_modes_fan_speed_;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_MODE_DRY:
 | 
			
		||||
          out_data->ac_power = 1;
 | 
			
		||||
          out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
 | 
			
		||||
          out_data->fan_mode = this->other_modes_fan_speed_;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_MODE_FAN_ONLY:
 | 
			
		||||
          out_data->ac_power = 1;
 | 
			
		||||
          out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::FAN;
 | 
			
		||||
          out_data->fan_mode = this->fan_mode_speed_;  // Auto doesn't work in fan only mode
 | 
			
		||||
          // Disabling boost and eco mode for Fan only
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          out_data->fast_mode = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_MODE_COOL:
 | 
			
		||||
          out_data->ac_power = 1;
 | 
			
		||||
          out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::COOL;
 | 
			
		||||
          out_data->fan_mode = this->other_modes_fan_speed_;
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          ESP_LOGE("Control", "Unsupported climate mode");
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Set fan speed, if we are in fan mode, reject auto in fan mode
 | 
			
		||||
    if (climate_control.fan_mode.has_value()) {
 | 
			
		||||
      switch (climate_control.fan_mode.value()) {
 | 
			
		||||
        case CLIMATE_FAN_LOW:
 | 
			
		||||
          out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_LOW;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_FAN_MEDIUM:
 | 
			
		||||
          out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_MID;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_FAN_HIGH:
 | 
			
		||||
          out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_HIGH;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_FAN_AUTO:
 | 
			
		||||
          if (mode != CLIMATE_MODE_FAN_ONLY)  // if we are not in fan only mode
 | 
			
		||||
            out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          ESP_LOGE("Control", "Unsupported fan mode");
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Set swing mode
 | 
			
		||||
    if (climate_control.swing_mode.has_value()) {
 | 
			
		||||
      switch (climate_control.swing_mode.value()) {
 | 
			
		||||
        case CLIMATE_SWING_OFF:
 | 
			
		||||
          out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
 | 
			
		||||
          out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_SWING_VERTICAL:
 | 
			
		||||
          out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
 | 
			
		||||
          out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_SWING_HORIZONTAL:
 | 
			
		||||
          out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO;
 | 
			
		||||
          out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_SWING_BOTH:
 | 
			
		||||
          out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO;
 | 
			
		||||
          out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO;
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (climate_control.target_temperature.has_value()) {
 | 
			
		||||
      out_data->set_point =
 | 
			
		||||
          climate_control.target_temperature.value() - 16;  // set the temperature at our offset, subtract 16.
 | 
			
		||||
    }
 | 
			
		||||
    if (out_data->ac_power == 0) {
 | 
			
		||||
      // If AC is off - no presets alowed
 | 
			
		||||
      out_data->quiet_mode = 0;
 | 
			
		||||
      out_data->fast_mode = 0;
 | 
			
		||||
      out_data->sleep_mode = 0;
 | 
			
		||||
    } else if (climate_control.preset.has_value()) {
 | 
			
		||||
      switch (climate_control.preset.value()) {
 | 
			
		||||
        case CLIMATE_PRESET_NONE:
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          out_data->fast_mode = 0;
 | 
			
		||||
          out_data->sleep_mode = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_ECO:
 | 
			
		||||
          // Eco is not supported in Fan only mode
 | 
			
		||||
          out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
 | 
			
		||||
          out_data->fast_mode = 0;
 | 
			
		||||
          out_data->sleep_mode = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_BOOST:
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          // Boost is not supported in Fan only mode
 | 
			
		||||
          out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
 | 
			
		||||
          out_data->sleep_mode = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_AWAY:
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          out_data->fast_mode = 0;
 | 
			
		||||
          out_data->sleep_mode = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_SLEEP:
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          out_data->fast_mode = 0;
 | 
			
		||||
          out_data->sleep_mode = 1;
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          ESP_LOGE("Control", "Unsupported preset");
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    if (out_data->vertical_swing_mode != (uint8_t) hon_protocol::VerticalSwingMode::AUTO)
 | 
			
		||||
      out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
 | 
			
		||||
    if (out_data->horizontal_swing_mode != (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)
 | 
			
		||||
      out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
 | 
			
		||||
  }
 | 
			
		||||
  out_data->beeper_status = ((!this->beeper_status_) || (!has_hvac_settings)) ? 1 : 0;
 | 
			
		||||
  control_out_buffer[4] = 0;  // This byte should be cleared before setting values
 | 
			
		||||
  out_data->display_status = this->display_status_ ? 1 : 0;
 | 
			
		||||
  out_data->health_mode = this->health_mode_ ? 1 : 0;
 | 
			
		||||
  switch (this->action_request_) {
 | 
			
		||||
    case ActionRequest::START_SELF_CLEAN:
 | 
			
		||||
      this->action_request_ = ActionRequest::NO_ACTION;
 | 
			
		||||
      out_data->self_cleaning_status = 1;
 | 
			
		||||
      out_data->steri_clean = 0;
 | 
			
		||||
      out_data->set_point = 0x06;
 | 
			
		||||
      out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
 | 
			
		||||
      out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
 | 
			
		||||
      out_data->ac_power = 1;
 | 
			
		||||
      out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
 | 
			
		||||
      out_data->light_status = 0;
 | 
			
		||||
      break;
 | 
			
		||||
    case ActionRequest::START_STERI_CLEAN:
 | 
			
		||||
      this->action_request_ = ActionRequest::NO_ACTION;
 | 
			
		||||
      out_data->self_cleaning_status = 0;
 | 
			
		||||
      out_data->steri_clean = 1;
 | 
			
		||||
      out_data->set_point = 0x06;
 | 
			
		||||
      out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
 | 
			
		||||
      out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
 | 
			
		||||
      out_data->ac_power = 1;
 | 
			
		||||
      out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
 | 
			
		||||
      out_data->light_status = 0;
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      // No change
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  return haier_protocol::HaierMessage((uint8_t) hon_protocol::FrameType::CONTROL,
 | 
			
		||||
                                      (uint16_t) hon_protocol::SubcomandsControl::SET_GROUP_PARAMETERS,
 | 
			
		||||
                                      control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
 | 
			
		||||
  if (size < sizeof(hon_protocol::HaierStatus))
 | 
			
		||||
    return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
 | 
			
		||||
  hon_protocol::HaierStatus packet;
 | 
			
		||||
  if (size < sizeof(hon_protocol::HaierStatus))
 | 
			
		||||
    size = sizeof(hon_protocol::HaierStatus);
 | 
			
		||||
  memcpy(&packet, packet_buffer, size);
 | 
			
		||||
  if (packet.sensors.error_status != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status);
 | 
			
		||||
  }
 | 
			
		||||
  if ((this->outdoor_sensor_ != nullptr) && (got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) {
 | 
			
		||||
    got_valid_outdoor_temp_ = true;
 | 
			
		||||
    float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET);
 | 
			
		||||
    if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp))
 | 
			
		||||
      this->outdoor_sensor_->publish_state(otemp);
 | 
			
		||||
  }
 | 
			
		||||
  bool should_publish = false;
 | 
			
		||||
  {
 | 
			
		||||
    // Extra modes/presets
 | 
			
		||||
    optional<ClimatePreset> old_preset = this->preset;
 | 
			
		||||
    if (packet.control.quiet_mode != 0) {
 | 
			
		||||
      this->preset = CLIMATE_PRESET_ECO;
 | 
			
		||||
    } else if (packet.control.fast_mode != 0) {
 | 
			
		||||
      this->preset = CLIMATE_PRESET_BOOST;
 | 
			
		||||
    } else if (packet.control.sleep_mode != 0) {
 | 
			
		||||
      this->preset = CLIMATE_PRESET_SLEEP;
 | 
			
		||||
    } else {
 | 
			
		||||
      this->preset = CLIMATE_PRESET_NONE;
 | 
			
		||||
    }
 | 
			
		||||
    should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value());
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    // Target temperature
 | 
			
		||||
    float old_target_temperature = this->target_temperature;
 | 
			
		||||
    this->target_temperature = packet.control.set_point + 16.0f;
 | 
			
		||||
    should_publish = should_publish || (old_target_temperature != this->target_temperature);
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    // Current temperature
 | 
			
		||||
    float old_current_temperature = this->current_temperature;
 | 
			
		||||
    this->current_temperature = packet.sensors.room_temperature / 2.0f;
 | 
			
		||||
    should_publish = should_publish || (old_current_temperature != this->current_temperature);
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    // Fan mode
 | 
			
		||||
    optional<ClimateFanMode> old_fan_mode = this->fan_mode;
 | 
			
		||||
    // remember the fan speed we last had for climate vs fan
 | 
			
		||||
    if (packet.control.ac_mode == (uint8_t) hon_protocol::ConditioningMode::FAN) {
 | 
			
		||||
      if (packet.control.fan_mode != (uint8_t) hon_protocol::FanMode::FAN_AUTO)
 | 
			
		||||
        this->fan_mode_speed_ = packet.control.fan_mode;
 | 
			
		||||
    } else {
 | 
			
		||||
      this->other_modes_fan_speed_ = packet.control.fan_mode;
 | 
			
		||||
    }
 | 
			
		||||
    switch (packet.control.fan_mode) {
 | 
			
		||||
      case (uint8_t) hon_protocol::FanMode::FAN_AUTO:
 | 
			
		||||
        if (packet.control.ac_mode != (uint8_t) hon_protocol::ConditioningMode::FAN) {
 | 
			
		||||
          this->fan_mode = CLIMATE_FAN_AUTO;
 | 
			
		||||
        } else {
 | 
			
		||||
          // Shouldn't accept fan speed auto in fan-only mode even if AC reports it
 | 
			
		||||
          ESP_LOGI(TAG, "Fan speed Auto is not supported in Fan only AC mode, ignoring");
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      case (uint8_t) hon_protocol::FanMode::FAN_MID:
 | 
			
		||||
        this->fan_mode = CLIMATE_FAN_MEDIUM;
 | 
			
		||||
        break;
 | 
			
		||||
      case (uint8_t) hon_protocol::FanMode::FAN_LOW:
 | 
			
		||||
        this->fan_mode = CLIMATE_FAN_LOW;
 | 
			
		||||
        break;
 | 
			
		||||
      case (uint8_t) hon_protocol::FanMode::FAN_HIGH:
 | 
			
		||||
        this->fan_mode = CLIMATE_FAN_HIGH;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
    should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value());
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    // Display status
 | 
			
		||||
    // should be before "Climate mode" because it is changing this->mode
 | 
			
		||||
    if (packet.control.ac_power != 0) {
 | 
			
		||||
      // if AC is off display status always ON so process it only when AC is on
 | 
			
		||||
      bool disp_status = packet.control.display_status != 0;
 | 
			
		||||
      if (disp_status != this->display_status_) {
 | 
			
		||||
        // Do something only if display status changed
 | 
			
		||||
        if (this->mode == CLIMATE_MODE_OFF) {
 | 
			
		||||
          // AC just turned on from remote need to turn off display
 | 
			
		||||
          this->set_force_send_control_(true);
 | 
			
		||||
        } else {
 | 
			
		||||
          this->display_status_ = disp_status;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    // Health mode
 | 
			
		||||
    bool old_health_mode = this->health_mode_;
 | 
			
		||||
    this->health_mode_ = packet.control.health_mode == 1;
 | 
			
		||||
    should_publish = should_publish || (old_health_mode != this->health_mode_);
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    CleaningState new_cleaning;
 | 
			
		||||
    if (packet.control.steri_clean == 1) {
 | 
			
		||||
      // Steri-cleaning
 | 
			
		||||
      new_cleaning = CleaningState::STERI_CLEAN;
 | 
			
		||||
    } else if (packet.control.self_cleaning_status == 1) {
 | 
			
		||||
      // Self-cleaning
 | 
			
		||||
      new_cleaning = CleaningState::SELF_CLEAN;
 | 
			
		||||
    } else {
 | 
			
		||||
      // No cleaning
 | 
			
		||||
      new_cleaning = CleaningState::NO_CLEANING;
 | 
			
		||||
    }
 | 
			
		||||
    if (new_cleaning != this->cleaning_status_) {
 | 
			
		||||
      ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning);
 | 
			
		||||
      if (new_cleaning == CleaningState::NO_CLEANING) {
 | 
			
		||||
        // Turnuin AC off after cleaning
 | 
			
		||||
        this->action_request_ = ActionRequest::TURN_POWER_OFF;
 | 
			
		||||
      }
 | 
			
		||||
      this->cleaning_status_ = new_cleaning;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    // Climate mode
 | 
			
		||||
    ClimateMode old_mode = this->mode;
 | 
			
		||||
    if (packet.control.ac_power == 0) {
 | 
			
		||||
      this->mode = CLIMATE_MODE_OFF;
 | 
			
		||||
    } else {
 | 
			
		||||
      // Check current hvac mode
 | 
			
		||||
      switch (packet.control.ac_mode) {
 | 
			
		||||
        case (uint8_t) hon_protocol::ConditioningMode::COOL:
 | 
			
		||||
          this->mode = CLIMATE_MODE_COOL;
 | 
			
		||||
          break;
 | 
			
		||||
        case (uint8_t) hon_protocol::ConditioningMode::HEAT:
 | 
			
		||||
          this->mode = CLIMATE_MODE_HEAT;
 | 
			
		||||
          break;
 | 
			
		||||
        case (uint8_t) hon_protocol::ConditioningMode::DRY:
 | 
			
		||||
          this->mode = CLIMATE_MODE_DRY;
 | 
			
		||||
          break;
 | 
			
		||||
        case (uint8_t) hon_protocol::ConditioningMode::FAN:
 | 
			
		||||
          this->mode = CLIMATE_MODE_FAN_ONLY;
 | 
			
		||||
          break;
 | 
			
		||||
        case (uint8_t) hon_protocol::ConditioningMode::AUTO:
 | 
			
		||||
          this->mode = CLIMATE_MODE_AUTO;
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    should_publish = should_publish || (old_mode != this->mode);
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    // Swing mode
 | 
			
		||||
    ClimateSwingMode old_swing_mode = this->swing_mode;
 | 
			
		||||
    if (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO) {
 | 
			
		||||
      if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) {
 | 
			
		||||
        this->swing_mode = CLIMATE_SWING_BOTH;
 | 
			
		||||
      } else {
 | 
			
		||||
        this->swing_mode = CLIMATE_SWING_HORIZONTAL;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) {
 | 
			
		||||
        this->swing_mode = CLIMATE_SWING_VERTICAL;
 | 
			
		||||
      } else {
 | 
			
		||||
        this->swing_mode = CLIMATE_SWING_OFF;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    should_publish = should_publish || (old_swing_mode != this->swing_mode);
 | 
			
		||||
  }
 | 
			
		||||
  this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
 | 
			
		||||
  if (this->forced_publish_ || should_publish) {
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
    std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now();
 | 
			
		||||
#endif
 | 
			
		||||
    this->publish_state();
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
    ESP_LOGV(TAG, "Publish delay: %lld ms",
 | 
			
		||||
             std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() -
 | 
			
		||||
                                                                   _publish_start)
 | 
			
		||||
                 .count());
 | 
			
		||||
#endif
 | 
			
		||||
    this->forced_publish_ = false;
 | 
			
		||||
  }
 | 
			
		||||
  if (should_publish) {
 | 
			
		||||
    ESP_LOGI(TAG, "HVAC values changed");
 | 
			
		||||
  }
 | 
			
		||||
  esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
 | 
			
		||||
                  "HVAC Mode = 0x%X", packet.control.ac_mode);
 | 
			
		||||
  esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
 | 
			
		||||
                  "Fan speed Status = 0x%X", packet.control.fan_mode);
 | 
			
		||||
  esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
 | 
			
		||||
                  "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode);
 | 
			
		||||
  esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
 | 
			
		||||
                  "Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode);
 | 
			
		||||
  esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
 | 
			
		||||
                  "Set Point Status = 0x%X", packet.control.set_point);
 | 
			
		||||
  return haier_protocol::HandlerError::HANDLER_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HonClimate::is_message_invalid(uint8_t message_type) {
 | 
			
		||||
  return message_type == (uint8_t) hon_protocol::FrameType::INVALID;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HonClimate::process_pending_action() {
 | 
			
		||||
  switch (this->action_request_) {
 | 
			
		||||
    case ActionRequest::START_SELF_CLEAN:
 | 
			
		||||
    case ActionRequest::START_STERI_CLEAN:
 | 
			
		||||
      // Will reset action with control message sending
 | 
			
		||||
      this->set_phase_(ProtocolPhases::SENDING_CONTROL);
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      HaierClimateBase::process_pending_action();
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace haier
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										95
									
								
								esphome/components/haier/hon_climate.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								esphome/components/haier/hon_climate.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,95 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "haier_base.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace haier {
 | 
			
		||||
 | 
			
		||||
enum class AirflowVerticalDirection : uint8_t {
 | 
			
		||||
  HEALTH_UP = 0,
 | 
			
		||||
  MAX_UP = 1,
 | 
			
		||||
  UP = 2,
 | 
			
		||||
  CENTER = 3,
 | 
			
		||||
  DOWN = 4,
 | 
			
		||||
  HEALTH_DOWN = 5,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class AirflowHorizontalDirection : uint8_t {
 | 
			
		||||
  MAX_LEFT = 0,
 | 
			
		||||
  LEFT = 1,
 | 
			
		||||
  CENTER = 2,
 | 
			
		||||
  RIGHT = 3,
 | 
			
		||||
  MAX_RIGHT = 4,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class CleaningState : uint8_t {
 | 
			
		||||
  NO_CLEANING = 0,
 | 
			
		||||
  SELF_CLEAN = 1,
 | 
			
		||||
  STERI_CLEAN = 2,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class HonClimate : public HaierClimateBase {
 | 
			
		||||
 public:
 | 
			
		||||
  HonClimate();
 | 
			
		||||
  HonClimate(const HonClimate &) = delete;
 | 
			
		||||
  HonClimate &operator=(const HonClimate &) = delete;
 | 
			
		||||
  ~HonClimate();
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void set_beeper_state(bool state);
 | 
			
		||||
  bool get_beeper_state() const;
 | 
			
		||||
  void set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor);
 | 
			
		||||
  AirflowVerticalDirection get_vertical_airflow() const;
 | 
			
		||||
  void set_vertical_airflow(AirflowVerticalDirection direction);
 | 
			
		||||
  AirflowHorizontalDirection get_horizontal_airflow() const;
 | 
			
		||||
  void set_horizontal_airflow(AirflowHorizontalDirection direction);
 | 
			
		||||
  std::string get_cleaning_status_text() const;
 | 
			
		||||
  CleaningState get_cleaning_status() const;
 | 
			
		||||
  void start_self_cleaning();
 | 
			
		||||
  void start_steri_cleaning();
 | 
			
		||||
  void set_send_wifi(bool send_wifi);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void set_answers_handlers() override;
 | 
			
		||||
  void process_phase(std::chrono::steady_clock::time_point now) override;
 | 
			
		||||
  haier_protocol::HaierMessage get_control_message() override;
 | 
			
		||||
  bool is_message_invalid(uint8_t message_type) override;
 | 
			
		||||
  void process_pending_action() override;
 | 
			
		||||
 | 
			
		||||
  // Answers handlers
 | 
			
		||||
  haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type,
 | 
			
		||||
                                                                  const uint8_t *data, size_t data_size);
 | 
			
		||||
  haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type,
 | 
			
		||||
                                                             const uint8_t *data, size_t data_size);
 | 
			
		||||
  haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data,
 | 
			
		||||
                                               size_t data_size);
 | 
			
		||||
  haier_protocol::HandlerError get_management_information_answer_handler_(uint8_t request_type, uint8_t message_type,
 | 
			
		||||
                                                                          const uint8_t *data, size_t data_size);
 | 
			
		||||
  haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type,
 | 
			
		||||
                                                                     const uint8_t *data, size_t data_size);
 | 
			
		||||
  haier_protocol::HandlerError get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type,
 | 
			
		||||
                                                                const uint8_t *data, size_t data_size);
 | 
			
		||||
  // Helper functions
 | 
			
		||||
  haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
 | 
			
		||||
  std::unique_ptr<uint8_t[]> last_status_message_;
 | 
			
		||||
  bool beeper_status_;
 | 
			
		||||
  CleaningState cleaning_status_;
 | 
			
		||||
  bool got_valid_outdoor_temp_;
 | 
			
		||||
  AirflowVerticalDirection vertical_direction_;
 | 
			
		||||
  AirflowHorizontalDirection horizontal_direction_;
 | 
			
		||||
  bool hvac_hardware_info_available_;
 | 
			
		||||
  std::string hvac_protocol_version_;
 | 
			
		||||
  std::string hvac_software_version_;
 | 
			
		||||
  std::string hvac_hardware_version_;
 | 
			
		||||
  std::string hvac_device_name_;
 | 
			
		||||
  bool hvac_functions_[5];
 | 
			
		||||
  bool &use_crc_;
 | 
			
		||||
  uint8_t active_alarms_[8];
 | 
			
		||||
  esphome::sensor::Sensor *outdoor_sensor_;
 | 
			
		||||
  bool send_wifi_signal_;
 | 
			
		||||
  std::chrono::steady_clock::time_point last_signal_request_;  // To send WiFI signal level
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace haier
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										228
									
								
								esphome/components/haier/hon_packet.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								esphome/components/haier/hon_packet.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,228 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace haier {
 | 
			
		||||
namespace hon_protocol {
 | 
			
		||||
 | 
			
		||||
enum class VerticalSwingMode : uint8_t {
 | 
			
		||||
  HEALTH_UP = 0x01,
 | 
			
		||||
  MAX_UP = 0x02,
 | 
			
		||||
  HEALTH_DOWN = 0x03,
 | 
			
		||||
  UP = 0x04,
 | 
			
		||||
  CENTER = 0x06,
 | 
			
		||||
  DOWN = 0x08,
 | 
			
		||||
  AUTO = 0x0C
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class HorizontalSwingMode : uint8_t {
 | 
			
		||||
  CENTER = 0x00,
 | 
			
		||||
  MAX_LEFT = 0x03,
 | 
			
		||||
  LEFT = 0x04,
 | 
			
		||||
  RIGHT = 0x05,
 | 
			
		||||
  MAX_RIGHT = 0x06,
 | 
			
		||||
  AUTO = 0x07
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class ConditioningMode : uint8_t {
 | 
			
		||||
  AUTO = 0x00,
 | 
			
		||||
  COOL = 0x01,
 | 
			
		||||
  DRY = 0x02,
 | 
			
		||||
  HEALTHY_DRY = 0x03,
 | 
			
		||||
  HEAT = 0x04,
 | 
			
		||||
  ENERGY_SAVING = 0x05,
 | 
			
		||||
  FAN = 0x06
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class SpecialMode : uint8_t { NONE = 0x00, ELDERLY = 0x01, CHILDREN = 0x02, PREGNANT = 0x03 };
 | 
			
		||||
 | 
			
		||||
enum class FanMode : uint8_t { FAN_HIGH = 0x01, FAN_MID = 0x02, FAN_LOW = 0x03, FAN_AUTO = 0x05 };
 | 
			
		||||
 | 
			
		||||
struct HaierPacketControl {
 | 
			
		||||
  // Control bytes starts here
 | 
			
		||||
  // 10
 | 
			
		||||
  uint8_t set_point;  // Target temperature with 16°C offset (0x00 = 16°C)
 | 
			
		||||
  // 11
 | 
			
		||||
  uint8_t vertical_swing_mode : 4;  // See enum VerticalSwingMode
 | 
			
		||||
  uint8_t : 0;
 | 
			
		||||
  // 12
 | 
			
		||||
  uint8_t fan_mode : 3;      // See enum FanMode
 | 
			
		||||
  uint8_t special_mode : 2;  // See enum SpecialMode
 | 
			
		||||
  uint8_t ac_mode : 3;       // See enum ConditioningMode
 | 
			
		||||
  // 13
 | 
			
		||||
  uint8_t : 8;
 | 
			
		||||
  // 14
 | 
			
		||||
  uint8_t ten_degree : 1;          // 10 degree status
 | 
			
		||||
  uint8_t display_status : 1;      // If 0 disables AC's display
 | 
			
		||||
  uint8_t half_degree : 1;         // Use half degree
 | 
			
		||||
  uint8_t intelegence_status : 1;  // Intelligence status
 | 
			
		||||
  uint8_t pmv_status : 1;          // Comfort/PMV status
 | 
			
		||||
  uint8_t use_fahrenheit : 1;      // Use Fahrenheit instead of Celsius
 | 
			
		||||
  uint8_t : 1;
 | 
			
		||||
  uint8_t steri_clean : 1;
 | 
			
		||||
  // 15
 | 
			
		||||
  uint8_t ac_power : 1;                 // Is ac on or off
 | 
			
		||||
  uint8_t health_mode : 1;              // Health mode (negative ions) on or off
 | 
			
		||||
  uint8_t electric_heating_status : 1;  // Electric heating status
 | 
			
		||||
  uint8_t fast_mode : 1;                // Fast mode
 | 
			
		||||
  uint8_t quiet_mode : 1;               // Quiet mode
 | 
			
		||||
  uint8_t sleep_mode : 1;               // Sleep mode
 | 
			
		||||
  uint8_t lock_remote : 1;              // Disable remote
 | 
			
		||||
  uint8_t beeper_status : 1;  // If 1 disables AC's command feedback beeper (need to be set on every control command)
 | 
			
		||||
  // 16
 | 
			
		||||
  uint8_t target_humidity;  // Target humidity (0=30% .. 3C=90%, step = 1%)
 | 
			
		||||
  // 17
 | 
			
		||||
  uint8_t horizontal_swing_mode : 3;  // See enum HorizontalSwingMode
 | 
			
		||||
  uint8_t : 3;
 | 
			
		||||
  uint8_t human_sensing_status : 2;  // Human sensing status
 | 
			
		||||
  // 18
 | 
			
		||||
  uint8_t change_filter : 1;  // Filter need replacement
 | 
			
		||||
  uint8_t : 0;
 | 
			
		||||
  // 19
 | 
			
		||||
  uint8_t fresh_air_status : 1;       // Fresh air status
 | 
			
		||||
  uint8_t humidification_status : 1;  // Humidification status
 | 
			
		||||
  uint8_t pm2p5_cleaning_status : 1;  // PM2.5 cleaning status
 | 
			
		||||
  uint8_t ch2o_cleaning_status : 1;   // CH2O cleaning status
 | 
			
		||||
  uint8_t self_cleaning_status : 1;   // Self cleaning status
 | 
			
		||||
  uint8_t light_status : 1;           // Light status
 | 
			
		||||
  uint8_t energy_saving_status : 1;   // Energy saving status
 | 
			
		||||
  uint8_t cleaning_time_status : 1;   // Cleaning time (0 - accumulation, 1 - clear)
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct HaierPacketSensors {
 | 
			
		||||
  // 20
 | 
			
		||||
  uint8_t room_temperature;  // 0.5°C step
 | 
			
		||||
  // 21
 | 
			
		||||
  uint8_t room_humidity;  // 0%-100% with 1% step
 | 
			
		||||
  // 22
 | 
			
		||||
  uint8_t outdoor_temperature;  // 1°C step, -64°C offset (0=-64°C)
 | 
			
		||||
  // 23
 | 
			
		||||
  uint8_t pm2p5_level : 2;    // Indoor PM2.5 grade (00: Excellent, 01: good, 02: Medium, 03: Bad)
 | 
			
		||||
  uint8_t air_quality : 2;    // Air quality grade (00: Excellent, 01: good, 02: Medium, 03: Bad)
 | 
			
		||||
  uint8_t human_sensing : 2;  // Human presence result (00: N/A, 01: not detected, 02: One, 03: Multiple)
 | 
			
		||||
  uint8_t : 1;
 | 
			
		||||
  uint8_t ac_type : 1;  // 00 - Heat and cool, 01 - Cool only)
 | 
			
		||||
  // 24
 | 
			
		||||
  uint8_t error_status;  // See enum ErrorStatus
 | 
			
		||||
  // 25
 | 
			
		||||
  uint8_t operation_source : 2;   // who is controlling AC (00: Other, 01: Remote control, 02: Button, 03: ESP)
 | 
			
		||||
  uint8_t operation_mode_hk : 2;  // Homekit only, operation mode (00: Cool, 01: Dry, 02: Heat, 03: Fan)
 | 
			
		||||
  uint8_t : 3;
 | 
			
		||||
  uint8_t err_confirmation : 1;  // If 1 clear error status
 | 
			
		||||
  // 26
 | 
			
		||||
  uint16_t total_cleaning_time;  // Cleaning cumulative time (1h step)
 | 
			
		||||
  // 28
 | 
			
		||||
  uint16_t indoor_pm2p5_value;  // Indoor PM2.5 value (0 ug/m3 -  4095 ug/m3, 1 ug/m3 step)
 | 
			
		||||
  // 30
 | 
			
		||||
  uint16_t outdoor_pm2p5_value;  // Outdoor PM2.5 value (0 ug/m3 -  4095 ug/m3, 1 ug/m3 step)
 | 
			
		||||
  // 32
 | 
			
		||||
  uint16_t ch2o_value;  // Formaldehyde value (0 ug/m3 -  10000 ug/m3, 1 ug/m3 step)
 | 
			
		||||
  // 34
 | 
			
		||||
  uint16_t voc_value;  // VOC value (Volatile Organic Compounds) (0 ug/m3 -  1023 ug/m3, 1 ug/m3 step)
 | 
			
		||||
  // 36
 | 
			
		||||
  uint16_t co2_value;  // CO2 value (0 PPM -  10000 PPM, 1 PPM step)
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct HaierStatus {
 | 
			
		||||
  uint16_t subcommand;
 | 
			
		||||
  HaierPacketControl control;
 | 
			
		||||
  HaierPacketSensors sensors;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct DeviceVersionAnswer {
 | 
			
		||||
  char protocol_version[8];
 | 
			
		||||
  char software_version[8];
 | 
			
		||||
  uint8_t encryption[3];
 | 
			
		||||
  char hardware_version[8];
 | 
			
		||||
  uint8_t : 8;
 | 
			
		||||
  char device_name[8];
 | 
			
		||||
  uint8_t functions[2];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// In this section comments:
 | 
			
		||||
//  - module is the ESP32 control module (communication module in Haier protocol document)
 | 
			
		||||
//  - device is the conditioner control board (network appliances in Haier protocol document)
 | 
			
		||||
enum class FrameType : uint8_t {
 | 
			
		||||
  CONTROL = 0x01,  // Requests or sets one or multiple parameters (module <-> device, required)
 | 
			
		||||
  STATUS = 0x02,   // Contains one or multiple parameters values, usually answer to control frame (module <-> device,
 | 
			
		||||
                   // required)
 | 
			
		||||
  INVALID = 0x03,  // Communication error indication (module <-> device, required)
 | 
			
		||||
  ALARM_STATUS = 0x04,  // Alarm status report (module <-> device, interactive, required)
 | 
			
		||||
  CONFIRM = 0x05,  // Acknowledgment, usually used to confirm reception of frame if there is no special answer (module
 | 
			
		||||
                   // <-> device, required)
 | 
			
		||||
  REPORT = 0x06,   // Report frame (module <-> device, interactive, required)
 | 
			
		||||
  STOP_FAULT_ALARM = 0x09,             // Stop fault alarm frame (module -> device, interactive, required)
 | 
			
		||||
  SYSTEM_DOWNLIK = 0x11,               // System downlink frame (module -> device, optional)
 | 
			
		||||
  DEVICE_UPLINK = 0x12,                // Device uplink frame (module <- device , interactive, optional)
 | 
			
		||||
  SYSTEM_QUERY = 0x13,                 // System query frame (module -> device, optional)
 | 
			
		||||
  SYSTEM_QUERY_RESPONSE = 0x14,        // System query response frame (module <- device , optional)
 | 
			
		||||
  DEVICE_QUERY = 0x15,                 // Device query frame (module <- device, optional)
 | 
			
		||||
  DEVICE_QUERY_RESPONSE = 0x16,        // Device query response frame (module -> device, optional)
 | 
			
		||||
  GROUP_COMMAND = 0x60,                // Group command frame (module -> device, interactive, optional)
 | 
			
		||||
  GET_DEVICE_VERSION = 0x61,           // Requests device version (module -> device, required)
 | 
			
		||||
  GET_DEVICE_VERSION_RESPONSE = 0x62,  // Device version answer (module <- device, required_
 | 
			
		||||
  GET_ALL_ADDRESSES = 0x67,            // Requests all devices addresses (module -> device, interactive, optional)
 | 
			
		||||
  GET_ALL_ADDRESSES_RESPONSE =
 | 
			
		||||
      0x68,  // Answer to request of all devices addresses (module <- device , interactive, optional)
 | 
			
		||||
  HANDSET_CHANGE_NOTIFICATION = 0x69,  // Handset change notification frame (module <- device , interactive, optional)
 | 
			
		||||
  GET_DEVICE_ID = 0x70,                // Requests Device ID (module -> device, required)
 | 
			
		||||
  GET_DEVICE_ID_RESPONSE = 0x71,       // Response to device ID request (module <- device , required)
 | 
			
		||||
  GET_ALARM_STATUS = 0x73,             // Alarm status request (module -> device, required)
 | 
			
		||||
  GET_ALARM_STATUS_RESPONSE = 0x74,    // Response to alarm status request (module <- device, required)
 | 
			
		||||
  GET_DEVICE_CONFIGURATION = 0x7C,     // Requests device configuration (module -> device, interactive, required)
 | 
			
		||||
  GET_DEVICE_CONFIGURATION_RESPONSE =
 | 
			
		||||
      0x7D,  // Response to device configuration request (module <- device, interactive, required)
 | 
			
		||||
  DOWNLINK_TRANSPARENT_TRANSMISSION = 0x8C,  // Downlink transparent transmission (proxy data Haier cloud -> device)
 | 
			
		||||
                                             // (module -> device, interactive, optional)
 | 
			
		||||
  UPLINK_TRANSPARENT_TRANSMISSION = 0x8D,  // Uplink transparent transmission (proxy data device -> Haier cloud) (module
 | 
			
		||||
                                           // <- device, interactive, optional)
 | 
			
		||||
  START_DEVICE_UPGRADE = 0xE1,             // Initiate device OTA upgrade (module -> device, OTA required)
 | 
			
		||||
  START_DEVICE_UPGRADE_RESPONSE = 0xE2,  // Response to initiate device upgrade command (module <- device, OTA required)
 | 
			
		||||
  GET_FIRMWARE_CONTENT = 0xE5,           // Requests to send firmware (module <- device, OTA required)
 | 
			
		||||
  GET_FIRMWARE_CONTENT_RESPONSE =
 | 
			
		||||
      0xE6,                 // Response to send firmware request (module -> device, OTA required) (multipacket?)
 | 
			
		||||
  CHANGE_BAUD_RATE = 0xE7,  // Requests to change port baud rate (module <- device, OTA required)
 | 
			
		||||
  CHANGE_BAUD_RATE_RESPONSE = 0xE8,    // Response to change port baud rate request (module -> device, OTA required)
 | 
			
		||||
  GET_SUBBOARD_INFO = 0xE9,            // Requests subboard information (module -> device, required)
 | 
			
		||||
  GET_SUBBOARD_INFO_RESPONSE = 0xEA,   // Response to subboard information request (module <- device, required)
 | 
			
		||||
  GET_HARDWARE_INFO = 0xEB,            // Requests information about device and subboard (module -> device, required)
 | 
			
		||||
  GET_HARDWARE_INFO_RESPONSE = 0xEC,   // Response to hardware information request (module <- device, required)
 | 
			
		||||
  GET_UPGRADE_RESULT = 0xED,           // Requests result of the firmware update (module <- device, OTA required)
 | 
			
		||||
  GET_UPGRADE_RESULT_RESPONSE = 0xEF,  // Response to firmware update results request (module -> device, OTA required)
 | 
			
		||||
  GET_NETWORK_STATUS = 0xF0,           // Requests network status (module <- device, interactive, optional)
 | 
			
		||||
  GET_NETWORK_STATUS_RESPONSE = 0xF1,  // Response to network status request (module -> device, interactive, optional)
 | 
			
		||||
  START_WIFI_CONFIGURATION = 0xF2,     // Starts WiFi configuration procedure (module <- device, interactive, required)
 | 
			
		||||
  START_WIFI_CONFIGURATION_RESPONSE =
 | 
			
		||||
      0xF3,  // Response to start WiFi configuration request (module -> device, interactive, required)
 | 
			
		||||
  STOP_WIFI_CONFIGURATION = 0xF4,  // Stop WiFi configuration procedure (module <- device, interactive, required)
 | 
			
		||||
  STOP_WIFI_CONFIGURATION_RESPONSE =
 | 
			
		||||
      0xF5,  // Response to stop WiFi configuration request (module -> device, interactive, required)
 | 
			
		||||
  REPORT_NETWORK_STATUS = 0xF7,  // Reports network status (module -> device, required)
 | 
			
		||||
  CLEAR_CONFIGURATION = 0xF8,    // Request to clear module configuration (module <- device, interactive, optional)
 | 
			
		||||
  BIG_DATA_REPORT_CONFIGURATION =
 | 
			
		||||
      0xFA,  // Configuration for autoreport device full status (module -> device, interactive, optional)
 | 
			
		||||
  BIG_DATA_REPORT_CONFIGURATION_RESPONSE =
 | 
			
		||||
      0xFB,  // Response to set big data configuration (module <- device, interactive, optional)
 | 
			
		||||
  GET_MANAGEMENT_INFORMATION = 0xFC,  // Request management information from device (module -> device, required)
 | 
			
		||||
  GET_MANAGEMENT_INFORMATION_RESPONSE =
 | 
			
		||||
      0xFD,        // Response to management information request (module <- device, required)
 | 
			
		||||
  WAKE_UP = 0xFE,  // Request to wake up (module <-> device, optional)
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class SubcomandsControl : uint16_t {
 | 
			
		||||
  GET_PARAMETERS = 0x4C01,  // Request specific parameters (packet content: parameter ID1 + parameter ID2 + ...)
 | 
			
		||||
  GET_USER_DATA = 0x4D01,   // Request all user data from device (packet content: None)
 | 
			
		||||
  GET_BIG_DATA = 0x4DFE,    // Request big data information from device (packet content: None)
 | 
			
		||||
  SET_PARAMETERS = 0x5C01,  // Set parameters of the device and device return parameters (packet content: parameter ID1
 | 
			
		||||
                            // + parameter data1 + parameter ID2 + parameter data 2 + ...)
 | 
			
		||||
  SET_SINGLE_PARAMETER = 0x5D00,  // Set single parameter (0x5DXX second byte define parameter ID) and return all user
 | 
			
		||||
                                  // data (packet content: ???)
 | 
			
		||||
  SET_GROUP_PARAMETERS = 0x6001,  // Set group parameters to device (0x60XX second byte define parameter is group ID,
 | 
			
		||||
                                  // the only group mentioned in document is 1) and return all user data (packet
 | 
			
		||||
                                  // content: all values like in status packet)
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace hon_protocol
 | 
			
		||||
}  // namespace haier
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										33
									
								
								esphome/components/haier/logger_handler.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/components/haier/logger_handler.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
#include "logger_handler.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace haier {
 | 
			
		||||
 | 
			
		||||
void esphome_logger(haier_protocol::HaierLogLevel level, const char *tag, const char *message) {
 | 
			
		||||
  switch (level) {
 | 
			
		||||
    case haier_protocol::HaierLogLevel::LEVEL_ERROR:
 | 
			
		||||
      esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, "%s", message);
 | 
			
		||||
      break;
 | 
			
		||||
    case haier_protocol::HaierLogLevel::LEVEL_WARNING:
 | 
			
		||||
      esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, "%s", message);
 | 
			
		||||
      break;
 | 
			
		||||
    case haier_protocol::HaierLogLevel::LEVEL_INFO:
 | 
			
		||||
      esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, "%s", message);
 | 
			
		||||
      break;
 | 
			
		||||
    case haier_protocol::HaierLogLevel::LEVEL_DEBUG:
 | 
			
		||||
      esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, "%s", message);
 | 
			
		||||
      break;
 | 
			
		||||
    case haier_protocol::HaierLogLevel::LEVEL_VERBOSE:
 | 
			
		||||
      esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, "%s", message);
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      // Just ignore everything else
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void init_haier_protocol_logging() { haier_protocol::set_log_handler(esphome::haier::esphome_logger); };
 | 
			
		||||
 | 
			
		||||
}  // namespace haier
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										14
									
								
								esphome/components/haier/logger_handler.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								esphome/components/haier/logger_handler.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
// HaierProtocol
 | 
			
		||||
#include <utils/haier_log.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace haier {
 | 
			
		||||
 | 
			
		||||
// This file is called in the code generated by python script
 | 
			
		||||
// Do not use it directly!
 | 
			
		||||
void init_haier_protocol_logging();
 | 
			
		||||
 | 
			
		||||
}  // namespace haier
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										457
									
								
								esphome/components/haier/smartair2_climate.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										457
									
								
								esphome/components/haier/smartair2_climate.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,457 @@
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include "esphome/components/climate/climate.h"
 | 
			
		||||
#include "esphome/components/uart/uart.h"
 | 
			
		||||
#include "smartair2_climate.h"
 | 
			
		||||
#include "smartair2_packet.h"
 | 
			
		||||
 | 
			
		||||
using namespace esphome::climate;
 | 
			
		||||
using namespace esphome::uart;
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace haier {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "haier.climate";
 | 
			
		||||
 | 
			
		||||
Smartair2Climate::Smartair2Climate()
 | 
			
		||||
    : last_status_message_(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]) {
 | 
			
		||||
  this->traits_.set_supported_presets({
 | 
			
		||||
      climate::CLIMATE_PRESET_NONE,
 | 
			
		||||
      climate::CLIMATE_PRESET_BOOST,
 | 
			
		||||
      climate::CLIMATE_PRESET_COMFORT,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_type, uint8_t message_type,
 | 
			
		||||
                                                               const uint8_t *data, size_t data_size) {
 | 
			
		||||
  haier_protocol::HandlerError result =
 | 
			
		||||
      this->answer_preprocess_(request_type, (uint8_t) smartair2_protocol::FrameType::CONTROL, message_type,
 | 
			
		||||
                               (uint8_t) smartair2_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
 | 
			
		||||
  if (result == haier_protocol::HandlerError::HANDLER_OK) {
 | 
			
		||||
    result = this->process_status_message_(data, data_size);
 | 
			
		||||
    if (result != haier_protocol::HandlerError::HANDLER_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result);
 | 
			
		||||
      this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
 | 
			
		||||
                                                                       : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
 | 
			
		||||
    } else {
 | 
			
		||||
      if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) {
 | 
			
		||||
        memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl));
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size,
 | 
			
		||||
                 sizeof(smartair2_protocol::HaierPacketControl));
 | 
			
		||||
      }
 | 
			
		||||
      if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) {
 | 
			
		||||
        ESP_LOGI(TAG, "First HVAC status received");
 | 
			
		||||
        this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
      } else if (this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) {
 | 
			
		||||
        this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
      } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) {
 | 
			
		||||
        this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
        this->set_force_send_control_(false);
 | 
			
		||||
        if (this->hvac_settings_.valid)
 | 
			
		||||
          this->hvac_settings_.reset();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return result;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
 | 
			
		||||
                                                                     : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Smartair2Climate::set_answers_handlers() {
 | 
			
		||||
  this->haier_protocol_.set_answer_handler(
 | 
			
		||||
      (uint8_t) (smartair2_protocol::FrameType::CONTROL),
 | 
			
		||||
      std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2,
 | 
			
		||||
                std::placeholders::_3, std::placeholders::_4));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Smartair2Climate::dump_config() {
 | 
			
		||||
  HaierClimateBase::dump_config();
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Protocol version: smartAir2");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
  switch (this->protocol_phase_) {
 | 
			
		||||
    case ProtocolPhases::SENDING_INIT_1:
 | 
			
		||||
      this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::WAITING_ANSWER_INIT_1:
 | 
			
		||||
    case ProtocolPhases::SENDING_INIT_2:
 | 
			
		||||
    case ProtocolPhases::WAITING_ANSWER_INIT_2:
 | 
			
		||||
    case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
 | 
			
		||||
    case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER:
 | 
			
		||||
      this->set_phase_(ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
 | 
			
		||||
    case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
 | 
			
		||||
    case ProtocolPhases::SENDING_SIGNAL_LEVEL:
 | 
			
		||||
    case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
 | 
			
		||||
      this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
 | 
			
		||||
      if (this->can_send_message() && this->is_protocol_initialisation_interval_exceded_(now)) {
 | 
			
		||||
        static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL,
 | 
			
		||||
                                                                 0x4D01);
 | 
			
		||||
        this->send_message_(STATUS_REQUEST, false);
 | 
			
		||||
        this->last_status_request_ = now;
 | 
			
		||||
        this->set_phase_(ProtocolPhases::WAITING_FIRST_STATUS_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_STATUS_REQUEST:
 | 
			
		||||
      if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
 | 
			
		||||
        static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL,
 | 
			
		||||
                                                                 0x4D01);
 | 
			
		||||
        this->send_message_(STATUS_REQUEST, false);
 | 
			
		||||
        this->last_status_request_ = now;
 | 
			
		||||
        this->set_phase_(ProtocolPhases::WAITING_STATUS_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_CONTROL:
 | 
			
		||||
      if (this->first_control_attempt_) {
 | 
			
		||||
        this->control_request_timestamp_ = now;
 | 
			
		||||
        this->first_control_attempt_ = false;
 | 
			
		||||
      }
 | 
			
		||||
      if (this->is_control_message_timeout_exceeded_(now)) {
 | 
			
		||||
        ESP_LOGW(TAG, "Sending control packet timeout!");
 | 
			
		||||
        this->set_force_send_control_(false);
 | 
			
		||||
        if (this->hvac_settings_.valid)
 | 
			
		||||
          this->hvac_settings_.reset();
 | 
			
		||||
        this->forced_request_status_ = true;
 | 
			
		||||
        this->forced_publish_ = true;
 | 
			
		||||
        this->set_phase_(ProtocolPhases::IDLE);
 | 
			
		||||
      } else if (this->can_send_message() && this->is_control_message_interval_exceeded_(
 | 
			
		||||
                                                 now))  // Using CONTROL_MESSAGES_INTERVAL_MS to speedup requests
 | 
			
		||||
      {
 | 
			
		||||
        haier_protocol::HaierMessage control_message = get_control_message();
 | 
			
		||||
        this->send_message_(control_message, false);
 | 
			
		||||
        ESP_LOGI(TAG, "Control packet sent");
 | 
			
		||||
        this->set_phase_(ProtocolPhases::WAITING_CONTROL_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_POWER_ON_COMMAND:
 | 
			
		||||
    case ProtocolPhases::SENDING_POWER_OFF_COMMAND:
 | 
			
		||||
      if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
 | 
			
		||||
        haier_protocol::HaierMessage power_cmd(
 | 
			
		||||
            (uint8_t) smartair2_protocol::FrameType::CONTROL,
 | 
			
		||||
            this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND ? 0x4D02 : 0x4D03);
 | 
			
		||||
        this->send_message_(power_cmd, false);
 | 
			
		||||
        this->set_phase_(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND
 | 
			
		||||
                             ? ProtocolPhases::WAITING_POWER_ON_ANSWER
 | 
			
		||||
                             : ProtocolPhases::WAITING_POWER_OFF_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER:
 | 
			
		||||
    case ProtocolPhases::WAITING_STATUS_ANSWER:
 | 
			
		||||
    case ProtocolPhases::WAITING_CONTROL_ANSWER:
 | 
			
		||||
    case ProtocolPhases::WAITING_POWER_ON_ANSWER:
 | 
			
		||||
    case ProtocolPhases::WAITING_POWER_OFF_ANSWER:
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::IDLE: {
 | 
			
		||||
      if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
 | 
			
		||||
        this->set_phase_(ProtocolPhases::SENDING_STATUS_REQUEST);
 | 
			
		||||
        this->forced_request_status_ = false;
 | 
			
		||||
      }
 | 
			
		||||
    } break;
 | 
			
		||||
    default:
 | 
			
		||||
      // Shouldn't get here
 | 
			
		||||
      ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_);
 | 
			
		||||
      this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
 | 
			
		||||
  uint8_t control_out_buffer[sizeof(smartair2_protocol::HaierPacketControl)];
 | 
			
		||||
  memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(smartair2_protocol::HaierPacketControl));
 | 
			
		||||
  smartair2_protocol::HaierPacketControl *out_data = (smartair2_protocol::HaierPacketControl *) control_out_buffer;
 | 
			
		||||
  out_data->cntrl = 0;
 | 
			
		||||
  if (this->hvac_settings_.valid) {
 | 
			
		||||
    HvacSettings climate_control;
 | 
			
		||||
    climate_control = this->hvac_settings_;
 | 
			
		||||
    if (climate_control.mode.has_value()) {
 | 
			
		||||
      switch (climate_control.mode.value()) {
 | 
			
		||||
        case CLIMATE_MODE_OFF:
 | 
			
		||||
          out_data->ac_power = 0;
 | 
			
		||||
          break;
 | 
			
		||||
 | 
			
		||||
        case CLIMATE_MODE_AUTO:
 | 
			
		||||
          out_data->ac_power = 1;
 | 
			
		||||
          out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO;
 | 
			
		||||
          out_data->fan_mode = this->other_modes_fan_speed_;
 | 
			
		||||
          break;
 | 
			
		||||
 | 
			
		||||
        case CLIMATE_MODE_HEAT:
 | 
			
		||||
          out_data->ac_power = 1;
 | 
			
		||||
          out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT;
 | 
			
		||||
          out_data->fan_mode = this->other_modes_fan_speed_;
 | 
			
		||||
          break;
 | 
			
		||||
 | 
			
		||||
        case CLIMATE_MODE_DRY:
 | 
			
		||||
          out_data->ac_power = 1;
 | 
			
		||||
          out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY;
 | 
			
		||||
          out_data->fan_mode = this->other_modes_fan_speed_;
 | 
			
		||||
          break;
 | 
			
		||||
 | 
			
		||||
        case CLIMATE_MODE_FAN_ONLY:
 | 
			
		||||
          out_data->ac_power = 1;
 | 
			
		||||
          out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::FAN;
 | 
			
		||||
          out_data->fan_mode = this->fan_mode_speed_;  // Auto doesn't work in fan only mode
 | 
			
		||||
          break;
 | 
			
		||||
 | 
			
		||||
        case CLIMATE_MODE_COOL:
 | 
			
		||||
          out_data->ac_power = 1;
 | 
			
		||||
          out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL;
 | 
			
		||||
          out_data->fan_mode = this->other_modes_fan_speed_;
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          ESP_LOGE("Control", "Unsupported climate mode");
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Set fan speed, if we are in fan mode, reject auto in fan mode
 | 
			
		||||
    if (climate_control.fan_mode.has_value()) {
 | 
			
		||||
      switch (climate_control.fan_mode.value()) {
 | 
			
		||||
        case CLIMATE_FAN_LOW:
 | 
			
		||||
          out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_LOW;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_FAN_MEDIUM:
 | 
			
		||||
          out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_MID;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_FAN_HIGH:
 | 
			
		||||
          out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_HIGH;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_FAN_AUTO:
 | 
			
		||||
          if (this->mode != CLIMATE_MODE_FAN_ONLY)  // if we are not in fan only mode
 | 
			
		||||
            out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_AUTO;
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          ESP_LOGE("Control", "Unsupported fan mode");
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Set swing mode
 | 
			
		||||
    if (climate_control.swing_mode.has_value()) {
 | 
			
		||||
      switch (climate_control.swing_mode.value()) {
 | 
			
		||||
        case CLIMATE_SWING_OFF:
 | 
			
		||||
          out_data->use_swing_bits = 0;
 | 
			
		||||
          out_data->swing_both = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_SWING_VERTICAL:
 | 
			
		||||
          out_data->swing_both = 0;
 | 
			
		||||
          out_data->vertical_swing = 1;
 | 
			
		||||
          out_data->horizontal_swing = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_SWING_HORIZONTAL:
 | 
			
		||||
          out_data->swing_both = 0;
 | 
			
		||||
          out_data->vertical_swing = 0;
 | 
			
		||||
          out_data->horizontal_swing = 1;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_SWING_BOTH:
 | 
			
		||||
          out_data->swing_both = 1;
 | 
			
		||||
          out_data->use_swing_bits = 0;
 | 
			
		||||
          out_data->vertical_swing = 0;
 | 
			
		||||
          out_data->horizontal_swing = 0;
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (climate_control.target_temperature.has_value()) {
 | 
			
		||||
      out_data->set_point =
 | 
			
		||||
          climate_control.target_temperature.value() - 16;  // set the temperature at our offset, subtract 16.
 | 
			
		||||
    }
 | 
			
		||||
    if (out_data->ac_power == 0) {
 | 
			
		||||
      // If AC is off - no presets alowed
 | 
			
		||||
      out_data->turbo_mode = 0;
 | 
			
		||||
      out_data->quiet_mode = 0;
 | 
			
		||||
    } else if (climate_control.preset.has_value()) {
 | 
			
		||||
      switch (climate_control.preset.value()) {
 | 
			
		||||
        case CLIMATE_PRESET_NONE:
 | 
			
		||||
          out_data->turbo_mode = 0;
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_BOOST:
 | 
			
		||||
          out_data->turbo_mode = 1;
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_COMFORT:
 | 
			
		||||
          out_data->turbo_mode = 0;
 | 
			
		||||
          out_data->quiet_mode = 1;
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          ESP_LOGE("Control", "Unsupported preset");
 | 
			
		||||
          out_data->turbo_mode = 0;
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  out_data->display_status = this->display_status_ ? 0 : 1;
 | 
			
		||||
  out_data->health_mode = this->health_mode_ ? 1 : 0;
 | 
			
		||||
  return haier_protocol::HaierMessage((uint8_t) smartair2_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer,
 | 
			
		||||
                                      sizeof(smartair2_protocol::HaierPacketControl));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
 | 
			
		||||
  if (size < sizeof(smartair2_protocol::HaierStatus))
 | 
			
		||||
    return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
 | 
			
		||||
  smartair2_protocol::HaierStatus packet;
 | 
			
		||||
  memcpy(&packet, packet_buffer, size);
 | 
			
		||||
  bool should_publish = false;
 | 
			
		||||
  {
 | 
			
		||||
    // Extra modes/presets
 | 
			
		||||
    optional<ClimatePreset> old_preset = this->preset;
 | 
			
		||||
    if (packet.control.turbo_mode != 0) {
 | 
			
		||||
      this->preset = CLIMATE_PRESET_BOOST;
 | 
			
		||||
    } else if (packet.control.quiet_mode != 0) {
 | 
			
		||||
      this->preset = CLIMATE_PRESET_COMFORT;
 | 
			
		||||
    } else {
 | 
			
		||||
      this->preset = CLIMATE_PRESET_NONE;
 | 
			
		||||
    }
 | 
			
		||||
    should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value());
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    // Target temperature
 | 
			
		||||
    float old_target_temperature = this->target_temperature;
 | 
			
		||||
    this->target_temperature = packet.control.set_point + 16.0f;
 | 
			
		||||
    should_publish = should_publish || (old_target_temperature != this->target_temperature);
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    // Current temperature
 | 
			
		||||
    float old_current_temperature = this->current_temperature;
 | 
			
		||||
    this->current_temperature = packet.control.room_temperature;
 | 
			
		||||
    should_publish = should_publish || (old_current_temperature != this->current_temperature);
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    // Fan mode
 | 
			
		||||
    optional<ClimateFanMode> old_fan_mode = this->fan_mode;
 | 
			
		||||
    // remember the fan speed we last had for climate vs fan
 | 
			
		||||
    if (packet.control.ac_mode == (uint8_t) smartair2_protocol::ConditioningMode::FAN) {
 | 
			
		||||
      if (packet.control.fan_mode != (uint8_t) smartair2_protocol::FanMode::FAN_AUTO)
 | 
			
		||||
        this->fan_mode_speed_ = packet.control.fan_mode;
 | 
			
		||||
    } else {
 | 
			
		||||
      this->other_modes_fan_speed_ = packet.control.fan_mode;
 | 
			
		||||
    }
 | 
			
		||||
    switch (packet.control.fan_mode) {
 | 
			
		||||
      case (uint8_t) smartair2_protocol::FanMode::FAN_AUTO:
 | 
			
		||||
        // Somtimes AC reports in fan only mode that fan speed is auto
 | 
			
		||||
        // but never accept this value back
 | 
			
		||||
        if (packet.control.ac_mode != (uint8_t) smartair2_protocol::ConditioningMode::FAN) {
 | 
			
		||||
          this->fan_mode = CLIMATE_FAN_AUTO;
 | 
			
		||||
        } else {
 | 
			
		||||
          should_publish = true;
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      case (uint8_t) smartair2_protocol::FanMode::FAN_MID:
 | 
			
		||||
        this->fan_mode = CLIMATE_FAN_MEDIUM;
 | 
			
		||||
        break;
 | 
			
		||||
      case (uint8_t) smartair2_protocol::FanMode::FAN_LOW:
 | 
			
		||||
        this->fan_mode = CLIMATE_FAN_LOW;
 | 
			
		||||
        break;
 | 
			
		||||
      case (uint8_t) smartair2_protocol::FanMode::FAN_HIGH:
 | 
			
		||||
        this->fan_mode = CLIMATE_FAN_HIGH;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
    should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value());
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    // Display status
 | 
			
		||||
    // should be before "Climate mode" because it is changing this->mode
 | 
			
		||||
    if (packet.control.ac_power != 0) {
 | 
			
		||||
      // if AC is off display status always ON so process it only when AC is on
 | 
			
		||||
      bool disp_status = packet.control.display_status == 0;
 | 
			
		||||
      if (disp_status != this->display_status_) {
 | 
			
		||||
        // Do something only if display status changed
 | 
			
		||||
        if (this->mode == CLIMATE_MODE_OFF) {
 | 
			
		||||
          // AC just turned on from remote need to turn off display
 | 
			
		||||
          this->set_force_send_control_(true);
 | 
			
		||||
        } else {
 | 
			
		||||
          this->display_status_ = disp_status;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    // Climate mode
 | 
			
		||||
    ClimateMode old_mode = this->mode;
 | 
			
		||||
    if (packet.control.ac_power == 0) {
 | 
			
		||||
      this->mode = CLIMATE_MODE_OFF;
 | 
			
		||||
    } else {
 | 
			
		||||
      // Check current hvac mode
 | 
			
		||||
      switch (packet.control.ac_mode) {
 | 
			
		||||
        case (uint8_t) smartair2_protocol::ConditioningMode::COOL:
 | 
			
		||||
          this->mode = CLIMATE_MODE_COOL;
 | 
			
		||||
          break;
 | 
			
		||||
        case (uint8_t) smartair2_protocol::ConditioningMode::HEAT:
 | 
			
		||||
          this->mode = CLIMATE_MODE_HEAT;
 | 
			
		||||
          break;
 | 
			
		||||
        case (uint8_t) smartair2_protocol::ConditioningMode::DRY:
 | 
			
		||||
          this->mode = CLIMATE_MODE_DRY;
 | 
			
		||||
          break;
 | 
			
		||||
        case (uint8_t) smartair2_protocol::ConditioningMode::FAN:
 | 
			
		||||
          this->mode = CLIMATE_MODE_FAN_ONLY;
 | 
			
		||||
          break;
 | 
			
		||||
        case (uint8_t) smartair2_protocol::ConditioningMode::AUTO:
 | 
			
		||||
          this->mode = CLIMATE_MODE_AUTO;
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    should_publish = should_publish || (old_mode != this->mode);
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    // Health mode
 | 
			
		||||
    bool old_health_mode = this->health_mode_;
 | 
			
		||||
    this->health_mode_ = packet.control.health_mode == 1;
 | 
			
		||||
    should_publish = should_publish || (old_health_mode != this->health_mode_);
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    // Swing mode
 | 
			
		||||
    ClimateSwingMode old_swing_mode = this->swing_mode;
 | 
			
		||||
    if (packet.control.swing_both == 0) {
 | 
			
		||||
      if (packet.control.vertical_swing != 0) {
 | 
			
		||||
        this->swing_mode = CLIMATE_SWING_VERTICAL;
 | 
			
		||||
      } else if (packet.control.horizontal_swing != 0) {
 | 
			
		||||
        this->swing_mode = CLIMATE_SWING_HORIZONTAL;
 | 
			
		||||
      } else {
 | 
			
		||||
        this->swing_mode = CLIMATE_SWING_OFF;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      swing_mode = CLIMATE_SWING_BOTH;
 | 
			
		||||
    }
 | 
			
		||||
    should_publish = should_publish || (old_swing_mode != this->swing_mode);
 | 
			
		||||
  }
 | 
			
		||||
  this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
 | 
			
		||||
  if (this->forced_publish_ || should_publish) {
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
    std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now();
 | 
			
		||||
#endif
 | 
			
		||||
    this->publish_state();
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
    ESP_LOGV(TAG, "Publish delay: %lld ms",
 | 
			
		||||
             std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() -
 | 
			
		||||
                                                                   _publish_start)
 | 
			
		||||
                 .count());
 | 
			
		||||
#endif
 | 
			
		||||
    this->forced_publish_ = false;
 | 
			
		||||
  }
 | 
			
		||||
  if (should_publish) {
 | 
			
		||||
    ESP_LOGI(TAG, "HVAC values changed");
 | 
			
		||||
  }
 | 
			
		||||
  esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
 | 
			
		||||
                  "HVAC Mode = 0x%X", packet.control.ac_mode);
 | 
			
		||||
  esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
 | 
			
		||||
                  "Fan speed Status = 0x%X", packet.control.fan_mode);
 | 
			
		||||
  esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
 | 
			
		||||
                  "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing);
 | 
			
		||||
  esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
 | 
			
		||||
                  "Vertical Swing Status = 0x%X", packet.control.vertical_swing);
 | 
			
		||||
  esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
 | 
			
		||||
                  "Set Point Status = 0x%X", packet.control.set_point);
 | 
			
		||||
  return haier_protocol::HandlerError::HANDLER_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Smartair2Climate::is_message_invalid(uint8_t message_type) {
 | 
			
		||||
  return message_type == (uint8_t) smartair2_protocol::FrameType::INVALID;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace haier
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										31
									
								
								esphome/components/haier/smartair2_climate.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								esphome/components/haier/smartair2_climate.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include "haier_base.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace haier {
 | 
			
		||||
 | 
			
		||||
class Smartair2Climate : public HaierClimateBase {
 | 
			
		||||
 public:
 | 
			
		||||
  Smartair2Climate();
 | 
			
		||||
  Smartair2Climate(const Smartair2Climate &) = delete;
 | 
			
		||||
  Smartair2Climate &operator=(const Smartair2Climate &) = delete;
 | 
			
		||||
  ~Smartair2Climate();
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void set_answers_handlers() override;
 | 
			
		||||
  void process_phase(std::chrono::steady_clock::time_point now) override;
 | 
			
		||||
  haier_protocol::HaierMessage get_control_message() override;
 | 
			
		||||
  bool is_message_invalid(uint8_t message_type) override;
 | 
			
		||||
  // Answers handlers
 | 
			
		||||
  haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data,
 | 
			
		||||
                                               size_t data_size);
 | 
			
		||||
  // Helper functions
 | 
			
		||||
  haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
 | 
			
		||||
  std::unique_ptr<uint8_t[]> last_status_message_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace haier
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										97
									
								
								esphome/components/haier/smartair2_packet.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								esphome/components/haier/smartair2_packet.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace haier {
 | 
			
		||||
namespace smartair2_protocol {
 | 
			
		||||
 | 
			
		||||
enum class ConditioningMode : uint8_t { AUTO = 0x00, COOL = 0x01, HEAT = 0x02, FAN = 0x03, DRY = 0x04 };
 | 
			
		||||
 | 
			
		||||
enum class FanMode : uint8_t { FAN_HIGH = 0x00, FAN_MID = 0x01, FAN_LOW = 0x02, FAN_AUTO = 0x03 };
 | 
			
		||||
 | 
			
		||||
struct HaierPacketControl {
 | 
			
		||||
  // Control bytes starts here
 | 
			
		||||
  // 10
 | 
			
		||||
  uint8_t : 8;  // Temperature high byte
 | 
			
		||||
  // 11
 | 
			
		||||
  uint8_t room_temperature;  // current room temperature 1°C step
 | 
			
		||||
  // 12
 | 
			
		||||
  uint8_t : 8;  // Humidity high byte
 | 
			
		||||
  // 13
 | 
			
		||||
  uint8_t room_humidity;  // Humidity 0%-100% with 1% step
 | 
			
		||||
  // 14
 | 
			
		||||
  uint8_t : 8;
 | 
			
		||||
  // 15
 | 
			
		||||
  uint8_t cntrl;  // In AC => ESP packets - 0x7F, in ESP => AC packets - 0x00
 | 
			
		||||
  // 16
 | 
			
		||||
  uint8_t : 8;
 | 
			
		||||
  // 17
 | 
			
		||||
  uint8_t : 8;
 | 
			
		||||
  // 18
 | 
			
		||||
  uint8_t : 8;
 | 
			
		||||
  // 19
 | 
			
		||||
  uint8_t : 8;
 | 
			
		||||
  // 20
 | 
			
		||||
  uint8_t : 8;
 | 
			
		||||
  // 21
 | 
			
		||||
  uint8_t ac_mode;  // See enum ConditioningMode
 | 
			
		||||
  // 22
 | 
			
		||||
  uint8_t : 8;
 | 
			
		||||
  // 23
 | 
			
		||||
  uint8_t fan_mode;  // See enum FanMode
 | 
			
		||||
  // 24
 | 
			
		||||
  uint8_t : 8;
 | 
			
		||||
  // 25
 | 
			
		||||
  uint8_t swing_both;  // If 1 - swing both direction, if 0 - horizontal_swing and vertical_swing define
 | 
			
		||||
                       // vertical/horizontal/off
 | 
			
		||||
  // 26
 | 
			
		||||
  uint8_t : 3;
 | 
			
		||||
  uint8_t use_fahrenheit : 1;
 | 
			
		||||
  uint8_t : 3;
 | 
			
		||||
  uint8_t lock_remote : 1;  // Disable remote
 | 
			
		||||
  // 27
 | 
			
		||||
  uint8_t ac_power : 1;  // Is ac on or off
 | 
			
		||||
  uint8_t : 2;
 | 
			
		||||
  uint8_t health_mode : 1;  // Health mode on or off
 | 
			
		||||
  uint8_t compressor : 1;   // Compressor on or off ???
 | 
			
		||||
  uint8_t : 1;
 | 
			
		||||
  uint8_t ten_degree : 1;  // 10 degree status (only work in heat mode)
 | 
			
		||||
  uint8_t : 0;
 | 
			
		||||
  // 28
 | 
			
		||||
  uint8_t : 8;
 | 
			
		||||
  // 29
 | 
			
		||||
  uint8_t use_swing_bits : 1;    // Indicate if horizontal_swing and vertical_swing should be used
 | 
			
		||||
  uint8_t turbo_mode : 1;        // Turbo mode
 | 
			
		||||
  uint8_t quiet_mode : 1;        // Sleep mode
 | 
			
		||||
  uint8_t horizontal_swing : 1;  // Horizontal swing (if swing_both == 0)
 | 
			
		||||
  uint8_t vertical_swing : 1;    // Vertical swing (if swing_both == 0) if vertical_swing and horizontal_swing both 0 =>
 | 
			
		||||
                                 // swing off
 | 
			
		||||
  uint8_t display_status : 1;    // Led on or off
 | 
			
		||||
  uint8_t : 0;
 | 
			
		||||
  // 30
 | 
			
		||||
  uint8_t : 8;
 | 
			
		||||
  // 31
 | 
			
		||||
  uint8_t : 8;
 | 
			
		||||
  // 32
 | 
			
		||||
  uint8_t : 8;  // Target temperature high byte
 | 
			
		||||
  // 33
 | 
			
		||||
  uint8_t set_point;  // Target temperature with 16°C offset, 1°C step
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct HaierStatus {
 | 
			
		||||
  uint16_t subcommand;
 | 
			
		||||
  HaierPacketControl control;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class FrameType : uint8_t {
 | 
			
		||||
  CONTROL = 0x01,
 | 
			
		||||
  STATUS = 0x02,
 | 
			
		||||
  INVALID = 0x03,
 | 
			
		||||
  CONFIRM = 0x05,
 | 
			
		||||
  GET_DEVICE_VERSION = 0x61,
 | 
			
		||||
  REPORT_NETWORK_STATUS = 0xF7,
 | 
			
		||||
  NO_COMMAND = 0xFF,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace smartair2_protocol
 | 
			
		||||
}  // namespace haier
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -14,6 +14,14 @@ ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len
 | 
			
		||||
  return bus_->read(address_, data, len);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) {
 | 
			
		||||
  a_register = convert_big_endian(a_register);
 | 
			
		||||
  ErrorCode const err = this->write(reinterpret_cast<const uint8_t *>(&a_register), 2, stop);
 | 
			
		||||
  if (err != ERROR_OK)
 | 
			
		||||
    return err;
 | 
			
		||||
  return bus_->read(address_, data, len);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) {
 | 
			
		||||
  WriteBuffer buffers[2];
 | 
			
		||||
  buffers[0].data = &a_register;
 | 
			
		||||
@@ -23,6 +31,16 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz
 | 
			
		||||
  return bus_->writev(address_, buffers, 2, stop);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) {
 | 
			
		||||
  a_register = convert_big_endian(a_register);
 | 
			
		||||
  WriteBuffer buffers[2];
 | 
			
		||||
  buffers[0].data = reinterpret_cast<const uint8_t *>(&a_register);
 | 
			
		||||
  buffers[0].len = 2;
 | 
			
		||||
  buffers[1].data = data;
 | 
			
		||||
  buffers[1].len = len;
 | 
			
		||||
  return bus_->writev(address_, buffers, 2, stop);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) {
 | 
			
		||||
  if (read_register(a_register, reinterpret_cast<uint8_t *>(data), len * 2) != ERROR_OK)
 | 
			
		||||
    return false;
 | 
			
		||||
@@ -60,5 +78,26 @@ uint8_t I2CRegister::get() const {
 | 
			
		||||
  return value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
I2CRegister16 &I2CRegister16::operator=(uint8_t value) {
 | 
			
		||||
  this->parent_->write_register16(this->register_, &value, 1);
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
I2CRegister16 &I2CRegister16::operator&=(uint8_t value) {
 | 
			
		||||
  value &= get();
 | 
			
		||||
  this->parent_->write_register16(this->register_, &value, 1);
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
I2CRegister16 &I2CRegister16::operator|=(uint8_t value) {
 | 
			
		||||
  value |= get();
 | 
			
		||||
  this->parent_->write_register16(this->register_, &value, 1);
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint8_t I2CRegister16::get() const {
 | 
			
		||||
  uint8_t value = 0x00;
 | 
			
		||||
  this->parent_->read_register16(this->register_, &value, 1);
 | 
			
		||||
  return value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace i2c
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user