mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 00:51:49 +00:00 
			
		
		
		
	Compare commits
	
		
			22 Commits
		
	
	
		
			2023.12.0b
			...
			jesserockz
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					970680b1b2 | ||
| 
						 | 
					f500bd5e6f | ||
| 
						 | 
					e2bb81e233 | ||
| 
						 | 
					26a1d14ee0 | ||
| 
						 | 
					97f07f8d13 | ||
| 
						 | 
					ff9bffc363 | ||
| 
						 | 
					89b3af8be4 | ||
| 
						 | 
					c9b2e54c1a | ||
| 
						 | 
					6dd92053b5 | ||
| 
						 | 
					33346c0b6a | ||
| 
						 | 
					161fbecfe1 | ||
| 
						 | 
					fce2eafda0 | ||
| 
						 | 
					c19f0cf6bc | ||
| 
						 | 
					b05e7bfe0a | ||
| 
						 | 
					3e58ee2130 | ||
| 
						 | 
					bab9c7c70e | ||
| 
						 | 
					0b60a1d9eb | ||
| 
						 | 
					f7455ad76a | ||
| 
						 | 
					3190e86ba8 | ||
| 
						 | 
					a34569d314 | ||
| 
						 | 
					6c1c200cf9 | ||
| 
						 | 
					3635179564 | 
@@ -37,7 +37,6 @@
 | 
			
		||||
          "!secret scalar",
 | 
			
		||||
          "!lambda scalar",
 | 
			
		||||
          "!extend scalar",
 | 
			
		||||
          "!remove scalar",
 | 
			
		||||
          "!include_dir_named scalar",
 | 
			
		||||
          "!include_dir_list scalar",
 | 
			
		||||
          "!include_dir_merge_list scalar",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										97
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										97
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
								
							@@ -1,97 +0,0 @@
 | 
			
		||||
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:
 | 
			
		||||
    - name: Set up Python ${{ inputs.python-version }}
 | 
			
		||||
      id: python
 | 
			
		||||
      uses: actions/setup-python@v5.0.0
 | 
			
		||||
      uses: actions/setup-python@v4.7.0
 | 
			
		||||
      with:
 | 
			
		||||
        python-version: ${{ inputs.python-version }}
 | 
			
		||||
    - 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:
 | 
			
		||||
      - uses: actions/checkout@v4.1.1
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.0.0
 | 
			
		||||
        uses: actions/setup-python@v4.7.1
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.9"
 | 
			
		||||
      - 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
 | 
			
		||||
      - name: Set up Python ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
        id: python
 | 
			
		||||
        uses: actions/setup-python@v5.0.0
 | 
			
		||||
        uses: actions/setup-python@v4.7.1
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							@@ -18,7 +18,7 @@ jobs:
 | 
			
		||||
  lock:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: dessant/lock-threads@v5.0.1
 | 
			
		||||
      - uses: dessant/lock-threads@v4.0.1
 | 
			
		||||
        with:
 | 
			
		||||
          pr-inactive-days: "1"
 | 
			
		||||
          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
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check for needs-docs label
 | 
			
		||||
        uses: actions/github-script@v7.0.1
 | 
			
		||||
        uses: actions/github-script@v6.4.1
 | 
			
		||||
        with:
 | 
			
		||||
          script: |
 | 
			
		||||
            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:
 | 
			
		||||
      - uses: actions/checkout@v4.1.1
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.0.0
 | 
			
		||||
        uses: actions/setup-python@v4.7.1
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.x"
 | 
			
		||||
      - name: Set up python environment
 | 
			
		||||
