mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-03 08:31:47 +00:00 
			
		
		
		
	Compare commits
	
		
			365 Commits
		
	
	
		
			2025.9.0b4
			...
			wifi_fixed
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					347501d895 | ||
| 
						 | 
					4c00861760 | ||
| 
						 | 
					2ff3e7fb2b | ||
| 
						 | 
					b0c20d7adb | ||
| 
						 | 
					2cc5e24b38 | ||
| 
						 | 
					3afa73b449 | ||
| 
						 | 
					dcf2697a2a | ||
| 
						 | 
					6a11700a6b | ||
| 
						 | 
					9bd9b043c8 | ||
| 
						 | 
					cb602c9b1a | ||
| 
						 | 
					b54beb357a | ||
| 
						 | 
					6abc2efd96 | ||
| 
						 | 
					be51093a7e | ||
| 
						 | 
					52219c4dcc | ||
| 
						 | 
					590cae13c0 | ||
| 
						 | 
					e15429b0f5 | ||
| 
						 | 
					b5cc668a45 | ||
| 
						 | 
					a1b0ae78e0 | ||
| 
						 | 
					fcc8a809e6 | ||
| 
						 | 
					48474c0f8c | ||
| 
						 | 
					9f9c95dd09 | ||
| 
						 | 
					a74fcbc8b6 | ||
| 
						 | 
					c8b898f9c5 | ||
| 
						 | 
					81bf2688b4 | ||
| 
						 | 
					87d2c9868f | ||
| 
						 | 
					5ca407e27c | ||
| 
						 | 
					5bbc2ab482 | ||
| 
						 | 
					309e8b4c92 | ||
| 
						 | 
					eee2987c99 | ||
| 
						 | 
					061e55f8c5 | ||
| 
						 | 
					9ad462d8c6 | ||
| 
						 | 
					56334b7832 | ||
| 
						 | 
					a4b7e0c700 | ||
| 
						 | 
					84ad7ee0e4 | ||
| 
						 | 
					d006008539 | ||
| 
						 | 
					f1af9d978c | ||
| 
						 | 
					6bb1e4c9c0 | ||
| 
						 | 
					785df05631 | ||
| 
						 | 
					82bdb08884 | ||
| 
						 | 
					b709ff84c3 | ||
| 
						 | 
					93266ad08f | ||
| 
						 | 
					2fac813f18 | ||
| 
						 | 
					a62c7a03dd | ||
| 
						 | 
					ec63247ae0 | ||
| 
						 | 
					0fe6e7169c | ||
| 
						 | 
					a0f4de1bfb | ||
| 
						 | 
					a541549d23 | ||
| 
						 | 
					b74715fe14 | ||
| 
						 | 
					5aff20a624 | ||
| 
						 | 
					7682b4e9a3 | ||
| 
						 | 
					6eabf709c6 | ||
| 
						 | 
					6209d4b493 | ||
| 
						 | 
					f10c361454 | ||
| 
						 | 
					27456c1370 | ||
| 
						 | 
					1aeefbe547 | ||
| 
						 | 
					3f3bce7ef4 | ||
| 
						 | 
					0acc58d5a1 | ||
| 
						 | 
					0b4ef0fea2 | ||
| 
						 | 
					a067bdb769 | ||
| 
						 | 
					301e7a7ac5 | ||
| 
						 | 
					ac566b7fd6 | ||
| 
						 | 
					fddb8b35f2 | ||
| 
						 | 
					27e1095cd7 | ||
| 
						 | 
					fa4541a4f3 | ||
| 
						 | 
					24dcc1843e | ||
| 
						 | 
					f670d775ac | ||
| 
						 | 
					59a31adac2 | ||
| 
						 | 
					a3c0acc7c9 | ||
| 
						 | 
					ad2c5b96a9 | ||
| 
						 | 
					9adc3bd943 | ||
| 
						 | 
					ad296a7d74 | ||
| 
						 | 
					fdd422c42a | ||
| 
						 | 
					3d82301c3d | ||
| 
						 | 
					2fa49be17d | ||
| 
						 | 
					75867842ea | ||
| 
						 | 
					cba85c0925 | ||
| 
						 | 
					42d1269aaf | ||
| 
						 | 
					f4df17673b | ||
| 
						 | 
					e340397b41 | ||
| 
						 | 
					abeadc7830 | ||
| 
						 | 
					8d4b347e5c | ||
| 
						 | 
					a7f556c25f | ||
| 
						 | 
					3f4250fcd7 | ||
| 
						 | 
					b532e04ae4 | ||
| 
						 | 
					697cab45dd | ||
| 
						 | 
					a88182c8e3 | ||
| 
						 | 
					8cfb6578d1 | ||
| 
						 | 
					eb16d322cd | ||
| 
						 | 
					22e06ba063 | ||
| 
						 | 
					7147479f90 | ||
| 
						 | 
					e55df1babc | ||
| 
						 | 
					4c8fc5f4e6 | ||
| 
						 | 
					646508006c | ||
| 
						 | 
					9384f0683b | ||
| 
						 | 
					5e7f5bf890 | ||
| 
						 | 
					2a8796437d | ||
| 
						 | 
					1635767aa2 | ||
| 
						 | 
					192856e8d1 | ||
| 
						 | 
					71be5a5f65 | ||
| 
						 | 
					f86b83cda5 | ||
| 
						 | 
					74c055745f | ||
| 
						 | 
					3edcdc7d80 | ||
| 
						 | 
					94fea68e3e | ||
| 
						 | 
					6880f9fc5c | ||
| 
						 | 
					26ebac8cb8 | ||
| 
						 | 
					5cf0046601 | ||
| 
						 | 
					c68017ddb4 | ||
| 
						 | 
					cfd241ff29 | ||
| 
						 | 
					f757a19e82 | ||
| 
						 | 
					e8854e0659 | ||
| 
						 | 
					a3622d878d | ||
| 
						 | 
					da2089c8be | ||
| 
						 | 
					118663f9e2 | ||
| 
						 | 
					4a99987bfe | ||
| 
						 | 
					d164c06f01 | ||
| 
						 | 
					972987acdf | ||
| 
						 | 
					eea2b6b81b | ||
| 
						 | 
					f62e06104e | ||
| 
						 | 
					f26e71bae6 | ||
| 
						 | 
					c6e4a7911c | ||
| 
						 | 
					e2c5eeef97 | ||
| 
						 | 
					7ea51b1865 | ||
| 
						 | 
					aa1afbd152 | ||
| 
						 | 
					20d9ae699c | ||
| 
						 | 
					c0fb0ae06f | ||
| 
						 | 
					9b6d62cd69 | ||
| 
						 | 
					5932a4bd0e | ||
| 
						 | 
					84c3cf5f17 | ||
| 
						 | 
					120a445abf | ||
| 
						 | 
					41c073a451 | ||
| 
						 | 
					0fd71ca211 | ||
| 
						 | 
					19439199cc | ||
| 
						 | 
					39d5cbc74a | ||
| 
						 | 
					722c5a94f2 | ||
| 
						 | 
					7b48fc292f | ||
| 
						 | 
					6c7d92e726 | ||
| 
						 | 
					b1859c50bd | ||
| 
						 | 
					3f9924eac2 | ||
| 
						 | 
					874db20b7d | ||
| 
						 | 
					2eea674c04 | ||
| 
						 | 
					0137954f2b | ||
| 
						 | 
					0a40a30e4a | ||
| 
						 | 
					d43b844e06 | ||
| 
						 | 
					2596b6096f | ||
| 
						 | 
					6f8e82aeb6 | ||
| 
						 | 
					ca0e738799 | ||
| 
						 | 
					14a23101f2 | ||
| 
						 | 
					2b389bb8f2 | ||
| 
						 | 
					89c3340ef6 | ||
| 
						 | 
					ba0532cda7 | ||
| 
						 | 
					5419b8bddb | ||
| 
						 | 
					624868bb05 | ||
| 
						 | 
					f2aa5a754c | ||
| 
						 | 
					638c6cc14e | ||
| 
						 | 
					8137d7600a | ||
| 
						 | 
					08afc3030a | ||
| 
						 | 
					1deb79a24b | ||
| 
						 | 
					de21c61b6a | ||
| 
						 | 
					db1aa82350 | ||
| 
						 | 
					fe4799b300 | ||
| 
						 | 
					93e18e850e | ||
| 
						 | 
					5cef75dbe1 | ||
| 
						 | 
					4194a940ae | ||
| 
						 | 
					59c0ffb98b | ||
| 
						 | 
					29658b79bc | ||
| 
						 | 
					158a59aa83 | ||
| 
						 | 
					c95180504a | ||
| 
						 | 
					848ba6b717 | ||
| 
						 | 
					922f4b6352 | ||
| 
						 | 
					fd3c05b42e | ||
| 
						 | 
					ab1f8326ee | ||
| 
						 | 
					2a915e4efd | ||
| 
						 | 
					f5e85a424f | ||
| 
						 | 
					c69603d916 | ||
| 
						 | 
					d75b7708a5 | ||
| 
						 | 
					b023453e81 | ||
| 
						 | 
					a5ba6237cb | ||
| 
						 | 
					0e623055df | ||
| 
						 | 
					6018f5f5d1 | ||
| 
						 | 
					96868aa754 | ||
| 
						 | 
					83d86c8c59 | ||
| 
						 | 
					7703cabb7b | ||
| 
						 | 
					300f1de11c | ||
| 
						 | 
					3b73738d9f | ||
| 
						 | 
					b176d1f890 | ||
| 
						 | 
					2aaafd6ebb | ||
| 
						 | 
					a96c013eb1 | ||
| 
						 | 
					054b215d8d | ||
| 
						 | 
					e3e98e2568 | ||
| 
						 | 
					29db576f79 | ||
| 
						 | 
					58166b3e71 | ||
| 
						 | 
					345fc0b6ca | ||
| 
						 | 
					127058e700 | ||
| 
						 | 
					57f7a709cf | ||
| 
						 | 
					f2a9e9265e | ||
| 
						 | 
					1ecd26adb5 | ||
| 
						 | 
					6d9fc672d5 | ||
| 
						 | 
					b9361b0868 | ||
| 
						 | 
					0246a8eb1d | ||
| 
						 | 
					a56d044d98 | ||
| 
						 | 
					f6253d52b4 | ||
| 
						 | 
					77dff52183 | ||
| 
						 | 
					4b86f31b66 | ||
| 
						 | 
					78655968df | ||
| 
						 | 
					ab79e596b5 | ||
| 
						 | 
					ef73ae2116 | ||
| 
						 | 
					0111f725ff | ||
| 
						 | 
					34b4cb46f6 | ||
| 
						 | 
					a2f833d665 | ||
| 
						 | 
					a7042687c1 | ||
| 
						 | 
					0d2d18c198 | ||
| 
						 | 
					3f03e8c423 | ||
| 
						 | 
					9dd6be4061 | ||
| 
						 | 
					2bf79a607f | ||
| 
						 | 
					61a11547ca | ||
| 
						 | 
					abf522bbb9 | ||
| 
						 | 
					25fc16163b | ||
| 
						 | 
					55593628ef | ||
| 
						 | 
					1f90d89731 | ||
| 
						 | 
					1560b8b8e2 | ||
| 
						 | 
					b26776fad4 | ||
| 
						 | 
					875ada86b0 | ||
| 
						 | 
					195d1be4a9 | ||
| 
						 | 
					2b12ff5874 | ||
| 
						 | 
					250b94d113 | ||
| 
						 | 
					28199c1cf8 | ||
| 
						 | 
					eeb3ccaef7 | ||
| 
						 | 
					460eb219ba | ||
| 
						 | 
					cef9cf49bf | ||
| 
						 | 
					28f09f9ed1 | ||
| 
						 | 
					3eb502b328 | ||
| 
						 | 
					7af77d0f82 | ||
| 
						 | 
					1c229947a8 | ||
| 
						 | 
					74f09a2b59 | ||
| 
						 | 
					549626bee2 | ||
| 
						 | 
					65a1d2b2ff | ||
| 
						 | 
					f7ed127182 | ||
| 
						 | 
					44767c32cf | ||
| 
						 | 
					0cc03dfe32 | ||
| 
						 | 
					1922b7b3ed | ||
| 
						 | 
					f22143f090 | ||
| 
						 | 
					be92903a6f | ||
| 
						 | 
					538941b3fd | ||
| 
						 | 
					ce8ac8b89d | ||
| 
						 | 
					6d0f134ff1 | ||
| 
						 | 
					11ccf0e591 | ||
| 
						 | 
					adfacdf1b7 | ||
| 
						 | 
					f8226cd481 | ||
| 
						 | 
					63326cbd6d | ||
| 
						 | 
					d0d7abb542 | ||
| 
						 | 
					cd7922faaf | ||
| 
						 | 
					365e3afa9b | ||
| 
						 | 
					e9c2e211ef | ||
| 
						 | 
					afda9500bf | ||
| 
						 | 
					bc7fc8df18 | ||
| 
						 | 
					2f8a4d0caa | ||
| 
						 | 
					a7ee7b962e | ||
| 
						 | 
					3cb2a4569c | ||
| 
						 | 
					3b20969171 | ||
| 
						 | 
					3b40172073 | ||
| 
						 | 
					2e220fcca2 | ||
| 
						 | 
					56e8af79c3 | ||
| 
						 | 
					25e9ec1782 | ||
| 
						 | 
					1771c852af | ||
| 
						 | 
					8714a45a5c | ||
| 
						 | 
					5e94460608 | ||
| 
						 | 
					d302c0c600 | ||
| 
						 | 
					5c943d7c13 | ||
| 
						 | 
					7629903afb | ||
| 
						 | 
					68eb4091b8 | ||
| 
						 | 
					5062e7a0e1 | ||
| 
						 | 
					30bb640c89 | ||
| 
						 | 
					fbb48c504f | ||
| 
						 | 
					440b0b5574 | ||
| 
						 | 
					c64d385fa6 | ||
| 
						 | 
					0432a10543 | ||
| 
						 | 
					4729bc87fa | ||
| 
						 | 
					e3b64103cc | ||
| 
						 | 
					ebdcb3e4d9 | ||
| 
						 | 
					971522574d | ||
| 
						 | 
					73e939dbbc | ||
| 
						 | 
					a96798ef98 | ||
| 
						 | 
					923e7049f1 | ||
| 
						 | 
					26df542036 | ||
| 
						 | 
					1ccec6950a | ||
| 
						 | 
					b3a122de3c | ||
| 
						 | 
					9ea3643b74 | ||
| 
						 | 
					de617c85c7 | ||
| 
						 | 
					e47f4ef602 | ||
| 
						 | 
					9c201afe76 | ||
| 
						 | 
					2bb64a189d | ||
| 
						 | 
					9853a2e6ab | ||
| 
						 | 
					961be7fd12 | ||
| 
						 | 
					a5a21f47d1 | ||
| 
						 | 
					a06cd84974 | ||
| 
						 | 
					e3703b43c1 | ||
| 
						 | 
					f6dc25c0ce | ||
| 
						 | 
					fad0ec7793 | ||
| 
						 | 
					a302cec993 | ||
| 
						 | 
					6781da45cb | ||
| 
						 | 
					37d526f003 | ||
| 
						 | 
					d74cfefeef | ||
| 
						 | 
					1ffb9d972a | ||
| 
						 | 
					4e5339801b | ||
| 
						 | 
					b8cee477fe | ||
| 
						 | 
					ff2df278d6 | ||
| 
						 | 
					429e989b69 | ||
| 
						 | 
					28541bdb1c | ||
| 
						 | 
					11c595bb09 | ||
| 
						 | 
					fd888eaa68 | ||
| 
						 | 
					3a233b2fd0 | ||
| 
						 | 
					d2df232706 | ||
| 
						 | 
					404e679e66 | ||
| 
						 | 
					4426bf6029 | ||
| 
						 | 
					27fa18dcec | ||
| 
						 | 
					22989592f0 | ||
| 
						 | 
					1f4b10f523 | ||
| 
						 | 
					cbaf8d309b | ||
| 
						 | 
					660223e269 | ||
| 
						 | 
					6d1de2106e | ||
| 
						 | 
					90e33306f1 | ||
| 
						 | 
					f3ac21b3b4 | ||
| 
						 | 
					4859fe67eb | ||
| 
						 | 
					a723673dcc | ||
| 
						 | 
					612fb4cc3c | ||
| 
						 | 
					5fac67d195 | ||
| 
						 | 
					d671862e9a | ||
| 
						 | 
					459ef7f262 | ||
| 
						 | 
					bd9dc43e59 | ||
| 
						 | 
					1d5a3b647d | ||
| 
						 | 
					af3e1788d1 | ||
| 
						 | 
					b946cb160d | ||
| 
						 | 
					e0241e9dcd | ||
| 
						 | 
					1accc409f6 | ||
| 
						 | 
					f756de276b | ||
| 
						 | 
					ac07a00141 | ||
| 
						 | 
					7ae11de2e4 | ||
| 
						 | 
					bb6be9c939 | ||
| 
						 | 
					9c85a7eff3 | ||
| 
						 | 
					10a665b864 | ||
| 
						 | 
					35dce3c80d | ||
| 
						 | 
					7e6b11ce84 | ||
| 
						 | 
					adcba4fd9a | ||
| 
						 | 
					d3592c451b | ||
| 
						 | 
					24eb33a1c0 | ||
| 
						 | 
					cf1fef8cfb | ||
| 
						 | 
					28bba0666c | ||
| 
						 | 
					4390fd80a3 | ||
| 
						 | 
					4813c5134e | ||
| 
						 | 
					bbef0e173e | ||
| 
						 | 
					3240e19a7c | ||
| 
						 | 
					ac0cd946f0 | ||
| 
						 | 
					61bac6c6e6 | ||
| 
						 | 
					5fd64c5c89 | ||
| 
						 | 
					625f108183 | ||
| 
						 | 
					c45efe8f40 | ||
| 
						 | 
					fe1371f4dc | ||
| 
						 | 
					e3f8a36eaa | ||
| 
						 | 
					41f0d1c622 | ||
| 
						 | 
					6469bb168d | ||
| 
						 | 
					af0da3f897 | ||
| 
						 | 
					32e4eb26ad | ||
| 
						 | 
					10aae33979 | ||
| 
						 | 
					55dd12c66b | ||
| 
						 | 
					9dd17b464d | 
@@ -186,6 +186,11 @@ This document provides essential context for AI models interacting with this pro
 | 
			
		||||
        └── components/[component]/ # Component-specific tests
 | 
			
		||||
        ```
 | 
			
		||||
        Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms.
 | 
			
		||||
    *   **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use:
 | 
			
		||||
        ```bash
 | 
			
		||||
        ./script/test_component_grouping.py -e config --all
 | 
			
		||||
        ```
 | 
			
		||||
        This tests all components in a single build to catch conflicts that might not appear when testing components individually. Use `-e config` for fast configuration validation, or `-e compile` for full compilation testing.
 | 
			
		||||
*   **Debugging and Troubleshooting:**
 | 
			
		||||
    *   **Debug Tools:**
 | 
			
		||||
        - `esphome config <file>.yaml` to validate configuration.
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1 @@
 | 
			
		||||
4368db58e8f884aff245996b1e8b644cc0796c0bb2fa706d5740d40b823d3ac9
 | 
			
		||||
049d60eed541730efaa4c0dc5d337b4287bf29b6daa350b5dfc1f23915f1c52f
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
								
							@@ -47,7 +47,7 @@ runs:
 | 
			
		||||
 | 
			
		||||
    - name: Build and push to ghcr by digest
 | 
			
		||||
      id: build-ghcr
 | 
			
		||||
      uses: docker/build-push-action@v6.18.0
 | 
			
		||||
      uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
 | 
			
		||||
      env:
 | 
			
		||||
        DOCKER_BUILD_SUMMARY: false
 | 
			
		||||
        DOCKER_BUILD_RECORD_UPLOAD: false
 | 
			
		||||
@@ -73,7 +73,7 @@ runs:
 | 
			
		||||
 | 
			
		||||
    - name: Build and push to dockerhub by digest
 | 
			
		||||
      id: build-dockerhub
 | 
			
		||||
      uses: docker/build-push-action@v6.18.0
 | 
			
		||||
      uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
 | 
			
		||||
      env:
 | 
			
		||||
        DOCKER_BUILD_SUMMARY: false
 | 
			
		||||
        DOCKER_BUILD_RECORD_UPLOAD: false
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							@@ -17,12 +17,12 @@ runs:
 | 
			
		||||
  steps:
 | 
			
		||||
    - name: Set up Python ${{ inputs.python-version }}
 | 
			
		||||
      id: python
 | 
			
		||||
      uses: actions/setup-python@v6.0.0
 | 
			
		||||
      uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
 | 
			
		||||
      with:
 | 
			
		||||
        python-version: ${{ inputs.python-version }}
 | 
			
		||||
    - name: Restore Python virtual environment
 | 
			
		||||
      id: cache-venv
 | 
			
		||||
      uses: actions/cache/restore@v4.2.4
 | 
			
		||||
      uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
      with:
 | 
			
		||||
        path: venv
 | 
			
		||||
        # yamllint disable-line rule:line-length
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							@@ -22,17 +22,17 @@ jobs:
 | 
			
		||||
    if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
 | 
			
		||||
      - name: Generate a token
 | 
			
		||||
        id: generate-token
 | 
			
		||||
        uses: actions/create-github-app-token@v2
 | 
			
		||||
        uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
 | 
			
		||||
        with:
 | 
			
		||||
          app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
 | 
			
		||||
          private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
 | 
			
		||||
 | 
			
		||||
      - name: Auto Label PR
 | 
			
		||||
        uses: actions/github-script@v8.0.0
 | 
			
		||||
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          github-token: ${{ steps.generate-token.outputs.token }}
 | 
			
		||||
          script: |
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							@@ -21,9 +21,9 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.11"
 | 
			
		||||
 | 
			
		||||
@@ -47,7 +47,7 @@ jobs:
 | 
			
		||||
          fi
 | 
			
		||||
      - if: failure()
 | 
			
		||||
        name: Review PR
 | 
			
		||||
        uses: actions/github-script@v8.0.0
 | 
			
		||||
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          script: |
 | 
			
		||||
            await github.rest.pulls.createReview({
 | 
			
		||||
@@ -62,7 +62,7 @@ jobs:
 | 
			
		||||
        run: git diff
 | 
			
		||||
      - if: failure()
 | 
			
		||||
        name: Archive artifacts
 | 
			
		||||
        uses: actions/upload-artifact@v4.6.2
 | 
			
		||||
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
 | 
			
		||||
        with:
 | 
			
		||||
          name: generated-proto-files
 | 
			
		||||
          path: |
 | 
			
		||||
@@ -70,7 +70,7 @@ jobs:
 | 
			
		||||
            esphome/components/api/api_pb2_service.*
 | 
			
		||||
      - if: success()
 | 
			
		||||
        name: Dismiss review
 | 
			
		||||
        uses: actions/github-script@v8.0.0
 | 
			
		||||
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          script: |
 | 
			
		||||
            let reviews = await github.rest.pulls.listReviews({
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								.github/workflows/ci-clang-tidy-hash.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/ci-clang-tidy-hash.yml
									
									
									
									
										vendored
									
									
								
							@@ -6,6 +6,7 @@ on:
 | 
			
		||||
      - ".clang-tidy"
 | 
			
		||||
      - "platformio.ini"
 | 
			
		||||
      - "requirements_dev.txt"
 | 
			
		||||
      - "sdkconfig.defaults"
 | 
			
		||||
      - ".clang-tidy.hash"
 | 
			
		||||
      - "script/clang_tidy_hash.py"
 | 
			
		||||
      - ".github/workflows/ci-clang-tidy-hash.yml"
 | 
			
		||||
@@ -20,10 +21,10 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.11"
 | 
			
		||||
 | 
			
		||||
@@ -41,7 +42,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - if: failure()
 | 
			
		||||
        name: Request changes
 | 
			
		||||
        uses: actions/github-script@v8.0.0
 | 
			
		||||
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          script: |
 | 
			
		||||
            await github.rest.pulls.createReview({
 | 
			
		||||
@@ -54,7 +55,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - if: success()
 | 
			
		||||
        name: Dismiss review
 | 
			
		||||
        uses: actions/github-script@v8.0.0
 | 
			
		||||
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          script: |
 | 
			
		||||
            let reviews = await github.rest.pulls.listReviews({
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							@@ -43,13 +43,13 @@ jobs:
 | 
			
		||||
          - "docker"
 | 
			
		||||
          # - "lint"
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5.0.0
 | 
			
		||||
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.11"
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.11.1
 | 
			
		||||
        uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
 | 
			
		||||
 | 
			
		||||
      - name: Set TAG
 | 
			
		||||
        run: |
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										143
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										143
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -36,18 +36,18 @@ jobs:
 | 
			
		||||
      cache-key: ${{ steps.cache-key.outputs.key }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Generate cache-key
 | 
			
		||||
        id: cache-key
 | 
			
		||||
        run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
 | 
			
		||||
      - name: Set up Python ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
        id: python
 | 
			
		||||
        uses: actions/setup-python@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
        id: cache-venv
 | 
			
		||||
        uses: actions/cache@v4.2.4
 | 
			
		||||
        uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
@@ -70,7 +70,7 @@ jobs:
 | 
			
		||||
    if: needs.determine-jobs.outputs.python-linters == 'true'
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -91,7 +91,7 @@ jobs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -105,6 +105,7 @@ jobs:
 | 
			
		||||
          script/ci-custom.py
 | 
			
		||||
          script/build_codeowners.py --check
 | 
			
		||||
          script/build_language_schema.py --check
 | 
			
		||||
          script/generate-esp32-boards.py --check
 | 
			
		||||
 | 
			
		||||
  pytest:
 | 
			
		||||
    name: Run pytest
 | 
			
		||||
@@ -136,7 +137,7 @@ jobs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        id: restore-python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
@@ -156,12 +157,12 @@ jobs:
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
 | 
			
		||||
      - name: Upload coverage to Codecov
 | 
			
		||||
        uses: codecov/codecov-action@v5.5.1
 | 
			
		||||
        uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
 | 
			
		||||
        with:
 | 
			
		||||
          token: ${{ secrets.CODECOV_TOKEN }}
 | 
			
		||||
      - name: Save Python virtual environment cache
 | 
			
		||||
        if: github.ref == 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache/save@v4.2.4
 | 
			
		||||
        uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
 | 
			
		||||
@@ -176,10 +177,11 @@ jobs:
 | 
			
		||||
      clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
 | 
			
		||||
      python-linters: ${{ steps.determine.outputs.python-linters }}
 | 
			
		||||
      changed-components: ${{ steps.determine.outputs.changed-components }}
 | 
			
		||||
      changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
 | 
			
		||||
      component-test-count: ${{ steps.determine.outputs.component-test-count }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          # Fetch enough history to find the merge base
 | 
			
		||||
          fetch-depth: 2
 | 
			
		||||
@@ -203,6 +205,7 @@ jobs:
 | 
			
		||||
          echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
  integration-tests:
 | 
			
		||||
@@ -214,15 +217,15 @@ jobs:
 | 
			
		||||
    if: needs.determine-jobs.outputs.integration-tests == 'true'
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Set up Python 3.13
 | 
			
		||||
        id: python
 | 
			
		||||
        uses: actions/setup-python@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.13"
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
        id: cache-venv
 | 
			
		||||
        uses: actions/cache@v4.2.4
 | 
			
		||||
        uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
 | 
			
		||||
@@ -287,7 +290,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          # Need history for HEAD~1 to work for checking changed files
 | 
			
		||||
          fetch-depth: 2
 | 
			
		||||
@@ -300,14 +303,14 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref == 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache@v4.2.4
 | 
			
		||||
        uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref != 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache/restore@v4.2.4
 | 
			
		||||
        uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
@@ -366,31 +369,32 @@ jobs:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      max-parallel: 2
 | 
			
		||||
      matrix:
 | 
			
		||||
        file: ${{ fromJson(needs.determine-jobs.outputs.changed-components) }}
 | 
			
		||||
        file: ${{ fromJson(needs.determine-jobs.outputs.changed-components-with-tests) }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: |
 | 
			
		||||
          sudo apt-get update
 | 
			
		||||
          sudo apt-get install libsdl2-dev
 | 
			
		||||
      - name: Cache apt packages
 | 
			
		||||
        uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
 | 
			
		||||
        with:
 | 
			
		||||
          packages: libsdl2-dev
 | 
			
		||||
          version: 1.0
 | 
			
		||||
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: test_build_components -e config -c ${{ matrix.file }}
 | 
			
		||||
      - name: Validate config for ${{ matrix.file }}
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          ./script/test_build_components -e config -c ${{ matrix.file }}
 | 
			
		||||
      - name: test_build_components -e compile -c ${{ matrix.file }}
 | 
			
		||||
          python3 script/test_build_components.py -e config -c ${{ matrix.file }}
 | 
			
		||||
      - name: Compile config for ${{ matrix.file }}
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          ./script/test_build_components -e compile -c ${{ matrix.file }}
 | 
			
		||||
          python3 script/test_build_components.py -e compile -c ${{ matrix.file }}
 | 
			
		||||
 | 
			
		||||
  test-build-components-splitter:
 | 
			
		||||
    name: Split components for testing into 20 groups maximum
 | 
			
		||||
    name: Split components for intelligent grouping (40 weighted per batch)
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
@@ -400,15 +404,27 @@ jobs:
 | 
			
		||||
      matrix: ${{ steps.split.outputs.components }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
      - name: Split components into 20 groups
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Split components intelligently based on bus configurations
 | 
			
		||||
        id: split
 | 
			
		||||
        run: |
 | 
			
		||||
          components=$(echo '${{ needs.determine-jobs.outputs.changed-components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]')
 | 
			
		||||
          echo "components=$components" >> $GITHUB_OUTPUT
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
 | 
			
		||||
          # Use intelligent splitter that groups components with same bus configs
 | 
			
		||||
          components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
 | 
			
		||||
 | 
			
		||||
          echo "Splitting components intelligently..."
 | 
			
		||||
          output=$(python3 script/split_components_for_ci.py --components "$components" --batch-size 40 --output github)
 | 
			
		||||
 | 
			
		||||
          echo "$output" >> $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
  test-build-components-split:
 | 
			
		||||
    name: Test split components
 | 
			
		||||
    name: Test components batch (${{ matrix.components }})
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
@@ -417,39 +433,62 @@ jobs:
 | 
			
		||||
    if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      max-parallel: 4
 | 
			
		||||
      max-parallel: 5
 | 
			
		||||
      matrix:
 | 
			
		||||
        components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Show disk space
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "Available disk space:"
 | 
			
		||||
          df -h
 | 
			
		||||
 | 
			
		||||
      - name: List components
 | 
			
		||||
        run: echo ${{ matrix.components }}
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: |
 | 
			
		||||
          sudo apt-get update
 | 
			
		||||
          sudo apt-get install libsdl2-dev
 | 
			
		||||
      - name: Cache apt packages
 | 
			
		||||
        uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
 | 
			
		||||
        with:
 | 
			
		||||
          packages: libsdl2-dev
 | 
			
		||||
          version: 1.0
 | 
			
		||||
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Validate config
 | 
			
		||||
      - name: Validate and compile components with intelligent grouping
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          for component in ${{ matrix.components }}; do
 | 
			
		||||
            ./script/test_build_components -e config -c $component
 | 
			
		||||
          done
 | 
			
		||||
      - name: Compile config
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          mkdir build_cache
 | 
			
		||||
          export PLATFORMIO_BUILD_CACHE_DIR=$PWD/build_cache
 | 
			
		||||
          for component in ${{ matrix.components }}; do
 | 
			
		||||
            ./script/test_build_components -e compile -c $component
 | 
			
		||||
          done
 | 
			
		||||
          # Use /mnt for build files (70GB available vs ~29GB on /)
 | 
			
		||||
          # Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
 | 
			
		||||
          sudo mkdir -p /mnt/platformio
 | 
			
		||||
          sudo chown $USER:$USER /mnt/platformio
 | 
			
		||||
          mkdir -p ~/.platformio
 | 
			
		||||
          sudo mount --bind /mnt/platformio ~/.platformio
 | 
			
		||||
 | 
			
		||||
          # Bind mount test build directory to /mnt
 | 
			
		||||
          sudo mkdir -p /mnt/test_build_components_build
 | 
			
		||||
          sudo chown $USER:$USER /mnt/test_build_components_build
 | 
			
		||||
          mkdir -p tests/test_build_components/build
 | 
			
		||||
          sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
 | 
			
		||||
 | 
			
		||||
          # Convert space-separated components to comma-separated for Python script
 | 
			
		||||
          components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',')
 | 
			
		||||
 | 
			
		||||
          echo "Testing components: $components_csv"
 | 
			
		||||
          echo ""
 | 
			
		||||
 | 
			
		||||
          # Run config validation with grouping
 | 
			
		||||
          python3 script/test_build_components.py -e config -c "$components_csv" -f
 | 
			
		||||
 | 
			
		||||
          echo ""
 | 
			
		||||
          echo "Config validation passed! Starting compilation..."
 | 
			
		||||
          echo ""
 | 
			
		||||
 | 
			
		||||
          # Run compilation with grouping
 | 
			
		||||
          python3 script/test_build_components.py -e compile -c "$components_csv" -f
 | 
			
		||||
 | 
			
		||||
  pre-commit-ci-lite:
 | 
			
		||||
    name: pre-commit.ci lite
 | 
			
		||||
@@ -459,16 +498,16 @@ jobs:
 | 
			
		||||
    if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release'
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - uses: pre-commit/action@v3.0.1
 | 
			
		||||
      - uses: esphome/action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
 | 
			
		||||
        env:
 | 
			
		||||
          SKIP: pylint,clang-tidy-hash
 | 
			
		||||
      - uses: pre-commit-ci/lite-action@v1.1.0
 | 
			
		||||
      - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
 | 
			
		||||
        if: always()
 | 
			
		||||
 | 
			
		||||
  ci-status:
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Request reviews from component codeowners
 | 
			
		||||
        uses: actions/github-script@v8.0.0
 | 
			
		||||
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          script: |
 | 
			
		||||
            const owner = context.repo.owner;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							@@ -54,11 +54,11 @@ jobs:
 | 
			
		||||
            # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout repository
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
 | 
			
		||||
      # Initializes the CodeQL tools for scanning.
 | 
			
		||||
      - name: Initialize CodeQL
 | 
			
		||||
        uses: github/codeql-action/init@v3
 | 
			
		||||
        uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
 | 
			
		||||
        with:
 | 
			
		||||
          languages: ${{ matrix.language }}
 | 
			
		||||
          build-mode: ${{ matrix.build-mode }}
 | 
			
		||||
@@ -86,6 +86,6 @@ jobs:
 | 
			
		||||
          exit 1
 | 
			
		||||
 | 
			
		||||
      - name: Perform CodeQL Analysis
 | 
			
		||||
        uses: github/codeql-action/analyze@v3
 | 
			
		||||
        uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
 | 
			
		||||
        with:
 | 
			
		||||
          category: "/language:${{matrix.language}}"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/external-component-bot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/external-component-bot.yml
									
									
									
									
										vendored
									
									
								
							@@ -15,7 +15,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Add external component comment
 | 
			
		||||
        uses: actions/github-script@v8.0.0
 | 
			
		||||
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          github-token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          script: |
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/issue-codeowner-notify.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/issue-codeowner-notify.yml
									
									
									
									
										vendored
									
									
								
							@@ -19,7 +19,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Notify codeowners for component issues
 | 
			
		||||
        uses: actions/github-script@v8.0.0
 | 
			
		||||
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          script: |
 | 
			
		||||
            const owner = context.repo.owner;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										34
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										34
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -20,7 +20,7 @@ jobs:
 | 
			
		||||
      branch_build: ${{ steps.tag.outputs.branch_build }}
 | 
			
		||||
      deploy_env: ${{ steps.tag.outputs.deploy_env }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5.0.0
 | 
			
		||||
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Get tag
 | 
			
		||||
        id: tag
 | 
			
		||||
        # yamllint disable rule:line-length
 | 
			
		||||
@@ -60,9 +60,9 @@ jobs:
 | 
			
		||||
      contents: read
 | 
			
		||||
      id-token: write
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5.0.0
 | 
			
		||||
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.x"
 | 
			
		||||
      - name: Build
 | 
			
		||||
@@ -70,7 +70,7 @@ jobs:
 | 
			
		||||
          pip3 install build
 | 
			
		||||
          python3 -m build
 | 
			
		||||
      - name: Publish
 | 
			
		||||
        uses: pypa/gh-action-pypi-publish@v1.13.0
 | 
			
		||||
        uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
 | 
			
		||||
        with:
 | 
			
		||||
          skip-existing: true
 | 
			
		||||
 | 
			
		||||
@@ -92,22 +92,22 @@ jobs:
 | 
			
		||||
            os: "ubuntu-24.04-arm"
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5.0.0
 | 
			
		||||
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.11"
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.11.1
 | 
			
		||||
        uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
 | 
			
		||||
 | 
			
		||||
      - name: Log in to docker hub
 | 
			
		||||
        uses: docker/login-action@v3.5.0
 | 
			
		||||
        uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKER_USER }}
 | 
			
		||||
          password: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
      - name: Log in to the GitHub container registry
 | 
			
		||||
        uses: docker/login-action@v3.5.0
 | 
			
		||||
        uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
@@ -138,7 +138,7 @@ jobs:
 | 
			
		||||
      #     version: ${{ needs.init.outputs.tag }}
 | 
			
		||||
 | 
			
		||||
      - name: Upload digests
 | 
			
		||||
        uses: actions/upload-artifact@v4.6.2
 | 
			
		||||
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
 | 
			
		||||
        with:
 | 
			
		||||
          name: digests-${{ matrix.platform.arch }}
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
@@ -168,27 +168,27 @@ jobs:
 | 
			
		||||
          - ghcr
 | 
			
		||||
          - dockerhub
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5.0.0
 | 
			
		||||
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
 | 
			
		||||
      - name: Download digests
 | 
			
		||||
        uses: actions/download-artifact@v5.0.0
 | 
			
		||||
        uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          pattern: digests-*
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
          merge-multiple: true
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.11.1
 | 
			
		||||
        uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
 | 
			
		||||
 | 
			
		||||
      - name: Log in to docker hub
 | 
			
		||||
        if: matrix.registry == 'dockerhub'
 | 
			
		||||
        uses: docker/login-action@v3.5.0
 | 
			
		||||
        uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKER_USER }}
 | 
			
		||||
          password: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
      - name: Log in to the GitHub container registry
 | 
			
		||||
        if: matrix.registry == 'ghcr'
 | 
			
		||||
        uses: docker/login-action@v3.5.0
 | 
			
		||||
        uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
@@ -220,7 +220,7 @@ jobs:
 | 
			
		||||
      - deploy-manifest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Trigger Workflow
 | 
			
		||||
        uses: actions/github-script@v8.0.0
 | 
			
		||||
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }}
 | 
			
		||||
          script: |
 | 
			
		||||
@@ -246,7 +246,7 @@ jobs:
 | 
			
		||||
    environment: ${{ needs.init.outputs.deploy_env }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Trigger Workflow
 | 
			
		||||
        uses: actions/github-script@v8.0.0
 | 
			
		||||
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }}
 | 
			
		||||
          script: |
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										52
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										52
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							@@ -15,36 +15,52 @@ concurrency:
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  stale:
 | 
			
		||||
    if: github.repository_owner == 'esphome'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/stale@v10.0.0
 | 
			
		||||
      - name: Stale
 | 
			
		||||
        uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
 | 
			
		||||
        with:
 | 
			
		||||
          debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
 | 
			
		||||
          remove-stale-when-updated: true
 | 
			
		||||
          operations-per-run: 400
 | 
			
		||||
 | 
			
		||||
          # The 90 day stale policy for PRs
 | 
			
		||||
          # - PRs
 | 
			
		||||
          # - No PRs marked as "not-stale"
 | 
			
		||||
          # - No Issues (see below)
 | 
			
		||||
          days-before-pr-stale: 90
 | 
			
		||||
          days-before-pr-close: 7
 | 
			
		||||
          days-before-issue-stale: -1
 | 
			
		||||
          days-before-issue-close: -1
 | 
			
		||||
          remove-stale-when-updated: true
 | 
			
		||||
          stale-pr-label: "stale"
 | 
			
		||||
          exempt-pr-labels: "not-stale"
 | 
			
		||||
          stale-pr-message: >
 | 
			
		||||
            There hasn't been any activity on this pull request recently. This
 | 
			
		||||
            pull request has been automatically marked as stale because of that
 | 
			
		||||
            and will be closed if no further activity occurs within 7 days.
 | 
			
		||||
            Thank you for your contributions.
 | 
			
		||||
 | 
			
		||||
  # Use stale to automatically close issues with a
 | 
			
		||||
  # reference to the issue tracker
 | 
			
		||||
  close-issues:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/stale@v10.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          days-before-pr-stale: -1
 | 
			
		||||
          days-before-pr-close: -1
 | 
			
		||||
          days-before-issue-stale: 1
 | 
			
		||||
          days-before-issue-close: 1
 | 
			
		||||
          remove-stale-when-updated: true
 | 
			
		||||
            If you are the author of this PR, please leave a comment if you want
 | 
			
		||||
            to keep it open. Also, please rebase your PR onto the latest dev
 | 
			
		||||
            branch to ensure that it's up to date with the latest changes.
 | 
			
		||||
 | 
			
		||||
            Thank you for your contribution!
 | 
			
		||||
 | 
			
		||||
          # The 90 day stale policy for Issues
 | 
			
		||||
          # - Issues
 | 
			
		||||
          # - No Issues marked as "not-stale"
 | 
			
		||||
          # - No PRs (see above)
 | 
			
		||||
          days-before-issue-stale: 90
 | 
			
		||||
          days-before-issue-close: 7
 | 
			
		||||
          stale-issue-label: "stale"
 | 
			
		||||
          exempt-issue-labels: "not-stale"
 | 
			
		||||
          stale-issue-message: >
 | 
			
		||||
            https://github.com/esphome/esphome/issues/430
 | 
			
		||||
            There hasn't been any activity on this issue recently. Due to the
 | 
			
		||||
            high number of incoming GitHub notifications, we have to clean some
 | 
			
		||||
            of the old issues, as many of them have already been resolved with
 | 
			
		||||
            the latest updates.
 | 
			
		||||
 | 
			
		||||
            Please make sure to update to the latest ESPHome version and
 | 
			
		||||
            check if that solves the issue. Let us know if that works for you by
 | 
			
		||||
            adding a comment 👍
 | 
			
		||||
 | 
			
		||||
            This issue has now been marked as stale and will be closed if no
 | 
			
		||||
            further activity occurs. Thank you for your contributions.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							@@ -16,7 +16,7 @@ jobs:
 | 
			
		||||
          - merge-after-release
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check for ${{ matrix.label }} label
 | 
			
		||||
        uses: actions/github-script@v8.0.0
 | 
			
		||||
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          script: |
 | 
			
		||||
            const { data: labels } = await github.rest.issues.listLabelsOnIssue({
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							@@ -13,16 +13,16 @@ jobs:
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
 | 
			
		||||
      - name: Checkout Home Assistant
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          repository: home-assistant/core
 | 
			
		||||
          path: lib/home-assistant
 | 
			
		||||
 | 
			
		||||
      - name: Setup Python
 | 
			
		||||
        uses: actions/setup-python@v6.0.0
 | 
			
		||||
        uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: 3.13
 | 
			
		||||
 | 
			
		||||
@@ -30,13 +30,18 @@ jobs:
 | 
			
		||||
        run: |
 | 
			
		||||
          python -m pip install --upgrade pip
 | 
			
		||||
          pip install -e lib/home-assistant
 | 
			
		||||
          pip install -r requirements_test.txt pre-commit
 | 
			
		||||
 | 
			
		||||
      - name: Sync
 | 
			
		||||
        run: |
 | 
			
		||||
          python ./script/sync-device_class.py
 | 
			
		||||
 | 
			
		||||
      - name: Run pre-commit hooks
 | 
			
		||||
        run: |
 | 
			
		||||
          python script/run-in-env.py pre-commit run --all-files
 | 
			
		||||
 | 
			
		||||
      - name: Commit changes
 | 
			
		||||
        uses: peter-evans/create-pull-request@v7.0.8
 | 
			
		||||
        uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
 | 
			
		||||
        with:
 | 
			
		||||
          commit-message: "Synchronise Device Classes from Home Assistant"
 | 
			
		||||
          committer: esphomebot <esphome@openhomefoundation.org>
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ ci:
 | 
			
		||||
repos:
 | 
			
		||||
  - repo: https://github.com/astral-sh/ruff-pre-commit
 | 
			
		||||
    # Ruff version.
 | 
			
		||||
    rev: v0.12.12
 | 
			
		||||
    rev: v0.14.0
 | 
			
		||||
    hooks:
 | 
			
		||||
      # Run the linter.
 | 
			
		||||
      - id: ruff
 | 
			
		||||
 
 | 
			
		||||
@@ -139,6 +139,7 @@ esphome/components/ens160_base/* @latonita @vincentscode
 | 
			
		||||
esphome/components/ens160_i2c/* @latonita
 | 
			
		||||
esphome/components/ens160_spi/* @latonita
 | 
			
		||||
esphome/components/ens210/* @itn3rd77
 | 
			
		||||
esphome/components/epaper_spi/* @esphome/core
 | 
			
		||||
esphome/components/es7210/* @kahrendt
 | 
			
		||||
esphome/components/es7243e/* @kbx81
 | 
			
		||||
esphome/components/es8156/* @kbx81
 | 
			
		||||
@@ -160,7 +161,6 @@ esphome/components/esp_ldo/* @clydebarrow
 | 
			
		||||
esphome/components/espnow/* @jesserockz
 | 
			
		||||
esphome/components/ethernet_info/* @gtjadsonsantos
 | 
			
		||||
esphome/components/event/* @nohat
 | 
			
		||||
esphome/components/event_emitter/* @Rapsssito
 | 
			
		||||
esphome/components/exposure_notifications/* @OttoWinter
 | 
			
		||||
esphome/components/ezo/* @ssieb
 | 
			
		||||
esphome/components/ezo_pmp/* @carlos-sarmiento
 | 
			
		||||
@@ -257,6 +257,7 @@ esphome/components/libretiny_pwm/* @kuba2k2
 | 
			
		||||
esphome/components/light/* @esphome/core
 | 
			
		||||
esphome/components/lightwaverf/* @max246
 | 
			
		||||
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
 | 
			
		||||
esphome/components/lm75b/* @beormund
 | 
			
		||||
esphome/components/ln882x/* @lamauny
 | 
			
		||||
esphome/components/lock/* @esphome/core
 | 
			
		||||
esphome/components/logger/* @esphome/core
 | 
			
		||||
@@ -407,6 +408,7 @@ esphome/components/sensor/* @esphome/core
 | 
			
		||||
esphome/components/sfa30/* @ghsensdev
 | 
			
		||||
esphome/components/sgp40/* @SenexCrenshaw
 | 
			
		||||
esphome/components/sgp4x/* @martgras @SenexCrenshaw
 | 
			
		||||
esphome/components/sha256/* @esphome/core
 | 
			
		||||
esphome/components/shelly_dimmer/* @edge90 @rnauber
 | 
			
		||||
esphome/components/sht3xd/* @mrtoy-me
 | 
			
		||||
esphome/components/sht4x/* @sjtrny
 | 
			
		||||
@@ -428,6 +430,7 @@ esphome/components/speaker/media_player/* @kahrendt @synesthesiam
 | 
			
		||||
esphome/components/spi/* @clydebarrow @esphome/core
 | 
			
		||||
esphome/components/spi_device/* @clydebarrow
 | 
			
		||||
esphome/components/spi_led_strip/* @clydebarrow
 | 
			
		||||
esphome/components/split_buffer/* @jesserockz
 | 
			
		||||
esphome/components/sprinkler/* @kbx81
 | 
			
		||||
esphome/components/sps30/* @martgras
 | 
			
		||||
esphome/components/ssd1322_base/* @kbx81
 | 
			
		||||
@@ -533,6 +536,7 @@ esphome/components/wk2204_spi/* @DrCoolZic
 | 
			
		||||
esphome/components/wk2212_i2c/* @DrCoolZic
 | 
			
		||||
esphome/components/wk2212_spi/* @DrCoolZic
 | 
			
		||||
esphome/components/wl_134/* @hobbypunk90
 | 
			
		||||
esphome/components/wts01/* @alepee
 | 
			
		||||
esphome/components/x9c/* @EtienneMD
 | 
			
		||||
esphome/components/xgzp68xx/* @gcormier
 | 
			
		||||
esphome/components/xiaomi_hhccjcy10/* @fariouche
 | 
			
		||||
@@ -548,3 +552,4 @@ esphome/components/xxtea/* @clydebarrow
 | 
			
		||||
esphome/components/zephyr/* @tomaszduda23
 | 
			
		||||
esphome/components/zhlt01/* @cfeenstra1024
 | 
			
		||||
esphome/components/zio_ultrasonic/* @kahrendt
 | 
			
		||||
esphome/components/zwave_proxy/* @kbx81
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							@@ -48,7 +48,7 @@ PROJECT_NAME           = ESPHome
 | 
			
		||||
# could be handy for archiving the generated documentation or if some version
 | 
			
		||||
# control system is used.
 | 
			
		||||
 | 
			
		||||
PROJECT_NUMBER         = 2025.9.0b4
 | 
			
		||||
PROJECT_NUMBER         = 2025.11.0-dev
 | 
			
		||||
 | 
			
		||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
 | 
			
		||||
# for a project that appears at the top of each page and should give viewer a
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import getpass
 | 
			
		||||
import importlib
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import re
 | 
			
		||||
import sys
 | 
			
		||||
import time
 | 
			
		||||
@@ -13,9 +14,11 @@ from typing import Protocol
 | 
			
		||||
 | 
			
		||||
import argcomplete
 | 
			
		||||
 | 
			
		||||
# Note: Do not import modules from esphome.components here, as this would
 | 
			
		||||
# cause them to be loaded before external components are processed, resulting
 | 
			
		||||
# in the built-in version being used instead of the external component one.
 | 
			
		||||
from esphome import const, writer, yaml_util
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components.mqtt import CONF_DISCOVER_IP
 | 
			
		||||
from esphome.config import iter_component_configs, read_config, strip_default_ids
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    ALLOWED_NAME_CHARS,
 | 
			
		||||
@@ -114,6 +117,14 @@ class Purpose(StrEnum):
 | 
			
		||||
    LOGGING = "logging"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _resolve_with_cache(address: str, purpose: Purpose) -> list[str]:
 | 
			
		||||
    """Resolve an address using cache if available, otherwise return the address itself."""
 | 
			
		||||
    if CORE.address_cache and (cached := CORE.address_cache.get_addresses(address)):
 | 
			
		||||
        _LOGGER.debug("Using cached addresses for %s: %s", purpose.value, cached)
 | 
			
		||||
        return cached
 | 
			
		||||
    return [address]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def choose_upload_log_host(
 | 
			
		||||
    default: list[str] | str | None,
 | 
			
		||||
    check_default: str | None,
 | 
			
		||||
@@ -142,7 +153,7 @@ def choose_upload_log_host(
 | 
			
		||||
                    (purpose == Purpose.LOGGING and has_api())
 | 
			
		||||
                    or (purpose == Purpose.UPLOADING and has_ota())
 | 
			
		||||
                ):
 | 
			
		||||
                    resolved.append(CORE.address)
 | 
			
		||||
                    resolved.extend(_resolve_with_cache(CORE.address, purpose))
 | 
			
		||||
 | 
			
		||||
                if purpose == Purpose.LOGGING:
 | 
			
		||||
                    if has_api() and has_mqtt_ip_lookup():
 | 
			
		||||
@@ -152,15 +163,14 @@ def choose_upload_log_host(
 | 
			
		||||
                        resolved.append("MQTT")
 | 
			
		||||
 | 
			
		||||
                    if has_api() and has_non_ip_address():
 | 
			
		||||
                        resolved.append(CORE.address)
 | 
			
		||||
                        resolved.extend(_resolve_with_cache(CORE.address, purpose))
 | 
			
		||||
 | 
			
		||||
                elif purpose == Purpose.UPLOADING:
 | 
			
		||||
                    if has_ota() and has_mqtt_ip_lookup():
 | 
			
		||||
                        resolved.append("MQTTIP")
 | 
			
		||||
 | 
			
		||||
                    if has_ota() and has_non_ip_address():
 | 
			
		||||
                        resolved.append(CORE.address)
 | 
			
		||||
 | 
			
		||||
                        resolved.extend(_resolve_with_cache(CORE.address, purpose))
 | 
			
		||||
            else:
 | 
			
		||||
                resolved.append(device)
 | 
			
		||||
        if not resolved:
 | 
			
		||||
@@ -212,7 +222,7 @@ def has_mqtt_logging() -> bool:
 | 
			
		||||
    if CONF_TOPIC not in log_topic:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    return log_topic[CONF_LEVEL] != "NONE"
 | 
			
		||||
    return log_topic.get(CONF_LEVEL, None) != "NONE"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_mqtt() -> bool:
 | 
			
		||||
@@ -232,6 +242,8 @@ def has_ota() -> bool:
 | 
			
		||||
 | 
			
		||||
def has_mqtt_ip_lookup() -> bool:
 | 
			
		||||
    """Check if MQTT is available and IP lookup is supported."""
 | 
			
		||||
    from esphome.components.mqtt import CONF_DISCOVER_IP
 | 
			
		||||
 | 
			
		||||
    if CONF_MQTT not in CORE.config:
 | 
			
		||||
        return False
 | 
			
		||||
    # Default Enabled
 | 
			
		||||
@@ -445,7 +457,7 @@ def upload_using_esptool(
 | 
			
		||||
            "detect",
 | 
			
		||||
        ]
 | 
			
		||||
        for img in flash_images:
 | 
			
		||||
            cmd += [img.offset, img.path]
 | 
			
		||||
            cmd += [img.offset, str(img.path)]
 | 
			
		||||
 | 
			
		||||
        if os.environ.get("ESPHOME_USE_SUBPROCESS") is None:
 | 
			
		||||
            import esptool
 | 
			
		||||
@@ -531,7 +543,10 @@ def upload_program(
 | 
			
		||||
 | 
			
		||||
    remote_port = int(ota_conf[CONF_PORT])
 | 
			
		||||
    password = ota_conf.get(CONF_PASSWORD, "")
 | 
			
		||||
    binary = args.file if getattr(args, "file", None) is not None else CORE.firmware_bin
 | 
			
		||||
    if getattr(args, "file", None) is not None:
 | 
			
		||||
        binary = Path(args.file)
 | 
			
		||||
    else:
 | 
			
		||||
        binary = CORE.firmware_bin
 | 
			
		||||
 | 
			
		||||
    # MQTT address resolution
 | 
			
		||||
    if get_port_type(host) in ("MQTT", "MQTTIP"):
 | 
			
		||||
@@ -598,7 +613,7 @@ def clean_mqtt(config: ConfigType, args: ArgsProtocol) -> int | None:
 | 
			
		||||
def command_wizard(args: ArgsProtocol) -> int | None:
 | 
			
		||||
    from esphome import wizard
 | 
			
		||||
 | 
			
		||||
    return wizard.wizard(args.configuration)
 | 
			
		||||
    return wizard.wizard(Path(args.configuration))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
@@ -720,6 +735,16 @@ def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    return clean_mqtt(config, args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_clean_all(args: ArgsProtocol) -> int | None:
 | 
			
		||||
    try:
 | 
			
		||||
        writer.clean_all(args.configuration)
 | 
			
		||||
    except OSError as err:
 | 
			
		||||
        _LOGGER.error("Error cleaning all files: %s", err)
 | 
			
		||||
        return 1
 | 
			
		||||
    _LOGGER.info("Done!")
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    from esphome import mqtt
 | 
			
		||||
 | 
			
		||||
@@ -761,7 +786,7 @@ def command_update_all(args: ArgsProtocol) -> int | None:
 | 
			
		||||
        safe_print(f"{half_line}{middle_text}{half_line}")
 | 
			
		||||
 | 
			
		||||
    for f in files:
 | 
			
		||||
        safe_print(f"Updating {color(AnsiFore.CYAN, f)}")
 | 
			
		||||
        safe_print(f"Updating {color(AnsiFore.CYAN, str(f))}")
 | 
			
		||||
        safe_print("-" * twidth)
 | 
			
		||||
        safe_print()
 | 
			
		||||
        if CORE.dashboard:
 | 
			
		||||
@@ -773,10 +798,10 @@ def command_update_all(args: ArgsProtocol) -> int | None:
 | 
			
		||||
                "esphome", "run", f, "--no-logs", "--device", "OTA"
 | 
			
		||||
            )
 | 
			
		||||
        if rc == 0:
 | 
			
		||||
            print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {f}")
 | 
			
		||||
            print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}")
 | 
			
		||||
            success[f] = True
 | 
			
		||||
        else:
 | 
			
		||||
            print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {f}")
 | 
			
		||||
            print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {str(f)}")
 | 
			
		||||
            success[f] = False
 | 
			
		||||
 | 
			
		||||
        safe_print()
 | 
			
		||||
@@ -787,9 +812,9 @@ def command_update_all(args: ArgsProtocol) -> int | None:
 | 
			
		||||
    failed = 0
 | 
			
		||||
    for f in files:
 | 
			
		||||
        if success[f]:
 | 
			
		||||
            safe_print(f"  - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}")
 | 
			
		||||
            safe_print(f"  - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}")
 | 
			
		||||
        else:
 | 
			
		||||
            safe_print(f"  - {f}: {color(AnsiFore.BOLD_RED, 'FAILED')}")
 | 
			
		||||
            safe_print(f"  - {str(f)}: {color(AnsiFore.BOLD_RED, 'FAILED')}")
 | 
			
		||||
            failed += 1
 | 
			
		||||
    return failed
 | 
			
		||||
 | 
			
		||||
@@ -811,7 +836,8 @@ def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    for c in args.name:
 | 
			
		||||
    new_name = args.name
 | 
			
		||||
    for c in new_name:
 | 
			
		||||
        if c not in ALLOWED_NAME_CHARS:
 | 
			
		||||
            print(
 | 
			
		||||
                color(
 | 
			
		||||
@@ -822,8 +848,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
            )
 | 
			
		||||
            return 1
 | 
			
		||||
    # Load existing yaml file
 | 
			
		||||
    with open(CORE.config_path, mode="r+", encoding="utf-8") as raw_file:
 | 
			
		||||
        raw_contents = raw_file.read()
 | 
			
		||||
    raw_contents = CORE.config_path.read_text(encoding="utf-8")
 | 
			
		||||
 | 
			
		||||
    yaml = yaml_util.load_yaml(CORE.config_path)
 | 
			
		||||
    if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
 | 
			
		||||
@@ -838,7 +863,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    if match is None:
 | 
			
		||||
        new_raw = re.sub(
 | 
			
		||||
            rf"name:\s+[\"']?{old_name}[\"']?",
 | 
			
		||||
            f'name: "{args.name}"',
 | 
			
		||||
            f'name: "{new_name}"',
 | 
			
		||||
            raw_contents,
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
@@ -858,29 +883,28 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
 | 
			
		||||
        new_raw = re.sub(
 | 
			
		||||
            rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?",
 | 
			
		||||
            f'\\1: "{args.name}"',
 | 
			
		||||
            f'\\1: "{new_name}"',
 | 
			
		||||
            raw_contents,
 | 
			
		||||
            flags=re.MULTILINE,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    new_path = os.path.join(CORE.config_dir, args.name + ".yaml")
 | 
			
		||||
    new_path: Path = CORE.config_dir / (new_name + ".yaml")
 | 
			
		||||
    print(
 | 
			
		||||
        f"Updating {color(AnsiFore.CYAN, CORE.config_path)} to {color(AnsiFore.CYAN, new_path)}"
 | 
			
		||||
        f"Updating {color(AnsiFore.CYAN, str(CORE.config_path))} to {color(AnsiFore.CYAN, str(new_path))}"
 | 
			
		||||
    )
 | 
			
		||||
    print()
 | 
			
		||||
 | 
			
		||||
    with open(new_path, mode="w", encoding="utf-8") as new_file:
 | 
			
		||||
        new_file.write(new_raw)
 | 
			
		||||
    new_path.write_text(new_raw, encoding="utf-8")
 | 
			
		||||
 | 
			
		||||
    rc = run_external_process("esphome", "config", new_path)
 | 
			
		||||
    rc = run_external_process("esphome", "config", str(new_path))
 | 
			
		||||
    if rc != 0:
 | 
			
		||||
        print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes."))
 | 
			
		||||
        os.remove(new_path)
 | 
			
		||||
        new_path.unlink()
 | 
			
		||||
        return 1
 | 
			
		||||
 | 
			
		||||
    cli_args = [
 | 
			
		||||
        "run",
 | 
			
		||||
        new_path,
 | 
			
		||||
        str(new_path),
 | 
			
		||||
        "--no-logs",
 | 
			
		||||
        "--device",
 | 
			
		||||
        CORE.address,
 | 
			
		||||
@@ -894,11 +918,11 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        rc = 1
 | 
			
		||||
    if rc != 0:
 | 
			
		||||
        os.remove(new_path)
 | 
			
		||||
        new_path.unlink()
 | 
			
		||||
        return 1
 | 
			
		||||
 | 
			
		||||
    if CORE.config_path != new_path:
 | 
			
		||||
        os.remove(CORE.config_path)
 | 
			
		||||
        CORE.config_path.unlink()
 | 
			
		||||
 | 
			
		||||
    print(color(AnsiFore.BOLD_GREEN, "SUCCESS"))
 | 
			
		||||
    print()
 | 
			
		||||
@@ -911,6 +935,7 @@ PRE_CONFIG_ACTIONS = {
 | 
			
		||||
    "dashboard": command_dashboard,
 | 
			
		||||
    "vscode": command_vscode,
 | 
			
		||||
    "update-all": command_update_all,
 | 
			
		||||
    "clean-all": command_clean_all,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
POST_CONFIG_ACTIONS = {
 | 
			
		||||
@@ -919,9 +944,9 @@ POST_CONFIG_ACTIONS = {
 | 
			
		||||
    "upload": command_upload,
 | 
			
		||||
    "logs": command_logs,
 | 
			
		||||
    "run": command_run,
 | 
			
		||||
    "clean": command_clean,
 | 
			
		||||
    "clean-mqtt": command_clean_mqtt,
 | 
			
		||||
    "mqtt-fingerprint": command_mqtt_fingerprint,
 | 
			
		||||
    "clean": command_clean,
 | 
			
		||||
    "idedata": command_idedata,
 | 
			
		||||
    "rename": command_rename,
 | 
			
		||||
    "discover": command_discover,
 | 
			
		||||
@@ -965,6 +990,24 @@ def parse_args(argv):
 | 
			
		||||
        help="Add a substitution",
 | 
			
		||||
        metavar=("key", "value"),
 | 
			
		||||
    )
 | 
			
		||||
    options_parser.add_argument(
 | 
			
		||||
        "--mdns-address-cache",
 | 
			
		||||
        help="mDNS address cache mapping in format 'hostname=ip1,ip2'",
 | 
			
		||||
        action="append",
 | 
			
		||||
        default=[],
 | 
			
		||||
    )
 | 
			
		||||
    options_parser.add_argument(
 | 
			
		||||
        "--dns-address-cache",
 | 
			
		||||
        help="DNS address cache mapping in format 'hostname=ip1,ip2'",
 | 
			
		||||
        action="append",
 | 
			
		||||
        default=[],
 | 
			
		||||
    )
 | 
			
		||||
    options_parser.add_argument(
 | 
			
		||||
        "--testing-mode",
 | 
			
		||||
        help="Enable testing mode (disables validation checks for grouped component testing)",
 | 
			
		||||
        action="store_true",
 | 
			
		||||
        default=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser = argparse.ArgumentParser(
 | 
			
		||||
        description=f"ESPHome {const.__version__}", parents=[options_parser]
 | 
			
		||||
@@ -1122,6 +1165,13 @@ def parse_args(argv):
 | 
			
		||||
        "configuration", help="Your YAML configuration file(s).", nargs="+"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser_clean_all = subparsers.add_parser(
 | 
			
		||||
        "clean-all", help="Clean all build and platform files."
 | 
			
		||||
    )
 | 
			
		||||
    parser_clean_all.add_argument(
 | 
			
		||||
        "configuration", help="Your YAML configuration directory.", nargs="*"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser_dashboard = subparsers.add_parser(
 | 
			
		||||
        "dashboard", help="Create a simple web server for a dashboard."
 | 
			
		||||
    )
 | 
			
		||||
@@ -1168,7 +1218,7 @@ def parse_args(argv):
 | 
			
		||||
 | 
			
		||||
    parser_update = subparsers.add_parser("update-all")
 | 
			
		||||
    parser_update.add_argument(
 | 
			
		||||
        "configuration", help="Your YAML configuration file directories.", nargs="+"
 | 
			
		||||
        "configuration", help="Your YAML configuration file or directory.", nargs="+"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser_idedata = subparsers.add_parser("idedata")
 | 
			
		||||
@@ -1212,9 +1262,16 @@ def parse_args(argv):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_esphome(argv):
 | 
			
		||||
    from esphome.address_cache import AddressCache
 | 
			
		||||
 | 
			
		||||
    args = parse_args(argv)
 | 
			
		||||
    CORE.dashboard = args.dashboard
 | 
			
		||||
    CORE.testing_mode = args.testing_mode
 | 
			
		||||
 | 
			
		||||
    # Create address cache from command-line arguments
 | 
			
		||||
    CORE.address_cache = AddressCache.from_cli_args(
 | 
			
		||||
        args.mdns_address_cache, args.dns_address_cache
 | 
			
		||||
    )
 | 
			
		||||
    # Override log level if verbose is set
 | 
			
		||||
    if args.verbose:
 | 
			
		||||
        args.log_level = "DEBUG"
 | 
			
		||||
@@ -1237,14 +1294,20 @@ def run_esphome(argv):
 | 
			
		||||
    _LOGGER.info("ESPHome %s", const.__version__)
 | 
			
		||||
 | 
			
		||||
    for conf_path in args.configuration:
 | 
			
		||||
        if any(os.path.basename(conf_path) == x for x in SECRETS_FILES):
 | 
			
		||||
        conf_path = Path(conf_path)
 | 
			
		||||
        if any(conf_path.name == x for x in SECRETS_FILES):
 | 
			
		||||
            _LOGGER.warning("Skipping secrets file %s", conf_path)
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        CORE.config_path = conf_path
 | 
			
		||||
        CORE.dashboard = args.dashboard
 | 
			
		||||
 | 
			
		||||
        config = read_config(dict(args.substitution) if args.substitution else {})
 | 
			
		||||
        # For logs command, skip updating external components
 | 
			
		||||
        skip_external = args.command == "logs"
 | 
			
		||||
        config = read_config(
 | 
			
		||||
            dict(args.substitution) if args.substitution else {},
 | 
			
		||||
            skip_external_update=skip_external,
 | 
			
		||||
        )
 | 
			
		||||
        if config is None:
 | 
			
		||||
            return 2
 | 
			
		||||
        CORE.config = config
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										142
									
								
								esphome/address_cache.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								esphome/address_cache.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
"""Address cache for DNS and mDNS lookups."""
 | 
			
		||||
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from collections.abc import Iterable
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def normalize_hostname(hostname: str) -> str:
 | 
			
		||||
    """Normalize hostname for cache lookups.
 | 
			
		||||
 | 
			
		||||
    Removes trailing dots and converts to lowercase.
 | 
			
		||||
    """
 | 
			
		||||
    return hostname.rstrip(".").lower()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AddressCache:
 | 
			
		||||
    """Cache for DNS and mDNS address lookups.
 | 
			
		||||
 | 
			
		||||
    This cache stores pre-resolved addresses from command-line arguments
 | 
			
		||||
    to avoid slow DNS/mDNS lookups during builds.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        mdns_cache: dict[str, list[str]] | None = None,
 | 
			
		||||
        dns_cache: dict[str, list[str]] | None = None,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        """Initialize the address cache.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            mdns_cache: Pre-populated mDNS addresses (hostname -> IPs)
 | 
			
		||||
            dns_cache: Pre-populated DNS addresses (hostname -> IPs)
 | 
			
		||||
        """
 | 
			
		||||
        self.mdns_cache = mdns_cache or {}
 | 
			
		||||
        self.dns_cache = dns_cache or {}
 | 
			
		||||
 | 
			
		||||
    def _get_cached_addresses(
 | 
			
		||||
        self, hostname: str, cache: dict[str, list[str]], cache_type: str
 | 
			
		||||
    ) -> list[str] | None:
 | 
			
		||||
        """Get cached addresses from a specific cache.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            hostname: The hostname to look up
 | 
			
		||||
            cache: The cache dictionary to check
 | 
			
		||||
            cache_type: Type of cache for logging ("mDNS" or "DNS")
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            List of IP addresses if found in cache, None otherwise
 | 
			
		||||
        """
 | 
			
		||||
        normalized = normalize_hostname(hostname)
 | 
			
		||||
        if addresses := cache.get(normalized):
 | 
			
		||||
            _LOGGER.debug("Using %s cache for %s: %s", cache_type, hostname, addresses)
 | 
			
		||||
            return addresses
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def get_mdns_addresses(self, hostname: str) -> list[str] | None:
 | 
			
		||||
        """Get cached mDNS addresses for a hostname.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            hostname: The hostname to look up (should end with .local)
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            List of IP addresses if found in cache, None otherwise
 | 
			
		||||
        """
 | 
			
		||||
        return self._get_cached_addresses(hostname, self.mdns_cache, "mDNS")
 | 
			
		||||
 | 
			
		||||
    def get_dns_addresses(self, hostname: str) -> list[str] | None:
 | 
			
		||||
        """Get cached DNS addresses for a hostname.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            hostname: The hostname to look up
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            List of IP addresses if found in cache, None otherwise
 | 
			
		||||
        """
 | 
			
		||||
        return self._get_cached_addresses(hostname, self.dns_cache, "DNS")
 | 
			
		||||
 | 
			
		||||
    def get_addresses(self, hostname: str) -> list[str] | None:
 | 
			
		||||
        """Get cached addresses for a hostname.
 | 
			
		||||
 | 
			
		||||
        Checks mDNS cache for .local domains, DNS cache otherwise.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            hostname: The hostname to look up
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            List of IP addresses if found in cache, None otherwise
 | 
			
		||||
        """
 | 
			
		||||
        normalized = normalize_hostname(hostname)
 | 
			
		||||
        if normalized.endswith(".local"):
 | 
			
		||||
            return self.get_mdns_addresses(hostname)
 | 
			
		||||
        return self.get_dns_addresses(hostname)
 | 
			
		||||
 | 
			
		||||
    def has_cache(self) -> bool:
 | 
			
		||||
        """Check if any cache entries exist."""
 | 
			
		||||
        return bool(self.mdns_cache or self.dns_cache)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_cli_args(
 | 
			
		||||
        cls, mdns_args: Iterable[str], dns_args: Iterable[str]
 | 
			
		||||
    ) -> AddressCache:
 | 
			
		||||
        """Create cache from command-line arguments.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            mdns_args: List of mDNS cache entries like ['host=ip1,ip2']
 | 
			
		||||
            dns_args: List of DNS cache entries like ['host=ip1,ip2']
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            Configured AddressCache instance
 | 
			
		||||
        """
 | 
			
		||||
        mdns_cache = cls._parse_cache_args(mdns_args)
 | 
			
		||||
        dns_cache = cls._parse_cache_args(dns_args)
 | 
			
		||||
        return cls(mdns_cache=mdns_cache, dns_cache=dns_cache)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _parse_cache_args(cache_args: Iterable[str]) -> dict[str, list[str]]:
 | 
			
		||||
        """Parse cache arguments into a dictionary.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            cache_args: List of cache mappings like ['host1=ip1,ip2', 'host2=ip3']
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            Dictionary mapping normalized hostnames to list of IP addresses
 | 
			
		||||
        """
 | 
			
		||||
        cache: dict[str, list[str]] = {}
 | 
			
		||||
        for arg in cache_args:
 | 
			
		||||
            if "=" not in arg:
 | 
			
		||||
                _LOGGER.warning(
 | 
			
		||||
                    "Invalid cache format: %s (expected 'hostname=ip1,ip2')", arg
 | 
			
		||||
                )
 | 
			
		||||
                continue
 | 
			
		||||
            hostname, ips = arg.split("=", 1)
 | 
			
		||||
            # Normalize hostname for consistent lookups
 | 
			
		||||
            normalized = normalize_hostname(hostname)
 | 
			
		||||
            cache[normalized] = [ip.strip() for ip in ips.split(",")]
 | 
			
		||||
        return cache
 | 
			
		||||
@@ -15,7 +15,10 @@ from esphome.const import (
 | 
			
		||||
    CONF_TYPE_ID,
 | 
			
		||||
    CONF_UPDATE_INTERVAL,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import ID
 | 
			
		||||
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
 | 
			
		||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
from esphome.util import Registry
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -49,11 +52,11 @@ def maybe_conf(conf, *validators):
 | 
			
		||||
    return validate
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register_action(name, action_type, schema):
 | 
			
		||||
def register_action(name: str, action_type: MockObjClass, schema: cv.Schema):
 | 
			
		||||
    return ACTION_REGISTRY.register(name, action_type, schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register_condition(name, condition_type, schema):
 | 
			
		||||
def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema):
 | 
			
		||||
    return CONDITION_REGISTRY.register(name, condition_type, schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -164,43 +167,78 @@ XorCondition = cg.esphome_ns.class_("XorCondition", Condition)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_condition("and", AndCondition, validate_condition_list)
 | 
			
		||||
async def and_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
async def and_condition_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    condition_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    conditions = await build_condition_list(config, template_arg, args)
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg, conditions)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_condition("or", OrCondition, validate_condition_list)
 | 
			
		||||
async def or_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
async def or_condition_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    condition_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    conditions = await build_condition_list(config, template_arg, args)
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg, conditions)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_condition("all", AndCondition, validate_condition_list)
 | 
			
		||||
async def all_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
async def all_condition_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    condition_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    conditions = await build_condition_list(config, template_arg, args)
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg, conditions)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_condition("any", OrCondition, validate_condition_list)
 | 
			
		||||
async def any_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
async def any_condition_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    condition_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    conditions = await build_condition_list(config, template_arg, args)
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg, conditions)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_condition("not", NotCondition, validate_potentially_and_condition)
 | 
			
		||||
async def not_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
async def not_condition_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    condition_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    condition = await build_condition(config, template_arg, args)
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg, condition)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_condition("xor", XorCondition, validate_condition_list)
 | 
			
		||||
async def xor_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
async def xor_condition_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    condition_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    conditions = await build_condition_list(config, template_arg, args)
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg, conditions)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_condition("lambda", LambdaCondition, cv.returning_lambda)
 | 
			
		||||
async def lambda_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
async def lambda_condition_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    condition_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    lambda_ = await cg.process_lambda(config, args, return_type=bool)
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg, lambda_)
 | 
			
		||||
 | 
			
		||||
@@ -217,7 +255,12 @@ async def lambda_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
)
 | 
			
		||||
async def for_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
async def for_condition_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    condition_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    condition = await build_condition(
 | 
			
		||||
        config[CONF_CONDITION], cg.TemplateArguments(), []
 | 
			
		||||
    )
 | 
			
		||||
@@ -231,7 +274,12 @@ async def for_condition_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
@register_action(
 | 
			
		||||
    "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
 | 
			
		||||
)
 | 
			
		||||
async def delay_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
async def delay_action_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    action_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg)
 | 
			
		||||
    await cg.register_component(var, {})
 | 
			
		||||
    template_ = await cg.templatable(config, args, cg.uint32)
 | 
			
		||||
@@ -256,10 +304,15 @@ async def delay_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
        cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL),
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def if_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
async def if_action_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    action_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION))
 | 
			
		||||
    conditions = await build_condition(config[cond_conf], template_arg, args)
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, conditions)
 | 
			
		||||
    condition = await build_condition(config[cond_conf], template_arg, args)
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, condition)
 | 
			
		||||
    if CONF_THEN in config:
 | 
			
		||||
        actions = await build_action_list(config[CONF_THEN], template_arg, args)
 | 
			
		||||
        cg.add(var.add_then(actions))
 | 
			
		||||
@@ -279,9 +332,14 @@ async def if_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def while_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    conditions = await build_condition(config[CONF_CONDITION], template_arg, args)
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, conditions)
 | 
			
		||||
async def while_action_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    action_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    condition = await build_condition(config[CONF_CONDITION], template_arg, args)
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, condition)
 | 
			
		||||
    actions = await build_action_list(config[CONF_THEN], template_arg, args)
 | 
			
		||||
    cg.add(var.add_then(actions))
 | 
			
		||||
    return var
 | 
			
		||||
@@ -297,7 +355,12 @@ async def while_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def repeat_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
async def repeat_action_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    action_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg)
 | 
			
		||||
    count_template = await cg.templatable(config[CONF_COUNT], args, cg.uint32)
 | 
			
		||||
    cg.add(var.set_count(count_template))
 | 
			
		||||
@@ -320,9 +383,14 @@ _validate_wait_until = cv.maybe_simple_value(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action("wait_until", WaitUntilAction, _validate_wait_until)
 | 
			
		||||
async def wait_until_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    conditions = await build_condition(config[CONF_CONDITION], template_arg, args)
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, conditions)
 | 
			
		||||
async def wait_until_action_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    action_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    condition = await build_condition(config[CONF_CONDITION], template_arg, args)
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, condition)
 | 
			
		||||
    if CONF_TIMEOUT in config:
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_TIMEOUT], args, cg.uint32)
 | 
			
		||||
        cg.add(var.set_timeout_value(template_))
 | 
			
		||||
@@ -331,7 +399,12 @@ async def wait_until_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action("lambda", LambdaAction, cv.lambda_)
 | 
			
		||||
async def lambda_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
async def lambda_action_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    action_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
 | 
			
		||||
    return cg.new_Pvariable(action_id, template_arg, lambda_)
 | 
			
		||||
 | 
			
		||||
@@ -345,7 +418,12 @@ async def lambda_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def component_update_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
async def component_update_action_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    action_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    comp = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    return cg.new_Pvariable(action_id, template_arg, comp)
 | 
			
		||||
 | 
			
		||||
@@ -359,7 +437,12 @@ async def component_update_action_to_code(config, action_id, template_arg, args)
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def component_suspend_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
async def component_suspend_action_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    action_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    comp = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    return cg.new_Pvariable(action_id, template_arg, comp)
 | 
			
		||||
 | 
			
		||||
@@ -376,7 +459,12 @@ async def component_suspend_action_to_code(config, action_id, template_arg, args
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def component_resume_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
async def component_resume_action_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    action_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    comp = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, comp)
 | 
			
		||||
    if CONF_UPDATE_INTERVAL in config:
 | 
			
		||||
@@ -385,7 +473,9 @@ async def component_resume_action_to_code(config, action_id, template_arg, args)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def build_action(full_config, template_arg, args):
 | 
			
		||||
async def build_action(
 | 
			
		||||
    full_config: ConfigType, template_arg: cg.TemplateArguments, args: TemplateArgsType
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    registry_entry, config = cg.extract_registry_entry_config(
 | 
			
		||||
        ACTION_REGISTRY, full_config
 | 
			
		||||
    )
 | 
			
		||||
@@ -394,15 +484,19 @@ async def build_action(full_config, template_arg, args):
 | 
			
		||||
    return await builder(config, action_id, template_arg, args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def build_action_list(config, templ, arg_type):
 | 
			
		||||
    actions = []
 | 
			
		||||
async def build_action_list(
 | 
			
		||||
    config: list[ConfigType], templ: cg.TemplateArguments, arg_type: TemplateArgsType
 | 
			
		||||
) -> list[MockObj]:
 | 
			
		||||
    actions: list[MockObj] = []
 | 
			
		||||
    for conf in config:
 | 
			
		||||
        action = await build_action(conf, templ, arg_type)
 | 
			
		||||
        actions.append(action)
 | 
			
		||||
    return actions
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def build_condition(full_config, template_arg, args):
 | 
			
		||||
async def build_condition(
 | 
			
		||||
    full_config: ConfigType, template_arg: cg.TemplateArguments, args: TemplateArgsType
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    registry_entry, config = cg.extract_registry_entry_config(
 | 
			
		||||
        CONDITION_REGISTRY, full_config
 | 
			
		||||
    )
 | 
			
		||||
@@ -411,15 +505,19 @@ async def build_condition(full_config, template_arg, args):
 | 
			
		||||
    return await builder(config, action_id, template_arg, args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def build_condition_list(config, templ, args):
 | 
			
		||||
    conditions = []
 | 
			
		||||
async def build_condition_list(
 | 
			
		||||
    config: ConfigType, templ: cg.TemplateArguments, args: TemplateArgsType
 | 
			
		||||
) -> list[MockObj]:
 | 
			
		||||
    conditions: list[MockObj] = []
 | 
			
		||||
    for conf in config:
 | 
			
		||||
        condition = await build_condition(conf, templ, args)
 | 
			
		||||
        conditions.append(condition)
 | 
			
		||||
    return conditions
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def build_automation(trigger, args, config):
 | 
			
		||||
async def build_automation(
 | 
			
		||||
    trigger: MockObj, args: TemplateArgsType, config: ConfigType
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    arg_types = [arg[0] for arg in args]
 | 
			
		||||
    templ = cg.TemplateArguments(*arg_types)
 | 
			
		||||
    obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, trigger)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,3 @@
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from esphome.const import __version__
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.helpers import mkdir_p, read_file, write_file_if_changed
 | 
			
		||||
@@ -63,7 +61,7 @@ def write_ini(content):
 | 
			
		||||
    update_storage_json()
 | 
			
		||||
    path = CORE.relative_build_path("platformio.ini")
 | 
			
		||||
 | 
			
		||||
    if os.path.isfile(path):
 | 
			
		||||
    if path.is_file():
 | 
			
		||||
        text = read_file(path)
 | 
			
		||||
        content_format = find_begin_end(
 | 
			
		||||
            text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ from esphome.cpp_generator import (  # noqa: F401
 | 
			
		||||
    ArrayInitializer,
 | 
			
		||||
    Expression,
 | 
			
		||||
    LineComment,
 | 
			
		||||
    LogStringLiteral,
 | 
			
		||||
    MockObj,
 | 
			
		||||
    MockObjClass,
 | 
			
		||||
    Pvariable,
 | 
			
		||||
 
 | 
			
		||||
@@ -26,12 +26,12 @@ uint32_t Animation::get_animation_frame_count() const { return this->animation_f
 | 
			
		||||
int Animation::get_current_frame() const { return this->current_frame_; }
 | 
			
		||||
void Animation::next_frame() {
 | 
			
		||||
  this->current_frame_++;
 | 
			
		||||
  if (loop_count_ && this->current_frame_ == loop_end_frame_ &&
 | 
			
		||||
  if (loop_count_ && static_cast<uint32_t>(this->current_frame_) == loop_end_frame_ &&
 | 
			
		||||
      (this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) {
 | 
			
		||||
    this->current_frame_ = loop_start_frame_;
 | 
			
		||||
    this->loop_current_iteration_++;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->current_frame_ >= animation_frame_count_) {
 | 
			
		||||
  if (static_cast<uint32_t>(this->current_frame_) >= animation_frame_count_) {
 | 
			
		||||
    this->loop_current_iteration_ = 1;
 | 
			
		||||
    this->current_frame_ = 0;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import base64
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome.automation import Condition
 | 
			
		||||
@@ -8,34 +9,59 @@ import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ACTION,
 | 
			
		||||
    CONF_ACTIONS,
 | 
			
		||||
    CONF_CAPTURE_RESPONSE,
 | 
			
		||||
    CONF_DATA,
 | 
			
		||||
    CONF_DATA_TEMPLATE,
 | 
			
		||||
    CONF_EVENT,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_KEY,
 | 
			
		||||
    CONF_MAX_CONNECTIONS,
 | 
			
		||||
    CONF_ON_CLIENT_CONNECTED,
 | 
			
		||||
    CONF_ON_CLIENT_DISCONNECTED,
 | 
			
		||||
    CONF_ON_ERROR,
 | 
			
		||||
    CONF_ON_SUCCESS,
 | 
			
		||||
    CONF_PASSWORD,
 | 
			
		||||
    CONF_PORT,
 | 
			
		||||
    CONF_REBOOT_TIMEOUT,
 | 
			
		||||
    CONF_RESPONSE_TEMPLATE,
 | 
			
		||||
    CONF_SERVICE,
 | 
			
		||||
    CONF_SERVICES,
 | 
			
		||||
    CONF_TAG,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    CONF_VARIABLES,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.cpp_generator import TemplateArgsType
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
DOMAIN = "api"
 | 
			
		||||
DEPENDENCIES = ["network"]
 | 
			
		||||
AUTO_LOAD = ["socket"]
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def AUTO_LOAD(config: ConfigType) -> list[str]:
 | 
			
		||||
    """Conditionally auto-load json only when capture_response is used."""
 | 
			
		||||
    base = ["socket"]
 | 
			
		||||
 | 
			
		||||
    # Check if any homeassistant.action/homeassistant.service has capture_response: true
 | 
			
		||||
    # This flag is set during config validation in _validate_response_config
 | 
			
		||||
    if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False):
 | 
			
		||||
        return base + ["json"]
 | 
			
		||||
 | 
			
		||||
    return base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
api_ns = cg.esphome_ns.namespace("api")
 | 
			
		||||
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
 | 
			
		||||
HomeAssistantServiceCallAction = api_ns.class_(
 | 
			
		||||
    "HomeAssistantServiceCallAction", automation.Action
 | 
			
		||||
)
 | 
			
		||||
ActionResponse = api_ns.class_("ActionResponse")
 | 
			
		||||
HomeAssistantActionResponseTrigger = api_ns.class_(
 | 
			
		||||
    "HomeAssistantActionResponseTrigger", automation.Trigger
 | 
			
		||||
)
 | 
			
		||||
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
 | 
			
		||||
 | 
			
		||||
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
 | 
			
		||||
@@ -55,6 +81,8 @@ CONF_BATCH_DELAY = "batch_delay"
 | 
			
		||||
CONF_CUSTOM_SERVICES = "custom_services"
 | 
			
		||||
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
 | 
			
		||||
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
 | 
			
		||||
CONF_LISTEN_BACKLOG = "listen_backlog"
 | 
			
		||||
CONF_MAX_SEND_QUEUE = "max_send_queue"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_encryption_key(value):
 | 
			
		||||
@@ -101,6 +129,32 @@ def _encryption_schema(config):
 | 
			
		||||
    return ENCRYPTION_SCHEMA(config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _validate_api_config(config: ConfigType) -> ConfigType:
 | 
			
		||||
    """Validate API configuration with mutual exclusivity check and deprecation warning."""
 | 
			
		||||
    # Check if both password and encryption are configured
 | 
			
		||||
    has_password = CONF_PASSWORD in config and config[CONF_PASSWORD]
 | 
			
		||||
    has_encryption = CONF_ENCRYPTION in config
 | 
			
		||||
 | 
			
		||||
    if has_password and has_encryption:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "The 'password' and 'encryption' options are mutually exclusive. "
 | 
			
		||||
            "The API client only supports one authentication method at a time. "
 | 
			
		||||
            "Please remove one of them. "
 | 
			
		||||
            "Note: 'password' authentication is deprecated and will be removed in version 2026.1.0. "
 | 
			
		||||
            "We strongly recommend using 'encryption' instead for better security."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Warn about password deprecation
 | 
			
		||||
    if has_password:
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "API 'password' authentication has been deprecated since May 2022 and will be removed in version 2026.1.0. "
 | 
			
		||||
            "Please migrate to the 'encryption' configuration. "
 | 
			
		||||
            "See https://esphome.io/components/api.html#configuration-variables"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
@@ -128,9 +182,46 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            cv.Optional(CONF_ON_CLIENT_DISCONNECTED): automation.validate_automation(
 | 
			
		||||
                single=True
 | 
			
		||||
            ),
 | 
			
		||||
            # Connection limits to prevent memory exhaustion on resource-constrained devices
 | 
			
		||||
            # Each connection uses ~500-1000 bytes of RAM plus system resources
 | 
			
		||||
            # Platform defaults based on available RAM and network stack implementation:
 | 
			
		||||
            cv.SplitDefault(
 | 
			
		||||
                CONF_LISTEN_BACKLOG,
 | 
			
		||||
                esp8266=1,  # Limited RAM (~40KB free), LWIP raw sockets
 | 
			
		||||
                esp32=4,  # More RAM (520KB), BSD sockets
 | 
			
		||||
                rp2040=1,  # Limited RAM (264KB), LWIP raw sockets like ESP8266
 | 
			
		||||
                bk72xx=4,  # Moderate RAM, BSD-style sockets
 | 
			
		||||
                rtl87xx=4,  # Moderate RAM, BSD-style sockets
 | 
			
		||||
                host=4,  # Abundant resources
 | 
			
		||||
                ln882x=4,  # Moderate RAM
 | 
			
		||||
            ): cv.int_range(min=1, max=10),
 | 
			
		||||
            cv.SplitDefault(
 | 
			
		||||
                CONF_MAX_CONNECTIONS,
 | 
			
		||||
                esp8266=4,  # ~40KB free RAM, each connection uses ~500-1000 bytes
 | 
			
		||||
                esp32=8,  # 520KB RAM available
 | 
			
		||||
                rp2040=4,  # 264KB RAM but LWIP constraints
 | 
			
		||||
                bk72xx=8,  # Moderate RAM
 | 
			
		||||
                rtl87xx=8,  # Moderate RAM
 | 
			
		||||
                host=8,  # Abundant resources
 | 
			
		||||
                ln882x=8,  # Moderate RAM
 | 
			
		||||
            ): cv.int_range(min=1, max=20),
 | 
			
		||||
            # Maximum queued send buffers per connection before dropping connection
 | 
			
		||||
            # Each buffer uses ~8-12 bytes overhead plus actual message size
 | 
			
		||||
            # Platform defaults based on available RAM and typical message rates:
 | 
			
		||||
            cv.SplitDefault(
 | 
			
		||||
                CONF_MAX_SEND_QUEUE,
 | 
			
		||||
                esp8266=5,  # Limited RAM, need to fail fast
 | 
			
		||||
                esp32=8,  # More RAM, can buffer more
 | 
			
		||||
                rp2040=5,  # Limited RAM
 | 
			
		||||
                bk72xx=8,  # Moderate RAM
 | 
			
		||||
                rtl87xx=8,  # Moderate RAM
 | 
			
		||||
                host=16,  # Abundant resources
 | 
			
		||||
                ln882x=8,  # Moderate RAM
 | 
			
		||||
            ): cv.int_range(min=1, max=64),
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
 | 
			
		||||
    _validate_api_config,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -145,6 +236,11 @@ async def to_code(config):
 | 
			
		||||
        cg.add(var.set_password(config[CONF_PASSWORD]))
 | 
			
		||||
    cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
 | 
			
		||||
    cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
 | 
			
		||||
    if CONF_LISTEN_BACKLOG in config:
 | 
			
		||||
        cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
 | 
			
		||||
    if CONF_MAX_CONNECTIONS in config:
 | 
			
		||||
        cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
 | 
			
		||||
    cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
 | 
			
		||||
 | 
			
		||||
    # Set USE_API_SERVICES if any services are enabled
 | 
			
		||||
    if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
 | 
			
		||||
@@ -193,6 +289,7 @@ async def to_code(config):
 | 
			
		||||
        if key := encryption_config.get(CONF_KEY):
 | 
			
		||||
            decoded = base64.b64decode(key)
 | 
			
		||||
            cg.add(var.set_noise_psk(list(decoded)))
 | 
			
		||||
            cg.add_define("USE_API_NOISE_PSK_FROM_YAML")
 | 
			
		||||
        else:
 | 
			
		||||
            # No key provided, but encryption desired
 | 
			
		||||
            # This will allow a plaintext client to provide a noise key,
 | 
			
		||||
@@ -212,6 +309,29 @@ async def to_code(config):
 | 
			
		||||
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _validate_response_config(config: ConfigType) -> ConfigType:
 | 
			
		||||
    # Validate dependencies:
 | 
			
		||||
    # - response_template requires capture_response: true
 | 
			
		||||
    # - capture_response: true requires on_success
 | 
			
		||||
    if CONF_RESPONSE_TEMPLATE in config and not config[CONF_CAPTURE_RESPONSE]:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_CAPTURE_RESPONSE}: true` to be set.",
 | 
			
		||||
            path=[CONF_RESPONSE_TEMPLATE],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if config[CONF_CAPTURE_RESPONSE] and CONF_ON_SUCCESS not in config:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"`{CONF_CAPTURE_RESPONSE}: true` requires `{CONF_ON_SUCCESS}` to be set.",
 | 
			
		||||
            path=[CONF_CAPTURE_RESPONSE],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Track if any action uses capture_response for AUTO_LOAD
 | 
			
		||||
    if config[CONF_CAPTURE_RESPONSE]:
 | 
			
		||||
        CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
 | 
			
		||||
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
@@ -227,10 +347,15 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
 | 
			
		||||
            cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
 | 
			
		||||
                {cv.string: cv.returning_lambda}
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string),
 | 
			
		||||
            cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_ON_SUCCESS): automation.validate_automation(single=True),
 | 
			
		||||
            cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
    cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
 | 
			
		||||
    cv.rename_key(CONF_SERVICE, CONF_ACTION),
 | 
			
		||||
    _validate_response_config,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -244,7 +369,12 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
 | 
			
		||||
    HomeAssistantServiceCallAction,
 | 
			
		||||
    HOMEASSISTANT_ACTION_ACTION_SCHEMA,
 | 
			
		||||
)
 | 
			
		||||
async def homeassistant_service_to_code(config, action_id, template_arg, args):
 | 
			
		||||
async def homeassistant_service_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    action_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
):
 | 
			
		||||
    cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
 | 
			
		||||
    serv = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, serv, False)
 | 
			
		||||
@@ -259,6 +389,40 @@ async def homeassistant_service_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    for key, value in config[CONF_VARIABLES].items():
 | 
			
		||||
        templ = await cg.templatable(value, args, None)
 | 
			
		||||
        cg.add(var.add_variable(key, templ))
 | 
			
		||||
 | 
			
		||||
    if on_error := config.get(CONF_ON_ERROR):
 | 
			
		||||
        cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
 | 
			
		||||
        cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS")
 | 
			
		||||
        cg.add(var.set_wants_status())
 | 
			
		||||
        await automation.build_automation(
 | 
			
		||||
            var.get_error_trigger(),
 | 
			
		||||
            [(cg.std_string, "error"), *args],
 | 
			
		||||
            on_error,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if on_success := config.get(CONF_ON_SUCCESS):
 | 
			
		||||
        cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
 | 
			
		||||
        cg.add(var.set_wants_status())
 | 
			
		||||
        if config[CONF_CAPTURE_RESPONSE]:
 | 
			
		||||
            cg.add(var.set_wants_response())
 | 
			
		||||
            cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON")
 | 
			
		||||
            await automation.build_automation(
 | 
			
		||||
                var.get_success_trigger_with_response(),
 | 
			
		||||
                [(cg.JsonObjectConst, "response"), *args],
 | 
			
		||||
                on_success,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            if response_template := config.get(CONF_RESPONSE_TEMPLATE):
 | 
			
		||||
                templ = await cg.templatable(response_template, args, cg.std_string)
 | 
			
		||||
                cg.add(var.set_response_template(templ))
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            await automation.build_automation(
 | 
			
		||||
                var.get_success_trigger(),
 | 
			
		||||
                args,
 | 
			
		||||
                on_success,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ service APIConnection {
 | 
			
		||||
    option (needs_setup_connection) = false;
 | 
			
		||||
    option (needs_authentication) = false;
 | 
			
		||||
  }
 | 
			
		||||
  rpc connect (ConnectRequest) returns (ConnectResponse) {
 | 
			
		||||
  rpc authenticate (AuthenticationRequest) returns (AuthenticationResponse) {
 | 
			
		||||
    option (needs_setup_connection) = false;
 | 
			
		||||
    option (needs_authentication) = false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -66,6 +66,9 @@ service APIConnection {
 | 
			
		||||
  rpc voice_assistant_set_configuration(VoiceAssistantSetConfiguration) returns (void) {}
 | 
			
		||||
 | 
			
		||||
  rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {}
 | 
			
		||||
 | 
			
		||||
  rpc zwave_proxy_frame(ZWaveProxyFrame) returns (void) {}
 | 
			
		||||
  rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -99,7 +102,7 @@ message HelloRequest {
 | 
			
		||||
  // For example "Home Assistant"
 | 
			
		||||
  // Not strictly necessary to send but nice for debugging
 | 
			
		||||
  // purposes.
 | 
			
		||||
  string client_info = 1;
 | 
			
		||||
  string client_info = 1 [(pointer_to_buffer) = true];
 | 
			
		||||
  uint32 api_version_major = 2;
 | 
			
		||||
  uint32 api_version_minor = 3;
 | 
			
		||||
}
 | 
			
		||||
@@ -129,21 +132,23 @@ message HelloResponse {
 | 
			
		||||
 | 
			
		||||
// Message sent at the beginning of each connection to authenticate the client
 | 
			
		||||
// Can only be sent by the client and only at the beginning of the connection
 | 
			
		||||
message ConnectRequest {
 | 
			
		||||
message AuthenticationRequest {
 | 
			
		||||
  option (id) = 3;
 | 
			
		||||
  option (source) = SOURCE_CLIENT;
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
  option (ifdef) = "USE_API_PASSWORD";
 | 
			
		||||
 | 
			
		||||
  // The password to log in with
 | 
			
		||||
  string password = 1;
 | 
			
		||||
  string password = 1 [(pointer_to_buffer) = true];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Confirmation of successful connection. After this the connection is available for all traffic.
 | 
			
		||||
// Can only be sent by the server and only at the beginning of the connection
 | 
			
		||||
message ConnectResponse {
 | 
			
		||||
message AuthenticationResponse {
 | 
			
		||||
  option (id) = 4;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
  option (ifdef) = "USE_API_PASSWORD";
 | 
			
		||||
 | 
			
		||||
  bool invalid_password = 1;
 | 
			
		||||
}
 | 
			
		||||
@@ -252,6 +257,10 @@ message DeviceInfoResponse {
 | 
			
		||||
 | 
			
		||||
  // Top-level area info to phase out suggested_area
 | 
			
		||||
  AreaInfo area = 22 [(field_ifdef) = "USE_AREAS"];
 | 
			
		||||
 | 
			
		||||
  // Indicates if Z-Wave proxy support is available and features supported
 | 
			
		||||
  uint32 zwave_proxy_feature_flags = 23 [(field_ifdef) = "USE_ZWAVE_PROXY"];
 | 
			
		||||
  uint32 zwave_home_id = 24 [(field_ifdef) = "USE_ZWAVE_PROXY"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message ListEntitiesRequest {
 | 
			
		||||
@@ -760,7 +769,7 @@ message HomeassistantServiceMap {
 | 
			
		||||
  string value = 2 [(no_zero_copy) = true];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message HomeassistantServiceResponse {
 | 
			
		||||
message HomeassistantActionRequest {
 | 
			
		||||
  option (id) = 35;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
@@ -771,6 +780,22 @@ message HomeassistantServiceResponse {
 | 
			
		||||
  repeated HomeassistantServiceMap data_template = 3;
 | 
			
		||||
  repeated HomeassistantServiceMap variables = 4;
 | 
			
		||||
  bool is_event = 5;
 | 
			
		||||
  uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
 | 
			
		||||
  bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
 | 
			
		||||
  string response_template = 8 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Message sent by Home Assistant to ESPHome with service call response data
 | 
			
		||||
message HomeassistantActionResponse {
 | 
			
		||||
  option (id) = 130;
 | 
			
		||||
  option (source) = SOURCE_CLIENT;
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
  option (ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES";
 | 
			
		||||
 | 
			
		||||
  uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest
 | 
			
		||||
  bool success = 2; // Whether the service call succeeded
 | 
			
		||||
  string error_message = 3; // Error message if success = false
 | 
			
		||||
  bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== IMPORT HOME ASSISTANT STATES ====================
 | 
			
		||||
@@ -815,7 +840,7 @@ message GetTimeResponse {
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
 | 
			
		||||
  fixed32 epoch_seconds = 1;
 | 
			
		||||
  string timezone = 2;
 | 
			
		||||
  string timezone = 2 [(pointer_to_buffer) = true];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== USER-DEFINES SERVICES ====================
 | 
			
		||||
@@ -1456,7 +1481,7 @@ message BluetoothDeviceRequest {
 | 
			
		||||
 | 
			
		||||
  uint64 address = 1;
 | 
			
		||||
  BluetoothDeviceRequestType request_type = 2;
 | 
			
		||||
  bool has_address_type = 3;
 | 
			
		||||
  bool has_address_type = 3;  // Deprecated, should be removed in 2027.8 - https://github.com/esphome/esphome/pull/10318
 | 
			
		||||
  uint32 address_type = 4;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1562,7 +1587,7 @@ message BluetoothGATTWriteRequest {
 | 
			
		||||
  uint32 handle = 2;
 | 
			
		||||
  bool response = 3;
 | 
			
		||||
 | 
			
		||||
  bytes data = 4;
 | 
			
		||||
  bytes data = 4 [(pointer_to_buffer) = true];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message BluetoothGATTReadDescriptorRequest {
 | 
			
		||||
@@ -1582,7 +1607,7 @@ message BluetoothGATTWriteDescriptorRequest {
 | 
			
		||||
  uint64 address = 1;
 | 
			
		||||
  uint32 handle = 2;
 | 
			
		||||
 | 
			
		||||
  bytes data = 3;
 | 
			
		||||
  bytes data = 3 [(pointer_to_buffer) = true];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message BluetoothGATTNotifyRequest {
 | 
			
		||||
@@ -1856,10 +1881,22 @@ message VoiceAssistantWakeWord {
 | 
			
		||||
  repeated string trained_languages = 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message VoiceAssistantExternalWakeWord {
 | 
			
		||||
  string id = 1;
 | 
			
		||||
  string wake_word = 2;
 | 
			
		||||
  repeated string trained_languages = 3;
 | 
			
		||||
  string model_type = 4;
 | 
			
		||||
  uint32 model_size = 5;
 | 
			
		||||
  string model_hash = 6;
 | 
			
		||||
  string url = 7;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message VoiceAssistantConfigurationRequest {
 | 
			
		||||
  option (id) = 121;
 | 
			
		||||
  option (source) = SOURCE_CLIENT;
 | 
			
		||||
  option (ifdef) = "USE_VOICE_ASSISTANT";
 | 
			
		||||
 | 
			
		||||
  repeated VoiceAssistantExternalWakeWord external_wake_words = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message VoiceAssistantConfigurationResponse {
 | 
			
		||||
@@ -2274,3 +2311,28 @@ message UpdateCommandRequest {
 | 
			
		||||
  UpdateCommand command = 2;
 | 
			
		||||
  uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== Z-WAVE ====================
 | 
			
		||||
 | 
			
		||||
message ZWaveProxyFrame {
 | 
			
		||||
  option (id) = 128;
 | 
			
		||||
  option (source) = SOURCE_BOTH;
 | 
			
		||||
  option (ifdef) = "USE_ZWAVE_PROXY";
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
 | 
			
		||||
  bytes data = 1 [(pointer_to_buffer) = true];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum ZWaveProxyRequestType {
 | 
			
		||||
  ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0;
 | 
			
		||||
  ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1;
 | 
			
		||||
  ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2;
 | 
			
		||||
}
 | 
			
		||||
message ZWaveProxyRequest {
 | 
			
		||||
  option (id) = 129;
 | 
			
		||||
  option (source) = SOURCE_BOTH;
 | 
			
		||||
  option (ifdef) = "USE_ZWAVE_PROXY";
 | 
			
		||||
 | 
			
		||||
  ZWaveProxyRequestType type = 1;
 | 
			
		||||
  bytes data = 2 [(pointer_to_buffer) = true];
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,9 +8,9 @@
 | 
			
		||||
#endif
 | 
			
		||||
#include <cerrno>
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <limits>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include "esphome/components/network/util.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
@@ -30,6 +30,9 @@
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
#include "esphome/components/voice_assistant/voice_assistant.h"
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
#include "esphome/components/zwave_proxy/zwave_proxy.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome::api {
 | 
			
		||||
 | 
			
		||||
@@ -113,8 +116,7 @@ void APIConnection::start() {
 | 
			
		||||
 | 
			
		||||
  APIError err = this->helper_->init();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    this->log_warning_(LOG_STR("Helper init failed"), err);
 | 
			
		||||
    this->fatal_error_with_log_(LOG_STR("Helper init failed"), err);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->client_info_.peername = helper_->getpeername();
 | 
			
		||||
@@ -144,8 +146,7 @@ void APIConnection::loop() {
 | 
			
		||||
 | 
			
		||||
  APIError err = this->helper_->loop();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    this->log_socket_operation_failed_(err);
 | 
			
		||||
    this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -160,17 +161,13 @@ void APIConnection::loop() {
 | 
			
		||||
        // No more data available
 | 
			
		||||
        break;
 | 
			
		||||
      } else if (err != APIError::OK) {
 | 
			
		||||
        on_fatal_error();
 | 
			
		||||
        this->log_warning_(LOG_STR("Reading failed"), err);
 | 
			
		||||
        this->fatal_error_with_log_(LOG_STR("Reading failed"), err);
 | 
			
		||||
        return;
 | 
			
		||||
      } else {
 | 
			
		||||
        this->last_traffic_ = now;
 | 
			
		||||
        // read a packet
 | 
			
		||||
        if (buffer.data_len > 0) {
 | 
			
		||||
          this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
 | 
			
		||||
        } else {
 | 
			
		||||
          this->read_message(0, buffer.type, nullptr);
 | 
			
		||||
        }
 | 
			
		||||
        this->read_message(buffer.data_len, buffer.type,
 | 
			
		||||
                           buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr);
 | 
			
		||||
        if (this->flags_.remove)
 | 
			
		||||
          return;
 | 
			
		||||
      }
 | 
			
		||||
@@ -202,7 +199,8 @@ void APIConnection::loop() {
 | 
			
		||||
    // Disconnect if not responded within 2.5*keepalive
 | 
			
		||||
    if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
 | 
			
		||||
      on_fatal_error();
 | 
			
		||||
      ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str());
 | 
			
		||||
      ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->client_info_.name.c_str(),
 | 
			
		||||
               this->client_info_.peername.c_str());
 | 
			
		||||
    }
 | 
			
		||||
  } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) {
 | 
			
		||||
    // Only send ping if we're not disconnecting
 | 
			
		||||
@@ -252,7 +250,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
 | 
			
		||||
  // remote initiated disconnect_client
 | 
			
		||||
  // don't close yet, we still need to send the disconnect response
 | 
			
		||||
  // close will happen on next loop
 | 
			
		||||
  ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str());
 | 
			
		||||
  ESP_LOGD(TAG, "%s (%s) disconnected", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
 | 
			
		||||
  this->flags_.next_close = true;
 | 
			
		||||
  DisconnectResponse resp;
 | 
			
		||||
  return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
 | 
			
		||||
@@ -1075,8 +1073,14 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
 | 
			
		||||
  if (homeassistant::global_homeassistant_time != nullptr) {
 | 
			
		||||
    homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
 | 
			
		||||
#ifdef USE_TIME_TIMEZONE
 | 
			
		||||
    if (!value.timezone.empty() && value.timezone != homeassistant::global_homeassistant_time->get_timezone()) {
 | 
			
		||||
      homeassistant::global_homeassistant_time->set_timezone(value.timezone);
 | 
			
		||||
    if (value.timezone_len > 0) {
 | 
			
		||||
      const std::string ¤t_tz = homeassistant::global_homeassistant_time->get_timezone();
 | 
			
		||||
      // Compare without allocating a string
 | 
			
		||||
      if (current_tz.length() != value.timezone_len ||
 | 
			
		||||
          memcmp(current_tz.c_str(), value.timezone, value.timezone_len) != 0) {
 | 
			
		||||
        homeassistant::global_homeassistant_time->set_timezone(
 | 
			
		||||
            std::string(reinterpret_cast<const char *>(value.timezone), value.timezone_len));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
@@ -1193,6 +1197,23 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA
 | 
			
		||||
      resp_wake_word.trained_languages.push_back(lang);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Filter external wake words
 | 
			
		||||
  for (auto &wake_word : msg.external_wake_words) {
 | 
			
		||||
    if (wake_word.model_type != "micro") {
 | 
			
		||||
      // microWakeWord only
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    resp.available_wake_words.emplace_back();
 | 
			
		||||
    auto &resp_wake_word = resp.available_wake_words.back();
 | 
			
		||||
    resp_wake_word.set_id(StringRef(wake_word.id));
 | 
			
		||||
    resp_wake_word.set_wake_word(StringRef(wake_word.wake_word));
 | 
			
		||||
    for (const auto &lang : wake_word.trained_languages) {
 | 
			
		||||
      resp_wake_word.trained_languages.push_back(lang);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  resp.active_wake_words = &config.active_wake_words;
 | 
			
		||||
  resp.max_active_wake_words = config.max_active_wake_words;
 | 
			
		||||
  return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
 | 
			
		||||
@@ -1203,7 +1224,16 @@ void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetCon
 | 
			
		||||
    voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
void APIConnection::zwave_proxy_frame(const ZWaveProxyFrame &msg) {
 | 
			
		||||
  zwave_proxy::global_zwave_proxy->send_frame(msg.data, msg.data_len);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIConnection::zwave_proxy_request(const ZWaveProxyRequest &msg) {
 | 
			
		||||
  zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ALARM_CONTROL_PANEL
 | 
			
		||||
@@ -1350,7 +1380,7 @@ void APIConnection::complete_authentication_() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
 | 
			
		||||
  ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str());
 | 
			
		||||
  ESP_LOGD(TAG, "%s (%s) connected", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
 | 
			
		||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
 | 
			
		||||
  this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername);
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1359,10 +1389,15 @@ void APIConnection::complete_authentication_() {
 | 
			
		||||
    this->send_time_request();
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  if (zwave_proxy::global_zwave_proxy != nullptr) {
 | 
			
		||||
    zwave_proxy::global_zwave_proxy->api_connection_authenticated(this);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool APIConnection::send_hello_response(const HelloRequest &msg) {
 | 
			
		||||
  this->client_info_.name = msg.client_info;
 | 
			
		||||
  this->client_info_.name.assign(reinterpret_cast<const char *>(msg.client_info), msg.client_info_len);
 | 
			
		||||
  this->client_info_.peername = this->helper_->getpeername();
 | 
			
		||||
  this->client_api_version_major_ = msg.api_version_major;
 | 
			
		||||
  this->client_api_version_minor_ = msg.api_version_minor;
 | 
			
		||||
@@ -1386,20 +1421,17 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
 | 
			
		||||
 | 
			
		||||
  return this->send_message(resp, HelloResponse::MESSAGE_TYPE);
 | 
			
		||||
}
 | 
			
		||||
bool APIConnection::send_connect_response(const ConnectRequest &msg) {
 | 
			
		||||
  bool correct = true;
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
  correct = this->parent_->check_password(msg.password);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  ConnectResponse resp;
 | 
			
		||||
bool APIConnection::send_authenticate_response(const AuthenticationRequest &msg) {
 | 
			
		||||
  AuthenticationResponse resp;
 | 
			
		||||
  // bool invalid_password = 1;
 | 
			
		||||
  resp.invalid_password = !correct;
 | 
			
		||||
  if (correct) {
 | 
			
		||||
  resp.invalid_password = !this->parent_->check_password(msg.password, msg.password_len);
 | 
			
		||||
  if (!resp.invalid_password) {
 | 
			
		||||
    this->complete_authentication_();
 | 
			
		||||
  }
 | 
			
		||||
  return this->send_message(resp, ConnectResponse::MESSAGE_TYPE);
 | 
			
		||||
  return this->send_message(resp, AuthenticationResponse::MESSAGE_TYPE);
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_API_PASSWORD
 | 
			
		||||
 | 
			
		||||
bool APIConnection::send_ping_response(const PingRequest &msg) {
 | 
			
		||||
  PingResponse resp;
 | 
			
		||||
@@ -1463,6 +1495,10 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
  resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags();
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  resp.zwave_proxy_feature_flags = zwave_proxy::global_zwave_proxy->get_feature_flags();
 | 
			
		||||
  resp.zwave_home_id = zwave_proxy::global_zwave_proxy->get_home_id();
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  resp.api_encryption_supported = true;
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1513,6 +1549,20 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) {
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  if (msg.response_data_len > 0) {
 | 
			
		||||
    this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data,
 | 
			
		||||
                                          msg.response_data_len);
 | 
			
		||||
  } else
 | 
			
		||||
#endif
 | 
			
		||||
  {
 | 
			
		||||
    this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
 | 
			
		||||
  NoiseEncryptionSetKeyResponse resp;
 | 
			
		||||
@@ -1543,8 +1593,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
 | 
			
		||||
  delay(0);
 | 
			
		||||
  APIError err = this->helper_->loop();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    this->log_socket_operation_failed_(err);
 | 
			
		||||
    this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->helper_->can_write_without_blocking())
 | 
			
		||||
@@ -1563,8 +1612,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
 | 
			
		||||
  if (err == APIError::WOULD_BLOCK)
 | 
			
		||||
    return false;
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    this->log_warning_(LOG_STR("Packet write failed"), err);
 | 
			
		||||
    this->fatal_error_with_log_(LOG_STR("Packet write failed"), err);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  // Do not set last_traffic_ on send
 | 
			
		||||
@@ -1573,12 +1621,12 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
void APIConnection::on_unauthenticated_access() {
 | 
			
		||||
  this->on_fatal_error();
 | 
			
		||||
  ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str());
 | 
			
		||||
  ESP_LOGD(TAG, "%s (%s) no authentication", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
void APIConnection::on_no_setup_connection() {
 | 
			
		||||
  this->on_fatal_error();
 | 
			
		||||
  ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str());
 | 
			
		||||
  ESP_LOGD(TAG, "%s (%s) no connection setup", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_fatal_error() {
 | 
			
		||||
  this->helper_->close();
 | 
			
		||||
@@ -1750,8 +1798,7 @@ void APIConnection::process_batch_() {
 | 
			
		||||
  APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf},
 | 
			
		||||
                                                       std::span<const PacketInfo>(packet_info, packet_count));
 | 
			
		||||
  if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    this->log_warning_(LOG_STR("Batch write failed"), err);
 | 
			
		||||
    this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
@@ -1830,12 +1877,8 @@ void APIConnection::process_state_subscriptions_() {
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
 | 
			
		||||
void APIConnection::log_warning_(const LogString *message, APIError err) {
 | 
			
		||||
  ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), LOG_STR_ARG(message),
 | 
			
		||||
           LOG_STR_ARG(api_error_to_logstr(err)), errno);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIConnection::log_socket_operation_failed_(APIError err) {
 | 
			
		||||
  this->log_warning_(LOG_STR("Socket operation failed"), err);
 | 
			
		||||
  ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name.c_str(), this->client_info_.peername.c_str(),
 | 
			
		||||
           LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::api
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,8 @@
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome::api {
 | 
			
		||||
 | 
			
		||||
@@ -19,14 +19,6 @@ namespace esphome::api {
 | 
			
		||||
struct ClientInfo {
 | 
			
		||||
  std::string name;      // Client name from Hello message
 | 
			
		||||
  std::string peername;  // IP:port from socket
 | 
			
		||||
 | 
			
		||||
  std::string get_combined_info() const {
 | 
			
		||||
    if (name == peername) {
 | 
			
		||||
      // Before Hello message, both are the same
 | 
			
		||||
      return name;
 | 
			
		||||
    }
 | 
			
		||||
    return name + " (" + peername + ")";
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Keepalive timeout in milliseconds
 | 
			
		||||
@@ -132,12 +124,15 @@ class APIConnection final : public APIServerConnection {
 | 
			
		||||
#endif
 | 
			
		||||
  bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
  void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
 | 
			
		||||
  void send_homeassistant_action(const HomeassistantActionRequest &call) {
 | 
			
		||||
    if (!this->flags_.service_call_subscription)
 | 
			
		||||
      return;
 | 
			
		||||
    this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE);
 | 
			
		||||
    this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override;
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
  void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
 | 
			
		||||
  void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
 | 
			
		||||
@@ -171,6 +166,11 @@ class APIConnection final : public APIServerConnection {
 | 
			
		||||
  void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  void zwave_proxy_frame(const ZWaveProxyFrame &msg) override;
 | 
			
		||||
  void zwave_proxy_request(const ZWaveProxyRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ALARM_CONTROL_PANEL
 | 
			
		||||
  bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
 | 
			
		||||
  void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
 | 
			
		||||
@@ -197,7 +197,9 @@ class APIConnection final : public APIServerConnection {
 | 
			
		||||
  void on_get_time_response(const GetTimeResponse &value) override;
 | 
			
		||||
#endif
 | 
			
		||||
  bool send_hello_response(const HelloRequest &msg) override;
 | 
			
		||||
  bool send_connect_response(const ConnectRequest &msg) override;
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
  bool send_authenticate_response(const AuthenticationRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
  bool send_disconnect_response(const DisconnectRequest &msg) override;
 | 
			
		||||
  bool send_ping_response(const PingRequest &msg) override;
 | 
			
		||||
  bool send_device_info_response(const DeviceInfoRequest &msg) override;
 | 
			
		||||
@@ -271,7 +273,8 @@ class APIConnection final : public APIServerConnection {
 | 
			
		||||
  bool try_to_clear_buffer(bool log_out_of_space);
 | 
			
		||||
  bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
 | 
			
		||||
 | 
			
		||||
  std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); }
 | 
			
		||||
  const std::string &get_name() const { return this->client_info_.name; }
 | 
			
		||||
  const std::string &get_peername() const { return this->client_info_.peername; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  // Helper function to handle authentication completion
 | 
			
		||||
@@ -732,8 +735,11 @@ class APIConnection final : public APIServerConnection {
 | 
			
		||||
 | 
			
		||||
  // Helper function to log API errors with errno
 | 
			
		||||
  void log_warning_(const LogString *message, APIError err);
 | 
			
		||||
  // Specific helper for duplicated error message
 | 
			
		||||
  void log_socket_operation_failed_(APIError err);
 | 
			
		||||
  // Helper to handle fatal errors with logging
 | 
			
		||||
  inline void fatal_error_with_log_(const LogString *message, APIError err) {
 | 
			
		||||
    this->on_fatal_error();
 | 
			
		||||
    this->log_warning_(message, err);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::api
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,8 @@ namespace esphome::api {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "api.frame_helper";
 | 
			
		||||
 | 
			
		||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
 | 
			
		||||
#define HELPER_LOG(msg, ...) \
 | 
			
		||||
  ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
 | 
			
		||||
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
 | 
			
		||||
@@ -80,7 +81,7 @@ const LogString *api_error_to_logstr(APIError err) {
 | 
			
		||||
 | 
			
		||||
// Default implementation for loop - handles sending buffered data
 | 
			
		||||
APIError APIFrameHelper::loop() {
 | 
			
		||||
  if (!this->tx_buf_.empty()) {
 | 
			
		||||
  if (this->tx_buf_count_ > 0) {
 | 
			
		||||
    APIError err = try_send_tx_buf_();
 | 
			
		||||
    if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
 | 
			
		||||
      return err;
 | 
			
		||||
@@ -102,9 +103,20 @@ APIError APIFrameHelper::handle_socket_write_error_() {
 | 
			
		||||
// Helper method to buffer data from IOVs
 | 
			
		||||
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
 | 
			
		||||
                                           uint16_t offset) {
 | 
			
		||||
  SendBuffer buffer;
 | 
			
		||||
  buffer.size = total_write_len - offset;
 | 
			
		||||
  buffer.data = std::make_unique<uint8_t[]>(buffer.size);
 | 
			
		||||
  // Check if queue is full
 | 
			
		||||
  if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) {
 | 
			
		||||
    HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_);
 | 
			
		||||
    this->state_ = State::FAILED;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint16_t buffer_size = total_write_len - offset;
 | 
			
		||||
  auto &buffer = this->tx_buf_[this->tx_buf_tail_];
 | 
			
		||||
  buffer = std::make_unique<SendBuffer>(SendBuffer{
 | 
			
		||||
      .data = std::make_unique<uint8_t[]>(buffer_size),
 | 
			
		||||
      .size = buffer_size,
 | 
			
		||||
      .offset = 0,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  uint16_t to_skip = offset;
 | 
			
		||||
  uint16_t write_pos = 0;
 | 
			
		||||
@@ -117,12 +129,15 @@ void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt,
 | 
			
		||||
      // Include this segment (partially or fully)
 | 
			
		||||
      const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
 | 
			
		||||
      uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
 | 
			
		||||
      std::memcpy(buffer.data.get() + write_pos, src, len);
 | 
			
		||||
      std::memcpy(buffer->data.get() + write_pos, src, len);
 | 
			
		||||
      write_pos += len;
 | 
			
		||||
      to_skip = 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  this->tx_buf_.push_back(std::move(buffer));
 | 
			
		||||
 | 
			
		||||
  // Update circular buffer tracking
 | 
			
		||||
  this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE;
 | 
			
		||||
  this->tx_buf_count_++;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This method writes data to socket or buffers it
 | 
			
		||||
@@ -140,7 +155,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Try to send any existing buffered data first if there is any
 | 
			
		||||
  if (!this->tx_buf_.empty()) {
 | 
			
		||||
  if (this->tx_buf_count_ > 0) {
 | 
			
		||||
    APIError send_result = try_send_tx_buf_();
 | 
			
		||||
    // If real error occurred (not just WOULD_BLOCK), return it
 | 
			
		||||
    if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
 | 
			
		||||
@@ -149,7 +164,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
 | 
			
		||||
 | 
			
		||||
    // If there is still data in the buffer, we can't send, buffer
 | 
			
		||||
    // the new data and return
 | 
			
		||||
    if (!this->tx_buf_.empty()) {
 | 
			
		||||
    if (this->tx_buf_count_ > 0) {
 | 
			
		||||
      this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
 | 
			
		||||
      return APIError::OK;  // Success, data buffered
 | 
			
		||||
    }
 | 
			
		||||
@@ -177,32 +192,31 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Common implementation for trying to send buffered data
 | 
			
		||||
// IMPORTANT: Caller MUST ensure tx_buf_ is not empty before calling this method
 | 
			
		||||
// IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method
 | 
			
		||||
APIError APIFrameHelper::try_send_tx_buf_() {
 | 
			
		||||
  // Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
 | 
			
		||||
  bool tx_buf_empty = false;
 | 
			
		||||
  while (!tx_buf_empty) {
 | 
			
		||||
  while (this->tx_buf_count_ > 0) {
 | 
			
		||||
    // Get the first buffer in the queue
 | 
			
		||||
    SendBuffer &front_buffer = this->tx_buf_.front();
 | 
			
		||||
    SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get();
 | 
			
		||||
 | 
			
		||||
    // Try to send the remaining data in this buffer
 | 
			
		||||
    ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining());
 | 
			
		||||
    ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining());
 | 
			
		||||
 | 
			
		||||
    if (sent == -1) {
 | 
			
		||||
      return this->handle_socket_write_error_();
 | 
			
		||||
    } else if (sent == 0) {
 | 
			
		||||
      // Nothing sent but not an error
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    } else if (static_cast<uint16_t>(sent) < front_buffer.remaining()) {
 | 
			
		||||
    } else if (static_cast<uint16_t>(sent) < front_buffer->remaining()) {
 | 
			
		||||
      // Partially sent, update offset
 | 
			
		||||
      // Cast to ensure no overflow issues with uint16_t
 | 
			
		||||
      front_buffer.offset += static_cast<uint16_t>(sent);
 | 
			
		||||
      front_buffer->offset += static_cast<uint16_t>(sent);
 | 
			
		||||
      return APIError::WOULD_BLOCK;  // Stop processing more buffers if we couldn't send a complete buffer
 | 
			
		||||
    } else {
 | 
			
		||||
      // Buffer completely sent, remove it from the queue
 | 
			
		||||
      this->tx_buf_.pop_front();
 | 
			
		||||
      // Update empty status for the loop condition
 | 
			
		||||
      tx_buf_empty = this->tx_buf_.empty();
 | 
			
		||||
      this->tx_buf_[this->tx_buf_head_].reset();
 | 
			
		||||
      this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE;
 | 
			
		||||
      this->tx_buf_count_--;
 | 
			
		||||
      // Continue loop to try sending the next buffer
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <deque>
 | 
			
		||||
#include <limits>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <span>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
@@ -17,6 +18,17 @@ namespace esphome::api {
 | 
			
		||||
// uncomment to log raw packets
 | 
			
		||||
//#define HELPER_LOG_PACKETS
 | 
			
		||||
 | 
			
		||||
// Maximum message size limits to prevent OOM on constrained devices
 | 
			
		||||
// Handshake messages are limited to a small size for security
 | 
			
		||||
static constexpr uint16_t MAX_HANDSHAKE_SIZE = 128;
 | 
			
		||||
 | 
			
		||||
// Data message limits vary by platform based on available memory
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
static constexpr uint16_t MAX_MESSAGE_SIZE = 8192;  // 8 KiB for ESP8266
 | 
			
		||||
#else
 | 
			
		||||
static constexpr uint16_t MAX_MESSAGE_SIZE = 32768;  // 32 KiB for ESP32 and other platforms
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
// Forward declaration
 | 
			
		||||
struct ClientInfo;
 | 
			
		||||
 | 
			
		||||
@@ -79,7 +91,7 @@ class APIFrameHelper {
 | 
			
		||||
  virtual APIError init() = 0;
 | 
			
		||||
  virtual APIError loop();
 | 
			
		||||
  virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
 | 
			
		||||
  bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
 | 
			
		||||
  bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
 | 
			
		||||
  std::string getpeername() { return socket_->getpeername(); }
 | 
			
		||||
  int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
 | 
			
		||||
  APIError close() {
 | 
			
		||||
@@ -161,7 +173,7 @@ class APIFrameHelper {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Containers (size varies, but typically 12+ bytes on 32-bit)
 | 
			
		||||
  std::deque<SendBuffer> tx_buf_;
 | 
			
		||||
  std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
 | 
			
		||||
  std::vector<struct iovec> reusable_iovs_;
 | 
			
		||||
  std::vector<uint8_t> rx_buf_;
 | 
			
		||||
 | 
			
		||||
@@ -174,7 +186,10 @@ class APIFrameHelper {
 | 
			
		||||
  State state_{State::INITIALIZE};
 | 
			
		||||
  uint8_t frame_header_padding_{0};
 | 
			
		||||
  uint8_t frame_footer_size_{0};
 | 
			
		||||
  // 5 bytes total, 3 bytes padding
 | 
			
		||||
  uint8_t tx_buf_head_{0};
 | 
			
		||||
  uint8_t tx_buf_tail_{0};
 | 
			
		||||
  uint8_t tx_buf_count_{0};
 | 
			
		||||
  // 8 bytes total, 0 bytes padding
 | 
			
		||||
 | 
			
		||||
  // Common initialization for both plaintext and noise protocols
 | 
			
		||||
  APIError init_common_();
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,8 @@ static const char *const PROLOGUE_INIT = "NoiseAPIInit";
 | 
			
		||||
#endif
 | 
			
		||||
static constexpr size_t PROLOGUE_INIT_LEN = 12;  // strlen("NoiseAPIInit")
 | 
			
		||||
 | 
			
		||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
 | 
			
		||||
#define HELPER_LOG(msg, ...) \
 | 
			
		||||
  ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
 | 
			
		||||
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
 | 
			
		||||
@@ -131,26 +132,16 @@ APIError APINoiseFrameHelper::loop() {
 | 
			
		||||
  return APIFrameHelper::loop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
 | 
			
		||||
/** Read a packet into the rx_buf_.
 | 
			
		||||
 *
 | 
			
		||||
 * @param frame: The struct to hold the frame information in.
 | 
			
		||||
 *   msg_start: points to the start of the payload - this pointer is only valid until the next
 | 
			
		||||
 *     try_receive_raw_ call
 | 
			
		||||
 *
 | 
			
		||||
 * @return 0 if a full packet is in rx_buf_
 | 
			
		||||
 * @return -1 if error, check errno.
 | 
			
		||||
 * @return APIError::OK if a full packet is in rx_buf_
 | 
			
		||||
 *
 | 
			
		||||
 * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
 | 
			
		||||
 * errno ENOMEM: Not enough memory for reading packet.
 | 
			
		||||
 * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
 | 
			
		||||
 * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
 | 
			
		||||
 */
 | 
			
		||||
APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
 | 
			
		||||
  if (frame == nullptr) {
 | 
			
		||||
    HELPER_LOG("Bad argument for try_read_frame_");
 | 
			
		||||
    return APIError::BAD_ARG;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
APIError APINoiseFrameHelper::try_read_frame_() {
 | 
			
		||||
  // read header
 | 
			
		||||
  if (rx_header_buf_len_ < 3) {
 | 
			
		||||
    // no header information yet
 | 
			
		||||
@@ -177,16 +168,17 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
 | 
			
		||||
  // read body
 | 
			
		||||
  uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA && msg_size > 128) {
 | 
			
		||||
    // for handshake message only permit up to 128 bytes
 | 
			
		||||
  // Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data
 | 
			
		||||
  uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
 | 
			
		||||
  if (msg_size > limit) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Bad packet len for handshake: %d", msg_size);
 | 
			
		||||
    return APIError::BAD_HANDSHAKE_PACKET_LEN;
 | 
			
		||||
    HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit);
 | 
			
		||||
    return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // reserve space for body
 | 
			
		||||
  if (rx_buf_.size() != msg_size) {
 | 
			
		||||
    rx_buf_.resize(msg_size);
 | 
			
		||||
  // Reserve space for body
 | 
			
		||||
  if (this->rx_buf_.size() != msg_size) {
 | 
			
		||||
    this->rx_buf_.resize(msg_size);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (rx_buf_len_ < msg_size) {
 | 
			
		||||
@@ -204,12 +196,12 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  LOG_PACKET_RECEIVED(rx_buf_);
 | 
			
		||||
  *frame = std::move(rx_buf_);
 | 
			
		||||
  // consume msg
 | 
			
		||||
  rx_buf_ = {};
 | 
			
		||||
  rx_buf_len_ = 0;
 | 
			
		||||
  rx_header_buf_len_ = 0;
 | 
			
		||||
  LOG_PACKET_RECEIVED(this->rx_buf_);
 | 
			
		||||
 | 
			
		||||
  // Clear state for next frame (rx_buf_ still contains data for caller)
 | 
			
		||||
  this->rx_buf_len_ = 0;
 | 
			
		||||
  this->rx_header_buf_len_ = 0;
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -231,18 +223,17 @@ APIError APINoiseFrameHelper::state_action_() {
 | 
			
		||||
  }
 | 
			
		||||
  if (state_ == State::CLIENT_HELLO) {
 | 
			
		||||
    // waiting for client hello
 | 
			
		||||
    std::vector<uint8_t> frame;
 | 
			
		||||
    aerr = try_read_frame_(&frame);
 | 
			
		||||
    aerr = this->try_read_frame_();
 | 
			
		||||
    if (aerr != APIError::OK) {
 | 
			
		||||
      return handle_handshake_frame_error_(aerr);
 | 
			
		||||
    }
 | 
			
		||||
    // ignore contents, may be used in future for flags
 | 
			
		||||
    // Resize for: existing prologue + 2 size bytes + frame data
 | 
			
		||||
    size_t old_size = prologue_.size();
 | 
			
		||||
    prologue_.resize(old_size + 2 + frame.size());
 | 
			
		||||
    prologue_[old_size] = (uint8_t) (frame.size() >> 8);
 | 
			
		||||
    prologue_[old_size + 1] = (uint8_t) frame.size();
 | 
			
		||||
    std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size());
 | 
			
		||||
    size_t old_size = this->prologue_.size();
 | 
			
		||||
    this->prologue_.resize(old_size + 2 + this->rx_buf_.size());
 | 
			
		||||
    this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8);
 | 
			
		||||
    this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size();
 | 
			
		||||
    std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size());
 | 
			
		||||
 | 
			
		||||
    state_ = State::SERVER_HELLO;
 | 
			
		||||
  }
 | 
			
		||||
@@ -284,24 +275,23 @@ APIError APINoiseFrameHelper::state_action_() {
 | 
			
		||||
    int action = noise_handshakestate_get_action(handshake_);
 | 
			
		||||
    if (action == NOISE_ACTION_READ_MESSAGE) {
 | 
			
		||||
      // waiting for handshake msg
 | 
			
		||||
      std::vector<uint8_t> frame;
 | 
			
		||||
      aerr = try_read_frame_(&frame);
 | 
			
		||||
      aerr = this->try_read_frame_();
 | 
			
		||||
      if (aerr != APIError::OK) {
 | 
			
		||||
        return handle_handshake_frame_error_(aerr);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (frame.empty()) {
 | 
			
		||||
      if (this->rx_buf_.empty()) {
 | 
			
		||||
        send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
 | 
			
		||||
        return APIError::BAD_HANDSHAKE_ERROR_BYTE;
 | 
			
		||||
      } else if (frame[0] != 0x00) {
 | 
			
		||||
        HELPER_LOG("Bad handshake error byte: %u", frame[0]);
 | 
			
		||||
      } else if (this->rx_buf_[0] != 0x00) {
 | 
			
		||||
        HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
 | 
			
		||||
        send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
 | 
			
		||||
        return APIError::BAD_HANDSHAKE_ERROR_BYTE;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      NoiseBuffer mbuf;
 | 
			
		||||
      noise_buffer_init(mbuf);
 | 
			
		||||
      noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1);
 | 
			
		||||
      noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
 | 
			
		||||
      err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
 | 
			
		||||
      if (err != 0) {
 | 
			
		||||
        // Special handling for MAC failure
 | 
			
		||||
@@ -378,35 +368,33 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
 | 
			
		||||
  state_ = orig_state;
 | 
			
		||||
}
 | 
			
		||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
  aerr = state_action_();
 | 
			
		||||
  APIError aerr = this->state_action_();
 | 
			
		||||
  if (aerr != APIError::OK) {
 | 
			
		||||
    return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
  if (this->state_ != State::DATA) {
 | 
			
		||||
    return APIError::WOULD_BLOCK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> frame;
 | 
			
		||||
  aerr = try_read_frame_(&frame);
 | 
			
		||||
  aerr = this->try_read_frame_();
 | 
			
		||||
  if (aerr != APIError::OK)
 | 
			
		||||
    return aerr;
 | 
			
		||||
 | 
			
		||||
  NoiseBuffer mbuf;
 | 
			
		||||
  noise_buffer_init(mbuf);
 | 
			
		||||
  noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size());
 | 
			
		||||
  err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
 | 
			
		||||
  noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size());
 | 
			
		||||
  int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf);
 | 
			
		||||
  APIError decrypt_err =
 | 
			
		||||
      handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
 | 
			
		||||
  if (decrypt_err != APIError::OK)
 | 
			
		||||
  if (decrypt_err != APIError::OK) {
 | 
			
		||||
    return decrypt_err;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint16_t msg_size = mbuf.size;
 | 
			
		||||
  uint8_t *msg_data = frame.data();
 | 
			
		||||
  uint8_t *msg_data = this->rx_buf_.data();
 | 
			
		||||
  if (msg_size < 4) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    this->state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Bad data packet: size %d too short", msg_size);
 | 
			
		||||
    return APIError::BAD_DATA_PACKET;
 | 
			
		||||
  }
 | 
			
		||||
@@ -414,12 +402,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
 | 
			
		||||
  uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
 | 
			
		||||
  if (data_len > msg_size - 4) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    this->state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
 | 
			
		||||
    return APIError::BAD_DATA_PACKET;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  buffer->container = std::move(frame);
 | 
			
		||||
  buffer->container = std::move(this->rx_buf_);
 | 
			
		||||
  buffer->data_offset = 4;
 | 
			
		||||
  buffer->data_len = data_len;
 | 
			
		||||
  buffer->type = type;
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  APIError state_action_();
 | 
			
		||||
  APIError try_read_frame_(std::vector<uint8_t> *frame);
 | 
			
		||||
  APIError try_read_frame_();
 | 
			
		||||
  APIError write_frame_(const uint8_t *data, uint16_t len);
 | 
			
		||||
  APIError init_handshake_();
 | 
			
		||||
  APIError check_handshake_finished_();
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,8 @@ namespace esphome::api {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "api.plaintext";
 | 
			
		||||
 | 
			
		||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
 | 
			
		||||
#define HELPER_LOG(msg, ...) \
 | 
			
		||||
  ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
 | 
			
		||||
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
 | 
			
		||||
@@ -46,21 +47,13 @@ APIError APIPlaintextFrameHelper::loop() {
 | 
			
		||||
  return APIFrameHelper::loop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
 | 
			
		||||
 *
 | 
			
		||||
 * @param frame: The struct to hold the frame information in.
 | 
			
		||||
 *   msg: store the parsed frame in that struct
 | 
			
		||||
/** Read a packet into the rx_buf_.
 | 
			
		||||
 *
 | 
			
		||||
 * @return See APIError
 | 
			
		||||
 *
 | 
			
		||||
 * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
 | 
			
		||||
 */
 | 
			
		||||
APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
 | 
			
		||||
  if (frame == nullptr) {
 | 
			
		||||
    HELPER_LOG("Bad argument for try_read_frame_");
 | 
			
		||||
    return APIError::BAD_ARG;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
APIError APIPlaintextFrameHelper::try_read_frame_() {
 | 
			
		||||
  // read header
 | 
			
		||||
  while (!rx_header_parsed_) {
 | 
			
		||||
    // Now that we know when the socket is ready, we can read up to 3 bytes
 | 
			
		||||
@@ -122,10 +115,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
 | 
			
		||||
    if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
 | 
			
		||||
                 std::numeric_limits<uint16_t>::max());
 | 
			
		||||
                 MAX_MESSAGE_SIZE);
 | 
			
		||||
      return APIError::BAD_DATA_PACKET;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_parsed_len_ = msg_size_varint->as_uint16();
 | 
			
		||||
@@ -149,9 +142,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
 | 
			
		||||
  }
 | 
			
		||||
  // header reading done
 | 
			
		||||
 | 
			
		||||
  // reserve space for body
 | 
			
		||||
  if (rx_buf_.size() != rx_header_parsed_len_) {
 | 
			
		||||
    rx_buf_.resize(rx_header_parsed_len_);
 | 
			
		||||
  // Reserve space for body
 | 
			
		||||
  if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
 | 
			
		||||
    this->rx_buf_.resize(this->rx_header_parsed_len_);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (rx_buf_len_ < rx_header_parsed_len_) {
 | 
			
		||||
@@ -169,24 +162,22 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  LOG_PACKET_RECEIVED(rx_buf_);
 | 
			
		||||
  *frame = std::move(rx_buf_);
 | 
			
		||||
  // consume msg
 | 
			
		||||
  rx_buf_ = {};
 | 
			
		||||
  rx_buf_len_ = 0;
 | 
			
		||||
  rx_header_buf_pos_ = 0;
 | 
			
		||||
  rx_header_parsed_ = false;
 | 
			
		||||
  LOG_PACKET_RECEIVED(this->rx_buf_);
 | 
			
		||||
 | 
			
		||||
  // Clear state for next frame (rx_buf_ still contains data for caller)
 | 
			
		||||
  this->rx_buf_len_ = 0;
 | 
			
		||||
  this->rx_header_buf_pos_ = 0;
 | 
			
		||||
  this->rx_header_parsed_ = false;
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  if (this->state_ != State::DATA) {
 | 
			
		||||
    return APIError::WOULD_BLOCK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> frame;
 | 
			
		||||
  aerr = try_read_frame_(&frame);
 | 
			
		||||
  APIError aerr = this->try_read_frame_();
 | 
			
		||||
  if (aerr != APIError::OK) {
 | 
			
		||||
    if (aerr == APIError::BAD_INDICATOR) {
 | 
			
		||||
      // Make sure to tell the remote that we don't
 | 
			
		||||
@@ -219,10 +210,10 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
    return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  buffer->container = std::move(frame);
 | 
			
		||||
  buffer->container = std::move(this->rx_buf_);
 | 
			
		||||
  buffer->data_offset = 0;
 | 
			
		||||
  buffer->data_len = rx_header_parsed_len_;
 | 
			
		||||
  buffer->type = rx_header_parsed_type_;
 | 
			
		||||
  buffer->data_len = this->rx_header_parsed_len_;
 | 
			
		||||
  buffer->type = this->rx_header_parsed_type_;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
 | 
			
		||||
  APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  APIError try_read_frame_(std::vector<uint8_t> *frame);
 | 
			
		||||
  APIError try_read_frame_();
 | 
			
		||||
 | 
			
		||||
  // Group 2-byte aligned types
 | 
			
		||||
  uint16_t rx_header_parsed_type_ = 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -32,6 +32,13 @@ extend google.protobuf.FieldOptions {
 | 
			
		||||
    optional string fixed_array_size_define = 50010;
 | 
			
		||||
    optional string fixed_array_with_length_define = 50011;
 | 
			
		||||
 | 
			
		||||
    // pointer_to_buffer: Use pointer instead of array for fixed-size byte fields
 | 
			
		||||
    // When set, the field will be declared as a pointer (const uint8_t *data)
 | 
			
		||||
    // instead of an array (uint8_t data[N]). This allows zero-copy on decode
 | 
			
		||||
    // by pointing directly to the protobuf buffer. The buffer must remain valid
 | 
			
		||||
    // until the message is processed (which is guaranteed for stack-allocated messages).
 | 
			
		||||
    optional bool pointer_to_buffer = 50012 [default=false];
 | 
			
		||||
 | 
			
		||||
    // container_pointer: Zero-copy optimization for repeated fields.
 | 
			
		||||
    //
 | 
			
		||||
    // When container_pointer is set on a repeated field, the generated message will
 | 
			
		||||
 
 | 
			
		||||
@@ -22,9 +22,12 @@ bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
}
 | 
			
		||||
bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      this->client_info = value.as_string();
 | 
			
		||||
    case 1: {
 | 
			
		||||
      // Use raw data directly to avoid allocation
 | 
			
		||||
      this->client_info = value.data();
 | 
			
		||||
      this->client_info_len = value.size();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -42,18 +45,23 @@ void HelloResponse::calculate_size(ProtoSize &size) const {
 | 
			
		||||
  size.add_length(1, this->server_info_ref_.size());
 | 
			
		||||
  size.add_length(1, this->name_ref_.size());
 | 
			
		||||
}
 | 
			
		||||
bool ConnectRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
bool AuthenticationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      this->password = value.as_string();
 | 
			
		||||
    case 1: {
 | 
			
		||||
      // Use raw data directly to avoid allocation
 | 
			
		||||
      this->password = value.data();
 | 
			
		||||
      this->password_len = value.size();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
void ConnectResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); }
 | 
			
		||||
void ConnectResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->invalid_password); }
 | 
			
		||||
void AuthenticationResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->invalid_password); }
 | 
			
		||||
void AuthenticationResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->invalid_password); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_AREAS
 | 
			
		||||
void AreaInfo::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_uint32(1, this->area_id);
 | 
			
		||||
@@ -127,6 +135,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
#ifdef USE_AREAS
 | 
			
		||||
  buffer.encode_message(22, this->area);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  buffer.encode_uint32(23, this->zwave_proxy_feature_flags);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  buffer.encode_uint32(24, this->zwave_home_id);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
void DeviceInfoResponse::calculate_size(ProtoSize &size) const {
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
@@ -179,6 +193,12 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const {
 | 
			
		||||
#ifdef USE_AREAS
 | 
			
		||||
  size.add_message_object(2, this->area);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  size.add_uint32(2, this->zwave_proxy_feature_flags);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  size.add_uint32(2, this->zwave_home_id);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
@@ -852,7 +872,7 @@ void HomeassistantServiceMap::calculate_size(ProtoSize &size) const {
 | 
			
		||||
  size.add_length(1, this->key_ref_.size());
 | 
			
		||||
  size.add_length(1, this->value.size());
 | 
			
		||||
}
 | 
			
		||||
void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_string(1, this->service_ref_);
 | 
			
		||||
  for (auto &it : this->data) {
 | 
			
		||||
    buffer.encode_message(2, it, true);
 | 
			
		||||
@@ -864,13 +884,64 @@ void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
    buffer.encode_message(4, it, true);
 | 
			
		||||
  }
 | 
			
		||||
  buffer.encode_bool(5, this->is_event);
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  buffer.encode_uint32(6, this->call_id);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  buffer.encode_bool(7, this->wants_response);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  buffer.encode_string(8, this->response_template);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
void HomeassistantServiceResponse::calculate_size(ProtoSize &size) const {
 | 
			
		||||
void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
 | 
			
		||||
  size.add_length(1, this->service_ref_.size());
 | 
			
		||||
  size.add_repeated_message(1, this->data);
 | 
			
		||||
  size.add_repeated_message(1, this->data_template);
 | 
			
		||||
  size.add_repeated_message(1, this->variables);
 | 
			
		||||
  size.add_bool(1, this->is_event);
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  size.add_uint32(1, this->call_id);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  size.add_bool(1, this->wants_response);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  size.add_length(1, this->response_template.size());
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      this->call_id = value.as_uint32();
 | 
			
		||||
      break;
 | 
			
		||||
    case 2:
 | 
			
		||||
      this->success = value.as_bool();
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 3:
 | 
			
		||||
      this->error_message = value.as_string();
 | 
			
		||||
      break;
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
    case 4: {
 | 
			
		||||
      // Use raw data directly to avoid allocation
 | 
			
		||||
      this->response_data = value.data();
 | 
			
		||||
      this->response_data_len = value.size();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
@@ -903,9 +974,12 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel
 | 
			
		||||
#endif
 | 
			
		||||
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 2:
 | 
			
		||||
      this->timezone = value.as_string();
 | 
			
		||||
    case 2: {
 | 
			
		||||
      // Use raw data directly to avoid allocation
 | 
			
		||||
      this->timezone = value.data();
 | 
			
		||||
      this->timezone_len = value.size();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -2014,9 +2088,12 @@ bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt val
 | 
			
		||||
}
 | 
			
		||||
bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 4:
 | 
			
		||||
      this->data = value.as_string();
 | 
			
		||||
    case 4: {
 | 
			
		||||
      // Use raw data directly to avoid allocation
 | 
			
		||||
      this->data = value.data();
 | 
			
		||||
      this->data_len = value.size();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -2050,9 +2127,12 @@ bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, Proto
 | 
			
		||||
}
 | 
			
		||||
bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 3:
 | 
			
		||||
      this->data = value.as_string();
 | 
			
		||||
    case 3: {
 | 
			
		||||
      // Use raw data directly to avoid allocation
 | 
			
		||||
      this->data = value.data();
 | 
			
		||||
      this->data_len = value.size();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -2368,6 +2448,52 @@ void VoiceAssistantWakeWord::calculate_size(ProtoSize &size) const {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 5:
 | 
			
		||||
      this->model_size = value.as_uint32();
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
bool VoiceAssistantExternalWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      this->id = value.as_string();
 | 
			
		||||
      break;
 | 
			
		||||
    case 2:
 | 
			
		||||
      this->wake_word = value.as_string();
 | 
			
		||||
      break;
 | 
			
		||||
    case 3:
 | 
			
		||||
      this->trained_languages.push_back(value.as_string());
 | 
			
		||||
      break;
 | 
			
		||||
    case 4:
 | 
			
		||||
      this->model_type = value.as_string();
 | 
			
		||||
      break;
 | 
			
		||||
    case 6:
 | 
			
		||||
      this->model_hash = value.as_string();
 | 
			
		||||
      break;
 | 
			
		||||
    case 7:
 | 
			
		||||
      this->url = value.as_string();
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      this->external_wake_words.emplace_back();
 | 
			
		||||
      value.decode_to_message(this->external_wake_words.back());
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  for (auto &it : this->available_wake_words) {
 | 
			
		||||
    buffer.encode_message(1, it, true);
 | 
			
		||||
@@ -3011,5 +3137,53 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 1: {
 | 
			
		||||
      // Use raw data directly to avoid allocation
 | 
			
		||||
      this->data = value.data();
 | 
			
		||||
      this->data_len = value.size();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
void ZWaveProxyFrame::encode(ProtoWriteBuffer buffer) const { buffer.encode_bytes(1, this->data, this->data_len); }
 | 
			
		||||
void ZWaveProxyFrame::calculate_size(ProtoSize &size) const { size.add_length(1, this->data_len); }
 | 
			
		||||
bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      this->type = static_cast<enums::ZWaveProxyRequestType>(value.as_uint32());
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
bool ZWaveProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 2: {
 | 
			
		||||
      // Use raw data directly to avoid allocation
 | 
			
		||||
      this->data = value.data();
 | 
			
		||||
      this->data_len = value.size();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
void ZWaveProxyRequest::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_uint32(1, static_cast<uint32_t>(this->type));
 | 
			
		||||
  buffer.encode_bytes(2, this->data, this->data_len);
 | 
			
		||||
}
 | 
			
		||||
void ZWaveProxyRequest::calculate_size(ProtoSize &size) const {
 | 
			
		||||
  size.add_uint32(1, static_cast<uint32_t>(this->type));
 | 
			
		||||
  size.add_length(2, this->data_len);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::api
 | 
			
		||||
 
 | 
			
		||||
@@ -276,6 +276,13 @@ enum UpdateCommand : uint32_t {
 | 
			
		||||
  UPDATE_COMMAND_CHECK = 2,
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
enum ZWaveProxyRequestType : uint32_t {
 | 
			
		||||
  ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0,
 | 
			
		||||
  ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1,
 | 
			
		||||
  ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2,
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}  // namespace enums
 | 
			
		||||
 | 
			
		||||
@@ -324,11 +331,12 @@ class CommandProtoMessage : public ProtoDecodableMessage {
 | 
			
		||||
class HelloRequest final : public ProtoDecodableMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 1;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 17;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 27;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "hello_request"; }
 | 
			
		||||
#endif
 | 
			
		||||
  std::string client_info{};
 | 
			
		||||
  const uint8_t *client_info{nullptr};
 | 
			
		||||
  uint16_t client_info_len{0};
 | 
			
		||||
  uint32_t api_version_major{0};
 | 
			
		||||
  uint32_t api_version_minor{0};
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
@@ -360,14 +368,16 @@ class HelloResponse final : public ProtoMessage {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
};
 | 
			
		||||
class ConnectRequest final : public ProtoDecodableMessage {
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
class AuthenticationRequest final : public ProtoDecodableMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 3;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 9;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 19;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "connect_request"; }
 | 
			
		||||
  const char *message_name() const override { return "authentication_request"; }
 | 
			
		||||
#endif
 | 
			
		||||
  std::string password{};
 | 
			
		||||
  const uint8_t *password{nullptr};
 | 
			
		||||
  uint16_t password_len{0};
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
#endif
 | 
			
		||||
@@ -375,12 +385,12 @@ class ConnectRequest final : public ProtoDecodableMessage {
 | 
			
		||||
 protected:
 | 
			
		||||
  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 | 
			
		||||
};
 | 
			
		||||
class ConnectResponse final : public ProtoMessage {
 | 
			
		||||
class AuthenticationResponse final : public ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 4;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 2;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "connect_response"; }
 | 
			
		||||
  const char *message_name() const override { return "authentication_response"; }
 | 
			
		||||
#endif
 | 
			
		||||
  bool invalid_password{false};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
@@ -391,6 +401,7 @@ class ConnectResponse final : public ProtoMessage {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
class DisconnectRequest final : public ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 5;
 | 
			
		||||
@@ -490,7 +501,7 @@ class DeviceInfo final : public ProtoMessage {
 | 
			
		||||
class DeviceInfoResponse final : public ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 10;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 247;
 | 
			
		||||
  static constexpr uint16_t ESTIMATED_SIZE = 257;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "device_info_response"; }
 | 
			
		||||
#endif
 | 
			
		||||
@@ -550,6 +561,12 @@ class DeviceInfoResponse final : public ProtoMessage {
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_AREAS
 | 
			
		||||
  AreaInfo area{};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  uint32_t zwave_proxy_feature_flags{0};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  uint32_t zwave_home_id{0};
 | 
			
		||||
#endif
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
  void calculate_size(ProtoSize &size) const override;
 | 
			
		||||
@@ -1084,12 +1101,12 @@ class HomeassistantServiceMap final : public ProtoMessage {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
};
 | 
			
		||||
class HomeassistantServiceResponse final : public ProtoMessage {
 | 
			
		||||
class HomeassistantActionRequest final : public ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 35;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 113;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 128;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "homeassistant_service_response"; }
 | 
			
		||||
  const char *message_name() const override { return "homeassistant_action_request"; }
 | 
			
		||||
#endif
 | 
			
		||||
  StringRef service_ref_{};
 | 
			
		||||
  void set_service(const StringRef &ref) { this->service_ref_ = ref; }
 | 
			
		||||
@@ -1097,6 +1114,15 @@ class HomeassistantServiceResponse final : public ProtoMessage {
 | 
			
		||||
  std::vector<HomeassistantServiceMap> data_template{};
 | 
			
		||||
  std::vector<HomeassistantServiceMap> variables{};
 | 
			
		||||
  bool is_event{false};
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  uint32_t call_id{0};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  bool wants_response{false};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  std::string response_template{};
 | 
			
		||||
#endif
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
  void calculate_size(ProtoSize &size) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
@@ -1106,6 +1132,30 @@ class HomeassistantServiceResponse final : public ProtoMessage {
 | 
			
		||||
 protected:
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
class HomeassistantActionResponse final : public ProtoDecodableMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 130;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 34;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "homeassistant_action_response"; }
 | 
			
		||||
#endif
 | 
			
		||||
  uint32_t call_id{0};
 | 
			
		||||
  bool success{false};
 | 
			
		||||
  std::string error_message{};
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  const uint8_t *response_data{nullptr};
 | 
			
		||||
  uint16_t response_data_len{0};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 | 
			
		||||
  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
@@ -1174,12 +1224,13 @@ class GetTimeRequest final : public ProtoMessage {
 | 
			
		||||
class GetTimeResponse final : public ProtoDecodableMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 37;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 14;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 24;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "get_time_response"; }
 | 
			
		||||
#endif
 | 
			
		||||
  uint32_t epoch_seconds{0};
 | 
			
		||||
  std::string timezone{};
 | 
			
		||||
  const uint8_t *timezone{nullptr};
 | 
			
		||||
  uint16_t timezone_len{0};
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1971,14 +2022,15 @@ class BluetoothGATTReadResponse final : public ProtoMessage {
 | 
			
		||||
class BluetoothGATTWriteRequest final : public ProtoDecodableMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 75;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 19;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 29;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "bluetooth_gatt_write_request"; }
 | 
			
		||||
#endif
 | 
			
		||||
  uint64_t address{0};
 | 
			
		||||
  uint32_t handle{0};
 | 
			
		||||
  bool response{false};
 | 
			
		||||
  std::string data{};
 | 
			
		||||
  const uint8_t *data{nullptr};
 | 
			
		||||
  uint16_t data_len{0};
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
#endif
 | 
			
		||||
@@ -2006,13 +2058,14 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage {
 | 
			
		||||
class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 77;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 17;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 27;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; }
 | 
			
		||||
#endif
 | 
			
		||||
  uint64_t address{0};
 | 
			
		||||
  uint32_t handle{0};
 | 
			
		||||
  std::string data{};
 | 
			
		||||
  const uint8_t *data{nullptr};
 | 
			
		||||
  uint16_t data_len{0};
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
#endif
 | 
			
		||||
@@ -2437,18 +2490,37 @@ class VoiceAssistantWakeWord final : public ProtoMessage {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
};
 | 
			
		||||
class VoiceAssistantConfigurationRequest final : public ProtoMessage {
 | 
			
		||||
class VoiceAssistantExternalWakeWord final : public ProtoDecodableMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 121;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 0;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "voice_assistant_configuration_request"; }
 | 
			
		||||
#endif
 | 
			
		||||
  std::string id{};
 | 
			
		||||
  std::string wake_word{};
 | 
			
		||||
  std::vector<std::string> trained_languages{};
 | 
			
		||||
  std::string model_type{};
 | 
			
		||||
  uint32_t model_size{0};
 | 
			
		||||
  std::string model_hash{};
 | 
			
		||||
  std::string url{};
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 | 
			
		||||
  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 | 
			
		||||
};
 | 
			
		||||
class VoiceAssistantConfigurationRequest final : public ProtoDecodableMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 121;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 34;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "voice_assistant_configuration_request"; }
 | 
			
		||||
#endif
 | 
			
		||||
  std::vector<VoiceAssistantExternalWakeWord> external_wake_words{};
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 | 
			
		||||
};
 | 
			
		||||
class VoiceAssistantConfigurationResponse final : public ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
@@ -2911,5 +2983,45 @@ class UpdateCommandRequest final : public CommandProtoMessage {
 | 
			
		||||
  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
class ZWaveProxyFrame final : public ProtoDecodableMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 128;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 19;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "z_wave_proxy_frame"; }
 | 
			
		||||
#endif
 | 
			
		||||
  const uint8_t *data{nullptr};
 | 
			
		||||
  uint16_t data_len{0};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
  void calculate_size(ProtoSize &size) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 | 
			
		||||
};
 | 
			
		||||
class ZWaveProxyRequest final : public ProtoDecodableMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 129;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 21;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "z_wave_proxy_request"; }
 | 
			
		||||
#endif
 | 
			
		||||
  enums::ZWaveProxyRequestType type{};
 | 
			
		||||
  const uint8_t *data{nullptr};
 | 
			
		||||
  uint16_t data_len{0};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
  void calculate_size(ProtoSize &size) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 | 
			
		||||
  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::api
 | 
			
		||||
 
 | 
			
		||||
@@ -655,10 +655,26 @@ template<> const char *proto_enum_to_string<enums::UpdateCommand>(enums::UpdateC
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums::ZWaveProxyRequestType value) {
 | 
			
		||||
  switch (value) {
 | 
			
		||||
    case enums::ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE:
 | 
			
		||||
      return "ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE";
 | 
			
		||||
    case enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
 | 
			
		||||
      return "ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE";
 | 
			
		||||
    case enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE:
 | 
			
		||||
      return "ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE";
 | 
			
		||||
    default:
 | 
			
		||||
      return "UNKNOWN";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void HelloRequest::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "HelloRequest");
 | 
			
		||||
  dump_field(out, "client_info", this->client_info);
 | 
			
		||||
  out.append("  client_info: ");
 | 
			
		||||
  out.append(format_hex_pretty(this->client_info, this->client_info_len));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
  dump_field(out, "api_version_major", this->api_version_major);
 | 
			
		||||
  dump_field(out, "api_version_minor", this->api_version_minor);
 | 
			
		||||
}
 | 
			
		||||
@@ -669,8 +685,18 @@ void HelloResponse::dump_to(std::string &out) const {
 | 
			
		||||
  dump_field(out, "server_info", this->server_info_ref_);
 | 
			
		||||
  dump_field(out, "name", this->name_ref_);
 | 
			
		||||
}
 | 
			
		||||
void ConnectRequest::dump_to(std::string &out) const { dump_field(out, "password", this->password); }
 | 
			
		||||
void ConnectResponse::dump_to(std::string &out) const { dump_field(out, "invalid_password", this->invalid_password); }
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
void AuthenticationRequest::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "AuthenticationRequest");
 | 
			
		||||
  out.append("  password: ");
 | 
			
		||||
  out.append(format_hex_pretty(this->password, this->password_len));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
}
 | 
			
		||||
void AuthenticationResponse::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "AuthenticationResponse");
 | 
			
		||||
  dump_field(out, "invalid_password", this->invalid_password);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); }
 | 
			
		||||
void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); }
 | 
			
		||||
void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); }
 | 
			
		||||
@@ -749,6 +775,12 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
 | 
			
		||||
  this->area.dump_to(out);
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  dump_field(out, "zwave_proxy_feature_flags", this->zwave_proxy_feature_flags);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  dump_field(out, "zwave_home_id", this->zwave_home_id);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); }
 | 
			
		||||
void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); }
 | 
			
		||||
@@ -1071,8 +1103,8 @@ void HomeassistantServiceMap::dump_to(std::string &out) const {
 | 
			
		||||
  dump_field(out, "key", this->key_ref_);
 | 
			
		||||
  dump_field(out, "value", this->value);
 | 
			
		||||
}
 | 
			
		||||
void HomeassistantServiceResponse::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "HomeassistantServiceResponse");
 | 
			
		||||
void HomeassistantActionRequest::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "HomeassistantActionRequest");
 | 
			
		||||
  dump_field(out, "service", this->service_ref_);
 | 
			
		||||
  for (const auto &it : this->data) {
 | 
			
		||||
    out.append("  data: ");
 | 
			
		||||
@@ -1090,6 +1122,28 @@ void HomeassistantServiceResponse::dump_to(std::string &out) const {
 | 
			
		||||
    out.append("\n");
 | 
			
		||||
  }
 | 
			
		||||
  dump_field(out, "is_event", this->is_event);
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  dump_field(out, "call_id", this->call_id);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  dump_field(out, "wants_response", this->wants_response);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  dump_field(out, "response_template", this->response_template);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
void HomeassistantActionResponse::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "HomeassistantActionResponse");
 | 
			
		||||
  dump_field(out, "call_id", this->call_id);
 | 
			
		||||
  dump_field(out, "success", this->success);
 | 
			
		||||
  dump_field(out, "error_message", this->error_message);
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  out.append("  response_data: ");
 | 
			
		||||
  out.append(format_hex_pretty(this->response_data, this->response_data_len));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
@@ -1113,7 +1167,9 @@ void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeReques
 | 
			
		||||
void GetTimeResponse::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "GetTimeResponse");
 | 
			
		||||
  dump_field(out, "epoch_seconds", this->epoch_seconds);
 | 
			
		||||
  dump_field(out, "timezone", this->timezone);
 | 
			
		||||
  out.append("  timezone: ");
 | 
			
		||||
  out.append(format_hex_pretty(this->timezone, this->timezone_len));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
}
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
void ListEntitiesServicesArgument::dump_to(std::string &out) const {
 | 
			
		||||
@@ -1626,7 +1682,7 @@ void BluetoothGATTWriteRequest::dump_to(std::string &out) const {
 | 
			
		||||
  dump_field(out, "handle", this->handle);
 | 
			
		||||
  dump_field(out, "response", this->response);
 | 
			
		||||
  out.append("  data: ");
 | 
			
		||||
  out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size()));
 | 
			
		||||
  out.append(format_hex_pretty(this->data, this->data_len));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
}
 | 
			
		||||
void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const {
 | 
			
		||||
@@ -1639,7 +1695,7 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const {
 | 
			
		||||
  dump_field(out, "address", this->address);
 | 
			
		||||
  dump_field(out, "handle", this->handle);
 | 
			
		||||
  out.append("  data: ");
 | 
			
		||||
  out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size()));
 | 
			
		||||
  out.append(format_hex_pretty(this->data, this->data_len));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
}
 | 
			
		||||
void BluetoothGATTNotifyRequest::dump_to(std::string &out) const {
 | 
			
		||||
@@ -1792,8 +1848,25 @@ void VoiceAssistantWakeWord::dump_to(std::string &out) const {
 | 
			
		||||
    dump_field(out, "trained_languages", it, 4);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void VoiceAssistantExternalWakeWord::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "VoiceAssistantExternalWakeWord");
 | 
			
		||||
  dump_field(out, "id", this->id);
 | 
			
		||||
  dump_field(out, "wake_word", this->wake_word);
 | 
			
		||||
  for (const auto &it : this->trained_languages) {
 | 
			
		||||
    dump_field(out, "trained_languages", it, 4);
 | 
			
		||||
  }
 | 
			
		||||
  dump_field(out, "model_type", this->model_type);
 | 
			
		||||
  dump_field(out, "model_size", this->model_size);
 | 
			
		||||
  dump_field(out, "model_hash", this->model_hash);
 | 
			
		||||
  dump_field(out, "url", this->url);
 | 
			
		||||
}
 | 
			
		||||
void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const {
 | 
			
		||||
  out.append("VoiceAssistantConfigurationRequest {}");
 | 
			
		||||
  MessageDumpHelper helper(out, "VoiceAssistantConfigurationRequest");
 | 
			
		||||
  for (const auto &it : this->external_wake_words) {
 | 
			
		||||
    out.append("  external_wake_words: ");
 | 
			
		||||
    it.dump_to(out);
 | 
			
		||||
    out.append("\n");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "VoiceAssistantConfigurationResponse");
 | 
			
		||||
@@ -2102,6 +2175,21 @@ void UpdateCommandRequest::dump_to(std::string &out) const {
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
void ZWaveProxyFrame::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "ZWaveProxyFrame");
 | 
			
		||||
  out.append("  data: ");
 | 
			
		||||
  out.append(format_hex_pretty(this->data, this->data_len));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
}
 | 
			
		||||
void ZWaveProxyRequest::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "ZWaveProxyRequest");
 | 
			
		||||
  dump_field(out, "type", static_cast<enums::ZWaveProxyRequestType>(this->type));
 | 
			
		||||
  out.append("  data: ");
 | 
			
		||||
  out.append(format_hex_pretty(this->data, this->data_len));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::api
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -24,15 +24,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
      this->on_hello_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ConnectRequest::MESSAGE_TYPE: {
 | 
			
		||||
      ConnectRequest msg;
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
    case AuthenticationRequest::MESSAGE_TYPE: {
 | 
			
		||||
      AuthenticationRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_connect_request: %s", msg.dump().c_str());
 | 
			
		||||
      ESP_LOGVV(TAG, "on_authentication_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_connect_request(msg);
 | 
			
		||||
      this->on_authentication_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    case DisconnectRequest::MESSAGE_TYPE: {
 | 
			
		||||
      DisconnectRequest msg;
 | 
			
		||||
      // Empty message: no decode needed
 | 
			
		||||
@@ -546,7 +548,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
    case VoiceAssistantConfigurationRequest::MESSAGE_TYPE: {
 | 
			
		||||
      VoiceAssistantConfigurationRequest msg;
 | 
			
		||||
      // Empty message: no decode needed
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
@@ -586,6 +588,39 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
      this->on_bluetooth_scanner_set_mode_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
    case ZWaveProxyFrame::MESSAGE_TYPE: {
 | 
			
		||||
      ZWaveProxyFrame msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_z_wave_proxy_frame: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_z_wave_proxy_frame(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
    case ZWaveProxyRequest::MESSAGE_TYPE: {
 | 
			
		||||
      ZWaveProxyRequest msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_z_wave_proxy_request: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_z_wave_proxy_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
    case HomeassistantActionResponse::MESSAGE_TYPE: {
 | 
			
		||||
      HomeassistantActionResponse msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_homeassistant_action_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_homeassistant_action_response(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
@@ -597,11 +632,13 @@ void APIServerConnection::on_hello_request(const HelloRequest &msg) {
 | 
			
		||||
    this->on_fatal_error();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_connect_request(const ConnectRequest &msg) {
 | 
			
		||||
  if (!this->send_connect_response(msg)) {
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
void APIServerConnection::on_authentication_request(const AuthenticationRequest &msg) {
 | 
			
		||||
  if (!this->send_authenticate_response(msg)) {
 | 
			
		||||
    this->on_fatal_error();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) {
 | 
			
		||||
  if (!this->send_disconnect_response(msg)) {
 | 
			
		||||
    this->on_fatal_error();
 | 
			
		||||
@@ -613,241 +650,139 @@ void APIServerConnection::on_ping_request(const PingRequest &msg) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
 | 
			
		||||
  if (this->check_connection_setup_() && !this->send_device_info_response(msg)) {
 | 
			
		||||
  if (!this->send_device_info_response(msg)) {
 | 
			
		||||
    this->on_fatal_error();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->list_entities(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { this->list_entities(msg); }
 | 
			
		||||
void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->subscribe_states(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->subscribe_logs(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->subscribe_states(msg);
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->subscribe_logs(msg); }
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
void APIServerConnection::on_subscribe_homeassistant_services_request(
 | 
			
		||||
    const SubscribeHomeassistantServicesRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->subscribe_homeassistant_services(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->subscribe_homeassistant_services(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->subscribe_home_assistant_states(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->subscribe_home_assistant_states(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->execute_service(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_() && !this->send_noise_encryption_set_key_response(msg)) {
 | 
			
		||||
  if (!this->send_noise_encryption_set_key_response(msg)) {
 | 
			
		||||
    this->on_fatal_error();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->button_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { this->button_command(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_CAMERA
 | 
			
		||||
void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->camera_image(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) { this->camera_image(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->climate_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) { this->climate_command(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_COVER
 | 
			
		||||
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->cover_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { this->cover_command(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_DATE
 | 
			
		||||
void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->date_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) { this->date_command(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_DATETIME
 | 
			
		||||
void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->datetime_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->datetime_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->fan_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { this->fan_command(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->light_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { this->light_command(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->lock_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { this->lock_command(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->media_player_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->media_player_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->number_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { this->number_command(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->select_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { this->select_command(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SIREN
 | 
			
		||||
void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->siren_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) { this->siren_command(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->switch_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { this->switch_command(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT
 | 
			
		||||
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->text_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { this->text_command(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_TIME
 | 
			
		||||
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->time_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { this->time_command(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_UPDATE
 | 
			
		||||
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->update_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { this->update_command(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VALVE
 | 
			
		||||
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->valve_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { this->valve_command(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
 | 
			
		||||
    const SubscribeBluetoothLEAdvertisementsRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->subscribe_bluetooth_le_advertisements(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->subscribe_bluetooth_le_advertisements(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_device_request(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->bluetooth_device_request(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_gatt_get_services(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->bluetooth_gatt_get_services(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_gatt_read(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->bluetooth_gatt_read(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_gatt_write(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->bluetooth_gatt_write(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_gatt_read_descriptor(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->bluetooth_gatt_read_descriptor(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_gatt_write_descriptor(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->bluetooth_gatt_write_descriptor(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_gatt_notify(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->bluetooth_gatt_notify(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
 | 
			
		||||
    const SubscribeBluetoothConnectionsFreeRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_() && !this->send_subscribe_bluetooth_connections_free_response(msg)) {
 | 
			
		||||
  if (!this->send_subscribe_bluetooth_connections_free_response(msg)) {
 | 
			
		||||
    this->on_fatal_error();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -855,45 +790,68 @@ void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request(
 | 
			
		||||
    const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->unsubscribe_bluetooth_le_advertisements(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->unsubscribe_bluetooth_le_advertisements(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_scanner_set_mode(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->bluetooth_scanner_set_mode(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->subscribe_voice_assistant(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->subscribe_voice_assistant(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_() && !this->send_voice_assistant_get_configuration_response(msg)) {
 | 
			
		||||
  if (!this->send_voice_assistant_get_configuration_response(msg)) {
 | 
			
		||||
    this->on_fatal_error();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->voice_assistant_set_configuration(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->voice_assistant_set_configuration(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ALARM_CONTROL_PANEL
 | 
			
		||||
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->alarm_control_panel_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
  this->alarm_control_panel_command(msg);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { this->zwave_proxy_frame(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
 | 
			
		||||
  // Check authentication/connection requirements for messages
 | 
			
		||||
  switch (msg_type) {
 | 
			
		||||
    case HelloRequest::MESSAGE_TYPE:  // No setup required
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
    case AuthenticationRequest::MESSAGE_TYPE:  // No setup required
 | 
			
		||||
#endif
 | 
			
		||||
    case DisconnectRequest::MESSAGE_TYPE:  // No setup required
 | 
			
		||||
    case PingRequest::MESSAGE_TYPE:        // No setup required
 | 
			
		||||
      break;                               // Skip all checks for these messages
 | 
			
		||||
    case DeviceInfoRequest::MESSAGE_TYPE:  // Connection setup only
 | 
			
		||||
      if (!this->check_connection_setup_()) {
 | 
			
		||||
        return;  // Connection not setup
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      // All other messages require authentication (which includes connection check)
 | 
			
		||||
      if (!this->check_authenticated_()) {
 | 
			
		||||
        return;  // Authentication failed
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Call base implementation to process the message
 | 
			
		||||
  APIServerConnectionBase::read_message(msg_size, msg_type, msg_data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::api
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,9 @@ class APIServerConnectionBase : public ProtoService {
 | 
			
		||||
 | 
			
		||||
  virtual void on_hello_request(const HelloRequest &value){};
 | 
			
		||||
 | 
			
		||||
  virtual void on_connect_request(const ConnectRequest &value){};
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
  virtual void on_authentication_request(const AuthenticationRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  virtual void on_disconnect_request(const DisconnectRequest &value){};
 | 
			
		||||
  virtual void on_disconnect_response(const DisconnectResponse &value){};
 | 
			
		||||
@@ -64,6 +66,9 @@ class APIServerConnectionBase : public ProtoService {
 | 
			
		||||
  virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
  virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
@@ -205,6 +210,12 @@ class APIServerConnectionBase : public ProtoService {
 | 
			
		||||
 | 
			
		||||
#ifdef USE_UPDATE
 | 
			
		||||
  virtual void on_update_command_request(const UpdateCommandRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  virtual void on_z_wave_proxy_frame(const ZWaveProxyFrame &value){};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
 protected:
 | 
			
		||||
  void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
 | 
			
		||||
@@ -213,7 +224,9 @@ class APIServerConnectionBase : public ProtoService {
 | 
			
		||||
class APIServerConnection : public APIServerConnectionBase {
 | 
			
		||||
 public:
 | 
			
		||||
  virtual bool send_hello_response(const HelloRequest &msg) = 0;
 | 
			
		||||
  virtual bool send_connect_response(const ConnectRequest &msg) = 0;
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
  virtual bool send_authenticate_response(const AuthenticationRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
  virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0;
 | 
			
		||||
  virtual bool send_ping_response(const PingRequest &msg) = 0;
 | 
			
		||||
  virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0;
 | 
			
		||||
@@ -331,10 +344,18 @@ class APIServerConnection : public APIServerConnectionBase {
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ALARM_CONTROL_PANEL
 | 
			
		||||
  virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  virtual void zwave_proxy_frame(const ZWaveProxyFrame &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  virtual void zwave_proxy_request(const ZWaveProxyRequest &msg) = 0;
 | 
			
		||||
#endif
 | 
			
		||||
 protected:
 | 
			
		||||
  void on_hello_request(const HelloRequest &msg) override;
 | 
			
		||||
  void on_connect_request(const ConnectRequest &msg) override;
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
  void on_authentication_request(const AuthenticationRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
  void on_disconnect_request(const DisconnectRequest &msg) override;
 | 
			
		||||
  void on_ping_request(const PingRequest &msg) override;
 | 
			
		||||
  void on_device_info_request(const DeviceInfoRequest &msg) override;
 | 
			
		||||
@@ -453,6 +474,13 @@ class APIServerConnection : public APIServerConnectionBase {
 | 
			
		||||
#ifdef USE_ALARM_CONTROL_PANEL
 | 
			
		||||
  void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
  void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::api
 | 
			
		||||
 
 | 
			
		||||
@@ -9,12 +9,16 @@
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/util.h"
 | 
			
		||||
#include "esphome/core/version.h"
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
#include "homeassistant_service.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
#include "esphome/components/logger/logger.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace esphome::api {
 | 
			
		||||
 | 
			
		||||
@@ -37,12 +41,14 @@ void APIServer::setup() {
 | 
			
		||||
 | 
			
		||||
  this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(hash, true);
 | 
			
		||||
 | 
			
		||||
#ifndef USE_API_NOISE_PSK_FROM_YAML
 | 
			
		||||
  // Only load saved PSK if not set from YAML
 | 
			
		||||
  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
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Schedule reboot if no clients connect within timeout
 | 
			
		||||
@@ -85,7 +91,7 @@ void APIServer::setup() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  err = this->socket_->listen(4);
 | 
			
		||||
  err = this->socket_->listen(this->listen_backlog_);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
@@ -138,9 +144,19 @@ void APIServer::loop() {
 | 
			
		||||
    while (true) {
 | 
			
		||||
      struct sockaddr_storage source_addr;
 | 
			
		||||
      socklen_t addr_len = sizeof(source_addr);
 | 
			
		||||
 | 
			
		||||
      auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
 | 
			
		||||
      if (!sock)
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      // Check if we're at the connection limit
 | 
			
		||||
      if (this->clients_.size() >= this->max_connections_) {
 | 
			
		||||
        ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, sock->getpeername().c_str());
 | 
			
		||||
        // Immediately close - socket destructor will handle cleanup
 | 
			
		||||
        sock.reset();
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str());
 | 
			
		||||
 | 
			
		||||
      auto *conn = new APIConnection(std::move(sock), this);
 | 
			
		||||
@@ -165,7 +181,8 @@ void APIServer::loop() {
 | 
			
		||||
    // Network is down - disconnect all clients
 | 
			
		||||
    for (auto &client : this->clients_) {
 | 
			
		||||
      client->on_fatal_error();
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str());
 | 
			
		||||
      ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(),
 | 
			
		||||
               client->client_info_.peername.c_str());
 | 
			
		||||
    }
 | 
			
		||||
    // Continue to process and clean up the clients below
 | 
			
		||||
  }
 | 
			
		||||
@@ -204,8 +221,10 @@ void APIServer::loop() {
 | 
			
		||||
void APIServer::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG,
 | 
			
		||||
                "Server:\n"
 | 
			
		||||
                "  Address: %s:%u",
 | 
			
		||||
                network::get_use_address().c_str(), this->port_);
 | 
			
		||||
                "  Address: %s:%u\n"
 | 
			
		||||
                "  Listen backlog: %u\n"
 | 
			
		||||
                "  Max connections: %u",
 | 
			
		||||
                network::get_use_address().c_str(), this->port_, this->listen_backlog_, this->max_connections_);
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
 | 
			
		||||
  if (!this->noise_ctx_->has_psk()) {
 | 
			
		||||
@@ -217,12 +236,12 @@ void APIServer::dump_config() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
bool APIServer::check_password(const std::string &password) const {
 | 
			
		||||
bool APIServer::check_password(const uint8_t *password_data, size_t password_len) const {
 | 
			
		||||
  // depend only on input password length
 | 
			
		||||
  const char *a = this->password_.c_str();
 | 
			
		||||
  uint32_t len_a = this->password_.length();
 | 
			
		||||
  const char *b = password.c_str();
 | 
			
		||||
  uint32_t len_b = password.length();
 | 
			
		||||
  const char *b = reinterpret_cast<const char *>(password_data);
 | 
			
		||||
  uint32_t len_b = password_len;
 | 
			
		||||
 | 
			
		||||
  // disable optimization with volatile
 | 
			
		||||
  volatile uint32_t length = len_b;
 | 
			
		||||
@@ -245,6 +264,7 @@ bool APIServer::check_password(const std::string &password) const {
 | 
			
		||||
 | 
			
		||||
  return result == 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void APIServer::handle_disconnect(APIConnection *conn) {}
 | 
			
		||||
@@ -355,6 +375,15 @@ void APIServer::on_update(update::UpdateEntity *obj) {
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) {
 | 
			
		||||
  // We could add code to manage a second subscription type, but, since this message type is
 | 
			
		||||
  //  very infrequent and small, we simply send it to all clients
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
    c->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ALARM_CONTROL_PANEL
 | 
			
		||||
API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
 | 
			
		||||
#endif
 | 
			
		||||
@@ -370,12 +399,43 @@ void APIServer::set_password(const std::string &password) { this->password_ = pa
 | 
			
		||||
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
 | 
			
		||||
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
 | 
			
		||||
  for (auto &client : this->clients_) {
 | 
			
		||||
    client->send_homeassistant_service_call(call);
 | 
			
		||||
    client->send_homeassistant_action(call);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) {
 | 
			
		||||
  this->action_response_callbacks_.push_back({call_id, std::move(callback)});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message) {
 | 
			
		||||
  for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
 | 
			
		||||
    if (it->call_id == call_id) {
 | 
			
		||||
      auto callback = std::move(it->callback);
 | 
			
		||||
      this->action_response_callbacks_.erase(it);
 | 
			
		||||
      ActionResponse response(success, error_message);
 | 
			
		||||
      callback(response);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
 | 
			
		||||
                                       const uint8_t *response_data, size_t response_data_len) {
 | 
			
		||||
  for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
 | 
			
		||||
    if (it->call_id == call_id) {
 | 
			
		||||
      auto callback = std::move(it->callback);
 | 
			
		||||
      this->action_response_callbacks_.erase(it);
 | 
			
		||||
      ActionResponse response(success, error_message, response_data, response_data_len);
 | 
			
		||||
      callback(response);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
 | 
			
		||||
@@ -409,6 +469,12 @@ void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeo
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
 | 
			
		||||
#ifdef USE_API_NOISE_PSK_FROM_YAML
 | 
			
		||||
  // When PSK is set from YAML, this function should never be called
 | 
			
		||||
  // but if it is, reject the change
 | 
			
		||||
  ESP_LOGW(TAG, "Key set in YAML");
 | 
			
		||||
  return false;
 | 
			
		||||
#else
 | 
			
		||||
  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");
 | 
			
		||||
@@ -437,6 +503,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@
 | 
			
		||||
#include "user_services.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include <map>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome::api {
 | 
			
		||||
@@ -37,13 +38,15 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
  void on_shutdown() override;
 | 
			
		||||
  bool teardown() override;
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
  bool check_password(const std::string &password) const;
 | 
			
		||||
  bool check_password(const uint8_t *password_data, size_t password_len) const;
 | 
			
		||||
  void set_password(const std::string &password);
 | 
			
		||||
#endif
 | 
			
		||||
  void set_port(uint16_t port);
 | 
			
		||||
  void set_reboot_timeout(uint32_t reboot_timeout);
 | 
			
		||||
  void set_batch_delay(uint16_t batch_delay);
 | 
			
		||||
  uint16_t get_batch_delay() const { return batch_delay_; }
 | 
			
		||||
  void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; }
 | 
			
		||||
  void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
 | 
			
		||||
 | 
			
		||||
  // Get reference to shared buffer for API connections
 | 
			
		||||
  std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; }
 | 
			
		||||
@@ -107,8 +110,19 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
  void on_media_player_update(media_player::MediaPlayer *obj) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
  void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
 | 
			
		||||
#endif
 | 
			
		||||
  void send_homeassistant_action(const HomeassistantActionRequest &call);
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  // Action response handling
 | 
			
		||||
  using ActionResponseCallback = std::function<void(const class ActionResponse &)>;
 | 
			
		||||
  void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback);
 | 
			
		||||
  void handle_action_response(uint32_t call_id, bool success, const std::string &error_message);
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  void handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
 | 
			
		||||
                              const uint8_t *response_data, size_t response_data_len);
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
  void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
 | 
			
		||||
#endif
 | 
			
		||||
@@ -125,6 +139,9 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
#ifdef USE_UPDATE
 | 
			
		||||
  void on_update(update::UpdateEntity *obj) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  bool is_connected() const;
 | 
			
		||||
 | 
			
		||||
@@ -181,12 +198,23 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
  std::vector<UserServiceDescriptor *> user_services_;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  struct PendingActionResponse {
 | 
			
		||||
    uint32_t call_id;
 | 
			
		||||
    ActionResponseCallback callback;
 | 
			
		||||
  };
 | 
			
		||||
  std::vector<PendingActionResponse> action_response_callbacks_;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Group smaller types together
 | 
			
		||||
  uint16_t port_{6053};
 | 
			
		||||
  uint16_t batch_delay_{100};
 | 
			
		||||
  // Connection limits - these defaults will be overridden by config values
 | 
			
		||||
  // from cv.SplitDefault in __init__.py which sets platform-specific defaults
 | 
			
		||||
  uint8_t listen_backlog_{4};
 | 
			
		||||
  uint8_t max_connections_{8};
 | 
			
		||||
  bool shutting_down_ = false;
 | 
			
		||||
  // 5 bytes used, 3 bytes padding
 | 
			
		||||
  // 7 bytes used, 1 byte padding
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
 | 
			
		||||
 
 | 
			
		||||
@@ -179,9 +179,9 @@ class CustomAPIDevice {
 | 
			
		||||
   * @param service_name The service to call.
 | 
			
		||||
   */
 | 
			
		||||
  void call_homeassistant_service(const std::string &service_name) {
 | 
			
		||||
    HomeassistantServiceResponse resp;
 | 
			
		||||
    HomeassistantActionRequest resp;
 | 
			
		||||
    resp.set_service(StringRef(service_name));
 | 
			
		||||
    global_api_server->send_homeassistant_service_call(resp);
 | 
			
		||||
    global_api_server->send_homeassistant_action(resp);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Call a Home Assistant service from ESPHome.
 | 
			
		||||
@@ -199,7 +199,7 @@ class CustomAPIDevice {
 | 
			
		||||
   * @param data The data for the service call, mapping from string to string.
 | 
			
		||||
   */
 | 
			
		||||
  void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
 | 
			
		||||
    HomeassistantServiceResponse resp;
 | 
			
		||||
    HomeassistantActionRequest resp;
 | 
			
		||||
    resp.set_service(StringRef(service_name));
 | 
			
		||||
    for (auto &it : data) {
 | 
			
		||||
      resp.data.emplace_back();
 | 
			
		||||
@@ -207,7 +207,7 @@ class CustomAPIDevice {
 | 
			
		||||
      kv.set_key(StringRef(it.first));
 | 
			
		||||
      kv.value = it.second;
 | 
			
		||||
    }
 | 
			
		||||
    global_api_server->send_homeassistant_service_call(resp);
 | 
			
		||||
    global_api_server->send_homeassistant_action(resp);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Fire an ESPHome event in Home Assistant.
 | 
			
		||||
@@ -221,10 +221,10 @@ class CustomAPIDevice {
 | 
			
		||||
   * @param event_name The event to fire.
 | 
			
		||||
   */
 | 
			
		||||
  void fire_homeassistant_event(const std::string &event_name) {
 | 
			
		||||
    HomeassistantServiceResponse resp;
 | 
			
		||||
    HomeassistantActionRequest resp;
 | 
			
		||||
    resp.set_service(StringRef(event_name));
 | 
			
		||||
    resp.is_event = true;
 | 
			
		||||
    global_api_server->send_homeassistant_service_call(resp);
 | 
			
		||||
    global_api_server->send_homeassistant_action(resp);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Fire an ESPHome event in Home Assistant.
 | 
			
		||||
@@ -241,7 +241,7 @@ class CustomAPIDevice {
 | 
			
		||||
   * @param data The data for the event, mapping from string to string.
 | 
			
		||||
   */
 | 
			
		||||
  void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
 | 
			
		||||
    HomeassistantServiceResponse resp;
 | 
			
		||||
    HomeassistantActionRequest resp;
 | 
			
		||||
    resp.set_service(StringRef(service_name));
 | 
			
		||||
    resp.is_event = true;
 | 
			
		||||
    for (auto &it : data) {
 | 
			
		||||
@@ -250,7 +250,7 @@ class CustomAPIDevice {
 | 
			
		||||
      kv.set_key(StringRef(it.first));
 | 
			
		||||
      kv.value = it.second;
 | 
			
		||||
    }
 | 
			
		||||
    global_api_server->send_homeassistant_service_call(resp);
 | 
			
		||||
    global_api_server->send_homeassistant_action(resp);
 | 
			
		||||
  }
 | 
			
		||||
#else
 | 
			
		||||
  template<typename T = void> void call_homeassistant_service(const std::string &service_name) {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,15 @@
 | 
			
		||||
#include "api_server.h"
 | 
			
		||||
#ifdef USE_API
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include "api_pb2.h"
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
#include "esphome/components/json/json_util.h"
 | 
			
		||||
#endif
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome::api {
 | 
			
		||||
 | 
			
		||||
@@ -44,9 +49,47 @@ template<typename... Ts> class TemplatableKeyValuePair {
 | 
			
		||||
  TemplatableStringValue<Ts...> value;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
// Represents the response data from a Home Assistant action
 | 
			
		||||
class ActionResponse {
 | 
			
		||||
 public:
 | 
			
		||||
  ActionResponse(bool success, std::string error_message = "")
 | 
			
		||||
      : success_(success), error_message_(std::move(error_message)) {}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len)
 | 
			
		||||
      : success_(success), error_message_(std::move(error_message)) {
 | 
			
		||||
    if (data == nullptr || data_len == 0)
 | 
			
		||||
      return;
 | 
			
		||||
    this->json_document_ = json::parse_json(data, data_len);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  bool is_success() const { return this->success_; }
 | 
			
		||||
  const std::string &get_error_message() const { return this->error_message_; }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  // Get data as parsed JSON object (const version returns read-only view)
 | 
			
		||||
  JsonObjectConst get_json() const { return this->json_document_.as<JsonObjectConst>(); }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool success_;
 | 
			
		||||
  std::string error_message_;
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  JsonDocument json_document_;
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Callback type for action responses
 | 
			
		||||
template<typename... Ts> using ActionResponseCallback = std::function<void(const ActionResponse &, Ts...)>;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {}
 | 
			
		||||
  explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent) {
 | 
			
		||||
    this->flags_.is_event = is_event;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  template<typename T> void set_service(T service) { this->service_ = service; }
 | 
			
		||||
 | 
			
		||||
@@ -61,11 +104,29 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
 | 
			
		||||
    this->variables_.emplace_back(std::move(key), value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  template<typename T> void set_response_template(T response_template) {
 | 
			
		||||
    this->response_template_ = response_template;
 | 
			
		||||
    this->flags_.has_response_template = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_wants_status() { this->flags_.wants_status = true; }
 | 
			
		||||
  void set_wants_response() { this->flags_.wants_response = true; }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() const {
 | 
			
		||||
    return this->success_trigger_with_response_;
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  Trigger<Ts...> *get_success_trigger() const { return this->success_trigger_; }
 | 
			
		||||
  Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    HomeassistantServiceResponse resp;
 | 
			
		||||
    HomeassistantActionRequest resp;
 | 
			
		||||
    std::string service_value = this->service_.value(x...);
 | 
			
		||||
    resp.set_service(StringRef(service_value));
 | 
			
		||||
    resp.is_event = this->is_event_;
 | 
			
		||||
    resp.is_event = this->flags_.is_event;
 | 
			
		||||
    for (auto &it : this->data_) {
 | 
			
		||||
      resp.data.emplace_back();
 | 
			
		||||
      auto &kv = resp.data.back();
 | 
			
		||||
@@ -84,18 +145,74 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
 | 
			
		||||
      kv.set_key(StringRef(it.key));
 | 
			
		||||
      kv.value = it.value.value(x...);
 | 
			
		||||
    }
 | 
			
		||||
    this->parent_->send_homeassistant_service_call(resp);
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
    if (this->flags_.wants_status) {
 | 
			
		||||
      // Generate a unique call ID for this service call
 | 
			
		||||
      static uint32_t call_id_counter = 1;
 | 
			
		||||
      uint32_t call_id = call_id_counter++;
 | 
			
		||||
      resp.call_id = call_id;
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
      if (this->flags_.wants_response) {
 | 
			
		||||
        resp.wants_response = true;
 | 
			
		||||
        // Set response template if provided
 | 
			
		||||
        if (this->flags_.has_response_template) {
 | 
			
		||||
          std::string response_template_value = this->response_template_.value(x...);
 | 
			
		||||
          resp.response_template = response_template_value;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
      auto captured_args = std::make_tuple(x...);
 | 
			
		||||
      this->parent_->register_action_response_callback(call_id, [this, captured_args](const ActionResponse &response) {
 | 
			
		||||
        std::apply(
 | 
			
		||||
            [this, &response](auto &&...args) {
 | 
			
		||||
              if (response.is_success()) {
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
                if (this->flags_.wants_response) {
 | 
			
		||||
                  this->success_trigger_with_response_->trigger(response.get_json(), args...);
 | 
			
		||||
                } else
 | 
			
		||||
#endif
 | 
			
		||||
                {
 | 
			
		||||
                  this->success_trigger_->trigger(args...);
 | 
			
		||||
                }
 | 
			
		||||
              } else {
 | 
			
		||||
                this->error_trigger_->trigger(response.get_error_message(), args...);
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
            captured_args);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    this->parent_->send_homeassistant_action(resp);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  APIServer *parent_;
 | 
			
		||||
  bool is_event_;
 | 
			
		||||
  TemplatableStringValue<Ts...> service_{};
 | 
			
		||||
  std::vector<TemplatableKeyValuePair<Ts...>> data_;
 | 
			
		||||
  std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
 | 
			
		||||
  std::vector<TemplatableKeyValuePair<Ts...>> variables_;
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  TemplatableStringValue<Ts...> response_template_{""};
 | 
			
		||||
  Trigger<JsonObjectConst, Ts...> *success_trigger_with_response_ = new Trigger<JsonObjectConst, Ts...>();
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  Trigger<Ts...> *success_trigger_ = new Trigger<Ts...>();
 | 
			
		||||
  Trigger<std::string, Ts...> *error_trigger_ = new Trigger<std::string, Ts...>();
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
 | 
			
		||||
  struct Flags {
 | 
			
		||||
    uint8_t is_event : 1;
 | 
			
		||||
    uint8_t wants_status : 1;
 | 
			
		||||
    uint8_t wants_response : 1;
 | 
			
		||||
    uint8_t has_response_template : 1;
 | 
			
		||||
    uint8_t reserved : 5;
 | 
			
		||||
  } flags_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::api
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -182,6 +182,10 @@ class ProtoLengthDelimited {
 | 
			
		||||
  explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
 | 
			
		||||
  std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
 | 
			
		||||
 | 
			
		||||
  // Direct access to raw data without string allocation
 | 
			
		||||
  const uint8_t *data() const { return this->value_; }
 | 
			
		||||
  size_t size() const { return this->length_; }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Decode the length-delimited data into an existing ProtoDecodableMessage instance.
 | 
			
		||||
   *
 | 
			
		||||
@@ -827,7 +831,7 @@ class ProtoService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Authentication helper methods
 | 
			
		||||
  bool check_connection_setup_() {
 | 
			
		||||
  inline bool check_connection_setup_() {
 | 
			
		||||
    if (!this->is_connection_setup()) {
 | 
			
		||||
      this->on_no_setup_connection();
 | 
			
		||||
      return false;
 | 
			
		||||
@@ -835,7 +839,7 @@ class ProtoService {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool check_authenticated_() {
 | 
			
		||||
  inline bool check_authenticated_() {
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
    if (!this->check_connection_setup_()) {
 | 
			
		||||
      return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
 | 
			
		||||
    msg.set_name(StringRef(this->name_));
 | 
			
		||||
    msg.key = this->key_;
 | 
			
		||||
    std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
 | 
			
		||||
    for (int i = 0; i < sizeof...(Ts); i++) {
 | 
			
		||||
    for (size_t i = 0; i < sizeof...(Ts); i++) {
 | 
			
		||||
      msg.args.emplace_back();
 | 
			
		||||
      auto &arg = msg.args.back();
 | 
			
		||||
      arg.type = arg_types[i];
 | 
			
		||||
@@ -55,7 +55,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  virtual void execute(Ts... x) = 0;
 | 
			
		||||
  template<int... S> void execute_(std::vector<ExecuteServiceArgument> args, seq<S...> type) {
 | 
			
		||||
  template<int... S> void execute_(const std::vector<ExecuteServiceArgument> &args, seq<S...> type) {
 | 
			
		||||
    this->execute((get_execute_arg_value<Ts>(args[S]))...);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_CLEAR,
 | 
			
		||||
    CONF_GAIN,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    DEVICE_CLASS_ILLUMINANCE,
 | 
			
		||||
@@ -29,7 +30,6 @@ CONF_F5 = "f5"
 | 
			
		||||
CONF_F6 = "f6"
 | 
			
		||||
CONF_F7 = "f7"
 | 
			
		||||
CONF_F8 = "f8"
 | 
			
		||||
CONF_CLEAR = "clear"
 | 
			
		||||
CONF_NIR = "nir"
 | 
			
		||||
 | 
			
		||||
UNIT_COUNTS = "#"
 | 
			
		||||
 
 | 
			
		||||
@@ -165,4 +165,4 @@ def final_validate_audio_schema(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add_library("esphome/esp-audio-libs", "1.1.4")
 | 
			
		||||
    cg.add_library("esphome/esp-audio-libs", "2.0.1")
 | 
			
		||||
 
 | 
			
		||||
@@ -57,7 +57,7 @@ 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) {
 | 
			
		||||
  // Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same.
 | 
			
		||||
  for (int i = 0; i < samples_to_scale; i++) {
 | 
			
		||||
  for (size_t i = 0; i < samples_to_scale; i++) {
 | 
			
		||||
    int32_t acc = (int32_t) audio_samples[i] * (int32_t) scale_factor;
 | 
			
		||||
    output_buffer[i] = (int16_t) (acc >> 15);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -229,18 +229,18 @@ FileDecoderState AudioDecoder::decode_flac_() {
 | 
			
		||||
    auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(),
 | 
			
		||||
                                                   this->input_transfer_buffer_->available());
 | 
			
		||||
 | 
			
		||||
    if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
 | 
			
		||||
      return FileDecoderState::POTENTIALLY_FAILED;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (result != esp_audio_libs::flac::FLAC_DECODER_SUCCESS) {
 | 
			
		||||
      // Couldn't read FLAC header
 | 
			
		||||
    if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
 | 
			
		||||
      // Serrious error reading FLAC header, there is no recovery
 | 
			
		||||
      return FileDecoderState::FAILED;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
 | 
			
		||||
    this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed);
 | 
			
		||||
 | 
			
		||||
    if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
 | 
			
		||||
      return FileDecoderState::MORE_TO_PROCESS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Reallocate the output transfer buffer to the smallest necessary size
 | 
			
		||||
    this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
 | 
			
		||||
    if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
 | 
			
		||||
@@ -256,9 +256,9 @@ FileDecoderState AudioDecoder::decode_flac_() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint32_t output_samples = 0;
 | 
			
		||||
  auto result = this->flac_decoder_->decode_frame(
 | 
			
		||||
      this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(),
 | 
			
		||||
      reinterpret_cast<int16_t *>(this->output_transfer_buffer_->get_buffer_end()), &output_samples);
 | 
			
		||||
  auto result = this->flac_decoder_->decode_frame(this->input_transfer_buffer_->get_buffer_start(),
 | 
			
		||||
                                                  this->input_transfer_buffer_->available(),
 | 
			
		||||
                                                  this->output_transfer_buffer_->get_buffer_end(), &output_samples);
 | 
			
		||||
 | 
			
		||||
  if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
 | 
			
		||||
    // Not an issue, just needs more data that we'll get next time.
 | 
			
		||||
 
 | 
			
		||||
@@ -97,10 +97,10 @@ void BL0906::handle_actions_() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  ActionCallbackFuncPtr ptr_func = nullptr;
 | 
			
		||||
  for (int i = 0; i < this->action_queue_.size(); i++) {
 | 
			
		||||
  for (size_t i = 0; i < this->action_queue_.size(); i++) {
 | 
			
		||||
    ptr_func = this->action_queue_[i];
 | 
			
		||||
    if (ptr_func) {
 | 
			
		||||
      ESP_LOGI(TAG, "HandleActionCallback[%d]", i);
 | 
			
		||||
      ESP_LOGI(TAG, "HandleActionCallback[%zu]", i);
 | 
			
		||||
      (this->*ptr_func)();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ void BL0942::loop() {
 | 
			
		||||
  if (!avail) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (avail < sizeof(buffer)) {
 | 
			
		||||
  if (static_cast<size_t>(avail) < sizeof(buffer)) {
 | 
			
		||||
    if (!this->rx_start_) {
 | 
			
		||||
      this->rx_start_ = millis();
 | 
			
		||||
    } else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
 | 
			
		||||
@@ -148,7 +148,7 @@ void BL0942::setup() {
 | 
			
		||||
 | 
			
		||||
  this->write_reg_(BL0942_REG_USR_WRPROT, 0);
 | 
			
		||||
 | 
			
		||||
  if (this->read_reg_(BL0942_REG_MODE) != mode)
 | 
			
		||||
  if (static_cast<uint32_t>(this->read_reg_(BL0942_REG_MODE)) != mode)
 | 
			
		||||
    this->status_set_warning(LOG_STR("BL0942 setup failed!"));
 | 
			
		||||
 | 
			
		||||
  this->flush();
 | 
			
		||||
 
 | 
			
		||||
@@ -116,7 +116,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA),
 | 
			
		||||
    esp32_ble_tracker.consume_connection_slots(1, "ble_client"),
 | 
			
		||||
    esp32_ble.consume_connection_slots(1, "ble_client"),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_BLE_CLIENT_ID = "ble_client_id"
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,6 @@ from esphome.components.esp32 import add_idf_sdkconfig_option
 | 
			
		||||
from esphome.components.esp32_ble import BTLoggers
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ACTIVE, CONF_ID
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.log import AnsiFore, color
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
 | 
			
		||||
DEPENDENCIES = ["api", "esp32"]
 | 
			
		||||
@@ -44,29 +42,7 @@ def validate_connections(config):
 | 
			
		||||
            )
 | 
			
		||||
    elif config[CONF_ACTIVE]:
 | 
			
		||||
        connection_slots: int = config[CONF_CONNECTION_SLOTS]
 | 
			
		||||
        esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
 | 
			
		||||
            config
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Warn about connection slot waste when using Arduino framework
 | 
			
		||||
        if CORE.using_arduino and connection_slots:
 | 
			
		||||
            _LOGGER.warning(
 | 
			
		||||
                "Bluetooth Proxy with active connections on Arduino framework has suboptimal performance.\n"
 | 
			
		||||
                "If BLE connections fail, they can waste connection slots for 10 seconds because\n"
 | 
			
		||||
                "Arduino doesn't allow configuring the BLE connection timeout (fixed at 30s).\n"
 | 
			
		||||
                "ESP-IDF framework allows setting it to 20s to match client timeouts.\n"
 | 
			
		||||
                "\n"
 | 
			
		||||
                "To switch to ESP-IDF, add this to your YAML:\n"
 | 
			
		||||
                "  esp32:\n"
 | 
			
		||||
                "    framework:\n"
 | 
			
		||||
                "      type: esp-idf\n"
 | 
			
		||||
                "\n"
 | 
			
		||||
                "For detailed migration instructions, see:\n"
 | 
			
		||||
                "%s",
 | 
			
		||||
                color(
 | 
			
		||||
                    AnsiFore.BLUE, "https://esphome.io/guides/esp32_arduino_to_idf.html"
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
        esp32_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config)
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            **config,
 | 
			
		||||
@@ -81,19 +57,17 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(): cv.declare_id(BluetoothProxy),
 | 
			
		||||
                cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
 | 
			
		||||
                cv.SplitDefault(CONF_CACHE_SERVICES, esp32_idf=True): cv.All(
 | 
			
		||||
                    cv.only_with_esp_idf, cv.boolean
 | 
			
		||||
                ),
 | 
			
		||||
                cv.Optional(CONF_CACHE_SERVICES, default=True): cv.boolean,
 | 
			
		||||
                cv.Optional(
 | 
			
		||||
                    CONF_CONNECTION_SLOTS,
 | 
			
		||||
                    default=DEFAULT_CONNECTION_SLOTS,
 | 
			
		||||
                ): cv.All(
 | 
			
		||||
                    cv.positive_int,
 | 
			
		||||
                    cv.Range(min=1, max=esp32_ble_tracker.max_connections()),
 | 
			
		||||
                    cv.Range(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS),
 | 
			
		||||
                ),
 | 
			
		||||
                cv.Optional(CONF_CONNECTIONS): cv.All(
 | 
			
		||||
                    cv.ensure_list(CONNECTION_SCHEMA),
 | 
			
		||||
                    cv.Length(min=1, max=esp32_ble_tracker.max_connections()),
 | 
			
		||||
                    cv.Length(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS),
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -514,7 +514,8 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
 | 
			
		||||
  return this->check_and_log_error_("esp_ble_gattc_read_char", err);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::string &data, bool response) {
 | 
			
		||||
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8_t *data, size_t length,
 | 
			
		||||
                                                    bool response) {
 | 
			
		||||
  if (!this->connected()) {
 | 
			
		||||
    this->log_gatt_not_connected_("write", "characteristic");
 | 
			
		||||
    return ESP_GATT_NOT_CONNECTED;
 | 
			
		||||
@@ -522,8 +523,11 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::
 | 
			
		||||
  ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
           handle);
 | 
			
		||||
 | 
			
		||||
  // ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
 | 
			
		||||
  // The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
 | 
			
		||||
  // const_cast is safe here and was previously hidden by a C-style cast
 | 
			
		||||
  esp_err_t err =
 | 
			
		||||
      esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
 | 
			
		||||
      esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, length, const_cast<uint8_t *>(data),
 | 
			
		||||
                               response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  return this->check_and_log_error_("esp_ble_gattc_write_char", err);
 | 
			
		||||
}
 | 
			
		||||
@@ -540,7 +544,7 @@ esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
 | 
			
		||||
  return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::string &data, bool response) {
 | 
			
		||||
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response) {
 | 
			
		||||
  if (!this->connected()) {
 | 
			
		||||
    this->log_gatt_not_connected_("write", "descriptor");
 | 
			
		||||
    return ESP_GATT_NOT_CONNECTED;
 | 
			
		||||
@@ -548,8 +552,11 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::stri
 | 
			
		||||
  ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
           handle);
 | 
			
		||||
 | 
			
		||||
  // ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
 | 
			
		||||
  // The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
 | 
			
		||||
  // const_cast is safe here and was previously hidden by a C-style cast
 | 
			
		||||
  esp_err_t err = esp_ble_gattc_write_char_descr(
 | 
			
		||||
      this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
 | 
			
		||||
      this->gattc_if_, this->conn_id_, handle, length, const_cast<uint8_t *>(data),
 | 
			
		||||
      response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,9 +18,9 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
 | 
			
		||||
  esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
 | 
			
		||||
 | 
			
		||||
  esp_err_t read_characteristic(uint16_t handle);
 | 
			
		||||
  esp_err_t write_characteristic(uint16_t handle, const std::string &data, bool response);
 | 
			
		||||
  esp_err_t write_characteristic(uint16_t handle, const uint8_t *data, size_t length, bool response);
 | 
			
		||||
  esp_err_t read_descriptor(uint16_t handle);
 | 
			
		||||
  esp_err_t write_descriptor(uint16_t handle, const std::string &data, bool response);
 | 
			
		||||
  esp_err_t write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response);
 | 
			
		||||
 | 
			
		||||
  esp_err_t notify_characteristic(uint16_t handle, bool enable);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -305,7 +305,7 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto err = connection->write_characteristic(msg.handle, msg.data, msg.response);
 | 
			
		||||
  auto err = connection->write_characteristic(msg.handle, msg.data, msg.data_len, msg.response);
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    this->send_gatt_error(msg.address, msg.handle, err);
 | 
			
		||||
  }
 | 
			
		||||
@@ -331,7 +331,7 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto err = connection->write_descriptor(msg.handle, msg.data, true);
 | 
			
		||||
  auto err = connection->write_descriptor(msg.handle, msg.data, msg.data_len, true);
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    this->send_gatt_error(msg.address, msg.handle, err);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ import esphome.codegen as cg
 | 
			
		||||
from esphome.components.esp32 import add_idf_component
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TYPE
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@DT-art1"]
 | 
			
		||||
@@ -51,9 +50,8 @@ async def to_code(config: ConfigType) -> None:
 | 
			
		||||
    buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID])
 | 
			
		||||
    cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE]))
 | 
			
		||||
    if config[CONF_TYPE] == ESP32_CAMERA_ENCODER:
 | 
			
		||||
        if CORE.using_esp_idf:
 | 
			
		||||
            add_idf_component(name="espressif/esp32-camera", ref="2.1.0")
 | 
			
		||||
        cg.add_build_flag("-DUSE_ESP32_CAMERA_JPEG_ENCODER")
 | 
			
		||||
        add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
 | 
			
		||||
        cg.add_define("USE_ESP32_CAMERA_JPEG_ENCODER")
 | 
			
		||||
        var = cg.new_Pvariable(
 | 
			
		||||
            config[CONF_ID],
 | 
			
		||||
            config[CONF_QUALITY],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_CAMERA_JPEG_ENCODER
 | 
			
		||||
 | 
			
		||||
#include "esp32_camera_jpeg_encoder.h"
 | 
			
		||||
@@ -15,7 +17,7 @@ camera::EncoderError ESP32CameraJPEGEncoder::encode_pixels(camera::CameraImageSp
 | 
			
		||||
  this->bytes_written_ = 0;
 | 
			
		||||
  this->out_of_output_memory_ = false;
 | 
			
		||||
  bool success = fmt2jpg_cb(pixels->get_data_buffer(), pixels->get_data_length(), spec->width, spec->height,
 | 
			
		||||
                            to_internal_(spec->format), this->quality_, callback_, this);
 | 
			
		||||
                            to_internal_(spec->format), this->quality_, callback, this);
 | 
			
		||||
 | 
			
		||||
  if (!success)
 | 
			
		||||
    return camera::ENCODER_ERROR_CONFIGURATION;
 | 
			
		||||
@@ -49,7 +51,7 @@ void ESP32CameraJPEGEncoder::dump_config() {
 | 
			
		||||
                this->output_->get_max_size(), this->quality_, this->buffer_expand_size_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
size_t ESP32CameraJPEGEncoder::callback_(void *arg, size_t index, const void *data, size_t len) {
 | 
			
		||||
size_t ESP32CameraJPEGEncoder::callback(void *arg, size_t index, const void *data, size_t len) {
 | 
			
		||||
  ESP32CameraJPEGEncoder *that = reinterpret_cast<ESP32CameraJPEGEncoder *>(arg);
 | 
			
		||||
  uint8_t *buffer = that->output_->get_data();
 | 
			
		||||
  size_t buffer_length = that->output_->get_max_size();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_CAMERA_JPEG_ENCODER
 | 
			
		||||
 | 
			
		||||
#include <esp_camera.h>
 | 
			
		||||
@@ -24,7 +26,7 @@ class ESP32CameraJPEGEncoder : public camera::Encoder {
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  // -------------------------
 | 
			
		||||
 protected:
 | 
			
		||||
  static size_t callback_(void *arg, size_t index, const void *data, size_t len);
 | 
			
		||||
  static size_t callback(void *arg, size_t index, const void *data, size_t len);
 | 
			
		||||
  pixformat_t to_internal_(camera::PixelFormat format);
 | 
			
		||||
 | 
			
		||||
  camera::EncoderBuffer *output_{};
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,8 @@ void Canbus::dump_config() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
 | 
			
		||||
                       const std::vector<uint8_t> &data) {
 | 
			
		||||
canbus::Error Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
 | 
			
		||||
                                const std::vector<uint8_t> &data) {
 | 
			
		||||
  struct CanFrame can_message;
 | 
			
		||||
 | 
			
		||||
  uint8_t size = static_cast<uint8_t>(data.size());
 | 
			
		||||
@@ -45,13 +45,15 @@ void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transm
 | 
			
		||||
    ESP_LOGVV(TAG, "  data[%d]=%02x", i, can_message.data[i]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->send_message(&can_message) != canbus::ERROR_OK) {
 | 
			
		||||
  canbus::Error error = this->send_message(&can_message);
 | 
			
		||||
  if (error != canbus::ERROR_OK) {
 | 
			
		||||
    if (use_extended_id) {
 | 
			
		||||
      ESP_LOGW(TAG, "send to extended id=0x%08" PRIx32 " failed!", can_id);
 | 
			
		||||
      ESP_LOGW(TAG, "send to extended id=0x%08" PRIx32 " failed with error %d!", can_id, error);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "send to standard id=0x%03" PRIx32 " failed!", can_id);
 | 
			
		||||
      ESP_LOGW(TAG, "send to standard id=0x%03" PRIx32 " failed with error %d!", can_id, error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return error;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Canbus::add_trigger(CanbusTrigger *trigger) {
 | 
			
		||||
 
 | 
			
		||||
@@ -70,11 +70,11 @@ class Canbus : public Component {
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::HARDWARE; }
 | 
			
		||||
  void loop() override;
 | 
			
		||||
 | 
			
		||||
  void send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
 | 
			
		||||
                 const std::vector<uint8_t> &data);
 | 
			
		||||
  void send_data(uint32_t can_id, bool use_extended_id, const std::vector<uint8_t> &data) {
 | 
			
		||||
  canbus::Error send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
 | 
			
		||||
                          const std::vector<uint8_t> &data);
 | 
			
		||||
  canbus::Error send_data(uint32_t can_id, bool use_extended_id, const std::vector<uint8_t> &data) {
 | 
			
		||||
    // for backwards compatibility only
 | 
			
		||||
    this->send_data(can_id, use_extended_id, false, data);
 | 
			
		||||
    return this->send_data(can_id, use_extended_id, false, data);
 | 
			
		||||
  }
 | 
			
		||||
  void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
 | 
			
		||||
  void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
 | 
			
		||||
@@ -105,9 +105,9 @@ class Canbus : public Component {
 | 
			
		||||
  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);
 | 
			
		||||
  virtual Error read_message(struct CanFrame *frame);
 | 
			
		||||
  virtual bool setup_internal() = 0;
 | 
			
		||||
  virtual Error send_message(struct CanFrame *frame) = 0;
 | 
			
		||||
  virtual Error read_message(struct CanFrame *frame) = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public Parented<Canbus> {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import web_server_base
 | 
			
		||||
from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
@@ -9,10 +10,19 @@ from esphome.const import (
 | 
			
		||||
    PLATFORM_ESP8266,
 | 
			
		||||
    PLATFORM_LN882X,
 | 
			
		||||
    PLATFORM_RTL87XX,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
from esphome.coroutine import CoroPriority
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def AUTO_LOAD() -> list[str]:
 | 
			
		||||
    auto_load = ["web_server_base", "ota.web_server"]
 | 
			
		||||
    if CORE.using_esp_idf:
 | 
			
		||||
        auto_load.append("socket")
 | 
			
		||||
    return auto_load
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["web_server_base", "ota.web_server"]
 | 
			
		||||
DEPENDENCIES = ["wifi"]
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
 | 
			
		||||
@@ -40,7 +50,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine_with_priority(CoroPriority.COMMUNICATION)
 | 
			
		||||
@coroutine_with_priority(CoroPriority.CAPTIVE_PORTAL)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
 | 
			
		||||
 | 
			
		||||
@@ -57,3 +67,11 @@ async def to_code(config):
 | 
			
		||||
            cg.add_library("DNSServer", None)
 | 
			
		||||
        if CORE.is_libretiny:
 | 
			
		||||
            cg.add_library("DNSServer", None)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Only compile the ESP-IDF DNS server when using ESP-IDF framework
 | 
			
		||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
    {
 | 
			
		||||
        "dns_server_esp32_idf.cpp": {PlatformFramework.ESP32_IDF},
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -11,14 +11,14 @@ namespace captive_portal {
 | 
			
		||||
static const char *const TAG = "captive_portal";
 | 
			
		||||
 | 
			
		||||
void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
 | 
			
		||||
  AsyncResponseStream *stream = request->beginResponseStream(F("application/json"));
 | 
			
		||||
  stream->addHeader(F("cache-control"), F("public, max-age=0, must-revalidate"));
 | 
			
		||||
  AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("application/json"));
 | 
			
		||||
  stream->addHeader(ESPHOME_F("cache-control"), ESPHOME_F("public, max-age=0, must-revalidate"));
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
  stream->print(F("{\"mac\":\""));
 | 
			
		||||
  stream->print(ESPHOME_F("{\"mac\":\""));
 | 
			
		||||
  stream->print(get_mac_address_pretty().c_str());
 | 
			
		||||
  stream->print(F("\",\"name\":\""));
 | 
			
		||||
  stream->print(ESPHOME_F("\",\"name\":\""));
 | 
			
		||||
  stream->print(App.get_name().c_str());
 | 
			
		||||
  stream->print(F("\",\"aps\":[{}"));
 | 
			
		||||
  stream->print(ESPHOME_F("\",\"aps\":[{}"));
 | 
			
		||||
#else
 | 
			
		||||
  stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
@@ -29,37 +29,35 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
 | 
			
		||||
 | 
			
		||||
      // Assumes no " in ssid, possible unicode isses?
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
    stream->print(F(",{\"ssid\":\""));
 | 
			
		||||
    stream->print(ESPHOME_F(",{\"ssid\":\""));
 | 
			
		||||
    stream->print(scan.get_ssid().c_str());
 | 
			
		||||
    stream->print(F("\",\"rssi\":"));
 | 
			
		||||
    stream->print(ESPHOME_F("\",\"rssi\":"));
 | 
			
		||||
    stream->print(scan.get_rssi());
 | 
			
		||||
    stream->print(F(",\"lock\":"));
 | 
			
		||||
    stream->print(ESPHOME_F(",\"lock\":"));
 | 
			
		||||
    stream->print(scan.get_with_auth());
 | 
			
		||||
    stream->print(F("}"));
 | 
			
		||||
    stream->print(ESPHOME_F("}"));
 | 
			
		||||
#else
 | 
			
		||||
    stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(),
 | 
			
		||||
                   scan.get_with_auth());
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
  stream->print(F("]}"));
 | 
			
		||||
  stream->print(ESPHOME_F("]}"));
 | 
			
		||||
  request->send(stream);
 | 
			
		||||
}
 | 
			
		||||
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
 | 
			
		||||
  std::string ssid = request->arg("ssid").c_str();
 | 
			
		||||
  std::string psk = request->arg("psk").c_str();
 | 
			
		||||
  std::string ssid = request->arg("ssid").c_str();  // NOLINT(readability-redundant-string-cstr)
 | 
			
		||||
  std::string psk = request->arg("psk").c_str();    // NOLINT(readability-redundant-string-cstr)
 | 
			
		||||
  ESP_LOGI(TAG, "Requested WiFi Settings Change:");
 | 
			
		||||
  ESP_LOGI(TAG, "  SSID='%s'", ssid.c_str());
 | 
			
		||||
  ESP_LOGI(TAG, "  Password=" LOG_SECRET("'%s'"), psk.c_str());
 | 
			
		||||
  wifi::global_wifi_component->save_wifi_sta(ssid, psk);
 | 
			
		||||
  wifi::global_wifi_component->start_scanning();
 | 
			
		||||
  request->redirect(F("/?save"));
 | 
			
		||||
  request->redirect(ESPHOME_F("/?save"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CaptivePortal::setup() {
 | 
			
		||||
#ifndef USE_ARDUINO
 | 
			
		||||
  // No DNS server needed for non-Arduino frameworks
 | 
			
		||||
  // Disable loop by default - will be enabled when captive portal starts
 | 
			
		||||
  this->disable_loop();
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
void CaptivePortal::start() {
 | 
			
		||||
  this->base_->init();
 | 
			
		||||
@@ -67,51 +65,47 @@ void CaptivePortal::start() {
 | 
			
		||||
    this->base_->add_handler(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
  // Create DNS server instance for ESP-IDF
 | 
			
		||||
  this->dns_server_ = make_unique<DNSServer>();
 | 
			
		||||
  this->dns_server_->start(ip);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
  this->dns_server_ = make_unique<DNSServer>();
 | 
			
		||||
  this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
 | 
			
		||||
  network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
 | 
			
		||||
  this->dns_server_->start(53, F("*"), ip);
 | 
			
		||||
  // Re-enable loop() when DNS server is started
 | 
			
		||||
  this->enable_loop();
 | 
			
		||||
  this->dns_server_->start(53, ESPHOME_F("*"), ip);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) {
 | 
			
		||||
    if (!this->active_ || req->host().c_str() == wifi::global_wifi_component->wifi_soft_ap_ip().str()) {
 | 
			
		||||
      req->send(404, F("text/html"), F("File not found"));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
    String url = F("http://");
 | 
			
		||||
    url += wifi::global_wifi_component->wifi_soft_ap_ip().str().c_str();
 | 
			
		||||
#else
 | 
			
		||||
    auto url = "http://" + wifi::global_wifi_component->wifi_soft_ap_ip().str();
 | 
			
		||||
#endif
 | 
			
		||||
    req->redirect(url.c_str());
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  this->initialized_ = true;
 | 
			
		||||
  this->active_ = true;
 | 
			
		||||
 | 
			
		||||
  // Enable loop() now that captive portal is active
 | 
			
		||||
  this->enable_loop();
 | 
			
		||||
 | 
			
		||||
  ESP_LOGV(TAG, "Captive portal started");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
 | 
			
		||||
  if (req->url() == F("/")) {
 | 
			
		||||
#ifndef USE_ESP8266
 | 
			
		||||
    auto *response = req->beginResponse(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
 | 
			
		||||
#else
 | 
			
		||||
    auto *response = req->beginResponse_P(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
 | 
			
		||||
#endif
 | 
			
		||||
    response->addHeader(F("Content-Encoding"), F("gzip"));
 | 
			
		||||
    req->send(response);
 | 
			
		||||
    return;
 | 
			
		||||
  } else if (req->url() == F("/config.json")) {
 | 
			
		||||
  if (req->url() == ESPHOME_F("/config.json")) {
 | 
			
		||||
    this->handle_config(req);
 | 
			
		||||
    return;
 | 
			
		||||
  } else if (req->url() == F("/wifisave")) {
 | 
			
		||||
  } else if (req->url() == ESPHOME_F("/wifisave")) {
 | 
			
		||||
    this->handle_wifisave(req);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // All other requests get the captive portal page
 | 
			
		||||
  // This includes OS captive portal detection endpoints which will trigger
 | 
			
		||||
  // the captive portal when they don't receive their expected responses
 | 
			
		||||
#ifndef USE_ESP8266
 | 
			
		||||
  auto *response = req->beginResponse(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
 | 
			
		||||
#else
 | 
			
		||||
  auto *response = req->beginResponse_P(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
 | 
			
		||||
#endif
 | 
			
		||||
  response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip"));
 | 
			
		||||
  req->send(response);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CaptivePortal::CaptivePortal(web_server_base::WebServerBase *base) : base_(base) { global_captive_portal = this; }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,9 @@
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
#include <DNSServer.h>
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
#include "dns_server_esp32_idf.h"
 | 
			
		||||
#endif
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
@@ -19,41 +22,36 @@ class CaptivePortal : public AsyncWebHandler, public Component {
 | 
			
		||||
  CaptivePortal(web_server_base::WebServerBase *base);
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
  void loop() override {
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
    if (this->dns_server_ != nullptr) {
 | 
			
		||||
      this->dns_server_->processNextRequest();
 | 
			
		||||
    } else {
 | 
			
		||||
      this->disable_loop();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
    if (this->dns_server_ != nullptr) {
 | 
			
		||||
      this->dns_server_->process_next_request();
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  void start();
 | 
			
		||||
  bool is_active() const { return this->active_; }
 | 
			
		||||
  void end() {
 | 
			
		||||
    this->active_ = false;
 | 
			
		||||
    this->disable_loop();  // Stop processing DNS requests
 | 
			
		||||
    this->base_->deinit();
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
    this->dns_server_->stop();
 | 
			
		||||
    this->dns_server_ = nullptr;
 | 
			
		||||
#endif
 | 
			
		||||
    if (this->dns_server_ != nullptr) {
 | 
			
		||||
      this->dns_server_->stop();
 | 
			
		||||
      this->dns_server_ = nullptr;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool canHandle(AsyncWebServerRequest *request) const override {
 | 
			
		||||
    if (!this->active_)
 | 
			
		||||
      return false;
 | 
			
		||||
 | 
			
		||||
    if (request->method() == HTTP_GET) {
 | 
			
		||||
      if (request->url() == F("/"))
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == F("/config.json"))
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == F("/wifisave"))
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
    // Handle all GET requests when captive portal is active
 | 
			
		||||
    // This allows us to respond with the portal page for any URL,
 | 
			
		||||
    // triggering OS captive portal detection
 | 
			
		||||
    return this->active_ && request->method() == HTTP_GET;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void handle_config(AsyncWebServerRequest *request);
 | 
			
		||||
@@ -66,7 +64,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
 | 
			
		||||
  web_server_base::WebServerBase *base_;
 | 
			
		||||
  bool initialized_{false};
 | 
			
		||||
  bool active_{false};
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
#if defined(USE_ARDUINO) || defined(USE_ESP_IDF)
 | 
			
		||||
  std::unique_ptr<DNSServer> dns_server_{nullptr};
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										205
									
								
								esphome/components/captive_portal/dns_server_esp32_idf.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								esphome/components/captive_portal/dns_server_esp32_idf.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,205 @@
 | 
			
		||||
#include "dns_server_esp32_idf.h"
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/components/socket/socket.h"
 | 
			
		||||
#include <lwip/sockets.h>
 | 
			
		||||
#include <lwip/inet.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome::captive_portal {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "captive_portal.dns";
 | 
			
		||||
 | 
			
		||||
// DNS constants
 | 
			
		||||
static constexpr uint16_t DNS_PORT = 53;
 | 
			
		||||
static constexpr uint16_t DNS_QR_FLAG = 1 << 15;
 | 
			
		||||
static constexpr uint16_t DNS_OPCODE_MASK = 0x7800;
 | 
			
		||||
static constexpr uint16_t DNS_QTYPE_A = 0x0001;
 | 
			
		||||
static constexpr uint16_t DNS_QCLASS_IN = 0x0001;
 | 
			
		||||
static constexpr uint16_t DNS_ANSWER_TTL = 300;
 | 
			
		||||
 | 
			
		||||
// DNS Header structure
 | 
			
		||||
struct DNSHeader {
 | 
			
		||||
  uint16_t id;
 | 
			
		||||
  uint16_t flags;
 | 
			
		||||
  uint16_t qd_count;
 | 
			
		||||
  uint16_t an_count;
 | 
			
		||||
  uint16_t ns_count;
 | 
			
		||||
  uint16_t ar_count;
 | 
			
		||||
} __attribute__((packed));
 | 
			
		||||
 | 
			
		||||
// DNS Question structure
 | 
			
		||||
struct DNSQuestion {
 | 
			
		||||
  uint16_t type;
 | 
			
		||||
  uint16_t dns_class;
 | 
			
		||||
} __attribute__((packed));
 | 
			
		||||
 | 
			
		||||
// DNS Answer structure
 | 
			
		||||
struct DNSAnswer {
 | 
			
		||||
  uint16_t ptr_offset;
 | 
			
		||||
  uint16_t type;
 | 
			
		||||
  uint16_t dns_class;
 | 
			
		||||
  uint32_t ttl;
 | 
			
		||||
  uint16_t addr_len;
 | 
			
		||||
  uint32_t ip_addr;
 | 
			
		||||
} __attribute__((packed));
 | 
			
		||||
 | 
			
		||||
void DNSServer::start(const network::IPAddress &ip) {
 | 
			
		||||
  this->server_ip_ = ip;
 | 
			
		||||
  ESP_LOGV(TAG, "Starting DNS server on %s", ip.str().c_str());
 | 
			
		||||
 | 
			
		||||
  // Create loop-monitored UDP socket
 | 
			
		||||
  this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP);
 | 
			
		||||
  if (this->socket_ == nullptr) {
 | 
			
		||||
    ESP_LOGE(TAG, "Socket create failed");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Set socket options
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable));
 | 
			
		||||
 | 
			
		||||
  // Bind to port 53
 | 
			
		||||
  struct sockaddr_storage server_addr = {};
 | 
			
		||||
  socklen_t addr_len = socket::set_sockaddr_any((struct sockaddr *) &server_addr, sizeof(server_addr), DNS_PORT);
 | 
			
		||||
 | 
			
		||||
  int err = this->socket_->bind((struct sockaddr *) &server_addr, addr_len);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGE(TAG, "Bind failed: %d", errno);
 | 
			
		||||
    this->socket_ = nullptr;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGV(TAG, "Bound to port %d", DNS_PORT);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DNSServer::stop() {
 | 
			
		||||
  if (this->socket_ != nullptr) {
 | 
			
		||||
    this->socket_->close();
 | 
			
		||||
    this->socket_ = nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGV(TAG, "Stopped");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DNSServer::process_next_request() {
 | 
			
		||||
  // Process one request if socket is valid and data is available
 | 
			
		||||
  if (this->socket_ == nullptr || !this->socket_->ready()) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  struct sockaddr_in client_addr;
 | 
			
		||||
  socklen_t client_addr_len = sizeof(client_addr);
 | 
			
		||||
 | 
			
		||||
  // Receive DNS request using raw fd for recvfrom
 | 
			
		||||
  int fd = this->socket_->get_fd();
 | 
			
		||||
  if (fd < 0) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ssize_t len = recvfrom(fd, this->buffer_, sizeof(this->buffer_), MSG_DONTWAIT, (struct sockaddr *) &client_addr,
 | 
			
		||||
                         &client_addr_len);
 | 
			
		||||
 | 
			
		||||
  if (len < 0) {
 | 
			
		||||
    if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
 | 
			
		||||
      ESP_LOGE(TAG, "recvfrom failed: %d", errno);
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGVV(TAG, "Received %d bytes from %s:%d", len, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
 | 
			
		||||
 | 
			
		||||
  if (len < static_cast<ssize_t>(sizeof(DNSHeader) + 1)) {
 | 
			
		||||
    ESP_LOGV(TAG, "Request too short: %d", len);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Parse DNS header
 | 
			
		||||
  DNSHeader *header = (DNSHeader *) this->buffer_;
 | 
			
		||||
  uint16_t flags = ntohs(header->flags);
 | 
			
		||||
  uint16_t qd_count = ntohs(header->qd_count);
 | 
			
		||||
 | 
			
		||||
  // Check if it's a standard query
 | 
			
		||||
  if ((flags & DNS_QR_FLAG) || (flags & DNS_OPCODE_MASK) || qd_count != 1) {
 | 
			
		||||
    ESP_LOGV(TAG, "Not a standard query: flags=0x%04X, qd_count=%d", flags, qd_count);
 | 
			
		||||
    return;  // Not a standard query
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Parse domain name (we don't actually care about it - redirect everything)
 | 
			
		||||
  uint8_t *ptr = this->buffer_ + sizeof(DNSHeader);
 | 
			
		||||
  uint8_t *end = this->buffer_ + len;
 | 
			
		||||
 | 
			
		||||
  while (ptr < end && *ptr != 0) {
 | 
			
		||||
    uint8_t label_len = *ptr;
 | 
			
		||||
    if (label_len > 63) {  // Check for invalid label length
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    // Check if we have room for this label plus the length byte
 | 
			
		||||
    if (ptr + label_len + 1 > end) {
 | 
			
		||||
      return;  // Would overflow
 | 
			
		||||
    }
 | 
			
		||||
    ptr += label_len + 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Check if we reached a proper null terminator
 | 
			
		||||
  if (ptr >= end || *ptr != 0) {
 | 
			
		||||
    return;  // Name not terminated or truncated
 | 
			
		||||
  }
 | 
			
		||||
  ptr++;  // Skip the null terminator
 | 
			
		||||
 | 
			
		||||
  // Check we have room for the question
 | 
			
		||||
  if (ptr + sizeof(DNSQuestion) > end) {
 | 
			
		||||
    return;  // Request truncated
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Parse DNS question
 | 
			
		||||
  DNSQuestion *question = (DNSQuestion *) ptr;
 | 
			
		||||
  uint16_t qtype = ntohs(question->type);
 | 
			
		||||
  uint16_t qclass = ntohs(question->dns_class);
 | 
			
		||||
 | 
			
		||||
  // We only handle A queries
 | 
			
		||||
  if (qtype != DNS_QTYPE_A || qclass != DNS_QCLASS_IN) {
 | 
			
		||||
    ESP_LOGV(TAG, "Not an A query: type=0x%04X, class=0x%04X", qtype, qclass);
 | 
			
		||||
    return;  // Not an A query
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Build DNS response by modifying the request in-place
 | 
			
		||||
  header->flags = htons(DNS_QR_FLAG | 0x8000);  // Response + Authoritative
 | 
			
		||||
  header->an_count = htons(1);                  // One answer
 | 
			
		||||
 | 
			
		||||
  // Add answer section after the question
 | 
			
		||||
  size_t question_len = (ptr + sizeof(DNSQuestion)) - this->buffer_ - sizeof(DNSHeader);
 | 
			
		||||
  size_t answer_offset = sizeof(DNSHeader) + question_len;
 | 
			
		||||
 | 
			
		||||
  // Check if we have room for the answer
 | 
			
		||||
  if (answer_offset + sizeof(DNSAnswer) > sizeof(this->buffer_)) {
 | 
			
		||||
    ESP_LOGW(TAG, "Response too large");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  DNSAnswer *answer = (DNSAnswer *) (this->buffer_ + answer_offset);
 | 
			
		||||
 | 
			
		||||
  // Pointer to name in question (offset from start of packet)
 | 
			
		||||
  answer->ptr_offset = htons(0xC000 | sizeof(DNSHeader));
 | 
			
		||||
  answer->type = htons(DNS_QTYPE_A);
 | 
			
		||||
  answer->dns_class = htons(DNS_QCLASS_IN);
 | 
			
		||||
  answer->ttl = htonl(DNS_ANSWER_TTL);
 | 
			
		||||
  answer->addr_len = htons(4);
 | 
			
		||||
 | 
			
		||||
  // Get the raw IP address
 | 
			
		||||
  ip4_addr_t addr = this->server_ip_;
 | 
			
		||||
  answer->ip_addr = addr.addr;
 | 
			
		||||
 | 
			
		||||
  size_t response_len = answer_offset + sizeof(DNSAnswer);
 | 
			
		||||
 | 
			
		||||
  // Send response
 | 
			
		||||
  ssize_t sent =
 | 
			
		||||
      this->socket_->sendto(this->buffer_, response_len, 0, (struct sockaddr *) &client_addr, client_addr_len);
 | 
			
		||||
  if (sent < 0) {
 | 
			
		||||
    ESP_LOGV(TAG, "Send failed: %d", errno);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGV(TAG, "Sent %d bytes", sent);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::captive_portal
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP_IDF
 | 
			
		||||
							
								
								
									
										27
									
								
								esphome/components/captive_portal/dns_server_esp32_idf.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/captive_portal/dns_server_esp32_idf.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/components/network/ip_address.h"
 | 
			
		||||
#include "esphome/components/socket/socket.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::captive_portal {
 | 
			
		||||
 | 
			
		||||
class DNSServer {
 | 
			
		||||
 public:
 | 
			
		||||
  void start(const network::IPAddress &ip);
 | 
			
		||||
  void stop();
 | 
			
		||||
  void process_next_request();
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  static constexpr size_t DNS_BUFFER_SIZE = 192;
 | 
			
		||||
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_{nullptr};
 | 
			
		||||
  network::IPAddress server_ip_;
 | 
			
		||||
  uint8_t buffer_[DNS_BUFFER_SIZE];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::captive_portal
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP_IDF
 | 
			
		||||
@@ -155,7 +155,7 @@ void CCS811Component::dump_config() {
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
  LOG_SENSOR("  ", "CO2 Sensor", this->co2_);
 | 
			
		||||
  LOG_SENSOR("  ", "TVOC Sensor", this->tvoc_);
 | 
			
		||||
  LOG_TEXT_SENSOR("  ", "Firmware Version Sensor", this->version_)
 | 
			
		||||
  LOG_TEXT_SENSOR("  ", "Firmware Version Sensor", this->version_);
 | 
			
		||||
  if (this->baseline_) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Baseline: %04X", *this->baseline_);
 | 
			
		||||
  } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -367,9 +367,11 @@ void Climate::save_state_() {
 | 
			
		||||
    state.uses_custom_fan_mode = true;
 | 
			
		||||
    const auto &supported = traits.get_supported_custom_fan_modes();
 | 
			
		||||
    std::vector<std::string> vec{supported.begin(), supported.end()};
 | 
			
		||||
    auto it = std::find(vec.begin(), vec.end(), custom_fan_mode);
 | 
			
		||||
    if (it != vec.end()) {
 | 
			
		||||
      state.custom_fan_mode = std::distance(vec.begin(), it);
 | 
			
		||||
    for (size_t i = 0; i < vec.size(); i++) {
 | 
			
		||||
      if (vec[i] == custom_fan_mode) {
 | 
			
		||||
        state.custom_fan_mode = i;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.get_supports_presets() && preset.has_value()) {
 | 
			
		||||
@@ -380,10 +382,11 @@ void Climate::save_state_() {
 | 
			
		||||
    state.uses_custom_preset = true;
 | 
			
		||||
    const auto &supported = traits.get_supported_custom_presets();
 | 
			
		||||
    std::vector<std::string> vec{supported.begin(), supported.end()};
 | 
			
		||||
    auto it = std::find(vec.begin(), vec.end(), custom_preset);
 | 
			
		||||
    // only set custom preset if value exists, otherwise leave it as is
 | 
			
		||||
    if (it != vec.cend()) {
 | 
			
		||||
      state.custom_preset = std::distance(vec.begin(), it);
 | 
			
		||||
    for (size_t i = 0; i < vec.size(); i++) {
 | 
			
		||||
      if (vec[i] == custom_preset) {
 | 
			
		||||
        state.custom_preset = i;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.get_supports_swing_modes()) {
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ static const uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03,
 | 
			
		||||
 | 
			
		||||
uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
 | 
			
		||||
  uint8_t crc = 0;
 | 
			
		||||
  for (int i = 0; i < len - 1; i++) {
 | 
			
		||||
  for (size_t i = 0; i < len - 1; i++) {
 | 
			
		||||
    crc -= response[i];
 | 
			
		||||
  }
 | 
			
		||||
  return crc;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ void CopyLock::setup() {
 | 
			
		||||
 | 
			
		||||
  traits.set_assumed_state(source_->traits.get_assumed_state());
 | 
			
		||||
  traits.set_requires_code(source_->traits.get_requires_code());
 | 
			
		||||
  traits.set_supported_states(source_->traits.get_supported_states());
 | 
			
		||||
  traits.set_supported_states_mask(source_->traits.get_supported_states_mask());
 | 
			
		||||
  traits.set_supports_open(source_->traits.get_supports_open());
 | 
			
		||||
 | 
			
		||||
  this->publish_state(source_->state);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#include "cover.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <strings.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace cover {
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ void DaikinArcClimate::transmit_query_() {
 | 
			
		||||
  uint8_t remote_header[8] = {0x11, 0xDA, 0x27, 0x00, 0x84, 0x87, 0x20, 0x00};
 | 
			
		||||
 | 
			
		||||
  // Calculate checksum
 | 
			
		||||
  for (int i = 0; i < sizeof(remote_header) - 1; i++) {
 | 
			
		||||
  for (size_t i = 0; i < sizeof(remote_header) - 1; i++) {
 | 
			
		||||
    remote_header[sizeof(remote_header) - 1] += remote_header[i];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -102,7 +102,7 @@ void DaikinArcClimate::transmit_state() {
 | 
			
		||||
  remote_state[9] = fan_speed & 0xff;
 | 
			
		||||
 | 
			
		||||
  // Calculate checksum
 | 
			
		||||
  for (int i = 0; i < sizeof(remote_header) - 1; i++) {
 | 
			
		||||
  for (size_t i = 0; i < sizeof(remote_header) - 1; i++) {
 | 
			
		||||
    remote_header[sizeof(remote_header) - 1] += remote_header[i];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -350,7 +350,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
  bool valid_daikin_frame = false;
 | 
			
		||||
  if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
 | 
			
		||||
    valid_daikin_frame = true;
 | 
			
		||||
    int bytes_count = data.size() / 2 / 8;
 | 
			
		||||
    size_t bytes_count = data.size() / 2 / 8;
 | 
			
		||||
    std::unique_ptr<char[]> buf(new char[bytes_count * 3 + 1]);
 | 
			
		||||
    buf[0] = '\0';
 | 
			
		||||
    for (size_t i = 0; i < bytes_count; i++) {
 | 
			
		||||
@@ -370,7 +370,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
  if (!valid_daikin_frame) {
 | 
			
		||||
    char sbuf[16 * 10 + 1];
 | 
			
		||||
    sbuf[0] = '\0';
 | 
			
		||||
    for (size_t j = 0; j < data.size(); j++) {
 | 
			
		||||
    for (size_t j = 0; j < static_cast<size_t>(data.size()); j++) {
 | 
			
		||||
      if ((j - 2) % 16 == 0) {
 | 
			
		||||
        if (j > 0) {
 | 
			
		||||
          ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf);
 | 
			
		||||
@@ -380,19 +380,26 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
      char type_ch = ' ';
 | 
			
		||||
      // debug_tolerance = 25%
 | 
			
		||||
 | 
			
		||||
      if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK))
 | 
			
		||||
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK)) <= data[j] &&
 | 
			
		||||
          data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK)))
 | 
			
		||||
        type_ch = 'P';
 | 
			
		||||
      if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE))
 | 
			
		||||
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE)) <= -data[j] &&
 | 
			
		||||
          -data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE)))
 | 
			
		||||
        type_ch = 'a';
 | 
			
		||||
      if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK))
 | 
			
		||||
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK)) <= data[j] &&
 | 
			
		||||
          data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK)))
 | 
			
		||||
        type_ch = 'H';
 | 
			
		||||
      if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE))
 | 
			
		||||
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE)) <= -data[j] &&
 | 
			
		||||
          -data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE)))
 | 
			
		||||
        type_ch = 'h';
 | 
			
		||||
      if (DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK))
 | 
			
		||||
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK)) <= data[j] &&
 | 
			
		||||
          data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK)))
 | 
			
		||||
        type_ch = 'B';
 | 
			
		||||
      if (DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE))
 | 
			
		||||
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE)) <= -data[j] &&
 | 
			
		||||
          -data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE)))
 | 
			
		||||
        type_ch = '1';
 | 
			
		||||
      if (DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE))
 | 
			
		||||
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE)) <= -data[j] &&
 | 
			
		||||
          -data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE)))
 | 
			
		||||
        type_ch = '0';
 | 
			
		||||
 | 
			
		||||
      if (abs(data[j]) > 100000) {
 | 
			
		||||
@@ -400,7 +407,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
      } else {
 | 
			
		||||
        sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch);
 | 
			
		||||
      }
 | 
			
		||||
      if (j == data.size() - 1) {
 | 
			
		||||
      if (j + 1 == static_cast<size_t>(data.size())) {
 | 
			
		||||
        ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ namespace dashboard_import {
 | 
			
		||||
 | 
			
		||||
static std::string g_package_import_url;  // NOLINT
 | 
			
		||||
 | 
			
		||||
std::string get_package_import_url() { return g_package_import_url; }
 | 
			
		||||
const std::string &get_package_import_url() { return g_package_import_url; }
 | 
			
		||||
void set_package_import_url(std::string url) { g_package_import_url = std::move(url); }
 | 
			
		||||
 | 
			
		||||
}  // namespace dashboard_import
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace dashboard_import {
 | 
			
		||||
 | 
			
		||||
std::string get_package_import_url();
 | 
			
		||||
const std::string &get_package_import_url();
 | 
			
		||||
void set_package_import_url(std::string url);
 | 
			
		||||
 | 
			
		||||
}  // namespace dashboard_import
 | 
			
		||||
 
 | 
			
		||||
@@ -197,7 +197,8 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All(
 | 
			
		||||
                cv.only_on_esp32,
 | 
			
		||||
                esp32.only_on_variant(
 | 
			
		||||
                    unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from ext1"
 | 
			
		||||
                    unsupported=[VARIANT_ESP32C2, VARIANT_ESP32C3],
 | 
			
		||||
                    msg_prefix="Wakeup from ext1",
 | 
			
		||||
                ),
 | 
			
		||||
                cv.Schema(
 | 
			
		||||
                    {
 | 
			
		||||
@@ -214,7 +215,13 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            cv.Optional(CONF_TOUCH_WAKEUP): cv.All(
 | 
			
		||||
                cv.only_on_esp32,
 | 
			
		||||
                esp32.only_on_variant(
 | 
			
		||||
                    unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from touch"
 | 
			
		||||
                    unsupported=[
 | 
			
		||||
                        VARIANT_ESP32C2,
 | 
			
		||||
                        VARIANT_ESP32C3,
 | 
			
		||||
                        VARIANT_ESP32C6,
 | 
			
		||||
                        VARIANT_ESP32H2,
 | 
			
		||||
                    ],
 | 
			
		||||
                    msg_prefix="Wakeup from touch",
 | 
			
		||||
                ),
 | 
			
		||||
                cv.boolean,
 | 
			
		||||
            ),
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ enum WakeupPinMode {
 | 
			
		||||
  WAKEUP_PIN_MODE_INVERT_WAKEUP,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
 | 
			
		||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
 | 
			
		||||
struct Ext1Wakeup {
 | 
			
		||||
  uint64_t mask;
 | 
			
		||||
  esp_sleep_ext1_wakeup_mode_t wakeup_mode;
 | 
			
		||||
@@ -50,7 +50,7 @@ struct WakeupCauseToRunDuration {
 | 
			
		||||
  uint32_t gpio_cause;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class EnterDeepSleepAction;
 | 
			
		||||
 | 
			
		||||
@@ -73,20 +73,22 @@ class DeepSleepComponent : public Component {
 | 
			
		||||
  void set_wakeup_pin(InternalGPIOPin *pin) { this->wakeup_pin_ = pin; }
 | 
			
		||||
 | 
			
		||||
  void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode);
 | 
			
		||||
#endif
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP32)
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C3)
 | 
			
		||||
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
 | 
			
		||||
  void set_ext1_wakeup(Ext1Wakeup ext1_wakeup);
 | 
			
		||||
 | 
			
		||||
  void set_touch_wakeup(bool touch_wakeup);
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
 | 
			
		||||
    !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
 | 
			
		||||
  void set_touch_wakeup(bool touch_wakeup);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Set the duration in ms for how long the code should run before entering
 | 
			
		||||
  // deep sleep mode, according to the cause the ESP32 has woken.
 | 
			
		||||
  void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration);
 | 
			
		||||
#endif
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 | 
			
		||||
  /// Set a duration in ms for how long the code should run before entering deep sleep mode.
 | 
			
		||||
  void set_run_duration(uint32_t time_ms);
 | 
			
		||||
@@ -117,13 +119,13 @@ class DeepSleepComponent : public Component {
 | 
			
		||||
  InternalGPIOPin *wakeup_pin_;
 | 
			
		||||
  WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE};
 | 
			
		||||
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C3)
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
 | 
			
		||||
  optional<Ext1Wakeup> ext1_wakeup_;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  optional<bool> touch_wakeup_;
 | 
			
		||||
  optional<WakeupCauseToRunDuration> wakeup_cause_to_run_duration_;
 | 
			
		||||
#endif
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
  optional<uint32_t> run_duration_;
 | 
			
		||||
  bool next_enter_deep_sleep_{false};
 | 
			
		||||
  bool prevent_{false};
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,26 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace deep_sleep {
 | 
			
		||||
 | 
			
		||||
// Deep Sleep feature support matrix for ESP32 variants:
 | 
			
		||||
//
 | 
			
		||||
// | Variant   | ext0 | ext1 | Touch | GPIO wakeup |
 | 
			
		||||
// |-----------|------|------|-------|-------------|
 | 
			
		||||
// | ESP32     | ✓    | ✓    | ✓     |             |
 | 
			
		||||
// | ESP32-S2  | ✓    | ✓    | ✓     |             |
 | 
			
		||||
// | ESP32-S3  | ✓    | ✓    | ✓     |             |
 | 
			
		||||
// | ESP32-C2  |      |      |       | ✓           |
 | 
			
		||||
// | ESP32-C3  |      |      |       | ✓           |
 | 
			
		||||
// | ESP32-C5  |      | (✓)  |       | (✓)         |
 | 
			
		||||
// | ESP32-C6  |      | ✓    |       | ✓           |
 | 
			
		||||
// | ESP32-H2  |      | ✓    |       |             |
 | 
			
		||||
//
 | 
			
		||||
// Notes:
 | 
			
		||||
// - (✓) = Supported by hardware but not yet implemented in ESPHome
 | 
			
		||||
// - ext0: Single pin wakeup using RTC GPIO (esp_sleep_enable_ext0_wakeup)
 | 
			
		||||
// - ext1: Multiple pin wakeup (esp_sleep_enable_ext1_wakeup)
 | 
			
		||||
// - Touch: Touch pad wakeup (esp_sleep_enable_touchpad_wakeup)
 | 
			
		||||
// - GPIO wakeup: GPIO wakeup for non-RTC pins (esp_deep_sleep_enable_gpio_wakeup)
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "deep_sleep";
 | 
			
		||||
 | 
			
		||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const {
 | 
			
		||||
@@ -30,13 +50,13 @@ void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) {
 | 
			
		||||
  this->wakeup_pin_mode_ = wakeup_pin_mode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6)
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
 | 
			
		||||
void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; }
 | 
			
		||||
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32H2)
 | 
			
		||||
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
 | 
			
		||||
    !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
 | 
			
		||||
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) {
 | 
			
		||||
@@ -72,9 +92,13 @@ bool DeepSleepComponent::prepare_to_sleep_() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void DeepSleepComponent::deep_sleep_() {
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
 | 
			
		||||
  // Timer wakeup - all variants support this
 | 
			
		||||
  if (this->sleep_duration_.has_value())
 | 
			
		||||
    esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
 | 
			
		||||
 | 
			
		||||
    // Single pin wakeup (ext0) - ESP32, S2, S3 only
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
 | 
			
		||||
    !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
 | 
			
		||||
  if (this->wakeup_pin_ != nullptr) {
 | 
			
		||||
    const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
 | 
			
		||||
    if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) {
 | 
			
		||||
@@ -95,32 +119,15 @@ void DeepSleepComponent::deep_sleep_() {
 | 
			
		||||
    }
 | 
			
		||||
    esp_sleep_enable_ext0_wakeup(gpio_pin, level);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->ext1_wakeup_.has_value()) {
 | 
			
		||||
    esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->touch_wakeup_.has_value() && *(this->touch_wakeup_)) {
 | 
			
		||||
    esp_sleep_enable_touchpad_wakeup();
 | 
			
		||||
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP32_VARIANT_ESP32H2)
 | 
			
		||||
  if (this->sleep_duration_.has_value())
 | 
			
		||||
    esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
 | 
			
		||||
  if (this->ext1_wakeup_.has_value()) {
 | 
			
		||||
    esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6)
 | 
			
		||||
  if (this->sleep_duration_.has_value())
 | 
			
		||||
    esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
 | 
			
		||||
  // GPIO wakeup - C2, C3, C6 only
 | 
			
		||||
#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6)
 | 
			
		||||
  if (this->wakeup_pin_ != nullptr) {
 | 
			
		||||
    const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
 | 
			
		||||
    if (this->wakeup_pin_->get_flags() && gpio::FLAG_PULLUP) {
 | 
			
		||||
    if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) {
 | 
			
		||||
      gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY);
 | 
			
		||||
    } else if (this->wakeup_pin_->get_flags() && gpio::FLAG_PULLDOWN) {
 | 
			
		||||
    } else if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLDOWN) {
 | 
			
		||||
      gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY);
 | 
			
		||||
    }
 | 
			
		||||
    gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
 | 
			
		||||
@@ -138,9 +145,26 @@ void DeepSleepComponent::deep_sleep_() {
 | 
			
		||||
                                      static_cast<esp_deepsleep_gpio_wake_up_mode_t>(level));
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Multiple pin wakeup (ext1) - All except C2, C3
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
 | 
			
		||||
  if (this->ext1_wakeup_.has_value()) {
 | 
			
		||||
    esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Touch wakeup - ESP32, S2, S3 only
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
 | 
			
		||||
    !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
 | 
			
		||||
  if (this->touch_wakeup_.has_value() && *(this->touch_wakeup_)) {
 | 
			
		||||
    esp_sleep_enable_touchpad_wakeup();
 | 
			
		||||
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  esp_deep_sleep_start();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace deep_sleep
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
#endif
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ from esphome import pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import i2c, touchscreen
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN
 | 
			
		||||
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@jesserockz"]
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
@@ -15,7 +15,7 @@ EKTF2232Touchscreen = ektf2232_ns.class_(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_EKTF2232_ID = "ektf2232_id"
 | 
			
		||||
CONF_RTS_PIN = "rts_pin"
 | 
			
		||||
CONF_RTS_PIN = "rts_pin"  # To be removed before 2026.4.0
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
@@ -24,7 +24,10 @@ CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
 | 
			
		||||
            cv.Required(CONF_INTERRUPT_PIN): cv.All(
 | 
			
		||||
                pins.internal_gpio_input_pin_schema
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Required(CONF_RTS_PIN): pins.gpio_output_pin_schema,
 | 
			
		||||
            cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_RTS_PIN): cv.invalid(
 | 
			
		||||
                f"{CONF_RTS_PIN} has been renamed to {CONF_RESET_PIN}"
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(i2c.i2c_device_schema(0x15))
 | 
			
		||||
)
 | 
			
		||||
@@ -37,5 +40,5 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN])
 | 
			
		||||
    cg.add(var.set_interrupt_pin(interrupt_pin))
 | 
			
		||||
    rts_pin = await cg.gpio_pin_expression(config[CONF_RTS_PIN])
 | 
			
		||||
    cg.add(var.set_rts_pin(rts_pin))
 | 
			
		||||
    reset_pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
 | 
			
		||||
    cg.add(var.set_reset_pin(reset_pin))
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ void EKTF2232Touchscreen::setup() {
 | 
			
		||||
 | 
			
		||||
  this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
 | 
			
		||||
 | 
			
		||||
  this->rts_pin_->setup();
 | 
			
		||||
  this->reset_pin_->setup();
 | 
			
		||||
 | 
			
		||||
  this->hard_reset_();
 | 
			
		||||
  if (!this->soft_reset_()) {
 | 
			
		||||
@@ -98,9 +98,9 @@ bool EKTF2232Touchscreen::get_power_state() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EKTF2232Touchscreen::hard_reset_() {
 | 
			
		||||
  this->rts_pin_->digital_write(false);
 | 
			
		||||
  this->reset_pin_->digital_write(false);
 | 
			
		||||
  delay(15);
 | 
			
		||||
  this->rts_pin_->digital_write(true);
 | 
			
		||||
  this->reset_pin_->digital_write(true);
 | 
			
		||||
  delay(15);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -127,7 +127,7 @@ void EKTF2232Touchscreen::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "EKT2232 Touchscreen:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  LOG_PIN("  Interrupt Pin: ", this->interrupt_pin_);
 | 
			
		||||
  LOG_PIN("  RTS Pin: ", this->rts_pin_);
 | 
			
		||||
  LOG_PIN("  Reset Pin: ", this->reset_pin_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace ektf2232
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ class EKTF2232Touchscreen : public Touchscreen, public i2c::I2CDevice {
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
  void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
 | 
			
		||||
  void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; }
 | 
			
		||||
  void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
 | 
			
		||||
 | 
			
		||||
  void set_power_state(bool enable);
 | 
			
		||||
  bool get_power_state();
 | 
			
		||||
@@ -28,7 +28,7 @@ class EKTF2232Touchscreen : public Touchscreen, public i2c::I2CDevice {
 | 
			
		||||
  void update_touches() override;
 | 
			
		||||
 | 
			
		||||
  InternalGPIOPin *interrupt_pin_;
 | 
			
		||||
  GPIOPin *rts_pin_;
 | 
			
		||||
  GPIOPin *reset_pin_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ektf2232
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/epaper_spi/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/epaper_spi/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
							
								
								
									
										80
									
								
								esphome/components/epaper_spi/display.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								esphome/components/epaper_spi/display.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
from esphome import core, pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import display, spi
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_BUSY_PIN,
 | 
			
		||||
    CONF_DC_PIN,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_LAMBDA,
 | 
			
		||||
    CONF_MODEL,
 | 
			
		||||
    CONF_PAGES,
 | 
			
		||||
    CONF_RESET_DURATION,
 | 
			
		||||
    CONF_RESET_PIN,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["split_buffer"]
 | 
			
		||||
DEPENDENCIES = ["spi"]
 | 
			
		||||
 | 
			
		||||
epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi")
 | 
			
		||||
EPaperBase = epaper_spi_ns.class_(
 | 
			
		||||
    "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase)
 | 
			
		||||
EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6)
 | 
			
		||||
 | 
			
		||||
MODELS = {
 | 
			
		||||
    "7.3in-spectra-e6": EPaper7p3InSpectraE6,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    display.FULL_DISPLAY_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(EPaperBase),
 | 
			
		||||
            cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
 | 
			
		||||
            cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"),
 | 
			
		||||
            cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_RESET_DURATION): cv.All(
 | 
			
		||||
                cv.positive_time_period_milliseconds,
 | 
			
		||||
                cv.Range(max=core.TimePeriod(milliseconds=500)),
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(spi.spi_device_schema()),
 | 
			
		||||
    cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
 | 
			
		||||
    "epaper_spi", require_miso=False, require_mosi=True
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    model = MODELS[config[CONF_MODEL]]
 | 
			
		||||
 | 
			
		||||
    rhs = model.new()
 | 
			
		||||
    var = cg.Pvariable(config[CONF_ID], rhs, model)
 | 
			
		||||
 | 
			
		||||
    await display.register_display(var, config)
 | 
			
		||||
    await spi.register_spi_device(var, config)
 | 
			
		||||
 | 
			
		||||
    dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
 | 
			
		||||
    cg.add(var.set_dc_pin(dc))
 | 
			
		||||
 | 
			
		||||
    if CONF_LAMBDA in config:
 | 
			
		||||
        lambda_ = await cg.process_lambda(
 | 
			
		||||
            config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(var.set_writer(lambda_))
 | 
			
		||||
    if CONF_RESET_PIN in config:
 | 
			
		||||
        reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
 | 
			
		||||
        cg.add(var.set_reset_pin(reset))
 | 
			
		||||
    if CONF_BUSY_PIN in config:
 | 
			
		||||
        busy = await cg.gpio_pin_expression(config[CONF_BUSY_PIN])
 | 
			
		||||
        cg.add(var.set_busy_pin(busy))
 | 
			
		||||
    if CONF_RESET_DURATION in config:
 | 
			
		||||
        cg.add(var.set_reset_duration(config[CONF_RESET_DURATION]))
 | 
			
		||||
							
								
								
									
										227
									
								
								esphome/components/epaper_spi/epaper_spi.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								esphome/components/epaper_spi/epaper_spi.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,227 @@
 | 
			
		||||
#include "epaper_spi.h"
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::epaper_spi {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "epaper_spi";
 | 
			
		||||
 | 
			
		||||
static const LogString *epaper_state_to_string(EPaperState state) {
 | 
			
		||||
  switch (state) {
 | 
			
		||||
    case EPaperState::IDLE:
 | 
			
		||||
      return LOG_STR("IDLE");
 | 
			
		||||
    case EPaperState::UPDATE:
 | 
			
		||||
      return LOG_STR("UPDATE");
 | 
			
		||||
    case EPaperState::RESET:
 | 
			
		||||
      return LOG_STR("RESET");
 | 
			
		||||
    case EPaperState::INITIALISE:
 | 
			
		||||
      return LOG_STR("INITIALISE");
 | 
			
		||||
    case EPaperState::TRANSFER_DATA:
 | 
			
		||||
      return LOG_STR("TRANSFER_DATA");
 | 
			
		||||
    case EPaperState::POWER_ON:
 | 
			
		||||
      return LOG_STR("POWER_ON");
 | 
			
		||||
    case EPaperState::REFRESH_SCREEN:
 | 
			
		||||
      return LOG_STR("REFRESH_SCREEN");
 | 
			
		||||
    case EPaperState::POWER_OFF:
 | 
			
		||||
      return LOG_STR("POWER_OFF");
 | 
			
		||||
    case EPaperState::DEEP_SLEEP:
 | 
			
		||||
      return LOG_STR("DEEP_SLEEP");
 | 
			
		||||
    default:
 | 
			
		||||
      return LOG_STR("UNKNOWN");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::setup() {
 | 
			
		||||
  if (!this->init_buffer_(this->get_buffer_length())) {
 | 
			
		||||
    this->mark_failed("Failed to initialise buffer");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->setup_pins_();
 | 
			
		||||
  this->spi_setup();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool EPaperBase::init_buffer_(size_t buffer_length) {
 | 
			
		||||
  if (!this->buffer_.init(buffer_length)) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  this->clear();
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::setup_pins_() {
 | 
			
		||||
  this->dc_pin_->setup();  // OUTPUT
 | 
			
		||||
  this->dc_pin_->digital_write(false);
 | 
			
		||||
 | 
			
		||||
  if (this->reset_pin_ != nullptr) {
 | 
			
		||||
    this->reset_pin_->setup();  // OUTPUT
 | 
			
		||||
    this->reset_pin_->digital_write(true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->busy_pin_ != nullptr) {
 | 
			
		||||
    this->busy_pin_->setup();  // INPUT
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float EPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; }
 | 
			
		||||
 | 
			
		||||
void EPaperBase::command(uint8_t value) {
 | 
			
		||||
  this->start_command_();
 | 
			
		||||
  this->write_byte(value);
 | 
			
		||||
  this->end_command_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::data(uint8_t value) {
 | 
			
		||||
  this->start_data_();
 | 
			
		||||
  this->write_byte(value);
 | 
			
		||||
  this->end_data_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// write a command followed by zero or more bytes of data.
 | 
			
		||||
// The command is the first byte, length is the length of data only in the second byte, followed by the data.
 | 
			
		||||
// [COMMAND, LENGTH, DATA...]
 | 
			
		||||
void EPaperBase::cmd_data(const uint8_t *data) {
 | 
			
		||||
  const uint8_t command = data[0];
 | 
			
		||||
  const uint8_t length = data[1];
 | 
			
		||||
  const uint8_t *ptr = data + 2;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length,
 | 
			
		||||
            format_hex_pretty(ptr, length, '.', false).c_str());
 | 
			
		||||
 | 
			
		||||
  this->dc_pin_->digital_write(false);
 | 
			
		||||
  this->enable();
 | 
			
		||||
  this->write_byte(command);
 | 
			
		||||
  if (length > 0) {
 | 
			
		||||
    this->dc_pin_->digital_write(true);
 | 
			
		||||
    this->write_array(ptr, length);
 | 
			
		||||
  }
 | 
			
		||||
  this->disable();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool EPaperBase::is_idle_() {
 | 
			
		||||
  if (this->busy_pin_ == nullptr) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  return !this->busy_pin_->digital_read();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::reset() {
 | 
			
		||||
  if (this->reset_pin_ != nullptr) {
 | 
			
		||||
    this->reset_pin_->digital_write(false);
 | 
			
		||||
    this->disable_loop();
 | 
			
		||||
    this->set_timeout(this->reset_duration_, [this] {
 | 
			
		||||
      this->reset_pin_->digital_write(true);
 | 
			
		||||
      this->set_timeout(20, [this] { this->enable_loop(); });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::update() {
 | 
			
		||||
  if (!this->state_queue_.empty()) {
 | 
			
		||||
    ESP_LOGE(TAG, "Display update already in progress - %s",
 | 
			
		||||
             LOG_STR_ARG(epaper_state_to_string(this->state_queue_.front())));
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->state_queue_.push(EPaperState::UPDATE);
 | 
			
		||||
  this->state_queue_.push(EPaperState::RESET);
 | 
			
		||||
  this->state_queue_.push(EPaperState::INITIALISE);
 | 
			
		||||
  this->state_queue_.push(EPaperState::TRANSFER_DATA);
 | 
			
		||||
  this->state_queue_.push(EPaperState::POWER_ON);
 | 
			
		||||
  this->state_queue_.push(EPaperState::REFRESH_SCREEN);
 | 
			
		||||
  this->state_queue_.push(EPaperState::POWER_OFF);
 | 
			
		||||
  this->state_queue_.push(EPaperState::DEEP_SLEEP);
 | 
			
		||||
  this->state_queue_.push(EPaperState::IDLE);
 | 
			
		||||
 | 
			
		||||
  this->enable_loop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::loop() {
 | 
			
		||||
  if (this->waiting_for_idle_) {
 | 
			
		||||
    if (this->is_idle_()) {
 | 
			
		||||
      this->waiting_for_idle_ = false;
 | 
			
		||||
    } else {
 | 
			
		||||
      if (App.get_loop_component_start_time() - this->waiting_for_idle_last_print_ >= 1000) {
 | 
			
		||||
        ESP_LOGV(TAG, "Waiting for idle");
 | 
			
		||||
        this->waiting_for_idle_last_print_ = App.get_loop_component_start_time();
 | 
			
		||||
      }
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto state = this->state_queue_.front();
 | 
			
		||||
 | 
			
		||||
  switch (state) {
 | 
			
		||||
    case EPaperState::IDLE:
 | 
			
		||||
      this->disable_loop();
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::UPDATE:
 | 
			
		||||
      this->do_update_();  // Calls ESPHome (current page) lambda
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::RESET:
 | 
			
		||||
      this->reset();
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::INITIALISE:
 | 
			
		||||
      this->initialise_();
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::TRANSFER_DATA:
 | 
			
		||||
      if (!this->transfer_data()) {
 | 
			
		||||
        return;  // Not done yet, come back next loop
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::POWER_ON:
 | 
			
		||||
      this->power_on();
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::REFRESH_SCREEN:
 | 
			
		||||
      this->refresh_screen();
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::POWER_OFF:
 | 
			
		||||
      this->power_off();
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::DEEP_SLEEP:
 | 
			
		||||
      this->deep_sleep();
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  this->state_queue_.pop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::start_command_() {
 | 
			
		||||
  this->dc_pin_->digital_write(false);
 | 
			
		||||
  this->enable();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::end_command_() { this->disable(); }
 | 
			
		||||
 | 
			
		||||
void EPaperBase::start_data_() {
 | 
			
		||||
  this->dc_pin_->digital_write(true);
 | 
			
		||||
  this->enable();
 | 
			
		||||
}
 | 
			
		||||
void EPaperBase::end_data_() { this->disable(); }
 | 
			
		||||
 | 
			
		||||
void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
 | 
			
		||||
 | 
			
		||||
void EPaperBase::initialise_() {
 | 
			
		||||
  size_t index = 0;
 | 
			
		||||
  const auto &sequence = this->init_sequence_;
 | 
			
		||||
  const size_t sequence_size = this->init_sequence_length_;
 | 
			
		||||
  while (index != sequence_size) {
 | 
			
		||||
    if (sequence_size - index < 2) {
 | 
			
		||||
      this->mark_failed("Malformed init sequence");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const auto *ptr = sequence + index;
 | 
			
		||||
    const uint8_t length = ptr[1];
 | 
			
		||||
    if (sequence_size - index < length + 2) {
 | 
			
		||||
      this->mark_failed("Malformed init sequence");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this->cmd_data(ptr);
 | 
			
		||||
    index += length + 2;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->power_on();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::epaper_spi
 | 
			
		||||
							
								
								
									
										93
									
								
								esphome/components/epaper_spi/epaper_spi.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								esphome/components/epaper_spi/epaper_spi.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,93 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/display/display_buffer.h"
 | 
			
		||||
#include "esphome/components/spi/spi.h"
 | 
			
		||||
#include "esphome/components/split_buffer/split_buffer.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
 | 
			
		||||
#include <queue>
 | 
			
		||||
 | 
			
		||||
namespace esphome::epaper_spi {
 | 
			
		||||
 | 
			
		||||
enum class EPaperState : uint8_t {
 | 
			
		||||
  IDLE,
 | 
			
		||||
  UPDATE,
 | 
			
		||||
  RESET,
 | 
			
		||||
  INITIALISE,
 | 
			
		||||
  TRANSFER_DATA,
 | 
			
		||||
  POWER_ON,
 | 
			
		||||
  REFRESH_SCREEN,
 | 
			
		||||
  POWER_OFF,
 | 
			
		||||
  DEEP_SLEEP,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
static const uint8_t MAX_TRANSFER_TIME = 10;  // Transfer in 10ms blocks to allow the loop to run
 | 
			
		||||
 | 
			
		||||
class EPaperBase : public display::DisplayBuffer,
 | 
			
		||||
                   public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
 | 
			
		||||
                                         spi::DATA_RATE_2MHZ> {
 | 
			
		||||
 public:
 | 
			
		||||
  EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length)
 | 
			
		||||
      : init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {}
 | 
			
		||||
  void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
 | 
			
		||||
  void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
 | 
			
		||||
  void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
 | 
			
		||||
 | 
			
		||||
  void command(uint8_t value);
 | 
			
		||||
  void data(uint8_t value);
 | 
			
		||||
  void cmd_data(const uint8_t *data);
 | 
			
		||||
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
 | 
			
		||||
  void on_safe_shutdown() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool is_idle_();
 | 
			
		||||
  void setup_pins_();
 | 
			
		||||
  virtual void reset();
 | 
			
		||||
  void initialise_();
 | 
			
		||||
  bool init_buffer_(size_t buffer_length);
 | 
			
		||||
 | 
			
		||||
  virtual int get_width_controller() { return this->get_width_internal(); };
 | 
			
		||||
  virtual void deep_sleep() = 0;
 | 
			
		||||
  /**
 | 
			
		||||
   * Send data to the device via SPI
 | 
			
		||||
   * @return true if done, false if should be called next loop
 | 
			
		||||
   */
 | 
			
		||||
  virtual bool transfer_data() = 0;
 | 
			
		||||
  virtual void refresh_screen() = 0;
 | 
			
		||||
 | 
			
		||||
  virtual void power_on() = 0;
 | 
			
		||||
  virtual void power_off() = 0;
 | 
			
		||||
  virtual uint32_t get_buffer_length() = 0;
 | 
			
		||||
 | 
			
		||||
  void start_command_();
 | 
			
		||||
  void end_command_();
 | 
			
		||||
  void start_data_();
 | 
			
		||||
  void end_data_();
 | 
			
		||||
 | 
			
		||||
  const size_t init_sequence_length_{0};
 | 
			
		||||
 | 
			
		||||
  size_t current_data_index_{0};
 | 
			
		||||
  uint32_t reset_duration_{200};
 | 
			
		||||
  uint32_t waiting_for_idle_last_print_{0};
 | 
			
		||||
 | 
			
		||||
  GPIOPin *dc_pin_;
 | 
			
		||||
  GPIOPin *busy_pin_{nullptr};
 | 
			
		||||
  GPIOPin *reset_pin_{nullptr};
 | 
			
		||||
 | 
			
		||||
  const uint8_t *init_sequence_{nullptr};
 | 
			
		||||
 | 
			
		||||
  bool waiting_for_idle_{false};
 | 
			
		||||
 | 
			
		||||
  split_buffer::SplitBuffer buffer_;
 | 
			
		||||
 | 
			
		||||
  std::queue<EPaperState> state_queue_{{EPaperState::IDLE}};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::epaper_spi
 | 
			
		||||
@@ -0,0 +1,42 @@
 | 
			
		||||
#include "epaper_spi_model_7p3in_spectra_e6.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::epaper_spi {
 | 
			
		||||
 | 
			
		||||
static constexpr const char *const TAG = "epaper_spi.7.3in-spectra-e6";
 | 
			
		||||
 | 
			
		||||
void EPaper7p3InSpectraE6::power_on() {
 | 
			
		||||
  ESP_LOGI(TAG, "Power on");
 | 
			
		||||
  this->command(0x04);
 | 
			
		||||
  this->waiting_for_idle_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaper7p3InSpectraE6::power_off() {
 | 
			
		||||
  ESP_LOGI(TAG, "Power off");
 | 
			
		||||
  this->command(0x02);
 | 
			
		||||
  this->data(0x00);
 | 
			
		||||
  this->waiting_for_idle_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaper7p3InSpectraE6::refresh_screen() {
 | 
			
		||||
  ESP_LOGI(TAG, "Refresh");
 | 
			
		||||
  this->command(0x12);
 | 
			
		||||
  this->data(0x00);
 | 
			
		||||
  this->waiting_for_idle_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaper7p3InSpectraE6::deep_sleep() {
 | 
			
		||||
  ESP_LOGI(TAG, "Deep sleep");
 | 
			
		||||
  this->command(0x07);
 | 
			
		||||
  this->data(0xA5);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaper7p3InSpectraE6::dump_config() {
 | 
			
		||||
  LOG_DISPLAY("", "E-Paper SPI", this);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Model: 7.3in Spectra E6");
 | 
			
		||||
  LOG_PIN("  Reset Pin: ", this->reset_pin_);
 | 
			
		||||
  LOG_PIN("  DC Pin: ", this->dc_pin_);
 | 
			
		||||
  LOG_PIN("  Busy Pin: ", this->busy_pin_);
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::epaper_spi
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "epaper_spi_spectra_e6.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::epaper_spi {
 | 
			
		||||
 | 
			
		||||
class EPaper7p3InSpectraE6 : public EPaperSpectraE6 {
 | 
			
		||||
  static constexpr const uint16_t WIDTH = 800;
 | 
			
		||||
  static constexpr const uint16_t HEIGHT = 480;
 | 
			
		||||
  // clang-format off
 | 
			
		||||
 | 
			
		||||
  // Command, data length, data
 | 
			
		||||
  static constexpr uint8_t INIT_SEQUENCE[] = {
 | 
			
		||||
    0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,
 | 
			
		||||
    0x01, 1, 0x3F,
 | 
			
		||||
    0x00, 2, 0x5F, 0x69,
 | 
			
		||||
    0x03, 4, 0x00, 0x54, 0x00, 0x44,
 | 
			
		||||
    0x05, 4, 0x40, 0x1F, 0x1F, 0x2C,
 | 
			
		||||
    0x06, 4, 0x6F, 0x1F, 0x17, 0x49,
 | 
			
		||||
    0x08, 4, 0x6F, 0x1F, 0x1F, 0x22,
 | 
			
		||||
    0x30, 1, 0x03,
 | 
			
		||||
    0x50, 1, 0x3F,
 | 
			
		||||
    0x60, 2, 0x02, 0x00,
 | 
			
		||||
    0x61, 4, WIDTH / 256, WIDTH % 256, HEIGHT / 256, HEIGHT % 256,
 | 
			
		||||
    0x84, 1, 0x01,
 | 
			
		||||
    0xE3, 1, 0x2F,
 | 
			
		||||
  };
 | 
			
		||||
  // clang-format on
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  EPaper7p3InSpectraE6() : EPaperSpectraE6(INIT_SEQUENCE, sizeof(INIT_SEQUENCE)) {}
 | 
			
		||||
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  int get_width_internal() override { return WIDTH; };
 | 
			
		||||
  int get_height_internal() override { return HEIGHT; };
 | 
			
		||||
 | 
			
		||||
  void refresh_screen() override;
 | 
			
		||||
  void power_on() override;
 | 
			
		||||
  void power_off() override;
 | 
			
		||||
  void deep_sleep() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::epaper_spi
 | 
			
		||||
							
								
								
									
										135
									
								
								esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,135 @@
 | 
			
		||||
#include "epaper_spi_spectra_e6.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::epaper_spi {
 | 
			
		||||
 | 
			
		||||
static constexpr const char *const TAG = "epaper_spi.6c";
 | 
			
		||||
 | 
			
		||||
static inline uint8_t color_to_hex(Color color) {
 | 
			
		||||
  if (color.red > 127) {
 | 
			
		||||
    if (color.green > 170) {
 | 
			
		||||
      if (color.blue > 127) {
 | 
			
		||||
        return 0x1;  // White
 | 
			
		||||
      } else {
 | 
			
		||||
        return 0x2;  // Yellow
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      return 0x3;  // Red (or Magenta)
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    if (color.green > 127) {
 | 
			
		||||
      if (color.blue > 127) {
 | 
			
		||||
        return 0x5;  // Cyan -> Blue
 | 
			
		||||
      } else {
 | 
			
		||||
        return 0x6;  // Green
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      if (color.blue > 127) {
 | 
			
		||||
        return 0x5;  // Blue
 | 
			
		||||
      } else {
 | 
			
		||||
        return 0x0;  // Black
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperSpectraE6::fill(Color color) {
 | 
			
		||||
  uint8_t pixel_color;
 | 
			
		||||
  if (color.is_on()) {
 | 
			
		||||
    pixel_color = color_to_hex(color);
 | 
			
		||||
  } else {
 | 
			
		||||
    pixel_color = 0x1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // We store 8 bitset<3> in 3 bytes
 | 
			
		||||
  // | byte 1 | byte 2 | byte 3 |
 | 
			
		||||
  // |aaabbbaa|abbbaaab|bbaaabbb|
 | 
			
		||||
  uint8_t byte_1 = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1;
 | 
			
		||||
  uint8_t byte_2 = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2;
 | 
			
		||||
  uint8_t byte_3 = pixel_color << 6 | pixel_color << 3 | pixel_color << 0;
 | 
			
		||||
 | 
			
		||||
  const size_t buffer_length = this->get_buffer_length();
 | 
			
		||||
  for (size_t i = 0; i < buffer_length; i += 3) {
 | 
			
		||||
    this->buffer_[i + 0] = byte_1;
 | 
			
		||||
    this->buffer_[i + 1] = byte_2;
 | 
			
		||||
    this->buffer_[i + 2] = byte_3;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint32_t EPaperSpectraE6::get_buffer_length() {
 | 
			
		||||
  // 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes
 | 
			
		||||
  return this->get_width_controller() * this->get_height_internal() / 8u * 3u;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) {
 | 
			
		||||
  if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  uint8_t pixel_bits = color_to_hex(color);
 | 
			
		||||
  uint32_t pixel_position = x + y * this->get_width_controller();
 | 
			
		||||
  uint32_t first_bit_position = pixel_position * 3;
 | 
			
		||||
  uint32_t byte_position = first_bit_position / 8u;
 | 
			
		||||
  uint32_t byte_subposition = first_bit_position % 8u;
 | 
			
		||||
 | 
			
		||||
  if (byte_subposition <= 5) {
 | 
			
		||||
    this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 << (5 - byte_subposition)))) |
 | 
			
		||||
                                   (pixel_bits << (5 - byte_subposition));
 | 
			
		||||
  } else {
 | 
			
		||||
    this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) |
 | 
			
		||||
                                   (pixel_bits >> (byte_subposition - 5));
 | 
			
		||||
 | 
			
		||||
    this->buffer_[byte_position + 1] =
 | 
			
		||||
        (this->buffer_[byte_position + 1] & (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) |
 | 
			
		||||
        (pixel_bits << (13 - byte_subposition));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HOT EPaperSpectraE6::transfer_data() {
 | 
			
		||||
  const uint32_t start_time = App.get_loop_component_start_time();
 | 
			
		||||
  if (this->current_data_index_ == 0) {
 | 
			
		||||
    ESP_LOGV(TAG, "Sending data");
 | 
			
		||||
    this->command(0x10);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint8_t bytes_to_send[4]{0};
 | 
			
		||||
  const size_t buffer_length = this->get_buffer_length();
 | 
			
		||||
  for (size_t i = this->current_data_index_; i < buffer_length; i += 3) {
 | 
			
		||||
    const uint32_t triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]);
 | 
			
		||||
    // 8 pixels are stored in 3 bytes
 | 
			
		||||
    // |aaabbbaa|abbbaaab|bbaaabbb|
 | 
			
		||||
    // | byte 1 | byte 2 | byte 3 |
 | 
			
		||||
    bytes_to_send[0] = ((triplet >> 17) & 0b01110000) | ((triplet >> 18) & 0b00000111);
 | 
			
		||||
    bytes_to_send[1] = ((triplet >> 11) & 0b01110000) | ((triplet >> 12) & 0b00000111);
 | 
			
		||||
    bytes_to_send[2] = ((triplet >> 5) & 0b01110000) | ((triplet >> 6) & 0b00000111);
 | 
			
		||||
    bytes_to_send[3] = ((triplet << 1) & 0b01110000) | ((triplet << 0) & 0b00000111);
 | 
			
		||||
 | 
			
		||||
    this->start_data_();
 | 
			
		||||
    this->write_array(bytes_to_send, sizeof(bytes_to_send));
 | 
			
		||||
    this->end_data_();
 | 
			
		||||
 | 
			
		||||
    if (millis() - start_time > MAX_TRANSFER_TIME) {
 | 
			
		||||
      // Let the main loop run and come back next loop
 | 
			
		||||
      this->current_data_index_ = i + 3;
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  // Finished the entire dataset
 | 
			
		||||
  this->current_data_index_ = 0;
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperSpectraE6::reset() {
 | 
			
		||||
  if (this->reset_pin_ != nullptr) {
 | 
			
		||||
    this->disable_loop();
 | 
			
		||||
    this->reset_pin_->digital_write(true);
 | 
			
		||||
    this->set_timeout(20, [this] {
 | 
			
		||||
      this->reset_pin_->digital_write(false);
 | 
			
		||||
      delay(2);
 | 
			
		||||
      this->reset_pin_->digital_write(true);
 | 
			
		||||
      this->set_timeout(20, [this] { this->enable_loop(); });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::epaper_spi
 | 
			
		||||
							
								
								
									
										23
									
								
								esphome/components/epaper_spi/epaper_spi_spectra_e6.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/epaper_spi/epaper_spi_spectra_e6.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "epaper_spi.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::epaper_spi {
 | 
			
		||||
 | 
			
		||||
class EPaperSpectraE6 : public EPaperBase {
 | 
			
		||||
 public:
 | 
			
		||||
  EPaperSpectraE6(const uint8_t *init_sequence, const size_t init_sequence_length)
 | 
			
		||||
      : EPaperBase(init_sequence, init_sequence_length) {}
 | 
			
		||||
 | 
			
		||||
  display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
 | 
			
		||||
  void fill(Color color) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void draw_absolute_pixel_internal(int x, int y, Color color) override;
 | 
			
		||||
  uint32_t get_buffer_length() override;
 | 
			
		||||
 | 
			
		||||
  bool transfer_data() override;
 | 
			
		||||
  void reset() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::epaper_spi
 | 
			
		||||
@@ -97,12 +97,12 @@ bool ES7210::set_mic_gain(float mic_gain) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ES7210::configure_sample_rate_() {
 | 
			
		||||
  int mclk_fre = this->sample_rate_ * MCLK_DIV_FRE;
 | 
			
		||||
  uint32_t mclk_fre = this->sample_rate_ * MCLK_DIV_FRE;
 | 
			
		||||
  int coeff = -1;
 | 
			
		||||
 | 
			
		||||
  for (int i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) {
 | 
			
		||||
  for (size_t i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) {
 | 
			
		||||
    if (ES7210_COEFFICIENTS[i].lrclk == this->sample_rate_ && ES7210_COEFFICIENTS[i].mclk == mclk_fre)
 | 
			
		||||
      coeff = i;
 | 
			
		||||
      coeff = static_cast<int>(i);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (coeff >= 0) {
 | 
			
		||||
 
 | 
			
		||||
@@ -36,9 +36,8 @@ from esphome.const import (
 | 
			
		||||
    __version__,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, HexInt, TimePeriod
 | 
			
		||||
from esphome.cpp_generator import RawExpression
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed
 | 
			
		||||
from esphome.helpers import copy_file_if_changed, write_file_if_changed
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
from esphome.writer import clean_cmake_cache
 | 
			
		||||
 | 
			
		||||
@@ -157,8 +156,6 @@ def set_core_data(config):
 | 
			
		||||
    conf = config[CONF_FRAMEWORK]
 | 
			
		||||
    if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
 | 
			
		||||
        CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "esp-idf"
 | 
			
		||||
        CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {}
 | 
			
		||||
        CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
 | 
			
		||||
    elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
 | 
			
		||||
        CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino"
 | 
			
		||||
        if variant not in ARDUINO_ALLOWED_VARIANTS:
 | 
			
		||||
@@ -166,6 +163,8 @@ def set_core_data(config):
 | 
			
		||||
                f"ESPHome does not support using the Arduino framework for the {variant}. Please use the ESP-IDF framework instead.",
 | 
			
		||||
                path=[CONF_FRAMEWORK, CONF_TYPE],
 | 
			
		||||
            )
 | 
			
		||||
    CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {}
 | 
			
		||||
    CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
 | 
			
		||||
    CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
 | 
			
		||||
        config[CONF_FRAMEWORK][CONF_VERSION]
 | 
			
		||||
    )
 | 
			
		||||
@@ -236,8 +235,6 @@ SdkconfigValueType = bool | int | HexInt | str | RawSdkconfigValue
 | 
			
		||||
 | 
			
		||||
def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType):
 | 
			
		||||
    """Set an esp-idf sdkconfig value."""
 | 
			
		||||
    if not CORE.using_esp_idf:
 | 
			
		||||
        raise ValueError("Not an esp-idf project")
 | 
			
		||||
    CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS][name] = value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -252,8 +249,6 @@ def add_idf_component(
 | 
			
		||||
    submodules: list[str] | None = None,
 | 
			
		||||
):
 | 
			
		||||
    """Add an esp-idf component to the project."""
 | 
			
		||||
    if not CORE.using_esp_idf:
 | 
			
		||||
        raise ValueError("Not an esp-idf project")
 | 
			
		||||
    if not repo and not ref and not path:
 | 
			
		||||
        raise ValueError("Requires at least one of repo, ref or path")
 | 
			
		||||
    if refresh or submodules or components:
 | 
			
		||||
@@ -277,14 +272,14 @@ def add_idf_component(
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_extra_script(stage: str, filename: str, path: str):
 | 
			
		||||
def add_extra_script(stage: str, filename: str, path: Path):
 | 
			
		||||
    """Add an extra script to the project."""
 | 
			
		||||
    key = f"{stage}:{filename}"
 | 
			
		||||
    if add_extra_build_file(filename, path):
 | 
			
		||||
        cg.add_platformio_option("extra_scripts", [key])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_extra_build_file(filename: str, path: str) -> bool:
 | 
			
		||||
def add_extra_build_file(filename: str, path: Path) -> bool:
 | 
			
		||||
    """Add an extra build file to the project."""
 | 
			
		||||
    if filename not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
 | 
			
		||||
        CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES][filename] = {
 | 
			
		||||
@@ -301,19 +296,25 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
 | 
			
		||||
    return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _format_framework_espidf_version(
 | 
			
		||||
    ver: cv.Version, release: str, for_platformio: bool
 | 
			
		||||
) -> str:
 | 
			
		||||
    # format the given arduino (https://github.com/espressif/esp-idf/releases) version to
 | 
			
		||||
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
 | 
			
		||||
    # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
 | 
			
		||||
    # a PIO platformio/framework-espidf value
 | 
			
		||||
    # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
 | 
			
		||||
    if for_platformio:
 | 
			
		||||
        return f"platformio/framework-espidf@~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
 | 
			
		||||
    if release:
 | 
			
		||||
        return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip"
 | 
			
		||||
    return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _is_framework_url(source: str) -> str:
 | 
			
		||||
    # platformio accepts many URL schemes for framework repositories and archives including http, https, git, file, and symlink
 | 
			
		||||
    import urllib.parse
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        parsed = urllib.parse.urlparse(source)
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        return False
 | 
			
		||||
    return bool(parsed.scheme)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# NOTE: Keep this in mind when updating the recommended version:
 | 
			
		||||
#  * New framework historically have had some regressions, especially for WiFi.
 | 
			
		||||
#    The new version needs to be thoroughly validated before changing the
 | 
			
		||||
@@ -322,154 +323,115 @@ def _format_framework_espidf_version(
 | 
			
		||||
 | 
			
		||||
# The default/recommended arduino framework version
 | 
			
		||||
#  - https://github.com/espressif/arduino-esp32/releases
 | 
			
		||||
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1)
 | 
			
		||||
# The platform-espressif32 version to use for arduino frameworks
 | 
			
		||||
#  - https://github.com/pioarduino/platform-espressif32/releases
 | 
			
		||||
ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
 | 
			
		||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
 | 
			
		||||
    "recommended": cv.Version(3, 2, 1),
 | 
			
		||||
    "latest": cv.Version(3, 3, 2),
 | 
			
		||||
    "dev": cv.Version(3, 3, 2),
 | 
			
		||||
}
 | 
			
		||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
 | 
			
		||||
    cv.Version(3, 3, 2): cv.Version(55, 3, 31, "1"),
 | 
			
		||||
    cv.Version(3, 3, 1): cv.Version(55, 3, 31, "1"),
 | 
			
		||||
    cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"),
 | 
			
		||||
    cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"),
 | 
			
		||||
    cv.Version(3, 2, 0): cv.Version(54, 3, 20),
 | 
			
		||||
    cv.Version(3, 1, 3): cv.Version(53, 3, 13),
 | 
			
		||||
    cv.Version(3, 1, 2): cv.Version(53, 3, 12),
 | 
			
		||||
    cv.Version(3, 1, 1): cv.Version(53, 3, 11),
 | 
			
		||||
    cv.Version(3, 1, 0): cv.Version(53, 3, 10),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# The default/recommended esp-idf framework version
 | 
			
		||||
#  - https://github.com/espressif/esp-idf/releases
 | 
			
		||||
#  - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
 | 
			
		||||
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2)
 | 
			
		||||
# The platformio/espressif32 version to use for esp-idf frameworks
 | 
			
		||||
#  - https://github.com/platformio/platform-espressif32/releases
 | 
			
		||||
#  - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
 | 
			
		||||
ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
 | 
			
		||||
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
 | 
			
		||||
    "recommended": cv.Version(5, 4, 2),
 | 
			
		||||
    "latest": cv.Version(5, 5, 1),
 | 
			
		||||
    "dev": cv.Version(5, 5, 1),
 | 
			
		||||
}
 | 
			
		||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
 | 
			
		||||
    cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"),
 | 
			
		||||
    cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"),
 | 
			
		||||
    cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
 | 
			
		||||
    cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
 | 
			
		||||
    cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"),
 | 
			
		||||
    cv.Version(5, 3, 2): cv.Version(53, 3, 13),
 | 
			
		||||
    cv.Version(5, 3, 1): cv.Version(53, 3, 13),
 | 
			
		||||
    cv.Version(5, 3, 0): cv.Version(53, 3, 13),
 | 
			
		||||
    cv.Version(5, 1, 6): cv.Version(51, 3, 7),
 | 
			
		||||
    cv.Version(5, 1, 5): cv.Version(51, 3, 7),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions
 | 
			
		||||
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
 | 
			
		||||
    cv.Version(5, 3, 1),
 | 
			
		||||
    cv.Version(5, 3, 0),
 | 
			
		||||
    cv.Version(5, 2, 2),
 | 
			
		||||
    cv.Version(5, 2, 1),
 | 
			
		||||
    cv.Version(5, 1, 2),
 | 
			
		||||
    cv.Version(5, 1, 1),
 | 
			
		||||
    cv.Version(5, 1, 0),
 | 
			
		||||
    cv.Version(5, 0, 2),
 | 
			
		||||
    cv.Version(5, 0, 1),
 | 
			
		||||
    cv.Version(5, 0, 0),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
# pioarduino versions that don't require a release number
 | 
			
		||||
# List based on https://github.com/pioarduino/esp-idf/releases
 | 
			
		||||
SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
 | 
			
		||||
    cv.Version(5, 5, 0),
 | 
			
		||||
    cv.Version(5, 4, 2),
 | 
			
		||||
    cv.Version(5, 4, 1),
 | 
			
		||||
    cv.Version(5, 4, 0),
 | 
			
		||||
    cv.Version(5, 3, 3),
 | 
			
		||||
    cv.Version(5, 3, 2),
 | 
			
		||||
    cv.Version(5, 3, 1),
 | 
			
		||||
    cv.Version(5, 3, 0),
 | 
			
		||||
    cv.Version(5, 1, 5),
 | 
			
		||||
    cv.Version(5, 1, 6),
 | 
			
		||||
]
 | 
			
		||||
# The platform-espressif32 version
 | 
			
		||||
#  - https://github.com/pioarduino/platform-espressif32/releases
 | 
			
		||||
PLATFORM_VERSION_LOOKUP = {
 | 
			
		||||
    "recommended": cv.Version(54, 3, 21, "2"),
 | 
			
		||||
    "latest": cv.Version(55, 3, 31, "1"),
 | 
			
		||||
    "dev": cv.Version(55, 3, 31, "1"),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _arduino_check_versions(value):
 | 
			
		||||
def _check_versions(value):
 | 
			
		||||
    value = value.copy()
 | 
			
		||||
    lookups = {
 | 
			
		||||
        "dev": (cv.Version(3, 2, 1), "https://github.com/espressif/arduino-esp32.git"),
 | 
			
		||||
        "latest": (cv.Version(3, 2, 1), None),
 | 
			
		||||
        "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if value[CONF_VERSION] in lookups:
 | 
			
		||||
        if CONF_SOURCE in value:
 | 
			
		||||
    if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP:
 | 
			
		||||
        if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value:
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                "Framework version needs to be explicitly specified when custom source is used."
 | 
			
		||||
                "Version needs to be explicitly set when a custom source or platform_version is used."
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        version, source = lookups[value[CONF_VERSION]]
 | 
			
		||||
        platform_lookup = PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]]
 | 
			
		||||
        value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup))
 | 
			
		||||
 | 
			
		||||
        if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
 | 
			
		||||
            version = ARDUINO_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]]
 | 
			
		||||
        else:
 | 
			
		||||
            version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]]
 | 
			
		||||
    else:
 | 
			
		||||
        version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
 | 
			
		||||
        source = value.get(CONF_SOURCE, None)
 | 
			
		||||
 | 
			
		||||
    value[CONF_VERSION] = str(version)
 | 
			
		||||
    value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
 | 
			
		||||
 | 
			
		||||
    value[CONF_PLATFORM_VERSION] = value.get(
 | 
			
		||||
        CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION))
 | 
			
		||||
    )
 | 
			
		||||
    if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
 | 
			
		||||
        if version < cv.Version(3, 0, 0):
 | 
			
		||||
            raise cv.Invalid("Only Arduino 3.0+ is supported.")
 | 
			
		||||
        recommended_version = ARDUINO_FRAMEWORK_VERSION_LOOKUP["recommended"]
 | 
			
		||||
        platform_lookup = ARDUINO_PLATFORM_VERSION_LOOKUP.get(version)
 | 
			
		||||
        value[CONF_SOURCE] = value.get(
 | 
			
		||||
            CONF_SOURCE, _format_framework_arduino_version(version)
 | 
			
		||||
        )
 | 
			
		||||
        if _is_framework_url(value[CONF_SOURCE]):
 | 
			
		||||
            value[CONF_SOURCE] = (
 | 
			
		||||
                f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}"
 | 
			
		||||
            )
 | 
			
		||||
    else:
 | 
			
		||||
        if version < cv.Version(5, 0, 0):
 | 
			
		||||
            raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
 | 
			
		||||
        recommended_version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"]
 | 
			
		||||
        platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version)
 | 
			
		||||
        value[CONF_SOURCE] = value.get(
 | 
			
		||||
            CONF_SOURCE,
 | 
			
		||||
            _format_framework_espidf_version(version, value.get(CONF_RELEASE, None)),
 | 
			
		||||
        )
 | 
			
		||||
        if _is_framework_url(value[CONF_SOURCE]):
 | 
			
		||||
            value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
 | 
			
		||||
 | 
			
		||||
    if value[CONF_SOURCE].startswith("http"):
 | 
			
		||||
        # prefix is necessary or platformio will complain with a cryptic error
 | 
			
		||||
        value[CONF_SOURCE] = f"framework-arduinoespressif32@{value[CONF_SOURCE]}"
 | 
			
		||||
    if CONF_PLATFORM_VERSION not in value:
 | 
			
		||||
        if platform_lookup is None:
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                "Framework version not recognized; please specify platform_version"
 | 
			
		||||
            )
 | 
			
		||||
        value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup))
 | 
			
		||||
 | 
			
		||||
    if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
 | 
			
		||||
    if version != recommended_version:
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "The selected Arduino framework version is not the recommended one. "
 | 
			
		||||
            "The selected framework version is not the recommended one. "
 | 
			
		||||
            "If there are connectivity or build issues please remove the manual version."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _esp_idf_check_versions(value):
 | 
			
		||||
    value = value.copy()
 | 
			
		||||
    lookups = {
 | 
			
		||||
        "dev": (cv.Version(5, 4, 2), "https://github.com/espressif/esp-idf.git"),
 | 
			
		||||
        "latest": (cv.Version(5, 2, 2), None),
 | 
			
		||||
        "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if value[CONF_VERSION] in lookups:
 | 
			
		||||
        if CONF_SOURCE in value:
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                "Framework version needs to be explicitly specified when custom source is used."
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        version, source = lookups[value[CONF_VERSION]]
 | 
			
		||||
    else:
 | 
			
		||||
        version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
 | 
			
		||||
        source = value.get(CONF_SOURCE, None)
 | 
			
		||||
 | 
			
		||||
    if version < cv.Version(5, 0, 0):
 | 
			
		||||
        raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
 | 
			
		||||
 | 
			
		||||
    # flag this for later *before* we set value[CONF_PLATFORM_VERSION] below
 | 
			
		||||
    has_platform_ver = CONF_PLATFORM_VERSION in value
 | 
			
		||||
 | 
			
		||||
    value[CONF_PLATFORM_VERSION] = value.get(
 | 
			
		||||
        CONF_PLATFORM_VERSION, _parse_platform_version(str(ESP_IDF_PLATFORM_VERSION))
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        is_platformio := _platform_is_platformio(value[CONF_PLATFORM_VERSION])
 | 
			
		||||
    ) and version not in SUPPORTED_PLATFORMIO_ESP_IDF_5X:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"ESP-IDF {str(version)} not supported by platformio/espressif32"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        version in SUPPORTED_PLATFORMIO_ESP_IDF_5X
 | 
			
		||||
        and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X
 | 
			
		||||
    ) and not has_platform_ver:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"ESP-IDF {value[CONF_VERSION]} may be supported by platformio/espressif32; please specify '{CONF_PLATFORM_VERSION}'"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        not is_platformio
 | 
			
		||||
        and CONF_RELEASE not in value
 | 
			
		||||
        and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X
 | 
			
		||||
    if value[CONF_PLATFORM_VERSION] != _parse_platform_version(
 | 
			
		||||
        str(PLATFORM_VERSION_LOOKUP["recommended"])
 | 
			
		||||
    ):
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"ESP-IDF {value[CONF_VERSION]} is not available with pioarduino; you may need to specify '{CONF_RELEASE}'"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    value[CONF_VERSION] = str(version)
 | 
			
		||||
    value[CONF_SOURCE] = source or _format_framework_espidf_version(
 | 
			
		||||
        version, value.get(CONF_RELEASE, None), is_platformio
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if value[CONF_SOURCE].startswith("http"):
 | 
			
		||||
        # prefix is necessary or platformio will complain with a cryptic error
 | 
			
		||||
        value[CONF_SOURCE] = f"framework-espidf@{value[CONF_SOURCE]}"
 | 
			
		||||
 | 
			
		||||
    if version != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION:
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "The selected ESP-IDF framework version is not the recommended one. "
 | 
			
		||||
            "The selected platform version is not the recommended one. "
 | 
			
		||||
            "If there are connectivity or build issues please remove the manual version."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -479,26 +441,14 @@ def _esp_idf_check_versions(value):
 | 
			
		||||
def _parse_platform_version(value):
 | 
			
		||||
    try:
 | 
			
		||||
        ver = cv.Version.parse(cv.version_number(value))
 | 
			
		||||
        if ver.major >= 50:  # a pioarduino version
 | 
			
		||||
            release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
 | 
			
		||||
            if ver.extra:
 | 
			
		||||
                release += f"-{ver.extra}"
 | 
			
		||||
            return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip"
 | 
			
		||||
        # if platform version is a valid version constraint, prefix the default package
 | 
			
		||||
        cv.platformio_version_constraint(value)
 | 
			
		||||
        return f"platformio/espressif32@{value}"
 | 
			
		||||
        release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
 | 
			
		||||
        if ver.extra:
 | 
			
		||||
            release += f"-{ver.extra}"
 | 
			
		||||
        return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip"
 | 
			
		||||
    except cv.Invalid:
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _platform_is_platformio(value):
 | 
			
		||||
    try:
 | 
			
		||||
        ver = cv.Version.parse(cv.version_number(value))
 | 
			
		||||
        return ver.major < 50
 | 
			
		||||
    except cv.Invalid:
 | 
			
		||||
        return "platformio" in value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _detect_variant(value):
 | 
			
		||||
    board = value.get(CONF_BOARD)
 | 
			
		||||
    variant = value.get(CONF_VARIANT)
 | 
			
		||||
@@ -588,24 +538,6 @@ def final_validate(config):
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ARDUINO_FRAMEWORK_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
 | 
			
		||||
            cv.Optional(CONF_SOURCE): cv.string_strict,
 | 
			
		||||
            cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version,
 | 
			
		||||
            cv.Optional(CONF_ADVANCED, default={}): cv.Schema(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.Optional(
 | 
			
		||||
                        CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False
 | 
			
		||||
                    ): cv.boolean,
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
    _arduino_check_versions,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_SDKCONFIG_OPTIONS = "sdkconfig_options"
 | 
			
		||||
CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server"
 | 
			
		||||
CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries"
 | 
			
		||||
@@ -624,9 +556,14 @@ def _validate_idf_component(config: ConfigType) -> ConfigType:
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
 | 
			
		||||
FRAMEWORK_ESP_IDF = "esp-idf"
 | 
			
		||||
FRAMEWORK_ARDUINO = "arduino"
 | 
			
		||||
FRAMEWORK_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Optional(CONF_TYPE, default=FRAMEWORK_ARDUINO): cv.one_of(
 | 
			
		||||
                FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
 | 
			
		||||
            cv.Optional(CONF_RELEASE): cv.string_strict,
 | 
			
		||||
            cv.Optional(CONF_SOURCE): cv.string_strict,
 | 
			
		||||
@@ -690,7 +627,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
    _esp_idf_check_versions,
 | 
			
		||||
    _check_versions,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -720,6 +657,7 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
 | 
			
		||||
        + "Why change? ESP-IDF offers:\n"
 | 
			
		||||
        + color(AnsiFore.GREEN, "  ✨ Up to 40% smaller binaries\n")
 | 
			
		||||
        + color(AnsiFore.GREEN, "  🚀 Better performance and optimization\n")
 | 
			
		||||
        + color(AnsiFore.GREEN, "  ⚡ 2-3x faster compile times\n")
 | 
			
		||||
        + color(AnsiFore.GREEN, "  📦 Custom-built firmware for your exact needs\n")
 | 
			
		||||
        + color(
 | 
			
		||||
            AnsiFore.GREEN,
 | 
			
		||||
@@ -727,7 +665,6 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
 | 
			
		||||
        )
 | 
			
		||||
        + "\n"
 | 
			
		||||
        + "Trade-offs:\n"
 | 
			
		||||
        + color(AnsiFore.YELLOW, "  ⏱️  Compile times are ~25% longer\n")
 | 
			
		||||
        + color(AnsiFore.YELLOW, "  🔄 Some components need migration\n")
 | 
			
		||||
        + "\n"
 | 
			
		||||
        + "What should I do?\n"
 | 
			
		||||
@@ -757,32 +694,18 @@ def _set_default_framework(config):
 | 
			
		||||
        config = config.copy()
 | 
			
		||||
 | 
			
		||||
        variant = config[CONF_VARIANT]
 | 
			
		||||
        config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({})
 | 
			
		||||
        if variant in ARDUINO_ALLOWED_VARIANTS:
 | 
			
		||||
            config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({})
 | 
			
		||||
            config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO
 | 
			
		||||
            # Show the migration message
 | 
			
		||||
            _show_framework_migration_message(
 | 
			
		||||
                config.get(CONF_NAME, "This device"), variant
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({})
 | 
			
		||||
            config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF
 | 
			
		||||
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FRAMEWORK_ESP_IDF = "esp-idf"
 | 
			
		||||
FRAMEWORK_ARDUINO = "arduino"
 | 
			
		||||
FRAMEWORK_SCHEMA = cv.typed_schema(
 | 
			
		||||
    {
 | 
			
		||||
        FRAMEWORK_ESP_IDF: ESP_IDF_FRAMEWORK_SCHEMA,
 | 
			
		||||
        FRAMEWORK_ARDUINO: ARDUINO_FRAMEWORK_SCHEMA,
 | 
			
		||||
    },
 | 
			
		||||
    lower=True,
 | 
			
		||||
    space="-",
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FLASH_SIZES = [
 | 
			
		||||
    "2MB",
 | 
			
		||||
    "4MB",
 | 
			
		||||
@@ -837,6 +760,8 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    conf = config[CONF_FRAMEWORK]
 | 
			
		||||
    cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
 | 
			
		||||
    if CONF_SOURCE in conf:
 | 
			
		||||
        cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
 | 
			
		||||
 | 
			
		||||
    if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
 | 
			
		||||
        cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
 | 
			
		||||
@@ -847,142 +772,146 @@ async def to_code(config):
 | 
			
		||||
    add_extra_script(
 | 
			
		||||
        "post",
 | 
			
		||||
        "post_build.py",
 | 
			
		||||
        os.path.join(os.path.dirname(__file__), "post_build.py.script"),
 | 
			
		||||
        Path(__file__).parent / "post_build.py.script",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    freq = config[CONF_CPU_FREQUENCY][:-3]
 | 
			
		||||
    if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
 | 
			
		||||
        cg.add_platformio_option("framework", "espidf")
 | 
			
		||||
        cg.add_build_flag("-DUSE_ESP_IDF")
 | 
			
		||||
        cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
 | 
			
		||||
        cg.add_build_flag("-Wno-nonnull-compare")
 | 
			
		||||
 | 
			
		||||
        cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
 | 
			
		||||
 | 
			
		||||
        add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
 | 
			
		||||
        add_idf_sdkconfig_option(
 | 
			
		||||
            f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
 | 
			
		||||
        )
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True)
 | 
			
		||||
        add_idf_sdkconfig_option(
 | 
			
		||||
            "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME", "partitions.csv"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_FREERTOS_HZ", 1000)
 | 
			
		||||
 | 
			
		||||
        # Setup watchdog
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False)
 | 
			
		||||
 | 
			
		||||
        # Disable dynamic log level control to save memory
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
 | 
			
		||||
 | 
			
		||||
        # Set default CPU frequency
 | 
			
		||||
        add_idf_sdkconfig_option(f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{freq}", True)
 | 
			
		||||
 | 
			
		||||
        # Apply LWIP optimization settings
 | 
			
		||||
        advanced = conf[CONF_ADVANCED]
 | 
			
		||||
        # DHCP server: only disable if explicitly set to false
 | 
			
		||||
        # WiFi component handles its own optimization when AP mode is not used
 | 
			
		||||
        if (
 | 
			
		||||
            CONF_ENABLE_LWIP_DHCP_SERVER in advanced
 | 
			
		||||
            and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER]
 | 
			
		||||
        ):
 | 
			
		||||
            add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
 | 
			
		||||
        if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True):
 | 
			
		||||
            add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
 | 
			
		||||
        if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
 | 
			
		||||
            add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
 | 
			
		||||
        if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
 | 
			
		||||
            add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
 | 
			
		||||
            add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
 | 
			
		||||
 | 
			
		||||
        # Apply LWIP core locking for better socket performance
 | 
			
		||||
        # This is already enabled by default in Arduino framework, where it provides
 | 
			
		||||
        # significant performance benefits. Our benchmarks show socket operations are
 | 
			
		||||
        # 24-200% faster with core locking enabled:
 | 
			
		||||
        # - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default)
 | 
			
		||||
        # - Up to 200% slower under load when all operations queue through tcpip_thread
 | 
			
		||||
        # Enabling this makes ESP-IDF socket performance match Arduino framework.
 | 
			
		||||
        if advanced.get(CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, True):
 | 
			
		||||
            add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True)
 | 
			
		||||
        if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True):
 | 
			
		||||
            add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True)
 | 
			
		||||
 | 
			
		||||
        cg.add_platformio_option("board_build.partitions", "partitions.csv")
 | 
			
		||||
        if CONF_PARTITIONS in config:
 | 
			
		||||
            add_extra_build_file(
 | 
			
		||||
                "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        if assertion_level := advanced.get(CONF_ASSERTION_LEVEL):
 | 
			
		||||
            for key, flag in ASSERTION_LEVELS.items():
 | 
			
		||||
                add_idf_sdkconfig_option(flag, assertion_level == key)
 | 
			
		||||
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False)
 | 
			
		||||
        compiler_optimization = advanced.get(CONF_COMPILER_OPTIMIZATION)
 | 
			
		||||
        for key, flag in COMPILER_OPTIMIZATIONS.items():
 | 
			
		||||
            add_idf_sdkconfig_option(flag, compiler_optimization == key)
 | 
			
		||||
 | 
			
		||||
        add_idf_sdkconfig_option(
 | 
			
		||||
            "CONFIG_LWIP_ESP_LWIP_ASSERT",
 | 
			
		||||
            conf[CONF_ADVANCED][CONF_ENABLE_LWIP_ASSERT],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if advanced.get(CONF_IGNORE_EFUSE_MAC_CRC):
 | 
			
		||||
            add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True)
 | 
			
		||||
            add_idf_sdkconfig_option(
 | 
			
		||||
                "CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False
 | 
			
		||||
            )
 | 
			
		||||
        if advanced.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES):
 | 
			
		||||
            _LOGGER.warning(
 | 
			
		||||
                "Using experimental features in ESP-IDF may result in unexpected failures."
 | 
			
		||||
            )
 | 
			
		||||
            add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True)
 | 
			
		||||
 | 
			
		||||
        cg.add_define(
 | 
			
		||||
            "USE_ESP_IDF_VERSION_CODE",
 | 
			
		||||
            cg.RawExpression(
 | 
			
		||||
                f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        add_idf_sdkconfig_option(
 | 
			
		||||
            f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
 | 
			
		||||
            add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
 | 
			
		||||
 | 
			
		||||
        for component in conf[CONF_COMPONENTS]:
 | 
			
		||||
            add_idf_component(
 | 
			
		||||
                name=component[CONF_NAME],
 | 
			
		||||
                repo=component.get(CONF_SOURCE),
 | 
			
		||||
                ref=component.get(CONF_REF),
 | 
			
		||||
                path=component.get(CONF_PATH),
 | 
			
		||||
            )
 | 
			
		||||
    elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
 | 
			
		||||
        cg.add_platformio_option("framework", "arduino")
 | 
			
		||||
    else:
 | 
			
		||||
        cg.add_platformio_option("framework", "arduino, espidf")
 | 
			
		||||
        cg.add_build_flag("-DUSE_ARDUINO")
 | 
			
		||||
        cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
 | 
			
		||||
        cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
 | 
			
		||||
 | 
			
		||||
        if CONF_PARTITIONS in config:
 | 
			
		||||
            cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS])
 | 
			
		||||
        else:
 | 
			
		||||
            cg.add_platformio_option("board_build.partitions", "partitions.csv")
 | 
			
		||||
 | 
			
		||||
        cg.add_platformio_option(
 | 
			
		||||
            "board_build.embed_txtfiles",
 | 
			
		||||
            [
 | 
			
		||||
                "managed_components/espressif__esp_insights/server_certs/https_server.crt",
 | 
			
		||||
                "managed_components/espressif__esp_rainmaker/server_certs/rmaker_mqtt_server.crt",
 | 
			
		||||
                "managed_components/espressif__esp_rainmaker/server_certs/rmaker_claim_service_server.crt",
 | 
			
		||||
                "managed_components/espressif__esp_rainmaker/server_certs/rmaker_ota_server.crt",
 | 
			
		||||
            ],
 | 
			
		||||
        )
 | 
			
		||||
        cg.add_define(
 | 
			
		||||
            "USE_ARDUINO_VERSION_CODE",
 | 
			
		||||
            cg.RawExpression(
 | 
			
		||||
                f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(RawExpression(f"setCpuFrequencyMhz({freq})"))
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
 | 
			
		||||
 | 
			
		||||
    cg.add_build_flag("-Wno-nonnull-compare")
 | 
			
		||||
 | 
			
		||||
    add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
 | 
			
		||||
    add_idf_sdkconfig_option(
 | 
			
		||||
        f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
 | 
			
		||||
    )
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False)
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True)
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM_FILENAME", "partitions.csv")
 | 
			
		||||
 | 
			
		||||
    # Increase freertos tick speed from 100Hz to 1kHz so that delay() resolution is 1ms
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_FREERTOS_HZ", 1000)
 | 
			
		||||
 | 
			
		||||
    # Setup watchdog
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT", True)
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True)
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False)
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False)
 | 
			
		||||
 | 
			
		||||
    # Disable dynamic log level control to save memory
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
 | 
			
		||||
 | 
			
		||||
    # Set default CPU frequency
 | 
			
		||||
    add_idf_sdkconfig_option(
 | 
			
		||||
        f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{config[CONF_CPU_FREQUENCY][:-3]}", True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Apply LWIP optimization settings
 | 
			
		||||
    advanced = conf[CONF_ADVANCED]
 | 
			
		||||
    # DHCP server: only disable if explicitly set to false
 | 
			
		||||
    # WiFi component handles its own optimization when AP mode is not used
 | 
			
		||||
    # When using Arduino with Ethernet, DHCP server functions must be available
 | 
			
		||||
    # for the Network library to compile, even if not actively used
 | 
			
		||||
    if (
 | 
			
		||||
        CONF_ENABLE_LWIP_DHCP_SERVER in advanced
 | 
			
		||||
        and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER]
 | 
			
		||||
        and not (
 | 
			
		||||
            conf[CONF_TYPE] == FRAMEWORK_ARDUINO
 | 
			
		||||
            and "ethernet" in CORE.loaded_integrations
 | 
			
		||||
        )
 | 
			
		||||
    ):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
 | 
			
		||||
    if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
 | 
			
		||||
    if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
 | 
			
		||||
    if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
 | 
			
		||||
 | 
			
		||||
    # Apply LWIP core locking for better socket performance
 | 
			
		||||
    # This is already enabled by default in Arduino framework, where it provides
 | 
			
		||||
    # significant performance benefits. Our benchmarks show socket operations are
 | 
			
		||||
    # 24-200% faster with core locking enabled:
 | 
			
		||||
    # - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default)
 | 
			
		||||
    # - Up to 200% slower under load when all operations queue through tcpip_thread
 | 
			
		||||
    # Enabling this makes ESP-IDF socket performance match Arduino framework.
 | 
			
		||||
    if advanced.get(CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, True):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True)
 | 
			
		||||
    if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True)
 | 
			
		||||
 | 
			
		||||
    cg.add_platformio_option("board_build.partitions", "partitions.csv")
 | 
			
		||||
    if CONF_PARTITIONS in config:
 | 
			
		||||
        add_extra_build_file(
 | 
			
		||||
            "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if assertion_level := advanced.get(CONF_ASSERTION_LEVEL):
 | 
			
		||||
        for key, flag in ASSERTION_LEVELS.items():
 | 
			
		||||
            add_idf_sdkconfig_option(flag, assertion_level == key)
 | 
			
		||||
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False)
 | 
			
		||||
    compiler_optimization = advanced.get(CONF_COMPILER_OPTIMIZATION)
 | 
			
		||||
    for key, flag in COMPILER_OPTIMIZATIONS.items():
 | 
			
		||||
        add_idf_sdkconfig_option(flag, compiler_optimization == key)
 | 
			
		||||
 | 
			
		||||
    add_idf_sdkconfig_option(
 | 
			
		||||
        "CONFIG_LWIP_ESP_LWIP_ASSERT",
 | 
			
		||||
        conf[CONF_ADVANCED][CONF_ENABLE_LWIP_ASSERT],
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if advanced.get(CONF_IGNORE_EFUSE_MAC_CRC):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False)
 | 
			
		||||
    if advanced.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES):
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "Using experimental features in ESP-IDF may result in unexpected failures."
 | 
			
		||||
        )
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True)
 | 
			
		||||
 | 
			
		||||
    cg.add_define(
 | 
			
		||||
        "USE_ESP_IDF_VERSION_CODE",
 | 
			
		||||
        cg.RawExpression(
 | 
			
		||||
            f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    add_idf_sdkconfig_option(f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True)
 | 
			
		||||
 | 
			
		||||
    for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
 | 
			
		||||
        add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
 | 
			
		||||
 | 
			
		||||
    for component in conf[CONF_COMPONENTS]:
 | 
			
		||||
        add_idf_component(
 | 
			
		||||
            name=component[CONF_NAME],
 | 
			
		||||
            repo=component.get(CONF_SOURCE),
 | 
			
		||||
            ref=component.get(CONF_REF),
 | 
			
		||||
            path=component.get(CONF_PATH),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
APP_PARTITION_SIZES = {
 | 
			
		||||
@@ -1056,13 +985,14 @@ def _write_sdkconfig():
 | 
			
		||||
        )
 | 
			
		||||
        + "\n"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if write_file_if_changed(internal_path, contents):
 | 
			
		||||
        # internal changed, update real one
 | 
			
		||||
        write_file_if_changed(sdk_path, contents)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _write_idf_component_yml():
 | 
			
		||||
    yml_path = Path(CORE.relative_build_path("src/idf_component.yml"))
 | 
			
		||||
    yml_path = CORE.relative_build_path("src/idf_component.yml")
 | 
			
		||||
    if CORE.data[KEY_ESP32][KEY_COMPONENTS]:
 | 
			
		||||
        components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS]
 | 
			
		||||
        dependencies = {}
 | 
			
		||||
@@ -1080,51 +1010,48 @@ def _write_idf_component_yml():
 | 
			
		||||
        contents = ""
 | 
			
		||||
    if write_file_if_changed(yml_path, contents):
 | 
			
		||||
        dependencies_lock = CORE.relative_build_path("dependencies.lock")
 | 
			
		||||
        if os.path.isfile(dependencies_lock):
 | 
			
		||||
            os.remove(dependencies_lock)
 | 
			
		||||
        if dependencies_lock.is_file():
 | 
			
		||||
            dependencies_lock.unlink()
 | 
			
		||||
        clean_cmake_cache()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Called by writer.py
 | 
			
		||||
def copy_files():
 | 
			
		||||
    if (
 | 
			
		||||
        CORE.using_arduino
 | 
			
		||||
        and "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]
 | 
			
		||||
    ):
 | 
			
		||||
        write_file_if_changed(
 | 
			
		||||
            CORE.relative_build_path("partitions.csv"),
 | 
			
		||||
            get_arduino_partition_csv(
 | 
			
		||||
                CORE.platformio_options.get("board_upload.flash_size")
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
    if CORE.using_esp_idf:
 | 
			
		||||
        _write_sdkconfig()
 | 
			
		||||
        _write_idf_component_yml()
 | 
			
		||||
        if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
 | 
			
		||||
    _write_sdkconfig()
 | 
			
		||||
    _write_idf_component_yml()
 | 
			
		||||
 | 
			
		||||
    if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
 | 
			
		||||
        if CORE.using_arduino:
 | 
			
		||||
            write_file_if_changed(
 | 
			
		||||
                CORE.relative_build_path("partitions.csv"),
 | 
			
		||||
                get_arduino_partition_csv(
 | 
			
		||||
                    CORE.platformio_options.get("board_upload.flash_size")
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            write_file_if_changed(
 | 
			
		||||
                CORE.relative_build_path("partitions.csv"),
 | 
			
		||||
                get_idf_partition_csv(
 | 
			
		||||
                    CORE.platformio_options.get("board_upload.flash_size")
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
        # IDF build scripts look for version string to put in the build.
 | 
			
		||||
        # However, if the build path does not have an initialized git repo,
 | 
			
		||||
        # and no version.txt file exists, the CMake script fails for some setups.
 | 
			
		||||
        # Fix by manually pasting a version.txt file, containing the ESPHome version
 | 
			
		||||
        write_file_if_changed(
 | 
			
		||||
            CORE.relative_build_path("version.txt"),
 | 
			
		||||
            __version__,
 | 
			
		||||
        )
 | 
			
		||||
    # IDF build scripts look for version string to put in the build.
 | 
			
		||||
    # However, if the build path does not have an initialized git repo,
 | 
			
		||||
    # and no version.txt file exists, the CMake script fails for some setups.
 | 
			
		||||
    # Fix by manually pasting a version.txt file, containing the ESPHome version
 | 
			
		||||
    write_file_if_changed(
 | 
			
		||||
        CORE.relative_build_path("version.txt"),
 | 
			
		||||
        __version__,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    for file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].values():
 | 
			
		||||
        if file[KEY_PATH].startswith("http"):
 | 
			
		||||
        name: str = file[KEY_NAME]
 | 
			
		||||
        path: Path = file[KEY_PATH]
 | 
			
		||||
        if str(path).startswith("http"):
 | 
			
		||||
            import requests
 | 
			
		||||
 | 
			
		||||
            mkdir_p(CORE.relative_build_path(os.path.dirname(file[KEY_NAME])))
 | 
			
		||||
            with open(CORE.relative_build_path(file[KEY_NAME]), "wb") as f:
 | 
			
		||||
                f.write(requests.get(file[KEY_PATH], timeout=30).content)
 | 
			
		||||
            CORE.relative_build_path(name).parent.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
            content = requests.get(path, timeout=30).content
 | 
			
		||||
            CORE.relative_build_path(name).write_bytes(content)
 | 
			
		||||
        else:
 | 
			
		||||
            copy_file_if_changed(
 | 
			
		||||
                file[KEY_PATH],
 | 
			
		||||
                CORE.relative_build_path(file[KEY_NAME]),
 | 
			
		||||
            )
 | 
			
		||||
            copy_file_if_changed(path, CORE.relative_build_path(name))
 | 
			
		||||
 
 | 
			
		||||
@@ -1504,6 +1504,10 @@ BOARDS = {
 | 
			
		||||
        "name": "BPI-Bit",
 | 
			
		||||
        "variant": VARIANT_ESP32,
 | 
			
		||||
    },
 | 
			
		||||
    "bpi-centi-s3": {
 | 
			
		||||
        "name": "BPI-Centi-S3",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "bpi_leaf_s3": {
 | 
			
		||||
        "name": "BPI-Leaf-S3",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
@@ -1664,10 +1668,46 @@ BOARDS = {
 | 
			
		||||
        "name": "Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-s3-devkitc-1-n32r8v": {
 | 
			
		||||
        "name": "Espressif ESP32-S3-DevKitC-1-N32R8V (32 MB Flash Octal, 8 MB PSRAM Octal)",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-s3-devkitc1-n16r16": {
 | 
			
		||||
        "name": "Espressif ESP32-S3-DevKitC-1-N16R16V (16 MB Flash Quad, 16 MB PSRAM Octal)",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-s3-devkitc1-n16r2": {
 | 
			
		||||
        "name": "Espressif ESP32-S3-DevKitC-1-N16R2 (16 MB Flash Quad, 2 MB PSRAM Quad)",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-s3-devkitc1-n16r8": {
 | 
			
		||||
        "name": "Espressif ESP32-S3-DevKitC-1-N16R8V (16 MB Flash Quad, 8 MB PSRAM Octal)",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-s3-devkitc1-n4r2": {
 | 
			
		||||
        "name": "Espressif ESP32-S3-DevKitC-1-N4R2 (4 MB Flash Quad, 2 MB PSRAM Quad)",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-s3-devkitc1-n4r8": {
 | 
			
		||||
        "name": "Espressif ESP32-S3-DevKitC-1-N4R8 (4 MB Flash Quad, 8 MB PSRAM Octal)",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-s3-devkitc1-n8r2": {
 | 
			
		||||
        "name": "Espressif ESP32-S3-DevKitC-1-N8R2 (8 MB Flash Quad, 2 MB PSRAM quad)",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-s3-devkitc1-n8r8": {
 | 
			
		||||
        "name": "Espressif ESP32-S3-DevKitC-1-N8R8 (8 MB Flash Quad, 8 MB PSRAM Octal)",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-s3-devkitm-1": {
 | 
			
		||||
        "name": "Espressif ESP32-S3-DevKitM-1",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-s3-fh4r2": {
 | 
			
		||||
        "name": "Espressif ESP32-S3-FH4R2 (4 MB QD, 2MB PSRAM)",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-solo1": {
 | 
			
		||||
        "name": "Espressif Generic ESP32-solo1 4M Flash",
 | 
			
		||||
        "variant": VARIANT_ESP32,
 | 
			
		||||
@@ -1764,6 +1804,10 @@ BOARDS = {
 | 
			
		||||
        "name": "Franzininho WiFi MSC",
 | 
			
		||||
        "variant": VARIANT_ESP32S2,
 | 
			
		||||
    },
 | 
			
		||||
    "freenove-esp32-s3-n8r8": {
 | 
			
		||||
        "name": "Freenove ESP32-S3 WROOM N8R8 (8MB Flash / 8MB PSRAM)",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "freenove_esp32_s3_wroom": {
 | 
			
		||||
        "name": "Freenove ESP32-S3 WROOM N8R8 (8MB Flash / 8MB PSRAM)",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
@@ -1964,6 +2008,10 @@ BOARDS = {
 | 
			
		||||
        "name": "M5Stack AtomS3",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "m5stack-atoms3u": {
 | 
			
		||||
        "name": "M5Stack AtomS3U",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "m5stack-core-esp32": {
 | 
			
		||||
        "name": "M5Stack Core ESP32",
 | 
			
		||||
        "variant": VARIANT_ESP32,
 | 
			
		||||
@@ -2084,6 +2132,10 @@ BOARDS = {
 | 
			
		||||
        "name": "Ai-Thinker NodeMCU-32S2 (ESP-12K)",
 | 
			
		||||
        "variant": VARIANT_ESP32S2,
 | 
			
		||||
    },
 | 
			
		||||
    "nologo_esp32c3_super_mini": {
 | 
			
		||||
        "name": "Nologo ESP32C3 SuperMini",
 | 
			
		||||
        "variant": VARIANT_ESP32C3,
 | 
			
		||||
    },
 | 
			
		||||
    "nscreen-32": {
 | 
			
		||||
        "name": "YeaCreate NSCREEN-32",
 | 
			
		||||
        "variant": VARIANT_ESP32,
 | 
			
		||||
@@ -2192,6 +2244,10 @@ BOARDS = {
 | 
			
		||||
        "name": "SparkFun LoRa Gateway 1-Channel",
 | 
			
		||||
        "variant": VARIANT_ESP32,
 | 
			
		||||
    },
 | 
			
		||||
    "sparkfun_pro_micro_esp32c3": {
 | 
			
		||||
        "name": "SparkFun Pro Micro ESP32-C3",
 | 
			
		||||
        "variant": VARIANT_ESP32C3,
 | 
			
		||||
    },
 | 
			
		||||
    "sparkfun_qwiic_pocket_esp32c6": {
 | 
			
		||||
        "name": "SparkFun ESP32-C6 Qwiic Pocket",
 | 
			
		||||
        "variant": VARIANT_ESP32C6,
 | 
			
		||||
@@ -2256,6 +2312,14 @@ BOARDS = {
 | 
			
		||||
        "name": "Turta IoT Node",
 | 
			
		||||
        "variant": VARIANT_ESP32,
 | 
			
		||||
    },
 | 
			
		||||
    "um_bling": {
 | 
			
		||||
        "name": "Unexpected Maker BLING!",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "um_edges3_d": {
 | 
			
		||||
        "name": "Unexpected Maker EDGES3[D]",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "um_feathers2": {
 | 
			
		||||
        "name": "Unexpected Maker FeatherS2",
 | 
			
		||||
        "variant": VARIANT_ESP32S2,
 | 
			
		||||
@@ -2268,10 +2332,18 @@ BOARDS = {
 | 
			
		||||
        "name": "Unexpected Maker FeatherS3",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "um_feathers3_neo": {
 | 
			
		||||
        "name": "Unexpected Maker FeatherS3 Neo",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "um_nanos3": {
 | 
			
		||||
        "name": "Unexpected Maker NanoS3",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "um_omgs3": {
 | 
			
		||||
        "name": "Unexpected Maker OMGS3",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "um_pros3": {
 | 
			
		||||
        "name": "Unexpected Maker PROS3",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
@@ -2280,6 +2352,14 @@ BOARDS = {
 | 
			
		||||
        "name": "Unexpected Maker RMP",
 | 
			
		||||
        "variant": VARIANT_ESP32S2,
 | 
			
		||||
    },
 | 
			
		||||
    "um_squixl": {
 | 
			
		||||
        "name": "Unexpected Maker SQUiXL",
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
    "um_tinyc6": {
 | 
			
		||||
        "name": "Unexpected Maker TinyC6",
 | 
			
		||||
        "variant": VARIANT_ESP32C6,
 | 
			
		||||
    },
 | 
			
		||||
    "um_tinys2": {
 | 
			
		||||
        "name": "Unexpected Maker TinyS2",
 | 
			
		||||
        "variant": VARIANT_ESP32S2,
 | 
			
		||||
@@ -2401,3 +2481,4 @@ BOARDS = {
 | 
			
		||||
        "variant": VARIANT_ESP32S3,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
# DO NOT ADD ANYTHING BELOW THIS LINE
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,14 @@ static const char *const TAG = "esp32.preferences";
 | 
			
		||||
 | 
			
		||||
struct NVSData {
 | 
			
		||||
  std::string key;
 | 
			
		||||
  std::vector<uint8_t> data;
 | 
			
		||||
  std::unique_ptr<uint8_t[]> data;
 | 
			
		||||
  size_t len;
 | 
			
		||||
 | 
			
		||||
  void set_data(const uint8_t *src, size_t size) {
 | 
			
		||||
    data = std::make_unique<uint8_t[]>(size);
 | 
			
		||||
    memcpy(data.get(), src, size);
 | 
			
		||||
    len = size;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
static std::vector<NVSData> s_pending_save;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
@@ -30,26 +37,26 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
 | 
			
		||||
    // try find in pending saves and update that
 | 
			
		||||
    for (auto &obj : s_pending_save) {
 | 
			
		||||
      if (obj.key == key) {
 | 
			
		||||
        obj.data.assign(data, data + len);
 | 
			
		||||
        obj.set_data(data, len);
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    NVSData save{};
 | 
			
		||||
    save.key = key;
 | 
			
		||||
    save.data.assign(data, data + len);
 | 
			
		||||
    s_pending_save.emplace_back(save);
 | 
			
		||||
    ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %d", key.c_str(), len);
 | 
			
		||||
    save.set_data(data, len);
 | 
			
		||||
    s_pending_save.emplace_back(std::move(save));
 | 
			
		||||
    ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %zu", key.c_str(), len);
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  bool load(uint8_t *data, size_t len) override {
 | 
			
		||||
    // try find in pending saves and load from that
 | 
			
		||||
    for (auto &obj : s_pending_save) {
 | 
			
		||||
      if (obj.key == key) {
 | 
			
		||||
        if (obj.data.size() != len) {
 | 
			
		||||
        if (obj.len != len) {
 | 
			
		||||
          // size mismatch
 | 
			
		||||
          return false;
 | 
			
		||||
        }
 | 
			
		||||
        memcpy(data, obj.data.data(), len);
 | 
			
		||||
        memcpy(data, obj.data.get(), len);
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@@ -61,7 +68,7 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    if (actual_len != len) {
 | 
			
		||||
      ESP_LOGVV(TAG, "NVS length does not match (%u!=%u)", actual_len, len);
 | 
			
		||||
      ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len);
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    err = nvs_get_blob(nvs_handle, key.c_str(), data, &len);
 | 
			
		||||
@@ -69,7 +76,7 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
 | 
			
		||||
      ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key.c_str(), esp_err_to_name(err));
 | 
			
		||||
      return false;
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %d", key.c_str(), len);
 | 
			
		||||
      ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key.c_str(), len);
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
@@ -112,7 +119,7 @@ class ESP32Preferences : public ESPPreferences {
 | 
			
		||||
    if (s_pending_save.empty())
 | 
			
		||||
      return true;
 | 
			
		||||
 | 
			
		||||
    ESP_LOGV(TAG, "Saving %d items...", s_pending_save.size());
 | 
			
		||||
    ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
 | 
			
		||||
    // goal try write all pending saves even if one fails
 | 
			
		||||
    int cached = 0, written = 0, failed = 0;
 | 
			
		||||
    esp_err_t last_err = ESP_OK;
 | 
			
		||||
@@ -123,11 +130,10 @@ class ESP32Preferences : public ESPPreferences {
 | 
			
		||||
      const auto &save = s_pending_save[i];
 | 
			
		||||
      ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str());
 | 
			
		||||
      if (is_changed(nvs_handle, save)) {
 | 
			
		||||
        esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size());
 | 
			
		||||
        ESP_LOGV(TAG, "sync: key: %s, len: %d", save.key.c_str(), save.data.size());
 | 
			
		||||
        esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.get(), save.len);
 | 
			
		||||
        ESP_LOGV(TAG, "sync: key: %s, len: %zu", save.key.c_str(), save.len);
 | 
			
		||||
        if (err != 0) {
 | 
			
		||||
          ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(),
 | 
			
		||||
                   esp_err_to_name(err));
 | 
			
		||||
          ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", save.key.c_str(), save.len, esp_err_to_name(err));
 | 
			
		||||
          failed++;
 | 
			
		||||
          last_err = err;
 | 
			
		||||
          last_key = save.key;
 | 
			
		||||
@@ -135,7 +141,7 @@ class ESP32Preferences : public ESPPreferences {
 | 
			
		||||
        }
 | 
			
		||||
        written++;
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGV(TAG, "NVS data not changed skipping %s  len=%u", save.key.c_str(), save.data.size());
 | 
			
		||||
        ESP_LOGV(TAG, "NVS data not changed skipping %s  len=%zu", save.key.c_str(), save.len);
 | 
			
		||||
        cached++;
 | 
			
		||||
      }
 | 
			
		||||
      s_pending_save.erase(s_pending_save.begin() + i);
 | 
			
		||||
@@ -164,7 +170,7 @@ class ESP32Preferences : public ESPPreferences {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    // Check size first before allocating memory
 | 
			
		||||
    if (actual_len != to_save.data.size()) {
 | 
			
		||||
    if (actual_len != to_save.len) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    auto stored_data = std::make_unique<uint8_t[]>(actual_len);
 | 
			
		||||
@@ -173,7 +179,7 @@ class ESP32Preferences : public ESPPreferences {
 | 
			
		||||
      ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err));
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
    return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
 | 
			
		||||
    return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool reset() override {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,8 @@
 | 
			
		||||
from collections.abc import Callable, MutableMapping
 | 
			
		||||
from enum import Enum
 | 
			
		||||
import logging
 | 
			
		||||
import re
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from esphome import automation
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
@@ -9,6 +12,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_ENABLE_ON_BOOT,
 | 
			
		||||
    CONF_ESPHOME,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_MAX_CONNECTIONS,
 | 
			
		||||
    CONF_NAME,
 | 
			
		||||
    CONF_NAME_ADD_MAC_SUFFIX,
 | 
			
		||||
)
 | 
			
		||||
@@ -19,6 +23,8 @@ DEPENDENCIES = ["esp32"]
 | 
			
		||||
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
 | 
			
		||||
DOMAIN = "esp32_ble"
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BTLoggers(Enum):
 | 
			
		||||
    """Bluetooth logger categories available in ESP-IDF.
 | 
			
		||||
@@ -127,6 +133,28 @@ CONF_DISABLE_BT_LOGS = "disable_bt_logs"
 | 
			
		||||
CONF_CONNECTION_TIMEOUT = "connection_timeout"
 | 
			
		||||
CONF_MAX_NOTIFICATIONS = "max_notifications"
 | 
			
		||||
 | 
			
		||||
# BLE connection limits
 | 
			
		||||
# ESP-IDF CONFIG_BT_ACL_CONNECTIONS has range 1-9, default 4
 | 
			
		||||
# Total instances: 10 (ADV + SCAN + connections)
 | 
			
		||||
# - ADV only: up to 9 connections
 | 
			
		||||
# - SCAN only: up to 9 connections
 | 
			
		||||
# - ADV + SCAN: up to 8 connections
 | 
			
		||||
DEFAULT_MAX_CONNECTIONS = 3
 | 
			
		||||
IDF_MAX_CONNECTIONS = 9
 | 
			
		||||
 | 
			
		||||
# Connection slot tracking keys
 | 
			
		||||
KEY_ESP32_BLE = "esp32_ble"
 | 
			
		||||
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
 | 
			
		||||
 | 
			
		||||
# Export for use by other components (bluetooth_proxy, etc.)
 | 
			
		||||
__all__ = [
 | 
			
		||||
    "DEFAULT_MAX_CONNECTIONS",
 | 
			
		||||
    "IDF_MAX_CONNECTIONS",
 | 
			
		||||
    "KEY_ESP32_BLE",
 | 
			
		||||
    "KEY_USED_CONNECTION_SLOTS",
 | 
			
		||||
    "consume_connection_slots",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
 | 
			
		||||
 | 
			
		||||
esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble")
 | 
			
		||||
@@ -174,19 +202,18 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            CONF_ADVERTISING_CYCLE_TIME, default="10s"
 | 
			
		||||
        ): cv.positive_time_period_milliseconds,
 | 
			
		||||
        cv.SplitDefault(CONF_DISABLE_BT_LOGS, esp32_idf=True): cv.All(
 | 
			
		||||
            cv.only_with_esp_idf, cv.boolean
 | 
			
		||||
        ),
 | 
			
		||||
        cv.SplitDefault(CONF_CONNECTION_TIMEOUT, esp32_idf="20s"): cv.All(
 | 
			
		||||
            cv.only_with_esp_idf,
 | 
			
		||||
        cv.Optional(CONF_DISABLE_BT_LOGS, default=True): cv.boolean,
 | 
			
		||||
        cv.Optional(CONF_CONNECTION_TIMEOUT, default="20s"): cv.All(
 | 
			
		||||
            cv.positive_time_period_seconds,
 | 
			
		||||
            cv.Range(min=TimePeriod(seconds=10), max=TimePeriod(seconds=180)),
 | 
			
		||||
        ),
 | 
			
		||||
        cv.SplitDefault(CONF_MAX_NOTIFICATIONS, esp32_idf=12): cv.All(
 | 
			
		||||
            cv.only_with_esp_idf,
 | 
			
		||||
        cv.Optional(CONF_MAX_NOTIFICATIONS, default=12): cv.All(
 | 
			
		||||
            cv.positive_int,
 | 
			
		||||
            cv.Range(min=1, max=64),
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
 | 
			
		||||
            cv.positive_int, cv.Range(min=1, max=IDF_MAX_CONNECTIONS)
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
@@ -234,6 +261,60 @@ def validate_variant(_):
 | 
			
		||||
        raise cv.Invalid(f"{variant} does not support Bluetooth")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def consume_connection_slots(
 | 
			
		||||
    value: int, consumer: str
 | 
			
		||||
) -> Callable[[MutableMapping], MutableMapping]:
 | 
			
		||||
    """Reserve BLE connection slots for a component.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        value: Number of connection slots to reserve
 | 
			
		||||
        consumer: Name of the component consuming the slots
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        A validator function that records the slot usage
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def _consume_connection_slots(config: MutableMapping) -> MutableMapping:
 | 
			
		||||
        data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE, {})
 | 
			
		||||
        slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, [])
 | 
			
		||||
        slots.extend([consumer] * value)
 | 
			
		||||
        return config
 | 
			
		||||
 | 
			
		||||
    return _consume_connection_slots
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_connection_slots(max_connections: int) -> None:
 | 
			
		||||
    """Validate that BLE connection slots don't exceed the configured maximum."""
 | 
			
		||||
    # Skip validation in testing mode to allow component grouping
 | 
			
		||||
    if CORE.testing_mode:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    ble_data = CORE.data.get(KEY_ESP32_BLE, {})
 | 
			
		||||
    used_slots = ble_data.get(KEY_USED_CONNECTION_SLOTS, [])
 | 
			
		||||
    num_used = len(used_slots)
 | 
			
		||||
 | 
			
		||||
    if num_used <= max_connections:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    slot_users = ", ".join(used_slots)
 | 
			
		||||
 | 
			
		||||
    if num_used > IDF_MAX_CONNECTIONS:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"BLE components require {num_used} connection slots but maximum is {IDF_MAX_CONNECTIONS}. "
 | 
			
		||||
            f"Reduce the number of BLE clients. Components: {slot_users}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    _LOGGER.warning(
 | 
			
		||||
        "BLE components require %d connection slot(s) but only %d configured. "
 | 
			
		||||
        "Please set 'max_connections: %d' in the 'esp32_ble' component. "
 | 
			
		||||
        "Components: %s",
 | 
			
		||||
        num_used,
 | 
			
		||||
        max_connections,
 | 
			
		||||
        num_used,
 | 
			
		||||
        slot_users,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def final_validation(config):
 | 
			
		||||
    validate_variant(config)
 | 
			
		||||
    if (name := config.get(CONF_NAME)) is not None:
 | 
			
		||||
@@ -246,6 +327,47 @@ def final_validation(config):
 | 
			
		||||
                f"Name '{name}' is too long, maximum length is {max_length} characters"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    # Set GATT Client/Server sdkconfig options based on which components are loaded
 | 
			
		||||
    full_config = fv.full_config.get()
 | 
			
		||||
 | 
			
		||||
    # Validate connection slots usage
 | 
			
		||||
    max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
 | 
			
		||||
    validate_connection_slots(max_connections)
 | 
			
		||||
 | 
			
		||||
    # Check if BLE Server is needed
 | 
			
		||||
    has_ble_server = "esp32_ble_server" in full_config
 | 
			
		||||
 | 
			
		||||
    # Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client)
 | 
			
		||||
    has_ble_client = (
 | 
			
		||||
        "esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled
 | 
			
		||||
    # This is an internal dependency in the Bluedroid stack (tested ESP-IDF 5.4.2-5.5.1)
 | 
			
		||||
    # See: https://github.com/espressif/esp-idf/issues/17724
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client)
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client)
 | 
			
		||||
 | 
			
		||||
    # Handle max_connections: check for deprecated location in esp32_ble_tracker
 | 
			
		||||
    max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
 | 
			
		||||
 | 
			
		||||
    # Use value from tracker if esp32_ble doesn't have it explicitly set (backward compat)
 | 
			
		||||
    if "esp32_ble_tracker" in full_config:
 | 
			
		||||
        tracker_config = full_config["esp32_ble_tracker"]
 | 
			
		||||
        if "max_connections" in tracker_config and CONF_MAX_CONNECTIONS not in config:
 | 
			
		||||
            max_connections = tracker_config["max_connections"]
 | 
			
		||||
 | 
			
		||||
    # Set CONFIG_BT_ACL_CONNECTIONS to the maximum connections needed + 1 for ADV/SCAN
 | 
			
		||||
    # This is the Bluedroid host stack total instance limit (range 1-9, default 4)
 | 
			
		||||
    # Total instances = ADV/SCAN (1) + connection slots (max_connections)
 | 
			
		||||
    # Shared between client (tracker/ble_client) and server
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", max_connections + 1)
 | 
			
		||||
 | 
			
		||||
    # Set controller-specific max connections for ESP32 (classic)
 | 
			
		||||
    # CONFIG_BTDM_CTRL_BLE_MAX_CONN is ESP32-specific controller limit (just connections, not ADV/SCAN)
 | 
			
		||||
    # For newer chips (C3/S3/etc), different configs are used automatically
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_BTDM_CTRL_BLE_MAX_CONN", max_connections)
 | 
			
		||||
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -261,43 +383,44 @@ async def to_code(config):
 | 
			
		||||
        cg.add(var.set_name(name))
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    if CORE.using_esp_idf:
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
 | 
			
		||||
    # Define max connections for use in C++ code (e.g., ble_server.h)
 | 
			
		||||
    max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
 | 
			
		||||
    cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections)
 | 
			
		||||
 | 
			
		||||
        # Register the core BLE loggers that are always needed
 | 
			
		||||
        register_bt_logger(BTLoggers.GAP, BTLoggers.BTM, BTLoggers.HCI)
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
 | 
			
		||||
 | 
			
		||||
        # Apply logger settings if log disabling is enabled
 | 
			
		||||
        if config.get(CONF_DISABLE_BT_LOGS, False):
 | 
			
		||||
            # Disable all Bluetooth loggers that are not required
 | 
			
		||||
            for logger in BTLoggers:
 | 
			
		||||
                if logger not in _required_loggers:
 | 
			
		||||
                    add_idf_sdkconfig_option(f"{logger.value}_NONE", True)
 | 
			
		||||
    # Register the core BLE loggers that are always needed
 | 
			
		||||
    register_bt_logger(BTLoggers.GAP, BTLoggers.BTM, BTLoggers.HCI)
 | 
			
		||||
 | 
			
		||||
        # Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector
 | 
			
		||||
        # Default is 20 seconds instead of ESP-IDF's 30 seconds. Because there is no way to
 | 
			
		||||
        # cancel a BLE connection in progress, when aioesphomeapi times out at 20 seconds,
 | 
			
		||||
        # the connection slot remains occupied for the remaining time, preventing new connection
 | 
			
		||||
        # attempts and wasting valuable connection slots.
 | 
			
		||||
        if CONF_CONNECTION_TIMEOUT in config:
 | 
			
		||||
            timeout_seconds = int(config[CONF_CONNECTION_TIMEOUT].total_seconds)
 | 
			
		||||
            add_idf_sdkconfig_option(
 | 
			
		||||
                "CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds
 | 
			
		||||
            )
 | 
			
		||||
            # Increase GATT client connection retry count for problematic devices
 | 
			
		||||
            # Default in ESP-IDF is 3, we increase to 10 for better reliability with
 | 
			
		||||
            # low-power/timing-sensitive devices
 | 
			
		||||
            add_idf_sdkconfig_option("CONFIG_BT_GATTC_CONNECT_RETRY_COUNT", 10)
 | 
			
		||||
    # Apply logger settings if log disabling is enabled
 | 
			
		||||
    if config.get(CONF_DISABLE_BT_LOGS, False):
 | 
			
		||||
        # Disable all Bluetooth loggers that are not required
 | 
			
		||||
        for logger in BTLoggers:
 | 
			
		||||
            if logger not in _required_loggers:
 | 
			
		||||
                add_idf_sdkconfig_option(f"{logger.value}_NONE", True)
 | 
			
		||||
 | 
			
		||||
        # Set the maximum number of notification registrations
 | 
			
		||||
        # This controls how many BLE characteristics can have notifications enabled
 | 
			
		||||
        # across all connections for a single GATT client interface
 | 
			
		||||
        # https://github.com/esphome/issues/issues/6808
 | 
			
		||||
        if CONF_MAX_NOTIFICATIONS in config:
 | 
			
		||||
            add_idf_sdkconfig_option(
 | 
			
		||||
                "CONFIG_BT_GATTC_NOTIF_REG_MAX", config[CONF_MAX_NOTIFICATIONS]
 | 
			
		||||
            )
 | 
			
		||||
    # Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector
 | 
			
		||||
    # Default is 20 seconds instead of ESP-IDF's 30 seconds. Because there is no way to
 | 
			
		||||
    # cancel a BLE connection in progress, when aioesphomeapi times out at 20 seconds,
 | 
			
		||||
    # the connection slot remains occupied for the remaining time, preventing new connection
 | 
			
		||||
    # attempts and wasting valuable connection slots.
 | 
			
		||||
    if CONF_CONNECTION_TIMEOUT in config:
 | 
			
		||||
        timeout_seconds = int(config[CONF_CONNECTION_TIMEOUT].total_seconds)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds)
 | 
			
		||||
        # Increase GATT client connection retry count for problematic devices
 | 
			
		||||
        # Default in ESP-IDF is 3, we increase to 10 for better reliability with
 | 
			
		||||
        # low-power/timing-sensitive devices
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_BT_GATTC_CONNECT_RETRY_COUNT", 10)
 | 
			
		||||
 | 
			
		||||
    # Set the maximum number of notification registrations
 | 
			
		||||
    # This controls how many BLE characteristics can have notifications enabled
 | 
			
		||||
    # across all connections for a single GATT client interface
 | 
			
		||||
    # https://github.com/esphome/issues/issues/6808
 | 
			
		||||
    if CONF_MAX_NOTIFICATIONS in config:
 | 
			
		||||
        add_idf_sdkconfig_option(
 | 
			
		||||
            "CONFIG_BT_GATTC_NOTIF_REG_MAX", config[CONF_MAX_NOTIFICATIONS]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_ESP32_BLE")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -73,6 +73,28 @@ void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &dat
 | 
			
		||||
  this->advertising_start();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLE::advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name) {
 | 
			
		||||
  // This method atomically updates both service data and device name inclusion in BLE advertising.
 | 
			
		||||
  // When include_name is true, the device name is included in the advertising packet making it
 | 
			
		||||
  // visible to passive BLE scanners. When false, the name is only visible in scan response
 | 
			
		||||
  // (requires active scanning). This atomic operation ensures we only restart advertising once
 | 
			
		||||
  // when changing both properties, avoiding the brief gap that would occur with separate calls.
 | 
			
		||||
 | 
			
		||||
  this->advertising_init_();
 | 
			
		||||
 | 
			
		||||
  if (include_name) {
 | 
			
		||||
    // When including name, clear service data first to avoid packet overflow
 | 
			
		||||
    this->advertising_->set_service_data(std::span<const uint8_t>{});
 | 
			
		||||
    this->advertising_->set_include_name(true);
 | 
			
		||||
  } else {
 | 
			
		||||
    // When including service data, clear name first to avoid packet overflow
 | 
			
		||||
    this->advertising_->set_include_name(false);
 | 
			
		||||
    this->advertising_->set_service_data(data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->advertising_start();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLE::advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback) {
 | 
			
		||||
  this->advertising_init_();
 | 
			
		||||
  this->advertising_->register_raw_advertisement_callback(std::move(callback));
 | 
			
		||||
@@ -167,6 +189,7 @@ bool ESP32BLE::ble_setup_() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER
 | 
			
		||||
  if (!this->gatts_event_handlers_.empty()) {
 | 
			
		||||
    err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler);
 | 
			
		||||
    if (err != ESP_OK) {
 | 
			
		||||
@@ -174,7 +197,9 @@ bool ESP32BLE::ble_setup_() {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_CLIENT
 | 
			
		||||
  if (!this->gattc_event_handlers_.empty()) {
 | 
			
		||||
    err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler);
 | 
			
		||||
    if (err != ESP_OK) {
 | 
			
		||||
@@ -182,20 +207,23 @@ bool ESP32BLE::ble_setup_() {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  std::string name;
 | 
			
		||||
  if (this->name_.has_value()) {
 | 
			
		||||
    name = this->name_.value();
 | 
			
		||||
    if (App.is_name_add_mac_suffix_enabled()) {
 | 
			
		||||
      name += "-" + get_mac_address().substr(6);
 | 
			
		||||
      name += "-";
 | 
			
		||||
      name += get_mac_address().substr(6);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    name = App.get_name();
 | 
			
		||||
    if (name.length() > 20) {
 | 
			
		||||
      if (App.is_name_add_mac_suffix_enabled()) {
 | 
			
		||||
        name.erase(name.begin() + 13, name.end() - 7);  // Remove characters between 13 and the mac address
 | 
			
		||||
        // Keep first 13 chars and last 7 chars (MAC suffix), remove middle
 | 
			
		||||
        name.erase(13, name.length() - 20);
 | 
			
		||||
      } else {
 | 
			
		||||
        name = name.substr(0, 20);
 | 
			
		||||
        name.resize(20);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -303,6 +331,7 @@ void ESP32BLE::loop() {
 | 
			
		||||
  BLEEvent *ble_event = this->ble_events_.pop();
 | 
			
		||||
  while (ble_event != nullptr) {
 | 
			
		||||
    switch (ble_event->type_) {
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER
 | 
			
		||||
      case BLEEvent::GATTS: {
 | 
			
		||||
        esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event;
 | 
			
		||||
        esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if;
 | 
			
		||||
@@ -313,6 +342,8 @@ void ESP32BLE::loop() {
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP32_BLE_CLIENT
 | 
			
		||||
      case BLEEvent::GATTC: {
 | 
			
		||||
        esp_gattc_cb_event_t event = ble_event->event_.gattc.gattc_event;
 | 
			
		||||
        esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if;
 | 
			
		||||
@@ -323,6 +354,7 @@ void ESP32BLE::loop() {
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      case BLEEvent::GAP: {
 | 
			
		||||
        esp_gap_ble_cb_event_t gap_event = ble_event->event_.gap.gap_event;
 | 
			
		||||
        switch (gap_event) {
 | 
			
		||||
@@ -416,13 +448,17 @@ void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_pa
 | 
			
		||||
  event->load_gap_event(e, p);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_CLIENT
 | 
			
		||||
void load_ble_event(BLEEvent *event, esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
 | 
			
		||||
  event->load_gattc_event(e, i, p);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER
 | 
			
		||||
void load_ble_event(BLEEvent *event, esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
 | 
			
		||||
  event->load_gatts_event(e, i, p);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
template<typename... Args> void enqueue_ble_event(Args... args) {
 | 
			
		||||
  // Allocate an event from the pool
 | 
			
		||||
@@ -443,8 +479,12 @@ template<typename... Args> void enqueue_ble_event(Args... args) {
 | 
			
		||||
 | 
			
		||||
// Explicit template instantiations for the friend function
 | 
			
		||||
template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *);
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER
 | 
			
		||||
template void enqueue_ble_event(esp_gatts_cb_event_t, esp_gatt_if_t, esp_ble_gatts_cb_param_t *);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP32_BLE_CLIENT
 | 
			
		||||
template void enqueue_ble_event(esp_gattc_cb_event_t, esp_gatt_if_t, esp_ble_gattc_cb_param_t *);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
@@ -484,15 +524,19 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
 | 
			
		||||
  ESP_LOGW(TAG, "Ignoring unexpected GAP event type: %d", event);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER
 | 
			
		||||
void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
 | 
			
		||||
                                   esp_ble_gatts_cb_param_t *param) {
 | 
			
		||||
  enqueue_ble_event(event, gatts_if, param);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_CLIENT
 | 
			
		||||
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                                   esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  enqueue_ble_event(event, gattc_if, param);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <span>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
@@ -74,17 +75,21 @@ class GAPScanEventHandler {
 | 
			
		||||
  virtual void gap_scan_event_handler(const BLEScanResult &scan_result) = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_CLIENT
 | 
			
		||||
class GATTcEventHandler {
 | 
			
		||||
 public:
 | 
			
		||||
  virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                                   esp_ble_gattc_cb_param_t *param) = 0;
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER
 | 
			
		||||
class GATTsEventHandler {
 | 
			
		||||
 public:
 | 
			
		||||
  virtual void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
 | 
			
		||||
                                   esp_ble_gatts_cb_param_t *param) = 0;
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
class BLEStatusEventHandler {
 | 
			
		||||
 public:
 | 
			
		||||
@@ -114,6 +119,7 @@ class ESP32BLE : public Component {
 | 
			
		||||
  void advertising_set_service_data(const std::vector<uint8_t> &data);
 | 
			
		||||
  void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
 | 
			
		||||
  void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
 | 
			
		||||
  void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
 | 
			
		||||
  void advertising_add_service_uuid(ESPBTUUID uuid);
 | 
			
		||||
  void advertising_remove_service_uuid(ESPBTUUID uuid);
 | 
			
		||||
  void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback);
 | 
			
		||||
@@ -123,16 +129,24 @@ class ESP32BLE : public Component {
 | 
			
		||||
  void register_gap_scan_event_handler(GAPScanEventHandler *handler) {
 | 
			
		||||
    this->gap_scan_event_handlers_.push_back(handler);
 | 
			
		||||
  }
 | 
			
		||||
#ifdef USE_ESP32_BLE_CLIENT
 | 
			
		||||
  void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER
 | 
			
		||||
  void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); }
 | 
			
		||||
#endif
 | 
			
		||||
  void register_ble_status_event_handler(BLEStatusEventHandler *handler) {
 | 
			
		||||
    this->ble_status_event_handlers_.push_back(handler);
 | 
			
		||||
  }
 | 
			
		||||
  void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER
 | 
			
		||||
  static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP32_BLE_CLIENT
 | 
			
		||||
  static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
 | 
			
		||||
#endif
 | 
			
		||||
  static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
 | 
			
		||||
 | 
			
		||||
  bool ble_setup_();
 | 
			
		||||
@@ -148,8 +162,12 @@ class ESP32BLE : public Component {
 | 
			
		||||
  // Vectors (12 bytes each on 32-bit, naturally aligned to 4 bytes)
 | 
			
		||||
  std::vector<GAPEventHandler *> gap_event_handlers_;
 | 
			
		||||
  std::vector<GAPScanEventHandler *> gap_scan_event_handlers_;
 | 
			
		||||
#ifdef USE_ESP32_BLE_CLIENT
 | 
			
		||||
  std::vector<GATTcEventHandler *> gattc_event_handlers_;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER
 | 
			
		||||
  std::vector<GATTsEventHandler *> gatts_event_handlers_;
 | 
			
		||||
#endif
 | 
			
		||||
  std::vector<BLEStatusEventHandler *> ble_status_event_handlers_;
 | 
			
		||||
 | 
			
		||||
  // Large objects (size depends on template parameters, but typically aligned to 4 bytes)
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,7 @@ void BLEAdvertising::remove_service_uuid(ESPBTUUID uuid) {
 | 
			
		||||
                                 this->advertising_uuids_.end());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
 | 
			
		||||
void BLEAdvertising::set_service_data(std::span<const uint8_t> data) {
 | 
			
		||||
  delete[] this->advertising_data_.p_service_data;
 | 
			
		||||
  this->advertising_data_.p_service_data = nullptr;
 | 
			
		||||
  this->advertising_data_.service_data_len = data.size();
 | 
			
		||||
@@ -54,6 +54,10 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
 | 
			
		||||
  this->set_service_data(std::span<const uint8_t>(data));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
 | 
			
		||||
  delete[] this->advertising_data_.p_manufacturer_data;
 | 
			
		||||
  this->advertising_data_.p_manufacturer_data = nullptr;
 | 
			
		||||
@@ -84,7 +88,7 @@ esp_err_t BLEAdvertising::services_advertisement_() {
 | 
			
		||||
  esp_err_t err;
 | 
			
		||||
 | 
			
		||||
  this->advertising_data_.set_scan_rsp = false;
 | 
			
		||||
  this->advertising_data_.include_name = !this->scan_response_;
 | 
			
		||||
  this->advertising_data_.include_name = this->include_name_in_adv_ || !this->scan_response_;
 | 
			
		||||
  this->advertising_data_.include_txpower = !this->scan_response_;
 | 
			
		||||
  err = esp_ble_gap_config_adv_data(&this->advertising_data_);
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
@@ -148,7 +152,7 @@ void BLEAdvertising::loop() {
 | 
			
		||||
  if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) {
 | 
			
		||||
    this->stop();
 | 
			
		||||
    this->current_adv_index_ += 1;
 | 
			
		||||
    if (this->current_adv_index_ >= this->raw_advertisements_callbacks_.size()) {
 | 
			
		||||
    if (static_cast<size_t>(this->current_adv_index_) >= this->raw_advertisements_callbacks_.size()) {
 | 
			
		||||
      this->current_adv_index_ = -1;
 | 
			
		||||
    }
 | 
			
		||||
    this->start();
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <span>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
@@ -36,6 +37,8 @@ class BLEAdvertising {
 | 
			
		||||
  void set_manufacturer_data(const std::vector<uint8_t> &data);
 | 
			
		||||
  void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
 | 
			
		||||
  void set_service_data(const std::vector<uint8_t> &data);
 | 
			
		||||
  void set_service_data(std::span<const uint8_t> data);
 | 
			
		||||
  void set_include_name(bool include_name) { this->include_name_in_adv_ = include_name; }
 | 
			
		||||
  void register_raw_advertisement_callback(std::function<void(bool)> &&callback);
 | 
			
		||||
 | 
			
		||||
  void start();
 | 
			
		||||
@@ -45,6 +48,7 @@ class BLEAdvertising {
 | 
			
		||||
  esp_err_t services_advertisement_();
 | 
			
		||||
 | 
			
		||||
  bool scan_response_;
 | 
			
		||||
  bool include_name_in_adv_{false};
 | 
			
		||||
  esp_ble_adv_data_t advertising_data_;
 | 
			
		||||
  esp_ble_adv_data_t scan_response_data_;
 | 
			
		||||
  esp_ble_adv_params_t advertising_params_;
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user