mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-03 08:31:47 +00:00 
			
		
		
		
	Compare commits
	
		
			314 Commits
		
	
	
		
			2025.4.0b2
			...
			2025.5.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					f74f89c6b5 | ||
| 
						 | 
					42390faf4a | ||
| 
						 | 
					fdc6c4a219 | ||
| 
						 | 
					6c08f5e343 | ||
| 
						 | 
					e0e4ba9592 | ||
| 
						 | 
					ad20825f31 | ||
| 
						 | 
					e4f3a952d5 | ||
| 
						 | 
					90e3c5bba2 | ||
| 
						 | 
					b1d5ad27f3 | ||
| 
						 | 
					5c54f75b7a | ||
| 
						 | 
					a5f85b4437 | ||
| 
						 | 
					da4e710249 | ||
| 
						 | 
					cdcd1cd292 | ||
| 
						 | 
					1cba22175f | ||
| 
						 | 
					801138da27 | ||
| 
						 | 
					51740a2e99 | ||
| 
						 | 
					d68a391e67 | ||
| 
						 | 
					756aa13779 | ||
| 
						 | 
					25bbc0c221 | ||
| 
						 | 
					220a14e1f8 | ||
| 
						 | 
					ac74b25c46 | ||
| 
						 | 
					937fe393a1 | ||
| 
						 | 
					4b552d9fba | ||
| 
						 | 
					aa53d8f1ee | ||
| 
						 | 
					a28932bc29 | ||
| 
						 | 
					afa7414ee1 | ||
| 
						 | 
					aed7ef481e | ||
| 
						 | 
					c820fee1f6 | ||
| 
						 | 
					5244ac4ff6 | ||
| 
						 | 
					89d283eee4 | ||
| 
						 | 
					ef053d23b4 | ||
| 
						 | 
					98470d32f0 | ||
| 
						 | 
					cab6edd800 | ||
| 
						 | 
					aaaf9b2b62 | ||
| 
						 | 
					38cfd32382 | ||
| 
						 | 
					1b9ae57b9d | ||
| 
						 | 
					4d54cb9b31 | ||
| 
						 | 
					15d0b4355e | ||
| 
						 | 
					316fe2f06c | ||
| 
						 | 
					f8681adec4 | ||
| 
						 | 
					868f5ff20c | ||
| 
						 | 
					59295a615e | ||
| 
						 | 
					d8516cfabb | ||
| 
						 | 
					d847b345b8 | ||
| 
						 | 
					c50e33f531 | ||
| 
						 | 
					5a84bab9ec | ||
| 
						 | 
					41f860c2a3 | ||
| 
						 | 
					c7e62d1279 | ||
| 
						 | 
					2341ff651a | ||
| 
						 | 
					9704de6647 | ||
| 
						 | 
					97fb8c2cdf | ||
| 
						 | 
					d9839f3a5c | ||
| 
						 | 
					498e3904a9 | ||
| 
						 | 
					7cb01bf842 | ||
| 
						 | 
					c050e8d0fb | ||
| 
						 | 
					4f2643e6e9 | ||
| 
						 | 
					7d0262dd1a | ||
| 
						 | 
					c30ffd0098 | ||
| 
						 | 
					ea31122979 | ||
| 
						 | 
					1e20440c8e | ||
| 
						 | 
					0630244195 | ||
| 
						 | 
					183659f527 | ||
| 
						 | 
					4ea63af796 | ||
| 
						 | 
					0aa7911b1b | ||
| 
						 | 
					032949bc77 | ||
| 
						 | 
					6f8ee65919 | ||
| 
						 | 
					c5654b4cb2 | ||
| 
						 | 
					410b6353fe | ||
| 
						 | 
					a36e1aab8e | ||
| 
						 | 
					864ae7a56c | ||
| 
						 | 
					2560d2b9d0 | ||
| 
						 | 
					0cf9b05afd | ||
| 
						 | 
					8b65d1673a | ||
| 
						 | 
					5e164b107a | ||
| 
						 | 
					a83959d738 | ||
| 
						 | 
					0ccc5bf714 | ||
| 
						 | 
					bc0956019b | ||
| 
						 | 
					49f631d6c5 | ||
| 
						 | 
					a9d5eb8470 | ||
| 
						 | 
					7c0546c9f0 | ||
| 
						 | 
					f4eb75e4e0 | ||
| 
						 | 
					5b2c19bc86 | ||
| 
						 | 
					185b84b8b2 | ||
| 
						 | 
					facf94699e | ||
| 
						 | 
					58104229e2 | ||
| 
						 | 
					50c88b7aa7 | ||
| 
						 | 
					81bae96109 | ||
| 
						 | 
					a3ed090594 | ||
| 
						 | 
					cff1820772 | ||
| 
						 | 
					bdd2774544 | ||
| 
						 | 
					38790793dd | ||
| 
						 | 
					dcd786d21c | ||
| 
						 | 
					71e88fe9b2 | ||
| 
						 | 
					11dcaf7383 | ||
| 
						 | 
					dded81d622 | ||
| 
						 | 
					8324b3244c | ||
| 
						 | 
					401c090edd | ||
| 
						 | 
					8757957e17 | ||
| 
						 | 
					e2c8a5b638 | ||
| 
						 | 
					7bb899bfa1 | ||
| 
						 | 
					3e2359ddff | ||
| 
						 | 
					04147a7f27 | ||
| 
						 | 
					cae3c030d2 | ||
| 
						 | 
					d7c615ec43 | ||
| 
						 | 
					1294e8ccd5 | ||
| 
						 | 
					37a2cb07d1 | ||
| 
						 | 
					2af3994f79 | ||
| 
						 | 
					0c0fe81814 | ||
| 
						 | 
					82c8614315 | ||
| 
						 | 
					a85dc65038 | ||
| 
						 | 
					290b8bdca0 | ||
| 
						 | 
					a96ed0b70a | ||
| 
						 | 
					cdc1a7c646 | ||
| 
						 | 
					7f59aff157 | ||
| 
						 | 
					cdce59f7f9 | ||
| 
						 | 
					ff1c3cb52e | ||
| 
						 | 
					bec9d91419 | ||
| 
						 | 
					8399d894c1 | ||
| 
						 | 
					e1732c4945 | ||
| 
						 | 
					ca221d6cb2 | ||
| 
						 | 
					8a90ce882a | ||
| 
						 | 
					b3400a1308 | ||
| 
						 | 
					23fb1bed61 | ||
| 
						 | 
					2b3757dff8 | ||
| 
						 | 
					1da8e99d27 | ||
| 
						 | 
					e94e71ded8 | ||
| 
						 | 
					00f20c1e55 | ||
| 
						 | 
					45d019a7e4 | ||
| 
						 | 
					8465017db9 | ||
| 
						 | 
					782d748210 | ||
| 
						 | 
					b01d85a974 | ||
| 
						 | 
					797a4c61f2 | ||
| 
						 | 
					8e29437900 | ||
| 
						 | 
					9e64e71cdf | ||
| 
						 | 
					ef2621aa54 | ||
| 
						 | 
					882273cb56 | ||
| 
						 | 
					ad2b74d9b4 | ||
| 
						 | 
					26669bd1b6 | ||
| 
						 | 
					54ead9a6b4 | ||
| 
						 | 
					d60e1f02c0 | ||
| 
						 | 
					213648564c | ||
| 
						 | 
					8bdbde9732 | ||
| 
						 | 
					e988762576 | ||
| 
						 | 
					75496849eb | ||
| 
						 | 
					39b119e9cc | ||
| 
						 | 
					4d43caf6c1 | ||
| 
						 | 
					ce5e1a6294 | ||
| 
						 | 
					88be14aaa3 | ||
| 
						 | 
					1ac56b06c5 | ||
| 
						 | 
					8bbc509b0b | ||
| 
						 | 
					6f35d0ac88 | ||
| 
						 | 
					3b8a5db97c | ||
| 
						 | 
					b8d83d0765 | ||
| 
						 | 
					e7a2b395fd | ||
| 
						 | 
					ad99d7fb45 | ||
| 
						 | 
					0b032e5c19 | ||
| 
						 | 
					c7523ace78 | ||
| 
						 | 
					2a6827e1d2 | ||
| 
						 | 
					125aff79ec | ||
| 
						 | 
					a31d8ec309 | ||
| 
						 | 
					3ed03edfec | ||
| 
						 | 
					4dc6cbe2d7 | ||
| 
						 | 
					524cd4b4e3 | ||
| 
						 | 
					84ebbf0762 | ||
| 
						 | 
					670ad7192c | ||
| 
						 | 
					bc6ee20270 | ||
| 
						 | 
					e869a3aec3 | ||
| 
						 | 
					8aff6d2fdd | ||
| 
						 | 
					8d33c6de36 | ||
| 
						 | 
					f4b5f32cb4 | ||
| 
						 | 
					2eb9582d0f | ||
| 
						 | 
					db97440b04 | ||
| 
						 | 
					ced7ae1d7a | ||
| 
						 | 
					d6699fa3c0 | ||
| 
						 | 
					836e5ffa43 | ||
| 
						 | 
					c7f597bc75 | ||
| 
						 | 
					e215fafebe | ||
| 
						 | 
					da9c755f67 | ||
| 
						 | 
					087ff865a7 | ||
| 
						 | 
					8cd62c0308 | ||
| 
						 | 
					f5241ff777 | ||
| 
						 | 
					1aa2b79311 | ||
| 
						 | 
					2dca2d5f85 | ||
| 
						 | 
					f03b42ced5 | ||
| 
						 | 
					0f8a0af244 | ||
| 
						 | 
					62646f5f32 | ||
| 
						 | 
					71f81d2f18 | ||
| 
						 | 
					4ec8414050 | ||
| 
						 | 
					807925fd38 | ||
| 
						 | 
					b597565165 | ||
| 
						 | 
					9a9b91b180 | ||
| 
						 | 
					9dcf295df8 | ||
| 
						 | 
					e8a3de2642 | ||
| 
						 | 
					d2b4dba51f | ||
| 
						 | 
					bf527b0331 | ||
| 
						 | 
					cdc77506de | ||
| 
						 | 
					6de6a0c82c | ||
| 
						 | 
					20062576a3 | ||
| 
						 | 
					07ba9fdf8f | ||
| 
						 | 
					caa255f5d1 | ||
| 
						 | 
					c0be2c14f3 | ||
| 
						 | 
					9f629dcaa2 | ||
| 
						 | 
					0fe6c65ba3 | ||
| 
						 | 
					c756bb3b3e | ||
| 
						 | 
					ecb91b0101 | ||
| 
						 | 
					5f9a509bdc | ||
| 
						 | 
					dc6dd9fe0d | ||
| 
						 | 
					5baa034d0d | ||
| 
						 | 
					b8ba26787e | ||
| 
						 | 
					844569e96b | ||
| 
						 | 
					43580739ac | ||
| 
						 | 
					c9f7ab6948 | ||
| 
						 | 
					7900660bb8 | ||
| 
						 | 
					f096567ac7 | ||
| 
						 | 
					5bfb5ccc34 | ||
| 
						 | 
					1c60038111 | ||
| 
						 | 
					b940db6549 | ||
| 
						 | 
					aa6e172e14 | ||
| 
						 | 
					86033b6612 | ||
| 
						 | 
					59b4a1f554 | ||
| 
						 | 
					b5bdfb3089 | ||
| 
						 | 
					a31a5e74bd | ||
| 
						 | 
					629481a526 | ||
| 
						 | 
					3291a11824 | ||
| 
						 | 
					d2ee2d3b23 | ||
| 
						 | 
					253e3ec6f6 | ||
| 
						 | 
					fdc4ec8a57 | ||
| 
						 | 
					1da0dff8b1 | ||
| 
						 | 
					38dae8489e | ||
| 
						 | 
					22c0e1079e | ||
| 
						 | 
					2d3f141140 | ||
| 
						 | 
					e49252ca3d | ||
| 
						 | 
					c9d1476ae0 | ||
| 
						 | 
					ee646d7324 | ||
| 
						 | 
					e557bca420 | ||
| 
						 | 
					adcd6517db | ||
| 
						 | 
					4c8f5275f9 | ||
| 
						 | 
					526db0102c | ||
| 
						 | 
					8a3fe9ce4c | ||
| 
						 | 
					fb97ef33a8 | ||
| 
						 | 
					805a6d85a5 | ||
| 
						 | 
					8f9fbb15b8 | ||
| 
						 | 
					3d24dea455 | ||
| 
						 | 
					666d5374ea | ||
| 
						 | 
					6792ff6d58 | ||
| 
						 | 
					f29ccb9e75 | ||
| 
						 | 
					911bd54765 | ||
| 
						 | 
					89b1b12993 | ||
| 
						 | 
					33d79e03d9 | ||
| 
						 | 
					991f3d3a10 | ||
| 
						 | 
					97823ddd16 | ||
| 
						 | 
					6ff180152a | ||
| 
						 | 
					dbb7cbed3e | ||
| 
						 | 
					fbf00f0af4 | ||
| 
						 | 
					82c6a40371 | ||
| 
						 | 
					0242ac56df | ||
| 
						 | 
					b82666002d | ||
| 
						 | 
					e11883e431 | ||
| 
						 | 
					ff5b9df607 | ||
| 
						 | 
					e5b7e3039a | ||
| 
						 | 
					31ed1eb6f0 | ||
| 
						 | 
					0c3daab649 | ||
| 
						 | 
					816371e3e9 | ||
| 
						 | 
					3c7bb65a23 | ||
| 
						 | 
					4a65fd76b3 | ||
| 
						 | 
					2704db5eef | ||
| 
						 | 
					f10bc73d31 | ||
| 
						 | 
					55e099450c | ||
| 
						 | 
					248dbd32a5 | ||
| 
						 | 
					a7b676231a | ||
| 
						 | 
					2fd5f9ac58 | ||
| 
						 | 
					ca4838a5f4 | ||
| 
						 | 
					1b72550236 | ||
| 
						 | 
					71afd49e3e | ||
| 
						 | 
					e5d718d1b1 | ||
| 
						 | 
					61f33d6401 | ||
| 
						 | 
					af9b568778 | ||
| 
						 | 
					4a1eec567f | ||
| 
						 | 
					5706b8476f | ||
| 
						 | 
					8981a86793 | ||
| 
						 | 
					a7fd6dc382 | ||
| 
						 | 
					cb0a87c1f9 | ||
| 
						 | 
					b913a0b178 | ||
| 
						 | 
					214454ff51 | ||
| 
						 | 
					3677ef71d1 | ||
| 
						 | 
					7e133171e0 | ||
| 
						 | 
					bc56d319b5 | ||
| 
						 | 
					c423a6fb61 | ||
| 
						 | 
					4034bf4f04 | ||
| 
						 | 
					477abc05ae | ||
| 
						 | 
					ff2b93a3e4 | ||
| 
						 | 
					a52d6388a9 | ||
| 
						 | 
					6259ca9ded | ||
| 
						 | 
					f6ef50505b | ||
| 
						 | 
					3c242b7296 | ||
| 
						 | 
					00dd5b8339 | ||
| 
						 | 
					a007a8237a | ||
| 
						 | 
					9b86cc37f0 | ||
| 
						 | 
					2dfcf950fa | ||
| 
						 | 
					5908b93e82 | ||
| 
						 | 
					995db1f961 | ||
| 
						 | 
					abcc656a6f | ||
| 
						 | 
					4a9f323d92 | ||
| 
						 | 
					34a4e70cc5 | ||
| 
						 | 
					fb5d697c22 | ||
| 
						 | 
					df4642208e | ||
| 
						 | 
					264e234efc | ||
| 
						 | 
					ca78dd44b5 | ||
| 
						 | 
					7edf458898 | ||
| 
						 | 
					d9873e24a7 | ||
| 
						 | 
					645bd490ba | ||
| 
						 | 
					27f6d00e7a | ||
| 
						 | 
					f9d668eeca | ||
| 
						 | 
					6b930595e2 | 
@@ -114,4 +114,5 @@ config/
 | 
			
		||||
examples/
 | 
			
		||||
Dockerfile
 | 
			
		||||
.git/
 | 
			
		||||
tests/build/
 | 
			
		||||
tests/
 | 
			
		||||
.*
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										33
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
								
							@@ -1,15 +1,11 @@
 | 
			
		||||
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"
 | 
			
		||||
  build_type:
 | 
			
		||||
    description: "Build type"
 | 
			
		||||
    required: true
 | 
			
		||||
    example: "docker"
 | 
			
		||||
  suffix:
 | 
			
		||||
@@ -19,6 +15,11 @@ inputs:
 | 
			
		||||
    description: "Version to build"
 | 
			
		||||
    required: true
 | 
			
		||||
    example: "2023.12.0"
 | 
			
		||||
  base_os:
 | 
			
		||||
    description: "Base OS to use"
 | 
			
		||||
    required: false
 | 
			
		||||
    default: "debian"
 | 
			
		||||
    example: "debian"
 | 
			
		||||
runs:
 | 
			
		||||
  using: "composite"
 | 
			
		||||
  steps:
 | 
			
		||||
@@ -46,52 +47,52 @@ runs:
 | 
			
		||||
 | 
			
		||||
    - name: Build and push to ghcr by digest
 | 
			
		||||
      id: build-ghcr
 | 
			
		||||
      uses: docker/build-push-action@v6.15.0
 | 
			
		||||
      uses: docker/build-push-action@v6.17.0
 | 
			
		||||
      env:
 | 
			
		||||
        DOCKER_BUILD_SUMMARY: false
 | 
			
		||||
        DOCKER_BUILD_RECORD_UPLOAD: false
 | 
			
		||||
      with:
 | 
			
		||||
        context: .
 | 
			
		||||
        file: ./docker/Dockerfile
 | 
			
		||||
        platforms: ${{ inputs.platform }}
 | 
			
		||||
        target: ${{ inputs.target }}
 | 
			
		||||
        cache-from: type=gha
 | 
			
		||||
        cache-to: ${{ steps.cache-to.outputs.value }}
 | 
			
		||||
        build-args: |
 | 
			
		||||
          BASEIMGTYPE=${{ inputs.baseimg }}
 | 
			
		||||
          BUILD_TYPE=${{ inputs.build_type }}
 | 
			
		||||
          BUILD_VERSION=${{ inputs.version }}
 | 
			
		||||
          BUILD_OS=${{ inputs.base_os }}
 | 
			
		||||
        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
 | 
			
		||||
        mkdir -p /tmp/digests/${{ inputs.build_type }}/ghcr
 | 
			
		||||
        digest="${{ steps.build-ghcr.outputs.digest }}"
 | 
			
		||||
        touch "/tmp/digests/${{ inputs.target }}/ghcr/${digest#sha256:}"
 | 
			
		||||
        touch "/tmp/digests/${{ inputs.build_type }}/ghcr/${digest#sha256:}"
 | 
			
		||||
 | 
			
		||||
    - name: Build and push to dockerhub by digest
 | 
			
		||||
      id: build-dockerhub
 | 
			
		||||
      uses: docker/build-push-action@v6.15.0
 | 
			
		||||
      uses: docker/build-push-action@v6.17.0
 | 
			
		||||
      env:
 | 
			
		||||
        DOCKER_BUILD_SUMMARY: false
 | 
			
		||||
        DOCKER_BUILD_RECORD_UPLOAD: false
 | 
			
		||||
      with:
 | 
			
		||||
        context: .
 | 
			
		||||
        file: ./docker/Dockerfile
 | 
			
		||||
        platforms: ${{ inputs.platform }}
 | 
			
		||||
        target: ${{ inputs.target }}
 | 
			
		||||
        cache-from: type=gha
 | 
			
		||||
        cache-to: ${{ steps.cache-to.outputs.value }}
 | 
			
		||||
        build-args: |
 | 
			
		||||
          BASEIMGTYPE=${{ inputs.baseimg }}
 | 
			
		||||
          BUILD_TYPE=${{ inputs.build_type }}
 | 
			
		||||
          BUILD_VERSION=${{ inputs.version }}
 | 
			
		||||
          BUILD_OS=${{ inputs.base_os }}
 | 
			
		||||
        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
 | 
			
		||||
        mkdir -p /tmp/digests/${{ inputs.build_type }}/dockerhub
 | 
			
		||||
        digest="${{ steps.build-dockerhub.outputs.digest }}"
 | 
			
		||||
        touch "/tmp/digests/${{ inputs.target }}/dockerhub/${digest#sha256:}"
 | 
			
		||||
        touch "/tmp/digests/${{ inputs.build_type }}/dockerhub/${digest#sha256:}"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.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.5.0
 | 
			
		||||
      uses: actions/setup-python@v5.6.0
 | 
			
		||||
      with:
 | 
			
		||||
        python-version: ${{ inputs.python-version }}
 | 
			
		||||
    - name: Restore Python virtual environment
 | 
			
		||||
@@ -34,7 +34,7 @@ runs:
 | 
			
		||||
        python -m venv venv
 | 
			
		||||
        source venv/bin/activate
 | 
			
		||||
        python --version
 | 
			
		||||
        pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
 | 
			
		||||
        pip install -r requirements.txt -r requirements_test.txt
 | 
			
		||||
        pip install -e .
 | 
			
		||||
    - name: Create Python virtual environment
 | 
			
		||||
      if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
 | 
			
		||||
@@ -43,5 +43,5 @@ runs:
 | 
			
		||||
        python -m venv venv
 | 
			
		||||
        ./venv/Scripts/activate
 | 
			
		||||
        python --version
 | 
			
		||||
        pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
 | 
			
		||||
        pip install -r requirements.txt -r requirements_test.txt
 | 
			
		||||
        pip install -e .
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							@@ -17,7 +17,6 @@ updates:
 | 
			
		||||
      docker-actions:
 | 
			
		||||
        applies-to: version-updates
 | 
			
		||||
        patterns:
 | 
			
		||||
          - "docker/setup-qemu-action"
 | 
			
		||||
          - "docker/login-action"
 | 
			
		||||
          - "docker/setup-buildx-action"
 | 
			
		||||
  - package-ecosystem: github-actions
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							@@ -23,7 +23,7 @@ jobs:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.5.0
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.11"
 | 
			
		||||
 | 
			
		||||
@@ -57,6 +57,17 @@ jobs:
 | 
			
		||||
              event: 'REQUEST_CHANGES',
 | 
			
		||||
              body: 'You have altered the generated proto files but they do not match what is expected.\nPlease run "script/api_protobuf/api_protobuf.py" and commit the changes.'
 | 
			
		||||
            })
 | 
			
		||||
      - if: failure()
 | 
			
		||||
        name: Show changes
 | 
			
		||||
        run: git diff
 | 
			
		||||
      - if: failure()
 | 
			
		||||
        name: Archive artifacts
 | 
			
		||||
        uses: actions/upload-artifact@v4.6.2
 | 
			
		||||
        with:
 | 
			
		||||
          name: generated-proto-files
 | 
			
		||||
          path: |
 | 
			
		||||
            esphome/components/api/api_pb2.*
 | 
			
		||||
            esphome/components/api/api_pb2_service.*
 | 
			
		||||
      - if: success()
 | 
			
		||||
        name: Dismiss review
 | 
			
		||||
        uses: actions/github-script@v7.0.1
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							@@ -37,12 +37,15 @@ jobs:
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        os: ["ubuntu-latest", "ubuntu-24.04-arm"]
 | 
			
		||||
        build_type: ["ha-addon", "docker", "lint"]
 | 
			
		||||
        os: ["ubuntu-24.04", "ubuntu-24.04-arm"]
 | 
			
		||||
        build_type:
 | 
			
		||||
          - "ha-addon"
 | 
			
		||||
          - "docker"
 | 
			
		||||
          # - "lint"
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.5.0
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.9"
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -39,10 +39,10 @@ jobs:
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Generate cache-key
 | 
			
		||||
        id: cache-key
 | 
			
		||||
        run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
 | 
			
		||||
        run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
 | 
			
		||||
      - name: Set up Python ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
        id: python
 | 
			
		||||
        uses: actions/setup-python@v5.5.0
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
@@ -58,7 +58,7 @@ jobs:
 | 
			
		||||
          python -m venv venv
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          python --version
 | 
			
		||||
          pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
 | 
			
		||||
          pip install -r requirements.txt -r requirements_test.txt
 | 
			
		||||
          pip install -e .
 | 
			
		||||
 | 
			
		||||
  ruff:
 | 
			
		||||
@@ -165,6 +165,7 @@ jobs:
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          script/ci-custom.py
 | 
			
		||||
          script/build_codeowners.py --check
 | 
			
		||||
          script/build_language_schema.py --check
 | 
			
		||||
 | 
			
		||||
  pytest:
 | 
			
		||||
    name: Run pytest
 | 
			
		||||
@@ -220,7 +221,7 @@ jobs:
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          pytest -vv --cov-report=xml --tb=native tests
 | 
			
		||||
      - name: Upload coverage to Codecov
 | 
			
		||||
        uses: codecov/codecov-action@v5
 | 
			
		||||
        uses: codecov/codecov-action@v5.4.2
 | 
			
		||||
        with:
 | 
			
		||||
          token: ${{ secrets.CODECOV_TOKEN }}
 | 
			
		||||
 | 
			
		||||
@@ -291,6 +292,11 @@ jobs:
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 IDF
 | 
			
		||||
            options: --environment esp32-idf-tidy --grep USE_ESP_IDF
 | 
			
		||||
            pio_cache_key: tidyesp32-idf
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ZEPHYR
 | 
			
		||||
            options: --environment nrf52-tidy --grep USE_ZEPHYR
 | 
			
		||||
            pio_cache_key: tidy-zephyr
 | 
			
		||||
            ignore_errors: true
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
@@ -330,13 +336,13 @@ jobs:
 | 
			
		||||
      - name: Run clang-tidy
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          script/clang-tidy --all-headers --fix ${{ matrix.options }}
 | 
			
		||||
          script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
 | 
			
		||||
        env:
 | 
			
		||||
          # Also cache libdeps, store them in a ~/.platformio subfolder
 | 
			
		||||
          PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
 | 
			
		||||
 | 
			
		||||
      - name: Suggested changes
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
        run: script/ci-suggest-changes ${{ matrix.ignore_errors && '|| true' || '' }}
 | 
			
		||||
        # yamllint disable-line rule:line-length
 | 
			
		||||
        if: always()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										106
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										106
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -18,6 +18,7 @@ jobs:
 | 
			
		||||
    outputs:
 | 
			
		||||
      tag: ${{ steps.tag.outputs.tag }}
 | 
			
		||||
      branch_build: ${{ steps.tag.outputs.branch_build }}
 | 
			
		||||
      deploy_env: ${{ steps.tag.outputs.deploy_env }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Get tag
 | 
			
		||||
@@ -27,6 +28,11 @@ jobs:
 | 
			
		||||
          if [[ "${{ github.event_name }}" = "release" ]]; then
 | 
			
		||||
            TAG="${{ github.event.release.tag_name}}"
 | 
			
		||||
            BRANCH_BUILD="false"
 | 
			
		||||
            if [[ "${{ github.event.release.prerelease }}" = "true" ]]; then
 | 
			
		||||
              ENVIRONMENT="beta"
 | 
			
		||||
            else
 | 
			
		||||
              ENVIRONMENT="production"
 | 
			
		||||
            fi
 | 
			
		||||
          else
 | 
			
		||||
            TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p")
 | 
			
		||||
            today="$(date --utc '+%Y%m%d')"
 | 
			
		||||
@@ -35,12 +41,15 @@ jobs:
 | 
			
		||||
            if [[ "$BRANCH" != "dev" ]]; then
 | 
			
		||||
              TAG="${TAG}-${BRANCH}"
 | 
			
		||||
              BRANCH_BUILD="true"
 | 
			
		||||
              ENVIRONMENT=""
 | 
			
		||||
            else
 | 
			
		||||
              BRANCH_BUILD="false"
 | 
			
		||||
              ENVIRONMENT="dev"
 | 
			
		||||
            fi
 | 
			
		||||
          fi
 | 
			
		||||
          echo "tag=${TAG}" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "branch_build=${BRANCH_BUILD}" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "deploy_env=${ENVIRONMENT}" >> $GITHUB_OUTPUT
 | 
			
		||||
        # yamllint enable rule:line-length
 | 
			
		||||
 | 
			
		||||
  deploy-pypi:
 | 
			
		||||
@@ -53,46 +62,44 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.5.0
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.x"
 | 
			
		||||
      - name: Set up python environment
 | 
			
		||||
        env:
 | 
			
		||||
          ESPHOME_NO_VENV: 1
 | 
			
		||||
        run: script/setup
 | 
			
		||||
      - name: Build
 | 
			
		||||
        run: |-
 | 
			
		||||
          pip3 install build
 | 
			
		||||
          python3 -m build
 | 
			
		||||
      - name: Publish
 | 
			
		||||
        uses: pypa/gh-action-pypi-publish@v1.12.4
 | 
			
		||||
        with:
 | 
			
		||||
          skip-existing: true
 | 
			
		||||
 | 
			
		||||
  deploy-docker:
 | 
			
		||||
    name: Build ESPHome ${{ matrix.platform }}
 | 
			
		||||
    name: Build ESPHome ${{ matrix.platform.arch }}
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    permissions:
 | 
			
		||||
      contents: read
 | 
			
		||||
      packages: write
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    runs-on: ${{ matrix.platform.os }}
 | 
			
		||||
    needs: [init]
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        platform:
 | 
			
		||||
          - linux/amd64
 | 
			
		||||
          - linux/arm64
 | 
			
		||||
          - arch: amd64
 | 
			
		||||
            os: "ubuntu-24.04"
 | 
			
		||||
          - arch: arm64
 | 
			
		||||
            os: "ubuntu-24.04-arm"
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.5.0
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.9"
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.10.0
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        if: matrix.platform != 'linux/amd64'
 | 
			
		||||
        uses: docker/setup-qemu-action@v3.6.0
 | 
			
		||||
 | 
			
		||||
      - name: Log in to docker hub
 | 
			
		||||
        uses: docker/login-action@v3.4.0
 | 
			
		||||
@@ -109,45 +116,36 @@ jobs:
 | 
			
		||||
      - name: Build docker
 | 
			
		||||
        uses: ./.github/actions/build-image
 | 
			
		||||
        with:
 | 
			
		||||
          platform: ${{ matrix.platform }}
 | 
			
		||||
          target: docker
 | 
			
		||||
          baseimg: docker
 | 
			
		||||
          target: final
 | 
			
		||||
          build_type: docker
 | 
			
		||||
          suffix: ""
 | 
			
		||||
          version: ${{ needs.init.outputs.tag }}
 | 
			
		||||
 | 
			
		||||
      - name: Build ha-addon
 | 
			
		||||
        uses: ./.github/actions/build-image
 | 
			
		||||
        with:
 | 
			
		||||
          platform: ${{ matrix.platform }}
 | 
			
		||||
          target: hassio
 | 
			
		||||
          baseimg: hassio
 | 
			
		||||
          target: final
 | 
			
		||||
          build_type: ha-addon
 | 
			
		||||
          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 }}
 | 
			
		||||
 | 
			
		||||
      - name: Sanitize platform name
 | 
			
		||||
        id: sanitize
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "${{ matrix.platform }}" | sed 's|/|-|g' > /tmp/platform
 | 
			
		||||
          echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT
 | 
			
		||||
      # - name: Build lint
 | 
			
		||||
      #   uses: ./.github/actions/build-image
 | 
			
		||||
      #   with:
 | 
			
		||||
      #     target: lint
 | 
			
		||||
      #     build_type: lint
 | 
			
		||||
      #     suffix: lint
 | 
			
		||||
      #     version: ${{ needs.init.outputs.tag }}
 | 
			
		||||
 | 
			
		||||
      - name: Upload digests
 | 
			
		||||
        uses: actions/upload-artifact@v4.6.2
 | 
			
		||||
        with:
 | 
			
		||||
          name: digests-${{ steps.sanitize.outputs.name }}
 | 
			
		||||
          name: digests-${{ matrix.platform.arch }}
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
          retention-days: 1
 | 
			
		||||
 | 
			
		||||
  deploy-manifest:
 | 
			
		||||
    name: Publish ESPHome ${{ matrix.image.title }} to ${{ matrix.registry }}
 | 
			
		||||
    name: Publish ESPHome ${{ matrix.image.build_type }} to ${{ matrix.registry }}
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs:
 | 
			
		||||
      - init
 | 
			
		||||
@@ -160,15 +158,12 @@ jobs:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        image:
 | 
			
		||||
          - title: "ha-addon"
 | 
			
		||||
            target: "hassio"
 | 
			
		||||
            suffix: "hassio"
 | 
			
		||||
          - title: "docker"
 | 
			
		||||
            target: "docker"
 | 
			
		||||
          - build_type: "docker"
 | 
			
		||||
            suffix: ""
 | 
			
		||||
          - title: "lint"
 | 
			
		||||
            target: "lint"
 | 
			
		||||
            suffix: "lint"
 | 
			
		||||
          - build_type: "ha-addon"
 | 
			
		||||
            suffix: "hassio"
 | 
			
		||||
            # - build_type: "lint"
 | 
			
		||||
            #   suffix: "lint"
 | 
			
		||||
        registry:
 | 
			
		||||
          - ghcr
 | 
			
		||||
          - dockerhub
 | 
			
		||||
@@ -176,7 +171,7 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v4.1.7
 | 
			
		||||
 | 
			
		||||
      - name: Download digests
 | 
			
		||||
        uses: actions/download-artifact@v4.2.1
 | 
			
		||||
        uses: actions/download-artifact@v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          pattern: digests-*
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
@@ -212,7 +207,7 @@ jobs:
 | 
			
		||||
          done
 | 
			
		||||
 | 
			
		||||
      - name: Create manifest list and push
 | 
			
		||||
        working-directory: /tmp/digests/${{ matrix.image.target }}/${{ matrix.registry }}
 | 
			
		||||
        working-directory: /tmp/digests/${{ matrix.image.build_type }}/${{ matrix.registry }}
 | 
			
		||||
        run: |
 | 
			
		||||
          docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \
 | 
			
		||||
            $(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *)
 | 
			
		||||
@@ -243,3 +238,24 @@ jobs:
 | 
			
		||||
                content: description
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
  deploy-esphome-schema:
 | 
			
		||||
    if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [init]
 | 
			
		||||
    environment: ${{ needs.init.outputs.deploy_env }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Trigger Workflow
 | 
			
		||||
        uses: actions/github-script@v7.0.1
 | 
			
		||||
        with:
 | 
			
		||||
          github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }}
 | 
			
		||||
          script: |
 | 
			
		||||
            github.rest.actions.createWorkflowDispatch({
 | 
			
		||||
              owner: "esphome",
 | 
			
		||||
              repo: "esphome-schema",
 | 
			
		||||
              workflow_id: "generate-schemas.yml",
 | 
			
		||||
              ref: "main",
 | 
			
		||||
              inputs: {
 | 
			
		||||
                version: "${{ needs.init.outputs.tag }}",
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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.5.0
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: 3.12
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -143,3 +143,4 @@ sdkconfig.*
 | 
			
		||||
/components
 | 
			
		||||
/managed_components
 | 
			
		||||
 | 
			
		||||
api-docs/
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
repos:
 | 
			
		||||
  - repo: https://github.com/astral-sh/ruff-pre-commit
 | 
			
		||||
    # Ruff version.
 | 
			
		||||
    rev: v0.11.0
 | 
			
		||||
    rev: v0.11.9
 | 
			
		||||
    hooks:
 | 
			
		||||
      # Run the linter.
 | 
			
		||||
      - id: ruff
 | 
			
		||||
@@ -33,7 +33,7 @@ repos:
 | 
			
		||||
      - id: pyupgrade
 | 
			
		||||
        args: [--py39-plus]
 | 
			
		||||
  - repo: https://github.com/adrienverge/yamllint.git
 | 
			
		||||
    rev: v1.35.1
 | 
			
		||||
    rev: v1.37.1
 | 
			
		||||
    hooks:
 | 
			
		||||
      - id: yamllint
 | 
			
		||||
  - repo: https://github.com/pre-commit/mirrors-clang-format
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								CODEOWNERS
									
									
									
									
									
								
							@@ -98,6 +98,7 @@ esphome/components/climate/* @esphome/core
 | 
			
		||||
esphome/components/climate_ir/* @glmnet
 | 
			
		||||
esphome/components/color_temperature/* @jesserockz
 | 
			
		||||
esphome/components/combination/* @Cat-Ion @kahrendt
 | 
			
		||||
esphome/components/const/* @esphome/core
 | 
			
		||||
esphome/components/coolix/* @glmnet
 | 
			
		||||
esphome/components/copy/* @OttoWinter
 | 
			
		||||
esphome/components/cover/* @esphome/core
 | 
			
		||||
@@ -168,7 +169,7 @@ esphome/components/gp2y1010au0f/* @zry98
 | 
			
		||||
esphome/components/gp8403/* @jesserockz
 | 
			
		||||
esphome/components/gpio/* @esphome/core
 | 
			
		||||
esphome/components/gpio/one_wire/* @ssieb
 | 
			
		||||
esphome/components/gps/* @coogle
 | 
			
		||||
esphome/components/gps/* @coogle @ximex
 | 
			
		||||
esphome/components/graph/* @synco
 | 
			
		||||
esphome/components/graphical_display_menu/* @MrMDavidson
 | 
			
		||||
esphome/components/gree/* @orestismers
 | 
			
		||||
@@ -250,6 +251,7 @@ esphome/components/ltr501/* @latonita
 | 
			
		||||
esphome/components/ltr_als_ps/* @latonita
 | 
			
		||||
esphome/components/lvgl/* @clydebarrow
 | 
			
		||||
esphome/components/m5stack_8angle/* @rnauber
 | 
			
		||||
esphome/components/mapping/* @clydebarrow
 | 
			
		||||
esphome/components/matrix_keypad/* @ssieb
 | 
			
		||||
esphome/components/max17043/* @blacknell
 | 
			
		||||
esphome/components/max31865/* @DAVe3283
 | 
			
		||||
@@ -276,10 +278,11 @@ esphome/components/mdns/* @esphome/core
 | 
			
		||||
esphome/components/media_player/* @jesserockz
 | 
			
		||||
esphome/components/micro_wake_word/* @jesserockz @kahrendt
 | 
			
		||||
esphome/components/micronova/* @jorre05
 | 
			
		||||
esphome/components/microphone/* @jesserockz
 | 
			
		||||
esphome/components/microphone/* @jesserockz @kahrendt
 | 
			
		||||
esphome/components/mics_4514/* @jesserockz
 | 
			
		||||
esphome/components/midea/* @dudanov
 | 
			
		||||
esphome/components/midea_ir/* @dudanov
 | 
			
		||||
esphome/components/mipi_spi/* @clydebarrow
 | 
			
		||||
esphome/components/mitsubishi/* @RubyBailey
 | 
			
		||||
esphome/components/mixer/speaker/* @kahrendt
 | 
			
		||||
esphome/components/mlx90393/* @functionpointer
 | 
			
		||||
@@ -317,6 +320,7 @@ esphome/components/online_image/* @clydebarrow @guillempages
 | 
			
		||||
esphome/components/opentherm/* @olegtarasov
 | 
			
		||||
esphome/components/ota/* @esphome/core
 | 
			
		||||
esphome/components/output/* @esphome/core
 | 
			
		||||
esphome/components/packet_transport/* @clydebarrow
 | 
			
		||||
esphome/components/pca6416a/* @Mat931
 | 
			
		||||
esphome/components/pca9554/* @clydebarrow @hwstar
 | 
			
		||||
esphome/components/pcf85063/* @brogon
 | 
			
		||||
@@ -324,7 +328,9 @@ esphome/components/pcf8563/* @KoenBreeman
 | 
			
		||||
esphome/components/pid/* @OttoWinter
 | 
			
		||||
esphome/components/pipsolar/* @andreashergert1984
 | 
			
		||||
esphome/components/pm1006/* @habbie
 | 
			
		||||
esphome/components/pm2005/* @andrewjswan
 | 
			
		||||
esphome/components/pmsa003i/* @sjtrny
 | 
			
		||||
esphome/components/pmsx003/* @ximex
 | 
			
		||||
esphome/components/pmwcs3/* @SeByDocKy
 | 
			
		||||
esphome/components/pn532/* @OttoWinter @jesserockz
 | 
			
		||||
esphome/components/pn532_i2c/* @OttoWinter @jesserockz
 | 
			
		||||
@@ -393,6 +399,7 @@ esphome/components/smt100/* @piechade
 | 
			
		||||
esphome/components/sn74hc165/* @jesserockz
 | 
			
		||||
esphome/components/socket/* @esphome/core
 | 
			
		||||
esphome/components/sonoff_d1/* @anatoly-savchenkov
 | 
			
		||||
esphome/components/sound_level/* @kahrendt
 | 
			
		||||
esphome/components/speaker/* @jesserockz @kahrendt
 | 
			
		||||
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
 | 
			
		||||
esphome/components/spi/* @clydebarrow @esphome/core
 | 
			
		||||
@@ -424,6 +431,7 @@ esphome/components/sun/* @OttoWinter
 | 
			
		||||
esphome/components/sun_gtil2/* @Mat931
 | 
			
		||||
esphome/components/switch/* @esphome/core
 | 
			
		||||
esphome/components/switch/binary_sensor/* @ssieb
 | 
			
		||||
esphome/components/syslog/* @clydebarrow
 | 
			
		||||
esphome/components/t6615/* @tylermenezes
 | 
			
		||||
esphome/components/tc74/* @sethgirvan
 | 
			
		||||
esphome/components/tca9548a/* @andreashergert1984
 | 
			
		||||
@@ -463,6 +471,7 @@ esphome/components/tuya/switch/* @jesserockz
 | 
			
		||||
esphome/components/tuya/text_sensor/* @dentra
 | 
			
		||||
esphome/components/uart/* @esphome/core
 | 
			
		||||
esphome/components/uart/button/* @ssieb
 | 
			
		||||
esphome/components/uart/packet_transport/* @clydebarrow
 | 
			
		||||
esphome/components/udp/* @clydebarrow
 | 
			
		||||
esphome/components/ufire_ec/* @pvizeli
 | 
			
		||||
esphome/components/ufire_ise/* @pvizeli
 | 
			
		||||
 
 | 
			
		||||
@@ -1,131 +1,56 @@
 | 
			
		||||
# Build these with the build.py script
 | 
			
		||||
# Example:
 | 
			
		||||
#   python3 docker/build.py --tag dev --arch amd64 --build-type docker build
 | 
			
		||||
ARG BUILD_VERSION=dev
 | 
			
		||||
ARG BUILD_OS=alpine
 | 
			
		||||
ARG BUILD_BASE_VERSION=2025.04.0
 | 
			
		||||
ARG BUILD_TYPE=docker
 | 
			
		||||
 | 
			
		||||
# One of "docker", "hassio"
 | 
			
		||||
ARG BASEIMGTYPE=docker
 | 
			
		||||
FROM ghcr.io/esphome/docker-base:${BUILD_OS}-${BUILD_BASE_VERSION} AS base-source-docker
 | 
			
		||||
FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS base-source-ha-addon
 | 
			
		||||
 | 
			
		||||
ARG BUILD_TYPE
 | 
			
		||||
FROM base-source-${BUILD_TYPE} AS base
 | 
			
		||||
 | 
			
		||||
# https://github.com/hassio-addons/addon-debian-base/releases
 | 
			
		||||
FROM ghcr.io/hassio-addons/debian-base:7.2.0 AS base-hassio
 | 
			
		||||
# https://hub.docker.com/_/debian?tab=tags&page=1&name=bookworm
 | 
			
		||||
FROM debian:12.2-slim AS base-docker
 | 
			
		||||
RUN git config --system --add safe.directory "*"
 | 
			
		||||
 | 
			
		||||
FROM base-${BASEIMGTYPE} AS base
 | 
			
		||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
 | 
			
		||||
 | 
			
		||||
RUN pip install --no-cache-dir -U pip uv==0.6.14
 | 
			
		||||
 | 
			
		||||
ARG TARGETARCH
 | 
			
		||||
ARG TARGETVARIANT
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Note that --break-system-packages is used below because
 | 
			
		||||
# https://peps.python.org/pep-0668/ added a safety check that prevents
 | 
			
		||||
# installing packages with the same name as a system package. This is
 | 
			
		||||
# not a problem for us because we are not concerned about overwriting
 | 
			
		||||
# system packages because we are running in an isolated container.
 | 
			
		||||
COPY requirements.txt /
 | 
			
		||||
 | 
			
		||||
RUN \
 | 
			
		||||
    apt-get update \
 | 
			
		||||
    # Use pinned versions so that we get updates with build caching
 | 
			
		||||
    && apt-get install -y --no-install-recommends \
 | 
			
		||||
        python3-pip=23.0.1+dfsg-1 \
 | 
			
		||||
        python3-setuptools=66.1.1-1+deb12u1 \
 | 
			
		||||
        python3-venv=3.11.2-1+b1 \
 | 
			
		||||
        python3-wheel=0.38.4-2 \
 | 
			
		||||
        iputils-ping=3:20221126-1+deb12u1 \
 | 
			
		||||
        git=1:2.39.5-0+deb12u2 \
 | 
			
		||||
        curl=7.88.1-10+deb12u12 \
 | 
			
		||||
        openssh-client=1:9.2p1-2+deb12u5 \
 | 
			
		||||
        python3-cffi=1.15.1-5 \
 | 
			
		||||
        libcairo2=1.16.0-7 \
 | 
			
		||||
        libmagic1=1:5.44-3 \
 | 
			
		||||
        patch=2.7.6-7 \
 | 
			
		||||
    && rm -rf \
 | 
			
		||||
        /tmp/* \
 | 
			
		||||
        /var/{cache,log}/* \
 | 
			
		||||
        /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
ENV \
 | 
			
		||||
  # Fix click python3 lang warning https://click.palletsprojects.com/en/7.x/python3/
 | 
			
		||||
  LANG=C.UTF-8 LC_ALL=C.UTF-8 \
 | 
			
		||||
  # Store globally installed pio libs in /piolibs
 | 
			
		||||
  PLATFORMIO_GLOBALLIB_DIR=/piolibs
 | 
			
		||||
    uv pip install --no-cache-dir \
 | 
			
		||||
    -r /requirements.txt
 | 
			
		||||
 | 
			
		||||
RUN \
 | 
			
		||||
    pip3 install \
 | 
			
		||||
    --break-system-packages --no-cache-dir \
 | 
			
		||||
    # Keep platformio version in sync with requirements.txt
 | 
			
		||||
    platformio==6.1.18 \
 | 
			
		||||
    # Change some platformio settings
 | 
			
		||||
    && platformio settings set enable_telemetry No \
 | 
			
		||||
    platformio settings set enable_telemetry No \
 | 
			
		||||
    && platformio settings set check_platformio_interval 1000000 \
 | 
			
		||||
    && mkdir -p /piolibs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# First install requirements to leverage caching when requirements don't change
 | 
			
		||||
# tmpfs is for https://github.com/rust-lang/cargo/issues/8719
 | 
			
		||||
 | 
			
		||||
COPY requirements.txt requirements_optional.txt /
 | 
			
		||||
RUN --mount=type=tmpfs,target=/root/.cargo <<END-OF-RUN
 | 
			
		||||
# Fail on any non-zero status
 | 
			
		||||
set -e
 | 
			
		||||
 | 
			
		||||
# install build tools in case wheels are not available
 | 
			
		||||
BUILD_DEPS="
 | 
			
		||||
    build-essential=12.9
 | 
			
		||||
    python3-dev=3.11.2-1+b1
 | 
			
		||||
    zlib1g-dev=1:1.2.13.dfsg-1
 | 
			
		||||
    libjpeg-dev=1:2.1.5-2
 | 
			
		||||
    libfreetype-dev=2.12.1+dfsg-5+deb12u4
 | 
			
		||||
    libssl-dev=3.0.15-1~deb12u1
 | 
			
		||||
    libffi-dev=3.4.4-1
 | 
			
		||||
    cargo=0.66.0+ds1-1
 | 
			
		||||
    pkg-config=1.8.1-1
 | 
			
		||||
"
 | 
			
		||||
LIB_DEPS="
 | 
			
		||||
    libtiff6=4.5.0-6+deb12u1
 | 
			
		||||
    libopenjp2-7=2.5.0-2+deb12u1
 | 
			
		||||
"
 | 
			
		||||
if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ]
 | 
			
		||||
then
 | 
			
		||||
    apt-get update
 | 
			
		||||
    apt-get install -y --no-install-recommends $BUILD_DEPS $LIB_DEPS
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo
 | 
			
		||||
pip3 install --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt
 | 
			
		||||
 | 
			
		||||
if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ]
 | 
			
		||||
then
 | 
			
		||||
    apt-get remove -y --purge --auto-remove $BUILD_DEPS
 | 
			
		||||
    rm -rf /tmp/* /var/{cache,log}/* /var/lib/apt/lists/*
 | 
			
		||||
fi
 | 
			
		||||
END-OF-RUN
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
COPY script/platformio_install_deps.py platformio.ini /
 | 
			
		||||
RUN /platformio_install_deps.py /platformio.ini --libraries
 | 
			
		||||
 | 
			
		||||
# Avoid unsafe git error when container user and file config volume permissions don't match
 | 
			
		||||
RUN git config --system --add safe.directory '*'
 | 
			
		||||
ARG BUILD_VERSION
 | 
			
		||||
 | 
			
		||||
LABEL \
 | 
			
		||||
    org.opencontainers.image.authors="The ESPHome Authors" \
 | 
			
		||||
    org.opencontainers.image.title="ESPHome" \
 | 
			
		||||
    org.opencontainers.image.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \
 | 
			
		||||
    org.opencontainers.image.url="https://esphome.io/" \
 | 
			
		||||
    org.opencontainers.image.documentation="https://esphome.io/" \
 | 
			
		||||
    org.opencontainers.image.source="https://github.com/esphome/esphome" \
 | 
			
		||||
    org.opencontainers.image.licenses="ESPHome" \
 | 
			
		||||
    org.opencontainers.image.version=${BUILD_VERSION}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ======================= docker-type image =======================
 | 
			
		||||
FROM base AS docker
 | 
			
		||||
 | 
			
		||||
# Copy esphome and install
 | 
			
		||||
COPY . /esphome
 | 
			
		||||
RUN pip3 install --break-system-packages --no-cache-dir -e /esphome
 | 
			
		||||
 | 
			
		||||
# Settings for dashboard
 | 
			
		||||
ENV USERNAME="" PASSWORD=""
 | 
			
		||||
FROM base AS base-docker
 | 
			
		||||
 | 
			
		||||
# Expose the dashboard to Docker
 | 
			
		||||
EXPOSE 6052
 | 
			
		||||
 | 
			
		||||
# Run healthcheck (heartbeat)
 | 
			
		||||
HEALTHCHECK --interval=30s --timeout=30s \
 | 
			
		||||
  CMD curl --fail http://localhost:6052/version -A "HealthCheck" || exit 1
 | 
			
		||||
    CMD curl --fail http://localhost:6052/version -A "HealthCheck" || exit 1
 | 
			
		||||
 | 
			
		||||
COPY docker/docker_entrypoint.sh /entrypoint.sh
 | 
			
		||||
 | 
			
		||||
@@ -139,43 +64,13 @@ ENTRYPOINT ["/entrypoint.sh"]
 | 
			
		||||
CMD ["dashboard", "/config"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ARG BUILD_VERSION=dev
 | 
			
		||||
 | 
			
		||||
# Labels
 | 
			
		||||
LABEL \
 | 
			
		||||
    org.opencontainers.image.authors="The ESPHome Authors" \
 | 
			
		||||
    org.opencontainers.image.title="ESPHome" \
 | 
			
		||||
    org.opencontainers.image.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \
 | 
			
		||||
    org.opencontainers.image.url="https://esphome.io/" \
 | 
			
		||||
    org.opencontainers.image.documentation="https://esphome.io/" \
 | 
			
		||||
    org.opencontainers.image.source="https://github.com/esphome/esphome" \
 | 
			
		||||
    org.opencontainers.image.licenses="ESPHome" \
 | 
			
		||||
    org.opencontainers.image.version=${BUILD_VERSION}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ======================= hassio-type image =======================
 | 
			
		||||
FROM base AS hassio
 | 
			
		||||
 | 
			
		||||
RUN \
 | 
			
		||||
    apt-get update \
 | 
			
		||||
    # Use pinned versions so that we get updates with build caching
 | 
			
		||||
    && apt-get install -y --no-install-recommends \
 | 
			
		||||
        nginx-light=1.22.1-9+deb12u1 \
 | 
			
		||||
    && rm -rf \
 | 
			
		||||
        /tmp/* \
 | 
			
		||||
        /var/{cache,log}/* \
 | 
			
		||||
        /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
ARG BUILD_VERSION=dev
 | 
			
		||||
# ======================= ha-addon-type image =======================
 | 
			
		||||
FROM base AS base-ha-addon
 | 
			
		||||
 | 
			
		||||
# Copy root filesystem
 | 
			
		||||
COPY docker/ha-addon-rootfs/ /
 | 
			
		||||
 | 
			
		||||
# Copy esphome and install
 | 
			
		||||
COPY . /esphome
 | 
			
		||||
RUN pip3 install --break-system-packages --no-cache-dir -e /esphome
 | 
			
		||||
 | 
			
		||||
# Labels
 | 
			
		||||
ARG BUILD_VERSION
 | 
			
		||||
LABEL \
 | 
			
		||||
    io.hass.name="ESPHome" \
 | 
			
		||||
    io.hass.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \
 | 
			
		||||
@@ -183,35 +78,9 @@ LABEL \
 | 
			
		||||
    io.hass.version="${BUILD_VERSION}"
 | 
			
		||||
    # io.hass.arch is inherited from addon-debian-base
 | 
			
		||||
 | 
			
		||||
ARG BUILD_TYPE
 | 
			
		||||
FROM base-${BUILD_TYPE} AS final
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ======================= lint-type image =======================
 | 
			
		||||
FROM base AS lint
 | 
			
		||||
 | 
			
		||||
ENV \
 | 
			
		||||
  PLATFORMIO_CORE_DIR=/esphome/.temp/platformio
 | 
			
		||||
 | 
			
		||||
RUN \
 | 
			
		||||
    curl -L https://apt.llvm.org/llvm-snapshot.gpg.key -o /etc/apt/trusted.gpg.d/apt.llvm.org.asc \
 | 
			
		||||
    && echo "deb http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-18 main" > /etc/apt/sources.list.d/llvm.sources.list \
 | 
			
		||||
    && apt-get update \
 | 
			
		||||
    # Use pinned versions so that we get updates with build caching
 | 
			
		||||
    && apt-get install -y --no-install-recommends \
 | 
			
		||||
        clang-format-13=1:13.0.1-11+b2 \
 | 
			
		||||
        patch=2.7.6-7 \
 | 
			
		||||
        software-properties-common=0.99.30-4.1~deb12u1 \
 | 
			
		||||
        nano=7.2-1+deb12u1 \
 | 
			
		||||
        build-essential=12.9 \
 | 
			
		||||
        python3-dev=3.11.2-1+b1 \
 | 
			
		||||
        clang-tidy-18=1:18.1.8~++20240731024826+3b5b5c1ec4a3-1~exp1~20240731144843.145 \
 | 
			
		||||
    && rm -rf \
 | 
			
		||||
        /tmp/* \
 | 
			
		||||
        /var/{cache,log}/* \
 | 
			
		||||
        /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
COPY requirements_test.txt /
 | 
			
		||||
RUN pip3 install --break-system-packages --no-cache-dir -r /requirements_test.txt
 | 
			
		||||
 | 
			
		||||
VOLUME ["/esphome"]
 | 
			
		||||
WORKDIR /esphome
 | 
			
		||||
# Copy esphome and install
 | 
			
		||||
COPY . /esphome
 | 
			
		||||
RUN uv pip install --no-cache-dir -e /esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ manifest_parser = subparsers.add_parser(
 | 
			
		||||
class DockerParams:
 | 
			
		||||
    build_to: str
 | 
			
		||||
    manifest_to: str
 | 
			
		||||
    baseimgtype: str
 | 
			
		||||
    build_type: str
 | 
			
		||||
    platform: str
 | 
			
		||||
    target: str
 | 
			
		||||
 | 
			
		||||
@@ -66,24 +66,19 @@ class DockerParams:
 | 
			
		||||
            TYPE_LINT: "esphome/esphome-lint",
 | 
			
		||||
        }[build_type]
 | 
			
		||||
        build_to = f"{prefix}-{arch}"
 | 
			
		||||
        baseimgtype = {
 | 
			
		||||
            TYPE_DOCKER: "docker",
 | 
			
		||||
            TYPE_HA_ADDON: "hassio",
 | 
			
		||||
            TYPE_LINT: "docker",
 | 
			
		||||
        }[build_type]
 | 
			
		||||
        platform = {
 | 
			
		||||
            ARCH_AMD64: "linux/amd64",
 | 
			
		||||
            ARCH_AARCH64: "linux/arm64",
 | 
			
		||||
        }[arch]
 | 
			
		||||
        target = {
 | 
			
		||||
            TYPE_DOCKER: "docker",
 | 
			
		||||
            TYPE_HA_ADDON: "hassio",
 | 
			
		||||
            TYPE_DOCKER: "final",
 | 
			
		||||
            TYPE_HA_ADDON: "final",
 | 
			
		||||
            TYPE_LINT: "lint",
 | 
			
		||||
        }[build_type]
 | 
			
		||||
        return cls(
 | 
			
		||||
            build_to=build_to,
 | 
			
		||||
            manifest_to=prefix,
 | 
			
		||||
            baseimgtype=baseimgtype,
 | 
			
		||||
            build_type=build_type,
 | 
			
		||||
            platform=platform,
 | 
			
		||||
            target=target,
 | 
			
		||||
        )
 | 
			
		||||
@@ -145,7 +140,7 @@ def main():
 | 
			
		||||
            "buildx",
 | 
			
		||||
            "build",
 | 
			
		||||
            "--build-arg",
 | 
			
		||||
            f"BASEIMGTYPE={params.baseimgtype}",
 | 
			
		||||
            f"BUILD_TYPE={params.build_type}",
 | 
			
		||||
            "--build-arg",
 | 
			
		||||
            f"BUILD_VERSION={args.tag}",
 | 
			
		||||
            "--cache-from",
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,7 @@ from esphome.const import (
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, EsphomeError, coroutine
 | 
			
		||||
from esphome.helpers import get_bool_env, indent, is_ip_address
 | 
			
		||||
from esphome.log import Fore, color, setup_log
 | 
			
		||||
from esphome.log import AnsiFore, color, setup_log
 | 
			
		||||
from esphome.util import (
 | 
			
		||||
    get_serial_ports,
 | 
			
		||||
    list_yaml_files,
 | 
			
		||||
@@ -83,7 +83,7 @@ def choose_prompt(options, purpose: str = None):
 | 
			
		||||
                raise ValueError
 | 
			
		||||
            break
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            safe_print(color(Fore.RED, f"Invalid option: '{opt}'"))
 | 
			
		||||
            safe_print(color(AnsiFore.RED, f"Invalid option: '{opt}'"))
 | 
			
		||||
    return options[opt - 1][1]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -375,10 +375,12 @@ def upload_program(config, args, host):
 | 
			
		||||
    password = ota_conf.get(CONF_PASSWORD, "")
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        not is_ip_address(CORE.address)  # pylint: disable=too-many-boolean-expressions
 | 
			
		||||
        and (get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED])
 | 
			
		||||
        and CONF_MQTT in config
 | 
			
		||||
        CONF_MQTT in config  # pylint: disable=too-many-boolean-expressions
 | 
			
		||||
        and (not args.device or args.device in ("MQTT", "OTA"))
 | 
			
		||||
        and (
 | 
			
		||||
            ((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address))
 | 
			
		||||
            or get_port_type(host) == "MQTT"
 | 
			
		||||
        )
 | 
			
		||||
    ):
 | 
			
		||||
        from esphome import mqtt
 | 
			
		||||
 | 
			
		||||
@@ -594,30 +596,30 @@ def command_update_all(args):
 | 
			
		||||
        click.echo(f"{half_line}{middle_text}{half_line}")
 | 
			
		||||
 | 
			
		||||
    for f in files:
 | 
			
		||||
        print(f"Updating {color(Fore.CYAN, f)}")
 | 
			
		||||
        print(f"Updating {color(AnsiFore.CYAN, f)}")
 | 
			
		||||
        print("-" * twidth)
 | 
			
		||||
        print()
 | 
			
		||||
        rc = run_external_process(
 | 
			
		||||
            "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
 | 
			
		||||
        )
 | 
			
		||||
        if rc == 0:
 | 
			
		||||
            print_bar(f"[{color(Fore.BOLD_GREEN, 'SUCCESS')}] {f}")
 | 
			
		||||
            print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {f}")
 | 
			
		||||
            success[f] = True
 | 
			
		||||
        else:
 | 
			
		||||
            print_bar(f"[{color(Fore.BOLD_RED, 'ERROR')}] {f}")
 | 
			
		||||
            print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {f}")
 | 
			
		||||
            success[f] = False
 | 
			
		||||
 | 
			
		||||
        print()
 | 
			
		||||
        print()
 | 
			
		||||
        print()
 | 
			
		||||
 | 
			
		||||
    print_bar(f"[{color(Fore.BOLD_WHITE, 'SUMMARY')}]")
 | 
			
		||||
    print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
 | 
			
		||||
    failed = 0
 | 
			
		||||
    for f in files:
 | 
			
		||||
        if success[f]:
 | 
			
		||||
            print(f"  - {f}: {color(Fore.GREEN, 'SUCCESS')}")
 | 
			
		||||
            print(f"  - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}")
 | 
			
		||||
        else:
 | 
			
		||||
            print(f"  - {f}: {color(Fore.BOLD_RED, 'FAILED')}")
 | 
			
		||||
            print(f"  - {f}: {color(AnsiFore.BOLD_RED, 'FAILED')}")
 | 
			
		||||
            failed += 1
 | 
			
		||||
    return failed
 | 
			
		||||
 | 
			
		||||
@@ -643,7 +645,7 @@ def command_rename(args, config):
 | 
			
		||||
        if c not in ALLOWED_NAME_CHARS:
 | 
			
		||||
            print(
 | 
			
		||||
                color(
 | 
			
		||||
                    Fore.BOLD_RED,
 | 
			
		||||
                    AnsiFore.BOLD_RED,
 | 
			
		||||
                    f"'{c}' is an invalid character for names. Valid characters are: "
 | 
			
		||||
                    f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)",
 | 
			
		||||
                )
 | 
			
		||||
@@ -656,7 +658,9 @@ def command_rename(args, config):
 | 
			
		||||
    yaml = yaml_util.load_yaml(CORE.config_path)
 | 
			
		||||
    if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
 | 
			
		||||
        print(
 | 
			
		||||
            color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.")
 | 
			
		||||
            color(
 | 
			
		||||
                AnsiFore.BOLD_RED, "Complex YAML files cannot be automatically renamed."
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        return 1
 | 
			
		||||
    old_name = yaml[CONF_ESPHOME][CONF_NAME]
 | 
			
		||||
@@ -679,7 +683,7 @@ def command_rename(args, config):
 | 
			
		||||
            )
 | 
			
		||||
            > 1
 | 
			
		||||
        ):
 | 
			
		||||
            print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename"))
 | 
			
		||||
            print(color(AnsiFore.BOLD_RED, "Too many matches in YAML to safely rename"))
 | 
			
		||||
            return 1
 | 
			
		||||
 | 
			
		||||
        new_raw = re.sub(
 | 
			
		||||
@@ -691,7 +695,7 @@ def command_rename(args, config):
 | 
			
		||||
 | 
			
		||||
    new_path = os.path.join(CORE.config_dir, args.name + ".yaml")
 | 
			
		||||
    print(
 | 
			
		||||
        f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}"
 | 
			
		||||
        f"Updating {color(AnsiFore.CYAN, CORE.config_path)} to {color(AnsiFore.CYAN, new_path)}"
 | 
			
		||||
    )
 | 
			
		||||
    print()
 | 
			
		||||
 | 
			
		||||
@@ -700,7 +704,7 @@ def command_rename(args, config):
 | 
			
		||||
 | 
			
		||||
    rc = run_external_process("esphome", "config", new_path)
 | 
			
		||||
    if rc != 0:
 | 
			
		||||
        print(color(Fore.BOLD_RED, "Rename failed. Reverting changes."))
 | 
			
		||||
        print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes."))
 | 
			
		||||
        os.remove(new_path)
 | 
			
		||||
        return 1
 | 
			
		||||
 | 
			
		||||
@@ -726,7 +730,7 @@ def command_rename(args, config):
 | 
			
		||||
    if CORE.config_path != new_path:
 | 
			
		||||
        os.remove(CORE.config_path)
 | 
			
		||||
 | 
			
		||||
    print(color(Fore.BOLD_GREEN, "SUCCESS"))
 | 
			
		||||
    print(color(AnsiFore.BOLD_GREEN, "SUCCESS"))
 | 
			
		||||
    print()
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -114,13 +114,14 @@ void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
 | 
			
		||||
    // fully off, disable output immediately
 | 
			
		||||
    this->gate_pin.digital_write(false);
 | 
			
		||||
  } else {
 | 
			
		||||
    auto min_us = this->cycle_time_us * this->min_power / 1000;
 | 
			
		||||
    if (this->method == DIM_METHOD_TRAILING) {
 | 
			
		||||
      this->enable_time_us = 1;  // cannot be 0
 | 
			
		||||
      this->disable_time_us = std::max((uint32_t) 10, this->value * this->cycle_time_us / 65535);
 | 
			
		||||
      // calculate time until disable in µs with integer arithmetic and take into account min_power
 | 
			
		||||
      this->disable_time_us = std::max((uint32_t) 10, this->value * (this->cycle_time_us - min_us) / 65535 + min_us);
 | 
			
		||||
    } else {
 | 
			
		||||
      // calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic
 | 
			
		||||
      // also take into account min_power
 | 
			
		||||
      auto min_us = this->cycle_time_us * this->min_power / 1000;
 | 
			
		||||
      this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535);
 | 
			
		||||
 | 
			
		||||
      if (this->method == DIM_METHOD_LEADING_PULSE) {
 | 
			
		||||
 
 | 
			
		||||
@@ -47,9 +47,10 @@ SAMPLING_MODES = {
 | 
			
		||||
adc1_channel_t = cg.global_ns.enum("adc1_channel_t")
 | 
			
		||||
adc2_channel_t = cg.global_ns.enum("adc2_channel_t")
 | 
			
		||||
 | 
			
		||||
# From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h
 | 
			
		||||
# pin to adc1 channel mapping
 | 
			
		||||
# https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h
 | 
			
		||||
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32: {
 | 
			
		||||
        36: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        37: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
@@ -60,6 +61,41 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
 | 
			
		||||
        34: adc1_channel_t.ADC1_CHANNEL_6,
 | 
			
		||||
        35: adc1_channel_t.ADC1_CHANNEL_7,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32C2: {
 | 
			
		||||
        0: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32C3: {
 | 
			
		||||
        0: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32C6: {
 | 
			
		||||
        0: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
        5: adc1_channel_t.ADC1_CHANNEL_5,
 | 
			
		||||
        6: adc1_channel_t.ADC1_CHANNEL_6,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32H2: {
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        5: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32S2: {
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
@@ -72,6 +108,7 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
 | 
			
		||||
        9: adc1_channel_t.ADC1_CHANNEL_8,
 | 
			
		||||
        10: adc1_channel_t.ADC1_CHANNEL_9,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32S3: {
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
@@ -84,40 +121,12 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
 | 
			
		||||
        9: adc1_channel_t.ADC1_CHANNEL_8,
 | 
			
		||||
        10: adc1_channel_t.ADC1_CHANNEL_9,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32C3: {
 | 
			
		||||
        0: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32C2: {
 | 
			
		||||
        0: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32C6: {
 | 
			
		||||
        0: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
        5: adc1_channel_t.ADC1_CHANNEL_5,
 | 
			
		||||
        6: adc1_channel_t.ADC1_CHANNEL_6,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32H2: {
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        5: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# pin to adc2 channel mapping
 | 
			
		||||
# https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h
 | 
			
		||||
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
 | 
			
		||||
    # TODO: add other variants
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32: {
 | 
			
		||||
        4: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
        0: adc2_channel_t.ADC2_CHANNEL_1,
 | 
			
		||||
@@ -130,6 +139,19 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
 | 
			
		||||
        25: adc2_channel_t.ADC2_CHANNEL_8,
 | 
			
		||||
        26: adc2_channel_t.ADC2_CHANNEL_9,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32C2: {
 | 
			
		||||
        5: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32C3: {
 | 
			
		||||
        5: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32C6: {},  # no ADC2
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32H2: {},  # no ADC2
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32S2: {
 | 
			
		||||
        11: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
        12: adc2_channel_t.ADC2_CHANNEL_1,
 | 
			
		||||
@@ -142,6 +164,7 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
 | 
			
		||||
        19: adc2_channel_t.ADC2_CHANNEL_8,
 | 
			
		||||
        20: adc2_channel_t.ADC2_CHANNEL_9,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32S3: {
 | 
			
		||||
        11: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
        12: adc2_channel_t.ADC2_CHANNEL_1,
 | 
			
		||||
@@ -154,12 +177,6 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
 | 
			
		||||
        19: adc2_channel_t.ADC2_CHANNEL_8,
 | 
			
		||||
        20: adc2_channel_t.ADC2_CHANNEL_9,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32C3: {
 | 
			
		||||
        5: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32C2: {},
 | 
			
		||||
    VARIANT_ESP32C6: {},
 | 
			
		||||
    VARIANT_ESP32H2: {},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ AirthingsWaveBase = airthings_wave_base_ns.class_(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
BASE_SCHEMA = (
 | 
			
		||||
    sensor.SENSOR_SCHEMA.extend(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@ from esphome.components import mqtt, web_server
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_CODE,
 | 
			
		||||
    CONF_ENTITY_CATEGORY,
 | 
			
		||||
    CONF_ICON,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_MQTT_ID,
 | 
			
		||||
    CONF_ON_STATE,
 | 
			
		||||
@@ -12,6 +14,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_WEB_SERVER,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
from esphome.cpp_generator import MockObjClass
 | 
			
		||||
from esphome.cpp_helpers import setup_entity
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@grahambrown11", "@hwstar"]
 | 
			
		||||
@@ -78,12 +81,11 @@ AlarmControlPanelCondition = alarm_control_panel_ns.class_(
 | 
			
		||||
    "AlarmControlPanelCondition", automation.Condition
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
ALARM_CONTROL_PANEL_SCHEMA = (
 | 
			
		||||
_ALARM_CONTROL_PANEL_SCHEMA = (
 | 
			
		||||
    cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
 | 
			
		||||
    .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(AlarmControlPanel),
 | 
			
		||||
            cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
 | 
			
		||||
                mqtt.MQTTAlarmControlPanelComponent
 | 
			
		||||
            ),
 | 
			
		||||
@@ -146,6 +148,33 @@ ALARM_CONTROL_PANEL_SCHEMA = (
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def alarm_control_panel_schema(
 | 
			
		||||
    class_: MockObjClass,
 | 
			
		||||
    *,
 | 
			
		||||
    entity_category: str = cv.UNDEFINED,
 | 
			
		||||
    icon: str = cv.UNDEFINED,
 | 
			
		||||
) -> cv.Schema:
 | 
			
		||||
    schema = {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(class_),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for key, default, validator in [
 | 
			
		||||
        (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
 | 
			
		||||
        (CONF_ICON, icon, cv.icon),
 | 
			
		||||
    ]:
 | 
			
		||||
        if default is not cv.UNDEFINED:
 | 
			
		||||
            schema[cv.Optional(key, default=default)] = validator
 | 
			
		||||
 | 
			
		||||
    return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel)
 | 
			
		||||
ALARM_CONTROL_PANEL_SCHEMA.add_extra(
 | 
			
		||||
    cv.deprecated_schema_constant("alarm_control_panel")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(AlarmControlPanel),
 | 
			
		||||
@@ -209,6 +238,12 @@ async def register_alarm_control_panel(var, config):
 | 
			
		||||
    await setup_alarm_control_panel_core_(var, config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def new_alarm_control_panel(config, *args):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID], *args)
 | 
			
		||||
    await register_alarm_control_panel(var, config)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -128,7 +128,7 @@ void AM2315C::update() {
 | 
			
		||||
  data[2] = 0x00;
 | 
			
		||||
  if (this->write(data, 3) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Write failed!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -138,12 +138,12 @@ void AM2315C::update() {
 | 
			
		||||
    uint8_t status = 0;
 | 
			
		||||
    if (this->read(&status, 1) != i2c::ERROR_OK) {
 | 
			
		||||
      ESP_LOGE(TAG, "Read failed!");
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if ((status & 0x80) == 0x80) {
 | 
			
		||||
      ESP_LOGE(TAG, "HW still busy!");
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -151,7 +151,7 @@ void AM2315C::update() {
 | 
			
		||||
    uint8_t data[7];
 | 
			
		||||
    if (this->read(data, 7) != i2c::ERROR_OK) {
 | 
			
		||||
      ESP_LOGE(TAG, "Read failed!");
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import ble_client, cover
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_PIN
 | 
			
		||||
from esphome.const import CONF_PIN
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@buxtronix"]
 | 
			
		||||
DEPENDENCIES = ["ble_client"]
 | 
			
		||||
@@ -15,9 +15,9 @@ Am43Component = am43_ns.class_(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cover.COVER_SCHEMA.extend(
 | 
			
		||||
    cover.cover_schema(Am43Component)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(Am43Component),
 | 
			
		||||
            cv.Optional(CONF_PIN, default=8888): cv.int_range(min=0, max=0xFFFF),
 | 
			
		||||
            cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
 | 
			
		||||
        }
 | 
			
		||||
@@ -28,9 +28,8 @@ CONFIG_SCHEMA = (
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    var = await cover.new_cover(config)
 | 
			
		||||
    cg.add(var.set_pin(config[CONF_PIN]))
 | 
			
		||||
    cg.add(var.set_invert_position(config[CONF_INVERT_POSITION]))
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await cover.register_cover(var, config)
 | 
			
		||||
    await ble_client.register_ble_node(var, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,8 @@ void AnalogThresholdBinarySensor::setup() {
 | 
			
		||||
  if (std::isnan(sensor_value)) {
 | 
			
		||||
    this->publish_initial_state(false);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->publish_initial_state(sensor_value >= (this->lower_threshold_ + this->upper_threshold_) / 2.0f);
 | 
			
		||||
    this->publish_initial_state(sensor_value >=
 | 
			
		||||
                                (this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -24,7 +25,8 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) {
 | 
			
		||||
  this->sensor_->add_on_state_callback([this](float sensor_value) {
 | 
			
		||||
    // if there is an invalid sensor reading, ignore the change and keep the current state
 | 
			
		||||
    if (!std::isnan(sensor_value)) {
 | 
			
		||||
      this->publish_state(sensor_value >= (this->state ? this->lower_threshold_ : this->upper_threshold_));
 | 
			
		||||
      this->publish_state(sensor_value >=
 | 
			
		||||
                          (this->state ? this->lower_threshold_.value() : this->upper_threshold_.value()));
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -32,8 +34,8 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) {
 | 
			
		||||
void AnalogThresholdBinarySensor::dump_config() {
 | 
			
		||||
  LOG_BINARY_SENSOR("", "Analog Threshold Binary Sensor", this);
 | 
			
		||||
  LOG_SENSOR("  ", "Sensor", this->sensor_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Upper threshold: %.11f", this->upper_threshold_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Lower threshold: %.11f", this->lower_threshold_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Upper threshold: %.11f", this->upper_threshold_.value());
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Lower threshold: %.11f", this->lower_threshold_.value());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace analog_threshold
 | 
			
		||||
 
 | 
			
		||||
@@ -15,14 +15,13 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
  void set_sensor(sensor::Sensor *analog_sensor);
 | 
			
		||||
  void set_upper_threshold(float threshold) { this->upper_threshold_ = threshold; }
 | 
			
		||||
  void set_lower_threshold(float threshold) { this->lower_threshold_ = threshold; }
 | 
			
		||||
  template<typename T> void set_upper_threshold(T upper_threshold) { this->upper_threshold_ = upper_threshold; }
 | 
			
		||||
  template<typename T> void set_lower_threshold(T lower_threshold) { this->lower_threshold_ = lower_threshold; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  sensor::Sensor *sensor_{nullptr};
 | 
			
		||||
 | 
			
		||||
  float upper_threshold_;
 | 
			
		||||
  float lower_threshold_;
 | 
			
		||||
  TemplatableValue<float> upper_threshold_{};
 | 
			
		||||
  TemplatableValue<float> lower_threshold_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace analog_threshold
 | 
			
		||||
 
 | 
			
		||||
@@ -18,11 +18,11 @@ CONFIG_SCHEMA = (
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_SENSOR_ID): cv.use_id(sensor.Sensor),
 | 
			
		||||
            cv.Required(CONF_THRESHOLD): cv.Any(
 | 
			
		||||
                cv.float_,
 | 
			
		||||
                cv.templatable(cv.float_),
 | 
			
		||||
                cv.Schema(
 | 
			
		||||
                    {
 | 
			
		||||
                        cv.Required(CONF_UPPER): cv.float_,
 | 
			
		||||
                        cv.Required(CONF_LOWER): cv.float_,
 | 
			
		||||
                        cv.Required(CONF_UPPER): cv.templatable(cv.float_),
 | 
			
		||||
                        cv.Required(CONF_LOWER): cv.templatable(cv.float_),
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
@@ -39,9 +39,11 @@ async def to_code(config):
 | 
			
		||||
    sens = await cg.get_variable(config[CONF_SENSOR_ID])
 | 
			
		||||
    cg.add(var.set_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if isinstance(config[CONF_THRESHOLD], float):
 | 
			
		||||
        cg.add(var.set_upper_threshold(config[CONF_THRESHOLD]))
 | 
			
		||||
        cg.add(var.set_lower_threshold(config[CONF_THRESHOLD]))
 | 
			
		||||
    if isinstance(config[CONF_THRESHOLD], dict):
 | 
			
		||||
        lower = await cg.templatable(config[CONF_THRESHOLD][CONF_LOWER], [], float)
 | 
			
		||||
        upper = await cg.templatable(config[CONF_THRESHOLD][CONF_UPPER], [], float)
 | 
			
		||||
    else:
 | 
			
		||||
        cg.add(var.set_upper_threshold(config[CONF_THRESHOLD][CONF_UPPER]))
 | 
			
		||||
        cg.add(var.set_lower_threshold(config[CONF_THRESHOLD][CONF_LOWER]))
 | 
			
		||||
        lower = await cg.templatable(config[CONF_THRESHOLD], [], float)
 | 
			
		||||
        upper = lower
 | 
			
		||||
    cg.add(var.set_upper_threshold(upper))
 | 
			
		||||
    cg.add(var.set_lower_threshold(lower))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import ble_client, climate
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_UNIT_OF_MEASUREMENT
 | 
			
		||||
from esphome.const import CONF_UNIT_OF_MEASUREMENT
 | 
			
		||||
 | 
			
		||||
UNITS = {
 | 
			
		||||
    "f": "f",
 | 
			
		||||
@@ -17,9 +17,9 @@ Anova = anova_ns.class_(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    climate.CLIMATE_SCHEMA.extend(
 | 
			
		||||
    climate.climate_schema(Anova)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(Anova),
 | 
			
		||||
            cv.Required(CONF_UNIT_OF_MEASUREMENT): cv.enum(UNITS),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
@@ -29,8 +29,7 @@ CONFIG_SCHEMA = (
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    var = await climate.new_climate(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await climate.register_climate(var, config)
 | 
			
		||||
    await ble_client.register_ble_node(var, config)
 | 
			
		||||
    cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT]))
 | 
			
		||||
 
 | 
			
		||||
@@ -82,6 +82,19 @@ ACTIONS_SCHEMA = automation.validate_automation(
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
ENCRYPTION_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Optional(CONF_KEY): validate_encryption_key,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _encryption_schema(config):
 | 
			
		||||
    if config is None:
 | 
			
		||||
        config = {}
 | 
			
		||||
    return ENCRYPTION_SCHEMA(config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
@@ -95,11 +108,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
                CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
 | 
			
		||||
            ): ACTIONS_SCHEMA,
 | 
			
		||||
            cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
 | 
			
		||||
            cv.Optional(CONF_ENCRYPTION): cv.Schema(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.Required(CONF_KEY): validate_encryption_key,
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ENCRYPTION): _encryption_schema,
 | 
			
		||||
            cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
 | 
			
		||||
                single=True
 | 
			
		||||
            ),
 | 
			
		||||
@@ -151,9 +160,17 @@ async def to_code(config):
 | 
			
		||||
            config[CONF_ON_CLIENT_DISCONNECTED],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if encryption_config := config.get(CONF_ENCRYPTION):
 | 
			
		||||
        decoded = base64.b64decode(encryption_config[CONF_KEY])
 | 
			
		||||
        cg.add(var.set_noise_psk(list(decoded)))
 | 
			
		||||
    if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
 | 
			
		||||
        if key := encryption_config.get(CONF_KEY):
 | 
			
		||||
            decoded = base64.b64decode(key)
 | 
			
		||||
            cg.add(var.set_noise_psk(list(decoded)))
 | 
			
		||||
        else:
 | 
			
		||||
            # No key provided, but encryption desired
 | 
			
		||||
            # This will allow a plaintext client to provide a noise key,
 | 
			
		||||
            # send it to the device, and then switch to noise.
 | 
			
		||||
            # The key will be saved in flash and used for future connections
 | 
			
		||||
            # and plaintext disabled. Only a factory reset can remove it.
 | 
			
		||||
            cg.add_define("USE_API_PLAINTEXT")
 | 
			
		||||
        cg.add_define("USE_API_NOISE")
 | 
			
		||||
        cg.add_library("esphome/noise-c", "0.1.6")
 | 
			
		||||
    else:
 | 
			
		||||
 
 | 
			
		||||
@@ -31,24 +31,26 @@ service APIConnection {
 | 
			
		||||
    option (needs_authentication) = false;
 | 
			
		||||
  }
 | 
			
		||||
  rpc execute_service (ExecuteServiceRequest) returns (void) {}
 | 
			
		||||
  rpc noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {}
 | 
			
		||||
 | 
			
		||||
  rpc cover_command (CoverCommandRequest) returns (void) {}
 | 
			
		||||
  rpc fan_command (FanCommandRequest) returns (void) {}
 | 
			
		||||
  rpc light_command (LightCommandRequest) returns (void) {}
 | 
			
		||||
  rpc switch_command (SwitchCommandRequest) returns (void) {}
 | 
			
		||||
  rpc button_command (ButtonCommandRequest) returns (void) {}
 | 
			
		||||
  rpc camera_image (CameraImageRequest) returns (void) {}
 | 
			
		||||
  rpc climate_command (ClimateCommandRequest) returns (void) {}
 | 
			
		||||
  rpc number_command (NumberCommandRequest) returns (void) {}
 | 
			
		||||
  rpc text_command (TextCommandRequest) returns (void) {}
 | 
			
		||||
  rpc select_command (SelectCommandRequest) returns (void) {}
 | 
			
		||||
  rpc button_command (ButtonCommandRequest) returns (void) {}
 | 
			
		||||
  rpc lock_command (LockCommandRequest) returns (void) {}
 | 
			
		||||
  rpc valve_command (ValveCommandRequest) returns (void) {}
 | 
			
		||||
  rpc media_player_command (MediaPlayerCommandRequest) returns (void) {}
 | 
			
		||||
  rpc cover_command (CoverCommandRequest) returns (void) {}
 | 
			
		||||
  rpc date_command (DateCommandRequest) returns (void) {}
 | 
			
		||||
  rpc time_command (TimeCommandRequest) returns (void) {}
 | 
			
		||||
  rpc datetime_command (DateTimeCommandRequest) returns (void) {}
 | 
			
		||||
  rpc fan_command (FanCommandRequest) returns (void) {}
 | 
			
		||||
  rpc light_command (LightCommandRequest) returns (void) {}
 | 
			
		||||
  rpc lock_command (LockCommandRequest) returns (void) {}
 | 
			
		||||
  rpc media_player_command (MediaPlayerCommandRequest) returns (void) {}
 | 
			
		||||
  rpc number_command (NumberCommandRequest) returns (void) {}
 | 
			
		||||
  rpc select_command (SelectCommandRequest) returns (void) {}
 | 
			
		||||
  rpc siren_command (SirenCommandRequest) returns (void) {}
 | 
			
		||||
  rpc switch_command (SwitchCommandRequest) returns (void) {}
 | 
			
		||||
  rpc text_command (TextCommandRequest) returns (void) {}
 | 
			
		||||
  rpc time_command (TimeCommandRequest) returns (void) {}
 | 
			
		||||
  rpc update_command (UpdateCommandRequest) returns (void) {}
 | 
			
		||||
  rpc valve_command (ValveCommandRequest) returns (void) {}
 | 
			
		||||
 | 
			
		||||
  rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
 | 
			
		||||
  rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {}
 | 
			
		||||
@@ -60,6 +62,7 @@ service APIConnection {
 | 
			
		||||
  rpc bluetooth_gatt_notify(BluetoothGATTNotifyRequest) returns (void) {}
 | 
			
		||||
  rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {}
 | 
			
		||||
  rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
 | 
			
		||||
  rpc bluetooth_scanner_set_mode(BluetoothScannerSetModeRequest) returns (void) {}
 | 
			
		||||
 | 
			
		||||
  rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {}
 | 
			
		||||
  rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {}
 | 
			
		||||
@@ -230,6 +233,9 @@ message DeviceInfoResponse {
 | 
			
		||||
 | 
			
		||||
  // The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA"
 | 
			
		||||
  string bluetooth_mac_address = 18;
 | 
			
		||||
 | 
			
		||||
  // Supports receiving and saving api encryption key
 | 
			
		||||
  bool api_encryption_supported = 19;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message ListEntitiesRequest {
 | 
			
		||||
@@ -650,10 +656,27 @@ message SubscribeLogsResponse {
 | 
			
		||||
  option (no_delay) = false;
 | 
			
		||||
 | 
			
		||||
  LogLevel level = 1;
 | 
			
		||||
  string message = 3;
 | 
			
		||||
  bytes message = 3;
 | 
			
		||||
  bool send_failed = 4;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== NOISE ENCRYPTION ====================
 | 
			
		||||
message NoiseEncryptionSetKeyRequest {
 | 
			
		||||
  option (id) = 124;
 | 
			
		||||
  option (source) = SOURCE_CLIENT;
 | 
			
		||||
  option (ifdef) = "USE_API_NOISE";
 | 
			
		||||
 | 
			
		||||
  bytes key = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message NoiseEncryptionSetKeyResponse {
 | 
			
		||||
  option (id) = 125;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
  option (ifdef) = "USE_API_NOISE";
 | 
			
		||||
 | 
			
		||||
  bool success = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== HOMEASSISTANT.SERVICE ====================
 | 
			
		||||
message SubscribeHomeassistantServicesRequest {
 | 
			
		||||
  option (id) = 34;
 | 
			
		||||
@@ -889,6 +912,7 @@ message ClimateStateResponse {
 | 
			
		||||
  float target_temperature = 4;
 | 
			
		||||
  float target_temperature_low = 5;
 | 
			
		||||
  float target_temperature_high = 6;
 | 
			
		||||
  // For older peers, equal to preset == CLIMATE_PRESET_AWAY
 | 
			
		||||
  bool unused_legacy_away = 7;
 | 
			
		||||
  ClimateAction action = 8;
 | 
			
		||||
  ClimateFanMode fan_mode = 9;
 | 
			
		||||
@@ -914,6 +938,7 @@ message ClimateCommandRequest {
 | 
			
		||||
  float target_temperature_low = 7;
 | 
			
		||||
  bool has_target_temperature_high = 8;
 | 
			
		||||
  float target_temperature_high = 9;
 | 
			
		||||
  // legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset
 | 
			
		||||
  bool unused_has_legacy_away = 10;
 | 
			
		||||
  bool unused_legacy_away = 11;
 | 
			
		||||
  bool has_fan_mode = 12;
 | 
			
		||||
@@ -1016,6 +1041,49 @@ message SelectCommandRequest {
 | 
			
		||||
  string state = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== SIREN ====================
 | 
			
		||||
message ListEntitiesSirenResponse {
 | 
			
		||||
  option (id) = 55;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
  option (ifdef) = "USE_SIREN";
 | 
			
		||||
 | 
			
		||||
  string object_id = 1;
 | 
			
		||||
  fixed32 key = 2;
 | 
			
		||||
  string name = 3;
 | 
			
		||||
  string unique_id = 4;
 | 
			
		||||
 | 
			
		||||
  string icon = 5;
 | 
			
		||||
  bool disabled_by_default = 6;
 | 
			
		||||
  repeated string tones = 7;
 | 
			
		||||
  bool supports_duration = 8;
 | 
			
		||||
  bool supports_volume = 9;
 | 
			
		||||
  EntityCategory entity_category = 10;
 | 
			
		||||
}
 | 
			
		||||
message SirenStateResponse {
 | 
			
		||||
  option (id) = 56;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
  option (ifdef) = "USE_SIREN";
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
 | 
			
		||||
  fixed32 key = 1;
 | 
			
		||||
  bool state = 2;
 | 
			
		||||
}
 | 
			
		||||
message SirenCommandRequest {
 | 
			
		||||
  option (id) = 57;
 | 
			
		||||
  option (source) = SOURCE_CLIENT;
 | 
			
		||||
  option (ifdef) = "USE_SIREN";
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
 | 
			
		||||
  fixed32 key = 1;
 | 
			
		||||
  bool has_state = 2;
 | 
			
		||||
  bool state = 3;
 | 
			
		||||
  bool has_tone = 4;
 | 
			
		||||
  string tone = 5;
 | 
			
		||||
  bool has_duration = 6;
 | 
			
		||||
  uint32 duration = 7;
 | 
			
		||||
  bool has_volume = 8;
 | 
			
		||||
  float volume = 9;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== LOCK ====================
 | 
			
		||||
enum LockState {
 | 
			
		||||
@@ -1185,8 +1253,8 @@ message SubscribeBluetoothLEAdvertisementsRequest {
 | 
			
		||||
 | 
			
		||||
message BluetoothServiceData {
 | 
			
		||||
  string uuid = 1;
 | 
			
		||||
  repeated uint32 legacy_data = 2 [deprecated = true];
 | 
			
		||||
  bytes data = 3; // Changed in proto version 1.7
 | 
			
		||||
  repeated uint32 legacy_data = 2 [deprecated = true];  // Removed in api version 1.7
 | 
			
		||||
  bytes data = 3;  // Added in api version 1.7
 | 
			
		||||
}
 | 
			
		||||
message BluetoothLEAdvertisementResponse {
 | 
			
		||||
  option (id) = 67;
 | 
			
		||||
@@ -1195,7 +1263,7 @@ message BluetoothLEAdvertisementResponse {
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
 | 
			
		||||
  uint64 address = 1;
 | 
			
		||||
  string name = 2;
 | 
			
		||||
  bytes name = 2;
 | 
			
		||||
  sint32 rssi = 3;
 | 
			
		||||
 | 
			
		||||
  repeated string service_uuids = 4;
 | 
			
		||||
@@ -1451,7 +1519,38 @@ message BluetoothDeviceClearCacheResponse {
 | 
			
		||||
  int32 error = 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== PUSH TO TALK ====================
 | 
			
		||||
enum BluetoothScannerState {
 | 
			
		||||
  BLUETOOTH_SCANNER_STATE_IDLE = 0;
 | 
			
		||||
  BLUETOOTH_SCANNER_STATE_STARTING = 1;
 | 
			
		||||
  BLUETOOTH_SCANNER_STATE_RUNNING = 2;
 | 
			
		||||
  BLUETOOTH_SCANNER_STATE_FAILED = 3;
 | 
			
		||||
  BLUETOOTH_SCANNER_STATE_STOPPING = 4;
 | 
			
		||||
  BLUETOOTH_SCANNER_STATE_STOPPED = 5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum BluetoothScannerMode {
 | 
			
		||||
  BLUETOOTH_SCANNER_MODE_PASSIVE = 0;
 | 
			
		||||
  BLUETOOTH_SCANNER_MODE_ACTIVE = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message BluetoothScannerStateResponse {
 | 
			
		||||
  option(id) = 126;
 | 
			
		||||
  option(source) = SOURCE_SERVER;
 | 
			
		||||
  option(ifdef) = "USE_BLUETOOTH_PROXY";
 | 
			
		||||
 | 
			
		||||
  BluetoothScannerState state = 1;
 | 
			
		||||
  BluetoothScannerMode mode = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message BluetoothScannerSetModeRequest {
 | 
			
		||||
  option(id) = 127;
 | 
			
		||||
  option(source) = SOURCE_CLIENT;
 | 
			
		||||
  option(ifdef) = "USE_BLUETOOTH_PROXY";
 | 
			
		||||
 | 
			
		||||
  BluetoothScannerMode mode = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== VOICE ASSISTANT ====================
 | 
			
		||||
enum VoiceAssistantSubscribeFlag {
 | 
			
		||||
  VOICE_ASSISTANT_SUBSCRIBE_NONE = 0;
 | 
			
		||||
  VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1;
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -8,13 +8,17 @@
 | 
			
		||||
#include "api_server.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
using send_message_t = bool(APIConnection *, void *);
 | 
			
		||||
// Keepalive timeout in milliseconds
 | 
			
		||||
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
 | 
			
		||||
 | 
			
		||||
using send_message_t = bool (APIConnection::*)(void *);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
  This class holds a pointer to the source component that wants to publish a message, and a pointer to a function that
 | 
			
		||||
@@ -30,10 +34,10 @@ class DeferredMessageQueue {
 | 
			
		||||
 | 
			
		||||
   protected:
 | 
			
		||||
    void *source_;
 | 
			
		||||
    send_message_t *send_message_;
 | 
			
		||||
    send_message_t send_message_;
 | 
			
		||||
 | 
			
		||||
   public:
 | 
			
		||||
    DeferredMessage(void *source, send_message_t *send_message) : source_(source), send_message_(send_message) {}
 | 
			
		||||
    DeferredMessage(void *source, send_message_t send_message) : source_(source), send_message_(send_message) {}
 | 
			
		||||
    bool operator==(const DeferredMessage &test) const {
 | 
			
		||||
      return (source_ == test.source_ && send_message_ == test.send_message_);
 | 
			
		||||
    }
 | 
			
		||||
@@ -46,12 +50,13 @@ class DeferredMessageQueue {
 | 
			
		||||
  APIConnection *api_connection_;
 | 
			
		||||
 | 
			
		||||
  // helper for allowing only unique entries in the queue
 | 
			
		||||
  void dmq_push_back_with_dedup_(void *source, send_message_t *send_message);
 | 
			
		||||
  void dmq_push_back_with_dedup_(void *source, send_message_t send_message);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  DeferredMessageQueue(APIConnection *api_connection) : api_connection_(api_connection) {}
 | 
			
		||||
  void process_queue();
 | 
			
		||||
  void defer(void *source, send_message_t *send_message);
 | 
			
		||||
  void defer(void *source, send_message_t send_message);
 | 
			
		||||
  bool empty() const { return deferred_queue_.empty(); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class APIConnection : public APIServerConnection {
 | 
			
		||||
@@ -69,137 +74,213 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
  bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state);
 | 
			
		||||
  void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor);
 | 
			
		||||
  static bool try_send_binary_sensor_state(APIConnection *api, void *v_binary_sensor);
 | 
			
		||||
  static bool try_send_binary_sensor_state(APIConnection *api, binary_sensor::BinarySensor *binary_sensor, bool state);
 | 
			
		||||
  static bool try_send_binary_sensor_info(APIConnection *api, void *v_binary_sensor);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_binary_sensor_state_(binary_sensor::BinarySensor *binary_sensor);
 | 
			
		||||
  bool try_send_binary_sensor_state_(binary_sensor::BinarySensor *binary_sensor, bool state);
 | 
			
		||||
  bool try_send_binary_sensor_info_(binary_sensor::BinarySensor *binary_sensor);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_COVER
 | 
			
		||||
  bool send_cover_state(cover::Cover *cover);
 | 
			
		||||
  void send_cover_info(cover::Cover *cover);
 | 
			
		||||
  static bool try_send_cover_state(APIConnection *api, void *v_cover);
 | 
			
		||||
  static bool try_send_cover_info(APIConnection *api, void *v_cover);
 | 
			
		||||
  void cover_command(const CoverCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_cover_state_(cover::Cover *cover);
 | 
			
		||||
  bool try_send_cover_info_(cover::Cover *cover);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
  bool send_fan_state(fan::Fan *fan);
 | 
			
		||||
  void send_fan_info(fan::Fan *fan);
 | 
			
		||||
  static bool try_send_fan_state(APIConnection *api, void *v_fan);
 | 
			
		||||
  static bool try_send_fan_info(APIConnection *api, void *v_fan);
 | 
			
		||||
  void fan_command(const FanCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_fan_state_(fan::Fan *fan);
 | 
			
		||||
  bool try_send_fan_info_(fan::Fan *fan);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
  bool send_light_state(light::LightState *light);
 | 
			
		||||
  void send_light_info(light::LightState *light);
 | 
			
		||||
  static bool try_send_light_state(APIConnection *api, void *v_light);
 | 
			
		||||
  static bool try_send_light_info(APIConnection *api, void *v_light);
 | 
			
		||||
  void light_command(const LightCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_light_state_(light::LightState *light);
 | 
			
		||||
  bool try_send_light_info_(light::LightState *light);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
  bool send_sensor_state(sensor::Sensor *sensor, float state);
 | 
			
		||||
  void send_sensor_info(sensor::Sensor *sensor);
 | 
			
		||||
  static bool try_send_sensor_state(APIConnection *api, void *v_sensor);
 | 
			
		||||
  static bool try_send_sensor_state(APIConnection *api, sensor::Sensor *sensor, float state);
 | 
			
		||||
  static bool try_send_sensor_info(APIConnection *api, void *v_sensor);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_sensor_state_(sensor::Sensor *sensor);
 | 
			
		||||
  bool try_send_sensor_state_(sensor::Sensor *sensor, float state);
 | 
			
		||||
  bool try_send_sensor_info_(sensor::Sensor *sensor);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  bool send_switch_state(switch_::Switch *a_switch, bool state);
 | 
			
		||||
  void send_switch_info(switch_::Switch *a_switch);
 | 
			
		||||
  static bool try_send_switch_state(APIConnection *api, void *v_a_switch);
 | 
			
		||||
  static bool try_send_switch_state(APIConnection *api, switch_::Switch *a_switch, bool state);
 | 
			
		||||
  static bool try_send_switch_info(APIConnection *api, void *v_a_switch);
 | 
			
		||||
  void switch_command(const SwitchCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_switch_state_(switch_::Switch *a_switch);
 | 
			
		||||
  bool try_send_switch_state_(switch_::Switch *a_switch, bool state);
 | 
			
		||||
  bool try_send_switch_info_(switch_::Switch *a_switch);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
  bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state);
 | 
			
		||||
  void send_text_sensor_info(text_sensor::TextSensor *text_sensor);
 | 
			
		||||
  static bool try_send_text_sensor_state(APIConnection *api, void *v_text_sensor);
 | 
			
		||||
  static bool try_send_text_sensor_state(APIConnection *api, text_sensor::TextSensor *text_sensor, std::string state);
 | 
			
		||||
  static bool try_send_text_sensor_info(APIConnection *api, void *v_text_sensor);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_text_sensor_state_(text_sensor::TextSensor *text_sensor);
 | 
			
		||||
  bool try_send_text_sensor_state_(text_sensor::TextSensor *text_sensor, std::string state);
 | 
			
		||||
  bool try_send_text_sensor_info_(text_sensor::TextSensor *text_sensor);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
  void set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image);
 | 
			
		||||
  void send_camera_info(esp32_camera::ESP32Camera *camera);
 | 
			
		||||
  static bool try_send_camera_info(APIConnection *api, void *v_camera);
 | 
			
		||||
  void camera_image(const CameraImageRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_camera_info_(esp32_camera::ESP32Camera *camera);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
  bool send_climate_state(climate::Climate *climate);
 | 
			
		||||
  void send_climate_info(climate::Climate *climate);
 | 
			
		||||
  static bool try_send_climate_state(APIConnection *api, void *v_climate);
 | 
			
		||||
  static bool try_send_climate_info(APIConnection *api, void *v_climate);
 | 
			
		||||
  void climate_command(const ClimateCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_climate_state_(climate::Climate *climate);
 | 
			
		||||
  bool try_send_climate_info_(climate::Climate *climate);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  bool send_number_state(number::Number *number, float state);
 | 
			
		||||
  void send_number_info(number::Number *number);
 | 
			
		||||
  static bool try_send_number_state(APIConnection *api, void *v_number);
 | 
			
		||||
  static bool try_send_number_state(APIConnection *api, number::Number *number, float state);
 | 
			
		||||
  static bool try_send_number_info(APIConnection *api, void *v_number);
 | 
			
		||||
  void number_command(const NumberCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_number_state_(number::Number *number);
 | 
			
		||||
  bool try_send_number_state_(number::Number *number, float state);
 | 
			
		||||
  bool try_send_number_info_(number::Number *number);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_DATE
 | 
			
		||||
  bool send_date_state(datetime::DateEntity *date);
 | 
			
		||||
  void send_date_info(datetime::DateEntity *date);
 | 
			
		||||
  static bool try_send_date_state(APIConnection *api, void *v_date);
 | 
			
		||||
  static bool try_send_date_info(APIConnection *api, void *v_date);
 | 
			
		||||
  void date_command(const DateCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_date_state_(datetime::DateEntity *date);
 | 
			
		||||
  bool try_send_date_info_(datetime::DateEntity *date);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_TIME
 | 
			
		||||
  bool send_time_state(datetime::TimeEntity *time);
 | 
			
		||||
  void send_time_info(datetime::TimeEntity *time);
 | 
			
		||||
  static bool try_send_time_state(APIConnection *api, void *v_time);
 | 
			
		||||
  static bool try_send_time_info(APIConnection *api, void *v_time);
 | 
			
		||||
  void time_command(const TimeCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_time_state_(datetime::TimeEntity *time);
 | 
			
		||||
  bool try_send_time_info_(datetime::TimeEntity *time);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_DATETIME
 | 
			
		||||
  bool send_datetime_state(datetime::DateTimeEntity *datetime);
 | 
			
		||||
  void send_datetime_info(datetime::DateTimeEntity *datetime);
 | 
			
		||||
  static bool try_send_datetime_state(APIConnection *api, void *v_datetime);
 | 
			
		||||
  static bool try_send_datetime_info(APIConnection *api, void *v_datetime);
 | 
			
		||||
  void datetime_command(const DateTimeCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_datetime_state_(datetime::DateTimeEntity *datetime);
 | 
			
		||||
  bool try_send_datetime_info_(datetime::DateTimeEntity *datetime);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT
 | 
			
		||||
  bool send_text_state(text::Text *text, std::string state);
 | 
			
		||||
  void send_text_info(text::Text *text);
 | 
			
		||||
  static bool try_send_text_state(APIConnection *api, void *v_text);
 | 
			
		||||
  static bool try_send_text_state(APIConnection *api, text::Text *text, std::string state);
 | 
			
		||||
  static bool try_send_text_info(APIConnection *api, void *v_text);
 | 
			
		||||
  void text_command(const TextCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_text_state_(text::Text *text);
 | 
			
		||||
  bool try_send_text_state_(text::Text *text, std::string state);
 | 
			
		||||
  bool try_send_text_info_(text::Text *text);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  bool send_select_state(select::Select *select, std::string state);
 | 
			
		||||
  void send_select_info(select::Select *select);
 | 
			
		||||
  static bool try_send_select_state(APIConnection *api, void *v_select);
 | 
			
		||||
  static bool try_send_select_state(APIConnection *api, select::Select *select, std::string state);
 | 
			
		||||
  static bool try_send_select_info(APIConnection *api, void *v_select);
 | 
			
		||||
  void select_command(const SelectCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_select_state_(select::Select *select);
 | 
			
		||||
  bool try_send_select_state_(select::Select *select, std::string state);
 | 
			
		||||
  bool try_send_select_info_(select::Select *select);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
  void send_button_info(button::Button *button);
 | 
			
		||||
  static bool try_send_button_info(APIConnection *api, void *v_button);
 | 
			
		||||
  void button_command(const ButtonCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_button_info_(button::Button *button);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
  bool send_lock_state(lock::Lock *a_lock, lock::LockState state);
 | 
			
		||||
  void send_lock_info(lock::Lock *a_lock);
 | 
			
		||||
  static bool try_send_lock_state(APIConnection *api, void *v_a_lock);
 | 
			
		||||
  static bool try_send_lock_state(APIConnection *api, lock::Lock *a_lock, lock::LockState state);
 | 
			
		||||
  static bool try_send_lock_info(APIConnection *api, void *v_a_lock);
 | 
			
		||||
  void lock_command(const LockCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_lock_state_(lock::Lock *a_lock);
 | 
			
		||||
  bool try_send_lock_state_(lock::Lock *a_lock, lock::LockState state);
 | 
			
		||||
  bool try_send_lock_info_(lock::Lock *a_lock);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VALVE
 | 
			
		||||
  bool send_valve_state(valve::Valve *valve);
 | 
			
		||||
  void send_valve_info(valve::Valve *valve);
 | 
			
		||||
  static bool try_send_valve_state(APIConnection *api, void *v_valve);
 | 
			
		||||
  static bool try_send_valve_info(APIConnection *api, void *v_valve);
 | 
			
		||||
  void valve_command(const ValveCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_valve_state_(valve::Valve *valve);
 | 
			
		||||
  bool try_send_valve_info_(valve::Valve *valve);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  bool send_media_player_state(media_player::MediaPlayer *media_player);
 | 
			
		||||
  void send_media_player_info(media_player::MediaPlayer *media_player);
 | 
			
		||||
  static bool try_send_media_player_state(APIConnection *api, void *v_media_player);
 | 
			
		||||
  static bool try_send_media_player_info(APIConnection *api, void *v_media_player);
 | 
			
		||||
  void media_player_command(const MediaPlayerCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_media_player_state_(media_player::MediaPlayer *media_player);
 | 
			
		||||
  bool try_send_media_player_info_(media_player::MediaPlayer *media_player);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
  bool try_send_log_message(int level, const char *tag, const char *line);
 | 
			
		||||
  void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
 | 
			
		||||
@@ -221,6 +302,7 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override;
 | 
			
		||||
  BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free(
 | 
			
		||||
      const SubscribeBluetoothConnectionsFreeRequest &msg) override;
 | 
			
		||||
  void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_HOMEASSISTANT_TIME
 | 
			
		||||
@@ -245,25 +327,37 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
#ifdef USE_ALARM_CONTROL_PANEL
 | 
			
		||||
  bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
 | 
			
		||||
  void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
 | 
			
		||||
  static bool try_send_alarm_control_panel_state(APIConnection *api, void *v_a_alarm_control_panel);
 | 
			
		||||
  static bool try_send_alarm_control_panel_info(APIConnection *api, void *v_a_alarm_control_panel);
 | 
			
		||||
  void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_alarm_control_panel_state_(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
 | 
			
		||||
  bool try_send_alarm_control_panel_info_(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_EVENT
 | 
			
		||||
  void send_event(event::Event *event, std::string event_type);
 | 
			
		||||
  void send_event_info(event::Event *event);
 | 
			
		||||
  static bool try_send_event(APIConnection *api, void *v_event);
 | 
			
		||||
  static bool try_send_event(APIConnection *api, event::Event *event, std::string event_type);
 | 
			
		||||
  static bool try_send_event_info(APIConnection *api, void *v_event);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_event_(event::Event *event);
 | 
			
		||||
  bool try_send_event_(event::Event *event, std::string event_type);
 | 
			
		||||
  bool try_send_event_info_(event::Event *event);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_UPDATE
 | 
			
		||||
  bool send_update_state(update::UpdateEntity *update);
 | 
			
		||||
  void send_update_info(update::UpdateEntity *update);
 | 
			
		||||
  static bool try_send_update_state(APIConnection *api, void *v_update);
 | 
			
		||||
  static bool try_send_update_info(APIConnection *api, void *v_update);
 | 
			
		||||
  void update_command(const UpdateCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_update_state_(update::UpdateEntity *update);
 | 
			
		||||
  bool try_send_update_info_(update::UpdateEntity *update);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  void on_disconnect_response(const DisconnectResponse &value) override;
 | 
			
		||||
@@ -300,6 +394,9 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
  void execute_service(const ExecuteServiceRequest &msg) override;
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; }
 | 
			
		||||
  bool is_connection_setup() override {
 | 
			
		||||
@@ -308,11 +405,20 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  void on_fatal_error() override;
 | 
			
		||||
  void on_unauthenticated_access() override;
 | 
			
		||||
  void on_no_setup_connection() override;
 | 
			
		||||
  ProtoWriteBuffer create_buffer() override {
 | 
			
		||||
  ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
 | 
			
		||||
    // FIXME: ensure no recursive writes can happen
 | 
			
		||||
    this->proto_write_buffer_.clear();
 | 
			
		||||
    // Get header padding size - used for both reserve and insert
 | 
			
		||||
    uint8_t header_padding = this->helper_->frame_header_padding();
 | 
			
		||||
    // Reserve space for header padding + message + footer
 | 
			
		||||
    // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
 | 
			
		||||
    // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
 | 
			
		||||
    this->proto_write_buffer_.reserve(reserve_size + header_padding + this->helper_->frame_footer_size());
 | 
			
		||||
    // Insert header padding bytes so message encoding starts at the correct position
 | 
			
		||||
    this->proto_write_buffer_.insert(this->proto_write_buffer_.begin(), header_padding, 0);
 | 
			
		||||
    return {&this->proto_write_buffer_};
 | 
			
		||||
  }
 | 
			
		||||
  bool try_to_clear_buffer(bool log_out_of_space);
 | 
			
		||||
  bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;
 | 
			
		||||
 | 
			
		||||
  std::string get_client_combined_info() const { return this->client_combined_info_; }
 | 
			
		||||
@@ -320,6 +426,99 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
 protected:
 | 
			
		||||
  friend APIServer;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Generic send entity state method to reduce code duplication.
 | 
			
		||||
   * Only attempts to build and send the message if the transmit buffer is available.
 | 
			
		||||
   *
 | 
			
		||||
   * This is the base version for entities that use their current state.
 | 
			
		||||
   *
 | 
			
		||||
   * @param entity The entity to send state for
 | 
			
		||||
   * @param try_send_func The function that tries to send the state
 | 
			
		||||
   * @return True on success or message deferred, false if subscription check failed
 | 
			
		||||
   */
 | 
			
		||||
  bool send_state_(esphome::EntityBase *entity, send_message_t try_send_func) {
 | 
			
		||||
    if (!this->state_subscription_)
 | 
			
		||||
      return false;
 | 
			
		||||
    if (this->try_to_clear_buffer(true) && (this->*try_send_func)(entity)) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    this->deferred_message_queue_.defer(entity, try_send_func);
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Send entity state method that handles explicit state values.
 | 
			
		||||
   * Only attempts to build and send the message if the transmit buffer is available.
 | 
			
		||||
   *
 | 
			
		||||
   * This method accepts a state parameter to be used instead of the entity's current state.
 | 
			
		||||
   * It attempts to send the state with the provided value first, and if that fails due to buffer constraints,
 | 
			
		||||
   * it defers the entity for later processing using the entity-only function.
 | 
			
		||||
   *
 | 
			
		||||
   * @tparam EntityT The entity type
 | 
			
		||||
   * @tparam StateT Type of the state parameter
 | 
			
		||||
   * @tparam Args Additional argument types (if any)
 | 
			
		||||
   * @param entity The entity to send state for
 | 
			
		||||
   * @param try_send_entity_func The function that tries to send the state with entity pointer only
 | 
			
		||||
   * @param try_send_state_func The function that tries to send the state with entity and state parameters
 | 
			
		||||
   * @param state The state value to send
 | 
			
		||||
   * @param args Additional arguments to pass to the try_send_state_func
 | 
			
		||||
   * @return True on success or message deferred, false if subscription check failed
 | 
			
		||||
   */
 | 
			
		||||
  template<typename EntityT, typename StateT, typename... Args>
 | 
			
		||||
  bool send_state_with_value_(EntityT *entity, bool (APIConnection::*try_send_entity_func)(EntityT *),
 | 
			
		||||
                              bool (APIConnection::*try_send_state_func)(EntityT *, StateT, Args...), StateT state,
 | 
			
		||||
                              Args... args) {
 | 
			
		||||
    if (!this->state_subscription_)
 | 
			
		||||
      return false;
 | 
			
		||||
    if (this->try_to_clear_buffer(true) && (this->*try_send_state_func)(entity, state, args...)) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    this->deferred_message_queue_.defer(entity, reinterpret_cast<send_message_t>(try_send_entity_func));
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Generic send entity info method to reduce code duplication.
 | 
			
		||||
   * Only attempts to build and send the message if the transmit buffer is available.
 | 
			
		||||
   *
 | 
			
		||||
   * @param entity The entity to send info for
 | 
			
		||||
   * @param try_send_func The function that tries to send the info
 | 
			
		||||
   */
 | 
			
		||||
  void send_info_(esphome::EntityBase *entity, send_message_t try_send_func) {
 | 
			
		||||
    if (this->try_to_clear_buffer(true) && (this->*try_send_func)(entity)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this->deferred_message_queue_.defer(entity, try_send_func);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Generic function for generating entity info response messages.
 | 
			
		||||
   * This is used to reduce duplication in the try_send_*_info functions.
 | 
			
		||||
   *
 | 
			
		||||
   * @param entity The entity to generate info for
 | 
			
		||||
   * @param response The response object
 | 
			
		||||
   * @param send_response_func Function pointer to send the response
 | 
			
		||||
   * @return True if the message was sent successfully
 | 
			
		||||
   */
 | 
			
		||||
  template<typename ResponseT>
 | 
			
		||||
  bool try_send_entity_info_(esphome::EntityBase *entity, ResponseT &response,
 | 
			
		||||
                             bool (APIServerConnectionBase::*send_response_func)(const ResponseT &)) {
 | 
			
		||||
    // Set common fields that are shared by all entity types
 | 
			
		||||
    response.key = entity->get_object_id_hash();
 | 
			
		||||
    response.object_id = entity->get_object_id();
 | 
			
		||||
 | 
			
		||||
    if (entity->has_own_name())
 | 
			
		||||
      response.name = entity->get_name();
 | 
			
		||||
 | 
			
		||||
    // Set common EntityBase properties
 | 
			
		||||
    response.icon = entity->get_icon();
 | 
			
		||||
    response.disabled_by_default = entity->is_disabled_by_default();
 | 
			
		||||
    response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
 | 
			
		||||
 | 
			
		||||
    // Send the response using the provided send method
 | 
			
		||||
    return (this->*send_response_func)(response);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool send_(const void *buf, size_t len, bool force);
 | 
			
		||||
 | 
			
		||||
  enum class ConnectionState {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "proto.h"
 | 
			
		||||
#include "api_pb2_size.h"
 | 
			
		||||
#include <cstring>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -72,6 +73,91 @@ const char *api_error_to_str(APIError err) {
 | 
			
		||||
  return "UNKNOWN";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Common implementation for writing raw data to socket
 | 
			
		||||
template<typename StateEnum>
 | 
			
		||||
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket,
 | 
			
		||||
                                    std::vector<uint8_t> &tx_buf, const std::string &info, StateEnum &state,
 | 
			
		||||
                                    StateEnum failed_state) {
 | 
			
		||||
  // This method writes data to socket or buffers it
 | 
			
		||||
  // Returns APIError::OK if successful (or would block, but data has been buffered)
 | 
			
		||||
  // Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to failed_state
 | 
			
		||||
 | 
			
		||||
  if (iovcnt == 0)
 | 
			
		||||
    return APIError::OK;  // Nothing to do, success
 | 
			
		||||
 | 
			
		||||
  size_t total_write_len = 0;
 | 
			
		||||
  for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
    ESP_LOGVV(TAG, "Sending raw: %s",
 | 
			
		||||
              format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
 | 
			
		||||
#endif
 | 
			
		||||
    total_write_len += iov[i].iov_len;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf.empty()) {
 | 
			
		||||
    // try to empty tx_buf first
 | 
			
		||||
    while (!tx_buf.empty()) {
 | 
			
		||||
      ssize_t sent = socket->write(tx_buf.data(), tx_buf.size());
 | 
			
		||||
      if (is_would_block(sent)) {
 | 
			
		||||
        break;
 | 
			
		||||
      } else if (sent == -1) {
 | 
			
		||||
        ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
 | 
			
		||||
        state = failed_state;
 | 
			
		||||
        return APIError::SOCKET_WRITE_FAILED;  // Socket write failed
 | 
			
		||||
      }
 | 
			
		||||
      // TODO: inefficient if multiple packets in txbuf
 | 
			
		||||
      // replace with deque of buffers
 | 
			
		||||
      tx_buf.erase(tx_buf.begin(), tx_buf.begin() + sent);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf.empty()) {
 | 
			
		||||
    // tx buf not empty, can't write now because then stream would be inconsistent
 | 
			
		||||
    // Reserve space upfront to avoid multiple reallocations
 | 
			
		||||
    tx_buf.reserve(tx_buf.size() + total_write_len);
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
 | 
			
		||||
                    reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;  // Success, data buffered
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ssize_t sent = socket->writev(iov, iovcnt);
 | 
			
		||||
  if (is_would_block(sent)) {
 | 
			
		||||
    // operation would block, add buffer to tx_buf
 | 
			
		||||
    // Reserve space upfront to avoid multiple reallocations
 | 
			
		||||
    tx_buf.reserve(tx_buf.size() + total_write_len);
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
 | 
			
		||||
                    reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;  // Success, data buffered
 | 
			
		||||
  } else if (sent == -1) {
 | 
			
		||||
    // an error occurred
 | 
			
		||||
    ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
 | 
			
		||||
    state = failed_state;
 | 
			
		||||
    return APIError::SOCKET_WRITE_FAILED;  // Socket write failed
 | 
			
		||||
  } else if ((size_t) sent != total_write_len) {
 | 
			
		||||
    // partially sent, add end to tx_buf
 | 
			
		||||
    size_t remaining = total_write_len - sent;
 | 
			
		||||
    // Reserve space upfront to avoid multiple reallocations
 | 
			
		||||
    tx_buf.reserve(tx_buf.size() + remaining);
 | 
			
		||||
 | 
			
		||||
    size_t to_consume = sent;
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      if (to_consume >= iov[i].iov_len) {
 | 
			
		||||
        to_consume -= iov[i].iov_len;
 | 
			
		||||
      } else {
 | 
			
		||||
        tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
 | 
			
		||||
                      reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
        to_consume = 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;  // Success, data buffered
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;  // Success, all data sent
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__)
 | 
			
		||||
// uncomment to log raw packets
 | 
			
		||||
//#define HELPER_LOG_PACKETS
 | 
			
		||||
@@ -407,9 +493,12 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &rea
 | 
			
		||||
  std::vector<uint8_t> data;
 | 
			
		||||
  data.resize(reason.length() + 1);
 | 
			
		||||
  data[0] = 0x01;  // failure
 | 
			
		||||
  for (size_t i = 0; i < reason.length(); i++) {
 | 
			
		||||
    data[i + 1] = (uint8_t) reason[i];
 | 
			
		||||
 | 
			
		||||
  // Copy error message in bulk
 | 
			
		||||
  if (!reason.empty()) {
 | 
			
		||||
    std::memcpy(data.data() + 1, reason.c_str(), reason.length());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // temporarily remove failed state
 | 
			
		||||
  auto orig_state = state_;
 | 
			
		||||
  state_ = State::EXPLICIT_REJECT;
 | 
			
		||||
@@ -471,7 +560,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
 | 
			
		||||
APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
 | 
			
		||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
  aerr = state_action_();
 | 
			
		||||
@@ -483,31 +572,36 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
 | 
			
		||||
    return APIError::WOULD_BLOCK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
 | 
			
		||||
  // Message data starts after padding
 | 
			
		||||
  size_t payload_len = raw_buffer->size() - frame_header_padding_;
 | 
			
		||||
  size_t padding = 0;
 | 
			
		||||
  size_t msg_len = 4 + payload_len + padding;
 | 
			
		||||
  size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_);
 | 
			
		||||
  auto tmpbuf = std::unique_ptr<uint8_t[]>{new (std::nothrow) uint8_t[frame_len]};
 | 
			
		||||
  if (tmpbuf == nullptr) {
 | 
			
		||||
    HELPER_LOG("Could not allocate for writing packet");
 | 
			
		||||
    return APIError::OUT_OF_MEMORY;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  tmpbuf[0] = 0x01;  // indicator
 | 
			
		||||
  // tmpbuf[1], tmpbuf[2] to be set later
 | 
			
		||||
  // We need to resize to include MAC space, but we already reserved it in create_buffer
 | 
			
		||||
  raw_buffer->resize(raw_buffer->size() + frame_footer_size_);
 | 
			
		||||
 | 
			
		||||
  // Write the noise header in the padded area
 | 
			
		||||
  // Buffer layout:
 | 
			
		||||
  // [0]    - 0x01 indicator byte
 | 
			
		||||
  // [1-2]  - Size of encrypted payload (filled after encryption)
 | 
			
		||||
  // [3-4]  - Message type (encrypted)
 | 
			
		||||
  // [5-6]  - Payload length (encrypted)
 | 
			
		||||
  // [7...] - Actual payload data (encrypted)
 | 
			
		||||
  uint8_t *buf_start = raw_buffer->data();
 | 
			
		||||
  buf_start[0] = 0x01;  // indicator
 | 
			
		||||
  // buf_start[1], buf_start[2] to be set later after encryption
 | 
			
		||||
  const uint8_t msg_offset = 3;
 | 
			
		||||
  const uint8_t payload_offset = msg_offset + 4;
 | 
			
		||||
  tmpbuf[msg_offset + 0] = (uint8_t) (type >> 8);  // type
 | 
			
		||||
  tmpbuf[msg_offset + 1] = (uint8_t) type;
 | 
			
		||||
  tmpbuf[msg_offset + 2] = (uint8_t) (payload_len >> 8);  // data_len
 | 
			
		||||
  tmpbuf[msg_offset + 3] = (uint8_t) payload_len;
 | 
			
		||||
  // copy data
 | 
			
		||||
  std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]);
 | 
			
		||||
  // fill padding with zeros
 | 
			
		||||
  std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0);
 | 
			
		||||
  buf_start[msg_offset + 0] = (uint8_t) (type >> 8);         // type high byte
 | 
			
		||||
  buf_start[msg_offset + 1] = (uint8_t) type;                // type low byte
 | 
			
		||||
  buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8);  // data_len high byte
 | 
			
		||||
  buf_start[msg_offset + 3] = (uint8_t) payload_len;         // data_len low byte
 | 
			
		||||
  // payload data is already in the buffer starting at position 7
 | 
			
		||||
 | 
			
		||||
  NoiseBuffer mbuf;
 | 
			
		||||
  noise_buffer_init(mbuf);
 | 
			
		||||
  noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset);
 | 
			
		||||
  // The capacity parameter should be msg_len + frame_footer_size_ (MAC length) to allow space for encryption
 | 
			
		||||
  noise_buffer_set_inout(mbuf, buf_start + msg_offset, msg_len, msg_len + frame_footer_size_);
 | 
			
		||||
  err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
@@ -516,11 +610,13 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  size_t total_len = 3 + mbuf.size;
 | 
			
		||||
  tmpbuf[1] = (uint8_t) (mbuf.size >> 8);
 | 
			
		||||
  tmpbuf[2] = (uint8_t) mbuf.size;
 | 
			
		||||
  buf_start[1] = (uint8_t) (mbuf.size >> 8);
 | 
			
		||||
  buf_start[2] = (uint8_t) mbuf.size;
 | 
			
		||||
 | 
			
		||||
  struct iovec iov;
 | 
			
		||||
  iov.iov_base = &tmpbuf[0];
 | 
			
		||||
  // Point iov_base to the beginning of the buffer (no unused padding in Noise)
 | 
			
		||||
  // We send the entire frame: indicator + size + encrypted(type + data_len + payload + MAC)
 | 
			
		||||
  iov.iov_base = buf_start;
 | 
			
		||||
  iov.iov_len = total_len;
 | 
			
		||||
 | 
			
		||||
  // write raw to not have two packets sent if NAGLE disabled
 | 
			
		||||
@@ -546,71 +642,6 @@ APIError APINoiseFrameHelper::try_send_tx_buf_() {
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
/** Write the data to the socket, or buffer it a write would block
 | 
			
		||||
 *
 | 
			
		||||
 * @param data The data to write
 | 
			
		||||
 * @param len The length of data
 | 
			
		||||
 */
 | 
			
		||||
APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
 | 
			
		||||
  if (iovcnt == 0)
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  size_t total_write_len = 0;
 | 
			
		||||
  for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
    ESP_LOGVV(TAG, "Sending raw: %s",
 | 
			
		||||
              format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
 | 
			
		||||
#endif
 | 
			
		||||
    total_write_len += iov[i].iov_len;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    // try to empty tx_buf_ first
 | 
			
		||||
    aerr = try_send_tx_buf_();
 | 
			
		||||
    if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
 | 
			
		||||
      return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    // tx buf not empty, can't write now because then stream would be inconsistent
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
 | 
			
		||||
                     reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ssize_t sent = socket_->writev(iov, iovcnt);
 | 
			
		||||
  if (is_would_block(sent)) {
 | 
			
		||||
    // operation would block, add buffer to tx_buf
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
 | 
			
		||||
                     reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  } else if (sent == -1) {
 | 
			
		||||
    // an error occurred
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Socket write failed with errno %d", errno);
 | 
			
		||||
    return APIError::SOCKET_WRITE_FAILED;
 | 
			
		||||
  } else if ((size_t) sent != total_write_len) {
 | 
			
		||||
    // partially sent, add end to tx_buf
 | 
			
		||||
    size_t to_consume = sent;
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      if (to_consume >= iov[i].iov_len) {
 | 
			
		||||
        to_consume -= iov[i].iov_len;
 | 
			
		||||
      } else {
 | 
			
		||||
        tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
 | 
			
		||||
                       reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
        to_consume = 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
  // fully sent
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
 | 
			
		||||
  uint8_t header[3];
 | 
			
		||||
  header[0] = 0x01;  // indicator
 | 
			
		||||
@@ -697,6 +728,8 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
 | 
			
		||||
    return APIError::HANDSHAKESTATE_SPLIT_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
 | 
			
		||||
 | 
			
		||||
  HELPER_LOG("Handshake complete!");
 | 
			
		||||
  noise_handshakestate_free(handshake_);
 | 
			
		||||
  handshake_ = nullptr;
 | 
			
		||||
@@ -744,6 +777,11 @@ void noise_rand_bytes(void *output, size_t len) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Explicit template instantiation for Noise
 | 
			
		||||
template APIError APIFrameHelper::write_raw_<APINoiseFrameHelper::State>(
 | 
			
		||||
    const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
 | 
			
		||||
    APINoiseFrameHelper::State &state, APINoiseFrameHelper::State failed_state);
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_PLAINTEXT
 | 
			
		||||
@@ -804,6 +842,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
  // read header
 | 
			
		||||
  while (!rx_header_parsed_) {
 | 
			
		||||
    uint8_t data;
 | 
			
		||||
    // Reading one byte at a time is fastest in practice for ESP32 when
 | 
			
		||||
    // there is no data on the wire (which is the common case).
 | 
			
		||||
    // This results in faster failure detection compared to
 | 
			
		||||
    // attempting to read multiple bytes at once.
 | 
			
		||||
    ssize_t received = socket_->read(&data, 1);
 | 
			
		||||
    if (received == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN) {
 | 
			
		||||
@@ -817,27 +859,60 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
      HELPER_LOG("Connection closed");
 | 
			
		||||
      return APIError::CONNECTION_CLOSED;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_buf_.push_back(data);
 | 
			
		||||
 | 
			
		||||
    // try parse header
 | 
			
		||||
    if (rx_header_buf_[0] != 0x00) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
 | 
			
		||||
      return APIError::BAD_INDICATOR;
 | 
			
		||||
    // Successfully read a byte
 | 
			
		||||
 | 
			
		||||
    // Process byte according to current buffer position
 | 
			
		||||
    if (rx_header_buf_pos_ == 0) {  // Case 1: First byte (indicator byte)
 | 
			
		||||
      if (data != 0x00) {
 | 
			
		||||
        state_ = State::FAILED;
 | 
			
		||||
        HELPER_LOG("Bad indicator byte %u", data);
 | 
			
		||||
        return APIError::BAD_INDICATOR;
 | 
			
		||||
      }
 | 
			
		||||
      // We don't store the indicator byte, just increment position
 | 
			
		||||
      rx_header_buf_pos_ = 1;  // Set to 1 directly
 | 
			
		||||
      continue;                // Need more bytes before we can parse
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    size_t i = 1;
 | 
			
		||||
    // Check buffer overflow before storing
 | 
			
		||||
    if (rx_header_buf_pos_ == 5) {  // Case 2: Buffer would overflow (5 bytes is max allowed)
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Header buffer overflow");
 | 
			
		||||
      return APIError::BAD_DATA_PACKET;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Store byte in buffer (adjust index to account for skipped indicator byte)
 | 
			
		||||
    rx_header_buf_[rx_header_buf_pos_ - 1] = data;
 | 
			
		||||
 | 
			
		||||
    // Increment position after storing
 | 
			
		||||
    rx_header_buf_pos_++;
 | 
			
		||||
 | 
			
		||||
    // Case 3: If we only have one varint byte, we need more
 | 
			
		||||
    if (rx_header_buf_pos_ == 2) {  // Have read indicator + 1 byte
 | 
			
		||||
      continue;                     // Need more bytes before we can parse
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // At this point, we have at least 3 bytes total:
 | 
			
		||||
    //   - Validated indicator byte (0x00) but not stored
 | 
			
		||||
    //   - At least 2 bytes in the buffer for the varints
 | 
			
		||||
    // Buffer layout:
 | 
			
		||||
    //   First 1-3 bytes: Message size varint (variable length)
 | 
			
		||||
    //     - 2 bytes would only allow up to 16383, which is less than noise's 65535
 | 
			
		||||
    //     - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
 | 
			
		||||
    //   Remaining 1-2 bytes: Message type varint (variable length)
 | 
			
		||||
    // We now attempt to parse both varints. If either is incomplete,
 | 
			
		||||
    // we'll continue reading more bytes.
 | 
			
		||||
 | 
			
		||||
    uint32_t consumed = 0;
 | 
			
		||||
    auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
 | 
			
		||||
    auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[0], rx_header_buf_pos_ - 1, &consumed);
 | 
			
		||||
    if (!msg_size_varint.has_value()) {
 | 
			
		||||
      // not enough data there yet
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    i += consumed;
 | 
			
		||||
    rx_header_parsed_len_ = msg_size_varint->as_uint32();
 | 
			
		||||
 | 
			
		||||
    auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
 | 
			
		||||
    auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[consumed], rx_header_buf_pos_ - 1 - consumed, &consumed);
 | 
			
		||||
    if (!msg_type_varint.has_value()) {
 | 
			
		||||
      // not enough data there yet
 | 
			
		||||
      continue;
 | 
			
		||||
@@ -883,7 +958,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
  // consume msg
 | 
			
		||||
  rx_buf_ = {};
 | 
			
		||||
  rx_buf_len_ = 0;
 | 
			
		||||
  rx_header_buf_.clear();
 | 
			
		||||
  rx_header_buf_pos_ = 0;
 | 
			
		||||
  rx_header_parsed_ = false;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
@@ -927,26 +1002,66 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
 | 
			
		||||
APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
 | 
			
		||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> header;
 | 
			
		||||
  header.push_back(0x00);
 | 
			
		||||
  ProtoVarInt(payload_len).encode(header);
 | 
			
		||||
  ProtoVarInt(type).encode(header);
 | 
			
		||||
  std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
 | 
			
		||||
  // Message data starts after padding (frame_header_padding_ = 6)
 | 
			
		||||
  size_t payload_len = raw_buffer->size() - frame_header_padding_;
 | 
			
		||||
 | 
			
		||||
  struct iovec iov[2];
 | 
			
		||||
  iov[0].iov_base = &header[0];
 | 
			
		||||
  iov[0].iov_len = header.size();
 | 
			
		||||
  if (payload_len == 0) {
 | 
			
		||||
    return write_raw_(iov, 1);
 | 
			
		||||
  // Calculate varint sizes for header components
 | 
			
		||||
  size_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(payload_len));
 | 
			
		||||
  size_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(type));
 | 
			
		||||
  size_t total_header_len = 1 + size_varint_len + type_varint_len;
 | 
			
		||||
 | 
			
		||||
  if (total_header_len > frame_header_padding_) {
 | 
			
		||||
    // Header is too large to fit in the padding
 | 
			
		||||
    return APIError::BAD_ARG;
 | 
			
		||||
  }
 | 
			
		||||
  iov[1].iov_base = const_cast<uint8_t *>(payload);
 | 
			
		||||
  iov[1].iov_len = payload_len;
 | 
			
		||||
 | 
			
		||||
  return write_raw_(iov, 2);
 | 
			
		||||
  // Calculate where to start writing the header
 | 
			
		||||
  // The header starts at the latest possible position to minimize unused padding
 | 
			
		||||
  //
 | 
			
		||||
  // Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
 | 
			
		||||
  // [0-2]  - Unused padding
 | 
			
		||||
  // [3]    - 0x00 indicator byte
 | 
			
		||||
  // [4]    - Payload size varint (1 byte, for sizes 0-127)
 | 
			
		||||
  // [5]    - Message type varint (1 byte, for types 0-127)
 | 
			
		||||
  // [6...] - Actual payload data
 | 
			
		||||
  //
 | 
			
		||||
  // Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
 | 
			
		||||
  // [0-1]  - Unused padding
 | 
			
		||||
  // [2]    - 0x00 indicator byte
 | 
			
		||||
  // [3-4]  - Payload size varint (2 bytes, for sizes 128-16383)
 | 
			
		||||
  // [5]    - Message type varint (1 byte, for types 0-127)
 | 
			
		||||
  // [6...] - Actual payload data
 | 
			
		||||
  //
 | 
			
		||||
  // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
 | 
			
		||||
  // [0]    - 0x00 indicator byte
 | 
			
		||||
  // [1-3]  - Payload size varint (3 bytes, for sizes 16384-2097151)
 | 
			
		||||
  // [4-5]  - Message type varint (2 bytes, for types 128-32767)
 | 
			
		||||
  // [6...] - Actual payload data
 | 
			
		||||
  uint8_t *buf_start = raw_buffer->data();
 | 
			
		||||
  size_t header_offset = frame_header_padding_ - total_header_len;
 | 
			
		||||
 | 
			
		||||
  // Write the plaintext header
 | 
			
		||||
  buf_start[header_offset] = 0x00;  // indicator
 | 
			
		||||
 | 
			
		||||
  // Encode size varint directly into buffer
 | 
			
		||||
  ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
 | 
			
		||||
 | 
			
		||||
  // Encode type varint directly into buffer
 | 
			
		||||
  ProtoVarInt(type).encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
 | 
			
		||||
 | 
			
		||||
  struct iovec iov;
 | 
			
		||||
  // Point iov_base to the beginning of our header (skip unused padding)
 | 
			
		||||
  // This ensures we only send the actual header and payload, not the empty padding bytes
 | 
			
		||||
  iov.iov_base = buf_start + header_offset;
 | 
			
		||||
  iov.iov_len = total_header_len + payload_len;
 | 
			
		||||
 | 
			
		||||
  return write_raw_(&iov, 1);
 | 
			
		||||
}
 | 
			
		||||
APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
 | 
			
		||||
  // try send from tx_buf
 | 
			
		||||
@@ -966,71 +1081,6 @@ APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
/** Write the data to the socket, or buffer it a write would block
 | 
			
		||||
 *
 | 
			
		||||
 * @param data The data to write
 | 
			
		||||
 * @param len The length of data
 | 
			
		||||
 */
 | 
			
		||||
APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
 | 
			
		||||
  if (iovcnt == 0)
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  size_t total_write_len = 0;
 | 
			
		||||
  for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
    ESP_LOGVV(TAG, "Sending raw: %s",
 | 
			
		||||
              format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
 | 
			
		||||
#endif
 | 
			
		||||
    total_write_len += iov[i].iov_len;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    // try to empty tx_buf_ first
 | 
			
		||||
    aerr = try_send_tx_buf_();
 | 
			
		||||
    if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
 | 
			
		||||
      return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    // tx buf not empty, can't write now because then stream would be inconsistent
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
 | 
			
		||||
                     reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ssize_t sent = socket_->writev(iov, iovcnt);
 | 
			
		||||
  if (is_would_block(sent)) {
 | 
			
		||||
    // operation would block, add buffer to tx_buf
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
 | 
			
		||||
                     reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  } else if (sent == -1) {
 | 
			
		||||
    // an error occurred
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Socket write failed with errno %d", errno);
 | 
			
		||||
    return APIError::SOCKET_WRITE_FAILED;
 | 
			
		||||
  } else if ((size_t) sent != total_write_len) {
 | 
			
		||||
    // partially sent, add end to tx_buf
 | 
			
		||||
    size_t to_consume = sent;
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      if (to_consume >= iov[i].iov_len) {
 | 
			
		||||
        to_consume -= iov[i].iov_len;
 | 
			
		||||
      } else {
 | 
			
		||||
        tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
 | 
			
		||||
                       reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
        to_consume = 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
  // fully sent
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APIPlaintextFrameHelper::close() {
 | 
			
		||||
  state_ = State::CLOSED;
 | 
			
		||||
@@ -1048,6 +1098,11 @@ APIError APIPlaintextFrameHelper::shutdown(int how) {
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Explicit template instantiation for Plaintext
 | 
			
		||||
template APIError APIFrameHelper::write_raw_<APIPlaintextFrameHelper::State>(
 | 
			
		||||
    const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
 | 
			
		||||
    APIPlaintextFrameHelper::State &state, APIPlaintextFrameHelper::State failed_state);
 | 
			
		||||
#endif  // USE_API_PLAINTEXT
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,8 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
class ProtoWriteBuffer;
 | 
			
		||||
 | 
			
		||||
struct ReadPacketBuffer {
 | 
			
		||||
  std::vector<uint8_t> container;
 | 
			
		||||
  uint16_t type;
 | 
			
		||||
@@ -65,26 +67,46 @@ class APIFrameHelper {
 | 
			
		||||
  virtual APIError loop() = 0;
 | 
			
		||||
  virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
 | 
			
		||||
  virtual bool can_write_without_blocking() = 0;
 | 
			
		||||
  virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0;
 | 
			
		||||
  virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
 | 
			
		||||
  virtual std::string getpeername() = 0;
 | 
			
		||||
  virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0;
 | 
			
		||||
  virtual APIError close() = 0;
 | 
			
		||||
  virtual APIError shutdown(int how) = 0;
 | 
			
		||||
  // Give this helper a name for logging
 | 
			
		||||
  virtual void set_log_info(std::string info) = 0;
 | 
			
		||||
  // Get the frame header padding required by this protocol
 | 
			
		||||
  virtual uint8_t frame_header_padding() = 0;
 | 
			
		||||
  // Get the frame footer size required by this protocol
 | 
			
		||||
  virtual uint8_t frame_footer_size() = 0;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  // Common implementation for writing raw data to socket
 | 
			
		||||
  template<typename StateEnum>
 | 
			
		||||
  APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
 | 
			
		||||
                      const std::string &info, StateEnum &state, StateEnum failed_state);
 | 
			
		||||
 | 
			
		||||
  uint8_t frame_header_padding_{0};
 | 
			
		||||
  uint8_t frame_footer_size_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
class APINoiseFrameHelper : public APIFrameHelper {
 | 
			
		||||
 public:
 | 
			
		||||
  APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
 | 
			
		||||
      : socket_(std::move(socket)), ctx_(std::move(std::move(ctx))) {}
 | 
			
		||||
      : socket_(std::move(socket)), ctx_(std::move(ctx)) {
 | 
			
		||||
    // Noise header structure:
 | 
			
		||||
    // Pos 0: indicator (0x01)
 | 
			
		||||
    // Pos 1-2: encrypted payload size (16-bit big-endian)
 | 
			
		||||
    // Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
 | 
			
		||||
    // Pos 7+: actual payload data
 | 
			
		||||
    frame_header_padding_ = 7;
 | 
			
		||||
  }
 | 
			
		||||
  ~APINoiseFrameHelper() override;
 | 
			
		||||
  APIError init() override;
 | 
			
		||||
  APIError loop() override;
 | 
			
		||||
  APIError read_packet(ReadPacketBuffer *buffer) override;
 | 
			
		||||
  bool can_write_without_blocking() override;
 | 
			
		||||
  APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
 | 
			
		||||
  APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
 | 
			
		||||
  std::string getpeername() override { return this->socket_->getpeername(); }
 | 
			
		||||
  int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
 | 
			
		||||
    return this->socket_->getpeername(addr, addrlen);
 | 
			
		||||
@@ -93,6 +115,10 @@ class APINoiseFrameHelper : public APIFrameHelper {
 | 
			
		||||
  APIError shutdown(int how) override;
 | 
			
		||||
  // Give this helper a name for logging
 | 
			
		||||
  void set_log_info(std::string info) override { info_ = std::move(info); }
 | 
			
		||||
  // Get the frame header padding required by this protocol
 | 
			
		||||
  uint8_t frame_header_padding() override { return frame_header_padding_; }
 | 
			
		||||
  // Get the frame footer size required by this protocol
 | 
			
		||||
  uint8_t frame_footer_size() override { return frame_footer_size_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  struct ParsedFrame {
 | 
			
		||||
@@ -103,7 +129,9 @@ class APINoiseFrameHelper : public APIFrameHelper {
 | 
			
		||||
  APIError try_read_frame_(ParsedFrame *frame);
 | 
			
		||||
  APIError try_send_tx_buf_();
 | 
			
		||||
  APIError write_frame_(const uint8_t *data, size_t len);
 | 
			
		||||
  APIError write_raw_(const struct iovec *iov, int iovcnt);
 | 
			
		||||
  inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
 | 
			
		||||
    return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
 | 
			
		||||
  }
 | 
			
		||||
  APIError init_handshake_();
 | 
			
		||||
  APIError check_handshake_finished_();
 | 
			
		||||
  void send_explicit_handshake_reject_(const std::string &reason);
 | 
			
		||||
@@ -111,6 +139,9 @@ class APINoiseFrameHelper : public APIFrameHelper {
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_;
 | 
			
		||||
 | 
			
		||||
  std::string info_;
 | 
			
		||||
  // Fixed-size header buffer for noise protocol:
 | 
			
		||||
  // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
 | 
			
		||||
  // Note: Maximum message size is 65535, with a limit of 128 bytes during handshake phase
 | 
			
		||||
  uint8_t rx_header_buf_[3];
 | 
			
		||||
  size_t rx_header_buf_len_ = 0;
 | 
			
		||||
  std::vector<uint8_t> rx_buf_;
 | 
			
		||||
@@ -141,13 +172,20 @@ class APINoiseFrameHelper : public APIFrameHelper {
 | 
			
		||||
#ifdef USE_API_PLAINTEXT
 | 
			
		||||
class APIPlaintextFrameHelper : public APIFrameHelper {
 | 
			
		||||
 public:
 | 
			
		||||
  APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
 | 
			
		||||
  APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {
 | 
			
		||||
    // Plaintext header structure (worst case):
 | 
			
		||||
    // Pos 0: indicator (0x00)
 | 
			
		||||
    // Pos 1-3: payload size varint (up to 3 bytes)
 | 
			
		||||
    // Pos 4-5: message type varint (up to 2 bytes)
 | 
			
		||||
    // Pos 6+: actual payload data
 | 
			
		||||
    frame_header_padding_ = 6;
 | 
			
		||||
  }
 | 
			
		||||
  ~APIPlaintextFrameHelper() override = default;
 | 
			
		||||
  APIError init() override;
 | 
			
		||||
  APIError loop() override;
 | 
			
		||||
  APIError read_packet(ReadPacketBuffer *buffer) override;
 | 
			
		||||
  bool can_write_without_blocking() override;
 | 
			
		||||
  APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
 | 
			
		||||
  APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
 | 
			
		||||
  std::string getpeername() override { return this->socket_->getpeername(); }
 | 
			
		||||
  int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
 | 
			
		||||
    return this->socket_->getpeername(addr, addrlen);
 | 
			
		||||
@@ -156,6 +194,10 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
 | 
			
		||||
  APIError shutdown(int how) override;
 | 
			
		||||
  // Give this helper a name for logging
 | 
			
		||||
  void set_log_info(std::string info) override { info_ = std::move(info); }
 | 
			
		||||
  // Get the frame header padding required by this protocol
 | 
			
		||||
  uint8_t frame_header_padding() override { return frame_header_padding_; }
 | 
			
		||||
  // Get the frame footer size required by this protocol
 | 
			
		||||
  uint8_t frame_footer_size() override { return frame_footer_size_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  struct ParsedFrame {
 | 
			
		||||
@@ -164,12 +206,23 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
 | 
			
		||||
 | 
			
		||||
  APIError try_read_frame_(ParsedFrame *frame);
 | 
			
		||||
  APIError try_send_tx_buf_();
 | 
			
		||||
  APIError write_raw_(const struct iovec *iov, int iovcnt);
 | 
			
		||||
  inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
 | 
			
		||||
    return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_;
 | 
			
		||||
 | 
			
		||||
  std::string info_;
 | 
			
		||||
  std::vector<uint8_t> rx_header_buf_;
 | 
			
		||||
  // Fixed-size header buffer for plaintext protocol:
 | 
			
		||||
  // We only need space for the two varints since we validate the indicator byte separately.
 | 
			
		||||
  // To match noise protocol's maximum message size (65535), we need:
 | 
			
		||||
  // 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
 | 
			
		||||
  //
 | 
			
		||||
  // While varints could theoretically be up to 10 bytes each for 64-bit values,
 | 
			
		||||
  // attempting to process messages with headers that large would likely crash the
 | 
			
		||||
  // ESP32 due to memory constraints.
 | 
			
		||||
  uint8_t rx_header_buf_[5];  // 5 bytes for varints (3 for size + 2 for type)
 | 
			
		||||
  uint8_t rx_header_buf_pos_ = 0;
 | 
			
		||||
  bool rx_header_parsed_ = false;
 | 
			
		||||
  uint32_t rx_header_parsed_type_ = 0;
 | 
			
		||||
  uint32_t rx_header_parsed_len_ = 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -11,11 +11,20 @@ using psk_t = std::array<uint8_t, 32>;
 | 
			
		||||
 | 
			
		||||
class APINoiseContext {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_psk(psk_t psk) { psk_ = psk; }
 | 
			
		||||
  const psk_t &get_psk() const { return psk_; }
 | 
			
		||||
  void set_psk(psk_t psk) {
 | 
			
		||||
    this->psk_ = psk;
 | 
			
		||||
    bool has_psk = false;
 | 
			
		||||
    for (auto i : psk) {
 | 
			
		||||
      has_psk |= i;
 | 
			
		||||
    }
 | 
			
		||||
    this->has_psk_ = has_psk;
 | 
			
		||||
  }
 | 
			
		||||
  const psk_t &get_psk() const { return this->psk_; }
 | 
			
		||||
  bool has_psk() const { return this->has_psk_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  psk_t psk_;
 | 
			
		||||
  psk_t psk_{};
 | 
			
		||||
  bool has_psk_{false};
 | 
			
		||||
};
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,5 +1,5 @@
 | 
			
		||||
// This file was automatically generated with a tool.
 | 
			
		||||
// See scripts/api_protobuf/api_protobuf.py
 | 
			
		||||
// See script/api_protobuf/api_protobuf.py
 | 
			
		||||
#include "api_pb2_service.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
@@ -179,6 +179,16 @@ bool APIServerConnectionBase::send_text_sensor_state_response(const TextSensorSt
 | 
			
		||||
bool APIServerConnectionBase::send_subscribe_logs_response(const SubscribeLogsResponse &msg) {
 | 
			
		||||
  return this->send_message_<SubscribeLogsResponse>(msg, 29);
 | 
			
		||||
}
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
bool APIServerConnectionBase::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_noise_encryption_set_key_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<NoiseEncryptionSetKeyResponse>(msg, 125);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
bool APIServerConnectionBase::send_homeassistant_service_response(const HomeassistantServiceResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_homeassistant_service_response: %s", msg.dump().c_str());
 | 
			
		||||
@@ -282,6 +292,24 @@ bool APIServerConnectionBase::send_select_state_response(const SelectStateRespon
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SIREN
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_siren_response(const ListEntitiesSirenResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_list_entities_siren_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<ListEntitiesSirenResponse>(msg, 55);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SIREN
 | 
			
		||||
bool APIServerConnectionBase::send_siren_state_response(const SirenStateResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_siren_state_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<SirenStateResponse>(msg, 56);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SIREN
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
bool APIServerConnectionBase::send_list_entities_lock_response(const ListEntitiesLockResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
@@ -462,6 +490,16 @@ bool APIServerConnectionBase::send_bluetooth_device_clear_cache_response(const B
 | 
			
		||||
  return this->send_message_<BluetoothDeviceClearCacheResponse>(msg, 88);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
bool APIServerConnectionBase::send_bluetooth_scanner_state_response(const BluetoothScannerStateResponse &msg) {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  ESP_LOGVV(TAG, "send_bluetooth_scanner_state_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
  return this->send_message_<BluetoothScannerStateResponse>(msg, 126);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
@@ -883,6 +921,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
      ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_select_command_request(msg);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 57: {
 | 
			
		||||
#ifdef USE_SIREN
 | 
			
		||||
      SirenCommandRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_siren_command_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_siren_command_request(msg);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
@@ -1191,6 +1240,28 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
      ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_voice_assistant_set_configuration(msg);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 124: {
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
      NoiseEncryptionSetKeyRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_noise_encryption_set_key_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_noise_encryption_set_key_request(msg);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case 127: {
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
      BluetoothScannerSetModeRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_bluetooth_scanner_set_mode_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_bluetooth_scanner_set_mode_request(msg);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
@@ -1311,8 +1382,8 @@ void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest
 | 
			
		||||
  }
 | 
			
		||||
  this->execute_service(msg);
 | 
			
		||||
}
 | 
			
		||||
#ifdef USE_COVER
 | 
			
		||||
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) {
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
@@ -1321,11 +1392,14 @@ void APIServerConnection::on_cover_command_request(const CoverCommandRequest &ms
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->cover_command(msg);
 | 
			
		||||
  NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg);
 | 
			
		||||
  if (!this->send_noise_encryption_set_key_response(ret)) {
 | 
			
		||||
    this->on_fatal_error();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
@@ -1334,33 +1408,7 @@ void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->fan_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->light_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->switch_command(msg);
 | 
			
		||||
  this->button_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
@@ -1389,8 +1437,8 @@ void APIServerConnection::on_climate_command_request(const ClimateCommandRequest
 | 
			
		||||
  this->climate_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) {
 | 
			
		||||
#ifdef USE_COVER
 | 
			
		||||
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
@@ -1399,85 +1447,7 @@ void APIServerConnection::on_number_command_request(const NumberCommandRequest &
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->number_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT
 | 
			
		||||
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->text_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->select_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->button_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->lock_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VALVE
 | 
			
		||||
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->valve_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->media_player_command(msg);
 | 
			
		||||
  this->cover_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_DATE
 | 
			
		||||
@@ -1493,19 +1463,6 @@ void APIServerConnection::on_date_command_request(const DateCommandRequest &msg)
 | 
			
		||||
  this->date_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_TIME
 | 
			
		||||
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->time_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_DATETIME
 | 
			
		||||
void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
@@ -1519,6 +1476,136 @@ void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequ
 | 
			
		||||
  this->datetime_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->fan_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->light_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->lock_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->media_player_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->number_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->select_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SIREN
 | 
			
		||||
void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->siren_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->switch_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT
 | 
			
		||||
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->text_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_TIME
 | 
			
		||||
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->time_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_UPDATE
 | 
			
		||||
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
@@ -1532,6 +1619,19 @@ void APIServerConnection::on_update_command_request(const UpdateCommandRequest &
 | 
			
		||||
  this->update_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VALVE
 | 
			
		||||
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->valve_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
 | 
			
		||||
    const SubscribeBluetoothLEAdvertisementsRequest &msg) {
 | 
			
		||||
@@ -1668,6 +1768,19 @@ void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request(
 | 
			
		||||
  this->unsubscribe_bluetooth_le_advertisements(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->bluetooth_scanner_set_mode(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
// This file was automatically generated with a tool.
 | 
			
		||||
// See scripts/api_protobuf/api_protobuf.py
 | 
			
		||||
// See script/api_protobuf/api_protobuf.py
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "api_pb2.h"
 | 
			
		||||
@@ -83,6 +83,12 @@ class APIServerConnectionBase : public ProtoService {
 | 
			
		||||
#endif
 | 
			
		||||
  virtual void on_subscribe_logs_request(const SubscribeLogsRequest &value){};
 | 
			
		||||
  bool send_subscribe_logs_response(const SubscribeLogsResponse &msg);
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyResponse &msg);
 | 
			
		||||
#endif
 | 
			
		||||
  virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
 | 
			
		||||
  bool send_homeassistant_service_response(const HomeassistantServiceResponse &msg);
 | 
			
		||||
  virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
 | 
			
		||||
@@ -130,6 +136,15 @@ class APIServerConnectionBase : public ProtoService {
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  virtual void on_select_command_request(const SelectCommandRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SIREN
 | 
			
		||||
  bool send_list_entities_siren_response(const ListEntitiesSirenResponse &msg);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SIREN
 | 
			
		||||
  bool send_siren_state_response(const SirenStateResponse &msg);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SIREN
 | 
			
		||||
  virtual void on_siren_command_request(const SirenCommandRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
  bool send_list_entities_lock_response(const ListEntitiesLockResponse &msg);
 | 
			
		||||
#endif
 | 
			
		||||
@@ -228,6 +243,12 @@ class APIServerConnectionBase : public ProtoService {
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
  bool send_bluetooth_device_clear_cache_response(const BluetoothDeviceClearCacheResponse &msg);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
  bool send_bluetooth_scanner_state_response(const BluetoothScannerStateResponse &msg);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
  virtual void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
  virtual void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
@@ -349,17 +370,11 @@ class APIServerConnection : public APIServerConnectionBase {
 | 
			
		||||
  virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
 | 
			
		||||
  virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0;
 | 
			
		||||
  virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
 | 
			
		||||
#ifdef USE_COVER
 | 
			
		||||
  virtual void cover_command(const CoverCommandRequest &msg) = 0;
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
  virtual void fan_command(const FanCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
  virtual void light_command(const LightCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  virtual void switch_command(const SwitchCommandRequest &msg) = 0;
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
  virtual void button_command(const ButtonCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
  virtual void camera_image(const CameraImageRequest &msg) = 0;
 | 
			
		||||
@@ -367,39 +382,51 @@ class APIServerConnection : public APIServerConnectionBase {
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
  virtual void climate_command(const ClimateCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  virtual void number_command(const NumberCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT
 | 
			
		||||
  virtual void text_command(const TextCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  virtual void select_command(const SelectCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
  virtual void button_command(const ButtonCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
  virtual void lock_command(const LockCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VALVE
 | 
			
		||||
  virtual void valve_command(const ValveCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0;
 | 
			
		||||
#ifdef USE_COVER
 | 
			
		||||
  virtual void cover_command(const CoverCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_DATE
 | 
			
		||||
  virtual void date_command(const DateCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_TIME
 | 
			
		||||
  virtual void time_command(const TimeCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_DATETIME
 | 
			
		||||
  virtual void datetime_command(const DateTimeCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
  virtual void fan_command(const FanCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
  virtual void light_command(const LightCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
  virtual void lock_command(const LockCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  virtual void number_command(const NumberCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  virtual void select_command(const SelectCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SIREN
 | 
			
		||||
  virtual void siren_command(const SirenCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  virtual void switch_command(const SwitchCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT
 | 
			
		||||
  virtual void text_command(const TextCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_TIME
 | 
			
		||||
  virtual void time_command(const TimeCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_UPDATE
 | 
			
		||||
  virtual void update_command(const UpdateCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VALVE
 | 
			
		||||
  virtual void valve_command(const ValveCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
  virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
@@ -431,6 +458,9 @@ class APIServerConnection : public APIServerConnectionBase {
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
  virtual void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
  virtual void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
  virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
@@ -457,17 +487,11 @@ class APIServerConnection : public APIServerConnectionBase {
 | 
			
		||||
  void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
 | 
			
		||||
  void on_get_time_request(const GetTimeRequest &msg) override;
 | 
			
		||||
  void on_execute_service_request(const ExecuteServiceRequest &msg) override;
 | 
			
		||||
#ifdef USE_COVER
 | 
			
		||||
  void on_cover_command_request(const CoverCommandRequest &msg) override;
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
  void on_fan_command_request(const FanCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
  void on_light_command_request(const LightCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  void on_switch_command_request(const SwitchCommandRequest &msg) override;
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
  void on_button_command_request(const ButtonCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
  void on_camera_image_request(const CameraImageRequest &msg) override;
 | 
			
		||||
@@ -475,39 +499,51 @@ class APIServerConnection : public APIServerConnectionBase {
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
  void on_climate_command_request(const ClimateCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  void on_number_command_request(const NumberCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT
 | 
			
		||||
  void on_text_command_request(const TextCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  void on_select_command_request(const SelectCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
  void on_button_command_request(const ButtonCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
  void on_lock_command_request(const LockCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VALVE
 | 
			
		||||
  void on_valve_command_request(const ValveCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
 | 
			
		||||
#ifdef USE_COVER
 | 
			
		||||
  void on_cover_command_request(const CoverCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_DATE
 | 
			
		||||
  void on_date_command_request(const DateCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_TIME
 | 
			
		||||
  void on_time_command_request(const TimeCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_DATETIME
 | 
			
		||||
  void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
  void on_fan_command_request(const FanCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
  void on_light_command_request(const LightCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
  void on_lock_command_request(const LockCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  void on_number_command_request(const NumberCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  void on_select_command_request(const SelectCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SIREN
 | 
			
		||||
  void on_siren_command_request(const SirenCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  void on_switch_command_request(const SwitchCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT
 | 
			
		||||
  void on_text_command_request(const TextCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_TIME
 | 
			
		||||
  void on_time_command_request(const TimeCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_UPDATE
 | 
			
		||||
  void on_update_command_request(const UpdateCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VALVE
 | 
			
		||||
  void on_valve_command_request(const ValveCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
  void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
@@ -539,6 +575,9 @@ class APIServerConnection : public APIServerConnectionBase {
 | 
			
		||||
  void on_unsubscribe_bluetooth_le_advertisements_request(
 | 
			
		||||
      const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
  void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
  void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										361
									
								
								esphome/components/api/api_pb2_size.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										361
									
								
								esphome/components/api/api_pb2_size.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,361 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "proto.h"
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
class ProtoSize {
 | 
			
		||||
 public:
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief ProtoSize class for Protocol Buffer serialization size calculation
 | 
			
		||||
   *
 | 
			
		||||
   * This class provides static methods to calculate the exact byte counts needed
 | 
			
		||||
   * for encoding various Protocol Buffer field types. All methods are designed to be
 | 
			
		||||
   * efficient for the common case where many fields have default values.
 | 
			
		||||
   *
 | 
			
		||||
   * Implements Protocol Buffer encoding size calculation according to:
 | 
			
		||||
   * https://protobuf.dev/programming-guides/encoding/
 | 
			
		||||
   *
 | 
			
		||||
   * Key features:
 | 
			
		||||
   * - Early-return optimization for zero/default values
 | 
			
		||||
   * - Direct total_size updates to avoid unnecessary additions
 | 
			
		||||
   * - Specialized handling for different field types according to protobuf spec
 | 
			
		||||
   * - Templated helpers for repeated fields and messages
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
 | 
			
		||||
   *
 | 
			
		||||
   * @param value The uint32_t value to calculate size for
 | 
			
		||||
   * @return The number of bytes needed to encode the value
 | 
			
		||||
   */
 | 
			
		||||
  static inline uint32_t varint(uint32_t value) {
 | 
			
		||||
    // Optimized varint size calculation using leading zeros
 | 
			
		||||
    // Each 7 bits requires one byte in the varint encoding
 | 
			
		||||
    if (value < 128)
 | 
			
		||||
      return 1;  // 7 bits, common case for small values
 | 
			
		||||
 | 
			
		||||
    // For larger values, count bytes needed based on the position of the highest bit set
 | 
			
		||||
    if (value < 16384) {
 | 
			
		||||
      return 2;  // 14 bits
 | 
			
		||||
    } else if (value < 2097152) {
 | 
			
		||||
      return 3;  // 21 bits
 | 
			
		||||
    } else if (value < 268435456) {
 | 
			
		||||
      return 4;  // 28 bits
 | 
			
		||||
    } else {
 | 
			
		||||
      return 5;  // 32 bits (maximum for uint32_t)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates the size in bytes needed to encode a uint64_t value as a varint
 | 
			
		||||
   *
 | 
			
		||||
   * @param value The uint64_t value to calculate size for
 | 
			
		||||
   * @return The number of bytes needed to encode the value
 | 
			
		||||
   */
 | 
			
		||||
  static inline uint32_t varint(uint64_t value) {
 | 
			
		||||
    // Handle common case of values fitting in uint32_t (vast majority of use cases)
 | 
			
		||||
    if (value <= UINT32_MAX) {
 | 
			
		||||
      return varint(static_cast<uint32_t>(value));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // For larger values, determine size based on highest bit position
 | 
			
		||||
    if (value < (1ULL << 35)) {
 | 
			
		||||
      return 5;  // 35 bits
 | 
			
		||||
    } else if (value < (1ULL << 42)) {
 | 
			
		||||
      return 6;  // 42 bits
 | 
			
		||||
    } else if (value < (1ULL << 49)) {
 | 
			
		||||
      return 7;  // 49 bits
 | 
			
		||||
    } else if (value < (1ULL << 56)) {
 | 
			
		||||
      return 8;  // 56 bits
 | 
			
		||||
    } else if (value < (1ULL << 63)) {
 | 
			
		||||
      return 9;  // 63 bits
 | 
			
		||||
    } else {
 | 
			
		||||
      return 10;  // 64 bits (maximum for uint64_t)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates the size in bytes needed to encode an int32_t value as a varint
 | 
			
		||||
   *
 | 
			
		||||
   * Special handling is needed for negative values, which are sign-extended to 64 bits
 | 
			
		||||
   * in Protocol Buffers, resulting in a 10-byte varint.
 | 
			
		||||
   *
 | 
			
		||||
   * @param value The int32_t value to calculate size for
 | 
			
		||||
   * @return The number of bytes needed to encode the value
 | 
			
		||||
   */
 | 
			
		||||
  static inline uint32_t varint(int32_t value) {
 | 
			
		||||
    // Negative values are sign-extended to 64 bits in protocol buffers,
 | 
			
		||||
    // which always results in a 10-byte varint for negative int32
 | 
			
		||||
    if (value < 0) {
 | 
			
		||||
      return 10;  // Negative int32 is always 10 bytes long
 | 
			
		||||
    }
 | 
			
		||||
    // For non-negative values, use the uint32_t implementation
 | 
			
		||||
    return varint(static_cast<uint32_t>(value));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates the size in bytes needed to encode an int64_t value as a varint
 | 
			
		||||
   *
 | 
			
		||||
   * @param value The int64_t value to calculate size for
 | 
			
		||||
   * @return The number of bytes needed to encode the value
 | 
			
		||||
   */
 | 
			
		||||
  static inline uint32_t varint(int64_t value) {
 | 
			
		||||
    // For int64_t, we convert to uint64_t and calculate the size
 | 
			
		||||
    // This works because the bit pattern determines the encoding size,
 | 
			
		||||
    // and we've handled negative int32 values as a special case above
 | 
			
		||||
    return varint(static_cast<uint64_t>(value));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates the size in bytes needed to encode a field ID and wire type
 | 
			
		||||
   *
 | 
			
		||||
   * @param field_id The field identifier
 | 
			
		||||
   * @param type The wire type value (from the WireType enum in the protobuf spec)
 | 
			
		||||
   * @return The number of bytes needed to encode the field ID and wire type
 | 
			
		||||
   */
 | 
			
		||||
  static inline uint32_t field(uint32_t field_id, uint32_t type) {
 | 
			
		||||
    uint32_t tag = (field_id << 3) | (type & 0b111);
 | 
			
		||||
    return varint(tag);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Common parameters for all add_*_field methods
 | 
			
		||||
   *
 | 
			
		||||
   * All add_*_field methods follow these common patterns:
 | 
			
		||||
   *
 | 
			
		||||
   * @param total_size Reference to the total message size to update
 | 
			
		||||
   * @param field_id_size Pre-calculated size of the field ID in bytes
 | 
			
		||||
   * @param value The value to calculate size for (type varies)
 | 
			
		||||
   * @param force Whether to calculate size even if the value is default/zero/empty
 | 
			
		||||
   *
 | 
			
		||||
   * Each method follows this implementation pattern:
 | 
			
		||||
   * 1. Skip calculation if value is default (0, false, empty) and not forced
 | 
			
		||||
   * 2. Calculate the size based on the field's encoding rules
 | 
			
		||||
   * 3. Add the field_id_size + calculated value size to total_size
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates and adds the size of an int32 field to the total message size
 | 
			
		||||
   */
 | 
			
		||||
  static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) {
 | 
			
		||||
    // Skip calculation if value is zero and not forced
 | 
			
		||||
    if (value == 0 && !force) {
 | 
			
		||||
      return;  // No need to update total_size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Calculate and directly add to total_size
 | 
			
		||||
    if (value < 0) {
 | 
			
		||||
      // Negative values are encoded as 10-byte varints in protobuf
 | 
			
		||||
      total_size += field_id_size + 10;
 | 
			
		||||
    } else {
 | 
			
		||||
      // For non-negative values, use the standard varint size
 | 
			
		||||
      total_size += field_id_size + varint(static_cast<uint32_t>(value));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates and adds the size of a uint32 field to the total message size
 | 
			
		||||
   */
 | 
			
		||||
  static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value,
 | 
			
		||||
                                      bool force = false) {
 | 
			
		||||
    // Skip calculation if value is zero and not forced
 | 
			
		||||
    if (value == 0 && !force) {
 | 
			
		||||
      return;  // No need to update total_size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Calculate and directly add to total_size
 | 
			
		||||
    total_size += field_id_size + varint(value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates and adds the size of a boolean field to the total message size
 | 
			
		||||
   */
 | 
			
		||||
  static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value, bool force = false) {
 | 
			
		||||
    // Skip calculation if value is false and not forced
 | 
			
		||||
    if (!value && !force) {
 | 
			
		||||
      return;  // No need to update total_size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Boolean fields always use 1 byte when true
 | 
			
		||||
    total_size += field_id_size + 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates and adds the size of a fixed field to the total message size
 | 
			
		||||
   *
 | 
			
		||||
   * Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double).
 | 
			
		||||
   *
 | 
			
		||||
   * @tparam NumBytes The number of bytes for this fixed field (4 or 8)
 | 
			
		||||
   * @param is_nonzero Whether the value is non-zero
 | 
			
		||||
   */
 | 
			
		||||
  template<uint32_t NumBytes>
 | 
			
		||||
  static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero,
 | 
			
		||||
                                     bool force = false) {
 | 
			
		||||
    // Skip calculation if value is zero and not forced
 | 
			
		||||
    if (!is_nonzero && !force) {
 | 
			
		||||
      return;  // No need to update total_size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Fixed fields always take exactly NumBytes
 | 
			
		||||
    total_size += field_id_size + NumBytes;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates and adds the size of an enum field to the total message size
 | 
			
		||||
   *
 | 
			
		||||
   * Enum fields are encoded as uint32 varints.
 | 
			
		||||
   */
 | 
			
		||||
  static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value, bool force = false) {
 | 
			
		||||
    // Skip calculation if value is zero and not forced
 | 
			
		||||
    if (value == 0 && !force) {
 | 
			
		||||
      return;  // No need to update total_size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Enums are encoded as uint32
 | 
			
		||||
    total_size += field_id_size + varint(value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates and adds the size of a sint32 field to the total message size
 | 
			
		||||
   *
 | 
			
		||||
   * Sint32 fields use ZigZag encoding, which is more efficient for negative values.
 | 
			
		||||
   */
 | 
			
		||||
  static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) {
 | 
			
		||||
    // Skip calculation if value is zero and not forced
 | 
			
		||||
    if (value == 0 && !force) {
 | 
			
		||||
      return;  // No need to update total_size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
 | 
			
		||||
    uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
 | 
			
		||||
    total_size += field_id_size + varint(zigzag);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates and adds the size of an int64 field to the total message size
 | 
			
		||||
   */
 | 
			
		||||
  static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) {
 | 
			
		||||
    // Skip calculation if value is zero and not forced
 | 
			
		||||
    if (value == 0 && !force) {
 | 
			
		||||
      return;  // No need to update total_size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Calculate and directly add to total_size
 | 
			
		||||
    total_size += field_id_size + varint(value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates and adds the size of a uint64 field to the total message size
 | 
			
		||||
   */
 | 
			
		||||
  static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value,
 | 
			
		||||
                                      bool force = false) {
 | 
			
		||||
    // Skip calculation if value is zero and not forced
 | 
			
		||||
    if (value == 0 && !force) {
 | 
			
		||||
      return;  // No need to update total_size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Calculate and directly add to total_size
 | 
			
		||||
    total_size += field_id_size + varint(value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates and adds the size of a sint64 field to the total message size
 | 
			
		||||
   *
 | 
			
		||||
   * Sint64 fields use ZigZag encoding, which is more efficient for negative values.
 | 
			
		||||
   */
 | 
			
		||||
  static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) {
 | 
			
		||||
    // Skip calculation if value is zero and not forced
 | 
			
		||||
    if (value == 0 && !force) {
 | 
			
		||||
      return;  // No need to update total_size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // ZigZag encoding for sint64: (n << 1) ^ (n >> 63)
 | 
			
		||||
    uint64_t zigzag = (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63));
 | 
			
		||||
    total_size += field_id_size + varint(zigzag);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates and adds the size of a string/bytes field to the total message size
 | 
			
		||||
   */
 | 
			
		||||
  static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str,
 | 
			
		||||
                                      bool force = false) {
 | 
			
		||||
    // Skip calculation if string is empty and not forced
 | 
			
		||||
    if (str.empty() && !force) {
 | 
			
		||||
      return;  // No need to update total_size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Calculate and directly add to total_size
 | 
			
		||||
    const uint32_t str_size = static_cast<uint32_t>(str.size());
 | 
			
		||||
    total_size += field_id_size + varint(str_size) + str_size;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates and adds the size of a nested message field to the total message size
 | 
			
		||||
   *
 | 
			
		||||
   * This helper function directly updates the total_size reference if the nested size
 | 
			
		||||
   * is greater than zero or force is true.
 | 
			
		||||
   *
 | 
			
		||||
   * @param nested_size The pre-calculated size of the nested message
 | 
			
		||||
   */
 | 
			
		||||
  static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size,
 | 
			
		||||
                                       bool force = false) {
 | 
			
		||||
    // Skip calculation if nested message is empty and not forced
 | 
			
		||||
    if (nested_size == 0 && !force) {
 | 
			
		||||
      return;  // No need to update total_size
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Calculate and directly add to total_size
 | 
			
		||||
    // Field ID + length varint + nested message content
 | 
			
		||||
    total_size += field_id_size + varint(nested_size) + nested_size;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates and adds the size of a nested message field to the total message size
 | 
			
		||||
   *
 | 
			
		||||
   * This templated version directly takes a message object, calculates its size internally,
 | 
			
		||||
   * and updates the total_size reference. This eliminates the need for a temporary variable
 | 
			
		||||
   * at the call site.
 | 
			
		||||
   *
 | 
			
		||||
   * @tparam MessageType The type of the nested message (inferred from parameter)
 | 
			
		||||
   * @param message The nested message object
 | 
			
		||||
   */
 | 
			
		||||
  template<typename MessageType>
 | 
			
		||||
  static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const MessageType &message,
 | 
			
		||||
                                        bool force = false) {
 | 
			
		||||
    uint32_t nested_size = 0;
 | 
			
		||||
    message.calculate_size(nested_size);
 | 
			
		||||
 | 
			
		||||
    // Use the base implementation with the calculated nested_size
 | 
			
		||||
    add_message_field(total_size, field_id_size, nested_size, force);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @brief Calculates and adds the sizes of all messages in a repeated field to the total message size
 | 
			
		||||
   *
 | 
			
		||||
   * This helper processes a vector of message objects, calculating the size for each message
 | 
			
		||||
   * and adding it to the total size.
 | 
			
		||||
   *
 | 
			
		||||
   * @tparam MessageType The type of the nested messages in the vector
 | 
			
		||||
   * @param messages Vector of message objects
 | 
			
		||||
   */
 | 
			
		||||
  template<typename MessageType>
 | 
			
		||||
  static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size,
 | 
			
		||||
                                          const std::vector<MessageType> &messages) {
 | 
			
		||||
    // Skip if the vector is empty
 | 
			
		||||
    if (messages.empty()) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // For repeated fields, always use force=true
 | 
			
		||||
    for (const auto &message : messages) {
 | 
			
		||||
      add_message_object(total_size, field_id_size, message, true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -22,22 +22,40 @@ namespace api {
 | 
			
		||||
static const char *const TAG = "api";
 | 
			
		||||
 | 
			
		||||
// APIServer
 | 
			
		||||
APIServer *global_api_server = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 | 
			
		||||
APIServer::APIServer() { global_api_server = this; }
 | 
			
		||||
 | 
			
		||||
void APIServer::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up Home Assistant API server...");
 | 
			
		||||
  this->setup_controller();
 | 
			
		||||
  socket_ = socket::socket_ip(SOCK_STREAM, 0);
 | 
			
		||||
  if (socket_ == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "Could not create socket.");
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  uint32_t hash = 88491486UL;
 | 
			
		||||
 | 
			
		||||
  this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(hash, true);
 | 
			
		||||
 | 
			
		||||
  SavedNoisePsk noise_pref_saved{};
 | 
			
		||||
  if (this->noise_pref_.load(&noise_pref_saved)) {
 | 
			
		||||
    ESP_LOGD(TAG, "Loaded saved Noise PSK");
 | 
			
		||||
 | 
			
		||||
    this->set_noise_psk(noise_pref_saved.psk);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  this->socket_ = socket::socket_ip(SOCK_STREAM, 0);
 | 
			
		||||
  if (this->socket_ == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "Could not create socket");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  int err = socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
 | 
			
		||||
  int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
 | 
			
		||||
    // we can still continue
 | 
			
		||||
  }
 | 
			
		||||
  err = socket_->setblocking(false);
 | 
			
		||||
  err = this->socket_->setblocking(false);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
@@ -53,14 +71,14 @@ void APIServer::setup() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  err = socket_->bind((struct sockaddr *) &server, sl);
 | 
			
		||||
  err = this->socket_->bind((struct sockaddr *) &server, sl);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  err = socket_->listen(4);
 | 
			
		||||
  err = this->socket_->listen(4);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
@@ -92,34 +110,45 @@ void APIServer::setup() {
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIServer::loop() {
 | 
			
		||||
  // Accept new clients
 | 
			
		||||
  while (true) {
 | 
			
		||||
    struct sockaddr_storage source_addr;
 | 
			
		||||
    socklen_t addr_len = sizeof(source_addr);
 | 
			
		||||
    auto sock = socket_->accept((struct sockaddr *) &source_addr, &addr_len);
 | 
			
		||||
    auto sock = this->socket_->accept((struct sockaddr *) &source_addr, &addr_len);
 | 
			
		||||
    if (!sock)
 | 
			
		||||
      break;
 | 
			
		||||
    ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str());
 | 
			
		||||
 | 
			
		||||
    auto *conn = new APIConnection(std::move(sock), this);
 | 
			
		||||
    clients_.emplace_back(conn);
 | 
			
		||||
    this->clients_.emplace_back(conn);
 | 
			
		||||
    conn->start();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Partition clients into remove and active
 | 
			
		||||
  auto new_end = std::partition(this->clients_.begin(), this->clients_.end(),
 | 
			
		||||
                                [](const std::unique_ptr<APIConnection> &conn) { return !conn->remove_; });
 | 
			
		||||
  // print disconnection messages
 | 
			
		||||
  for (auto it = new_end; it != this->clients_.end(); ++it) {
 | 
			
		||||
    this->client_disconnected_trigger_->trigger((*it)->client_info_, (*it)->client_peername_);
 | 
			
		||||
    ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str());
 | 
			
		||||
  }
 | 
			
		||||
  // resize vector
 | 
			
		||||
  this->clients_.erase(new_end, this->clients_.end());
 | 
			
		||||
  // Process clients and remove disconnected ones in a single pass
 | 
			
		||||
  if (!this->clients_.empty()) {
 | 
			
		||||
    size_t client_index = 0;
 | 
			
		||||
    while (client_index < this->clients_.size()) {
 | 
			
		||||
      auto &client = this->clients_[client_index];
 | 
			
		||||
 | 
			
		||||
  for (auto &client : this->clients_) {
 | 
			
		||||
    client->loop();
 | 
			
		||||
      if (client->remove_) {
 | 
			
		||||
        // Handle disconnection
 | 
			
		||||
        this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_);
 | 
			
		||||
        ESP_LOGV(TAG, "Removing connection to %s", client->client_info_.c_str());
 | 
			
		||||
 | 
			
		||||
        // Swap with the last element and pop (avoids expensive vector shifts)
 | 
			
		||||
        if (client_index < this->clients_.size() - 1) {
 | 
			
		||||
          std::swap(this->clients_[client_index], this->clients_.back());
 | 
			
		||||
        }
 | 
			
		||||
        this->clients_.pop_back();
 | 
			
		||||
        // Don't increment client_index since we need to process the swapped element
 | 
			
		||||
      } else {
 | 
			
		||||
        // Process active client
 | 
			
		||||
        client->loop();
 | 
			
		||||
        client_index++;  // Move to next client
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->reboot_timeout_ != 0) {
 | 
			
		||||
@@ -136,16 +165,22 @@ void APIServer::loop() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIServer::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "API Server:");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Address: %s:%u", network::get_use_address().c_str(), this->port_);
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Using noise encryption: YES");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Using noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
 | 
			
		||||
  if (!this->noise_ctx_->has_psk()) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Supports noise encryption: YES");
 | 
			
		||||
  }
 | 
			
		||||
#else
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Using noise encryption: NO");
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool APIServer::uses_password() const { return !this->password_.empty(); }
 | 
			
		||||
 | 
			
		||||
bool APIServer::check_password(const std::string &password) const {
 | 
			
		||||
  // depend only on input password length
 | 
			
		||||
  const char *a = this->password_.c_str();
 | 
			
		||||
@@ -174,7 +209,9 @@ bool APIServer::check_password(const std::string &password) const {
 | 
			
		||||
 | 
			
		||||
  return result == 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIServer::handle_disconnect(APIConnection *conn) {}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
@@ -342,57 +379,6 @@ void APIServer::on_update(update::UpdateEntity *obj) {
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
 | 
			
		||||
void APIServer::set_port(uint16_t port) { this->port_ = port; }
 | 
			
		||||
APIServer *global_api_server = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 | 
			
		||||
void APIServer::set_password(const std::string &password) { this->password_ = password; }
 | 
			
		||||
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
 | 
			
		||||
  for (auto &client : this->clients_) {
 | 
			
		||||
    client->send_homeassistant_service_call(call);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIServer::APIServer() { global_api_server = this; }
 | 
			
		||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
 | 
			
		||||
                                               std::function<void(std::string)> f) {
 | 
			
		||||
  this->state_subs_.push_back(HomeAssistantStateSubscription{
 | 
			
		||||
      .entity_id = std::move(entity_id),
 | 
			
		||||
      .attribute = std::move(attribute),
 | 
			
		||||
      .callback = std::move(f),
 | 
			
		||||
      .once = false,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
 | 
			
		||||
                                         std::function<void(std::string)> f) {
 | 
			
		||||
  this->state_subs_.push_back(HomeAssistantStateSubscription{
 | 
			
		||||
      .entity_id = std::move(entity_id),
 | 
			
		||||
      .attribute = std::move(attribute),
 | 
			
		||||
      .callback = std::move(f),
 | 
			
		||||
      .once = true,
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
 | 
			
		||||
  return this->state_subs_;
 | 
			
		||||
}
 | 
			
		||||
uint16_t APIServer::get_port() const { return this->port_; }
 | 
			
		||||
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
 | 
			
		||||
#ifdef USE_HOMEASSISTANT_TIME
 | 
			
		||||
void APIServer::request_time() {
 | 
			
		||||
  for (auto &client : this->clients_) {
 | 
			
		||||
    if (!client->remove_ && client->is_authenticated())
 | 
			
		||||
      client->send_time_request();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
bool APIServer::is_connected() const { return !this->clients_.empty(); }
 | 
			
		||||
void APIServer::on_shutdown() {
 | 
			
		||||
  for (auto &c : this->clients_) {
 | 
			
		||||
    c->send_disconnect_request(DisconnectRequest());
 | 
			
		||||
  }
 | 
			
		||||
  delay(10);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ALARM_CONTROL_PANEL
 | 
			
		||||
void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
@@ -402,6 +388,96 @@ void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
 | 
			
		||||
 | 
			
		||||
void APIServer::set_port(uint16_t port) { this->port_ = port; }
 | 
			
		||||
 | 
			
		||||
void APIServer::set_password(const std::string &password) { this->password_ = password; }
 | 
			
		||||
 | 
			
		||||
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
 | 
			
		||||
  for (auto &client : this->clients_) {
 | 
			
		||||
    client->send_homeassistant_service_call(call);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
 | 
			
		||||
                                               std::function<void(std::string)> f) {
 | 
			
		||||
  this->state_subs_.push_back(HomeAssistantStateSubscription{
 | 
			
		||||
      .entity_id = std::move(entity_id),
 | 
			
		||||
      .attribute = std::move(attribute),
 | 
			
		||||
      .callback = std::move(f),
 | 
			
		||||
      .once = false,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
 | 
			
		||||
                                         std::function<void(std::string)> f) {
 | 
			
		||||
  this->state_subs_.push_back(HomeAssistantStateSubscription{
 | 
			
		||||
      .entity_id = std::move(entity_id),
 | 
			
		||||
      .attribute = std::move(attribute),
 | 
			
		||||
      .callback = std::move(f),
 | 
			
		||||
      .once = true,
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
 | 
			
		||||
  return this->state_subs_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint16_t APIServer::get_port() const { return this->port_; }
 | 
			
		||||
 | 
			
		||||
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
 | 
			
		||||
  auto &old_psk = this->noise_ctx_->get_psk();
 | 
			
		||||
  if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) {
 | 
			
		||||
    ESP_LOGW(TAG, "New PSK matches old");
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  SavedNoisePsk new_saved_psk{psk};
 | 
			
		||||
  if (!this->noise_pref_.save(&new_saved_psk)) {
 | 
			
		||||
    ESP_LOGW(TAG, "Failed to save Noise PSK");
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  // ensure it's written immediately
 | 
			
		||||
  if (!global_preferences->sync()) {
 | 
			
		||||
    ESP_LOGW(TAG, "Failed to sync preferences");
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGD(TAG, "Noise PSK saved");
 | 
			
		||||
  if (make_active) {
 | 
			
		||||
    this->set_timeout(100, [this, psk]() {
 | 
			
		||||
      ESP_LOGW(TAG, "Disconnecting all clients to reset connections");
 | 
			
		||||
      this->set_noise_psk(psk);
 | 
			
		||||
      for (auto &c : this->clients_) {
 | 
			
		||||
        c->send_disconnect_request(DisconnectRequest());
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_HOMEASSISTANT_TIME
 | 
			
		||||
void APIServer::request_time() {
 | 
			
		||||
  for (auto &client : this->clients_) {
 | 
			
		||||
    if (!client->remove_ && client->is_authenticated())
 | 
			
		||||
      client->send_time_request();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
bool APIServer::is_connected() const { return !this->clients_.empty(); }
 | 
			
		||||
 | 
			
		||||
void APIServer::on_shutdown() {
 | 
			
		||||
  for (auto &c : this->clients_) {
 | 
			
		||||
    c->send_disconnect_request(DisconnectRequest());
 | 
			
		||||
  }
 | 
			
		||||
  delay(10);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,12 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
struct SavedNoisePsk {
 | 
			
		||||
  psk_t psk;
 | 
			
		||||
} PACKED;  // NOLINT
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
class APIServer : public Component, public Controller {
 | 
			
		||||
 public:
 | 
			
		||||
  APIServer();
 | 
			
		||||
@@ -35,6 +41,7 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
  void set_reboot_timeout(uint32_t reboot_timeout);
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  bool save_noise_psk(psk_t psk, bool make_active = true);
 | 
			
		||||
  void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); }
 | 
			
		||||
  std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
@@ -142,6 +149,7 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
 | 
			
		||||
  ESPPreferenceObject noise_pref_;
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -20,16 +20,26 @@ class ProtoVarInt {
 | 
			
		||||
  explicit ProtoVarInt(uint64_t value) : value_(value) {}
 | 
			
		||||
 | 
			
		||||
  static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
 | 
			
		||||
    if (consumed != nullptr)
 | 
			
		||||
      *consumed = 0;
 | 
			
		||||
 | 
			
		||||
    if (len == 0)
 | 
			
		||||
    if (len == 0) {
 | 
			
		||||
      if (consumed != nullptr)
 | 
			
		||||
        *consumed = 0;
 | 
			
		||||
      return {};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    uint64_t result = 0;
 | 
			
		||||
    uint8_t bitpos = 0;
 | 
			
		||||
    // Most common case: single-byte varint (values 0-127)
 | 
			
		||||
    if ((buffer[0] & 0x80) == 0) {
 | 
			
		||||
      if (consumed != nullptr)
 | 
			
		||||
        *consumed = 1;
 | 
			
		||||
      return ProtoVarInt(buffer[0]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (uint32_t i = 0; i < len; i++) {
 | 
			
		||||
    // General case for multi-byte varints
 | 
			
		||||
    // Since we know buffer[0]'s high bit is set, initialize with its value
 | 
			
		||||
    uint64_t result = buffer[0] & 0x7F;
 | 
			
		||||
    uint8_t bitpos = 7;
 | 
			
		||||
 | 
			
		||||
    // Start from the second byte since we've already processed the first
 | 
			
		||||
    for (uint32_t i = 1; i < len; i++) {
 | 
			
		||||
      uint8_t val = buffer[i];
 | 
			
		||||
      result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
 | 
			
		||||
      bitpos += 7;
 | 
			
		||||
@@ -40,7 +50,9 @@ class ProtoVarInt {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {};
 | 
			
		||||
    if (consumed != nullptr)
 | 
			
		||||
      *consumed = 0;
 | 
			
		||||
    return {};  // Incomplete or invalid varint
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint32_t as_uint32() const { return this->value_; }
 | 
			
		||||
@@ -71,6 +83,34 @@ class ProtoVarInt {
 | 
			
		||||
      return static_cast<int64_t>(this->value_ >> 1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * Encode the varint value to a pre-allocated buffer without bounds checking.
 | 
			
		||||
   *
 | 
			
		||||
   * @param buffer The pre-allocated buffer to write the encoded varint to
 | 
			
		||||
   * @param len The size of the buffer in bytes
 | 
			
		||||
   *
 | 
			
		||||
   * @note The caller is responsible for ensuring the buffer is large enough
 | 
			
		||||
   *       to hold the encoded value. Use ProtoSize::varint() to calculate
 | 
			
		||||
   *       the exact size needed before calling this method.
 | 
			
		||||
   * @note No bounds checking is performed for performance reasons.
 | 
			
		||||
   */
 | 
			
		||||
  void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) {
 | 
			
		||||
    uint64_t val = this->value_;
 | 
			
		||||
    if (val <= 0x7F) {
 | 
			
		||||
      buffer[0] = val;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    size_t i = 0;
 | 
			
		||||
    while (val && i < len) {
 | 
			
		||||
      uint8_t temp = val & 0x7F;
 | 
			
		||||
      val >>= 7;
 | 
			
		||||
      if (val) {
 | 
			
		||||
        buffer[i++] = temp | 0x80;
 | 
			
		||||
      } else {
 | 
			
		||||
        buffer[i++] = temp;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  void encode(std::vector<uint8_t> &out) {
 | 
			
		||||
    uint64_t val = this->value_;
 | 
			
		||||
    if (val <= 0x7F) {
 | 
			
		||||
@@ -149,6 +189,18 @@ class ProtoWriteBuffer {
 | 
			
		||||
  void write(uint8_t value) { this->buffer_->push_back(value); }
 | 
			
		||||
  void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
 | 
			
		||||
  void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
 | 
			
		||||
  /**
 | 
			
		||||
   * Encode a field key (tag/wire type combination).
 | 
			
		||||
   *
 | 
			
		||||
   * @param field_id Field number (tag) in the protobuf message
 | 
			
		||||
   * @param type Wire type value:
 | 
			
		||||
   *   - 0: Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum)
 | 
			
		||||
   *   - 1: 64-bit (fixed64, sfixed64, double)
 | 
			
		||||
   *   - 2: Length-delimited (string, bytes, embedded messages, packed repeated fields)
 | 
			
		||||
   *   - 5: 32-bit (fixed32, sfixed32, float)
 | 
			
		||||
   *
 | 
			
		||||
   * Following https://protobuf.dev/programming-guides/encoding/#structure
 | 
			
		||||
   */
 | 
			
		||||
  void encode_field_raw(uint32_t field_id, uint32_t type) {
 | 
			
		||||
    uint32_t val = (field_id << 3) | (type & 0b111);
 | 
			
		||||
    this->encode_varint_raw(val);
 | 
			
		||||
@@ -157,7 +209,7 @@ class ProtoWriteBuffer {
 | 
			
		||||
    if (len == 0 && !force)
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
    this->encode_field_raw(field_id, 2);
 | 
			
		||||
    this->encode_field_raw(field_id, 2);  // type 2: Length-delimited string
 | 
			
		||||
    this->encode_varint_raw(len);
 | 
			
		||||
    auto *data = reinterpret_cast<const uint8_t *>(string);
 | 
			
		||||
    this->buffer_->insert(this->buffer_->end(), data, data + len);
 | 
			
		||||
@@ -171,26 +223,26 @@ class ProtoWriteBuffer {
 | 
			
		||||
  void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) {
 | 
			
		||||
    if (value == 0 && !force)
 | 
			
		||||
      return;
 | 
			
		||||
    this->encode_field_raw(field_id, 0);
 | 
			
		||||
    this->encode_field_raw(field_id, 0);  // type 0: Varint - uint32
 | 
			
		||||
    this->encode_varint_raw(value);
 | 
			
		||||
  }
 | 
			
		||||
  void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) {
 | 
			
		||||
    if (value == 0 && !force)
 | 
			
		||||
      return;
 | 
			
		||||
    this->encode_field_raw(field_id, 0);
 | 
			
		||||
    this->encode_field_raw(field_id, 0);  // type 0: Varint - uint64
 | 
			
		||||
    this->encode_varint_raw(ProtoVarInt(value));
 | 
			
		||||
  }
 | 
			
		||||
  void encode_bool(uint32_t field_id, bool value, bool force = false) {
 | 
			
		||||
    if (!value && !force)
 | 
			
		||||
      return;
 | 
			
		||||
    this->encode_field_raw(field_id, 0);
 | 
			
		||||
    this->encode_field_raw(field_id, 0);  // type 0: Varint - bool
 | 
			
		||||
    this->write(0x01);
 | 
			
		||||
  }
 | 
			
		||||
  void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
 | 
			
		||||
    if (value == 0 && !force)
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
    this->encode_field_raw(field_id, 5);
 | 
			
		||||
    this->encode_field_raw(field_id, 5);  // type 5: 32-bit fixed32
 | 
			
		||||
    this->write((value >> 0) & 0xFF);
 | 
			
		||||
    this->write((value >> 8) & 0xFF);
 | 
			
		||||
    this->write((value >> 16) & 0xFF);
 | 
			
		||||
@@ -200,7 +252,7 @@ class ProtoWriteBuffer {
 | 
			
		||||
    if (value == 0 && !force)
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
    this->encode_field_raw(field_id, 5);
 | 
			
		||||
    this->encode_field_raw(field_id, 1);  // type 1: 64-bit fixed64
 | 
			
		||||
    this->write((value >> 0) & 0xFF);
 | 
			
		||||
    this->write((value >> 8) & 0xFF);
 | 
			
		||||
    this->write((value >> 16) & 0xFF);
 | 
			
		||||
@@ -254,7 +306,7 @@ class ProtoWriteBuffer {
 | 
			
		||||
    this->encode_uint64(field_id, uvalue, force);
 | 
			
		||||
  }
 | 
			
		||||
  template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) {
 | 
			
		||||
    this->encode_field_raw(field_id, 2);
 | 
			
		||||
    this->encode_field_raw(field_id, 2);  // type 2: Length-delimited message
 | 
			
		||||
    size_t begin = this->buffer_->size();
 | 
			
		||||
 | 
			
		||||
    value.encode(*this);
 | 
			
		||||
@@ -276,6 +328,7 @@ class ProtoMessage {
 | 
			
		||||
  virtual ~ProtoMessage() = default;
 | 
			
		||||
  virtual void encode(ProtoWriteBuffer buffer) const = 0;
 | 
			
		||||
  void decode(const uint8_t *buffer, size_t length);
 | 
			
		||||
  virtual void calculate_size(uint32_t &total_size) const = 0;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  std::string dump() const;
 | 
			
		||||
  virtual void dump_to(std::string &out) const = 0;
 | 
			
		||||
@@ -298,13 +351,29 @@ class ProtoService {
 | 
			
		||||
  virtual void on_fatal_error() = 0;
 | 
			
		||||
  virtual void on_unauthenticated_access() = 0;
 | 
			
		||||
  virtual void on_no_setup_connection() = 0;
 | 
			
		||||
  virtual ProtoWriteBuffer create_buffer() = 0;
 | 
			
		||||
  /**
 | 
			
		||||
   * Create a buffer with a reserved size.
 | 
			
		||||
   * @param reserve_size The number of bytes to pre-allocate in the buffer. This is a hint
 | 
			
		||||
   *                     to optimize memory usage and avoid reallocations during encoding.
 | 
			
		||||
   *                     Implementations should aim to allocate at least this size.
 | 
			
		||||
   * @return A ProtoWriteBuffer object with the reserved size.
 | 
			
		||||
   */
 | 
			
		||||
  virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
 | 
			
		||||
  virtual bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) = 0;
 | 
			
		||||
  virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
 | 
			
		||||
 | 
			
		||||
  // Optimized method that pre-allocates buffer based on message size
 | 
			
		||||
  template<class C> bool send_message_(const C &msg, uint32_t message_type) {
 | 
			
		||||
    auto buffer = this->create_buffer();
 | 
			
		||||
    uint32_t msg_size = 0;
 | 
			
		||||
    msg.calculate_size(msg_size);
 | 
			
		||||
 | 
			
		||||
    // Create a pre-sized buffer
 | 
			
		||||
    auto buffer = this->create_buffer(msg_size);
 | 
			
		||||
 | 
			
		||||
    // Encode message into the buffer
 | 
			
		||||
    msg.encode(buffer);
 | 
			
		||||
 | 
			
		||||
    // Send the buffer
 | 
			
		||||
    return this->send_buffer(buffer, message_type);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,7 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/as3935/as3935.h"
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/binary_sensor/binary_sensor.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace as3935_i2c {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace as7341 {
 | 
			
		||||
 | 
			
		||||
static const uint8_t AS7341_CHIP_ID = 0X09;
 | 
			
		||||
static const uint8_t AS7341_CHIP_ID = 0x09;
 | 
			
		||||
 | 
			
		||||
static const uint8_t AS7341_CONFIG = 0x70;
 | 
			
		||||
static const uint8_t AS7341_LED = 0x74;
 | 
			
		||||
 
 | 
			
		||||
@@ -14,11 +14,8 @@ namespace esphome {
 | 
			
		||||
namespace at581x {
 | 
			
		||||
 | 
			
		||||
class AT581XComponent : public Component, public i2c::I2CDevice {
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
 protected:
 | 
			
		||||
  switch_::Switch *rf_power_switch_{nullptr};
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  void set_rf_power_switch(switch_::Switch *s) {
 | 
			
		||||
    this->rf_power_switch_ = s;
 | 
			
		||||
    s->turn_on();
 | 
			
		||||
@@ -48,6 +45,9 @@ class AT581XComponent : public Component, public i2c::I2CDevice {
 | 
			
		||||
  bool i2c_read_reg(uint8_t addr, uint8_t &data);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  switch_::Switch *rf_power_switch_{nullptr};
 | 
			
		||||
#endif
 | 
			
		||||
  int freq_;
 | 
			
		||||
  int self_check_time_ms_;   /*!< Power-on self-test time, range: 0 ~ 65536 ms */
 | 
			
		||||
  int protect_time_ms_;      /*!< Protection time, recommended 1000 ms */
 | 
			
		||||
 
 | 
			
		||||
@@ -3,5 +3,6 @@ import esphome.codegen as cg
 | 
			
		||||
CODEOWNERS = ["@circuitsetup", "@descipher"]
 | 
			
		||||
 | 
			
		||||
atm90e32_ns = cg.esphome_ns.namespace("atm90e32")
 | 
			
		||||
ATM90E32Component = atm90e32_ns.class_("ATM90E32Component", cg.Component)
 | 
			
		||||
 | 
			
		||||
CONF_ATM90E32_ID = "atm90e32_id"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
#include "atm90e32.h"
 | 
			
		||||
#include "atm90e32_reg.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include <cmath>
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace atm90e32 {
 | 
			
		||||
@@ -11,115 +11,84 @@ void ATM90E32Component::loop() {
 | 
			
		||||
  if (this->get_publish_interval_flag_()) {
 | 
			
		||||
    this->set_publish_interval_flag_(false);
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].voltage_sensor_ != nullptr) {
 | 
			
		||||
      if (this->phase_[phase].voltage_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].voltage_ = this->get_phase_voltage_(phase);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].current_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].current_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].current_ = this->get_phase_current_(phase);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].power_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].power_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].active_power_ = this->get_phase_active_power_(phase);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].power_factor_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].power_factor_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].power_factor_ = this->get_phase_power_factor_(phase);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].reactive_power_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].reactive_power_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].reactive_power_ = this->get_phase_reactive_power_(phase);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].apparent_power_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].apparent_power_ = this->get_phase_apparent_power_(phase);
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].forward_active_energy_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].forward_active_energy_ = this->get_phase_forward_active_energy_(phase);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].reverse_active_energy_ = this->get_phase_reverse_active_energy_(phase);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].phase_angle_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].phase_angle_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].phase_angle_ = this->get_phase_angle_(phase);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].harmonic_active_power_ = this->get_phase_harmonic_active_power_(phase);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].peak_current_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].peak_current_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].peak_current_ = this->get_phase_peak_current_(phase);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // After the local store in collected we can publish them trusting they are withing +-1 haardware sampling
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].voltage_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      // After the local store is collected we can publish them trusting they are within +-1 hardware sampling
 | 
			
		||||
      if (this->phase_[phase].voltage_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].voltage_sensor_->publish_state(this->get_local_phase_voltage_(phase));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].current_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].current_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].current_sensor_->publish_state(this->get_local_phase_current_(phase));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].power_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].power_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].power_sensor_->publish_state(this->get_local_phase_active_power_(phase));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].power_factor_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].power_factor_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].power_factor_sensor_->publish_state(this->get_local_phase_power_factor_(phase));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].reactive_power_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].reactive_power_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].reactive_power_sensor_->publish_state(this->get_local_phase_reactive_power_(phase));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].apparent_power_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].apparent_power_sensor_->publish_state(this->get_local_phase_apparent_power_(phase));
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) {
 | 
			
		||||
        this->phase_[phase].forward_active_energy_sensor_->publish_state(
 | 
			
		||||
            this->get_local_phase_forward_active_energy_(phase));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) {
 | 
			
		||||
        this->phase_[phase].reverse_active_energy_sensor_->publish_state(
 | 
			
		||||
            this->get_local_phase_reverse_active_energy_(phase));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].phase_angle_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].phase_angle_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].phase_angle_sensor_->publish_state(this->get_local_phase_angle_(phase));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) {
 | 
			
		||||
        this->phase_[phase].harmonic_active_power_sensor_->publish_state(
 | 
			
		||||
            this->get_local_phase_harmonic_active_power_(phase));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      if (this->phase_[phase].peak_current_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
      if (this->phase_[phase].peak_current_sensor_ != nullptr)
 | 
			
		||||
        this->phase_[phase].peak_current_sensor_->publish_state(this->get_local_phase_peak_current_(phase));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (this->freq_sensor_ != nullptr) {
 | 
			
		||||
    if (this->freq_sensor_ != nullptr)
 | 
			
		||||
      this->freq_sensor_->publish_state(this->get_frequency_());
 | 
			
		||||
    }
 | 
			
		||||
    if (this->chip_temperature_sensor_ != nullptr) {
 | 
			
		||||
 | 
			
		||||
    if (this->chip_temperature_sensor_ != nullptr)
 | 
			
		||||
      this->chip_temperature_sensor_->publish_state(this->get_chip_temperature_());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -130,82 +99,30 @@ void ATM90E32Component::update() {
 | 
			
		||||
  }
 | 
			
		||||
  this->set_publish_interval_flag_(true);
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::restore_calibrations_() {
 | 
			
		||||
  if (enable_offset_calibration_) {
 | 
			
		||||
    this->pref_.load(&this->offset_phase_);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::run_offset_calibrations() {
 | 
			
		||||
  // Run the calibrations and
 | 
			
		||||
  // Setup voltage and current calibration offsets for PHASE A
 | 
			
		||||
  this->offset_phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA);
 | 
			
		||||
  this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_;
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_);  // C Voltage offset
 | 
			
		||||
  this->offset_phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA);
 | 
			
		||||
  this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_;
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_);  // C Current offset
 | 
			
		||||
  // Setup voltage and current calibration offsets for PHASE B
 | 
			
		||||
  this->offset_phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB);
 | 
			
		||||
  this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_;
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_);  // C Voltage offset
 | 
			
		||||
  this->offset_phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB);
 | 
			
		||||
  this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_;
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_);  // C Current offset
 | 
			
		||||
  // Setup voltage and current calibration offsets for PHASE C
 | 
			
		||||
  this->offset_phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC);
 | 
			
		||||
  this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_;
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_);  // C Voltage offset
 | 
			
		||||
  this->offset_phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC);
 | 
			
		||||
  this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_;
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_);  // C Current offset
 | 
			
		||||
  this->pref_.save(&this->offset_phase_);
 | 
			
		||||
  ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_,
 | 
			
		||||
           this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_);
 | 
			
		||||
  ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_,
 | 
			
		||||
           this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::clear_offset_calibrations() {
 | 
			
		||||
  // Clear the calibrations and
 | 
			
		||||
  this->offset_phase_[PHASEA].voltage_offset_ = 0;
 | 
			
		||||
  this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_;
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_);  // C Voltage offset
 | 
			
		||||
  this->offset_phase_[PHASEA].current_offset_ = 0;
 | 
			
		||||
  this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_;
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_);  // C Current offset
 | 
			
		||||
  this->offset_phase_[PHASEB].voltage_offset_ = 0;
 | 
			
		||||
  this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_;
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_);  // C Voltage offset
 | 
			
		||||
  this->offset_phase_[PHASEB].current_offset_ = 0;
 | 
			
		||||
  this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_;
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_);  // C Current offset
 | 
			
		||||
  this->offset_phase_[PHASEC].voltage_offset_ = 0;
 | 
			
		||||
  this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_;
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_);  // C Voltage offset
 | 
			
		||||
  this->offset_phase_[PHASEC].current_offset_ = 0;
 | 
			
		||||
  this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_;
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_);  // C Current offset
 | 
			
		||||
  this->pref_.save(&this->offset_phase_);
 | 
			
		||||
  ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_,
 | 
			
		||||
           this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_);
 | 
			
		||||
  ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_,
 | 
			
		||||
           this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_);
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
  this->check_phase_status();
 | 
			
		||||
  this->check_over_current();
 | 
			
		||||
  this->check_freq_status();
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component...");
 | 
			
		||||
  this->spi_setup();
 | 
			
		||||
  if (this->enable_offset_calibration_) {
 | 
			
		||||
    uint32_t hash = fnv1_hash(App.get_friendly_name());
 | 
			
		||||
    this->pref_ = global_preferences->make_preference<Calibration[3]>(hash, true);
 | 
			
		||||
    this->restore_calibrations_();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint16_t mmode0 = 0x87;  // 3P4W 50Hz
 | 
			
		||||
  uint16_t high_thresh = 0;
 | 
			
		||||
  uint16_t low_thresh = 0;
 | 
			
		||||
 | 
			
		||||
  if (line_freq_ == 60) {
 | 
			
		||||
    mmode0 |= 1 << 12;  // sets 12th bit to 1, 60Hz
 | 
			
		||||
    // for freq threshold registers
 | 
			
		||||
    high_thresh = 6300;  // 63.00 Hz
 | 
			
		||||
    low_thresh = 5700;   // 57.00 Hz
 | 
			
		||||
  } else {
 | 
			
		||||
    high_thresh = 5300;  // 53.00 Hz
 | 
			
		||||
    low_thresh = 4700;   // 47.00 Hz
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (current_phases_ == 2) {
 | 
			
		||||
@@ -216,34 +133,84 @@ void ATM90E32Component::setup() {
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A);    // Perform soft reset
 | 
			
		||||
  delay(6);                                               // Wait for the minimum 5ms + 1ms
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);  // enable register config access
 | 
			
		||||
  if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != 0x55AA) {
 | 
			
		||||
  if (!this->validate_spi_read_(0x55AA, "setup()")) {
 | 
			
		||||
    ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_METEREN, 0x0001);        // Enable Metering
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_SAGPEAKDETCFG, 0xFF3F);  // Peak Detector time ms (15:8), Sag Period ms (7:0)
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_SAGPEAKDETCFG, 0xFF3F);  // Peak Detector time (15:8) 255ms, Sag Period (7:0) 63ms
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_PLCONSTH, 0x0861);       // PL Constant MSB (default) = 140625000
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_PLCONSTL, 0xC468);       // PL Constant LSB (default)
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654);       // ZX2, ZX1, ZX0 pin config
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654);       // Zero crossing (ZX2, ZX1, ZX0) pin config
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_MMODE0, mmode0);         // Mode Config (frequency set in main program)
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_);      // PGA Gain Configuration for Current Channels
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_FREQHITH, high_thresh);  // Frequency high threshold
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_FREQLOTH, low_thresh);   // Frequency low threshold
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x1D4C);       // All Active Startup Power Threshold - 0.02A/0.00032 = 7500
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x1D4C);       // All Reactive Startup Power Threshold - 50%
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C);       // All Reactive Startup Power Threshold - 50%
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE);       // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE);       // Each phase Reactive Phase Threshold - 10%
 | 
			
		||||
  // Setup voltage and current gain for PHASE A
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[PHASEA].voltage_gain_);  // A Voltage rms gain
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[PHASEA].ct_gain_);       // A line current gain
 | 
			
		||||
  // Setup voltage and current gain for PHASE B
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[PHASEB].voltage_gain_);  // B Voltage rms gain
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[PHASEB].ct_gain_);       // B line current gain
 | 
			
		||||
  // Setup voltage and current gain for PHASE C
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[PHASEC].voltage_gain_);  // C Voltage rms gain
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[PHASEC].ct_gain_);       // C line current gain
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);                         // end configuration
 | 
			
		||||
 | 
			
		||||
  if (this->enable_offset_calibration_) {
 | 
			
		||||
    // Initialize flash storage for offset calibrations
 | 
			
		||||
    uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_->dump_summary());
 | 
			
		||||
    this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
 | 
			
		||||
    this->restore_offset_calibrations_();
 | 
			
		||||
 | 
			
		||||
    // Initialize flash storage for power offset calibrations
 | 
			
		||||
    uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_->dump_summary());
 | 
			
		||||
    this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
 | 
			
		||||
    this->restore_power_offset_calibrations_();
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Power & Voltage/Current offset calibration is disabled. Using config file values.");
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
      this->write16_(this->voltage_offset_registers[phase],
 | 
			
		||||
                     static_cast<uint16_t>(this->offset_phase_[phase].voltage_offset_));
 | 
			
		||||
      this->write16_(this->current_offset_registers[phase],
 | 
			
		||||
                     static_cast<uint16_t>(this->offset_phase_[phase].current_offset_));
 | 
			
		||||
      this->write16_(this->power_offset_registers[phase],
 | 
			
		||||
                     static_cast<uint16_t>(this->power_offset_phase_[phase].active_power_offset));
 | 
			
		||||
      this->write16_(this->reactive_power_offset_registers[phase],
 | 
			
		||||
                     static_cast<uint16_t>(this->power_offset_phase_[phase].reactive_power_offset));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->enable_gain_calibration_) {
 | 
			
		||||
    // Initialize flash storage for gain calibration
 | 
			
		||||
    uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_->dump_summary());
 | 
			
		||||
    this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
 | 
			
		||||
    this->restore_gain_calibrations_();
 | 
			
		||||
 | 
			
		||||
    if (this->using_saved_calibrations_) {
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION] Successfully restored gain calibration from memory.");
 | 
			
		||||
    } else {
 | 
			
		||||
      for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
        this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
 | 
			
		||||
        this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Gain calibration is disabled. Using config file values.");
 | 
			
		||||
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
      this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
 | 
			
		||||
      this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Sag threshold (78%)
 | 
			
		||||
  uint16_t sagth = calculate_voltage_threshold(line_freq_, this->phase_[0].voltage_gain_, 0.78f);
 | 
			
		||||
  // Overvoltage threshold (122%)
 | 
			
		||||
  uint16_t ovth = calculate_voltage_threshold(line_freq_, this->phase_[0].voltage_gain_, 1.22f);
 | 
			
		||||
 | 
			
		||||
  // Write to registers
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_SAGTH, sagth);
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_OVTH, ovth);
 | 
			
		||||
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);  // end configuration
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::dump_config() {
 | 
			
		||||
@@ -257,6 +224,7 @@ void ATM90E32Component::dump_config() {
 | 
			
		||||
  LOG_SENSOR("  ", "Current A", this->phase_[PHASEA].current_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Power A", this->phase_[PHASEA].power_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Reactive Power A", this->phase_[PHASEA].reactive_power_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Apparent Power A", this->phase_[PHASEA].apparent_power_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "PF A", this->phase_[PHASEA].power_factor_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Active Forward Energy A", this->phase_[PHASEA].forward_active_energy_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Active Reverse Energy A", this->phase_[PHASEA].reverse_active_energy_sensor_);
 | 
			
		||||
@@ -267,22 +235,24 @@ void ATM90E32Component::dump_config() {
 | 
			
		||||
  LOG_SENSOR("  ", "Current B", this->phase_[PHASEB].current_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Power B", this->phase_[PHASEB].power_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Reactive Power B", this->phase_[PHASEB].reactive_power_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Apparent Power B", this->phase_[PHASEB].apparent_power_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "PF B", this->phase_[PHASEB].power_factor_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Active Forward Energy B", this->phase_[PHASEB].forward_active_energy_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Active Reverse Energy B", this->phase_[PHASEB].reverse_active_energy_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Harmonic Power A", this->phase_[PHASEB].harmonic_active_power_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Phase Angle A", this->phase_[PHASEB].phase_angle_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Peak Current A", this->phase_[PHASEB].peak_current_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Harmonic Power B", this->phase_[PHASEB].harmonic_active_power_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Phase Angle B", this->phase_[PHASEB].phase_angle_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Peak Current B", this->phase_[PHASEB].peak_current_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Voltage C", this->phase_[PHASEC].voltage_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Current C", this->phase_[PHASEC].current_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Power C", this->phase_[PHASEC].power_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Reactive Power C", this->phase_[PHASEC].reactive_power_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Apparent Power C", this->phase_[PHASEC].apparent_power_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "PF C", this->phase_[PHASEC].power_factor_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Active Forward Energy C", this->phase_[PHASEC].forward_active_energy_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Active Reverse Energy C", this->phase_[PHASEC].reverse_active_energy_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Harmonic Power A", this->phase_[PHASEC].harmonic_active_power_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Phase Angle A", this->phase_[PHASEC].phase_angle_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Peak Current A", this->phase_[PHASEC].peak_current_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Harmonic Power C", this->phase_[PHASEC].harmonic_active_power_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Phase Angle C", this->phase_[PHASEC].phase_angle_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Peak Current C", this->phase_[PHASEC].peak_current_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Frequency", this->freq_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Chip Temp", this->chip_temperature_sensor_);
 | 
			
		||||
}
 | 
			
		||||
@@ -298,7 +268,7 @@ uint16_t ATM90E32Component::read16_(uint16_t a_register) {
 | 
			
		||||
  uint8_t data[2];
 | 
			
		||||
  uint16_t output;
 | 
			
		||||
  this->enable();
 | 
			
		||||
  delay_microseconds_safe(10);
 | 
			
		||||
  delay_microseconds_safe(1);  // min delay between CS low and first SCK is 200ns - 1ms is plenty
 | 
			
		||||
  this->write_byte(addrh);
 | 
			
		||||
  this->write_byte(addrl);
 | 
			
		||||
  this->read_array(data, 2);
 | 
			
		||||
@@ -328,8 +298,7 @@ void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) {
 | 
			
		||||
  this->write_byte16(a_register);
 | 
			
		||||
  this->write_byte16(val);
 | 
			
		||||
  this->disable();
 | 
			
		||||
  if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != val)
 | 
			
		||||
    ESP_LOGW(TAG, "SPI write error 0x%04X val 0x%04X", a_register, val);
 | 
			
		||||
  this->validate_spi_read_(val, "write16()");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; }
 | 
			
		||||
@@ -340,6 +309,8 @@ float ATM90E32Component::get_local_phase_active_power_(uint8_t phase) { return t
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_local_phase_reactive_power_(uint8_t phase) { return this->phase_[phase].reactive_power_; }
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_local_phase_apparent_power_(uint8_t phase) { return this->phase_[phase].apparent_power_; }
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_local_phase_power_factor_(uint8_t phase) { return this->phase_[phase].power_factor_; }
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_local_phase_forward_active_energy_(uint8_t phase) {
 | 
			
		||||
@@ -360,8 +331,7 @@ float ATM90E32Component::get_local_phase_peak_current_(uint8_t phase) { return t
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_phase_voltage_(uint8_t phase) {
 | 
			
		||||
  const uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMS + phase);
 | 
			
		||||
  if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage)
 | 
			
		||||
    ESP_LOGW(TAG, "SPI URMS voltage register read error.");
 | 
			
		||||
  this->validate_spi_read_(voltage, "get_phase_voltage()");
 | 
			
		||||
  return (float) voltage / 100;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -371,8 +341,7 @@ float ATM90E32Component::get_phase_voltage_avg_(uint8_t phase) {
 | 
			
		||||
  uint16_t voltage = 0;
 | 
			
		||||
  for (uint8_t i = 0; i < reads; i++) {
 | 
			
		||||
    voltage = this->read16_(ATM90E32_REGISTER_URMS + phase);
 | 
			
		||||
    if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage)
 | 
			
		||||
      ESP_LOGW(TAG, "SPI URMS voltage register read error.");
 | 
			
		||||
    this->validate_spi_read_(voltage, "get_phase_voltage_avg_()");
 | 
			
		||||
    accumulation += voltage;
 | 
			
		||||
  }
 | 
			
		||||
  voltage = accumulation / reads;
 | 
			
		||||
@@ -386,8 +355,7 @@ float ATM90E32Component::get_phase_current_avg_(uint8_t phase) {
 | 
			
		||||
  uint16_t current = 0;
 | 
			
		||||
  for (uint8_t i = 0; i < reads; i++) {
 | 
			
		||||
    current = this->read16_(ATM90E32_REGISTER_IRMS + phase);
 | 
			
		||||
    if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current)
 | 
			
		||||
      ESP_LOGW(TAG, "SPI IRMS current register read error.");
 | 
			
		||||
    this->validate_spi_read_(current, "get_phase_current_avg_()");
 | 
			
		||||
    accumulation += current;
 | 
			
		||||
  }
 | 
			
		||||
  current = accumulation / reads;
 | 
			
		||||
@@ -397,8 +365,7 @@ float ATM90E32Component::get_phase_current_avg_(uint8_t phase) {
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_phase_current_(uint8_t phase) {
 | 
			
		||||
  const uint16_t current = this->read16_(ATM90E32_REGISTER_IRMS + phase);
 | 
			
		||||
  if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current)
 | 
			
		||||
    ESP_LOGW(TAG, "SPI IRMS current register read error.");
 | 
			
		||||
  this->validate_spi_read_(current, "get_phase_current_()");
 | 
			
		||||
  return (float) current / 1000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -412,11 +379,15 @@ float ATM90E32Component::get_phase_reactive_power_(uint8_t phase) {
 | 
			
		||||
  return val * 0.00032f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_phase_apparent_power_(uint8_t phase) {
 | 
			
		||||
  const int val = this->read32_(ATM90E32_REGISTER_SMEAN + phase, ATM90E32_REGISTER_SMEANLSB + phase);
 | 
			
		||||
  return val * 0.00032f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_phase_power_factor_(uint8_t phase) {
 | 
			
		||||
  const int16_t powerfactor = this->read16_(ATM90E32_REGISTER_PFMEAN + phase);
 | 
			
		||||
  if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != powerfactor)
 | 
			
		||||
    ESP_LOGW(TAG, "SPI power factor read error.");
 | 
			
		||||
  return (float) powerfactor / 1000;
 | 
			
		||||
  uint16_t powerfactor = this->read16_(ATM90E32_REGISTER_PFMEAN + phase);  // unsigned to compare to lastspidata
 | 
			
		||||
  this->validate_spi_read_(powerfactor, "get_phase_power_factor_()");
 | 
			
		||||
  return (float) ((int16_t) powerfactor) / 1000;  // make it signed again
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) {
 | 
			
		||||
@@ -426,17 +397,19 @@ float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) {
 | 
			
		||||
  } else {
 | 
			
		||||
    this->phase_[phase].cumulative_forward_active_energy_ = val;
 | 
			
		||||
  }
 | 
			
		||||
  return ((float) this->phase_[phase].cumulative_forward_active_energy_ * 10 / 3200);
 | 
			
		||||
  // 0.01CF resolution = 0.003125 Wh per count
 | 
			
		||||
  return ((float) this->phase_[phase].cumulative_forward_active_energy_ * (10.0f / 3200.0f));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_phase_reverse_active_energy_(uint8_t phase) {
 | 
			
		||||
  const uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGY);
 | 
			
		||||
  const uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGY + phase);
 | 
			
		||||
  if (UINT32_MAX - this->phase_[phase].cumulative_reverse_active_energy_ > val) {
 | 
			
		||||
    this->phase_[phase].cumulative_reverse_active_energy_ += val;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->phase_[phase].cumulative_reverse_active_energy_ = val;
 | 
			
		||||
  }
 | 
			
		||||
  return ((float) this->phase_[phase].cumulative_reverse_active_energy_ * 10 / 3200);
 | 
			
		||||
  // 0.01CF resolution = 0.003125 Wh per count
 | 
			
		||||
  return ((float) this->phase_[phase].cumulative_reverse_active_energy_ * (10.0f / 3200.0f));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) {
 | 
			
		||||
@@ -446,15 +419,15 @@ float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) {
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_phase_angle_(uint8_t phase) {
 | 
			
		||||
  uint16_t val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0;
 | 
			
		||||
  return (float) (val > 180) ? val - 360.0 : val;
 | 
			
		||||
  return (val > 180) ? (float) (val - 360.0f) : (float) val;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_phase_peak_current_(uint8_t phase) {
 | 
			
		||||
  int16_t val = (float) this->read16_(ATM90E32_REGISTER_IPEAK + phase);
 | 
			
		||||
  if (!this->peak_current_signed_)
 | 
			
		||||
    val = abs(val);
 | 
			
		||||
    val = std::abs(val);
 | 
			
		||||
  // phase register * phase current gain value  / 1000 * 2^13
 | 
			
		||||
  return (float) (val * this->phase_[phase].ct_gain_ / 8192000.0);
 | 
			
		||||
  return (val * this->phase_[phase].ct_gain_ / 8192000.0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_frequency_() {
 | 
			
		||||
@@ -467,29 +440,433 @@ float ATM90E32Component::get_chip_temperature_() {
 | 
			
		||||
  return (float) ctemp;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint16_t ATM90E32Component::calibrate_voltage_offset_phase(uint8_t phase) {
 | 
			
		||||
  const uint8_t num_reads = 5;
 | 
			
		||||
  uint64_t total_value = 0;
 | 
			
		||||
  for (int i = 0; i < num_reads; ++i) {
 | 
			
		||||
    const uint32_t measurement_value = read32_(ATM90E32_REGISTER_URMS + phase, ATM90E32_REGISTER_URMSLSB + phase);
 | 
			
		||||
    total_value += measurement_value;
 | 
			
		||||
void ATM90E32Component::run_gain_calibrations() {
 | 
			
		||||
  if (!this->enable_gain_calibration_) {
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] Gain calibration is disabled! Enable it first with enable_gain_calibration: true");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const uint32_t average_value = total_value / num_reads;
 | 
			
		||||
  const uint32_t shifted_value = average_value >> 7;
 | 
			
		||||
  const uint32_t voltage_offset = ~shifted_value + 1;
 | 
			
		||||
  return voltage_offset & 0xFFFF;  // Take the lower 16 bits
 | 
			
		||||
 | 
			
		||||
  float ref_voltages[3] = {
 | 
			
		||||
      this->get_reference_voltage(0),
 | 
			
		||||
      this->get_reference_voltage(1),
 | 
			
		||||
      this->get_reference_voltage(2),
 | 
			
		||||
  };
 | 
			
		||||
  float ref_currents[3] = {this->get_reference_current(0), this->get_reference_current(1),
 | 
			
		||||
                           this->get_reference_current(2)};
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] ");
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] ========================= Gain Calibration  =========================");
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------");
 | 
			
		||||
  ESP_LOGI(TAG,
 | 
			
		||||
           "[CALIBRATION] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref  | V_gain (old→new) | I_gain (old→new) |");
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------");
 | 
			
		||||
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
    float measured_voltage = this->get_phase_voltage_avg_(phase);
 | 
			
		||||
    float measured_current = this->get_phase_current_avg_(phase);
 | 
			
		||||
 | 
			
		||||
    float ref_voltage = ref_voltages[phase];
 | 
			
		||||
    float ref_current = ref_currents[phase];
 | 
			
		||||
 | 
			
		||||
    uint16_t current_voltage_gain = this->read16_(voltage_gain_registers[phase]);
 | 
			
		||||
    uint16_t current_current_gain = this->read16_(current_gain_registers[phase]);
 | 
			
		||||
 | 
			
		||||
    bool did_voltage = false;
 | 
			
		||||
    bool did_current = false;
 | 
			
		||||
 | 
			
		||||
    // Voltage calibration
 | 
			
		||||
    if (ref_voltage <= 0.0f) {
 | 
			
		||||
      ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: reference voltage is 0.",
 | 
			
		||||
               phase_labels[phase]);
 | 
			
		||||
    } else if (measured_voltage == 0.0f) {
 | 
			
		||||
      ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: measured voltage is 0.",
 | 
			
		||||
               phase_labels[phase]);
 | 
			
		||||
    } else {
 | 
			
		||||
      uint32_t new_voltage_gain = static_cast<uint16_t>((ref_voltage / measured_voltage) * current_voltage_gain);
 | 
			
		||||
      if (new_voltage_gain == 0) {
 | 
			
		||||
        ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Voltage gain would be 0. Check reference and measured voltage.",
 | 
			
		||||
                 phase_labels[phase]);
 | 
			
		||||
      } else {
 | 
			
		||||
        if (new_voltage_gain >= 65535) {
 | 
			
		||||
          ESP_LOGW(
 | 
			
		||||
              TAG,
 | 
			
		||||
              "[CALIBRATION] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage transformer.",
 | 
			
		||||
              phase_labels[phase]);
 | 
			
		||||
          new_voltage_gain = 65535;
 | 
			
		||||
        }
 | 
			
		||||
        this->gain_phase_[phase].voltage_gain = static_cast<uint16_t>(new_voltage_gain);
 | 
			
		||||
        did_voltage = true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Current calibration
 | 
			
		||||
    if (ref_current == 0.0f) {
 | 
			
		||||
      ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: reference current is 0.",
 | 
			
		||||
               phase_labels[phase]);
 | 
			
		||||
    } else if (measured_current == 0.0f) {
 | 
			
		||||
      ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: measured current is 0.",
 | 
			
		||||
               phase_labels[phase]);
 | 
			
		||||
    } else {
 | 
			
		||||
      uint32_t new_current_gain = static_cast<uint16_t>((ref_current / measured_current) * current_current_gain);
 | 
			
		||||
      if (new_current_gain == 0) {
 | 
			
		||||
        ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain would be 0. Check reference and measured current.",
 | 
			
		||||
                 phase_labels[phase]);
 | 
			
		||||
      } else {
 | 
			
		||||
        if (new_current_gain >= 65535) {
 | 
			
		||||
          ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.",
 | 
			
		||||
                   phase_labels[phase]);
 | 
			
		||||
          new_current_gain = 65535;
 | 
			
		||||
        }
 | 
			
		||||
        this->gain_phase_[phase].current_gain = static_cast<uint16_t>(new_current_gain);
 | 
			
		||||
        did_current = true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Final row output
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] |   %c   |  %9.2f |  %9.4f | %5.2f | %6.4f |  %5u → %-5u  |  %5u → %-5u  |",
 | 
			
		||||
             'A' + phase, measured_voltage, measured_current, ref_voltage, ref_current, current_voltage_gain,
 | 
			
		||||
             did_voltage ? this->gain_phase_[phase].voltage_gain : current_voltage_gain, current_current_gain,
 | 
			
		||||
             did_current ? this->gain_phase_[phase].current_gain : current_current_gain);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] =====================================================================\n");
 | 
			
		||||
 | 
			
		||||
  this->save_gain_calibration_to_memory_();
 | 
			
		||||
  this->write_gains_to_registers_();
 | 
			
		||||
  this->verify_gain_writes_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint16_t ATM90E32Component::calibrate_current_offset_phase(uint8_t phase) {
 | 
			
		||||
void ATM90E32Component::save_gain_calibration_to_memory_() {
 | 
			
		||||
  bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
 | 
			
		||||
  if (success) {
 | 
			
		||||
    this->using_saved_calibrations_ = true;
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Gain calibration saved to memory.");
 | 
			
		||||
  } else {
 | 
			
		||||
    this->using_saved_calibrations_ = false;
 | 
			
		||||
    ESP_LOGE(TAG, "[CALIBRATION] Failed to save gain calibration to memory!");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::run_offset_calibrations() {
 | 
			
		||||
  if (!this->enable_offset_calibration_) {
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] Offset calibration is disabled! Enable it first with enable_offset_calibration: true");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
    int16_t voltage_offset = calibrate_offset(phase, true);
 | 
			
		||||
    int16_t current_offset = calibrate_offset(phase, false);
 | 
			
		||||
 | 
			
		||||
    this->write_offsets_to_registers_(phase, voltage_offset, current_offset);
 | 
			
		||||
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage: %d, offset_current: %d", 'A' + phase, voltage_offset,
 | 
			
		||||
             current_offset);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->offset_pref_.save(&this->offset_phase_);  // Save to flash
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::run_power_offset_calibrations() {
 | 
			
		||||
  if (!this->enable_offset_calibration_) {
 | 
			
		||||
    ESP_LOGW(
 | 
			
		||||
        TAG,
 | 
			
		||||
        "[CALIBRATION] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
    int16_t active_offset = calibrate_power_offset(phase, false);
 | 
			
		||||
    int16_t reactive_offset = calibrate_power_offset(phase, true);
 | 
			
		||||
 | 
			
		||||
    this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset);
 | 
			
		||||
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase,
 | 
			
		||||
             active_offset, reactive_offset);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->power_offset_pref_.save(&this->power_offset_phase_);  // Save to flash
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::write_gains_to_registers_() {
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);
 | 
			
		||||
 | 
			
		||||
  for (int phase = 0; phase < 3; phase++) {
 | 
			
		||||
    this->write16_(voltage_gain_registers[phase], this->gain_phase_[phase].voltage_gain);
 | 
			
		||||
    this->write16_(current_gain_registers[phase], this->gain_phase_[phase].current_gain);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset) {
 | 
			
		||||
  // Save to runtime
 | 
			
		||||
  this->offset_phase_[phase].voltage_offset_ = voltage_offset;
 | 
			
		||||
  this->phase_[phase].voltage_offset_ = voltage_offset;
 | 
			
		||||
 | 
			
		||||
  // Save to flash-storable struct
 | 
			
		||||
  this->offset_phase_[phase].current_offset_ = current_offset;
 | 
			
		||||
  this->phase_[phase].current_offset_ = current_offset;
 | 
			
		||||
 | 
			
		||||
  // Write to registers
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);
 | 
			
		||||
  this->write16_(voltage_offset_registers[phase], static_cast<uint16_t>(voltage_offset));
 | 
			
		||||
  this->write16_(current_offset_registers[phase], static_cast<uint16_t>(current_offset));
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset) {
 | 
			
		||||
  // Save to runtime
 | 
			
		||||
  this->phase_[phase].active_power_offset_ = p_offset;
 | 
			
		||||
  this->phase_[phase].reactive_power_offset_ = q_offset;
 | 
			
		||||
 | 
			
		||||
  // Save to flash-storable struct
 | 
			
		||||
  this->power_offset_phase_[phase].active_power_offset = p_offset;
 | 
			
		||||
  this->power_offset_phase_[phase].reactive_power_offset = q_offset;
 | 
			
		||||
 | 
			
		||||
  // Write to registers
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);
 | 
			
		||||
  this->write16_(this->power_offset_registers[phase], static_cast<uint16_t>(p_offset));
 | 
			
		||||
  this->write16_(this->reactive_power_offset_registers[phase], static_cast<uint16_t>(q_offset));
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::restore_gain_calibrations_() {
 | 
			
		||||
  if (this->gain_calibration_pref_.load(&this->gain_phase_)) {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Restoring saved gain calibrations to registers:");
 | 
			
		||||
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      uint16_t v_gain = this->gain_phase_[phase].voltage_gain;
 | 
			
		||||
      uint16_t i_gain = this->gain_phase_[phase].current_gain;
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION]   Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase, v_gain, i_gain);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this->write_gains_to_registers_();
 | 
			
		||||
 | 
			
		||||
    if (this->verify_gain_writes_()) {
 | 
			
		||||
      this->using_saved_calibrations_ = true;
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION] Gain calibration loaded and verified successfully.");
 | 
			
		||||
    } else {
 | 
			
		||||
      this->using_saved_calibrations_ = false;
 | 
			
		||||
      ESP_LOGE(TAG, "[CALIBRATION] Gain verification failed! Calibration may not be applied correctly.");
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    this->using_saved_calibrations_ = false;
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] No stored gain calibrations found. Using config file values.");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::restore_offset_calibrations_() {
 | 
			
		||||
  if (this->offset_pref_.load(&this->offset_phase_)) {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Successfully restored offset calibration from memory.");
 | 
			
		||||
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      auto &offset = this->offset_phase_[phase];
 | 
			
		||||
      write_offsets_to_registers_(phase, offset.voltage_offset_, offset.current_offset_);
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage:: %d, offset_current: %d", 'A' + phase,
 | 
			
		||||
               offset.voltage_offset_, offset.current_offset_);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] No stored offset calibrations found. Using default values.");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::restore_power_offset_calibrations_() {
 | 
			
		||||
  if (this->power_offset_pref_.load(&this->power_offset_phase_)) {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Successfully restored power offset calibration from memory.");
 | 
			
		||||
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
      auto &offset = this->power_offset_phase_[phase];
 | 
			
		||||
      write_power_offsets_to_registers_(phase, offset.active_power_offset, offset.reactive_power_offset);
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase,
 | 
			
		||||
               offset.active_power_offset, offset.reactive_power_offset);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] No stored power offsets found. Using default values.");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::clear_gain_calibrations() {
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] Clearing stored gain calibrations and restoring config-defined values...");
 | 
			
		||||
 | 
			
		||||
  for (int phase = 0; phase < 3; phase++) {
 | 
			
		||||
    gain_phase_[phase].voltage_gain = this->phase_[phase].voltage_gain_;
 | 
			
		||||
    gain_phase_[phase].current_gain = this->phase_[phase].ct_gain_;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
 | 
			
		||||
  this->using_saved_calibrations_ = false;
 | 
			
		||||
 | 
			
		||||
  if (success) {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Gain calibrations cleared. Config values restored:");
 | 
			
		||||
    for (int phase = 0; phase < 3; phase++) {
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION]   Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase,
 | 
			
		||||
               gain_phase_[phase].voltage_gain, gain_phase_[phase].current_gain);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGE(TAG, "[CALIBRATION] Failed to clear gain calibrations!");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->write_gains_to_registers_();  // Apply them to the chip immediately
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::clear_offset_calibrations() {
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
    this->write_offsets_to_registers_(phase, 0, 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->offset_pref_.save(&this->offset_phase_);  // Save cleared values to flash memory
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] Offsets cleared.");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::clear_power_offset_calibrations() {
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
    this->write_power_offsets_to_registers_(phase, 0, 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->power_offset_pref_.save(&this->power_offset_phase_);
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] Power offsets cleared.");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) {
 | 
			
		||||
  const uint8_t num_reads = 5;
 | 
			
		||||
  uint64_t total_value = 0;
 | 
			
		||||
  for (int i = 0; i < num_reads; ++i) {
 | 
			
		||||
    const uint32_t measurement_value = read32_(ATM90E32_REGISTER_IRMS + phase, ATM90E32_REGISTER_IRMSLSB + phase);
 | 
			
		||||
    total_value += measurement_value;
 | 
			
		||||
 | 
			
		||||
  for (uint8_t i = 0; i < num_reads; ++i) {
 | 
			
		||||
    uint32_t reading = voltage ? this->read32_(ATM90E32_REGISTER_URMS + phase, ATM90E32_REGISTER_URMSLSB + phase)
 | 
			
		||||
                               : this->read32_(ATM90E32_REGISTER_IRMS + phase, ATM90E32_REGISTER_IRMSLSB + phase);
 | 
			
		||||
    total_value += reading;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const uint32_t average_value = total_value / num_reads;
 | 
			
		||||
  const uint32_t current_offset = ~average_value + 1;
 | 
			
		||||
  return current_offset & 0xFFFF;  // Take the lower 16 bits
 | 
			
		||||
  const uint32_t shifted = average_value >> 7;
 | 
			
		||||
  const uint32_t offset = ~shifted + 1;
 | 
			
		||||
  return static_cast<int16_t>(offset);  // Takes lower 16 bits
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive) {
 | 
			
		||||
  const uint8_t num_reads = 5;
 | 
			
		||||
  uint64_t total_value = 0;
 | 
			
		||||
 | 
			
		||||
  for (uint8_t i = 0; i < num_reads; ++i) {
 | 
			
		||||
    uint32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase)
 | 
			
		||||
                                : this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase);
 | 
			
		||||
    total_value += reading;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const uint32_t average_value = total_value / num_reads;
 | 
			
		||||
  const uint32_t power_offset = ~average_value + 1;
 | 
			
		||||
  return static_cast<int16_t>(power_offset);  // Takes the lower 16 bits
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ATM90E32Component::verify_gain_writes_() {
 | 
			
		||||
  bool success = true;
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
    uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]);
 | 
			
		||||
    uint16_t read_current = this->read16_(current_gain_registers[phase]);
 | 
			
		||||
 | 
			
		||||
    if (read_voltage != this->gain_phase_[phase].voltage_gain ||
 | 
			
		||||
        read_current != this->gain_phase_[phase].current_gain) {
 | 
			
		||||
      ESP_LOGE(TAG, "[CALIBRATION] Mismatch detected for Phase %s!", phase_labels[phase]);
 | 
			
		||||
      success = false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return success;  // Return true if all writes were successful, false otherwise
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
void ATM90E32Component::check_phase_status() {
 | 
			
		||||
  uint16_t state0 = this->read16_(ATM90E32_REGISTER_EMMSTATE0);
 | 
			
		||||
  uint16_t state1 = this->read16_(ATM90E32_REGISTER_EMMSTATE1);
 | 
			
		||||
 | 
			
		||||
  for (int phase = 0; phase < 3; phase++) {
 | 
			
		||||
    std::string status;
 | 
			
		||||
 | 
			
		||||
    if (state0 & over_voltage_flags[phase])
 | 
			
		||||
      status += "Over Voltage; ";
 | 
			
		||||
    if (state1 & voltage_sag_flags[phase])
 | 
			
		||||
      status += "Voltage Sag; ";
 | 
			
		||||
    if (state1 & phase_loss_flags[phase])
 | 
			
		||||
      status += "Phase Loss; ";
 | 
			
		||||
 | 
			
		||||
    auto *sensor = this->phase_status_text_sensor_[phase];
 | 
			
		||||
    const char *phase_name = sensor ? sensor->get_name().c_str() : "Unknown Phase";
 | 
			
		||||
    if (!status.empty()) {
 | 
			
		||||
      status.pop_back();  // remove space
 | 
			
		||||
      status.pop_back();  // remove semicolon
 | 
			
		||||
      ESP_LOGW(TAG, "%s: %s", phase_name, status.c_str());
 | 
			
		||||
      if (sensor != nullptr)
 | 
			
		||||
        sensor->publish_state(status);
 | 
			
		||||
    } else {
 | 
			
		||||
      if (sensor != nullptr)
 | 
			
		||||
        sensor->publish_state("Okay");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::check_freq_status() {
 | 
			
		||||
  uint16_t state1 = this->read16_(ATM90E32_REGISTER_EMMSTATE1);
 | 
			
		||||
 | 
			
		||||
  std::string freq_status;
 | 
			
		||||
 | 
			
		||||
  if (state1 & ATM90E32_STATUS_S1_FREQHIST) {
 | 
			
		||||
    freq_status = "HIGH";
 | 
			
		||||
  } else if (state1 & ATM90E32_STATUS_S1_FREQLOST) {
 | 
			
		||||
    freq_status = "LOW";
 | 
			
		||||
  } else {
 | 
			
		||||
    freq_status = "Normal";
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str());
 | 
			
		||||
 | 
			
		||||
  if (this->freq_status_text_sensor_ != nullptr) {
 | 
			
		||||
    this->freq_status_text_sensor_->publish_state(freq_status);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::check_over_current() {
 | 
			
		||||
  constexpr float max_current_threshold = 65.53f;
 | 
			
		||||
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
    float current_val =
 | 
			
		||||
        this->phase_[phase].current_sensor_ != nullptr ? this->phase_[phase].current_sensor_->state : 0.0f;
 | 
			
		||||
 | 
			
		||||
    if (current_val > max_current_threshold) {
 | 
			
		||||
      ESP_LOGW(TAG, "Over current detected on Phase %c: %.2f A", 'A' + phase, current_val);
 | 
			
		||||
      ESP_LOGW(TAG, "You may need to half your gain_ct: value & multiply the current and power values by 2");
 | 
			
		||||
      if (this->phase_status_text_sensor_[phase] != nullptr) {
 | 
			
		||||
        this->phase_status_text_sensor_[phase]->publish_state("Over Current; ");
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
uint16_t ATM90E32Component::calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier) {
 | 
			
		||||
  // this assumes that 60Hz electrical systems use 120V mains,
 | 
			
		||||
  // which is usually, but not always the case
 | 
			
		||||
  float nominal_voltage = (line_freq == 60) ? 120.0f : 220.0f;
 | 
			
		||||
  float target_voltage = nominal_voltage * multiplier;
 | 
			
		||||
 | 
			
		||||
  float peak_01v = target_voltage * 100.0f * std::sqrt(2.0f);  // convert RMS → peak, scale to 0.01V
 | 
			
		||||
  float divider = (2.0f * ugain) / 32768.0f;
 | 
			
		||||
 | 
			
		||||
  float threshold = peak_01v / divider;
 | 
			
		||||
 | 
			
		||||
  return static_cast<uint16_t>(threshold);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ATM90E32Component::validate_spi_read_(uint16_t expected, const char *context) {
 | 
			
		||||
  uint16_t last = this->read16_(ATM90E32_REGISTER_LASTSPIDATA);
 | 
			
		||||
  if (last != expected) {
 | 
			
		||||
    if (context != nullptr) {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] SPI read mismatch: expected 0x%04X, got 0x%04X", context, expected, last);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "SPI read mismatch: expected 0x%04X, got 0x%04X", expected, last);
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace atm90e32
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <unordered_map>
 | 
			
		||||
#include "atm90e32_reg.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/spi/spi.h"
 | 
			
		||||
@@ -18,6 +19,26 @@ class ATM90E32Component : public PollingComponent,
 | 
			
		||||
  static const uint8_t PHASEA = 0;
 | 
			
		||||
  static const uint8_t PHASEB = 1;
 | 
			
		||||
  static const uint8_t PHASEC = 2;
 | 
			
		||||
  const char *phase_labels[3] = {"A", "B", "C"};
 | 
			
		||||
  // these registers are not sucessive, so we can't just do 'base + phase'
 | 
			
		||||
  const uint16_t voltage_gain_registers[3] = {ATM90E32_REGISTER_UGAINA, ATM90E32_REGISTER_UGAINB,
 | 
			
		||||
                                              ATM90E32_REGISTER_UGAINC};
 | 
			
		||||
  const uint16_t current_gain_registers[3] = {ATM90E32_REGISTER_IGAINA, ATM90E32_REGISTER_IGAINB,
 | 
			
		||||
                                              ATM90E32_REGISTER_IGAINC};
 | 
			
		||||
  const uint16_t voltage_offset_registers[3] = {ATM90E32_REGISTER_UOFFSETA, ATM90E32_REGISTER_UOFFSETB,
 | 
			
		||||
                                                ATM90E32_REGISTER_UOFFSETC};
 | 
			
		||||
  const uint16_t current_offset_registers[3] = {ATM90E32_REGISTER_IOFFSETA, ATM90E32_REGISTER_IOFFSETB,
 | 
			
		||||
                                                ATM90E32_REGISTER_IOFFSETC};
 | 
			
		||||
  const uint16_t power_offset_registers[3] = {ATM90E32_REGISTER_POFFSETA, ATM90E32_REGISTER_POFFSETB,
 | 
			
		||||
                                              ATM90E32_REGISTER_POFFSETC};
 | 
			
		||||
  const uint16_t reactive_power_offset_registers[3] = {ATM90E32_REGISTER_QOFFSETA, ATM90E32_REGISTER_QOFFSETB,
 | 
			
		||||
                                                       ATM90E32_REGISTER_QOFFSETC};
 | 
			
		||||
  const uint16_t over_voltage_flags[3] = {ATM90E32_STATUS_S0_OVPHASEAST, ATM90E32_STATUS_S0_OVPHASEBST,
 | 
			
		||||
                                          ATM90E32_STATUS_S0_OVPHASECST};
 | 
			
		||||
  const uint16_t voltage_sag_flags[3] = {ATM90E32_STATUS_S1_SAGPHASEAST, ATM90E32_STATUS_S1_SAGPHASEBST,
 | 
			
		||||
                                         ATM90E32_STATUS_S1_SAGPHASECST};
 | 
			
		||||
  const uint16_t phase_loss_flags[3] = {ATM90E32_STATUS_S1_PHASELOSSAST, ATM90E32_STATUS_S1_PHASELOSSBST,
 | 
			
		||||
                                        ATM90E32_STATUS_S1_PHASELOSSCST};
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
@@ -42,6 +63,14 @@ class ATM90E32Component : public PollingComponent,
 | 
			
		||||
  void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; }
 | 
			
		||||
  void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].voltage_gain_ = gain; }
 | 
			
		||||
  void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; }
 | 
			
		||||
  void set_voltage_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].voltage_offset_ = offset; }
 | 
			
		||||
  void set_current_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].current_offset_ = offset; }
 | 
			
		||||
  void set_active_power_offset(uint8_t phase, int16_t offset) {
 | 
			
		||||
    this->power_offset_phase_[phase].active_power_offset = offset;
 | 
			
		||||
  }
 | 
			
		||||
  void set_reactive_power_offset(uint8_t phase, int16_t offset) {
 | 
			
		||||
    this->power_offset_phase_[phase].reactive_power_offset = offset;
 | 
			
		||||
  }
 | 
			
		||||
  void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; }
 | 
			
		||||
  void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; }
 | 
			
		||||
  void set_chip_temperature_sensor(sensor::Sensor *chip_temperature_sensor) {
 | 
			
		||||
@@ -51,53 +80,104 @@ class ATM90E32Component : public PollingComponent,
 | 
			
		||||
  void set_current_phases(int phases) { current_phases_ = phases; }
 | 
			
		||||
  void set_pga_gain(uint16_t gain) { pga_gain_ = gain; }
 | 
			
		||||
  void run_offset_calibrations();
 | 
			
		||||
  void run_power_offset_calibrations();
 | 
			
		||||
  void clear_offset_calibrations();
 | 
			
		||||
  void clear_power_offset_calibrations();
 | 
			
		||||
  void clear_gain_calibrations();
 | 
			
		||||
  void set_enable_offset_calibration(bool flag) { enable_offset_calibration_ = flag; }
 | 
			
		||||
  uint16_t calibrate_voltage_offset_phase(uint8_t /*phase*/);
 | 
			
		||||
  uint16_t calibrate_current_offset_phase(uint8_t /*phase*/);
 | 
			
		||||
  void set_enable_gain_calibration(bool flag) { enable_gain_calibration_ = flag; }
 | 
			
		||||
  int16_t calibrate_offset(uint8_t phase, bool voltage);
 | 
			
		||||
  int16_t calibrate_power_offset(uint8_t phase, bool reactive);
 | 
			
		||||
  void run_gain_calibrations();
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  void set_reference_voltage(uint8_t phase, number::Number *ref_voltage) { ref_voltages_[phase] = ref_voltage; }
 | 
			
		||||
  void set_reference_current(uint8_t phase, number::Number *ref_current) { ref_currents_[phase] = ref_current; }
 | 
			
		||||
#endif
 | 
			
		||||
  float get_reference_voltage(uint8_t phase) {
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
    return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0;  // Default voltage
 | 
			
		||||
#else
 | 
			
		||||
    return 120.0;  // Default voltage
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
  float get_reference_current(uint8_t phase) {
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
    return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f;  // Default current
 | 
			
		||||
#else
 | 
			
		||||
    return 5.0f;   // Default current
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
  bool using_saved_calibrations_ = false;  // Track if stored calibrations are being used
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
  void check_phase_status();
 | 
			
		||||
  void check_freq_status();
 | 
			
		||||
  void check_over_current();
 | 
			
		||||
  void set_phase_status_text_sensor(uint8_t phase, text_sensor::TextSensor *sensor) {
 | 
			
		||||
    this->phase_status_text_sensor_[phase] = sensor;
 | 
			
		||||
  }
 | 
			
		||||
  void set_freq_status_text_sensor(text_sensor::TextSensor *sensor) { this->freq_status_text_sensor_ = sensor; }
 | 
			
		||||
#endif
 | 
			
		||||
  uint16_t calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier);
 | 
			
		||||
  int32_t last_periodic_millis = millis();
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  number::Number *ref_voltages_[3]{nullptr, nullptr, nullptr};
 | 
			
		||||
  number::Number *ref_currents_[3]{nullptr, nullptr, nullptr};
 | 
			
		||||
#endif
 | 
			
		||||
  uint16_t read16_(uint16_t a_register);
 | 
			
		||||
  int read32_(uint16_t addr_h, uint16_t addr_l);
 | 
			
		||||
  void write16_(uint16_t a_register, uint16_t val);
 | 
			
		||||
  float get_local_phase_voltage_(uint8_t /*phase*/);
 | 
			
		||||
  float get_local_phase_current_(uint8_t /*phase*/);
 | 
			
		||||
  float get_local_phase_active_power_(uint8_t /*phase*/);
 | 
			
		||||
  float get_local_phase_reactive_power_(uint8_t /*phase*/);
 | 
			
		||||
  float get_local_phase_power_factor_(uint8_t /*phase*/);
 | 
			
		||||
  float get_local_phase_forward_active_energy_(uint8_t /*phase*/);
 | 
			
		||||
  float get_local_phase_reverse_active_energy_(uint8_t /*phase*/);
 | 
			
		||||
  float get_local_phase_angle_(uint8_t /*phase*/);
 | 
			
		||||
  float get_local_phase_harmonic_active_power_(uint8_t /*phase*/);
 | 
			
		||||
  float get_local_phase_peak_current_(uint8_t /*phase*/);
 | 
			
		||||
  float get_phase_voltage_(uint8_t /*phase*/);
 | 
			
		||||
  float get_phase_voltage_avg_(uint8_t /*phase*/);
 | 
			
		||||
  float get_phase_current_(uint8_t /*phase*/);
 | 
			
		||||
  float get_phase_current_avg_(uint8_t /*phase*/);
 | 
			
		||||
  float get_phase_active_power_(uint8_t /*phase*/);
 | 
			
		||||
  float get_phase_reactive_power_(uint8_t /*phase*/);
 | 
			
		||||
  float get_phase_power_factor_(uint8_t /*phase*/);
 | 
			
		||||
  float get_phase_forward_active_energy_(uint8_t /*phase*/);
 | 
			
		||||
  float get_phase_reverse_active_energy_(uint8_t /*phase*/);
 | 
			
		||||
  float get_phase_angle_(uint8_t /*phase*/);
 | 
			
		||||
  float get_phase_harmonic_active_power_(uint8_t /*phase*/);
 | 
			
		||||
  float get_phase_peak_current_(uint8_t /*phase*/);
 | 
			
		||||
  float get_local_phase_voltage_(uint8_t phase);
 | 
			
		||||
  float get_local_phase_current_(uint8_t phase);
 | 
			
		||||
  float get_local_phase_active_power_(uint8_t phase);
 | 
			
		||||
  float get_local_phase_reactive_power_(uint8_t phase);
 | 
			
		||||
  float get_local_phase_apparent_power_(uint8_t phase);
 | 
			
		||||
  float get_local_phase_power_factor_(uint8_t phase);
 | 
			
		||||
  float get_local_phase_forward_active_energy_(uint8_t phase);
 | 
			
		||||
  float get_local_phase_reverse_active_energy_(uint8_t phase);
 | 
			
		||||
  float get_local_phase_angle_(uint8_t phase);
 | 
			
		||||
  float get_local_phase_harmonic_active_power_(uint8_t phase);
 | 
			
		||||
  float get_local_phase_peak_current_(uint8_t phase);
 | 
			
		||||
  float get_phase_voltage_(uint8_t phase);
 | 
			
		||||
  float get_phase_voltage_avg_(uint8_t phase);
 | 
			
		||||
  float get_phase_current_(uint8_t phase);
 | 
			
		||||
  float get_phase_current_avg_(uint8_t phase);
 | 
			
		||||
  float get_phase_active_power_(uint8_t phase);
 | 
			
		||||
  float get_phase_reactive_power_(uint8_t phase);
 | 
			
		||||
  float get_phase_apparent_power_(uint8_t phase);
 | 
			
		||||
  float get_phase_power_factor_(uint8_t phase);
 | 
			
		||||
  float get_phase_forward_active_energy_(uint8_t phase);
 | 
			
		||||
  float get_phase_reverse_active_energy_(uint8_t phase);
 | 
			
		||||
  float get_phase_angle_(uint8_t phase);
 | 
			
		||||
  float get_phase_harmonic_active_power_(uint8_t phase);
 | 
			
		||||
  float get_phase_peak_current_(uint8_t phase);
 | 
			
		||||
  float get_frequency_();
 | 
			
		||||
  float get_chip_temperature_();
 | 
			
		||||
  bool get_publish_interval_flag_() { return publish_interval_flag_; };
 | 
			
		||||
  void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; };
 | 
			
		||||
  void restore_calibrations_();
 | 
			
		||||
  void restore_offset_calibrations_();
 | 
			
		||||
  void restore_power_offset_calibrations_();
 | 
			
		||||
  void restore_gain_calibrations_();
 | 
			
		||||
  void save_gain_calibration_to_memory_();
 | 
			
		||||
  void write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset);
 | 
			
		||||
  void write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset);
 | 
			
		||||
  void write_gains_to_registers_();
 | 
			
		||||
  bool verify_gain_writes_();
 | 
			
		||||
  bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
 | 
			
		||||
 | 
			
		||||
  struct ATM90E32Phase {
 | 
			
		||||
    uint16_t voltage_gain_{0};
 | 
			
		||||
    uint16_t ct_gain_{0};
 | 
			
		||||
    uint16_t voltage_offset_{0};
 | 
			
		||||
    uint16_t current_offset_{0};
 | 
			
		||||
    int16_t voltage_offset_{0};
 | 
			
		||||
    int16_t current_offset_{0};
 | 
			
		||||
    int16_t active_power_offset_{0};
 | 
			
		||||
    int16_t reactive_power_offset_{0};
 | 
			
		||||
    float voltage_{0};
 | 
			
		||||
    float current_{0};
 | 
			
		||||
    float active_power_{0};
 | 
			
		||||
    float reactive_power_{0};
 | 
			
		||||
    float apparent_power_{0};
 | 
			
		||||
    float power_factor_{0};
 | 
			
		||||
    float forward_active_energy_{0};
 | 
			
		||||
    float reverse_active_energy_{0};
 | 
			
		||||
@@ -119,14 +199,30 @@ class ATM90E32Component : public PollingComponent,
 | 
			
		||||
    uint32_t cumulative_reverse_active_energy_{0};
 | 
			
		||||
  } phase_[3];
 | 
			
		||||
 | 
			
		||||
  struct Calibration {
 | 
			
		||||
    uint16_t voltage_offset_{0};
 | 
			
		||||
    uint16_t current_offset_{0};
 | 
			
		||||
  struct OffsetCalibration {
 | 
			
		||||
    int16_t voltage_offset_{0};
 | 
			
		||||
    int16_t current_offset_{0};
 | 
			
		||||
  } offset_phase_[3];
 | 
			
		||||
 | 
			
		||||
  ESPPreferenceObject pref_;
 | 
			
		||||
  struct PowerOffsetCalibration {
 | 
			
		||||
    int16_t active_power_offset{0};
 | 
			
		||||
    int16_t reactive_power_offset{0};
 | 
			
		||||
  } power_offset_phase_[3];
 | 
			
		||||
 | 
			
		||||
  struct GainCalibration {
 | 
			
		||||
    uint16_t voltage_gain{1};
 | 
			
		||||
    uint16_t current_gain{1};
 | 
			
		||||
  } gain_phase_[3];
 | 
			
		||||
 | 
			
		||||
  ESPPreferenceObject offset_pref_;
 | 
			
		||||
  ESPPreferenceObject power_offset_pref_;
 | 
			
		||||
  ESPPreferenceObject gain_calibration_pref_;
 | 
			
		||||
 | 
			
		||||
  sensor::Sensor *freq_sensor_{nullptr};
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
  text_sensor::TextSensor *phase_status_text_sensor_[3]{nullptr};
 | 
			
		||||
  text_sensor::TextSensor *freq_status_text_sensor_{nullptr};
 | 
			
		||||
#endif
 | 
			
		||||
  sensor::Sensor *chip_temperature_sensor_{nullptr};
 | 
			
		||||
  uint16_t pga_gain_{0x15};
 | 
			
		||||
  int line_freq_{60};
 | 
			
		||||
@@ -134,6 +230,7 @@ class ATM90E32Component : public PollingComponent,
 | 
			
		||||
  bool publish_interval_flag_{false};
 | 
			
		||||
  bool peak_current_signed_{false};
 | 
			
		||||
  bool enable_offset_calibration_{false};
 | 
			
		||||
  bool enable_gain_calibration_{false};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace atm90e32
 | 
			
		||||
 
 | 
			
		||||
@@ -176,16 +176,17 @@ static const uint16_t ATM90E32_REGISTER_ANENERGYCH = 0xAF;  // C Reverse Harm. E
 | 
			
		||||
 | 
			
		||||
/* POWER & P.F. REGISTERS */
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_PMEANT = 0xB0;   // Total Mean Power (P)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_PMEAN = 0xB1;    // Mean Power Reg Base (P)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_PMEAN = 0xB1;    // Active Power Reg Base (P)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_PMEANA = 0xB1;   // A Mean Power (P)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_PMEANB = 0xB2;   // B Mean Power (P)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_PMEANC = 0xB3;   // C Mean Power (P)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_QMEANT = 0xB4;   // Total Mean Power (Q)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_QMEAN = 0xB5;    // Mean Power Reg Base (Q)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_QMEAN = 0xB5;    // Reactive Power Reg Base (Q)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_QMEANA = 0xB5;   // A Mean Power (Q)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_QMEANB = 0xB6;   // B Mean Power (Q)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_QMEANC = 0xB7;   // C Mean Power (Q)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_SMEANT = 0xB8;   // Total Mean Power (S)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_SMEAN = 0xB9;    // Apparent Mean Power Base (S)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_SMEANA = 0xB9;   // A Mean Power (S)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_SMEANB = 0xBA;   // B Mean Power (S)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_SMEANC = 0xBB;   // C Mean Power (S)
 | 
			
		||||
@@ -206,6 +207,7 @@ static const uint16_t ATM90E32_REGISTER_QMEANALSB = 0xC5;   // Lower Word (A Rea
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_QMEANBLSB = 0xC6;   // Lower Word (B React. Power)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_QMEANCLSB = 0xC7;   // Lower Word (C React. Power)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_SAMEANTLSB = 0xC8;  // Lower Word (Tot. App. Power)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_SMEANLSB = 0xC9;    // Lower Word Reg Base (Apparent Power)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_SMEANALSB = 0xC9;   // Lower Word (A App. Power)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_SMEANBLSB = 0xCA;   // Lower Word (B App. Power)
 | 
			
		||||
static const uint16_t ATM90E32_REGISTER_SMEANCLSB = 0xCB;   // Lower Word (C App. Power)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,43 +1,95 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import button
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_CHIP, ICON_SCALE
 | 
			
		||||
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE
 | 
			
		||||
 | 
			
		||||
from .. import atm90e32_ns
 | 
			
		||||
from ..sensor import ATM90E32Component
 | 
			
		||||
 | 
			
		||||
CONF_RUN_GAIN_CALIBRATION = "run_gain_calibration"
 | 
			
		||||
CONF_CLEAR_GAIN_CALIBRATION = "clear_gain_calibration"
 | 
			
		||||
CONF_RUN_OFFSET_CALIBRATION = "run_offset_calibration"
 | 
			
		||||
CONF_CLEAR_OFFSET_CALIBRATION = "clear_offset_calibration"
 | 
			
		||||
CONF_RUN_POWER_OFFSET_CALIBRATION = "run_power_offset_calibration"
 | 
			
		||||
CONF_CLEAR_POWER_OFFSET_CALIBRATION = "clear_power_offset_calibration"
 | 
			
		||||
 | 
			
		||||
ATM90E32CalibrationButton = atm90e32_ns.class_(
 | 
			
		||||
    "ATM90E32CalibrationButton",
 | 
			
		||||
    button.Button,
 | 
			
		||||
ATM90E32GainCalibrationButton = atm90e32_ns.class_(
 | 
			
		||||
    "ATM90E32GainCalibrationButton", button.Button
 | 
			
		||||
)
 | 
			
		||||
ATM90E32ClearCalibrationButton = atm90e32_ns.class_(
 | 
			
		||||
    "ATM90E32ClearCalibrationButton",
 | 
			
		||||
    button.Button,
 | 
			
		||||
ATM90E32ClearGainCalibrationButton = atm90e32_ns.class_(
 | 
			
		||||
    "ATM90E32ClearGainCalibrationButton", button.Button
 | 
			
		||||
)
 | 
			
		||||
ATM90E32OffsetCalibrationButton = atm90e32_ns.class_(
 | 
			
		||||
    "ATM90E32OffsetCalibrationButton", button.Button
 | 
			
		||||
)
 | 
			
		||||
ATM90E32ClearOffsetCalibrationButton = atm90e32_ns.class_(
 | 
			
		||||
    "ATM90E32ClearOffsetCalibrationButton", button.Button
 | 
			
		||||
)
 | 
			
		||||
ATM90E32PowerOffsetCalibrationButton = atm90e32_ns.class_(
 | 
			
		||||
    "ATM90E32PowerOffsetCalibrationButton", button.Button
 | 
			
		||||
)
 | 
			
		||||
ATM90E32ClearPowerOffsetCalibrationButton = atm90e32_ns.class_(
 | 
			
		||||
    "ATM90E32ClearPowerOffsetCalibrationButton", button.Button
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = {
 | 
			
		||||
    cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component),
 | 
			
		||||
    cv.Optional(CONF_RUN_GAIN_CALIBRATION): button.button_schema(
 | 
			
		||||
        ATM90E32GainCalibrationButton,
 | 
			
		||||
        entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
        icon="mdi:scale-balance",
 | 
			
		||||
    ),
 | 
			
		||||
    cv.Optional(CONF_CLEAR_GAIN_CALIBRATION): button.button_schema(
 | 
			
		||||
        ATM90E32ClearGainCalibrationButton,
 | 
			
		||||
        entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
        icon="mdi:delete",
 | 
			
		||||
    ),
 | 
			
		||||
    cv.Optional(CONF_RUN_OFFSET_CALIBRATION): button.button_schema(
 | 
			
		||||
        ATM90E32CalibrationButton,
 | 
			
		||||
        ATM90E32OffsetCalibrationButton,
 | 
			
		||||
        entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
        icon=ICON_SCALE,
 | 
			
		||||
    ),
 | 
			
		||||
    cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): button.button_schema(
 | 
			
		||||
        ATM90E32ClearCalibrationButton,
 | 
			
		||||
        ATM90E32ClearOffsetCalibrationButton,
 | 
			
		||||
        entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
        icon=ICON_CHIP,
 | 
			
		||||
        icon="mdi:delete",
 | 
			
		||||
    ),
 | 
			
		||||
    cv.Optional(CONF_RUN_POWER_OFFSET_CALIBRATION): button.button_schema(
 | 
			
		||||
        ATM90E32PowerOffsetCalibrationButton,
 | 
			
		||||
        entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
        icon=ICON_SCALE,
 | 
			
		||||
    ),
 | 
			
		||||
    cv.Optional(CONF_CLEAR_POWER_OFFSET_CALIBRATION): button.button_schema(
 | 
			
		||||
        ATM90E32ClearPowerOffsetCalibrationButton,
 | 
			
		||||
        entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
        icon="mdi:delete",
 | 
			
		||||
    ),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    parent = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
 | 
			
		||||
    if run_gain := config.get(CONF_RUN_GAIN_CALIBRATION):
 | 
			
		||||
        b = await button.new_button(run_gain)
 | 
			
		||||
        await cg.register_parented(b, parent)
 | 
			
		||||
 | 
			
		||||
    if clear_gain := config.get(CONF_CLEAR_GAIN_CALIBRATION):
 | 
			
		||||
        b = await button.new_button(clear_gain)
 | 
			
		||||
        await cg.register_parented(b, parent)
 | 
			
		||||
 | 
			
		||||
    if run_offset := config.get(CONF_RUN_OFFSET_CALIBRATION):
 | 
			
		||||
        b = await button.new_button(run_offset)
 | 
			
		||||
        await cg.register_parented(b, parent)
 | 
			
		||||
 | 
			
		||||
    if clear_offset := config.get(CONF_CLEAR_OFFSET_CALIBRATION):
 | 
			
		||||
        b = await button.new_button(clear_offset)
 | 
			
		||||
        await cg.register_parented(b, parent)
 | 
			
		||||
 | 
			
		||||
    if run_power := config.get(CONF_RUN_POWER_OFFSET_CALIBRATION):
 | 
			
		||||
        b = await button.new_button(run_power)
 | 
			
		||||
        await cg.register_parented(b, parent)
 | 
			
		||||
 | 
			
		||||
    if clear_power := config.get(CONF_CLEAR_POWER_OFFSET_CALIBRATION):
 | 
			
		||||
        b = await button.new_button(clear_power)
 | 
			
		||||
        await cg.register_parented(b, parent)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
#include "atm90e32_button.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -6,15 +7,73 @@ namespace atm90e32 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "atm90e32.button";
 | 
			
		||||
 | 
			
		||||
void ATM90E32CalibrationButton::press_action() {
 | 
			
		||||
  ESP_LOGI(TAG, "Running offset calibrations, Note: CTs and ACVs must be 0 during this process...");
 | 
			
		||||
void ATM90E32GainCalibrationButton::press_action() {
 | 
			
		||||
  if (this->parent_ == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Gain Calibration button [%s]", this->get_name().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "%s", this->get_name().c_str());
 | 
			
		||||
  ESP_LOGI(TAG,
 | 
			
		||||
           "[CALIBRATION] Use gain_ct: & gain_voltage: under each phase_x: in your config file to save these values");
 | 
			
		||||
  this->parent_->run_gain_calibrations();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32ClearGainCalibrationButton::press_action() {
 | 
			
		||||
  if (this->parent_ == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Gain button [%s]", this->get_name().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "%s", this->get_name().c_str());
 | 
			
		||||
  this->parent_->clear_gain_calibrations();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32OffsetCalibrationButton::press_action() {
 | 
			
		||||
  if (this->parent_ == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Offset Calibration button [%s]", this->get_name().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "%s", this->get_name().c_str());
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs and ACVs must be 0 during this process. USB power only**");
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] Use offset_voltage: & offset_current: under each phase_x: in your config file to save "
 | 
			
		||||
                "these values");
 | 
			
		||||
  this->parent_->run_offset_calibrations();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32ClearCalibrationButton::press_action() {
 | 
			
		||||
  ESP_LOGI(TAG, "Offset calibrations cleared.");
 | 
			
		||||
void ATM90E32ClearOffsetCalibrationButton::press_action() {
 | 
			
		||||
  if (this->parent_ == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Offset button [%s]", this->get_name().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "%s", this->get_name().c_str());
 | 
			
		||||
  this->parent_->clear_offset_calibrations();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32PowerOffsetCalibrationButton::press_action() {
 | 
			
		||||
  if (this->parent_ == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Power Calibration button [%s]", this->get_name().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "%s", this->get_name().c_str());
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs must be 0 during this process. Voltage reference should be present**");
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] Use offset_active_power: & offset_reactive_power: under each phase_x: in your config "
 | 
			
		||||
                "file to save these values");
 | 
			
		||||
  this->parent_->run_power_offset_calibrations();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32ClearPowerOffsetCalibrationButton::press_action() {
 | 
			
		||||
  if (this->parent_ == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Power button [%s]", this->get_name().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "%s", this->get_name().c_str());
 | 
			
		||||
  this->parent_->clear_power_offset_calibrations();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace atm90e32
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -7,17 +7,49 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace atm90e32 {
 | 
			
		||||
 | 
			
		||||
class ATM90E32CalibrationButton : public button::Button, public Parented<ATM90E32Component> {
 | 
			
		||||
class ATM90E32GainCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  ATM90E32CalibrationButton() = default;
 | 
			
		||||
  ATM90E32GainCalibrationButton() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void press_action() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ATM90E32ClearCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
 | 
			
		||||
class ATM90E32ClearGainCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  ATM90E32ClearCalibrationButton() = default;
 | 
			
		||||
  ATM90E32ClearGainCalibrationButton() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void press_action() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ATM90E32OffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  ATM90E32OffsetCalibrationButton() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void press_action() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ATM90E32ClearOffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  ATM90E32ClearOffsetCalibrationButton() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void press_action() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ATM90E32PowerOffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  ATM90E32PowerOffsetCalibrationButton() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void press_action() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ATM90E32ClearPowerOffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  ATM90E32ClearPowerOffsetCalibrationButton() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void press_action() override;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										130
									
								
								esphome/components/atm90e32/number/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								esphome/components/atm90e32/number/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,130 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import number
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_MAX_VALUE,
 | 
			
		||||
    CONF_MIN_VALUE,
 | 
			
		||||
    CONF_MODE,
 | 
			
		||||
    CONF_PHASE_A,
 | 
			
		||||
    CONF_PHASE_B,
 | 
			
		||||
    CONF_PHASE_C,
 | 
			
		||||
    CONF_REFERENCE_VOLTAGE,
 | 
			
		||||
    CONF_STEP,
 | 
			
		||||
    ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from .. import atm90e32_ns
 | 
			
		||||
from ..sensor import ATM90E32Component
 | 
			
		||||
 | 
			
		||||
ATM90E32Number = atm90e32_ns.class_(
 | 
			
		||||
    "ATM90E32Number", number.Number, cg.Parented.template(ATM90E32Component)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_REFERENCE_CURRENT = "reference_current"
 | 
			
		||||
PHASE_KEYS = [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
REFERENCE_VOLTAGE_PHASE_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Optional(CONF_MODE, default="box"): cv.string,
 | 
			
		||||
            cv.Optional(CONF_MIN_VALUE, default=100.0): cv.float_,
 | 
			
		||||
            cv.Optional(CONF_MAX_VALUE, default=260.0): cv.float_,
 | 
			
		||||
            cv.Optional(CONF_STEP, default=0.1): cv.float_,
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(
 | 
			
		||||
        number.number_schema(
 | 
			
		||||
            class_=ATM90E32Number,
 | 
			
		||||
            unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
            entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
            icon="mdi:power-plug",
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
REFERENCE_CURRENT_PHASE_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Optional(CONF_MODE, default="box"): cv.string,
 | 
			
		||||
            cv.Optional(CONF_MIN_VALUE, default=1.0): cv.float_,
 | 
			
		||||
            cv.Optional(CONF_MAX_VALUE, default=200.0): cv.float_,
 | 
			
		||||
            cv.Optional(CONF_STEP, default=0.1): cv.float_,
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(
 | 
			
		||||
        number.number_schema(
 | 
			
		||||
            class_=ATM90E32Number,
 | 
			
		||||
            unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
            entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
            icon="mdi:home-lightning-bolt",
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
REFERENCE_VOLTAGE_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Optional(CONF_PHASE_A): REFERENCE_VOLTAGE_PHASE_SCHEMA,
 | 
			
		||||
        cv.Optional(CONF_PHASE_B): REFERENCE_VOLTAGE_PHASE_SCHEMA,
 | 
			
		||||
        cv.Optional(CONF_PHASE_C): REFERENCE_VOLTAGE_PHASE_SCHEMA,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
REFERENCE_CURRENT_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Optional(CONF_PHASE_A): REFERENCE_CURRENT_PHASE_SCHEMA,
 | 
			
		||||
        cv.Optional(CONF_PHASE_B): REFERENCE_CURRENT_PHASE_SCHEMA,
 | 
			
		||||
        cv.Optional(CONF_PHASE_C): REFERENCE_CURRENT_PHASE_SCHEMA,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component),
 | 
			
		||||
        cv.Optional(CONF_REFERENCE_VOLTAGE): REFERENCE_VOLTAGE_SCHEMA,
 | 
			
		||||
        cv.Optional(CONF_REFERENCE_CURRENT): REFERENCE_CURRENT_SCHEMA,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    parent = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
 | 
			
		||||
    if voltage_cfg := config.get(CONF_REFERENCE_VOLTAGE):
 | 
			
		||||
        voltage_objs = [None, None, None]
 | 
			
		||||
 | 
			
		||||
        for i, key in enumerate(PHASE_KEYS):
 | 
			
		||||
            if validated := voltage_cfg.get(key):
 | 
			
		||||
                obj = await number.new_number(
 | 
			
		||||
                    validated,
 | 
			
		||||
                    min_value=validated["min_value"],
 | 
			
		||||
                    max_value=validated["max_value"],
 | 
			
		||||
                    step=validated["step"],
 | 
			
		||||
                )
 | 
			
		||||
                await cg.register_parented(obj, parent)
 | 
			
		||||
                voltage_objs[i] = obj
 | 
			
		||||
 | 
			
		||||
        # Inherit from A → B/C if only A defined
 | 
			
		||||
        if voltage_objs[0] is not None:
 | 
			
		||||
            for i in range(3):
 | 
			
		||||
                if voltage_objs[i] is None:
 | 
			
		||||
                    voltage_objs[i] = voltage_objs[0]
 | 
			
		||||
 | 
			
		||||
        for i, obj in enumerate(voltage_objs):
 | 
			
		||||
            if obj is not None:
 | 
			
		||||
                cg.add(parent.set_reference_voltage(i, obj))
 | 
			
		||||
 | 
			
		||||
    if current_cfg := config.get(CONF_REFERENCE_CURRENT):
 | 
			
		||||
        for i, key in enumerate(PHASE_KEYS):
 | 
			
		||||
            if validated := current_cfg.get(key):
 | 
			
		||||
                obj = await number.new_number(
 | 
			
		||||
                    validated,
 | 
			
		||||
                    min_value=validated["min_value"],
 | 
			
		||||
                    max_value=validated["max_value"],
 | 
			
		||||
                    step=validated["step"],
 | 
			
		||||
                )
 | 
			
		||||
                await cg.register_parented(obj, parent)
 | 
			
		||||
                cg.add(parent.set_reference_current(i, obj))
 | 
			
		||||
							
								
								
									
										16
									
								
								esphome/components/atm90e32/number/atm90e32_number.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								esphome/components/atm90e32/number/atm90e32_number.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/atm90e32/atm90e32.h"
 | 
			
		||||
#include "esphome/components/number/number.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace atm90e32 {
 | 
			
		||||
 | 
			
		||||
class ATM90E32Number : public number::Number, public Parented<ATM90E32Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  void control(float value) override { this->publish_state(value); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace atm90e32
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -33,6 +33,7 @@ from esphome.const import (
 | 
			
		||||
    UNIT_DEGREES,
 | 
			
		||||
    UNIT_HERTZ,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    UNIT_VOLT_AMPS,
 | 
			
		||||
    UNIT_VOLT_AMPS_REACTIVE,
 | 
			
		||||
    UNIT_WATT,
 | 
			
		||||
    UNIT_WATT_HOURS,
 | 
			
		||||
@@ -45,10 +46,17 @@ CONF_GAIN_PGA = "gain_pga"
 | 
			
		||||
CONF_CURRENT_PHASES = "current_phases"
 | 
			
		||||
CONF_GAIN_VOLTAGE = "gain_voltage"
 | 
			
		||||
CONF_GAIN_CT = "gain_ct"
 | 
			
		||||
CONF_OFFSET_VOLTAGE = "offset_voltage"
 | 
			
		||||
CONF_OFFSET_CURRENT = "offset_current"
 | 
			
		||||
CONF_OFFSET_ACTIVE_POWER = "offset_active_power"
 | 
			
		||||
CONF_OFFSET_REACTIVE_POWER = "offset_reactive_power"
 | 
			
		||||
CONF_HARMONIC_POWER = "harmonic_power"
 | 
			
		||||
CONF_PEAK_CURRENT = "peak_current"
 | 
			
		||||
CONF_PEAK_CURRENT_SIGNED = "peak_current_signed"
 | 
			
		||||
CONF_ENABLE_OFFSET_CALIBRATION = "enable_offset_calibration"
 | 
			
		||||
CONF_ENABLE_GAIN_CALIBRATION = "enable_gain_calibration"
 | 
			
		||||
CONF_PHASE_STATUS = "phase_status"
 | 
			
		||||
CONF_FREQUENCY_STATUS = "frequency_status"
 | 
			
		||||
UNIT_DEG = "degrees"
 | 
			
		||||
LINE_FREQS = {
 | 
			
		||||
    "50HZ": 50,
 | 
			
		||||
@@ -92,10 +100,11 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
 | 
			
		||||
            unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE,
 | 
			
		||||
            icon=ICON_LIGHTBULB,
 | 
			
		||||
            accuracy_decimals=2,
 | 
			
		||||
            device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_WATT,
 | 
			
		||||
            unit_of_measurement=UNIT_VOLT_AMPS,
 | 
			
		||||
            accuracy_decimals=2,
 | 
			
		||||
            device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
@@ -137,6 +146,10 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t,
 | 
			
		||||
        cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t,
 | 
			
		||||
        cv.Optional(CONF_OFFSET_VOLTAGE, default=0): cv.int_,
 | 
			
		||||
        cv.Optional(CONF_OFFSET_CURRENT, default=0): cv.int_,
 | 
			
		||||
        cv.Optional(CONF_OFFSET_ACTIVE_POWER, default=0): cv.int_,
 | 
			
		||||
        cv.Optional(CONF_OFFSET_REACTIVE_POWER, default=0): cv.int_,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -164,9 +177,10 @@ CONFIG_SCHEMA = (
 | 
			
		||||
            cv.Optional(CONF_CURRENT_PHASES, default="3"): cv.enum(
 | 
			
		||||
                CURRENT_PHASES, upper=True
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_GAIN_PGA, default="2X"): cv.enum(PGA_GAINS, upper=True),
 | 
			
		||||
            cv.Optional(CONF_GAIN_PGA, default="1X"): cv.enum(PGA_GAINS, upper=True),
 | 
			
		||||
            cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_ENABLE_OFFSET_CALIBRATION, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_ENABLE_GAIN_CALIBRATION, default=False): cv.boolean,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
@@ -185,6 +199,10 @@ async def to_code(config):
 | 
			
		||||
        conf = config[phase]
 | 
			
		||||
        cg.add(var.set_volt_gain(i, conf[CONF_GAIN_VOLTAGE]))
 | 
			
		||||
        cg.add(var.set_ct_gain(i, conf[CONF_GAIN_CT]))
 | 
			
		||||
        cg.add(var.set_voltage_offset(i, conf[CONF_OFFSET_VOLTAGE]))
 | 
			
		||||
        cg.add(var.set_current_offset(i, conf[CONF_OFFSET_CURRENT]))
 | 
			
		||||
        cg.add(var.set_active_power_offset(i, conf[CONF_OFFSET_ACTIVE_POWER]))
 | 
			
		||||
        cg.add(var.set_reactive_power_offset(i, conf[CONF_OFFSET_REACTIVE_POWER]))
 | 
			
		||||
        if voltage_config := conf.get(CONF_VOLTAGE):
 | 
			
		||||
            sens = await sensor.new_sensor(voltage_config)
 | 
			
		||||
            cg.add(var.set_voltage_sensor(i, sens))
 | 
			
		||||
@@ -218,16 +236,15 @@ async def to_code(config):
 | 
			
		||||
        if peak_current_config := conf.get(CONF_PEAK_CURRENT):
 | 
			
		||||
            sens = await sensor.new_sensor(peak_current_config)
 | 
			
		||||
            cg.add(var.set_peak_current_sensor(i, sens))
 | 
			
		||||
 | 
			
		||||
    if frequency_config := config.get(CONF_FREQUENCY):
 | 
			
		||||
        sens = await sensor.new_sensor(frequency_config)
 | 
			
		||||
        cg.add(var.set_freq_sensor(sens))
 | 
			
		||||
    if chip_temperature_config := config.get(CONF_CHIP_TEMPERATURE):
 | 
			
		||||
        sens = await sensor.new_sensor(chip_temperature_config)
 | 
			
		||||
        cg.add(var.set_chip_temperature_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
 | 
			
		||||
    cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES]))
 | 
			
		||||
    cg.add(var.set_pga_gain(config[CONF_GAIN_PGA]))
 | 
			
		||||
    cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED]))
 | 
			
		||||
    cg.add(var.set_enable_offset_calibration(config[CONF_ENABLE_OFFSET_CALIBRATION]))
 | 
			
		||||
    cg.add(var.set_enable_gain_calibration(config[CONF_ENABLE_GAIN_CALIBRATION]))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										48
									
								
								esphome/components/atm90e32/text_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								esphome/components/atm90e32/text_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import text_sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C
 | 
			
		||||
 | 
			
		||||
from ..sensor import ATM90E32Component
 | 
			
		||||
 | 
			
		||||
CONF_PHASE_STATUS = "phase_status"
 | 
			
		||||
CONF_FREQUENCY_STATUS = "frequency_status"
 | 
			
		||||
PHASE_KEYS = [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]
 | 
			
		||||
 | 
			
		||||
PHASE_STATUS_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Optional(CONF_PHASE_A): text_sensor.text_sensor_schema(
 | 
			
		||||
            icon="mdi:flash-alert"
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_PHASE_B): text_sensor.text_sensor_schema(
 | 
			
		||||
            icon="mdi:flash-alert"
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_PHASE_C): text_sensor.text_sensor_schema(
 | 
			
		||||
            icon="mdi:flash-alert"
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(ATM90E32Component),
 | 
			
		||||
        cv.Optional(CONF_PHASE_STATUS): PHASE_STATUS_SCHEMA,
 | 
			
		||||
        cv.Optional(CONF_FREQUENCY_STATUS): text_sensor.text_sensor_schema(
 | 
			
		||||
            icon="mdi:lightbulb-alert"
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    parent = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
 | 
			
		||||
    if phase_cfg := config.get(CONF_PHASE_STATUS):
 | 
			
		||||
        for i, key in enumerate(PHASE_KEYS):
 | 
			
		||||
            if sub_phase_cfg := phase_cfg.get(key):
 | 
			
		||||
                sens = await text_sensor.new_text_sensor(sub_phase_cfg)
 | 
			
		||||
                cg.add(parent.set_phase_status_text_sensor(i, sens))
 | 
			
		||||
 | 
			
		||||
    if freq_status_config := config.get(CONF_FREQUENCY_STATUS):
 | 
			
		||||
        sens = await text_sensor.new_text_sensor(freq_status_config)
 | 
			
		||||
        cg.add(parent.set_freq_status_text_sensor(sens))
 | 
			
		||||
@@ -37,29 +37,32 @@ AUDIO_COMPONENT_SCHEMA = cv.Schema(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_UNDEF = object()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def set_stream_limits(
 | 
			
		||||
    min_bits_per_sample: int = _UNDEF,
 | 
			
		||||
    max_bits_per_sample: int = _UNDEF,
 | 
			
		||||
    min_channels: int = _UNDEF,
 | 
			
		||||
    max_channels: int = _UNDEF,
 | 
			
		||||
    min_sample_rate: int = _UNDEF,
 | 
			
		||||
    max_sample_rate: int = _UNDEF,
 | 
			
		||||
    min_bits_per_sample: int = cv.UNDEFINED,
 | 
			
		||||
    max_bits_per_sample: int = cv.UNDEFINED,
 | 
			
		||||
    min_channels: int = cv.UNDEFINED,
 | 
			
		||||
    max_channels: int = cv.UNDEFINED,
 | 
			
		||||
    min_sample_rate: int = cv.UNDEFINED,
 | 
			
		||||
    max_sample_rate: int = cv.UNDEFINED,
 | 
			
		||||
):
 | 
			
		||||
    """Sets the limits for the audio stream that audio component can handle
 | 
			
		||||
 | 
			
		||||
    When the component sinks audio (e.g., a speaker), these indicate the limits to the audio it can receive.
 | 
			
		||||
    When the component sources audio (e.g., a microphone), these indicate the limits to the audio it can send.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def set_limits_in_config(config):
 | 
			
		||||
        if min_bits_per_sample is not _UNDEF:
 | 
			
		||||
        if min_bits_per_sample is not cv.UNDEFINED:
 | 
			
		||||
            config[CONF_MIN_BITS_PER_SAMPLE] = min_bits_per_sample
 | 
			
		||||
        if max_bits_per_sample is not _UNDEF:
 | 
			
		||||
        if max_bits_per_sample is not cv.UNDEFINED:
 | 
			
		||||
            config[CONF_MAX_BITS_PER_SAMPLE] = max_bits_per_sample
 | 
			
		||||
        if min_channels is not _UNDEF:
 | 
			
		||||
        if min_channels is not cv.UNDEFINED:
 | 
			
		||||
            config[CONF_MIN_CHANNELS] = min_channels
 | 
			
		||||
        if max_channels is not _UNDEF:
 | 
			
		||||
        if max_channels is not cv.UNDEFINED:
 | 
			
		||||
            config[CONF_MAX_CHANNELS] = max_channels
 | 
			
		||||
        if min_sample_rate is not _UNDEF:
 | 
			
		||||
        if min_sample_rate is not cv.UNDEFINED:
 | 
			
		||||
            config[CONF_MIN_SAMPLE_RATE] = min_sample_rate
 | 
			
		||||
        if max_sample_rate is not _UNDEF:
 | 
			
		||||
        if max_sample_rate is not cv.UNDEFINED:
 | 
			
		||||
            config[CONF_MAX_SAMPLE_RATE] = max_sample_rate
 | 
			
		||||
 | 
			
		||||
    return set_limits_in_config
 | 
			
		||||
@@ -69,43 +72,87 @@ def final_validate_audio_schema(
 | 
			
		||||
    name: str,
 | 
			
		||||
    *,
 | 
			
		||||
    audio_device: str,
 | 
			
		||||
    bits_per_sample: int,
 | 
			
		||||
    channels: int,
 | 
			
		||||
    sample_rate: int,
 | 
			
		||||
    bits_per_sample: int = cv.UNDEFINED,
 | 
			
		||||
    channels: int = cv.UNDEFINED,
 | 
			
		||||
    sample_rate: int = cv.UNDEFINED,
 | 
			
		||||
    enabled_channels: list[int] = cv.UNDEFINED,
 | 
			
		||||
    audio_device_issue: bool = False,
 | 
			
		||||
):
 | 
			
		||||
    """Validates audio compatibility when passed between different components.
 | 
			
		||||
 | 
			
		||||
    The component derived from ``AUDIO_COMPONENT_SCHEMA`` should call ``set_stream_limits`` in a validator to specify its compatible settings
 | 
			
		||||
 | 
			
		||||
      - If audio_device_issue is True, then the error message indicates the user should adjust the AUDIO_COMPONENT_SCHEMA derived component's configuration to match the values passed to this function
 | 
			
		||||
      - If audio_device_issue is False, then the error message indicates the user should adjust the configuration of the component calling this function, as it falls out of the valid stream limits
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        name (str): Friendly name of the component calling this function with an audio component to validate
 | 
			
		||||
        audio_device (str): The configuration parameter name that contains the ID of an AUDIO_COMPONENT_SCHEMA derived component to validate against
 | 
			
		||||
        bits_per_sample (int, optional): The desired bits per sample
 | 
			
		||||
        channels (int, optional): The desired number of channels
 | 
			
		||||
        sample_rate (int, optional): The desired sample rate
 | 
			
		||||
        enabled_channels (list[int], optional): The desired enabled channels
 | 
			
		||||
        audio_device_issue (bool, optional): Format the error message to indicate the problem is in the configuration for the ``audio_device`` component. Defaults to False.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def validate_audio_compatiblity(audio_config):
 | 
			
		||||
        audio_schema = {}
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            cv.int_range(
 | 
			
		||||
                min=audio_config.get(CONF_MIN_BITS_PER_SAMPLE),
 | 
			
		||||
                max=audio_config.get(CONF_MAX_BITS_PER_SAMPLE),
 | 
			
		||||
            )(bits_per_sample)
 | 
			
		||||
        except cv.Invalid as exc:
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                f"Invalid configuration for the {name} component. The {CONF_BITS_PER_SAMPLE} {str(exc)}"
 | 
			
		||||
            ) from exc
 | 
			
		||||
        if bits_per_sample is not cv.UNDEFINED:
 | 
			
		||||
            try:
 | 
			
		||||
                cv.int_range(
 | 
			
		||||
                    min=audio_config.get(CONF_MIN_BITS_PER_SAMPLE),
 | 
			
		||||
                    max=audio_config.get(CONF_MAX_BITS_PER_SAMPLE),
 | 
			
		||||
                )(bits_per_sample)
 | 
			
		||||
            except cv.Invalid as exc:
 | 
			
		||||
                if audio_device_issue:
 | 
			
		||||
                    error_string = f"Invalid configuration for the specified {audio_device}. The {name} component requires {bits_per_sample} bits per sample."
 | 
			
		||||
                else:
 | 
			
		||||
                    error_string = f"Invalid configuration for the {name} component. The {CONF_BITS_PER_SAMPLE} {str(exc)}"
 | 
			
		||||
                raise cv.Invalid(error_string) from exc
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            cv.int_range(
 | 
			
		||||
                min=audio_config.get(CONF_MIN_CHANNELS),
 | 
			
		||||
                max=audio_config.get(CONF_MAX_CHANNELS),
 | 
			
		||||
            )(channels)
 | 
			
		||||
        except cv.Invalid as exc:
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                f"Invalid configuration for the {name} component. The {CONF_NUM_CHANNELS} {str(exc)}"
 | 
			
		||||
            ) from exc
 | 
			
		||||
        if channels is not cv.UNDEFINED:
 | 
			
		||||
            try:
 | 
			
		||||
                cv.int_range(
 | 
			
		||||
                    min=audio_config.get(CONF_MIN_CHANNELS),
 | 
			
		||||
                    max=audio_config.get(CONF_MAX_CHANNELS),
 | 
			
		||||
                )(channels)
 | 
			
		||||
            except cv.Invalid as exc:
 | 
			
		||||
                if audio_device_issue:
 | 
			
		||||
                    error_string = f"Invalid configuration for the specified {audio_device}. The {name} component requires {channels} channels."
 | 
			
		||||
                else:
 | 
			
		||||
                    error_string = f"Invalid configuration for the {name} component. The {CONF_NUM_CHANNELS} {str(exc)}"
 | 
			
		||||
                raise cv.Invalid(error_string) from exc
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            cv.int_range(
 | 
			
		||||
                min=audio_config.get(CONF_MIN_SAMPLE_RATE),
 | 
			
		||||
                max=audio_config.get(CONF_MAX_SAMPLE_RATE),
 | 
			
		||||
            )(sample_rate)
 | 
			
		||||
            return cv.Schema(audio_schema, extra=cv.ALLOW_EXTRA)(audio_config)
 | 
			
		||||
        except cv.Invalid as exc:
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                f"Invalid configuration for the {name} component. The {CONF_SAMPLE_RATE} {str(exc)}"
 | 
			
		||||
            ) from exc
 | 
			
		||||
        if sample_rate is not cv.UNDEFINED:
 | 
			
		||||
            try:
 | 
			
		||||
                cv.int_range(
 | 
			
		||||
                    min=audio_config.get(CONF_MIN_SAMPLE_RATE),
 | 
			
		||||
                    max=audio_config.get(CONF_MAX_SAMPLE_RATE),
 | 
			
		||||
                )(sample_rate)
 | 
			
		||||
            except cv.Invalid as exc:
 | 
			
		||||
                if audio_device_issue:
 | 
			
		||||
                    error_string = f"Invalid configuration for the specified {audio_device}. The {name} component requires a {sample_rate} sample rate."
 | 
			
		||||
                else:
 | 
			
		||||
                    error_string = f"Invalid configuration for the {name} component. The {CONF_SAMPLE_RATE} {str(exc)}"
 | 
			
		||||
                raise cv.Invalid(error_string) from exc
 | 
			
		||||
 | 
			
		||||
        if enabled_channels is not cv.UNDEFINED:
 | 
			
		||||
            for channel in enabled_channels:
 | 
			
		||||
                try:
 | 
			
		||||
                    # Channels are 0-indexed
 | 
			
		||||
                    cv.int_range(
 | 
			
		||||
                        min=0,
 | 
			
		||||
                        max=audio_config.get(CONF_MAX_CHANNELS) - 1,
 | 
			
		||||
                    )(channel)
 | 
			
		||||
                except cv.Invalid as exc:
 | 
			
		||||
                    if audio_device_issue:
 | 
			
		||||
                        error_string = f"Invalid configuration for the specified {audio_device}. The {name} component requires channel {channel}."
 | 
			
		||||
                    else:
 | 
			
		||||
                        error_string = f"Invalid configuration for the {name} component. Enabled channel {channel} {str(exc)}"
 | 
			
		||||
                    raise cv.Invalid(error_string) from exc
 | 
			
		||||
 | 
			
		||||
        return cv.Schema(audio_schema, extra=cv.ALLOW_EXTRA)(audio_config)
 | 
			
		||||
 | 
			
		||||
    return cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
@@ -118,4 +165,4 @@ def final_validate_audio_schema(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add_library("esphome/esp-audio-libs", "1.1.3")
 | 
			
		||||
    cg.add_library("esphome/esp-audio-libs", "1.1.4")
 | 
			
		||||
 
 | 
			
		||||
@@ -135,5 +135,53 @@ const char *audio_file_type_to_string(AudioFileType file_type);
 | 
			
		||||
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
 | 
			
		||||
                         size_t samples_to_scale);
 | 
			
		||||
 | 
			
		||||
/// @brief Unpacks a quantized audio sample into a Q31 fixed-point number.
 | 
			
		||||
/// @param data Pointer to uint8_t array containing the audio sample
 | 
			
		||||
/// @param bytes_per_sample The number of bytes per sample
 | 
			
		||||
/// @return Q31 sample
 | 
			
		||||
inline int32_t unpack_audio_sample_to_q31(const uint8_t *data, size_t bytes_per_sample) {
 | 
			
		||||
  int32_t sample = 0;
 | 
			
		||||
  if (bytes_per_sample == 1) {
 | 
			
		||||
    sample |= data[0] << 24;
 | 
			
		||||
  } else if (bytes_per_sample == 2) {
 | 
			
		||||
    sample |= data[0] << 16;
 | 
			
		||||
    sample |= data[1] << 24;
 | 
			
		||||
  } else if (bytes_per_sample == 3) {
 | 
			
		||||
    sample |= data[0] << 8;
 | 
			
		||||
    sample |= data[1] << 16;
 | 
			
		||||
    sample |= data[2] << 24;
 | 
			
		||||
  } else if (bytes_per_sample == 4) {
 | 
			
		||||
    sample |= data[0];
 | 
			
		||||
    sample |= data[1] << 8;
 | 
			
		||||
    sample |= data[2] << 16;
 | 
			
		||||
    sample |= data[3] << 24;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return sample;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @brief Packs a Q31 fixed-point number as an audio sample with the specified number of bytes per sample.
 | 
			
		||||
/// Packs the most significant bits - no dithering is applied.
 | 
			
		||||
/// @param sample Q31 fixed-point number to pack
 | 
			
		||||
/// @param data Pointer to data array to store
 | 
			
		||||
/// @param bytes_per_sample The audio data's bytes per sample
 | 
			
		||||
inline void pack_q31_as_audio_sample(int32_t sample, uint8_t *data, size_t bytes_per_sample) {
 | 
			
		||||
  if (bytes_per_sample == 1) {
 | 
			
		||||
    data[0] = static_cast<uint8_t>(sample >> 24);
 | 
			
		||||
  } else if (bytes_per_sample == 2) {
 | 
			
		||||
    data[0] = static_cast<uint8_t>(sample >> 16);
 | 
			
		||||
    data[1] = static_cast<uint8_t>(sample >> 24);
 | 
			
		||||
  } else if (bytes_per_sample == 3) {
 | 
			
		||||
    data[0] = static_cast<uint8_t>(sample >> 8);
 | 
			
		||||
    data[1] = static_cast<uint8_t>(sample >> 16);
 | 
			
		||||
    data[2] = static_cast<uint8_t>(sample >> 24);
 | 
			
		||||
  } else if (bytes_per_sample == 4) {
 | 
			
		||||
    data[0] = static_cast<uint8_t>(sample);
 | 
			
		||||
    data[1] = static_cast<uint8_t>(sample >> 8);
 | 
			
		||||
    data[2] = static_cast<uint8_t>(sample >> 16);
 | 
			
		||||
    data[3] = static_cast<uint8_t>(sample >> 24);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace audio
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -171,7 +171,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
 | 
			
		||||
 | 
			
		||||
    bytes_available_before_processing = this->input_transfer_buffer_->available();
 | 
			
		||||
 | 
			
		||||
    if ((this->potentially_failed_count_ > 10) && (bytes_read == 0)) {
 | 
			
		||||
    if ((this->potentially_failed_count_ > 0) && (bytes_read == 0)) {
 | 
			
		||||
      // Failed to decode in last attempt and there is no new data
 | 
			
		||||
 | 
			
		||||
      if ((this->input_transfer_buffer_->free() == 0) && first_loop_iteration) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,8 @@
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
 | 
			
		||||
#include <cstring>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace audio {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
#include "audio_transfer_buffer.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/ring_buffer.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SPEAKER
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,5 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import climate_ir
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["climate_ir"]
 | 
			
		||||
CODEOWNERS = ["@bazuchan"]
 | 
			
		||||
@@ -9,13 +7,8 @@ CODEOWNERS = ["@bazuchan"]
 | 
			
		||||
ballu_ns = cg.esphome_ns.namespace("ballu")
 | 
			
		||||
BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(BalluClimate),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(BalluClimate)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await climate_ir.register_climate_ir(var, config)
 | 
			
		||||
    await climate_ir.new_climate_ir(config)
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_DEFAULT_TARGET_TEMPERATURE_LOW,
 | 
			
		||||
    CONF_HEAT_ACTION,
 | 
			
		||||
    CONF_HUMIDITY_SENSOR,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_IDLE_ACTION,
 | 
			
		||||
    CONF_SENSOR,
 | 
			
		||||
)
 | 
			
		||||
@@ -19,9 +18,9 @@ BangBangClimate = bang_bang_ns.class_("BangBangClimate", climate.Climate, cg.Com
 | 
			
		||||
BangBangClimateTargetTempConfig = bang_bang_ns.struct("BangBangClimateTargetTempConfig")
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    climate.CLIMATE_SCHEMA.extend(
 | 
			
		||||
    climate.climate_schema(BangBangClimate)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            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,
 | 
			
		||||
@@ -36,15 +35,15 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_HEAT_ACTION),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    var = await climate.new_climate(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await climate.register_climate(var, config)
 | 
			
		||||
 | 
			
		||||
    sens = await cg.get_variable(config[CONF_SENSOR])
 | 
			
		||||
    cg.add(var.set_sensor(sens))
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
#include "bedjet_hub.h"
 | 
			
		||||
#include "bedjet_child.h"
 | 
			
		||||
#include "bedjet_const.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,8 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import ble_client, climate
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_HEAT_MODE,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_RECEIVE_TIMEOUT,
 | 
			
		||||
    CONF_TEMPERATURE_SOURCE,
 | 
			
		||||
    CONF_TIME_ID,
 | 
			
		||||
@@ -13,7 +10,6 @@ from esphome.const import (
 | 
			
		||||
 | 
			
		||||
from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
CODEOWNERS = ["@jhansche"]
 | 
			
		||||
DEPENDENCIES = ["bedjet"]
 | 
			
		||||
 | 
			
		||||
@@ -30,9 +26,9 @@ BEDJET_TEMPERATURE_SOURCES = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    climate.CLIMATE_SCHEMA.extend(
 | 
			
		||||
    climate.climate_schema(BedJetClimate)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BedJetClimate),
 | 
			
		||||
            cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum(
 | 
			
		||||
                BEDJET_HEAT_MODES, lower=True
 | 
			
		||||
            ),
 | 
			
		||||
@@ -63,9 +59,8 @@ CONFIG_SCHEMA = (
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    var = await climate.new_climate(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await climate.register_climate(var, config)
 | 
			
		||||
    await register_bedjet_child(var, config)
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_heating_mode(config[CONF_HEAT_MODE]))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +1,22 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import fan
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
CODEOWNERS = ["@jhansche"]
 | 
			
		||||
DEPENDENCIES = ["bedjet"]
 | 
			
		||||
 | 
			
		||||
BedJetFan = bedjet_ns.class_("BedJetFan", fan.Fan, cg.PollingComponent)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    fan.FAN_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BedJetFan),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    fan.fan_schema(BedJetFan)
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(BEDJET_CLIENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    var = await fan.new_fan(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await fan.register_fan(var, config)
 | 
			
		||||
    await register_bedjet_child(var, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +1,28 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import fan, output
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_DIRECTION_OUTPUT,
 | 
			
		||||
    CONF_OSCILLATION_OUTPUT,
 | 
			
		||||
    CONF_OUTPUT,
 | 
			
		||||
    CONF_OUTPUT_ID,
 | 
			
		||||
)
 | 
			
		||||
from esphome.const import CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT
 | 
			
		||||
 | 
			
		||||
from .. import binary_ns
 | 
			
		||||
 | 
			
		||||
BinaryFan = binary_ns.class_("BinaryFan", fan.Fan, cg.Component)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryFan),
 | 
			
		||||
        cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
 | 
			
		||||
        cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput),
 | 
			
		||||
        cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    fan.fan_schema(BinaryFan)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
 | 
			
		||||
            cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput),
 | 
			
		||||
            cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
 | 
			
		||||
    var = await fan.new_fan(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await fan.register_fan(var, config)
 | 
			
		||||
 | 
			
		||||
    output_ = await cg.get_variable(config[CONF_OUTPUT])
 | 
			
		||||
    cg.add(var.set_output(output_))
 | 
			
		||||
 
 | 
			
		||||
@@ -386,7 +386,7 @@ def validate_click_timing(value):
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
BINARY_SENSOR_SCHEMA = (
 | 
			
		||||
_BINARY_SENSOR_SCHEMA = (
 | 
			
		||||
    cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
 | 
			
		||||
    .extend(cv.MQTT_COMPONENT_SCHEMA)
 | 
			
		||||
    .extend(
 | 
			
		||||
@@ -458,19 +458,17 @@ BINARY_SENSOR_SCHEMA = (
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
_UNDEF = object()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def binary_sensor_schema(
 | 
			
		||||
    class_: MockObjClass = _UNDEF,
 | 
			
		||||
    class_: MockObjClass = cv.UNDEFINED,
 | 
			
		||||
    *,
 | 
			
		||||
    icon: str = _UNDEF,
 | 
			
		||||
    entity_category: str = _UNDEF,
 | 
			
		||||
    device_class: str = _UNDEF,
 | 
			
		||||
    icon: str = cv.UNDEFINED,
 | 
			
		||||
    entity_category: str = cv.UNDEFINED,
 | 
			
		||||
    device_class: str = cv.UNDEFINED,
 | 
			
		||||
) -> cv.Schema:
 | 
			
		||||
    schema = {}
 | 
			
		||||
 | 
			
		||||
    if class_ is not _UNDEF:
 | 
			
		||||
    if class_ is not cv.UNDEFINED:
 | 
			
		||||
        # Not cv.optional
 | 
			
		||||
        schema[cv.GenerateID()] = cv.declare_id(class_)
 | 
			
		||||
 | 
			
		||||
@@ -479,10 +477,15 @@ def binary_sensor_schema(
 | 
			
		||||
        (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
 | 
			
		||||
        (CONF_DEVICE_CLASS, device_class, validate_device_class),
 | 
			
		||||
    ]:
 | 
			
		||||
        if default is not _UNDEF:
 | 
			
		||||
        if default is not cv.UNDEFINED:
 | 
			
		||||
            schema[cv.Optional(key, default=default)] = validator
 | 
			
		||||
 | 
			
		||||
    return BINARY_SENSOR_SCHEMA.extend(schema)
 | 
			
		||||
    return _BINARY_SENSOR_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
BINARY_SENSOR_SCHEMA = binary_sensor_schema()
 | 
			
		||||
BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_binary_sensor_core_(var, config):
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,7 @@ static const uint8_t BL0906_WRITE_COMMAND = 0xCA;
 | 
			
		||||
static const uint8_t BL0906_V_RMS = 0x16;
 | 
			
		||||
 | 
			
		||||
// Total power
 | 
			
		||||
static const uint8_t BL0906_WATT_SUM = 0X2C;
 | 
			
		||||
static const uint8_t BL0906_WATT_SUM = 0x2C;
 | 
			
		||||
 | 
			
		||||
// Current1~6
 | 
			
		||||
static const uint8_t BL0906_I_1_RMS = 0x0D;  // current_1
 | 
			
		||||
@@ -56,29 +56,29 @@ static const uint8_t BL0906_I_5_RMS = 0x13;
 | 
			
		||||
static const uint8_t BL0906_I_6_RMS = 0x14;  // current_6
 | 
			
		||||
 | 
			
		||||
// Power1~6
 | 
			
		||||
static const uint8_t BL0906_WATT_1 = 0X23;  // power_1
 | 
			
		||||
static const uint8_t BL0906_WATT_2 = 0X24;
 | 
			
		||||
static const uint8_t BL0906_WATT_3 = 0X25;
 | 
			
		||||
static const uint8_t BL0906_WATT_4 = 0X26;
 | 
			
		||||
static const uint8_t BL0906_WATT_5 = 0X29;
 | 
			
		||||
static const uint8_t BL0906_WATT_6 = 0X2A;  // power_6
 | 
			
		||||
static const uint8_t BL0906_WATT_1 = 0x23;  // power_1
 | 
			
		||||
static const uint8_t BL0906_WATT_2 = 0x24;
 | 
			
		||||
static const uint8_t BL0906_WATT_3 = 0x25;
 | 
			
		||||
static const uint8_t BL0906_WATT_4 = 0x26;
 | 
			
		||||
static const uint8_t BL0906_WATT_5 = 0x29;
 | 
			
		||||
static const uint8_t BL0906_WATT_6 = 0x2A;  // power_6
 | 
			
		||||
 | 
			
		||||
// Active pulse count, unsigned
 | 
			
		||||
static const uint8_t BL0906_CF_1_CNT = 0X30;  // Channel_1
 | 
			
		||||
static const uint8_t BL0906_CF_2_CNT = 0X31;
 | 
			
		||||
static const uint8_t BL0906_CF_3_CNT = 0X32;
 | 
			
		||||
static const uint8_t BL0906_CF_4_CNT = 0X33;
 | 
			
		||||
static const uint8_t BL0906_CF_5_CNT = 0X36;
 | 
			
		||||
static const uint8_t BL0906_CF_6_CNT = 0X37;  // Channel_6
 | 
			
		||||
static const uint8_t BL0906_CF_1_CNT = 0x30;  // Channel_1
 | 
			
		||||
static const uint8_t BL0906_CF_2_CNT = 0x31;
 | 
			
		||||
static const uint8_t BL0906_CF_3_CNT = 0x32;
 | 
			
		||||
static const uint8_t BL0906_CF_4_CNT = 0x33;
 | 
			
		||||
static const uint8_t BL0906_CF_5_CNT = 0x36;
 | 
			
		||||
static const uint8_t BL0906_CF_6_CNT = 0x37;  // Channel_6
 | 
			
		||||
 | 
			
		||||
// Total active pulse count, unsigned
 | 
			
		||||
static const uint8_t BL0906_CF_SUM_CNT = 0X39;
 | 
			
		||||
static const uint8_t BL0906_CF_SUM_CNT = 0x39;
 | 
			
		||||
 | 
			
		||||
// Voltage frequency cycle
 | 
			
		||||
static const uint8_t BL0906_FREQUENCY = 0X4E;
 | 
			
		||||
static const uint8_t BL0906_FREQUENCY = 0x4E;
 | 
			
		||||
 | 
			
		||||
// Internal temperature
 | 
			
		||||
static const uint8_t BL0906_TEMPERATURE = 0X5E;
 | 
			
		||||
static const uint8_t BL0906_TEMPERATURE = 0x5E;
 | 
			
		||||
 | 
			
		||||
// Calibration register
 | 
			
		||||
// RMS gain adjustment register
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ from esphome.components import ble_client, esp32_ble_tracker, text_sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_CHARACTERISTIC_UUID,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_NOTIFY,
 | 
			
		||||
    CONF_SERVICE_UUID,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
@@ -32,9 +31,9 @@ BLETextSensorNotifyTrigger = ble_client_ns.class_(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    text_sensor.TEXT_SENSOR_SCHEMA.extend(
 | 
			
		||||
    text_sensor.text_sensor_schema(BLETextSensor)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BLETextSensor),
 | 
			
		||||
            cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
 | 
			
		||||
            cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid,
 | 
			
		||||
            cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid,
 | 
			
		||||
@@ -54,7 +53,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    var = await text_sensor.new_text_sensor(config)
 | 
			
		||||
    if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
 | 
			
		||||
        cg.add(
 | 
			
		||||
            var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))
 | 
			
		||||
@@ -101,7 +100,6 @@ async def to_code(config):
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await ble_client.register_ble_node(var, config)
 | 
			
		||||
    cg.add(var.set_enable_notify(config[CONF_NOTIFY]))
 | 
			
		||||
    await text_sensor.register_text_sensor(var, config)
 | 
			
		||||
    for conf in config.get(CONF_ON_NOTIFY, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await ble_client.register_ble_node(trigger, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -73,9 +73,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
 | 
			
		||||
      resp.address = this->address_;
 | 
			
		||||
      resp.handle = param->read.handle;
 | 
			
		||||
      resp.data.reserve(param->read.value_len);
 | 
			
		||||
      for (uint16_t i = 0; i < param->read.value_len; i++) {
 | 
			
		||||
        resp.data.push_back(param->read.value[i]);
 | 
			
		||||
      }
 | 
			
		||||
      // Use bulk insert instead of individual push_backs
 | 
			
		||||
      resp.data.insert(resp.data.end(), param->read.value, param->read.value + param->read.value_len);
 | 
			
		||||
      this->proxy_->get_api_connection()->send_bluetooth_gatt_read_response(resp);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
@@ -127,9 +126,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
 | 
			
		||||
      resp.address = this->address_;
 | 
			
		||||
      resp.handle = param->notify.handle;
 | 
			
		||||
      resp.data.reserve(param->notify.value_len);
 | 
			
		||||
      for (uint16_t i = 0; i < param->notify.value_len; i++) {
 | 
			
		||||
        resp.data.push_back(param->notify.value[i]);
 | 
			
		||||
      }
 | 
			
		||||
      // Use bulk insert instead of individual push_backs
 | 
			
		||||
      resp.data.insert(resp.data.end(), param->notify.value, param->notify.value + param->notify.value_len);
 | 
			
		||||
      this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_data_response(resp);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/macros.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
@@ -25,6 +26,22 @@ std::vector<uint64_t> get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) {
 | 
			
		||||
 | 
			
		||||
BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; }
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::setup() {
 | 
			
		||||
  this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
 | 
			
		||||
    if (this->api_connection_ != nullptr) {
 | 
			
		||||
      this->send_bluetooth_scanner_state_(state);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state) {
 | 
			
		||||
  api::BluetoothScannerStateResponse resp;
 | 
			
		||||
  resp.state = static_cast<api::enums::BluetoothScannerState>(state);
 | 
			
		||||
  resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
 | 
			
		||||
                                               : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE;
 | 
			
		||||
  this->api_connection_->send_bluetooth_scanner_state_response(resp);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
 | 
			
		||||
  if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || this->raw_advertisements_)
 | 
			
		||||
    return false;
 | 
			
		||||
@@ -35,33 +52,60 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static constexpr size_t FLUSH_BATCH_SIZE = 8;
 | 
			
		||||
static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() {
 | 
			
		||||
  static std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
 | 
			
		||||
  return batch_buffer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) {
 | 
			
		||||
  if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_)
 | 
			
		||||
    return false;
 | 
			
		||||
 | 
			
		||||
  api::BluetoothLERawAdvertisementsResponse resp;
 | 
			
		||||
  // Get the batch buffer reference
 | 
			
		||||
  auto &batch_buffer = get_batch_buffer();
 | 
			
		||||
 | 
			
		||||
  // Reserve additional capacity if needed
 | 
			
		||||
  size_t new_size = batch_buffer.size() + count;
 | 
			
		||||
  if (batch_buffer.capacity() < new_size) {
 | 
			
		||||
    batch_buffer.reserve(new_size);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Add new advertisements to the batch buffer
 | 
			
		||||
  for (size_t i = 0; i < count; i++) {
 | 
			
		||||
    auto &result = advertisements[i];
 | 
			
		||||
    api::BluetoothLERawAdvertisement adv;
 | 
			
		||||
    uint8_t length = result.adv_data_len + result.scan_rsp_len;
 | 
			
		||||
 | 
			
		||||
    batch_buffer.emplace_back();
 | 
			
		||||
    auto &adv = batch_buffer.back();
 | 
			
		||||
    adv.address = esp32_ble::ble_addr_to_uint64(result.bda);
 | 
			
		||||
    adv.rssi = result.rssi;
 | 
			
		||||
    adv.address_type = result.ble_addr_type;
 | 
			
		||||
    adv.data.assign(&result.ble_adv[0], &result.ble_adv[length]);
 | 
			
		||||
 | 
			
		||||
    uint8_t length = result.adv_data_len + result.scan_rsp_len;
 | 
			
		||||
    adv.data.reserve(length);
 | 
			
		||||
    for (uint16_t i = 0; i < length; i++) {
 | 
			
		||||
      adv.data.push_back(result.ble_adv[i]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    resp.advertisements.push_back(std::move(adv));
 | 
			
		||||
 | 
			
		||||
    ESP_LOGV(TAG, "Proxying raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0],
 | 
			
		||||
    ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0],
 | 
			
		||||
             result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi);
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGV(TAG, "Proxying %d packets", count);
 | 
			
		||||
  this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp);
 | 
			
		||||
 | 
			
		||||
  // Only send if we've accumulated a good batch size to maximize batching efficiency
 | 
			
		||||
  // https://github.com/esphome/backlog/issues/21
 | 
			
		||||
  if (batch_buffer.size() >= FLUSH_BATCH_SIZE) {
 | 
			
		||||
    this->flush_pending_advertisements();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::flush_pending_advertisements() {
 | 
			
		||||
  auto &batch_buffer = get_batch_buffer();
 | 
			
		||||
  if (batch_buffer.empty() || !api::global_api_server->is_connected() || this->api_connection_ == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  api::BluetoothLERawAdvertisementsResponse resp;
 | 
			
		||||
  resp.advertisements.swap(batch_buffer);
 | 
			
		||||
  this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) {
 | 
			
		||||
  api::BluetoothLEAdvertisementResponse resp;
 | 
			
		||||
  resp.address = device.address_uint64();
 | 
			
		||||
@@ -69,21 +113,34 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi
 | 
			
		||||
  if (!device.get_name().empty())
 | 
			
		||||
    resp.name = device.get_name();
 | 
			
		||||
  resp.rssi = device.get_rssi();
 | 
			
		||||
  for (auto uuid : device.get_service_uuids()) {
 | 
			
		||||
    resp.service_uuids.push_back(uuid.to_string());
 | 
			
		||||
 | 
			
		||||
  // Pre-allocate vectors based on known sizes
 | 
			
		||||
  auto service_uuids = device.get_service_uuids();
 | 
			
		||||
  resp.service_uuids.reserve(service_uuids.size());
 | 
			
		||||
  for (auto &uuid : service_uuids) {
 | 
			
		||||
    resp.service_uuids.emplace_back(uuid.to_string());
 | 
			
		||||
  }
 | 
			
		||||
  for (auto &data : device.get_service_datas()) {
 | 
			
		||||
    api::BluetoothServiceData service_data;
 | 
			
		||||
 | 
			
		||||
  // Pre-allocate service data vector
 | 
			
		||||
  auto service_datas = device.get_service_datas();
 | 
			
		||||
  resp.service_data.reserve(service_datas.size());
 | 
			
		||||
  for (auto &data : service_datas) {
 | 
			
		||||
    resp.service_data.emplace_back();
 | 
			
		||||
    auto &service_data = resp.service_data.back();
 | 
			
		||||
    service_data.uuid = data.uuid.to_string();
 | 
			
		||||
    service_data.data.assign(data.data.begin(), data.data.end());
 | 
			
		||||
    resp.service_data.push_back(std::move(service_data));
 | 
			
		||||
  }
 | 
			
		||||
  for (auto &data : device.get_manufacturer_datas()) {
 | 
			
		||||
    api::BluetoothServiceData manufacturer_data;
 | 
			
		||||
 | 
			
		||||
  // Pre-allocate manufacturer data vector
 | 
			
		||||
  auto manufacturer_datas = device.get_manufacturer_datas();
 | 
			
		||||
  resp.manufacturer_data.reserve(manufacturer_datas.size());
 | 
			
		||||
  for (auto &data : manufacturer_datas) {
 | 
			
		||||
    resp.manufacturer_data.emplace_back();
 | 
			
		||||
    auto &manufacturer_data = resp.manufacturer_data.back();
 | 
			
		||||
    manufacturer_data.uuid = data.uuid.to_string();
 | 
			
		||||
    manufacturer_data.data.assign(data.data.begin(), data.data.end());
 | 
			
		||||
    resp.manufacturer_data.push_back(std::move(manufacturer_data));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->api_connection_->send_bluetooth_le_advertisement(resp);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -117,6 +174,18 @@ void BluetoothProxy::loop() {
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Flush any pending BLE advertisements that have been accumulated but not yet sent
 | 
			
		||||
  if (this->raw_advertisements_) {
 | 
			
		||||
    static uint32_t last_flush_time = 0;
 | 
			
		||||
    uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
 | 
			
		||||
    // Flush accumulated advertisements every 100ms
 | 
			
		||||
    if (now - last_flush_time >= 100) {
 | 
			
		||||
      this->flush_pending_advertisements();
 | 
			
		||||
      last_flush_time = now;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  for (auto *connection : this->connections_) {
 | 
			
		||||
    if (connection->send_service_ == connection->service_count_) {
 | 
			
		||||
      connection->send_service_ = DONE_SENDING_SERVICES;
 | 
			
		||||
@@ -145,11 +214,27 @@ void BluetoothProxy::loop() {
 | 
			
		||||
      }
 | 
			
		||||
      api::BluetoothGATTGetServicesResponse resp;
 | 
			
		||||
      resp.address = connection->get_address();
 | 
			
		||||
      resp.services.reserve(1);  // Always one service per response in this implementation
 | 
			
		||||
      api::BluetoothGATTService service_resp;
 | 
			
		||||
      service_resp.uuid = get_128bit_uuid_vec(service_result.uuid);
 | 
			
		||||
      service_resp.handle = service_result.start_handle;
 | 
			
		||||
      uint16_t char_offset = 0;
 | 
			
		||||
      esp_gattc_char_elem_t char_result;
 | 
			
		||||
      // Get the number of characteristics directly with one call
 | 
			
		||||
      uint16_t total_char_count = 0;
 | 
			
		||||
      esp_gatt_status_t char_count_status = esp_ble_gattc_get_attr_count(
 | 
			
		||||
          connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_CHARACTERISTIC,
 | 
			
		||||
          service_result.start_handle, service_result.end_handle, 0, &total_char_count);
 | 
			
		||||
 | 
			
		||||
      if (char_count_status == ESP_GATT_OK && total_char_count > 0) {
 | 
			
		||||
        // Only reserve if we successfully got a count
 | 
			
		||||
        service_resp.characteristics.reserve(total_char_count);
 | 
			
		||||
      } else if (char_count_status != ESP_GATT_OK) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", connection->get_connection_index(),
 | 
			
		||||
                 connection->address_str().c_str(), char_count_status);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Now process characteristics
 | 
			
		||||
      while (true) {  // characteristics
 | 
			
		||||
        uint16_t char_count = 1;
 | 
			
		||||
        esp_gatt_status_t char_status = esp_ble_gattc_get_all_char(
 | 
			
		||||
@@ -171,6 +256,23 @@ void BluetoothProxy::loop() {
 | 
			
		||||
        characteristic_resp.handle = char_result.char_handle;
 | 
			
		||||
        characteristic_resp.properties = char_result.properties;
 | 
			
		||||
        char_offset++;
 | 
			
		||||
 | 
			
		||||
        // Get the number of descriptors directly with one call
 | 
			
		||||
        uint16_t total_desc_count = 0;
 | 
			
		||||
        esp_gatt_status_t desc_count_status =
 | 
			
		||||
            esp_ble_gattc_get_attr_count(connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_DESCRIPTOR,
 | 
			
		||||
                                         char_result.char_handle, service_result.end_handle, 0, &total_desc_count);
 | 
			
		||||
 | 
			
		||||
        if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) {
 | 
			
		||||
          // Only reserve if we successfully got a count
 | 
			
		||||
          characteristic_resp.descriptors.reserve(total_desc_count);
 | 
			
		||||
        } else if (desc_count_status != ESP_GATT_OK) {
 | 
			
		||||
          ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d",
 | 
			
		||||
                   connection->get_connection_index(), connection->address_str().c_str(), char_result.char_handle,
 | 
			
		||||
                   desc_count_status);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Now process descriptors
 | 
			
		||||
        uint16_t desc_offset = 0;
 | 
			
		||||
        esp_gattc_descr_elem_t desc_result;
 | 
			
		||||
        while (true) {  // descriptors
 | 
			
		||||
@@ -265,6 +367,12 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
 | 
			
		||||
                 connection->get_connection_index(), connection->address_str().c_str());
 | 
			
		||||
        return;
 | 
			
		||||
      } else if (connection->state() == espbt::ClientState::CONNECTING) {
 | 
			
		||||
        if (connection->disconnect_pending()) {
 | 
			
		||||
          ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect",
 | 
			
		||||
                   connection->get_connection_index(), connection->address_str().c_str());
 | 
			
		||||
          connection->cancel_pending_disconnect();
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already connecting", connection->get_connection_index(),
 | 
			
		||||
                 connection->address_str().c_str());
 | 
			
		||||
        return;
 | 
			
		||||
@@ -447,6 +555,8 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection
 | 
			
		||||
  this->api_connection_ = api_connection;
 | 
			
		||||
  this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS;
 | 
			
		||||
  this->parent_->recalculate_advertisement_parser_types();
 | 
			
		||||
 | 
			
		||||
  this->send_bluetooth_scanner_state_(this->parent_->get_scanner_state());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connection) {
 | 
			
		||||
@@ -519,6 +629,17 @@ void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_e
 | 
			
		||||
  this->api_connection_->send_bluetooth_device_unpairing_response(call);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {
 | 
			
		||||
  if (this->parent_->get_scan_active() == active) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGD(TAG, "Setting scanner mode to %s", active ? "active" : "passive");
 | 
			
		||||
  this->parent_->set_scan_active(active);
 | 
			
		||||
  this->parent_->stop_scan();
 | 
			
		||||
  this->parent_->set_scan_continuous(
 | 
			
		||||
      true);  // Set this to true to automatically start scanning again when it has cleaned up.
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
BluetoothProxy *global_bluetooth_proxy = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 | 
			
		||||
}  // namespace bluetooth_proxy
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,7 @@ enum BluetoothProxyFeature : uint32_t {
 | 
			
		||||
  FEATURE_PAIRING = 1 << 3,
 | 
			
		||||
  FEATURE_CACHE_CLEARING = 1 << 4,
 | 
			
		||||
  FEATURE_RAW_ADVERTISEMENTS = 1 << 5,
 | 
			
		||||
  FEATURE_STATE_AND_MODE = 1 << 6,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum BluetoothProxySubscriptionFlag : uint32_t {
 | 
			
		||||
@@ -53,7 +54,9 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
 | 
			
		||||
  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
 | 
			
		||||
  bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void flush_pending_advertisements();
 | 
			
		||||
  esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
 | 
			
		||||
 | 
			
		||||
  void register_connection(BluetoothConnection *connection) {
 | 
			
		||||
@@ -84,6 +87,8 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
 | 
			
		||||
  void send_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK);
 | 
			
		||||
  void send_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK);
 | 
			
		||||
 | 
			
		||||
  void bluetooth_scanner_set_mode(bool active);
 | 
			
		||||
 | 
			
		||||
  static void uint64_to_bd_addr(uint64_t address, esp_bd_addr_t bd_addr) {
 | 
			
		||||
    bd_addr[0] = (address >> 40) & 0xff;
 | 
			
		||||
    bd_addr[1] = (address >> 32) & 0xff;
 | 
			
		||||
@@ -107,6 +112,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
 | 
			
		||||
    uint32_t flags = 0;
 | 
			
		||||
    flags |= BluetoothProxyFeature::FEATURE_PASSIVE_SCAN;
 | 
			
		||||
    flags |= BluetoothProxyFeature::FEATURE_RAW_ADVERTISEMENTS;
 | 
			
		||||
    flags |= BluetoothProxyFeature::FEATURE_STATE_AND_MODE;
 | 
			
		||||
    if (this->active_) {
 | 
			
		||||
      flags |= BluetoothProxyFeature::FEATURE_ACTIVE_CONNECTIONS;
 | 
			
		||||
      flags |= BluetoothProxyFeature::FEATURE_REMOTE_CACHING;
 | 
			
		||||
@@ -124,6 +130,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device);
 | 
			
		||||
  void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state);
 | 
			
		||||
 | 
			
		||||
  BluetoothConnection *get_connection_(uint64_t address, bool reserve);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,7 @@ ButtonPressTrigger = button_ns.class_(
 | 
			
		||||
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
BUTTON_SCHEMA = (
 | 
			
		||||
_BUTTON_SCHEMA = (
 | 
			
		||||
    cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
 | 
			
		||||
    .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
 | 
			
		||||
    .extend(
 | 
			
		||||
@@ -60,15 +60,13 @@ BUTTON_SCHEMA = (
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
_UNDEF = object()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def button_schema(
 | 
			
		||||
    class_: MockObjClass,
 | 
			
		||||
    *,
 | 
			
		||||
    icon: str = _UNDEF,
 | 
			
		||||
    entity_category: str = _UNDEF,
 | 
			
		||||
    device_class: str = _UNDEF,
 | 
			
		||||
    icon: str = cv.UNDEFINED,
 | 
			
		||||
    entity_category: str = cv.UNDEFINED,
 | 
			
		||||
    device_class: str = cv.UNDEFINED,
 | 
			
		||||
) -> cv.Schema:
 | 
			
		||||
    schema = {cv.GenerateID(): cv.declare_id(class_)}
 | 
			
		||||
 | 
			
		||||
@@ -77,10 +75,15 @@ def button_schema(
 | 
			
		||||
        (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
 | 
			
		||||
        (CONF_DEVICE_CLASS, device_class, validate_device_class),
 | 
			
		||||
    ]:
 | 
			
		||||
        if default is not _UNDEF:
 | 
			
		||||
        if default is not cv.UNDEFINED:
 | 
			
		||||
            schema[cv.Optional(key, default=default)] = validator
 | 
			
		||||
 | 
			
		||||
    return BUTTON_SCHEMA.extend(schema)
 | 
			
		||||
    return _BUTTON_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
BUTTON_SCHEMA = button_schema(Button)
 | 
			
		||||
BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_button_core_(var, config):
 | 
			
		||||
 
 | 
			
		||||
@@ -86,6 +86,9 @@ void Canbus::loop() {
 | 
			
		||||
      data.push_back(can_message.data[i]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this->callback_manager_(can_message.can_id, can_message.use_extended_id, can_message.remote_transmission_request,
 | 
			
		||||
                            data);
 | 
			
		||||
 | 
			
		||||
    // fire all triggers
 | 
			
		||||
    for (auto *trigger : this->triggers_) {
 | 
			
		||||
      if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) &&
 | 
			
		||||
 
 | 
			
		||||
@@ -81,6 +81,20 @@ class Canbus : public Component {
 | 
			
		||||
  void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; }
 | 
			
		||||
 | 
			
		||||
  void add_trigger(CanbusTrigger *trigger);
 | 
			
		||||
  /**
 | 
			
		||||
   * Add a callback to be called when a CAN message is received. All received messages
 | 
			
		||||
   * are passed to the callback without filtering.
 | 
			
		||||
   *
 | 
			
		||||
   * The callback function receives:
 | 
			
		||||
   * - can_id of the received data
 | 
			
		||||
   * - extended_id True if the can_id is an extended id
 | 
			
		||||
   * - rtr If this is a remote transmission request
 | 
			
		||||
   * - data The message data
 | 
			
		||||
   */
 | 
			
		||||
  void add_callback(
 | 
			
		||||
      std::function<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)> callback) {
 | 
			
		||||
    this->callback_manager_.add(std::move(callback));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  template<typename... Ts> friend class CanbusSendAction;
 | 
			
		||||
@@ -88,6 +102,8 @@ class Canbus : public Component {
 | 
			
		||||
  uint32_t can_id_;
 | 
			
		||||
  bool use_extended_id_;
 | 
			
		||||
  CanSpeed bit_rate_;
 | 
			
		||||
  CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
 | 
			
		||||
      callback_manager_{};
 | 
			
		||||
 | 
			
		||||
  virtual bool setup_internal();
 | 
			
		||||
  virtual Error send_message(struct CanFrame *frame);
 | 
			
		||||
 
 | 
			
		||||
@@ -32,14 +32,14 @@ CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(CCS811Component),
 | 
			
		||||
            cv.Required(CONF_ECO2): sensor.sensor_schema(
 | 
			
		||||
            cv.Optional(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(
 | 
			
		||||
            cv.Optional(CONF_TVOC): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PARTS_PER_BILLION,
 | 
			
		||||
                icon=ICON_RADIATOR,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
@@ -64,10 +64,13 @@ async def to_code(config):
 | 
			
		||||
    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))
 | 
			
		||||
    if eco2_config := config.get(CONF_ECO2):
 | 
			
		||||
        sens = await sensor.new_sensor(eco2_config)
 | 
			
		||||
        cg.add(var.set_co2(sens))
 | 
			
		||||
 | 
			
		||||
    if tvoc_config := config.get(CONF_TVOC):
 | 
			
		||||
        sens = await sensor.new_sensor(tvoc_config)
 | 
			
		||||
        cg.add(var.set_tvoc(sens))
 | 
			
		||||
 | 
			
		||||
    if version_config := config.get(CONF_VERSION):
 | 
			
		||||
        sens = await text_sensor.new_text_sensor(version_config)
 | 
			
		||||
 
 | 
			
		||||
@@ -11,9 +11,11 @@ from esphome.const import (
 | 
			
		||||
    CONF_CURRENT_TEMPERATURE_STATE_TOPIC,
 | 
			
		||||
    CONF_CUSTOM_FAN_MODE,
 | 
			
		||||
    CONF_CUSTOM_PRESET,
 | 
			
		||||
    CONF_ENTITY_CATEGORY,
 | 
			
		||||
    CONF_FAN_MODE,
 | 
			
		||||
    CONF_FAN_MODE_COMMAND_TOPIC,
 | 
			
		||||
    CONF_FAN_MODE_STATE_TOPIC,
 | 
			
		||||
    CONF_ICON,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_MAX_TEMPERATURE,
 | 
			
		||||
    CONF_MIN_TEMPERATURE,
 | 
			
		||||
@@ -46,6 +48,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_WEB_SERVER,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
from esphome.cpp_generator import MockObjClass
 | 
			
		||||
from esphome.cpp_helpers import setup_entity
 | 
			
		||||
 | 
			
		||||
IS_PLATFORM_COMPONENT = True
 | 
			
		||||
@@ -151,12 +154,11 @@ ControlTrigger = climate_ns.class_(
 | 
			
		||||
    "ControlTrigger", automation.Trigger.template(ClimateCall.operator("ref"))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CLIMATE_SCHEMA = (
 | 
			
		||||
_CLIMATE_SCHEMA = (
 | 
			
		||||
    cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
 | 
			
		||||
    .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(Climate),
 | 
			
		||||
            cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent),
 | 
			
		||||
            cv.Optional(CONF_VISUAL, default={}): cv.Schema(
 | 
			
		||||
                {
 | 
			
		||||
@@ -245,6 +247,31 @@ CLIMATE_SCHEMA = (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def climate_schema(
 | 
			
		||||
    class_: MockObjClass,
 | 
			
		||||
    *,
 | 
			
		||||
    entity_category: str = cv.UNDEFINED,
 | 
			
		||||
    icon: str = cv.UNDEFINED,
 | 
			
		||||
) -> cv.Schema:
 | 
			
		||||
    schema = {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(class_),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for key, default, validator in [
 | 
			
		||||
        (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
 | 
			
		||||
        (CONF_ICON, icon, cv.icon),
 | 
			
		||||
    ]:
 | 
			
		||||
        if default is not cv.UNDEFINED:
 | 
			
		||||
            schema[cv.Optional(key, default=default)] = validator
 | 
			
		||||
 | 
			
		||||
    return _CLIMATE_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
CLIMATE_SCHEMA = climate_schema(Climate)
 | 
			
		||||
CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_climate_core_(var, config):
 | 
			
		||||
    await setup_entity(var, config)
 | 
			
		||||
 | 
			
		||||
@@ -419,6 +446,12 @@ async def register_climate(var, config):
 | 
			
		||||
    await setup_climate_core_(var, config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def new_climate(config, *args):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID], *args)
 | 
			
		||||
    await register_climate(var, config)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_ID): cv.use_id(Climate),
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ enum ClimateMode : uint8_t {
 | 
			
		||||
  CLIMATE_MODE_FAN_ONLY = 4,
 | 
			
		||||
  /// The climate device is set to dry/humidity mode
 | 
			
		||||
  CLIMATE_MODE_DRY = 5,
 | 
			
		||||
  /** The climate device is adjusting the temperatre dynamically.
 | 
			
		||||
  /** The climate device is adjusting the temperature dynamically.
 | 
			
		||||
   * For example, the target temperature can be adjusted based on a schedule, or learned behavior.
 | 
			
		||||
   * The target temperature can't be adjusted when in this mode.
 | 
			
		||||
   */
 | 
			
		||||
 
 | 
			
		||||
@@ -40,24 +40,24 @@ namespace climate {
 | 
			
		||||
 */
 | 
			
		||||
class ClimateTraits {
 | 
			
		||||
 public:
 | 
			
		||||
  bool get_supports_current_temperature() const { return supports_current_temperature_; }
 | 
			
		||||
  bool get_supports_current_temperature() const { return this->supports_current_temperature_; }
 | 
			
		||||
  void set_supports_current_temperature(bool supports_current_temperature) {
 | 
			
		||||
    supports_current_temperature_ = supports_current_temperature;
 | 
			
		||||
    this->supports_current_temperature_ = supports_current_temperature;
 | 
			
		||||
  }
 | 
			
		||||
  bool get_supports_current_humidity() const { return supports_current_humidity_; }
 | 
			
		||||
  bool get_supports_current_humidity() const { return this->supports_current_humidity_; }
 | 
			
		||||
  void set_supports_current_humidity(bool supports_current_humidity) {
 | 
			
		||||
    supports_current_humidity_ = supports_current_humidity;
 | 
			
		||||
    this->supports_current_humidity_ = supports_current_humidity;
 | 
			
		||||
  }
 | 
			
		||||
  bool get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; }
 | 
			
		||||
  bool get_supports_two_point_target_temperature() const { return this->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;
 | 
			
		||||
    this->supports_two_point_target_temperature_ = supports_two_point_target_temperature;
 | 
			
		||||
  }
 | 
			
		||||
  bool get_supports_target_humidity() const { return supports_target_humidity_; }
 | 
			
		||||
  bool get_supports_target_humidity() const { return this->supports_target_humidity_; }
 | 
			
		||||
  void set_supports_target_humidity(bool supports_target_humidity) {
 | 
			
		||||
    supports_target_humidity_ = supports_target_humidity;
 | 
			
		||||
    this->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); }
 | 
			
		||||
  void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
 | 
			
		||||
  void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
@@ -72,15 +72,15 @@ class ClimateTraits {
 | 
			
		||||
  }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); }
 | 
			
		||||
  bool supports_mode(ClimateMode mode) const { return supported_modes_.count(mode); }
 | 
			
		||||
  const std::set<ClimateMode> &get_supported_modes() const { return supported_modes_; }
 | 
			
		||||
  bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
 | 
			
		||||
  const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
 | 
			
		||||
 | 
			
		||||
  void set_supports_action(bool supports_action) { supports_action_ = supports_action; }
 | 
			
		||||
  bool get_supports_action() const { return supports_action_; }
 | 
			
		||||
  void set_supports_action(bool supports_action) { this->supports_action_ = supports_action; }
 | 
			
		||||
  bool get_supports_action() const { return this->supports_action_; }
 | 
			
		||||
 | 
			
		||||
  void set_supported_fan_modes(std::set<ClimateFanMode> modes) { supported_fan_modes_ = std::move(modes); }
 | 
			
		||||
  void add_supported_fan_mode(ClimateFanMode mode) { supported_fan_modes_.insert(mode); }
 | 
			
		||||
  void add_supported_custom_fan_mode(const std::string &mode) { supported_custom_fan_modes_.insert(mode); }
 | 
			
		||||
  void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
 | 
			
		||||
  void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
 | 
			
		||||
  void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
@@ -99,35 +99,37 @@ class ClimateTraits {
 | 
			
		||||
  void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); }
 | 
			
		||||
  bool supports_fan_mode(ClimateFanMode fan_mode) const { return supported_fan_modes_.count(fan_mode); }
 | 
			
		||||
  bool get_supports_fan_modes() const { return !supported_fan_modes_.empty() || !supported_custom_fan_modes_.empty(); }
 | 
			
		||||
  const std::set<ClimateFanMode> &get_supported_fan_modes() const { return supported_fan_modes_; }
 | 
			
		||||
  bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
 | 
			
		||||
  bool get_supports_fan_modes() const {
 | 
			
		||||
    return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
 | 
			
		||||
  }
 | 
			
		||||
  const std::set<ClimateFanMode> &get_supported_fan_modes() const { return this->supported_fan_modes_; }
 | 
			
		||||
 | 
			
		||||
  void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) {
 | 
			
		||||
    supported_custom_fan_modes_ = std::move(supported_custom_fan_modes);
 | 
			
		||||
    this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes);
 | 
			
		||||
  }
 | 
			
		||||
  const std::set<std::string> &get_supported_custom_fan_modes() const { return supported_custom_fan_modes_; }
 | 
			
		||||
  const std::set<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
 | 
			
		||||
  bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
 | 
			
		||||
    return supported_custom_fan_modes_.count(custom_fan_mode);
 | 
			
		||||
    return this->supported_custom_fan_modes_.count(custom_fan_mode);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_supported_presets(std::set<ClimatePreset> presets) { supported_presets_ = std::move(presets); }
 | 
			
		||||
  void add_supported_preset(ClimatePreset preset) { supported_presets_.insert(preset); }
 | 
			
		||||
  void add_supported_custom_preset(const std::string &preset) { supported_custom_presets_.insert(preset); }
 | 
			
		||||
  bool supports_preset(ClimatePreset preset) const { return supported_presets_.count(preset); }
 | 
			
		||||
  bool get_supports_presets() const { return !supported_presets_.empty(); }
 | 
			
		||||
  const std::set<climate::ClimatePreset> &get_supported_presets() const { return supported_presets_; }
 | 
			
		||||
  void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); }
 | 
			
		||||
  void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); }
 | 
			
		||||
  void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.insert(preset); }
 | 
			
		||||
  bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
 | 
			
		||||
  bool get_supports_presets() const { return !this->supported_presets_.empty(); }
 | 
			
		||||
  const std::set<climate::ClimatePreset> &get_supported_presets() const { return this->supported_presets_; }
 | 
			
		||||
 | 
			
		||||
  void set_supported_custom_presets(std::set<std::string> supported_custom_presets) {
 | 
			
		||||
    supported_custom_presets_ = std::move(supported_custom_presets);
 | 
			
		||||
    this->supported_custom_presets_ = std::move(supported_custom_presets);
 | 
			
		||||
  }
 | 
			
		||||
  const std::set<std::string> &get_supported_custom_presets() const { return supported_custom_presets_; }
 | 
			
		||||
  const std::set<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
 | 
			
		||||
  bool supports_custom_preset(const std::string &custom_preset) const {
 | 
			
		||||
    return supported_custom_presets_.count(custom_preset);
 | 
			
		||||
    return this->supported_custom_presets_.count(custom_preset);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { supported_swing_modes_ = std::move(modes); }
 | 
			
		||||
  void add_supported_swing_mode(ClimateSwingMode mode) { supported_swing_modes_.insert(mode); }
 | 
			
		||||
  void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
 | 
			
		||||
  void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
 | 
			
		||||
@@ -138,54 +140,58 @@ class ClimateTraits {
 | 
			
		||||
  void set_supports_swing_mode_horizontal(bool supported) {
 | 
			
		||||
    set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported);
 | 
			
		||||
  }
 | 
			
		||||
  bool supports_swing_mode(ClimateSwingMode swing_mode) const { return supported_swing_modes_.count(swing_mode); }
 | 
			
		||||
  bool get_supports_swing_modes() const { return !supported_swing_modes_.empty(); }
 | 
			
		||||
  const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return supported_swing_modes_; }
 | 
			
		||||
  bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
 | 
			
		||||
  bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
 | 
			
		||||
  const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; }
 | 
			
		||||
 | 
			
		||||
  float get_visual_min_temperature() const { return visual_min_temperature_; }
 | 
			
		||||
  void set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; }
 | 
			
		||||
  float get_visual_max_temperature() const { return visual_max_temperature_; }
 | 
			
		||||
  void set_visual_max_temperature(float visual_max_temperature) { visual_max_temperature_ = visual_max_temperature; }
 | 
			
		||||
  float get_visual_target_temperature_step() const { return visual_target_temperature_step_; }
 | 
			
		||||
  float get_visual_current_temperature_step() const { return visual_current_temperature_step_; }
 | 
			
		||||
  float get_visual_min_temperature() const { return this->visual_min_temperature_; }
 | 
			
		||||
  void set_visual_min_temperature(float visual_min_temperature) {
 | 
			
		||||
    this->visual_min_temperature_ = visual_min_temperature;
 | 
			
		||||
  }
 | 
			
		||||
  float get_visual_max_temperature() const { return this->visual_max_temperature_; }
 | 
			
		||||
  void set_visual_max_temperature(float visual_max_temperature) {
 | 
			
		||||
    this->visual_max_temperature_ = visual_max_temperature;
 | 
			
		||||
  }
 | 
			
		||||
  float get_visual_target_temperature_step() const { return this->visual_target_temperature_step_; }
 | 
			
		||||
  float get_visual_current_temperature_step() const { return this->visual_current_temperature_step_; }
 | 
			
		||||
  void set_visual_target_temperature_step(float temperature_step) {
 | 
			
		||||
    visual_target_temperature_step_ = temperature_step;
 | 
			
		||||
    this->visual_target_temperature_step_ = temperature_step;
 | 
			
		||||
  }
 | 
			
		||||
  void set_visual_current_temperature_step(float temperature_step) {
 | 
			
		||||
    visual_current_temperature_step_ = temperature_step;
 | 
			
		||||
    this->visual_current_temperature_step_ = temperature_step;
 | 
			
		||||
  }
 | 
			
		||||
  void set_visual_temperature_step(float temperature_step) {
 | 
			
		||||
    visual_target_temperature_step_ = temperature_step;
 | 
			
		||||
    visual_current_temperature_step_ = temperature_step;
 | 
			
		||||
    this->visual_target_temperature_step_ = temperature_step;
 | 
			
		||||
    this->visual_current_temperature_step_ = temperature_step;
 | 
			
		||||
  }
 | 
			
		||||
  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; }
 | 
			
		||||
  float get_visual_min_humidity() const { return this->visual_min_humidity_; }
 | 
			
		||||
  void set_visual_min_humidity(float visual_min_humidity) { this->visual_min_humidity_ = visual_min_humidity; }
 | 
			
		||||
  float get_visual_max_humidity() const { return this->visual_max_humidity_; }
 | 
			
		||||
  void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void set_mode_support_(climate::ClimateMode mode, bool supported) {
 | 
			
		||||
    if (supported) {
 | 
			
		||||
      supported_modes_.insert(mode);
 | 
			
		||||
      this->supported_modes_.insert(mode);
 | 
			
		||||
    } else {
 | 
			
		||||
      supported_modes_.erase(mode);
 | 
			
		||||
      this->supported_modes_.erase(mode);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  void set_fan_mode_support_(climate::ClimateFanMode mode, bool supported) {
 | 
			
		||||
    if (supported) {
 | 
			
		||||
      supported_fan_modes_.insert(mode);
 | 
			
		||||
      this->supported_fan_modes_.insert(mode);
 | 
			
		||||
    } else {
 | 
			
		||||
      supported_fan_modes_.erase(mode);
 | 
			
		||||
      this->supported_fan_modes_.erase(mode);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  void set_swing_mode_support_(climate::ClimateSwingMode mode, bool supported) {
 | 
			
		||||
    if (supported) {
 | 
			
		||||
      supported_swing_modes_.insert(mode);
 | 
			
		||||
      this->supported_swing_modes_.insert(mode);
 | 
			
		||||
    } else {
 | 
			
		||||
      supported_swing_modes_.erase(mode);
 | 
			
		||||
      this->supported_swing_modes_.erase(mode);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,13 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from esphome import core
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import climate, remote_base, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
 | 
			
		||||
from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
 | 
			
		||||
from esphome.cpp_generator import MockObjClass
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["remote_transmitter"]
 | 
			
		||||
AUTO_LOAD = ["sensor", "remote_base"]
 | 
			
		||||
@@ -16,30 +22,58 @@ ClimateIR = climate_ir_ns.class_(
 | 
			
		||||
    remote_base.RemoteTransmittable,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CLIMATE_IR_SCHEMA = (
 | 
			
		||||
    climate.CLIMATE_SCHEMA.extend(
 | 
			
		||||
 | 
			
		||||
def climate_ir_schema(
 | 
			
		||||
    class_: MockObjClass,
 | 
			
		||||
) -> cv.Schema:
 | 
			
		||||
    return (
 | 
			
		||||
        climate.climate_schema(class_)
 | 
			
		||||
        .extend(
 | 
			
		||||
            {
 | 
			
		||||
                cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
 | 
			
		||||
                cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
 | 
			
		||||
                cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
        .extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def climate_ir_with_receiver_schema(
 | 
			
		||||
    class_: MockObjClass,
 | 
			
		||||
) -> cv.Schema:
 | 
			
		||||
    return climate_ir_schema(class_).extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
 | 
			
		||||
            cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id(
 | 
			
		||||
                remote_base.RemoteReceiverBase
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
    .extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id(
 | 
			
		||||
            remote_base.RemoteReceiverBase
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
def deprecated_schema_constant(config):
 | 
			
		||||
    type: str = "unknown"
 | 
			
		||||
    if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID):
 | 
			
		||||
        type = str(id.type).split("::", maxsplit=1)[0]
 | 
			
		||||
    _LOGGER.warning(
 | 
			
		||||
        "Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
 | 
			
		||||
        "Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. "
 | 
			
		||||
        "If you are seeing this, report an issue to the external_component author and ask them to update it. "
 | 
			
		||||
        "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
 | 
			
		||||
        "Component using this schema: %s",
 | 
			
		||||
        type,
 | 
			
		||||
    )
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR)
 | 
			
		||||
CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def register_climate_ir(var, config):
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await climate.register_climate(var, config)
 | 
			
		||||
    await remote_base.register_transmittable(var, config)
 | 
			
		||||
    cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
 | 
			
		||||
    cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
 | 
			
		||||
@@ -48,3 +82,9 @@ async def register_climate_ir(var, config):
 | 
			
		||||
    if sensor_id := config.get(CONF_SENSOR):
 | 
			
		||||
        sens = await cg.get_variable(sensor_id)
 | 
			
		||||
        cg.add(var.set_sensor(sens))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def new_climate_ir(config, *args):
 | 
			
		||||
    var = await climate.new_climate(config, *args)
 | 
			
		||||
    await register_climate_ir(var, config)
 | 
			
		||||
    return var
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import climate_ir
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["climate_ir"]
 | 
			
		||||
 | 
			
		||||
@@ -14,9 +13,8 @@ CONF_BIT_HIGH = "bit_high"
 | 
			
		||||
CONF_BIT_ONE_LOW = "bit_one_low"
 | 
			
		||||
CONF_BIT_ZERO_LOW = "bit_zero_low"
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
 | 
			
		||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(LgIrClimate).extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(LgIrClimate),
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            CONF_HEADER_HIGH, default="8000us"
 | 
			
		||||
        ): cv.positive_time_period_microseconds,
 | 
			
		||||
@@ -37,8 +35,7 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await climate_ir.register_climate_ir(var, config)
 | 
			
		||||
    var = await climate_ir.new_climate_ir(config)
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_header_high(config[CONF_HEADER_HIGH]))
 | 
			
		||||
    cg.add(var.set_header_low(config[CONF_HEADER_LOW]))
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ const uint32_t FAN_MAX = 0x40;
 | 
			
		||||
 | 
			
		||||
// Temperature
 | 
			
		||||
const uint8_t TEMP_RANGE = TEMP_MAX - TEMP_MIN + 1;
 | 
			
		||||
const uint32_t TEMP_MASK = 0XF00;
 | 
			
		||||
const uint32_t TEMP_MASK = 0xF00;
 | 
			
		||||
const uint32_t TEMP_SHIFT = 8;
 | 
			
		||||
 | 
			
		||||
const uint16_t BITS = 28;
 | 
			
		||||
@@ -43,11 +43,11 @@ void LgIrClimate::transmit_state() {
 | 
			
		||||
  // ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_);
 | 
			
		||||
 | 
			
		||||
  // Set command
 | 
			
		||||
  if (send_swing_cmd_) {
 | 
			
		||||
    send_swing_cmd_ = false;
 | 
			
		||||
  if (this->send_swing_cmd_) {
 | 
			
		||||
    this->send_swing_cmd_ = false;
 | 
			
		||||
    remote_state |= COMMAND_SWING;
 | 
			
		||||
  } else {
 | 
			
		||||
    bool climate_is_off = (mode_before_ == climate::CLIMATE_MODE_OFF);
 | 
			
		||||
    bool climate_is_off = (this->mode_before_ == climate::CLIMATE_MODE_OFF);
 | 
			
		||||
    switch (this->mode) {
 | 
			
		||||
      case climate::CLIMATE_MODE_COOL:
 | 
			
		||||
        remote_state |= climate_is_off ? COMMAND_ON_COOL : COMMAND_COOL;
 | 
			
		||||
@@ -71,7 +71,7 @@ void LgIrClimate::transmit_state() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  mode_before_ = this->mode;
 | 
			
		||||
  this->mode_before_ = this->mode;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode);
 | 
			
		||||
 | 
			
		||||
@@ -102,7 +102,7 @@ void LgIrClimate::transmit_state() {
 | 
			
		||||
    remote_state |= ((temp - 15) << TEMP_SHIFT);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  transmit_(remote_state);
 | 
			
		||||
  this->transmit_(remote_state);
 | 
			
		||||
  this->publish_state();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -187,7 +187,7 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LgIrClimate::transmit_(uint32_t value) {
 | 
			
		||||
  calc_checksum_(value);
 | 
			
		||||
  this->calc_checksum_(value);
 | 
			
		||||
  ESP_LOGD(TAG, "Sending climate_lg_ir code: 0x%02" PRIX32, value);
 | 
			
		||||
 | 
			
		||||
  auto transmit = this->transmitter_->transmit();
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ class LgIrClimate : public climate_ir::ClimateIR {
 | 
			
		||||
 | 
			
		||||
  /// Override control to change settings of the climate device.
 | 
			
		||||
  void control(const climate::ClimateCall &call) override {
 | 
			
		||||
    send_swing_cmd_ = call.get_swing_mode().has_value();
 | 
			
		||||
    this->send_swing_cmd_ = call.get_swing_mode().has_value();
 | 
			
		||||
    // swing resets after unit powered off
 | 
			
		||||
    if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF)
 | 
			
		||||
      this->swing_mode = climate::CLIMATE_SWING_OFF;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@ from esphome.const import CONF_BLUE, CONF_GREEN, CONF_ID, CONF_RED, CONF_WHITE
 | 
			
		||||
 | 
			
		||||
ColorStruct = cg.esphome_ns.struct("Color")
 | 
			
		||||
 | 
			
		||||
INSTANCE_TYPE = ColorStruct
 | 
			
		||||
 | 
			
		||||
MULTI_CONF = True
 | 
			
		||||
 | 
			
		||||
CONF_RED_INT = "red_int"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								esphome/components/const/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								esphome/components/const/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
"""Constants used by esphome components."""
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
 | 
			
		||||
CONF_DRAW_ROUNDING = "draw_rounding"
 | 
			
		||||
@@ -1,7 +1,5 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import climate_ir
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["climate_ir"]
 | 
			
		||||
CODEOWNERS = ["@glmnet"]
 | 
			
		||||
@@ -9,13 +7,8 @@ CODEOWNERS = ["@glmnet"]
 | 
			
		||||
coolix_ns = cg.esphome_ns.namespace("coolix")
 | 
			
		||||
CoolixClimate = coolix_ns.class_("CoolixClimate", climate_ir.ClimateIR)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(CoolixClimate),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(CoolixClimate)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await climate_ir.register_climate_ir(var, config)
 | 
			
		||||
    await climate_ir.new_climate_ir(config)
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_DEVICE_CLASS,
 | 
			
		||||
    CONF_ENTITY_CATEGORY,
 | 
			
		||||
    CONF_ICON,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_SOURCE_ID,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core.entity_helpers import inherit_property_from
 | 
			
		||||
@@ -15,12 +14,15 @@ from .. import copy_ns
 | 
			
		||||
CopyCover = copy_ns.class_("CopyCover", cover.Cover, cg.Component)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(CopyCover),
 | 
			
		||||
        cv.Required(CONF_SOURCE_ID): cv.use_id(cover.Cover),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cover.cover_schema(CopyCover)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_SOURCE_ID): cv.use_id(cover.Cover),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.All(
 | 
			
		||||
    inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
 | 
			
		||||
@@ -30,8 +32,7 @@ FINAL_VALIDATE_SCHEMA = cv.All(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cover.register_cover(var, config)
 | 
			
		||||
    var = await cover.new_cover(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    source = await cg.get_variable(config[CONF_SOURCE_ID])
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import fan
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_SOURCE_ID
 | 
			
		||||
from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID
 | 
			
		||||
from esphome.core.entity_helpers import inherit_property_from
 | 
			
		||||
 | 
			
		||||
from .. import copy_ns
 | 
			
		||||
@@ -9,12 +9,15 @@ from .. import copy_ns
 | 
			
		||||
CopyFan = copy_ns.class_("CopyFan", fan.Fan, cg.Component)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(CopyFan),
 | 
			
		||||
        cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    fan.fan_schema(CopyFan)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.All(
 | 
			
		||||
    inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
 | 
			
		||||
@@ -23,8 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await fan.register_fan(var, config)
 | 
			
		||||
    var = await fan.new_fan(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    source = await cg.get_variable(config[CONF_SOURCE_ID])
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import lock
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_SOURCE_ID
 | 
			
		||||
from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID
 | 
			
		||||
from esphome.core.entity_helpers import inherit_property_from
 | 
			
		||||
 | 
			
		||||
from .. import copy_ns
 | 
			
		||||
@@ -9,12 +9,15 @@ from .. import copy_ns
 | 
			
		||||
CopyLock = copy_ns.class_("CopyLock", lock.Lock, cg.Component)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(CopyLock),
 | 
			
		||||
        cv.Required(CONF_SOURCE_ID): cv.use_id(lock.Lock),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    lock.lock_schema(CopyLock)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_SOURCE_ID): cv.use_id(lock.Lock),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.All(
 | 
			
		||||
    inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
 | 
			
		||||
@@ -23,8 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await lock.register_lock(var, config)
 | 
			
		||||
    var = await lock.new_lock(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    source = await cg.get_variable(config[CONF_SOURCE_ID])
 | 
			
		||||
 
 | 
			
		||||
@@ -9,12 +9,15 @@ from .. import copy_ns
 | 
			
		||||
CopyText = copy_ns.class_("CopyText", text.Text, cg.Component)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(CopyText),
 | 
			
		||||
        cv.Required(CONF_SOURCE_ID): cv.use_id(text.Text),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    text.text_schema(CopyText)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_SOURCE_ID): cv.use_id(text.Text),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.All(
 | 
			
		||||
    inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@ from esphome.components import mqtt, web_server
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_DEVICE_CLASS,
 | 
			
		||||
    CONF_ENTITY_CATEGORY,
 | 
			
		||||
    CONF_ICON,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_MQTT_ID,
 | 
			
		||||
    CONF_ON_OPEN,
 | 
			
		||||
@@ -31,6 +33,7 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_WINDOW,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
from esphome.cpp_generator import MockObjClass
 | 
			
		||||
from esphome.cpp_helpers import setup_entity
 | 
			
		||||
 | 
			
		||||
IS_PLATFORM_COMPONENT = True
 | 
			
		||||
@@ -89,12 +92,11 @@ CoverClosedTrigger = cover_ns.class_(
 | 
			
		||||
 | 
			
		||||
CONF_ON_CLOSED = "on_closed"
 | 
			
		||||
 | 
			
		||||
COVER_SCHEMA = (
 | 
			
		||||
_COVER_SCHEMA = (
 | 
			
		||||
    cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
 | 
			
		||||
    .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(Cover),
 | 
			
		||||
            cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
 | 
			
		||||
            cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
 | 
			
		||||
            cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
 | 
			
		||||
@@ -124,6 +126,33 @@ COVER_SCHEMA = (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def cover_schema(
 | 
			
		||||
    class_: MockObjClass,
 | 
			
		||||
    *,
 | 
			
		||||
    device_class: str = cv.UNDEFINED,
 | 
			
		||||
    entity_category: str = cv.UNDEFINED,
 | 
			
		||||
    icon: str = cv.UNDEFINED,
 | 
			
		||||
) -> cv.Schema:
 | 
			
		||||
    schema = {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(class_),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for key, default, validator in [
 | 
			
		||||
        (CONF_DEVICE_CLASS, device_class, cv.one_of(*DEVICE_CLASSES, lower=True)),
 | 
			
		||||
        (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
 | 
			
		||||
        (CONF_ICON, icon, cv.icon),
 | 
			
		||||
    ]:
 | 
			
		||||
        if default is not cv.UNDEFINED:
 | 
			
		||||
            schema[cv.Optional(key, default=default)] = validator
 | 
			
		||||
 | 
			
		||||
    return _COVER_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
COVER_SCHEMA = cover_schema(Cover)
 | 
			
		||||
COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_cover_core_(var, config):
 | 
			
		||||
    await setup_entity(var, config)
 | 
			
		||||
 | 
			
		||||
@@ -163,6 +192,12 @@ async def register_cover(var, config):
 | 
			
		||||
    await setup_cover_core_(var, config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def new_cover(config, *args):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID], *args)
 | 
			
		||||
    await register_cover(var, config)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
COVER_ACTION_SCHEMA = maybe_simple_id(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_ID): cv.use_id(Cover),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#include "cse7766.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace cse7766 {
 | 
			
		||||
@@ -7,7 +8,7 @@ namespace cse7766 {
 | 
			
		||||
static const char *const TAG = "cse7766";
 | 
			
		||||
 | 
			
		||||
void CSE7766Component::loop() {
 | 
			
		||||
  const uint32_t now = millis();
 | 
			
		||||
  const uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
  if (now - this->last_transmission_ >= 500) {
 | 
			
		||||
    // last transmission too long ago. Reset RX index.
 | 
			
		||||
    this->raw_data_index_ = 0;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								esphome/components/cst226/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								esphome/components/cst226/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import binary_sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
 | 
			
		||||
from .. import cst226_ns
 | 
			
		||||
from ..touchscreen import CST226ButtonListener, CST226Touchscreen
 | 
			
		||||
 | 
			
		||||
CONF_CST226_ID = "cst226_id"
 | 
			
		||||
 | 
			
		||||
CST226Button = cst226_ns.class_(
 | 
			
		||||
    "CST226Button",
 | 
			
		||||
    binary_sensor.BinarySensor,
 | 
			
		||||
    cg.Component,
 | 
			
		||||
    CST226ButtonListener,
 | 
			
		||||
    cg.Parented.template(CST226Touchscreen),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CST226Button).extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(CONF_CST226_ID): cv.use_id(CST226Touchscreen),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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_CST226_ID])
 | 
			
		||||
							
								
								
									
										22
									
								
								esphome/components/cst226/binary_sensor/cs226_button.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								esphome/components/cst226/binary_sensor/cs226_button.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/binary_sensor/binary_sensor.h"
 | 
			
		||||
#include "../touchscreen/cst226_touchscreen.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace cst226 {
 | 
			
		||||
 | 
			
		||||
class CST226Button : public binary_sensor::BinarySensor,
 | 
			
		||||
                     public Component,
 | 
			
		||||
                     public CST226ButtonListener,
 | 
			
		||||
                     public Parented<CST226Touchscreen> {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
  void update_button(bool state) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace cst226
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										19
									
								
								esphome/components/cst226/binary_sensor/cstt6_button.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								esphome/components/cst226/binary_sensor/cstt6_button.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
#include "cs226_button.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace cst226 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "CST226.binary_sensor";
 | 
			
		||||
 | 
			
		||||
void CST226Button::setup() {
 | 
			
		||||
  this->parent_->register_button_listener(this);
 | 
			
		||||
  this->publish_initial_state(false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CST226Button::dump_config() { LOG_BINARY_SENSOR("", "CST226 Button", this); }
 | 
			
		||||
 | 
			
		||||
void CST226Button::update_button(bool state) { this->publish_state(state); }
 | 
			
		||||
 | 
			
		||||
}  // namespace cst226
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -3,8 +3,10 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace cst226 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "cst226.touchscreen";
 | 
			
		||||
 | 
			
		||||
void CST226Touchscreen::setup() {
 | 
			
		||||
  esph_log_config(TAG, "Setting up CST226 Touchscreen...");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up CST226 Touchscreen...");
 | 
			
		||||
  if (this->reset_pin_ != nullptr) {
 | 
			
		||||
    this->reset_pin_->setup();
 | 
			
		||||
    this->reset_pin_->digital_write(true);
 | 
			
		||||
@@ -26,6 +28,11 @@ void CST226Touchscreen::update_touches() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
  if (data[0] == 0x83 && data[1] == 0x17 && data[5] == 0x80) {
 | 
			
		||||
    this->update_button_state_(true);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->update_button_state_(false);
 | 
			
		||||
  if (data[6] != 0xAB || data[0] == 0xAB || data[5] == 0x80) {
 | 
			
		||||
    this->skip_update_ = true;
 | 
			
		||||
    return;
 | 
			
		||||
@@ -43,13 +50,21 @@ void CST226Touchscreen::update_touches() {
 | 
			
		||||
    int16_t y = (data[index + 2] << 4) | (data[index + 3] & 0x0F);
 | 
			
		||||
    int16_t z = data[index + 4];
 | 
			
		||||
    this->add_raw_touch_position_(id, x, y, z);
 | 
			
		||||
    esph_log_v(TAG, "Read touch %d: %d/%d", id, x, y);
 | 
			
		||||
    ESP_LOGV(TAG, "Read touch %d: %d/%d", id, x, y);
 | 
			
		||||
    index += 5;
 | 
			
		||||
    if (i == 0)
 | 
			
		||||
      index += 2;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool CST226Touchscreen::read16_(uint16_t addr, uint8_t *data, size_t len) {
 | 
			
		||||
  if (this->read_register16(addr, data, len) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Read data from 0x%04X failed", addr);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
void CST226Touchscreen::continue_setup_() {
 | 
			
		||||
  uint8_t buffer[8];
 | 
			
		||||
  if (this->interrupt_pin_ != nullptr) {
 | 
			
		||||
@@ -58,7 +73,7 @@ void CST226Touchscreen::continue_setup_() {
 | 
			
		||||
  }
 | 
			
		||||
  buffer[0] = 0xD1;
 | 
			
		||||
  if (this->write_register16(0xD1, buffer, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    esph_log_e(TAG, "Write byte to 0xD1 failed");
 | 
			
		||||
    ESP_LOGE(TAG, "Write byte to 0xD1 failed");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
@@ -66,7 +81,7 @@ void CST226Touchscreen::continue_setup_() {
 | 
			
		||||
  if (this->read16_(0xD204, buffer, 4)) {
 | 
			
		||||
    uint16_t chip_id = buffer[2] + (buffer[3] << 8);
 | 
			
		||||
    uint16_t project_id = buffer[0] + (buffer[1] << 8);
 | 
			
		||||
    esph_log_config(TAG, "Chip ID %X, project ID %x", chip_id, project_id);
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "Chip ID %X, project ID %x", chip_id, project_id);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) {
 | 
			
		||||
    if (this->read16_(0xD1F8, buffer, 4)) {
 | 
			
		||||
@@ -80,7 +95,14 @@ void CST226Touchscreen::continue_setup_() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  this->setup_complete_ = true;
 | 
			
		||||
  esph_log_config(TAG, "CST226 Touchscreen setup complete");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "CST226 Touchscreen setup complete");
 | 
			
		||||
}
 | 
			
		||||
void CST226Touchscreen::update_button_state_(bool state) {
 | 
			
		||||
  if (this->button_touched_ == state)
 | 
			
		||||
    return;
 | 
			
		||||
  this->button_touched_ = state;
 | 
			
		||||
  for (auto *listener : this->button_listeners_)
 | 
			
		||||
    listener->update_button(state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CST226Touchscreen::dump_config() {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,10 +9,13 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace cst226 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "cst226.touchscreen";
 | 
			
		||||
 | 
			
		||||
static const uint8_t CST226_REG_STATUS = 0x00;
 | 
			
		||||
 | 
			
		||||
class CST226ButtonListener {
 | 
			
		||||
 public:
 | 
			
		||||
  virtual void update_button(bool state) = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
@@ -22,22 +25,19 @@ class CST226Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
 | 
			
		||||
  void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
 | 
			
		||||
  void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
 | 
			
		||||
  bool can_proceed() override { return this->setup_complete_ || this->is_failed(); }
 | 
			
		||||
  void register_button_listener(CST226ButtonListener *listener) { this->button_listeners_.push_back(listener); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool read16_(uint16_t addr, uint8_t *data, size_t len) {
 | 
			
		||||
    if (this->read_register16(addr, data, len) != i2c::ERROR_OK) {
 | 
			
		||||
      esph_log_e(TAG, "Read data from 0x%04X failed", addr);
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  bool read16_(uint16_t addr, uint8_t *data, size_t len);
 | 
			
		||||
  void continue_setup_();
 | 
			
		||||
  void update_button_state_(bool state);
 | 
			
		||||
 | 
			
		||||
  InternalGPIOPin *interrupt_pin_{};
 | 
			
		||||
  GPIOPin *reset_pin_{};
 | 
			
		||||
  uint8_t chip_id_{};
 | 
			
		||||
  bool setup_complete_{};
 | 
			
		||||
  std::vector<CST226ButtonListener *> button_listeners_;
 | 
			
		||||
  bool button_touched_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace cst226
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@ import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_CLOSE_ACTION,
 | 
			
		||||
    CONF_CLOSE_DURATION,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_MAX_DURATION,
 | 
			
		||||
    CONF_OPEN_ACTION,
 | 
			
		||||
    CONF_OPEN_DURATION,
 | 
			
		||||
@@ -30,45 +29,47 @@ CurrentBasedCover = current_based_ns.class_(
 | 
			
		||||
    "CurrentBasedCover", cover.Cover, cg.Component
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(CurrentBasedCover),
 | 
			
		||||
        cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
 | 
			
		||||
        cv.Required(CONF_OPEN_SENSOR): cv.use_id(sensor.Sensor),
 | 
			
		||||
        cv.Required(CONF_OPEN_MOVING_CURRENT_THRESHOLD): cv.float_range(
 | 
			
		||||
            min=0, min_included=False
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD): cv.float_range(
 | 
			
		||||
            min=0, min_included=False
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
 | 
			
		||||
        cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
 | 
			
		||||
        cv.Required(CONF_CLOSE_SENSOR): cv.use_id(sensor.Sensor),
 | 
			
		||||
        cv.Required(CONF_CLOSE_MOVING_CURRENT_THRESHOLD): cv.float_range(
 | 
			
		||||
            min=0, min_included=False
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD): cv.float_range(
 | 
			
		||||
            min=0, min_included=False
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
 | 
			
		||||
        cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
 | 
			
		||||
        cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage,
 | 
			
		||||
        cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds,
 | 
			
		||||
        cv.Optional(CONF_MALFUNCTION_DETECTION, default=True): cv.boolean,
 | 
			
		||||
        cv.Optional(CONF_MALFUNCTION_ACTION): automation.validate_automation(
 | 
			
		||||
            single=True
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            CONF_START_SENSING_DELAY, default="500ms"
 | 
			
		||||
        ): cv.positive_time_period_milliseconds,
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cover.cover_schema(CurrentBasedCover)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
 | 
			
		||||
            cv.Required(CONF_OPEN_SENSOR): cv.use_id(sensor.Sensor),
 | 
			
		||||
            cv.Required(CONF_OPEN_MOVING_CURRENT_THRESHOLD): cv.float_range(
 | 
			
		||||
                min=0, min_included=False
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD): cv.float_range(
 | 
			
		||||
                min=0, min_included=False
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
 | 
			
		||||
            cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
 | 
			
		||||
            cv.Required(CONF_CLOSE_SENSOR): cv.use_id(sensor.Sensor),
 | 
			
		||||
            cv.Required(CONF_CLOSE_MOVING_CURRENT_THRESHOLD): cv.float_range(
 | 
			
		||||
                min=0, min_included=False
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD): cv.float_range(
 | 
			
		||||
                min=0, min_included=False
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
 | 
			
		||||
            cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
 | 
			
		||||
            cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage,
 | 
			
		||||
            cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds,
 | 
			
		||||
            cv.Optional(CONF_MALFUNCTION_DETECTION, default=True): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_MALFUNCTION_ACTION): automation.validate_automation(
 | 
			
		||||
                single=True
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
                CONF_START_SENSING_DELAY, default="500ms"
 | 
			
		||||
            ): cv.positive_time_period_milliseconds,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    var = await cover.new_cover(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await cover.register_cover(var, config)
 | 
			
		||||
 | 
			
		||||
    await automation.build_automation(
 | 
			
		||||
        var.get_stop_trigger(), [], config[CONF_STOP_ACTION]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
#include "current_based_cover.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include <cfloat>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -60,7 +61,7 @@ void CurrentBasedCover::loop() {
 | 
			
		||||
  if (this->current_operation == COVER_OPERATION_IDLE)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  const uint32_t now = millis();
 | 
			
		||||
  const uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
 | 
			
		||||
  if (this->current_operation == COVER_OPERATION_OPENING) {
 | 
			
		||||
    if (this->malfunction_detection_ && this->is_closing_()) {  // Malfunction
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,13 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import climate_ir
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["climate_ir"]
 | 
			
		||||
 | 
			
		||||
daikin_ns = cg.esphome_ns.namespace("daikin")
 | 
			
		||||
DaikinClimate = daikin_ns.class_("DaikinClimate", climate_ir.ClimateIR)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(DaikinClimate),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinClimate)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await climate_ir.register_climate_ir(var, config)
 | 
			
		||||
    await climate_ir.new_climate_ir(config)
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user