@@ -63,31 +63,40 @@ jobs:
 | 
			
		||||
        run: twine upload dist/*
 | 
			
		||||
 | 
			
		||||
  deploy-docker:
 | 
			
		||||
    name: Build ESPHome ${{ matrix.platform }}
 | 
			
		||||
    name: Build and publish ESPHome ${{ matrix.image.title}}
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    permissions:
 | 
			
		||||
      contents: read
 | 
			
		||||
      packages: write
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    continue-on-error: ${{ matrix.image.title == 'lint' }}
 | 
			
		||||
    needs: [init]
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        platform:
 | 
			
		||||
          - linux/amd64
 | 
			
		||||
          - linux/arm/v7
 | 
			
		||||
          - linux/arm64
 | 
			
		||||
        image:
 | 
			
		||||
          - title: "ha-addon"
 | 
			
		||||
            suffix: "hassio"
 | 
			
		||||
            target: "hassio"
 | 
			
		||||
            baseimg: "hassio"
 | 
			
		||||
          - title: "docker"
 | 
			
		||||
            suffix: ""
 | 
			
		||||
            target: "docker"
 | 
			
		||||
            baseimg: "docker"
 | 
			
		||||
          - title: "lint"
 | 
			
		||||
            suffix: "lint"
 | 
			
		||||
            target: "lint"
 | 
			
		||||
            baseimg: "docker"
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4.1.1
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.0.0
 | 
			
		||||
        uses: actions/setup-python@v4.7.1
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.9"
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.0.0
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        if: matrix.platform != 'linux/amd64'
 | 
			
		||||
        uses: docker/setup-qemu-action@v3.0.0
 | 
			
		||||
 | 
			
		||||
      - name: Log in to docker hub
 | 
			
		||||
@@ -102,108 +111,37 @@ jobs:
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
          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
 | 
			
		||||
        id: tags
 | 
			
		||||
        run: |
 | 
			
		||||
          output=$(docker/generate_tags.py \
 | 
			
		||||
          docker/generate_tags.py \
 | 
			
		||||
            --tag "${{ needs.init.outputs.tag }}" \
 | 
			
		||||
            --suffix "${{ matrix.image.suffix }}" \
 | 
			
		||||
            --registry "${{ matrix.registry }}")
 | 
			
		||||
          echo $output
 | 
			
		||||
          for l in $output; do
 | 
			
		||||
            echo $l >> $GITHUB_OUTPUT
 | 
			
		||||
          done
 | 
			
		||||
            --suffix "${{ matrix.image.suffix }}"
 | 
			
		||||
 | 
			
		||||
      - name: Create manifest list and push
 | 
			
		||||
        working-directory: /tmp/digests
 | 
			
		||||
        run: |
 | 
			
		||||
          docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \
 | 
			
		||||
            $(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *)
 | 
			
		||||
      - name: Build and push
 | 
			
		||||
        uses: docker/build-push-action@v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          file: ./docker/Dockerfile
 | 
			
		||||
          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:
 | 
			
		||||
    if: github.repository == 'esphome/esphome' && github.event_name == 'release'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [deploy-manifest]
 | 
			
		||||
    needs: [deploy-docker]
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Trigger Workflow
 | 
			
		||||
        uses: actions/github-script@v7.0.1
 | 
			
		||||
        uses: actions/github-script@v6.4.1
 | 
			
		||||
        with:
 | 
			
		||||
          github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }}
 | 
			
		||||
          script: |
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							@@ -18,7 +18,7 @@ jobs:
 | 
			
		||||
  stale:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/stale@v9.0.0
 | 
			
		||||
      - uses: actions/stale@v8.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          days-before-pr-stale: 90
 | 
			
		||||
          days-before-pr-close: 7
 | 
			
		||||
@@ -38,7 +38,7 @@ jobs:
 | 
			
		||||
  close-issues:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/stale@v9.0.0
 | 
			
		||||
      - uses: actions/stale@v8.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          days-before-pr-stale: -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
 | 
			
		||||
 | 
			
		||||
      - name: Setup Python
 | 
			
		||||
        uses: actions/setup-python@v5.0.0
 | 
			
		||||
        uses: actions/setup-python@v4.7.1
 | 
			
		||||
        with:
 | 
			
		||||
          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
 | 
			
		||||
        uses: actions/checkout@v4.1.1
 | 
			
		||||
      - name: Run yamllint
 | 
			
		||||
        uses: frenck/action-yamllint@v1.4.2
 | 
			
		||||
        uses: frenck/action-yamllint@v1.4.1
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
# See https://pre-commit.com/hooks.html for more hooks
 | 
			
		||||
repos:
 | 
			
		||||
  - repo: https://github.com/psf/black-pre-commit-mirror
 | 
			
		||||
    rev: 23.12.0
 | 
			
		||||
    rev: 23.11.0
 | 
			
		||||
    hooks:
 | 
			
		||||
      - id: black
 | 
			
		||||
        args:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								CODEOWNERS
									
									
									
									
									
								
							@@ -12,7 +12,6 @@ esphome/core/* @esphome/core
 | 
			
		||||
 | 
			
		||||
# Integrations
 | 
			
		||||
esphome/components/a01nyub/* @MrSuicideParrot
 | 
			
		||||
esphome/components/a02yyuw/* @TH-Braemer
 | 
			
		||||
esphome/components/absolute_humidity/* @DAVe3283
 | 
			
		||||
esphome/components/ac_dimmer/* @glmnet
 | 
			
		||||
esphome/components/adc/* @esphome/core
 | 
			
		||||
@@ -89,9 +88,8 @@ esphome/components/ds1307/* @badbadc0ffee
 | 
			
		||||
esphome/components/dsmr/* @glmnet @zuidwijk
 | 
			
		||||
esphome/components/duty_time/* @dudanov
 | 
			
		||||
esphome/components/ee895/* @Stock-M
 | 
			
		||||
esphome/components/ektf2232/touchscreen/* @jesserockz
 | 
			
		||||
esphome/components/ektf2232/* @jesserockz
 | 
			
		||||
esphome/components/emc2101/* @ellull
 | 
			
		||||
esphome/components/ens160/* @vincentscode
 | 
			
		||||
esphome/components/ens210/* @itn3rd77
 | 
			
		||||
esphome/components/esp32/* @esphome/core
 | 
			
		||||
esphome/components/esp32_ble/* @Rapsssito @jesserockz
 | 
			
		||||
@@ -102,6 +100,7 @@ esphome/components/esp32_can/* @Sympatron
 | 
			
		||||
esphome/components/esp32_improv/* @jesserockz
 | 
			
		||||
esphome/components/esp32_rmt_led_strip/* @jesserockz
 | 
			
		||||
esphome/components/esp8266/* @esphome/core
 | 
			
		||||
esphome/components/esp_adf/* @jesserockz
 | 
			
		||||
esphome/components/ethernet_info/* @gtjadsonsantos
 | 
			
		||||
esphome/components/exposure_notifications/* @OttoWinter
 | 
			
		||||
esphome/components/ezo/* @ssieb
 | 
			
		||||
@@ -111,24 +110,19 @@ esphome/components/fastled_base/* @OttoWinter
 | 
			
		||||
esphome/components/feedback/* @ianchi
 | 
			
		||||
esphome/components/fingerprint_grow/* @OnFreund @loongyh
 | 
			
		||||
esphome/components/fs3000/* @kahrendt
 | 
			
		||||
esphome/components/ft5x06/* @clydebarrow
 | 
			
		||||
esphome/components/ft63x6/* @gpambrozio
 | 
			
		||||
esphome/components/gcja5/* @gcormier
 | 
			
		||||
esphome/components/globals/* @esphome/core
 | 
			
		||||
esphome/components/gp8403/* @jesserockz
 | 
			
		||||
esphome/components/gpio/* @esphome/core
 | 
			
		||||
esphome/components/gps/* @coogle
 | 
			
		||||
esphome/components/graph/* @synco
 | 
			
		||||
esphome/components/graphical_display_menu/* @MrMDavidson
 | 
			
		||||
esphome/components/gree/* @orestismers
 | 
			
		||||
esphome/components/grove_tb6612fng/* @max246
 | 
			
		||||
esphome/components/growatt_solar/* @leeuwte
 | 
			
		||||
esphome/components/gt911/* @clydebarrow @jesserockz
 | 
			
		||||
esphome/components/haier/* @paveldn
 | 
			
		||||
esphome/components/havells_solar/* @sourabhjaiswal
 | 
			
		||||
esphome/components/hbridge/fan/* @WeekendWarrior
 | 
			
		||||
esphome/components/hbridge/light/* @DotNetDann
 | 
			
		||||
esphome/components/he60r/* @clydebarrow
 | 
			
		||||
esphome/components/heatpumpir/* @rob-deutsch
 | 
			
		||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
 | 
			
		||||
esphome/components/hm3301/* @freekode
 | 
			
		||||
@@ -240,17 +234,11 @@ esphome/components/pmwcs3/* @SeByDocKy
 | 
			
		||||
esphome/components/pn532/* @OttoWinter @jesserockz
 | 
			
		||||
esphome/components/pn532_i2c/* @OttoWinter @jesserockz
 | 
			
		||||
esphome/components/pn532_spi/* @OttoWinter @jesserockz
 | 
			
		||||
esphome/components/pn7150/* @jesserockz @kbx81
 | 
			
		||||
esphome/components/pn7150_i2c/* @jesserockz @kbx81
 | 
			
		||||
esphome/components/pn7160/* @jesserockz @kbx81
 | 
			
		||||
esphome/components/pn7160_i2c/* @jesserockz @kbx81
 | 
			
		||||
esphome/components/pn7160_spi/* @jesserockz @kbx81
 | 
			
		||||
esphome/components/power_supply/* @esphome/core
 | 
			
		||||
esphome/components/preferences/* @esphome/core
 | 
			
		||||
esphome/components/psram/* @esphome/core
 | 
			
		||||
esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter
 | 
			
		||||
esphome/components/pvvx_mithermometer/* @pasiz
 | 
			
		||||
esphome/components/pylontech/* @functionpointer
 | 
			
		||||
esphome/components/qmp6988/* @andrewpc
 | 
			
		||||
esphome/components/qr_code/* @wjtje
 | 
			
		||||
esphome/components/qwiic_pir/* @kahrendt
 | 
			
		||||
@@ -339,7 +327,7 @@ esphome/components/tmp1075/* @sybrenstuvel
 | 
			
		||||
esphome/components/tmp117/* @Azimath
 | 
			
		||||
esphome/components/tof10120/* @wstrzalka
 | 
			
		||||
esphome/components/toshiba/* @kbx81
 | 
			
		||||
esphome/components/touchscreen/* @jesserockz @nielsnl68
 | 
			
		||||
esphome/components/touchscreen/* @jesserockz
 | 
			
		||||
esphome/components/tsl2591/* @wjcarpenter
 | 
			
		||||
esphome/components/tt21100/* @kroimon
 | 
			
		||||
esphome/components/tuya/binary_sensor/* @jesserockz
 | 
			
		||||
@@ -372,6 +360,6 @@ esphome/components/xiaomi_mhoc303/* @drug123
 | 
			
		||||
esphome/components/xiaomi_mhoc401/* @vevsvevs
 | 
			
		||||
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
 | 
			
		||||
esphome/components/xl9535/* @mreditor97
 | 
			
		||||
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
 | 
			
		||||
esphome/components/xpt2046/* @nielsnl68 @numo68
 | 
			
		||||
esphome/components/zhlt01/* @cfeenstra1024
 | 
			
		||||
esphome/components/zio_ultrasonic/* @kahrendt
 | 
			
		||||
 
 | 
			
		||||
@@ -10,3 +10,5 @@ Things to note when contributing:
 | 
			
		||||
   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
 | 
			
		||||
   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.
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,6 @@ RUN \
 | 
			
		||||
        openssh-client=1:9.2p1-2+deb12u1 \
 | 
			
		||||
        python3-cffi=1.15.1-5 \
 | 
			
		||||
        libcairo2=1.16.0-7 \
 | 
			
		||||
        libmagic1=1:5.44-3 \
 | 
			
		||||
        patch=2.7.6-7; \
 | 
			
		||||
    if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
 | 
			
		||||
        apt-get install -y --no-install-recommends \
 | 
			
		||||
@@ -49,8 +48,6 @@ RUN \
 | 
			
		||||
          libfreetype-dev=2.12.1+dfsg-5 \
 | 
			
		||||
          libssl-dev=3.0.11-1~deb12u2 \
 | 
			
		||||
          libffi-dev=3.4.4-1 \
 | 
			
		||||
          libopenjp2-7=2.5.0-2 \
 | 
			
		||||
          libtiff6=4.5.0-6 \
 | 
			
		||||
          cargo=0.66.0+ds1-1 \
 | 
			
		||||
          pkg-config=1.8.1-1 \
 | 
			
		||||
          gcc-arm-linux-gnueabihf=4:12.2.0-3; \
 | 
			
		||||
@@ -71,7 +68,7 @@ ENV \
 | 
			
		||||
# See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian
 | 
			
		||||
RUN \
 | 
			
		||||
    if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
 | 
			
		||||
        ln -s /lib/arm-linux-gnueabihf/ld-linux-armhf.so.3 /lib/ld-linux.so.3; \
 | 
			
		||||
        ln -s /lib/arm-linux-gnueabihf/ld-linux.so.3 /lib/ld-linux.so.3; \
 | 
			
		||||
    fi
 | 
			
		||||
 | 
			
		||||
RUN \
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,13 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
import re
 | 
			
		||||
import os
 | 
			
		||||
import argparse
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
CHANNEL_DEV = "dev"
 | 
			
		||||
CHANNEL_BETA = "beta"
 | 
			
		||||
CHANNEL_RELEASE = "release"
 | 
			
		||||
 | 
			
		||||
GHCR = "ghcr"
 | 
			
		||||
DOCKERHUB = "dockerhub"
 | 
			
		||||
 | 
			
		||||
parser = argparse.ArgumentParser()
 | 
			
		||||
parser.add_argument(
 | 
			
		||||
    "--tag",
 | 
			
		||||
@@ -22,31 +21,21 @@ parser.add_argument(
 | 
			
		||||
    required=True,
 | 
			
		||||
    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():
 | 
			
		||||
    args = parser.parse_args()
 | 
			
		||||
 | 
			
		||||
    # detect channel from tag
 | 
			
		||||
    match = re.match(r"^(\d+\.\d+)(?:\.\d+)(?:(b\d+)|(-dev\d+))?$", args.tag)
 | 
			
		||||
    match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag)
 | 
			
		||||
    major_minor_version = 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
 | 
			
		||||
    if match is None:
 | 
			
		||||
        channel = CHANNEL_DEV
 | 
			
		||||
    elif match.group(2) is not None:  # eg 2023.12.0b1
 | 
			
		||||
        channel = CHANNEL_BETA
 | 
			
		||||
    else:  # eg 2023.12.0
 | 
			
		||||
    elif match.group(2) is None:
 | 
			
		||||
        major_minor_version = match.group(1)
 | 
			
		||||
        channel = CHANNEL_RELEASE
 | 
			
		||||
    else:
 | 
			
		||||
        channel = CHANNEL_BETA
 | 
			
		||||
 | 
			
		||||
    tags_to_push = [args.tag]
 | 
			
		||||
    if channel == CHANNEL_DEV:
 | 
			
		||||
@@ -64,28 +53,15 @@ def main():
 | 
			
		||||
 | 
			
		||||
    suffix = f"-{args.suffix}" if args.suffix else ""
 | 
			
		||||
 | 
			
		||||
    image_name = f"esphome/esphome{suffix}"
 | 
			
		||||
    with open(os.environ["GITHUB_OUTPUT"], "w") as f:
 | 
			
		||||
        print(f"channel={channel}", file=f)
 | 
			
		||||
        print(f"image=esphome/esphome{suffix}", file=f)
 | 
			
		||||
        full_tags = []
 | 
			
		||||
 | 
			
		||||
    print(f"channel={channel}")
 | 
			
		||||
 | 
			
		||||
    if args.registry is None:
 | 
			
		||||
        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)}")
 | 
			
		||||
        for tag in tags_to_push:
 | 
			
		||||
            full_tags += [f"ghcr.io/esphome/esphome{suffix}:{tag}"]
 | 
			
		||||
            full_tags += [f"esphome/esphome{suffix}:{tag}"]
 | 
			
		||||
        print(f"tags={','.join(full_tags)}", file=f)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
 
 | 
			
		||||
@@ -389,8 +389,7 @@ def command_config(args, config):
 | 
			
		||||
        output = re.sub(
 | 
			
		||||
            r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[5m\2\\033[6m", output
 | 
			
		||||
        )
 | 
			
		||||
    if not CORE.quiet:
 | 
			
		||||
        safe_print(output)
 | 
			
		||||
    safe_print(output)
 | 
			
		||||
    _LOGGER.info("Configuration is valid!")
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,37 +8,50 @@ namespace esphome {
 | 
			
		||||
namespace a01nyub {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "a01nyub.sensor";
 | 
			
		||||
static const uint8_t MAX_DATA_LENGTH_BYTES = 4;
 | 
			
		||||
 | 
			
		||||
void A01nyubComponent::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)
 | 
			
		||||
    if (this->read_byte(&data)) {
 | 
			
		||||
      buffer_.push_back(data);
 | 
			
		||||
      this->check_buffer_();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void A01nyubComponent::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 > 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());
 | 
			
		||||
  if (this->buffer_.size() >= MAX_DATA_LENGTH_BYTES) {
 | 
			
		||||
    size_t i;
 | 
			
		||||
    for (i = 0; i < this->buffer_.size(); i++) {
 | 
			
		||||
      // Look for the first packet
 | 
			
		||||
      if (this->buffer_[i] == 0xFF) {
 | 
			
		||||
        if (i + 1 + 3 < this->buffer_.size()) {  // Packet is not complete
 | 
			
		||||
          return;                                // Wait for completion
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]);
 | 
			
		||||
    this->buffer_.clear();
 | 
			
		||||
  }
 | 
			
		||||
  this->buffer_.clear();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void A01nyubComponent::dump_config() { LOG_SENSOR("", "A01nyub Sensor", this); }
 | 
			
		||||
void A01nyubComponent::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "A01nyub Sensor:");
 | 
			
		||||
  LOG_SENSOR("  ", "Distance", this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace a01nyub
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
CODEOWNERS = ["@TH-Braemer"]
 | 
			
		||||
@@ -1,43 +0,0 @@
 | 
			
		||||
// 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
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,41 +0,0 @@
 | 
			
		||||
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.config_validation as cv
 | 
			
		||||
from esphome import pins
 | 
			
		||||
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER
 | 
			
		||||
from esphome.const import CONF_ANALOG, CONF_INPUT
 | 
			
		||||
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.components.esp32 import get_esp32_variant
 | 
			
		||||
@@ -152,8 +152,7 @@ def validate_adc_pin(value):
 | 
			
		||||
        return cv.only_on_rp2040("TEMPERATURE")
 | 
			
		||||
 | 
			
		||||
    if CORE.is_esp32:
 | 
			
		||||
        conf = pins.internal_gpio_input_pin_schema(value)
 | 
			
		||||
        value = conf[CONF_NUMBER]
 | 
			
		||||
        value = pins.internal_gpio_input_pin_number(value)
 | 
			
		||||
        variant = get_esp32_variant()
 | 
			
		||||
        if (
 | 
			
		||||
            variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL
 | 
			
		||||
@@ -167,23 +166,24 @@ def validate_adc_pin(value):
 | 
			
		||||
        ):
 | 
			
		||||
            raise cv.Invalid(f"{variant} doesn't support ADC on this pin")
 | 
			
		||||
 | 
			
		||||
        return conf
 | 
			
		||||
        return pins.internal_gpio_input_pin_schema(value)
 | 
			
		||||
 | 
			
		||||
    if CORE.is_esp8266:
 | 
			
		||||
        conf = pins.gpio_pin_schema(
 | 
			
		||||
        value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})(
 | 
			
		||||
            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
 | 
			
		||||
        )(value)
 | 
			
		||||
 | 
			
		||||
        if conf[CONF_NUMBER] != 17:  # A0
 | 
			
		||||
            raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC")
 | 
			
		||||
        return conf
 | 
			
		||||
 | 
			
		||||
    if CORE.is_rp2040:
 | 
			
		||||
        conf = pins.internal_gpio_input_pin_schema(value)
 | 
			
		||||
        number = conf[CONF_NUMBER]
 | 
			
		||||
        if number not in (26, 27, 28, 29):
 | 
			
		||||
        value = pins.internal_gpio_input_pin_number(value)
 | 
			
		||||
        if value not in (26, 27, 28, 29):
 | 
			
		||||
            raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC")
 | 
			
		||||
        return conf
 | 
			
		||||
        return pins.internal_gpio_input_pin_schema(value)
 | 
			
		||||
 | 
			
		||||
    if CORE.is_libretiny:
 | 
			
		||||
        return pins.gpio_pin_schema(
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace addressable_light {
 | 
			
		||||
 | 
			
		||||
class AddressableLightDisplay : public display::DisplayBuffer {
 | 
			
		||||
class AddressableLightDisplay : public display::DisplayBuffer, public PollingComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  light::AddressableLight *get_light() const { return this->light_; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -45,6 +45,7 @@ async def to_code(config):
 | 
			
		||||
    cg.add(var.set_height(config[CONF_HEIGHT]))
 | 
			
		||||
    cg.add(var.set_light(wrapped_light))
 | 
			
		||||
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await display.register_display(var, config)
 | 
			
		||||
 | 
			
		||||
    if pixel_mapper := config.get(CONF_PIXEL_MAPPER):
 | 
			
		||||
 
 | 
			
		||||
@@ -21,49 +21,36 @@ namespace esphome {
 | 
			
		||||
namespace aht10 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "aht10";
 | 
			
		||||
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_CALIBRATE_CMD[] = {0xE1};
 | 
			
		||||
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_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_CAL_ATTEMPTS = 10;
 | 
			
		||||
static const uint8_t AHT10_STATUS_BUSY = 0x80;
 | 
			
		||||
 | 
			
		||||
void AHT10Component::setup() {
 | 
			
		||||
  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");
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up AHT10...");
 | 
			
		||||
 | 
			
		||||
  if (this->write(calibrate_cmd, SIZE_CALIBRATE_CMD) != i2c::ERROR_OK) {
 | 
			
		||||
  if (!this->write_bytes(0, AHT10_CALIBRATE_CMD, sizeof(AHT10_CALIBRATE_CMD))) {
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with AHT10 failed!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  uint8_t data = AHT10_STATUS_BUSY;
 | 
			
		||||
  int cal_attempts = 0;
 | 
			
		||||
  while (data & AHT10_STATUS_BUSY) {
 | 
			
		||||
    delay(AHT10_DEFAULT_DELAY);
 | 
			
		||||
    if (this->read(&data, 1) != i2c::ERROR_OK) {
 | 
			
		||||
      ESP_LOGE(TAG, "Communication with AHT10 failed!");
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    ++cal_attempts;
 | 
			
		||||
    if (cal_attempts > AHT10_CAL_ATTEMPTS) {
 | 
			
		||||
      ESP_LOGE(TAG, "AHT10 calibration timed out!");
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  uint8_t data = 0;
 | 
			
		||||
  if (this->write(&data, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGD(TAG, "Communication with AHT10 failed!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  delay(AHT10_DEFAULT_DELAY);
 | 
			
		||||
  if (this->read(&data, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGD(TAG, "Communication with AHT10 failed!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->read(&data, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGD(TAG, "Communication with AHT10 failed!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if ((data & 0x68) != 0x08) {  // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED
 | 
			
		||||
    ESP_LOGE(TAG, "AHT10 calibration failed!");
 | 
			
		||||
@@ -75,7 +62,7 @@ void AHT10Component::setup() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AHT10Component::update() {
 | 
			
		||||
  if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
 | 
			
		||||
  if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) {
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with AHT10 failed!");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
@@ -102,7 +89,7 @@ void AHT10Component::update() {
 | 
			
		||||
        break;
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying...");
 | 
			
		||||
        if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
 | 
			
		||||
        if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) {
 | 
			
		||||
          ESP_LOGE(TAG, "Communication with AHT10 failed!");
 | 
			
		||||
          this->status_set_warning();
 | 
			
		||||
          return;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,5 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
@@ -9,15 +7,12 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace aht10 {
 | 
			
		||||
 | 
			
		||||
enum AHT10Variant { AHT10, AHT20 };
 | 
			
		||||
 | 
			
		||||
class AHT10Component : public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void dump_config() 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_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
 | 
			
		||||
@@ -25,7 +20,6 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
 protected:
 | 
			
		||||
  sensor::Sensor *temperature_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *humidity_sensor_{nullptr};
 | 
			
		||||
  AHT10Variant variant_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace aht10
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,6 @@ from esphome.const import (
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
    CONF_VARIANT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
@@ -18,12 +17,6 @@ DEPENDENCIES = ["i2c"]
 | 
			
		||||
aht10_ns = cg.esphome_ns.namespace("aht10")
 | 
			
		||||
AHT10Component = aht10_ns.class_("AHT10Component", cg.PollingComponent, i2c.I2CDevice)
 | 
			
		||||
 | 
			
		||||
AHT10Variant = aht10_ns.enum("AHT10Variant")
 | 
			
		||||
AHT10_VARIANTS = {
 | 
			
		||||
    "AHT10": AHT10Variant.AHT10,
 | 
			
		||||
    "AHT20": AHT10Variant.AHT20,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
@@ -40,9 +33,6 @@ CONFIG_SCHEMA = (
 | 
			
		||||
                device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_VARIANT, default="AHT10"): cv.enum(
 | 
			
		||||
                AHT10_VARIANTS, upper=True
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
@@ -54,7 +44,6 @@ 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)
 | 
			
		||||
    cg.add(var.set_variant(config[CONF_VARIANT]))
 | 
			
		||||
 | 
			
		||||
    if temperature := config.get(CONF_TEMPERATURE):
 | 
			
		||||
        sens = await sensor.new_sensor(temperature)
 | 
			
		||||
 
 | 
			
		||||
@@ -365,7 +365,6 @@ message ListEntitiesFanResponse {
 | 
			
		||||
  bool disabled_by_default = 9;
 | 
			
		||||
  string icon = 10;
 | 
			
		||||
  EntityCategory entity_category = 11;
 | 
			
		||||
  repeated string supported_preset_modes = 12;
 | 
			
		||||
}
 | 
			
		||||
enum FanSpeed {
 | 
			
		||||
  FAN_SPEED_LOW = 0;
 | 
			
		||||
@@ -388,7 +387,6 @@ message FanStateResponse {
 | 
			
		||||
  FanSpeed speed = 4 [deprecated = true];
 | 
			
		||||
  FanDirection direction = 5;
 | 
			
		||||
  int32 speed_level = 6;
 | 
			
		||||
  string preset_mode = 7;
 | 
			
		||||
}
 | 
			
		||||
message FanCommandRequest {
 | 
			
		||||
  option (id) = 31;
 | 
			
		||||
@@ -407,8 +405,6 @@ message FanCommandRequest {
 | 
			
		||||
  FanDirection direction = 9;
 | 
			
		||||
  bool has_speed_level = 10;
 | 
			
		||||
  int32 speed_level = 11;
 | 
			
		||||
  bool has_preset_mode = 12;
 | 
			
		||||
  string preset_mode = 13;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== LIGHT ====================
 | 
			
		||||
@@ -859,10 +855,6 @@ message ListEntitiesClimateResponse {
 | 
			
		||||
  string icon = 19;
 | 
			
		||||
  EntityCategory entity_category = 20;
 | 
			
		||||
  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 {
 | 
			
		||||
  option (id) = 47;
 | 
			
		||||
@@ -883,8 +875,6 @@ message ClimateStateResponse {
 | 
			
		||||
  string custom_fan_mode = 11;
 | 
			
		||||
  ClimatePreset preset = 12;
 | 
			
		||||
  string custom_preset = 13;
 | 
			
		||||
  float current_humidity = 14;
 | 
			
		||||
  float target_humidity = 15;
 | 
			
		||||
}
 | 
			
		||||
message ClimateCommandRequest {
 | 
			
		||||
  option (id) = 48;
 | 
			
		||||
@@ -913,8 +903,6 @@ message ClimateCommandRequest {
 | 
			
		||||
  ClimatePreset preset = 19;
 | 
			
		||||
  bool has_custom_preset = 20;
 | 
			
		||||
  string custom_preset = 21;
 | 
			
		||||
  bool has_target_humidity = 22;
 | 
			
		||||
  float target_humidity = 23;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== NUMBER ====================
 | 
			
		||||
 
 | 
			
		||||
@@ -293,8 +293,6 @@ bool APIConnection::send_fan_state(fan::Fan *fan) {
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.supports_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);
 | 
			
		||||
}
 | 
			
		||||
bool APIConnection::send_fan_info(fan::Fan *fan) {
 | 
			
		||||
@@ -309,8 +307,6 @@ bool APIConnection::send_fan_info(fan::Fan *fan) {
 | 
			
		||||
  msg.supports_speed = traits.supports_speed();
 | 
			
		||||
  msg.supports_direction = traits.supports_direction();
 | 
			
		||||
  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.icon = fan->get_icon();
 | 
			
		||||
  msg.entity_category = static_cast<enums::EntityCategory>(fan->get_entity_category());
 | 
			
		||||
@@ -332,8 +328,6 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
 | 
			
		||||
  }
 | 
			
		||||
  if (msg.has_direction)
 | 
			
		||||
    call.set_direction(static_cast<fan::FanDirection>(msg.direction));
 | 
			
		||||
  if (msg.has_preset_mode)
 | 
			
		||||
    call.set_preset_mode(msg.preset_mode);
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -560,10 +554,6 @@ bool APIConnection::send_climate_state(climate::Climate *climate) {
 | 
			
		||||
    resp.custom_preset = climate->custom_preset.value();
 | 
			
		||||
  if (traits.get_supports_swing_modes())
 | 
			
		||||
    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);
 | 
			
		||||
}
 | 
			
		||||
bool APIConnection::send_climate_info(climate::Climate *climate) {
 | 
			
		||||
@@ -580,9 +570,7 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
 | 
			
		||||
  msg.entity_category = static_cast<enums::EntityCategory>(climate->get_entity_category());
 | 
			
		||||
 | 
			
		||||
  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_target_humidity = traits.get_supports_target_humidity();
 | 
			
		||||
 | 
			
		||||
  for (auto mode : traits.get_supported_modes())
 | 
			
		||||
    msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode));
 | 
			
		||||
@@ -591,8 +579,6 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
 | 
			
		||||
  msg.visual_max_temperature = traits.get_visual_max_temperature();
 | 
			
		||||
  msg.visual_target_temperature_step = traits.get_visual_target_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.supports_action = traits.get_supports_action();
 | 
			
		||||
@@ -623,8 +609,6 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
 | 
			
		||||
    call.set_target_temperature_low(msg.target_temperature_low);
 | 
			
		||||
  if (msg.has_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)
 | 
			
		||||
    call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
 | 
			
		||||
  if (msg.has_custom_fan_mode)
 | 
			
		||||
 
 | 
			
		||||
@@ -1375,10 +1375,6 @@ bool ListEntitiesFanResponse::decode_length(uint32_t field_id, ProtoLengthDelimi
 | 
			
		||||
      this->icon = value.as_string();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 12: {
 | 
			
		||||
      this->supported_preset_modes.push_back(value.as_string());
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -1405,9 +1401,6 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_bool(9, this->disabled_by_default);
 | 
			
		||||
  buffer.encode_string(10, this->icon);
 | 
			
		||||
  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
 | 
			
		||||
void ListEntitiesFanResponse::dump_to(std::string &out) const {
 | 
			
		||||
@@ -1458,12 +1451,6 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const {
 | 
			
		||||
  out.append("  entity_category: ");
 | 
			
		||||
  out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
 | 
			
		||||
  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("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1493,16 +1480,6 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
      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) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 1: {
 | 
			
		||||
@@ -1520,7 +1497,6 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_enum<enums::FanSpeed>(4, this->speed);
 | 
			
		||||
  buffer.encode_enum<enums::FanDirection>(5, this->direction);
 | 
			
		||||
  buffer.encode_int32(6, this->speed_level);
 | 
			
		||||
  buffer.encode_string(7, this->preset_mode);
 | 
			
		||||
}
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
void FanStateResponse::dump_to(std::string &out) const {
 | 
			
		||||
@@ -1551,10 +1527,6 @@ void FanStateResponse::dump_to(std::string &out) const {
 | 
			
		||||
  sprintf(buffer, "%" PRId32, this->speed_level);
 | 
			
		||||
  out.append(buffer);
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  out.append("  preset_mode: ");
 | 
			
		||||
  out.append("'").append(this->preset_mode).append("'");
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
  out.append("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1600,20 +1572,6 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
      this->speed_level = value.as_int32();
 | 
			
		||||
      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:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -1640,8 +1598,6 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_enum<enums::FanDirection>(9, this->direction);
 | 
			
		||||
  buffer.encode_bool(10, this->has_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
 | 
			
		||||
void FanCommandRequest::dump_to(std::string &out) const {
 | 
			
		||||
@@ -1692,14 +1648,6 @@ void FanCommandRequest::dump_to(std::string &out) const {
 | 
			
		||||
  sprintf(buffer, "%" PRId32, this->speed_level);
 | 
			
		||||
  out.append(buffer);
 | 
			
		||||
  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("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -3611,14 +3559,6 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v
 | 
			
		||||
      this->entity_category = value.as_enum<enums::EntityCategory>();
 | 
			
		||||
      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:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -3675,14 +3615,6 @@ bool ListEntitiesClimateResponse::decode_32bit(uint32_t field_id, Proto32Bit val
 | 
			
		||||
      this->visual_current_temperature_step = value.as_float();
 | 
			
		||||
      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:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -3721,10 +3653,6 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_string(19, this->icon);
 | 
			
		||||
  buffer.encode_enum<enums::EntityCategory>(20, this->entity_category);
 | 
			
		||||
  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
 | 
			
		||||
void ListEntitiesClimateResponse::dump_to(std::string &out) const {
 | 
			
		||||
@@ -3830,24 +3758,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
 | 
			
		||||
  sprintf(buffer, "%g", this->visual_current_temperature_step);
 | 
			
		||||
  out.append(buffer);
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  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");
 | 
			
		||||
  out.append("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
@@ -3916,14 +3827,6 @@ bool ClimateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
 | 
			
		||||
      this->target_temperature_high = value.as_float();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 14: {
 | 
			
		||||
      this->current_humidity = value.as_float();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 15: {
 | 
			
		||||
      this->target_humidity = value.as_float();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -3942,8 +3845,6 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_string(11, this->custom_fan_mode);
 | 
			
		||||
  buffer.encode_enum<enums::ClimatePreset>(12, this->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
 | 
			
		||||
void ClimateStateResponse::dump_to(std::string &out) const {
 | 
			
		||||
@@ -4005,16 +3906,7 @@ void ClimateStateResponse::dump_to(std::string &out) const {
 | 
			
		||||
  out.append("  custom_preset: ");
 | 
			
		||||
  out.append("'").append(this->custom_preset).append("'");
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
 | 
			
		||||
  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");
 | 
			
		||||
  out.append("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
@@ -4079,10 +3971,6 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
 | 
			
		||||
      this->has_custom_preset = value.as_bool();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 22: {
 | 
			
		||||
      this->has_target_humidity = value.as_bool();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -4119,10 +4007,6 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
 | 
			
		||||
      this->target_temperature_high = value.as_float();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    case 23: {
 | 
			
		||||
      this->target_humidity = value.as_float();
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -4149,8 +4033,6 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_enum<enums::ClimatePreset>(19, this->preset);
 | 
			
		||||
  buffer.encode_bool(20, this->has_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
 | 
			
		||||
void ClimateCommandRequest::dump_to(std::string &out) const {
 | 
			
		||||
@@ -4243,15 +4125,6 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
 | 
			
		||||
  out.append("  custom_preset: ");
 | 
			
		||||
  out.append("'").append(this->custom_preset).append("'");
 | 
			
		||||
  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("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -472,7 +472,6 @@ class ListEntitiesFanResponse : public ProtoMessage {
 | 
			
		||||
  bool disabled_by_default{false};
 | 
			
		||||
  std::string icon{};
 | 
			
		||||
  enums::EntityCategory entity_category{};
 | 
			
		||||
  std::vector<std::string> supported_preset_modes{};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
@@ -491,7 +490,6 @@ class FanStateResponse : public ProtoMessage {
 | 
			
		||||
  enums::FanSpeed speed{};
 | 
			
		||||
  enums::FanDirection direction{};
 | 
			
		||||
  int32_t speed_level{0};
 | 
			
		||||
  std::string preset_mode{};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
@@ -499,7 +497,6 @@ class FanStateResponse : public ProtoMessage {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  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;
 | 
			
		||||
};
 | 
			
		||||
class FanCommandRequest : public ProtoMessage {
 | 
			
		||||
@@ -515,8 +512,6 @@ class FanCommandRequest : public ProtoMessage {
 | 
			
		||||
  enums::FanDirection direction{};
 | 
			
		||||
  bool has_speed_level{false};
 | 
			
		||||
  int32_t speed_level{0};
 | 
			
		||||
  bool has_preset_mode{false};
 | 
			
		||||
  std::string preset_mode{};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
@@ -524,7 +519,6 @@ class FanCommandRequest : public ProtoMessage {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  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;
 | 
			
		||||
};
 | 
			
		||||
class ListEntitiesLightResponse : public ProtoMessage {
 | 
			
		||||
@@ -985,10 +979,6 @@ class ListEntitiesClimateResponse : public ProtoMessage {
 | 
			
		||||
  std::string icon{};
 | 
			
		||||
  enums::EntityCategory entity_category{};
 | 
			
		||||
  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;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
@@ -1014,8 +1004,6 @@ class ClimateStateResponse : public ProtoMessage {
 | 
			
		||||
  std::string custom_fan_mode{};
 | 
			
		||||
  enums::ClimatePreset preset{};
 | 
			
		||||
  std::string custom_preset{};
 | 
			
		||||
  float current_humidity{0.0f};
 | 
			
		||||
  float target_humidity{0.0f};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
@@ -1049,8 +1037,6 @@ class ClimateCommandRequest : public ProtoMessage {
 | 
			
		||||
  enums::ClimatePreset preset{};
 | 
			
		||||
  bool has_custom_preset{false};
 | 
			
		||||
  std::string custom_preset{};
 | 
			
		||||
  bool has_target_humidity{false};
 | 
			
		||||
  float target_humidity{0.0f};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ from typing import Any
 | 
			
		||||
from aioesphomeapi import APIClient
 | 
			
		||||
from aioesphomeapi.api_pb2 import SubscribeLogsResponse
 | 
			
		||||
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.core import CORE
 | 
			
		||||
@@ -17,22 +18,24 @@ from . import CONF_ENCRYPTION
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def async_run_logs(config: dict[str, Any], address: str) -> None:
 | 
			
		||||
async def async_run_logs(config, address):
 | 
			
		||||
    """Run the logs command in the event loop."""
 | 
			
		||||
    conf = config["api"]
 | 
			
		||||
    name = config["esphome"]["name"]
 | 
			
		||||
    port: int = int(conf[CONF_PORT])
 | 
			
		||||
    password: str = conf[CONF_PASSWORD]
 | 
			
		||||
    noise_psk: str | None = None
 | 
			
		||||
    if CONF_ENCRYPTION in conf:
 | 
			
		||||
        noise_psk = conf[CONF_ENCRYPTION][CONF_KEY]
 | 
			
		||||
    _LOGGER.info("Starting log output from %s using esphome API", address)
 | 
			
		||||
    aiozc = AsyncZeroconf()
 | 
			
		||||
 | 
			
		||||
    cli = APIClient(
 | 
			
		||||
        address,
 | 
			
		||||
        port,
 | 
			
		||||
        password,
 | 
			
		||||
        client_info=f"ESPHome Logs {__version__}",
 | 
			
		||||
        noise_psk=noise_psk,
 | 
			
		||||
        zeroconf_instance=aiozc.zeroconf,
 | 
			
		||||
    )
 | 
			
		||||
    dashboard = CORE.dashboard
 | 
			
		||||
 | 
			
		||||
@@ -45,10 +48,12 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
 | 
			
		||||
            text = text.replace("\033", "\\033")
 | 
			
		||||
        print(f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]{text}")
 | 
			
		||||
 | 
			
		||||
    stop = await async_run(cli, on_log, name=name)
 | 
			
		||||
    stop = await async_run(cli, on_log, aio_zeroconf_instance=aiozc)
 | 
			
		||||
    try:
 | 
			
		||||
        await asyncio.Event().wait()
 | 
			
		||||
        while True:
 | 
			
		||||
            await asyncio.sleep(60)
 | 
			
		||||
    finally:
 | 
			
		||||
        await aiozc.async_close()
 | 
			
		||||
        await stop()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,16 +15,6 @@ void BangBangClimate::setup() {
 | 
			
		||||
    this->publish_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
 | 
			
		||||
  auto restore = this->restore_state_();
 | 
			
		||||
  if (restore.has_value()) {
 | 
			
		||||
@@ -57,8 +47,6 @@ void BangBangClimate::control(const climate::ClimateCall &call) {
 | 
			
		||||
climate::ClimateTraits BangBangClimate::traits() {
 | 
			
		||||
  auto traits = climate::ClimateTraits();
 | 
			
		||||
  traits.set_supports_current_temperature(true);
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr)
 | 
			
		||||
    traits.set_supports_current_humidity(true);
 | 
			
		||||
  traits.set_supported_modes({
 | 
			
		||||
      climate::CLIMATE_MODE_OFF,
 | 
			
		||||
  });
 | 
			
		||||
@@ -183,7 +171,6 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa
 | 
			
		||||
BangBangClimate::BangBangClimate()
 | 
			
		||||
    : 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_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
 | 
			
		||||
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
 | 
			
		||||
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
 | 
			
		||||
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,6 @@ class BangBangClimate : public climate::Climate, public Component {
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
  void set_sensor(sensor::Sensor *sensor);
 | 
			
		||||
  void set_humidity_sensor(sensor::Sensor *humidity_sensor);
 | 
			
		||||
  Trigger<> *get_idle_trigger() const;
 | 
			
		||||
  Trigger<> *get_cool_trigger() const;
 | 
			
		||||
  void set_supports_cool(bool supports_cool);
 | 
			
		||||
@@ -49,9 +48,6 @@ class BangBangClimate : public climate::Climate, public Component {
 | 
			
		||||
 | 
			
		||||
  /// The sensor used for getting the current temperature
 | 
			
		||||
  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.
 | 
			
		||||
   *
 | 
			
		||||
   * In idle mode, the controller is assumed to have both heating and cooling disabled.
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_DEFAULT_TARGET_TEMPERATURE_HIGH,
 | 
			
		||||
    CONF_DEFAULT_TARGET_TEMPERATURE_LOW,
 | 
			
		||||
    CONF_HEAT_ACTION,
 | 
			
		||||
    CONF_HUMIDITY_SENSOR,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_IDLE_ACTION,
 | 
			
		||||
    CONF_SENSOR,
 | 
			
		||||
@@ -23,7 +22,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BangBangClimate),
 | 
			
		||||
            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_HIGH): cv.temperature,
 | 
			
		||||
            cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True),
 | 
			
		||||
@@ -49,10 +47,6 @@ async def to_code(config):
 | 
			
		||||
    sens = await cg.get_variable(config[CONF_SENSOR])
 | 
			
		||||
    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(
 | 
			
		||||
        config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW],
 | 
			
		||||
        config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH],
 | 
			
		||||
 
 | 
			
		||||
@@ -90,41 +90,40 @@ void BP1658CJ::set_channel_value_(uint8_t channel, uint16_t value) {
 | 
			
		||||
 | 
			
		||||
void BP1658CJ::write_bit_(bool value) {
 | 
			
		||||
  this->data_pin_->digital_write(value);
 | 
			
		||||
  delayMicroseconds(BP1658CJ_DELAY);
 | 
			
		||||
  this->clock_pin_->digital_write(true);
 | 
			
		||||
 | 
			
		||||
  delayMicroseconds(BP1658CJ_DELAY);
 | 
			
		||||
 | 
			
		||||
  this->clock_pin_->digital_write(false);
 | 
			
		||||
  delayMicroseconds(BP1658CJ_DELAY);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BP1658CJ::write_byte_(uint8_t data) {
 | 
			
		||||
  for (uint8_t mask = 0x80; mask; mask >>= 1) {
 | 
			
		||||
    this->write_bit_(data & mask);
 | 
			
		||||
    delayMicroseconds(BP1658CJ_DELAY);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // ack bit
 | 
			
		||||
  this->data_pin_->pin_mode(gpio::FLAG_INPUT);
 | 
			
		||||
  this->clock_pin_->digital_write(true);
 | 
			
		||||
 | 
			
		||||
  delayMicroseconds(BP1658CJ_DELAY);
 | 
			
		||||
 | 
			
		||||
  this->clock_pin_->digital_write(false);
 | 
			
		||||
  delayMicroseconds(BP1658CJ_DELAY);
 | 
			
		||||
  this->data_pin_->pin_mode(gpio::FLAG_OUTPUT);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BP1658CJ::write_buffer_(uint8_t *buffer, uint8_t size) {
 | 
			
		||||
  this->data_pin_->digital_write(false);
 | 
			
		||||
  delayMicroseconds(BP1658CJ_DELAY);
 | 
			
		||||
  this->clock_pin_->digital_write(false);
 | 
			
		||||
  delayMicroseconds(BP1658CJ_DELAY);
 | 
			
		||||
 | 
			
		||||
  for (uint32_t i = 0; i < size; i++) {
 | 
			
		||||
    this->write_byte_(buffer[i]);
 | 
			
		||||
    delayMicroseconds(BP1658CJ_DELAY);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->clock_pin_->digital_write(true);
 | 
			
		||||
  delayMicroseconds(BP1658CJ_DELAY);
 | 
			
		||||
  this->data_pin_->digital_write(true);
 | 
			
		||||
  delayMicroseconds(BP1658CJ_DELAY);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace bp1658cj
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_AWAY,
 | 
			
		||||
    CONF_AWAY_COMMAND_TOPIC,
 | 
			
		||||
    CONF_AWAY_STATE_TOPIC,
 | 
			
		||||
    CONF_CURRENT_HUMIDITY_STATE_TOPIC,
 | 
			
		||||
    CONF_CURRENT_TEMPERATURE_STATE_TOPIC,
 | 
			
		||||
    CONF_CUSTOM_FAN_MODE,
 | 
			
		||||
    CONF_CUSTOM_PRESET,
 | 
			
		||||
@@ -29,8 +28,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_SWING_MODE,
 | 
			
		||||
    CONF_SWING_MODE_COMMAND_TOPIC,
 | 
			
		||||
    CONF_SWING_MODE_STATE_TOPIC,
 | 
			
		||||
    CONF_TARGET_HUMIDITY_COMMAND_TOPIC,
 | 
			
		||||
    CONF_TARGET_HUMIDITY_STATE_TOPIC,
 | 
			
		||||
    CONF_TARGET_TEMPERATURE,
 | 
			
		||||
    CONF_TARGET_TEMPERATURE_COMMAND_TOPIC,
 | 
			
		||||
    CONF_TARGET_TEMPERATURE_STATE_TOPIC,
 | 
			
		||||
@@ -109,9 +106,6 @@ CLIMATE_SWING_MODES = {
 | 
			
		||||
validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True)
 | 
			
		||||
 | 
			
		||||
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", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?"
 | 
			
		||||
@@ -159,8 +153,6 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
 | 
			
		||||
                cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature,
 | 
			
		||||
                cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature,
 | 
			
		||||
                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(
 | 
			
		||||
@@ -175,9 +167,6 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
 | 
			
		||||
        cv.Optional(CONF_CURRENT_TEMPERATURE_STATE_TOPIC): cv.All(
 | 
			
		||||
            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.requires_component("mqtt"), cv.publish_topic
 | 
			
		||||
        ),
 | 
			
		||||
@@ -220,12 +209,6 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
 | 
			
		||||
        cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All(
 | 
			
		||||
            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.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger),
 | 
			
		||||
@@ -255,10 +238,6 @@ async def setup_climate_core_(var, config):
 | 
			
		||||
                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:
 | 
			
		||||
        mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
 | 
			
		||||
@@ -276,12 +255,6 @@ async def setup_climate_core_(var, config):
 | 
			
		||||
                    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:
 | 
			
		||||
            cg.add(
 | 
			
		||||
                mqtt_.set_custom_fan_mode_command_topic(
 | 
			
		||||
@@ -350,18 +323,6 @@ async def setup_climate_core_(var, config):
 | 
			
		||||
                    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, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
@@ -390,7 +351,6 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
 | 
			
		||||
        cv.Optional(CONF_TARGET_TEMPERATURE): 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_HUMIDITY): cv.templatable(cv.percentage_int),
 | 
			
		||||
        cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"),
 | 
			
		||||
        cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
 | 
			
		||||
            validate_climate_fan_mode
 | 
			
		||||
@@ -427,9 +387,6 @@ async def climate_control_to_code(config, action_id, template_arg, args):
 | 
			
		||||
            config[CONF_TARGET_TEMPERATURE_HIGH], args, float
 | 
			
		||||
        )
 | 
			
		||||
        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:
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode)
 | 
			
		||||
        cg.add(var.set_fan_mode(template_))
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
 | 
			
		||||
  TEMPLATABLE_VALUE(float, target_temperature)
 | 
			
		||||
  TEMPLATABLE_VALUE(float, target_temperature_low)
 | 
			
		||||
  TEMPLATABLE_VALUE(float, target_temperature_high)
 | 
			
		||||
  TEMPLATABLE_VALUE(float, target_humidity)
 | 
			
		||||
  TEMPLATABLE_VALUE(bool, away)
 | 
			
		||||
  TEMPLATABLE_VALUE(ClimateFanMode, fan_mode)
 | 
			
		||||
  TEMPLATABLE_VALUE(std::string, custom_fan_mode)
 | 
			
		||||
@@ -28,7 +27,6 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
 | 
			
		||||
    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_high(this->target_temperature_high_.optional_value(x...));
 | 
			
		||||
    call.set_target_humidity(this->target_humidity_.optional_value(x...));
 | 
			
		||||
    if (away_.has_value()) {
 | 
			
		||||
      call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -45,9 +45,6 @@ void ClimateCall::perform() {
 | 
			
		||||
  if (this->target_temperature_high_.has_value()) {
 | 
			
		||||
    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);
 | 
			
		||||
}
 | 
			
		||||
void ClimateCall::validate_() {
 | 
			
		||||
@@ -265,16 +262,10 @@ ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_h
 | 
			
		||||
  this->target_temperature_high_ = target_temperature_high;
 | 
			
		||||
  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<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_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<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
 | 
			
		||||
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
 | 
			
		||||
@@ -292,10 +283,6 @@ ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperat
 | 
			
		||||
  this->target_temperature_ = target_temperature;
 | 
			
		||||
  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) {
 | 
			
		||||
  this->mode_ = mode;
 | 
			
		||||
  return *this;
 | 
			
		||||
@@ -356,9 +343,6 @@ void Climate::save_state_() {
 | 
			
		||||
  } else {
 | 
			
		||||
    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()) {
 | 
			
		||||
    state.uses_custom_fan_mode = false;
 | 
			
		||||
    state.fan_mode = this->fan_mode.value();
 | 
			
		||||
@@ -424,12 +408,6 @@ void Climate::publish_state() {
 | 
			
		||||
  } else {
 | 
			
		||||
    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
 | 
			
		||||
  this->state_callback_.call(*this);
 | 
			
		||||
@@ -449,12 +427,6 @@ ClimateTraits Climate::get_traits() {
 | 
			
		||||
    traits.set_visual_target_temperature_step(*this->visual_target_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;
 | 
			
		||||
}
 | 
			
		||||
@@ -469,12 +441,6 @@ void Climate::set_visual_temperature_step_override(float target, float current)
 | 
			
		||||
  this->visual_target_temperature_step_override_ = target;
 | 
			
		||||
  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); }
 | 
			
		||||
 | 
			
		||||
@@ -488,9 +454,6 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
 | 
			
		||||
  } else {
 | 
			
		||||
    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()) {
 | 
			
		||||
    call.set_fan_mode(this->fan_mode);
 | 
			
		||||
  }
 | 
			
		||||
@@ -511,9 +474,6 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
 | 
			
		||||
  } else {
 | 
			
		||||
    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) {
 | 
			
		||||
    climate->fan_mode = this->fan_mode;
 | 
			
		||||
  }
 | 
			
		||||
@@ -570,25 +530,17 @@ void Climate::dump_traits_(const char *tag) {
 | 
			
		||||
  auto traits = this->get_traits();
 | 
			
		||||
  ESP_LOGCONFIG(tag, "ClimateTraits:");
 | 
			
		||||
  ESP_LOGCONFIG(tag, "  [x] Visual settings:");
 | 
			
		||||
  ESP_LOGCONFIG(tag, "      - Min temperature: %.1f", traits.get_visual_min_temperature());
 | 
			
		||||
  ESP_LOGCONFIG(tag, "      - Max temperature: %.1f", traits.get_visual_max_temperature());
 | 
			
		||||
  ESP_LOGCONFIG(tag, "      - Temperature step:");
 | 
			
		||||
  ESP_LOGCONFIG(tag, "      - Min: %.1f", traits.get_visual_min_temperature());
 | 
			
		||||
  ESP_LOGCONFIG(tag, "      - Max: %.1f", traits.get_visual_max_temperature());
 | 
			
		||||
  ESP_LOGCONFIG(tag, "      - 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, "      - 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()) {
 | 
			
		||||
    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()) {
 | 
			
		||||
    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()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports action");
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -64,10 +64,6 @@ class ClimateCall {
 | 
			
		||||
   * For climate devices with two point target temperature control
 | 
			
		||||
   */
 | 
			
		||||
  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.
 | 
			
		||||
  ClimateCall &set_fan_mode(ClimateFanMode fan_mode);
 | 
			
		||||
  /// Set the fan mode of the climate device.
 | 
			
		||||
