mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Merge branch 'dev' into bump-2023.12.0b1
This commit is contained in:
		| @@ -37,6 +37,7 @@ | |||||||
|           "!secret scalar", |           "!secret scalar", | ||||||
|           "!lambda scalar", |           "!lambda scalar", | ||||||
|           "!extend scalar", |           "!extend scalar", | ||||||
|  |           "!remove scalar", | ||||||
|           "!include_dir_named scalar", |           "!include_dir_named scalar", | ||||||
|           "!include_dir_list scalar", |           "!include_dir_list scalar", | ||||||
|           "!include_dir_merge_list scalar", |           "!include_dir_merge_list scalar", | ||||||
|   | |||||||
							
								
								
									
										97
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | |||||||
|  | name: Build Image | ||||||
|  | inputs: | ||||||
|  |   platform: | ||||||
|  |     description: "Platform to build for" | ||||||
|  |     required: true | ||||||
|  |     example: "linux/amd64" | ||||||
|  |   target: | ||||||
|  |     description: "Target to build" | ||||||
|  |     required: true | ||||||
|  |     example: "docker" | ||||||
|  |   baseimg: | ||||||
|  |     description: "Base image type" | ||||||
|  |     required: true | ||||||
|  |     example: "docker" | ||||||
|  |   suffix: | ||||||
|  |     description: "Suffix to add to tags" | ||||||
|  |     required: true | ||||||
|  |   version: | ||||||
|  |     description: "Version to build" | ||||||
|  |     required: true | ||||||
|  |     example: "2023.12.0" | ||||||
|  | runs: | ||||||
|  |   using: "composite" | ||||||
|  |   steps: | ||||||
|  |     - name: Generate short tags | ||||||
|  |       id: tags | ||||||
|  |       shell: bash | ||||||
|  |       run: | | ||||||
|  |         output=$(docker/generate_tags.py \ | ||||||
|  |           --tag "${{ inputs.version }}" \ | ||||||
|  |           --suffix "${{ inputs.suffix }}") | ||||||
|  |         echo $output | ||||||
|  |         for l in $output; do | ||||||
|  |           echo $l >> $GITHUB_OUTPUT | ||||||
|  |         done | ||||||
|  |  | ||||||
|  |     - name: Build and push to ghcr by digest | ||||||
|  |       id: build-ghcr | ||||||
|  |       uses: docker/build-push-action@v5.0.0 | ||||||
|  |       with: | ||||||
|  |         context: . | ||||||
|  |         file: ./docker/Dockerfile | ||||||
|  |         platforms: ${{ inputs.platform }} | ||||||
|  |         target: ${{ inputs.target }} | ||||||
|  |         cache-from: type=gha | ||||||
|  |         cache-to: type=gha,mode=max | ||||||
|  |         build-args: | | ||||||
|  |           BASEIMGTYPE=${{ inputs.baseimg }} | ||||||
|  |           BUILD_VERSION=${{ inputs.version }} | ||||||
|  |         outputs: | | ||||||
|  |           type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true | ||||||
|  |  | ||||||
|  |     - name: Export ghcr digests | ||||||
|  |       shell: bash | ||||||
|  |       run: | | ||||||
|  |         mkdir -p /tmp/digests/${{ inputs.target }}/ghcr | ||||||
|  |         digest="${{ steps.build-ghcr.outputs.digest }}" | ||||||
|  |         touch "/tmp/digests/${{ inputs.target }}/ghcr/${digest#sha256:}" | ||||||
|  |  | ||||||
|  |     - name: Upload ghcr digest | ||||||
|  |       uses: actions/upload-artifact@v3.1.3 | ||||||
|  |       with: | ||||||
|  |         name: digests-${{ inputs.target }}-ghcr | ||||||
|  |         path: /tmp/digests/${{ inputs.target }}/ghcr/* | ||||||
|  |         if-no-files-found: error | ||||||
|  |         retention-days: 1 | ||||||
|  |  | ||||||
|  |     - name: Build and push to dockerhub by digest | ||||||
|  |       id: build-dockerhub | ||||||
|  |       uses: docker/build-push-action@v5.0.0 | ||||||
|  |       with: | ||||||
|  |         context: . | ||||||
|  |         file: ./docker/Dockerfile | ||||||
|  |         platforms: ${{ inputs.platform }} | ||||||
|  |         target: ${{ inputs.target }} | ||||||
|  |         cache-from: type=gha | ||||||
|  |         cache-to: type=gha,mode=max | ||||||
|  |         build-args: | | ||||||
|  |           BASEIMGTYPE=${{ inputs.baseimg }} | ||||||
|  |           BUILD_VERSION=${{ inputs.version }} | ||||||
|  |         outputs: | | ||||||
|  |           type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true | ||||||
|  |  | ||||||
|  |     - name: Export dockerhub digests | ||||||
|  |       shell: bash | ||||||
|  |       run: | | ||||||
|  |         mkdir -p /tmp/digests/${{ inputs.target }}/dockerhub | ||||||
|  |         digest="${{ steps.build-dockerhub.outputs.digest }}" | ||||||
|  |         touch "/tmp/digests/${{ inputs.target }}/dockerhub/${digest#sha256:}" | ||||||
|  |  | ||||||
|  |     - name: Upload dockerhub digest | ||||||
|  |       uses: actions/upload-artifact@v3.1.3 | ||||||
|  |       with: | ||||||
|  |         name: digests-${{ inputs.target }}-dockerhub | ||||||
|  |         path: /tmp/digests/${{ inputs.target }}/dockerhub/* | ||||||
|  |         if-no-files-found: error | ||||||
|  |         retention-days: 1 | ||||||
							
								
								
									
										2
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ runs: | |||||||
|   steps: |   steps: | ||||||
|     - name: Set up Python ${{ inputs.python-version }} |     - name: Set up Python ${{ inputs.python-version }} | ||||||
|       id: python |       id: python | ||||||
|       uses: actions/setup-python@v4.7.0 |       uses: actions/setup-python@v5.0.0 | ||||||
|       with: |       with: | ||||||
|         python-version: ${{ inputs.python-version }} |         python-version: ${{ inputs.python-version }} | ||||||
|     - name: Restore Python virtual environment |     - name: Restore Python virtual environment | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							| @@ -42,7 +42,7 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4.1.1 |       - uses: actions/checkout@v4.1.1 | ||||||
|       - name: Set up Python |       - name: Set up Python | ||||||
|         uses: actions/setup-python@v4.7.1 |         uses: actions/setup-python@v5.0.0 | ||||||
|         with: |         with: | ||||||
|           python-version: "3.9" |           python-version: "3.9" | ||||||
|       - name: Set up Docker Buildx |       - name: Set up Docker Buildx | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -40,7 +40,7 @@ jobs: | |||||||
|         run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT |         run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT | ||||||
|       - name: Set up Python ${{ env.DEFAULT_PYTHON }} |       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||||
|         id: python |         id: python | ||||||
|         uses: actions/setup-python@v4.7.1 |         uses: actions/setup-python@v5.0.0 | ||||||
|         with: |         with: | ||||||
|           python-version: ${{ env.DEFAULT_PYTHON }} |           python-version: ${{ env.DEFAULT_PYTHON }} | ||||||
|       - name: Restore Python virtual environment |       - name: Restore Python virtual environment | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							| @@ -18,7 +18,7 @@ jobs: | |||||||
|   lock: |   lock: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: dessant/lock-threads@v4.0.1 |       - uses: dessant/lock-threads@v5.0.1 | ||||||
|         with: |         with: | ||||||
|           pr-inactive-days: "1" |           pr-inactive-days: "1" | ||||||
|           pr-lock-reason: "" |           pr-lock-reason: "" | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/needs-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/needs-docs.yml
									
									
									
									
										vendored
									
									
								
							| @@ -10,7 +10,7 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Check for needs-docs label |       - name: Check for needs-docs label | ||||||
|         uses: actions/github-script@v6.4.1 |         uses: actions/github-script@v7.0.1 | ||||||
|         with: |         with: | ||||||
|           script: | |           script: | | ||||||
|             const { data: labels } = await github.rest.issues.listLabelsOnIssue({ |             const { data: labels } = await github.rest.issues.listLabelsOnIssue({ | ||||||
|   | |||||||
							
								
								
									
										136
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										136
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -45,7 +45,7 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4.1.1 |       - uses: actions/checkout@v4.1.1 | ||||||
|       - name: Set up Python |       - name: Set up Python | ||||||
|         uses: actions/setup-python@v4.7.1 |         uses: actions/setup-python@v5.0.0 | ||||||
|         with: |         with: | ||||||
|           python-version: "3.x" |           python-version: "3.x" | ||||||
|       - name: Set up python environment |       - name: Set up python environment | ||||||
| @@ -63,40 +63,31 @@ jobs: | |||||||
|         run: twine upload dist/* |         run: twine upload dist/* | ||||||
|  |  | ||||||
|   deploy-docker: |   deploy-docker: | ||||||
|     name: Build and publish ESPHome ${{ matrix.image.title}} |     name: Build ESPHome ${{ matrix.platform }} | ||||||
|     if: github.repository == 'esphome/esphome' |     if: github.repository == 'esphome/esphome' | ||||||
|     permissions: |     permissions: | ||||||
|       contents: read |       contents: read | ||||||
|       packages: write |       packages: write | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     continue-on-error: ${{ matrix.image.title == 'lint' }} |  | ||||||
|     needs: [init] |     needs: [init] | ||||||
|     strategy: |     strategy: | ||||||
|       fail-fast: false |       fail-fast: false | ||||||
|       matrix: |       matrix: | ||||||
|         image: |         platform: | ||||||
|           - title: "ha-addon" |           - linux/amd64 | ||||||
|             suffix: "hassio" |           - linux/arm/v7 | ||||||
|             target: "hassio" |           - linux/arm64 | ||||||
|             baseimg: "hassio" |  | ||||||
|           - title: "docker" |  | ||||||
|             suffix: "" |  | ||||||
|             target: "docker" |  | ||||||
|             baseimg: "docker" |  | ||||||
|           - title: "lint" |  | ||||||
|             suffix: "lint" |  | ||||||
|             target: "lint" |  | ||||||
|             baseimg: "docker" |  | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v4.1.1 |       - uses: actions/checkout@v4.1.1 | ||||||
|       - name: Set up Python |       - name: Set up Python | ||||||
|         uses: actions/setup-python@v4.7.1 |         uses: actions/setup-python@v5.0.0 | ||||||
|         with: |         with: | ||||||
|           python-version: "3.9" |           python-version: "3.9" | ||||||
|  |  | ||||||
|       - name: Set up Docker Buildx |       - name: Set up Docker Buildx | ||||||
|         uses: docker/setup-buildx-action@v3.0.0 |         uses: docker/setup-buildx-action@v3.0.0 | ||||||
|       - name: Set up QEMU |       - name: Set up QEMU | ||||||
|  |         if: matrix.platform != 'linux/amd64' | ||||||
|         uses: docker/setup-qemu-action@v3.0.0 |         uses: docker/setup-qemu-action@v3.0.0 | ||||||
|  |  | ||||||
|       - name: Log in to docker hub |       - name: Log in to docker hub | ||||||
| @@ -111,37 +102,108 @@ jobs: | |||||||
|           username: ${{ github.actor }} |           username: ${{ github.actor }} | ||||||
|           password: ${{ secrets.GITHUB_TOKEN }} |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |  | ||||||
|  |       - name: Build docker | ||||||
|  |         uses: ./.github/actions/build-image | ||||||
|  |         with: | ||||||
|  |           platform: ${{ matrix.platform }} | ||||||
|  |           target: docker | ||||||
|  |           baseimg: docker | ||||||
|  |           suffix: "" | ||||||
|  |           version: ${{ needs.init.outputs.tag }} | ||||||
|  |  | ||||||
|  |       - name: Build ha-addon | ||||||
|  |         uses: ./.github/actions/build-image | ||||||
|  |         with: | ||||||
|  |           platform: ${{ matrix.platform }} | ||||||
|  |           target: hassio | ||||||
|  |           baseimg: hassio | ||||||
|  |           suffix: "hassio" | ||||||
|  |           version: ${{ needs.init.outputs.tag }} | ||||||
|  |  | ||||||
|  |       - name: Build lint | ||||||
|  |         uses: ./.github/actions/build-image | ||||||
|  |         with: | ||||||
|  |           platform: ${{ matrix.platform }} | ||||||
|  |           target: lint | ||||||
|  |           baseimg: docker | ||||||
|  |           suffix: lint | ||||||
|  |           version: ${{ needs.init.outputs.tag }} | ||||||
|  |  | ||||||
|  |   deploy-manifest: | ||||||
|  |     name: Publish ESPHome ${{ matrix.image.title }} to ${{ matrix.registry }} | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: | ||||||
|  |       - init | ||||||
|  |       - deploy-docker | ||||||
|  |     if: github.repository == 'esphome/esphome' | ||||||
|  |     permissions: | ||||||
|  |       contents: read | ||||||
|  |       packages: write | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         image: | ||||||
|  |           - title: "ha-addon" | ||||||
|  |             target: "hassio" | ||||||
|  |             suffix: "hassio" | ||||||
|  |           - title: "docker" | ||||||
|  |             target: "docker" | ||||||
|  |             suffix: "" | ||||||
|  |           - title: "lint" | ||||||
|  |             target: "lint" | ||||||
|  |             suffix: "lint" | ||||||
|  |         registry: | ||||||
|  |           - ghcr | ||||||
|  |           - dockerhub | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v4.1.1 | ||||||
|  |       - name: Download digests | ||||||
|  |         uses: actions/download-artifact@v3.0.2 | ||||||
|  |         with: | ||||||
|  |           name: digests-${{ matrix.image.target }}-${{ matrix.registry }} | ||||||
|  |           path: /tmp/digests | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v3.0.0 | ||||||
|  |  | ||||||
|  |       - name: Log in to docker hub | ||||||
|  |         if: matrix.registry == 'dockerhub' | ||||||
|  |         uses: docker/login-action@v3.0.0 | ||||||
|  |         with: | ||||||
|  |           username: ${{ secrets.DOCKER_USER }} | ||||||
|  |           password: ${{ secrets.DOCKER_PASSWORD }} | ||||||
|  |       - name: Log in to the GitHub container registry | ||||||
|  |         if: matrix.registry == 'ghcr' | ||||||
|  |         uses: docker/login-action@v3.0.0 | ||||||
|  |         with: | ||||||
|  |           registry: ghcr.io | ||||||
|  |           username: ${{ github.actor }} | ||||||
|  |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |  | ||||||
|       - name: Generate short tags |       - name: Generate short tags | ||||||
|         id: tags |         id: tags | ||||||
|         run: | |         run: | | ||||||
|           docker/generate_tags.py \ |           output=$(docker/generate_tags.py \ | ||||||
|             --tag "${{ needs.init.outputs.tag }}" \ |             --tag "${{ needs.init.outputs.tag }}" \ | ||||||
|             --suffix "${{ matrix.image.suffix }}" |             --suffix "${{ matrix.image.suffix }}" \ | ||||||
|  |             --registry "${{ matrix.registry }}") | ||||||
|  |           echo $output | ||||||
|  |           for l in $output; do | ||||||
|  |             echo $l >> $GITHUB_OUTPUT | ||||||
|  |           done | ||||||
|  |  | ||||||
|       - name: Build and push |       - name: Create manifest list and push | ||||||
|         uses: docker/build-push-action@v5.0.0 |         working-directory: /tmp/digests | ||||||
|         with: |         run: | | ||||||
|           context: . |           docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \ | ||||||
|           file: ./docker/Dockerfile |             $(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *) | ||||||
|           platforms: linux/amd64,linux/arm/v7,linux/arm64 |  | ||||||
|           target: ${{ matrix.image.target }} |  | ||||||
|           push: true |  | ||||||
|           # yamllint disable rule:line-length |  | ||||||
|           cache-from: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }} |  | ||||||
|           cache-to: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }},mode=max |  | ||||||
|           # yamllint enable rule:line-length |  | ||||||
|           tags: ${{ steps.tags.outputs.tags }} |  | ||||||
|           build-args: | |  | ||||||
|             BASEIMGTYPE=${{ matrix.image.baseimg }} |  | ||||||
|             BUILD_VERSION=${{ needs.init.outputs.tag }} |  | ||||||
|  |  | ||||||
|   deploy-ha-addon-repo: |   deploy-ha-addon-repo: | ||||||
|     if: github.repository == 'esphome/esphome' && github.event_name == 'release' |     if: github.repository == 'esphome/esphome' && github.event_name == 'release' | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     needs: [deploy-docker] |     needs: [deploy-manifest] | ||||||
|     steps: |     steps: | ||||||
|       - name: Trigger Workflow |       - name: Trigger Workflow | ||||||
|         uses: actions/github-script@v6.4.1 |         uses: actions/github-script@v7.0.1 | ||||||
|         with: |         with: | ||||||
|           github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} |           github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} | ||||||
|           script: | |           script: | | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -18,7 +18,7 @@ jobs: | |||||||
|   stale: |   stale: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/stale@v8.0.0 |       - uses: actions/stale@v9.0.0 | ||||||
|         with: |         with: | ||||||
|           days-before-pr-stale: 90 |           days-before-pr-stale: 90 | ||||||
|           days-before-pr-close: 7 |           days-before-pr-close: 7 | ||||||
| @@ -38,7 +38,7 @@ jobs: | |||||||
|   close-issues: |   close-issues: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/stale@v8.0.0 |       - uses: actions/stale@v9.0.0 | ||||||
|         with: |         with: | ||||||
|           days-before-pr-stale: -1 |           days-before-pr-stale: -1 | ||||||
|           days-before-pr-close: -1 |           days-before-pr-close: -1 | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							| @@ -22,7 +22,7 @@ jobs: | |||||||
|           path: lib/home-assistant |           path: lib/home-assistant | ||||||
|  |  | ||||||
|       - name: Setup Python |       - name: Setup Python | ||||||
|         uses: actions/setup-python@v4.7.1 |         uses: actions/setup-python@v5.0.0 | ||||||
|         with: |         with: | ||||||
|           python-version: 3.11 |           python-version: 3.11 | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/yaml-lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/yaml-lint.yml
									
									
									
									
										vendored
									
									
								
							| @@ -19,4 +19,4 @@ jobs: | |||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@v4.1.1 |         uses: actions/checkout@v4.1.1 | ||||||
|       - name: Run yamllint |       - name: Run yamllint | ||||||
|         uses: frenck/action-yamllint@v1.4.1 |         uses: frenck/action-yamllint@v1.4.2 | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
| # See https://pre-commit.com/hooks.html for more hooks | # See https://pre-commit.com/hooks.html for more hooks | ||||||
| repos: | repos: | ||||||
|   - repo: https://github.com/psf/black-pre-commit-mirror |   - repo: https://github.com/psf/black-pre-commit-mirror | ||||||
|     rev: 23.10.1 |     rev: 23.12.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: black |       - id: black | ||||||
|         args: |         args: | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								CODEOWNERS
									
									
									
									
									
								
							| @@ -12,6 +12,7 @@ esphome/core/* @esphome/core | |||||||
|  |  | ||||||
| # Integrations | # Integrations | ||||||
| esphome/components/a01nyub/* @MrSuicideParrot | esphome/components/a01nyub/* @MrSuicideParrot | ||||||
|  | esphome/components/a02yyuw/* @TH-Braemer | ||||||
| esphome/components/absolute_humidity/* @DAVe3283 | esphome/components/absolute_humidity/* @DAVe3283 | ||||||
| esphome/components/ac_dimmer/* @glmnet | esphome/components/ac_dimmer/* @glmnet | ||||||
| esphome/components/adc/* @esphome/core | esphome/components/adc/* @esphome/core | ||||||
| @@ -88,8 +89,9 @@ esphome/components/ds1307/* @badbadc0ffee | |||||||
| esphome/components/dsmr/* @glmnet @zuidwijk | esphome/components/dsmr/* @glmnet @zuidwijk | ||||||
| esphome/components/duty_time/* @dudanov | esphome/components/duty_time/* @dudanov | ||||||
| esphome/components/ee895/* @Stock-M | esphome/components/ee895/* @Stock-M | ||||||
| esphome/components/ektf2232/* @jesserockz | esphome/components/ektf2232/touchscreen/* @jesserockz | ||||||
| esphome/components/emc2101/* @ellull | esphome/components/emc2101/* @ellull | ||||||
|  | esphome/components/ens160/* @vincentscode | ||||||
| esphome/components/ens210/* @itn3rd77 | esphome/components/ens210/* @itn3rd77 | ||||||
| esphome/components/esp32/* @esphome/core | esphome/components/esp32/* @esphome/core | ||||||
| esphome/components/esp32_ble/* @Rapsssito @jesserockz | esphome/components/esp32_ble/* @Rapsssito @jesserockz | ||||||
| @@ -109,19 +111,24 @@ esphome/components/fastled_base/* @OttoWinter | |||||||
| esphome/components/feedback/* @ianchi | esphome/components/feedback/* @ianchi | ||||||
| esphome/components/fingerprint_grow/* @OnFreund @loongyh | esphome/components/fingerprint_grow/* @OnFreund @loongyh | ||||||
| esphome/components/fs3000/* @kahrendt | esphome/components/fs3000/* @kahrendt | ||||||
|  | esphome/components/ft5x06/* @clydebarrow | ||||||
|  | esphome/components/ft63x6/* @gpambrozio | ||||||
| esphome/components/gcja5/* @gcormier | esphome/components/gcja5/* @gcormier | ||||||
| esphome/components/globals/* @esphome/core | esphome/components/globals/* @esphome/core | ||||||
| esphome/components/gp8403/* @jesserockz | esphome/components/gp8403/* @jesserockz | ||||||
| esphome/components/gpio/* @esphome/core | esphome/components/gpio/* @esphome/core | ||||||
| esphome/components/gps/* @coogle | esphome/components/gps/* @coogle | ||||||
| esphome/components/graph/* @synco | esphome/components/graph/* @synco | ||||||
|  | esphome/components/graphical_display_menu/* @MrMDavidson | ||||||
| esphome/components/gree/* @orestismers | esphome/components/gree/* @orestismers | ||||||
| esphome/components/grove_tb6612fng/* @max246 | esphome/components/grove_tb6612fng/* @max246 | ||||||
| esphome/components/growatt_solar/* @leeuwte | esphome/components/growatt_solar/* @leeuwte | ||||||
|  | esphome/components/gt911/* @clydebarrow @jesserockz | ||||||
| esphome/components/haier/* @paveldn | esphome/components/haier/* @paveldn | ||||||
| esphome/components/havells_solar/* @sourabhjaiswal | esphome/components/havells_solar/* @sourabhjaiswal | ||||||
| esphome/components/hbridge/fan/* @WeekendWarrior | esphome/components/hbridge/fan/* @WeekendWarrior | ||||||
| esphome/components/hbridge/light/* @DotNetDann | esphome/components/hbridge/light/* @DotNetDann | ||||||
|  | esphome/components/he60r/* @clydebarrow | ||||||
| esphome/components/heatpumpir/* @rob-deutsch | esphome/components/heatpumpir/* @rob-deutsch | ||||||
| esphome/components/hitachi_ac424/* @sourabhjaiswal | esphome/components/hitachi_ac424/* @sourabhjaiswal | ||||||
| esphome/components/hm3301/* @freekode | esphome/components/hm3301/* @freekode | ||||||
| @@ -233,11 +240,17 @@ esphome/components/pmwcs3/* @SeByDocKy | |||||||
| esphome/components/pn532/* @OttoWinter @jesserockz | esphome/components/pn532/* @OttoWinter @jesserockz | ||||||
| esphome/components/pn532_i2c/* @OttoWinter @jesserockz | esphome/components/pn532_i2c/* @OttoWinter @jesserockz | ||||||
| esphome/components/pn532_spi/* @OttoWinter @jesserockz | esphome/components/pn532_spi/* @OttoWinter @jesserockz | ||||||
|  | esphome/components/pn7150/* @jesserockz @kbx81 | ||||||
|  | esphome/components/pn7150_i2c/* @jesserockz @kbx81 | ||||||
|  | esphome/components/pn7160/* @jesserockz @kbx81 | ||||||
|  | esphome/components/pn7160_i2c/* @jesserockz @kbx81 | ||||||
|  | esphome/components/pn7160_spi/* @jesserockz @kbx81 | ||||||
| esphome/components/power_supply/* @esphome/core | esphome/components/power_supply/* @esphome/core | ||||||
| esphome/components/preferences/* @esphome/core | esphome/components/preferences/* @esphome/core | ||||||
| esphome/components/psram/* @esphome/core | esphome/components/psram/* @esphome/core | ||||||
| esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter | esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter | ||||||
| esphome/components/pvvx_mithermometer/* @pasiz | esphome/components/pvvx_mithermometer/* @pasiz | ||||||
|  | esphome/components/pylontech/* @functionpointer | ||||||
| esphome/components/qmp6988/* @andrewpc | esphome/components/qmp6988/* @andrewpc | ||||||
| esphome/components/qr_code/* @wjtje | esphome/components/qr_code/* @wjtje | ||||||
| esphome/components/qwiic_pir/* @kahrendt | esphome/components/qwiic_pir/* @kahrendt | ||||||
| @@ -326,7 +339,7 @@ esphome/components/tmp1075/* @sybrenstuvel | |||||||
| esphome/components/tmp117/* @Azimath | esphome/components/tmp117/* @Azimath | ||||||
| esphome/components/tof10120/* @wstrzalka | esphome/components/tof10120/* @wstrzalka | ||||||
| esphome/components/toshiba/* @kbx81 | esphome/components/toshiba/* @kbx81 | ||||||
| esphome/components/touchscreen/* @jesserockz | esphome/components/touchscreen/* @jesserockz @nielsnl68 | ||||||
| esphome/components/tsl2591/* @wjcarpenter | esphome/components/tsl2591/* @wjcarpenter | ||||||
| esphome/components/tt21100/* @kroimon | esphome/components/tt21100/* @kroimon | ||||||
| esphome/components/tuya/binary_sensor/* @jesserockz | esphome/components/tuya/binary_sensor/* @jesserockz | ||||||
| @@ -359,6 +372,6 @@ esphome/components/xiaomi_mhoc303/* @drug123 | |||||||
| esphome/components/xiaomi_mhoc401/* @vevsvevs | esphome/components/xiaomi_mhoc401/* @vevsvevs | ||||||
| esphome/components/xiaomi_rtcgq02lm/* @jesserockz | esphome/components/xiaomi_rtcgq02lm/* @jesserockz | ||||||
| esphome/components/xl9535/* @mreditor97 | esphome/components/xl9535/* @mreditor97 | ||||||
| esphome/components/xpt2046/* @nielsnl68 @numo68 | esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 | ||||||
| esphome/components/zhlt01/* @cfeenstra1024 | esphome/components/zhlt01/* @cfeenstra1024 | ||||||
| esphome/components/zio_ultrasonic/* @kahrendt | esphome/components/zio_ultrasonic/* @kahrendt | ||||||
|   | |||||||
| @@ -10,5 +10,3 @@ Things to note when contributing: | |||||||
|    for more information. |    for more information. | ||||||
|  - Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files |  - Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files | ||||||
|    which checks if your new feature compiles correctly. |    which checks if your new feature compiles correctly. | ||||||
|  - Sometimes I will let pull requests linger because I'm not 100% sure about them. Please feel free to ping |  | ||||||
|    me after some time. |  | ||||||
|   | |||||||
| @@ -1,13 +1,14 @@ | |||||||
| #!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||||
| import re | import re | ||||||
| import os |  | ||||||
| import argparse | import argparse | ||||||
| import json |  | ||||||
|  |  | ||||||
| CHANNEL_DEV = "dev" | CHANNEL_DEV = "dev" | ||||||
| CHANNEL_BETA = "beta" | CHANNEL_BETA = "beta" | ||||||
| CHANNEL_RELEASE = "release" | CHANNEL_RELEASE = "release" | ||||||
|  |  | ||||||
|  | GHCR = "ghcr" | ||||||
|  | DOCKERHUB = "dockerhub" | ||||||
|  |  | ||||||
| parser = argparse.ArgumentParser() | parser = argparse.ArgumentParser() | ||||||
| parser.add_argument( | parser.add_argument( | ||||||
|     "--tag", |     "--tag", | ||||||
| @@ -21,21 +22,31 @@ parser.add_argument( | |||||||
|     required=True, |     required=True, | ||||||
|     help="The suffix of the tag.", |     help="The suffix of the tag.", | ||||||
| ) | ) | ||||||
|  | parser.add_argument( | ||||||
|  |     "--registry", | ||||||
|  |     type=str, | ||||||
|  |     choices=[GHCR, DOCKERHUB], | ||||||
|  |     required=False, | ||||||
|  |     action="append", | ||||||
|  |     help="The registry to build tags for.", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| def main(): | def main(): | ||||||
|     args = parser.parse_args() |     args = parser.parse_args() | ||||||
|  |  | ||||||
|     # detect channel from tag |     # detect channel from tag | ||||||
|     match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag) |     match = re.match(r"^(\d+\.\d+)(?:\.\d+)(?:(b\d+)|(-dev\d+))?$", args.tag) | ||||||
|     major_minor_version = None |     major_minor_version = None | ||||||
|     if match is None: |     if match is None:  # eg 2023.12.0-dev20231109-testbranch | ||||||
|  |         channel = None  # Ran with custom tag for a branch etc | ||||||
|  |     elif match.group(3) is not None:  # eg 2023.12.0-dev20231109 | ||||||
|         channel = CHANNEL_DEV |         channel = CHANNEL_DEV | ||||||
|     elif match.group(2) is None: |     elif match.group(2) is not None:  # eg 2023.12.0b1 | ||||||
|  |         channel = CHANNEL_BETA | ||||||
|  |     else:  # eg 2023.12.0 | ||||||
|         major_minor_version = match.group(1) |         major_minor_version = match.group(1) | ||||||
|         channel = CHANNEL_RELEASE |         channel = CHANNEL_RELEASE | ||||||
|     else: |  | ||||||
|         channel = CHANNEL_BETA |  | ||||||
|  |  | ||||||
|     tags_to_push = [args.tag] |     tags_to_push = [args.tag] | ||||||
|     if channel == CHANNEL_DEV: |     if channel == CHANNEL_DEV: | ||||||
| @@ -53,15 +64,28 @@ def main(): | |||||||
|  |  | ||||||
|     suffix = f"-{args.suffix}" if args.suffix else "" |     suffix = f"-{args.suffix}" if args.suffix else "" | ||||||
|  |  | ||||||
|     with open(os.environ["GITHUB_OUTPUT"], "w") as f: |     image_name = f"esphome/esphome{suffix}" | ||||||
|         print(f"channel={channel}", file=f) |  | ||||||
|         print(f"image=esphome/esphome{suffix}", file=f) |  | ||||||
|         full_tags = [] |  | ||||||
|  |  | ||||||
|         for tag in tags_to_push: |     print(f"channel={channel}") | ||||||
|             full_tags += [f"ghcr.io/esphome/esphome{suffix}:{tag}"] |  | ||||||
|             full_tags += [f"esphome/esphome{suffix}:{tag}"] |     if args.registry is None: | ||||||
|         print(f"tags={','.join(full_tags)}", file=f) |         args.registry = [GHCR, DOCKERHUB] | ||||||
|  |     elif len(args.registry) == 1: | ||||||
|  |         if GHCR in args.registry: | ||||||
|  |             print(f"image=ghcr.io/{image_name}") | ||||||
|  |         if DOCKERHUB in args.registry: | ||||||
|  |             print(f"image=docker.io/{image_name}") | ||||||
|  |  | ||||||
|  |     print(f"image_name={image_name}") | ||||||
|  |  | ||||||
|  |     full_tags = [] | ||||||
|  |  | ||||||
|  |     for tag in tags_to_push: | ||||||
|  |         if GHCR in args.registry: | ||||||
|  |             full_tags += [f"ghcr.io/{image_name}:{tag}"] | ||||||
|  |         if DOCKERHUB in args.registry: | ||||||
|  |             full_tags += [f"docker.io/{image_name}:{tag}"] | ||||||
|  |     print(f"tags={','.join(full_tags)}") | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|   | |||||||
| @@ -389,7 +389,8 @@ def command_config(args, config): | |||||||
|         output = re.sub( |         output = re.sub( | ||||||
|             r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[5m\2\\033[6m", output |             r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[5m\2\\033[6m", output | ||||||
|         ) |         ) | ||||||
|     safe_print(output) |     if not CORE.quiet: | ||||||
|  |         safe_print(output) | ||||||
|     _LOGGER.info("Configuration is valid!") |     _LOGGER.info("Configuration is valid!") | ||||||
|     return 0 |     return 0 | ||||||
|  |  | ||||||
| @@ -514,7 +515,7 @@ def command_clean(args, config): | |||||||
| def command_dashboard(args): | def command_dashboard(args): | ||||||
|     from esphome.dashboard import dashboard |     from esphome.dashboard import dashboard | ||||||
|  |  | ||||||
|     return dashboard.start_web_server(args) |     return dashboard.start_dashboard(args) | ||||||
|  |  | ||||||
|  |  | ||||||
| def command_update_all(args): | def command_update_all(args): | ||||||
|   | |||||||
| @@ -8,50 +8,37 @@ namespace esphome { | |||||||
| namespace a01nyub { | namespace a01nyub { | ||||||
|  |  | ||||||
| static const char *const TAG = "a01nyub.sensor"; | static const char *const TAG = "a01nyub.sensor"; | ||||||
| static const uint8_t MAX_DATA_LENGTH_BYTES = 4; |  | ||||||
|  |  | ||||||
| void A01nyubComponent::loop() { | void A01nyubComponent::loop() { | ||||||
|   uint8_t data; |   uint8_t data; | ||||||
|   while (this->available() > 0) { |   while (this->available() > 0) { | ||||||
|     if (this->read_byte(&data)) { |     this->read_byte(&data); | ||||||
|       buffer_.push_back(data); |     if (this->buffer_.empty() && (data != 0xff)) | ||||||
|  |       continue; | ||||||
|  |     buffer_.push_back(data); | ||||||
|  |     if (this->buffer_.size() == 4) | ||||||
|       this->check_buffer_(); |       this->check_buffer_(); | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void A01nyubComponent::check_buffer_() { | void A01nyubComponent::check_buffer_() { | ||||||
|   if (this->buffer_.size() >= MAX_DATA_LENGTH_BYTES) { |   uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; | ||||||
|     size_t i; |   if (this->buffer_[3] == checksum) { | ||||||
|     for (i = 0; i < this->buffer_.size(); i++) { |     float distance = (this->buffer_[1] << 8) + this->buffer_[2]; | ||||||
|       // Look for the first packet |     if (distance > 280) { | ||||||
|       if (this->buffer_[i] == 0xFF) { |       float meters = distance / 1000.0; | ||||||
|         if (i + 1 + 3 < this->buffer_.size()) {  // Packet is not complete |       ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters); | ||||||
|           return;                                // Wait for completion |       this->publish_state(meters); | ||||||
|         } |     } else { | ||||||
|  |       ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); | ||||||
|         uint8_t checksum = (this->buffer_[i] + this->buffer_[i + 1] + this->buffer_[i + 2]) & 0xFF; |  | ||||||
|         if (this->buffer_[i + 3] == checksum) { |  | ||||||
|           float distance = (this->buffer_[i + 1] << 8) + this->buffer_[i + 2]; |  | ||||||
|           if (distance > 280) { |  | ||||||
|             float meters = distance / 1000.0; |  | ||||||
|             ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters); |  | ||||||
|             this->publish_state(meters); |  | ||||||
|           } else { |  | ||||||
|             ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         break; |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|     this->buffer_.clear(); |   } else { | ||||||
|  |     ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); | ||||||
|   } |   } | ||||||
|  |   this->buffer_.clear(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void A01nyubComponent::dump_config() { | void A01nyubComponent::dump_config() { LOG_SENSOR("", "A01nyub Sensor", this); } | ||||||
|   ESP_LOGCONFIG(TAG, "A01nyub Sensor:"); |  | ||||||
|   LOG_SENSOR("  ", "Distance", this); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| }  // namespace a01nyub | }  // namespace a01nyub | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								esphome/components/a02yyuw/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/a02yyuw/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | CODEOWNERS = ["@TH-Braemer"] | ||||||
							
								
								
									
										43
									
								
								esphome/components/a02yyuw/a02yyuw.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								esphome/components/a02yyuw/a02yyuw.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | // Datasheet https://wiki.dfrobot.com/_A02YYUW_Waterproof_Ultrasonic_Sensor_SKU_SEN0311 | ||||||
|  |  | ||||||
|  | #include "a02yyuw.h" | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace a02yyuw { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "a02yyuw.sensor"; | ||||||
|  |  | ||||||
|  | void A02yyuwComponent::loop() { | ||||||
|  |   uint8_t data; | ||||||
|  |   while (this->available() > 0) { | ||||||
|  |     this->read_byte(&data); | ||||||
|  |     if (this->buffer_.empty() && (data != 0xff)) | ||||||
|  |       continue; | ||||||
|  |     buffer_.push_back(data); | ||||||
|  |     if (this->buffer_.size() == 4) | ||||||
|  |       this->check_buffer_(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void A02yyuwComponent::check_buffer_() { | ||||||
|  |   uint8_t checksum = this->buffer_[0] + this->buffer_[1] + this->buffer_[2]; | ||||||
|  |   if (this->buffer_[3] == checksum) { | ||||||
|  |     float distance = (this->buffer_[1] << 8) + this->buffer_[2]; | ||||||
|  |     if (distance > 30) { | ||||||
|  |       ESP_LOGV(TAG, "Distance from sensor: %f mm", distance); | ||||||
|  |       this->publish_state(distance); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str()); | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]); | ||||||
|  |   } | ||||||
|  |   this->buffer_.clear(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void A02yyuwComponent::dump_config() { LOG_SENSOR("", "A02yyuw Sensor", this); } | ||||||
|  |  | ||||||
|  | }  // namespace a02yyuw | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										27
									
								
								esphome/components/a02yyuw/a02yyuw.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/a02yyuw/a02yyuw.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/components/sensor/sensor.h" | ||||||
|  | #include "esphome/components/uart/uart.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace a02yyuw { | ||||||
|  |  | ||||||
|  | class A02yyuwComponent : public sensor::Sensor, public Component, public uart::UARTDevice { | ||||||
|  |  public: | ||||||
|  |   // Nothing really public. | ||||||
|  |  | ||||||
|  |   // ========== INTERNAL METHODS ========== | ||||||
|  |   void loop() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void check_buffer_(); | ||||||
|  |  | ||||||
|  |   std::vector<uint8_t> buffer_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace a02yyuw | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										41
									
								
								esphome/components/a02yyuw/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								esphome/components/a02yyuw/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | from esphome.components import sensor, uart | ||||||
|  | from esphome.const import ( | ||||||
|  |     STATE_CLASS_MEASUREMENT, | ||||||
|  |     ICON_ARROW_EXPAND_VERTICAL, | ||||||
|  |     DEVICE_CLASS_DISTANCE, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@TH-Braemer"] | ||||||
|  | DEPENDENCIES = ["uart"] | ||||||
|  | UNIT_MILLIMETERS = "mm" | ||||||
|  |  | ||||||
|  | a02yyuw_ns = cg.esphome_ns.namespace("a02yyuw") | ||||||
|  | A02yyuwComponent = a02yyuw_ns.class_( | ||||||
|  |     "A02yyuwComponent", sensor.Sensor, cg.Component, uart.UARTDevice | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = sensor.sensor_schema( | ||||||
|  |     A02yyuwComponent, | ||||||
|  |     unit_of_measurement=UNIT_MILLIMETERS, | ||||||
|  |     icon=ICON_ARROW_EXPAND_VERTICAL, | ||||||
|  |     accuracy_decimals=0, | ||||||
|  |     state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |     device_class=DEVICE_CLASS_DISTANCE, | ||||||
|  | ).extend(uart.UART_DEVICE_SCHEMA) | ||||||
|  |  | ||||||
|  | FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( | ||||||
|  |     "a02yyuw", | ||||||
|  |     baud_rate=9600, | ||||||
|  |     require_tx=False, | ||||||
|  |     require_rx=True, | ||||||
|  |     data_bits=8, | ||||||
|  |     parity=None, | ||||||
|  |     stop_bits=1, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = await sensor.new_sensor(config) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await uart.register_uart_device(var, config) | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome import pins | from esphome import pins | ||||||
| from esphome.const import CONF_ANALOG, CONF_INPUT | from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER | ||||||
|  |  | ||||||
| from esphome.core import CORE | from esphome.core import CORE | ||||||
| from esphome.components.esp32 import get_esp32_variant | from esphome.components.esp32 import get_esp32_variant | ||||||
| @@ -152,7 +152,8 @@ def validate_adc_pin(value): | |||||||
|         return cv.only_on_rp2040("TEMPERATURE") |         return cv.only_on_rp2040("TEMPERATURE") | ||||||
|  |  | ||||||
|     if CORE.is_esp32: |     if CORE.is_esp32: | ||||||
|         value = pins.internal_gpio_input_pin_number(value) |         conf = pins.internal_gpio_input_pin_schema(value) | ||||||
|  |         value = conf[CONF_NUMBER] | ||||||
|         variant = get_esp32_variant() |         variant = get_esp32_variant() | ||||||
|         if ( |         if ( | ||||||
|             variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL |             variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL | ||||||
| @@ -166,24 +167,23 @@ def validate_adc_pin(value): | |||||||
|         ): |         ): | ||||||
|             raise cv.Invalid(f"{variant} doesn't support ADC on this pin") |             raise cv.Invalid(f"{variant} doesn't support ADC on this pin") | ||||||
|  |  | ||||||
|         return pins.internal_gpio_input_pin_schema(value) |         return conf | ||||||
|  |  | ||||||
|     if CORE.is_esp8266: |     if CORE.is_esp8266: | ||||||
|         value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})( |         conf = pins.gpio_pin_schema( | ||||||
|             value |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         if value != 17:  # A0 |  | ||||||
|             raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC") |  | ||||||
|         return pins.gpio_pin_schema( |  | ||||||
|             {CONF_ANALOG: True, CONF_INPUT: True}, internal=True |             {CONF_ANALOG: True, CONF_INPUT: True}, internal=True | ||||||
|         )(value) |         )(value) | ||||||
|  |  | ||||||
|  |         if conf[CONF_NUMBER] != 17:  # A0 | ||||||
|  |             raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC") | ||||||
|  |         return conf | ||||||
|  |  | ||||||
|     if CORE.is_rp2040: |     if CORE.is_rp2040: | ||||||
|         value = pins.internal_gpio_input_pin_number(value) |         conf = pins.internal_gpio_input_pin_schema(value) | ||||||
|         if value not in (26, 27, 28, 29): |         number = conf[CONF_NUMBER] | ||||||
|  |         if number 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) |         return conf | ||||||
|  |  | ||||||
|     if CORE.is_libretiny: |     if CORE.is_libretiny: | ||||||
|         return pins.gpio_pin_schema( |         return pins.gpio_pin_schema( | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace addressable_light { | namespace addressable_light { | ||||||
|  |  | ||||||
| class AddressableLightDisplay : public display::DisplayBuffer, public PollingComponent { | class AddressableLightDisplay : public display::DisplayBuffer { | ||||||
|  public: |  public: | ||||||
|   light::AddressableLight *get_light() const { return this->light_; } |   light::AddressableLight *get_light() const { return this->light_; } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -45,7 +45,6 @@ async def to_code(config): | |||||||
|     cg.add(var.set_height(config[CONF_HEIGHT])) |     cg.add(var.set_height(config[CONF_HEIGHT])) | ||||||
|     cg.add(var.set_light(wrapped_light)) |     cg.add(var.set_light(wrapped_light)) | ||||||
|  |  | ||||||
|     await cg.register_component(var, config) |  | ||||||
|     await display.register_display(var, config) |     await display.register_display(var, config) | ||||||
|  |  | ||||||
|     if pixel_mapper := config.get(CONF_PIXEL_MAPPER): |     if pixel_mapper := config.get(CONF_PIXEL_MAPPER): | ||||||
|   | |||||||
| @@ -21,36 +21,49 @@ namespace esphome { | |||||||
| namespace aht10 { | namespace aht10 { | ||||||
|  |  | ||||||
| static const char *const TAG = "aht10"; | static const char *const TAG = "aht10"; | ||||||
| static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1}; | static const size_t SIZE_CALIBRATE_CMD = 3; | ||||||
|  | static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1, 0x08, 0x00}; | ||||||
|  | static const uint8_t AHT20_CALIBRATE_CMD[] = {0xBE, 0x08, 0x00}; | ||||||
| static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00}; | static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00}; | ||||||
| static const uint8_t AHT10_DEFAULT_DELAY = 5;    // ms, for calibration and temperature measurement | static const uint8_t AHT10_DEFAULT_DELAY = 5;    // ms, for calibration and temperature measurement | ||||||
| static const uint8_t AHT10_HUMIDITY_DELAY = 30;  // ms | static const uint8_t AHT10_HUMIDITY_DELAY = 30;  // ms | ||||||
| static const uint8_t AHT10_ATTEMPTS = 3;         // safety margin, normally 3 attempts are enough: 3*30=90ms | static const uint8_t AHT10_ATTEMPTS = 3;         // safety margin, normally 3 attempts are enough: 3*30=90ms | ||||||
|  | static const uint8_t AHT10_CAL_ATTEMPTS = 10; | ||||||
|  | static const uint8_t AHT10_STATUS_BUSY = 0x80; | ||||||
|  |  | ||||||
| void AHT10Component::setup() { | void AHT10Component::setup() { | ||||||
|   ESP_LOGCONFIG(TAG, "Setting up AHT10..."); |   const uint8_t *calibrate_cmd; | ||||||
|  |   switch (this->variant_) { | ||||||
|  |     case AHT10Variant::AHT20: | ||||||
|  |       calibrate_cmd = AHT20_CALIBRATE_CMD; | ||||||
|  |       ESP_LOGCONFIG(TAG, "Setting up AHT20"); | ||||||
|  |       break; | ||||||
|  |     case AHT10Variant::AHT10: | ||||||
|  |     default: | ||||||
|  |       calibrate_cmd = AHT10_CALIBRATE_CMD; | ||||||
|  |       ESP_LOGCONFIG(TAG, "Setting up AHT10"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   if (!this->write_bytes(0, AHT10_CALIBRATE_CMD, sizeof(AHT10_CALIBRATE_CMD))) { |   if (this->write(calibrate_cmd, SIZE_CALIBRATE_CMD) != i2c::ERROR_OK) { | ||||||
|     ESP_LOGE(TAG, "Communication with AHT10 failed!"); |     ESP_LOGE(TAG, "Communication with AHT10 failed!"); | ||||||
|     this->mark_failed(); |     this->mark_failed(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   uint8_t data = 0; |   uint8_t data = AHT10_STATUS_BUSY; | ||||||
|   if (this->write(&data, 1) != i2c::ERROR_OK) { |   int cal_attempts = 0; | ||||||
|     ESP_LOGD(TAG, "Communication with AHT10 failed!"); |   while (data & AHT10_STATUS_BUSY) { | ||||||
|     this->mark_failed(); |     delay(AHT10_DEFAULT_DELAY); | ||||||
|     return; |     if (this->read(&data, 1) != i2c::ERROR_OK) { | ||||||
|   } |       ESP_LOGE(TAG, "Communication with AHT10 failed!"); | ||||||
|   delay(AHT10_DEFAULT_DELAY); |       this->mark_failed(); | ||||||
|   if (this->read(&data, 1) != i2c::ERROR_OK) { |       return; | ||||||
|     ESP_LOGD(TAG, "Communication with AHT10 failed!"); |     } | ||||||
|     this->mark_failed(); |     ++cal_attempts; | ||||||
|     return; |     if (cal_attempts > AHT10_CAL_ATTEMPTS) { | ||||||
|   } |       ESP_LOGE(TAG, "AHT10 calibration timed out!"); | ||||||
|   if (this->read(&data, 1) != i2c::ERROR_OK) { |       this->mark_failed(); | ||||||
|     ESP_LOGD(TAG, "Communication with AHT10 failed!"); |       return; | ||||||
|     this->mark_failed(); |     } | ||||||
|     return; |  | ||||||
|   } |   } | ||||||
|   if ((data & 0x68) != 0x08) {  // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED |   if ((data & 0x68) != 0x08) {  // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED | ||||||
|     ESP_LOGE(TAG, "AHT10 calibration failed!"); |     ESP_LOGE(TAG, "AHT10 calibration failed!"); | ||||||
| @@ -62,7 +75,7 @@ void AHT10Component::setup() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void AHT10Component::update() { | void AHT10Component::update() { | ||||||
|   if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { |   if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { | ||||||
|     ESP_LOGE(TAG, "Communication with AHT10 failed!"); |     ESP_LOGE(TAG, "Communication with AHT10 failed!"); | ||||||
|     this->status_set_warning(); |     this->status_set_warning(); | ||||||
|     return; |     return; | ||||||
| @@ -89,7 +102,7 @@ void AHT10Component::update() { | |||||||
|         break; |         break; | ||||||
|       } else { |       } else { | ||||||
|         ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying..."); |         ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying..."); | ||||||
|         if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { |         if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { | ||||||
|           ESP_LOGE(TAG, "Communication with AHT10 failed!"); |           ESP_LOGE(TAG, "Communication with AHT10 failed!"); | ||||||
|           this->status_set_warning(); |           this->status_set_warning(); | ||||||
|           return; |           return; | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
|  | #include <utility> | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/components/sensor/sensor.h" | #include "esphome/components/sensor/sensor.h" | ||||||
| #include "esphome/components/i2c/i2c.h" | #include "esphome/components/i2c/i2c.h" | ||||||
| @@ -7,12 +9,15 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace aht10 { | namespace aht10 { | ||||||
|  |  | ||||||
|  | enum AHT10Variant { AHT10, AHT20 }; | ||||||
|  |  | ||||||
| class AHT10Component : public PollingComponent, public i2c::I2CDevice { | class AHT10Component : public PollingComponent, public i2c::I2CDevice { | ||||||
|  public: |  public: | ||||||
|   void setup() override; |   void setup() override; | ||||||
|   void update() override; |   void update() override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   float get_setup_priority() const override; |   float get_setup_priority() const override; | ||||||
|  |   void set_variant(AHT10Variant variant) { this->variant_ = variant; } | ||||||
|  |  | ||||||
|   void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } |   void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } | ||||||
|   void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } |   void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } | ||||||
| @@ -20,6 +25,7 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice { | |||||||
|  protected: |  protected: | ||||||
|   sensor::Sensor *temperature_sensor_{nullptr}; |   sensor::Sensor *temperature_sensor_{nullptr}; | ||||||
|   sensor::Sensor *humidity_sensor_{nullptr}; |   sensor::Sensor *humidity_sensor_{nullptr}; | ||||||
|  |   AHT10Variant variant_{}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace aht10 | }  // namespace aht10 | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ from esphome.const import ( | |||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     UNIT_CELSIUS, |     UNIT_CELSIUS, | ||||||
|     UNIT_PERCENT, |     UNIT_PERCENT, | ||||||
|  |     CONF_VARIANT, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| DEPENDENCIES = ["i2c"] | DEPENDENCIES = ["i2c"] | ||||||
| @@ -17,6 +18,12 @@ DEPENDENCIES = ["i2c"] | |||||||
| aht10_ns = cg.esphome_ns.namespace("aht10") | aht10_ns = cg.esphome_ns.namespace("aht10") | ||||||
| AHT10Component = aht10_ns.class_("AHT10Component", cg.PollingComponent, i2c.I2CDevice) | AHT10Component = aht10_ns.class_("AHT10Component", cg.PollingComponent, i2c.I2CDevice) | ||||||
|  |  | ||||||
|  | AHT10Variant = aht10_ns.enum("AHT10Variant") | ||||||
|  | AHT10_VARIANTS = { | ||||||
|  |     "AHT10": AHT10Variant.AHT10, | ||||||
|  |     "AHT20": AHT10Variant.AHT20, | ||||||
|  | } | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = ( | CONFIG_SCHEMA = ( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
|         { |         { | ||||||
| @@ -33,6 +40,9 @@ CONFIG_SCHEMA = ( | |||||||
|                 device_class=DEVICE_CLASS_HUMIDITY, |                 device_class=DEVICE_CLASS_HUMIDITY, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|             ), |             ), | ||||||
|  |             cv.Optional(CONF_VARIANT, default="AHT10"): cv.enum( | ||||||
|  |                 AHT10_VARIANTS, upper=True | ||||||
|  |             ), | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     .extend(cv.polling_component_schema("60s")) |     .extend(cv.polling_component_schema("60s")) | ||||||
| @@ -44,6 +54,7 @@ async def to_code(config): | |||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|     await cg.register_component(var, config) |     await cg.register_component(var, config) | ||||||
|     await i2c.register_i2c_device(var, config) |     await i2c.register_i2c_device(var, config) | ||||||
|  |     cg.add(var.set_variant(config[CONF_VARIANT])) | ||||||
|  |  | ||||||
|     if temperature := config.get(CONF_TEMPERATURE): |     if temperature := config.get(CONF_TEMPERATURE): | ||||||
|         sens = await sensor.new_sensor(temperature) |         sens = await sensor.new_sensor(temperature) | ||||||
|   | |||||||
| @@ -365,6 +365,7 @@ message ListEntitiesFanResponse { | |||||||
|   bool disabled_by_default = 9; |   bool disabled_by_default = 9; | ||||||
|   string icon = 10; |   string icon = 10; | ||||||
|   EntityCategory entity_category = 11; |   EntityCategory entity_category = 11; | ||||||
|  |   repeated string supported_preset_modes = 12; | ||||||
| } | } | ||||||
| enum FanSpeed { | enum FanSpeed { | ||||||
|   FAN_SPEED_LOW = 0; |   FAN_SPEED_LOW = 0; | ||||||
| @@ -387,6 +388,7 @@ message FanStateResponse { | |||||||
|   FanSpeed speed = 4 [deprecated = true]; |   FanSpeed speed = 4 [deprecated = true]; | ||||||
|   FanDirection direction = 5; |   FanDirection direction = 5; | ||||||
|   int32 speed_level = 6; |   int32 speed_level = 6; | ||||||
|  |   string preset_mode = 7; | ||||||
| } | } | ||||||
| message FanCommandRequest { | message FanCommandRequest { | ||||||
|   option (id) = 31; |   option (id) = 31; | ||||||
| @@ -405,6 +407,8 @@ message FanCommandRequest { | |||||||
|   FanDirection direction = 9; |   FanDirection direction = 9; | ||||||
|   bool has_speed_level = 10; |   bool has_speed_level = 10; | ||||||
|   int32 speed_level = 11; |   int32 speed_level = 11; | ||||||
|  |   bool has_preset_mode = 12; | ||||||
|  |   string preset_mode = 13; | ||||||
| } | } | ||||||
|  |  | ||||||
| // ==================== LIGHT ==================== | // ==================== LIGHT ==================== | ||||||
| @@ -855,6 +859,10 @@ message ListEntitiesClimateResponse { | |||||||
|   string icon = 19; |   string icon = 19; | ||||||
|   EntityCategory entity_category = 20; |   EntityCategory entity_category = 20; | ||||||
|   float visual_current_temperature_step = 21; |   float visual_current_temperature_step = 21; | ||||||
|  |   bool supports_current_humidity = 22; | ||||||
|  |   bool supports_target_humidity = 23; | ||||||
|  |   float visual_min_humidity = 24; | ||||||
|  |   float visual_max_humidity = 25; | ||||||
| } | } | ||||||
| message ClimateStateResponse { | message ClimateStateResponse { | ||||||
|   option (id) = 47; |   option (id) = 47; | ||||||
| @@ -875,6 +883,8 @@ message ClimateStateResponse { | |||||||
|   string custom_fan_mode = 11; |   string custom_fan_mode = 11; | ||||||
|   ClimatePreset preset = 12; |   ClimatePreset preset = 12; | ||||||
|   string custom_preset = 13; |   string custom_preset = 13; | ||||||
|  |   float current_humidity = 14; | ||||||
|  |   float target_humidity = 15; | ||||||
| } | } | ||||||
| message ClimateCommandRequest { | message ClimateCommandRequest { | ||||||
|   option (id) = 48; |   option (id) = 48; | ||||||
| @@ -903,6 +913,8 @@ message ClimateCommandRequest { | |||||||
|   ClimatePreset preset = 19; |   ClimatePreset preset = 19; | ||||||
|   bool has_custom_preset = 20; |   bool has_custom_preset = 20; | ||||||
|   string custom_preset = 21; |   string custom_preset = 21; | ||||||
|  |   bool has_target_humidity = 22; | ||||||
|  |   float target_humidity = 23; | ||||||
| } | } | ||||||
|  |  | ||||||
| // ==================== NUMBER ==================== | // ==================== NUMBER ==================== | ||||||
|   | |||||||
| @@ -293,6 +293,8 @@ bool APIConnection::send_fan_state(fan::Fan *fan) { | |||||||
|   } |   } | ||||||
|   if (traits.supports_direction()) |   if (traits.supports_direction()) | ||||||
|     resp.direction = static_cast<enums::FanDirection>(fan->direction); |     resp.direction = static_cast<enums::FanDirection>(fan->direction); | ||||||
|  |   if (traits.supports_preset_modes()) | ||||||
|  |     resp.preset_mode = fan->preset_mode; | ||||||
|   return this->send_fan_state_response(resp); |   return this->send_fan_state_response(resp); | ||||||
| } | } | ||||||
| bool APIConnection::send_fan_info(fan::Fan *fan) { | bool APIConnection::send_fan_info(fan::Fan *fan) { | ||||||
| @@ -307,6 +309,8 @@ bool APIConnection::send_fan_info(fan::Fan *fan) { | |||||||
|   msg.supports_speed = traits.supports_speed(); |   msg.supports_speed = traits.supports_speed(); | ||||||
|   msg.supports_direction = traits.supports_direction(); |   msg.supports_direction = traits.supports_direction(); | ||||||
|   msg.supported_speed_count = traits.supported_speed_count(); |   msg.supported_speed_count = traits.supported_speed_count(); | ||||||
|  |   for (auto const &preset : traits.supported_preset_modes()) | ||||||
|  |     msg.supported_preset_modes.push_back(preset); | ||||||
|   msg.disabled_by_default = fan->is_disabled_by_default(); |   msg.disabled_by_default = fan->is_disabled_by_default(); | ||||||
|   msg.icon = fan->get_icon(); |   msg.icon = fan->get_icon(); | ||||||
|   msg.entity_category = static_cast<enums::EntityCategory>(fan->get_entity_category()); |   msg.entity_category = static_cast<enums::EntityCategory>(fan->get_entity_category()); | ||||||
| @@ -328,6 +332,8 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { | |||||||
|   } |   } | ||||||
|   if (msg.has_direction) |   if (msg.has_direction) | ||||||
|     call.set_direction(static_cast<fan::FanDirection>(msg.direction)); |     call.set_direction(static_cast<fan::FanDirection>(msg.direction)); | ||||||
|  |   if (msg.has_preset_mode) | ||||||
|  |     call.set_preset_mode(msg.preset_mode); | ||||||
|   call.perform(); |   call.perform(); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| @@ -554,6 +560,10 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { | |||||||
|     resp.custom_preset = climate->custom_preset.value(); |     resp.custom_preset = climate->custom_preset.value(); | ||||||
|   if (traits.get_supports_swing_modes()) |   if (traits.get_supports_swing_modes()) | ||||||
|     resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode); |     resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode); | ||||||
|  |   if (traits.get_supports_current_humidity()) | ||||||
|  |     resp.current_humidity = climate->current_humidity; | ||||||
|  |   if (traits.get_supports_target_humidity()) | ||||||
|  |     resp.target_humidity = climate->target_humidity; | ||||||
|   return this->send_climate_state_response(resp); |   return this->send_climate_state_response(resp); | ||||||
| } | } | ||||||
| bool APIConnection::send_climate_info(climate::Climate *climate) { | bool APIConnection::send_climate_info(climate::Climate *climate) { | ||||||
| @@ -570,7 +580,9 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { | |||||||
|   msg.entity_category = static_cast<enums::EntityCategory>(climate->get_entity_category()); |   msg.entity_category = static_cast<enums::EntityCategory>(climate->get_entity_category()); | ||||||
|  |  | ||||||
|   msg.supports_current_temperature = traits.get_supports_current_temperature(); |   msg.supports_current_temperature = traits.get_supports_current_temperature(); | ||||||
|  |   msg.supports_current_humidity = traits.get_supports_current_humidity(); | ||||||
|   msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); |   msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); | ||||||
|  |   msg.supports_target_humidity = traits.get_supports_target_humidity(); | ||||||
|  |  | ||||||
|   for (auto mode : traits.get_supported_modes()) |   for (auto mode : traits.get_supported_modes()) | ||||||
|     msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode)); |     msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode)); | ||||||
| @@ -579,6 +591,8 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { | |||||||
|   msg.visual_max_temperature = traits.get_visual_max_temperature(); |   msg.visual_max_temperature = traits.get_visual_max_temperature(); | ||||||
|   msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); |   msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); | ||||||
|   msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); |   msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); | ||||||
|  |   msg.visual_min_humidity = traits.get_visual_min_humidity(); | ||||||
|  |   msg.visual_max_humidity = traits.get_visual_max_humidity(); | ||||||
|  |  | ||||||
|   msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); |   msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); | ||||||
|   msg.supports_action = traits.get_supports_action(); |   msg.supports_action = traits.get_supports_action(); | ||||||
| @@ -609,6 +623,8 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { | |||||||
|     call.set_target_temperature_low(msg.target_temperature_low); |     call.set_target_temperature_low(msg.target_temperature_low); | ||||||
|   if (msg.has_target_temperature_high) |   if (msg.has_target_temperature_high) | ||||||
|     call.set_target_temperature_high(msg.target_temperature_high); |     call.set_target_temperature_high(msg.target_temperature_high); | ||||||
|  |   if (msg.has_target_humidity) | ||||||
|  |     call.set_target_humidity(msg.target_humidity); | ||||||
|   if (msg.has_fan_mode) |   if (msg.has_fan_mode) | ||||||
|     call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode)); |     call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode)); | ||||||
|   if (msg.has_custom_fan_mode) |   if (msg.has_custom_fan_mode) | ||||||
|   | |||||||
| @@ -1375,6 +1375,10 @@ bool ListEntitiesFanResponse::decode_length(uint32_t field_id, ProtoLengthDelimi | |||||||
|       this->icon = value.as_string(); |       this->icon = value.as_string(); | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|  |     case 12: { | ||||||
|  |       this->supported_preset_modes.push_back(value.as_string()); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|     default: |     default: | ||||||
|       return false; |       return false; | ||||||
|   } |   } | ||||||
| @@ -1401,6 +1405,9 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { | |||||||
|   buffer.encode_bool(9, this->disabled_by_default); |   buffer.encode_bool(9, this->disabled_by_default); | ||||||
|   buffer.encode_string(10, this->icon); |   buffer.encode_string(10, this->icon); | ||||||
|   buffer.encode_enum<enums::EntityCategory>(11, this->entity_category); |   buffer.encode_enum<enums::EntityCategory>(11, this->entity_category); | ||||||
|  |   for (auto &it : this->supported_preset_modes) { | ||||||
|  |     buffer.encode_string(12, it, true); | ||||||
|  |   } | ||||||
| } | } | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
| void ListEntitiesFanResponse::dump_to(std::string &out) const { | void ListEntitiesFanResponse::dump_to(std::string &out) const { | ||||||
| @@ -1451,6 +1458,12 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { | |||||||
|   out.append("  entity_category: "); |   out.append("  entity_category: "); | ||||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); |   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   for (const auto &it : this->supported_preset_modes) { | ||||||
|  |     out.append("  supported_preset_modes: "); | ||||||
|  |     out.append("'").append(it).append("'"); | ||||||
|  |     out.append("\n"); | ||||||
|  |   } | ||||||
|   out.append("}"); |   out.append("}"); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| @@ -1480,6 +1493,16 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | |||||||
|       return false; |       return false; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | bool FanStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 7: { | ||||||
|  |       this->preset_mode = value.as_string(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
| bool FanStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { | bool FanStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||||
|   switch (field_id) { |   switch (field_id) { | ||||||
|     case 1: { |     case 1: { | ||||||
| @@ -1497,6 +1520,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { | |||||||
|   buffer.encode_enum<enums::FanSpeed>(4, this->speed); |   buffer.encode_enum<enums::FanSpeed>(4, this->speed); | ||||||
|   buffer.encode_enum<enums::FanDirection>(5, this->direction); |   buffer.encode_enum<enums::FanDirection>(5, this->direction); | ||||||
|   buffer.encode_int32(6, this->speed_level); |   buffer.encode_int32(6, this->speed_level); | ||||||
|  |   buffer.encode_string(7, this->preset_mode); | ||||||
| } | } | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
| void FanStateResponse::dump_to(std::string &out) const { | void FanStateResponse::dump_to(std::string &out) const { | ||||||
| @@ -1527,6 +1551,10 @@ void FanStateResponse::dump_to(std::string &out) const { | |||||||
|   sprintf(buffer, "%" PRId32, this->speed_level); |   sprintf(buffer, "%" PRId32, this->speed_level); | ||||||
|   out.append(buffer); |   out.append(buffer); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  preset_mode: "); | ||||||
|  |   out.append("'").append(this->preset_mode).append("'"); | ||||||
|  |   out.append("\n"); | ||||||
|   out.append("}"); |   out.append("}"); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| @@ -1572,6 +1600,20 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | |||||||
|       this->speed_level = value.as_int32(); |       this->speed_level = value.as_int32(); | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|  |     case 12: { | ||||||
|  |       this->has_preset_mode = value.as_bool(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 13: { | ||||||
|  |       this->preset_mode = value.as_string(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|     default: |     default: | ||||||
|       return false; |       return false; | ||||||
|   } |   } | ||||||
| @@ -1598,6 +1640,8 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { | |||||||
|   buffer.encode_enum<enums::FanDirection>(9, this->direction); |   buffer.encode_enum<enums::FanDirection>(9, this->direction); | ||||||
|   buffer.encode_bool(10, this->has_speed_level); |   buffer.encode_bool(10, this->has_speed_level); | ||||||
|   buffer.encode_int32(11, this->speed_level); |   buffer.encode_int32(11, this->speed_level); | ||||||
|  |   buffer.encode_bool(12, this->has_preset_mode); | ||||||
|  |   buffer.encode_string(13, this->preset_mode); | ||||||
| } | } | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
| void FanCommandRequest::dump_to(std::string &out) const { | void FanCommandRequest::dump_to(std::string &out) const { | ||||||
| @@ -1648,6 +1692,14 @@ void FanCommandRequest::dump_to(std::string &out) const { | |||||||
|   sprintf(buffer, "%" PRId32, this->speed_level); |   sprintf(buffer, "%" PRId32, this->speed_level); | ||||||
|   out.append(buffer); |   out.append(buffer); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  has_preset_mode: "); | ||||||
|  |   out.append(YESNO(this->has_preset_mode)); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  preset_mode: "); | ||||||
|  |   out.append("'").append(this->preset_mode).append("'"); | ||||||
|  |   out.append("\n"); | ||||||
|   out.append("}"); |   out.append("}"); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| @@ -3559,6 +3611,14 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v | |||||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); |       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|  |     case 22: { | ||||||
|  |       this->supports_current_humidity = value.as_bool(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     case 23: { | ||||||
|  |       this->supports_target_humidity = value.as_bool(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|     default: |     default: | ||||||
|       return false; |       return false; | ||||||
|   } |   } | ||||||
| @@ -3615,6 +3675,14 @@ bool ListEntitiesClimateResponse::decode_32bit(uint32_t field_id, Proto32Bit val | |||||||
|       this->visual_current_temperature_step = value.as_float(); |       this->visual_current_temperature_step = value.as_float(); | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|  |     case 24: { | ||||||
|  |       this->visual_min_humidity = value.as_float(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     case 25: { | ||||||
|  |       this->visual_max_humidity = value.as_float(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|     default: |     default: | ||||||
|       return false; |       return false; | ||||||
|   } |   } | ||||||
| @@ -3653,6 +3721,10 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { | |||||||
|   buffer.encode_string(19, this->icon); |   buffer.encode_string(19, this->icon); | ||||||
|   buffer.encode_enum<enums::EntityCategory>(20, this->entity_category); |   buffer.encode_enum<enums::EntityCategory>(20, this->entity_category); | ||||||
|   buffer.encode_float(21, this->visual_current_temperature_step); |   buffer.encode_float(21, this->visual_current_temperature_step); | ||||||
|  |   buffer.encode_bool(22, this->supports_current_humidity); | ||||||
|  |   buffer.encode_bool(23, this->supports_target_humidity); | ||||||
|  |   buffer.encode_float(24, this->visual_min_humidity); | ||||||
|  |   buffer.encode_float(25, this->visual_max_humidity); | ||||||
| } | } | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
| void ListEntitiesClimateResponse::dump_to(std::string &out) const { | void ListEntitiesClimateResponse::dump_to(std::string &out) const { | ||||||
| @@ -3758,7 +3830,24 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { | |||||||
|   sprintf(buffer, "%g", this->visual_current_temperature_step); |   sprintf(buffer, "%g", this->visual_current_temperature_step); | ||||||
|   out.append(buffer); |   out.append(buffer); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|   out.append("}"); |  | ||||||
|  |   out.append("  supports_current_humidity: "); | ||||||
|  |   out.append(YESNO(this->supports_current_humidity)); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  supports_target_humidity: "); | ||||||
|  |   out.append(YESNO(this->supports_target_humidity)); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  visual_min_humidity: "); | ||||||
|  |   sprintf(buffer, "%g", this->visual_min_humidity); | ||||||
|  |   out.append(buffer); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  visual_max_humidity: "); | ||||||
|  |   sprintf(buffer, "%g", this->visual_max_humidity); | ||||||
|  |   out.append(buffer); | ||||||
|  |   out.append("\n"); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||||
| @@ -3827,6 +3916,14 @@ bool ClimateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { | |||||||
|       this->target_temperature_high = value.as_float(); |       this->target_temperature_high = value.as_float(); | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|  |     case 14: { | ||||||
|  |       this->current_humidity = value.as_float(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     case 15: { | ||||||
|  |       this->target_humidity = value.as_float(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|     default: |     default: | ||||||
|       return false; |       return false; | ||||||
|   } |   } | ||||||
| @@ -3845,6 +3942,8 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { | |||||||
|   buffer.encode_string(11, this->custom_fan_mode); |   buffer.encode_string(11, this->custom_fan_mode); | ||||||
|   buffer.encode_enum<enums::ClimatePreset>(12, this->preset); |   buffer.encode_enum<enums::ClimatePreset>(12, this->preset); | ||||||
|   buffer.encode_string(13, this->custom_preset); |   buffer.encode_string(13, this->custom_preset); | ||||||
|  |   buffer.encode_float(14, this->current_humidity); | ||||||
|  |   buffer.encode_float(15, this->target_humidity); | ||||||
| } | } | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
| void ClimateStateResponse::dump_to(std::string &out) const { | void ClimateStateResponse::dump_to(std::string &out) const { | ||||||
| @@ -3906,7 +4005,16 @@ void ClimateStateResponse::dump_to(std::string &out) const { | |||||||
|   out.append("  custom_preset: "); |   out.append("  custom_preset: "); | ||||||
|   out.append("'").append(this->custom_preset).append("'"); |   out.append("'").append(this->custom_preset).append("'"); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|   out.append("}"); |  | ||||||
|  |   out.append("  current_humidity: "); | ||||||
|  |   sprintf(buffer, "%g", this->current_humidity); | ||||||
|  |   out.append(buffer); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  target_humidity: "); | ||||||
|  |   sprintf(buffer, "%g", this->target_humidity); | ||||||
|  |   out.append(buffer); | ||||||
|  |   out.append("\n"); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||||
| @@ -3971,6 +4079,10 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) | |||||||
|       this->has_custom_preset = value.as_bool(); |       this->has_custom_preset = value.as_bool(); | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|  |     case 22: { | ||||||
|  |       this->has_target_humidity = value.as_bool(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|     default: |     default: | ||||||
|       return false; |       return false; | ||||||
|   } |   } | ||||||
| @@ -4007,6 +4119,10 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { | |||||||
|       this->target_temperature_high = value.as_float(); |       this->target_temperature_high = value.as_float(); | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|  |     case 23: { | ||||||
|  |       this->target_humidity = value.as_float(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|     default: |     default: | ||||||
|       return false; |       return false; | ||||||
|   } |   } | ||||||
| @@ -4033,6 +4149,8 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { | |||||||
|   buffer.encode_enum<enums::ClimatePreset>(19, this->preset); |   buffer.encode_enum<enums::ClimatePreset>(19, this->preset); | ||||||
|   buffer.encode_bool(20, this->has_custom_preset); |   buffer.encode_bool(20, this->has_custom_preset); | ||||||
|   buffer.encode_string(21, this->custom_preset); |   buffer.encode_string(21, this->custom_preset); | ||||||
|  |   buffer.encode_bool(22, this->has_target_humidity); | ||||||
|  |   buffer.encode_float(23, this->target_humidity); | ||||||
| } | } | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
| void ClimateCommandRequest::dump_to(std::string &out) const { | void ClimateCommandRequest::dump_to(std::string &out) const { | ||||||
| @@ -4125,6 +4243,15 @@ void ClimateCommandRequest::dump_to(std::string &out) const { | |||||||
|   out.append("  custom_preset: "); |   out.append("  custom_preset: "); | ||||||
|   out.append("'").append(this->custom_preset).append("'"); |   out.append("'").append(this->custom_preset).append("'"); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  has_target_humidity: "); | ||||||
|  |   out.append(YESNO(this->has_target_humidity)); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  target_humidity: "); | ||||||
|  |   sprintf(buffer, "%g", this->target_humidity); | ||||||
|  |   out.append(buffer); | ||||||
|  |   out.append("\n"); | ||||||
|   out.append("}"); |   out.append("}"); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -472,6 +472,7 @@ class ListEntitiesFanResponse : public ProtoMessage { | |||||||
|   bool disabled_by_default{false}; |   bool disabled_by_default{false}; | ||||||
|   std::string icon{}; |   std::string icon{}; | ||||||
|   enums::EntityCategory entity_category{}; |   enums::EntityCategory entity_category{}; | ||||||
|  |   std::vector<std::string> supported_preset_modes{}; | ||||||
|   void encode(ProtoWriteBuffer buffer) const override; |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   void dump_to(std::string &out) const override; |   void dump_to(std::string &out) const override; | ||||||
| @@ -490,6 +491,7 @@ class FanStateResponse : public ProtoMessage { | |||||||
|   enums::FanSpeed speed{}; |   enums::FanSpeed speed{}; | ||||||
|   enums::FanDirection direction{}; |   enums::FanDirection direction{}; | ||||||
|   int32_t speed_level{0}; |   int32_t speed_level{0}; | ||||||
|  |   std::string preset_mode{}; | ||||||
|   void encode(ProtoWriteBuffer buffer) const override; |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   void dump_to(std::string &out) const override; |   void dump_to(std::string &out) const override; | ||||||
| @@ -497,6 +499,7 @@ class FanStateResponse : public ProtoMessage { | |||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; |   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||||
|  |   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; |   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||||
| }; | }; | ||||||
| class FanCommandRequest : public ProtoMessage { | class FanCommandRequest : public ProtoMessage { | ||||||
| @@ -512,6 +515,8 @@ class FanCommandRequest : public ProtoMessage { | |||||||
|   enums::FanDirection direction{}; |   enums::FanDirection direction{}; | ||||||
|   bool has_speed_level{false}; |   bool has_speed_level{false}; | ||||||
|   int32_t speed_level{0}; |   int32_t speed_level{0}; | ||||||
|  |   bool has_preset_mode{false}; | ||||||
|  |   std::string preset_mode{}; | ||||||
|   void encode(ProtoWriteBuffer buffer) const override; |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   void dump_to(std::string &out) const override; |   void dump_to(std::string &out) const override; | ||||||
| @@ -519,6 +524,7 @@ class FanCommandRequest : public ProtoMessage { | |||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; |   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||||
|  |   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; |   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||||
| }; | }; | ||||||
| class ListEntitiesLightResponse : public ProtoMessage { | class ListEntitiesLightResponse : public ProtoMessage { | ||||||
| @@ -979,6 +985,10 @@ class ListEntitiesClimateResponse : public ProtoMessage { | |||||||
|   std::string icon{}; |   std::string icon{}; | ||||||
|   enums::EntityCategory entity_category{}; |   enums::EntityCategory entity_category{}; | ||||||
|   float visual_current_temperature_step{0.0f}; |   float visual_current_temperature_step{0.0f}; | ||||||
|  |   bool supports_current_humidity{false}; | ||||||
|  |   bool supports_target_humidity{false}; | ||||||
|  |   float visual_min_humidity{0.0f}; | ||||||
|  |   float visual_max_humidity{0.0f}; | ||||||
|   void encode(ProtoWriteBuffer buffer) const override; |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   void dump_to(std::string &out) const override; |   void dump_to(std::string &out) const override; | ||||||
| @@ -1004,6 +1014,8 @@ class ClimateStateResponse : public ProtoMessage { | |||||||
|   std::string custom_fan_mode{}; |   std::string custom_fan_mode{}; | ||||||
|   enums::ClimatePreset preset{}; |   enums::ClimatePreset preset{}; | ||||||
|   std::string custom_preset{}; |   std::string custom_preset{}; | ||||||
|  |   float current_humidity{0.0f}; | ||||||
|  |   float target_humidity{0.0f}; | ||||||
|   void encode(ProtoWriteBuffer buffer) const override; |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   void dump_to(std::string &out) const override; |   void dump_to(std::string &out) const override; | ||||||
| @@ -1037,6 +1049,8 @@ class ClimateCommandRequest : public ProtoMessage { | |||||||
|   enums::ClimatePreset preset{}; |   enums::ClimatePreset preset{}; | ||||||
|   bool has_custom_preset{false}; |   bool has_custom_preset{false}; | ||||||
|   std::string custom_preset{}; |   std::string custom_preset{}; | ||||||
|  |   bool has_target_humidity{false}; | ||||||
|  |   float target_humidity{0.0f}; | ||||||
|   void encode(ProtoWriteBuffer buffer) const override; |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   void dump_to(std::string &out) const override; |   void dump_to(std::string &out) const override; | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ from typing import Any | |||||||
| from aioesphomeapi import APIClient | from aioesphomeapi import APIClient | ||||||
| from aioesphomeapi.api_pb2 import SubscribeLogsResponse | from aioesphomeapi.api_pb2 import SubscribeLogsResponse | ||||||
| from aioesphomeapi.log_runner import async_run | from aioesphomeapi.log_runner import async_run | ||||||
| from zeroconf.asyncio import AsyncZeroconf |  | ||||||
|  |  | ||||||
| from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ | from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ | ||||||
| from esphome.core import CORE | from esphome.core import CORE | ||||||
| @@ -18,24 +17,22 @@ from . import CONF_ENCRYPTION | |||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def async_run_logs(config, address): | async def async_run_logs(config: dict[str, Any], address: str) -> None: | ||||||
|     """Run the logs command in the event loop.""" |     """Run the logs command in the event loop.""" | ||||||
|     conf = config["api"] |     conf = config["api"] | ||||||
|  |     name = config["esphome"]["name"] | ||||||
|     port: int = int(conf[CONF_PORT]) |     port: int = int(conf[CONF_PORT]) | ||||||
|     password: str = conf[CONF_PASSWORD] |     password: str = conf[CONF_PASSWORD] | ||||||
|     noise_psk: str | None = None |     noise_psk: str | None = None | ||||||
|     if CONF_ENCRYPTION in conf: |     if CONF_ENCRYPTION in conf: | ||||||
|         noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] |         noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] | ||||||
|     _LOGGER.info("Starting log output from %s using esphome API", address) |     _LOGGER.info("Starting log output from %s using esphome API", address) | ||||||
|     aiozc = AsyncZeroconf() |  | ||||||
|  |  | ||||||
|     cli = APIClient( |     cli = APIClient( | ||||||
|         address, |         address, | ||||||
|         port, |         port, | ||||||
|         password, |         password, | ||||||
|         client_info=f"ESPHome Logs {__version__}", |         client_info=f"ESPHome Logs {__version__}", | ||||||
|         noise_psk=noise_psk, |         noise_psk=noise_psk, | ||||||
|         zeroconf_instance=aiozc.zeroconf, |  | ||||||
|     ) |     ) | ||||||
|     dashboard = CORE.dashboard |     dashboard = CORE.dashboard | ||||||
|  |  | ||||||
| @@ -48,12 +45,10 @@ async def async_run_logs(config, address): | |||||||
|             text = text.replace("\033", "\\033") |             text = text.replace("\033", "\\033") | ||||||
|         print(f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]{text}") |         print(f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]{text}") | ||||||
|  |  | ||||||
|     stop = await async_run(cli, on_log, aio_zeroconf_instance=aiozc) |     stop = await async_run(cli, on_log, name=name) | ||||||
|     try: |     try: | ||||||
|         while True: |         await asyncio.Event().wait() | ||||||
|             await asyncio.sleep(60) |  | ||||||
|     finally: |     finally: | ||||||
|         await aiozc.async_close() |  | ||||||
|         await stop() |         await stop() | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,6 +15,16 @@ void BangBangClimate::setup() { | |||||||
|     this->publish_state(); |     this->publish_state(); | ||||||
|   }); |   }); | ||||||
|   this->current_temperature = this->sensor_->state; |   this->current_temperature = this->sensor_->state; | ||||||
|  |  | ||||||
|  |   // register for humidity values and get initial state | ||||||
|  |   if (this->humidity_sensor_ != nullptr) { | ||||||
|  |     this->humidity_sensor_->add_on_state_callback([this](float state) { | ||||||
|  |       this->current_humidity = state; | ||||||
|  |       this->publish_state(); | ||||||
|  |     }); | ||||||
|  |     this->current_humidity = this->humidity_sensor_->state; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // restore set points |   // restore set points | ||||||
|   auto restore = this->restore_state_(); |   auto restore = this->restore_state_(); | ||||||
|   if (restore.has_value()) { |   if (restore.has_value()) { | ||||||
| @@ -47,6 +57,8 @@ void BangBangClimate::control(const climate::ClimateCall &call) { | |||||||
| climate::ClimateTraits BangBangClimate::traits() { | climate::ClimateTraits BangBangClimate::traits() { | ||||||
|   auto traits = climate::ClimateTraits(); |   auto traits = climate::ClimateTraits(); | ||||||
|   traits.set_supports_current_temperature(true); |   traits.set_supports_current_temperature(true); | ||||||
|  |   if (this->humidity_sensor_ != nullptr) | ||||||
|  |     traits.set_supports_current_humidity(true); | ||||||
|   traits.set_supported_modes({ |   traits.set_supported_modes({ | ||||||
|       climate::CLIMATE_MODE_OFF, |       climate::CLIMATE_MODE_OFF, | ||||||
|   }); |   }); | ||||||
| @@ -171,6 +183,7 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa | |||||||
| BangBangClimate::BangBangClimate() | BangBangClimate::BangBangClimate() | ||||||
|     : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} |     : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} | ||||||
| void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } | void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } | ||||||
|  | void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } | ||||||
| Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } | Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } | ||||||
| Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } | Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } | ||||||
| void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } | void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ class BangBangClimate : public climate::Climate, public Component { | |||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|  |  | ||||||
|   void set_sensor(sensor::Sensor *sensor); |   void set_sensor(sensor::Sensor *sensor); | ||||||
|  |   void set_humidity_sensor(sensor::Sensor *humidity_sensor); | ||||||
|   Trigger<> *get_idle_trigger() const; |   Trigger<> *get_idle_trigger() const; | ||||||
|   Trigger<> *get_cool_trigger() const; |   Trigger<> *get_cool_trigger() const; | ||||||
|   void set_supports_cool(bool supports_cool); |   void set_supports_cool(bool supports_cool); | ||||||
| @@ -48,6 +49,9 @@ class BangBangClimate : public climate::Climate, public Component { | |||||||
|  |  | ||||||
|   /// The sensor used for getting the current temperature |   /// The sensor used for getting the current temperature | ||||||
|   sensor::Sensor *sensor_{nullptr}; |   sensor::Sensor *sensor_{nullptr}; | ||||||
|  |   /// The sensor used for getting the current humidity | ||||||
|  |   sensor::Sensor *humidity_sensor_{nullptr}; | ||||||
|  |  | ||||||
|   /** The trigger to call when the controller should switch to idle mode. |   /** The trigger to call when the controller should switch to idle mode. | ||||||
|    * |    * | ||||||
|    * In idle mode, the controller is assumed to have both heating and cooling disabled. |    * In idle mode, the controller is assumed to have both heating and cooling disabled. | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ from esphome.const import ( | |||||||
|     CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, |     CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, | ||||||
|     CONF_DEFAULT_TARGET_TEMPERATURE_LOW, |     CONF_DEFAULT_TARGET_TEMPERATURE_LOW, | ||||||
|     CONF_HEAT_ACTION, |     CONF_HEAT_ACTION, | ||||||
|  |     CONF_HUMIDITY_SENSOR, | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_IDLE_ACTION, |     CONF_IDLE_ACTION, | ||||||
|     CONF_SENSOR, |     CONF_SENSOR, | ||||||
| @@ -22,6 +23,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|         { |         { | ||||||
|             cv.GenerateID(): cv.declare_id(BangBangClimate), |             cv.GenerateID(): cv.declare_id(BangBangClimate), | ||||||
|             cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), |             cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), | ||||||
|  |             cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), | ||||||
|             cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, |             cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, | ||||||
|             cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, |             cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, | ||||||
|             cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), |             cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), | ||||||
| @@ -47,6 +49,10 @@ async def to_code(config): | |||||||
|     sens = await cg.get_variable(config[CONF_SENSOR]) |     sens = await cg.get_variable(config[CONF_SENSOR]) | ||||||
|     cg.add(var.set_sensor(sens)) |     cg.add(var.set_sensor(sens)) | ||||||
|  |  | ||||||
|  |     if CONF_HUMIDITY_SENSOR in config: | ||||||
|  |         sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR]) | ||||||
|  |         cg.add(var.set_humidity_sensor(sens)) | ||||||
|  |  | ||||||
|     normal_config = BangBangClimateTargetTempConfig( |     normal_config = BangBangClimateTargetTempConfig( | ||||||
|         config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], |         config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], | ||||||
|         config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], |         config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], | ||||||
|   | |||||||
| @@ -90,40 +90,41 @@ void BP1658CJ::set_channel_value_(uint8_t channel, uint16_t value) { | |||||||
|  |  | ||||||
| void BP1658CJ::write_bit_(bool value) { | void BP1658CJ::write_bit_(bool value) { | ||||||
|   this->data_pin_->digital_write(value); |   this->data_pin_->digital_write(value); | ||||||
|   this->clock_pin_->digital_write(true); |  | ||||||
|  |  | ||||||
|   delayMicroseconds(BP1658CJ_DELAY); |   delayMicroseconds(BP1658CJ_DELAY); | ||||||
|  |   this->clock_pin_->digital_write(true); | ||||||
|  |   delayMicroseconds(BP1658CJ_DELAY); | ||||||
|   this->clock_pin_->digital_write(false); |   this->clock_pin_->digital_write(false); | ||||||
|  |   delayMicroseconds(BP1658CJ_DELAY); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BP1658CJ::write_byte_(uint8_t data) { | void BP1658CJ::write_byte_(uint8_t data) { | ||||||
|   for (uint8_t mask = 0x80; mask; mask >>= 1) { |   for (uint8_t mask = 0x80; mask; mask >>= 1) { | ||||||
|     this->write_bit_(data & mask); |     this->write_bit_(data & mask); | ||||||
|     delayMicroseconds(BP1658CJ_DELAY); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // ack bit |   // ack bit | ||||||
|   this->data_pin_->pin_mode(gpio::FLAG_INPUT); |   this->data_pin_->pin_mode(gpio::FLAG_INPUT); | ||||||
|   this->clock_pin_->digital_write(true); |   this->clock_pin_->digital_write(true); | ||||||
|  |  | ||||||
|   delayMicroseconds(BP1658CJ_DELAY); |   delayMicroseconds(BP1658CJ_DELAY); | ||||||
|  |  | ||||||
|   this->clock_pin_->digital_write(false); |   this->clock_pin_->digital_write(false); | ||||||
|  |   delayMicroseconds(BP1658CJ_DELAY); | ||||||
|   this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); |   this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BP1658CJ::write_buffer_(uint8_t *buffer, uint8_t size) { | void BP1658CJ::write_buffer_(uint8_t *buffer, uint8_t size) { | ||||||
|   this->data_pin_->digital_write(false); |   this->data_pin_->digital_write(false); | ||||||
|  |   delayMicroseconds(BP1658CJ_DELAY); | ||||||
|   this->clock_pin_->digital_write(false); |   this->clock_pin_->digital_write(false); | ||||||
|  |   delayMicroseconds(BP1658CJ_DELAY); | ||||||
|  |  | ||||||
|   for (uint32_t i = 0; i < size; i++) { |   for (uint32_t i = 0; i < size; i++) { | ||||||
|     this->write_byte_(buffer[i]); |     this->write_byte_(buffer[i]); | ||||||
|     delayMicroseconds(BP1658CJ_DELAY); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   this->clock_pin_->digital_write(true); |   this->clock_pin_->digital_write(true); | ||||||
|  |   delayMicroseconds(BP1658CJ_DELAY); | ||||||
|   this->data_pin_->digital_write(true); |   this->data_pin_->digital_write(true); | ||||||
|  |   delayMicroseconds(BP1658CJ_DELAY); | ||||||
| } | } | ||||||
|  |  | ||||||
| }  // namespace bp1658cj | }  // namespace bp1658cj | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ from esphome.const import ( | |||||||
|     CONF_AWAY, |     CONF_AWAY, | ||||||
|     CONF_AWAY_COMMAND_TOPIC, |     CONF_AWAY_COMMAND_TOPIC, | ||||||
|     CONF_AWAY_STATE_TOPIC, |     CONF_AWAY_STATE_TOPIC, | ||||||
|  |     CONF_CURRENT_HUMIDITY_STATE_TOPIC, | ||||||
|     CONF_CURRENT_TEMPERATURE_STATE_TOPIC, |     CONF_CURRENT_TEMPERATURE_STATE_TOPIC, | ||||||
|     CONF_CUSTOM_FAN_MODE, |     CONF_CUSTOM_FAN_MODE, | ||||||
|     CONF_CUSTOM_PRESET, |     CONF_CUSTOM_PRESET, | ||||||
| @@ -28,6 +29,8 @@ from esphome.const import ( | |||||||
|     CONF_SWING_MODE, |     CONF_SWING_MODE, | ||||||
|     CONF_SWING_MODE_COMMAND_TOPIC, |     CONF_SWING_MODE_COMMAND_TOPIC, | ||||||
|     CONF_SWING_MODE_STATE_TOPIC, |     CONF_SWING_MODE_STATE_TOPIC, | ||||||
|  |     CONF_TARGET_HUMIDITY_COMMAND_TOPIC, | ||||||
|  |     CONF_TARGET_HUMIDITY_STATE_TOPIC, | ||||||
|     CONF_TARGET_TEMPERATURE, |     CONF_TARGET_TEMPERATURE, | ||||||
|     CONF_TARGET_TEMPERATURE_COMMAND_TOPIC, |     CONF_TARGET_TEMPERATURE_COMMAND_TOPIC, | ||||||
|     CONF_TARGET_TEMPERATURE_STATE_TOPIC, |     CONF_TARGET_TEMPERATURE_STATE_TOPIC, | ||||||
| @@ -106,6 +109,9 @@ CLIMATE_SWING_MODES = { | |||||||
| validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) | validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) | ||||||
|  |  | ||||||
| CONF_CURRENT_TEMPERATURE = "current_temperature" | CONF_CURRENT_TEMPERATURE = "current_temperature" | ||||||
|  | CONF_MIN_HUMIDITY = "min_humidity" | ||||||
|  | CONF_MAX_HUMIDITY = "max_humidity" | ||||||
|  | CONF_TARGET_HUMIDITY = "target_humidity" | ||||||
|  |  | ||||||
| visual_temperature = cv.float_with_unit( | visual_temperature = cv.float_with_unit( | ||||||
|     "visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?" |     "visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?" | ||||||
| @@ -153,6 +159,8 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). | |||||||
|                 cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, |                 cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, | ||||||
|                 cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, |                 cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, | ||||||
|                 cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA, |                 cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA, | ||||||
|  |                 cv.Optional(CONF_MIN_HUMIDITY): cv.percentage_int, | ||||||
|  |                 cv.Optional(CONF_MAX_HUMIDITY): cv.percentage_int, | ||||||
|             } |             } | ||||||
|         ), |         ), | ||||||
|         cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All( |         cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All( | ||||||
| @@ -167,6 +175,9 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). | |||||||
|         cv.Optional(CONF_CURRENT_TEMPERATURE_STATE_TOPIC): cv.All( |         cv.Optional(CONF_CURRENT_TEMPERATURE_STATE_TOPIC): cv.All( | ||||||
|             cv.requires_component("mqtt"), cv.publish_topic |             cv.requires_component("mqtt"), cv.publish_topic | ||||||
|         ), |         ), | ||||||
|  |         cv.Optional(CONF_CURRENT_HUMIDITY_STATE_TOPIC): cv.All( | ||||||
|  |             cv.requires_component("mqtt"), cv.publish_topic | ||||||
|  |         ), | ||||||
|         cv.Optional(CONF_FAN_MODE_COMMAND_TOPIC): cv.All( |         cv.Optional(CONF_FAN_MODE_COMMAND_TOPIC): cv.All( | ||||||
|             cv.requires_component("mqtt"), cv.publish_topic |             cv.requires_component("mqtt"), cv.publish_topic | ||||||
|         ), |         ), | ||||||
| @@ -209,6 +220,12 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA). | |||||||
|         cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All( |         cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All( | ||||||
|             cv.requires_component("mqtt"), cv.publish_topic |             cv.requires_component("mqtt"), cv.publish_topic | ||||||
|         ), |         ), | ||||||
|  |         cv.Optional(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): cv.All( | ||||||
|  |             cv.requires_component("mqtt"), cv.publish_topic | ||||||
|  |         ), | ||||||
|  |         cv.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): cv.All( | ||||||
|  |             cv.requires_component("mqtt"), cv.publish_topic | ||||||
|  |         ), | ||||||
|         cv.Optional(CONF_ON_CONTROL): automation.validate_automation( |         cv.Optional(CONF_ON_CONTROL): automation.validate_automation( | ||||||
|             { |             { | ||||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger), |                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger), | ||||||
| @@ -238,6 +255,10 @@ async def setup_climate_core_(var, config): | |||||||
|                 visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE], |                 visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE], | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|  |     if CONF_MIN_HUMIDITY in visual: | ||||||
|  |         cg.add(var.set_visual_min_humidity_override(visual[CONF_MIN_HUMIDITY])) | ||||||
|  |     if CONF_MAX_HUMIDITY in visual: | ||||||
|  |         cg.add(var.set_visual_max_humidity_override(visual[CONF_MAX_HUMIDITY])) | ||||||
|  |  | ||||||
|     if CONF_MQTT_ID in config: |     if CONF_MQTT_ID in config: | ||||||
|         mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) |         mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) | ||||||
| @@ -255,6 +276,12 @@ async def setup_climate_core_(var, config): | |||||||
|                     config[CONF_CURRENT_TEMPERATURE_STATE_TOPIC] |                     config[CONF_CURRENT_TEMPERATURE_STATE_TOPIC] | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|  |         if CONF_CURRENT_HUMIDITY_STATE_TOPIC in config: | ||||||
|  |             cg.add( | ||||||
|  |                 mqtt_.set_custom_current_humidity_state_topic( | ||||||
|  |                     config[CONF_CURRENT_HUMIDITY_STATE_TOPIC] | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|         if CONF_FAN_MODE_COMMAND_TOPIC in config: |         if CONF_FAN_MODE_COMMAND_TOPIC in config: | ||||||
|             cg.add( |             cg.add( | ||||||
|                 mqtt_.set_custom_fan_mode_command_topic( |                 mqtt_.set_custom_fan_mode_command_topic( | ||||||
| @@ -323,6 +350,18 @@ async def setup_climate_core_(var, config): | |||||||
|                     config[CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC] |                     config[CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC] | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|  |         if CONF_TARGET_HUMIDITY_COMMAND_TOPIC in config: | ||||||
|  |             cg.add( | ||||||
|  |                 mqtt_.set_custom_target_humidity_command_topic( | ||||||
|  |                     config[CONF_TARGET_HUMIDITY_COMMAND_TOPIC] | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         if CONF_TARGET_HUMIDITY_STATE_TOPIC in config: | ||||||
|  |             cg.add( | ||||||
|  |                 mqtt_.set_custom_target_humidity_state_topic( | ||||||
|  |                     config[CONF_TARGET_HUMIDITY_STATE_TOPIC] | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |  | ||||||
|     for conf in config.get(CONF_ON_STATE, []): |     for conf in config.get(CONF_ON_STATE, []): | ||||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
| @@ -351,6 +390,7 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( | |||||||
|         cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), |         cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), | ||||||
|         cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), |         cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), | ||||||
|         cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), |         cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), | ||||||
|  |         cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int), | ||||||
|         cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"), |         cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"), | ||||||
|         cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( |         cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( | ||||||
|             validate_climate_fan_mode |             validate_climate_fan_mode | ||||||
| @@ -387,6 +427,9 @@ async def climate_control_to_code(config, action_id, template_arg, args): | |||||||
|             config[CONF_TARGET_TEMPERATURE_HIGH], args, float |             config[CONF_TARGET_TEMPERATURE_HIGH], args, float | ||||||
|         ) |         ) | ||||||
|         cg.add(var.set_target_temperature_high(template_)) |         cg.add(var.set_target_temperature_high(template_)) | ||||||
|  |     if CONF_TARGET_HUMIDITY in config: | ||||||
|  |         template_ = await cg.templatable(config[CONF_TARGET_HUMIDITY], args, float) | ||||||
|  |         cg.add(var.set_target_humidity(template_)) | ||||||
|     if CONF_FAN_MODE in config: |     if CONF_FAN_MODE in config: | ||||||
|         template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) |         template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) | ||||||
|         cg.add(var.set_fan_mode(template_)) |         cg.add(var.set_fan_mode(template_)) | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> { | |||||||
|   TEMPLATABLE_VALUE(float, target_temperature) |   TEMPLATABLE_VALUE(float, target_temperature) | ||||||
|   TEMPLATABLE_VALUE(float, target_temperature_low) |   TEMPLATABLE_VALUE(float, target_temperature_low) | ||||||
|   TEMPLATABLE_VALUE(float, target_temperature_high) |   TEMPLATABLE_VALUE(float, target_temperature_high) | ||||||
|  |   TEMPLATABLE_VALUE(float, target_humidity) | ||||||
|   TEMPLATABLE_VALUE(bool, away) |   TEMPLATABLE_VALUE(bool, away) | ||||||
|   TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) |   TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) | ||||||
|   TEMPLATABLE_VALUE(std::string, custom_fan_mode) |   TEMPLATABLE_VALUE(std::string, custom_fan_mode) | ||||||
| @@ -27,6 +28,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> { | |||||||
|     call.set_target_temperature(this->target_temperature_.optional_value(x...)); |     call.set_target_temperature(this->target_temperature_.optional_value(x...)); | ||||||
|     call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...)); |     call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...)); | ||||||
|     call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...)); |     call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...)); | ||||||
|  |     call.set_target_humidity(this->target_humidity_.optional_value(x...)); | ||||||
|     if (away_.has_value()) { |     if (away_.has_value()) { | ||||||
|       call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME); |       call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -45,6 +45,9 @@ void ClimateCall::perform() { | |||||||
|   if (this->target_temperature_high_.has_value()) { |   if (this->target_temperature_high_.has_value()) { | ||||||
|     ESP_LOGD(TAG, "  Target Temperature High: %.2f", *this->target_temperature_high_); |     ESP_LOGD(TAG, "  Target Temperature High: %.2f", *this->target_temperature_high_); | ||||||
|   } |   } | ||||||
|  |   if (this->target_humidity_.has_value()) { | ||||||
|  |     ESP_LOGD(TAG, "  Target Humidity: %.0f", *this->target_humidity_); | ||||||
|  |   } | ||||||
|   this->parent_->control(*this); |   this->parent_->control(*this); | ||||||
| } | } | ||||||
| void ClimateCall::validate_() { | void ClimateCall::validate_() { | ||||||
| @@ -262,10 +265,16 @@ ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_h | |||||||
|   this->target_temperature_high_ = target_temperature_high; |   this->target_temperature_high_ = target_temperature_high; | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  | ClimateCall &ClimateCall::set_target_humidity(float target_humidity) { | ||||||
|  |   this->target_humidity_ = target_humidity; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
| const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; } | const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; } | ||||||
| const optional<float> &ClimateCall::get_target_temperature() const { return this->target_temperature_; } | const optional<float> &ClimateCall::get_target_temperature() const { return this->target_temperature_; } | ||||||
| const optional<float> &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } | const optional<float> &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } | ||||||
| const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } | const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } | ||||||
|  | const optional<float> &ClimateCall::get_target_humidity() const { return this->target_humidity_; } | ||||||
| const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; } | const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; } | ||||||
| const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } | const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } | ||||||
| const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; } | const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; } | ||||||
| @@ -283,6 +292,10 @@ ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperat | |||||||
|   this->target_temperature_ = target_temperature; |   this->target_temperature_ = target_temperature; | ||||||
|   return *this; |   return *this; | ||||||
| } | } | ||||||
|  | ClimateCall &ClimateCall::set_target_humidity(optional<float> target_humidity) { | ||||||
|  |   this->target_humidity_ = target_humidity; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
| ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) { | ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) { | ||||||
|   this->mode_ = mode; |   this->mode_ = mode; | ||||||
|   return *this; |   return *this; | ||||||
| @@ -343,6 +356,9 @@ void Climate::save_state_() { | |||||||
|   } else { |   } else { | ||||||
|     state.target_temperature = this->target_temperature; |     state.target_temperature = this->target_temperature; | ||||||
|   } |   } | ||||||
|  |   if (traits.get_supports_target_humidity()) { | ||||||
|  |     state.target_humidity = this->target_humidity; | ||||||
|  |   } | ||||||
|   if (traits.get_supports_fan_modes() && fan_mode.has_value()) { |   if (traits.get_supports_fan_modes() && fan_mode.has_value()) { | ||||||
|     state.uses_custom_fan_mode = false; |     state.uses_custom_fan_mode = false; | ||||||
|     state.fan_mode = this->fan_mode.value(); |     state.fan_mode = this->fan_mode.value(); | ||||||
| @@ -408,6 +424,12 @@ void Climate::publish_state() { | |||||||
|   } else { |   } else { | ||||||
|     ESP_LOGD(TAG, "  Target Temperature: %.2f°C", this->target_temperature); |     ESP_LOGD(TAG, "  Target Temperature: %.2f°C", this->target_temperature); | ||||||
|   } |   } | ||||||
|  |   if (traits.get_supports_current_humidity()) { | ||||||
|  |     ESP_LOGD(TAG, "  Current Humidity: %.0f%%", this->current_humidity); | ||||||
|  |   } | ||||||
|  |   if (traits.get_supports_target_humidity()) { | ||||||
|  |     ESP_LOGD(TAG, "  Target Humidity: %.0f%%", this->target_humidity); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // Send state to frontend |   // Send state to frontend | ||||||
|   this->state_callback_.call(*this); |   this->state_callback_.call(*this); | ||||||
| @@ -427,6 +449,12 @@ ClimateTraits Climate::get_traits() { | |||||||
|     traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_); |     traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_); | ||||||
|     traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_); |     traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_); | ||||||
|   } |   } | ||||||
|  |   if (this->visual_min_humidity_override_.has_value()) { | ||||||
|  |     traits.set_visual_min_humidity(*this->visual_min_humidity_override_); | ||||||
|  |   } | ||||||
|  |   if (this->visual_max_humidity_override_.has_value()) { | ||||||
|  |     traits.set_visual_max_humidity(*this->visual_max_humidity_override_); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return traits; |   return traits; | ||||||
| } | } | ||||||
| @@ -441,6 +469,12 @@ void Climate::set_visual_temperature_step_override(float target, float current) | |||||||
|   this->visual_target_temperature_step_override_ = target; |   this->visual_target_temperature_step_override_ = target; | ||||||
|   this->visual_current_temperature_step_override_ = current; |   this->visual_current_temperature_step_override_ = current; | ||||||
| } | } | ||||||
|  | void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) { | ||||||
|  |   this->visual_min_humidity_override_ = visual_min_humidity_override; | ||||||
|  | } | ||||||
|  | void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) { | ||||||
|  |   this->visual_max_humidity_override_ = visual_max_humidity_override; | ||||||
|  | } | ||||||
|  |  | ||||||
| ClimateCall Climate::make_call() { return ClimateCall(this); } | ClimateCall Climate::make_call() { return ClimateCall(this); } | ||||||
|  |  | ||||||
| @@ -454,6 +488,9 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { | |||||||
|   } else { |   } else { | ||||||
|     call.set_target_temperature(this->target_temperature); |     call.set_target_temperature(this->target_temperature); | ||||||
|   } |   } | ||||||
|  |   if (traits.get_supports_target_humidity()) { | ||||||
|  |     call.set_target_humidity(this->target_humidity); | ||||||
|  |   } | ||||||
|   if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { |   if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { | ||||||
|     call.set_fan_mode(this->fan_mode); |     call.set_fan_mode(this->fan_mode); | ||||||
|   } |   } | ||||||
| @@ -474,6 +511,9 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { | |||||||
|   } else { |   } else { | ||||||
|     climate->target_temperature = this->target_temperature; |     climate->target_temperature = this->target_temperature; | ||||||
|   } |   } | ||||||
|  |   if (traits.get_supports_target_humidity()) { | ||||||
|  |     climate->target_humidity = this->target_humidity; | ||||||
|  |   } | ||||||
|   if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) { |   if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) { | ||||||
|     climate->fan_mode = this->fan_mode; |     climate->fan_mode = this->fan_mode; | ||||||
|   } |   } | ||||||
| @@ -530,17 +570,25 @@ void Climate::dump_traits_(const char *tag) { | |||||||
|   auto traits = this->get_traits(); |   auto traits = this->get_traits(); | ||||||
|   ESP_LOGCONFIG(tag, "ClimateTraits:"); |   ESP_LOGCONFIG(tag, "ClimateTraits:"); | ||||||
|   ESP_LOGCONFIG(tag, "  [x] Visual settings:"); |   ESP_LOGCONFIG(tag, "  [x] Visual settings:"); | ||||||
|   ESP_LOGCONFIG(tag, "      - Min: %.1f", traits.get_visual_min_temperature()); |   ESP_LOGCONFIG(tag, "      - Min temperature: %.1f", traits.get_visual_min_temperature()); | ||||||
|   ESP_LOGCONFIG(tag, "      - Max: %.1f", traits.get_visual_max_temperature()); |   ESP_LOGCONFIG(tag, "      - Max temperature: %.1f", traits.get_visual_max_temperature()); | ||||||
|   ESP_LOGCONFIG(tag, "      - Step:"); |   ESP_LOGCONFIG(tag, "      - Temperature step:"); | ||||||
|   ESP_LOGCONFIG(tag, "          Target: %.1f", traits.get_visual_target_temperature_step()); |   ESP_LOGCONFIG(tag, "          Target: %.1f", traits.get_visual_target_temperature_step()); | ||||||
|   ESP_LOGCONFIG(tag, "          Current: %.1f", traits.get_visual_current_temperature_step()); |   ESP_LOGCONFIG(tag, "          Current: %.1f", traits.get_visual_current_temperature_step()); | ||||||
|  |   ESP_LOGCONFIG(tag, "      - Min humidity: %.0f", traits.get_visual_min_humidity()); | ||||||
|  |   ESP_LOGCONFIG(tag, "      - Max humidity: %.0f", traits.get_visual_max_humidity()); | ||||||
|   if (traits.get_supports_current_temperature()) { |   if (traits.get_supports_current_temperature()) { | ||||||
|     ESP_LOGCONFIG(tag, "  [x] Supports current temperature"); |     ESP_LOGCONFIG(tag, "  [x] Supports current temperature"); | ||||||
|   } |   } | ||||||
|  |   if (traits.get_supports_current_humidity()) { | ||||||
|  |     ESP_LOGCONFIG(tag, "  [x] Supports current humidity"); | ||||||
|  |   } | ||||||
|   if (traits.get_supports_two_point_target_temperature()) { |   if (traits.get_supports_two_point_target_temperature()) { | ||||||
|     ESP_LOGCONFIG(tag, "  [x] Supports two-point target temperature"); |     ESP_LOGCONFIG(tag, "  [x] Supports two-point target temperature"); | ||||||
|   } |   } | ||||||
|  |   if (traits.get_supports_target_humidity()) { | ||||||
|  |     ESP_LOGCONFIG(tag, "  [x] Supports target humidity"); | ||||||
|  |   } | ||||||
|   if (traits.get_supports_action()) { |   if (traits.get_supports_action()) { | ||||||
|     ESP_LOGCONFIG(tag, "  [x] Supports action"); |     ESP_LOGCONFIG(tag, "  [x] Supports action"); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -64,6 +64,10 @@ class ClimateCall { | |||||||
|    * For climate devices with two point target temperature control |    * For climate devices with two point target temperature control | ||||||
|    */ |    */ | ||||||
|   ClimateCall &set_target_temperature_high(optional<float> target_temperature_high); |   ClimateCall &set_target_temperature_high(optional<float> target_temperature_high); | ||||||
|  |   /// Set the target humidity of the climate device. | ||||||
|  |   ClimateCall &set_target_humidity(float target_humidity); | ||||||
|  |   /// Set the target humidity of the climate device. | ||||||
|  |   ClimateCall &set_target_humidity(optional<float> target_humidity); | ||||||
|   /// Set the fan mode of the climate device. |   /// Set the fan mode of the climate device. | ||||||
|   ClimateCall &set_fan_mode(ClimateFanMode fan_mode); |   ClimateCall &set_fan_mode(ClimateFanMode fan_mode); | ||||||
|   /// Set the fan mode of the climate device. |   /// Set the fan mode of the climate device. | ||||||
| @@ -93,6 +97,7 @@ class ClimateCall { | |||||||
|   const optional<float> &get_target_temperature() const; |   const optional<float> &get_target_temperature() const; | ||||||
|   const optional<float> &get_target_temperature_low() const; |   const optional<float> &get_target_temperature_low() const; | ||||||
|   const optional<float> &get_target_temperature_high() const; |   const optional<float> &get_target_temperature_high() const; | ||||||
|  |   const optional<float> &get_target_humidity() const; | ||||||
|   const optional<ClimateFanMode> &get_fan_mode() const; |   const optional<ClimateFanMode> &get_fan_mode() const; | ||||||
|   const optional<ClimateSwingMode> &get_swing_mode() const; |   const optional<ClimateSwingMode> &get_swing_mode() const; | ||||||
|   const optional<std::string> &get_custom_fan_mode() const; |   const optional<std::string> &get_custom_fan_mode() const; | ||||||
| @@ -107,6 +112,7 @@ class ClimateCall { | |||||||
|   optional<float> target_temperature_; |   optional<float> target_temperature_; | ||||||
|   optional<float> target_temperature_low_; |   optional<float> target_temperature_low_; | ||||||
|   optional<float> target_temperature_high_; |   optional<float> target_temperature_high_; | ||||||
|  |   optional<float> target_humidity_; | ||||||
|   optional<ClimateFanMode> fan_mode_; |   optional<ClimateFanMode> fan_mode_; | ||||||
|   optional<ClimateSwingMode> swing_mode_; |   optional<ClimateSwingMode> swing_mode_; | ||||||
|   optional<std::string> custom_fan_mode_; |   optional<std::string> custom_fan_mode_; | ||||||
| @@ -136,6 +142,7 @@ struct ClimateDeviceRestoreState { | |||||||
|       float target_temperature_high; |       float target_temperature_high; | ||||||
|     }; |     }; | ||||||
|   }; |   }; | ||||||
|  |   float target_humidity; | ||||||
|  |  | ||||||
|   /// Convert this struct to a climate call that can be performed. |   /// Convert this struct to a climate call that can be performed. | ||||||
|   ClimateCall to_call(Climate *climate); |   ClimateCall to_call(Climate *climate); | ||||||
| @@ -160,24 +167,34 @@ struct ClimateDeviceRestoreState { | |||||||
|  */ |  */ | ||||||
| class Climate : public EntityBase { | class Climate : public EntityBase { | ||||||
|  public: |  public: | ||||||
|  |   Climate() {} | ||||||
|  |  | ||||||
|   /// The active mode of the climate device. |   /// The active mode of the climate device. | ||||||
|   ClimateMode mode{CLIMATE_MODE_OFF}; |   ClimateMode mode{CLIMATE_MODE_OFF}; | ||||||
|  |  | ||||||
|   /// The active state of the climate device. |   /// The active state of the climate device. | ||||||
|   ClimateAction action{CLIMATE_ACTION_OFF}; |   ClimateAction action{CLIMATE_ACTION_OFF}; | ||||||
|  |  | ||||||
|   /// The current temperature of the climate device, as reported from the integration. |   /// The current temperature of the climate device, as reported from the integration. | ||||||
|   float current_temperature{NAN}; |   float current_temperature{NAN}; | ||||||
|  |  | ||||||
|  |   /// The current humidity of the climate device, as reported from the integration. | ||||||
|  |   float current_humidity{NAN}; | ||||||
|  |  | ||||||
|   union { |   union { | ||||||
|     /// The target temperature of the climate device. |     /// The target temperature of the climate device. | ||||||
|     float target_temperature; |     float target_temperature; | ||||||
|     struct { |     struct { | ||||||
|       /// The minimum target temperature of the climate device, for climate devices with split target temperature. |       /// The minimum target temperature of the climate device, for climate devices with split target temperature. | ||||||
|       float target_temperature_low; |       float target_temperature_low{NAN}; | ||||||
|       /// The maximum target temperature of the climate device, for climate devices with split target temperature. |       /// The maximum target temperature of the climate device, for climate devices with split target temperature. | ||||||
|       float target_temperature_high; |       float target_temperature_high{NAN}; | ||||||
|     }; |     }; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   /// The target humidity of the climate device. | ||||||
|  |   float target_humidity; | ||||||
|  |  | ||||||
|   /// The active fan mode of the climate device. |   /// The active fan mode of the climate device. | ||||||
|   optional<ClimateFanMode> fan_mode; |   optional<ClimateFanMode> fan_mode; | ||||||
|  |  | ||||||
| @@ -231,6 +248,8 @@ class Climate : public EntityBase { | |||||||
|   void set_visual_min_temperature_override(float visual_min_temperature_override); |   void set_visual_min_temperature_override(float visual_min_temperature_override); | ||||||
|   void set_visual_max_temperature_override(float visual_max_temperature_override); |   void set_visual_max_temperature_override(float visual_max_temperature_override); | ||||||
|   void set_visual_temperature_step_override(float target, float current); |   void set_visual_temperature_step_override(float target, float current); | ||||||
|  |   void set_visual_min_humidity_override(float visual_min_humidity_override); | ||||||
|  |   void set_visual_max_humidity_override(float visual_max_humidity_override); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   friend ClimateCall; |   friend ClimateCall; | ||||||
| @@ -280,6 +299,8 @@ class Climate : public EntityBase { | |||||||
|   optional<float> visual_max_temperature_override_{}; |   optional<float> visual_max_temperature_override_{}; | ||||||
|   optional<float> visual_target_temperature_step_override_{}; |   optional<float> visual_target_temperature_step_override_{}; | ||||||
|   optional<float> visual_current_temperature_step_override_{}; |   optional<float> visual_current_temperature_step_override_{}; | ||||||
|  |   optional<float> visual_min_humidity_override_{}; | ||||||
|  |   optional<float> visual_max_humidity_override_{}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace climate | }  // namespace climate | ||||||
|   | |||||||
| @@ -44,10 +44,18 @@ class ClimateTraits { | |||||||
|   void set_supports_current_temperature(bool supports_current_temperature) { |   void set_supports_current_temperature(bool supports_current_temperature) { | ||||||
|     supports_current_temperature_ = supports_current_temperature; |     supports_current_temperature_ = supports_current_temperature; | ||||||
|   } |   } | ||||||
|  |   bool get_supports_current_humidity() const { return supports_current_humidity_; } | ||||||
|  |   void set_supports_current_humidity(bool supports_current_humidity) { | ||||||
|  |     supports_current_humidity_ = supports_current_humidity; | ||||||
|  |   } | ||||||
|   bool get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; } |   bool get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; } | ||||||
|   void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { |   void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { | ||||||
|     supports_two_point_target_temperature_ = supports_two_point_target_temperature; |     supports_two_point_target_temperature_ = supports_two_point_target_temperature; | ||||||
|   } |   } | ||||||
|  |   bool get_supports_target_humidity() const { return supports_target_humidity_; } | ||||||
|  |   void set_supports_target_humidity(bool supports_target_humidity) { | ||||||
|  |     supports_target_humidity_ = supports_target_humidity; | ||||||
|  |   } | ||||||
|   void set_supported_modes(std::set<ClimateMode> modes) { supported_modes_ = std::move(modes); } |   void set_supported_modes(std::set<ClimateMode> modes) { supported_modes_ = std::move(modes); } | ||||||
|   void add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); } |   void add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); } | ||||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") |   ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") | ||||||
| @@ -153,6 +161,11 @@ class ClimateTraits { | |||||||
|   int8_t get_target_temperature_accuracy_decimals() const; |   int8_t get_target_temperature_accuracy_decimals() const; | ||||||
|   int8_t get_current_temperature_accuracy_decimals() const; |   int8_t get_current_temperature_accuracy_decimals() const; | ||||||
|  |  | ||||||
|  |   float get_visual_min_humidity() const { return visual_min_humidity_; } | ||||||
|  |   void set_visual_min_humidity(float visual_min_humidity) { visual_min_humidity_ = visual_min_humidity; } | ||||||
|  |   float get_visual_max_humidity() const { return visual_max_humidity_; } | ||||||
|  |   void set_visual_max_humidity(float visual_max_humidity) { visual_max_humidity_ = visual_max_humidity; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   void set_mode_support_(climate::ClimateMode mode, bool supported) { |   void set_mode_support_(climate::ClimateMode mode, bool supported) { | ||||||
|     if (supported) { |     if (supported) { | ||||||
| @@ -177,7 +190,9 @@ class ClimateTraits { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   bool supports_current_temperature_{false}; |   bool supports_current_temperature_{false}; | ||||||
|  |   bool supports_current_humidity_{false}; | ||||||
|   bool supports_two_point_target_temperature_{false}; |   bool supports_two_point_target_temperature_{false}; | ||||||
|  |   bool supports_target_humidity_{false}; | ||||||
|   std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF}; |   std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF}; | ||||||
|   bool supports_action_{false}; |   bool supports_action_{false}; | ||||||
|   std::set<climate::ClimateFanMode> supported_fan_modes_; |   std::set<climate::ClimateFanMode> supported_fan_modes_; | ||||||
| @@ -190,6 +205,8 @@ class ClimateTraits { | |||||||
|   float visual_max_temperature_{30}; |   float visual_max_temperature_{30}; | ||||||
|   float visual_target_temperature_step_{0.1}; |   float visual_target_temperature_step_{0.1}; | ||||||
|   float visual_current_temperature_step_{0.1}; |   float visual_current_temperature_step_{0.1}; | ||||||
|  |   float visual_min_humidity_{30}; | ||||||
|  |   float visual_max_humidity_{99}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace climate | }  // namespace climate | ||||||
|   | |||||||
| @@ -1,38 +1,37 @@ | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.components import ( | from esphome.components import climate, sensor, remote_base | ||||||
|     climate, |  | ||||||
|     remote_transmitter, |  | ||||||
|     remote_receiver, |  | ||||||
|     sensor, |  | ||||||
|     remote_base, |  | ||||||
| ) |  | ||||||
| from esphome.components.remote_base import CONF_RECEIVER_ID, CONF_TRANSMITTER_ID |  | ||||||
| from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR | from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR | ||||||
|  |  | ||||||
|  | DEPENDENCIES = ["remote_transmitter"] | ||||||
| AUTO_LOAD = ["sensor", "remote_base"] | AUTO_LOAD = ["sensor", "remote_base"] | ||||||
| CODEOWNERS = ["@glmnet"] | CODEOWNERS = ["@glmnet"] | ||||||
|  |  | ||||||
| climate_ir_ns = cg.esphome_ns.namespace("climate_ir") | climate_ir_ns = cg.esphome_ns.namespace("climate_ir") | ||||||
| ClimateIR = climate_ir_ns.class_( | ClimateIR = climate_ir_ns.class_( | ||||||
|     "ClimateIR", climate.Climate, cg.Component, remote_base.RemoteReceiverListener |     "ClimateIR", | ||||||
|  |     climate.Climate, | ||||||
|  |     cg.Component, | ||||||
|  |     remote_base.RemoteReceiverListener, | ||||||
|  |     remote_base.RemoteTransmittable, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| CLIMATE_IR_SCHEMA = climate.CLIMATE_SCHEMA.extend( | CLIMATE_IR_SCHEMA = ( | ||||||
|     { |     climate.CLIMATE_SCHEMA.extend( | ||||||
|         cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id( |         { | ||||||
|             remote_transmitter.RemoteTransmitterComponent |             cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, | ||||||
|         ), |             cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, | ||||||
|         cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, |             cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), | ||||||
|         cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, |         } | ||||||
|         cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), |     ) | ||||||
|     } |     .extend(cv.COMPONENT_SCHEMA) | ||||||
| ).extend(cv.COMPONENT_SCHEMA) |     .extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA) | ||||||
|  | ) | ||||||
|  |  | ||||||
| CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend( | CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend( | ||||||
|     { |     { | ||||||
|         cv.Optional(CONF_RECEIVER_ID): cv.use_id( |         cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id( | ||||||
|             remote_receiver.RemoteReceiverComponent |             remote_base.RemoteReceiverBase | ||||||
|         ), |         ), | ||||||
|     } |     } | ||||||
| ) | ) | ||||||
| @@ -41,15 +40,11 @@ CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend( | |||||||
| async def register_climate_ir(var, config): | async def register_climate_ir(var, config): | ||||||
|     await cg.register_component(var, config) |     await cg.register_component(var, config) | ||||||
|     await climate.register_climate(var, config) |     await climate.register_climate(var, config) | ||||||
|  |     await remote_base.register_transmittable(var, config) | ||||||
|     cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) |     cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) | ||||||
|     cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) |     cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) | ||||||
|  |     if remote_base.CONF_RECEIVER_ID in config: | ||||||
|  |         await remote_base.register_listener(var, config) | ||||||
|     if sensor_id := config.get(CONF_SENSOR): |     if sensor_id := config.get(CONF_SENSOR): | ||||||
|         sens = await cg.get_variable(sensor_id) |         sens = await cg.get_variable(sensor_id) | ||||||
|         cg.add(var.set_sensor(sens)) |         cg.add(var.set_sensor(sens)) | ||||||
|     if receiver_id := config.get(CONF_RECEIVER_ID): |  | ||||||
|         receiver = await cg.get_variable(receiver_id) |  | ||||||
|         cg.add(receiver.register_listener(var)) |  | ||||||
|  |  | ||||||
|     transmitter = await cg.get_variable(config[CONF_TRANSMITTER_ID]) |  | ||||||
|     cg.add(var.set_transmitter(transmitter)) |  | ||||||
|   | |||||||
| @@ -18,7 +18,10 @@ namespace climate_ir { | |||||||
|     Likewise to decode a IR into the AC state, implement |     Likewise to decode a IR into the AC state, implement | ||||||
|       bool RemoteReceiverListener::on_receive(remote_base::RemoteReceiveData data) and return true |       bool RemoteReceiverListener::on_receive(remote_base::RemoteReceiveData data) and return true | ||||||
| */ | */ | ||||||
| class ClimateIR : public climate::Climate, public Component, public remote_base::RemoteReceiverListener { | class ClimateIR : public Component, | ||||||
|  |                   public climate::Climate, | ||||||
|  |                   public remote_base::RemoteReceiverListener, | ||||||
|  |                   public remote_base::RemoteTransmittable { | ||||||
|  public: |  public: | ||||||
|   ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, |   ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, | ||||||
|             bool supports_dry = false, bool supports_fan_only = false, std::set<climate::ClimateFanMode> fan_modes = {}, |             bool supports_dry = false, bool supports_fan_only = false, std::set<climate::ClimateFanMode> fan_modes = {}, | ||||||
| @@ -35,9 +38,6 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: | |||||||
|  |  | ||||||
|   void setup() override; |   void setup() override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { |  | ||||||
|     this->transmitter_ = transmitter; |  | ||||||
|   } |  | ||||||
|   void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } |   void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } | ||||||
|   void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } |   void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } | ||||||
|   void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } |   void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } | ||||||
| @@ -64,7 +64,6 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: | |||||||
|   std::set<climate::ClimateSwingMode> swing_modes_ = {}; |   std::set<climate::ClimateSwingMode> swing_modes_ = {}; | ||||||
|   std::set<climate::ClimatePreset> presets_ = {}; |   std::set<climate::ClimatePreset> presets_ = {}; | ||||||
|  |  | ||||||
|   remote_transmitter::RemoteTransmitterComponent *transmitter_; |  | ||||||
|   sensor::Sensor *sensor_{nullptr}; |   sensor::Sensor *sensor_{nullptr}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -102,11 +102,7 @@ void CoolixClimate::transmit_state() { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   ESP_LOGV(TAG, "Sending coolix code: 0x%06" PRIX32, remote_state); |   ESP_LOGV(TAG, "Sending coolix code: 0x%06" PRIX32, remote_state); | ||||||
|  |   this->transmit_<remote_base::CoolixProtocol>(remote_state); | ||||||
|   auto transmit = this->transmitter_->transmit(); |  | ||||||
|   auto *data = transmit.get_data(); |  | ||||||
|   remote_base::CoolixProtocol().encode(data, remote_state); |  | ||||||
|   transmit.perform(); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteReceiveData data) { | bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteReceiveData data) { | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ void CopyFan::setup() { | |||||||
|     this->oscillating = source_->oscillating; |     this->oscillating = source_->oscillating; | ||||||
|     this->speed = source_->speed; |     this->speed = source_->speed; | ||||||
|     this->direction = source_->direction; |     this->direction = source_->direction; | ||||||
|  |     this->preset_mode = source_->preset_mode; | ||||||
|     this->publish_state(); |     this->publish_state(); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
| @@ -19,6 +20,7 @@ void CopyFan::setup() { | |||||||
|   this->oscillating = source_->oscillating; |   this->oscillating = source_->oscillating; | ||||||
|   this->speed = source_->speed; |   this->speed = source_->speed; | ||||||
|   this->direction = source_->direction; |   this->direction = source_->direction; | ||||||
|  |   this->preset_mode = source_->preset_mode; | ||||||
|   this->publish_state(); |   this->publish_state(); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -33,6 +35,7 @@ fan::FanTraits CopyFan::get_traits() { | |||||||
|   traits.set_speed(base.supports_speed()); |   traits.set_speed(base.supports_speed()); | ||||||
|   traits.set_supported_speed_count(base.supported_speed_count()); |   traits.set_supported_speed_count(base.supported_speed_count()); | ||||||
|   traits.set_direction(base.supports_direction()); |   traits.set_direction(base.supports_direction()); | ||||||
|  |   traits.set_supported_preset_modes(base.supported_preset_modes()); | ||||||
|   return traits; |   return traits; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -46,6 +49,8 @@ void CopyFan::control(const fan::FanCall &call) { | |||||||
|     call2.set_speed(*call.get_speed()); |     call2.set_speed(*call.get_speed()); | ||||||
|   if (call.get_direction().has_value()) |   if (call.get_direction().has_value()) | ||||||
|     call2.set_direction(*call.get_direction()); |     call2.set_direction(*call.get_direction()); | ||||||
|  |   if (!call.get_preset_mode().empty()) | ||||||
|  |     call2.set_preset_mode(call.get_preset_mode()); | ||||||
|   call2.perform(); |   call2.perform(); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -58,7 +58,7 @@ BASIC_DISPLAY_SCHEMA = cv.Schema( | |||||||
|     { |     { | ||||||
|         cv.Optional(CONF_LAMBDA): cv.lambda_, |         cv.Optional(CONF_LAMBDA): cv.lambda_, | ||||||
|     } |     } | ||||||
| ) | ).extend(cv.polling_component_schema("1s")) | ||||||
|  |  | ||||||
| FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( | FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( | ||||||
|     { |     { | ||||||
| @@ -116,6 +116,7 @@ async def setup_display_core_(var, config): | |||||||
|  |  | ||||||
|  |  | ||||||
| async def register_display(var, config): | async def register_display(var, config): | ||||||
|  |     await cg.register_component(var, config) | ||||||
|     await setup_display_core_(var, config) |     await setup_display_core_(var, config) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -166,6 +166,13 @@ void Display::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, in | |||||||
| } | } | ||||||
| #endif  // USE_QR_CODE | #endif  // USE_QR_CODE | ||||||
|  |  | ||||||
|  | #ifdef USE_GRAPHICAL_DISPLAY_MENU | ||||||
|  | void Display::menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height) { | ||||||
|  |   Rect rect(x, y, width, height); | ||||||
|  |   menu->draw(this, &rect); | ||||||
|  | } | ||||||
|  | #endif  // USE_GRAPHICAL_DISPLAY_MENU | ||||||
|  |  | ||||||
| void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, | 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 *width, int *height) { | ||||||
|   int x_offset, baseline; |   int x_offset, baseline; | ||||||
|   | |||||||
| @@ -17,6 +17,10 @@ | |||||||
| #include "esphome/components/qr_code/qr_code.h" | #include "esphome/components/qr_code/qr_code.h" | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_GRAPHICAL_DISPLAY_MENU | ||||||
|  | #include "esphome/components/graphical_display_menu/graphical_display_menu.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace display { | namespace display { | ||||||
|  |  | ||||||
| @@ -163,7 +167,7 @@ class BaseFont { | |||||||
|   virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0; |   virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class Display { | class Display : public PollingComponent { | ||||||
|  public: |  public: | ||||||
|   /// Fill the entire screen with the given color. |   /// Fill the entire screen with the given color. | ||||||
|   virtual void fill(Color color); |   virtual void fill(Color color); | ||||||
| @@ -392,6 +396,17 @@ class Display { | |||||||
|   void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1); |   void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1); | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_GRAPHICAL_DISPLAY_MENU | ||||||
|  |   /** | ||||||
|  |    * @param x The x coordinate of the upper left corner | ||||||
|  |    * @param y The y coordinate of the upper left corner | ||||||
|  |    * @param menu The GraphicalDisplayMenu to draw | ||||||
|  |    * @param width Width of the menu | ||||||
|  |    * @param height Height of the menu | ||||||
|  |    */ | ||||||
|  |   void menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height); | ||||||
|  | #endif  // USE_GRAPHICAL_DISPLAY_MENU | ||||||
|  |  | ||||||
|   /** Get the text bounds of the given string. |   /** 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 x The x coordinate to place the string at, can be 0 if only interested in dimensions. | ||||||
|   | |||||||
| @@ -172,6 +172,8 @@ void DisplayMenuComponent::show_main() { | |||||||
|  |  | ||||||
|   this->process_initial_(); |   this->process_initial_(); | ||||||
|  |  | ||||||
|  |   this->on_before_show(); | ||||||
|  |  | ||||||
|   if (this->active_ && this->editing_) |   if (this->active_ && this->editing_) | ||||||
|     this->finish_editing_(); |     this->finish_editing_(); | ||||||
|  |  | ||||||
| @@ -188,6 +190,8 @@ void DisplayMenuComponent::show_main() { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   this->draw_and_update(); |   this->draw_and_update(); | ||||||
|  |  | ||||||
|  |   this->on_after_show(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void DisplayMenuComponent::show() { | void DisplayMenuComponent::show() { | ||||||
| @@ -196,18 +200,26 @@ void DisplayMenuComponent::show() { | |||||||
|  |  | ||||||
|   this->process_initial_(); |   this->process_initial_(); | ||||||
|  |  | ||||||
|  |   this->on_before_show(); | ||||||
|  |  | ||||||
|   if (!this->active_) { |   if (!this->active_) { | ||||||
|     this->active_ = true; |     this->active_ = true; | ||||||
|     this->draw_and_update(); |     this->draw_and_update(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   this->on_after_show(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void DisplayMenuComponent::hide() { | void DisplayMenuComponent::hide() { | ||||||
|   if (this->check_healthy_and_active_()) { |   if (this->check_healthy_and_active_()) { | ||||||
|  |     this->on_before_hide(); | ||||||
|  |  | ||||||
|     if (this->editing_) |     if (this->editing_) | ||||||
|       this->finish_editing_(); |       this->finish_editing_(); | ||||||
|     this->active_ = false; |     this->active_ = false; | ||||||
|     this->update(); |     this->update(); | ||||||
|  |  | ||||||
|  |     this->on_after_hide(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -60,6 +60,11 @@ class DisplayMenuComponent : public Component { | |||||||
|     update(); |     update(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   virtual void on_before_show(){}; | ||||||
|  |   virtual void on_after_show(){}; | ||||||
|  |   virtual void on_before_hide(){}; | ||||||
|  |   virtual void on_after_hide(){}; | ||||||
|  |  | ||||||
|   uint8_t rows_; |   uint8_t rows_; | ||||||
|   bool active_; |   bool active_; | ||||||
|   MenuMode mode_; |   MenuMode mode_; | ||||||
|   | |||||||
| @@ -5,6 +5,29 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace display_menu_base { | namespace display_menu_base { | ||||||
|  |  | ||||||
|  | const LogString *menu_item_type_to_string(MenuItemType type) { | ||||||
|  |   switch (type) { | ||||||
|  |     case MenuItemType::MENU_ITEM_LABEL: | ||||||
|  |       return LOG_STR("MENU_ITEM_LABEL"); | ||||||
|  |     case MenuItemType::MENU_ITEM_MENU: | ||||||
|  |       return LOG_STR("MENU_ITEM_MENU"); | ||||||
|  |     case MenuItemType::MENU_ITEM_BACK: | ||||||
|  |       return LOG_STR("MENU_ITEM_BACK"); | ||||||
|  |     case MenuItemType::MENU_ITEM_SELECT: | ||||||
|  |       return LOG_STR("MENU_ITEM_SELECT"); | ||||||
|  |     case MenuItemType::MENU_ITEM_NUMBER: | ||||||
|  |       return LOG_STR("MENU_ITEM_NUMBER"); | ||||||
|  |     case MenuItemType::MENU_ITEM_SWITCH: | ||||||
|  |       return LOG_STR("MENU_ITEM_SWITCH"); | ||||||
|  |     case MenuItemType::MENU_ITEM_COMMAND: | ||||||
|  |       return LOG_STR("MENU_ITEM_COMMAND"); | ||||||
|  |     case MenuItemType::MENU_ITEM_CUSTOM: | ||||||
|  |       return LOG_STR("MENU_ITEM_CUSTOM"); | ||||||
|  |     default: | ||||||
|  |       return LOG_STR("UNKNOWN"); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| void MenuItem::on_enter() { this->on_enter_callbacks_.call(); } | void MenuItem::on_enter() { this->on_enter_callbacks_.call(); } | ||||||
|  |  | ||||||
| void MenuItem::on_leave() { this->on_leave_callbacks_.call(); } | void MenuItem::on_leave() { this->on_leave_callbacks_.call(); } | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ | |||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #include <vector> | #include <vector> | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace display_menu_base { | namespace display_menu_base { | ||||||
| @@ -29,6 +30,9 @@ enum MenuItemType { | |||||||
|   MENU_ITEM_CUSTOM, |   MENU_ITEM_CUSTOM, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | /// @brief Returns a string representation of a menu item type suitable for logging | ||||||
|  | const LogString *menu_item_type_to_string(MenuItemType type); | ||||||
|  |  | ||||||
| class MenuItem; | class MenuItem; | ||||||
| class MenuItemMenu; | class MenuItemMenu; | ||||||
| using value_getter_t = std::function<std::string(const MenuItem *)>; | using value_getter_t = std::function<std::string(const MenuItem *)>; | ||||||
|   | |||||||
| @@ -12,7 +12,6 @@ ektf2232_ns = cg.esphome_ns.namespace("ektf2232") | |||||||
| EKTF2232Touchscreen = ektf2232_ns.class_( | EKTF2232Touchscreen = ektf2232_ns.class_( | ||||||
|     "EKTF2232Touchscreen", |     "EKTF2232Touchscreen", | ||||||
|     touchscreen.Touchscreen, |     touchscreen.Touchscreen, | ||||||
|     cg.Component, |  | ||||||
|     i2c.I2CDevice, |     i2c.I2CDevice, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @@ -28,17 +27,14 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( | |||||||
|             ), |             ), | ||||||
|             cv.Required(CONF_RTS_PIN): pins.gpio_output_pin_schema, |             cv.Required(CONF_RTS_PIN): pins.gpio_output_pin_schema, | ||||||
|         } |         } | ||||||
|     ) |     ).extend(i2c.i2c_device_schema(0x15)) | ||||||
|     .extend(i2c.i2c_device_schema(0x15)) |  | ||||||
|     .extend(cv.COMPONENT_SCHEMA) |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|     await cg.register_component(var, config) |  | ||||||
|     await i2c.register_i2c_device(var, config) |  | ||||||
|     await touchscreen.register_touchscreen(var, config) |     await touchscreen.register_touchscreen(var, config) | ||||||
|  |     await i2c.register_i2c_device(var, config) | ||||||
| 
 | 
 | ||||||
|     interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) |     interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) | ||||||
|     cg.add(var.set_interrupt_pin(interrupt_pin)) |     cg.add(var.set_interrupt_pin(interrupt_pin)) | ||||||
| @@ -15,16 +15,12 @@ static const uint8_t GET_X_RES[4] = {0x53, 0x60, 0x00, 0x00}; | |||||||
| static const uint8_t GET_Y_RES[4] = {0x53, 0x63, 0x00, 0x00}; | static const uint8_t GET_Y_RES[4] = {0x53, 0x63, 0x00, 0x00}; | ||||||
| static const uint8_t GET_POWER_STATE_CMD[4] = {0x53, 0x50, 0x00, 0x01}; | static const uint8_t GET_POWER_STATE_CMD[4] = {0x53, 0x50, 0x00, 0x01}; | ||||||
| 
 | 
 | ||||||
| void EKTF2232TouchscreenStore::gpio_intr(EKTF2232TouchscreenStore *store) { store->touch = true; } |  | ||||||
| 
 |  | ||||||
| void EKTF2232Touchscreen::setup() { | void EKTF2232Touchscreen::setup() { | ||||||
|   ESP_LOGCONFIG(TAG, "Setting up EKT2232 Touchscreen..."); |   ESP_LOGCONFIG(TAG, "Setting up EKT2232 Touchscreen..."); | ||||||
|   this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); |   this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); | ||||||
|   this->interrupt_pin_->setup(); |   this->interrupt_pin_->setup(); | ||||||
| 
 | 
 | ||||||
|   this->store_.pin = this->interrupt_pin_->to_isr(); |   this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); | ||||||
|   this->interrupt_pin_->attach_interrupt(EKTF2232TouchscreenStore::gpio_intr, &this->store_, |  | ||||||
|                                          gpio::INTERRUPT_FALLING_EDGE); |  | ||||||
| 
 | 
 | ||||||
|   this->rts_pin_->setup(); |   this->rts_pin_->setup(); | ||||||
| 
 | 
 | ||||||
| @@ -45,7 +41,7 @@ void EKTF2232Touchscreen::setup() { | |||||||
|     this->mark_failed(); |     this->mark_failed(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   this->x_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4); |   this->x_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4); | ||||||
| 
 | 
 | ||||||
|   this->write(GET_Y_RES, 4); |   this->write(GET_Y_RES, 4); | ||||||
|   if (this->read(received, 4)) { |   if (this->read(received, 4)) { | ||||||
| @@ -54,19 +50,14 @@ void EKTF2232Touchscreen::setup() { | |||||||
|     this->mark_failed(); |     this->mark_failed(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   this->y_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4); |   this->y_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4); | ||||||
|   this->store_.touch = false; |  | ||||||
| 
 | 
 | ||||||
|   this->set_power_state(true); |   this->set_power_state(true); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void EKTF2232Touchscreen::loop() { | void EKTF2232Touchscreen::update_touches() { | ||||||
|   if (!this->store_.touch) |  | ||||||
|     return; |  | ||||||
|   this->store_.touch = false; |  | ||||||
| 
 |  | ||||||
|   uint8_t touch_count = 0; |   uint8_t touch_count = 0; | ||||||
|   std::vector<TouchPoint> touches; |   int16_t x_raw, y_raw; | ||||||
| 
 | 
 | ||||||
|   uint8_t raw[8]; |   uint8_t raw[8]; | ||||||
|   this->read(raw, 8); |   this->read(raw, 8); | ||||||
| @@ -75,45 +66,15 @@ void EKTF2232Touchscreen::loop() { | |||||||
|       touch_count++; |       touch_count++; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (touch_count == 0) { |  | ||||||
|     for (auto *listener : this->touch_listeners_) |  | ||||||
|       listener->release(); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   touch_count = std::min<uint8_t>(touch_count, 2); |   touch_count = std::min<uint8_t>(touch_count, 2); | ||||||
| 
 | 
 | ||||||
|   ESP_LOGV(TAG, "Touch count: %d", touch_count); |   ESP_LOGV(TAG, "Touch count: %d", touch_count); | ||||||
| 
 | 
 | ||||||
|   for (int i = 0; i < touch_count; i++) { |   for (int i = 0; i < touch_count; i++) { | ||||||
|     uint8_t *d = raw + 1 + (i * 3); |     uint8_t *d = raw + 1 + (i * 3); | ||||||
|     uint32_t raw_x = (d[0] & 0xF0) << 4 | d[1]; |     x_raw = (d[0] & 0xF0) << 4 | d[1]; | ||||||
|     uint32_t raw_y = (d[0] & 0x0F) << 8 | d[2]; |     y_raw = (d[0] & 0x0F) << 8 | d[2]; | ||||||
| 
 |     this->set_raw_touch_position_(i, x_raw, y_raw); | ||||||
|     raw_x = raw_x * this->display_height_ - 1; |  | ||||||
|     raw_y = raw_y * this->display_width_ - 1; |  | ||||||
| 
 |  | ||||||
|     TouchPoint tp; |  | ||||||
|     switch (this->rotation_) { |  | ||||||
|       case ROTATE_0_DEGREES: |  | ||||||
|         tp.y = raw_x / this->x_resolution_; |  | ||||||
|         tp.x = this->display_width_ - 1 - (raw_y / this->y_resolution_); |  | ||||||
|         break; |  | ||||||
|       case ROTATE_90_DEGREES: |  | ||||||
|         tp.x = raw_x / this->x_resolution_; |  | ||||||
|         tp.y = raw_y / this->y_resolution_; |  | ||||||
|         break; |  | ||||||
|       case ROTATE_180_DEGREES: |  | ||||||
|         tp.y = this->display_height_ - 1 - (raw_x / this->x_resolution_); |  | ||||||
|         tp.x = raw_y / this->y_resolution_; |  | ||||||
|         break; |  | ||||||
|       case ROTATE_270_DEGREES: |  | ||||||
|         tp.x = this->display_height_ - 1 - (raw_x / this->x_resolution_); |  | ||||||
|         tp.y = this->display_width_ - 1 - (raw_y / this->y_resolution_); |  | ||||||
|         break; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     this->defer([this, tp]() { this->send_touch_(tp); }); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @@ -126,7 +87,7 @@ void EKTF2232Touchscreen::set_power_state(bool enable) { | |||||||
| bool EKTF2232Touchscreen::get_power_state() { | bool EKTF2232Touchscreen::get_power_state() { | ||||||
|   uint8_t received[4]; |   uint8_t received[4]; | ||||||
|   this->write(GET_POWER_STATE_CMD, 4); |   this->write(GET_POWER_STATE_CMD, 4); | ||||||
|   this->store_.touch = false; |   this->store_.touched = false; | ||||||
|   this->read(received, 4); |   this->read(received, 4); | ||||||
|   return (received[1] >> 3) & 1; |   return (received[1] >> 3) & 1; | ||||||
| } | } | ||||||
| @@ -145,14 +106,14 @@ bool EKTF2232Touchscreen::soft_reset_() { | |||||||
| 
 | 
 | ||||||
|   uint8_t received[4]; |   uint8_t received[4]; | ||||||
|   uint16_t timeout = 1000; |   uint16_t timeout = 1000; | ||||||
|   while (!this->store_.touch && timeout > 0) { |   while (!this->store_.touched && timeout > 0) { | ||||||
|     delay(1); |     delay(1); | ||||||
|     timeout--; |     timeout--; | ||||||
|   } |   } | ||||||
|   if (timeout > 0) |   if (timeout > 0) | ||||||
|     this->store_.touch = true; |     this->store_.touched = true; | ||||||
|   this->read(received, 4); |   this->read(received, 4); | ||||||
|   this->store_.touch = false; |   this->store_.touched = false; | ||||||
| 
 | 
 | ||||||
|   return !memcmp(received, HELLO, 4); |   return !memcmp(received, HELLO, 4); | ||||||
| } | } | ||||||
| @@ -9,19 +9,11 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace ektf2232 { | namespace ektf2232 { | ||||||
| 
 | 
 | ||||||
| struct EKTF2232TouchscreenStore { |  | ||||||
|   volatile bool touch; |  | ||||||
|   ISRInternalGPIOPin pin; |  | ||||||
| 
 |  | ||||||
|   static void gpio_intr(EKTF2232TouchscreenStore *store); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| using namespace touchscreen; | using namespace touchscreen; | ||||||
| 
 | 
 | ||||||
| class EKTF2232Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { | class EKTF2232Touchscreen : public Touchscreen, public i2c::I2CDevice { | ||||||
|  public: |  public: | ||||||
|   void setup() override; |   void setup() override; | ||||||
|   void loop() override; |  | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
| 
 | 
 | ||||||
|   void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } |   void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } | ||||||
| @@ -33,12 +25,10 @@ class EKTF2232Touchscreen : public Touchscreen, public Component, public i2c::I2 | |||||||
|  protected: |  protected: | ||||||
|   void hard_reset_(); |   void hard_reset_(); | ||||||
|   bool soft_reset_(); |   bool soft_reset_(); | ||||||
|  |   void update_touches() override; | ||||||
| 
 | 
 | ||||||
|   InternalGPIOPin *interrupt_pin_; |   InternalGPIOPin *interrupt_pin_; | ||||||
|   GPIOPin *rts_pin_; |   GPIOPin *rts_pin_; | ||||||
|   EKTF2232TouchscreenStore store_; |  | ||||||
|   uint16_t x_resolution_; |  | ||||||
|   uint16_t y_resolution_; |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| }  // namespace ektf2232
 | }  // namespace ektf2232
 | ||||||
							
								
								
									
										1
									
								
								esphome/components/ens160/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/ens160/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | CODEOWNERS = ["@vincentscode"] | ||||||
							
								
								
									
										321
									
								
								esphome/components/ens160/ens160.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										321
									
								
								esphome/components/ens160/ens160.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,321 @@ | |||||||
|  | // ENS160 sensor with I2C interface from ScioSense | ||||||
|  | // | ||||||
|  | // Datasheet: https://www.sciosense.com/wp-content/uploads/documents/SC-001224-DS-7-ENS160-Datasheet.pdf | ||||||
|  | // | ||||||
|  | // Implementation based on: | ||||||
|  | //   https://github.com/sciosense/ENS160_driver | ||||||
|  |  | ||||||
|  | #include "ens160.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace ens160 { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "ens160"; | ||||||
|  |  | ||||||
|  | static const uint8_t ENS160_BOOTING = 10; | ||||||
|  |  | ||||||
|  | static const uint16_t ENS160_PART_ID = 0x0160; | ||||||
|  |  | ||||||
|  | static const uint8_t ENS160_REG_PART_ID = 0x00; | ||||||
|  | static const uint8_t ENS160_REG_OPMODE = 0x10; | ||||||
|  | static const uint8_t ENS160_REG_CONFIG = 0x11; | ||||||
|  | static const uint8_t ENS160_REG_COMMAND = 0x12; | ||||||
|  | static const uint8_t ENS160_REG_TEMP_IN = 0x13; | ||||||
|  | static const uint8_t ENS160_REG_DATA_STATUS = 0x20; | ||||||
|  | static const uint8_t ENS160_REG_DATA_AQI = 0x21; | ||||||
|  | static const uint8_t ENS160_REG_DATA_TVOC = 0x22; | ||||||
|  | static const uint8_t ENS160_REG_DATA_ECO2 = 0x24; | ||||||
|  |  | ||||||
|  | static const uint8_t ENS160_REG_GPR_READ_0 = 0x48; | ||||||
|  | static const uint8_t ENS160_REG_GPR_READ_4 = ENS160_REG_GPR_READ_0 + 4; | ||||||
|  |  | ||||||
|  | static const uint8_t ENS160_COMMAND_NOP = 0x00; | ||||||
|  | static const uint8_t ENS160_COMMAND_CLRGPR = 0xCC; | ||||||
|  | static const uint8_t ENS160_COMMAND_GET_APPVER = 0x0E; | ||||||
|  |  | ||||||
|  | static const uint8_t ENS160_OPMODE_RESET = 0xF0; | ||||||
|  | static const uint8_t ENS160_OPMODE_IDLE = 0x01; | ||||||
|  | static const uint8_t ENS160_OPMODE_STD = 0x02; | ||||||
|  |  | ||||||
|  | static const uint8_t ENS160_DATA_STATUS_STATAS = 0x80; | ||||||
|  | static const uint8_t ENS160_DATA_STATUS_STATER = 0x40; | ||||||
|  | static const uint8_t ENS160_DATA_STATUS_VALIDITY = 0x0C; | ||||||
|  | static const uint8_t ENS160_DATA_STATUS_NEWDAT = 0x02; | ||||||
|  | static const uint8_t ENS160_DATA_STATUS_NEWGPR = 0x01; | ||||||
|  |  | ||||||
|  | // helps remove reserved bits in aqi data register | ||||||
|  | static const uint8_t ENS160_DATA_AQI = 0x07; | ||||||
|  |  | ||||||
|  | void ENS160Component::setup() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "Setting up ENS160..."); | ||||||
|  |  | ||||||
|  |   // check part_id | ||||||
|  |   uint16_t part_id; | ||||||
|  |   if (!this->read_bytes(ENS160_REG_PART_ID, reinterpret_cast<uint8_t *>(&part_id), 2)) { | ||||||
|  |     this->error_code_ = COMMUNICATION_FAILED; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   if (part_id != ENS160_PART_ID) { | ||||||
|  |     this->error_code_ = INVALID_ID; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // set mode to reset | ||||||
|  |   if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_RESET)) { | ||||||
|  |     this->error_code_ = WRITE_FAILED; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   delay(ENS160_BOOTING); | ||||||
|  |  | ||||||
|  |   // check status | ||||||
|  |   uint8_t status_value; | ||||||
|  |   if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) { | ||||||
|  |     this->error_code_ = READ_FAILED; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   this->validity_flag_ = static_cast<ValidityFlag>((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2); | ||||||
|  |  | ||||||
|  |   if (this->validity_flag_ == INVALID_OUTPUT) { | ||||||
|  |     this->error_code_ = VALIDITY_INVALID; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // set mode to idle | ||||||
|  |   if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_IDLE)) { | ||||||
|  |     this->error_code_ = WRITE_FAILED; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   // clear command | ||||||
|  |   if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_NOP)) { | ||||||
|  |     this->error_code_ = WRITE_FAILED; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_CLRGPR)) { | ||||||
|  |     this->error_code_ = WRITE_FAILED; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // read firmware version | ||||||
|  |   if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_GET_APPVER)) { | ||||||
|  |     this->error_code_ = WRITE_FAILED; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   uint8_t version_data[3]; | ||||||
|  |   if (!this->read_bytes(ENS160_REG_GPR_READ_4, version_data, 3)) { | ||||||
|  |     this->error_code_ = READ_FAILED; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   this->firmware_ver_major_ = version_data[0]; | ||||||
|  |   this->firmware_ver_minor_ = version_data[1]; | ||||||
|  |   this->firmware_ver_build_ = version_data[2]; | ||||||
|  |  | ||||||
|  |   // set mode to standard | ||||||
|  |   if (!this->write_byte(ENS160_REG_OPMODE, ENS160_OPMODE_STD)) { | ||||||
|  |     this->error_code_ = WRITE_FAILED; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // read opmode and check standard mode is achieved before finishing Setup | ||||||
|  |   uint8_t op_mode; | ||||||
|  |   if (!this->read_byte(ENS160_REG_OPMODE, &op_mode)) { | ||||||
|  |     this->error_code_ = READ_FAILED; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (op_mode != ENS160_OPMODE_STD) { | ||||||
|  |     this->error_code_ = STD_OPMODE_FAILED; | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ENS160Component::update() { | ||||||
|  |   uint8_t status_value, data_ready; | ||||||
|  |  | ||||||
|  |   if (!this->read_byte(ENS160_REG_DATA_STATUS, &status_value)) { | ||||||
|  |     ESP_LOGW(TAG, "Error reading status register"); | ||||||
|  |     this->status_set_warning(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // verbose status logging | ||||||
|  |   ESP_LOGV(TAG, "Status: ENS160 STATAS bit    0x%x", | ||||||
|  |            (ENS160_DATA_STATUS_STATAS & (status_value)) == ENS160_DATA_STATUS_STATAS); | ||||||
|  |   ESP_LOGV(TAG, "Status: ENS160 STATER bit    0x%x", | ||||||
|  |            (ENS160_DATA_STATUS_STATER & (status_value)) == ENS160_DATA_STATUS_STATER); | ||||||
|  |   ESP_LOGV(TAG, "Status: ENS160 VALIDITY FLAG 0x%02x", (ENS160_DATA_STATUS_VALIDITY & status_value) >> 2); | ||||||
|  |   ESP_LOGV(TAG, "Status: ENS160 NEWDAT bit    0x%x", | ||||||
|  |            (ENS160_DATA_STATUS_NEWDAT & (status_value)) == ENS160_DATA_STATUS_NEWDAT); | ||||||
|  |   ESP_LOGV(TAG, "Status: ENS160 NEWGPR bit    0x%x", | ||||||
|  |            (ENS160_DATA_STATUS_NEWGPR & (status_value)) == ENS160_DATA_STATUS_NEWGPR); | ||||||
|  |  | ||||||
|  |   data_ready = ENS160_DATA_STATUS_NEWDAT & status_value; | ||||||
|  |   this->validity_flag_ = static_cast<ValidityFlag>((ENS160_DATA_STATUS_VALIDITY & status_value) >> 2); | ||||||
|  |  | ||||||
|  |   switch (validity_flag_) { | ||||||
|  |     case NORMAL_OPERATION: | ||||||
|  |       if (data_ready != ENS160_DATA_STATUS_NEWDAT) { | ||||||
|  |         ESP_LOGD(TAG, "ENS160 readings unavailable - Normal Operation but readings not ready"); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     case INITIAL_STARTUP: | ||||||
|  |       if (!this->initial_startup_) { | ||||||
|  |         this->initial_startup_ = true; | ||||||
|  |         ESP_LOGI(TAG, "ENS160 readings unavailable - 1 hour startup required after first power on"); | ||||||
|  |       } | ||||||
|  |       return; | ||||||
|  |     case WARMING_UP: | ||||||
|  |       if (!this->warming_up_) { | ||||||
|  |         this->warming_up_ = true; | ||||||
|  |         ESP_LOGI(TAG, "ENS160 readings not available yet - Warming up requires 3 minutes"); | ||||||
|  |         this->send_env_data_(); | ||||||
|  |       } | ||||||
|  |       return; | ||||||
|  |     case INVALID_OUTPUT: | ||||||
|  |       ESP_LOGE(TAG, "ENS160 Invalid Status - No Invalid Output"); | ||||||
|  |       this->status_set_warning(); | ||||||
|  |       return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // read new data | ||||||
|  |   uint16_t data_eco2; | ||||||
|  |   if (!this->read_bytes(ENS160_REG_DATA_ECO2, reinterpret_cast<uint8_t *>(&data_eco2), 2)) { | ||||||
|  |     ESP_LOGW(TAG, "Error reading eCO2 data register"); | ||||||
|  |     this->status_set_warning(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   if (this->co2_ != nullptr) { | ||||||
|  |     this->co2_->publish_state(data_eco2); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   uint16_t data_tvoc; | ||||||
|  |   if (!this->read_bytes(ENS160_REG_DATA_TVOC, reinterpret_cast<uint8_t *>(&data_tvoc), 2)) { | ||||||
|  |     ESP_LOGW(TAG, "Error reading TVOC data register"); | ||||||
|  |     this->status_set_warning(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   if (this->tvoc_ != nullptr) { | ||||||
|  |     this->tvoc_->publish_state(data_tvoc); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   uint8_t data_aqi; | ||||||
|  |   if (!this->read_byte(ENS160_REG_DATA_AQI, &data_aqi)) { | ||||||
|  |     ESP_LOGW(TAG, "Error reading AQI data register"); | ||||||
|  |     this->status_set_warning(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   if (this->aqi_ != nullptr) { | ||||||
|  |     // remove reserved bits, just in case they are used in future | ||||||
|  |     data_aqi = ENS160_DATA_AQI & data_aqi; | ||||||
|  |  | ||||||
|  |     this->aqi_->publish_state(data_aqi); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   this->status_clear_warning(); | ||||||
|  |  | ||||||
|  |   // set temperature and humidity compensation data | ||||||
|  |   this->send_env_data_(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ENS160Component::send_env_data_() { | ||||||
|  |   if (this->temperature_ == nullptr && this->humidity_ == nullptr) | ||||||
|  |     return; | ||||||
|  |  | ||||||
|  |   float temperature = NAN; | ||||||
|  |   if (this->temperature_ != nullptr) | ||||||
|  |     temperature = this->temperature_->state; | ||||||
|  |  | ||||||
|  |   if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { | ||||||
|  |     ESP_LOGW(TAG, "Invalid external temperature - compensation values not updated"); | ||||||
|  |     return; | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGV(TAG, "External temperature compensation: %.1f°C", temperature); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   float humidity = NAN; | ||||||
|  |   if (this->humidity_ != nullptr) | ||||||
|  |     humidity = this->humidity_->state; | ||||||
|  |  | ||||||
|  |   if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { | ||||||
|  |     ESP_LOGW(TAG, "Invalid external humidity - compensation values not updated"); | ||||||
|  |     return; | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGV(TAG, "External humidity compensation:    %.1f%%", humidity); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   uint16_t t = (uint16_t) ((temperature + 273.15f) * 64.0f); | ||||||
|  |   uint16_t h = (uint16_t) (humidity * 512.0f); | ||||||
|  |  | ||||||
|  |   uint8_t data[4]; | ||||||
|  |   data[0] = t & 0xff; | ||||||
|  |   data[1] = (t >> 8) & 0xff; | ||||||
|  |   data[2] = h & 0xff; | ||||||
|  |   data[3] = (h >> 8) & 0xff; | ||||||
|  |  | ||||||
|  |   if (!this->write_bytes(ENS160_REG_TEMP_IN, data, 4)) { | ||||||
|  |     ESP_LOGE(TAG, "Error writing compensation values"); | ||||||
|  |     this->status_set_warning(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void ENS160Component::dump_config() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "ENS160:"); | ||||||
|  |  | ||||||
|  |   switch (this->error_code_) { | ||||||
|  |     case COMMUNICATION_FAILED: | ||||||
|  |       ESP_LOGE(TAG, "Communication failed! Is the sensor connected?"); | ||||||
|  |       break; | ||||||
|  |     case READ_FAILED: | ||||||
|  |       ESP_LOGE(TAG, "Error reading from register"); | ||||||
|  |       break; | ||||||
|  |     case WRITE_FAILED: | ||||||
|  |       ESP_LOGE(TAG, "Error writing to register"); | ||||||
|  |       break; | ||||||
|  |     case INVALID_ID: | ||||||
|  |       ESP_LOGE(TAG, "Sensor reported an invalid ID. Is this a ENS160?"); | ||||||
|  |       break; | ||||||
|  |     case VALIDITY_INVALID: | ||||||
|  |       ESP_LOGE(TAG, "Invalid Device Status - No valid output"); | ||||||
|  |       break; | ||||||
|  |     case STD_OPMODE_FAILED: | ||||||
|  |       ESP_LOGE(TAG, "Device failed to achieve Standard Operating Mode"); | ||||||
|  |       break; | ||||||
|  |     case NONE: | ||||||
|  |       ESP_LOGD(TAG, "Setup successful"); | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  |   ESP_LOGI(TAG, "Firmware Version: %d.%d.%d", this->firmware_ver_major_, this->firmware_ver_minor_, | ||||||
|  |            this->firmware_ver_build_); | ||||||
|  |  | ||||||
|  |   LOG_I2C_DEVICE(this); | ||||||
|  |   LOG_UPDATE_INTERVAL(this); | ||||||
|  |   LOG_SENSOR("  ", "CO2 Sensor:", this->co2_); | ||||||
|  |   LOG_SENSOR("  ", "TVOC Sensor:", this->tvoc_); | ||||||
|  |   LOG_SENSOR("  ", "AQI Sensor:", this->aqi_); | ||||||
|  |  | ||||||
|  |   if (this->temperature_ != nullptr && this->humidity_ != nullptr) { | ||||||
|  |     LOG_SENSOR("  ", "  Temperature Compensation:", this->temperature_); | ||||||
|  |     LOG_SENSOR("  ", "  Humidity Compensation:", this->humidity_); | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGCONFIG(TAG, "  Compensation: Not configured"); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace ens160 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										60
									
								
								esphome/components/ens160/ens160.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								esphome/components/ens160/ens160.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/components/sensor/sensor.h" | ||||||
|  | #include "esphome/components/i2c/i2c.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace ens160 { | ||||||
|  |  | ||||||
|  | class ENS160Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { | ||||||
|  |  public: | ||||||
|  |   void set_co2(sensor::Sensor *co2) { co2_ = co2; } | ||||||
|  |   void set_tvoc(sensor::Sensor *tvoc) { tvoc_ = tvoc; } | ||||||
|  |   void set_aqi(sensor::Sensor *aqi) { aqi_ = aqi; } | ||||||
|  |  | ||||||
|  |   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } | ||||||
|  |   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } | ||||||
|  |  | ||||||
|  |   void setup() override; | ||||||
|  |   void update() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::DATA; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void send_env_data_(); | ||||||
|  |  | ||||||
|  |   enum ErrorCode { | ||||||
|  |     NONE = 0, | ||||||
|  |     COMMUNICATION_FAILED, | ||||||
|  |     INVALID_ID, | ||||||
|  |     VALIDITY_INVALID, | ||||||
|  |     READ_FAILED, | ||||||
|  |     WRITE_FAILED, | ||||||
|  |     STD_OPMODE_FAILED, | ||||||
|  |   } error_code_{NONE}; | ||||||
|  |  | ||||||
|  |   enum ValidityFlag { | ||||||
|  |     NORMAL_OPERATION = 0, | ||||||
|  |     WARMING_UP, | ||||||
|  |     INITIAL_STARTUP, | ||||||
|  |     INVALID_OUTPUT, | ||||||
|  |   } validity_flag_; | ||||||
|  |  | ||||||
|  |   bool warming_up_{false}; | ||||||
|  |   bool initial_startup_{false}; | ||||||
|  |  | ||||||
|  |   uint8_t firmware_ver_major_{0}; | ||||||
|  |   uint8_t firmware_ver_minor_{0}; | ||||||
|  |   uint8_t firmware_ver_build_{0}; | ||||||
|  |  | ||||||
|  |   sensor::Sensor *co2_{nullptr}; | ||||||
|  |   sensor::Sensor *tvoc_{nullptr}; | ||||||
|  |   sensor::Sensor *aqi_{nullptr}; | ||||||
|  |  | ||||||
|  |   sensor::Sensor *humidity_{nullptr}; | ||||||
|  |   sensor::Sensor *temperature_{nullptr}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace ens160 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										88
									
								
								esphome/components/ens160/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								esphome/components/ens160/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import i2c, sensor | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_ECO2, | ||||||
|  |     CONF_HUMIDITY, | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_TEMPERATURE, | ||||||
|  |     CONF_TVOC, | ||||||
|  |     DEVICE_CLASS_AQI, | ||||||
|  |     DEVICE_CLASS_CARBON_DIOXIDE, | ||||||
|  |     DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, | ||||||
|  |     ICON_CHEMICAL_WEAPON, | ||||||
|  |     ICON_MOLECULE_CO2, | ||||||
|  |     ICON_RADIATOR, | ||||||
|  |     STATE_CLASS_MEASUREMENT, | ||||||
|  |     UNIT_PARTS_PER_BILLION, | ||||||
|  |     UNIT_PARTS_PER_MILLION, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@vincentscode"] | ||||||
|  | DEPENDENCIES = ["i2c"] | ||||||
|  |  | ||||||
|  | ens160_ns = cg.esphome_ns.namespace("ens160") | ||||||
|  | ENS160Component = ens160_ns.class_( | ||||||
|  |     "ENS160Component", cg.PollingComponent, i2c.I2CDevice, sensor.Sensor | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONF_AQI = "aqi" | ||||||
|  | CONF_COMPENSATION = "compensation" | ||||||
|  | UNIT_INDEX = "index" | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = ( | ||||||
|  |     cv.Schema( | ||||||
|  |         { | ||||||
|  |             cv.GenerateID(): cv.declare_id(ENS160Component), | ||||||
|  |             cv.Required(CONF_ECO2): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_PARTS_PER_MILLION, | ||||||
|  |                 icon=ICON_MOLECULE_CO2, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_CARBON_DIOXIDE, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Required(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, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Required(CONF_AQI): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_INDEX, | ||||||
|  |                 icon=ICON_CHEMICAL_WEAPON, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_AQI, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_COMPENSATION): cv.Schema( | ||||||
|  |                 { | ||||||
|  |                     cv.Required(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), | ||||||
|  |                     cv.Required(CONF_HUMIDITY): cv.use_id(sensor.Sensor), | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     .extend(cv.polling_component_schema("60s")) | ||||||
|  |     .extend(i2c.i2c_device_schema(0x53)) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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) | ||||||
|  |  | ||||||
|  |     sens = await sensor.new_sensor(config[CONF_ECO2]) | ||||||
|  |     cg.add(var.set_co2(sens)) | ||||||
|  |     sens = await sensor.new_sensor(config[CONF_TVOC]) | ||||||
|  |     cg.add(var.set_tvoc(sens)) | ||||||
|  |     sens = await sensor.new_sensor(config[CONF_AQI]) | ||||||
|  |     cg.add(var.set_aqi(sens)) | ||||||
|  |  | ||||||
|  |     if CONF_COMPENSATION in config: | ||||||
|  |         compensation_config = config[CONF_COMPENSATION] | ||||||
|  |         sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE]) | ||||||
|  |         cg.add(var.set_temperature(sens)) | ||||||
|  |         sens = await cg.get_variable(compensation_config[CONF_HUMIDITY]) | ||||||
|  |         cg.add(var.set_humidity(sens)) | ||||||
| @@ -462,7 +462,7 @@ async def to_code(config): | |||||||
|  |  | ||||||
|     add_extra_script( |     add_extra_script( | ||||||
|         "post", |         "post", | ||||||
|         "post_build2.py", |         "post_build.py", | ||||||
|         os.path.join(os.path.dirname(__file__), "post_build.py.script"), |         os.path.join(os.path.dirname(__file__), "post_build.py.script"), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| @@ -497,10 +497,11 @@ async def to_code(config): | |||||||
|         add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) |         add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) | ||||||
|         add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) |         add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) | ||||||
|  |  | ||||||
|  |         cg.add_platformio_option("board_build.partitions", "partitions.csv") | ||||||
|         if CONF_PARTITIONS in config: |         if CONF_PARTITIONS in config: | ||||||
|             cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) |             add_extra_build_file( | ||||||
|         else: |                 "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) | ||||||
|             cg.add_platformio_option("board_build.partitions", "partitions.csv") |             ) | ||||||
|  |  | ||||||
|         for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): |         for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): | ||||||
|             add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) |             add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) | ||||||
| @@ -639,20 +640,22 @@ def _write_sdkconfig(): | |||||||
| # Called by writer.py | # Called by writer.py | ||||||
| def copy_files(): | def copy_files(): | ||||||
|     if CORE.using_arduino: |     if CORE.using_arduino: | ||||||
|         write_file_if_changed( |         if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: | ||||||
|             CORE.relative_build_path("partitions.csv"), |             write_file_if_changed( | ||||||
|             get_arduino_partition_csv( |                 CORE.relative_build_path("partitions.csv"), | ||||||
|                 CORE.platformio_options.get("board_upload.flash_size") |                 get_arduino_partition_csv( | ||||||
|             ), |                     CORE.platformio_options.get("board_upload.flash_size") | ||||||
|         ) |                 ), | ||||||
|  |             ) | ||||||
|     if CORE.using_esp_idf: |     if CORE.using_esp_idf: | ||||||
|         _write_sdkconfig() |         _write_sdkconfig() | ||||||
|         write_file_if_changed( |         if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: | ||||||
|             CORE.relative_build_path("partitions.csv"), |             write_file_if_changed( | ||||||
|             get_idf_partition_csv( |                 CORE.relative_build_path("partitions.csv"), | ||||||
|                 CORE.platformio_options.get("board_upload.flash_size") |                 get_idf_partition_csv( | ||||||
|             ), |                     CORE.platformio_options.get("board_upload.flash_size") | ||||||
|         ) |                 ), | ||||||
|  |             ) | ||||||
|         # IDF build scripts look for version string to put in the build. |         # IDF build scripts look for version string to put in the build. | ||||||
|         # However, if the build path does not have an initialized git repo, |         # However, if the build path does not have an initialized git repo, | ||||||
|         # and no version.txt file exists, the CMake script fails for some setups. |         # and no version.txt file exists, the CMake script fails for some setups. | ||||||
|   | |||||||
| @@ -3,15 +3,13 @@ from typing import Any | |||||||
|  |  | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_INPUT, |  | ||||||
|     CONF_INVERTED, |     CONF_INVERTED, | ||||||
|     CONF_MODE, |     CONF_MODE, | ||||||
|     CONF_NUMBER, |     CONF_NUMBER, | ||||||
|     CONF_OPEN_DRAIN, |     CONF_OPEN_DRAIN, | ||||||
|     CONF_OUTPUT, |     CONF_OUTPUT, | ||||||
|     CONF_PULLDOWN, |  | ||||||
|     CONF_PULLUP, |  | ||||||
|     CONF_IGNORE_STRAPPING_WARNING, |     CONF_IGNORE_STRAPPING_WARNING, | ||||||
|  |     PLATFORM_ESP32, | ||||||
| ) | ) | ||||||
| from esphome import pins | from esphome import pins | ||||||
| from esphome.core import CORE | from esphome.core import CORE | ||||||
| @@ -33,7 +31,6 @@ from .const import ( | |||||||
|     esp32_ns, |     esp32_ns, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports | from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports | ||||||
| from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports | from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports | ||||||
| from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports | from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports | ||||||
| @@ -42,7 +39,6 @@ from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_support | |||||||
| from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports | from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports | ||||||
| from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports | from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports | ||||||
|  |  | ||||||
|  |  | ||||||
| ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin) | ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -161,33 +157,22 @@ DRIVE_STRENGTHS = { | |||||||
| } | } | ||||||
| gpio_num_t = cg.global_ns.enum("gpio_num_t") | gpio_num_t = cg.global_ns.enum("gpio_num_t") | ||||||
|  |  | ||||||
|  |  | ||||||
| CONF_DRIVE_STRENGTH = "drive_strength" | CONF_DRIVE_STRENGTH = "drive_strength" | ||||||
| ESP32_PIN_SCHEMA = cv.All( | ESP32_PIN_SCHEMA = cv.All( | ||||||
|     { |     pins.gpio_base_schema(ESP32InternalGPIOPin, validate_gpio_pin).extend( | ||||||
|         cv.GenerateID(): cv.declare_id(ESP32InternalGPIOPin), |         { | ||||||
|         cv.Required(CONF_NUMBER): validate_gpio_pin, |             cv.Optional(CONF_IGNORE_STRAPPING_WARNING, default=False): cv.boolean, | ||||||
|         cv.Optional(CONF_MODE, default={}): cv.Schema( |             cv.Optional(CONF_DRIVE_STRENGTH, default="20mA"): cv.All( | ||||||
|             { |                 cv.float_with_unit("current", "mA", optional_unit=True), | ||||||
|                 cv.Optional(CONF_INPUT, default=False): cv.boolean, |                 cv.enum(DRIVE_STRENGTHS), | ||||||
|                 cv.Optional(CONF_OUTPUT, default=False): cv.boolean, |             ), | ||||||
|                 cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, |         } | ||||||
|                 cv.Optional(CONF_PULLUP, default=False): cv.boolean, |     ), | ||||||
|                 cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, |  | ||||||
|             } |  | ||||||
|         ), |  | ||||||
|         cv.Optional(CONF_INVERTED, default=False): cv.boolean, |  | ||||||
|         cv.Optional(CONF_IGNORE_STRAPPING_WARNING, default=False): cv.boolean, |  | ||||||
|         cv.Optional(CONF_DRIVE_STRENGTH, default="20mA"): cv.All( |  | ||||||
|             cv.float_with_unit("current", "mA", optional_unit=True), |  | ||||||
|             cv.enum(DRIVE_STRENGTHS), |  | ||||||
|         ), |  | ||||||
|     }, |  | ||||||
|     validate_supports, |     validate_supports, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @pins.PIN_SCHEMA_REGISTRY.register("esp32", ESP32_PIN_SCHEMA) | @pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP32, ESP32_PIN_SCHEMA) | ||||||
| async def esp32_pin_to_code(config): | async def esp32_pin_to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|     num = config[CONF_NUMBER] |     num = config[CONF_NUMBER] | ||||||
|   | |||||||
| @@ -25,6 +25,11 @@ AUTO_LOAD = ["psram"] | |||||||
|  |  | ||||||
| esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") | esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") | ||||||
| ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) | ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) | ||||||
|  | ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData") | ||||||
|  | # Triggers | ||||||
|  | ESP32CameraImageTrigger = esp32_camera_ns.class_( | ||||||
|  |     "ESP32CameraImageTrigger", automation.Trigger.template() | ||||||
|  | ) | ||||||
| ESP32CameraStreamStartTrigger = esp32_camera_ns.class_( | ESP32CameraStreamStartTrigger = esp32_camera_ns.class_( | ||||||
|     "ESP32CameraStreamStartTrigger", |     "ESP32CameraStreamStartTrigger", | ||||||
|     automation.Trigger.template(), |     automation.Trigger.template(), | ||||||
| @@ -139,6 +144,7 @@ CONF_IDLE_FRAMERATE = "idle_framerate" | |||||||
| # stream trigger | # stream trigger | ||||||
| CONF_ON_STREAM_START = "on_stream_start" | CONF_ON_STREAM_START = "on_stream_start" | ||||||
| CONF_ON_STREAM_STOP = "on_stream_stop" | CONF_ON_STREAM_STOP = "on_stream_stop" | ||||||
|  | CONF_ON_IMAGE = "on_image" | ||||||
|  |  | ||||||
| camera_range_param = cv.int_range(min=-2, max=2) | camera_range_param = cv.int_range(min=-2, max=2) | ||||||
|  |  | ||||||
| @@ -221,6 +227,11 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( | |||||||
|                 ), |                 ), | ||||||
|             } |             } | ||||||
|         ), |         ), | ||||||
|  |         cv.Optional(CONF_ON_IMAGE): automation.validate_automation( | ||||||
|  |             { | ||||||
|  |                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32CameraImageTrigger), | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|     } |     } | ||||||
| ).extend(cv.COMPONENT_SCHEMA) | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
| @@ -289,3 +300,9 @@ async def to_code(config): | |||||||
|     for conf in config.get(CONF_ON_STREAM_STOP, []): |     for conf in config.get(CONF_ON_STREAM_STOP, []): | ||||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|         await automation.build_automation(trigger, [], conf) |         await automation.build_automation(trigger, [], conf) | ||||||
|  |  | ||||||
|  |     for conf in config.get(CONF_ON_IMAGE, []): | ||||||
|  |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|  |         await automation.build_automation( | ||||||
|  |             trigger, [(ESP32CameraImageData, "image")], conf | ||||||
|  |         ) | ||||||
|   | |||||||
| @@ -335,8 +335,8 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) { | |||||||
| } | } | ||||||
|  |  | ||||||
| /* ---------------- public API (specific) ---------------- */ | /* ---------------- public API (specific) ---------------- */ | ||||||
| void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&f) { | void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&callback) { | ||||||
|   this->new_image_callback_.add(std::move(f)); |   this->new_image_callback_.add(std::move(callback)); | ||||||
| } | } | ||||||
| void ESP32Camera::add_stream_start_callback(std::function<void()> &&callback) { | void ESP32Camera::add_stream_start_callback(std::function<void()> &&callback) { | ||||||
|   this->stream_start_callback_.add(std::move(callback)); |   this->stream_start_callback_.add(std::move(callback)); | ||||||
|   | |||||||
| @@ -86,6 +86,11 @@ class CameraImage { | |||||||
|   uint8_t requesters_; |   uint8_t requesters_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | struct CameraImageData { | ||||||
|  |   uint8_t *data; | ||||||
|  |   size_t length; | ||||||
|  | }; | ||||||
|  |  | ||||||
| /* ---------------- CameraImageReader class ---------------- */ | /* ---------------- CameraImageReader class ---------------- */ | ||||||
| class CameraImageReader { | class CameraImageReader { | ||||||
|  public: |  public: | ||||||
| @@ -147,12 +152,12 @@ class ESP32Camera : public Component, public EntityBase { | |||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   float get_setup_priority() const override; |   float get_setup_priority() const override; | ||||||
|   /* public API (specific) */ |   /* public API (specific) */ | ||||||
|   void add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&f); |  | ||||||
|   void start_stream(CameraRequester requester); |   void start_stream(CameraRequester requester); | ||||||
|   void stop_stream(CameraRequester requester); |   void stop_stream(CameraRequester requester); | ||||||
|   void request_image(CameraRequester requester); |   void request_image(CameraRequester requester); | ||||||
|   void update_camera_parameters(); |   void update_camera_parameters(); | ||||||
|  |  | ||||||
|  |   void add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&callback); | ||||||
|   void add_stream_start_callback(std::function<void()> &&callback); |   void add_stream_start_callback(std::function<void()> &&callback); | ||||||
|   void add_stream_stop_callback(std::function<void()> &&callback); |   void add_stream_stop_callback(std::function<void()> &&callback); | ||||||
|  |  | ||||||
| @@ -196,7 +201,7 @@ class ESP32Camera : public Component, public EntityBase { | |||||||
|   uint8_t stream_requesters_{0}; |   uint8_t stream_requesters_{0}; | ||||||
|   QueueHandle_t framebuffer_get_queue_; |   QueueHandle_t framebuffer_get_queue_; | ||||||
|   QueueHandle_t framebuffer_return_queue_; |   QueueHandle_t framebuffer_return_queue_; | ||||||
|   CallbackManager<void(std::shared_ptr<CameraImage>)> new_image_callback_; |   CallbackManager<void(std::shared_ptr<CameraImage>)> new_image_callback_{}; | ||||||
|   CallbackManager<void()> stream_start_callback_{}; |   CallbackManager<void()> stream_start_callback_{}; | ||||||
|   CallbackManager<void()> stream_stop_callback_{}; |   CallbackManager<void()> stream_stop_callback_{}; | ||||||
|  |  | ||||||
| @@ -207,6 +212,18 @@ class ESP32Camera : public Component, public EntityBase { | |||||||
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
| extern ESP32Camera *global_esp32_camera; | extern ESP32Camera *global_esp32_camera; | ||||||
|  |  | ||||||
|  | class ESP32CameraImageTrigger : public Trigger<CameraImageData> { | ||||||
|  |  public: | ||||||
|  |   explicit ESP32CameraImageTrigger(ESP32Camera *parent) { | ||||||
|  |     parent->add_image_callback([this](const std::shared_ptr<esp32_camera::CameraImage> &image) { | ||||||
|  |       CameraImageData camera_image_data{}; | ||||||
|  |       camera_image_data.length = image->get_data_length(); | ||||||
|  |       camera_image_data.data = image->get_data_buffer(); | ||||||
|  |       this->trigger(camera_image_data); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
| class ESP32CameraStreamStartTrigger : public Trigger<> { | class ESP32CameraStreamStartTrigger : public Trigger<> { | ||||||
|  public: |  public: | ||||||
|   explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { |   explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ from esphome.const import ( | |||||||
|     CONF_OUTPUT, |     CONF_OUTPUT, | ||||||
|     CONF_PULLDOWN, |     CONF_PULLDOWN, | ||||||
|     CONF_PULLUP, |     CONF_PULLUP, | ||||||
|  |     PLATFORM_ESP8266, | ||||||
| ) | ) | ||||||
| from esphome import pins | from esphome import pins | ||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, coroutine_with_priority | ||||||
| @@ -21,10 +22,8 @@ import esphome.codegen as cg | |||||||
| from . import boards | from . import boards | ||||||
| from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns | from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns | ||||||
|  |  | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| ESP8266GPIOPin = esp8266_ns.class_("ESP8266GPIOPin", cg.InternalGPIOPin) | ESP8266GPIOPin = esp8266_ns.class_("ESP8266GPIOPin", cg.InternalGPIOPin) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -124,6 +123,8 @@ def validate_supports(value): | |||||||
|         (True, False, False, False, False), |         (True, False, False, False, False), | ||||||
|         # OUTPUT |         # OUTPUT | ||||||
|         (False, True, False, False, False), |         (False, True, False, False, False), | ||||||
|  |         # INPUT and OUTPUT, e.g. for i2c | ||||||
|  |         (True, True, False, False, False), | ||||||
|         # INPUT_PULLUP |         # INPUT_PULLUP | ||||||
|         (True, False, False, True, False), |         (True, False, False, True, False), | ||||||
|         # INPUT_PULLDOWN_16 |         # INPUT_PULLDOWN_16 | ||||||
| @@ -142,21 +143,11 @@ def validate_supports(value): | |||||||
|  |  | ||||||
|  |  | ||||||
| ESP8266_PIN_SCHEMA = cv.All( | ESP8266_PIN_SCHEMA = cv.All( | ||||||
|     { |     pins.gpio_base_schema( | ||||||
|         cv.GenerateID(): cv.declare_id(ESP8266GPIOPin), |         ESP8266GPIOPin, | ||||||
|         cv.Required(CONF_NUMBER): validate_gpio_pin, |         validate_gpio_pin, | ||||||
|         cv.Optional(CONF_MODE, default={}): cv.Schema( |         modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,), | ||||||
|             { |     ), | ||||||
|                 cv.Optional(CONF_ANALOG, default=False): cv.boolean, |  | ||||||
|                 cv.Optional(CONF_INPUT, default=False): cv.boolean, |  | ||||||
|                 cv.Optional(CONF_OUTPUT, default=False): cv.boolean, |  | ||||||
|                 cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, |  | ||||||
|                 cv.Optional(CONF_PULLUP, default=False): cv.boolean, |  | ||||||
|                 cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, |  | ||||||
|             } |  | ||||||
|         ), |  | ||||||
|         cv.Optional(CONF_INVERTED, default=False): cv.boolean, |  | ||||||
|     }, |  | ||||||
|     validate_supports, |     validate_supports, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -167,7 +158,7 @@ class PinInitialState: | |||||||
|     level: int = 255 |     level: int = 255 | ||||||
|  |  | ||||||
|  |  | ||||||
| @pins.PIN_SCHEMA_REGISTRY.register("esp8266", ESP8266_PIN_SCHEMA) | @pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP8266, ESP8266_PIN_SCHEMA) | ||||||
| async def esp8266_pin_to_code(config): | async def esp8266_pin_to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|     num = config[CONF_NUMBER] |     num = config[CONF_NUMBER] | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ from esphome.const import ( | |||||||
|     CONF_ON_SPEED_SET, |     CONF_ON_SPEED_SET, | ||||||
|     CONF_ON_TURN_OFF, |     CONF_ON_TURN_OFF, | ||||||
|     CONF_ON_TURN_ON, |     CONF_ON_TURN_ON, | ||||||
|  |     CONF_ON_PRESET_SET, | ||||||
|     CONF_TRIGGER_ID, |     CONF_TRIGGER_ID, | ||||||
|     CONF_DIRECTION, |     CONF_DIRECTION, | ||||||
|     CONF_RESTORE_MODE, |     CONF_RESTORE_MODE, | ||||||
| @@ -57,6 +58,9 @@ CycleSpeedAction = fan_ns.class_("CycleSpeedAction", automation.Action) | |||||||
| FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template()) | FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template()) | ||||||
| FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template()) | FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", automation.Trigger.template()) | ||||||
| FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template()) | FanSpeedSetTrigger = fan_ns.class_("FanSpeedSetTrigger", automation.Trigger.template()) | ||||||
|  | FanPresetSetTrigger = fan_ns.class_( | ||||||
|  |     "FanPresetSetTrigger", automation.Trigger.template() | ||||||
|  | ) | ||||||
|  |  | ||||||
| FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) | FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) | ||||||
| FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template()) | FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template()) | ||||||
| @@ -101,9 +105,46 @@ FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).exte | |||||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger), |                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanSpeedSetTrigger), | ||||||
|             } |             } | ||||||
|         ), |         ), | ||||||
|  |         cv.Optional(CONF_ON_PRESET_SET): automation.validate_automation( | ||||||
|  |             { | ||||||
|  |                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FanPresetSetTrigger), | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|     } |     } | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | _PRESET_MODES_SCHEMA = cv.All( | ||||||
|  |     cv.ensure_list(cv.string_strict), | ||||||
|  |     cv.Length(min=1), | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_preset_modes(value): | ||||||
|  |     # Check against defined schema | ||||||
|  |     value = _PRESET_MODES_SCHEMA(value) | ||||||
|  |  | ||||||
|  |     # Ensure preset names are unique | ||||||
|  |     errors = [] | ||||||
|  |     presets = set() | ||||||
|  |     for i, preset in enumerate(value): | ||||||
|  |         # If name does not exist yet add it | ||||||
|  |         if preset not in presets: | ||||||
|  |             presets.add(preset) | ||||||
|  |             continue | ||||||
|  |  | ||||||
|  |         # Otherwise it's an error | ||||||
|  |         errors.append( | ||||||
|  |             cv.Invalid( | ||||||
|  |                 f"Found duplicate preset name '{preset}'. Presets must have unique names.", | ||||||
|  |                 [i], | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     if errors: | ||||||
|  |         raise cv.MultipleInvalid(errors) | ||||||
|  |  | ||||||
|  |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
| async def setup_fan_core_(var, config): | async def setup_fan_core_(var, config): | ||||||
|     await setup_entity(var, config) |     await setup_entity(var, config) | ||||||
| @@ -154,6 +195,9 @@ async def setup_fan_core_(var, config): | |||||||
|     for conf in config.get(CONF_ON_SPEED_SET, []): |     for conf in config.get(CONF_ON_SPEED_SET, []): | ||||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|         await automation.build_automation(trigger, [], conf) |         await automation.build_automation(trigger, [], conf) | ||||||
|  |     for conf in config.get(CONF_ON_PRESET_SET, []): | ||||||
|  |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|  |         await automation.build_automation(trigger, [], conf) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def register_fan(var, config): | async def register_fan(var, config): | ||||||
|   | |||||||
| @@ -165,5 +165,23 @@ class FanSpeedSetTrigger : public Trigger<> { | |||||||
|   int last_speed_; |   int last_speed_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | class FanPresetSetTrigger : public Trigger<> { | ||||||
|  |  public: | ||||||
|  |   FanPresetSetTrigger(Fan *state) { | ||||||
|  |     state->add_on_state_callback([this, state]() { | ||||||
|  |       auto preset_mode = state->preset_mode; | ||||||
|  |       auto should_trigger = preset_mode != this->last_preset_mode_; | ||||||
|  |       this->last_preset_mode_ = preset_mode; | ||||||
|  |       if (should_trigger) { | ||||||
|  |         this->trigger(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     this->last_preset_mode_ = state->preset_mode; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   std::string last_preset_mode_; | ||||||
|  | }; | ||||||
|  |  | ||||||
| }  // namespace fan | }  // namespace fan | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -32,9 +32,12 @@ void FanCall::perform() { | |||||||
|   if (this->direction_.has_value()) { |   if (this->direction_.has_value()) { | ||||||
|     ESP_LOGD(TAG, "  Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); |     ESP_LOGD(TAG, "  Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); | ||||||
|   } |   } | ||||||
|  |   if (!this->preset_mode_.empty()) { | ||||||
|  |     ESP_LOGD(TAG, "  Preset Mode: %s", this->preset_mode_.c_str()); | ||||||
|  |   } | ||||||
|   this->parent_.control(*this); |   this->parent_.control(*this); | ||||||
| } | } | ||||||
|  |  | ||||||
| void FanCall::validate_() { | void FanCall::validate_() { | ||||||
|   auto traits = this->parent_.get_traits(); |   auto traits = this->parent_.get_traits(); | ||||||
|  |  | ||||||
| @@ -62,6 +65,15 @@ void FanCall::validate_() { | |||||||
|     ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str()); |     ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str()); | ||||||
|     this->direction_.reset(); |     this->direction_.reset(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   if (!this->preset_mode_.empty()) { | ||||||
|  |     const auto &preset_modes = traits.supported_preset_modes(); | ||||||
|  |     if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { | ||||||
|  |       ESP_LOGW(TAG, "'%s' - This fan does not support preset mode '%s'!", this->parent_.get_name().c_str(), | ||||||
|  |                this->preset_mode_.c_str()); | ||||||
|  |       this->preset_mode_.clear(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| FanCall FanRestoreState::to_call(Fan &fan) { | FanCall FanRestoreState::to_call(Fan &fan) { | ||||||
| @@ -70,6 +82,14 @@ FanCall FanRestoreState::to_call(Fan &fan) { | |||||||
|   call.set_oscillating(this->oscillating); |   call.set_oscillating(this->oscillating); | ||||||
|   call.set_speed(this->speed); |   call.set_speed(this->speed); | ||||||
|   call.set_direction(this->direction); |   call.set_direction(this->direction); | ||||||
|  |  | ||||||
|  |   if (fan.get_traits().supports_preset_modes()) { | ||||||
|  |     // Use stored preset index to get preset name | ||||||
|  |     const auto &preset_modes = fan.get_traits().supported_preset_modes(); | ||||||
|  |     if (this->preset_mode < preset_modes.size()) { | ||||||
|  |       call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|   return call; |   return call; | ||||||
| } | } | ||||||
| void FanRestoreState::apply(Fan &fan) { | void FanRestoreState::apply(Fan &fan) { | ||||||
| @@ -77,6 +97,14 @@ void FanRestoreState::apply(Fan &fan) { | |||||||
|   fan.oscillating = this->oscillating; |   fan.oscillating = this->oscillating; | ||||||
|   fan.speed = this->speed; |   fan.speed = this->speed; | ||||||
|   fan.direction = this->direction; |   fan.direction = this->direction; | ||||||
|  |  | ||||||
|  |   if (fan.get_traits().supports_preset_modes()) { | ||||||
|  |     // Use stored preset index to get preset name | ||||||
|  |     const auto &preset_modes = fan.get_traits().supported_preset_modes(); | ||||||
|  |     if (this->preset_mode < preset_modes.size()) { | ||||||
|  |       fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|   fan.publish_state(); |   fan.publish_state(); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -100,7 +128,9 @@ void Fan::publish_state() { | |||||||
|   if (traits.supports_direction()) { |   if (traits.supports_direction()) { | ||||||
|     ESP_LOGD(TAG, "  Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); |     ESP_LOGD(TAG, "  Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); | ||||||
|   } |   } | ||||||
|  |   if (traits.supports_preset_modes() && !this->preset_mode.empty()) { | ||||||
|  |     ESP_LOGD(TAG, "  Preset Mode: %s", this->preset_mode.c_str()); | ||||||
|  |   } | ||||||
|   this->state_callback_.call(); |   this->state_callback_.call(); | ||||||
|   this->save_state_(); |   this->save_state_(); | ||||||
| } | } | ||||||
| @@ -143,20 +173,36 @@ void Fan::save_state_() { | |||||||
|   state.oscillating = this->oscillating; |   state.oscillating = this->oscillating; | ||||||
|   state.speed = this->speed; |   state.speed = this->speed; | ||||||
|   state.direction = this->direction; |   state.direction = this->direction; | ||||||
|  |  | ||||||
|  |   if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { | ||||||
|  |     const auto &preset_modes = this->get_traits().supported_preset_modes(); | ||||||
|  |     // Store index of current preset mode | ||||||
|  |     auto preset_iterator = preset_modes.find(this->preset_mode); | ||||||
|  |     if (preset_iterator != preset_modes.end()) | ||||||
|  |       state.preset_mode = std::distance(preset_modes.begin(), preset_iterator); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   this->rtc_.save(&state); |   this->rtc_.save(&state); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Fan::dump_traits_(const char *tag, const char *prefix) { | void Fan::dump_traits_(const char *tag, const char *prefix) { | ||||||
|   if (this->get_traits().supports_speed()) { |   auto traits = this->get_traits(); | ||||||
|  |  | ||||||
|  |   if (traits.supports_speed()) { | ||||||
|     ESP_LOGCONFIG(tag, "%s  Speed: YES", prefix); |     ESP_LOGCONFIG(tag, "%s  Speed: YES", prefix); | ||||||
|     ESP_LOGCONFIG(tag, "%s  Speed count: %d", prefix, this->get_traits().supported_speed_count()); |     ESP_LOGCONFIG(tag, "%s  Speed count: %d", prefix, traits.supported_speed_count()); | ||||||
|   } |   } | ||||||
|   if (this->get_traits().supports_oscillation()) { |   if (traits.supports_oscillation()) { | ||||||
|     ESP_LOGCONFIG(tag, "%s  Oscillation: YES", prefix); |     ESP_LOGCONFIG(tag, "%s  Oscillation: YES", prefix); | ||||||
|   } |   } | ||||||
|   if (this->get_traits().supports_direction()) { |   if (traits.supports_direction()) { | ||||||
|     ESP_LOGCONFIG(tag, "%s  Direction: YES", prefix); |     ESP_LOGCONFIG(tag, "%s  Direction: YES", prefix); | ||||||
|   } |   } | ||||||
|  |   if (traits.supports_preset_modes()) { | ||||||
|  |     ESP_LOGCONFIG(tag, "%s  Supported presets:", prefix); | ||||||
|  |     for (const std::string &s : traits.supported_preset_modes()) | ||||||
|  |       ESP_LOGCONFIG(tag, "%s    - %s", prefix, s.c_str()); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| }  // namespace fan | }  // namespace fan | ||||||
|   | |||||||
| @@ -72,6 +72,11 @@ class FanCall { | |||||||
|     return *this; |     return *this; | ||||||
|   } |   } | ||||||
|   optional<FanDirection> get_direction() const { return this->direction_; } |   optional<FanDirection> get_direction() const { return this->direction_; } | ||||||
|  |   FanCall &set_preset_mode(const std::string &preset_mode) { | ||||||
|  |     this->preset_mode_ = preset_mode; | ||||||
|  |     return *this; | ||||||
|  |   } | ||||||
|  |   std::string get_preset_mode() const { return this->preset_mode_; } | ||||||
|  |  | ||||||
|   void perform(); |   void perform(); | ||||||
|  |  | ||||||
| @@ -83,6 +88,7 @@ class FanCall { | |||||||
|   optional<bool> oscillating_; |   optional<bool> oscillating_; | ||||||
|   optional<int> speed_; |   optional<int> speed_; | ||||||
|   optional<FanDirection> direction_{}; |   optional<FanDirection> direction_{}; | ||||||
|  |   std::string preset_mode_{}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| struct FanRestoreState { | struct FanRestoreState { | ||||||
| @@ -90,6 +96,7 @@ struct FanRestoreState { | |||||||
|   int speed; |   int speed; | ||||||
|   bool oscillating; |   bool oscillating; | ||||||
|   FanDirection direction; |   FanDirection direction; | ||||||
|  |   uint8_t preset_mode; | ||||||
|  |  | ||||||
|   /// Convert this struct to a fan call that can be performed. |   /// Convert this struct to a fan call that can be performed. | ||||||
|   FanCall to_call(Fan &fan); |   FanCall to_call(Fan &fan); | ||||||
| @@ -107,6 +114,8 @@ class Fan : public EntityBase { | |||||||
|   int speed{0}; |   int speed{0}; | ||||||
|   /// The current direction of the fan |   /// The current direction of the fan | ||||||
|   FanDirection direction{FanDirection::FORWARD}; |   FanDirection direction{FanDirection::FORWARD}; | ||||||
|  |   // The current preset mode of the fan | ||||||
|  |   std::string preset_mode{}; | ||||||
|  |  | ||||||
|   FanCall turn_on(); |   FanCall turn_on(); | ||||||
|   FanCall turn_off(); |   FanCall turn_off(); | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | #include <set> | ||||||
|  | #include <utility> | ||||||
|  |  | ||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| @@ -25,12 +28,19 @@ class FanTraits { | |||||||
|   bool supports_direction() const { return this->direction_; } |   bool supports_direction() const { return this->direction_; } | ||||||
|   /// Set whether this fan supports changing direction |   /// Set whether this fan supports changing direction | ||||||
|   void set_direction(bool direction) { this->direction_ = direction; } |   void set_direction(bool direction) { this->direction_ = direction; } | ||||||
|  |   /// Return the preset modes supported by the fan. | ||||||
|  |   std::set<std::string> supported_preset_modes() const { return this->preset_modes_; } | ||||||
|  |   /// Set the preset modes supported by the fan. | ||||||
|  |   void set_supported_preset_modes(const std::set<std::string> &preset_modes) { this->preset_modes_ = preset_modes; } | ||||||
|  |   /// Return if preset modes are supported | ||||||
|  |   bool supports_preset_modes() const { return !this->preset_modes_.empty(); } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   bool oscillation_{false}; |   bool oscillation_{false}; | ||||||
|   bool speed_{false}; |   bool speed_{false}; | ||||||
|   bool direction_{false}; |   bool direction_{false}; | ||||||
|   int speed_count_{}; |   int speed_count_{}; | ||||||
|  |   std::set<std::string> preset_modes_{}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace fan | }  // namespace fan | ||||||
|   | |||||||
| @@ -67,13 +67,13 @@ def validate_pillow_installed(value): | |||||||
|     except ImportError as err: |     except ImportError as err: | ||||||
|         raise cv.Invalid( |         raise cv.Invalid( | ||||||
|             "Please install the pillow python package to use this feature. " |             "Please install the pillow python package to use this feature. " | ||||||
|             '(pip install "pillow==10.0.1")' |             '(pip install "pillow==10.1.0")' | ||||||
|         ) from err |         ) from err | ||||||
|  |  | ||||||
|     if version.parse(PIL.__version__) != version.parse("10.0.1"): |     if version.parse(PIL.__version__) != version.parse("10.1.0"): | ||||||
|         raise cv.Invalid( |         raise cv.Invalid( | ||||||
|             "Please update your pillow installation to 10.0.1. " |             "Please update your pillow installation to 10.1.0. " | ||||||
|             '(pip install "pillow==10.0.1")' |             '(pip install "pillow==10.1.0")' | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     return value |     return value | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								esphome/components/ft5x06/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								esphome/components/ft5x06/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@clydebarrow"] | ||||||
|  | DEPENDENCIES = ["i2c"] | ||||||
|  |  | ||||||
|  | ft5x06_ns = cg.esphome_ns.namespace("ft5x06") | ||||||
							
								
								
									
										26
									
								
								esphome/components/ft5x06/touchscreen/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								esphome/components/ft5x06/touchscreen/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  |  | ||||||
|  | from esphome.components import i2c, touchscreen | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  | from .. import ft5x06_ns | ||||||
|  |  | ||||||
|  | FT5x06ButtonListener = ft5x06_ns.class_("FT5x06ButtonListener") | ||||||
|  | FT5x06Touchscreen = ft5x06_ns.class_( | ||||||
|  |     "FT5x06Touchscreen", | ||||||
|  |     touchscreen.Touchscreen, | ||||||
|  |     cg.Component, | ||||||
|  |     i2c.I2CDevice, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(FT5x06Touchscreen), | ||||||
|  |     } | ||||||
|  | ).extend(i2c.i2c_device_schema(0x48)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await i2c.register_i2c_device(var, config) | ||||||
|  |     await touchscreen.register_touchscreen(var, config) | ||||||
							
								
								
									
										124
									
								
								esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								esphome/components/ft5x06/touchscreen/ft5x06_touchscreen.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/components/i2c/i2c.h" | ||||||
|  | #include "esphome/components/touchscreen/touchscreen.h" | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace ft5x06 { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "ft5x06.touchscreen"; | ||||||
|  |  | ||||||
|  | enum VendorId { | ||||||
|  |   FT5X06_ID_UNKNOWN = 0, | ||||||
|  |   FT5X06_ID_1 = 0x51, | ||||||
|  |   FT5X06_ID_2 = 0x11, | ||||||
|  |   FT5X06_ID_3 = 0xCD, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum FTCmd : uint8_t { | ||||||
|  |   FT5X06_MODE_REG = 0x00, | ||||||
|  |   FT5X06_ORIGIN_REG = 0x08, | ||||||
|  |   FT5X06_RESOLUTION_REG = 0x0C, | ||||||
|  |   FT5X06_VENDOR_ID_REG = 0xA8, | ||||||
|  |   FT5X06_TD_STATUS = 0x02, | ||||||
|  |   FT5X06_TOUCH_DATA = 0x03, | ||||||
|  |   FT5X06_I_MODE = 0xA4, | ||||||
|  |   FT5X06_TOUCH_MAX = 0x4C, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum FTMode : uint8_t { | ||||||
|  |   FT5X06_OP_MODE = 0, | ||||||
|  |   FT5X06_SYSINFO_MODE = 0x10, | ||||||
|  |   FT5X06_TEST_MODE = 0x40, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | static const size_t MAX_TOUCHES = 5;  // max number of possible touches reported | ||||||
|  |  | ||||||
|  | class FT5x06Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { | ||||||
|  |  public: | ||||||
|  |   void setup() override { | ||||||
|  |     esph_log_config(TAG, "Setting up FT5x06 Touchscreen..."); | ||||||
|  |     // wait 200ms after reset. | ||||||
|  |     this->set_timeout(200, [this] { this->continue_setup_(); }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void continue_setup_(void) { | ||||||
|  |     uint8_t data[4]; | ||||||
|  |     if (!this->set_mode_(FT5X06_OP_MODE)) | ||||||
|  |       return; | ||||||
|  |  | ||||||
|  |     if (!this->err_check_(this->read_register(FT5X06_VENDOR_ID_REG, data, 1), "Read Vendor ID")) | ||||||
|  |       return; | ||||||
|  |     switch (data[0]) { | ||||||
|  |       case FT5X06_ID_1: | ||||||
|  |       case FT5X06_ID_2: | ||||||
|  |       case FT5X06_ID_3: | ||||||
|  |         this->vendor_id_ = (VendorId) data[0]; | ||||||
|  |         esph_log_d(TAG, "Read vendor ID 0x%X", data[0]); | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       default: | ||||||
|  |         esph_log_e(TAG, "Unknown vendor ID 0x%X", data[0]); | ||||||
|  |         this->mark_failed(); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     // reading the chip registers to get max x/y does not seem to work. | ||||||
|  |     this->x_raw_max_ = this->display_->get_width(); | ||||||
|  |     this->y_raw_max_ = this->display_->get_height(); | ||||||
|  |     esph_log_config(TAG, "FT5x06 Touchscreen setup complete"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void update_touches() override { | ||||||
|  |     uint8_t touch_cnt; | ||||||
|  |     uint8_t data[MAX_TOUCHES][6]; | ||||||
|  |  | ||||||
|  |     if (!this->read_byte(FT5X06_TD_STATUS, &touch_cnt) || touch_cnt > MAX_TOUCHES) { | ||||||
|  |       esph_log_w(TAG, "Failed to read status"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (touch_cnt == 0) | ||||||
|  |       return; | ||||||
|  |  | ||||||
|  |     if (!this->read_bytes(FT5X06_TOUCH_DATA, (uint8_t *) data, touch_cnt * 6)) { | ||||||
|  |       esph_log_w(TAG, "Failed to read touch data"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     for (uint8_t i = 0; i != touch_cnt; i++) { | ||||||
|  |       uint8_t status = data[i][0] >> 6; | ||||||
|  |       uint8_t id = data[i][2] >> 3; | ||||||
|  |       uint16_t x = encode_uint16(data[i][0] & 0x0F, data[i][1]); | ||||||
|  |       uint16_t y = encode_uint16(data[i][2] & 0xF, data[i][3]); | ||||||
|  |  | ||||||
|  |       esph_log_d(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y); | ||||||
|  |       if (status == 0 || status == 2) { | ||||||
|  |         this->set_raw_touch_position_(id, x, y); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void dump_config() override { | ||||||
|  |     esph_log_config(TAG, "FT5x06 Touchscreen:"); | ||||||
|  |     esph_log_config(TAG, "  Address: 0x%02X", this->address_); | ||||||
|  |     esph_log_config(TAG, "  Vendor ID: 0x%X", (int) this->vendor_id_); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   bool err_check_(i2c::ErrorCode err, const char *msg) { | ||||||
|  |     if (err != i2c::ERROR_OK) { | ||||||
|  |       this->mark_failed(); | ||||||
|  |       esph_log_e(TAG, "%s failed - err 0x%X", msg, err); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |   bool set_mode_(FTMode mode) { | ||||||
|  |     return this->err_check_(this->write_register(FT5X06_MODE_REG, (uint8_t *) &mode, 1), "Set mode"); | ||||||
|  |   } | ||||||
|  |   VendorId vendor_id_{FT5X06_ID_UNKNOWN}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace ft5x06 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										1
									
								
								esphome/components/ft63x6/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/ft63x6/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | CODEOWNERS = ["@gpambrozio"] | ||||||
							
								
								
									
										99
									
								
								esphome/components/ft63x6/ft63x6.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								esphome/components/ft63x6/ft63x6.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | |||||||
|  | /**************************************************************************/ | ||||||
|  | /*! | ||||||
|  |   Author: Gustavo Ambrozio | ||||||
|  |   Based on work by: Atsushi Sasaki (https://github.com/aselectroworks/Arduino-FT6336U) | ||||||
|  | */ | ||||||
|  | /**************************************************************************/ | ||||||
|  |  | ||||||
|  | #include "ft63x6.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | // Registers | ||||||
|  | // Reference: https://focuslcds.com/content/FT6236.pdf | ||||||
|  | namespace esphome { | ||||||
|  | namespace ft63x6 { | ||||||
|  |  | ||||||
|  | static const uint8_t FT63X6_ADDR_TOUCH_COUNT = 0x02; | ||||||
|  |  | ||||||
|  | static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05; | ||||||
|  | static const uint8_t FT63X6_ADDR_TOUCH1_X = 0x03; | ||||||
|  | static const uint8_t FT63X6_ADDR_TOUCH1_Y = 0x05; | ||||||
|  |  | ||||||
|  | static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B; | ||||||
|  | static const uint8_t FT63X6_ADDR_TOUCH2_X = 0x09; | ||||||
|  | static const uint8_t FT63X6_ADDR_TOUCH2_Y = 0x0B; | ||||||
|  |  | ||||||
|  | static const char *const TAG = "FT63X6Touchscreen"; | ||||||
|  |  | ||||||
|  | void FT63X6Touchscreen::setup() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "Setting up FT63X6Touchscreen Touchscreen..."); | ||||||
|  |   if (this->interrupt_pin_ != nullptr) { | ||||||
|  |     this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); | ||||||
|  |     this->interrupt_pin_->setup(); | ||||||
|  |     this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (this->reset_pin_ != nullptr) { | ||||||
|  |     this->reset_pin_->setup(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   this->hard_reset_(); | ||||||
|  |  | ||||||
|  |   // Get touch resolution | ||||||
|  |   this->x_raw_max_ = 320; | ||||||
|  |   this->y_raw_max_ = 480; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void FT63X6Touchscreen::update_touches() { | ||||||
|  |   int touch_count = this->read_touch_count_(); | ||||||
|  |   if (touch_count == 0) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   uint8_t touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH1_ID);  // id1 = 0 or 1 | ||||||
|  |   int16_t x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_X); | ||||||
|  |   int16_t y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_Y); | ||||||
|  |   this->set_raw_touch_position_(touch_id, x, y); | ||||||
|  |  | ||||||
|  |   if (touch_count >= 2) { | ||||||
|  |     touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH2_ID);  // id2 = 0 or 1(~id1 & 0x01) | ||||||
|  |     x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_X); | ||||||
|  |     y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_Y); | ||||||
|  |     this->set_raw_touch_position_(touch_id, x, y); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void FT63X6Touchscreen::hard_reset_() { | ||||||
|  |   if (this->reset_pin_ != nullptr) { | ||||||
|  |     this->reset_pin_->digital_write(false); | ||||||
|  |     delay(10); | ||||||
|  |     this->reset_pin_->digital_write(true); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void FT63X6Touchscreen::dump_config() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "FT63X6 Touchscreen:"); | ||||||
|  |   LOG_I2C_DEVICE(this); | ||||||
|  |   LOG_PIN("  Interrupt Pin: ", this->interrupt_pin_); | ||||||
|  |   LOG_PIN("  Reset Pin: ", this->reset_pin_); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint8_t FT63X6Touchscreen::read_touch_count_() { return this->read_byte_(FT63X6_ADDR_TOUCH_COUNT); } | ||||||
|  |  | ||||||
|  | // Touch functions | ||||||
|  | uint16_t FT63X6Touchscreen::read_touch_coordinate_(uint8_t coordinate) { | ||||||
|  |   uint8_t read_buf[2]; | ||||||
|  |   read_buf[0] = this->read_byte_(coordinate); | ||||||
|  |   read_buf[1] = this->read_byte_(coordinate + 1); | ||||||
|  |   return ((read_buf[0] & 0x0f) << 8) | read_buf[1]; | ||||||
|  | } | ||||||
|  | uint8_t FT63X6Touchscreen::read_touch_id_(uint8_t id_address) { return this->read_byte_(id_address) >> 4; } | ||||||
|  |  | ||||||
|  | uint8_t FT63X6Touchscreen::read_byte_(uint8_t addr) { | ||||||
|  |   uint8_t byte = 0; | ||||||
|  |   this->read_byte(addr, &byte); | ||||||
|  |   return byte; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace ft63x6 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										41
									
								
								esphome/components/ft63x6/ft63x6.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								esphome/components/ft63x6/ft63x6.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | |||||||
|  | /**************************************************************************/ | ||||||
|  | /*! | ||||||
|  |   Author: Gustavo Ambrozio | ||||||
|  |   Based on work by: Atsushi Sasaki (https://github.com/aselectroworks/Arduino-FT6336U) | ||||||
|  | */ | ||||||
|  | /**************************************************************************/ | ||||||
|  |  | ||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/components/i2c/i2c.h" | ||||||
|  | #include "esphome/components/touchscreen/touchscreen.h" | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace ft63x6 { | ||||||
|  |  | ||||||
|  | using namespace touchscreen; | ||||||
|  |  | ||||||
|  | class FT63X6Touchscreen : public Touchscreen, public i2c::I2CDevice { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |  | ||||||
|  |   void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } | ||||||
|  |   void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void hard_reset_(); | ||||||
|  |   uint8_t read_byte_(uint8_t addr); | ||||||
|  |   void update_touches() override; | ||||||
|  |  | ||||||
|  |   InternalGPIOPin *interrupt_pin_{nullptr}; | ||||||
|  |   GPIOPin *reset_pin_{nullptr}; | ||||||
|  |  | ||||||
|  |   uint8_t read_touch_count_(); | ||||||
|  |   uint16_t read_touch_coordinate_(uint8_t coordinate); | ||||||
|  |   uint8_t read_touch_id_(uint8_t id_address); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace ft63x6 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										44
									
								
								esphome/components/ft63x6/touchscreen.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								esphome/components/ft63x6/touchscreen.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  |  | ||||||
|  | from esphome import pins | ||||||
|  | from esphome.components import i2c, touchscreen | ||||||
|  | from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@gpambrozio"] | ||||||
|  | DEPENDENCIES = ["i2c"] | ||||||
|  |  | ||||||
|  | ft6336u_ns = cg.esphome_ns.namespace("ft63x6") | ||||||
|  | FT63X6Touchscreen = ft6336u_ns.class_( | ||||||
|  |     "FT63X6Touchscreen", | ||||||
|  |     touchscreen.Touchscreen, | ||||||
|  |     i2c.I2CDevice, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONF_FT63X6_ID = "ft63x6_id" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( | ||||||
|  |     cv.Schema( | ||||||
|  |         { | ||||||
|  |             cv.GenerateID(): cv.declare_id(FT63X6Touchscreen), | ||||||
|  |             cv.Optional(CONF_INTERRUPT_PIN): cv.All( | ||||||
|  |                 pins.internal_gpio_input_pin_schema | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, | ||||||
|  |         } | ||||||
|  |     ).extend(i2c.i2c_device_schema(0x38)) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await touchscreen.register_touchscreen(var, config) | ||||||
|  |     await i2c.register_i2c_device(var, config) | ||||||
|  |  | ||||||
|  |     if interrupt_pin_config := config.get(CONF_INTERRUPT_PIN): | ||||||
|  |         interrupt_pin = await cg.gpio_pin_expression(interrupt_pin_config) | ||||||
|  |         cg.add(var.set_interrupt_pin(interrupt_pin)) | ||||||
|  |     if reset_pin_config := config.get(CONF_RESET_PIN): | ||||||
|  |         reset_pin = await cg.gpio_pin_expression(reset_pin_config) | ||||||
|  |         cg.add(var.set_reset_pin(reset_pin)) | ||||||
							
								
								
									
										96
									
								
								esphome/components/graphical_display_menu/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								esphome/components/graphical_display_menu/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import display, font, color | ||||||
|  | from esphome.const import CONF_ID, CONF_TRIGGER_ID | ||||||
|  | from esphome import automation, core | ||||||
|  |  | ||||||
|  | from esphome.components.display_menu_base import ( | ||||||
|  |     DISPLAY_MENU_BASE_SCHEMA, | ||||||
|  |     DisplayMenuComponent, | ||||||
|  |     display_menu_to_code, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONF_DISPLAY = "display" | ||||||
|  | CONF_FONT = "font" | ||||||
|  | CONF_MENU_ITEM_VALUE = "menu_item_value" | ||||||
|  | CONF_FOREGROUND_COLOR = "foreground_color" | ||||||
|  | CONF_BACKGROUND_COLOR = "background_color" | ||||||
|  | CONF_ON_REDRAW = "on_redraw" | ||||||
|  |  | ||||||
|  | graphical_display_menu_ns = cg.esphome_ns.namespace("graphical_display_menu") | ||||||
|  | GraphicalDisplayMenu = graphical_display_menu_ns.class_( | ||||||
|  |     "GraphicalDisplayMenu", DisplayMenuComponent | ||||||
|  | ) | ||||||
|  | GraphicalDisplayMenuConstPtr = GraphicalDisplayMenu.operator("ptr").operator("const") | ||||||
|  | MenuItemValueArguments = graphical_display_menu_ns.struct("MenuItemValueArguments") | ||||||
|  | MenuItemValueArgumentsConstPtr = MenuItemValueArguments.operator("ptr").operator( | ||||||
|  |     "const" | ||||||
|  | ) | ||||||
|  | GraphicalDisplayMenuOnRedrawTrigger = graphical_display_menu_ns.class_( | ||||||
|  |     "GraphicalDisplayMenuOnRedrawTrigger", automation.Trigger | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@MrMDavidson"] | ||||||
|  |  | ||||||
|  | AUTO_LOAD = ["display_menu_base"] | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend( | ||||||
|  |     cv.Schema( | ||||||
|  |         { | ||||||
|  |             cv.GenerateID(): cv.declare_id(GraphicalDisplayMenu), | ||||||
|  |             cv.Optional(CONF_DISPLAY): cv.use_id(display.DisplayBuffer), | ||||||
|  |             cv.Required(CONF_FONT): cv.use_id(font.Font), | ||||||
|  |             cv.Optional(CONF_MENU_ITEM_VALUE): cv.templatable(cv.string), | ||||||
|  |             cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct), | ||||||
|  |             cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct), | ||||||
|  |             cv.Optional(CONF_ON_REDRAW): automation.validate_automation( | ||||||
|  |                 { | ||||||
|  |                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||||
|  |                         GraphicalDisplayMenuOnRedrawTrigger | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |  | ||||||
|  |     if display_config := config.get(CONF_DISPLAY): | ||||||
|  |         drawing_display = await cg.get_variable(display_config) | ||||||
|  |         cg.add(var.set_display(drawing_display)) | ||||||
|  |  | ||||||
|  |     menu_font = await cg.get_variable(config[CONF_FONT]) | ||||||
|  |     cg.add(var.set_font(menu_font)) | ||||||
|  |  | ||||||
|  |     if (menu_item_value_config := config.get(CONF_MENU_ITEM_VALUE, None)) is not None: | ||||||
|  |         if isinstance(menu_item_value_config, core.Lambda): | ||||||
|  |             template_ = await cg.templatable( | ||||||
|  |                 menu_item_value_config, | ||||||
|  |                 [(MenuItemValueArgumentsConstPtr, "it")], | ||||||
|  |                 cg.std_string, | ||||||
|  |             ) | ||||||
|  |             cg.add(var.set_menu_item_value(template_)) | ||||||
|  |         else: | ||||||
|  |             cg.add(var.set_menu_item_value(menu_item_value_config)) | ||||||
|  |  | ||||||
|  |     if foreground_color_config := config.get(CONF_FOREGROUND_COLOR): | ||||||
|  |         foreground_color = await cg.get_variable(foreground_color_config) | ||||||
|  |         cg.add(var.set_foreground_color(foreground_color)) | ||||||
|  |  | ||||||
|  |     if background_color_config := config.get(CONF_BACKGROUND_COLOR): | ||||||
|  |         background_color = await cg.get_variable(background_color_config) | ||||||
|  |         cg.add(var.set_background_color(background_color)) | ||||||
|  |  | ||||||
|  |     for conf in config.get(CONF_ON_REDRAW, []): | ||||||
|  |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|  |         await automation.build_automation( | ||||||
|  |             trigger, [(GraphicalDisplayMenuConstPtr, "it")], conf | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     await display_menu_to_code(var, config) | ||||||
|  |  | ||||||
|  |     cg.add_define("USE_GRAPHICAL_DISPLAY_MENU") | ||||||
| @@ -0,0 +1,243 @@ | |||||||
|  | #include "graphical_display_menu.h" | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include <cstdlib> | ||||||
|  | #include "esphome/components/display/display.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace graphical_display_menu { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "graphical_display_menu"; | ||||||
|  |  | ||||||
|  | void GraphicalDisplayMenu::setup() { | ||||||
|  |   if (this->display_ != nullptr) { | ||||||
|  |     display::display_writer_t writer = [this](display::Display &it) { this->draw_menu(); }; | ||||||
|  |     this->display_page_ = make_unique<display::DisplayPage>(writer); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!this->menu_item_value_.has_value()) { | ||||||
|  |     this->menu_item_value_ = [](const MenuItemValueArguments *it) { | ||||||
|  |       std::string label = " "; | ||||||
|  |       if (it->is_item_selected && it->is_menu_editing) { | ||||||
|  |         label.append(">"); | ||||||
|  |         label.append(it->item->get_value_text()); | ||||||
|  |         label.append("<"); | ||||||
|  |       } else { | ||||||
|  |         label.append("("); | ||||||
|  |         label.append(it->item->get_value_text()); | ||||||
|  |         label.append(")"); | ||||||
|  |       } | ||||||
|  |       return label; | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   display_menu_base::DisplayMenuComponent::setup(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void GraphicalDisplayMenu::dump_config() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "Graphical Display Menu"); | ||||||
|  |   ESP_LOGCONFIG(TAG, "Has Display: %s", YESNO(this->display_ != nullptr)); | ||||||
|  |   ESP_LOGCONFIG(TAG, "Popup Mode: %s", YESNO(this->display_ != nullptr)); | ||||||
|  |   ESP_LOGCONFIG(TAG, "Advanced Drawing Mode: %s", YESNO(this->display_ == nullptr)); | ||||||
|  |   ESP_LOGCONFIG(TAG, "Has Font: %s", YESNO(this->font_ != nullptr)); | ||||||
|  |   ESP_LOGCONFIG(TAG, "Mode: %s", this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick"); | ||||||
|  |   ESP_LOGCONFIG(TAG, "Active: %s", YESNO(this->active_)); | ||||||
|  |   ESP_LOGCONFIG(TAG, "Menu items:"); | ||||||
|  |   for (size_t i = 0; i < this->displayed_item_->items_size(); i++) { | ||||||
|  |     auto *item = this->displayed_item_->get_item(i); | ||||||
|  |     ESP_LOGCONFIG(TAG, "  %i: %s (Type: %s, Immediate Edit: %s)", i, item->get_text().c_str(), | ||||||
|  |                   LOG_STR_ARG(display_menu_base::menu_item_type_to_string(item->get_type())), | ||||||
|  |                   YESNO(item->get_immediate_edit())); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void GraphicalDisplayMenu::set_display(display::Display *display) { this->display_ = display; } | ||||||
|  |  | ||||||
|  | void GraphicalDisplayMenu::set_font(display::BaseFont *font) { this->font_ = font; } | ||||||
|  |  | ||||||
|  | void GraphicalDisplayMenu::set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; } | ||||||
|  | void GraphicalDisplayMenu::set_background_color(Color background_color) { this->background_color_ = background_color; } | ||||||
|  |  | ||||||
|  | void GraphicalDisplayMenu::on_before_show() { | ||||||
|  |   if (this->display_ != nullptr) { | ||||||
|  |     this->previous_display_page_ = this->display_->get_active_page(); | ||||||
|  |     this->display_->show_page(this->display_page_.get()); | ||||||
|  |     this->display_->clear(); | ||||||
|  |   } else { | ||||||
|  |     this->update(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void GraphicalDisplayMenu::on_before_hide() { | ||||||
|  |   if (this->previous_display_page_ != nullptr) { | ||||||
|  |     this->display_->show_page((display::DisplayPage *) this->previous_display_page_); | ||||||
|  |     this->display_->clear(); | ||||||
|  |     this->update(); | ||||||
|  |     this->previous_display_page_ = nullptr; | ||||||
|  |   } else { | ||||||
|  |     this->update(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void GraphicalDisplayMenu::draw_and_update() { | ||||||
|  |   this->update(); | ||||||
|  |  | ||||||
|  |   // If we're in advanced drawing mode we won't have a display and will instead require the update callback to do | ||||||
|  |   // our drawing | ||||||
|  |   if (this->display_ != nullptr) { | ||||||
|  |     draw_menu(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void GraphicalDisplayMenu::draw_menu() { | ||||||
|  |   if (this->display_ == nullptr) { | ||||||
|  |     ESP_LOGE(TAG, "draw_menu() called without a display_. This is only available when using the menu in pop up mode"); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   display::Rect bounds(0, 0, this->display_->get_width(), this->display_->get_height()); | ||||||
|  |   this->draw_menu_internal_(this->display_, &bounds); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void GraphicalDisplayMenu::draw(display::Display *display, const display::Rect *bounds) { | ||||||
|  |   this->draw_menu_internal_(display, bounds); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const display::Rect *bounds) { | ||||||
|  |   int total_height = 0; | ||||||
|  |   int y_padding = 2; | ||||||
|  |   bool scroll_menu_items = false; | ||||||
|  |   std::vector<display::Rect> menu_dimensions; | ||||||
|  |   int number_items_fit_to_screen = 0; | ||||||
|  |   const int max_item_index = this->displayed_item_->items_size() - 1; | ||||||
|  |  | ||||||
|  |   for (size_t i = 0; i <= max_item_index; i++) { | ||||||
|  |     const auto *item = this->displayed_item_->get_item(i); | ||||||
|  |     const bool selected = i == this->cursor_index_; | ||||||
|  |     const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected); | ||||||
|  |  | ||||||
|  |     menu_dimensions.push_back(item_dimensions); | ||||||
|  |     total_height += item_dimensions.h + (i == 0 ? 0 : y_padding); | ||||||
|  |  | ||||||
|  |     if (total_height <= bounds->h) { | ||||||
|  |       number_items_fit_to_screen++; | ||||||
|  |     } else { | ||||||
|  |       // Scroll the display if the selected item or the item immediately after it overflows | ||||||
|  |       if ((selected) || (i == this->cursor_index_ + 1)) { | ||||||
|  |         scroll_menu_items = true; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Determine what items to draw | ||||||
|  |   int first_item_index = 0; | ||||||
|  |   int last_item_index = max_item_index; | ||||||
|  |  | ||||||
|  |   if (number_items_fit_to_screen <= 1) { | ||||||
|  |     // If only one item can fit to the bounds draw the current cursor item | ||||||
|  |     last_item_index = std::min(last_item_index, this->cursor_index_ + 1); | ||||||
|  |     first_item_index = this->cursor_index_; | ||||||
|  |   } else { | ||||||
|  |     if (scroll_menu_items) { | ||||||
|  |       // Attempt to draw the item after the current item (+1 for equality check in the draw loop) | ||||||
|  |       last_item_index = std::min(last_item_index, this->cursor_index_ + 1); | ||||||
|  |  | ||||||
|  |       // Go back through the measurements to determine how many prior items we can fit | ||||||
|  |       int height_left_to_use = bounds->h; | ||||||
|  |       for (int i = last_item_index; i >= 0; i--) { | ||||||
|  |         const display::Rect item_dimensions = menu_dimensions[i]; | ||||||
|  |         height_left_to_use -= (item_dimensions.h + y_padding); | ||||||
|  |  | ||||||
|  |         if (height_left_to_use <= 0) { | ||||||
|  |           // Ran out of space -  this is our first item to draw | ||||||
|  |           first_item_index = i; | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       const int items_to_draw = last_item_index - first_item_index; | ||||||
|  |       // Dont't draw last item partially if it is the selected item | ||||||
|  |       if ((this->cursor_index_ == last_item_index) && (number_items_fit_to_screen <= items_to_draw) && | ||||||
|  |           (first_item_index < max_item_index)) { | ||||||
|  |         first_item_index++; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Render the items into the view port | ||||||
|  |   display->start_clipping(*bounds); | ||||||
|  |  | ||||||
|  |   int y_offset = bounds->y; | ||||||
|  |   for (size_t i = first_item_index; i <= last_item_index; i++) { | ||||||
|  |     const auto *item = this->displayed_item_->get_item(i); | ||||||
|  |     const bool selected = i == this->cursor_index_; | ||||||
|  |     display::Rect dimensions = menu_dimensions[i]; | ||||||
|  |  | ||||||
|  |     dimensions.y = y_offset; | ||||||
|  |     dimensions.x = bounds->x; | ||||||
|  |     this->draw_item(display, item, &dimensions, selected); | ||||||
|  |  | ||||||
|  |     y_offset = dimensions.y + dimensions.h + y_padding; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   display->end_clipping(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | display::Rect GraphicalDisplayMenu::measure_item(display::Display *display, const display_menu_base::MenuItem *item, | ||||||
|  |                                                  const display::Rect *bounds, const bool selected) { | ||||||
|  |   display::Rect dimensions(0, 0, 0, 0); | ||||||
|  |  | ||||||
|  |   if (selected) { | ||||||
|  |     // TODO: Support selection glyph | ||||||
|  |     dimensions.w += 0; | ||||||
|  |     dimensions.h += 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   std::string label = item->get_text(); | ||||||
|  |   if (item->has_value()) { | ||||||
|  |     // Append to label | ||||||
|  |     MenuItemValueArguments args(item, selected, this->editing_); | ||||||
|  |     label.append(this->menu_item_value_.value(&args)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   int x1; | ||||||
|  |   int y1; | ||||||
|  |   int width; | ||||||
|  |   int height; | ||||||
|  |   display->get_text_bounds(0, 0, label.c_str(), this->font_, display::TextAlign::TOP_LEFT, &x1, &y1, &width, &height); | ||||||
|  |  | ||||||
|  |   dimensions.w = std::min((int16_t) width, bounds->w); | ||||||
|  |   dimensions.h = std::min((int16_t) height, bounds->h); | ||||||
|  |  | ||||||
|  |   return dimensions; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline void GraphicalDisplayMenu::draw_item(display::Display *display, const display_menu_base::MenuItem *item, | ||||||
|  |                                             const display::Rect *bounds, const bool selected) { | ||||||
|  |   const auto background_color = selected ? this->foreground_color_ : this->background_color_; | ||||||
|  |   const auto foreground_color = selected ? this->background_color_ : this->foreground_color_; | ||||||
|  |  | ||||||
|  |   // int background_width = std::max(bounds->width, available_width); | ||||||
|  |   int background_width = bounds->w; | ||||||
|  |  | ||||||
|  |   if (selected) { | ||||||
|  |     display->filled_rectangle(bounds->x, bounds->y, background_width, bounds->h, background_color); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   std::string label = item->get_text(); | ||||||
|  |   if (item->has_value()) { | ||||||
|  |     MenuItemValueArguments args(item, selected, this->editing_); | ||||||
|  |     label.append(this->menu_item_value_.value(&args)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   display->print(bounds->x, bounds->y, this->font_, foreground_color, display::TextAlign::TOP_LEFT, label.c_str()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, const uint8_t row, const bool selected) { | ||||||
|  |   ESP_LOGE(TAG, "draw_item(MenuItem *item, uint8_t row, bool selected) called. The graphical_display_menu specific " | ||||||
|  |                 "draw_item should be called."); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(); } | ||||||
|  |  | ||||||
|  | }  // namespace graphical_display_menu | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,84 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/color.h" | ||||||
|  | #include "esphome/components/display_menu_base/display_menu_base.h" | ||||||
|  | #include "esphome/components/display_menu_base/menu_item.h" | ||||||
|  | #include "esphome/core/automation.h" | ||||||
|  | #include <cstdlib> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  |  | ||||||
|  | // forward declare from display namespace | ||||||
|  | namespace display { | ||||||
|  | class Display; | ||||||
|  | class DisplayPage; | ||||||
|  | class BaseFont; | ||||||
|  | class Rect; | ||||||
|  | }  // namespace display | ||||||
|  |  | ||||||
|  | namespace graphical_display_menu { | ||||||
|  |  | ||||||
|  | const Color COLOR_ON(255, 255, 255, 255); | ||||||
|  | const Color COLOR_OFF(0, 0, 0, 0); | ||||||
|  |  | ||||||
|  | struct MenuItemValueArguments { | ||||||
|  |   MenuItemValueArguments(const display_menu_base::MenuItem *item, bool is_item_selected, bool is_menu_editing) { | ||||||
|  |     this->item = item; | ||||||
|  |     this->is_item_selected = is_item_selected; | ||||||
|  |     this->is_menu_editing = is_menu_editing; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const display_menu_base::MenuItem *item; | ||||||
|  |   bool is_item_selected; | ||||||
|  |   bool is_menu_editing; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |  | ||||||
|  |   void set_display(display::Display *display); | ||||||
|  |   void set_font(display::BaseFont *font); | ||||||
|  |   template<typename V> void set_menu_item_value(V menu_item_value) { this->menu_item_value_ = menu_item_value; } | ||||||
|  |   void set_foreground_color(Color foreground_color); | ||||||
|  |   void set_background_color(Color background_color); | ||||||
|  |  | ||||||
|  |   void add_on_redraw_callback(std::function<void()> &&cb) { this->on_redraw_callbacks_.add(std::move(cb)); } | ||||||
|  |  | ||||||
|  |   void draw(display::Display *display, const display::Rect *bounds); | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void draw_and_update() override; | ||||||
|  |   void draw_menu() override; | ||||||
|  |   void draw_menu_internal_(display::Display *display, const display::Rect *bounds); | ||||||
|  |   void draw_item(const display_menu_base::MenuItem *item, uint8_t row, bool selected) override; | ||||||
|  |   virtual display::Rect measure_item(display::Display *display, const display_menu_base::MenuItem *item, | ||||||
|  |                                      const display::Rect *bounds, bool selected); | ||||||
|  |   virtual void draw_item(display::Display *display, const display_menu_base::MenuItem *item, | ||||||
|  |                          const display::Rect *bounds, bool selected); | ||||||
|  |   void update() override; | ||||||
|  |  | ||||||
|  |   void on_before_show() override; | ||||||
|  |   void on_before_hide() override; | ||||||
|  |  | ||||||
|  |   std::unique_ptr<display::DisplayPage> display_page_{nullptr}; | ||||||
|  |   const display::DisplayPage *previous_display_page_{nullptr}; | ||||||
|  |   display::Display *display_{nullptr}; | ||||||
|  |   display::BaseFont *font_{nullptr}; | ||||||
|  |   TemplatableValue<std::string, const MenuItemValueArguments *> menu_item_value_; | ||||||
|  |   Color foreground_color_{COLOR_ON}; | ||||||
|  |   Color background_color_{COLOR_OFF}; | ||||||
|  |  | ||||||
|  |   CallbackManager<void()> on_redraw_callbacks_{}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class GraphicalDisplayMenuOnRedrawTrigger : public Trigger<const GraphicalDisplayMenu *> { | ||||||
|  |  public: | ||||||
|  |   explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) { | ||||||
|  |     parent->add_on_redraw_callback([this, parent]() { this->trigger(parent); }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace graphical_display_menu | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										6
									
								
								esphome/components/gt911/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								esphome/components/gt911/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@jesserockz", "@clydebarrow"] | ||||||
|  | DEPENDENCIES = ["i2c"] | ||||||
|  |  | ||||||
|  | gt911_ns = cg.esphome_ns.namespace("gt911") | ||||||
							
								
								
									
										31
									
								
								esphome/components/gt911/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								esphome/components/gt911/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import binary_sensor | ||||||
|  | from esphome.const import CONF_INDEX | ||||||
|  |  | ||||||
|  | from .. import gt911_ns | ||||||
|  | from ..touchscreen import GT911Touchscreen, GT911ButtonListener | ||||||
|  |  | ||||||
|  | CONF_GT911_ID = "gt911_id" | ||||||
|  |  | ||||||
|  | GT911Button = gt911_ns.class_( | ||||||
|  |     "GT911Button", | ||||||
|  |     binary_sensor.BinarySensor, | ||||||
|  |     cg.Component, | ||||||
|  |     GT911ButtonListener, | ||||||
|  |     cg.Parented.template(GT911Touchscreen), | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(GT911Button).extend( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(CONF_GT911_ID): cv.use_id(GT911Touchscreen), | ||||||
|  |         cv.Optional(CONF_INDEX, default=0): cv.int_range(min=0, max=3), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = await binary_sensor.new_binary_sensor(config) | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |     await cg.register_parented(var, config[CONF_GT911_ID]) | ||||||
|  |     cg.add(var.set_index(config[CONF_INDEX])) | ||||||
							
								
								
									
										27
									
								
								esphome/components/gt911/binary_sensor/gt911_button.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/gt911/binary_sensor/gt911_button.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | #include "gt911_button.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace gt911 { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "GT911.binary_sensor"; | ||||||
|  |  | ||||||
|  | void GT911Button::setup() { | ||||||
|  |   this->parent_->register_button_listener(this); | ||||||
|  |   this->publish_initial_state(false); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void GT911Button::dump_config() { | ||||||
|  |   LOG_BINARY_SENSOR("", "GT911 Button", this); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Index: %u", this->index_); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void GT911Button::update_button(uint8_t index, bool state) { | ||||||
|  |   if (index != this->index_) | ||||||
|  |     return; | ||||||
|  |  | ||||||
|  |   this->publish_state(state); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace gt911 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										28
									
								
								esphome/components/gt911/binary_sensor/gt911_button.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								esphome/components/gt911/binary_sensor/gt911_button.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/components/binary_sensor/binary_sensor.h" | ||||||
|  | #include "esphome/components/gt911/touchscreen/gt911_touchscreen.h" | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace gt911 { | ||||||
|  |  | ||||||
|  | class GT911Button : public binary_sensor::BinarySensor, | ||||||
|  |                     public Component, | ||||||
|  |                     public GT911ButtonListener, | ||||||
|  |                     public Parented<GT911Touchscreen> { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |  | ||||||
|  |   void set_index(uint8_t index) { this->index_ = index; } | ||||||
|  |  | ||||||
|  |   void update_button(uint8_t index, bool state) override; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   uint8_t index_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace gt911 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										31
									
								
								esphome/components/gt911/touchscreen/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								esphome/components/gt911/touchscreen/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  |  | ||||||
|  | from esphome import pins | ||||||
|  | from esphome.components import i2c, touchscreen | ||||||
|  | from esphome.const import CONF_INTERRUPT_PIN, CONF_ID | ||||||
|  | from .. import gt911_ns | ||||||
|  |  | ||||||
|  |  | ||||||
|  | GT911ButtonListener = gt911_ns.class_("GT911ButtonListener") | ||||||
|  | GT911Touchscreen = gt911_ns.class_( | ||||||
|  |     "GT911Touchscreen", | ||||||
|  |     touchscreen.Touchscreen, | ||||||
|  |     i2c.I2CDevice, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(GT911Touchscreen), | ||||||
|  |         cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, | ||||||
|  |     } | ||||||
|  | ).extend(i2c.i2c_device_schema(0x5D)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await touchscreen.register_touchscreen(var, config) | ||||||
|  |     await i2c.register_i2c_device(var, config) | ||||||
|  |  | ||||||
|  |     if interrupt_pin := config.get(CONF_INTERRUPT_PIN): | ||||||
|  |         cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) | ||||||
							
								
								
									
										111
									
								
								esphome/components/gt911/touchscreen/gt911_touchscreen.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								esphome/components/gt911/touchscreen/gt911_touchscreen.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | |||||||
|  | #include "gt911_touchscreen.h" | ||||||
|  |  | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace gt911 { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "gt911.touchscreen"; | ||||||
|  |  | ||||||
|  | static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E}; | ||||||
|  | static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00}; | ||||||
|  | static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F}; | ||||||
|  | static const uint8_t GET_SWITCHES[2] = {0x80, 0x4D}; | ||||||
|  | static const uint8_t GET_MAX_VALUES[2] = {0x80, 0x48}; | ||||||
|  | static const size_t MAX_TOUCHES = 5;  // max number of possible touches reported | ||||||
|  |  | ||||||
|  | #define ERROR_CHECK(err) \ | ||||||
|  |   if ((err) != i2c::ERROR_OK) { \ | ||||||
|  |     ESP_LOGE(TAG, "Failed to communicate!"); \ | ||||||
|  |     this->status_set_warning(); \ | ||||||
|  |     return; \ | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | void GT911Touchscreen::setup() { | ||||||
|  |   i2c::ErrorCode err; | ||||||
|  |   ESP_LOGCONFIG(TAG, "Setting up GT911 Touchscreen..."); | ||||||
|  |  | ||||||
|  |   // check the configuration of the int line. | ||||||
|  |   uint8_t data[4]; | ||||||
|  |   err = this->write(GET_SWITCHES, 2); | ||||||
|  |   if (err == i2c::ERROR_OK) { | ||||||
|  |     err = this->read(data, 1); | ||||||
|  |     if (err == i2c::ERROR_OK) { | ||||||
|  |       ESP_LOGD(TAG, "Read from switches: 0x%02X", data[0]); | ||||||
|  |       if (this->interrupt_pin_ != nullptr) { | ||||||
|  |         // datasheet says NOT to use pullup/down on the int line. | ||||||
|  |         this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT); | ||||||
|  |         this->interrupt_pin_->setup(); | ||||||
|  |         this->attach_interrupt_(this->interrupt_pin_, | ||||||
|  |                                 (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (err == i2c::ERROR_OK) { | ||||||
|  |     err = this->write(GET_MAX_VALUES, 2); | ||||||
|  |     if (err == i2c::ERROR_OK) { | ||||||
|  |       err = this->read(data, sizeof(data)); | ||||||
|  |       if (err == i2c::ERROR_OK) { | ||||||
|  |         this->x_raw_max_ = encode_uint16(data[1], data[0]); | ||||||
|  |         this->y_raw_max_ = encode_uint16(data[3], data[2]); | ||||||
|  |         esph_log_d(TAG, "Read max_x/max_y %d/%d", this->x_raw_max_, this->y_raw_max_); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (err != i2c::ERROR_OK) { | ||||||
|  |     ESP_LOGE(TAG, "Failed to communicate!"); | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void GT911Touchscreen::update_touches() { | ||||||
|  |   i2c::ErrorCode err; | ||||||
|  |   uint8_t touch_state = 0; | ||||||
|  |   uint8_t data[MAX_TOUCHES + 1][8];  // 8 bytes each for each point, plus extra space for the key byte | ||||||
|  |  | ||||||
|  |   err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE), false); | ||||||
|  |   ERROR_CHECK(err); | ||||||
|  |   err = this->read(&touch_state, 1); | ||||||
|  |   ERROR_CHECK(err); | ||||||
|  |   this->write(CLEAR_TOUCH_STATE, sizeof(CLEAR_TOUCH_STATE)); | ||||||
|  |   uint8_t num_of_touches = touch_state & 0x07; | ||||||
|  |  | ||||||
|  |   if ((touch_state & 0x80) == 0 || num_of_touches > MAX_TOUCHES) { | ||||||
|  |     this->skip_update_ = true;  // skip send touch events, touchscreen is not ready yet. | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (num_of_touches == 0) | ||||||
|  |     return; | ||||||
|  |  | ||||||
|  |   err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false); | ||||||
|  |   ERROR_CHECK(err); | ||||||
|  |   // num_of_touches is guaranteed to be 0..5. Also read the key data | ||||||
|  |   err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1); | ||||||
|  |   ERROR_CHECK(err); | ||||||
|  |  | ||||||
|  |   for (uint8_t i = 0; i != num_of_touches; i++) { | ||||||
|  |     uint16_t id = data[i][0]; | ||||||
|  |     uint16_t x = encode_uint16(data[i][2], data[i][1]); | ||||||
|  |     uint16_t y = encode_uint16(data[i][4], data[i][3]); | ||||||
|  |     this->set_raw_touch_position_(id, x, y); | ||||||
|  |   } | ||||||
|  |   auto keys = data[num_of_touches][0]; | ||||||
|  |   for (size_t i = 0; i != 4; i++) { | ||||||
|  |     for (auto *listener : this->button_listeners_) | ||||||
|  |       listener->update_button(i, (keys & (1 << i)) != 0); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void GT911Touchscreen::dump_config() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "GT911 Touchscreen:"); | ||||||
|  |   LOG_I2C_DEVICE(this); | ||||||
|  |   LOG_PIN("  Interrupt Pin: ", this->interrupt_pin_); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace gt911 | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										32
									
								
								esphome/components/gt911/touchscreen/gt911_touchscreen.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								esphome/components/gt911/touchscreen/gt911_touchscreen.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/components/i2c/i2c.h" | ||||||
|  | #include "esphome/components/touchscreen/touchscreen.h" | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace gt911 { | ||||||
|  |  | ||||||
|  | class GT911ButtonListener { | ||||||
|  |  public: | ||||||
|  |   virtual void update_button(uint8_t index, bool state) = 0; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |  | ||||||
|  |   void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } | ||||||
|  |   void register_button_listener(GT911ButtonListener *listener) { this->button_listeners_.push_back(listener); } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void update_touches() override; | ||||||
|  |  | ||||||
|  |   InternalGPIOPin *interrupt_pin_{}; | ||||||
|  |   std::vector<GT911ButtonListener *> button_listeners_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace gt911 | ||||||
|  | }  // namespace esphome | ||||||
| @@ -38,16 +38,20 @@ PROTOCOL_MIN_TEMPERATURE = 16.0 | |||||||
| PROTOCOL_MAX_TEMPERATURE = 30.0 | PROTOCOL_MAX_TEMPERATURE = 30.0 | ||||||
| PROTOCOL_TARGET_TEMPERATURE_STEP = 1.0 | PROTOCOL_TARGET_TEMPERATURE_STEP = 1.0 | ||||||
| PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5 | PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5 | ||||||
|  | PROTOCOL_CONTROL_PACKET_SIZE = 10 | ||||||
|  |  | ||||||
| CODEOWNERS = ["@paveldn"] | CODEOWNERS = ["@paveldn"] | ||||||
| AUTO_LOAD = ["sensor"] | AUTO_LOAD = ["sensor"] | ||||||
| DEPENDENCIES = ["climate", "uart"] | DEPENDENCIES = ["climate", "uart"] | ||||||
| CONF_WIFI_SIGNAL = "wifi_signal" | CONF_ALTERNATIVE_SWING_CONTROL = "alternative_swing_control" | ||||||
| CONF_ANSWER_TIMEOUT = "answer_timeout" | CONF_ANSWER_TIMEOUT = "answer_timeout" | ||||||
|  | CONF_CONTROL_METHOD = "control_method" | ||||||
|  | CONF_CONTROL_PACKET_SIZE = "control_packet_size" | ||||||
| CONF_DISPLAY = "display" | CONF_DISPLAY = "display" | ||||||
|  | CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" | ||||||
| CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" | CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" | ||||||
| CONF_VERTICAL_AIRFLOW = "vertical_airflow" | CONF_VERTICAL_AIRFLOW = "vertical_airflow" | ||||||
| CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" | CONF_WIFI_SIGNAL = "wifi_signal" | ||||||
|  |  | ||||||
| PROTOCOL_HON = "HON" | PROTOCOL_HON = "HON" | ||||||
| PROTOCOL_SMARTAIR2 = "SMARTAIR2" | PROTOCOL_SMARTAIR2 = "SMARTAIR2" | ||||||
| @@ -107,6 +111,13 @@ SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = { | |||||||
|     "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, |     "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | HonControlMethod = haier_ns.enum("HonControlMethod", True) | ||||||
|  | SUPPORTED_HON_CONTROL_METHODS = { | ||||||
|  |     "MONITOR_ONLY": HonControlMethod.MONITOR_ONLY, | ||||||
|  |     "SET_GROUP_PARAMETERS": HonControlMethod.SET_GROUP_PARAMETERS, | ||||||
|  |     "SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER, | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_visual(config): | def validate_visual(config): | ||||||
|     if CONF_VISUAL in config: |     if CONF_VISUAL in config: | ||||||
| @@ -184,6 +195,9 @@ CONFIG_SCHEMA = cv.All( | |||||||
|             PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend( |             PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend( | ||||||
|                 { |                 { | ||||||
|                     cv.GenerateID(): cv.declare_id(Smartair2Climate), |                     cv.GenerateID(): cv.declare_id(Smartair2Climate), | ||||||
|  |                     cv.Optional( | ||||||
|  |                         CONF_ALTERNATIVE_SWING_CONTROL, default=False | ||||||
|  |                     ): cv.boolean, | ||||||
|                     cv.Optional( |                     cv.Optional( | ||||||
|                         CONF_SUPPORTED_PRESETS, |                         CONF_SUPPORTED_PRESETS, | ||||||
|                         default=list( |                         default=list( | ||||||
| @@ -197,7 +211,15 @@ CONFIG_SCHEMA = cv.All( | |||||||
|             PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend( |             PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend( | ||||||
|                 { |                 { | ||||||
|                     cv.GenerateID(): cv.declare_id(HonClimate), |                     cv.GenerateID(): cv.declare_id(HonClimate), | ||||||
|  |                     cv.Optional( | ||||||
|  |                         CONF_CONTROL_METHOD, default="SET_GROUP_PARAMETERS" | ||||||
|  |                     ): cv.ensure_list( | ||||||
|  |                         cv.enum(SUPPORTED_HON_CONTROL_METHODS, upper=True) | ||||||
|  |                     ), | ||||||
|                     cv.Optional(CONF_BEEPER, default=True): cv.boolean, |                     cv.Optional(CONF_BEEPER, default=True): cv.boolean, | ||||||
|  |                     cv.Optional( | ||||||
|  |                         CONF_CONTROL_PACKET_SIZE, default=PROTOCOL_CONTROL_PACKET_SIZE | ||||||
|  |                     ): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50), | ||||||
|                     cv.Optional( |                     cv.Optional( | ||||||
|                         CONF_SUPPORTED_PRESETS, |                         CONF_SUPPORTED_PRESETS, | ||||||
|                         default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()), |                         default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()), | ||||||
| @@ -408,6 +430,8 @@ async def to_code(config): | |||||||
|     await climate.register_climate(var, config) |     await climate.register_climate(var, config) | ||||||
|  |  | ||||||
|     cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL])) |     cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL])) | ||||||
|  |     if CONF_CONTROL_METHOD in config: | ||||||
|  |         cg.add(var.set_control_method(config[CONF_CONTROL_METHOD])) | ||||||
|     if CONF_BEEPER in config: |     if CONF_BEEPER in config: | ||||||
|         cg.add(var.set_beeper_state(config[CONF_BEEPER])) |         cg.add(var.set_beeper_state(config[CONF_BEEPER])) | ||||||
|     if CONF_DISPLAY in config: |     if CONF_DISPLAY in config: | ||||||
| @@ -423,5 +447,15 @@ async def to_code(config): | |||||||
|         cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) |         cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) | ||||||
|     if CONF_ANSWER_TIMEOUT in config: |     if CONF_ANSWER_TIMEOUT in config: | ||||||
|         cg.add(var.set_answer_timeout(config[CONF_ANSWER_TIMEOUT])) |         cg.add(var.set_answer_timeout(config[CONF_ANSWER_TIMEOUT])) | ||||||
|  |     if CONF_ALTERNATIVE_SWING_CONTROL in config: | ||||||
|  |         cg.add( | ||||||
|  |             var.set_alternative_swing_control(config[CONF_ALTERNATIVE_SWING_CONTROL]) | ||||||
|  |         ) | ||||||
|  |     if CONF_CONTROL_PACKET_SIZE in config: | ||||||
|  |         cg.add( | ||||||
|  |             var.set_extra_control_packet_bytes_size( | ||||||
|  |                 config[CONF_CONTROL_PACKET_SIZE] - PROTOCOL_CONTROL_PACKET_SIZE | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|     # https://github.com/paveldn/HaierProtocol |     # https://github.com/paveldn/HaierProtocol | ||||||
|     cg.add_library("pavlodn/HaierProtocol", "0.9.20") |     cg.add_library("pavlodn/HaierProtocol", "0.9.24") | ||||||
|   | |||||||
| @@ -19,56 +19,45 @@ constexpr size_t STATUS_REQUEST_INTERVAL_MS = 5000; | |||||||
| constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000; | constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000; | ||||||
| constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000; | constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000; | ||||||
| constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400; | 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) { | const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) { | ||||||
|   static const char *phase_names[] = { |   static const char *phase_names[] = { | ||||||
|       "SENDING_INIT_1", |       "SENDING_INIT_1", | ||||||
|       "WAITING_INIT_1_ANSWER", |  | ||||||
|       "SENDING_INIT_2", |       "SENDING_INIT_2", | ||||||
|       "WAITING_INIT_2_ANSWER", |  | ||||||
|       "SENDING_FIRST_STATUS_REQUEST", |       "SENDING_FIRST_STATUS_REQUEST", | ||||||
|       "WAITING_FIRST_STATUS_ANSWER", |  | ||||||
|       "SENDING_ALARM_STATUS_REQUEST", |       "SENDING_ALARM_STATUS_REQUEST", | ||||||
|       "WAITING_ALARM_STATUS_ANSWER", |  | ||||||
|       "IDLE", |       "IDLE", | ||||||
|       "UNKNOWN", |  | ||||||
|       "SENDING_STATUS_REQUEST", |       "SENDING_STATUS_REQUEST", | ||||||
|       "WAITING_STATUS_ANSWER", |  | ||||||
|       "SENDING_UPDATE_SIGNAL_REQUEST", |       "SENDING_UPDATE_SIGNAL_REQUEST", | ||||||
|       "WAITING_UPDATE_SIGNAL_ANSWER", |  | ||||||
|       "SENDING_SIGNAL_LEVEL", |       "SENDING_SIGNAL_LEVEL", | ||||||
|       "WAITING_SIGNAL_LEVEL_ANSWER", |  | ||||||
|       "SENDING_CONTROL", |       "SENDING_CONTROL", | ||||||
|       "WAITING_CONTROL_ANSWER", |       "SENDING_ACTION_COMMAND", | ||||||
|       "SENDING_POWER_ON_COMMAND", |  | ||||||
|       "WAITING_POWER_ON_ANSWER", |  | ||||||
|       "SENDING_POWER_OFF_COMMAND", |  | ||||||
|       "WAITING_POWER_OFF_ANSWER", |  | ||||||
|       "UNKNOWN"  // Should be the last! |       "UNKNOWN"  // Should be the last! | ||||||
|   }; |   }; | ||||||
|  |   static_assert( | ||||||
|  |       (sizeof(phase_names) / sizeof(char *)) == (((int) ProtocolPhases::NUM_PROTOCOL_PHASES) + 1), | ||||||
|  |       "Wrong phase_names array size. Please, make sure that this array is aligned with the enum ProtocolPhases"); | ||||||
|   int phase_index = (int) phase; |   int phase_index = (int) phase; | ||||||
|   if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0)) |   if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0)) | ||||||
|     phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES; |     phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES; | ||||||
|   return phase_names[phase_index]; |   return phase_names[phase_index]; | ||||||
| } | } | ||||||
| #endif |  | ||||||
|  | bool 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; | ||||||
|  | } | ||||||
|  |  | ||||||
| HaierClimateBase::HaierClimateBase() | HaierClimateBase::HaierClimateBase() | ||||||
|     : haier_protocol_(*this), |     : haier_protocol_(*this), | ||||||
|       protocol_phase_(ProtocolPhases::SENDING_INIT_1), |       protocol_phase_(ProtocolPhases::SENDING_INIT_1), | ||||||
|       action_request_(ActionRequest::NO_ACTION), |  | ||||||
|       display_status_(true), |       display_status_(true), | ||||||
|       health_mode_(false), |       health_mode_(false), | ||||||
|       force_send_control_(false), |       force_send_control_(false), | ||||||
|       forced_publish_(false), |  | ||||||
|       forced_request_status_(false), |       forced_request_status_(false), | ||||||
|       first_control_attempt_(false), |  | ||||||
|       reset_protocol_request_(false), |       reset_protocol_request_(false), | ||||||
|       send_wifi_signal_(true) { |       send_wifi_signal_(true), | ||||||
|  |       use_crc_(false) { | ||||||
|   this->traits_ = climate::ClimateTraits(); |   this->traits_ = climate::ClimateTraits(); | ||||||
|   this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, |   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_FAN_ONLY, climate::CLIMATE_MODE_DRY, | ||||||
| @@ -84,42 +73,43 @@ HaierClimateBase::~HaierClimateBase() {} | |||||||
|  |  | ||||||
| void HaierClimateBase::set_phase(ProtocolPhases phase) { | void HaierClimateBase::set_phase(ProtocolPhases phase) { | ||||||
|   if (this->protocol_phase_ != 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)); |     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; |     this->protocol_phase_ = phase; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| bool HaierClimateBase::check_timeout_(std::chrono::steady_clock::time_point now, | void HaierClimateBase::reset_phase_() { | ||||||
|                                       std::chrono::steady_clock::time_point tpoint, size_t timeout) { |   this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE | ||||||
|   return std::chrono::duration_cast<std::chrono::milliseconds>(now - tpoint).count() > timeout; |                                                                   : ProtocolPhases::SENDING_INIT_1); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void HaierClimateBase::reset_to_idle_() { | ||||||
|  |   this->force_send_control_ = false; | ||||||
|  |   if (this->current_hvac_settings_.valid) | ||||||
|  |     this->current_hvac_settings_.reset(); | ||||||
|  |   this->forced_request_status_ = true; | ||||||
|  |   this->set_phase(ProtocolPhases::IDLE); | ||||||
|  |   this->action_request_.reset(); | ||||||
| } | } | ||||||
|  |  | ||||||
| bool HaierClimateBase::is_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { | 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); |   return 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) { | 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); |   return 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) { | 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); |   return check_timeout(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS); | ||||||
| } | } | ||||||
|  |  | ||||||
| bool HaierClimateBase::is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now) { | bool HaierClimateBase::is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now) { | ||||||
|   return this->check_timeout_(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL); |   return check_timeout(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL); | ||||||
| } | } | ||||||
|  |  | ||||||
| #ifdef USE_WIFI | #ifdef USE_WIFI | ||||||
| haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_(uint8_t message_type) { | haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_() { | ||||||
|   static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00}; |   static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00}; | ||||||
|   if (wifi::global_wifi_component->is_connected()) { |   if (wifi::global_wifi_component->is_connected()) { | ||||||
|     wifi_status_data[1] = 0; |     wifi_status_data[1] = 0; | ||||||
| @@ -131,7 +121,8 @@ haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_(uint8_t | |||||||
|     wifi_status_data[1] = 1; |     wifi_status_data[1] = 1; | ||||||
|     wifi_status_data[3] = 0; |     wifi_status_data[3] = 0; | ||||||
|   } |   } | ||||||
|   return haier_protocol::HaierMessage(message_type, wifi_status_data, sizeof(wifi_status_data)); |   return haier_protocol::HaierMessage(haier_protocol::FrameType::REPORT_NETWORK_STATUS, wifi_status_data, | ||||||
|  |                                       sizeof(wifi_status_data)); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| @@ -140,7 +131,7 @@ bool HaierClimateBase::get_display_state() const { return this->display_status_; | |||||||
| void HaierClimateBase::set_display_state(bool state) { | void HaierClimateBase::set_display_state(bool state) { | ||||||
|   if (this->display_status_ != state) { |   if (this->display_status_ != state) { | ||||||
|     this->display_status_ = state; |     this->display_status_ = state; | ||||||
|     this->set_force_send_control_(true); |     this->force_send_control_ = true; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -149,15 +140,24 @@ bool HaierClimateBase::get_health_mode() const { return this->health_mode_; } | |||||||
| void HaierClimateBase::set_health_mode(bool state) { | void HaierClimateBase::set_health_mode(bool state) { | ||||||
|   if (this->health_mode_ != state) { |   if (this->health_mode_ != state) { | ||||||
|     this->health_mode_ = state; |     this->health_mode_ = state; | ||||||
|     this->set_force_send_control_(true); |     this->force_send_control_ = true; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void HaierClimateBase::send_power_on_command() { this->action_request_ = ActionRequest::TURN_POWER_ON; } | void HaierClimateBase::send_power_on_command() { | ||||||
|  |   this->action_request_ = | ||||||
|  |       PendingAction({ActionRequest::TURN_POWER_ON, esphome::optional<haier_protocol::HaierMessage>()}); | ||||||
|  | } | ||||||
|  |  | ||||||
| void HaierClimateBase::send_power_off_command() { this->action_request_ = ActionRequest::TURN_POWER_OFF; } | void HaierClimateBase::send_power_off_command() { | ||||||
|  |   this->action_request_ = | ||||||
|  |       PendingAction({ActionRequest::TURN_POWER_OFF, esphome::optional<haier_protocol::HaierMessage>()}); | ||||||
|  | } | ||||||
|  |  | ||||||
| void HaierClimateBase::toggle_power() { this->action_request_ = ActionRequest::TOGGLE_POWER; } | void HaierClimateBase::toggle_power() { | ||||||
|  |   this->action_request_ = | ||||||
|  |       PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()}); | ||||||
|  | } | ||||||
|  |  | ||||||
| void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) { | void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) { | ||||||
|   this->traits_.set_supported_swing_modes(modes); |   this->traits_.set_supported_swing_modes(modes); | ||||||
| @@ -165,9 +165,7 @@ void HaierClimateBase::set_supported_swing_modes(const std::set<climate::Climate | |||||||
|     this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); |     this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); | ||||||
| } | } | ||||||
|  |  | ||||||
| void HaierClimateBase::set_answer_timeout(uint32_t timeout) { | void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); } | ||||||
|   this->answer_timeout_ = std::chrono::milliseconds(timeout); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) { | void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) { | ||||||
|   this->traits_.set_supported_modes(modes); |   this->traits_.set_supported_modes(modes); | ||||||
| @@ -183,29 +181,42 @@ void HaierClimateBase::set_supported_presets(const std::set<climate::ClimatePres | |||||||
|  |  | ||||||
| void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } | void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } | ||||||
|  |  | ||||||
| haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(uint8_t request_message_type, | void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &message) { | ||||||
|                                                                   uint8_t expected_request_message_type, |   this->action_request_ = PendingAction({ActionRequest::SEND_CUSTOM_COMMAND, message}); | ||||||
|                                                                   uint8_t answer_message_type, | } | ||||||
|                                                                   uint8_t expected_answer_message_type, |  | ||||||
|                                                                   ProtocolPhases expected_phase) { | haier_protocol::HandlerError HaierClimateBase::answer_preprocess_( | ||||||
|  |     haier_protocol::FrameType request_message_type, haier_protocol::FrameType expected_request_message_type, | ||||||
|  |     haier_protocol::FrameType answer_message_type, haier_protocol::FrameType expected_answer_message_type, | ||||||
|  |     ProtocolPhases expected_phase) { | ||||||
|   haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK; |   haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK; | ||||||
|   if ((expected_request_message_type != NO_COMMAND) && (request_message_type != expected_request_message_type)) |   if ((expected_request_message_type != haier_protocol::FrameType::UNKNOWN_FRAME_TYPE) && | ||||||
|  |       (request_message_type != expected_request_message_type)) | ||||||
|     result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; |     result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; | ||||||
|   if ((expected_answer_message_type != NO_COMMAND) && (answer_message_type != expected_answer_message_type)) |   if ((expected_answer_message_type != haier_protocol::FrameType::UNKNOWN_FRAME_TYPE) && | ||||||
|  |       (answer_message_type != expected_answer_message_type)) | ||||||
|     result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; |     result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; | ||||||
|   if ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_)) |   if (!this->haier_protocol_.is_waiting_for_answer() || | ||||||
|  |       ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_))) | ||||||
|     result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE; |     result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE; | ||||||
|   if (is_message_invalid(answer_message_type)) |   if (answer_message_type == haier_protocol::FrameType::INVALID) | ||||||
|     result = haier_protocol::HandlerError::INVALID_ANSWER; |     result = haier_protocol::HandlerError::INVALID_ANSWER; | ||||||
|   return result; |   return result; | ||||||
| } | } | ||||||
|  |  | ||||||
| haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(uint8_t request_type) { | haier_protocol::HandlerError HaierClimateBase::report_network_status_answer_handler_( | ||||||
| #if (HAIER_LOG_LEVEL > 4) |     haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, | ||||||
|   ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", request_type, phase_to_string_(this->protocol_phase_)); |     size_t data_size) { | ||||||
| #else |   haier_protocol::HandlerError result = | ||||||
|   ESP_LOGW(TAG, "Answer timeout for command %02X, phase %d", request_type, (int) this->protocol_phase_); |       this->answer_preprocess_(request_type, haier_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, | ||||||
| #endif |                                haier_protocol::FrameType::CONFIRM, ProtocolPhases::SENDING_SIGNAL_LEVEL); | ||||||
|  |   this->set_phase(ProtocolPhases::IDLE); | ||||||
|  |   return result; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(haier_protocol::FrameType request_type) { | ||||||
|  |   ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) request_type, | ||||||
|  |            phase_to_string_(this->protocol_phase_)); | ||||||
|   if (this->protocol_phase_ > ProtocolPhases::IDLE) { |   if (this->protocol_phase_ > ProtocolPhases::IDLE) { | ||||||
|     this->set_phase(ProtocolPhases::IDLE); |     this->set_phase(ProtocolPhases::IDLE); | ||||||
|   } else { |   } else { | ||||||
| @@ -219,79 +230,95 @@ void HaierClimateBase::setup() { | |||||||
|   // Set timestamp here to give AC time to boot |   // Set timestamp here to give AC time to boot | ||||||
|   this->last_request_timestamp_ = std::chrono::steady_clock::now(); |   this->last_request_timestamp_ = std::chrono::steady_clock::now(); | ||||||
|   this->set_phase(ProtocolPhases::SENDING_INIT_1); |   this->set_phase(ProtocolPhases::SENDING_INIT_1); | ||||||
|   this->set_handlers(); |  | ||||||
|   this->haier_protocol_.set_default_timeout_handler( |   this->haier_protocol_.set_default_timeout_handler( | ||||||
|       std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1)); |       std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1)); | ||||||
|  |   this->set_handlers(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void HaierClimateBase::dump_config() { | void HaierClimateBase::dump_config() { | ||||||
|   LOG_CLIMATE("", "Haier Climate", this); |   LOG_CLIMATE("", "Haier Climate", this); | ||||||
|   ESP_LOGCONFIG(TAG, "  Device communication status: %s", |   ESP_LOGCONFIG(TAG, "  Device communication status: %s", this->valid_connection() ? "established" : "none"); | ||||||
|                 (this->protocol_phase_ >= ProtocolPhases::IDLE) ? "established" : "none"); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void HaierClimateBase::loop() { | void HaierClimateBase::loop() { | ||||||
|   std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); |   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() > |   if ((std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_valid_status_timestamp_).count() > | ||||||
|        COMMUNICATION_TIMEOUT_MS) || |        COMMUNICATION_TIMEOUT_MS) || | ||||||
|       (this->reset_protocol_request_)) { |       (this->reset_protocol_request_ && (!this->haier_protocol_.is_waiting_for_answer()))) { | ||||||
|  |     this->last_valid_status_timestamp_ = now; | ||||||
|     if (this->protocol_phase_ >= ProtocolPhases::IDLE) { |     if (this->protocol_phase_ >= ProtocolPhases::IDLE) { | ||||||
|       // No status too long, reseting protocol |       // No status too long, reseting protocol | ||||||
|  |       // No need to reset protocol if we didn't pass initialization phase | ||||||
|       if (this->reset_protocol_request_) { |       if (this->reset_protocol_request_) { | ||||||
|         this->reset_protocol_request_ = false; |         this->reset_protocol_request_ = false; | ||||||
|         ESP_LOGW(TAG, "Protocol reset requested"); |         ESP_LOGW(TAG, "Protocol reset requested"); | ||||||
|       } else { |       } else { | ||||||
|         ESP_LOGW(TAG, "Communication timeout, reseting protocol"); |         ESP_LOGW(TAG, "Communication timeout, reseting protocol"); | ||||||
|       } |       } | ||||||
|       this->last_valid_status_timestamp_ = now; |       this->process_protocol_reset(); | ||||||
|       this->set_force_send_control_(false); |  | ||||||
|       if (this->hvac_settings_.valid) |  | ||||||
|         this->hvac_settings_.reset(); |  | ||||||
|       this->set_phase(ProtocolPhases::SENDING_INIT_1); |  | ||||||
|       return; |       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) || |   if ((!this->haier_protocol_.is_waiting_for_answer()) && | ||||||
|       (this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) || |       ((this->protocol_phase_ == ProtocolPhases::IDLE) || | ||||||
|       (this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) || |        (this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) || | ||||||
|       (this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL)) { |        (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 |     // If control message or action is pending we should send it ASAP unless we are in initialisation | ||||||
|     // procedure or waiting for an answer |     // procedure or waiting for an answer | ||||||
|     if (this->action_request_ != ActionRequest::NO_ACTION) { |     if (this->action_request_.has_value() && this->prepare_pending_action()) { | ||||||
|       this->process_pending_action(); |       this->set_phase(ProtocolPhases::SENDING_ACTION_COMMAND); | ||||||
|     } else if (this->hvac_settings_.valid || this->force_send_control_) { |     } else if (this->next_hvac_settings_.valid || this->force_send_control_) { | ||||||
|       ESP_LOGV(TAG, "Control packet is pending..."); |       ESP_LOGV(TAG, "Control packet is pending..."); | ||||||
|       this->set_phase(ProtocolPhases::SENDING_CONTROL); |       this->set_phase(ProtocolPhases::SENDING_CONTROL); | ||||||
|  |       if (this->next_hvac_settings_.valid) { | ||||||
|  |         this->current_hvac_settings_ = this->next_hvac_settings_; | ||||||
|  |         this->next_hvac_settings_.reset(); | ||||||
|  |       } else { | ||||||
|  |         this->current_hvac_settings_.reset(); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   this->process_phase(now); |   this->process_phase(now); | ||||||
|   this->haier_protocol_.loop(); |   this->haier_protocol_.loop(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void HaierClimateBase::process_pending_action() { | void HaierClimateBase::process_protocol_reset() { | ||||||
|   ActionRequest request = this->action_request_; |   this->force_send_control_ = false; | ||||||
|   if (this->action_request_ == ActionRequest::TOGGLE_POWER) { |   if (this->current_hvac_settings_.valid) | ||||||
|     request = this->mode == CLIMATE_MODE_OFF ? ActionRequest::TURN_POWER_ON : ActionRequest::TURN_POWER_OFF; |     this->current_hvac_settings_.reset(); | ||||||
|   } |   if (this->next_hvac_settings_.valid) | ||||||
|   switch (request) { |     this->next_hvac_settings_.reset(); | ||||||
|     case ActionRequest::TURN_POWER_ON: |   this->mode = CLIMATE_MODE_OFF; | ||||||
|       this->set_phase(ProtocolPhases::SENDING_POWER_ON_COMMAND); |   this->current_temperature = NAN; | ||||||
|       break; |   this->target_temperature = NAN; | ||||||
|     case ActionRequest::TURN_POWER_OFF: |   this->fan_mode.reset(); | ||||||
|       this->set_phase(ProtocolPhases::SENDING_POWER_OFF_COMMAND); |   this->preset.reset(); | ||||||
|       break; |   this->publish_state(); | ||||||
|     case ActionRequest::TOGGLE_POWER: |   this->set_phase(ProtocolPhases::SENDING_INIT_1); | ||||||
|     case ActionRequest::NO_ACTION: | } | ||||||
|       // shouldn't get here, do nothing |  | ||||||
|       break; | bool HaierClimateBase::prepare_pending_action() { | ||||||
|     default: |   if (this->action_request_.has_value()) { | ||||||
|       ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_); |     switch (this->action_request_.value().action) { | ||||||
|       break; |       case ActionRequest::SEND_CUSTOM_COMMAND: | ||||||
|   } |         return true; | ||||||
|   this->action_request_ = ActionRequest::NO_ACTION; |       case ActionRequest::TURN_POWER_ON: | ||||||
|  |         this->action_request_.value().message = this->get_power_message(true); | ||||||
|  |         return true; | ||||||
|  |       case ActionRequest::TURN_POWER_OFF: | ||||||
|  |         this->action_request_.value().message = this->get_power_message(false); | ||||||
|  |         return true; | ||||||
|  |       case ActionRequest::TOGGLE_POWER: | ||||||
|  |         this->action_request_.value().message = this->get_power_message(this->mode == ClimateMode::CLIMATE_MODE_OFF); | ||||||
|  |         return true; | ||||||
|  |       default: | ||||||
|  |         ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_.value().action); | ||||||
|  |         this->action_request_.reset(); | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |   } else | ||||||
|  |     return false; | ||||||
| } | } | ||||||
|  |  | ||||||
| ClimateTraits HaierClimateBase::traits() { return traits_; } | ClimateTraits HaierClimateBase::traits() { return traits_; } | ||||||
| @@ -302,23 +329,22 @@ void HaierClimateBase::control(const ClimateCall &call) { | |||||||
|     ESP_LOGW(TAG, "Can't send control packet, first poll answer not received"); |     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. |     return;  // cancel the control, we cant do it without a poll answer. | ||||||
|   } |   } | ||||||
|   if (this->hvac_settings_.valid) { |   if (this->current_hvac_settings_.valid) { | ||||||
|     ESP_LOGW(TAG, "Overriding old valid settings before they were applied!"); |     ESP_LOGW(TAG, "New settings come faster then processed!"); | ||||||
|   } |   } | ||||||
|   { |   { | ||||||
|     if (call.get_mode().has_value()) |     if (call.get_mode().has_value()) | ||||||
|       this->hvac_settings_.mode = call.get_mode(); |       this->next_hvac_settings_.mode = call.get_mode(); | ||||||
|     if (call.get_fan_mode().has_value()) |     if (call.get_fan_mode().has_value()) | ||||||
|       this->hvac_settings_.fan_mode = call.get_fan_mode(); |       this->next_hvac_settings_.fan_mode = call.get_fan_mode(); | ||||||
|     if (call.get_swing_mode().has_value()) |     if (call.get_swing_mode().has_value()) | ||||||
|       this->hvac_settings_.swing_mode = call.get_swing_mode(); |       this->next_hvac_settings_.swing_mode = call.get_swing_mode(); | ||||||
|     if (call.get_target_temperature().has_value()) |     if (call.get_target_temperature().has_value()) | ||||||
|       this->hvac_settings_.target_temperature = call.get_target_temperature(); |       this->next_hvac_settings_.target_temperature = call.get_target_temperature(); | ||||||
|     if (call.get_preset().has_value()) |     if (call.get_preset().has_value()) | ||||||
|       this->hvac_settings_.preset = call.get_preset(); |       this->next_hvac_settings_.preset = call.get_preset(); | ||||||
|     this->hvac_settings_.valid = true; |     this->next_hvac_settings_.valid = true; | ||||||
|   } |   } | ||||||
|   this->first_control_attempt_ = true; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void HaierClimateBase::HvacSettings::reset() { | void HaierClimateBase::HvacSettings::reset() { | ||||||
| @@ -330,19 +356,9 @@ void HaierClimateBase::HvacSettings::reset() { | |||||||
|   this->preset.reset(); |   this->preset.reset(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void HaierClimateBase::set_force_send_control_(bool status) { | void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats, | ||||||
|   this->force_send_control_ = status; |                                      std::chrono::milliseconds interval) { | ||||||
|   if (status) { |   this->haier_protocol_.send_message(command, use_crc, num_repeats, interval); | ||||||
|     this->first_control_attempt_ = true; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc) { |  | ||||||
|   if (this->answer_timeout_.has_value()) { |  | ||||||
|     this->haier_protocol_.send_message(command, use_crc, this->answer_timeout_.value()); |  | ||||||
|   } else { |  | ||||||
|     this->haier_protocol_.send_message(command, use_crc); |  | ||||||
|   } |  | ||||||
|   this->last_request_timestamp_ = std::chrono::steady_clock::now(); |   this->last_request_timestamp_ = std::chrono::steady_clock::now(); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ namespace esphome { | |||||||
| namespace haier { | namespace haier { | ||||||
|  |  | ||||||
| enum class ActionRequest : uint8_t { | enum class ActionRequest : uint8_t { | ||||||
|   NO_ACTION = 0, |   SEND_CUSTOM_COMMAND = 0, | ||||||
|   TURN_POWER_ON = 1, |   TURN_POWER_ON = 1, | ||||||
|   TURN_POWER_OFF = 2, |   TURN_POWER_OFF = 2, | ||||||
|   TOGGLE_POWER = 3, |   TOGGLE_POWER = 3, | ||||||
| @@ -33,7 +33,6 @@ class HaierClimateBase : public esphome::Component, | |||||||
|   void control(const esphome::climate::ClimateCall &call) override; |   void control(const esphome::climate::ClimateCall &call) override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; } |   float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; } | ||||||
|   void set_fahrenheit(bool fahrenheit); |  | ||||||
|   void set_display_state(bool state); |   void set_display_state(bool state); | ||||||
|   bool get_display_state() const; |   bool get_display_state() const; | ||||||
|   void set_health_mode(bool state); |   void set_health_mode(bool state); | ||||||
| @@ -45,6 +44,7 @@ class HaierClimateBase : public esphome::Component, | |||||||
|   void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes); |   void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes); | ||||||
|   void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes); |   void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes); | ||||||
|   void set_supported_presets(const std::set<esphome::climate::ClimatePreset> &presets); |   void set_supported_presets(const std::set<esphome::climate::ClimatePreset> &presets); | ||||||
|  |   bool valid_connection() { return this->protocol_phase_ >= ProtocolPhases::IDLE; }; | ||||||
|   size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; |   size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; | ||||||
|   size_t read_array(uint8_t *data, size_t len) noexcept override { |   size_t read_array(uint8_t *data, size_t len) noexcept override { | ||||||
|     return esphome::uart::UARTDevice::read_array(data, len) ? len : 0; |     return esphome::uart::UARTDevice::read_array(data, len) ? len : 0; | ||||||
| @@ -55,63 +55,56 @@ class HaierClimateBase : public esphome::Component, | |||||||
|   bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; }; |   bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; }; | ||||||
|   void set_answer_timeout(uint32_t timeout); |   void set_answer_timeout(uint32_t timeout); | ||||||
|   void set_send_wifi(bool send_wifi); |   void set_send_wifi(bool send_wifi); | ||||||
|  |   void send_custom_command(const haier_protocol::HaierMessage &message); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   enum class ProtocolPhases { |   enum class ProtocolPhases { | ||||||
|     UNKNOWN = -1, |     UNKNOWN = -1, | ||||||
|     // INITIALIZATION |     // INITIALIZATION | ||||||
|     SENDING_INIT_1 = 0, |     SENDING_INIT_1 = 0, | ||||||
|     WAITING_INIT_1_ANSWER = 1, |     SENDING_INIT_2, | ||||||
|     SENDING_INIT_2 = 2, |     SENDING_FIRST_STATUS_REQUEST, | ||||||
|     WAITING_INIT_2_ANSWER = 3, |     SENDING_ALARM_STATUS_REQUEST, | ||||||
|     SENDING_FIRST_STATUS_REQUEST = 4, |  | ||||||
|     WAITING_FIRST_STATUS_ANSWER = 5, |  | ||||||
|     SENDING_ALARM_STATUS_REQUEST = 6, |  | ||||||
|     WAITING_ALARM_STATUS_ANSWER = 7, |  | ||||||
|     // FUNCTIONAL STATE |     // FUNCTIONAL STATE | ||||||
|     IDLE = 8, |     IDLE, | ||||||
|     SENDING_STATUS_REQUEST = 10, |     SENDING_STATUS_REQUEST, | ||||||
|     WAITING_STATUS_ANSWER = 11, |     SENDING_UPDATE_SIGNAL_REQUEST, | ||||||
|     SENDING_UPDATE_SIGNAL_REQUEST = 12, |     SENDING_SIGNAL_LEVEL, | ||||||
|     WAITING_UPDATE_SIGNAL_ANSWER = 13, |     SENDING_CONTROL, | ||||||
|     SENDING_SIGNAL_LEVEL = 14, |     SENDING_ACTION_COMMAND, | ||||||
|     WAITING_SIGNAL_LEVEL_ANSWER = 15, |  | ||||||
|     SENDING_CONTROL = 16, |  | ||||||
|     WAITING_CONTROL_ANSWER = 17, |  | ||||||
|     SENDING_POWER_ON_COMMAND = 18, |  | ||||||
|     WAITING_POWER_ON_ANSWER = 19, |  | ||||||
|     SENDING_POWER_OFF_COMMAND = 20, |  | ||||||
|     WAITING_POWER_OFF_ANSWER = 21, |  | ||||||
|     NUM_PROTOCOL_PHASES |     NUM_PROTOCOL_PHASES | ||||||
|   }; |   }; | ||||||
| #if (HAIER_LOG_LEVEL > 4) |  | ||||||
|   const char *phase_to_string_(ProtocolPhases phase); |   const char *phase_to_string_(ProtocolPhases phase); | ||||||
| #endif |  | ||||||
|   virtual void set_handlers() = 0; |   virtual void set_handlers() = 0; | ||||||
|   virtual void process_phase(std::chrono::steady_clock::time_point now) = 0; |   virtual void process_phase(std::chrono::steady_clock::time_point now) = 0; | ||||||
|   virtual haier_protocol::HaierMessage get_control_message() = 0; |   virtual haier_protocol::HaierMessage get_control_message() = 0; | ||||||
|   virtual bool is_message_invalid(uint8_t message_type) = 0; |   virtual haier_protocol::HaierMessage get_power_message(bool state) = 0; | ||||||
|   virtual void process_pending_action(); |   virtual bool prepare_pending_action(); | ||||||
|  |   virtual void process_protocol_reset(); | ||||||
|   esphome::climate::ClimateTraits traits() override; |   esphome::climate::ClimateTraits traits() override; | ||||||
|   // Answers handlers |   // Answer handlers | ||||||
|   haier_protocol::HandlerError answer_preprocess_(uint8_t request_message_type, uint8_t expected_request_message_type, |   haier_protocol::HandlerError answer_preprocess_(haier_protocol::FrameType request_message_type, | ||||||
|                                                   uint8_t answer_message_type, uint8_t expected_answer_message_type, |                                                   haier_protocol::FrameType expected_request_message_type, | ||||||
|  |                                                   haier_protocol::FrameType answer_message_type, | ||||||
|  |                                                   haier_protocol::FrameType expected_answer_message_type, | ||||||
|                                                   ProtocolPhases expected_phase); |                                                   ProtocolPhases expected_phase); | ||||||
|  |   haier_protocol::HandlerError report_network_status_answer_handler_(haier_protocol::FrameType request_type, | ||||||
|  |                                                                      haier_protocol::FrameType message_type, | ||||||
|  |                                                                      const uint8_t *data, size_t data_size); | ||||||
|   // Timeout handler |   // Timeout handler | ||||||
|   haier_protocol::HandlerError timeout_default_handler_(uint8_t request_type); |   haier_protocol::HandlerError timeout_default_handler_(haier_protocol::FrameType request_type); | ||||||
|   // Helper functions |   // Helper functions | ||||||
|   void set_force_send_control_(bool status); |   void send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats = 0, | ||||||
|   void send_message_(const haier_protocol::HaierMessage &command, bool use_crc); |                      std::chrono::milliseconds interval = std::chrono::milliseconds::zero()); | ||||||
|   virtual void set_phase(ProtocolPhases phase); |   virtual void set_phase(ProtocolPhases phase); | ||||||
|   bool check_timeout_(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint, |   void reset_phase_(); | ||||||
|                       size_t timeout); |   void reset_to_idle_(); | ||||||
|   bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now); |   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_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_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now); | ||||||
|   bool is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now); |   bool is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now); | ||||||
| #ifdef USE_WIFI | #ifdef USE_WIFI | ||||||
|   haier_protocol::HaierMessage get_wifi_signal_message_(uint8_t message_type); |   haier_protocol::HaierMessage get_wifi_signal_message_(); | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   struct HvacSettings { |   struct HvacSettings { | ||||||
| @@ -122,29 +115,34 @@ class HaierClimateBase : public esphome::Component, | |||||||
|     esphome::optional<esphome::climate::ClimatePreset> preset; |     esphome::optional<esphome::climate::ClimatePreset> preset; | ||||||
|     bool valid; |     bool valid; | ||||||
|     HvacSettings() : valid(false){}; |     HvacSettings() : valid(false){}; | ||||||
|  |     HvacSettings(const HvacSettings &) = default; | ||||||
|  |     HvacSettings &operator=(const HvacSettings &) = default; | ||||||
|     void reset(); |     void reset(); | ||||||
|   }; |   }; | ||||||
|  |   struct PendingAction { | ||||||
|  |     ActionRequest action; | ||||||
|  |     esphome::optional<haier_protocol::HaierMessage> message; | ||||||
|  |   }; | ||||||
|   haier_protocol::ProtocolHandler haier_protocol_; |   haier_protocol::ProtocolHandler haier_protocol_; | ||||||
|   ProtocolPhases protocol_phase_; |   ProtocolPhases protocol_phase_; | ||||||
|   ActionRequest action_request_; |   esphome::optional<PendingAction> action_request_; | ||||||
|   uint8_t fan_mode_speed_; |   uint8_t fan_mode_speed_; | ||||||
|   uint8_t other_modes_fan_speed_; |   uint8_t other_modes_fan_speed_; | ||||||
|   bool display_status_; |   bool display_status_; | ||||||
|   bool health_mode_; |   bool health_mode_; | ||||||
|   bool force_send_control_; |   bool force_send_control_; | ||||||
|   bool forced_publish_; |  | ||||||
|   bool forced_request_status_; |   bool forced_request_status_; | ||||||
|   bool first_control_attempt_; |  | ||||||
|   bool reset_protocol_request_; |   bool reset_protocol_request_; | ||||||
|  |   bool send_wifi_signal_; | ||||||
|  |   bool use_crc_; | ||||||
|   esphome::climate::ClimateTraits traits_; |   esphome::climate::ClimateTraits traits_; | ||||||
|   HvacSettings hvac_settings_; |   HvacSettings current_hvac_settings_; | ||||||
|  |   HvacSettings next_hvac_settings_; | ||||||
|  |   std::unique_ptr<uint8_t[]> last_status_message_; | ||||||
|   std::chrono::steady_clock::time_point last_request_timestamp_;       // For interval between messages |   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_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 last_status_request_;          // To request AC status | ||||||
|   std::chrono::steady_clock::time_point control_request_timestamp_;    // To send control message |   std::chrono::steady_clock::time_point last_signal_request_;          // To send WiFI signal level | ||||||
|   optional<std::chrono::milliseconds> answer_timeout_;                 // Message answer timeout |  | ||||||
|   bool send_wifi_signal_; |  | ||||||
|   std::chrono::steady_clock::time_point last_signal_request_;  // To send WiFI signal level |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace haier | }  // namespace haier | ||||||
|   | |||||||
| @@ -14,6 +14,8 @@ namespace haier { | |||||||
| static const char *const TAG = "haier.climate"; | static const char *const TAG = "haier.climate"; | ||||||
| constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; | constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; | ||||||
| constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64; | constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64; | ||||||
|  | constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5; | ||||||
|  | constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500); | ||||||
|  |  | ||||||
| hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) { | hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) { | ||||||
|   switch (direction) { |   switch (direction) { | ||||||
| @@ -48,14 +50,11 @@ hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDir | |||||||
| } | } | ||||||
|  |  | ||||||
| HonClimate::HonClimate() | HonClimate::HonClimate() | ||||||
|     : last_status_message_(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]), |     : cleaning_status_(CleaningState::NO_CLEANING), | ||||||
|       cleaning_status_(CleaningState::NO_CLEANING), |  | ||||||
|       got_valid_outdoor_temp_(false), |       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}, |       active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, | ||||||
|       outdoor_sensor_(nullptr) { |       outdoor_sensor_(nullptr) { | ||||||
|  |   last_status_message_ = std::unique_ptr<uint8_t[]>(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]); | ||||||
|   this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID; |   this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID; | ||||||
|   this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO; |   this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO; | ||||||
| } | } | ||||||
| @@ -72,14 +71,14 @@ AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this- | |||||||
|  |  | ||||||
| void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) { | void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) { | ||||||
|   this->vertical_direction_ = direction; |   this->vertical_direction_ = direction; | ||||||
|   this->set_force_send_control_(true); |   this->force_send_control_ = true; | ||||||
| } | } | ||||||
|  |  | ||||||
| AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; } | AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; } | ||||||
|  |  | ||||||
| void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) { | void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) { | ||||||
|   this->horizontal_direction_ = direction; |   this->horizontal_direction_ = direction; | ||||||
|   this->set_force_send_control_(true); |   this->force_send_control_ = true; | ||||||
| } | } | ||||||
|  |  | ||||||
| std::string HonClimate::get_cleaning_status_text() const { | std::string HonClimate::get_cleaning_status_text() const { | ||||||
| @@ -98,35 +97,35 @@ CleaningState HonClimate::get_cleaning_status() const { return this->cleaning_st | |||||||
| void HonClimate::start_self_cleaning() { | void HonClimate::start_self_cleaning() { | ||||||
|   if (this->cleaning_status_ == CleaningState::NO_CLEANING) { |   if (this->cleaning_status_ == CleaningState::NO_CLEANING) { | ||||||
|     ESP_LOGI(TAG, "Sending self cleaning start request"); |     ESP_LOGI(TAG, "Sending self cleaning start request"); | ||||||
|     this->action_request_ = ActionRequest::START_SELF_CLEAN; |     this->action_request_ = | ||||||
|     this->set_force_send_control_(true); |         PendingAction({ActionRequest::START_SELF_CLEAN, esphome::optional<haier_protocol::HaierMessage>()}); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void HonClimate::start_steri_cleaning() { | void HonClimate::start_steri_cleaning() { | ||||||
|   if (this->cleaning_status_ == CleaningState::NO_CLEANING) { |   if (this->cleaning_status_ == CleaningState::NO_CLEANING) { | ||||||
|     ESP_LOGI(TAG, "Sending steri cleaning start request"); |     ESP_LOGI(TAG, "Sending steri cleaning start request"); | ||||||
|     this->action_request_ = ActionRequest::START_STERI_CLEAN; |     this->action_request_ = | ||||||
|     this->set_force_send_control_(true); |         PendingAction({ActionRequest::START_STERI_CLEAN, esphome::optional<haier_protocol::HaierMessage>()}); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, | haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type, | ||||||
|  |                                                                             haier_protocol::FrameType message_type, | ||||||
|                                                                             const uint8_t *data, size_t data_size) { |                                                                             const uint8_t *data, size_t data_size) { | ||||||
|   // Should check this before preprocess |   // Should check this before preprocess | ||||||
|   if (message_type == (uint8_t) hon_protocol::FrameType::INVALID) { |   if (message_type == haier_protocol::FrameType::INVALID) { | ||||||
|     ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the smartAir2 " |     ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the smartAir2 " | ||||||
|                   "protocol instead of hOn"); |                   "protocol instead of hOn"); | ||||||
|     this->set_phase(ProtocolPhases::SENDING_INIT_1); |     this->set_phase(ProtocolPhases::SENDING_INIT_1); | ||||||
|     return haier_protocol::HandlerError::INVALID_ANSWER; |     return haier_protocol::HandlerError::INVALID_ANSWER; | ||||||
|   } |   } | ||||||
|   haier_protocol::HandlerError result = this->answer_preprocess_( |   haier_protocol::HandlerError result = | ||||||
|       request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type, |       this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_VERSION, message_type, | ||||||
|       (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_INIT_1_ANSWER); |                                haier_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::SENDING_INIT_1); | ||||||
|   if (result == haier_protocol::HandlerError::HANDLER_OK) { |   if (result == haier_protocol::HandlerError::HANDLER_OK) { | ||||||
|     if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) { |     if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) { | ||||||
|       // Wrong structure |       // Wrong structure | ||||||
|       this->set_phase(ProtocolPhases::SENDING_INIT_1); |  | ||||||
|       return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; |       return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; | ||||||
|     } |     } | ||||||
|     // All OK |     // All OK | ||||||
| @@ -134,54 +133,57 @@ haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint | |||||||
|     char tmp[9]; |     char tmp[9]; | ||||||
|     tmp[8] = 0; |     tmp[8] = 0; | ||||||
|     strncpy(tmp, answr->protocol_version, 8); |     strncpy(tmp, answr->protocol_version, 8); | ||||||
|     this->hvac_protocol_version_ = std::string(tmp); |     this->hvac_hardware_info_ = HardwareInfo(); | ||||||
|  |     this->hvac_hardware_info_.value().protocol_version_ = std::string(tmp); | ||||||
|     strncpy(tmp, answr->software_version, 8); |     strncpy(tmp, answr->software_version, 8); | ||||||
|     this->hvac_software_version_ = std::string(tmp); |     this->hvac_hardware_info_.value().software_version_ = std::string(tmp); | ||||||
|     strncpy(tmp, answr->hardware_version, 8); |     strncpy(tmp, answr->hardware_version, 8); | ||||||
|     this->hvac_hardware_version_ = std::string(tmp); |     this->hvac_hardware_info_.value().hardware_version_ = std::string(tmp); | ||||||
|     strncpy(tmp, answr->device_name, 8); |     strncpy(tmp, answr->device_name, 8); | ||||||
|     this->hvac_device_name_ = std::string(tmp); |     this->hvac_hardware_info_.value().device_name_ = std::string(tmp); | ||||||
|     this->hvac_functions_[0] = (answr->functions[1] & 0x01) != 0;  // interactive mode support |     this->hvac_hardware_info_.value().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_hardware_info_.value().functions_[1] = | ||||||
|     this->hvac_functions_[2] = (answr->functions[1] & 0x04) != 0;  // crc support |         (answr->functions[1] & 0x02) != 0;  // controller-device mode support | ||||||
|     this->hvac_functions_[3] = (answr->functions[1] & 0x08) != 0;  // multiple AC support |     this->hvac_hardware_info_.value().functions_[2] = (answr->functions[1] & 0x04) != 0;  // crc support | ||||||
|     this->hvac_functions_[4] = (answr->functions[1] & 0x20) != 0;  // roles support |     this->hvac_hardware_info_.value().functions_[3] = (answr->functions[1] & 0x08) != 0;  // multiple AC support | ||||||
|     this->hvac_hardware_info_available_ = true; |     this->hvac_hardware_info_.value().functions_[4] = (answr->functions[1] & 0x20) != 0;  // roles support | ||||||
|  |     this->use_crc_ = this->hvac_hardware_info_.value().functions_[2]; | ||||||
|     this->set_phase(ProtocolPhases::SENDING_INIT_2); |     this->set_phase(ProtocolPhases::SENDING_INIT_2); | ||||||
|     return result; |     return result; | ||||||
|   } else { |   } else { | ||||||
|     this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE |     this->reset_phase_(); | ||||||
|                                                                     : ProtocolPhases::SENDING_INIT_1); |  | ||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, | haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(haier_protocol::FrameType request_type, | ||||||
|  |                                                                        haier_protocol::FrameType message_type, | ||||||
|                                                                        const uint8_t *data, size_t data_size) { |                                                                        const uint8_t *data, size_t data_size) { | ||||||
|   haier_protocol::HandlerError result = this->answer_preprocess_( |   haier_protocol::HandlerError result = | ||||||
|       request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID, message_type, |       this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_ID, message_type, | ||||||
|       (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_INIT_2_ANSWER); |                                haier_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::SENDING_INIT_2); | ||||||
|   if (result == haier_protocol::HandlerError::HANDLER_OK) { |   if (result == haier_protocol::HandlerError::HANDLER_OK) { | ||||||
|     this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); |     this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); | ||||||
|     return result; |     return result; | ||||||
|   } else { |   } else { | ||||||
|     this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE |     this->reset_phase_(); | ||||||
|                                                                     : ProtocolPhases::SENDING_INIT_1); |  | ||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, uint8_t message_type, | haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameType request_type, | ||||||
|                                                          const uint8_t *data, size_t data_size) { |                                                          haier_protocol::FrameType message_type, const uint8_t *data, | ||||||
|  |                                                          size_t data_size) { | ||||||
|   haier_protocol::HandlerError result = |   haier_protocol::HandlerError result = | ||||||
|       this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::CONTROL, message_type, |       this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type, | ||||||
|                                (uint8_t) hon_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); |                                haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); | ||||||
|   if (result == haier_protocol::HandlerError::HANDLER_OK) { |   if (result == haier_protocol::HandlerError::HANDLER_OK) { | ||||||
|     result = this->process_status_message_(data, data_size); |     result = this->process_status_message_(data, data_size); | ||||||
|     if (result != haier_protocol::HandlerError::HANDLER_OK) { |     if (result != haier_protocol::HandlerError::HANDLER_OK) { | ||||||
|       ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); |       ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); | ||||||
|       this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE |       this->reset_phase_(); | ||||||
|                                                                       : ProtocolPhases::SENDING_INIT_1); |       this->action_request_.reset(); | ||||||
|  |       this->force_send_control_ = false; | ||||||
|     } else { |     } else { | ||||||
|       if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) { |       if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) { | ||||||
|         memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl)); |         memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl)); | ||||||
| @@ -189,36 +191,48 @@ haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, u | |||||||
|         ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, |         ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, | ||||||
|                  sizeof(hon_protocol::HaierPacketControl)); |                  sizeof(hon_protocol::HaierPacketControl)); | ||||||
|       } |       } | ||||||
|       if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { |       switch (this->protocol_phase_) { | ||||||
|         ESP_LOGI(TAG, "First HVAC status received"); |         case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: | ||||||
|         this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); |           ESP_LOGI(TAG, "First HVAC status received"); | ||||||
|       } else if ((this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) || |           this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); | ||||||
|                  (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_ON_ANSWER) || |           break; | ||||||
|                  (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_OFF_ANSWER)) { |         case ProtocolPhases::SENDING_ACTION_COMMAND: | ||||||
|         this->set_phase(ProtocolPhases::IDLE); |           // Do nothing, phase will be changed in process_phase | ||||||
|       } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { |           break; | ||||||
|         this->set_phase(ProtocolPhases::IDLE); |         case ProtocolPhases::SENDING_STATUS_REQUEST: | ||||||
|         this->set_force_send_control_(false); |           this->set_phase(ProtocolPhases::IDLE); | ||||||
|         if (this->hvac_settings_.valid) |           break; | ||||||
|           this->hvac_settings_.reset(); |         case ProtocolPhases::SENDING_CONTROL: | ||||||
|  |           if (!this->control_messages_queue_.empty()) | ||||||
|  |             this->control_messages_queue_.pop(); | ||||||
|  |           if (this->control_messages_queue_.empty()) { | ||||||
|  |             this->set_phase(ProtocolPhases::IDLE); | ||||||
|  |             this->force_send_control_ = false; | ||||||
|  |             if (this->current_hvac_settings_.valid) | ||||||
|  |               this->current_hvac_settings_.reset(); | ||||||
|  |           } else { | ||||||
|  |             this->set_phase(ProtocolPhases::SENDING_CONTROL); | ||||||
|  |           } | ||||||
|  |           break; | ||||||
|  |         default: | ||||||
|  |           break; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     return result; |     return result; | ||||||
|   } else { |   } else { | ||||||
|     this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE |     this->action_request_.reset(); | ||||||
|                                                                     : ProtocolPhases::SENDING_INIT_1); |     this->force_send_control_ = false; | ||||||
|  |     this->reset_phase_(); | ||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(uint8_t request_type, | haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_( | ||||||
|                                                                                     uint8_t message_type, |     haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, | ||||||
|                                                                                     const uint8_t *data, |     size_t data_size) { | ||||||
|                                                                                     size_t data_size) { |   haier_protocol::HandlerError result = this->answer_preprocess_( | ||||||
|   haier_protocol::HandlerError result = |       request_type, haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, message_type, | ||||||
|       this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION, |       haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST); | ||||||
|                                message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, |  | ||||||
|                                ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER); |  | ||||||
|   if (result == haier_protocol::HandlerError::HANDLER_OK) { |   if (result == haier_protocol::HandlerError::HANDLER_OK) { | ||||||
|     this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); |     this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); | ||||||
|     return result; |     return result; | ||||||
| @@ -228,25 +242,16 @@ haier_protocol::HandlerError HonClimate::get_management_information_answer_handl | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| haier_protocol::HandlerError HonClimate::report_network_status_answer_handler_(uint8_t request_type, | haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_protocol::FrameType request_type, | ||||||
|                                                                                uint8_t message_type, |                                                                           haier_protocol::FrameType 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) { |                                                                           const uint8_t *data, size_t data_size) { | ||||||
|   if (request_type == (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS) { |   if (request_type == haier_protocol::FrameType::GET_ALARM_STATUS) { | ||||||
|     if (message_type != (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) { |     if (message_type != haier_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) { | ||||||
|       // Unexpected answer to request |       // Unexpected answer to request | ||||||
|       this->set_phase(ProtocolPhases::IDLE); |       this->set_phase(ProtocolPhases::IDLE); | ||||||
|       return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; |       return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; | ||||||
|     } |     } | ||||||
|     if (this->protocol_phase_ != ProtocolPhases::WAITING_ALARM_STATUS_ANSWER) { |     if (this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) { | ||||||
|       // Don't expect this answer now |       // Don't expect this answer now | ||||||
|       this->set_phase(ProtocolPhases::IDLE); |       this->set_phase(ProtocolPhases::IDLE); | ||||||
|       return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; |       return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; | ||||||
| @@ -263,27 +268,27 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_ | |||||||
| void HonClimate::set_handlers() { | void HonClimate::set_handlers() { | ||||||
|   // Set handlers |   // Set handlers | ||||||
|   this->haier_protocol_.set_answer_handler( |   this->haier_protocol_.set_answer_handler( | ||||||
|       (uint8_t) (hon_protocol::FrameType::GET_DEVICE_VERSION), |       haier_protocol::FrameType::GET_DEVICE_VERSION, | ||||||
|       std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, |       std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, | ||||||
|                 std::placeholders::_3, std::placeholders::_4)); |                 std::placeholders::_3, std::placeholders::_4)); | ||||||
|   this->haier_protocol_.set_answer_handler( |   this->haier_protocol_.set_answer_handler( | ||||||
|       (uint8_t) (hon_protocol::FrameType::GET_DEVICE_ID), |       haier_protocol::FrameType::GET_DEVICE_ID, | ||||||
|       std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, |       std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, | ||||||
|                 std::placeholders::_3, std::placeholders::_4)); |                 std::placeholders::_3, std::placeholders::_4)); | ||||||
|   this->haier_protocol_.set_answer_handler( |   this->haier_protocol_.set_answer_handler( | ||||||
|       (uint8_t) (hon_protocol::FrameType::CONTROL), |       haier_protocol::FrameType::CONTROL, | ||||||
|       std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, |       std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, | ||||||
|                 std::placeholders::_4)); |                 std::placeholders::_4)); | ||||||
|   this->haier_protocol_.set_answer_handler( |   this->haier_protocol_.set_answer_handler( | ||||||
|       (uint8_t) (hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION), |       haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, | ||||||
|       std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1, |       std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1, | ||||||
|                 std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); |                 std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); | ||||||
|   this->haier_protocol_.set_answer_handler( |   this->haier_protocol_.set_answer_handler( | ||||||
|       (uint8_t) (hon_protocol::FrameType::GET_ALARM_STATUS), |       haier_protocol::FrameType::GET_ALARM_STATUS, | ||||||
|       std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, |       std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, | ||||||
|                 std::placeholders::_3, std::placeholders::_4)); |                 std::placeholders::_3, std::placeholders::_4)); | ||||||
|   this->haier_protocol_.set_answer_handler( |   this->haier_protocol_.set_answer_handler( | ||||||
|       (uint8_t) (hon_protocol::FrameType::REPORT_NETWORK_STATUS), |       haier_protocol::FrameType::REPORT_NETWORK_STATUS, | ||||||
|       std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, |       std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, | ||||||
|                 std::placeholders::_3, std::placeholders::_4)); |                 std::placeholders::_3, std::placeholders::_4)); | ||||||
| } | } | ||||||
| @@ -291,14 +296,18 @@ void HonClimate::set_handlers() { | |||||||
| void HonClimate::dump_config() { | void HonClimate::dump_config() { | ||||||
|   HaierClimateBase::dump_config(); |   HaierClimateBase::dump_config(); | ||||||
|   ESP_LOGCONFIG(TAG, "  Protocol version: hOn"); |   ESP_LOGCONFIG(TAG, "  Protocol version: hOn"); | ||||||
|   if (this->hvac_hardware_info_available_) { |   ESP_LOGCONFIG(TAG, "  Control method: %d", (uint8_t) this->control_method_); | ||||||
|     ESP_LOGCONFIG(TAG, "  Device protocol version: %s", this->hvac_protocol_version_.c_str()); |   if (this->hvac_hardware_info_.has_value()) { | ||||||
|     ESP_LOGCONFIG(TAG, "  Device software version: %s", this->hvac_software_version_.c_str()); |     ESP_LOGCONFIG(TAG, "  Device protocol version: %s", this->hvac_hardware_info_.value().protocol_version_.c_str()); | ||||||
|     ESP_LOGCONFIG(TAG, "  Device hardware version: %s", this->hvac_hardware_version_.c_str()); |     ESP_LOGCONFIG(TAG, "  Device software version: %s", this->hvac_hardware_info_.value().software_version_.c_str()); | ||||||
|     ESP_LOGCONFIG(TAG, "  Device name: %s", this->hvac_device_name_.c_str()); |     ESP_LOGCONFIG(TAG, "  Device hardware version: %s", this->hvac_hardware_info_.value().hardware_version_.c_str()); | ||||||
|     ESP_LOGCONFIG(TAG, "  Device features:%s%s%s%s%s", (this->hvac_functions_[0] ? " interactive" : ""), |     ESP_LOGCONFIG(TAG, "  Device name: %s", this->hvac_hardware_info_.value().device_name_.c_str()); | ||||||
|                   (this->hvac_functions_[1] ? " controller-device" : ""), (this->hvac_functions_[2] ? " crc" : ""), |     ESP_LOGCONFIG(TAG, "  Device features:%s%s%s%s%s", | ||||||
|                   (this->hvac_functions_[3] ? " multinode" : ""), (this->hvac_functions_[4] ? " role" : "")); |                   (this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""), | ||||||
|  |                   (this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""), | ||||||
|  |                   (this->hvac_hardware_info_.value().functions_[2] ? " crc" : ""), | ||||||
|  |                   (this->hvac_hardware_info_.value().functions_[3] ? " multinode" : ""), | ||||||
|  |                   (this->hvac_hardware_info_.value().functions_[4] ? " role" : "")); | ||||||
|     ESP_LOGCONFIG(TAG, "  Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str()); |     ESP_LOGCONFIG(TAG, "  Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str()); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -307,7 +316,6 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { | |||||||
|   switch (this->protocol_phase_) { |   switch (this->protocol_phase_) { | ||||||
|     case ProtocolPhases::SENDING_INIT_1: |     case ProtocolPhases::SENDING_INIT_1: | ||||||
|       if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) { |       if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) { | ||||||
|         this->hvac_hardware_info_available_ = false; |  | ||||||
|         // Indicate device capabilities: |         // Indicate device capabilities: | ||||||
|         // bit 0 - if 1 module support interactive mode |         // bit 0 - if 1 module support interactive mode | ||||||
|         // bit 1 - if 1 module support controller-device mode |         // bit 1 - if 1 module support controller-device mode | ||||||
| @@ -316,109 +324,95 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { | |||||||
|         // bit 4..bit 15 - not used |         // bit 4..bit 15 - not used | ||||||
|         uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; |         uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; | ||||||
|         static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( |         static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( | ||||||
|             (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); |             haier_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); | ||||||
|         this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_); |         this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_); | ||||||
|         this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER); |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     case ProtocolPhases::SENDING_INIT_2: |     case ProtocolPhases::SENDING_INIT_2: | ||||||
|       if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { |       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); |         static const haier_protocol::HaierMessage DEVICEID_REQUEST(haier_protocol::FrameType::GET_DEVICE_ID); | ||||||
|         this->send_message_(DEVICEID_REQUEST, this->use_crc_); |         this->send_message_(DEVICEID_REQUEST, this->use_crc_); | ||||||
|         this->set_phase(ProtocolPhases::WAITING_INIT_2_ANSWER); |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: |     case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: | ||||||
|     case ProtocolPhases::SENDING_STATUS_REQUEST: |     case ProtocolPhases::SENDING_STATUS_REQUEST: | ||||||
|       if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { |       if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { | ||||||
|         static const haier_protocol::HaierMessage STATUS_REQUEST( |         static const haier_protocol::HaierMessage STATUS_REQUEST( | ||||||
|             (uint8_t) hon_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA); |             haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA); | ||||||
|         this->send_message_(STATUS_REQUEST, this->use_crc_); |         this->send_message_(STATUS_REQUEST, this->use_crc_); | ||||||
|         this->last_status_request_ = now; |         this->last_status_request_ = now; | ||||||
|         this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1)); |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
| #ifdef USE_WIFI | #ifdef USE_WIFI | ||||||
|     case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: |     case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: | ||||||
|       if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { |       if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { | ||||||
|         static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST( |         static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST( | ||||||
|             (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION); |             haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION); | ||||||
|         this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_); |         this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_); | ||||||
|         this->last_signal_request_ = now; |         this->last_signal_request_ = now; | ||||||
|         this->set_phase(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER); |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     case ProtocolPhases::SENDING_SIGNAL_LEVEL: |     case ProtocolPhases::SENDING_SIGNAL_LEVEL: | ||||||
|       if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { |       if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { | ||||||
|         this->send_message_(this->get_wifi_signal_message_((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS), |         this->send_message_(this->get_wifi_signal_message_(), this->use_crc_); | ||||||
|                             this->use_crc_); |  | ||||||
|         this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: |  | ||||||
|     case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: |  | ||||||
|       break; |  | ||||||
| #else | #else | ||||||
|     case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: |     case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: | ||||||
|     case ProtocolPhases::SENDING_SIGNAL_LEVEL: |     case ProtocolPhases::SENDING_SIGNAL_LEVEL: | ||||||
|     case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: |  | ||||||
|     case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: |  | ||||||
|       this->set_phase(ProtocolPhases::IDLE); |       this->set_phase(ProtocolPhases::IDLE); | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|     case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: |     case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: | ||||||
|       if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { |       if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { | ||||||
|         static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST( |         static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(haier_protocol::FrameType::GET_ALARM_STATUS); | ||||||
|             (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS); |  | ||||||
|         this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_); |         this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_); | ||||||
|         this->set_phase(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER); |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     case ProtocolPhases::SENDING_CONTROL: |     case ProtocolPhases::SENDING_CONTROL: | ||||||
|       if (this->first_control_attempt_) { |       if (this->control_messages_queue_.empty()) { | ||||||
|         this->control_request_timestamp_ = now; |         switch (this->control_method_) { | ||||||
|         this->first_control_attempt_ = false; |           case HonControlMethod::SET_GROUP_PARAMETERS: { | ||||||
|  |             haier_protocol::HaierMessage control_message = this->get_control_message(); | ||||||
|  |             this->control_messages_queue_.push(control_message); | ||||||
|  |           } break; | ||||||
|  |           case HonControlMethod::SET_SINGLE_PARAMETER: | ||||||
|  |             this->fill_control_messages_queue_(); | ||||||
|  |             break; | ||||||
|  |           case HonControlMethod::MONITOR_ONLY: | ||||||
|  |             ESP_LOGI(TAG, "AC control is disabled, monitor only"); | ||||||
|  |             this->reset_to_idle_(); | ||||||
|  |             return; | ||||||
|  |           default: | ||||||
|  |             ESP_LOGW(TAG, "Unsupported control method for hOn protocol!"); | ||||||
|  |             this->reset_to_idle_(); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|       if (this->is_control_message_timeout_exceeded_(now)) { |       if (this->control_messages_queue_.empty()) { | ||||||
|         ESP_LOGW(TAG, "Sending control packet timeout!"); |         ESP_LOGW(TAG, "Control message queue is empty!"); | ||||||
|         this->set_force_send_control_(false); |         this->reset_to_idle_(); | ||||||
|         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)) { |       } else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { | ||||||
|         haier_protocol::HaierMessage control_message = get_control_message(); |         ESP_LOGI(TAG, "Sending control packet, queue size %d", this->control_messages_queue_.size()); | ||||||
|         this->send_message_(control_message, this->use_crc_); |         this->send_message_(this->control_messages_queue_.front(), this->use_crc_, CONTROL_MESSAGE_RETRIES, | ||||||
|         ESP_LOGI(TAG, "Control packet sent"); |                             CONTROL_MESSAGE_RETRIES_INTERVAL); | ||||||
|         this->set_phase(ProtocolPhases::WAITING_CONTROL_ANSWER); |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     case ProtocolPhases::SENDING_POWER_ON_COMMAND: |     case ProtocolPhases::SENDING_ACTION_COMMAND: | ||||||
|     case ProtocolPhases::SENDING_POWER_OFF_COMMAND: |       if (this->action_request_.has_value()) { | ||||||
|       if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { |         if (this->action_request_.value().message.has_value()) { | ||||||
|         uint8_t pwr_cmd_buf[2] = {0x00, 0x00}; |           this->send_message_(this->action_request_.value().message.value(), this->use_crc_); | ||||||
|         if (this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND) |           this->action_request_.value().message.reset(); | ||||||
|           pwr_cmd_buf[1] = 0x01; |         } else { | ||||||
|         haier_protocol::HaierMessage power_cmd((uint8_t) hon_protocol::FrameType::CONTROL, |           // Message already sent, reseting request and return to idle | ||||||
|                                                ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1, |           this->action_request_.reset(); | ||||||
|                                                pwr_cmd_buf, sizeof(pwr_cmd_buf)); |           this->set_phase(ProtocolPhases::IDLE); | ||||||
|         this->send_message_(power_cmd, this->use_crc_); |         } | ||||||
|         this->set_phase(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND |       } else { | ||||||
|                             ? ProtocolPhases::WAITING_POWER_ON_ANSWER |         ESP_LOGW(TAG, "SENDING_ACTION_COMMAND phase without action request!"); | ||||||
|                             : ProtocolPhases::WAITING_POWER_OFF_ANSWER); |         this->set_phase(ProtocolPhases::IDLE); | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|  |  | ||||||
|     case ProtocolPhases::WAITING_INIT_1_ANSWER: |  | ||||||
|     case ProtocolPhases::WAITING_INIT_2_ANSWER: |  | ||||||
|     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: { |     case ProtocolPhases::IDLE: { | ||||||
|       if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { |       if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { | ||||||
|         this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); |         this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); | ||||||
| @@ -433,26 +427,35 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) { | |||||||
|     } break; |     } break; | ||||||
|     default: |     default: | ||||||
|       // Shouldn't get here |       // Shouldn't get here | ||||||
| #if (HAIER_LOG_LEVEL > 4) |  | ||||||
|       ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", |       ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", | ||||||
|                phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); |                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); |       this->set_phase(ProtocolPhases::SENDING_INIT_1); | ||||||
|       break; |       break; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | haier_protocol::HaierMessage HonClimate::get_power_message(bool state) { | ||||||
|  |   if (state) { | ||||||
|  |     static haier_protocol::HaierMessage power_on_message( | ||||||
|  |         haier_protocol::FrameType::CONTROL, ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1, | ||||||
|  |         std::initializer_list<uint8_t>({0x00, 0x01}).begin(), 2); | ||||||
|  |     return power_on_message; | ||||||
|  |   } else { | ||||||
|  |     static haier_protocol::HaierMessage power_off_message( | ||||||
|  |         haier_protocol::FrameType::CONTROL, ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1, | ||||||
|  |         std::initializer_list<uint8_t>({0x00, 0x00}).begin(), 2); | ||||||
|  |     return power_off_message; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| haier_protocol::HaierMessage HonClimate::get_control_message() { | haier_protocol::HaierMessage HonClimate::get_control_message() { | ||||||
|   uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; |   uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; | ||||||
|   memcpy(control_out_buffer, this->last_status_message_.get(), 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; |   hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; | ||||||
|   bool has_hvac_settings = false; |   bool has_hvac_settings = false; | ||||||
|   if (this->hvac_settings_.valid) { |   if (this->current_hvac_settings_.valid) { | ||||||
|     has_hvac_settings = true; |     has_hvac_settings = true; | ||||||
|     HvacSettings climate_control; |     HvacSettings &climate_control = this->current_hvac_settings_; | ||||||
|     climate_control = this->hvac_settings_; |  | ||||||
|     if (climate_control.mode.has_value()) { |     if (climate_control.mode.has_value()) { | ||||||
|       switch (climate_control.mode.value()) { |       switch (climate_control.mode.value()) { | ||||||
|         case CLIMATE_MODE_OFF: |         case CLIMATE_MODE_OFF: | ||||||
| @@ -535,7 +538,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { | |||||||
|     } |     } | ||||||
|     if (climate_control.target_temperature.has_value()) { |     if (climate_control.target_temperature.has_value()) { | ||||||
|       float target_temp = climate_control.target_temperature.value(); |       float target_temp = climate_control.target_temperature.value(); | ||||||
|       out_data->set_point = ((int) target_temp) - 16;  // set the temperature at our offset, subtract 16. |       out_data->set_point = ((int) target_temp) - 16;  // set the temperature with offset 16 | ||||||
|       out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; |       out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; | ||||||
|     } |     } | ||||||
|     if (out_data->ac_power == 0) { |     if (out_data->ac_power == 0) { | ||||||
| @@ -587,50 +590,28 @@ haier_protocol::HaierMessage HonClimate::get_control_message() { | |||||||
|   control_out_buffer[4] = 0;  // This byte should be cleared before setting values |   control_out_buffer[4] = 0;  // This byte should be cleared before setting values | ||||||
|   out_data->display_status = this->display_status_ ? 1 : 0; |   out_data->display_status = this->display_status_ ? 1 : 0; | ||||||
|   out_data->health_mode = this->health_mode_ ? 1 : 0; |   out_data->health_mode = this->health_mode_ ? 1 : 0; | ||||||
|   switch (this->action_request_) { |   return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, | ||||||
|     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::SubcommandsControl::SET_GROUP_PARAMETERS, |                                       (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, | ||||||
|                                       control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); |                                       control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); | ||||||
| } | } | ||||||
|  |  | ||||||
| haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { | haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { | ||||||
|   if (size < sizeof(hon_protocol::HaierStatus)) |   if (size < hon_protocol::HAIER_STATUS_FRAME_SIZE + this->extra_control_packet_bytes_) | ||||||
|     return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; |     return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; | ||||||
|   hon_protocol::HaierStatus packet; |   struct { | ||||||
|   if (size < sizeof(hon_protocol::HaierStatus)) |     hon_protocol::HaierPacketControl control; | ||||||
|     size = sizeof(hon_protocol::HaierStatus); |     hon_protocol::HaierPacketSensors sensors; | ||||||
|   memcpy(&packet, packet_buffer, size); |   } packet; | ||||||
|  |   memcpy(&packet.control, packet_buffer + 2, sizeof(hon_protocol::HaierPacketControl)); | ||||||
|  |   memcpy(&packet.sensors, | ||||||
|  |          packet_buffer + 2 + sizeof(hon_protocol::HaierPacketControl) + this->extra_control_packet_bytes_, | ||||||
|  |          sizeof(hon_protocol::HaierPacketSensors)); | ||||||
|   if (packet.sensors.error_status != 0) { |   if (packet.sensors.error_status != 0) { | ||||||
|     ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status); |     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))) { |   if ((this->outdoor_sensor_ != nullptr) && | ||||||
|     got_valid_outdoor_temp_ = true; |       (this->got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) { | ||||||
|  |     this->got_valid_outdoor_temp_ = true; | ||||||
|     float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET); |     float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET); | ||||||
|     if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp)) |     if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp)) | ||||||
|       this->outdoor_sensor_->publish_state(otemp); |       this->outdoor_sensor_->publish_state(otemp); | ||||||
| @@ -703,7 +684,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * | |||||||
|         // Do something only if display status changed |         // Do something only if display status changed | ||||||
|         if (this->mode == CLIMATE_MODE_OFF) { |         if (this->mode == CLIMATE_MODE_OFF) { | ||||||
|           // AC just turned on from remote need to turn off display |           // AC just turned on from remote need to turn off display | ||||||
|           this->set_force_send_control_(true); |           this->force_send_control_ = true; | ||||||
|         } else { |         } else { | ||||||
|           this->display_status_ = disp_status; |           this->display_status_ = disp_status; | ||||||
|         } |         } | ||||||
| @@ -732,7 +713,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * | |||||||
|       ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning); |       ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning); | ||||||
|       if (new_cleaning == CleaningState::NO_CLEANING) { |       if (new_cleaning == CleaningState::NO_CLEANING) { | ||||||
|         // Turning AC off after cleaning |         // Turning AC off after cleaning | ||||||
|         this->action_request_ = ActionRequest::TURN_POWER_OFF; |         this->action_request_ = | ||||||
|  |             PendingAction({ActionRequest::TURN_POWER_OFF, esphome::optional<haier_protocol::HaierMessage>()}); | ||||||
|       } |       } | ||||||
|       this->cleaning_status_ = new_cleaning; |       this->cleaning_status_ = new_cleaning; | ||||||
|     } |     } | ||||||
| @@ -783,51 +765,257 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t * | |||||||
|     should_publish = should_publish || (old_swing_mode != this->swing_mode); |     should_publish = should_publish || (old_swing_mode != this->swing_mode); | ||||||
|   } |   } | ||||||
|   this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); |   this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); | ||||||
|   if (this->forced_publish_ || should_publish) { |   if (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(); |     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) { |   if (should_publish) { | ||||||
|     ESP_LOGI(TAG, "HVAC values changed"); |     ESP_LOGI(TAG, "HVAC values changed"); | ||||||
|   } |   } | ||||||
|   esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, |   int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG; | ||||||
|                   "HVAC Mode = 0x%X", packet.control.ac_mode); |   esp_log_printf_(log_level, 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__, |   esp_log_printf_(log_level, TAG, __LINE__, "Fan speed Status = 0x%X", packet.control.fan_mode); | ||||||
|                   "Fan speed Status = 0x%X", packet.control.fan_mode); |   esp_log_printf_(log_level, 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__, |   esp_log_printf_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode); | ||||||
|                   "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode); |   esp_log_printf_(log_level, TAG, __LINE__, "Set Point Status = 0x%X", packet.control.set_point); | ||||||
|   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; |   return haier_protocol::HandlerError::HANDLER_OK; | ||||||
| } | } | ||||||
|  |  | ||||||
| bool HonClimate::is_message_invalid(uint8_t message_type) { | void HonClimate::fill_control_messages_queue_() { | ||||||
|   return message_type == (uint8_t) hon_protocol::FrameType::INVALID; |   static uint8_t one_buf[] = {0x00, 0x01}; | ||||||
|  |   static uint8_t zero_buf[] = {0x00, 0x00}; | ||||||
|  |   if (!this->current_hvac_settings_.valid && !this->force_send_control_) | ||||||
|  |     return; | ||||||
|  |   this->clear_control_messages_queue_(); | ||||||
|  |   HvacSettings climate_control; | ||||||
|  |   climate_control = this->current_hvac_settings_; | ||||||
|  |   // Beeper command | ||||||
|  |   { | ||||||
|  |     this->control_messages_queue_.push( | ||||||
|  |         haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, | ||||||
|  |                                      (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||||
|  |                                          (uint8_t) hon_protocol::DataParameters::BEEPER_STATUS, | ||||||
|  |                                      this->beeper_status_ ? zero_buf : one_buf, 2)); | ||||||
|  |   } | ||||||
|  |   // Health mode | ||||||
|  |   { | ||||||
|  |     this->control_messages_queue_.push( | ||||||
|  |         haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, | ||||||
|  |                                      (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||||
|  |                                          (uint8_t) hon_protocol::DataParameters::HEALTH_MODE, | ||||||
|  |                                      this->health_mode_ ? one_buf : zero_buf, 2)); | ||||||
|  |   } | ||||||
|  |   // Climate mode | ||||||
|  |   bool new_power = this->mode != CLIMATE_MODE_OFF; | ||||||
|  |   uint8_t fan_mode_buf[] = {0x00, 0xFF}; | ||||||
|  |   uint8_t quiet_mode_buf[] = {0x00, 0xFF}; | ||||||
|  |   if (climate_control.mode.has_value()) { | ||||||
|  |     uint8_t buffer[2] = {0x00, 0x00}; | ||||||
|  |     switch (climate_control.mode.value()) { | ||||||
|  |       case CLIMATE_MODE_OFF: | ||||||
|  |         new_power = false; | ||||||
|  |         break; | ||||||
|  |       case CLIMATE_MODE_HEAT_COOL: | ||||||
|  |         new_power = true; | ||||||
|  |         buffer[1] = (uint8_t) hon_protocol::ConditioningMode::AUTO; | ||||||
|  |         this->control_messages_queue_.push( | ||||||
|  |             haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, | ||||||
|  |                                          (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||||
|  |                                              (uint8_t) hon_protocol::DataParameters::AC_MODE, | ||||||
|  |                                          buffer, 2)); | ||||||
|  |         fan_mode_buf[1] = this->other_modes_fan_speed_; | ||||||
|  |         break; | ||||||
|  |       case CLIMATE_MODE_HEAT: | ||||||
|  |         new_power = true; | ||||||
|  |         buffer[1] = (uint8_t) hon_protocol::ConditioningMode::HEAT; | ||||||
|  |         this->control_messages_queue_.push( | ||||||
|  |             haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, | ||||||
|  |                                          (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||||
|  |                                              (uint8_t) hon_protocol::DataParameters::AC_MODE, | ||||||
|  |                                          buffer, 2)); | ||||||
|  |         fan_mode_buf[1] = this->other_modes_fan_speed_; | ||||||
|  |         break; | ||||||
|  |       case CLIMATE_MODE_DRY: | ||||||
|  |         new_power = true; | ||||||
|  |         buffer[1] = (uint8_t) hon_protocol::ConditioningMode::DRY; | ||||||
|  |         this->control_messages_queue_.push( | ||||||
|  |             haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, | ||||||
|  |                                          (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||||
|  |                                              (uint8_t) hon_protocol::DataParameters::AC_MODE, | ||||||
|  |                                          buffer, 2)); | ||||||
|  |         fan_mode_buf[1] = this->other_modes_fan_speed_; | ||||||
|  |         break; | ||||||
|  |       case CLIMATE_MODE_FAN_ONLY: | ||||||
|  |         new_power = true; | ||||||
|  |         buffer[1] = (uint8_t) hon_protocol::ConditioningMode::FAN; | ||||||
|  |         this->control_messages_queue_.push( | ||||||
|  |             haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, | ||||||
|  |                                          (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||||
|  |                                              (uint8_t) hon_protocol::DataParameters::AC_MODE, | ||||||
|  |                                          buffer, 2)); | ||||||
|  |         fan_mode_buf[1] = this->other_modes_fan_speed_;  // Auto doesn't work in fan only mode | ||||||
|  |         // Disabling eco mode for Fan only | ||||||
|  |         quiet_mode_buf[1] = 0; | ||||||
|  |         break; | ||||||
|  |       case CLIMATE_MODE_COOL: | ||||||
|  |         new_power = true; | ||||||
|  |         buffer[1] = (uint8_t) hon_protocol::ConditioningMode::COOL; | ||||||
|  |         this->control_messages_queue_.push( | ||||||
|  |             haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, | ||||||
|  |                                          (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||||
|  |                                              (uint8_t) hon_protocol::DataParameters::AC_MODE, | ||||||
|  |                                          buffer, 2)); | ||||||
|  |         fan_mode_buf[1] = this->other_modes_fan_speed_; | ||||||
|  |         break; | ||||||
|  |       default: | ||||||
|  |         ESP_LOGE("Control", "Unsupported climate mode"); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   // Climate power | ||||||
|  |   { | ||||||
|  |     this->control_messages_queue_.push( | ||||||
|  |         haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, | ||||||
|  |                                      (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||||
|  |                                          (uint8_t) hon_protocol::DataParameters::AC_POWER, | ||||||
|  |                                      new_power ? one_buf : zero_buf, 2)); | ||||||
|  |   } | ||||||
|  |   // CLimate preset | ||||||
|  |   { | ||||||
|  |     uint8_t fast_mode_buf[] = {0x00, 0xFF}; | ||||||
|  |     if (!new_power) { | ||||||
|  |       // If AC is off - no presets allowed | ||||||
|  |       quiet_mode_buf[1] = 0x00; | ||||||
|  |       fast_mode_buf[1] = 0x00; | ||||||
|  |     } else if (climate_control.preset.has_value()) { | ||||||
|  |       switch (climate_control.preset.value()) { | ||||||
|  |         case CLIMATE_PRESET_NONE: | ||||||
|  |           quiet_mode_buf[1] = 0x00; | ||||||
|  |           fast_mode_buf[1] = 0x00; | ||||||
|  |           break; | ||||||
|  |         case CLIMATE_PRESET_ECO: | ||||||
|  |           // Eco is not supported in Fan only mode | ||||||
|  |           quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00; | ||||||
|  |           fast_mode_buf[1] = 0x00; | ||||||
|  |           break; | ||||||
|  |         case CLIMATE_PRESET_BOOST: | ||||||
|  |           quiet_mode_buf[1] = 0x00; | ||||||
|  |           // Boost is not supported in Fan only mode | ||||||
|  |           fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00; | ||||||
|  |           break; | ||||||
|  |         default: | ||||||
|  |           ESP_LOGE("Control", "Unsupported preset"); | ||||||
|  |           break; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if (quiet_mode_buf[1] != 0xFF) { | ||||||
|  |       this->control_messages_queue_.push( | ||||||
|  |           haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, | ||||||
|  |                                        (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||||
|  |                                            (uint8_t) hon_protocol::DataParameters::QUIET_MODE, | ||||||
|  |                                        quiet_mode_buf, 2)); | ||||||
|  |     } | ||||||
|  |     if (fast_mode_buf[1] != 0xFF) { | ||||||
|  |       this->control_messages_queue_.push( | ||||||
|  |           haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, | ||||||
|  |                                        (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||||
|  |                                            (uint8_t) hon_protocol::DataParameters::FAST_MODE, | ||||||
|  |                                        fast_mode_buf, 2)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   // Target temperature | ||||||
|  |   if (climate_control.target_temperature.has_value()) { | ||||||
|  |     uint8_t buffer[2] = {0x00, 0x00}; | ||||||
|  |     buffer[1] = ((uint8_t) climate_control.target_temperature.value()) - 16; | ||||||
|  |     this->control_messages_queue_.push( | ||||||
|  |         haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, | ||||||
|  |                                      (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||||
|  |                                          (uint8_t) hon_protocol::DataParameters::SET_POINT, | ||||||
|  |                                      buffer, 2)); | ||||||
|  |   } | ||||||
|  |   // Fan mode | ||||||
|  |   if (climate_control.fan_mode.has_value()) { | ||||||
|  |     switch (climate_control.fan_mode.value()) { | ||||||
|  |       case CLIMATE_FAN_LOW: | ||||||
|  |         fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_LOW; | ||||||
|  |         break; | ||||||
|  |       case CLIMATE_FAN_MEDIUM: | ||||||
|  |         fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_MID; | ||||||
|  |         break; | ||||||
|  |       case CLIMATE_FAN_HIGH: | ||||||
|  |         fan_mode_buf[1] = (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 | ||||||
|  |           fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_AUTO; | ||||||
|  |         break; | ||||||
|  |       default: | ||||||
|  |         ESP_LOGE("Control", "Unsupported fan mode"); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |     if (fan_mode_buf[1] != 0xFF) { | ||||||
|  |       this->control_messages_queue_.push( | ||||||
|  |           haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, | ||||||
|  |                                        (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER + | ||||||
|  |                                            (uint8_t) hon_protocol::DataParameters::FAN_MODE, | ||||||
|  |                                        fan_mode_buf, 2)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void HonClimate::process_pending_action() { | void HonClimate::clear_control_messages_queue_() { | ||||||
|   switch (this->action_request_) { |   while (!this->control_messages_queue_.empty()) | ||||||
|     case ActionRequest::START_SELF_CLEAN: |     this->control_messages_queue_.pop(); | ||||||
|     case ActionRequest::START_STERI_CLEAN: | } | ||||||
|       // Will reset action with control message sending |  | ||||||
|       this->set_phase(ProtocolPhases::SENDING_CONTROL); | bool HonClimate::prepare_pending_action() { | ||||||
|       break; |   switch (this->action_request_.value().action) { | ||||||
|  |     case ActionRequest::START_SELF_CLEAN: { | ||||||
|  |       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; | ||||||
|  |       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; | ||||||
|  |       this->action_request_.value().message = haier_protocol::HaierMessage( | ||||||
|  |           haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, | ||||||
|  |           control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); | ||||||
|  |     } | ||||||
|  |       return true; | ||||||
|  |     case ActionRequest::START_STERI_CLEAN: { | ||||||
|  |       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; | ||||||
|  |       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; | ||||||
|  |       this->action_request_.value().message = haier_protocol::HaierMessage( | ||||||
|  |           haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, | ||||||
|  |           control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); | ||||||
|  |     } | ||||||
|  |       return true; | ||||||
|     default: |     default: | ||||||
|       HaierClimateBase::process_pending_action(); |       return HaierClimateBase::prepare_pending_action(); | ||||||
|       break; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void HonClimate::process_protocol_reset() { | ||||||
|  |   HaierClimateBase::process_protocol_reset(); | ||||||
|  |   if (this->outdoor_sensor_ != nullptr) { | ||||||
|  |     this->outdoor_sensor_->publish_state(NAN); | ||||||
|  |   } | ||||||
|  |   this->got_valid_outdoor_temp_ = false; | ||||||
|  |   this->hvac_hardware_info_.reset(); | ||||||
|  | } | ||||||
|  |  | ||||||
| }  // namespace haier | }  // namespace haier | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -30,6 +30,8 @@ enum class CleaningState : uint8_t { | |||||||
|   STERI_CLEAN = 2, |   STERI_CLEAN = 2, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER }; | ||||||
|  |  | ||||||
| class HonClimate : public HaierClimateBase { | class HonClimate : public HaierClimateBase { | ||||||
|  public: |  public: | ||||||
|   HonClimate(); |   HonClimate(); | ||||||
| @@ -48,44 +50,57 @@ class HonClimate : public HaierClimateBase { | |||||||
|   CleaningState get_cleaning_status() const; |   CleaningState get_cleaning_status() const; | ||||||
|   void start_self_cleaning(); |   void start_self_cleaning(); | ||||||
|   void start_steri_cleaning(); |   void start_steri_cleaning(); | ||||||
|  |   void set_extra_control_packet_bytes_size(size_t size) { this->extra_control_packet_bytes_ = size; }; | ||||||
|  |   void set_control_method(HonControlMethod method) { this->control_method_ = method; }; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   void set_handlers() override; |   void set_handlers() override; | ||||||
|   void process_phase(std::chrono::steady_clock::time_point now) override; |   void process_phase(std::chrono::steady_clock::time_point now) override; | ||||||
|   haier_protocol::HaierMessage get_control_message() override; |   haier_protocol::HaierMessage get_control_message() override; | ||||||
|   bool is_message_invalid(uint8_t message_type) override; |   haier_protocol::HaierMessage get_power_message(bool state) override; | ||||||
|   void process_pending_action() override; |   bool prepare_pending_action() override; | ||||||
|  |   void process_protocol_reset() override; | ||||||
|  |  | ||||||
|   // Answers handlers |   // Answers handlers | ||||||
|   haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, |   haier_protocol::HandlerError get_device_version_answer_handler_(haier_protocol::FrameType request_type, | ||||||
|  |                                                                   haier_protocol::FrameType message_type, | ||||||
|                                                                   const uint8_t *data, size_t data_size); |                                                                   const uint8_t *data, size_t data_size); | ||||||
|   haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, |   haier_protocol::HandlerError get_device_id_answer_handler_(haier_protocol::FrameType request_type, | ||||||
|  |                                                              haier_protocol::FrameType message_type, | ||||||
|                                                              const uint8_t *data, size_t data_size); |                                                              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, |   haier_protocol::HandlerError status_handler_(haier_protocol::FrameType request_type, | ||||||
|  |                                                haier_protocol::FrameType message_type, const uint8_t *data, | ||||||
|                                                size_t data_size); |                                                size_t data_size); | ||||||
|   haier_protocol::HandlerError get_management_information_answer_handler_(uint8_t request_type, uint8_t message_type, |   haier_protocol::HandlerError get_management_information_answer_handler_(haier_protocol::FrameType request_type, | ||||||
|  |                                                                           haier_protocol::FrameType message_type, | ||||||
|                                                                           const uint8_t *data, size_t data_size); |                                                                           const uint8_t *data, size_t data_size); | ||||||
|   haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type, |   haier_protocol::HandlerError get_alarm_status_answer_handler_(haier_protocol::FrameType request_type, | ||||||
|                                                                      const uint8_t *data, size_t data_size); |                                                                 haier_protocol::FrameType message_type, | ||||||
|   haier_protocol::HandlerError get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type, |  | ||||||
|                                                                 const uint8_t *data, size_t data_size); |                                                                 const uint8_t *data, size_t data_size); | ||||||
|   // Helper functions |   // Helper functions | ||||||
|   haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); |   haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); | ||||||
|   std::unique_ptr<uint8_t[]> last_status_message_; |   void fill_control_messages_queue_(); | ||||||
|  |   void clear_control_messages_queue_(); | ||||||
|  |  | ||||||
|  |   struct HardwareInfo { | ||||||
|  |     std::string protocol_version_; | ||||||
|  |     std::string software_version_; | ||||||
|  |     std::string hardware_version_; | ||||||
|  |     std::string device_name_; | ||||||
|  |     bool functions_[5]; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   bool beeper_status_; |   bool beeper_status_; | ||||||
|   CleaningState cleaning_status_; |   CleaningState cleaning_status_; | ||||||
|   bool got_valid_outdoor_temp_; |   bool got_valid_outdoor_temp_; | ||||||
|   AirflowVerticalDirection vertical_direction_; |   AirflowVerticalDirection vertical_direction_; | ||||||
|   AirflowHorizontalDirection horizontal_direction_; |   AirflowHorizontalDirection horizontal_direction_; | ||||||
|   bool hvac_hardware_info_available_; |   esphome::optional<HardwareInfo> hvac_hardware_info_; | ||||||
|   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]; |   uint8_t active_alarms_[8]; | ||||||
|  |   int extra_control_packet_bytes_; | ||||||
|  |   HonControlMethod control_method_; | ||||||
|   esphome::sensor::Sensor *outdoor_sensor_; |   esphome::sensor::Sensor *outdoor_sensor_; | ||||||
|  |   std::queue<haier_protocol::HaierMessage> control_messages_queue_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace haier | }  // namespace haier | ||||||
|   | |||||||
| @@ -35,6 +35,20 @@ enum class ConditioningMode : uint8_t { | |||||||
|   FAN = 0x06 |   FAN = 0x06 | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | enum class DataParameters : uint8_t { | ||||||
|  |   AC_POWER = 0x01, | ||||||
|  |   SET_POINT = 0x02, | ||||||
|  |   AC_MODE = 0x04, | ||||||
|  |   FAN_MODE = 0x05, | ||||||
|  |   USE_FAHRENHEIT = 0x07, | ||||||
|  |   TEN_DEGREE = 0x0A, | ||||||
|  |   HEALTH_MODE = 0x0B, | ||||||
|  |   BEEPER_STATUS = 0x16, | ||||||
|  |   LOCK_REMOTE = 0x17, | ||||||
|  |   QUIET_MODE = 0x19, | ||||||
|  |   FAST_MODE = 0x1A, | ||||||
|  | }; | ||||||
|  |  | ||||||
| enum class SpecialMode : uint8_t { NONE = 0x00, ELDERLY = 0x01, CHILDREN = 0x02, PREGNANT = 0x03 }; | 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 }; | enum class FanMode : uint8_t { FAN_HIGH = 0x01, FAN_MID = 0x02, FAN_LOW = 0x03, FAN_AUTO = 0x05 }; | ||||||
| @@ -124,11 +138,7 @@ struct HaierPacketSensors { | |||||||
|   uint16_t co2_value;  // CO2 value (0 PPM -  10000 PPM, 1 PPM step) |   uint16_t co2_value;  // CO2 value (0 PPM -  10000 PPM, 1 PPM step) | ||||||
| }; | }; | ||||||
|  |  | ||||||
| struct HaierStatus { | constexpr size_t HAIER_STATUS_FRAME_SIZE = 2 + sizeof(HaierPacketControl) + sizeof(HaierPacketSensors); | ||||||
|   uint16_t subcommand; |  | ||||||
|   HaierPacketControl control; |  | ||||||
|   HaierPacketSensors sensors; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| struct DeviceVersionAnswer { | struct DeviceVersionAnswer { | ||||||
|   char protocol_version[8]; |   char protocol_version[8]; | ||||||
| @@ -140,76 +150,6 @@ struct DeviceVersionAnswer { | |||||||
|   uint8_t functions[2]; |   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_DOWNLINK = 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 SubcommandsControl : uint16_t { | enum class SubcommandsControl : uint16_t { | ||||||
|   GET_PARAMETERS = 0x4C01,  // Request specific parameters (packet content: parameter ID1 + parameter ID2 + ...) |   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_USER_DATA = 0x4D01,   // Request all user data from device (packet content: None) | ||||||
|   | |||||||
| @@ -12,21 +12,28 @@ namespace haier { | |||||||
|  |  | ||||||
| static const char *const TAG = "haier.climate"; | static const char *const TAG = "haier.climate"; | ||||||
| constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; | constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; | ||||||
|  | constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5; | ||||||
|  | constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500); | ||||||
|  | constexpr uint8_t INIT_REQUESTS_RETRY = 2; | ||||||
|  | constexpr std::chrono::milliseconds INIT_REQUESTS_RETRY_INTERVAL = std::chrono::milliseconds(2000); | ||||||
|  |  | ||||||
| Smartair2Climate::Smartair2Climate() | Smartair2Climate::Smartair2Climate() { | ||||||
|     : last_status_message_(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]), timeouts_counter_(0) {} |   last_status_message_ = std::unique_ptr<uint8_t[]>(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]); | ||||||
|  | } | ||||||
|  |  | ||||||
| haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_type, uint8_t message_type, | haier_protocol::HandlerError Smartair2Climate::status_handler_(haier_protocol::FrameType request_type, | ||||||
|  |                                                                haier_protocol::FrameType message_type, | ||||||
|                                                                const uint8_t *data, size_t data_size) { |                                                                const uint8_t *data, size_t data_size) { | ||||||
|   haier_protocol::HandlerError result = |   haier_protocol::HandlerError result = | ||||||
|       this->answer_preprocess_(request_type, (uint8_t) smartair2_protocol::FrameType::CONTROL, message_type, |       this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type, | ||||||
|                                (uint8_t) smartair2_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); |                                haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); | ||||||
|   if (result == haier_protocol::HandlerError::HANDLER_OK) { |   if (result == haier_protocol::HandlerError::HANDLER_OK) { | ||||||
|     result = this->process_status_message_(data, data_size); |     result = this->process_status_message_(data, data_size); | ||||||
|     if (result != haier_protocol::HandlerError::HANDLER_OK) { |     if (result != haier_protocol::HandlerError::HANDLER_OK) { | ||||||
|       ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); |       ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); | ||||||
|       this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE |       this->reset_phase_(); | ||||||
|                                                                       : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); |       this->action_request_.reset(); | ||||||
|  |       this->force_send_control_ = false; | ||||||
|     } else { |     } else { | ||||||
|       if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) { |       if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) { | ||||||
|         memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl)); |         memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl)); | ||||||
| @@ -34,36 +41,45 @@ haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_t | |||||||
|         ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, |         ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, | ||||||
|                  sizeof(smartair2_protocol::HaierPacketControl)); |                  sizeof(smartair2_protocol::HaierPacketControl)); | ||||||
|       } |       } | ||||||
|       if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { |       switch (this->protocol_phase_) { | ||||||
|         ESP_LOGI(TAG, "First HVAC status received"); |         case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: | ||||||
|         this->set_phase(ProtocolPhases::IDLE); |           ESP_LOGI(TAG, "First HVAC status received"); | ||||||
|       } else if (this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) { |           this->set_phase(ProtocolPhases::IDLE); | ||||||
|         this->set_phase(ProtocolPhases::IDLE); |           break; | ||||||
|       } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { |         case ProtocolPhases::SENDING_ACTION_COMMAND: | ||||||
|         this->set_phase(ProtocolPhases::IDLE); |           // Do nothing, phase will be changed in process_phase | ||||||
|         this->set_force_send_control_(false); |           break; | ||||||
|         if (this->hvac_settings_.valid) |         case ProtocolPhases::SENDING_STATUS_REQUEST: | ||||||
|           this->hvac_settings_.reset(); |           this->set_phase(ProtocolPhases::IDLE); | ||||||
|  |           break; | ||||||
|  |         case ProtocolPhases::SENDING_CONTROL: | ||||||
|  |           this->set_phase(ProtocolPhases::IDLE); | ||||||
|  |           this->force_send_control_ = false; | ||||||
|  |           if (this->current_hvac_settings_.valid) | ||||||
|  |             this->current_hvac_settings_.reset(); | ||||||
|  |           break; | ||||||
|  |         default: | ||||||
|  |           break; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     return result; |     return result; | ||||||
|   } else { |   } else { | ||||||
|     this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE |     this->action_request_.reset(); | ||||||
|                                                                     : ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); |     this->force_send_control_ = false; | ||||||
|  |     this->reset_phase_(); | ||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler_(uint8_t request_type, | haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler_( | ||||||
|                                                                                   uint8_t message_type, |     haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data, | ||||||
|                                                                                   const uint8_t *data, |     size_t data_size) { | ||||||
|                                                                                   size_t data_size) { |   if (request_type != haier_protocol::FrameType::GET_DEVICE_VERSION) | ||||||
|   if (request_type != (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION) |  | ||||||
|     return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; |     return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; | ||||||
|   if (ProtocolPhases::WAITING_INIT_1_ANSWER != this->protocol_phase_) |   if (ProtocolPhases::SENDING_INIT_1 != this->protocol_phase_) | ||||||
|     return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; |     return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; | ||||||
|   // Invalid packet is expected answer |   // Invalid packet is expected answer | ||||||
|   if ((message_type == (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE) && (data_size >= 39) && |   if ((message_type == haier_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE) && (data_size >= 39) && | ||||||
|       ((data[37] & 0x04) != 0)) { |       ((data[37] & 0x04) != 0)) { | ||||||
|     ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the hOn protocol " |     ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the hOn protocol " | ||||||
|                   "instead of smartAir2"); |                   "instead of smartAir2"); | ||||||
| @@ -72,58 +88,35 @@ haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler | |||||||
|   return haier_protocol::HandlerError::HANDLER_OK; |   return haier_protocol::HandlerError::HANDLER_OK; | ||||||
| } | } | ||||||
|  |  | ||||||
| haier_protocol::HandlerError Smartair2Climate::report_network_status_answer_handler_(uint8_t request_type, | haier_protocol::HandlerError Smartair2Climate::messages_timeout_handler_with_cycle_for_init_( | ||||||
|                                                                                      uint8_t message_type, |     haier_protocol::FrameType message_type) { | ||||||
|                                                                                      const uint8_t *data, |  | ||||||
|                                                                                      size_t data_size) { |  | ||||||
|   haier_protocol::HandlerError result = this->answer_preprocess_( |  | ||||||
|       request_type, (uint8_t) smartair2_protocol::FrameType::REPORT_NETWORK_STATUS, message_type, |  | ||||||
|       (uint8_t) smartair2_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); |  | ||||||
|   this->set_phase(ProtocolPhases::IDLE); |  | ||||||
|   return result; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| haier_protocol::HandlerError Smartair2Climate::initial_messages_timeout_handler_(uint8_t message_type) { |  | ||||||
|   if (this->protocol_phase_ >= ProtocolPhases::IDLE) |   if (this->protocol_phase_ >= ProtocolPhases::IDLE) | ||||||
|     return HaierClimateBase::timeout_default_handler_(message_type); |     return HaierClimateBase::timeout_default_handler_(message_type); | ||||||
|   this->timeouts_counter_++; |   ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type, | ||||||
|   ESP_LOGI(TAG, "Answer timeout for command %02X, phase %d, timeout counter %d", message_type, |            phase_to_string_(this->protocol_phase_)); | ||||||
|            (int) this->protocol_phase_, this->timeouts_counter_); |   ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1); | ||||||
|   if (this->timeouts_counter_ >= 3) { |   if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) | ||||||
|     ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1); |     new_phase = ProtocolPhases::SENDING_INIT_1; | ||||||
|     if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) |   this->set_phase(new_phase); | ||||||
|       new_phase = ProtocolPhases::SENDING_INIT_1; |  | ||||||
|     this->set_phase(new_phase); |  | ||||||
|   } else { |  | ||||||
|     // Returning to the previous state to try again |  | ||||||
|     this->set_phase((ProtocolPhases) ((int) this->protocol_phase_ - 1)); |  | ||||||
|   } |  | ||||||
|   return haier_protocol::HandlerError::HANDLER_OK; |   return haier_protocol::HandlerError::HANDLER_OK; | ||||||
| } | } | ||||||
|  |  | ||||||
| void Smartair2Climate::set_handlers() { | void Smartair2Climate::set_handlers() { | ||||||
|   // Set handlers |   // Set handlers | ||||||
|   this->haier_protocol_.set_answer_handler( |   this->haier_protocol_.set_answer_handler( | ||||||
|       (uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_VERSION), |       haier_protocol::FrameType::GET_DEVICE_VERSION, | ||||||
|       std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1, |       std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1, | ||||||
|                 std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); |                 std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); | ||||||
|   this->haier_protocol_.set_answer_handler( |   this->haier_protocol_.set_answer_handler( | ||||||
|       (uint8_t) (smartair2_protocol::FrameType::CONTROL), |       haier_protocol::FrameType::CONTROL, | ||||||
|       std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, |       std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, | ||||||
|                 std::placeholders::_3, std::placeholders::_4)); |                 std::placeholders::_3, std::placeholders::_4)); | ||||||
|   this->haier_protocol_.set_answer_handler( |   this->haier_protocol_.set_answer_handler( | ||||||
|       (uint8_t) (smartair2_protocol::FrameType::REPORT_NETWORK_STATUS), |       haier_protocol::FrameType::REPORT_NETWORK_STATUS, | ||||||
|       std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1, |       std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1, | ||||||
|                 std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); |                 std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); | ||||||
|   this->haier_protocol_.set_timeout_handler( |   this->haier_protocol_.set_default_timeout_handler( | ||||||
|       (uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_ID), |       std::bind(&Smartair2Climate::messages_timeout_handler_with_cycle_for_init_, this, std::placeholders::_1)); | ||||||
|       std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1)); |  | ||||||
|   this->haier_protocol_.set_timeout_handler( |  | ||||||
|       (uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_VERSION), |  | ||||||
|       std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1)); |  | ||||||
|   this->haier_protocol_.set_timeout_handler( |  | ||||||
|       (uint8_t) (smartair2_protocol::FrameType::CONTROL), |  | ||||||
|       std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1)); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void Smartair2Climate::dump_config() { | void Smartair2Climate::dump_config() { | ||||||
| @@ -134,9 +127,7 @@ void Smartair2Climate::dump_config() { | |||||||
| void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) { | void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) { | ||||||
|   switch (this->protocol_phase_) { |   switch (this->protocol_phase_) { | ||||||
|     case ProtocolPhases::SENDING_INIT_1: |     case ProtocolPhases::SENDING_INIT_1: | ||||||
|       if (this->can_send_message() && |       if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) { | ||||||
|           (((this->timeouts_counter_ == 0) && (this->is_protocol_initialisation_interval_exceeded_(now))) || |  | ||||||
|            ((this->timeouts_counter_ > 0) && (this->is_message_interval_exceeded_(now))))) { |  | ||||||
|         // Indicate device capabilities: |         // Indicate device capabilities: | ||||||
|         // bit 0 - if 1 module support interactive mode |         // bit 0 - if 1 module support interactive mode | ||||||
|         // bit 1 - if 1 module support controller-device mode |         // bit 1 - if 1 module support controller-device mode | ||||||
| @@ -145,92 +136,65 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) | |||||||
|         // bit 4..bit 15 - not used |         // bit 4..bit 15 - not used | ||||||
|         uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; |         uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; | ||||||
|         static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( |         static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( | ||||||
|             (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, |             haier_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); | ||||||
|             sizeof(module_capabilities)); |         this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_, INIT_REQUESTS_RETRY, INIT_REQUESTS_RETRY_INTERVAL); | ||||||
|         this->send_message_(DEVICE_VERSION_REQUEST, false); |  | ||||||
|         this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER); |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     case ProtocolPhases::SENDING_INIT_2: |     case ProtocolPhases::SENDING_INIT_2: | ||||||
|     case ProtocolPhases::WAITING_INIT_2_ANSWER: |  | ||||||
|       this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); |       this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); | ||||||
|       break; |       break; | ||||||
|     case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: |     case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: | ||||||
|     case ProtocolPhases::SENDING_STATUS_REQUEST: |     case ProtocolPhases::SENDING_STATUS_REQUEST: | ||||||
|       if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { |       if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { | ||||||
|         static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL, |         static const haier_protocol::HaierMessage STATUS_REQUEST(haier_protocol::FrameType::CONTROL, 0x4D01); | ||||||
|                                                                  0x4D01); |         if (this->protocol_phase_ == ProtocolPhases::SENDING_FIRST_STATUS_REQUEST) { | ||||||
|         this->send_message_(STATUS_REQUEST, false); |           this->send_message_(STATUS_REQUEST, this->use_crc_, INIT_REQUESTS_RETRY, INIT_REQUESTS_RETRY_INTERVAL); | ||||||
|  |         } else { | ||||||
|  |           this->send_message_(STATUS_REQUEST, this->use_crc_); | ||||||
|  |         } | ||||||
|         this->last_status_request_ = now; |         this->last_status_request_ = now; | ||||||
|         this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1)); |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
| #ifdef USE_WIFI | #ifdef USE_WIFI | ||||||
|     case ProtocolPhases::SENDING_SIGNAL_LEVEL: |     case ProtocolPhases::SENDING_SIGNAL_LEVEL: | ||||||
|       if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { |       if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { | ||||||
|         this->send_message_( |         this->send_message_(this->get_wifi_signal_message_(), this->use_crc_); | ||||||
|             this->get_wifi_signal_message_((uint8_t) smartair2_protocol::FrameType::REPORT_NETWORK_STATUS), false); |  | ||||||
|         this->last_signal_request_ = now; |         this->last_signal_request_ = now; | ||||||
|         this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER); |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: |  | ||||||
|       break; |  | ||||||
| #else | #else | ||||||
|     case ProtocolPhases::SENDING_SIGNAL_LEVEL: |     case ProtocolPhases::SENDING_SIGNAL_LEVEL: | ||||||
|     case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER: |  | ||||||
|       this->set_phase(ProtocolPhases::IDLE); |       this->set_phase(ProtocolPhases::IDLE); | ||||||
|       break; |       break; | ||||||
| #endif | #endif | ||||||
|     case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: |     case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: | ||||||
|     case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER: |  | ||||||
|       this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); |       this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); | ||||||
|       break; |       break; | ||||||
|     case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: |     case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: | ||||||
|     case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER: |  | ||||||
|       this->set_phase(ProtocolPhases::SENDING_INIT_1); |       this->set_phase(ProtocolPhases::SENDING_INIT_1); | ||||||
|       break; |       break; | ||||||
|     case ProtocolPhases::SENDING_CONTROL: |     case ProtocolPhases::SENDING_CONTROL: | ||||||
|       if (this->first_control_attempt_) { |       if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { | ||||||
|         this->control_request_timestamp_ = now; |         ESP_LOGI(TAG, "Sending control packet"); | ||||||
|         this->first_control_attempt_ = false; |         this->send_message_(get_control_message(), this->use_crc_, CONTROL_MESSAGE_RETRIES, | ||||||
|  |                             CONTROL_MESSAGE_RETRIES_INTERVAL); | ||||||
|       } |       } | ||||||
|       if (this->is_control_message_timeout_exceeded_(now)) { |       break; | ||||||
|         ESP_LOGW(TAG, "Sending control packet timeout!"); |     case ProtocolPhases::SENDING_ACTION_COMMAND: | ||||||
|         this->set_force_send_control_(false); |       if (this->action_request_.has_value()) { | ||||||
|         if (this->hvac_settings_.valid) |         if (this->action_request_.value().message.has_value()) { | ||||||
|           this->hvac_settings_.reset(); |           this->send_message_(this->action_request_.value().message.value(), this->use_crc_); | ||||||
|         this->forced_request_status_ = true; |           this->action_request_.value().message.reset(); | ||||||
|         this->forced_publish_ = true; |         } else { | ||||||
|  |           // Message already sent, reseting request and return to idle | ||||||
|  |           this->action_request_.reset(); | ||||||
|  |           this->set_phase(ProtocolPhases::IDLE); | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         ESP_LOGW(TAG, "SENDING_ACTION_COMMAND phase without action request!"); | ||||||
|         this->set_phase(ProtocolPhases::IDLE); |         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; |       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_INIT_1_ANSWER: |  | ||||||
|     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: { |     case ProtocolPhases::IDLE: { | ||||||
|       if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { |       if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { | ||||||
|         this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); |         this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); | ||||||
| @@ -245,55 +209,55 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) | |||||||
|     } break; |     } break; | ||||||
|     default: |     default: | ||||||
|       // Shouldn't get here |       // Shouldn't get here | ||||||
| #if (HAIER_LOG_LEVEL > 4) |  | ||||||
|       ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", |       ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", | ||||||
|                phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); |                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); |       this->set_phase(ProtocolPhases::SENDING_INIT_1); | ||||||
|       break; |       break; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | haier_protocol::HaierMessage Smartair2Climate::get_power_message(bool state) { | ||||||
|  |   if (state) { | ||||||
|  |     static haier_protocol::HaierMessage power_on_message(haier_protocol::FrameType::CONTROL, 0x4D02); | ||||||
|  |     return power_on_message; | ||||||
|  |   } else { | ||||||
|  |     static haier_protocol::HaierMessage power_off_message(haier_protocol::FrameType::CONTROL, 0x4D03); | ||||||
|  |     return power_off_message; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| haier_protocol::HaierMessage Smartair2Climate::get_control_message() { | haier_protocol::HaierMessage Smartair2Climate::get_control_message() { | ||||||
|   uint8_t control_out_buffer[sizeof(smartair2_protocol::HaierPacketControl)]; |   uint8_t control_out_buffer[sizeof(smartair2_protocol::HaierPacketControl)]; | ||||||
|   memcpy(control_out_buffer, this->last_status_message_.get(), 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; |   smartair2_protocol::HaierPacketControl *out_data = (smartair2_protocol::HaierPacketControl *) control_out_buffer; | ||||||
|   out_data->cntrl = 0; |   out_data->cntrl = 0; | ||||||
|   if (this->hvac_settings_.valid) { |   if (this->current_hvac_settings_.valid) { | ||||||
|     HvacSettings climate_control; |     HvacSettings &climate_control = this->current_hvac_settings_; | ||||||
|     climate_control = this->hvac_settings_; |  | ||||||
|     if (climate_control.mode.has_value()) { |     if (climate_control.mode.has_value()) { | ||||||
|       switch (climate_control.mode.value()) { |       switch (climate_control.mode.value()) { | ||||||
|         case CLIMATE_MODE_OFF: |         case CLIMATE_MODE_OFF: | ||||||
|           out_data->ac_power = 0; |           out_data->ac_power = 0; | ||||||
|           break; |           break; | ||||||
|  |  | ||||||
|         case CLIMATE_MODE_HEAT_COOL: |         case CLIMATE_MODE_HEAT_COOL: | ||||||
|           out_data->ac_power = 1; |           out_data->ac_power = 1; | ||||||
|           out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO; |           out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO; | ||||||
|           out_data->fan_mode = this->other_modes_fan_speed_; |           out_data->fan_mode = this->other_modes_fan_speed_; | ||||||
|           break; |           break; | ||||||
|  |  | ||||||
|         case CLIMATE_MODE_HEAT: |         case CLIMATE_MODE_HEAT: | ||||||
|           out_data->ac_power = 1; |           out_data->ac_power = 1; | ||||||
|           out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT; |           out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT; | ||||||
|           out_data->fan_mode = this->other_modes_fan_speed_; |           out_data->fan_mode = this->other_modes_fan_speed_; | ||||||
|           break; |           break; | ||||||
|  |  | ||||||
|         case CLIMATE_MODE_DRY: |         case CLIMATE_MODE_DRY: | ||||||
|           out_data->ac_power = 1; |           out_data->ac_power = 1; | ||||||
|           out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY; |           out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY; | ||||||
|           out_data->fan_mode = this->other_modes_fan_speed_; |           out_data->fan_mode = this->other_modes_fan_speed_; | ||||||
|           break; |           break; | ||||||
|  |  | ||||||
|         case CLIMATE_MODE_FAN_ONLY: |         case CLIMATE_MODE_FAN_ONLY: | ||||||
|           out_data->ac_power = 1; |           out_data->ac_power = 1; | ||||||
|           out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::FAN; |           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 |           out_data->fan_mode = this->fan_mode_speed_;  // Auto doesn't work in fan only mode | ||||||
|           break; |           break; | ||||||
|  |  | ||||||
|         case CLIMATE_MODE_COOL: |         case CLIMATE_MODE_COOL: | ||||||
|           out_data->ac_power = 1; |           out_data->ac_power = 1; | ||||||
|           out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL; |           out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL; | ||||||
| @@ -327,32 +291,49 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() { | |||||||
|     } |     } | ||||||
|     // Set swing mode |     // Set swing mode | ||||||
|     if (climate_control.swing_mode.has_value()) { |     if (climate_control.swing_mode.has_value()) { | ||||||
|       switch (climate_control.swing_mode.value()) { |       if (this->use_alternative_swing_control_) { | ||||||
|         case CLIMATE_SWING_OFF: |         switch (climate_control.swing_mode.value()) { | ||||||
|           out_data->use_swing_bits = 0; |           case CLIMATE_SWING_OFF: | ||||||
|           out_data->swing_both = 0; |             out_data->swing_mode = 0; | ||||||
|           break; |             break; | ||||||
|         case CLIMATE_SWING_VERTICAL: |           case CLIMATE_SWING_VERTICAL: | ||||||
|           out_data->swing_both = 0; |             out_data->swing_mode = 1; | ||||||
|           out_data->vertical_swing = 1; |             break; | ||||||
|           out_data->horizontal_swing = 0; |           case CLIMATE_SWING_HORIZONTAL: | ||||||
|           break; |             out_data->swing_mode = 2; | ||||||
|         case CLIMATE_SWING_HORIZONTAL: |             break; | ||||||
|           out_data->swing_both = 0; |           case CLIMATE_SWING_BOTH: | ||||||
|           out_data->vertical_swing = 0; |             out_data->swing_mode = 3; | ||||||
|           out_data->horizontal_swing = 1; |             break; | ||||||
|           break; |         } | ||||||
|         case CLIMATE_SWING_BOTH: |       } else { | ||||||
|           out_data->swing_both = 1; |         switch (climate_control.swing_mode.value()) { | ||||||
|           out_data->use_swing_bits = 0; |           case CLIMATE_SWING_OFF: | ||||||
|           out_data->vertical_swing = 0; |             out_data->use_swing_bits = 0; | ||||||
|           out_data->horizontal_swing = 0; |             out_data->swing_mode = 0; | ||||||
|           break; |             break; | ||||||
|  |           case CLIMATE_SWING_VERTICAL: | ||||||
|  |             out_data->swing_mode = 0; | ||||||
|  |             out_data->vertical_swing = 1; | ||||||
|  |             out_data->horizontal_swing = 0; | ||||||
|  |             break; | ||||||
|  |           case CLIMATE_SWING_HORIZONTAL: | ||||||
|  |             out_data->swing_mode = 0; | ||||||
|  |             out_data->vertical_swing = 0; | ||||||
|  |             out_data->horizontal_swing = 1; | ||||||
|  |             break; | ||||||
|  |           case CLIMATE_SWING_BOTH: | ||||||
|  |             out_data->swing_mode = 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()) { |     if (climate_control.target_temperature.has_value()) { | ||||||
|       float target_temp = climate_control.target_temperature.value(); |       float target_temp = climate_control.target_temperature.value(); | ||||||
|       out_data->set_point = target_temp - 16;  // set the temperature with offset 16 |       out_data->set_point = ((int) target_temp) - 16;  // set the temperature with offset 16 | ||||||
|       out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; |       out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; | ||||||
|     } |     } | ||||||
|     if (out_data->ac_power == 0) { |     if (out_data->ac_power == 0) { | ||||||
| @@ -383,7 +364,7 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() { | |||||||
|   } |   } | ||||||
|   out_data->display_status = this->display_status_ ? 0 : 1; |   out_data->display_status = this->display_status_ ? 0 : 1; | ||||||
|   out_data->health_mode = this->health_mode_ ? 1 : 0; |   out_data->health_mode = this->health_mode_ ? 1 : 0; | ||||||
|   return haier_protocol::HaierMessage((uint8_t) smartair2_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer, |   return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer, | ||||||
|                                       sizeof(smartair2_protocol::HaierPacketControl)); |                                       sizeof(smartair2_protocol::HaierPacketControl)); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -459,13 +440,19 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin | |||||||
|         // Do something only if display status changed |         // Do something only if display status changed | ||||||
|         if (this->mode == CLIMATE_MODE_OFF) { |         if (this->mode == CLIMATE_MODE_OFF) { | ||||||
|           // AC just turned on from remote need to turn off display |           // AC just turned on from remote need to turn off display | ||||||
|           this->set_force_send_control_(true); |           this->force_send_control_ = true; | ||||||
|         } else { |         } else { | ||||||
|           this->display_status_ = disp_status; |           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_); | ||||||
|  |   } | ||||||
|   { |   { | ||||||
|     // Climate mode |     // Climate mode | ||||||
|     ClimateMode old_mode = this->mode; |     ClimateMode old_mode = this->mode; | ||||||
| @@ -493,70 +480,57 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin | |||||||
|     } |     } | ||||||
|     should_publish = should_publish || (old_mode != this->mode); |     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 |     // Swing mode | ||||||
|     ClimateSwingMode old_swing_mode = this->swing_mode; |     ClimateSwingMode old_swing_mode = this->swing_mode; | ||||||
|     if (packet.control.swing_both == 0) { |     if (this->use_alternative_swing_control_) { | ||||||
|       if (packet.control.vertical_swing != 0) { |       switch (packet.control.swing_mode) { | ||||||
|         this->swing_mode = CLIMATE_SWING_VERTICAL; |         case 1: | ||||||
|       } else if (packet.control.horizontal_swing != 0) { |           this->swing_mode = CLIMATE_SWING_VERTICAL; | ||||||
|         this->swing_mode = CLIMATE_SWING_HORIZONTAL; |           break; | ||||||
|       } else { |         case 2: | ||||||
|         this->swing_mode = CLIMATE_SWING_OFF; |           this->swing_mode = CLIMATE_SWING_HORIZONTAL; | ||||||
|  |           break; | ||||||
|  |         case 3: | ||||||
|  |           this->swing_mode = CLIMATE_SWING_BOTH; | ||||||
|  |           break; | ||||||
|  |         default: | ||||||
|  |           this->swing_mode = CLIMATE_SWING_OFF; | ||||||
|  |           break; | ||||||
|       } |       } | ||||||
|     } else { |     } else { | ||||||
|       swing_mode = CLIMATE_SWING_BOTH; |       if (packet.control.swing_mode == 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); |     should_publish = should_publish || (old_swing_mode != this->swing_mode); | ||||||
|   } |   } | ||||||
|   this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); |   this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); | ||||||
|   if (this->forced_publish_ || should_publish) { |   if (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(); |     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) { |   if (should_publish) { | ||||||
|     ESP_LOGI(TAG, "HVAC values changed"); |     ESP_LOGI(TAG, "HVAC values changed"); | ||||||
|   } |   } | ||||||
|   esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, |   int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG; | ||||||
|                   "HVAC Mode = 0x%X", packet.control.ac_mode); |   esp_log_printf_(log_level, 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__, |   esp_log_printf_(log_level, TAG, __LINE__, "Fan speed Status = 0x%X", packet.control.fan_mode); | ||||||
|                   "Fan speed Status = 0x%X", packet.control.fan_mode); |   esp_log_printf_(log_level, 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__, |   esp_log_printf_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_swing); | ||||||
|                   "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing); |   esp_log_printf_(log_level, TAG, __LINE__, "Set Point Status = 0x%X", packet.control.set_point); | ||||||
|   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; |   return haier_protocol::HandlerError::HANDLER_OK; | ||||||
| } | } | ||||||
|  |  | ||||||
| bool Smartair2Climate::is_message_invalid(uint8_t message_type) { | void Smartair2Climate::set_alternative_swing_control(bool swing_control) { | ||||||
|   return message_type == (uint8_t) smartair2_protocol::FrameType::INVALID; |   this->use_alternative_swing_control_ = swing_control; | ||||||
| } |  | ||||||
|  |  | ||||||
| void Smartair2Climate::set_phase(HaierClimateBase::ProtocolPhases phase) { |  | ||||||
|   int old_phase = (int) this->protocol_phase_; |  | ||||||
|   int new_phase = (int) phase; |  | ||||||
|   int min_p = std::min(old_phase, new_phase); |  | ||||||
|   int max_p = std::max(old_phase, new_phase); |  | ||||||
|   if ((min_p % 2 != 0) || (max_p - min_p > 1)) |  | ||||||
|     this->timeouts_counter_ = 0; |  | ||||||
|   HaierClimateBase::set_phase(phase); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| }  // namespace haier | }  // namespace haier | ||||||
|   | |||||||
| @@ -13,27 +13,27 @@ class Smartair2Climate : public HaierClimateBase { | |||||||
|   Smartair2Climate &operator=(const Smartair2Climate &) = delete; |   Smartair2Climate &operator=(const Smartair2Climate &) = delete; | ||||||
|   ~Smartair2Climate(); |   ~Smartair2Climate(); | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|  |   void set_alternative_swing_control(bool swing_control); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   void set_handlers() override; |   void set_handlers() override; | ||||||
|   void process_phase(std::chrono::steady_clock::time_point now) override; |   void process_phase(std::chrono::steady_clock::time_point now) override; | ||||||
|  |   haier_protocol::HaierMessage get_power_message(bool state) override; | ||||||
|   haier_protocol::HaierMessage get_control_message() override; |   haier_protocol::HaierMessage get_control_message() override; | ||||||
|   bool is_message_invalid(uint8_t message_type) override; |   // Answer handlers | ||||||
|   void set_phase(HaierClimateBase::ProtocolPhases phase) override; |   haier_protocol::HandlerError status_handler_(haier_protocol::FrameType request_type, | ||||||
|   // Answer and timeout handlers |                                                haier_protocol::FrameType message_type, const uint8_t *data, | ||||||
|   haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, |  | ||||||
|                                                size_t data_size); |                                                size_t data_size); | ||||||
|   haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, |   haier_protocol::HandlerError get_device_version_answer_handler_(haier_protocol::FrameType request_type, | ||||||
|  |                                                                   haier_protocol::FrameType message_type, | ||||||
|                                                                   const uint8_t *data, size_t data_size); |                                                                   const uint8_t *data, size_t data_size); | ||||||
|   haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, |   haier_protocol::HandlerError get_device_id_answer_handler_(haier_protocol::FrameType request_type, | ||||||
|  |                                                              haier_protocol::FrameType message_type, | ||||||
|                                                              const uint8_t *data, size_t data_size); |                                                              const uint8_t *data, size_t data_size); | ||||||
|   haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type, |   haier_protocol::HandlerError messages_timeout_handler_with_cycle_for_init_(haier_protocol::FrameType message_type); | ||||||
|                                                                      const uint8_t *data, size_t data_size); |  | ||||||
|   haier_protocol::HandlerError initial_messages_timeout_handler_(uint8_t message_type); |  | ||||||
|   // Helper functions |   // Helper functions | ||||||
|   haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); |   haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); | ||||||
|   std::unique_ptr<uint8_t[]> last_status_message_; |   bool use_alternative_swing_control_; | ||||||
|   unsigned int timeouts_counter_; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace haier | }  // namespace haier | ||||||
|   | |||||||
| @@ -41,8 +41,9 @@ struct HaierPacketControl { | |||||||
|   // 24 |   // 24 | ||||||
|   uint8_t : 8; |   uint8_t : 8; | ||||||
|   // 25 |   // 25 | ||||||
|   uint8_t swing_both;  // If 1 - swing both direction, if 0 - horizontal_swing and vertical_swing define |   uint8_t swing_mode;  // In normal mode: If 1 - swing both direction, if 0 - horizontal_swing and | ||||||
|                        // vertical/horizontal/off |                        // vertical_swing define vertical/horizontal/off | ||||||
|  |                        // In alternative mode: 0 - off, 01 - vertical,  02 - horizontal, 03 - both | ||||||
|   // 26 |   // 26 | ||||||
|   uint8_t : 3; |   uint8_t : 3; | ||||||
|   uint8_t use_fahrenheit : 1; |   uint8_t use_fahrenheit : 1; | ||||||
| @@ -82,19 +83,6 @@ struct HaierStatus { | |||||||
|   HaierPacketControl control; |   HaierPacketControl control; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum class FrameType : uint8_t { |  | ||||||
|   CONTROL = 0x01, |  | ||||||
|   STATUS = 0x02, |  | ||||||
|   INVALID = 0x03, |  | ||||||
|   CONFIRM = 0x05, |  | ||||||
|   GET_DEVICE_VERSION = 0x61, |  | ||||||
|   GET_DEVICE_VERSION_RESPONSE = 0x62, |  | ||||||
|   GET_DEVICE_ID = 0x70, |  | ||||||
|   GET_DEVICE_ID_RESPONSE = 0x71, |  | ||||||
|   REPORT_NETWORK_STATUS = 0xF7, |  | ||||||
|   NO_COMMAND = 0xFF, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| }  // namespace smartair2_protocol | }  // namespace smartair2_protocol | ||||||
| }  // namespace haier | }  // namespace haier | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import esphome.config_validation as cv | |||||||
| from esphome import automation | from esphome import automation | ||||||
| from esphome.automation import maybe_simple_id | from esphome.automation import maybe_simple_id | ||||||
| from esphome.components import fan, output | from esphome.components import fan, output | ||||||
|  | from esphome.components.fan import validate_preset_modes | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_DECAY_MODE, |     CONF_DECAY_MODE, | ||||||
| @@ -10,6 +11,7 @@ from esphome.const import ( | |||||||
|     CONF_PIN_A, |     CONF_PIN_A, | ||||||
|     CONF_PIN_B, |     CONF_PIN_B, | ||||||
|     CONF_ENABLE_PIN, |     CONF_ENABLE_PIN, | ||||||
|  |     CONF_PRESET_MODES, | ||||||
| ) | ) | ||||||
| from .. import hbridge_ns | from .. import hbridge_ns | ||||||
|  |  | ||||||
| @@ -28,7 +30,6 @@ DECAY_MODE_OPTIONS = { | |||||||
| # Actions | # Actions | ||||||
| BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action) | BrakeAction = hbridge_ns.class_("BrakeAction", automation.Action) | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( | CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( | ||||||
|     { |     { | ||||||
|         cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan), |         cv.GenerateID(CONF_ID): cv.declare_id(HBridgeFan), | ||||||
| @@ -39,6 +40,7 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( | |||||||
|         ), |         ), | ||||||
|         cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), |         cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1), | ||||||
|         cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), |         cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput), | ||||||
|  |         cv.Optional(CONF_PRESET_MODES): validate_preset_modes, | ||||||
|     } |     } | ||||||
| ).extend(cv.COMPONENT_SCHEMA) | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
| @@ -69,3 +71,6 @@ async def to_code(config): | |||||||
|     if CONF_ENABLE_PIN in config: |     if CONF_ENABLE_PIN in config: | ||||||
|         enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN]) |         enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN]) | ||||||
|         cg.add(var.set_enable_pin(enable_pin)) |         cg.add(var.set_enable_pin(enable_pin)) | ||||||
|  |  | ||||||
|  |     if CONF_PRESET_MODES in config: | ||||||
|  |         cg.add(var.set_preset_modes(config[CONF_PRESET_MODES])) | ||||||
|   | |||||||
| @@ -33,7 +33,12 @@ void HBridgeFan::setup() { | |||||||
|     restore->apply(*this); |     restore->apply(*this); | ||||||
|     this->write_state_(); |     this->write_state_(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // Construct traits | ||||||
|  |   this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); | ||||||
|  |   this->traits_.set_supported_preset_modes(this->preset_modes_); | ||||||
| } | } | ||||||
|  |  | ||||||
| void HBridgeFan::dump_config() { | void HBridgeFan::dump_config() { | ||||||
|   LOG_FAN("", "H-Bridge Fan", this); |   LOG_FAN("", "H-Bridge Fan", this); | ||||||
|   if (this->decay_mode_ == DECAY_MODE_SLOW) { |   if (this->decay_mode_ == DECAY_MODE_SLOW) { | ||||||
| @@ -42,9 +47,7 @@ void HBridgeFan::dump_config() { | |||||||
|     ESP_LOGCONFIG(TAG, "  Decay Mode: Fast"); |     ESP_LOGCONFIG(TAG, "  Decay Mode: Fast"); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| fan::FanTraits HBridgeFan::get_traits() { |  | ||||||
|   return fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); |  | ||||||
| } |  | ||||||
| void HBridgeFan::control(const fan::FanCall &call) { | void HBridgeFan::control(const fan::FanCall &call) { | ||||||
|   if (call.get_state().has_value()) |   if (call.get_state().has_value()) | ||||||
|     this->state = *call.get_state(); |     this->state = *call.get_state(); | ||||||
| @@ -54,10 +57,12 @@ void HBridgeFan::control(const fan::FanCall &call) { | |||||||
|     this->oscillating = *call.get_oscillating(); |     this->oscillating = *call.get_oscillating(); | ||||||
|   if (call.get_direction().has_value()) |   if (call.get_direction().has_value()) | ||||||
|     this->direction = *call.get_direction(); |     this->direction = *call.get_direction(); | ||||||
|  |   this->preset_mode = call.get_preset_mode(); | ||||||
|  |  | ||||||
|   this->write_state_(); |   this->write_state_(); | ||||||
|   this->publish_state(); |   this->publish_state(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void HBridgeFan::write_state_() { | void HBridgeFan::write_state_() { | ||||||
|   float speed = this->state ? static_cast<float>(this->speed) / static_cast<float>(this->speed_count_) : 0.0f; |   float speed = this->state ? static_cast<float>(this->speed) / static_cast<float>(this->speed_count_) : 0.0f; | ||||||
|   if (speed == 0.0f) {  // off means idle |   if (speed == 0.0f) {  // off means idle | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
|  | #include <set> | ||||||
|  |  | ||||||
| #include "esphome/core/automation.h" | #include "esphome/core/automation.h" | ||||||
| #include "esphome/components/output/binary_output.h" | #include "esphome/components/output/binary_output.h" | ||||||
| #include "esphome/components/output/float_output.h" | #include "esphome/components/output/float_output.h" | ||||||
| @@ -20,10 +22,11 @@ class HBridgeFan : public Component, public fan::Fan { | |||||||
|   void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } |   void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } | ||||||
|   void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } |   void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } | ||||||
|   void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } |   void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } | ||||||
|  |   void set_preset_modes(const std::set<std::string> &presets) { preset_modes_ = presets; } | ||||||
|  |  | ||||||
|   void setup() override; |   void setup() override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   fan::FanTraits get_traits() override; |   fan::FanTraits get_traits() override { return this->traits_; } | ||||||
|  |  | ||||||
|   fan::FanCall brake(); |   fan::FanCall brake(); | ||||||
|  |  | ||||||
| @@ -34,6 +37,8 @@ class HBridgeFan : public Component, public fan::Fan { | |||||||
|   output::BinaryOutput *oscillating_{nullptr}; |   output::BinaryOutput *oscillating_{nullptr}; | ||||||
|   int speed_count_{}; |   int speed_count_{}; | ||||||
|   DecayMode decay_mode_{DECAY_MODE_SLOW}; |   DecayMode decay_mode_{DECAY_MODE_SLOW}; | ||||||
|  |   fan::FanTraits traits_; | ||||||
|  |   std::set<std::string> preset_modes_{}; | ||||||
|  |  | ||||||
|   void control(const fan::FanCall &call) override; |   void control(const fan::FanCall &call) override; | ||||||
|   void write_state_(); |   void write_state_(); | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user