@@ -97,7 +93,6 @@ class ClimateCall {
 | 
			
		||||
  const optional<float> &get_target_temperature() const;
 | 
			
		||||
  const optional<float> &get_target_temperature_low() 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<ClimateSwingMode> &get_swing_mode() const;
 | 
			
		||||
  const optional<std::string> &get_custom_fan_mode() const;
 | 
			
		||||
@@ -112,7 +107,6 @@ class ClimateCall {
 | 
			
		||||
  optional<float> target_temperature_;
 | 
			
		||||
  optional<float> target_temperature_low_;
 | 
			
		||||
  optional<float> target_temperature_high_;
 | 
			
		||||
  optional<float> target_humidity_;
 | 
			
		||||
  optional<ClimateFanMode> fan_mode_;
 | 
			
		||||
  optional<ClimateSwingMode> swing_mode_;
 | 
			
		||||
  optional<std::string> custom_fan_mode_;
 | 
			
		||||
@@ -142,7 +136,6 @@ struct ClimateDeviceRestoreState {
 | 
			
		||||
      float target_temperature_high;
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
  float target_humidity;
 | 
			
		||||
 | 
			
		||||
  /// Convert this struct to a climate call that can be performed.
 | 
			
		||||
  ClimateCall to_call(Climate *climate);
 | 
			
		||||
@@ -167,34 +160,24 @@ struct ClimateDeviceRestoreState {
 | 
			
		||||
 */
 | 
			
		||||
class Climate : public EntityBase {
 | 
			
		||||
 public:
 | 
			
		||||
  Climate() {}
 | 
			
		||||
 | 
			
		||||
  /// The active mode of the climate device.
 | 
			
		||||
  ClimateMode mode{CLIMATE_MODE_OFF};
 | 
			
		||||
 | 
			
		||||
  /// The active state of the climate device.
 | 
			
		||||
  ClimateAction action{CLIMATE_ACTION_OFF};
 | 
			
		||||
 | 
			
		||||
  /// The current temperature of the climate device, as reported from the integration.
 | 
			
		||||
  float current_temperature{NAN};
 | 
			
		||||
 | 
			
		||||
  /// The current humidity of the climate device, as reported from the integration.
 | 
			
		||||
  float current_humidity{NAN};
 | 
			
		||||
 | 
			
		||||
  union {
 | 
			
		||||
    /// The target temperature of the climate device.
 | 
			
		||||
    float target_temperature;
 | 
			
		||||
    struct {
 | 
			
		||||
      /// The minimum target temperature of the climate device, for climate devices with split target temperature.
 | 
			
		||||
      float target_temperature_low{NAN};
 | 
			
		||||
      float target_temperature_low;
 | 
			
		||||
      /// The maximum target temperature of the climate device, for climate devices with split target temperature.
 | 
			
		||||
      float target_temperature_high{NAN};
 | 
			
		||||
      float target_temperature_high;
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /// The target humidity of the climate device.
 | 
			
		||||
  float target_humidity;
 | 
			
		||||
 | 
			
		||||
  /// The active fan mode of the climate device.
 | 
			
		||||
  optional<ClimateFanMode> fan_mode;
 | 
			
		||||
 | 
			
		||||
@@ -248,8 +231,6 @@ class Climate : public EntityBase {
 | 
			
		||||
  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_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:
 | 
			
		||||
  friend ClimateCall;
 | 
			
		||||
@@ -299,8 +280,6 @@ class Climate : public EntityBase {
 | 
			
		||||
  optional<float> visual_max_temperature_override_{};
 | 
			
		||||
  optional<float> visual_target_temperature_step_override_{};
 | 
			
		||||
  optional<float> visual_current_temperature_step_override_{};
 | 
			
		||||
  optional<float> visual_min_humidity_override_{};
 | 
			
		||||
  optional<float> visual_max_humidity_override_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace climate
 | 
			
		||||
 
 | 
			
		||||
@@ -44,18 +44,10 @@ class ClimateTraits {
 | 
			
		||||
  void set_supports_current_temperature(bool 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_; }
 | 
			
		||||
  void set_supports_two_point_target_temperature(bool 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 add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
@@ -161,11 +153,6 @@ class ClimateTraits {
 | 
			
		||||
  int8_t get_target_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:
 | 
			
		||||
  void set_mode_support_(climate::ClimateMode mode, bool supported) {
 | 
			
		||||
    if (supported) {
 | 
			
		||||
@@ -190,9 +177,7 @@ class ClimateTraits {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool supports_current_temperature_{false};
 | 
			
		||||
  bool supports_current_humidity_{false};
 | 
			
		||||
  bool supports_two_point_target_temperature_{false};
 | 
			
		||||
  bool supports_target_humidity_{false};
 | 
			
		||||
  std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
 | 
			
		||||
  bool supports_action_{false};
 | 
			
		||||
  std::set<climate::ClimateFanMode> supported_fan_modes_;
 | 
			
		||||
@@ -205,8 +190,6 @@ class ClimateTraits {
 | 
			
		||||
  float visual_max_temperature_{30};
 | 
			
		||||
  float visual_target_temperature_step_{0.1};
 | 
			
		||||
  float visual_current_temperature_step_{0.1};
 | 
			
		||||
  float visual_min_humidity_{30};
 | 
			
		||||
  float visual_max_humidity_{99};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace climate
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,6 @@ void CopyFan::setup() {
 | 
			
		||||
    this->oscillating = source_->oscillating;
 | 
			
		||||
    this->speed = source_->speed;
 | 
			
		||||
    this->direction = source_->direction;
 | 
			
		||||
    this->preset_mode = source_->preset_mode;
 | 
			
		||||
    this->publish_state();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@@ -20,7 +19,6 @@ void CopyFan::setup() {
 | 
			
		||||
  this->oscillating = source_->oscillating;
 | 
			
		||||
  this->speed = source_->speed;
 | 
			
		||||
  this->direction = source_->direction;
 | 
			
		||||
  this->preset_mode = source_->preset_mode;
 | 
			
		||||
  this->publish_state();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -35,7 +33,6 @@ fan::FanTraits CopyFan::get_traits() {
 | 
			
		||||
  traits.set_speed(base.supports_speed());
 | 
			
		||||
  traits.set_supported_speed_count(base.supported_speed_count());
 | 
			
		||||
  traits.set_direction(base.supports_direction());
 | 
			
		||||
  traits.set_supported_preset_modes(base.supported_preset_modes());
 | 
			
		||||
  return traits;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -49,8 +46,6 @@ void CopyFan::control(const fan::FanCall &call) {
 | 
			
		||||
    call2.set_speed(*call.get_speed());
 | 
			
		||||
  if (call.get_direction().has_value())
 | 
			
		||||
    call2.set_direction(*call.get_direction());
 | 
			
		||||
  if (!call.get_preset_mode().empty())
 | 
			
		||||
    call2.set_preset_mode(call.get_preset_mode());
 | 
			
		||||
  call2.perform();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,7 @@ BASIC_DISPLAY_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Optional(CONF_LAMBDA): cv.lambda_,
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.polling_component_schema("1s"))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
@@ -116,7 +116,6 @@ async def setup_display_core_(var, config):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def register_display(var, config):
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await setup_display_core_(var, config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -166,13 +166,6 @@ void Display::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, in
 | 
			
		||||
}
 | 
			
		||||
#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,
 | 
			
		||||
                              int *width, int *height) {
 | 
			
		||||
  int x_offset, baseline;
 | 
			
		||||
 
 | 
			
		||||
@@ -17,10 +17,6 @@
 | 
			
		||||
#include "esphome/components/qr_code/qr_code.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_GRAPHICAL_DISPLAY_MENU
 | 
			
		||||
#include "esphome/components/graphical_display_menu/graphical_display_menu.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace display {
 | 
			
		||||
 | 
			
		||||
@@ -167,7 +163,7 @@ class BaseFont {
 | 
			
		||||
  virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class Display : public PollingComponent {
 | 
			
		||||
class Display {
 | 
			
		||||
 public:
 | 
			
		||||
  /// Fill the entire screen with the given color.
 | 
			
		||||
  virtual void fill(Color color);
 | 
			
		||||
@@ -396,17 +392,6 @@ class Display : public PollingComponent {
 | 
			
		||||
  void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1);
 | 
			
		||||
#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.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate to place the string at, can be 0 if only interested in dimensions.
 | 
			
		||||
 
 | 
			
		||||
@@ -172,8 +172,6 @@ void DisplayMenuComponent::show_main() {
 | 
			
		||||
 | 
			
		||||
  this->process_initial_();
 | 
			
		||||
 | 
			
		||||
  this->on_before_show();
 | 
			
		||||
 | 
			
		||||
  if (this->active_ && this->editing_)
 | 
			
		||||
    this->finish_editing_();
 | 
			
		||||
 | 
			
		||||
@@ -190,8 +188,6 @@ void DisplayMenuComponent::show_main() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->draw_and_update();
 | 
			
		||||
 | 
			
		||||
  this->on_after_show();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DisplayMenuComponent::show() {
 | 
			
		||||
@@ -200,26 +196,18 @@ void DisplayMenuComponent::show() {
 | 
			
		||||
 | 
			
		||||
  this->process_initial_();
 | 
			
		||||
 | 
			
		||||
  this->on_before_show();
 | 
			
		||||
 | 
			
		||||
  if (!this->active_) {
 | 
			
		||||
    this->active_ = true;
 | 
			
		||||
    this->draw_and_update();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->on_after_show();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DisplayMenuComponent::hide() {
 | 
			
		||||
  if (this->check_healthy_and_active_()) {
 | 
			
		||||
    this->on_before_hide();
 | 
			
		||||
 | 
			
		||||
    if (this->editing_)
 | 
			
		||||
      this->finish_editing_();
 | 
			
		||||
    this->active_ = false;
 | 
			
		||||
    this->update();
 | 
			
		||||
 | 
			
		||||
    this->on_after_hide();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -60,11 +60,6 @@ class DisplayMenuComponent : public Component {
 | 
			
		||||
    update();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  virtual void on_before_show(){};
 | 
			
		||||
  virtual void on_after_show(){};
 | 
			
		||||
  virtual void on_before_hide(){};
 | 
			
		||||
  virtual void on_after_hide(){};
 | 
			
		||||
 | 
			
		||||
  uint8_t rows_;
 | 
			
		||||
  bool active_;
 | 
			
		||||
  MenuMode mode_;
 | 
			
		||||
 
 | 
			
		||||
@@ -5,29 +5,6 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
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_leave() { this->on_leave_callbacks_.call(); }
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace display_menu_base {
 | 
			
		||||
@@ -30,9 +29,6 @@ enum MenuItemType {
 | 
			
		||||
  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 MenuItemMenu;
 | 
			
		||||
using value_getter_t = std::function<std::string(const MenuItem *)>;
 | 
			
		||||
 
 | 
			
		||||
@@ -15,12 +15,16 @@ 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_POWER_STATE_CMD[4] = {0x53, 0x50, 0x00, 0x01};
 | 
			
		||||
 | 
			
		||||
void EKTF2232TouchscreenStore::gpio_intr(EKTF2232TouchscreenStore *store) { store->touch = true; }
 | 
			
		||||
 | 
			
		||||
void EKTF2232Touchscreen::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up EKT2232 Touchscreen...");
 | 
			
		||||
  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);
 | 
			
		||||
  this->store_.pin = this->interrupt_pin_->to_isr();
 | 
			
		||||
  this->interrupt_pin_->attach_interrupt(EKTF2232TouchscreenStore::gpio_intr, &this->store_,
 | 
			
		||||
                                         gpio::INTERRUPT_FALLING_EDGE);
 | 
			
		||||
 | 
			
		||||
  this->rts_pin_->setup();
 | 
			
		||||
 | 
			
		||||
@@ -41,7 +45,7 @@ void EKTF2232Touchscreen::setup() {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->x_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
 | 
			
		||||
  this->x_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4);
 | 
			
		||||
 | 
			
		||||
  this->write(GET_Y_RES, 4);
 | 
			
		||||
  if (this->read(received, 4)) {
 | 
			
		||||
@@ -50,14 +54,19 @@ void EKTF2232Touchscreen::setup() {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->y_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
 | 
			
		||||
  this->y_resolution_ = ((received[2])) | ((received[3] & 0xf0) << 4);
 | 
			
		||||
  this->store_.touch = false;
 | 
			
		||||
 | 
			
		||||
  this->set_power_state(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EKTF2232Touchscreen::update_touches() {
 | 
			
		||||
void EKTF2232Touchscreen::loop() {
 | 
			
		||||
  if (!this->store_.touch)
 | 
			
		||||
    return;
 | 
			
		||||
  this->store_.touch = false;
 | 
			
		||||
 | 
			
		||||
  uint8_t touch_count = 0;
 | 
			
		||||
  int16_t x_raw, y_raw;
 | 
			
		||||
  std::vector<TouchPoint> touches;
 | 
			
		||||
 | 
			
		||||
  uint8_t raw[8];
 | 
			
		||||
  this->read(raw, 8);
 | 
			
		||||
@@ -66,15 +75,45 @@ void EKTF2232Touchscreen::update_touches() {
 | 
			
		||||
      touch_count++;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (touch_count == 0) {
 | 
			
		||||
    for (auto *listener : this->touch_listeners_)
 | 
			
		||||
      listener->release();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  touch_count = std::min<uint8_t>(touch_count, 2);
 | 
			
		||||
 | 
			
		||||
  ESP_LOGV(TAG, "Touch count: %d", touch_count);
 | 
			
		||||
 | 
			
		||||
  for (int i = 0; i < touch_count; i++) {
 | 
			
		||||
    uint8_t *d = raw + 1 + (i * 3);
 | 
			
		||||
    x_raw = (d[0] & 0xF0) << 4 | d[1];
 | 
			
		||||
    y_raw = (d[0] & 0x0F) << 8 | d[2];
 | 
			
		||||
    this->set_raw_touch_position_(i, x_raw, y_raw);
 | 
			
		||||
    uint32_t raw_x = (d[0] & 0xF0) << 4 | d[1];
 | 
			
		||||
    uint32_t raw_y = (d[0] & 0x0F) << 8 | d[2];
 | 
			
		||||
 | 
			
		||||
    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); });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -87,7 +126,7 @@ void EKTF2232Touchscreen::set_power_state(bool enable) {
 | 
			
		||||
bool EKTF2232Touchscreen::get_power_state() {
 | 
			
		||||
  uint8_t received[4];
 | 
			
		||||
  this->write(GET_POWER_STATE_CMD, 4);
 | 
			
		||||
  this->store_.touched = false;
 | 
			
		||||
  this->store_.touch = false;
 | 
			
		||||
  this->read(received, 4);
 | 
			
		||||
  return (received[1] >> 3) & 1;
 | 
			
		||||
}
 | 
			
		||||
@@ -106,14 +145,14 @@ bool EKTF2232Touchscreen::soft_reset_() {
 | 
			
		||||
 | 
			
		||||
  uint8_t received[4];
 | 
			
		||||
  uint16_t timeout = 1000;
 | 
			
		||||
  while (!this->store_.touched && timeout > 0) {
 | 
			
		||||
  while (!this->store_.touch && timeout > 0) {
 | 
			
		||||
    delay(1);
 | 
			
		||||
    timeout--;
 | 
			
		||||
  }
 | 
			
		||||
  if (timeout > 0)
 | 
			
		||||
    this->store_.touched = true;
 | 
			
		||||
    this->store_.touch = true;
 | 
			
		||||
  this->read(received, 4);
 | 
			
		||||
  this->store_.touched = false;
 | 
			
		||||
  this->store_.touch = false;
 | 
			
		||||
 | 
			
		||||
  return !memcmp(received, HELLO, 4);
 | 
			
		||||
}
 | 
			
		||||
@@ -9,11 +9,19 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ektf2232 {
 | 
			
		||||
 | 
			
		||||
struct EKTF2232TouchscreenStore {
 | 
			
		||||
  volatile bool touch;
 | 
			
		||||
  ISRInternalGPIOPin pin;
 | 
			
		||||
 | 
			
		||||
  static void gpio_intr(EKTF2232TouchscreenStore *store);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
using namespace touchscreen;
 | 
			
		||||
 | 
			
		||||
class EKTF2232Touchscreen : public Touchscreen, public i2c::I2CDevice {
 | 
			
		||||
class EKTF2232Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
  void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
 | 
			
		||||
@@ -25,10 +33,12 @@ class EKTF2232Touchscreen : public Touchscreen, public i2c::I2CDevice {
 | 
			
		||||
 protected:
 | 
			
		||||
  void hard_reset_();
 | 
			
		||||
  bool soft_reset_();
 | 
			
		||||
  void update_touches() override;
 | 
			
		||||
 | 
			
		||||
  InternalGPIOPin *interrupt_pin_;
 | 
			
		||||
  GPIOPin *rts_pin_;
 | 
			
		||||
  EKTF2232TouchscreenStore store_;
 | 
			
		||||
  uint16_t x_resolution_;
 | 
			
		||||
  uint16_t y_resolution_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ektf2232
 | 
			
		||||
@@ -12,6 +12,7 @@ ektf2232_ns = cg.esphome_ns.namespace("ektf2232")
 | 
			
		||||
EKTF2232Touchscreen = ektf2232_ns.class_(
 | 
			
		||||
    "EKTF2232Touchscreen",
 | 
			
		||||
    touchscreen.Touchscreen,
 | 
			
		||||
    cg.Component,
 | 
			
		||||
    i2c.I2CDevice,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -27,14 +28,17 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
 | 
			
		||||
            ),
 | 
			
		||||
            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):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await touchscreen.register_touchscreen(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
    await touchscreen.register_touchscreen(var, config)
 | 
			
		||||
 | 
			
		||||
    interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN])
 | 
			
		||||
    cg.add(var.set_interrupt_pin(interrupt_pin))
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
CODEOWNERS = ["@vincentscode"]
 | 
			
		||||
@@ -1,321 +0,0 @@
 | 
			
		||||
// 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
 | 
			
		||||
@@ -1,60 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,88 +0,0 @@
 | 
			
		||||
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))
 | 
			
		||||
@@ -3,26 +3,23 @@ from typing import Union, Optional
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
 | 
			
		||||
from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ADVANCED,
 | 
			
		||||
    CONF_BOARD,
 | 
			
		||||
    CONF_COMPONENTS,
 | 
			
		||||
    CONF_ESPHOME,
 | 
			
		||||
    CONF_FRAMEWORK,
 | 
			
		||||
    CONF_IGNORE_EFUSE_MAC_CRC,
 | 
			
		||||
    CONF_NAME,
 | 
			
		||||
    CONF_PATH,
 | 
			
		||||
    CONF_PLATFORMIO_OPTIONS,
 | 
			
		||||
    CONF_REF,
 | 
			
		||||
    CONF_REFRESH,
 | 
			
		||||
    CONF_SOURCE,
 | 
			
		||||
    CONF_TYPE,
 | 
			
		||||
    CONF_URL,
 | 
			
		||||
    CONF_VARIANT,
 | 
			
		||||
    CONF_VERSION,
 | 
			
		||||
    CONF_ADVANCED,
 | 
			
		||||
    CONF_REFRESH,
 | 
			
		||||
    CONF_PATH,
 | 
			
		||||
    CONF_URL,
 | 
			
		||||
    CONF_REF,
 | 
			
		||||
    CONF_IGNORE_EFUSE_MAC_CRC,
 | 
			
		||||
    KEY_CORE,
 | 
			
		||||
    KEY_FRAMEWORK_VERSION,
 | 
			
		||||
    KEY_NAME,
 | 
			
		||||
@@ -330,32 +327,6 @@ def _detect_variant(value):
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def final_validate(config):
 | 
			
		||||
    if CONF_PLATFORMIO_OPTIONS not in fv.full_config.get()[CONF_ESPHOME]:
 | 
			
		||||
        return config
 | 
			
		||||
 | 
			
		||||
    pio_flash_size_key = "board_upload.flash_size"
 | 
			
		||||
    pio_partitions_key = "board_build.partitions"
 | 
			
		||||
    if (
 | 
			
		||||
        CONF_PARTITIONS in config
 | 
			
		||||
        and pio_partitions_key
 | 
			
		||||
        in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS]
 | 
			
		||||
    ):
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        pio_flash_size_key
 | 
			
		||||
        in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS]
 | 
			
		||||
    ):
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONF_PLATFORM_VERSION = "platform_version"
 | 
			
		||||
 | 
			
		||||
ARDUINO_FRAMEWORK_SCHEMA = cv.All(
 | 
			
		||||
@@ -416,7 +387,6 @@ FRAMEWORK_SCHEMA = cv.typed_schema(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FLASH_SIZES = [
 | 
			
		||||
    "2MB",
 | 
			
		||||
    "4MB",
 | 
			
		||||
    "8MB",
 | 
			
		||||
    "16MB",
 | 
			
		||||
@@ -424,7 +394,6 @@ FLASH_SIZES = [
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
CONF_FLASH_SIZE = "flash_size"
 | 
			
		||||
CONF_PARTITIONS = "partitions"
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
@@ -432,7 +401,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of(
 | 
			
		||||
                *FLASH_SIZES, upper=True
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_PARTITIONS): cv.file_,
 | 
			
		||||
            cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
 | 
			
		||||
            cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA,
 | 
			
		||||
        }
 | 
			
		||||
@@ -442,9 +410,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add_platformio_option("board", config[CONF_BOARD])
 | 
			
		||||
    cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
 | 
			
		||||
@@ -462,7 +427,7 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    add_extra_script(
 | 
			
		||||
        "post",
 | 
			
		||||
        "post_build.py",
 | 
			
		||||
        "post_build2.py",
 | 
			
		||||
        os.path.join(os.path.dirname(__file__), "post_build.py.script"),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@@ -498,10 +463,6 @@ async def to_code(config):
 | 
			
		||||
        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:
 | 
			
		||||
            add_extra_build_file(
 | 
			
		||||
                "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
 | 
			
		||||
            add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
 | 
			
		||||
@@ -546,10 +507,7 @@ async def to_code(config):
 | 
			
		||||
            [f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if CONF_PARTITIONS in config:
 | 
			
		||||
            cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS])
 | 
			
		||||
        else:
 | 
			
		||||
            cg.add_platformio_option("board_build.partitions", "partitions.csv")
 | 
			
		||||
        cg.add_platformio_option("board_build.partitions", "partitions.csv")
 | 
			
		||||
 | 
			
		||||
        cg.add_define(
 | 
			
		||||
            "USE_ARDUINO_VERSION_CODE",
 | 
			
		||||
@@ -560,7 +518,6 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
APP_PARTITION_SIZES = {
 | 
			
		||||
    "2MB": 0x0C0000,  # 768 KB
 | 
			
		||||
    "4MB": 0x1C0000,  # 1792 KB
 | 
			
		||||
    "8MB": 0x3C0000,  # 3840 KB
 | 
			
		||||
    "16MB": 0x7C0000,  # 7936 KB
 | 
			
		||||
@@ -640,22 +597,20 @@ def _write_sdkconfig():
 | 
			
		||||
# Called by writer.py
 | 
			
		||||
def copy_files():
 | 
			
		||||
    if CORE.using_arduino:
 | 
			
		||||
        if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
 | 
			
		||||
            write_file_if_changed(
 | 
			
		||||
                CORE.relative_build_path("partitions.csv"),
 | 
			
		||||
                get_arduino_partition_csv(
 | 
			
		||||
                    CORE.platformio_options.get("board_upload.flash_size")
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
        write_file_if_changed(
 | 
			
		||||
            CORE.relative_build_path("partitions.csv"),
 | 
			
		||||
            get_arduino_partition_csv(
 | 
			
		||||
                CORE.platformio_options.get("board_upload.flash_size")
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
    if CORE.using_esp_idf:
 | 
			
		||||
        _write_sdkconfig()
 | 
			
		||||
        if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
 | 
			
		||||
            write_file_if_changed(
 | 
			
		||||
                CORE.relative_build_path("partitions.csv"),
 | 
			
		||||
                get_idf_partition_csv(
 | 
			
		||||
                    CORE.platformio_options.get("board_upload.flash_size")
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
        write_file_if_changed(
 | 
			
		||||
            CORE.relative_build_path("partitions.csv"),
 | 
			
		||||
            get_idf_partition_csv(
 | 
			
		||||
                CORE.platformio_options.get("board_upload.flash_size")
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        # IDF build scripts look for version string to put in the build.
 | 
			
		||||
        # 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.
 | 
			
		||||
 
 | 
			
		||||
@@ -3,13 +3,15 @@ from typing import Any
 | 
			
		||||
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_INPUT,
 | 
			
		||||
    CONF_INVERTED,
 | 
			
		||||
    CONF_MODE,
 | 
			
		||||
    CONF_NUMBER,
 | 
			
		||||
    CONF_OPEN_DRAIN,
 | 
			
		||||
    CONF_OUTPUT,
 | 
			
		||||
    CONF_PULLDOWN,
 | 
			
		||||
    CONF_PULLUP,
 | 
			
		||||
    CONF_IGNORE_STRAPPING_WARNING,
 | 
			
		||||
    PLATFORM_ESP32,
 | 
			
		||||
)
 | 
			
		||||
from esphome import pins
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
@@ -31,6 +33,7 @@ from .const import (
 | 
			
		||||
    esp32_ns,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports
 | 
			
		||||
@@ -39,6 +42,7 @@ 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_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ESP32InternalGPIOPin = esp32_ns.class_("ESP32InternalGPIOPin", cg.InternalGPIOPin)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -157,22 +161,33 @@ DRIVE_STRENGTHS = {
 | 
			
		||||
}
 | 
			
		||||
gpio_num_t = cg.global_ns.enum("gpio_num_t")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONF_DRIVE_STRENGTH = "drive_strength"
 | 
			
		||||
ESP32_PIN_SCHEMA = cv.All(
 | 
			
		||||
    pins.gpio_base_schema(ESP32InternalGPIOPin, validate_gpio_pin).extend(
 | 
			
		||||
        {
 | 
			
		||||
            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),
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(ESP32InternalGPIOPin),
 | 
			
		||||
        cv.Required(CONF_NUMBER): validate_gpio_pin,
 | 
			
		||||
        cv.Optional(CONF_MODE, default={}): cv.Schema(
 | 
			
		||||
            {
 | 
			
		||||
                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,
 | 
			
		||||
        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,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP32, ESP32_PIN_SCHEMA)
 | 
			
		||||
@pins.PIN_SCHEMA_REGISTRY.register("esp32", ESP32_PIN_SCHEMA)
 | 
			
		||||
async def esp32_pin_to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    num = config[CONF_NUMBER]
 | 
			
		||||
 
 | 
			
		||||
@@ -25,11 +25,6 @@ AUTO_LOAD = ["psram"]
 | 
			
		||||
 | 
			
		||||
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
 | 
			
		||||
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",
 | 
			
		||||
    automation.Trigger.template(),
 | 
			
		||||
@@ -144,7 +139,6 @@ CONF_IDLE_FRAMERATE = "idle_framerate"
 | 
			
		||||
# stream trigger
 | 
			
		||||
CONF_ON_STREAM_START = "on_stream_start"
 | 
			
		||||
CONF_ON_STREAM_STOP = "on_stream_stop"
 | 
			
		||||
CONF_ON_IMAGE = "on_image"
 | 
			
		||||
 | 
			
		||||
camera_range_param = cv.int_range(min=-2, max=2)
 | 
			
		||||
 | 
			
		||||
@@ -227,11 +221,6 @@ 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)
 | 
			
		||||
 | 
			
		||||
@@ -300,9 +289,3 @@ async def to_code(config):
 | 
			
		||||
    for conf in config.get(CONF_ON_STREAM_STOP, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        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) ---------------- */
 | 
			
		||||
void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&callback) {
 | 
			
		||||
  this->new_image_callback_.add(std::move(callback));
 | 
			
		||||
void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&f) {
 | 
			
		||||
  this->new_image_callback_.add(std::move(f));
 | 
			
		||||
}
 | 
			
		||||
void ESP32Camera::add_stream_start_callback(std::function<void()> &&callback) {
 | 
			
		||||
  this->stream_start_callback_.add(std::move(callback));
 | 
			
		||||
 
 | 
			
		||||
@@ -86,11 +86,6 @@ class CameraImage {
 | 
			
		||||
  uint8_t requesters_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct CameraImageData {
 | 
			
		||||
  uint8_t *data;
 | 
			
		||||
  size_t length;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/* ---------------- CameraImageReader class ---------------- */
 | 
			
		||||
class CameraImageReader {
 | 
			
		||||
 public:
 | 
			
		||||
@@ -152,12 +147,12 @@ class ESP32Camera : public Component, public EntityBase {
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  /* public API (specific) */
 | 
			
		||||
  void add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&f);
 | 
			
		||||
  void start_stream(CameraRequester requester);
 | 
			
		||||
  void stop_stream(CameraRequester requester);
 | 
			
		||||
  void request_image(CameraRequester requester);
 | 
			
		||||
  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_stop_callback(std::function<void()> &&callback);
 | 
			
		||||
 | 
			
		||||
@@ -201,7 +196,7 @@ class ESP32Camera : public Component, public EntityBase {
 | 
			
		||||
  uint8_t stream_requesters_{0};
 | 
			
		||||
  QueueHandle_t framebuffer_get_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_stop_callback_{};
 | 
			
		||||
 | 
			
		||||
@@ -212,18 +207,6 @@ class ESP32Camera : public Component, public EntityBase {
 | 
			
		||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
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<> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_OUTPUT,
 | 
			
		||||
    CONF_PULLDOWN,
 | 
			
		||||
    CONF_PULLUP,
 | 
			
		||||
    PLATFORM_ESP8266,
 | 
			
		||||
)
 | 
			
		||||
from esphome import pins
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
@@ -22,8 +21,10 @@ import esphome.codegen as cg
 | 
			
		||||
from . import boards
 | 
			
		||||
from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ESP8266GPIOPin = esp8266_ns.class_("ESP8266GPIOPin", cg.InternalGPIOPin)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -123,8 +124,6 @@ def validate_supports(value):
 | 
			
		||||
        (True, False, False, False, False),
 | 
			
		||||
        # OUTPUT
 | 
			
		||||
        (False, True, False, False, False),
 | 
			
		||||
        # INPUT and OUTPUT, e.g. for i2c
 | 
			
		||||
        (True, True, False, False, False),
 | 
			
		||||
        # INPUT_PULLUP
 | 
			
		||||
        (True, False, False, True, False),
 | 
			
		||||
        # INPUT_PULLDOWN_16
 | 
			
		||||
@@ -143,11 +142,21 @@ def validate_supports(value):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ESP8266_PIN_SCHEMA = cv.All(
 | 
			
		||||
    pins.gpio_base_schema(
 | 
			
		||||
        ESP8266GPIOPin,
 | 
			
		||||
        validate_gpio_pin,
 | 
			
		||||
        modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,),
 | 
			
		||||
    ),
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(ESP8266GPIOPin),
 | 
			
		||||
        cv.Required(CONF_NUMBER): validate_gpio_pin,
 | 
			
		||||
        cv.Optional(CONF_MODE, default={}): cv.Schema(
 | 
			
		||||
            {
 | 
			
		||||
                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,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -158,7 +167,7 @@ class PinInitialState:
 | 
			
		||||
    level: int = 255
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP8266, ESP8266_PIN_SCHEMA)
 | 
			
		||||
@pins.PIN_SCHEMA_REGISTRY.register("esp8266", ESP8266_PIN_SCHEMA)
 | 
			
		||||
async def esp8266_pin_to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    num = config[CONF_NUMBER]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										102
									
								
								esphome/components/esp_adf/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								esphome/components/esp_adf/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
 | 
			
		||||
from esphome.components import esp32
 | 
			
		||||
 | 
			
		||||
from esphome.const import CONF_ID, CONF_BOARD
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@jesserockz"]
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
 | 
			
		||||
CONF_ESP_ADF_ID = "esp_adf_id"
 | 
			
		||||
CONF_ESP_ADF = "esp_adf"
 | 
			
		||||
 | 
			
		||||
esp_adf_ns = cg.esphome_ns.namespace("esp_adf")
 | 
			
		||||
ESPADF = esp_adf_ns.class_("ESPADF", cg.Component)
 | 
			
		||||
ESPADFPipeline = esp_adf_ns.class_("ESPADFPipeline", cg.Parented.template(ESPADF))
 | 
			
		||||
 | 
			
		||||
SUPPORTED_BOARDS = {
 | 
			
		||||
    "esp32s3box": "CONFIG_ESP32_S3_BOX_BOARD",
 | 
			
		||||
    "esp32s3boxlite": "CONFIG_ESP32_S3_BOX_LITE_BOARD",
 | 
			
		||||
    "esp32s3box3": "CONFIG_ESP32_S3_BOX_3_BOARD",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _default_board(config):
 | 
			
		||||
    config = config.copy()
 | 
			
		||||
    if board := config.get(CONF_BOARD) is None:
 | 
			
		||||
        board = esp32.get_board()
 | 
			
		||||
        if board in SUPPORTED_BOARDS:
 | 
			
		||||
            config[CONF_BOARD] = board
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def final_validate_usable_board(platform: str):
 | 
			
		||||
    def _validate(adf_config):
 | 
			
		||||
        board = adf_config.get(CONF_BOARD)
 | 
			
		||||
        if board not in SUPPORTED_BOARDS:
 | 
			
		||||
            raise cv.Invalid(f"Board {board} is not supported by esp-adf {platform}")
 | 
			
		||||
        return adf_config
 | 
			
		||||
 | 
			
		||||
    return cv.Schema(
 | 
			
		||||
        {cv.Required(CONF_ESP_ADF_ID): fv.id_declaration_match_schema(_validate)},
 | 
			
		||||
        extra=cv.ALLOW_EXTRA,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ESPADF),
 | 
			
		||||
            cv.Optional(CONF_BOARD): cv.string_strict,
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
    _default_board,
 | 
			
		||||
    cv.only_with_esp_idf,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_ESP_ADF")
 | 
			
		||||
 | 
			
		||||
    cg.add_platformio_option("build_unflags", "-Wl,--end-group")
 | 
			
		||||
 | 
			
		||||
    esp32.add_idf_component(
 | 
			
		||||
        name="esp-adf",
 | 
			
		||||
        repo="https://github.com/espressif/esp-adf",
 | 
			
		||||
        path="components",
 | 
			
		||||
        ref="v2.5",
 | 
			
		||||
        components=["*"],
 | 
			
		||||
        submodules=["components/esp-sr", "components/esp-adf-libs"],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    esp32.add_idf_component(
 | 
			
		||||
        name="esp-dsp",
 | 
			
		||||
        repo="https://github.com/espressif/esp-dsp",
 | 
			
		||||
        ref="v1.2.0",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    cg.add_platformio_option(
 | 
			
		||||
        "board_build.embed_txtfiles", "components/dueros_service/duer_profile"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if board := config.get(CONF_BOARD):
 | 
			
		||||
        cg.add_define("USE_ESP_ADF_BOARD")
 | 
			
		||||
 | 
			
		||||
        esp32.add_idf_sdkconfig_option(SUPPORTED_BOARDS[board], True)
 | 
			
		||||
 | 
			
		||||
        esp32.add_extra_script(
 | 
			
		||||
            "pre",
 | 
			
		||||
            "apply_adf_patches.py",
 | 
			
		||||
            os.path.join(os.path.dirname(__file__), "apply_adf_patches.py.script"),
 | 
			
		||||
        )
 | 
			
		||||
        esp32.add_extra_build_file(
 | 
			
		||||
            "esp_adf_patches/idf_v4.4_freertos.patch",
 | 
			
		||||
            "https://github.com/espressif/esp-adf/raw/v2.5/idf_patches/idf_v4.4_freertos.patch",
 | 
			
		||||
        )
 | 
			
		||||
							
								
								
									
										23
									
								
								esphome/components/esp_adf/apply_adf_patches.py.script
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/esp_adf/apply_adf_patches.py.script
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
from os.path import join, isfile
 | 
			
		||||
 | 
			
		||||
Import("env")
 | 
			
		||||
 | 
			
		||||
FRAMEWORK_DIR = env.PioPlatform().get_package_dir("framework-espidf")
 | 
			
		||||
patchflag_path = join(FRAMEWORK_DIR, ".adf-patching-done")
 | 
			
		||||
 | 
			
		||||
PROJECT_DIR = env.get('PROJECT_DIR')
 | 
			
		||||
 | 
			
		||||
PATCH_FILE = join(PROJECT_DIR, "esp_adf_patches", "idf_v4.4_freertos.patch")
 | 
			
		||||
 | 
			
		||||
# patch file only if we didn't do it before
 | 
			
		||||
if not isfile(patchflag_path):
 | 
			
		||||
    print(PATCH_FILE)
 | 
			
		||||
    assert isfile(PATCH_FILE)
 | 
			
		||||
 | 
			
		||||
    env.Execute("patch -p1 -d %s -i %s" % (FRAMEWORK_DIR, PATCH_FILE))
 | 
			
		||||
 | 
			
		||||
    def _touch(path):
 | 
			
		||||
        with open(path, "w") as fp:
 | 
			
		||||
            fp.write("")
 | 
			
		||||
 | 
			
		||||
    env.Execute(lambda *args, **kwargs: _touch(patchflag_path))
 | 
			
		||||
							
								
								
									
										30
									
								
								esphome/components/esp_adf/esp_adf.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								esphome/components/esp_adf/esp_adf.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
#include "esp_adf.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP_ADF_BOARD
 | 
			
		||||
#include <board.h>
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp_adf {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "esp_adf";
 | 
			
		||||
 | 
			
		||||
void ESPADF::setup() {
 | 
			
		||||
#ifdef USE_ESP_ADF_BOARD
 | 
			
		||||
  ESP_LOGI(TAG, "Start codec chip");
 | 
			
		||||
  audio_board_handle_t board_handle = audio_board_init();
 | 
			
		||||
  audio_hal_ctrl_codec(board_handle->audio_hal, AUDIO_HAL_CODEC_MODE_BOTH, AUDIO_HAL_CTRL_START);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ESPADF::get_setup_priority() const { return setup_priority::HARDWARE; }
 | 
			
		||||
 | 
			
		||||
}  // namespace esp_adf
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP_IDF
 | 
			
		||||
							
								
								
									
										58
									
								
								esphome/components/esp_adf/esp_adf.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								esphome/components/esp_adf/esp_adf.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp_adf {
 | 
			
		||||
 | 
			
		||||
static const size_t BUFFER_SIZE = 1024;
 | 
			
		||||
 | 
			
		||||
enum class TaskEventType : uint8_t {
 | 
			
		||||
  STARTING = 0,
 | 
			
		||||
  STARTED,
 | 
			
		||||
  RUNNING,
 | 
			
		||||
  STOPPING,
 | 
			
		||||
  STOPPED,
 | 
			
		||||
  WARNING = 255,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct TaskEvent {
 | 
			
		||||
  TaskEventType type;
 | 
			
		||||
  esp_err_t err;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct CommandEvent {
 | 
			
		||||
  bool stop;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct DataEvent {
 | 
			
		||||
  bool stop;
 | 
			
		||||
  size_t len;
 | 
			
		||||
  uint8_t data[BUFFER_SIZE];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ESPADF;
 | 
			
		||||
 | 
			
		||||
class ESPADFPipeline : public Parented<ESPADF> {};
 | 
			
		||||
 | 
			
		||||
class ESPADF : public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
 | 
			
		||||
  void lock() { this->lock_.lock(); }
 | 
			
		||||
  bool try_lock() { return this->lock_.try_lock(); }
 | 
			
		||||
  void unlock() { this->lock_.unlock(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  Mutex lock_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp_adf
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP_IDF
 | 
			
		||||
							
								
								
									
										41
									
								
								esphome/components/esp_adf/microphone/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								esphome/components/esp_adf/microphone/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import microphone
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
from .. import (
 | 
			
		||||
    CONF_ESP_ADF_ID,
 | 
			
		||||
    ESPADF,
 | 
			
		||||
    ESPADFPipeline,
 | 
			
		||||
    esp_adf_ns,
 | 
			
		||||
    final_validate_usable_board,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["esp_adf"]
 | 
			
		||||
CONFLICTS_WITH = ["i2s_audio"]
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
 | 
			
		||||
ESPADFMicrophone = esp_adf_ns.class_(
 | 
			
		||||
    "ESPADFMicrophone", ESPADFPipeline, microphone.Microphone, cg.Component
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    microphone.MICROPHONE_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ESPADFMicrophone),
 | 
			
		||||
            cv.GenerateID(CONF_ESP_ADF_ID): cv.use_id(ESPADF),
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.only_with_esp_idf,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = final_validate_usable_board("microphone")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await cg.register_parented(var, config[CONF_ESP_ADF_ID])
 | 
			
		||||
 | 
			
		||||
    await microphone.register_microphone(var, config)
 | 
			
		||||
							
								
								
									
										336
									
								
								esphome/components/esp_adf/microphone/esp_adf_microphone.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										336
									
								
								esphome/components/esp_adf/microphone/esp_adf_microphone.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,336 @@
 | 
			
		||||
#include "esp_adf_microphone.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
 | 
			
		||||
#include <driver/i2s.h>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include <algorithm_stream.h>
 | 
			
		||||
#include <audio_element.h>
 | 
			
		||||
#include <audio_hal.h>
 | 
			
		||||
#include <audio_pipeline.h>
 | 
			
		||||
#include <filter_resample.h>
 | 
			
		||||
#include <i2s_stream.h>
 | 
			
		||||
#include <raw_stream.h>
 | 
			
		||||
#include <recorder_sr.h>
 | 
			
		||||
 | 
			
		||||
#include <board.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp_adf {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "esp_adf.microphone";
 | 
			
		||||
 | 
			
		||||
void ESPADFMicrophone::setup() {
 | 
			
		||||
  this->ring_buffer_ = rb_create(8000, sizeof(int16_t));
 | 
			
		||||
  if (this->ring_buffer_ == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "Could not allocate ring buffer.");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->read_event_queue_ = xQueueCreate(20, sizeof(TaskEvent));
 | 
			
		||||
  this->read_command_queue_ = xQueueCreate(20, sizeof(CommandEvent));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPADFMicrophone::start() {
 | 
			
		||||
  if (this->is_failed())
 | 
			
		||||
    return;
 | 
			
		||||
  if (this->state_ == microphone::STATE_STOPPING) {
 | 
			
		||||
    ESP_LOGW(TAG, "Microphone is stopping, cannot start.");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->state_ = microphone::STATE_STARTING;
 | 
			
		||||
}
 | 
			
		||||
void ESPADFMicrophone::start_() {
 | 
			
		||||
  if (!this->parent_->try_lock()) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  xTaskCreate(ESPADFMicrophone::read_task, "read_task", 8192, (void *) this, 0, &this->read_task_handle_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPADFMicrophone::read_task(void *params) {
 | 
			
		||||
  ESPADFMicrophone *this_mic = (ESPADFMicrophone *) params;
 | 
			
		||||
  TaskEvent event;
 | 
			
		||||
 | 
			
		||||
  ExternalRAMAllocator<int16_t> allocator(ExternalRAMAllocator<int16_t>::ALLOW_FAILURE);
 | 
			
		||||
  int16_t *buffer = allocator.allocate(BUFFER_SIZE / sizeof(int16_t));
 | 
			
		||||
  if (buffer == nullptr) {
 | 
			
		||||
    event.type = TaskEventType::WARNING;
 | 
			
		||||
    event.err = ESP_ERR_NO_MEM;
 | 
			
		||||
    xQueueSend(this_mic->read_event_queue_, &event, portMAX_DELAY);
 | 
			
		||||
 | 
			
		||||
    event.type = TaskEventType::STOPPED;
 | 
			
		||||
    event.err = ESP_OK;
 | 
			
		||||
    xQueueSend(this_mic->read_event_queue_, &event, portMAX_DELAY);
 | 
			
		||||
 | 
			
		||||
    while (true) {
 | 
			
		||||
      delay(10);
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  event.type = TaskEventType::STARTING;
 | 
			
		||||
  xQueueSend(this_mic->read_event_queue_, &event, portMAX_DELAY);
 | 
			
		||||
 | 
			
		||||
  audio_pipeline_cfg_t pipeline_cfg = {
 | 
			
		||||
      .rb_size = 8 * 1024,
 | 
			
		||||
  };
 | 
			
		||||
  audio_pipeline_handle_t pipeline = audio_pipeline_init(&pipeline_cfg);
 | 
			
		||||
 | 
			
		||||
  i2s_driver_config_t i2s_config = {
 | 
			
		||||
      .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX),
 | 
			
		||||
      .sample_rate = 16000,
 | 
			
		||||
      .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
 | 
			
		||||
      .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
 | 
			
		||||
      .communication_format = I2S_COMM_FORMAT_STAND_I2S,
 | 
			
		||||
      .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2 | ESP_INTR_FLAG_IRAM,
 | 
			
		||||
      .dma_buf_count = 8,
 | 
			
		||||
      .dma_buf_len = 128,
 | 
			
		||||
      .use_apll = false,
 | 
			
		||||
      .tx_desc_auto_clear = true,
 | 
			
		||||
      .fixed_mclk = 0,
 | 
			
		||||
      .mclk_multiple = I2S_MCLK_MULTIPLE_256,
 | 
			
		||||
      .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  i2s_stream_cfg_t i2s_cfg = {
 | 
			
		||||
      .type = AUDIO_STREAM_READER,
 | 
			
		||||
      .i2s_config = i2s_config,
 | 
			
		||||
      .i2s_port = static_cast<i2s_port_t>(CODEC_ADC_I2S_PORT),
 | 
			
		||||
      .use_alc = false,
 | 
			
		||||
      .volume = 0,
 | 
			
		||||
      .out_rb_size = I2S_STREAM_RINGBUFFER_SIZE,
 | 
			
		||||
      .task_stack = I2S_STREAM_TASK_STACK,
 | 
			
		||||
      .task_core = I2S_STREAM_TASK_CORE,
 | 
			
		||||
      .task_prio = I2S_STREAM_TASK_PRIO,
 | 
			
		||||
      .stack_in_ext = false,
 | 
			
		||||
      .multi_out_num = 0,
 | 
			
		||||
      .uninstall_drv = true,
 | 
			
		||||
      .need_expand = false,
 | 
			
		||||
      .expand_src_bits = I2S_BITS_PER_SAMPLE_16BIT,
 | 
			
		||||
  };
 | 
			
		||||
  audio_element_handle_t i2s_stream_reader = i2s_stream_init(&i2s_cfg);
 | 
			
		||||
 | 
			
		||||
  rsp_filter_cfg_t rsp_cfg = {
 | 
			
		||||
      .src_rate = 16000,
 | 
			
		||||
      .src_ch = 2,
 | 
			
		||||
      .dest_rate = 16000,
 | 
			
		||||
      .dest_bits = 16,
 | 
			
		||||
      .dest_ch = 1,
 | 
			
		||||
      .src_bits = I2S_BITS_PER_SAMPLE_16BIT,
 | 
			
		||||
      .mode = RESAMPLE_DECODE_MODE,
 | 
			
		||||
      .max_indata_bytes = RSP_FILTER_BUFFER_BYTE,
 | 
			
		||||
      .out_len_bytes = RSP_FILTER_BUFFER_BYTE,
 | 
			
		||||
      .type = ESP_RESAMPLE_TYPE_AUTO,
 | 
			
		||||
      .complexity = 2,
 | 
			
		||||
      .down_ch_idx = 0,
 | 
			
		||||
      .prefer_flag = ESP_RSP_PREFER_TYPE_SPEED,
 | 
			
		||||
      .out_rb_size = RSP_FILTER_RINGBUFFER_SIZE,
 | 
			
		||||
      .task_stack = RSP_FILTER_TASK_STACK,
 | 
			
		||||
      .task_core = RSP_FILTER_TASK_CORE,
 | 
			
		||||
      .task_prio = RSP_FILTER_TASK_PRIO,
 | 
			
		||||
      .stack_in_ext = true,
 | 
			
		||||
  };
 | 
			
		||||
  audio_element_handle_t filter = rsp_filter_init(&rsp_cfg);
 | 
			
		||||
 | 
			
		||||
  algorithm_stream_cfg_t algo_cfg = {
 | 
			
		||||
      .input_type = ALGORITHM_STREAM_INPUT_TYPE1,
 | 
			
		||||
      .task_stack = 10 * 1024,
 | 
			
		||||
      .task_prio = ALGORITHM_STREAM_TASK_PERIOD,
 | 
			
		||||
      .task_core = ALGORITHM_STREAM_PINNED_TO_CORE,
 | 
			
		||||
      .out_rb_size = ALGORITHM_STREAM_RINGBUFFER_SIZE,
 | 
			
		||||
      .stack_in_ext = true,
 | 
			
		||||
      .rec_linear_factor = 1,
 | 
			
		||||
      .ref_linear_factor = 1,
 | 
			
		||||
      .debug_input = false,
 | 
			
		||||
      .swap_ch = false,
 | 
			
		||||
      // .algo_mask = ALGORITHM_STREAM_USE_AGC,
 | 
			
		||||
      // .algo_mask = (ALGORITHM_STREAM_USE_AEC | ALGORITHM_STREAM_USE_AGC | ALGORITHM_STREAM_USE_NS),
 | 
			
		||||
      // .algo_mask = (ALGORITHM_STREAM_USE_AGC | ALGORITHM_STREAM_USE_NS),
 | 
			
		||||
      .algo_mask = (ALGORITHM_STREAM_USE_AEC | ALGORITHM_STREAM_USE_NS),
 | 
			
		||||
      // .algo_mask = (ALGORITHM_STREAM_USE_NS),
 | 
			
		||||
      .sample_rate = 16000,
 | 
			
		||||
      .mic_ch = 1,
 | 
			
		||||
      .agc_gain = 10,
 | 
			
		||||
      .aec_low_cost = false,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // audio_element_handle_t algo_stream = algo_stream_init(&algo_cfg);
 | 
			
		||||
 | 
			
		||||
  raw_stream_cfg_t raw_cfg = {
 | 
			
		||||
      .type = AUDIO_STREAM_READER,
 | 
			
		||||
      .out_rb_size = 8 * 1024,
 | 
			
		||||
  };
 | 
			
		||||
  audio_element_handle_t raw_read = raw_stream_init(&raw_cfg);
 | 
			
		||||
 | 
			
		||||
  audio_pipeline_register(pipeline, i2s_stream_reader, "i2s");
 | 
			
		||||
  audio_pipeline_register(pipeline, filter, "filter");
 | 
			
		||||
  // audio_pipeline_register(pipeline, algo_stream, "algo");
 | 
			
		||||
  audio_pipeline_register(pipeline, raw_read, "raw");
 | 
			
		||||
 | 
			
		||||
  const char *link_tag[4] = {
 | 
			
		||||
      "i2s",
 | 
			
		||||
      "filter",
 | 
			
		||||
      // "algo",
 | 
			
		||||
      "raw",
 | 
			
		||||
  };
 | 
			
		||||
  audio_pipeline_link(pipeline, &link_tag[0], 3);
 | 
			
		||||
 | 
			
		||||
  audio_pipeline_run(pipeline);
 | 
			
		||||
 | 
			
		||||
  event.type = TaskEventType::STARTED;
 | 
			
		||||
  xQueueSend(this_mic->read_event_queue_, &event, portMAX_DELAY);
 | 
			
		||||
 | 
			
		||||
  CommandEvent command_event;
 | 
			
		||||
 | 
			
		||||
  while (true) {
 | 
			
		||||
    if (xQueueReceive(this_mic->read_command_queue_, &command_event, 0) == pdTRUE) {
 | 
			
		||||
      if (command_event.stop) {
 | 
			
		||||
        // Stop signal from main thread
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    int bytes_read = raw_stream_read(raw_read, (char *) buffer, BUFFER_SIZE);
 | 
			
		||||
 | 
			
		||||
    if (bytes_read == -2 || bytes_read == 0) {
 | 
			
		||||
      // No data in buffers to read.
 | 
			
		||||
      continue;
 | 
			
		||||
    } else if (bytes_read < 0) {
 | 
			
		||||
      event.type = TaskEventType::WARNING;
 | 
			
		||||
      event.err = bytes_read;
 | 
			
		||||
      xQueueSend(this_mic->read_event_queue_, &event, 0);
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    event.type = TaskEventType::RUNNING;
 | 
			
		||||
    event.err = bytes_read;
 | 
			
		||||
    xQueueSend(this_mic->read_event_queue_, &event, 0);
 | 
			
		||||
 | 
			
		||||
    int available = rb_bytes_available(this_mic->ring_buffer_);
 | 
			
		||||
    if (available < bytes_read) {
 | 
			
		||||
      rb_read(this_mic->ring_buffer_, nullptr, bytes_read - available, 0);
 | 
			
		||||
    }
 | 
			
		||||
    rb_write(this_mic->ring_buffer_, (char *) buffer, bytes_read, 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  allocator.deallocate(buffer, BUFFER_SIZE / sizeof(int16_t));
 | 
			
		||||
 | 
			
		||||
  audio_pipeline_stop(pipeline);
 | 
			
		||||
  audio_pipeline_wait_for_stop(pipeline);
 | 
			
		||||
  audio_pipeline_terminate(pipeline);
 | 
			
		||||
 | 
			
		||||
  event.type = TaskEventType::STOPPING;
 | 
			
		||||
  xQueueSend(this_mic->read_event_queue_, &event, portMAX_DELAY);
 | 
			
		||||
 | 
			
		||||
  audio_pipeline_unregister(pipeline, i2s_stream_reader);
 | 
			
		||||
  audio_pipeline_unregister(pipeline, filter);
 | 
			
		||||
  // audio_pipeline_unregister(pipeline, algo_stream);
 | 
			
		||||
  audio_pipeline_unregister(pipeline, raw_read);
 | 
			
		||||
 | 
			
		||||
  audio_pipeline_deinit(pipeline);
 | 
			
		||||
  audio_element_deinit(i2s_stream_reader);
 | 
			
		||||
  audio_element_deinit(filter);
 | 
			
		||||
  // audio_element_deinit(algo_stream);
 | 
			
		||||
  audio_element_deinit(raw_read);
 | 
			
		||||
 | 
			
		||||
  event.type = TaskEventType::STOPPED;
 | 
			
		||||
  xQueueSend(this_mic->read_event_queue_, &event, portMAX_DELAY);
 | 
			
		||||
 | 
			
		||||
  while (true) {
 | 
			
		||||
    delay(10);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPADFMicrophone::stop() {
 | 
			
		||||
  if (this->state_ == microphone::STATE_STOPPED || this->state_ == microphone::STATE_STOPPING || this->is_failed())
 | 
			
		||||
    return;
 | 
			
		||||
  this->state_ = microphone::STATE_STOPPING;
 | 
			
		||||
  CommandEvent command_event;
 | 
			
		||||
  command_event.stop = true;
 | 
			
		||||
  xQueueSendToFront(this->read_command_queue_, &command_event, portMAX_DELAY);
 | 
			
		||||
  ESP_LOGD(TAG, "Stopping microphone");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
size_t ESPADFMicrophone::read(int16_t *buf, size_t len) {
 | 
			
		||||
  if (rb_bytes_available(this->ring_buffer_) == 0) {
 | 
			
		||||
    return 0;  // No data
 | 
			
		||||
  }
 | 
			
		||||
  int bytes_read = rb_read(this->ring_buffer_, (char *) buf, len, 0);
 | 
			
		||||
 | 
			
		||||
  if (bytes_read == -4 || bytes_read == -2 || bytes_read == 0) {
 | 
			
		||||
    // No data in buffers to read.
 | 
			
		||||
    return 0;
 | 
			
		||||
  } else if (bytes_read < 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Error reading from I2S microphone %s (%d)", esp_err_to_name(bytes_read), bytes_read);
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
 | 
			
		||||
  return bytes_read;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPADFMicrophone::read_() {
 | 
			
		||||
  std::vector<int16_t> samples;
 | 
			
		||||
  samples.resize(BUFFER_SIZE);
 | 
			
		||||
  this->read(samples.data(), samples.size());
 | 
			
		||||
 | 
			
		||||
  this->data_callbacks_.call(samples);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPADFMicrophone::watch_() {
 | 
			
		||||
  TaskEvent event;
 | 
			
		||||
  if (xQueueReceive(this->read_event_queue_, &event, 0) == pdTRUE) {
 | 
			
		||||
    switch (event.type) {
 | 
			
		||||
      case TaskEventType::STARTING:
 | 
			
		||||
      case TaskEventType::STOPPING:
 | 
			
		||||
        break;
 | 
			
		||||
      case TaskEventType::STARTED:
 | 
			
		||||
        ESP_LOGD(TAG, "Microphone started");
 | 
			
		||||
        this->state_ = microphone::STATE_RUNNING;
 | 
			
		||||
        break;
 | 
			
		||||
      case TaskEventType::RUNNING:
 | 
			
		||||
        this->status_clear_warning();
 | 
			
		||||
        // ESP_LOGD(TAG, "Putting %d bytes into ring buffer", event.err);
 | 
			
		||||
        break;
 | 
			
		||||
      case TaskEventType::STOPPED:
 | 
			
		||||
        this->parent_->unlock();
 | 
			
		||||
        this->state_ = microphone::STATE_STOPPED;
 | 
			
		||||
        vTaskDelete(this->read_task_handle_);
 | 
			
		||||
        this->read_task_handle_ = nullptr;
 | 
			
		||||
        ESP_LOGD(TAG, "Microphone stopped");
 | 
			
		||||
        break;
 | 
			
		||||
      case TaskEventType::WARNING:
 | 
			
		||||
        ESP_LOGW(TAG, "Error writing to pipeline: %s", esp_err_to_name(event.err));
 | 
			
		||||
        this->status_set_warning();
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPADFMicrophone::loop() {
 | 
			
		||||
  this->watch_();
 | 
			
		||||
  switch (this->state_) {
 | 
			
		||||
    case microphone::STATE_STOPPED:
 | 
			
		||||
    case microphone::STATE_STOPPING:
 | 
			
		||||
      break;
 | 
			
		||||
    case microphone::STATE_STARTING:
 | 
			
		||||
      this->start_();
 | 
			
		||||
      break;
 | 
			
		||||
    case microphone::STATE_RUNNING:
 | 
			
		||||
      if (this->data_callbacks_.size() > 0) {
 | 
			
		||||
        this->read_();
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esp_adf
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP_IDF
 | 
			
		||||
							
								
								
									
										42
									
								
								esphome/components/esp_adf/microphone/esp_adf_microphone.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								esphome/components/esp_adf/microphone/esp_adf_microphone.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
 | 
			
		||||
#include "../esp_adf.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/microphone/microphone.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
 | 
			
		||||
#include <ringbuf.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp_adf {
 | 
			
		||||
 | 
			
		||||
class ESPADFMicrophone : public ESPADFPipeline, public microphone::Microphone, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void start() override;
 | 
			
		||||
  void stop() override;
 | 
			
		||||
 | 
			
		||||
  void loop() override;
 | 
			
		||||
 | 
			
		||||
  size_t read(int16_t *buf, size_t len) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void start_();
 | 
			
		||||
  void read_();
 | 
			
		||||
  void watch_();
 | 
			
		||||
 | 
			
		||||
  static void read_task(void *params);
 | 
			
		||||
 | 
			
		||||
  ringbuf_handle_t ring_buffer_;
 | 
			
		||||
 | 
			
		||||
  TaskHandle_t read_task_handle_{nullptr};
 | 
			
		||||
  QueueHandle_t read_event_queue_;
 | 
			
		||||
  QueueHandle_t read_command_queue_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp_adf
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP_IDF
 | 
			
		||||
							
								
								
									
										41
									
								
								esphome/components/esp_adf/speaker/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								esphome/components/esp_adf/speaker/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import speaker
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
from .. import (
 | 
			
		||||
    CONF_ESP_ADF_ID,
 | 
			
		||||
    ESPADF,
 | 
			
		||||
    ESPADFPipeline,
 | 
			
		||||
    esp_adf_ns,
 | 
			
		||||
    final_validate_usable_board,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["esp_adf"]
 | 
			
		||||
CONFLICTS_WITH = ["i2s_audio"]
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
 | 
			
		||||
ESPADFSpeaker = esp_adf_ns.class_(
 | 
			
		||||
    "ESPADFSpeaker", ESPADFPipeline, speaker.Speaker, cg.Component
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ESPADFSpeaker),
 | 
			
		||||
            cv.GenerateID(CONF_ESP_ADF_ID): cv.use_id(ESPADF),
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.only_with_esp_idf,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = final_validate_usable_board("speaker")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await cg.register_parented(var, config[CONF_ESP_ADF_ID])
 | 
			
		||||
 | 
			
		||||
    await speaker.register_speaker(var, config)
 | 
			
		||||
							
								
								
									
										274
									
								
								esphome/components/esp_adf/speaker/esp_adf_speaker.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										274
									
								
								esphome/components/esp_adf/speaker/esp_adf_speaker.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,274 @@
 | 
			
		||||
#include "esp_adf_speaker.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
 | 
			
		||||
#include <driver/i2s.h>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include <audio_hal.h>
 | 
			
		||||
#include <filter_resample.h>
 | 
			
		||||
#include <i2s_stream.h>
 | 
			
		||||
#include <raw_stream.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp_adf {
 | 
			
		||||
 | 
			
		||||
static const size_t BUFFER_COUNT = 50;
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "esp_adf.speaker";
 | 
			
		||||
 | 
			
		||||
void ESPADFSpeaker::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up ESP ADF Speaker...");
 | 
			
		||||
 | 
			
		||||
  this->buffer_queue_ = xQueueCreate(BUFFER_COUNT, sizeof(DataEvent));
 | 
			
		||||
  this->event_queue_ = xQueueCreate(20, sizeof(TaskEvent));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPADFSpeaker::start() { this->state_ = speaker::STATE_STARTING; }
 | 
			
		||||
void ESPADFSpeaker::start_() {
 | 
			
		||||
  if (!this->parent_->try_lock()) {
 | 
			
		||||
    return;  // Waiting for another i2s component to return lock
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  xTaskCreate(ESPADFSpeaker::player_task, "speaker_task", 8192, (void *) this, 0, &this->player_task_handle_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPADFSpeaker::player_task(void *params) {
 | 
			
		||||
  ESPADFSpeaker *this_speaker = (ESPADFSpeaker *) params;
 | 
			
		||||
 | 
			
		||||
  TaskEvent event;
 | 
			
		||||
  event.type = TaskEventType::STARTING;
 | 
			
		||||
  xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
 | 
			
		||||
 | 
			
		||||
  i2s_driver_config_t i2s_config = {
 | 
			
		||||
      .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_TX),
 | 
			
		||||
      .sample_rate = 16000,
 | 
			
		||||
      .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
 | 
			
		||||
      .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
 | 
			
		||||
      .communication_format = I2S_COMM_FORMAT_STAND_I2S,
 | 
			
		||||
      .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2 | ESP_INTR_FLAG_IRAM,
 | 
			
		||||
      .dma_buf_count = 8,
 | 
			
		||||
      .dma_buf_len = 1024,
 | 
			
		||||
      .use_apll = false,
 | 
			
		||||
      .tx_desc_auto_clear = true,
 | 
			
		||||
      .fixed_mclk = 0,
 | 
			
		||||
      .mclk_multiple = I2S_MCLK_MULTIPLE_256,
 | 
			
		||||
      .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  audio_pipeline_cfg_t pipeline_cfg = {
 | 
			
		||||
      .rb_size = 8 * 1024,
 | 
			
		||||
  };
 | 
			
		||||
  audio_pipeline_handle_t pipeline = audio_pipeline_init(&pipeline_cfg);
 | 
			
		||||
 | 
			
		||||
  i2s_stream_cfg_t i2s_cfg = {
 | 
			
		||||
      .type = AUDIO_STREAM_WRITER,
 | 
			
		||||
      .i2s_config = i2s_config,
 | 
			
		||||
      .i2s_port = I2S_NUM_0,
 | 
			
		||||
      .use_alc = false,
 | 
			
		||||
      .volume = 0,
 | 
			
		||||
      .out_rb_size = I2S_STREAM_RINGBUFFER_SIZE,
 | 
			
		||||
      .task_stack = I2S_STREAM_TASK_STACK,
 | 
			
		||||
      .task_core = I2S_STREAM_TASK_CORE,
 | 
			
		||||
      .task_prio = I2S_STREAM_TASK_PRIO,
 | 
			
		||||
      .stack_in_ext = false,
 | 
			
		||||
      .multi_out_num = 0,
 | 
			
		||||
      .uninstall_drv = true,
 | 
			
		||||
      .need_expand = false,
 | 
			
		||||
      .expand_src_bits = I2S_BITS_PER_SAMPLE_16BIT,
 | 
			
		||||
  };
 | 
			
		||||
  audio_element_handle_t i2s_stream_writer = i2s_stream_init(&i2s_cfg);
 | 
			
		||||
 | 
			
		||||
  rsp_filter_cfg_t rsp_cfg = {
 | 
			
		||||
      .src_rate = 16000,
 | 
			
		||||
      .src_ch = 1,
 | 
			
		||||
      .dest_rate = 16000,
 | 
			
		||||
      .dest_bits = 16,
 | 
			
		||||
      .dest_ch = 2,
 | 
			
		||||
      .src_bits = 16,
 | 
			
		||||
      .mode = RESAMPLE_DECODE_MODE,
 | 
			
		||||
      .max_indata_bytes = RSP_FILTER_BUFFER_BYTE,
 | 
			
		||||
      .out_len_bytes = RSP_FILTER_BUFFER_BYTE,
 | 
			
		||||
      .type = ESP_RESAMPLE_TYPE_AUTO,
 | 
			
		||||
      .complexity = 2,
 | 
			
		||||
      .down_ch_idx = 0,
 | 
			
		||||
      .prefer_flag = ESP_RSP_PREFER_TYPE_SPEED,
 | 
			
		||||
      .out_rb_size = RSP_FILTER_RINGBUFFER_SIZE,
 | 
			
		||||
      .task_stack = RSP_FILTER_TASK_STACK,
 | 
			
		||||
      .task_core = RSP_FILTER_TASK_CORE,
 | 
			
		||||
      .task_prio = RSP_FILTER_TASK_PRIO,
 | 
			
		||||
      .stack_in_ext = true,
 | 
			
		||||
  };
 | 
			
		||||
  audio_element_handle_t filter = rsp_filter_init(&rsp_cfg);
 | 
			
		||||
 | 
			
		||||
  raw_stream_cfg_t raw_cfg = {
 | 
			
		||||
      .type = AUDIO_STREAM_WRITER,
 | 
			
		||||
      .out_rb_size = 8 * 1024,
 | 
			
		||||
  };
 | 
			
		||||
  audio_element_handle_t raw_write = raw_stream_init(&raw_cfg);
 | 
			
		||||
 | 
			
		||||
  audio_pipeline_register(pipeline, raw_write, "raw");
 | 
			
		||||
  audio_pipeline_register(pipeline, filter, "filter");
 | 
			
		||||
  audio_pipeline_register(pipeline, i2s_stream_writer, "i2s");
 | 
			
		||||
 | 
			
		||||
  const char *link_tag[3] = {
 | 
			
		||||
      "raw",
 | 
			
		||||
      // "filter",
 | 
			
		||||
      "i2s",
 | 
			
		||||
  };
 | 
			
		||||
  audio_pipeline_link(pipeline, &link_tag[0], 2);
 | 
			
		||||
 | 
			
		||||
  audio_pipeline_run(pipeline);
 | 
			
		||||
 | 
			
		||||
  DataEvent data_event;
 | 
			
		||||
 | 
			
		||||
  event.type = TaskEventType::STARTED;
 | 
			
		||||
  xQueueSend(this_speaker->event_queue_, &event, 0);
 | 
			
		||||
 | 
			
		||||
  uint32_t last_received = millis();
 | 
			
		||||
 | 
			
		||||
  while (true) {
 | 
			
		||||
    if (xQueueReceive(this_speaker->buffer_queue_, &data_event, 0) != pdTRUE) {
 | 
			
		||||
      if (millis() - last_received > 500) {
 | 
			
		||||
        // No audio for 500ms, stop
 | 
			
		||||
        break;
 | 
			
		||||
      } else {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (data_event.stop) {
 | 
			
		||||
      // Stop signal from main thread
 | 
			
		||||
      while (xQueueReceive(this_speaker->buffer_queue_, &data_event, 0) == pdTRUE) {
 | 
			
		||||
        // Flush queue
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    size_t remaining = data_event.len;
 | 
			
		||||
    size_t current = 0;
 | 
			
		||||
    if (remaining > 0)
 | 
			
		||||
      last_received = millis();
 | 
			
		||||
 | 
			
		||||
    while (remaining > 0) {
 | 
			
		||||
      int bytes_written = raw_stream_write(raw_write, (char *) data_event.data + current, remaining);
 | 
			
		||||
      if (bytes_written == ESP_FAIL) {
 | 
			
		||||
        event = {.type = TaskEventType::WARNING, .err = ESP_FAIL};
 | 
			
		||||
        xQueueSend(this_speaker->event_queue_, &event, 0);
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      remaining -= bytes_written;
 | 
			
		||||
      current += bytes_written;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    event.type = TaskEventType::RUNNING;
 | 
			
		||||
    xQueueSend(this_speaker->event_queue_, &event, 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  audio_pipeline_stop(pipeline);
 | 
			
		||||
  audio_pipeline_wait_for_stop(pipeline);
 | 
			
		||||
  audio_pipeline_terminate(pipeline);
 | 
			
		||||
 | 
			
		||||
  event.type = TaskEventType::STOPPING;
 | 
			
		||||
  xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
 | 
			
		||||
 | 
			
		||||
  audio_pipeline_unregister(pipeline, i2s_stream_writer);
 | 
			
		||||
  audio_pipeline_unregister(pipeline, filter);
 | 
			
		||||
  audio_pipeline_unregister(pipeline, raw_write);
 | 
			
		||||
 | 
			
		||||
  audio_pipeline_deinit(pipeline);
 | 
			
		||||
  audio_element_deinit(i2s_stream_writer);
 | 
			
		||||
  audio_element_deinit(filter);
 | 
			
		||||
  audio_element_deinit(raw_write);
 | 
			
		||||
 | 
			
		||||
  event.type = TaskEventType::STOPPED;
 | 
			
		||||
  xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
 | 
			
		||||
 | 
			
		||||
  while (true) {
 | 
			
		||||
    delay(10);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPADFSpeaker::stop() {
 | 
			
		||||
  if (this->state_ == speaker::STATE_STOPPED)
 | 
			
		||||
    return;
 | 
			
		||||
  if (this->state_ == speaker::STATE_STARTING) {
 | 
			
		||||
    this->state_ = speaker::STATE_STOPPED;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->state_ = speaker::STATE_STOPPING;
 | 
			
		||||
  DataEvent data;
 | 
			
		||||
  data.stop = true;
 | 
			
		||||
  xQueueSendToFront(this->buffer_queue_, &data, portMAX_DELAY);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPADFSpeaker::watch_() {
 | 
			
		||||
  TaskEvent event;
 | 
			
		||||
  if (xQueueReceive(this->event_queue_, &event, 0) == pdTRUE) {
 | 
			
		||||
    switch (event.type) {
 | 
			
		||||
      case TaskEventType::STARTING:
 | 
			
		||||
      case TaskEventType::STOPPING:
 | 
			
		||||
        break;
 | 
			
		||||
      case TaskEventType::STARTED:
 | 
			
		||||
        this->state_ = speaker::STATE_RUNNING;
 | 
			
		||||
        break;
 | 
			
		||||
      case TaskEventType::RUNNING:
 | 
			
		||||
        this->status_clear_warning();
 | 
			
		||||
        break;
 | 
			
		||||
      case TaskEventType::STOPPED:
 | 
			
		||||
        this->parent_->unlock();
 | 
			
		||||
        this->state_ = speaker::STATE_STOPPED;
 | 
			
		||||
        vTaskDelete(this->player_task_handle_);
 | 
			
		||||
        this->player_task_handle_ = nullptr;
 | 
			
		||||
        break;
 | 
			
		||||
      case TaskEventType::WARNING:
 | 
			
		||||
        ESP_LOGW(TAG, "Error writing to pipeline: %s", esp_err_to_name(event.err));
 | 
			
		||||
        this->status_set_warning();
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPADFSpeaker::loop() {
 | 
			
		||||
  this->watch_();
 | 
			
		||||
  switch (this->state_) {
 | 
			
		||||
    case speaker::STATE_STARTING:
 | 
			
		||||
      this->start_();
 | 
			
		||||
      break;
 | 
			
		||||
    case speaker::STATE_RUNNING:
 | 
			
		||||
    case speaker::STATE_STOPPING:
 | 
			
		||||
    case speaker::STATE_STOPPED:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
size_t ESPADFSpeaker::play(const uint8_t *data, size_t length) {
 | 
			
		||||
  if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) {
 | 
			
		||||
    this->start();
 | 
			
		||||
  }
 | 
			
		||||
  size_t remaining = length;
 | 
			
		||||
  size_t index = 0;
 | 
			
		||||
  while (remaining > 0) {
 | 
			
		||||
    DataEvent event;
 | 
			
		||||
    event.stop = false;
 | 
			
		||||
    size_t to_send_length = std::min(remaining, BUFFER_SIZE);
 | 
			
		||||
    event.len = to_send_length;
 | 
			
		||||
    memcpy(event.data, data + index, to_send_length);
 | 
			
		||||
    if (xQueueSend(this->buffer_queue_, &event, 0) != pdTRUE) {
 | 
			
		||||
      return index;  // Queue full
 | 
			
		||||
    }
 | 
			
		||||
    remaining -= to_send_length;
 | 
			
		||||
    index += to_send_length;
 | 
			
		||||
  }
 | 
			
		||||
  return index;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ESPADFSpeaker::has_buffered_data() const { return uxQueueMessagesWaiting(this->buffer_queue_) > 0; }
 | 
			
		||||
 | 
			
		||||
}  // namespace esp_adf
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP_IDF
 | 
			
		||||
							
								
								
									
										48
									
								
								esphome/components/esp_adf/speaker/esp_adf_speaker.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								esphome/components/esp_adf/speaker/esp_adf_speaker.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
 | 
			
		||||
#include "../esp_adf.h"
 | 
			
		||||
 | 
			
		||||
#include <freertos/FreeRTOS.h>
 | 
			
		||||
#include <freertos/queue.h>
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/speaker/speaker.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#include <audio_element.h>
 | 
			
		||||
#include <audio_pipeline.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp_adf {
 | 
			
		||||
 | 
			
		||||
class ESPADFSpeaker : public ESPADFPipeline, public speaker::Speaker, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  float get_setup_priority() const override { return esphome::setup_priority::LATE; }
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
 | 
			
		||||
  void start() override;
 | 
			
		||||
  void stop() override;
 | 
			
		||||
 | 
			
		||||
  size_t play(const uint8_t *data, size_t length) override;
 | 
			
		||||
 | 
			
		||||
  bool has_buffered_data() const override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void start_();
 | 
			
		||||
  void watch_();
 | 
			
		||||
 | 
			
		||||
  static void player_task(void *params);
 | 
			
		||||
 | 
			
		||||
  TaskHandle_t player_task_handle_{nullptr};
 | 
			
		||||
  QueueHandle_t buffer_queue_;
 | 
			
		||||
  QueueHandle_t event_queue_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp_adf
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP_IDF
 | 
			
		||||
@@ -18,7 +18,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_ON_SPEED_SET,
 | 
			
		||||
    CONF_ON_TURN_OFF,
 | 
			
		||||
    CONF_ON_TURN_ON,
 | 
			
		||||
    CONF_ON_PRESET_SET,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    CONF_DIRECTION,
 | 
			
		||||
    CONF_RESTORE_MODE,
 | 
			
		||||
@@ -58,9 +57,6 @@ CycleSpeedAction = fan_ns.class_("CycleSpeedAction", automation.Action)
 | 
			
		||||
FanTurnOnTrigger = fan_ns.class_("FanTurnOnTrigger", automation.Trigger.template())
 | 
			
		||||
FanTurnOffTrigger = fan_ns.class_("FanTurnOffTrigger", 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())
 | 
			
		||||
FanIsOffCondition = fan_ns.class_("FanIsOffCondition", automation.Condition.template())
 | 
			
		||||
@@ -105,46 +101,9 @@ FAN_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).exte
 | 
			
		||||
                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):
 | 
			
		||||
    await setup_entity(var, config)
 | 
			
		||||
@@ -195,9 +154,6 @@ async def setup_fan_core_(var, config):
 | 
			
		||||
    for conf in config.get(CONF_ON_SPEED_SET, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        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):
 | 
			
		||||
 
 | 
			
		||||
@@ -165,23 +165,5 @@ class FanSpeedSetTrigger : public Trigger<> {
 | 
			
		||||
  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 esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -32,12 +32,9 @@ void FanCall::perform() {
 | 
			
		||||
  if (this->direction_.has_value()) {
 | 
			
		||||
    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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FanCall::validate_() {
 | 
			
		||||
  auto traits = this->parent_.get_traits();
 | 
			
		||||
 | 
			
		||||
@@ -65,15 +62,6 @@ void FanCall::validate_() {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str());
 | 
			
		||||
    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) {
 | 
			
		||||
@@ -82,14 +70,6 @@ FanCall FanRestoreState::to_call(Fan &fan) {
 | 
			
		||||
  call.set_oscillating(this->oscillating);
 | 
			
		||||
  call.set_speed(this->speed);
 | 
			
		||||
  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;
 | 
			
		||||
}
 | 
			
		||||
void FanRestoreState::apply(Fan &fan) {
 | 
			
		||||
@@ -97,14 +77,6 @@ void FanRestoreState::apply(Fan &fan) {
 | 
			
		||||
  fan.oscillating = this->oscillating;
 | 
			
		||||
  fan.speed = this->speed;
 | 
			
		||||
  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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -128,9 +100,7 @@ void Fan::publish_state() {
 | 
			
		||||
  if (traits.supports_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->save_state_();
 | 
			
		||||
}
 | 
			
		||||
@@ -173,36 +143,20 @@ void Fan::save_state_() {
 | 
			
		||||
  state.oscillating = this->oscillating;
 | 
			
		||||
  state.speed = this->speed;
 | 
			
		||||
  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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Fan::dump_traits_(const char *tag, const char *prefix) {
 | 
			
		||||
  auto traits = this->get_traits();
 | 
			
		||||
 | 
			
		||||
  if (traits.supports_speed()) {
 | 
			
		||||
  if (this->get_traits().supports_speed()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "%s  Speed: YES", prefix);
 | 
			
		||||
    ESP_LOGCONFIG(tag, "%s  Speed count: %d", prefix, traits.supported_speed_count());
 | 
			
		||||
    ESP_LOGCONFIG(tag, "%s  Speed count: %d", prefix, this->get_traits().supported_speed_count());
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.supports_oscillation()) {
 | 
			
		||||
  if (this->get_traits().supports_oscillation()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "%s  Oscillation: YES", prefix);
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.supports_direction()) {
 | 
			
		||||
  if (this->get_traits().supports_direction()) {
 | 
			
		||||
    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
 | 
			
		||||
 
 | 
			
		||||
@@ -72,11 +72,6 @@ class FanCall {
 | 
			
		||||
    return *this;
 | 
			
		||||
  }
 | 
			
		||||
  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();
 | 
			
		||||
 | 
			
		||||
@@ -88,7 +83,6 @@ class FanCall {
 | 
			
		||||
  optional<bool> oscillating_;
 | 
			
		||||
  optional<int> speed_;
 | 
			
		||||
  optional<FanDirection> direction_{};
 | 
			
		||||
  std::string preset_mode_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct FanRestoreState {
 | 
			
		||||
@@ -96,7 +90,6 @@ struct FanRestoreState {
 | 
			
		||||
  int speed;
 | 
			
		||||
  bool oscillating;
 | 
			
		||||
  FanDirection direction;
 | 
			
		||||
  uint8_t preset_mode;
 | 
			
		||||
 | 
			
		||||
  /// Convert this struct to a fan call that can be performed.
 | 
			
		||||
  FanCall to_call(Fan &fan);
 | 
			
		||||
@@ -114,8 +107,6 @@ class Fan : public EntityBase {
 | 
			
		||||
  int speed{0};
 | 
			
		||||
  /// The current direction of the fan
 | 
			
		||||
  FanDirection direction{FanDirection::FORWARD};
 | 
			
		||||
  // The current preset mode of the fan
 | 
			
		||||
  std::string preset_mode{};
 | 
			
		||||
 | 
			
		||||
  FanCall turn_on();
 | 
			
		||||
  FanCall turn_off();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,3 @@
 | 
			
		||||
#include <set>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -28,19 +25,12 @@ class FanTraits {
 | 
			
		||||
  bool supports_direction() const { return this->direction_; }
 | 
			
		||||
  /// Set whether this fan supports changing 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:
 | 
			
		||||
  bool oscillation_{false};
 | 
			
		||||
  bool speed_{false};
 | 
			
		||||
  bool direction_{false};
 | 
			
		||||
  int speed_count_{};
 | 
			
		||||
  std::set<std::string> preset_modes_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace fan
 | 
			
		||||
 
 | 
			
		||||
@@ -67,13 +67,13 @@ def validate_pillow_installed(value):
 | 
			
		||||
    except ImportError as err:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "Please install the pillow python package to use this feature. "
 | 
			
		||||
            '(pip install "pillow==10.1.0")'
 | 
			
		||||
            '(pip install "pillow==10.0.1")'
 | 
			
		||||
        ) from err
 | 
			
		||||
 | 
			
		||||
    if version.parse(PIL.__version__) != version.parse("10.1.0"):
 | 
			
		||||
    if version.parse(PIL.__version__) != version.parse("10.0.1"):
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "Please update your pillow installation to 10.1.0. "
 | 
			
		||||
            '(pip install "pillow==10.1.0")'
 | 
			
		||||
            "Please update your pillow installation to 10.0.1. "
 | 
			
		||||
            '(pip install "pillow==10.0.1")'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return value
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@clydebarrow"]
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
 | 
			
		||||
ft5x06_ns = cg.esphome_ns.namespace("ft5x06")
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
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)
 | 
			
		||||
@@ -1,124 +0,0 @@
 | 
			
		||||
#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 +0,0 @@
 | 
			
		||||
CODEOWNERS = ["@gpambrozio"]
 | 
			
		||||
@@ -1,99 +0,0 @@
 | 
			
		||||
/**************************************************************************/
 | 
			
		||||
/*!
 | 
			
		||||
  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
 | 
			
		||||
@@ -1,41 +0,0 @@
 | 
			
		||||
/**************************************************************************/
 | 
			
		||||
/*!
 | 
			
		||||
  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
 | 
			
		||||
@@ -1,44 +0,0 @@
 | 
			
		||||
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))
 | 
			
		||||
@@ -1,96 +0,0 @@
 | 
			
		||||
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")
 | 
			
		||||
@@ -1,243 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,84 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@jesserockz", "@clydebarrow"]
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
 | 
			
		||||
gt911_ns = cg.esphome_ns.namespace("gt911")
 | 
			
		||||
@@ -1,31 +0,0 @@
 | 
			
		||||
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]))
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,31 +0,0 @@
 | 
			
		||||
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)))
 | 
			
		||||
@@ -1,111 +0,0 @@
 | 
			
		||||
#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
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
#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,20 +38,16 @@ PROTOCOL_MIN_TEMPERATURE = 16.0
 | 
			
		||||
PROTOCOL_MAX_TEMPERATURE = 30.0
 | 
			
		||||
PROTOCOL_TARGET_TEMPERATURE_STEP = 1.0
 | 
			
		||||
PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5
 | 
			
		||||
PROTOCOL_CONTROL_PACKET_SIZE = 10
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@paveldn"]
 | 
			
		||||
AUTO_LOAD = ["sensor"]
 | 
			
		||||
DEPENDENCIES = ["climate", "uart"]
 | 
			
		||||
CONF_ALTERNATIVE_SWING_CONTROL = "alternative_swing_control"
 | 
			
		||||
CONF_WIFI_SIGNAL = "wifi_signal"
 | 
			
		||||
CONF_ANSWER_TIMEOUT = "answer_timeout"
 | 
			
		||||
CONF_CONTROL_METHOD = "control_method"
 | 
			
		||||
CONF_CONTROL_PACKET_SIZE = "control_packet_size"
 | 
			
		||||
CONF_DISPLAY = "display"
 | 
			
		||||
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
 | 
			
		||||
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
 | 
			
		||||
CONF_VERTICAL_AIRFLOW = "vertical_airflow"
 | 
			
		||||
CONF_WIFI_SIGNAL = "wifi_signal"
 | 
			
		||||
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
 | 
			
		||||
 | 
			
		||||
PROTOCOL_HON = "HON"
 | 
			
		||||
PROTOCOL_SMARTAIR2 = "SMARTAIR2"
 | 
			
		||||
@@ -111,13 +107,6 @@ SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = {
 | 
			
		||||
    "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):
 | 
			
		||||
    if CONF_VISUAL in config:
 | 
			
		||||
@@ -195,9 +184,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(): cv.declare_id(Smartair2Climate),
 | 
			
		||||
                    cv.Optional(
 | 
			
		||||
                        CONF_ALTERNATIVE_SWING_CONTROL, default=False
 | 
			
		||||
                    ): cv.boolean,
 | 
			
		||||
                    cv.Optional(
 | 
			
		||||
                        CONF_SUPPORTED_PRESETS,
 | 
			
		||||
                        default=list(
 | 
			
		||||
@@ -211,15 +197,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend(
 | 
			
		||||
                {
 | 
			
		||||
                    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_CONTROL_PACKET_SIZE, default=PROTOCOL_CONTROL_PACKET_SIZE
 | 
			
		||||
                    ): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50),
 | 
			
		||||
                    cv.Optional(
 | 
			
		||||
                        CONF_SUPPORTED_PRESETS,
 | 
			
		||||
                        default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()),
 | 
			
		||||
@@ -430,8 +408,6 @@ async def to_code(config):
 | 
			
		||||
    await climate.register_climate(var, config)
 | 
			
		||||
 | 
			
		||||
    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:
 | 
			
		||||
        cg.add(var.set_beeper_state(config[CONF_BEEPER]))
 | 
			
		||||
    if CONF_DISPLAY in config:
 | 
			
		||||
@@ -447,15 +423,5 @@ async def to_code(config):
 | 
			
		||||
        cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS]))
 | 
			
		||||
    if CONF_ANSWER_TIMEOUT in config:
 | 
			
		||||
        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
 | 
			
		||||
    cg.add_library("pavlodn/HaierProtocol", "0.9.24")
 | 
			
		||||
    cg.add_library("pavlodn/HaierProtocol", "0.9.20")
 | 
			
		||||
 
 | 
			
		||||
@@ -19,45 +19,56 @@ constexpr size_t STATUS_REQUEST_INTERVAL_MS = 5000;
 | 
			
		||||
constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000;
 | 
			
		||||
constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000;
 | 
			
		||||
constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400;
 | 
			
		||||
constexpr size_t CONTROL_TIMEOUT_MS = 7000;
 | 
			
		||||
constexpr size_t NO_COMMAND = 0xFF;  // Indicate that there is no command supplied
 | 
			
		||||
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
// To reduce size of binary this function only available when log level is Verbose
 | 
			
		||||
const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
 | 
			
		||||
  static const char *phase_names[] = {
 | 
			
		||||
      "SENDING_INIT_1",
 | 
			
		||||
      "WAITING_INIT_1_ANSWER",
 | 
			
		||||
      "SENDING_INIT_2",
 | 
			
		||||
      "WAITING_INIT_2_ANSWER",
 | 
			
		||||
      "SENDING_FIRST_STATUS_REQUEST",
 | 
			
		||||
      "WAITING_FIRST_STATUS_ANSWER",
 | 
			
		||||
      "SENDING_ALARM_STATUS_REQUEST",
 | 
			
		||||
      "WAITING_ALARM_STATUS_ANSWER",
 | 
			
		||||
      "IDLE",
 | 
			
		||||
      "UNKNOWN",
 | 
			
		||||
      "SENDING_STATUS_REQUEST",
 | 
			
		||||
      "WAITING_STATUS_ANSWER",
 | 
			
		||||
      "SENDING_UPDATE_SIGNAL_REQUEST",
 | 
			
		||||
      "WAITING_UPDATE_SIGNAL_ANSWER",
 | 
			
		||||
      "SENDING_SIGNAL_LEVEL",
 | 
			
		||||
      "WAITING_SIGNAL_LEVEL_ANSWER",
 | 
			
		||||
      "SENDING_CONTROL",
 | 
			
		||||
      "SENDING_ACTION_COMMAND",
 | 
			
		||||
      "WAITING_CONTROL_ANSWER",
 | 
			
		||||
      "SENDING_POWER_ON_COMMAND",
 | 
			
		||||
      "WAITING_POWER_ON_ANSWER",
 | 
			
		||||
      "SENDING_POWER_OFF_COMMAND",
 | 
			
		||||
      "WAITING_POWER_OFF_ANSWER",
 | 
			
		||||
      "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;
 | 
			
		||||
  if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0))
 | 
			
		||||
    phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES;
 | 
			
		||||
  return phase_names[phase_index];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
HaierClimateBase::HaierClimateBase()
 | 
			
		||||
    : haier_protocol_(*this),
 | 
			
		||||
      protocol_phase_(ProtocolPhases::SENDING_INIT_1),
 | 
			
		||||
      action_request_(ActionRequest::NO_ACTION),
 | 
			
		||||
      display_status_(true),
 | 
			
		||||
      health_mode_(false),
 | 
			
		||||
      force_send_control_(false),
 | 
			
		||||
      forced_publish_(false),
 | 
			
		||||
      forced_request_status_(false),
 | 
			
		||||
      first_control_attempt_(false),
 | 
			
		||||
      reset_protocol_request_(false),
 | 
			
		||||
      send_wifi_signal_(true),
 | 
			
		||||
      use_crc_(false) {
 | 
			
		||||
      send_wifi_signal_(true) {
 | 
			
		||||
  this->traits_ = climate::ClimateTraits();
 | 
			
		||||
  this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT,
 | 
			
		||||
                                     climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY,
 | 
			
		||||
@@ -73,43 +84,42 @@ HaierClimateBase::~HaierClimateBase() {}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::set_phase(ProtocolPhases phase) {
 | 
			
		||||
  if (this->protocol_phase_ != phase) {
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
    ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase));
 | 
			
		||||
#else
 | 
			
		||||
    ESP_LOGV(TAG, "Phase transition: %d => %d", (int) this->protocol_phase_, (int) phase);
 | 
			
		||||
#endif
 | 
			
		||||
    this->protocol_phase_ = phase;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::reset_phase_() {
 | 
			
		||||
  this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
 | 
			
		||||
                                                                  : 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::check_timeout_(std::chrono::steady_clock::time_point now,
 | 
			
		||||
                                      std::chrono::steady_clock::time_point tpoint, size_t timeout) {
 | 
			
		||||
  return std::chrono::duration_cast<std::chrono::milliseconds>(now - tpoint).count() > timeout;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HaierClimateBase::is_message_interval_exceeded_(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
  return check_timeout(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS);
 | 
			
		||||
  return this->check_timeout_(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HaierClimateBase::is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
  return check_timeout(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS);
 | 
			
		||||
  return this->check_timeout_(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HaierClimateBase::is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
  return this->check_timeout_(now, this->control_request_timestamp_, CONTROL_TIMEOUT_MS);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HaierClimateBase::is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
  return check_timeout(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS);
 | 
			
		||||
  return this->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) {
 | 
			
		||||
  return check_timeout(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL);
 | 
			
		||||
  return this->check_timeout_(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_WIFI
 | 
			
		||||
haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_() {
 | 
			
		||||
haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_(uint8_t message_type) {
 | 
			
		||||
  static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00};
 | 
			
		||||
  if (wifi::global_wifi_component->is_connected()) {
 | 
			
		||||
    wifi_status_data[1] = 0;
 | 
			
		||||
@@ -121,8 +131,7 @@ haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_() {
 | 
			
		||||
    wifi_status_data[1] = 1;
 | 
			
		||||
    wifi_status_data[3] = 0;
 | 
			
		||||
  }
 | 
			
		||||
  return haier_protocol::HaierMessage(haier_protocol::FrameType::REPORT_NETWORK_STATUS, wifi_status_data,
 | 
			
		||||
                                      sizeof(wifi_status_data));
 | 
			
		||||
  return haier_protocol::HaierMessage(message_type, wifi_status_data, sizeof(wifi_status_data));
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
@@ -131,7 +140,7 @@ bool HaierClimateBase::get_display_state() const { return this->display_status_;
 | 
			
		||||
void HaierClimateBase::set_display_state(bool state) {
 | 
			
		||||
  if (this->display_status_ != state) {
 | 
			
		||||
    this->display_status_ = state;
 | 
			
		||||
    this->force_send_control_ = true;
 | 
			
		||||
    this->set_force_send_control_(true);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -140,24 +149,15 @@ bool HaierClimateBase::get_health_mode() const { return this->health_mode_; }
 | 
			
		||||
void HaierClimateBase::set_health_mode(bool state) {
 | 
			
		||||
  if (this->health_mode_ != state) {
 | 
			
		||||
    this->health_mode_ = state;
 | 
			
		||||
    this->force_send_control_ = true;
 | 
			
		||||
    this->set_force_send_control_(true);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::send_power_on_command() {
 | 
			
		||||
  this->action_request_ =
 | 
			
		||||
      PendingAction({ActionRequest::TURN_POWER_ON, esphome::optional<haier_protocol::HaierMessage>()});
 | 
			
		||||
}
 | 
			
		||||
void HaierClimateBase::send_power_on_command() { this->action_request_ = ActionRequest::TURN_POWER_ON; }
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::send_power_off_command() {
 | 
			
		||||
  this->action_request_ =
 | 
			
		||||
      PendingAction({ActionRequest::TURN_POWER_OFF, esphome::optional<haier_protocol::HaierMessage>()});
 | 
			
		||||
}
 | 
			
		||||
void HaierClimateBase::send_power_off_command() { this->action_request_ = ActionRequest::TURN_POWER_OFF; }
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::toggle_power() {
 | 
			
		||||
  this->action_request_ =
 | 
			
		||||
      PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()});
 | 
			
		||||
}
 | 
			
		||||
void HaierClimateBase::toggle_power() { this->action_request_ = ActionRequest::TOGGLE_POWER; }
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
 | 
			
		||||
  this->traits_.set_supported_swing_modes(modes);
 | 
			
		||||
@@ -165,7 +165,9 @@ void HaierClimateBase::set_supported_swing_modes(const std::set<climate::Climate
 | 
			
		||||
    this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); }
 | 
			
		||||
void HaierClimateBase::set_answer_timeout(uint32_t timeout) {
 | 
			
		||||
  this->answer_timeout_ = std::chrono::milliseconds(timeout);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) {
 | 
			
		||||
  this->traits_.set_supported_modes(modes);
 | 
			
		||||
@@ -181,42 +183,29 @@ 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::send_custom_command(const haier_protocol::HaierMessage &message) {
 | 
			
		||||
  this->action_request_ = PendingAction({ActionRequest::SEND_CUSTOM_COMMAND, message});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 HaierClimateBase::answer_preprocess_(uint8_t request_message_type,
 | 
			
		||||
                                                                  uint8_t expected_request_message_type,
 | 
			
		||||
                                                                  uint8_t answer_message_type,
 | 
			
		||||
                                                                  uint8_t expected_answer_message_type,
 | 
			
		||||
                                                                  ProtocolPhases expected_phase) {
 | 
			
		||||
  haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK;
 | 
			
		||||
  if ((expected_request_message_type != haier_protocol::FrameType::UNKNOWN_FRAME_TYPE) &&
 | 
			
		||||
      (request_message_type != expected_request_message_type))
 | 
			
		||||
  if ((expected_request_message_type != NO_COMMAND) && (request_message_type != expected_request_message_type))
 | 
			
		||||
    result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
 | 
			
		||||
  if ((expected_answer_message_type != haier_protocol::FrameType::UNKNOWN_FRAME_TYPE) &&
 | 
			
		||||
      (answer_message_type != expected_answer_message_type))
 | 
			
		||||
  if ((expected_answer_message_type != NO_COMMAND) && (answer_message_type != expected_answer_message_type))
 | 
			
		||||
    result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
 | 
			
		||||
  if (!this->haier_protocol_.is_waiting_for_answer() ||
 | 
			
		||||
      ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_)))
 | 
			
		||||
  if ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_))
 | 
			
		||||
    result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
 | 
			
		||||
  if (answer_message_type == haier_protocol::FrameType::INVALID)
 | 
			
		||||
  if (is_message_invalid(answer_message_type))
 | 
			
		||||
    result = haier_protocol::HandlerError::INVALID_ANSWER;
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HaierClimateBase::report_network_status_answer_handler_(
 | 
			
		||||
    haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data,
 | 
			
		||||
    size_t data_size) {
 | 
			
		||||
  haier_protocol::HandlerError result =
 | 
			
		||||
      this->answer_preprocess_(request_type, haier_protocol::FrameType::REPORT_NETWORK_STATUS, message_type,
 | 
			
		||||
                               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_));
 | 
			
		||||
haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(uint8_t request_type) {
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
  ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", request_type, phase_to_string_(this->protocol_phase_));
 | 
			
		||||
#else
 | 
			
		||||
  ESP_LOGW(TAG, "Answer timeout for command %02X, phase %d", request_type, (int) this->protocol_phase_);
 | 
			
		||||
#endif
 | 
			
		||||
  if (this->protocol_phase_ > ProtocolPhases::IDLE) {
 | 
			
		||||
    this->set_phase(ProtocolPhases::IDLE);
 | 
			
		||||
  } else {
 | 
			
		||||
@@ -230,95 +219,79 @@ void HaierClimateBase::setup() {
 | 
			
		||||
  // Set timestamp here to give AC time to boot
 | 
			
		||||
  this->last_request_timestamp_ = std::chrono::steady_clock::now();
 | 
			
		||||
  this->set_phase(ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
  this->set_handlers();
 | 
			
		||||
  this->haier_protocol_.set_default_timeout_handler(
 | 
			
		||||
      std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1));
 | 
			
		||||
  this->set_handlers();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::dump_config() {
 | 
			
		||||
  LOG_CLIMATE("", "Haier Climate", this);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Device communication status: %s", this->valid_connection() ? "established" : "none");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Device communication status: %s",
 | 
			
		||||
                (this->protocol_phase_ >= ProtocolPhases::IDLE) ? "established" : "none");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::loop() {
 | 
			
		||||
  std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
 | 
			
		||||
  if ((std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_valid_status_timestamp_).count() >
 | 
			
		||||
       COMMUNICATION_TIMEOUT_MS) ||
 | 
			
		||||
      (this->reset_protocol_request_ && (!this->haier_protocol_.is_waiting_for_answer()))) {
 | 
			
		||||
    this->last_valid_status_timestamp_ = now;
 | 
			
		||||
      (this->reset_protocol_request_)) {
 | 
			
		||||
    if (this->protocol_phase_ >= ProtocolPhases::IDLE) {
 | 
			
		||||
      // No status too long, reseting protocol
 | 
			
		||||
      // No need to reset protocol if we didn't pass initialization phase
 | 
			
		||||
      if (this->reset_protocol_request_) {
 | 
			
		||||
        this->reset_protocol_request_ = false;
 | 
			
		||||
        ESP_LOGW(TAG, "Protocol reset requested");
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGW(TAG, "Communication timeout, reseting protocol");
 | 
			
		||||
      }
 | 
			
		||||
      this->process_protocol_reset();
 | 
			
		||||
      this->last_valid_status_timestamp_ = now;
 | 
			
		||||
      this->set_force_send_control_(false);
 | 
			
		||||
      if (this->hvac_settings_.valid)
 | 
			
		||||
        this->hvac_settings_.reset();
 | 
			
		||||
      this->set_phase(ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
      return;
 | 
			
		||||
    } else {
 | 
			
		||||
      // No need to reset protocol if we didn't pass initialization phase
 | 
			
		||||
      this->last_valid_status_timestamp_ = now;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  if ((!this->haier_protocol_.is_waiting_for_answer()) &&
 | 
			
		||||
      ((this->protocol_phase_ == ProtocolPhases::IDLE) ||
 | 
			
		||||
       (this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) ||
 | 
			
		||||
       (this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) ||
 | 
			
		||||
       (this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL))) {
 | 
			
		||||
  if ((this->protocol_phase_ == ProtocolPhases::IDLE) ||
 | 
			
		||||
      (this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) ||
 | 
			
		||||
      (this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) ||
 | 
			
		||||
      (this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL)) {
 | 
			
		||||
    // If control message or action is pending we should send it ASAP unless we are in initialisation
 | 
			
		||||
    // procedure or waiting for an answer
 | 
			
		||||
    if (this->action_request_.has_value() && this->prepare_pending_action()) {
 | 
			
		||||
      this->set_phase(ProtocolPhases::SENDING_ACTION_COMMAND);
 | 
			
		||||
    } else if (this->next_hvac_settings_.valid || this->force_send_control_) {
 | 
			
		||||
    if (this->action_request_ != ActionRequest::NO_ACTION) {
 | 
			
		||||
      this->process_pending_action();
 | 
			
		||||
    } else if (this->hvac_settings_.valid || this->force_send_control_) {
 | 
			
		||||
      ESP_LOGV(TAG, "Control packet is pending...");
 | 
			
		||||
      this->set_phase(ProtocolPhases::SENDING_CONTROL);
 | 
			
		||||
      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->haier_protocol_.loop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::process_protocol_reset() {
 | 
			
		||||
  this->force_send_control_ = false;
 | 
			
		||||
  if (this->current_hvac_settings_.valid)
 | 
			
		||||
    this->current_hvac_settings_.reset();
 | 
			
		||||
  if (this->next_hvac_settings_.valid)
 | 
			
		||||
    this->next_hvac_settings_.reset();
 | 
			
		||||
  this->mode = CLIMATE_MODE_OFF;
 | 
			
		||||
  this->current_temperature = NAN;
 | 
			
		||||
  this->target_temperature = NAN;
 | 
			
		||||
  this->fan_mode.reset();
 | 
			
		||||
  this->preset.reset();
 | 
			
		||||
  this->publish_state();
 | 
			
		||||
  this->set_phase(ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HaierClimateBase::prepare_pending_action() {
 | 
			
		||||
  if (this->action_request_.has_value()) {
 | 
			
		||||
    switch (this->action_request_.value().action) {
 | 
			
		||||
      case ActionRequest::SEND_CUSTOM_COMMAND:
 | 
			
		||||
        return true;
 | 
			
		||||
      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;
 | 
			
		||||
void HaierClimateBase::process_pending_action() {
 | 
			
		||||
  ActionRequest request = this->action_request_;
 | 
			
		||||
  if (this->action_request_ == ActionRequest::TOGGLE_POWER) {
 | 
			
		||||
    request = this->mode == CLIMATE_MODE_OFF ? ActionRequest::TURN_POWER_ON : ActionRequest::TURN_POWER_OFF;
 | 
			
		||||
  }
 | 
			
		||||
  switch (request) {
 | 
			
		||||
    case ActionRequest::TURN_POWER_ON:
 | 
			
		||||
      this->set_phase(ProtocolPhases::SENDING_POWER_ON_COMMAND);
 | 
			
		||||
      break;
 | 
			
		||||
    case ActionRequest::TURN_POWER_OFF:
 | 
			
		||||
      this->set_phase(ProtocolPhases::SENDING_POWER_OFF_COMMAND);
 | 
			
		||||
      break;
 | 
			
		||||
    case ActionRequest::TOGGLE_POWER:
 | 
			
		||||
    case ActionRequest::NO_ACTION:
 | 
			
		||||
      // shouldn't get here, do nothing
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_);
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  this->action_request_ = ActionRequest::NO_ACTION;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateTraits HaierClimateBase::traits() { return traits_; }
 | 
			
		||||
@@ -329,22 +302,23 @@ void HaierClimateBase::control(const ClimateCall &call) {
 | 
			
		||||
    ESP_LOGW(TAG, "Can't send control packet, first poll answer not received");
 | 
			
		||||
    return;  // cancel the control, we cant do it without a poll answer.
 | 
			
		||||
  }
 | 
			
		||||
  if (this->current_hvac_settings_.valid) {
 | 
			
		||||
    ESP_LOGW(TAG, "New settings come faster then processed!");
 | 
			
		||||
  if (this->hvac_settings_.valid) {
 | 
			
		||||
    ESP_LOGW(TAG, "Overriding old valid settings before they were applied!");
 | 
			
		||||
  }
 | 
			
		||||
  {
 | 
			
		||||
    if (call.get_mode().has_value())
 | 
			
		||||
      this->next_hvac_settings_.mode = call.get_mode();
 | 
			
		||||
      this->hvac_settings_.mode = call.get_mode();
 | 
			
		||||
    if (call.get_fan_mode().has_value())
 | 
			
		||||
      this->next_hvac_settings_.fan_mode = call.get_fan_mode();
 | 
			
		||||
      this->hvac_settings_.fan_mode = call.get_fan_mode();
 | 
			
		||||
    if (call.get_swing_mode().has_value())
 | 
			
		||||
      this->next_hvac_settings_.swing_mode = call.get_swing_mode();
 | 
			
		||||
      this->hvac_settings_.swing_mode = call.get_swing_mode();
 | 
			
		||||
    if (call.get_target_temperature().has_value())
 | 
			
		||||
      this->next_hvac_settings_.target_temperature = call.get_target_temperature();
 | 
			
		||||
      this->hvac_settings_.target_temperature = call.get_target_temperature();
 | 
			
		||||
    if (call.get_preset().has_value())
 | 
			
		||||
      this->next_hvac_settings_.preset = call.get_preset();
 | 
			
		||||
    this->next_hvac_settings_.valid = true;
 | 
			
		||||
      this->hvac_settings_.preset = call.get_preset();
 | 
			
		||||
    this->hvac_settings_.valid = true;
 | 
			
		||||
  }
 | 
			
		||||
  this->first_control_attempt_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::HvacSettings::reset() {
 | 
			
		||||
@@ -356,9 +330,19 @@ void HaierClimateBase::HvacSettings::reset() {
 | 
			
		||||
  this->preset.reset();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats,
 | 
			
		||||
                                     std::chrono::milliseconds interval) {
 | 
			
		||||
  this->haier_protocol_.send_message(command, use_crc, num_repeats, interval);
 | 
			
		||||
void HaierClimateBase::set_force_send_control_(bool status) {
 | 
			
		||||
  this->force_send_control_ = status;
 | 
			
		||||
  if (status) {
 | 
			
		||||
    this->first_control_attempt_ = true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc) {
 | 
			
		||||
  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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ namespace esphome {
 | 
			
		||||
namespace haier {
 | 
			
		||||
 | 
			
		||||
enum class ActionRequest : uint8_t {
 | 
			
		||||
  SEND_CUSTOM_COMMAND = 0,
 | 
			
		||||
  NO_ACTION = 0,
 | 
			
		||||
  TURN_POWER_ON = 1,
 | 
			
		||||
  TURN_POWER_OFF = 2,
 | 
			
		||||
  TOGGLE_POWER = 3,
 | 
			
		||||
@@ -33,6 +33,7 @@ class HaierClimateBase : public esphome::Component,
 | 
			
		||||
  void control(const esphome::climate::ClimateCall &call) override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; }
 | 
			
		||||
  void set_fahrenheit(bool fahrenheit);
 | 
			
		||||
  void set_display_state(bool state);
 | 
			
		||||
  bool get_display_state() const;
 | 
			
		||||
  void set_health_mode(bool state);
 | 
			
		||||
@@ -44,7 +45,6 @@ class HaierClimateBase : public esphome::Component,
 | 
			
		||||
  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_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 read_array(uint8_t *data, size_t len) noexcept override {
 | 
			
		||||
    return esphome::uart::UARTDevice::read_array(data, len) ? len : 0;
 | 
			
		||||
@@ -55,56 +55,63 @@ class HaierClimateBase : public esphome::Component,
 | 
			
		||||
  bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; };
 | 
			
		||||
  void set_answer_timeout(uint32_t timeout);
 | 
			
		||||
  void set_send_wifi(bool send_wifi);
 | 
			
		||||
  void send_custom_command(const haier_protocol::HaierMessage &message);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  enum class ProtocolPhases {
 | 
			
		||||
    UNKNOWN = -1,
 | 
			
		||||
    // INITIALIZATION
 | 
			
		||||
    SENDING_INIT_1 = 0,
 | 
			
		||||
    SENDING_INIT_2,
 | 
			
		||||
    SENDING_FIRST_STATUS_REQUEST,
 | 
			
		||||
    SENDING_ALARM_STATUS_REQUEST,
 | 
			
		||||
    WAITING_INIT_1_ANSWER = 1,
 | 
			
		||||
    SENDING_INIT_2 = 2,
 | 
			
		||||
    WAITING_INIT_2_ANSWER = 3,
 | 
			
		||||
    SENDING_FIRST_STATUS_REQUEST = 4,
 | 
			
		||||
    WAITING_FIRST_STATUS_ANSWER = 5,
 | 
			
		||||
    SENDING_ALARM_STATUS_REQUEST = 6,
 | 
			
		||||
    WAITING_ALARM_STATUS_ANSWER = 7,
 | 
			
		||||
    // FUNCTIONAL STATE
 | 
			
		||||
    IDLE,
 | 
			
		||||
    SENDING_STATUS_REQUEST,
 | 
			
		||||
    SENDING_UPDATE_SIGNAL_REQUEST,
 | 
			
		||||
    SENDING_SIGNAL_LEVEL,
 | 
			
		||||
    SENDING_CONTROL,
 | 
			
		||||
    SENDING_ACTION_COMMAND,
 | 
			
		||||
    IDLE = 8,
 | 
			
		||||
    SENDING_STATUS_REQUEST = 10,
 | 
			
		||||
    WAITING_STATUS_ANSWER = 11,
 | 
			
		||||
    SENDING_UPDATE_SIGNAL_REQUEST = 12,
 | 
			
		||||
    WAITING_UPDATE_SIGNAL_ANSWER = 13,
 | 
			
		||||
    SENDING_SIGNAL_LEVEL = 14,
 | 
			
		||||
    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
 | 
			
		||||
  };
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
  const char *phase_to_string_(ProtocolPhases phase);
 | 
			
		||||
#endif
 | 
			
		||||
  virtual void set_handlers() = 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_power_message(bool state) = 0;
 | 
			
		||||
  virtual bool prepare_pending_action();
 | 
			
		||||
  virtual void process_protocol_reset();
 | 
			
		||||
  virtual bool is_message_invalid(uint8_t message_type) = 0;
 | 
			
		||||
  virtual void process_pending_action();
 | 
			
		||||
  esphome::climate::ClimateTraits traits() override;
 | 
			
		||||
  // Answer handlers
 | 
			
		||||
  haier_protocol::HandlerError 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,
 | 
			
		||||
  // Answers handlers
 | 
			
		||||
  haier_protocol::HandlerError answer_preprocess_(uint8_t request_message_type, uint8_t expected_request_message_type,
 | 
			
		||||
                                                  uint8_t answer_message_type, uint8_t expected_answer_message_type,
 | 
			
		||||
                                                  ProtocolPhases expected_phase);
 | 
			
		||||
  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
 | 
			
		||||
  haier_protocol::HandlerError timeout_default_handler_(haier_protocol::FrameType request_type);
 | 
			
		||||
  haier_protocol::HandlerError timeout_default_handler_(uint8_t request_type);
 | 
			
		||||
  // Helper functions
 | 
			
		||||
  void send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats = 0,
 | 
			
		||||
                     std::chrono::milliseconds interval = std::chrono::milliseconds::zero());
 | 
			
		||||
  void set_force_send_control_(bool status);
 | 
			
		||||
  void send_message_(const haier_protocol::HaierMessage &command, bool use_crc);
 | 
			
		||||
  virtual void set_phase(ProtocolPhases phase);
 | 
			
		||||
  void reset_phase_();
 | 
			
		||||
  void reset_to_idle_();
 | 
			
		||||
  bool check_timeout_(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint,
 | 
			
		||||
                      size_t timeout);
 | 
			
		||||
  bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now);
 | 
			
		||||
  bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now);
 | 
			
		||||
  bool is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now);
 | 
			
		||||
  bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now);
 | 
			
		||||
  bool is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now);
 | 
			
		||||
#ifdef USE_WIFI
 | 
			
		||||
  haier_protocol::HaierMessage get_wifi_signal_message_();
 | 
			
		||||
  haier_protocol::HaierMessage get_wifi_signal_message_(uint8_t message_type);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  struct HvacSettings {
 | 
			
		||||
@@ -115,34 +122,29 @@ class HaierClimateBase : public esphome::Component,
 | 
			
		||||
    esphome::optional<esphome::climate::ClimatePreset> preset;
 | 
			
		||||
    bool valid;
 | 
			
		||||
    HvacSettings() : valid(false){};
 | 
			
		||||
    HvacSettings(const HvacSettings &) = default;
 | 
			
		||||
    HvacSettings &operator=(const HvacSettings &) = default;
 | 
			
		||||
    void reset();
 | 
			
		||||
  };
 | 
			
		||||
  struct PendingAction {
 | 
			
		||||
    ActionRequest action;
 | 
			
		||||
    esphome::optional<haier_protocol::HaierMessage> message;
 | 
			
		||||
  };
 | 
			
		||||
  haier_protocol::ProtocolHandler haier_protocol_;
 | 
			
		||||
  ProtocolPhases protocol_phase_;
 | 
			
		||||
  esphome::optional<PendingAction> action_request_;
 | 
			
		||||
  ActionRequest action_request_;
 | 
			
		||||
  uint8_t fan_mode_speed_;
 | 
			
		||||
  uint8_t other_modes_fan_speed_;
 | 
			
		||||
  bool display_status_;
 | 
			
		||||
  bool health_mode_;
 | 
			
		||||
  bool force_send_control_;
 | 
			
		||||
  bool forced_publish_;
 | 
			
		||||
  bool forced_request_status_;
 | 
			
		||||
  bool first_control_attempt_;
 | 
			
		||||
  bool reset_protocol_request_;
 | 
			
		||||
  bool send_wifi_signal_;
 | 
			
		||||
  bool use_crc_;
 | 
			
		||||
  esphome::climate::ClimateTraits traits_;
 | 
			
		||||
  HvacSettings current_hvac_settings_;
 | 
			
		||||
  HvacSettings next_hvac_settings_;
 | 
			
		||||
  std::unique_ptr<uint8_t[]> last_status_message_;
 | 
			
		||||
  HvacSettings hvac_settings_;
 | 
			
		||||
  std::chrono::steady_clock::time_point last_request_timestamp_;       // For interval between messages
 | 
			
		||||
  std::chrono::steady_clock::time_point last_valid_status_timestamp_;  // For protocol timeout
 | 
			
		||||
  std::chrono::steady_clock::time_point last_status_request_;          // To request AC status
 | 
			
		||||
  std::chrono::steady_clock::time_point last_signal_request_;          // To send WiFI signal level
 | 
			
		||||
  std::chrono::steady_clock::time_point control_request_timestamp_;    // To send control message
 | 
			
		||||
  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
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,6 @@ namespace haier {
 | 
			
		||||
static const char *const TAG = "haier.climate";
 | 
			
		||||
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
 | 
			
		||||
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
 | 
			
		||||
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) {
 | 
			
		||||
  switch (direction) {
 | 
			
		||||
@@ -50,11 +48,14 @@ hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDir
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
HonClimate::HonClimate()
 | 
			
		||||
    : cleaning_status_(CleaningState::NO_CLEANING),
 | 
			
		||||
    : last_status_message_(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]),
 | 
			
		||||
      cleaning_status_(CleaningState::NO_CLEANING),
 | 
			
		||||
      got_valid_outdoor_temp_(false),
 | 
			
		||||
      hvac_hardware_info_available_(false),
 | 
			
		||||
      hvac_functions_{false, false, false, false, false},
 | 
			
		||||
      use_crc_(hvac_functions_[2]),
 | 
			
		||||
      active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
 | 
			
		||||
      outdoor_sensor_(nullptr) {
 | 
			
		||||
  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->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
 | 
			
		||||
}
 | 
			
		||||
@@ -71,14 +72,14 @@ AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this-
 | 
			
		||||
 | 
			
		||||
void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) {
 | 
			
		||||
  this->vertical_direction_ = direction;
 | 
			
		||||
  this->force_send_control_ = true;
 | 
			
		||||
  this->set_force_send_control_(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; }
 | 
			
		||||
 | 
			
		||||
void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) {
 | 
			
		||||
  this->horizontal_direction_ = direction;
 | 
			
		||||
  this->force_send_control_ = true;
 | 
			
		||||
  this->set_force_send_control_(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string HonClimate::get_cleaning_status_text() const {
 | 
			
		||||
@@ -97,35 +98,35 @@ CleaningState HonClimate::get_cleaning_status() const { return this->cleaning_st
 | 
			
		||||
void HonClimate::start_self_cleaning() {
 | 
			
		||||
  if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
 | 
			
		||||
    ESP_LOGI(TAG, "Sending self cleaning start request");
 | 
			
		||||
    this->action_request_ =
 | 
			
		||||
        PendingAction({ActionRequest::START_SELF_CLEAN, esphome::optional<haier_protocol::HaierMessage>()});
 | 
			
		||||
    this->action_request_ = ActionRequest::START_SELF_CLEAN;
 | 
			
		||||
    this->set_force_send_control_(true);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HonClimate::start_steri_cleaning() {
 | 
			
		||||
  if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
 | 
			
		||||
    ESP_LOGI(TAG, "Sending steri cleaning start request");
 | 
			
		||||
    this->action_request_ =
 | 
			
		||||
        PendingAction({ActionRequest::START_STERI_CLEAN, esphome::optional<haier_protocol::HaierMessage>()});
 | 
			
		||||
    this->action_request_ = ActionRequest::START_STERI_CLEAN;
 | 
			
		||||
    this->set_force_send_control_(true);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type,
 | 
			
		||||
                                                                            haier_protocol::FrameType message_type,
 | 
			
		||||
haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type,
 | 
			
		||||
                                                                            const uint8_t *data, size_t data_size) {
 | 
			
		||||
  // Should check this before preprocess
 | 
			
		||||
  if (message_type == haier_protocol::FrameType::INVALID) {
 | 
			
		||||
  if (message_type == (uint8_t) hon_protocol::FrameType::INVALID) {
 | 
			
		||||
    ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the smartAir2 "
 | 
			
		||||
                  "protocol instead of hOn");
 | 
			
		||||
    this->set_phase(ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
    return haier_protocol::HandlerError::INVALID_ANSWER;
 | 
			
		||||
  }
 | 
			
		||||
  haier_protocol::HandlerError result =
 | 
			
		||||
      this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_VERSION, message_type,
 | 
			
		||||
                               haier_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
  haier_protocol::HandlerError result = this->answer_preprocess_(
 | 
			
		||||
      request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type,
 | 
			
		||||
      (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_INIT_1_ANSWER);
 | 
			
		||||
  if (result == haier_protocol::HandlerError::HANDLER_OK) {
 | 
			
		||||
    if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) {
 | 
			
		||||
      // Wrong structure
 | 
			
		||||
      this->set_phase(ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
      return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
 | 
			
		||||
    }
 | 
			
		||||
    // All OK
 | 
			
		||||
@@ -133,57 +134,54 @@ haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haie
 | 
			
		||||
    char tmp[9];
 | 
			
		||||
    tmp[8] = 0;
 | 
			
		||||
    strncpy(tmp, answr->protocol_version, 8);
 | 
			
		||||
    this->hvac_hardware_info_ = HardwareInfo();
 | 
			
		||||
    this->hvac_hardware_info_.value().protocol_version_ = std::string(tmp);
 | 
			
		||||
    this->hvac_protocol_version_ = std::string(tmp);
 | 
			
		||||
    strncpy(tmp, answr->software_version, 8);
 | 
			
		||||
    this->hvac_hardware_info_.value().software_version_ = std::string(tmp);
 | 
			
		||||
    this->hvac_software_version_ = std::string(tmp);
 | 
			
		||||
    strncpy(tmp, answr->hardware_version, 8);
 | 
			
		||||
    this->hvac_hardware_info_.value().hardware_version_ = std::string(tmp);
 | 
			
		||||
    this->hvac_hardware_version_ = std::string(tmp);
 | 
			
		||||
    strncpy(tmp, answr->device_name, 8);
 | 
			
		||||
    this->hvac_hardware_info_.value().device_name_ = std::string(tmp);
 | 
			
		||||
    this->hvac_hardware_info_.value().functions_[0] = (answr->functions[1] & 0x01) != 0;  // interactive mode support
 | 
			
		||||
    this->hvac_hardware_info_.value().functions_[1] =
 | 
			
		||||
        (answr->functions[1] & 0x02) != 0;  // controller-device mode support
 | 
			
		||||
    this->hvac_hardware_info_.value().functions_[2] = (answr->functions[1] & 0x04) != 0;  // crc support
 | 
			
		||||
    this->hvac_hardware_info_.value().functions_[3] = (answr->functions[1] & 0x08) != 0;  // multiple AC support
 | 
			
		||||
    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->hvac_device_name_ = std::string(tmp);
 | 
			
		||||
    this->hvac_functions_[0] = (answr->functions[1] & 0x01) != 0;  // interactive mode support
 | 
			
		||||
    this->hvac_functions_[1] = (answr->functions[1] & 0x02) != 0;  // controller-device mode support
 | 
			
		||||
    this->hvac_functions_[2] = (answr->functions[1] & 0x04) != 0;  // crc support
 | 
			
		||||
    this->hvac_functions_[3] = (answr->functions[1] & 0x08) != 0;  // multiple AC support
 | 
			
		||||
    this->hvac_functions_[4] = (answr->functions[1] & 0x20) != 0;  // roles support
 | 
			
		||||
    this->hvac_hardware_info_available_ = true;
 | 
			
		||||
    this->set_phase(ProtocolPhases::SENDING_INIT_2);
 | 
			
		||||
    return result;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->reset_phase_();
 | 
			
		||||
    this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
 | 
			
		||||
                                                                    : ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(haier_protocol::FrameType request_type,
 | 
			
		||||
                                                                       haier_protocol::FrameType message_type,
 | 
			
		||||
haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type,
 | 
			
		||||
                                                                       const uint8_t *data, size_t data_size) {
 | 
			
		||||
  haier_protocol::HandlerError result =
 | 
			
		||||
      this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_ID, message_type,
 | 
			
		||||
                               haier_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::SENDING_INIT_2);
 | 
			
		||||
  haier_protocol::HandlerError result = this->answer_preprocess_(
 | 
			
		||||
      request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID, message_type,
 | 
			
		||||
      (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_INIT_2_ANSWER);
 | 
			
		||||
  if (result == haier_protocol::HandlerError::HANDLER_OK) {
 | 
			
		||||
    this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
 | 
			
		||||
    return result;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->reset_phase_();
 | 
			
		||||
    this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
 | 
			
		||||
                                                                    : ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameType request_type,
 | 
			
		||||
                                                         haier_protocol::FrameType message_type, const uint8_t *data,
 | 
			
		||||
                                                         size_t data_size) {
 | 
			
		||||
haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, uint8_t message_type,
 | 
			
		||||
                                                         const uint8_t *data, size_t data_size) {
 | 
			
		||||
  haier_protocol::HandlerError result =
 | 
			
		||||
      this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type,
 | 
			
		||||
                               haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
 | 
			
		||||
      this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::CONTROL, message_type,
 | 
			
		||||
                               (uint8_t) hon_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
 | 
			
		||||
  if (result == haier_protocol::HandlerError::HANDLER_OK) {
 | 
			
		||||
    result = this->process_status_message_(data, data_size);
 | 
			
		||||
    if (result != haier_protocol::HandlerError::HANDLER_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result);
 | 
			
		||||
      this->reset_phase_();
 | 
			
		||||
      this->action_request_.reset();
 | 
			
		||||
      this->force_send_control_ = false;
 | 
			
		||||
      this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
 | 
			
		||||
                                                                      : ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
    } else {
 | 
			
		||||
      if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) {
 | 
			
		||||
        memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl));
 | 
			
		||||
@@ -191,48 +189,36 @@ haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameTy
 | 
			
		||||
        ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size,
 | 
			
		||||
                 sizeof(hon_protocol::HaierPacketControl));
 | 
			
		||||
      }
 | 
			
		||||
      switch (this->protocol_phase_) {
 | 
			
		||||
        case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
 | 
			
		||||
          ESP_LOGI(TAG, "First HVAC status received");
 | 
			
		||||
          this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
 | 
			
		||||
          break;
 | 
			
		||||
        case ProtocolPhases::SENDING_ACTION_COMMAND:
 | 
			
		||||
          // Do nothing, phase will be changed in process_phase
 | 
			
		||||
          break;
 | 
			
		||||
        case ProtocolPhases::SENDING_STATUS_REQUEST:
 | 
			
		||||
          this->set_phase(ProtocolPhases::IDLE);
 | 
			
		||||
          break;
 | 
			
		||||
        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;
 | 
			
		||||
      if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) {
 | 
			
		||||
        ESP_LOGI(TAG, "First HVAC status received");
 | 
			
		||||
        this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
 | 
			
		||||
      } else if ((this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) ||
 | 
			
		||||
                 (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_ON_ANSWER) ||
 | 
			
		||||
                 (this->protocol_phase_ == ProtocolPhases::WAITING_POWER_OFF_ANSWER)) {
 | 
			
		||||
        this->set_phase(ProtocolPhases::IDLE);
 | 
			
		||||
      } else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) {
 | 
			
		||||
        this->set_phase(ProtocolPhases::IDLE);
 | 
			
		||||
        this->set_force_send_control_(false);
 | 
			
		||||
        if (this->hvac_settings_.valid)
 | 
			
		||||
          this->hvac_settings_.reset();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return result;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->action_request_.reset();
 | 
			
		||||
    this->force_send_control_ = false;
 | 
			
		||||
    this->reset_phase_();
 | 
			
		||||
    this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
 | 
			
		||||
                                                                    : ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(
 | 
			
		||||
    haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data,
 | 
			
		||||
    size_t data_size) {
 | 
			
		||||
  haier_protocol::HandlerError result = this->answer_preprocess_(
 | 
			
		||||
      request_type, haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, message_type,
 | 
			
		||||
      haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
 | 
			
		||||
haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(uint8_t request_type,
 | 
			
		||||
                                                                                    uint8_t message_type,
 | 
			
		||||
                                                                                    const uint8_t *data,
 | 
			
		||||
                                                                                    size_t data_size) {
 | 
			
		||||
  haier_protocol::HandlerError result =
 | 
			
		||||
      this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION,
 | 
			
		||||
                               message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE,
 | 
			
		||||
                               ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
 | 
			
		||||
  if (result == haier_protocol::HandlerError::HANDLER_OK) {
 | 
			
		||||
    this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL);
 | 
			
		||||
    return result;
 | 
			
		||||
@@ -242,16 +228,25 @@ haier_protocol::HandlerError HonClimate::get_management_information_answer_handl
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_protocol::FrameType request_type,
 | 
			
		||||
                                                                          haier_protocol::FrameType message_type,
 | 
			
		||||
haier_protocol::HandlerError HonClimate::report_network_status_answer_handler_(uint8_t request_type,
 | 
			
		||||
                                                                               uint8_t message_type,
 | 
			
		||||
                                                                               const uint8_t *data, size_t data_size) {
 | 
			
		||||
  haier_protocol::HandlerError result =
 | 
			
		||||
      this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS, message_type,
 | 
			
		||||
                               (uint8_t) hon_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
 | 
			
		||||
  this->set_phase(ProtocolPhases::IDLE);
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type,
 | 
			
		||||
                                                                          const uint8_t *data, size_t data_size) {
 | 
			
		||||
  if (request_type == haier_protocol::FrameType::GET_ALARM_STATUS) {
 | 
			
		||||
    if (message_type != haier_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) {
 | 
			
		||||
  if (request_type == (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS) {
 | 
			
		||||
    if (message_type != (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) {
 | 
			
		||||
      // Unexpected answer to request
 | 
			
		||||
      this->set_phase(ProtocolPhases::IDLE);
 | 
			
		||||
      return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) {
 | 
			
		||||
    if (this->protocol_phase_ != ProtocolPhases::WAITING_ALARM_STATUS_ANSWER) {
 | 
			
		||||
      // Don't expect this answer now
 | 
			
		||||
      this->set_phase(ProtocolPhases::IDLE);
 | 
			
		||||
      return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
 | 
			
		||||
@@ -268,27 +263,27 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_
 | 
			
		||||
void HonClimate::set_handlers() {
 | 
			
		||||
  // Set handlers
 | 
			
		||||
  this->haier_protocol_.set_answer_handler(
 | 
			
		||||
      haier_protocol::FrameType::GET_DEVICE_VERSION,
 | 
			
		||||
      (uint8_t) (hon_protocol::FrameType::GET_DEVICE_VERSION),
 | 
			
		||||
      std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
 | 
			
		||||
                std::placeholders::_3, std::placeholders::_4));
 | 
			
		||||
  this->haier_protocol_.set_answer_handler(
 | 
			
		||||
      haier_protocol::FrameType::GET_DEVICE_ID,
 | 
			
		||||
      (uint8_t) (hon_protocol::FrameType::GET_DEVICE_ID),
 | 
			
		||||
      std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
 | 
			
		||||
                std::placeholders::_3, std::placeholders::_4));
 | 
			
		||||
  this->haier_protocol_.set_answer_handler(
 | 
			
		||||
      haier_protocol::FrameType::CONTROL,
 | 
			
		||||
      (uint8_t) (hon_protocol::FrameType::CONTROL),
 | 
			
		||||
      std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3,
 | 
			
		||||
                std::placeholders::_4));
 | 
			
		||||
  this->haier_protocol_.set_answer_handler(
 | 
			
		||||
      haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION,
 | 
			
		||||
      (uint8_t) (hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION),
 | 
			
		||||
      std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1,
 | 
			
		||||
                std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
 | 
			
		||||
  this->haier_protocol_.set_answer_handler(
 | 
			
		||||
      haier_protocol::FrameType::GET_ALARM_STATUS,
 | 
			
		||||
      (uint8_t) (hon_protocol::FrameType::GET_ALARM_STATUS),
 | 
			
		||||
      std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
 | 
			
		||||
                std::placeholders::_3, std::placeholders::_4));
 | 
			
		||||
  this->haier_protocol_.set_answer_handler(
 | 
			
		||||
      haier_protocol::FrameType::REPORT_NETWORK_STATUS,
 | 
			
		||||
      (uint8_t) (hon_protocol::FrameType::REPORT_NETWORK_STATUS),
 | 
			
		||||
      std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
 | 
			
		||||
                std::placeholders::_3, std::placeholders::_4));
 | 
			
		||||
}
 | 
			
		||||
@@ -296,18 +291,14 @@ void HonClimate::set_handlers() {
 | 
			
		||||
void HonClimate::dump_config() {
 | 
			
		||||
  HaierClimateBase::dump_config();
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Protocol version: hOn");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Control method: %d", (uint8_t) this->control_method_);
 | 
			
		||||
  if (this->hvac_hardware_info_.has_value()) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Device protocol version: %s", this->hvac_hardware_info_.value().protocol_version_.c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Device software version: %s", this->hvac_hardware_info_.value().software_version_.c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Device hardware version: %s", this->hvac_hardware_info_.value().hardware_version_.c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Device name: %s", this->hvac_hardware_info_.value().device_name_.c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Device features:%s%s%s%s%s",
 | 
			
		||||
                  (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" : ""));
 | 
			
		||||
  if (this->hvac_hardware_info_available_) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Device protocol version: %s", this->hvac_protocol_version_.c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Device software version: %s", this->hvac_software_version_.c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Device hardware version: %s", this->hvac_hardware_version_.c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Device name: %s", this->hvac_device_name_.c_str());
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Device features:%s%s%s%s%s", (this->hvac_functions_[0] ? " interactive" : ""),
 | 
			
		||||
                  (this->hvac_functions_[1] ? " controller-device" : ""), (this->hvac_functions_[2] ? " crc" : ""),
 | 
			
		||||
                  (this->hvac_functions_[3] ? " multinode" : ""), (this->hvac_functions_[4] ? " role" : ""));
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -316,6 +307,7 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
  switch (this->protocol_phase_) {
 | 
			
		||||
    case ProtocolPhases::SENDING_INIT_1:
 | 
			
		||||
      if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) {
 | 
			
		||||
        this->hvac_hardware_info_available_ = false;
 | 
			
		||||
        // Indicate device capabilities:
 | 
			
		||||
        // bit 0 - if 1 module support interactive mode
 | 
			
		||||
        // bit 1 - if 1 module support controller-device mode
 | 
			
		||||
@@ -324,95 +316,109 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
        // bit 4..bit 15 - not used
 | 
			
		||||
        uint8_t module_capabilities[2] = {0b00000000, 0b00000111};
 | 
			
		||||
        static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST(
 | 
			
		||||
            haier_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities));
 | 
			
		||||
            (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities));
 | 
			
		||||
        this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_);
 | 
			
		||||
        this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_INIT_2:
 | 
			
		||||
      if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
 | 
			
		||||
        static const haier_protocol::HaierMessage DEVICEID_REQUEST(haier_protocol::FrameType::GET_DEVICE_ID);
 | 
			
		||||
        static const haier_protocol::HaierMessage DEVICEID_REQUEST((uint8_t) hon_protocol::FrameType::GET_DEVICE_ID);
 | 
			
		||||
        this->send_message_(DEVICEID_REQUEST, this->use_crc_);
 | 
			
		||||
        this->set_phase(ProtocolPhases::WAITING_INIT_2_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
 | 
			
		||||
    case ProtocolPhases::SENDING_STATUS_REQUEST:
 | 
			
		||||
      if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
 | 
			
		||||
        static const haier_protocol::HaierMessage STATUS_REQUEST(
 | 
			
		||||
            haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA);
 | 
			
		||||
            (uint8_t) hon_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA);
 | 
			
		||||
        this->send_message_(STATUS_REQUEST, this->use_crc_);
 | 
			
		||||
        this->last_status_request_ = now;
 | 
			
		||||
        this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1));
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
#ifdef USE_WIFI
 | 
			
		||||
    case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
 | 
			
		||||
      if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
 | 
			
		||||
        static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST(
 | 
			
		||||
            haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION);
 | 
			
		||||
            (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION);
 | 
			
		||||
        this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_);
 | 
			
		||||
        this->last_signal_request_ = now;
 | 
			
		||||
        this->set_phase(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_SIGNAL_LEVEL:
 | 
			
		||||
      if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
 | 
			
		||||
        this->send_message_(this->get_wifi_signal_message_(), this->use_crc_);
 | 
			
		||||
        this->send_message_(this->get_wifi_signal_message_((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS),
 | 
			
		||||
                            this->use_crc_);
 | 
			
		||||
        this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
 | 
			
		||||
    case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
 | 
			
		||||
      break;
 | 
			
		||||
#else
 | 
			
		||||
    case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
 | 
			
		||||
    case ProtocolPhases::SENDING_SIGNAL_LEVEL:
 | 
			
		||||
    case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
 | 
			
		||||
    case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
 | 
			
		||||
      this->set_phase(ProtocolPhases::IDLE);
 | 
			
		||||
      break;
 | 
			
		||||
#endif
 | 
			
		||||
    case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
 | 
			
		||||
      if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
 | 
			
		||||
        static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(haier_protocol::FrameType::GET_ALARM_STATUS);
 | 
			
		||||
        static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(
 | 
			
		||||
            (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS);
 | 
			
		||||
        this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_);
 | 
			
		||||
        this->set_phase(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_CONTROL:
 | 
			
		||||
      if (this->control_messages_queue_.empty()) {
 | 
			
		||||
        switch (this->control_method_) {
 | 
			
		||||
          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->first_control_attempt_) {
 | 
			
		||||
        this->control_request_timestamp_ = now;
 | 
			
		||||
        this->first_control_attempt_ = false;
 | 
			
		||||
      }
 | 
			
		||||
      if (this->control_messages_queue_.empty()) {
 | 
			
		||||
        ESP_LOGW(TAG, "Control message queue is empty!");
 | 
			
		||||
        this->reset_to_idle_();
 | 
			
		||||
      if (this->is_control_message_timeout_exceeded_(now)) {
 | 
			
		||||
        ESP_LOGW(TAG, "Sending control packet timeout!");
 | 
			
		||||
        this->set_force_send_control_(false);
 | 
			
		||||
        if (this->hvac_settings_.valid)
 | 
			
		||||
          this->hvac_settings_.reset();
 | 
			
		||||
        this->forced_request_status_ = true;
 | 
			
		||||
        this->forced_publish_ = true;
 | 
			
		||||
        this->set_phase(ProtocolPhases::IDLE);
 | 
			
		||||
      } else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) {
 | 
			
		||||
        ESP_LOGI(TAG, "Sending control packet, queue size %d", this->control_messages_queue_.size());
 | 
			
		||||
        this->send_message_(this->control_messages_queue_.front(), this->use_crc_, CONTROL_MESSAGE_RETRIES,
 | 
			
		||||
                            CONTROL_MESSAGE_RETRIES_INTERVAL);
 | 
			
		||||
        haier_protocol::HaierMessage control_message = get_control_message();
 | 
			
		||||
        this->send_message_(control_message, this->use_crc_);
 | 
			
		||||
        ESP_LOGI(TAG, "Control packet sent");
 | 
			
		||||
        this->set_phase(ProtocolPhases::WAITING_CONTROL_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_ACTION_COMMAND:
 | 
			
		||||
      if (this->action_request_.has_value()) {
 | 
			
		||||
        if (this->action_request_.value().message.has_value()) {
 | 
			
		||||
          this->send_message_(this->action_request_.value().message.value(), this->use_crc_);
 | 
			
		||||
          this->action_request_.value().message.reset();
 | 
			
		||||
        } 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);
 | 
			
		||||
    case ProtocolPhases::SENDING_POWER_ON_COMMAND:
 | 
			
		||||
    case ProtocolPhases::SENDING_POWER_OFF_COMMAND:
 | 
			
		||||
      if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
 | 
			
		||||
        uint8_t pwr_cmd_buf[2] = {0x00, 0x00};
 | 
			
		||||
        if (this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND)
 | 
			
		||||
          pwr_cmd_buf[1] = 0x01;
 | 
			
		||||
        haier_protocol::HaierMessage power_cmd((uint8_t) hon_protocol::FrameType::CONTROL,
 | 
			
		||||
                                               ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1,
 | 
			
		||||
                                               pwr_cmd_buf, sizeof(pwr_cmd_buf));
 | 
			
		||||
        this->send_message_(power_cmd, this->use_crc_);
 | 
			
		||||
        this->set_phase(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND
 | 
			
		||||
                            ? ProtocolPhases::WAITING_POWER_ON_ANSWER
 | 
			
		||||
                            : ProtocolPhases::WAITING_POWER_OFF_ANSWER);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case ProtocolPhases::WAITING_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: {
 | 
			
		||||
      if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
 | 
			
		||||
        this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST);
 | 
			
		||||
@@ -427,35 +433,26 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
    } break;
 | 
			
		||||
    default:
 | 
			
		||||
      // Shouldn't get here
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
      ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
 | 
			
		||||
               phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_);
 | 
			
		||||
#else
 | 
			
		||||
      ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_);
 | 
			
		||||
#endif
 | 
			
		||||
      this->set_phase(ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HaierMessage HonClimate::get_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() {
 | 
			
		||||
  uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
 | 
			
		||||
  memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
 | 
			
		||||
  hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
 | 
			
		||||
  bool has_hvac_settings = false;
 | 
			
		||||
  if (this->current_hvac_settings_.valid) {
 | 
			
		||||
  if (this->hvac_settings_.valid) {
 | 
			
		||||
    has_hvac_settings = true;
 | 
			
		||||
    HvacSettings &climate_control = this->current_hvac_settings_;
 | 
			
		||||
    HvacSettings climate_control;
 | 
			
		||||
    climate_control = this->hvac_settings_;
 | 
			
		||||
    if (climate_control.mode.has_value()) {
 | 
			
		||||
      switch (climate_control.mode.value()) {
 | 
			
		||||
        case CLIMATE_MODE_OFF:
 | 
			
		||||
@@ -538,7 +535,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
 | 
			
		||||
    }
 | 
			
		||||
    if (climate_control.target_temperature.has_value()) {
 | 
			
		||||
      float target_temp = climate_control.target_temperature.value();
 | 
			
		||||
      out_data->set_point = ((int) target_temp) - 16;  // set the temperature with offset 16
 | 
			
		||||
      out_data->set_point = ((int) target_temp) - 16;  // set the temperature at our offset, subtract 16.
 | 
			
		||||
      out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0;
 | 
			
		||||
    }
 | 
			
		||||
    if (out_data->ac_power == 0) {
 | 
			
		||||
@@ -590,28 +587,50 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
 | 
			
		||||
  control_out_buffer[4] = 0;  // This byte should be cleared before setting values
 | 
			
		||||
  out_data->display_status = this->display_status_ ? 1 : 0;
 | 
			
		||||
  out_data->health_mode = this->health_mode_ ? 1 : 0;
 | 
			
		||||
  return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
 | 
			
		||||
  switch (this->action_request_) {
 | 
			
		||||
    case ActionRequest::START_SELF_CLEAN:
 | 
			
		||||
      this->action_request_ = ActionRequest::NO_ACTION;
 | 
			
		||||
      out_data->self_cleaning_status = 1;
 | 
			
		||||
      out_data->steri_clean = 0;
 | 
			
		||||
      out_data->set_point = 0x06;
 | 
			
		||||
      out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
 | 
			
		||||
      out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
 | 
			
		||||
      out_data->ac_power = 1;
 | 
			
		||||
      out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
 | 
			
		||||
      out_data->light_status = 0;
 | 
			
		||||
      break;
 | 
			
		||||
    case ActionRequest::START_STERI_CLEAN:
 | 
			
		||||
      this->action_request_ = ActionRequest::NO_ACTION;
 | 
			
		||||
      out_data->self_cleaning_status = 0;
 | 
			
		||||
      out_data->steri_clean = 1;
 | 
			
		||||
      out_data->set_point = 0x06;
 | 
			
		||||
      out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
 | 
			
		||||
      out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
 | 
			
		||||
      out_data->ac_power = 1;
 | 
			
		||||
      out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
 | 
			
		||||
      out_data->light_status = 0;
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      // No change
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  return haier_protocol::HaierMessage((uint8_t) hon_protocol::FrameType::CONTROL,
 | 
			
		||||
                                      (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS,
 | 
			
		||||
                                      control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
 | 
			
		||||
  if (size < hon_protocol::HAIER_STATUS_FRAME_SIZE + this->extra_control_packet_bytes_)
 | 
			
		||||
  if (size < sizeof(hon_protocol::HaierStatus))
 | 
			
		||||
    return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
 | 
			
		||||
  struct {
 | 
			
		||||
    hon_protocol::HaierPacketControl control;
 | 
			
		||||
    hon_protocol::HaierPacketSensors sensors;
 | 
			
		||||
  } 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));
 | 
			
		||||
  hon_protocol::HaierStatus packet;
 | 
			
		||||
  if (size < sizeof(hon_protocol::HaierStatus))
 | 
			
		||||
    size = sizeof(hon_protocol::HaierStatus);
 | 
			
		||||
  memcpy(&packet, packet_buffer, size);
 | 
			
		||||
  if (packet.sensors.error_status != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status);
 | 
			
		||||
  }
 | 
			
		||||
  if ((this->outdoor_sensor_ != nullptr) &&
 | 
			
		||||
      (this->got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) {
 | 
			
		||||
    this->got_valid_outdoor_temp_ = true;
 | 
			
		||||
  if ((this->outdoor_sensor_ != nullptr) && (got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) {
 | 
			
		||||
    got_valid_outdoor_temp_ = true;
 | 
			
		||||
    float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET);
 | 
			
		||||
    if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp))
 | 
			
		||||
      this->outdoor_sensor_->publish_state(otemp);
 | 
			
		||||
@@ -684,7 +703,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
 | 
			
		||||
        // Do something only if display status changed
 | 
			
		||||
        if (this->mode == CLIMATE_MODE_OFF) {
 | 
			
		||||
          // AC just turned on from remote need to turn off display
 | 
			
		||||
          this->force_send_control_ = true;
 | 
			
		||||
          this->set_force_send_control_(true);
 | 
			
		||||
        } else {
 | 
			
		||||
          this->display_status_ = disp_status;
 | 
			
		||||
        }
 | 
			
		||||
@@ -713,8 +732,7 @@ 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);
 | 
			
		||||
      if (new_cleaning == CleaningState::NO_CLEANING) {
 | 
			
		||||
        // Turning AC off after cleaning
 | 
			
		||||
        this->action_request_ =
 | 
			
		||||
            PendingAction({ActionRequest::TURN_POWER_OFF, esphome::optional<haier_protocol::HaierMessage>()});
 | 
			
		||||
        this->action_request_ = ActionRequest::TURN_POWER_OFF;
 | 
			
		||||
      }
 | 
			
		||||
      this->cleaning_status_ = new_cleaning;
 | 
			
		||||
    }
 | 
			
		||||
@@ -765,257 +783,51 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
 | 
			
		||||
    should_publish = should_publish || (old_swing_mode != this->swing_mode);
 | 
			
		||||
  }
 | 
			
		||||
  this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
 | 
			
		||||
  if (should_publish) {
 | 
			
		||||
  if (this->forced_publish_ || should_publish) {
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
    std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now();
 | 
			
		||||
#endif
 | 
			
		||||
    this->publish_state();
 | 
			
		||||
#if (HAIER_LOG_LEVEL > 4)
 | 
			
		||||
    ESP_LOGV(TAG, "Publish delay: %lld ms",
 | 
			
		||||
             std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() -
 | 
			
		||||
                                                                   _publish_start)
 | 
			
		||||
                 .count());
 | 
			
		||||
#endif
 | 
			
		||||
    this->forced_publish_ = false;
 | 
			
		||||
  }
 | 
			
		||||
  if (should_publish) {
 | 
			
		||||
    ESP_LOGI(TAG, "HVAC values changed");
 | 
			
		||||
  }
 | 
			
		||||
  int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG;
 | 
			
		||||
  esp_log_printf_(log_level, TAG, __LINE__, "HVAC Mode = 0x%X", packet.control.ac_mode);
 | 
			
		||||
  esp_log_printf_(log_level, TAG, __LINE__, "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_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_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__,
 | 
			
		||||
                  "HVAC Mode = 0x%X", packet.control.ac_mode);
 | 
			
		||||
  esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
 | 
			
		||||
                  "Fan speed Status = 0x%X", packet.control.fan_mode);
 | 
			
		||||
  esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
 | 
			
		||||
                  "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode);
 | 
			
		||||
  esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
 | 
			
		||||
                  "Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode);
 | 
			
		||||
  esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
 | 
			
		||||
                  "Set Point Status = 0x%X", packet.control.set_point);
 | 
			
		||||
  return haier_protocol::HandlerError::HANDLER_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HonClimate::fill_control_messages_queue_() {
 | 
			
		||||
  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));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
bool HonClimate::is_message_invalid(uint8_t message_type) {
 | 
			
		||||
  return message_type == (uint8_t) hon_protocol::FrameType::INVALID;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HonClimate::clear_control_messages_queue_() {
 | 
			
		||||
  while (!this->control_messages_queue_.empty())
 | 
			
		||||
    this->control_messages_queue_.pop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HonClimate::prepare_pending_action() {
 | 
			
		||||
  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;
 | 
			
		||||
void HonClimate::process_pending_action() {
 | 
			
		||||
  switch (this->action_request_) {
 | 
			
		||||
    case ActionRequest::START_SELF_CLEAN:
 | 
			
		||||
    case ActionRequest::START_STERI_CLEAN:
 | 
			
		||||
      // Will reset action with control message sending
 | 
			
		||||
      this->set_phase(ProtocolPhases::SENDING_CONTROL);
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      return HaierClimateBase::prepare_pending_action();
 | 
			
		||||
      HaierClimateBase::process_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 esphome
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user